Skip to content

Commit 39a89d4

Browse files
committed
Refactor Settings Compose Setup
This PR refines the Settings screen setup using Jetpack Compose: * **Navigation Routing**: Updated navigation logic to use **Serialized Routes**, as recommended in the [official Compose navigation documentation](https://developer.android.com/develop/ui/compose/navigation). * **Screen Title Management**: Migrated screen title handling to a Compose-native approach instead of using `addOnDestinationChangedListener`. * **Toolbar Enhancements**: * Refined the toolbar layout and styling to improve alignment with Material theming. * Implemented proper ripple effects and consistent color usage. * Fixed back navigation behavior to align with Compose conventions. * **Theming Note**: The current Compose-based theme system does not yet fully replicate the color palette used in the legacy (pre-Compose) implementation, especially across different services. A `TODO` has been added in code, and a separate task will be created to address this comprehensively.
1 parent abfde87 commit 39a89d4

File tree

6 files changed

+143
-68
lines changed

6 files changed

+143
-68
lines changed
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
package org.schabi.newpipe.settings
22

33
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.padding
45
import androidx.compose.material3.HorizontalDivider
6+
import androidx.compose.material3.MaterialTheme
57
import androidx.compose.runtime.Composable
68
import androidx.compose.ui.Modifier
7-
import androidx.compose.ui.graphics.Color
9+
import androidx.compose.ui.unit.dp
810
import org.schabi.newpipe.R
11+
import org.schabi.newpipe.ui.SettingsRoutes
912
import org.schabi.newpipe.ui.TextPreference
13+
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall
1014

1115
@Composable
1216
fun SettingsScreen(
13-
onSelectSettingOption: (SettingsScreenKey) -> Unit,
17+
onSelectSettingOption: (settingsRoute: SettingsRoutes) -> Unit,
1418
modifier: Modifier = Modifier
1519
) {
1620
Column(modifier = modifier) {
1721
TextPreference(
1822
title = R.string.settings_category_debug_title,
19-
onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) }
23+
onClick = { onSelectSettingOption(SettingsRoutes.SettingsDebugRoute) }
24+
)
25+
HorizontalDivider(
26+
color = MaterialTheme.colorScheme.onBackground,
27+
thickness = 0.6.dp,
28+
modifier = Modifier.padding(horizontal = SpacingExtraSmall)
2029
)
21-
HorizontalDivider(color = Color.Black)
2230
}
2331
}

app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ import androidx.compose.material3.Scaffold
1010
import androidx.compose.runtime.getValue
1111
import androidx.compose.runtime.mutableIntStateOf
1212
import androidx.compose.runtime.remember
13-
import androidx.compose.runtime.setValue
1413
import androidx.compose.ui.Modifier
1514
import androidx.compose.ui.res.stringResource
1615
import androidx.navigation.compose.NavHost
1716
import androidx.navigation.compose.composable
17+
import androidx.navigation.compose.currentBackStackEntryAsState
1818
import androidx.navigation.compose.rememberNavController
19-
import androidx.navigation.navArgument
2019
import dagger.hilt.android.AndroidEntryPoint
2120
import org.schabi.newpipe.R
2221
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
22+
import org.schabi.newpipe.ui.SettingsRoutes
2323
import org.schabi.newpipe.ui.Toolbar
2424
import org.schabi.newpipe.ui.theme.AppTheme
2525

