add basis for onboarding flow and some more theming options

This commit is contained in:
Owen LeJeune
2022-06-22 14:51:51 -04:00
parent d997b7bf46
commit 58b740cfb0
42 changed files with 692 additions and 204 deletions

View File

@@ -13,15 +13,25 @@
android:supportsRtl="true"
android:theme="@style/Theme.TVTime"
android:usesCleartextTraffic="true">
<activity android:name=".AppRoutingActivity"
android:exported="true"
android:theme="@style/Theme.TVTime">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".OnboardingActivity"
android:exported="true"
android:theme="@style/Theme.TVTime" />
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.TVTime"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />

View File

@@ -0,0 +1,23 @@
package com.owenlejeune.tvtime
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.owenlejeune.tvtime.extensions.launchActivity
import com.owenlejeune.tvtime.preferences.AppPreferences
import org.koin.android.ext.android.inject
class AppRoutingActivity: AppCompatActivity() {
private val preferences: AppPreferences by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (preferences.firstLaunch) {
launchActivity(OnboardingActivity::class.java)
} else {
launchActivity(MainActivity::class.java)
}
}
}

View File

@@ -1,7 +1,6 @@
package com.owenlejeune.tvtime
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.Image
@@ -37,9 +36,9 @@ import com.owenlejeune.tvtime.ui.components.SearchFab
import com.owenlejeune.tvtime.ui.navigation.BottomNavItem
import com.owenlejeune.tvtime.ui.navigation.MainNavGraph
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.MediaDetailView
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.PersonDetailView
import com.owenlejeune.tvtime.ui.screens.main.MediaDetailView
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.PersonDetailView
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
import com.owenlejeune.tvtime.utils.KeyboardManager
import com.owenlejeune.tvtime.utils.SessionManager

View File

@@ -0,0 +1,156 @@
package com.owenlejeune.tvtime
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope
import com.google.accompanist.pager.*
import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.tvtime.extensions.launchActivity
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.screens.onboarding.OnboardingPage
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@OptIn(ExperimentalPagerApi::class)
class OnboardingActivity: MonetCompatActivity() {
private val preferences: AppPreferences by inject()
private lateinit var pagerState: PagerState
private lateinit var coroutineScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
monet.awaitMonetReady()
setContent {
TVTimeTheme(monetCompat = monet) {
OnboardingUi()
}
}
}
}
@Composable
fun OnboardingUi() {
pagerState = rememberPagerState()
coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier.background(color = MaterialTheme.colorScheme.background)
) {
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
// .background(MaterialTheme.colorScheme.primary),
count = OnboardingPage.values().size,
userScrollEnabled = false
) { page ->
OnboardingPageUi(page = OnboardingPage[page])
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(all = 12.dp)
) {
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp),
activeColor = MaterialTheme.colorScheme.secondary
)
val isLastPage = pagerState.currentPage == OnboardingPage.values().size - 1
Button(
modifier = Modifier.align(Alignment.CenterEnd),
shape = CircleShape,
onClick = {
if (isLastPage) {
preferences.firstLaunch = false
launchActivity(MainActivity::class.java)
} else {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
}
) {
if (isLastPage) {
Text(stringResource(id = R.string.get_started))
} else {
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = null)
}
}
}
}
}
@Composable
fun OnboardingPageUi(page: OnboardingPage) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
page.image?.let {
Image(
painter = painterResource(id = it),
contentDescription = null,
modifier = Modifier.size(200.dp),
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
)
Spacer(modifier = Modifier.height(20.dp))
}
Text(
text = stringResource(id = page.title),
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = page.description),
textAlign = TextAlign.Center,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(12.dp))
page.additionalContent(this@OnboardingActivity)
}
}
override fun onBackPressed() {
if (pagerState.currentPage == 0) {
finish()
} else {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}
}
}

View File

