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.
This commit is contained in:
parent
61c25d4589
commit
017f991fe2
@ -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)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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"
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,85 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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)
|
||||
}
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.settings.navigation
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.settings.screens.DebugScreen
|
||||
import org.schabi.newpipe.settings.screens.SettingsHomeScreen
|
||||
import org.schabi.newpipe.ui.screens.Screens
|
||||
|
||||
@Composable
|
||||
fun SettingsNavigation(onExitSettings: () -> Unit) {
|
||||
val backStack = rememberNavBackStack(Screens.Settings.Home)
|
||||
|
||||
val handleBack: () -> Unit = {
|
||||
if (backStack.size > 1) {
|
||||
backStack.removeLastOrNull()
|
||||
} else {
|
||||
onExitSettings()
|
||||
}
|
||||
}
|
||||
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = handleBack,
|
||||
entryProvider = entryProvider {
|
||||
entry<Screens.Settings.Home> { SettingsHomeScreen(backStack, handleBack) }
|
||||
entry<Screens.Settings.Player> { Text(stringResource(id = R.string.settings_category_player_title)) }
|
||||
entry<Screens.Settings.Behaviour> { Text(stringResource(id = R.string.settings_category_player_behavior_title)) }
|
||||
entry<Screens.Settings.Download> { Text(stringResource(id = R.string.settings_category_downloads_title)) }
|
||||
entry<Screens.Settings.LookFeel> { Text(stringResource(id = R.string.settings_category_look_and_feel_title)) }
|
||||
entry<Screens.Settings.HistoryCache> { Text(stringResource(id = R.string.settings_category_history_title)) }
|
||||
entry<Screens.Settings.Content> { Text(stringResource(id = R.string.settings_category_content_title)) }
|
||||
entry<Screens.Settings.Feed> { Text(stringResource(id = R.string.settings_category_feed_title)) }
|
||||
entry<Screens.Settings.Services> { Text(stringResource(id = R.string.settings_category_services_title)) }
|
||||
entry<Screens.Settings.Language> { Text(stringResource(id = R.string.settings_category_language_title)) }
|
||||
entry<Screens.Settings.BackupRestore> { Text(stringResource(id = R.string.settings_category_backup_restore_title)) }
|
||||
entry<Screens.Settings.Updates> { Text(stringResource(id = R.string.settings_category_updates_title)) }
|
||||
entry<Screens.Settings.Debug> { DebugScreen(backStack) }
|
||||
},
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.settings.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
|
||||
import org.schabi.newpipe.ui.SwitchPreference
|
||||
import org.schabi.newpipe.ui.TextPreference
|
||||
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||
|
||||
private const val DUMMY = "Dummy"
|
||||
|
||||
@Composable
|
||||
fun DebugScreen(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
|
||||
|
||||
val isLeakCanaryAvailable by viewModel.isLeakCanaryAvailable.collectAsState()
|
||||
val allowHeapDumping by viewModel.allowHeapDumping.collectAsState()
|
||||
val allowDisposedExceptions by viewModel.allowDisposedExceptions.collectAsState()
|
||||
val showOriginalTimeAgo by viewModel.showOriginalTimeAgo.collectAsState()
|
||||
val showCrashThePlayer by viewModel.showCrashThePlayer.collectAsState()
|
||||
|
||||
ScaffoldWithToolbar(
|
||||
title = stringResource(id = R.string.settings_category_debug_title),
|
||||
onBackClick = { backStack.removeLastOrNull() }
|
||||
) { paddingValues ->
|
||||
Column(modifier = modifier.padding(paddingValues)) {
|
||||
SwitchPreference(
|
||||
title = R.string.leakcanary,
|
||||
summary = if (isLeakCanaryAvailable) R.string.enable_leak_canary_summary else R.string.leak_canary_not_available,
|
||||
isChecked = allowHeapDumping,
|
||||
onCheckedChange = viewModel::toggleAllowHeapDumping,
|
||||
enabled = isLeakCanaryAvailable
|
||||
)
|
||||
TextPreference(
|
||||
title = R.string.show_memory_leaks,
|
||||
summary = if (isLeakCanaryAvailable) null else R.string.leak_canary_not_available,
|
||||
onClick = {
|
||||
viewModel.getLeakDisplayActivityIntent()?.let {
|
||||
context.startActivity(it)
|
||||
}
|
||||
},
|
||||
enabled = isLeakCanaryAvailable
|
||||
)
|
||||
SwitchPreference(
|
||||
title = R.string.enable_disposed_exceptions_title,
|
||||
summary = R.string.enable_disposed_exceptions_summary,
|
||||
isChecked = allowDisposedExceptions,
|
||||
onCheckedChange = viewModel::toggleAllowDisposedExceptions
|
||||
)
|
||||
SwitchPreference(
|
||||
title = R.string.show_original_time_ago_title,
|
||||
summary = R.string.show_original_time_ago_summary,
|
||||
isChecked = showOriginalTimeAgo,
|
||||
onCheckedChange = viewModel::toggleShowOriginalTimeAgo
|
||||
)
|
||||
SwitchPreference(
|
||||
title = R.string.show_crash_the_player_title,
|
||||
summary = R.string.show_crash_the_player_summary,
|
||||
isChecked = showCrashThePlayer,
|
||||
onCheckedChange = viewModel::toggleShowCrashThePlayer
|
||||
)
|
||||
TextPreference(
|
||||
title = R.string.check_new_streams,
|
||||
onClick = viewModel::checkNewStreams
|
||||
)
|
||||
TextPreference(
|
||||
title = R.string.crash_the_app,
|
||||
onClick = {
|
||||
throw RuntimeException(DUMMY)
|
||||
}
|
||||
)
|
||||
TextPreference(
|
||||
title = R.string.show_error_snackbar,
|
||||
onClick = {
|
||||
ErrorUtil.showUiErrorSnackbar(
|
||||
context,
|
||||
DUMMY, RuntimeException(DUMMY)
|
||||
)
|
||||
}
|
||||
)
|
||||
TextPreference(
|
||||
title = R.string.create_error_notification,
|
||||
onClick = {
|
||||
createNotification(
|
||||
context,
|
||||
ErrorInfo(
|
||||
RuntimeException(DUMMY),
|
||||
UserAction.UI_ERROR,
|
||||
DUMMY
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
SwitchPreference(
|
||||
title = R.string.settings_layout_redesign,
|
||||
isChecked = settingsLayoutRedesign,
|
||||
onCheckedChange = viewModel::toggleSettingsLayoutRedesign
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.settings.screens
|
||||
|
||||
import androidx.compose.foundation.layout.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<NavKey>, 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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,40 +1,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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<Boolean> =
|
||||
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<Boolean> = _state.asStateFlow()
|
||||
|
||||
fun toggle(newValue: Boolean) {
|
||||
preferenceManager.edit { putBoolean(key, newValue) }
|
||||
_state.value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
55
app/src/main/java/org/schabi/newpipe/ui/TextBase.kt
Normal file
55
app/src/main/java/org/schabi/newpipe/ui/TextBase.kt
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.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)
|
||||
}
|
||||
@ -1,3 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>)? = 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")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* 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<String>)? = 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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
55
app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt
Normal file
55
app/src/main/java/org/schabi/newpipe/ui/screens/Screens.kt
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.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
|
||||
}
|
||||
}
|
||||
@ -154,11 +154,15 @@
|
||||
<string name="settings_category_video_audio_title">Video and audio</string>
|
||||
<string name="settings_category_history_title">History and cache</string>
|
||||
<string name="settings_category_appearance_title">Appearance</string>
|
||||
<string name="settings_category_look_and_feel_title">Look and feel</string>
|
||||
<string name="settings_category_debug_title">Debug</string>
|
||||
<string name="settings_category_updates_title">Updates</string>
|
||||
<string name="settings_category_player_notification_title">Player notification</string>
|
||||
<string name="settings_category_player_notification_summary">Configure current playing stream notification</string>
|
||||
<string name="settings_category_backup_restore_title">Backup and restore</string>
|
||||
<string name="settings_category_content_title">Content</string>
|
||||
<string name="settings_category_services_title">Services</string>
|
||||
<string name="settings_category_language_title">Language</string>
|
||||
<string name="background_player_playing_toast">Playing in background</string>
|
||||
<string name="popup_playing_toast">Playing in popup mode</string>
|
||||
<string name="content">Content</string>
|
||||
@ -899,4 +903,5 @@
|
||||
<string name="youtube_player_http_403">HTTP error 403 received from server while playing, likely caused by an IP ban or streaming URL deobfuscation issues</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%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).</string>
|
||||
<string name="unsupported_content_in_country">This content is not available for the currently selected content country.\n\nChange your selection from \"Settings > Content > Default content country\".</string>
|
||||
<string name="navigate_back">Navigate back</string>
|
||||
</resources>
|
||||
|
||||
@ -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" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user