From de344100db4d75e7abe4e83d1dcbe151ac34e089 Mon Sep 17 00:00:00 2001 From: Hatake Kakashri Date: Sat, 15 Nov 2025 11:23:08 +0530 Subject: [PATCH 1/4] Complete the debug screen migration to Compose with Nav3 - Completed the UI and logic for pending items of Debug screen using Jetpack Compose. - Implemented a new navigation system for settings using the navigation3 lib as it is now stable. - Reuses the `ScaffoldWithToolbar` composable and removed the previous `Toolbar` composable to avoid redundancy in code. - Refactored the `SettingsViewModel` to use a `BooleanPreference` helper class to reuse and reducing boilerplate for state management. - Created a shared `TextBase` composable to remove duplicated UI logic between `SwitchPreference` and `TextPreference`. - Move the build-variant-dependent logic for LeakCanary and reused it in Compose and Fragment, this logic is used for ensuring the leak canary fields are only enabled in debug variants. - Fixed a layout bug in `SwitchPreference` where long summary text could misalign the switch component and also adjusted the paddings for consistency. --- app/build.gradle.kts | 7 +- .../settings/DebugSettingsBVDLeakCanary.java | 20 -- .../settings/DebugSettingsBVDLeakCanary.kt | 23 +++ .../settings/DebugSettingsBVDLeakCanaryAPI.kt | 20 ++ .../settings/DebugSettingsFragment.java | 18 +- .../schabi/newpipe/settings/SettingsScreen.kt | 23 --- .../newpipe/settings/SettingsV2Activity.kt | 75 +------- .../settings/navigation/SettingsNavigation.kt | 56 ++++++ .../newpipe/settings/screens/DebugScreen.kt | 125 +++++++++++++ .../settings/screens/SettingsHomeScreen.kt | 129 +++++++++++++ .../settings/viewmodel/SettingsViewModel.kt | 118 ++++++++++-- .../org/schabi/newpipe/ui/SwitchPreference.kt | 46 +++-- .../java/org/schabi/newpipe/ui/TextBase.kt | 55 ++++++ .../org/schabi/newpipe/ui/TextPreference.kt | 35 ++-- .../components/common/ScaffoldWithToolbar.kt | 176 +++++++++++++++--- .../org/schabi/newpipe/ui/screens/Screens.kt | 55 ++++++ app/src/main/res/values/strings.xml | 5 + gradle/libs.versions.toml | 8 +- 18 files changed, 784 insertions(+), 210 deletions(-) delete mode 100644 app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java create mode 100644 app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanaryAPI.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/TextBase.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt 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/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..1202fb65bbc 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.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/navigation/SettingsNavigation.kt b/app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt new file mode 100644 index 00000000000..12559c6cda1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * 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 { SettingsHomeScreen(backStack, 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(backStack) } + }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ) + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt new file mode 100644 index 00000000000..ab84b605ad1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * 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, + 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 + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt new file mode 100644 index 00000000000..1f2ed5a6c91 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.screens + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import org.schabi.newpipe.BuildConfig +import org.schabi.newpipe.R +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, handleBack: () -> Unit) { + ScaffoldWithToolbar( + title = stringResource(id = R.string.settings), + onBackClick = { + handleBack() + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + item { + TextPreference( + title = R.string.settings_category_player_title, + icon = R.drawable.ic_play_arrow, + onClick = { backStack.add(Screens.Settings.Player) } + ) + } + item { + TextPreference( + title = R.string.settings_category_player_behavior_title, + icon = R.drawable.ic_settings, + onClick = { backStack.add(Screens.Settings.Behaviour) } + ) + } + item { + TextPreference( + title = R.string.settings_category_downloads_title, + icon = R.drawable.ic_file_download, + onClick = { backStack.add(Screens.Settings.Download) } + ) + } + item { + TextPreference( + title = R.string.settings_category_look_and_feel_title, + icon = R.drawable.ic_palette, + onClick = { backStack.add(Screens.Settings.LookFeel) } + ) + } + item { + TextPreference( + title = R.string.settings_category_history_title, + icon = R.drawable.ic_history, + onClick = { backStack.add(Screens.Settings.HistoryCache) } + ) + } + item { + TextPreference( + title = R.string.settings_category_content_title, + icon = R.drawable.ic_tv, + onClick = { backStack.add(Screens.Settings.Content) } + ) + } + item { + TextPreference( + title = R.string.settings_category_feed_title, + icon = R.drawable.ic_rss_feed, + onClick = { backStack.add(Screens.Settings.Feed) } + ) + } + item { + TextPreference( + title = R.string.settings_category_services_title, + icon = R.drawable.ic_subscriptions, + onClick = { backStack.add(Screens.Settings.Services) } + ) + } + item { + TextPreference( + title = R.string.settings_category_language_title, + icon = R.drawable.ic_language, + onClick = { backStack.add(Screens.Settings.Language) } + ) + } + item { + TextPreference( + title = R.string.settings_category_backup_restore_title, + icon = R.drawable.ic_backup, + onClick = { backStack.add(Screens.Settings.BackupRestore) } + ) + } + // Show Updates only on release builds + if (!BuildConfig.DEBUG) { + item { + TextPreference( + title = R.string.settings_category_updates_title, + icon = R.drawable.ic_newpipe_update, + onClick = { backStack.add(Screens.Settings.Updates) } + ) + } + } + // Show Debug only on debug builds + if (BuildConfig.DEBUG) { + item { + TextPreference( + title = R.string.settings_category_debug_title, + icon = R.drawable.ic_bug_report, + onClick = { backStack.add(Screens.Settings.Debug) } + ) + } + } + } + } +} 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 index 7453096d6a3..29d9f27f05f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt @@ -1,40 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.settings.viewmodel import android.app.Application import android.content.Context +import android.content.Intent import android.content.SharedPreferences +import androidx.annotation.StringRes +import androidx.core.content.edit 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.StateFlow 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.util.Localization @HiltViewModel class SettingsViewModel @Inject constructor( @ApplicationContext context: Context, - private val preferenceManager: SharedPreferences + preferenceManager: SharedPreferences ) : AndroidViewModel(context.applicationContext as Application) { - private var settingsLayoutRedesignPref: Boolean - get() = preferenceManager.getBoolean( - Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key), - false + private val bvdLeakCanaryApi: DebugSettingsBVDLeakCanaryAPI? = runCatching { + // Try to find the implementation of the LeakCanary API + 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 ) - 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 + + private val showCrashThePlayerPref = BooleanPreference( + R.string.show_crash_the_player_key, + false, + context.applicationContext, + preferenceManager + ) + + private val settingsLayoutRedesignPref = + BooleanPreference( + R.string.settings_layout_redesign_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 + + val settingsLayoutRedesign = settingsLayoutRedesignPref.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(getApplication()) + } + fun toggleSettingsLayoutRedesign(newValue: Boolean) = + settingsLayoutRedesignPref.toggle(newValue) +} + +/** + * Encapsulates the state and update logic for a boolean preference. + * + * @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. + */ +private class BooleanPreference( + @StringRes keyResId: Int, + 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() + + fun toggle(newValue: Boolean) { + preferenceManager.edit { putBoolean(key, newValue) } + _state.value = newValue } } 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..3159bceb2a5 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt @@ -1,21 +1,22 @@ +/* + * 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 @@ -24,30 +25,25 @@ fun SwitchPreference( @StringRes title: Int, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, - @StringRes summary: Int? = null + @StringRes summary: Int? = 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..6fd4c2ce505 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt @@ -0,0 +1,55 @@ +/* + * 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.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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import org.schabi.newpipe.R + +/** + * 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 resource ID of the string to be used as the title + * @param summary the optional resource ID of the string to be used as the summary + * @param enabled whether the text should be displayed in an enabled or disabled state + */ +@Composable +internal fun TextBase( + @StringRes title: Int, + @StringRes summary: Int?, + enabled: Boolean = true +) { + Column { + Text( + text = stringResource(id = 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 = stringResource(id = summary), + 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 +fun TextBasePreview() { + TextBase(R.string.settings_category_debug_title, R.string.settings_category_debug_title) +} 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..72b15e0bc46 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt @@ -1,3 +1,9 @@ +/* + * 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 @@ -13,13 +19,11 @@ 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 @@ -28,39 +32,28 @@ fun TextPreference( @StringRes title: Int, @DrawableRes icon: Int? = null, @StringRes summary: Int? = null, - onClick: () -> Unit + 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 = "icon for $title preference", + 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..3c6273091ce 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/Screens.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt new file mode 100644 index 00000000000..c17280259bb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * 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 + } +} 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..35acff72954 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.0" 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" } From 881792ebda2de13fd39790306a8b8b24de9a7537 Mon Sep 17 00:00:00 2001 From: Ida Delphine Date: Sun, 29 Mar 2026 23:50:52 +0100 Subject: [PATCH 2/4] add navigation keys --- .../org/schabi/newpipe/navigation/Screen.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/src/main/java/org/schabi/newpipe/navigation/Screen.kt 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..f3cefbded06 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2017-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.navigation + +import kotlinx.serialization.Serializable + + +@Serializable +sealed interface Screen { + + 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 + } + +} \ No newline at end of file From fbfa1fd2b1f40aa1d82674bef2fc943c7e4350fa Mon Sep 17 00:00:00 2001 From: Ida Delphine Date: Mon, 30 Mar 2026 01:17:14 +0100 Subject: [PATCH 3/4] refactor navigation and settings screens --- .../org/schabi/newpipe/navigation/Screen.kt | 9 +- .../newpipe/navigation/SettingsNavigation.kt | 55 +++++ .../settings/navigation/SettingsNavigation.kt | 56 ----- .../newpipe/settings/screens/DebugScreen.kt | 125 ----------- .../schabi/newpipe/ui/screens/DebugScreen.kt | 194 ++++++++++++++++++ .../org/schabi/newpipe/ui/screens/Screens.kt | 55 ----- .../screens/SettingsScreen.kt} | 35 ++-- 7 files changed, 273 insertions(+), 256 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt rename app/src/main/java/org/schabi/newpipe/{settings/screens/SettingsHomeScreen.kt => ui/screens/SettingsScreen.kt} (75%) diff --git a/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt b/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt index f3cefbded06..b791fb80495 100644 --- a/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt +++ b/app/src/main/java/org/schabi/newpipe/navigation/Screen.kt @@ -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 } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt b/app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt new file mode 100644 index 00000000000..e3f772a30b8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * 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 { SettingsScreen(backStack, 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(backStack) } + }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ) + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt b/app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt deleted file mode 100644 index 12559c6cda1..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/navigation/SettingsNavigation.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. - * 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 { SettingsHomeScreen(backStack, 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(backStack) } - }, - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator() - ) - ) -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt deleted file mode 100644 index ab84b605ad1..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/screens/DebugScreen.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. - * 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, - 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 - ) - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt new file mode 100644 index 00000000000..6d20d68940b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. + * 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, + 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 = {} + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt deleted file mode 100644 index c17280259bb..00000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. - * 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 - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/SettingsScreen.kt similarity index 75% rename from app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt rename to app/src/main/java/org/schabi/newpipe/ui/screens/SettingsScreen.kt index 1f2ed5a6c91..20b4940f409 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/screens/SettingsHomeScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/SettingsScreen.kt @@ -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, handleBack: () -> Unit) { +fun SettingsScreen(backStack: NavBackStack, handleBack: () -> Unit) { ScaffoldWithToolbar( title = stringResource(id = R.string.settings), onBackClick = { @@ -38,70 +39,70 @@ fun SettingsHomeScreen(backStack: NavBackStack, 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, 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, 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 = {}) From d33ba8ac757b1815fb1ca999155e012338ad690d Mon Sep 17 00:00:00 2001 From: Ida Delphine Date: Mon, 30 Mar 2026 01:40:05 +0100 Subject: [PATCH 4/4] refactor settings screen, debug, viewmodels, etc --- .../schabi/newpipe/settings/DebugScreen.kt | 26 --- .../newpipe/settings/SettingsV2Activity.kt | 4 +- .../org/schabi/newpipe/ui/SwitchPreference.kt | 5 +- .../java/org/schabi/newpipe/ui/TextBase.kt | 21 +- .../org/schabi/newpipe/ui/TextPreference.kt | 7 +- .../components/common/ScaffoldWithToolbar.kt | 6 +- .../schabi/newpipe/ui/screens/DebugScreen.kt | 194 ---------------- .../ui/screens/settings/BooleanPreference.kt | 52 +++++ .../ui/screens/settings/SettingsViewModel.kt | 33 +++ .../ui/screens/settings/debug/DebugScreen.kt | 215 ++++++++++++++++++ .../settings/debug/DebugSettingsViewModel.kt} | 76 ++----- .../home/SettingsHomeScreen.kt} | 65 +++--- .../navigation/SettingsNavigation.kt | 18 +- gradle/libs.versions.toml | 2 +- 14 files changed, 382 insertions(+), 342 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/settings/BooleanPreference.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/settings/SettingsViewModel.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugScreen.kt rename app/src/main/java/org/schabi/newpipe/{settings/viewmodel/SettingsViewModel.kt => ui/screens/settings/debug/DebugSettingsViewModel.kt} (54%) rename app/src/main/java/org/schabi/newpipe/ui/screens/{SettingsScreen.kt => settings/home/SettingsHomeScreen.kt} (56%) rename app/src/main/java/org/schabi/newpipe/{ => ui/screens/settings}/navigation/SettingsNavigation.kt (80%) 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/SettingsV2Activity.kt b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt index 1202fb65bbc..bcbc43ef3a3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt @@ -10,7 +10,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import dagger.hilt.android.AndroidEntryPoint -import org.schabi.newpipe.settings.navigation.SettingsNavigation +import org.schabi.newpipe.ui.screens.settings.navigation.SettingsNavigation import org.schabi.newpipe.ui.theme.AppTheme @AndroidEntryPoint @@ -22,7 +22,7 @@ class SettingsV2Activity : ComponentActivity() { setContent { AppTheme { SettingsNavigation( - onExitSettings = { finish() }, + onExitSettings = { finish() } ) } } 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 3159bceb2a5..1b35272f679 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt @@ -6,7 +6,6 @@ package org.schabi.newpipe.ui -import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -22,10 +21,10 @@ 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( diff --git a/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt b/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt index 6fd4c2ce505..1bf390bfd25 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/TextBase.kt @@ -5,41 +5,38 @@ package org.schabi.newpipe.ui -import androidx.annotation.StringRes 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.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import org.schabi.newpipe.R /** * A base composable that displays a title and an optional summary text. Used in settings preference - * items such as TextPreference and SwitchPreference + * items such as TextPreference and SwitchPreference. * - * @param title the resource ID of the string to be used as the title - * @param summary the optional resource ID of the string to be used as the summary + * @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( - @StringRes title: Int, - @StringRes summary: Int?, + title: String, + summary: String?, enabled: Boolean = true ) { Column { Text( - text = stringResource(id = title), + 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 = stringResource(id = summary), + text = it, style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Start, color = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) @@ -50,6 +47,6 @@ internal fun TextBase( @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) @Composable -fun TextBasePreview() { - TextBase(R.string.settings_category_debug_title, R.string.settings_category_debug_title) +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 72b15e0bc46..8b8a751ac5c 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt @@ -7,7 +7,6 @@ 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 @@ -29,9 +28,9 @@ 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, + summary: String? = null, onClick: () -> Unit, enabled: Boolean = true ) { @@ -47,7 +46,7 @@ fun TextPreference( 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)) 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 3c6273091ce..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 @@ -86,7 +86,7 @@ fun ScaffoldWithToolbar( ) }, expanded = true, - onExpandedChange = { isSearchActive = it }, + onExpandedChange = { isSearchActive = it } ) { val suggestions = onSearchQueryChange?.invoke(query) ?: emptyList() if (suggestions.isNotEmpty()) { @@ -96,7 +96,7 @@ fun ScaffoldWithToolbar( } } } else { - DefaultSearchNoResults() + DefaultSearchNoResults() } } } else { @@ -107,7 +107,7 @@ fun ScaffoldWithToolbar( scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer, navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer ), navigationIcon = { IconButton(onClick = onBackClick) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt deleted file mode 100644 index 6d20d68940b..00000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/DebugScreen.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. - * 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, - 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 = {} - ) -} 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/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugSettingsViewModel.kt similarity index 54% rename from app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt rename to app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugSettingsViewModel.kt index 29d9f27f05f..28c69af52c4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/debug/DebugSettingsViewModel.kt @@ -3,38 +3,37 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package org.schabi.newpipe.settings.viewmodel +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.annotation.StringRes -import androidx.core.content.edit -import androidx.lifecycle.AndroidViewModel +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.StateFlow 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.util.Localization +import org.schabi.newpipe.ui.screens.settings.BooleanPreference @HiltViewModel -class SettingsViewModel @Inject constructor( +class DebugSettingsViewModel @Inject constructor( @ApplicationContext context: Context, preferenceManager: SharedPreferences -) : AndroidViewModel(context.applicationContext as Application) { +) : ViewModel() { + + private val application = context.applicationContext as Application private val bvdLeakCanaryApi: DebugSettingsBVDLeakCanaryAPI? = runCatching { - // Try to find the implementation of the LeakCanary API Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS) .getDeclaredConstructor() .newInstance() as DebugSettingsBVDLeakCanaryAPI }.getOrNull() + private val _isLeakCanaryAvailable = MutableStateFlow(bvdLeakCanaryApi != null) private val allowHeapDumpingPref = BooleanPreference( @@ -49,15 +48,12 @@ class SettingsViewModel @Inject constructor( context.applicationContext, preferenceManager ) - - private val showOriginalTimeAgoPref = - BooleanPreference( - R.string.show_original_time_ago_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, @@ -65,58 +61,22 @@ class SettingsViewModel @Inject constructor( preferenceManager ) - private val settingsLayoutRedesignPref = - BooleanPreference( - R.string.settings_layout_redesign_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 - val settingsLayoutRedesign = settingsLayoutRedesignPref.state - fun getLeakDisplayActivityIntent(): Intent? { return bvdLeakCanaryApi?.getNewLeakDisplayActivityIntent() } + fun toggleAllowHeapDumping(newValue: Boolean) = allowHeapDumpingPref.toggle(newValue) - fun toggleAllowDisposedExceptions(newValue: Boolean) = - allowDisposedExceptionsPref.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(getApplication()) - } - fun toggleSettingsLayoutRedesign(newValue: Boolean) = - settingsLayoutRedesignPref.toggle(newValue) -} - -/** - * Encapsulates the state and update logic for a boolean preference. - * - * @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. - */ -private class BooleanPreference( - @StringRes keyResId: Int, - 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() - fun toggle(newValue: Boolean) { - preferenceManager.edit { putBoolean(key, newValue) } - _state.value = newValue + fun checkNewStreams() { + NotificationWorker.runNow(application) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/SettingsScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/home/SettingsHomeScreen.kt similarity index 56% rename from app/src/main/java/org/schabi/newpipe/ui/screens/SettingsScreen.kt rename to app/src/main/java/org/schabi/newpipe/ui/screens/settings/home/SettingsHomeScreen.kt index 20b4940f409..fff42377240 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/home/SettingsHomeScreen.kt @@ -3,32 +3,29 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package org.schabi.newpipe.ui.screens +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.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 -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsScreen(backStack: NavBackStack, handleBack: () -> Unit) { +fun SettingsHomeScreen( + onNavigate: (Screen.Settings) -> Unit, + onBackClick: () -> Unit +) { ScaffoldWithToolbar( title = stringResource(id = R.string.settings), - onBackClick = { - handleBack() - } + onBackClick = onBackClick ) { paddingValues -> LazyColumn( modifier = Modifier @@ -37,81 +34,81 @@ fun SettingsScreen(backStack: NavBackStack, handleBack: () -> Unit) { ) { item { TextPreference( - title = R.string.settings_category_player_title, + title = stringResource(R.string.settings_category_player_title), icon = R.drawable.ic_play_arrow, - onClick = { backStack.add(Screen.Settings.Player) } + onClick = { onNavigate(Screen.Settings.Player) } ) } item { TextPreference( - title = R.string.settings_category_player_behavior_title, + title = stringResource(R.string.settings_category_player_behavior_title), icon = R.drawable.ic_settings, - onClick = { backStack.add(Screen.Settings.Behaviour) } + onClick = { onNavigate(Screen.Settings.Behaviour) } ) } item { TextPreference( - title = R.string.settings_category_downloads_title, + title = stringResource(R.string.settings_category_downloads_title), icon = R.drawable.ic_file_download, - onClick = { backStack.add(Screen.Settings.Download) } + onClick = { onNavigate(Screen.Settings.Download) } ) } item { TextPreference( - title = R.string.settings_category_look_and_feel_title, + title = stringResource(R.string.settings_category_look_and_feel_title), icon = R.drawable.ic_palette, - onClick = { backStack.add(Screen.Settings.LookFeel) } + onClick = { onNavigate(Screen.Settings.LookFeel) } ) } item { TextPreference( - title = R.string.settings_category_history_title, + title = stringResource(R.string.settings_category_history_title), icon = R.drawable.ic_history, - onClick = { backStack.add(Screen.Settings.HistoryCache) } + onClick = { onNavigate(Screen.Settings.HistoryCache) } ) } item { TextPreference( - title = R.string.settings_category_content_title, + title = stringResource(R.string.settings_category_content_title), icon = R.drawable.ic_tv, - onClick = { backStack.add(Screen.Settings.Content) } + onClick = { onNavigate(Screen.Settings.Content) } ) } item { TextPreference( - title = R.string.settings_category_feed_title, + title = stringResource(R.string.settings_category_feed_title), icon = R.drawable.ic_rss_feed, - onClick = { backStack.add(Screen.Settings.Feed) } + onClick = { onNavigate(Screen.Settings.Feed) } ) } item { TextPreference( - title = R.string.settings_category_services_title, + title = stringResource(R.string.settings_category_services_title), icon = R.drawable.ic_subscriptions, - onClick = { backStack.add(Screen.Settings.Services) } + onClick = { onNavigate(Screen.Settings.Services) } ) } item { TextPreference( - title = R.string.settings_category_language_title, + title = stringResource(R.string.settings_category_language_title), icon = R.drawable.ic_language, - onClick = { backStack.add(Screen.Settings.Language) } + onClick = { onNavigate(Screen.Settings.Language) } ) } item { TextPreference( - title = R.string.settings_category_backup_restore_title, + title = stringResource(R.string.settings_category_backup_restore_title), icon = R.drawable.ic_backup, - onClick = { backStack.add(Screen.Settings.BackupRestore) } + onClick = { onNavigate(Screen.Settings.BackupRestore) } ) } // Show Updates only on release builds if (!BuildConfig.DEBUG) { item { TextPreference( - title = R.string.settings_category_updates_title, + title = stringResource(R.string.settings_category_updates_title), icon = R.drawable.ic_newpipe_update, - onClick = { backStack.add(Screen.Settings.Updates) } + onClick = { onNavigate(Screen.Settings.Updates) } ) } } @@ -119,9 +116,9 @@ fun SettingsScreen(backStack: NavBackStack, handleBack: () -> Unit) { if (BuildConfig.DEBUG) { item { TextPreference( - title = R.string.settings_category_debug_title, + title = stringResource(R.string.settings_category_debug_title), icon = R.drawable.ic_bug_report, - onClick = { backStack.add(Screen.Settings.Debug) } + onClick = { onNavigate(Screen.Settings.Debug) } ) } } @@ -131,4 +128,4 @@ fun SettingsScreen(backStack: NavBackStack, handleBack: () -> Unit) { @Preview @Composable -fun SettingsScreenPreview() = SettingsScreen(backStack = NavBackStack(), handleBack = {}) +private fun SettingsHomeScreenPreview() = SettingsHomeScreen(onNavigate = {}, onBackClick = {}) diff --git a/app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/navigation/SettingsNavigation.kt similarity index 80% rename from app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt rename to app/src/main/java/org/schabi/newpipe/ui/screens/settings/navigation/SettingsNavigation.kt index e3f772a30b8..421d71aea1d 100644 --- a/app/src/main/java/org/schabi/newpipe/navigation/SettingsNavigation.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/settings/navigation/SettingsNavigation.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package org.schabi.newpipe.navigation +package org.schabi.newpipe.ui.screens.settings.navigation import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -14,8 +14,9 @@ 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 +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) { @@ -33,7 +34,12 @@ fun SettingsNavigation(onExitSettings: () -> Unit) { backStack = backStack, onBack = handleBack, entryProvider = entryProvider { - entry { SettingsScreen(backStack, handleBack) } + 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)) } @@ -45,7 +51,9 @@ fun SettingsNavigation(onExitSettings: () -> Unit) { 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(backStack) } + entry { + DebugScreen(onBackClick = { backStack.removeLastOrNull() }) + } }, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35acff72954..37f892f847a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,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" -nav3Core = "1.0.0" +nav3Core = "1.0.1" okhttp = "5.3.2" paging-compose = "3.3.2" phoenix = "3.0.0"