@@ -6,6 +6,9 @@ import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.di.modules.appModule
import com.owenlejeune.tvtime.di.modules.networkModule
import com.owenlejeune.tvtime.di.modules.preferencesModule
import com.owenlejeune.tvtime.preferences.AppPreferences
import dev.kdrag0n.monet.factory.ColorSchemeFactory
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
@@ -13,6 +16,8 @@ import org.koin.core.logger.Level
class TvTimeApplication: Application() {
private val preferences: AppPreferences by inject()
override fun onCreate() {
super.onCreate()
@@ -30,6 +35,12 @@ class TvTimeApplication: Application() {
}
MonetCompat.enablePaletteCompat()
MonetCompat.chromaMultiplier = preferences.chromaMultiplier
MonetCompat.wallpaperColorPicker = {
val userPickedColor = preferences.selectedColor
it?.firstOrNull { color -> color == userPickedColor } ?: it?.firstOrNull()
}
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)

View File

@@ -2,7 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.*
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import java.lang.reflect.Type
class KnownForDeserializer: JsonDeserializer<KnownFor> {

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class DetailCast(
@SerializedName("id") val id: Int,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class DetailCrew(
@SerializedName("id") val id: Int,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class KnowForTv(
backdropPath: String?,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
abstract class KnownFor(
@SerializedName("backdrop_path") val backdropPath: String?,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class KnownForMovie(
backdropPath: String?,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class MarkAsFavoriteBody(
@SerializedName("media_type") val mediaType: MediaViewType,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class WatchlistBody(
@SerializedName("media_type") val mediaType: MediaViewType,

View File

@@ -4,7 +4,7 @@ import com.google.gson.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListTv
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import java.lang.reflect.Type
class ListItemDeserializer: JsonDeserializer<ListItem> {

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class AddToListBody(
@SerializedName("items") val items: List<AddToListBodyItem>

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class DeleteListItemsBody(
@SerializedName("media_id") val id: Int,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
abstract class ListItem(
@SerializedName("backdrop_path") val backdropPath: String?,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class ListItemStatusResponse(
statusMessage: String,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class ListMovie(
backdropPath: String?,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class ListTv(
backdropPath: String?,

View File

@@ -1,7 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class UpdateListItemBody(
@SerializedName("items") val items: List<UpdateListItemBodyItem>

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.extensions
import android.app.Activity
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Size
@@ -39,4 +40,10 @@ private fun getWindowSizeClass(windowDpSize: DpSize): WindowSizeClass = when {
windowDpSize.width < 600.dp -> WindowSizeClass.Compact
windowDpSize.width < 840.dp -> WindowSizeClass.Medium
else -> WindowSizeClass.Expanded
}
fun <T> Activity.launchActivity(activity: Class<T>) {
val intent = Intent(this, activity)
startActivity(intent)
finish()
}

View File

@@ -2,6 +2,8 @@ package com.owenlejeune.tvtime.preferences
import android.content.Context
import android.content.SharedPreferences
import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.BuildConfig
class AppPreferences(context: Context) {
@@ -13,6 +15,11 @@ class AppPreferences(context: Context) {
private val HIDE_TITLE = "hide_title"
private val GUEST_SESSION = "guest_session_id"
private val AUTHORIZED_SESSION = "authorized_session_id"
private val FIRST_LAUNCH = "first_launch"
private val FIRST_LAUNCH_TESTING = "first_launch_testing"
private val CHROMA_MULTIPLIER = "chroma_multiplier"
private val USE_SYSTEM_COLORS = "use_system_colors"
private val SELECTED_COLOR = "selected_color"
}
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
@@ -37,6 +44,26 @@ class AppPreferences(context: Context) {
// get() = preferences.getBoolean(USE_PREFERENCES, false)
// set(value) { preferences.put(USE_PREFERENCES, value) }
var firstLaunchTesting: Boolean
get() = preferences.getBoolean(FIRST_LAUNCH_TESTING, false)
set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) }
var firstLaunch: Boolean
get() = if (BuildConfig.DEBUG) firstLaunchTesting else preferences.getBoolean(FIRST_LAUNCH, true)
set(value) { preferences.put(FIRST_LAUNCH, value) }
var useSystemColors: Boolean
get() = preferences.getBoolean(USE_SYSTEM_COLORS, true)
set(value) { preferences.put(USE_SYSTEM_COLORS, value) }
var chromaMultiplier: Double
get() = preferences.getFloat(CHROMA_MULTIPLIER, MonetCompat.chromaMultiplier.toFloat()).toDouble()
set(value) { preferences.put(CHROMA_MULTIPLIER, value) }
var selectedColor: Int
get() = preferences.getInt(SELECTED_COLOR, Int.MAX_VALUE)
set(value) { preferences.put(SELECTED_COLOR, value) }
private fun SharedPreferences.put(key: String, value: Any?) {
edit().apply {
when (value) {
@@ -44,12 +71,16 @@ class AppPreferences(context: Context) {
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is Double -> putFloat(key, value.toFloat())
is String -> putString(key, value)
else -> throw UnsupportedTypeError()
}
apply()
}
}
class UnsupportedTypeError: Exception()
// private fun <T> SharedPreferences.get(key: String, java: Class<T>): T {
//
// }

View File

@@ -1,9 +1,11 @@
package com.owenlejeune.tvtime.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -73,4 +75,41 @@ fun SwitchPreference(
@Composable
private fun SwitchPreferencePreview() {
SwitchPreference("Title", true, subtitleText = "Subtitle")
}
@Composable
fun SliderPreference(
modifier: Modifier = Modifier,
titleText: String,
value: Float,
onValueChangeFinished: (Float) -> Unit ,
titleTextColor: Color = MaterialTheme.colorScheme.onBackground,
disabledTextColor: Color = MaterialTheme.colorScheme.outline,
enabled: Boolean = true
) {
Column(
modifier = modifier
.wrapContentHeight()
.padding(all = 8.dp)
) {
val titleColor = if (enabled) titleTextColor else disabledTextColor
Text(text = titleText, style = MaterialTheme.typography.titleLarge, color = titleColor, fontSize = 20.sp)
var sliderValue by remember { mutableStateOf(value) }
Slider(
modifier = Modifier.fillMaxWidth().padding(start = 8.dp),
value = sliderValue,
onValueChange = { sliderValue = it },
enabled = enabled,
valueRange = 0f..100f,
onValueChangeFinished = {
onValueChangeFinished(sliderValue)
},
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary
)
)
}
}

View File

@@ -20,10 +20,7 @@ import androidx.compose.material.Card
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -65,6 +62,7 @@ import androidx.core.text.HtmlCompat
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow
import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails
import com.owenlejeune.tvtime.ui.theme.RatingSelected
@@ -924,4 +922,40 @@ fun TimeoutSnackbar(
onDismiss()
}
}
}
@Composable
fun CenteredIconCircle(
size: Dp,
backgroundColor: Color,
iconTint: Color,
icon: ImageVector,
contentDescription: String?,
showIcon: Boolean = true,
onClick: () -> Unit = {}
) {
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
.background(color = backgroundColor)
.clickable {
onClick()
}
) {
if (showIcon) {
Column(
modifier = Modifier.size(size),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(size / 2),
tint = iconTint
)
}
}
}
}

View File

@@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.AccountTabContent
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.AccountTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.core.component.inject

View File

@@ -5,8 +5,8 @@ import androidx.navigation.NavHostController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTabContent
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import org.koin.core.component.inject
import retrofit2.Response

View File

@@ -9,11 +9,9 @@ import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.owenlejeune.tvtime.ui.screens.MediaDetailView
import com.owenlejeune.tvtime.ui.screens.MainAppView
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.PersonDetailView
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.*
import com.owenlejeune.tvtime.ui.screens.main.MediaDetailView
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.*
object NavConstants {
const val ID_KEY = "id_key"

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
package com.owenlejeune.tvtime.ui.screens.main
import android.content.Context
import androidx.compose.foundation.clickable
@@ -12,7 +12,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -29,8 +28,7 @@ import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.top.ScrollableTabs
import com.owenlejeune.tvtime.ui.screens.main.tabs.top.ScrollableTabs
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.Image

View File

@@ -1,17 +1,13 @@
package com.owenlejeune.tvtime.ui.screens
package com.owenlejeune.tvtime.ui.screens.main
import android.content.Context
import android.widget.Toast
import androidx.compose.animation.*
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Send
@@ -29,7 +25,6 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
@@ -16,8 +16,7 @@ import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
import com.owenlejeune.tvtime.ui.screens.main.tabs.top.Tabs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens
package com.owenlejeune.tvtime.ui.screens.main
import com.google.gson.annotations.SerializedName

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@@ -14,7 +14,6 @@ import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import com.owenlejeune.tvtime.ui.components.PeoplePosterGrid
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@@ -0,0 +1,296 @@
package com.owenlejeune.tvtime.ui.screens.main
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ResetTv
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material3.*
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.utils.SessionManager
import dev.kdrag0n.monet.factory.ColorSchemeFactory
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get
@Composable
fun SettingsTab(preferences: AppPreferences = get(AppPreferences::class.java)) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 8.dp, top = 8.dp, bottom = 8.dp, end = 24.dp)
.verticalScroll(scrollState)
) {
// val usePreferences = remember { mutableStateOf(preferences.usePreferences) }
// TopLevelSwitch(
// text = "Enable Preferences",
// checkedState = usePreferences,
// onCheckChanged = { isChecked ->
// usePreferences.value = isChecked
// preferences.usePreferences = isChecked
// }
// )
//
// Column(
// modifier = Modifier
// .fillMaxWidth()
// .wrapContentHeight()
// .padding(start = 8.dp, top = 8.dp, bottom = 8.dp, end = 24.dp)
// .verticalScroll()
// ) {
PreferenceHeading(text = stringResource(R.string.preference_heading_search))
val persistentSearch = remember { mutableStateOf(preferences.persistentSearch) }
SwitchPreference(
titleText = stringResource(R.string.preferences_persistent_search_title),
subtitleText = stringResource(R.string.preferences_persistent_search_subtitle),
checkState = persistentSearch.value,
onCheckedChange = { isChecked ->
persistentSearch.value = isChecked
preferences.persistentSearch = isChecked
}
)
val hideTitle = remember { mutableStateOf(preferences.hideTitle) }
SwitchPreference(
titleText = stringResource(R.string.preferences_hide_heading_title),
subtitleText = stringResource(R.string.preferences_hide_heading_subtitle),
checkState = hideTitle.value,
onCheckedChange = { isChecked ->
hideTitle.value = isChecked
preferences.hideTitle = isChecked
},
enabled = persistentSearch.value
)
PreferenceHeading(text = stringResource(id = R.string.preference_heading_design))
val isSystemColorsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
if (isSystemColorsSupported) {
val useSystemColors = remember { mutableStateOf(preferences.useSystemColors) }
SwitchPreference(
titleText = stringResource(id = R.string.preference_system_colors_heading),
checkState = useSystemColors.value,
onCheckedChange = { isChecked ->
useSystemColors.value = isChecked
preferences.useSystemColors = isChecked
MonetCompat.useSystemColorsOnAndroid12 = isChecked
MonetCompat.getInstance().updateMonetColors()
}
)
}
SliderPreference(
titleText = stringResource(id = R.string.preference_chroma_factor_heading),
value = preferences.chromaMultiplier.toFloat() * 50f,
onValueChangeFinished = { value ->
with((value / 50f).toDouble()) {
preferences.chromaMultiplier = this
MonetCompat.chromaMultiplier = this
}
MonetCompat.getInstance().updateMonetColors()
},
enabled = if (isSystemColorsSupported) !preferences.useSystemColors else true
)
val showWallpaperPicker = remember { mutableStateOf(false) }
val wallpaperPickerModifier = if (isSystemColorsSupported && preferences.useSystemColors) {
Modifier
} else {
Modifier.clickable { showWallpaperPicker.value = true }
}
Text(
text = stringResource(id = R.string.preference_wallpaper_color_heading),
style = MaterialTheme.typography.titleLarge,
fontSize = 20.sp,
color = if (isSystemColorsSupported && preferences.useSystemColors) {
MaterialTheme.colorScheme.outline
} else {
MaterialTheme.colorScheme.onBackground
},
modifier = wallpaperPickerModifier
.padding(start = 8.dp)
)
if (showWallpaperPicker.value) {
WallpaperPicker(showPopup = showWallpaperPicker)
}
if (BuildConfig.DEBUG) {
Divider(modifier = Modifier.padding(start = 8.dp, top = 20.dp))
Text(
modifier = Modifier.padding(start = 8.dp, top = 20.dp),
text = stringResource(R.string.preferences_debug_title),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary
)
DebugOptions()
}
}
}
@Composable
private fun WallpaperPicker(
showPopup: MutableState<Boolean>,
preference: AppPreferences = get(AppPreferences::class.java)
) {
val context = LocalContext.current
val wallpaperColors = remember { mutableStateOf<List<Int>>(emptyList()) }
val selectedWallpaperColor = remember { mutableStateOf(0) }
LaunchedEffect(true) {
val colors = MonetCompat.getInstance().getAvailableWallpaperColors() ?: emptyList()
if (colors.isEmpty()) {
Toast.makeText(context, context.getString(R.string.preference_no_wallpaper_colors), Toast.LENGTH_SHORT).show()
showPopup.value = false
}
wallpaperColors.value = colors
selectedWallpaperColor.value = MonetCompat.getInstance().getSelectedWallpaperColor() ?: 0
}
if (wallpaperColors.value.isNotEmpty() && selectedWallpaperColor.value != 0) {
val colorCircleSize = 50.dp
AlertDialog(
modifier = Modifier.padding(12.dp),
title = { Text(text = stringResource(id = R.string.preference_wallpaper_color_heading)) },
text = {
Row(
modifier = Modifier
.height(colorCircleSize)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
wallpaperColors.value.forEach {
CenteredIconCircle(
size = colorCircleSize,
backgroundColor = Color(it),
iconTint = Color.White,
icon = Icons.Filled.Check,
contentDescription = null,
showIcon = it == selectedWallpaperColor.value,
onClick = {
if (it != selectedWallpaperColor.value) {
preference.selectedColor = it
MonetCompat
.getInstance()
.updateMonetColors()
}
}
)
}
CenteredIconCircle(
size = colorCircleSize,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
iconTint = MaterialTheme.colorScheme.onSecondaryContainer,
icon = Icons.Filled.RestartAlt,
contentDescription = null,
onClick = {
if (preference.selectedColor != Int.MAX_VALUE) {
preference.selectedColor = Int.MAX_VALUE
MonetCompat.getInstance().updateMonetColors()
} else {
Toast.makeText(context, context.getString(R.string.preference_wallpaper_color_default_selected), Toast.LENGTH_SHORT).show()
}
}
)
}
},
dismissButton = {
TextButton(onClick = { showPopup.value = false }) {
Text(text = stringResource(id = R.string.action_cancel))
}
},
confirmButton = {},
onDismissRequest = { showPopup.value = false }
)
}
}
@Composable
private fun DebugOptions(preferences: AppPreferences = get(AppPreferences::class.java)) {
val context = LocalContext.current
val firstLaunchTesting = remember { mutableStateOf(preferences.firstLaunchTesting) }
SwitchPreference(
titleText = "Always show onboarding flow",
subtitleText = "Show onboarding flow on each launch",
checkState = firstLaunchTesting.value,
onCheckedChange = { isChecked ->
firstLaunchTesting.value = isChecked
preferences.firstLaunchTesting = isChecked
}
)
val shouldShowPalette = remember { mutableStateOf(false) }
Text(
text = "Show material palette",
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 12.dp)
.clickable(
onClick = {
shouldShowPalette.value = true
}
)
)
if (shouldShowPalette.value) {
AlertDialog(
modifier = Modifier.padding(12.dp),
title = { Text(text = "Palette") },
text = { PaletteView() },
dismissButton = {
TextButton(onClick = { shouldShowPalette.value = false }) {
Text("Dismiss")
}
},
confirmButton = {},
onDismissRequest = { shouldShowPalette.value = false }
)
}
Text(
text = "Clear session",
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 12.dp)
.clickable(
onClick = {
preferences.guestSessionId = ""
SessionManager.clearSession {
Toast
.makeText(context, "Cleared session: $it", Toast.LENGTH_SHORT)
.show()
}
}
)
)
}

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs.top
package com.owenlejeune.tvtime.ui.screens.main.tabs.top
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer

View File

@@ -0,0 +1,28 @@
package com.owenlejeune.tvtime.ui.screens.onboarding
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import com.owenlejeune.tvtime.R
sealed class OnboardingPage(
@StringRes val title: Int,
@StringRes val description: Int,
@DrawableRes val image: Int? = null,
val additionalContent: @Composable (AppCompatActivity) -> Unit = {}
) {
companion object {
fun values() = listOf(ExamplePage)
operator fun get(i: Int) = values()[i]
}
object ExamplePage: OnboardingPage(
R.string.app_name,
R.string.example_page_desc,
R.drawable.ic_launcher_foreground
)
}

View File

@@ -1,145 +0,0 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Switch
import androidx.compose.material.TextButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.di.modules.preferencesModule
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.java.KoinJavaComponent.get
@Composable
fun SettingsTab(preferences: AppPreferences = get(AppPreferences::class.java)) {
val scrollState = rememberScrollState()
Column(modifier = Modifier
.fillMaxSize()
.padding(start = 8.dp, top = 8.dp, bottom = 8.dp, end = 24.dp)
.verticalScroll(scrollState)
) {
// val usePreferences = remember { mutableStateOf(preferences.usePreferences) }
// TopLevelSwitch(
// text = "Enable Preferences",
// checkedState = usePreferences,
// onCheckChanged = { isChecked ->
// usePreferences.value = isChecked
// preferences.usePreferences = isChecked
// }
// )
//
// Column(
// modifier = Modifier
// .fillMaxWidth()
// .wrapContentHeight()
// .padding(start = 8.dp, top = 8.dp, bottom = 8.dp, end = 24.dp)
// .verticalScroll()
// ) {
PreferenceHeading(text = stringResource(R.string.preference_heading_search))
val persistentSearch = remember { mutableStateOf(preferences.persistentSearch) }
SwitchPreference(
titleText = stringResource(R.string.preferences_persistent_search_title),
subtitleText = stringResource(R.string.preferences_persistent_search_subtitle),
checkState = persistentSearch.value,
onCheckedChange = { isChecked ->
persistentSearch.value = isChecked
preferences.persistentSearch = isChecked
}
)
val hideTitle = remember { mutableStateOf(preferences.hideTitle) }
SwitchPreference(
titleText = stringResource(R.string.preferences_hide_heading_title),
subtitleText = stringResource(R.string.preferences_hide_heading_subtitle),
checkState = hideTitle.value,
onCheckedChange = { isChecked ->
hideTitle.value = isChecked
preferences.hideTitle = isChecked
},
enabled = persistentSearch.value
)
if (BuildConfig.DEBUG) {
Text(
modifier = Modifier.padding(start = 8.dp, top = 20.dp),
text = stringResource(R.string.preferences_debug_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onBackground
)
DebugOptions()
}
}
}
@Composable
private fun DebugOptions(preferences: AppPreferences = get(AppPreferences::class.java)) {
val context = LocalContext.current
val shouldShowPalette = remember { mutableStateOf(false) }
Text(
text = "Show material palette",
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 12.dp)
.clickable(
onClick = {
shouldShowPalette.value = true
}
)
)
if (shouldShowPalette.value) {
PaletteDialog(shouldShowPalette)
}
Text(
text = "Clear session",
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 12.dp)
.clickable(
onClick = {
preferences.guestSessionId = ""
SessionManager.clearSession {
Toast.makeText(context, "Cleared session: $it", Toast.LENGTH_SHORT).show()
}
}
)
)
}
@Composable
private fun PaletteDialog(showDialog: MutableState<Boolean>) {
AlertDialog(
modifier = Modifier.padding(12.dp),
title = { Text(text = "Palette") },
text = { PaletteView() },
dismissButton = {
TextButton(onClick = { showDialog.value = false }) {
Text("Dismiss")
}
},
confirmButton = {},
onDismissRequest = { showDialog.value = false }
)
}

View File

@@ -54,6 +54,12 @@
<string name="preferences_hide_heading_title">Expanded search bar</string>
<string name="preferences_hide_heading_subtitle">Keep search bar expanded at all times</string>
<string name="preferences_debug_title">Developer options</string>
<string name="preference_heading_design">Design</string>
<string name="preference_system_colors_heading">Use system colors</string>
<string name="preference_chroma_factor_heading">Saturation</string>
<string name="preference_wallpaper_color_heading">Wallpaper Color Picker</string>
<string name="preference_no_wallpaper_colors">Unavailable for the current wallpaper or your current settings</string>
<string name="preference_wallpaper_color_default_selected">Wallpaper color already set to default</string>
<!-- video type -->
<string name="video_type_clip">Clips</string>
@@ -104,4 +110,8 @@
<!-- intent routes -->
<string name="intent_route_auth_return">tvtime.auth.return</string>
<!-- onboarding -->
<string name="get_started">Get started</string>
<string name="example_page_desc">This is an example</string>
</resources>