mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-10-04 14:51:48 +00:00
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:
committed by
Ida Delphine
parent
5fafccba33
commit
de344100db
@@ -271,9 +271,14 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui.text) // Needed for parsing HTML to AnnotatedString
|
implementation(libs.androidx.compose.ui.text) // Needed for parsing HTML to AnnotatedString
|
||||||
implementation(libs.androidx.compose.material.icons.extended)
|
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
|
// Jetpack Compose related dependencies
|
||||||
implementation(libs.androidx.paging.compose)
|
implementation(libs.androidx.paging.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
|
||||||
// Coroutines interop
|
// Coroutines interop
|
||||||
implementation(libs.kotlinx.coroutines.rx3)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
@@ -88,15 +93,4 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
|||||||
return Optional.empty();
|
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
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.settings.navigation.SettingsNavigation
|
||||||
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
|
|
||||||
import org.schabi.newpipe.ui.Toolbar
|
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY"
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SettingsV2Activity : ComponentActivity() {
|
class SettingsV2Activity : ComponentActivity() {
|
||||||
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
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 {
|
AppTheme {
|
||||||
Scaffold(topBar = {
|
SettingsNavigation(
|
||||||
Toolbar(
|
onExitSettings = { finish() },
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package org.schabi.newpipe.settings.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker
|
||||||
|
import org.schabi.newpipe.settings.DebugSettingsBVDLeakCanaryAPI
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel @Inject constructor(
|
class SettingsViewModel @Inject constructor(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
private val preferenceManager: SharedPreferences
|
preferenceManager: SharedPreferences
|
||||||
) : AndroidViewModel(context.applicationContext as Application) {
|
) : AndroidViewModel(context.applicationContext as Application) {
|
||||||
|
|
||||||
private var settingsLayoutRedesignPref: Boolean
|
private val bvdLeakCanaryApi: DebugSettingsBVDLeakCanaryAPI? = runCatching {
|
||||||
get() = preferenceManager.getBoolean(
|
// Try to find the implementation of the LeakCanary API
|
||||||
Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key),
|
Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS)
|
||||||
false
|
.getDeclaredConstructor()
|
||||||
)
|
.newInstance() as DebugSettingsBVDLeakCanaryAPI
|
||||||
set(value) {
|
}.getOrNull()
|
||||||
preferenceManager.edit().putBoolean(
|
private val _isLeakCanaryAvailable = MutableStateFlow(bvdLeakCanaryApi != null)
|
||||||
Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key),
|
|
||||||
value
|
|
||||||
).apply()
|
|
||||||
}
|
|
||||||
private val _settingsLayoutRedesign: MutableStateFlow<Boolean> =
|
|
||||||
MutableStateFlow(settingsLayoutRedesignPref)
|
|
||||||
val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow()
|
|
||||||
|
|
||||||
fun toggleSettingsLayoutRedesign(newState: Boolean) {
|
private val allowHeapDumpingPref = BooleanPreference(
|
||||||
_settingsLayoutRedesign.value = newState
|
R.string.allow_heap_dumping_key,
|
||||||
settingsLayoutRedesignPref = newState
|
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
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -24,30 +25,25 @@ fun SwitchPreference(
|
|||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
@StringRes summary: Int? = null
|
@StringRes summary: Int? = null,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
modifier = modifier
|
||||||
modifier = modifier.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(SizeTokens.SpacingSmall)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(
|
||||||
Text(
|
modifier = Modifier.weight(1f)
|
||||||
text = stringResource(id = title),
|
) {
|
||||||
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
TextBase(title = title, summary = summary, enabled = enabled)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
|
Spacer(modifier = Modifier.width(SizeTokens.SpacingExtraSmall))
|
||||||
Switch(checked = isChecked, onCheckedChange = onCheckedChange)
|
Switch(
|
||||||
|
checked = isChecked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
@@ -13,13 +19,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
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
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -28,39 +32,28 @@ fun TextPreference(
|
|||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
@DrawableRes icon: Int? = null,
|
@DrawableRes icon: Int? = null,
|
||||||
@StringRes summary: Int? = null,
|
@StringRes summary: Int? = null,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.Start,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(SizeTokens.SpacingSmall)
|
.padding(SizeTokens.SpacingSmall)
|
||||||
.defaultMinSize(minHeight = SizeTokens.SpaceMinSize)
|
.defaultMinSize(minHeight = SizeTokens.SpaceMinSize)
|
||||||
.clickable { onClick() }
|
.clickable(enabled = enabled) { onClick() }
|
||||||
) {
|
) {
|
||||||
icon?.let {
|
icon?.let {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = 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))
|
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
Text(
|
TextBase(title = title, summary = summary, enabled = enabled)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
import android.content.res.Configuration
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.RowScope
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -10,11 +22,23 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
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 androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -22,42 +46,148 @@ fun ScaffoldWithToolbar(
|
|||||||
title: String,
|
title: String,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
actions: @Composable RowScope.() -> 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
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var query by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
if (isSearchActive) {
|
||||||
title = { Text(text = title) },
|
SearchBar(
|
||||||
// TODO decide whether to use default colors instead
|
inputField = {
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
SearchBarDefaults.InputField(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
query = query,
|
||||||
scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
onQueryChange = { query = it },
|
||||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
onSearch = {
|
||||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
onSearchAction?.invoke(it)
|
||||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
isSearchActive = false
|
||||||
),
|
},
|
||||||
navigationIcon = {
|
expanded = true,
|
||||||
IconButton(onClick = onBackClick) {
|
onExpandedChange = { isSearchActive = it },
|
||||||
Icon(
|
placeholder = searchPlaceholder ?: {
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
Text(stringResource(id = R.string.search))
|
||||||
contentDescription = null
|
},
|
||||||
|
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
|
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 = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScaffoldWithToolbarPreview() {
|
private fun ScaffoldWithToolbarPreview() {
|
||||||
ScaffoldWithToolbar(
|
AppTheme {
|
||||||
title = "Example",
|
ScaffoldWithToolbar(
|
||||||
onBackClick = {},
|
title = "Example",
|
||||||
content = {}
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_video_audio_title">Video and audio</string>
|
||||||
<string name="settings_category_history_title">History and cache</string>
|
<string name="settings_category_history_title">History and cache</string>
|
||||||
<string name="settings_category_appearance_title">Appearance</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_debug_title">Debug</string>
|
||||||
<string name="settings_category_updates_title">Updates</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_title">Player notification</string>
|
||||||
<string name="settings_category_player_notification_summary">Configure current playing stream 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_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="background_player_playing_toast">Playing in background</string>
|
||||||
<string name="popup_playing_toast">Playing in popup mode</string>
|
<string name="popup_playing_toast">Playing in popup mode</string>
|
||||||
<string name="content">Content</string>
|
<string name="content">Content</string>
|
||||||
@@ -904,4 +908,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="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="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="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>
|
</resources>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ exoplayer = "2.19.1"
|
|||||||
fragment-compose = "1.8.9"
|
fragment-compose = "1.8.9"
|
||||||
groupie = "2.10.1"
|
groupie = "2.10.1"
|
||||||
hilt = "2.58" # Newer version requires AGP 9
|
hilt = "2.58" # Newer version requires AGP 9
|
||||||
|
hilt-navigation-compose = "1.3.0"
|
||||||
jsoup = "1.21.2"
|
jsoup = "1.21.2"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junit-ext = "1.3.0"
|
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
|
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"
|
media = "1.7.1"
|
||||||
mockitoCore = "5.21.0"
|
mockitoCore = "5.21.0"
|
||||||
navigation-compose = "2.8.3"
|
nav3Core = "1.0.0"
|
||||||
okhttp = "5.3.2"
|
okhttp = "5.3.2"
|
||||||
paging-compose = "3.3.2"
|
paging-compose = "3.3.2"
|
||||||
phoenix = "3.0.0"
|
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-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
||||||
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
|
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
|
||||||
androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "fragment-compose" }
|
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-junit = { module = "androidx.test.ext:junit", version.ref = "junit-ext" }
|
||||||
androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
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-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" }
|
||||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||||
androidx-media = { module = "androidx.media:media", version.ref = "media" }
|
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-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging-compose" }
|
||||||
androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
|
androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
|
||||||
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||||
|
|||||||
Reference in New Issue
Block a user