diff --git a/app/src/main/java/com/owenlejeune/tvtime/AppRoutingActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/AppRoutingActivity.kt index 13f91a4..fae515a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/AppRoutingActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/AppRoutingActivity.kt @@ -13,7 +13,7 @@ class AppRoutingActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (preferences.firstLaunch) { + if (preferences.firstLaunchTesting || preferences.firstLaunch) { launchActivity(OnboardingActivity::class.java) } else { launchActivity(MainActivity::class.java) diff --git a/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt index 03fcddb..571aad8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt @@ -7,15 +7,13 @@ 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.ArrowBack 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.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -63,16 +61,38 @@ class OnboardingActivity: MonetCompatActivity() { Column( modifier = Modifier.background(color = MaterialTheme.colorScheme.background) ) { + val isFirstPage = pagerState.currentPage == 0 + val isLastPage = pagerState.currentPage == OnboardingPage.values().size - 1 + + if (!isFirstPage) { + IconButton( + modifier = Modifier.padding(12.dp), + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + } + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + + val nextButtonEnabled = remember { mutableStateOf(true) } 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.padding(start = 12.dp, end = 24.dp)) { + OnboardingPageUi(page = OnboardingPage[page], nextButtonEnabled) + } } Box( @@ -80,6 +100,17 @@ class OnboardingActivity: MonetCompatActivity() { .fillMaxWidth() .padding(all = 12.dp) ) { + Button( + modifier = Modifier.align(Alignment.CenterStart), + enabled = true, + onClick = { + preferences.firstLaunch = false + launchActivity(MainActivity::class.java) + } + ) { + Text(text = "Skip") + } + HorizontalPagerIndicator( pagerState = pagerState, modifier = Modifier @@ -88,10 +119,10 @@ class OnboardingActivity: MonetCompatActivity() { activeColor = MaterialTheme.colorScheme.secondary ) - val isLastPage = pagerState.currentPage == OnboardingPage.values().size - 1 Button( modifier = Modifier.align(Alignment.CenterEnd), shape = CircleShape, + enabled = nextButtonEnabled.value, onClick = { if (isLastPage) { preferences.firstLaunch = false @@ -103,7 +134,7 @@ class OnboardingActivity: MonetCompatActivity() { } } ) { - if (isLastPage) { + if (!isLastPage) { Text(stringResource(id = R.string.get_started)) } else { Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = null) @@ -114,7 +145,7 @@ class OnboardingActivity: MonetCompatActivity() { } @Composable - fun OnboardingPageUi(page: OnboardingPage) { + fun OnboardingPageUi(page: OnboardingPage, nextButtonEnabled: MutableState) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { @@ -122,8 +153,8 @@ class OnboardingActivity: MonetCompatActivity() { Image( painter = painterResource(id = it), contentDescription = null, - modifier = Modifier.size(200.dp), - colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.secondary) + modifier = Modifier.size(100.dp), + colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary) ) Spacer(modifier = Modifier.height(20.dp)) } @@ -140,8 +171,8 @@ class OnboardingActivity: MonetCompatActivity() { fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground ) - Spacer(modifier = Modifier.height(12.dp)) - page.additionalContent(this@OnboardingActivity) + Spacer(modifier = Modifier.height(50.dp)) + page.additionalContent(this@OnboardingActivity, nextButtonEnabled) } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index 2ab4bc0..8e89f7c 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -126,7 +126,7 @@ class AppPreferences(context: Context) { set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) } var firstLaunch: Boolean - get() = if (BuildConfig.DEBUG) firstLaunchTesting else preferences.getBoolean(FIRST_LAUNCH, true) + get() = preferences.getBoolean(FIRST_LAUNCH, true) set(value) { preferences.put(FIRST_LAUNCH, value) } val useV4ApiDefault: Boolean = false diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt index b44c5de..bcc9a73 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/PreferenceWidgets.kt @@ -119,6 +119,7 @@ fun RadioButtonPreference( selected: Boolean, onClick: () -> Unit, title: String, + description: String? = null, icon: ImageVector, titleTextColor: Color = MaterialTheme.colorScheme.onBackground, disabledTextColor: Color = MaterialTheme.colorScheme.outline @@ -142,19 +143,31 @@ fun RadioButtonPreference( ) val titleColor = if (enabled) titleTextColor else disabledTextColor - Text( + Column( modifier = Modifier - .align(Alignment.CenterVertically), - text = title, - style = MaterialTheme.typography.titleLarge, - color = titleColor, - fontSize = 20.sp - ) + .align(Alignment.CenterVertically) + .padding(end = 36.dp) + .fillMaxWidth() + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = titleColor, + fontSize = 20.sp + ) + description?.let { + Text( + text = description, + color = titleColor, + fontSize = 14.sp + ) + } + } Spacer(modifier = Modifier.weight(1f)) RadioButton( - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier.align(Alignment.CenterVertically).weight(1f), selected = selected, onClick = null ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt index b5198b8..cb52057 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt @@ -173,10 +173,7 @@ private fun BackdropGallery( with (pagerState) { val target = if (currentPage < pageCount - 1) currentPage + 1 else 0 - animateScrollToPage( - page = target, - pageOffset = 0f - ) + pagerState.animateScrollToPage(target) } } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/onboarding/OnboardingPage.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/onboarding/OnboardingPage.kt index c5f8cb4..8a4570b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/onboarding/OnboardingPage.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/onboarding/OnboardingPage.kt @@ -1,28 +1,197 @@ package com.owenlejeune.tvtime.ui.screens.onboarding +import android.os.Build import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Brightness6 +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.DarkMode +import androidx.compose.material.icons.outlined.LightMode +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.kieronquinn.monetcompat.core.MonetCompat import com.owenlejeune.tvtime.R +import com.owenlejeune.tvtime.preferences.AppPreferences +import com.owenlejeune.tvtime.ui.components.RadioButtonPreference +import com.owenlejeune.tvtime.ui.components.SearchBar +import com.owenlejeune.tvtime.ui.components.SwitchPreference +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject sealed class OnboardingPage( + val order: Int, @StringRes val title: Int, @StringRes val description: Int, @DrawableRes val image: Int? = null, - val additionalContent: @Composable (AppCompatActivity) -> Unit = {} + val additionalContent: @Composable (AppCompatActivity, MutableState) -> Unit = { a, e -> } ) { - companion object { - fun values() = listOf(ExamplePage) + companion object: KoinComponent { + private val preferences: AppPreferences by inject() + + fun values() = listOf(IntroPage, SearchViewSettings, SearchTypeSettings, DarkModePage, SystemColorPage).sortedBy { it.order } operator fun get(i: Int) = values()[i] } - object ExamplePage: OnboardingPage( - R.string.app_name, - R.string.example_page_desc, - R.drawable.ic_launcher_foreground + object IntroPage: OnboardingPage( + order = 0, + title = R.string.app_name, + description = R.string.intro_page_desc, + image = R.drawable.ic_launcher_foreground + ) + + object SignInPage: OnboardingPage( + order = 1, + title = R.string.sign_in_page_title, + description = R.string.sign_in_page_description, + image = null, + additionalContent = @Composable { activity, nextButtonEnabled -> + + } + ) + + object SearchViewSettings: OnboardingPage( + order = 2, + title = R.string.search_page_title, + description = R.string.search_page_description, + image = R.drawable.ic_search, + additionalContent = @Composable { _, _ -> + val persistentSearch = remember { mutableStateOf(preferences.showSearchBar) } + + if (persistentSearch.value) { + SearchBar(placeholder = "") {} + } else { + FloatingActionButton(onClick = {}) { + Icon( + Icons.Filled.Search, + stringResource(id = R.string.preference_heading_search) + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + 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.showSearchBar = isChecked + } + ) + } + ) + + object SearchTypeSettings: OnboardingPage( + order = 3, + title = R.string.search_type_title, + description = R.string.search_type_description, + image = R.drawable.ic_search, + additionalContent = @Composable { _, _ -> + val multiSearch = remember { mutableStateOf(preferences.multiSearch) } + SwitchPreference( + titleText = stringResource(R.string.preference_multi_search_title), + subtitleText = stringResource(R.string.preference_multi_search_subtitle), + checkState = multiSearch.value, + onCheckedChange = { isChecked -> + multiSearch.value = isChecked + preferences.multiSearch = isChecked + } + ) + } + ) + + object DarkModePage: OnboardingPage( + order = 4, + title = R.string.dark_mode_page_title, + description = R.string.dark_mode_page_description, + image = R.drawable.ic_baseline_dark_mode, + additionalContent = @Composable { activity, _ -> + val selectedValue = remember { mutableStateOf(preferences.darkTheme) } + + val isSelected: (Int) -> Boolean = { selectedValue.value == it } + val onChangeState: (Int) -> Unit = { + selectedValue.value = it + preferences.darkTheme = it + activity.recreate() + } + + RadioButtonPreference( + selected = isSelected(AppPreferences.DarkMode.Automatic.ordinal), + title = stringResource(R.string.preference_dark_mode_follow_system_label), + description = "Automatically change between dark and light mode", + icon = Icons.Filled.Brightness6, + onClick = { + onChangeState(AppPreferences.DarkMode.Automatic.ordinal) + } + ) + RadioButtonPreference( + selected = isSelected(AppPreferences.DarkMode.Light.ordinal), + title = stringResource(R.string.preference_dark_mode_light_mode_label), + icon = Icons.Outlined.LightMode, + onClick = { + onChangeState(AppPreferences.DarkMode.Light.ordinal) + } + ) + RadioButtonPreference( + selected = isSelected(AppPreferences.DarkMode.Dark.ordinal), + title = stringResource(R.string.preference_dark_mode_dark_mode_label), + icon = Icons.Outlined.DarkMode, + onClick = { + onChangeState(AppPreferences.DarkMode.Dark.ordinal) + } + ) + } + ) + + object SystemColorPage: OnboardingPage( + order = 4, + title = R.string.system_color_page_title, + description = R.string.system_color_page_description, + image = R.drawable.ic_baseline_palette, + additionalContent = @Composable { activity, _ -> + val useWallpaperColors = remember { mutableStateOf(preferences.useWallpaperColors) } + SwitchPreference( + titleText = stringResource(R.string.preferences_use_wallpaper_colors_title), + subtitleText = stringResource(R.string.preferences_use_wallpaper_colors_subtitle), + checkState = useWallpaperColors.value, + onCheckedChange = { isChecked -> + useWallpaperColors.value = isChecked + preferences.useWallpaperColors = isChecked + activity.recreate() + } + ) + + 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), + subtitleText = stringResource(R.string.preference_system_colors_subtitle), + checkState = useSystemColors.value, + onCheckedChange = { isChecked -> + useSystemColors.value = isChecked + preferences.useSystemColors = isChecked + MonetCompat.useSystemColorsOnAndroid12 = isChecked + MonetCompat.getInstance().updateMonetColors() + }, + enabled = useWallpaperColors.value + ) + } + } ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_dark_mode.xml b/app/src/main/res/drawable/ic_baseline_dark_mode.xml new file mode 100644 index 0000000..c384362 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_dark_mode.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_palette.xml b/app/src/main/res/drawable/ic_baseline_palette.xml new file mode 100644 index 0000000..c3d2c7b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_palette.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13c024b..6201fb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,8 +132,18 @@ tvtime.auth.return - Get started - This is an example + Start + This is an example + Sign into TMDb + Sign into TMDb to add content to your favourites or watchlist and leave reviews + Search type + Show search view with a search bar or fab + Search mode + Search across individual media types, or across all at once + Dark mode + + Theme management + Theme the app using colors pulled from your wallpaper or the color theme built into the app TV Series