refactor navigation and settings screens

This commit is contained in:
Ida Delphine
2026-03-30 01:17:14 +01:00
parent 881792ebda
commit fbfa1fd2b1
7 changed files with 273 additions and 256 deletions
@@ -5,13 +5,13 @@
package org.schabi.newpipe.navigation
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@Serializable
sealed interface Screen {
sealed interface Screen : NavKey {
sealed interface Settings: Screen {
sealed interface Settings : Screen {
@Serializable
data object Home : Settings
@@ -51,5 +51,4 @@ sealed interface Screen {
@Serializable
data object Debug : Settings
}
}
}
@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.navigation
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import org.schabi.newpipe.R
import org.schabi.newpipe.ui.screens.DebugScreen
import org.schabi.newpipe.ui.screens.SettingsScreen
@Composable
fun SettingsNavigation(onExitSettings: () -> Unit) {
val backStack = rememberNavBackStack(Screen.Settings.Home)
val handleBack: () -> Unit = {
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
onExitSettings()
}
}
NavDisplay(
backStack = backStack,
onBack = handleBack,
entryProvider = entryProvider {
entry<Screen.Settings.Home> { SettingsScreen(backStack, handleBack) }
entry<Screen.Settings.Player> { Text(stringResource(id = R.string.settings_category_player_title)) }
entry<Screen.Settings.Behaviour> { Text(stringResource(id = R.string.settings_category_player_behavior_title)) }
entry<Screen.Settings.Download> { Text(stringResource(id = R.string.settings_category_downloads_title)) }
entry<Screen.Settings.LookFeel> { Text(stringResource(id = R.string.settings_category_look_and_feel_title)) }
entry<Screen.Settings.HistoryCache> { Text(stringResource(id = R.string.settings_category_history_title)) }
entry<Screen.Settings.Content> { Text(stringResource(id = R.string.settings_category_content_title)) }
entry<Screen.Settings.Feed> { Text(stringResource(id = R.string.settings_category_feed_title)) }
entry<Screen.Settings.Services> { Text(stringResource(id = R.string.settings_category_services_title)) }
entry<Screen.Settings.Language> { Text(stringResource(id = R.string.settings_category_language_title)) }
entry<Screen.Settings.BackupRestore> { Text(stringResource(id = R.string.settings_category_backup_restore_title)) }
entry<Screen.Settings.Updates> { Text(stringResource(id = R.string.settings_category_updates_title)) }
entry<Screen.Settings.Debug> { DebugScreen(backStack) }
},
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
)
)
}
@@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.navigation
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import org.schabi.newpipe.R
import org.schabi.newpipe.settings.screens.DebugScreen
import org.schabi.newpipe.settings.screens.SettingsHomeScreen
import org.schabi.newpipe.ui.screens.Screens
@Composable
fun SettingsNavigation(onExitSettings: () -> Unit) {
val backStack = rememberNavBackStack(Screens.Settings.Home)
val handleBack: () -> Unit = {
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
onExitSettings()
}
}
NavDisplay(
backStack = backStack,
onBack = handleBack,
entryProvider = entryProvider {
entry<Screens.Settings.Home> { SettingsHomeScreen(backStack, handleBack) }
entry<Screens.Settings.Player> { Text(stringResource(id = R.string.settings_category_player_title)) }
entry<Screens.Settings.Behaviour> { Text(stringResource(id = R.string.settings_category_player_behavior_title)) }
entry<Screens.Settings.Download> { Text(stringResource(id = R.string.settings_category_downloads_title)) }
entry<Screens.Settings.LookFeel> { Text(stringResource(id = R.string.settings_category_look_and_feel_title)) }
entry<Screens.Settings.HistoryCache> { Text(stringResource(id = R.string.settings_category_history_title)) }
entry<Screens.Settings.Content> { Text(stringResource(id = R.string.settings_category_content_title)) }
entry<Screens.Settings.Feed> { Text(stringResource(id = R.string.settings_category_feed_title)) }
entry<Screens.Settings.Services> { Text(stringResource(id = R.string.settings_category_services_title)) }
entry<Screens.Settings.Language> { Text(stringResource(id = R.string.settings_category_language_title)) }
entry<Screens.Settings.BackupRestore> { Text(stringResource(id = R.string.settings_category_backup_restore_title)) }
entry<Screens.Settings.Updates> { Text(stringResource(id = R.string.settings_category_updates_title)) }
entry<Screens.Settings.Debug> { DebugScreen(backStack) }
},
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
)
)
}
@@ -1,125 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
import org.schabi.newpipe.ui.SwitchPreference
import org.schabi.newpipe.ui.TextPreference
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
private const val DUMMY = "Dummy"
@Composable
fun DebugScreen(
backStack: NavBackStack<NavKey>,
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
val isLeakCanaryAvailable by viewModel.isLeakCanaryAvailable.collectAsState()
val allowHeapDumping by viewModel.allowHeapDumping.collectAsState()
val allowDisposedExceptions by viewModel.allowDisposedExceptions.collectAsState()
val showOriginalTimeAgo by viewModel.showOriginalTimeAgo.collectAsState()
val showCrashThePlayer by viewModel.showCrashThePlayer.collectAsState()
ScaffoldWithToolbar(
title = stringResource(id = R.string.settings_category_debug_title),
onBackClick = { backStack.removeLastOrNull() }
) { paddingValues ->
Column(modifier = modifier.padding(paddingValues)) {
SwitchPreference(
title = R.string.leakcanary,
summary = if (isLeakCanaryAvailable) R.string.enable_leak_canary_summary else R.string.leak_canary_not_available,
isChecked = allowHeapDumping,
onCheckedChange = viewModel::toggleAllowHeapDumping,
enabled = isLeakCanaryAvailable
)
TextPreference(
title = R.string.show_memory_leaks,
summary = if (isLeakCanaryAvailable) null else R.string.leak_canary_not_available,
onClick = {
viewModel.getLeakDisplayActivityIntent()?.let {
context.startActivity(it)
}
},
enabled = isLeakCanaryAvailable
)
SwitchPreference(
title = R.string.enable_disposed_exceptions_title,
summary = R.string.enable_disposed_exceptions_summary,
isChecked = allowDisposedExceptions,
onCheckedChange = viewModel::toggleAllowDisposedExceptions
)
SwitchPreference(
title = R.string.show_original_time_ago_title,
summary = R.string.show_original_time_ago_summary,
isChecked = showOriginalTimeAgo,
onCheckedChange = viewModel::toggleShowOriginalTimeAgo
)
SwitchPreference(
title = R.string.show_crash_the_player_title,
summary = R.string.show_crash_the_player_summary,
isChecked = showCrashThePlayer,
onCheckedChange = viewModel::toggleShowCrashThePlayer
)
TextPreference(
title = R.string.check_new_streams,
onClick = viewModel::checkNewStreams
)
TextPreference(
title = R.string.crash_the_app,
onClick = {
throw RuntimeException(DUMMY)
}
)
TextPreference(
title = R.string.show_error_snackbar,
onClick = {
ErrorUtil.showUiErrorSnackbar(
context,
DUMMY, RuntimeException(DUMMY)
)
}
)
TextPreference(
title = R.string.create_error_notification,
onClick = {
createNotification(
context,
ErrorInfo(
RuntimeException(DUMMY),
UserAction.UI_ERROR,
DUMMY
)
)
}
)
SwitchPreference(
title = R.string.settings_layout_redesign,
isChecked = settingsLayoutRedesign,
onCheckedChange = viewModel::toggleSettingsLayoutRedesign
)
}
}
}
@@ -0,0 +1,194 @@
/*
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
import org.schabi.newpipe.ui.SwitchPreference
import org.schabi.newpipe.ui.TextPreference
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
private const val DUMMY = "Dummy"
@Composable
fun DebugScreen(
backStack: NavBackStack<NavKey>,
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = hiltViewModel()
) {
val context = LocalContext.current
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
val isLeakCanaryAvailable by viewModel.isLeakCanaryAvailable.collectAsState()
val allowHeapDumping by viewModel.allowHeapDumping.collectAsState()
val allowDisposedExceptions by viewModel.allowDisposedExceptions.collectAsState()
val showOriginalTimeAgo by viewModel.showOriginalTimeAgo.collectAsState()
val showCrashThePlayer by viewModel.showCrashThePlayer.collectAsState()
DebugScreenContent(
settingsLayoutRedesign = settingsLayoutRedesign,
isLeakCanaryAvailable = isLeakCanaryAvailable,
allowHeapDumping = allowHeapDumping,
allowDisposedExceptions = allowDisposedExceptions,
showOriginalTimeAgo = showOriginalTimeAgo,
showCrashThePlayer = showCrashThePlayer,
onBackClick = { backStack.removeLastOrNull() },
onToggleAllowHeapDumping = viewModel::toggleAllowHeapDumping,
onShowMemoryLeaksClick = {
viewModel.getLeakDisplayActivityIntent()?.let {
context.startActivity(it)
}
},
onToggleAllowDisposedExceptions = viewModel::toggleAllowDisposedExceptions,
onToggleShowOriginalTimeAgo = viewModel::toggleShowOriginalTimeAgo,
onToggleShowCrashThePlayer = viewModel::toggleShowCrashThePlayer,
onCheckNewStreamsClick = viewModel::checkNewStreams,
onCrashTheAppClick = {
throw RuntimeException(DUMMY)
},
onShowErrorSnackbarClick = {
ErrorUtil.showUiErrorSnackbar(
context,
DUMMY,
RuntimeException(DUMMY)
)
},
onCreateErrorNotificationClick = {
createNotification(
context,
ErrorInfo(
RuntimeException(DUMMY),
UserAction.UI_ERROR,
DUMMY
)
)
},
onToggleSettingsLayoutRedesign = viewModel::toggleSettingsLayoutRedesign,
modifier = modifier
)
}
@Composable
fun DebugScreenContent(
settingsLayoutRedesign: Boolean,
isLeakCanaryAvailable: Boolean,
allowHeapDumping: Boolean,
allowDisposedExceptions: Boolean,
showOriginalTimeAgo: Boolean,
showCrashThePlayer: Boolean,
onBackClick: () -> Unit,
onToggleAllowHeapDumping: (Boolean) -> Unit,
onShowMemoryLeaksClick: () -> Unit,
onToggleAllowDisposedExceptions: (Boolean) -> Unit,
onToggleShowOriginalTimeAgo: (Boolean) -> Unit,
onToggleShowCrashThePlayer: (Boolean) -> Unit,
onCheckNewStreamsClick: () -> Unit,
onCrashTheAppClick: () -> Unit,
onShowErrorSnackbarClick: () -> Unit,
onCreateErrorNotificationClick: () -> Unit,
onToggleSettingsLayoutRedesign: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
ScaffoldWithToolbar(
title = stringResource(id = R.string.settings_category_debug_title),
onBackClick = onBackClick
) { paddingValues ->
Column(modifier = modifier.padding(paddingValues)) {
SwitchPreference(
title = R.string.leakcanary,
summary = if (isLeakCanaryAvailable) R.string.enable_leak_canary_summary else R.string.leak_canary_not_available,
isChecked = allowHeapDumping,
onCheckedChange = onToggleAllowHeapDumping,
enabled = isLeakCanaryAvailable
)
TextPreference(
title = R.string.show_memory_leaks,
summary = if (isLeakCanaryAvailable) null else R.string.leak_canary_not_available,
onClick = onShowMemoryLeaksClick,
enabled = isLeakCanaryAvailable
)
SwitchPreference(
title = R.string.enable_disposed_exceptions_title,
summary = R.string.enable_disposed_exceptions_summary,
isChecked = allowDisposedExceptions,
onCheckedChange = onToggleAllowDisposedExceptions
)
SwitchPreference(
title = R.string.show_original_time_ago_title,
summary = R.string.show_original_time_ago_summary,
isChecked = showOriginalTimeAgo,
onCheckedChange = onToggleShowOriginalTimeAgo
)
SwitchPreference(
title = R.string.show_crash_the_player_title,
summary = R.string.show_crash_the_player_summary,
isChecked = showCrashThePlayer,
onCheckedChange = onToggleShowCrashThePlayer
)
TextPreference(
title = R.string.check_new_streams,
onClick = onCheckNewStreamsClick
)
TextPreference(
title = R.string.crash_the_app,
onClick = onCrashTheAppClick
)
TextPreference(
title = R.string.show_error_snackbar,
onClick = onShowErrorSnackbarClick
)
TextPreference(
title = R.string.create_error_notification,
onClick = onCreateErrorNotificationClick
)
SwitchPreference(
title = R.string.settings_layout_redesign,
isChecked = settingsLayoutRedesign,
onCheckedChange = onToggleSettingsLayoutRedesign
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DebugScreenPreview() {
DebugScreenContent(
settingsLayoutRedesign = false,
isLeakCanaryAvailable = true,
allowHeapDumping = false,
allowDisposedExceptions = true,
showOriginalTimeAgo = false,
showCrashThePlayer = true,
onBackClick = {},
onToggleAllowHeapDumping = {},
onShowMemoryLeaksClick = {},
onToggleAllowDisposedExceptions = {},
onToggleShowOriginalTimeAgo = {},
onToggleShowCrashThePlayer = {},
onCheckNewStreamsClick = {},
onCrashTheAppClick = {},
onShowErrorSnackbarClick = {},
onCreateErrorNotificationClick = {},
onToggleSettingsLayoutRedesign = {}
)
}
@@ -1,55 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.ui.screens
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* Represents the screen keys for the app.
*/
sealed interface Screens : NavKey {
sealed interface Settings : Screens {
@Serializable
data object Home : Settings
@Serializable
data object Player : Settings
@Serializable
data object Behaviour : Settings
@Serializable
data object Download : Settings
@Serializable
data object LookFeel : Settings
@Serializable
data object HistoryCache : Settings
@Serializable
data object Content : Settings
@Serializable
data object Feed : Settings
@Serializable
data object Services : Settings
@Serializable
data object Language : Settings
@Serializable
data object BackupRestore : Settings
@Serializable
data object Updates : Settings
@Serializable
data object Debug : Settings
}
}
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.screens
package org.schabi.newpipe.ui.screens
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -12,17 +12,18 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.navigation.Screen
import org.schabi.newpipe.ui.TextPreference
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
import org.schabi.newpipe.ui.screens.Screens
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsHomeScreen(backStack: NavBackStack<NavKey>, handleBack: () -> Unit) {
fun SettingsScreen(backStack: NavBackStack<NavKey>, handleBack: () -> Unit) {
ScaffoldWithToolbar(
title = stringResource(id = R.string.settings),
onBackClick = {
@@ -38,70 +39,70 @@ fun SettingsHomeScreen(backStack: NavBackStack<NavKey>, handleBack: () -> Unit)
TextPreference(
title = R.string.settings_category_player_title,
icon = R.drawable.ic_play_arrow,
onClick = { backStack.add(Screens.Settings.Player) }
onClick = { backStack.add(Screen.Settings.Player) }
)
}
item {
TextPreference(
title = R.string.settings_category_player_behavior_title,
icon = R.drawable.ic_settings,
onClick = { backStack.add(Screens.Settings.Behaviour) }
onClick = { backStack.add(Screen.Settings.Behaviour) }
)
}
item {
TextPreference(
title = R.string.settings_category_downloads_title,
icon = R.drawable.ic_file_download,
onClick = { backStack.add(Screens.Settings.Download) }
onClick = { backStack.add(Screen.Settings.Download) }
)
}
item {
TextPreference(
title = R.string.settings_category_look_and_feel_title,
icon = R.drawable.ic_palette,
onClick = { backStack.add(Screens.Settings.LookFeel) }
onClick = { backStack.add(Screen.Settings.LookFeel) }
)
}
item {
TextPreference(
title = R.string.settings_category_history_title,
icon = R.drawable.ic_history,
onClick = { backStack.add(Screens.Settings.HistoryCache) }
onClick = { backStack.add(Screen.Settings.HistoryCache) }
)
}
item {
TextPreference(
title = R.string.settings_category_content_title,
icon = R.drawable.ic_tv,
onClick = { backStack.add(Screens.Settings.Content) }
onClick = { backStack.add(Screen.Settings.Content) }
)
}
item {
TextPreference(
title = R.string.settings_category_feed_title,
icon = R.drawable.ic_rss_feed,
onClick = { backStack.add(Screens.Settings.Feed) }
onClick = { backStack.add(Screen.Settings.Feed) }
)
}
item {
TextPreference(
title = R.string.settings_category_services_title,
icon = R.drawable.ic_subscriptions,
onClick = { backStack.add(Screens.Settings.Services) }
onClick = { backStack.add(Screen.Settings.Services) }
)
}
item {
TextPreference(
title = R.string.settings_category_language_title,
icon = R.drawable.ic_language,
onClick = { backStack.add(Screens.Settings.Language) }
onClick = { backStack.add(Screen.Settings.Language) }
)
}
item {
TextPreference(
title = R.string.settings_category_backup_restore_title,
icon = R.drawable.ic_backup,
onClick = { backStack.add(Screens.Settings.BackupRestore) }
onClick = { backStack.add(Screen.Settings.BackupRestore) }
)
}
// Show Updates only on release builds
@@ -110,7 +111,7 @@ fun SettingsHomeScreen(backStack: NavBackStack<NavKey>, handleBack: () -> Unit)
TextPreference(
title = R.string.settings_category_updates_title,
icon = R.drawable.ic_newpipe_update,
onClick = { backStack.add(Screens.Settings.Updates) }
onClick = { backStack.add(Screen.Settings.Updates) }
)
}
}
@@ -120,10 +121,14 @@ fun SettingsHomeScreen(backStack: NavBackStack<NavKey>, handleBack: () -> Unit)
TextPreference(
title = R.string.settings_category_debug_title,
icon = R.drawable.ic_bug_report,
onClick = { backStack.add(Screens.Settings.Debug) }
onClick = { backStack.add(Screen.Settings.Debug) }
)
}
}
}
}
}
@Preview
@Composable
fun SettingsScreenPreview() = SettingsScreen(backStack = NavBackStack(), handleBack = {})