26-
const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY"
27-
2826
@AndroidEntryPoint
2927
class SettingsV2Activity : ComponentActivity() {
3028

@@ -35,37 +33,44 @@ class SettingsV2Activity : ComponentActivity() {
3533

3634
setContent {
3735
val navController = rememberNavController()
38-
var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) }
39-
navController.addOnDestinationChangedListener { _, _, arguments ->
40-
screenTitle =
41-
arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle
36+
val navBackStackEntry by navController.currentBackStackEntryAsState()
37+
@StringRes val screenTitleRes by remember(navBackStackEntry) {
38+
mutableIntStateOf(
39+
when (navBackStackEntry?.destination?.route) {
40+
SettingsRoutes.SettingsMainRoute::class.java.canonicalName -> SettingsRoutes.SettingsMainRoute.screenTitleRes
41+
SettingsRoutes.SettingsDebugRoute::class.java.canonicalName -> SettingsRoutes.SettingsDebugRoute.screenTitleRes
42+
else -> R.string.settings
43+
}
44+
)
4245
}
4346

4447
AppTheme {
4548
Scaffold(topBar = {
4649
Toolbar(
47-
title = stringResource(id = screenTitle),
50+
title = stringResource(screenTitleRes),
51+
onNavigateBack = {
52+
if (!navController.popBackStack()) {
53+
finish()
54+
}
55+
},
4856
hasSearch = true,
49-
onSearchQueryChange = null // TODO: Add suggestions logic
57+
onSearch = {
58+
// TODO: Add suggestions logic
59+
},
60+
searchResults = emptyList()
5061
)
5162
}) { padding ->
5263
NavHost(
5364
navController = navController,
54-
startDestination = SettingsScreenKey.ROOT.name,
65+
startDestination = SettingsRoutes.SettingsMainRoute,
5566
modifier = Modifier.padding(padding)
5667
) {
57-
composable(
58-
SettingsScreenKey.ROOT.name,
59-
listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle))
60-
) {
61-
SettingsScreen(onSelectSettingOption = { screen ->
62-
navController.navigate(screen.name)
68+
composable<SettingsRoutes.SettingsMainRoute> {
69+
SettingsScreen(onSelectSettingOption = { route ->
70+
navController.navigate(route)
6371
})
6472
}
65-
composable(
66-
SettingsScreenKey.DEBUG.name,
67-
listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle))
68-
) {
73+
composable<SettingsRoutes.SettingsDebugRoute> {
6974
DebugScreen(settingsViewModel)
7075
}
7176
}
@@ -74,12 +79,3 @@ class SettingsV2Activity : ComponentActivity() {
7479
}
7580
}
7681
}
77-
78-
fun createScreenTitleArg(@StringRes screenTitle: Int) = navArgument(SCREEN_TITLE_KEY) {
79-
defaultValue = screenTitle
80-
}
81-
82-
enum class SettingsScreenKey(@StringRes val screenTitle: Int) {
83-
ROOT(R.string.settings),
84-
DEBUG(R.string.settings_category_debug_title)
85-
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.schabi.newpipe.ui
2+
3+
import androidx.annotation.StringRes
4+
import kotlinx.serialization.Serializable
5+
import org.schabi.newpipe.R
6+
7+
// Settings screens
8+
@Serializable
9+
sealed class SettingsRoutes(
10+
@get:StringRes
11+
val screenTitleRes: Int
12+
) {
13+
@Serializable
14+
object SettingsMainRoute : SettingsRoutes(R.string.settings)
15+
@Serializable
16+
object SettingsDebugRoute : SettingsRoutes(R.string.settings_category_debug_title)
17+
}

app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,60 @@
11
package org.schabi.newpipe.ui
22

3+
import androidx.compose.foundation.clickable
34
import androidx.compose.foundation.layout.Box
45
import androidx.compose.foundation.layout.Column
56
import androidx.compose.foundation.layout.RowScope
6-
import androidx.compose.foundation.layout.fillMaxHeight
7+
import androidx.compose.foundation.layout.fillMaxSize
78
import androidx.compose.foundation.layout.fillMaxWidth
89
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.lazy.LazyColumn
11+
import androidx.compose.foundation.lazy.items
12+
import androidx.compose.foundation.text.input.rememberTextFieldState
913
import androidx.compose.material.icons.Icons
1014
import androidx.compose.material.icons.automirrored.filled.ArrowBack
1115
import androidx.compose.material3.ExperimentalMaterial3Api
1216
import androidx.compose.material3.Icon
1317
import androidx.compose.material3.IconButton
18+
import androidx.compose.material3.ListItem
1419
import androidx.compose.material3.MaterialTheme
1520
import androidx.compose.material3.SearchBar
21+
import androidx.compose.material3.SearchBarDefaults
1622
import androidx.compose.material3.Text
1723
import androidx.compose.material3.TopAppBar
24+
import androidx.compose.material3.TopAppBarDefaults
1825
import androidx.compose.runtime.Composable
1926
import androidx.compose.runtime.getValue
2027
import androidx.compose.runtime.mutableStateOf
2128
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.saveable.rememberSaveable
2230
import androidx.compose.runtime.setValue
2331
import androidx.compose.ui.Alignment
2432
import androidx.compose.ui.Modifier
2533
import androidx.compose.ui.res.painterResource
2634
import androidx.compose.ui.res.stringResource
35+
import androidx.compose.ui.semantics.isTraversalGroup
36+
import androidx.compose.ui.semantics.semantics
37+
import androidx.compose.ui.semantics.traversalIndex
2738
import androidx.compose.ui.tooling.preview.Preview
2839
import org.schabi.newpipe.R
2940
import org.schabi.newpipe.ui.theme.AppTheme
3041
import org.schabi.newpipe.ui.theme.SizeTokens
42+
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall
3143

3244
@Composable
3345
fun TextAction(text: String, modifier: Modifier = Modifier) {
34-
Text(text = text, color = MaterialTheme.colorScheme.onSurface, modifier = modifier)
46+
Text(text = text, color = MaterialTheme.colorScheme.onPrimary, modifier = modifier)
3547
}
3648

3749
@Composable
38-
fun NavigationIcon() {
39-
Icon(
40-
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back",
41-
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
42-
)
50+
fun NavigationIcon(navigateBack: () -> Unit) {
51+
IconButton(onClick = navigateBack) {
52+
Icon(
53+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
54+
contentDescription = "Back",
55+
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
56+
)
57+
}
4358
}
4459

4560
@Composable
@@ -53,60 +68,95 @@ fun SearchSuggestionItem(text: String) {
5368
fun Toolbar(
5469
title: String,
5570
modifier: Modifier = Modifier,
56-
hasNavigationIcon: Boolean = true,
71+
onNavigateBack: (() -> Unit)? = null,
5772
hasSearch: Boolean = false,
58-
onSearchQueryChange: ((String) -> List<String>)? = null,
73+
onSearch: (String) -> Unit,
74+
searchResults: List<String>,
5975
actions: @Composable RowScope.() -> Unit = {}
6076
) {
6177
var isSearchActive by remember { mutableStateOf(false) }
62-
var query by remember { mutableStateOf("") }
78+
var expanded by rememberSaveable { mutableStateOf(false) }
79+
val textFieldState = rememberTextFieldState()
6380

6481
Column {
6582
TopAppBar(
6683
title = { Text(text = title) },
6784
modifier = modifier,
68-
navigationIcon = { if (hasNavigationIcon) NavigationIcon() },
85+
colors = TopAppBarDefaults.topAppBarColors(
86+
containerColor = MaterialTheme.colorScheme.primary,
87+
titleContentColor = MaterialTheme.colorScheme.onPrimary,
88+
),
89+
navigationIcon = {
90+
onNavigateBack?.let { NavigationIcon(onNavigateBack) }
91+
},
6992
actions = {
7093
actions()
7194
if (hasSearch) {
7295
IconButton(onClick = { isSearchActive = true }) {
7396
Icon(
7497
painterResource(id = R.drawable.ic_search),
7598
contentDescription = stringResource(id = R.string.search),
76-
tint = MaterialTheme.colorScheme.onSurface
99+
tint = MaterialTheme.colorScheme.onPrimary
77100
)
78101
}
79102
}
80103
}
81104
)
82105
if (isSearchActive) {
83-
SearchBar(
84-
query = query,
85-
onQueryChange = { query = it },
86-
onSearch = {},
87-
placeholder = {
88-
Text(text = stringResource(id = R.string.search))
89-
},
90-
active = true,
91-
onActiveChange = {
92-
isSearchActive = it
93-
}
106+
Box(
107+
modifier
108+
.fillMaxSize()
109+
.semantics { isTraversalGroup = true }
94110
) {
95-
onSearchQueryChange?.invoke(query)?.takeIf { it.isNotEmpty() }
96-
?.map { suggestionText -> SearchSuggestionItem(text = suggestionText) }
97-
?: run {
111+
SearchBar(
112+
modifier = Modifier
113+
.align(Alignment.TopCenter)
114+
.semantics { traversalIndex = 0f },
115+
inputField = {
116+
SearchBarDefaults.InputField(
117+
query = textFieldState.text.toString(),
118+
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
119+
onSearch = {
120+
onSearch(textFieldState.text.toString())
121+
expanded = false
122+
},
123+
expanded = expanded,
124+
onExpandedChange = { expanded = it },
125+
placeholder = { Text(text = stringResource(id = R.string.search)) },
126+
modifier = Modifier.padding(horizontal = SpacingExtraSmall)
127+
)
128+
},
129+
expanded = expanded,
130+
onExpandedChange = { expanded = it },
131+
) {
132+
if (searchResults.isEmpty()) {
98133
Box(
99134
modifier = Modifier
100-
.fillMaxHeight()
101-
.fillMaxWidth(),
102-
contentAlignment = Alignment.Center
135+
.fillMaxSize()
136+
.padding(SpacingExtraSmall),
137+
contentAlignment = Alignment.Center,
103138
) {
104139
Column {
105140
Text(text = "╰(°●°╰)")
106141
Text(text = stringResource(id = R.string.search_no_results))
107142
}
108143
}
144+
} else {
145+
LazyColumn {
146+
items(searchResults) { result ->
147+
ListItem(
148+
headlineContent = { SearchSuggestionItem(result) },
149+
modifier = Modifier
150+
.clickable {
151+
textFieldState.edit { replace(0, length, result) }
152+
expanded = false
153+
}
154+
.fillMaxWidth()
155+
)
156+
}
157+
}
109158
}
159+
}
110160
}
111161
}
112162
}
@@ -119,7 +169,8 @@ fun ToolbarPreview() {
119169
Toolbar(
120170
title = "Title",
121171
hasSearch = true,
122-
onSearchQueryChange = { emptyList() },
172+
onSearch = {},
173+
searchResults = emptyList(),
123174
actions = {
124175
TextAction(text = "Action1")
125176
TextAction(text = "Action2")

app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.schabi.newpipe.ui.theme
22

3+
// Color.kt is generated using the Material theme builder https://material-foundation.github.io/material-theme-builder/
4+
// TODO: Update the colors to properly match the existing color scheme + also add colors schemes for other services
5+
36
import androidx.compose.ui.graphics.Color
47

5-
val primaryLight = Color(0xFF904A45)
8+
val primaryLight = Color(0xFFE53935)
69
val onPrimaryLight = Color(0xFFFFFFFF)
710
val primaryContainerLight = Color(0xFFFFDAD6)
811
val onPrimaryContainerLight = Color(0xFF3B0908)
@@ -38,8 +41,8 @@ val surfaceContainerLight = Color(0xFFFCEAE8)
3841
val surfaceContainerHighLight = Color(0xFFF6E4E2)
3942
val surfaceContainerHighestLight = Color(0xFFF1DEDC)
4043

41-
val primaryDark = Color(0xFFFFB3AC)
42-
val onPrimaryDark = Color(0xFF571E1B)
44+
val primaryDark = Color(0xFF992722)
45+
val onPrimaryDark = Color(0xFFF4D2D2)
4346
val primaryContainerDark = Color(0xFF73332F)
4447
val onPrimaryContainerDark = Color(0xFFFFDAD6)
4548
val secondaryDark = Color(0xFFE7BDB8)

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p
137137
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
138138
kotlinx-coroutines-rx3 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx3", version.ref = "kotlinxCoroutinesRx3" }
139139
kotlinx-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
140-
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
140+
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
141141
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
142142
leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
143143
leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }

0 commit comments

Comments
 (0)