From 017f991fe229c381a9153a19c0dc9bbd780c9279 Mon Sep 17 00:00:00 2001 From: Hatake Kakashri Date: Sat, 15 Nov 2025 11:23:08 +0530 Subject: [PATCH] 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 +++ .../schabi/newpipe/settings/DebugScreen.kt | 27 --- .../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 | 33 ++-- .../java/org/schabi/newpipe/ui/Toolbar.kt | 129 ------------- .../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 +- 20 files changed, 783 insertions(+), 365 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 delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/DebugScreen.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 delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/Toolbar.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 517fb9ce5..a85b65b24 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -262,9 +262,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 a2d65f6f4..000000000 --- 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 000000000..5ad63635f --- /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/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt deleted file mode 100644 index ac08dd36b..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.schabi.newpipe.settings - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import org.schabi.newpipe.R -import org.schabi.newpipe.settings.viewmodel.SettingsViewModel -import org.schabi.newpipe.ui.SwitchPreference -import org.schabi.newpipe.ui.theme.SizeTokens - -@Composable -fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) { - - val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState() - - Column(modifier = modifier) { - SwitchPreference( - modifier = Modifier.padding(SizeTokens.SpacingExtraSmall), - R.string.settings_layout_redesign, - settingsLayoutRedesign, - viewModel::toggleSettingsLayoutRedesign - ) - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanaryAPI.kt b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanaryAPI.kt new file mode 100644 index 000000000..777df5084 --- /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 229de7005..f822e46f2 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 @@ public class DebugSettingsFragment extends BasePreferenceFragment { 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 5bd8f2b08..000000000 --- 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 821ff0187..1202fb65b 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 000000000..12559c6cd --- /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 000000000..ab84b605a --- /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 000000000..1f2ed5a6c --- /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 1e48fef5e..76b3eb9fc 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 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 javax.inject.Inject @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 - ) - 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() + 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) - fun toggleSettingsLayoutRedesign(newState: Boolean) { - _settingsLayoutRedesign.value = newState - _settingsLayoutRedesignPref = newState + private val allowHeapDumpingPref = BooleanPreference( + R.string.allow_heap_dumping_key, + false, + context.applicationContext, + preferenceManager + ) + private val allowDisposedExceptionsPref = BooleanPreference( + R.string.allow_disposed_exceptions_key, + false, + context.applicationContext, + preferenceManager + ) + + private val showOriginalTimeAgoPref = + BooleanPreference( + R.string.show_original_time_ago_key, + false, + context.applicationContext, + preferenceManager + ) + + private val showCrashThePlayerPref = BooleanPreference( + R.string.show_crash_the_player_key, + false, + context.applicationContext, + preferenceManager + ) + + 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 d479343f5..3159bceb2 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 000000000..6fd4c2ce5 --- /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 f58f2f305..72b15e0bc 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 @@ -29,38 +33,27 @@ fun TextPreference( @DrawableRes icon: Int? = null, @StringRes summary: Int? = null, onClick: () -> Unit, + enabled: Boolean = true ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.Start, modifier = modifier .fillMaxWidth() .padding(SizeTokens.SpacingSmall) .defaultMinSize(minHeight = SizeTokens.SpaceMinSize) - .clickable { onClick() } + .clickable(enabled = enabled) { onClick() } ) { icon?.let { Icon( painter = painterResource(id = icon), - contentDescription = "icon for $title preference" + contentDescription = "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/Toolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt deleted file mode 100644 index de0c97540..000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt +++ /dev/null @@ -1,129 +0,0 @@ -package org.schabi.newpipe.ui - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.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 -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBar -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -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 -import org.schabi.newpipe.ui.theme.SizeTokens - -@Composable -fun TextAction(text: String, modifier: Modifier = Modifier) { - Text(text = text, color = MaterialTheme.colorScheme.onSurface, modifier = modifier) -} - -@Composable -fun NavigationIcon() { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", - modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall) - ) -} - -@Composable -fun SearchSuggestionItem(text: String) { - // TODO: Add more components here to display all the required details of a search suggestion item. - Text(text = text) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun Toolbar( - title: String, - modifier: Modifier = Modifier, - hasNavigationIcon: Boolean = true, - hasSearch: Boolean = false, - onSearchQueryChange: ((String) -> List)? = null, - actions: @Composable RowScope.() -> Unit = {} -) { - var isSearchActive by remember { mutableStateOf(false) } - var query by remember { mutableStateOf("") } - - Column { - TopAppBar( - title = { Text(text = title) }, - modifier = modifier, - navigationIcon = { if (hasNavigationIcon) NavigationIcon() }, - actions = { - actions() - if (hasSearch) { - IconButton(onClick = { isSearchActive = true }) { - Icon( - painterResource(id = R.drawable.ic_search), - contentDescription = stringResource(id = R.string.search), - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - } - ) - if (isSearchActive) { - SearchBar( - query = query, - onQueryChange = { query = it }, - onSearch = {}, - placeholder = { - Text(text = stringResource(id = R.string.search)) - }, - active = true, - onActiveChange = { - isSearchActive = it - } - ) { - onSearchQueryChange?.invoke(query)?.takeIf { it.isNotEmpty() } - ?.map { suggestionText -> SearchSuggestionItem(text = suggestionText) } - ?: run { - Box( - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Column { - Text(text = "╰(°●°╰)") - Text(text = stringResource(id = R.string.search_no_results)) - } - } - } - } - } - } -} - -@Preview -@Composable -fun ToolbarPreview() { - AppTheme { - Toolbar( - title = "Title", - hasSearch = true, - onSearchQueryChange = { emptyList() }, - actions = { - TextAction(text = "Action1") - TextAction(text = "Action2") - } - ) - } -} 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 18139c7a6..3c6273091 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() } - }, - actions = actions - ) + } + } 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 + ) + } + } + } + ) + } }, 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 000000000..c17280259 --- /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 c439f19e2..3964a3d60 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 @@ -899,4 +903,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 efa55488d..0ac56e351 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.57.2" +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" }