diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt index 5bd8f2b08..d1e5a1dd0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt @@ -1,23 +1,31 @@ package org.schabi.newpipe.settings import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import org.schabi.newpipe.R +import org.schabi.newpipe.ui.SettingsRoutes import org.schabi.newpipe.ui.TextPreference +import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall @Composable fun SettingsScreen( - onSelectSettingOption: (SettingsScreenKey) -> Unit, + onSelectSettingOption: (settingsRoute: SettingsRoutes) -> Unit, modifier: Modifier = Modifier ) { Column(modifier = modifier) { TextPreference( title = R.string.settings_category_debug_title, - onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) } + onClick = { onSelectSettingOption(SettingsRoutes.SettingsDebugRoute) } + ) + HorizontalDivider( + color = MaterialTheme.colorScheme.onBackground, + thickness = 0.6.dp, + modifier = Modifier.padding(horizontal = SpacingExtraSmall) ) - HorizontalDivider(color = Color.Black) } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt index 821ff0187..2fc005164 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt @@ -10,21 +10,19 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import dagger.hilt.android.AndroidEntryPoint import org.schabi.newpipe.R import org.schabi.newpipe.settings.viewmodel.SettingsViewModel +import org.schabi.newpipe.ui.SettingsRoutes import org.schabi.newpipe.ui.Toolbar import org.schabi.newpipe.ui.theme.AppTheme -const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY" - @AndroidEntryPoint class SettingsV2Activity : ComponentActivity() { @@ -35,37 +33,44 @@ class SettingsV2Activity : ComponentActivity() { setContent { val navController = rememberNavController() - var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) } - navController.addOnDestinationChangedListener { _, _, arguments -> - screenTitle = - arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle + val navBackStackEntry by navController.currentBackStackEntryAsState() + @StringRes val screenTitleRes by remember(navBackStackEntry) { + mutableIntStateOf( + when (navBackStackEntry?.destination?.route) { + SettingsRoutes.SettingsMainRoute::class.java.canonicalName -> SettingsRoutes.SettingsMainRoute.screenTitleRes + SettingsRoutes.SettingsDebugRoute::class.java.canonicalName -> SettingsRoutes.SettingsDebugRoute.screenTitleRes + else -> R.string.settings + } + ) } AppTheme { Scaffold(topBar = { Toolbar( - title = stringResource(id = screenTitle), + title = stringResource(screenTitleRes), + onNavigateBack = { + if (!navController.popBackStack()) { + finish() + } + }, hasSearch = true, - onSearchQueryChange = null // TODO: Add suggestions logic + onSearch = { + // TODO: Add suggestions logic + }, + searchResults = emptyList() ) }) { padding -> NavHost( navController = navController, - startDestination = SettingsScreenKey.ROOT.name, + startDestination = SettingsRoutes.SettingsMainRoute, modifier = Modifier.padding(padding) ) { - composable( - SettingsScreenKey.ROOT.name, - listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle)) - ) { - SettingsScreen(onSelectSettingOption = { screen -> - navController.navigate(screen.name) + composable { + SettingsScreen(onSelectSettingOption = { route -> + navController.navigate(route) }) } - composable( - SettingsScreenKey.DEBUG.name, - listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle)) - ) { + composable { DebugScreen(settingsViewModel) } } @@ -74,12 +79,3 @@ class SettingsV2Activity : ComponentActivity() { } } } - -fun createScreenTitleArg(@StringRes screenTitle: Int) = navArgument(SCREEN_TITLE_KEY) { - defaultValue = screenTitle -} - -enum class SettingsScreenKey(@StringRes val screenTitle: Int) { - ROOT(R.string.settings), - DEBUG(R.string.settings_category_debug_title) -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/SettingsRoutes.kt b/app/src/main/java/org/schabi/newpipe/ui/SettingsRoutes.kt new file mode 100644 index 000000000..93892d67c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/SettingsRoutes.kt @@ -0,0 +1,17 @@ +package org.schabi.newpipe.ui + +import androidx.annotation.StringRes +import kotlinx.serialization.Serializable +import org.schabi.newpipe.R + +// Settings screens +@Serializable +sealed class SettingsRoutes( + @get:StringRes + val screenTitleRes: Int +) { + @Serializable + object SettingsMainRoute : SettingsRoutes(R.string.settings) + @Serializable + object SettingsDebugRoute : SettingsRoutes(R.string.settings_category_debug_title) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt index de0c97540..9f254d359 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt @@ -1,45 +1,60 @@ package org.schabi.newpipe.ui +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.tooling.preview.Preview import org.schabi.newpipe.R import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.ui.theme.SizeTokens +import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall @Composable fun TextAction(text: String, modifier: Modifier = Modifier) { - Text(text = text, color = MaterialTheme.colorScheme.onSurface, modifier = modifier) + Text(text = text, color = MaterialTheme.colorScheme.onPrimary, modifier = modifier) } @Composable -fun NavigationIcon() { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", - modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall) - ) +fun NavigationIcon(navigateBack: () -> Unit) { + IconButton(onClick = navigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall) + ) + } } @Composable @@ -53,19 +68,27 @@ fun SearchSuggestionItem(text: String) { fun Toolbar( title: String, modifier: Modifier = Modifier, - hasNavigationIcon: Boolean = true, + onNavigateBack: (() -> Unit)? = null, hasSearch: Boolean = false, - onSearchQueryChange: ((String) -> List)? = null, + onSearch: (String) -> Unit, + searchResults: List, actions: @Composable RowScope.() -> Unit = {} ) { var isSearchActive by remember { mutableStateOf(false) } - var query by remember { mutableStateOf("") } + var expanded by rememberSaveable { mutableStateOf(false) } + val textFieldState = rememberTextFieldState() Column { TopAppBar( title = { Text(text = title) }, modifier = modifier, - navigationIcon = { if (hasNavigationIcon) NavigationIcon() }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + ), + navigationIcon = { + onNavigateBack?.let { NavigationIcon(onNavigateBack) } + }, actions = { actions() if (hasSearch) { @@ -73,40 +96,67 @@ fun Toolbar( Icon( painterResource(id = R.drawable.ic_search), contentDescription = stringResource(id = R.string.search), - tint = MaterialTheme.colorScheme.onSurface + tint = MaterialTheme.colorScheme.onPrimary ) } } } ) if (isSearchActive) { - SearchBar( - query = query, - onQueryChange = { query = it }, - onSearch = {}, - placeholder = { - Text(text = stringResource(id = R.string.search)) - }, - active = true, - onActiveChange = { - isSearchActive = it - } + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } ) { - onSearchQueryChange?.invoke(query)?.takeIf { it.isNotEmpty() } - ?.map { suggestionText -> SearchSuggestionItem(text = suggestionText) } - ?: run { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = textFieldState.text.toString(), + onQueryChange = { textFieldState.edit { replace(0, length, it) } }, + onSearch = { + onSearch(textFieldState.text.toString()) + expanded = false + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text(text = stringResource(id = R.string.search)) }, + modifier = Modifier.padding(horizontal = SpacingExtraSmall) + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + if (searchResults.isEmpty()) { Box( modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(), - contentAlignment = Alignment.Center + .fillMaxSize() + .padding(SpacingExtraSmall), + contentAlignment = Alignment.Center, ) { Column { Text(text = "╰(°●°╰)") Text(text = stringResource(id = R.string.search_no_results)) } } + } else { + LazyColumn { + items(searchResults) { result -> + ListItem( + headlineContent = { SearchSuggestionItem(result) }, + modifier = Modifier + .clickable { + textFieldState.edit { replace(0, length, result) } + expanded = false + } + .fillMaxWidth() + ) + } + } } + } } } } @@ -119,7 +169,8 @@ fun ToolbarPreview() { Toolbar( title = "Title", hasSearch = true, - onSearchQueryChange = { emptyList() }, + onSearch = {}, + searchResults = emptyList(), actions = { TextAction(text = "Action1") TextAction(text = "Action2") diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt b/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt index 87625a385..b14aa68df 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt @@ -1,8 +1,11 @@ package org.schabi.newpipe.ui.theme +// Color.kt is generated using the Material theme builder https://material-foundation.github.io/material-theme-builder/ +// TODO: Update the colors to properly match the existing color scheme + also add colors schemes for other services + import androidx.compose.ui.graphics.Color -val primaryLight = Color(0xFF904A45) +val primaryLight = Color(0xFFE53935) val onPrimaryLight = Color(0xFFFFFFFF) val primaryContainerLight = Color(0xFFFFDAD6) val onPrimaryContainerLight = Color(0xFF3B0908) @@ -38,8 +41,8 @@ val surfaceContainerLight = Color(0xFFFCEAE8) val surfaceContainerHighLight = Color(0xFFF6E4E2) val surfaceContainerHighestLight = Color(0xFFF1DEDC) -val primaryDark = Color(0xFFFFB3AC) -val onPrimaryDark = Color(0xFF571E1B) +val primaryDark = Color(0xFF992722) +val onPrimaryDark = Color(0xFFF4D2D2) val primaryContainerDark = Color(0xFF73332F) val onPrimaryContainerDark = Color(0xFFFFDAD6) val secondaryDark = Color(0xFFE7BDB8) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bea2550ad..47ab32c1e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -137,7 +137,7 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } kotlinx-coroutines-rx3 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx3", version.ref = "kotlinxCoroutinesRx3" } kotlinx-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" } leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" } leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }