diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c9215a4a1be..75e20bcaf1c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -271,9 +271,14 @@ dependencies { implementation(libs.androidx.compose.ui.text) // Needed for parsing HTML to AnnotatedString implementation(libs.androidx.compose.material.icons.extended) + // Jetpack navigatio3 + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.viewmodel) + // Jetpack Compose related dependencies implementation(libs.androidx.paging.compose) - implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.hilt.navigation.compose) // Coroutines interop implementation(libs.kotlinx.coroutines.rx3) diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java deleted file mode 100644 index a2d65f6f471..00000000000 --- a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.content.Intent; - -import leakcanary.LeakCanary; - -/** - * Build variant dependent (BVD) leak canary API implementation for the debug settings fragment. - * This class is loaded via reflection by - * {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}. - */ -@SuppressWarnings("unused") // Class is used but loaded via reflection -public class DebugSettingsBVDLeakCanary - implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI { - - @Override - public Intent getNewLeakDisplayActivityIntent() { - return LeakCanary.INSTANCE.newLeakDisplayActivityIntent(); - } -} diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.kt b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.kt new file mode 100644 index 00000000000..5ad63635f47 --- /dev/null +++ b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.kt @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings + +import android.content.Intent +import leakcanary.LeakCanary.newLeakDisplayActivityIntent + +/** + * Build variant dependent (BVD) leak canary API implementation for the debug settings fragment. + * This class is loaded via reflection by + * [DebugSettingsBVDLeakCanaryAPI]. + */ +@Suppress("unused") // Class is used but loaded via reflection +class DebugSettingsBVDLeakCanary : + + DebugSettingsBVDLeakCanaryAPI { + override fun getNewLeakDisplayActivityIntent(): Intent { + return newLeakDisplayActivityIntent() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt b/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt new file mode 100644 index 00000000000..b791fb80495 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2017-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +sealed interface Screen : NavKey { + + sealed interface Settings : Screen { + @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 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt deleted file mode 100644 index 45d85866e7a..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.schabi.newpipe.settings - -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 org.schabi.newpipe.R -import org.schabi.newpipe.settings.viewmodel.SettingsViewModel -import org.schabi.newpipe.ui.SwitchPreference -import org.schabi.newpipe.ui.theme.SizeTokens - -@Composable -fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) { - val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState() - - Column(modifier = modifier) { - SwitchPreference( - modifier = Modifier.padding(SizeTokens.SpacingExtraSmall), - R.string.settings_layout_redesign, - settingsLayoutRedesign, - viewModel::toggleSettingsLayoutRedesign - ) - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanaryAPI.kt b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanaryAPI.kt new file mode 100644 index 00000000000..777df508463 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanaryAPI.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings + +import android.content.Intent + +/** + * Build variant dependent (BVD) leak canary API. + * Why is LeakCanary not used directly? Because it can't be assured to be available. + */ +interface DebugSettingsBVDLeakCanaryAPI { + fun getNewLeakDisplayActivityIntent(): Intent + + companion object { + const val IMPL_CLASS = "org.schabi.newpipe.settings.DebugSettingsBVDLeakCanary" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 229de7005c3..f822e46f23d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.settings; -import android.content.Intent; import android.os.Bundle; import androidx.preference.Preference; @@ -88,15 +93,4 @@ private Optional getBVDLeakCanary() { return Optional.empty(); } } - - /** - * Build variant dependent (BVD) leak canary API for this fragment. - * Why is LeakCanary not used directly? Because it can't be assured - */ - public interface DebugSettingsBVDLeakCanaryAPI { - String IMPL_CLASS = - "org.schabi.newpipe.settings.DebugSettingsBVDLeakCanary"; - - Intent getNewLeakDisplayActivityIntent(); - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt deleted file mode 100644 index 5bd8f2b088b..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.schabi.newpipe.settings - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.schabi.newpipe.R -import org.schabi.newpipe.ui.TextPreference - -@Composable -fun SettingsScreen( - onSelectSettingOption: (SettingsScreenKey) -> Unit, - modifier: Modifier = Modifier -) { - Column(modifier = modifier) { - TextPreference( - title = R.string.settings_category_debug_title, - onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) } - ) - 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 821ff018746..bcbc43ef3a3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt @@ -1,85 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.settings import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.padding -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.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.Toolbar +import org.schabi.newpipe.ui.screens.settings.navigation.SettingsNavigation import org.schabi.newpipe.ui.theme.AppTheme -const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY" - @AndroidEntryPoint class SettingsV2Activity : ComponentActivity() { - private val settingsViewModel: SettingsViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - val navController = rememberNavController() - var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) } - navController.addOnDestinationChangedListener { _, _, arguments -> - screenTitle = - arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle - } - AppTheme { - Scaffold(topBar = { - Toolbar( - title = stringResource(id = screenTitle), - hasSearch = true, - onSearchQueryChange = null // TODO: Add suggestions logic - ) - }) { padding -> - NavHost( - navController = navController, - startDestination = SettingsScreenKey.ROOT.name, - modifier = Modifier.padding(padding) - ) { - composable( - SettingsScreenKey.ROOT.name, - listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle)) - ) { - SettingsScreen(onSelectSettingOption = { screen -> - navController.navigate(screen.name) - }) - } - composable( - SettingsScreenKey.DEBUG.name, - listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle)) - ) { - DebugScreen(settingsViewModel) - } - } - } + SettingsNavigation( + onExitSettings = { finish() } + ) } } } } - -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/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt deleted file mode 100644 index 7453096d6a3..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.schabi.newpipe.settings.viewmodel - -import android.app.Application -import android.content.Context -import android.content.SharedPreferences -import androidx.lifecycle.AndroidViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.schabi.newpipe.R -import org.schabi.newpipe.util.Localization - -@HiltViewModel -class SettingsViewModel @Inject constructor( - @ApplicationContext context: Context, - private val preferenceManager: SharedPreferences -) : AndroidViewModel(context.applicationContext as Application) { - - private var settingsLayoutRedesignPref: Boolean - get() = preferenceManager.getBoolean( - Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key), - false - ) - set(value) { - preferenceManager.edit().putBoolean( - Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key), - value - ).apply() - } - private val _settingsLayoutRedesign: MutableStateFlow = - MutableStateFlow(settingsLayoutRedesignPref) - val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow() - - fun toggleSettingsLayoutRedesign(newState: Boolean) { - _settingsLayoutRedesign.value = newState - settingsLayoutRedesignPref = newState - } -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt b/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt index 37e46e34ff2..1b35272f679 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt @@ -1,53 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.ui -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import org.schabi.newpipe.ui.theme.SizeTokens @Composable fun SwitchPreference( modifier: Modifier = Modifier, - @StringRes title: Int, + title: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, - @StringRes summary: Int? = null + summary: String? = null, + enabled: Boolean = true ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = modifier.fillMaxWidth() + modifier = modifier + .fillMaxWidth() + .padding(SizeTokens.SpacingSmall) ) { - Column { - Text( - text = stringResource(id = title), - modifier = Modifier.padding(SizeTokens.SpacingExtraSmall), - style = MaterialTheme.typography.titleSmall, - textAlign = TextAlign.Start - ) - summary?.let { - Text( - text = stringResource(id = summary), - modifier = Modifier.padding(SizeTokens.SpacingExtraSmall), - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Start - ) - } + Column( + modifier = Modifier.weight(1f) + ) { + TextBase(title = title, summary = summary, enabled = enabled) } - Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall)) - Switch(checked = isChecked, onCheckedChange = onCheckedChange) + Spacer(modifier = Modifier.width(SizeTokens.SpacingExtraSmall)) + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange, + enabled = enabled + ) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt b/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt new file mode 100644 index 00000000000..1bf390bfd25 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview + +/** + * A base composable that displays a title and an optional summary text. Used in settings preference + * items such as TextPreference and SwitchPreference. + * + * @param title the title text to display + * @param summary the optional summary text to display below the title + * @param enabled whether the text should be displayed in an enabled or disabled state + */ +@Composable +internal fun TextBase( + title: String, + summary: String?, + enabled: Boolean = true +) { + Column { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Start, + color = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) + summary?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Start, + color = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun TextBasePreview() { + TextBase("Debug", "Debug settings summary") +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt b/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt index 2fad42d4d9a..8b8a751ac5c 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.ui import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,54 +18,41 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import org.schabi.newpipe.ui.theme.SizeTokens @Composable fun TextPreference( modifier: Modifier = Modifier, - @StringRes title: Int, + title: String, @DrawableRes icon: Int? = null, - @StringRes summary: Int? = null, - onClick: () -> Unit + summary: String? = null, + onClick: () -> Unit, + enabled: Boolean = true ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.Start, modifier = modifier .fillMaxWidth() .padding(SizeTokens.SpacingSmall) .defaultMinSize(minHeight = SizeTokens.SpaceMinSize) - .clickable { onClick() } + .clickable(enabled = enabled) { onClick() } ) { icon?.let { Icon( painter = painterResource(id = icon), - contentDescription = "icon for $title preference" + contentDescription = null, + tint = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) ) Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall)) } Column { - Text( - text = stringResource(id = title), - modifier = Modifier.padding(SizeTokens.SpacingExtraSmall), - style = MaterialTheme.typography.titleSmall, - textAlign = TextAlign.Start - ) - summary?.let { - Text( - text = stringResource(id = summary), - modifier = Modifier.padding(SizeTokens.SpacingExtraSmall), - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Start - ) - } + TextBase(title = title, summary = summary, enabled = enabled) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt index 4780e78a346..ebed0311cdd 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt @@ -1,8 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.ui.components.common import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api @@ -10,11 +22,23 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +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.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.tooling.preview.Preview +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.theme.AppTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -22,42 +46,148 @@ fun ScaffoldWithToolbar( title: String, onBackClick: () -> Unit, actions: @Composable RowScope.() -> Unit = {}, + hasSearch: Boolean = false, + onSearchQueryChange: ((String) -> List)? = null, + onSearchAction: ((String) -> Unit)? = null, + searchPlaceholder: @Composable (() -> Unit)? = null, content: @Composable (PaddingValues) -> Unit ) { + var isSearchActive by rememberSaveable { mutableStateOf(false) } + var query by rememberSaveable { mutableStateOf("") } + Scaffold( topBar = { - TopAppBar( - title = { Text(text = title) }, - // TODO decide whether to use default colors instead - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer, - navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer - ), - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null + if (isSearchActive) { + SearchBar( + inputField = { + SearchBarDefaults.InputField( + query = query, + onQueryChange = { query = it }, + onSearch = { + onSearchAction?.invoke(it) + isSearchActive = false + }, + expanded = true, + onExpandedChange = { isSearchActive = it }, + placeholder = searchPlaceholder ?: { + Text(stringResource(id = R.string.search)) + }, + leadingIcon = { + IconButton(onClick = { + isSearchActive = false + query = "" + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.navigate_back) + ) + } + } ) + }, + expanded = true, + onExpandedChange = { isSearchActive = it } + ) { + val suggestions = onSearchQueryChange?.invoke(query) ?: emptyList() + if (suggestions.isNotEmpty()) { + Column(Modifier.fillMaxWidth()) { + suggestions.forEach { suggestionText -> + SearchSuggestionItem(text = suggestionText) + } + } + } else { + DefaultSearchNoResults() + } + } + } else { + TopAppBar( + title = { Text(text = title) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.navigate_back) + ) + } + }, + actions = { + actions() + // existing actions + if (hasSearch) { + // Show search icon + IconButton(onClick = { isSearchActive = true }) { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = stringResource(id = R.string.search), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } } - }, - actions = actions - ) + ) + } }, content = content ) } +@Composable +fun SearchSuggestionItem(text: String) { + // TODO: Add more components here to display all the required details of a search suggestion item. + Text(text = text) +} + +@Composable +private fun DefaultSearchNoResults() { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column { + Text(text = "╰(°●°╰)") + Text(text = stringResource(id = R.string.search_no_results)) + } + } +} + @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ScaffoldWithToolbarPreview() { - ScaffoldWithToolbar( - title = "Example", - onBackClick = {}, - content = {} - ) + AppTheme { + ScaffoldWithToolbar( + title = "Example", + onBackClick = {}, + hasSearch = true, + onSearchQueryChange = { query -> + if (query.isNotBlank()) { + listOf("Suggestion 1 for $query", "Suggestion 2 for $query") + } else { + emptyList() + } + }, + onSearchAction = { query -> + println("Searching for: $query") + }, + content = { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Screen Content") + } + } + ) + } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/settings/BooleanPreference.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/BooleanPreference.kt new file mode 100644 index 00000000000..1e29a117abe --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/BooleanPreference.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui.screens.settings + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.StringRes +import androidx.core.content.edit +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.schabi.newpipe.util.Localization + +/** + * Encapsulates the state and update logic for a boolean preference. + * + * Registers a [SharedPreferences.OnSharedPreferenceChangeListener] so the + * exposed [state] stays in sync even when the preference is changed externally + * (e.g. by another screen, a background service, or a data migration). + * + * @param keyResId The string resource ID for the preference key. + * @param defaultValue The default value of the preference. + * @param context The application context. + * @param preferenceManager The [SharedPreferences] manager. + */ +internal class BooleanPreference( + @StringRes keyResId: Int, + private val defaultValue: Boolean, + context: Context, + private val preferenceManager: SharedPreferences +) { + private val key = Localization.compatGetString(context, keyResId) + private val _state = MutableStateFlow(preferenceManager.getBoolean(key, defaultValue)) + val state: StateFlow = _state.asStateFlow() + + private val listener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, changedKey -> + if (changedKey == key) { + _state.value = prefs.getBoolean(key, defaultValue) + } + } + + init { + preferenceManager.registerOnSharedPreferenceChangeListener(listener) + } + + fun toggle(newValue: Boolean) { + preferenceManager.edit { putBoolean(key, newValue) } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/SettingsViewModel.kt new file mode 100644 index 00000000000..65aa3d00fd2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/SettingsViewModel.kt @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui.screens.settings + +import android.content.Context +import android.content.SharedPreferences +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import org.schabi.newpipe.R + +@HiltViewModel +class SettingsViewModel @Inject constructor( + @ApplicationContext context: Context, + preferenceManager: SharedPreferences +) : ViewModel() { + + private val settingsLayoutRedesignPref = + BooleanPreference( + R.string.settings_layout_redesign_key, + false, + context.applicationContext, + preferenceManager + ) + + val settingsLayoutRedesign = settingsLayoutRedesignPref.state + + fun toggleSettingsLayoutRedesign(newValue: Boolean) = settingsLayoutRedesignPref.toggle(newValue) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugScreen.kt new file mode 100644 index 00000000000..bcf6149cb3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugScreen.kt @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui.screens.settings.debug + +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.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 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.ui.SwitchPreference +import org.schabi.newpipe.ui.TextPreference +import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar +import org.schabi.newpipe.ui.screens.settings.SettingsViewModel + +private const val DUMMY = "Dummy" + +data class DebugScreenState( + val settingsLayoutRedesign: Boolean, + val isLeakCanaryAvailable: Boolean, + val allowHeapDumping: Boolean, + val allowDisposedExceptions: Boolean, + val showOriginalTimeAgo: Boolean, + val showCrashThePlayer: Boolean +) + +data class DebugScreenActions( + val onBackClick: () -> Unit, + val onToggleAllowHeapDumping: (Boolean) -> Unit, + val onShowMemoryLeaksClick: () -> Unit, + val onToggleAllowDisposedExceptions: (Boolean) -> Unit, + val onToggleShowOriginalTimeAgo: (Boolean) -> Unit, + val onToggleShowCrashThePlayer: (Boolean) -> Unit, + val onCheckNewStreamsClick: () -> Unit, + val onCrashTheAppClick: () -> Unit, + val onShowErrorSnackbarClick: () -> Unit, + val onCreateErrorNotificationClick: () -> Unit, + val onToggleSettingsLayoutRedesign: (Boolean) -> Unit +) + +@Composable +fun DebugScreen( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + debugViewModel: DebugSettingsViewModel = hiltViewModel(), + settingsViewModel: SettingsViewModel = hiltViewModel() +) { + val context = LocalContext.current + + val state = DebugScreenState( + settingsLayoutRedesign = settingsViewModel.settingsLayoutRedesign.collectAsState().value, + isLeakCanaryAvailable = debugViewModel.isLeakCanaryAvailable.collectAsState().value, + allowHeapDumping = debugViewModel.allowHeapDumping.collectAsState().value, + allowDisposedExceptions = debugViewModel.allowDisposedExceptions.collectAsState().value, + showOriginalTimeAgo = debugViewModel.showOriginalTimeAgo.collectAsState().value, + showCrashThePlayer = debugViewModel.showCrashThePlayer.collectAsState().value + ) + + val actions = DebugScreenActions( + onBackClick = onBackClick, + onToggleAllowHeapDumping = debugViewModel::toggleAllowHeapDumping, + onShowMemoryLeaksClick = { + debugViewModel.getLeakDisplayActivityIntent()?.let { + context.startActivity(it) + } + }, + onToggleAllowDisposedExceptions = debugViewModel::toggleAllowDisposedExceptions, + onToggleShowOriginalTimeAgo = debugViewModel::toggleShowOriginalTimeAgo, + onToggleShowCrashThePlayer = debugViewModel::toggleShowCrashThePlayer, + onCheckNewStreamsClick = debugViewModel::checkNewStreams, + onCrashTheAppClick = { + throw RuntimeException(DUMMY) + }, + onShowErrorSnackbarClick = { + ErrorUtil.showUiErrorSnackbar( + context, + DUMMY, + RuntimeException(DUMMY) + ) + }, + onCreateErrorNotificationClick = { + createNotification( + context, + ErrorInfo( + RuntimeException(DUMMY), + UserAction.UI_ERROR, + DUMMY + ) + ) + }, + onToggleSettingsLayoutRedesign = settingsViewModel::toggleSettingsLayoutRedesign + ) + + DebugScreenContent( + state = state, + actions = actions, + modifier = modifier + ) +} + +@Composable +fun DebugScreenContent( + state: DebugScreenState, + actions: DebugScreenActions, + modifier: Modifier = Modifier +) { + ScaffoldWithToolbar( + title = stringResource(id = R.string.settings_category_debug_title), + onBackClick = actions.onBackClick + ) { paddingValues -> + Column(modifier = modifier.padding(paddingValues)) { + SwitchPreference( + title = stringResource(R.string.leakcanary), + summary = stringResource( + if (state.isLeakCanaryAvailable) { + R.string.enable_leak_canary_summary + } else { + R.string.leak_canary_not_available + } + ), + isChecked = state.allowHeapDumping, + onCheckedChange = actions.onToggleAllowHeapDumping, + enabled = state.isLeakCanaryAvailable + ) + TextPreference( + title = stringResource(R.string.show_memory_leaks), + summary = if (state.isLeakCanaryAvailable) { + null + } else { + stringResource(R.string.leak_canary_not_available) + }, + onClick = actions.onShowMemoryLeaksClick, + enabled = state.isLeakCanaryAvailable + ) + SwitchPreference( + title = stringResource(R.string.enable_disposed_exceptions_title), + summary = stringResource(R.string.enable_disposed_exceptions_summary), + isChecked = state.allowDisposedExceptions, + onCheckedChange = actions.onToggleAllowDisposedExceptions + ) + SwitchPreference( + title = stringResource(R.string.show_original_time_ago_title), + summary = stringResource(R.string.show_original_time_ago_summary), + isChecked = state.showOriginalTimeAgo, + onCheckedChange = actions.onToggleShowOriginalTimeAgo + ) + SwitchPreference( + title = stringResource(R.string.show_crash_the_player_title), + summary = stringResource(R.string.show_crash_the_player_summary), + isChecked = state.showCrashThePlayer, + onCheckedChange = actions.onToggleShowCrashThePlayer + ) + TextPreference( + title = stringResource(R.string.check_new_streams), + onClick = actions.onCheckNewStreamsClick + ) + TextPreference( + title = stringResource(R.string.crash_the_app), + onClick = actions.onCrashTheAppClick + ) + TextPreference( + title = stringResource(R.string.show_error_snackbar), + onClick = actions.onShowErrorSnackbarClick + ) + TextPreference( + title = stringResource(R.string.create_error_notification), + onClick = actions.onCreateErrorNotificationClick + ) + SwitchPreference( + title = stringResource(R.string.settings_layout_redesign), + isChecked = state.settingsLayoutRedesign, + onCheckedChange = actions.onToggleSettingsLayoutRedesign + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun DebugScreenPreview() { + DebugScreenContent( + state = DebugScreenState( + settingsLayoutRedesign = false, + isLeakCanaryAvailable = true, + allowHeapDumping = false, + allowDisposedExceptions = true, + showOriginalTimeAgo = false, + showCrashThePlayer = true + ), + actions = DebugScreenActions( + onBackClick = {}, + onToggleAllowHeapDumping = {}, + onShowMemoryLeaksClick = {}, + onToggleAllowDisposedExceptions = {}, + onToggleShowOriginalTimeAgo = {}, + onToggleShowCrashThePlayer = {}, + onCheckNewStreamsClick = {}, + onCrashTheAppClick = {}, + onShowErrorSnackbarClick = {}, + onCreateErrorNotificationClick = {}, + onToggleSettingsLayoutRedesign = {} + ) + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugSettingsViewModel.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugSettingsViewModel.kt new file mode 100644 index 00000000000..28c69af52c4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugSettingsViewModel.kt @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui.screens.settings.debug + +import android.app.Application +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.schabi.newpipe.R +import org.schabi.newpipe.local.feed.notifications.NotificationWorker +import org.schabi.newpipe.settings.DebugSettingsBVDLeakCanaryAPI +import org.schabi.newpipe.ui.screens.settings.BooleanPreference + +@HiltViewModel +class DebugSettingsViewModel @Inject constructor( + @ApplicationContext context: Context, + preferenceManager: SharedPreferences +) : ViewModel() { + + private val application = context.applicationContext as Application + + private val bvdLeakCanaryApi: DebugSettingsBVDLeakCanaryAPI? = runCatching { + Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS) + .getDeclaredConstructor() + .newInstance() as DebugSettingsBVDLeakCanaryAPI + }.getOrNull() + + private val _isLeakCanaryAvailable = MutableStateFlow(bvdLeakCanaryApi != null) + + private val allowHeapDumpingPref = BooleanPreference( + R.string.allow_heap_dumping_key, + false, + context.applicationContext, + preferenceManager + ) + private val allowDisposedExceptionsPref = BooleanPreference( + R.string.allow_disposed_exceptions_key, + false, + context.applicationContext, + preferenceManager + ) + private val showOriginalTimeAgoPref = BooleanPreference( + R.string.show_original_time_ago_key, + false, + context.applicationContext, + preferenceManager + ) + private val showCrashThePlayerPref = BooleanPreference( + R.string.show_crash_the_player_key, + false, + context.applicationContext, + preferenceManager + ) + + val isLeakCanaryAvailable = _isLeakCanaryAvailable.asStateFlow() + val allowHeapDumping = allowHeapDumpingPref.state + val allowDisposedExceptions = allowDisposedExceptionsPref.state + val showOriginalTimeAgo = showOriginalTimeAgoPref.state + val showCrashThePlayer = showCrashThePlayerPref.state + + fun getLeakDisplayActivityIntent(): Intent? { + return bvdLeakCanaryApi?.getNewLeakDisplayActivityIntent() + } + + fun toggleAllowHeapDumping(newValue: Boolean) = allowHeapDumpingPref.toggle(newValue) + fun toggleAllowDisposedExceptions(newValue: Boolean) = allowDisposedExceptionsPref.toggle(newValue) + fun toggleShowOriginalTimeAgo(newValue: Boolean) = showOriginalTimeAgoPref.toggle(newValue) + fun toggleShowCrashThePlayer(newValue: Boolean) = showCrashThePlayerPref.toggle(newValue) + + fun checkNewStreams() { + NotificationWorker.runNow(application) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/settings/home/SettingsHomeScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/home/SettingsHomeScreen.kt new file mode 100644 index 00000000000..fff42377240 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/home/SettingsHomeScreen.kt @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui.screens.settings.home + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +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 + +@Composable +fun SettingsHomeScreen( + onNavigate: (Screen.Settings) -> Unit, + onBackClick: () -> Unit +) { + ScaffoldWithToolbar( + title = stringResource(id = R.string.settings), + onBackClick = onBackClick + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + item { + TextPreference( + title = stringResource(R.string.settings_category_player_title), + icon = R.drawable.ic_play_arrow, + onClick = { onNavigate(Screen.Settings.Player) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_player_behavior_title), + icon = R.drawable.ic_settings, + onClick = { onNavigate(Screen.Settings.Behaviour) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_downloads_title), + icon = R.drawable.ic_file_download, + onClick = { onNavigate(Screen.Settings.Download) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_look_and_feel_title), + icon = R.drawable.ic_palette, + onClick = { onNavigate(Screen.Settings.LookFeel) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_history_title), + icon = R.drawable.ic_history, + onClick = { onNavigate(Screen.Settings.HistoryCache) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_content_title), + icon = R.drawable.ic_tv, + onClick = { onNavigate(Screen.Settings.Content) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_feed_title), + icon = R.drawable.ic_rss_feed, + onClick = { onNavigate(Screen.Settings.Feed) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_services_title), + icon = R.drawable.ic_subscriptions, + onClick = { onNavigate(Screen.Settings.Services) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_language_title), + icon = R.drawable.ic_language, + onClick = { onNavigate(Screen.Settings.Language) } + ) + } + item { + TextPreference( + title = stringResource(R.string.settings_category_backup_restore_title), + icon = R.drawable.ic_backup, + onClick = { onNavigate(Screen.Settings.BackupRestore) } + ) + } + // Show Updates only on release builds + if (!BuildConfig.DEBUG) { + item { + TextPreference( + title = stringResource(R.string.settings_category_updates_title), + icon = R.drawable.ic_newpipe_update, + onClick = { onNavigate(Screen.Settings.Updates) } + ) + } + } + // Show Debug only on debug builds + if (BuildConfig.DEBUG) { + item { + TextPreference( + title = stringResource(R.string.settings_category_debug_title), + icon = R.drawable.ic_bug_report, + onClick = { onNavigate(Screen.Settings.Debug) } + ) + } + } + } + } +} + +@Preview +@Composable +private fun SettingsHomeScreenPreview() = SettingsHomeScreen(onNavigate = {}, onBackClick = {}) diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/settings/navigation/SettingsNavigation.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/navigation/SettingsNavigation.kt new file mode 100644 index 00000000000..421d71aea1d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/navigation/SettingsNavigation.kt @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.ui.screens.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.navigation.Screen +import org.schabi.newpipe.ui.screens.settings.debug.DebugScreen +import org.schabi.newpipe.ui.screens.settings.home.SettingsHomeScreen + +@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 { + SettingsHomeScreen( + onNavigate = { screen -> backStack.add(screen) }, + onBackClick = handleBack + ) + } + entry { Text(stringResource(id = R.string.settings_category_player_title)) } + entry { Text(stringResource(id = R.string.settings_category_player_behavior_title)) } + entry { Text(stringResource(id = R.string.settings_category_downloads_title)) } + entry { Text(stringResource(id = R.string.settings_category_look_and_feel_title)) } + entry { Text(stringResource(id = R.string.settings_category_history_title)) } + entry { Text(stringResource(id = R.string.settings_category_content_title)) } + entry { Text(stringResource(id = R.string.settings_category_feed_title)) } + entry { Text(stringResource(id = R.string.settings_category_services_title)) } + entry { Text(stringResource(id = R.string.settings_category_language_title)) } + entry { Text(stringResource(id = R.string.settings_category_backup_restore_title)) } + entry { Text(stringResource(id = R.string.settings_category_updates_title)) } + entry { + DebugScreen(onBackClick = { backStack.removeLastOrNull() }) + } + }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ) + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05ea6c9756a..28da627070d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -154,11 +154,15 @@ Video and audio History and cache Appearance + Look and feel Debug Updates Player notification Configure current playing stream notification Backup and restore + Content + Services + Language Playing in background Playing in popup mode Content @@ -904,4 +908,5 @@ HTTP error 403 received from server while playing, likely caused by an IP ban or streaming URL deobfuscation issues %1$s refused to provide data, asking for a login to confirm the requester is not a bot.\n\nYour IP might have been temporarily banned by %1$s, you can wait some time or switch to a different IP (for example by turning on/off a VPN, or by switching from WiFi to mobile data). This content is not available for the currently selected content country.\n\nChange your selection from \"Settings > Content > Default content country\". + Navigate back diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index caa013ba190..37f892f847a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ exoplayer = "2.19.1" fragment-compose = "1.8.9" groupie = "2.10.1" hilt = "2.58" # Newer version requires AGP 9 +hilt-navigation-compose = "1.3.0" jsoup = "1.21.2" junit = "4.13.2" junit-ext = "1.3.0" @@ -39,7 +40,7 @@ markwon = "4.6.2" material = "1.11.0" # TODO: update to newer version after bug is fixed. See https://github.com/TeamNewPipe/NewPipe/pull/13018 media = "1.7.1" mockitoCore = "5.21.0" -navigation-compose = "2.8.3" +nav3Core = "1.0.1" okhttp = "5.3.2" paging-compose = "3.3.2" phoenix = "3.0.0" @@ -89,12 +90,15 @@ androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayo androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "fragment-compose" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit-ext" } androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-media = { module = "androidx.media:media", version.ref = "media" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } +androidx-navigation3-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging-compose" } androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }