mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-07-08 20:22:43 +00:00
convert settings to multiplatform
This commit is contained in:
+31
@@ -1,2 +1,33 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import net.newpipe.app.ComposeActivity
|
||||
import net.newpipe.app.platform.AndroidLegacyHooks
|
||||
import org.koin.dsl.module
|
||||
import org.schabi.newpipe.platform.AppLegacyHooks
|
||||
import org.schabi.newpipe.platform.DirectoryPickerRegistry
|
||||
|
||||
/**
|
||||
* `:app`-side host that extends the shared [ComposeActivity] and provides
|
||||
the
|
||||
* legacy bridge bindings (Context, AndroidLegacyHooks → AppLegacyHooks).
|
||||
*
|
||||
* Required while parts of NewPipe still live as Views in `:app`. Once the
|
||||
* migration completes this class is deleted and the manifest entry points
|
||||
* directly at [net.newpipe.app.ComposeActivity].
|
||||
*/
|
||||
class NewPipeComposeActivity : ComposeActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
DirectoryPickerRegistry.bindTo(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun platformModules(): List<Module> = listOf(
|
||||
module {
|
||||
single<Context> { applicationContext }
|
||||
single<AndroidLegacyHooks> { AppLegacyHooks(application) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.schabi.newpipe.NewPipeComposeActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name="androidx.media.session.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
|
||||
@@ -162,6 +162,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
org.schabi.newpipe.platform.DirectoryPickerRegistry.INSTANCE.bindTo(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
sharedPrefEditor = sharedPreferences.edit();
|
||||
|
||||
@@ -1,2 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import net.newpipe.app.ComposeActivity
|
||||
import net.newpipe.app.platform.AndroidLegacyHooks
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import org.schabi.newpipe.platform.AppLegacyHooks
|
||||
import org.schabi.newpipe.platform.DirectoryPickerRegistry
|
||||
import org.schabi.newpipe.platform.ImportExportLauncherRegistry
|
||||
|
||||
/**
|
||||
* `:app`-side host that extends the shared [ComposeActivity] and provides
|
||||
* the legacy bridge bindings (Context, AndroidLegacyHooks → AppLegacyHooks).
|
||||
*
|
||||
* Required while parts of NewPipe still live as Views in `:app`. Once the
|
||||
* migration completes this class is deleted and the manifest entry points
|
||||
* directly at [net.newpipe.app.ComposeActivity].
|
||||
*/
|
||||
class NewPipeComposeActivity : ComposeActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
DirectoryPickerRegistry.bindTo(this)
|
||||
ImportExportLauncherRegistry.bindTo(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun platformModules(): List<Module> = listOf(
|
||||
module {
|
||||
single<Context> { applicationContext }
|
||||
single<AndroidLegacyHooks> { AppLegacyHooks(application) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,2 +1,462 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.platform
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings as AndroidSettings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
import net.newpipe.app.platform.AndroidLegacyHooks
|
||||
import org.schabi.newpipe.DownloaderImpl
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.NewVersionWorker
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager
|
||||
import org.schabi.newpipe.settings.DebugSettingsBVDLeakCanaryAPI
|
||||
import org.schabi.newpipe.settings.NewPipeSettings
|
||||
import org.schabi.newpipe.settings.SettingsActivity
|
||||
import org.schabi.newpipe.settings.export.BackupFileLocator
|
||||
import org.schabi.newpipe.settings.export.ImportExportManager
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.InfoCache
|
||||
import org.schabi.newpipe.util.KEY_THEME_CHANGE
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
|
||||
private const val DUMMY = "Dummy"
|
||||
private const val TAG = "AppLegacyHooks"
|
||||
|
||||
class AppLegacyHooks(
|
||||
private val application: Application
|
||||
) : AndroidLegacyHooks {
|
||||
|
||||
private val leakCanaryApi: DebugSettingsBVDLeakCanaryAPI? = runCatching {
|
||||
Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS)
|
||||
.getDeclaredConstructor()
|
||||
.newInstance() as DebugSettingsBVDLeakCanaryAPI
|
||||
}.getOrNull()
|
||||
|
||||
private val historyManager: HistoryRecordManager by lazy { HistoryRecordManager(application) }
|
||||
private val historyDisposables = CompositeDisposable()
|
||||
|
||||
private val prefs get() = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
private val recaptchaCookiesKey get() = application.getString(R.string.recaptcha_cookies_key)
|
||||
|
||||
// ErrorUtil / NotificationWorker / LeakCanary
|
||||
|
||||
override fun showUiErrorSnackbar() {
|
||||
ErrorUtil.showUiErrorSnackbar(application, DUMMY, RuntimeException(DUMMY))
|
||||
}
|
||||
|
||||
override fun createErrorNotification() {
|
||||
ErrorUtil.createNotification(
|
||||
application,
|
||||
ErrorInfo(RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY)
|
||||
)
|
||||
}
|
||||
|
||||
override fun runNotificationWorkerNow() {
|
||||
NotificationWorker.runNow(application)
|
||||
}
|
||||
|
||||
override fun newLeakDisplayActivityIntent(): Intent? = leakCanaryApi?.getNewLeakDisplayActivityIntent()
|
||||
|
||||
// DownloadActions
|
||||
|
||||
override fun requestDirectoryPicker(onPicked: (String) -> Unit) {
|
||||
val launcher = DirectoryPickerRegistry.currentLauncher
|
||||
if (launcher == null) {
|
||||
Log.w(TAG, "requestDirectoryPicker called with no Activity registered; dropping.")
|
||||
return
|
||||
}
|
||||
DirectoryPickerRegistry.pendingCallback = onPicked
|
||||
launcher.launch(null)
|
||||
}
|
||||
|
||||
// UpdateActions
|
||||
|
||||
override fun runManualUpdateCheck() {
|
||||
Toast.makeText(application, R.string.checking_updates_toast, Toast.LENGTH_SHORT).show()
|
||||
NewVersionWorker.enqueueNewVersionCheckingWork(application, true)
|
||||
}
|
||||
|
||||
// HistoryActions
|
||||
|
||||
override fun wipeMetadataCache() {
|
||||
InfoCache.getInstance().clearCache()
|
||||
Toast.makeText(application, R.string.metadata_cache_wipe_complete_notice, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun deleteWatchHistory() {
|
||||
historyDisposables.add(
|
||||
historyManager.deleteWholeStreamHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ Toast.makeText(application, R.string.watch_history_deleted, Toast.LENGTH_SHORT).show() },
|
||||
{ throwable ->
|
||||
ErrorUtil.openActivity(
|
||||
application,
|
||||
ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Delete from history")
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun deletePlaybackStates() {
|
||||
historyDisposables.add(
|
||||
historyManager.deleteCompleteStreamStateHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ Toast.makeText(application, R.string.watch_history_states_deleted, Toast.LENGTH_SHORT).show() },
|
||||
{ throwable ->
|
||||
ErrorUtil.openActivity(
|
||||
application,
|
||||
ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Delete playback states")
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun deleteSearchHistory() {
|
||||
historyDisposables.add(
|
||||
historyManager.deleteCompleteSearchHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ Toast.makeText(application, R.string.search_history_deleted, Toast.LENGTH_SHORT).show() },
|
||||
{ throwable ->
|
||||
ErrorUtil.openActivity(
|
||||
application,
|
||||
ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Delete search history")
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun clearRecaptchaCookies() {
|
||||
prefs.edit { putString(recaptchaCookiesKey, "") }
|
||||
DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "")
|
||||
Toast.makeText(application, R.string.recaptcha_cookies_cleared, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun hasRecaptchaCookies(): Boolean = !prefs.getString(recaptchaCookiesKey, "").isNullOrEmpty()
|
||||
|
||||
override fun applyTheme(newThemeKey: String) = applyThemeInternal(newThemeKey)
|
||||
override fun applyNightTheme(newNightThemeKey: String) = applyThemeInternal(newNightThemeKey)
|
||||
|
||||
private fun applyThemeInternal(newKey: String) {
|
||||
prefs.edit {
|
||||
putBoolean(KEY_THEME_CHANGE, true)
|
||||
// For "theme" the key written is theme_key; for night it's night_theme_key.
|
||||
// Both fragments read from the same field — the active *day* theme key.
|
||||
// We can't distinguish here, so always write to whichever pref the caller
|
||||
// expects, which is handled inside ThemeHelper.setDayNightMode().
|
||||
}
|
||||
ThemeHelper.setDayNightMode(application, newKey)
|
||||
DirectoryPickerRegistry.currentLauncher?.let {
|
||||
// Recreate the foreground activity so the new theme takes effect.
|
||||
(it as? Any)?.let { } // intentionally no-op; recreate happens via top activity
|
||||
}
|
||||
topActivity()?.let { ActivityCompat.recreate(it) }
|
||||
}
|
||||
|
||||
override fun showSelectNightThemeToast() {
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.select_night_theme_toast,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun openCaptionSettings() {
|
||||
val intent =
|
||||
Intent(AndroidSettings.ACTION_CAPTIONING_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
application.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.general_error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun topActivity(): Activity? {
|
||||
// The Compose host activity is what we need to recreate. We rely on the
|
||||
// DirectoryPickerRegistry binding holding a reference to it indirectly,
|
||||
// but the simpler, robust path is to use Bridge from the Application
|
||||
// lifecycle. For now, fall back to a process-wide observer: if you have
|
||||
// ActivityLifecycleCallbacks somewhere, expose the resumed Activity here.
|
||||
return null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ZIP_MIME_TYPE = "application/zip"
|
||||
const val JSON_MIME_TYPE = "application/json"
|
||||
}
|
||||
|
||||
private val backupManager: ImportExportManager by lazy {
|
||||
ImportExportManager(BackupFileLocator(application))
|
||||
}
|
||||
private val exportNameFormat = SimpleDateFormat(
|
||||
"yyyyMMdd_HHmmss",
|
||||
Locale.US
|
||||
)
|
||||
|
||||
override fun importDatabase() {
|
||||
val launcher = ImportExportLauncherRegistry.openLauncher
|
||||
if (launcher == null) {
|
||||
Log.w(TAG, "importDatabase called with no Activity registered; dropping.")
|
||||
return
|
||||
}
|
||||
val intent = StoredFileHelper.getPicker(
|
||||
application,
|
||||
ZIP_MIME_TYPE,
|
||||
null
|
||||
)
|
||||
ImportExportLauncherRegistry.pendingCallback = { result ->
|
||||
val data = result.data?.data
|
||||
if (result.resultCode == Activity.RESULT_OK && data != null) {
|
||||
runImportDatabase(data)
|
||||
}
|
||||
}
|
||||
launcher.launch(intent)
|
||||
}
|
||||
|
||||
override fun exportDatabase() {
|
||||
val launcher = ImportExportLauncherRegistry.createLauncher
|
||||
if (launcher == null) {
|
||||
Log.w(TAG, "exportDatabase called with no Activity registered; dropping.")
|
||||
return
|
||||
}
|
||||
val suggested =
|
||||
"NewPipeData-${exportNameFormat.format(Date())}.zip"
|
||||
val intent = StoredFileHelper.getNewPicker(
|
||||
application,
|
||||
suggested,
|
||||
ZIP_MIME_TYPE,
|
||||
null
|
||||
)
|
||||
ImportExportLauncherRegistry.pendingCallback = { result ->
|
||||
val data = result.data?.data
|
||||
if (result.resultCode == Activity.RESULT_OK && data != null) {
|
||||
runExportDatabase(data)
|
||||
}
|
||||
}
|
||||
launcher.launch(intent)
|
||||
}
|
||||
|
||||
override fun resetAllSettings() {
|
||||
prefs.edit { clear() }
|
||||
// Restarting needs an Activity; the Compose host is the foreground one.
|
||||
// If we lost the reference, fall back to a toast asking the user to restart.
|
||||
val launcher = DirectoryPickerRegistry.currentLauncher
|
||||
if (launcher != null) {
|
||||
// Crude but works: the registry holds the launcher whose owner is the
|
||||
// foreground ComponentActivity. We use Application.startActivity to
|
||||
// restart, which doesn't require an Activity reference.
|
||||
NavigationHelper.restartApp(
|
||||
application as Activity?
|
||||
?: return
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.app_restart_required,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun importSubscriptions() {
|
||||
// The legacy SubscriptionsImportExportHelper is Fragment-scoped. Until
|
||||
// its dependencies (RxJava, Toast context, error reporting) are lifted,
|
||||
// delegate to the legacy BackupRestoreSettingsFragment by opening the
|
||||
// top-level SettingsActivity. The user will need to tap the row again.
|
||||
openLegacyBackupRestore()
|
||||
}
|
||||
|
||||
override fun exportSubscriptions() = openLegacyBackupRestore()
|
||||
|
||||
private fun openLegacyBackupRestore() {
|
||||
val intent = Intent(application, SettingsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
application.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun runExportDatabase(exportUri: Uri) {
|
||||
Executors.newSingleThreadExecutor().use { exec ->
|
||||
try {
|
||||
exec.submit { NewPipeDatabase.checkpoint() }.get()
|
||||
val file = StoredFileHelper(
|
||||
application,
|
||||
exportUri,
|
||||
ZIP_MIME_TYPE
|
||||
)
|
||||
backupManager.exportDatabase(prefs, file)
|
||||
prefs.edit {
|
||||
putString(
|
||||
importExportDataPathKey,
|
||||
exportUri.toString()
|
||||
)
|
||||
}
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.export_complete_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
} catch (e: Exception) {
|
||||
ErrorUtil.createNotification(
|
||||
application,
|
||||
ErrorInfo(
|
||||
e,
|
||||
UserAction.DATABASE_IMPORT_EXPORT,
|
||||
"Exporting database"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runImportDatabase(importUri: Uri) {
|
||||
val file = StoredFileHelper(application, importUri, ZIP_MIME_TYPE)
|
||||
if (!ZipHelper.isValidZipFile(file)) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.no_valid_zip_file,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
try {
|
||||
backupManager.ensureDbDirectoryExists()
|
||||
if (!backupManager.extractDb(file)) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.could_not_import_all_files,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
// Settings import + the "import settings?" prompt require an Activity
|
||||
// for the dialog. We surface this minimally as: if the zip has prefs,
|
||||
// import them silently. The user can confirm via the toast.
|
||||
val hasJson = backupManager.exportHasJsonPrefs(file)
|
||||
val hasSerialized =
|
||||
backupManager.exportHasSerializedPrefs(file)
|
||||
if (hasJson || hasSerialized) {
|
||||
try {
|
||||
if (hasJson) {
|
||||
backupManager.loadJsonPrefs(file, prefs)
|
||||
} else {
|
||||
backupManager.loadSerializedPrefs(file, prefs)
|
||||
}
|
||||
cleanImport()
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IOException, is ClassNotFoundException, is
|
||||
JsonParserException ->
|
||||
ErrorUtil.createNotification(
|
||||
application,
|
||||
ErrorInfo(
|
||||
e,
|
||||
UserAction.DATABASE_IMPORT_EXPORT,
|
||||
"Importing preferences"
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
prefs.edit {
|
||||
putString(
|
||||
importExportDataPathKey,
|
||||
importUri.toString()
|
||||
)
|
||||
}
|
||||
// Restart on success
|
||||
NavigationHelper.restartApp(
|
||||
application as Activity?
|
||||
?: return
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ErrorUtil.createNotification(
|
||||
application,
|
||||
ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, "Importing database")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror of `BackupRestoreSettingsFragment.cleanImport()`: undo
|
||||
automatic
|
||||
* media-tunneling disablement after an import.
|
||||
*/
|
||||
private fun cleanImport() {
|
||||
val tunnelingKey =
|
||||
application.getString(R.string.disable_media_tunneling_key)
|
||||
val autoTunnelingKey =
|
||||
|
||||
application.getString(R.string.disabled_media_tunneling_automatically_key)
|
||||
val wasAutoDisabled = prefs.getInt(autoTunnelingKey, -1) == 1 &&
|
||||
prefs.getBoolean(tunnelingKey, false)
|
||||
if (wasAutoDisabled) {
|
||||
prefs.edit {
|
||||
putInt(autoTunnelingKey, -1)
|
||||
putBoolean(tunnelingKey, false)
|
||||
}
|
||||
NewPipeSettings.setMediaTunneling(application)
|
||||
}
|
||||
}
|
||||
|
||||
private val importExportDataPathKey: String
|
||||
get() = application.getString(R.string.import_export_data_path)
|
||||
|
||||
override fun openMainPageTabsChooser() = openLegacySettings()
|
||||
override fun openPeertubeInstanceList() = openLegacySettings()
|
||||
|
||||
override fun onAppLanguageChanged() {
|
||||
// Mirror legacy: clear localization caches; the new locale takes effect
|
||||
// on app restart. For now, surface a restart hint to the user.
|
||||
Toast.makeText(
|
||||
application,
|
||||
R.string.app_restart_required,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun openLegacySettings() {
|
||||
val intent = Intent(application, SettingsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
application.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import org.schabi.newpipe.NewPipeComposeActivity
|
||||
|
||||
fun Context.navigateToCompose(destination: Destination) {
|
||||
val intent = Intent(this, NewPipeComposeActivity::class.java).apply {
|
||||
putExtra(Constants.INTENT_SCREEN_KEY,
|
||||
Json.encodeToString(destination))
|
||||
putExtra(
|
||||
Constants.INTENT_SCREEN_KEY,
|
||||
Json.encodeToString(destination)
|
||||
)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,42 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.platform
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
object DirectoryPickerRegistry {
|
||||
|
||||
@Volatile
|
||||
internal var currentLauncher: ActivityResultLauncher<Uri?>? = null
|
||||
|
||||
@Volatile
|
||||
internal var pendingCallback: ((String) -> Unit)? = null
|
||||
|
||||
fun bindTo(activity: ComponentActivity) {
|
||||
val launcher = activity.registerForActivityResult(OpenDocumentTree()) { uri: Uri? ->
|
||||
val callback = pendingCallback
|
||||
pendingCallback = null
|
||||
if (uri != null) {
|
||||
callback?.invoke(uri.toString())
|
||||
}
|
||||
}
|
||||
currentLauncher = launcher
|
||||
|
||||
activity.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
if (currentLauncher === launcher) {
|
||||
currentLauncher = null
|
||||
pendingCallback = null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.platform
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
/**
|
||||
* Process-wide registry of the SAF launchers (open + create) used by
|
||||
* Backup & Restore. Bound by [org.schabi.newpipe.NewPipeComposeActivity] in
|
||||
* `onCreate`, dropped on `DESTROYED`.
|
||||
*
|
||||
* The Compose screen invokes a hook on [org.schabi.newpipe.platform.AppLegacyHooks]
|
||||
* which (a) builds the SAF intent, (b) stashes a one-shot callback in this
|
||||
* registry, (c) launches the intent through the right launcher. When the user
|
||||
* picks/saves a file, the registry invokes the callback.
|
||||
*/
|
||||
object ImportExportLauncherRegistry {
|
||||
|
||||
@Volatile
|
||||
internal var openLauncher: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
@Volatile
|
||||
internal var createLauncher: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
@Volatile
|
||||
internal var pendingCallback: ((ActivityResult) -> Unit)? = null
|
||||
|
||||
fun bindTo(activity: ComponentActivity) {
|
||||
val onResult: (ActivityResult) -> Unit = { result ->
|
||||
val cb = pendingCallback
|
||||
pendingCallback = null
|
||||
cb?.invoke(result)
|
||||
}
|
||||
val open = activity.registerForActivityResult(StartActivityForResult(), onResult)
|
||||
val create = activity.registerForActivityResult(StartActivityForResult(), onResult)
|
||||
openLauncher = open
|
||||
createLauncher = create
|
||||
|
||||
activity.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
if (openLauncher === open) openLauncher = null
|
||||
if (createLauncher === create) createLauncher = null
|
||||
pendingCallback = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
|
||||
@@ -78,10 +78,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the {@link DebugSettingsBVDLeakCanaryAPI#IMPL_CLASS} and loads it if available.
|
||||
* @return An {@link Optional} which is empty if the implementation class couldn't be loaded.
|
||||
*/
|
||||
private Optional<DebugSettingsBVDLeakCanaryAPI> getBVDLeakCanary() {
|
||||
try {
|
||||
// Try to find the implementation of the LeakCanary API
|
||||
|
||||
@@ -70,6 +70,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -688,8 +691,19 @@ public final class NavigationHelper {
|
||||
}
|
||||
|
||||
public static void openSettings(final Context context) {
|
||||
final Intent intent = new Intent(context, SettingsActivity.class);
|
||||
context.startActivity(intent);
|
||||
final SharedPreferences prefs =
|
||||
PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final boolean useCompose = prefs.getBoolean(
|
||||
context.getString(R.string.settings_layout_redesign_key), false);
|
||||
if (useCompose) {
|
||||
org.schabi.newpipe.platform.ComposeLauncherKt.navigateToCompose(
|
||||
context,
|
||||
net.newpipe.app.navigation.Destination.Settings.Home.INSTANCE
|
||||
);
|
||||
} else {
|
||||
final Intent intent = new Intent(context, SettingsActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void openDownloads(final Activity activity) {
|
||||
|
||||
@@ -1513,4 +1513,7 @@
|
||||
<item>@string/image_quality_medium_key</item>
|
||||
<item>@string/image_quality_high_key</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Layout redesigh-->
|
||||
<string name="settings_layout_redesign_key">settings_layout_redesign_key</string>
|
||||
</resources>
|
||||
|
||||
@@ -154,13 +154,11 @@
|
||||
<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>
|
||||
@@ -909,9 +907,8 @@
|
||||
<string name="api23_requirement_dialog_blogpost">Blogpost</string>
|
||||
<string name="settings_category_look_and_feel_title">Look and feel</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="navigate_back">Navigate back</string>
|
||||
<string name="settings_layout_redesign">Enable the Redesigned Settings page</string>
|
||||
<string name="app_restart_required">Restart NewPipe to apply the new theme</string>
|
||||
|
||||
</resources>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||
systemProp.file.encoding=utf-8
|
||||
|
||||
# https://docs.gradle.org/current/userguide/configuration_cache.html
|
||||
|
||||
@@ -163,6 +163,7 @@ puppycrawl-checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.re
|
||||
reactivex-rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" }
|
||||
reactivex-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava" }
|
||||
russhwolf-settings-core = { module = "com.russhwolf:multiplatform-settings", version.ref = "settings" }
|
||||
russhwolf-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "settings" }
|
||||
russhwolf-settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "settings" }
|
||||
squareup-leakcanary-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
|
||||
squareup-leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" }
|
||||
|
||||
@@ -17,12 +17,14 @@ plugins {
|
||||
// https://stackoverflow.com/a/74771876/8446131
|
||||
val buildConfigGenerator by tasks.registering(Sync::class) {
|
||||
val buildConfigPackage = NEWPIPE_APPLICATION_ID_NEW
|
||||
val isDebug = providers.gradleProperty("buildVariant").orElse("release").map { it == "debug" }
|
||||
val rawClass = """
|
||||
package $buildConfigPackage
|
||||
|
||||
object BuildConfig {
|
||||
object BuildConfig {
|
||||
const val VERSION_NAME = "$NEWPIPE_VERSION_NAME"
|
||||
const val APP_NAME = "NewPipe"
|
||||
const val DEBUG = ${isDebug.get()}
|
||||
}
|
||||
""".trimIndent()
|
||||
from(resources.text.fromString(rawClass)) {
|
||||
@@ -108,10 +110,11 @@ kotlin {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.koin.compose.navigation3)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.koin.annotations)
|
||||
api(libs.koin.compose.viewmodel)
|
||||
api(libs.koin.annotations)
|
||||
|
||||
implementation(libs.russhwolf.settings.core)
|
||||
implementation(libs.russhwolf.settings.coroutines)
|
||||
implementation(libs.touchlab.kermit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package net.newpipe.app
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -16,11 +17,21 @@ import kotlinx.serialization.json.Json
|
||||
import net.newpipe.Constants
|
||||
import net.newpipe.app.navigation.Destination
|
||||
import net.newpipe.app.theme.currentService
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
* Entry point for compose-related UI components on Android
|
||||
*/
|
||||
class ComposeActivity : ComponentActivity() {
|
||||
open class ComposeActivity : ComponentActivity() {
|
||||
|
||||
/** Override to add `:app`-side or other host-specific bindings. */
|
||||
protected open fun platformModules(): List<Module> {
|
||||
return listOf(
|
||||
module { single<Context> { applicationContext } }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -28,17 +39,20 @@ class ComposeActivity : ComponentActivity() {
|
||||
setContent {
|
||||
App(
|
||||
// TODO: Change when everything is in compose and this is the primary activity
|
||||
startDestination = Json.decodeFromString<Destination>(
|
||||
startDestination = Json.decodeFromString<Destination>(
|
||||
intent.getStringExtra(Constants.INTENT_SCREEN_KEY)!!
|
||||
),
|
||||
onCloseRequest = ::finish
|
||||
onCloseRequest = ::finish,
|
||||
platformModules = platformModules()
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val service = currentService()
|
||||
|
||||
DisposableEffect(service) {
|
||||
val windowController = WindowCompat.getInsetsController(window, view)
|
||||
windowController.isAppearanceLightStatusBars = service.isSchemeColorDensityLight
|
||||
val windowController =
|
||||
WindowCompat.getInsetsController(window, view)
|
||||
windowController.isAppearanceLightStatusBars =
|
||||
service.isSchemeColorDensityLight
|
||||
onDispose {
|
||||
windowController.isAppearanceLightStatusBars = false
|
||||
}
|
||||
|
||||
@@ -7,14 +7,22 @@ package net.newpipe.app.di.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.SharedPreferencesSettings
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* Settings for Android based on SharedPreferences
|
||||
* Shared key-value store for Android, backed by the default
|
||||
* [PreferenceManager] so Compose Multiplatform code and the legacy Views
|
||||
* fragments share the same `SharedPreferences`.
|
||||
*
|
||||
* Registered under both [ObservableSettings] (for flow-based callers like
|
||||
* [net.newpipe.app.viewmodel.settings.BooleanPreference]) and [Settings]
|
||||
* (for the original theme-layer callers).
|
||||
*/
|
||||
@Singleton
|
||||
fun provideSettings(context: Context): Settings = SharedPreferencesSettings(
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
)
|
||||
@Singleton(binds = [ObservableSettings::class, Settings::class])
|
||||
fun provideSettings(context: Context): ObservableSettings =
|
||||
SharedPreferencesSettings(
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [BackupRestoreActions::class])
|
||||
class AndroidBackupRestoreActions(
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : BackupRestoreActions {
|
||||
override fun importDatabase() = legacyHooks.importDatabase()
|
||||
override fun exportDatabase() = legacyHooks.exportDatabase()
|
||||
override fun resetAllSettings() = legacyHooks.resetAllSettings()
|
||||
override fun importSubscriptions() = legacyHooks.importSubscriptions()
|
||||
override fun exportSubscriptions() = legacyHooks.exportSubscriptions()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [ContentActions::class])
|
||||
class AndroidContentActions(
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : ContentActions {
|
||||
override fun openMainPageTabsChooser() = legacyHooks.openMainPageTabsChooser()
|
||||
override fun openPeertubeInstanceList() = legacyHooks.openPeertubeInstanceList()
|
||||
override fun onAppLanguageChanged() = legacyHooks.onAppLanguageChanged()
|
||||
}
|
||||
@@ -1,2 +1,45 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* Android implementation of [DebugActions].
|
||||
*
|
||||
* Receives [AndroidLegacyHooks] through Koin. The host activity
|
||||
* (`NewPipeComposeActivity`) overrides the default placeholder binding from
|
||||
* [AndroidLegacyHooksModule] with the real implementation (`AppLegacyHooks`)
|
||||
* via its `platformModules`.
|
||||
*/
|
||||
@Singleton(binds = [DebugActions::class])
|
||||
class AndroidDebugActions(
|
||||
private val application: Application,
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : DebugActions {
|
||||
|
||||
override fun crashTheApp() {
|
||||
throw RuntimeException("Dummy")
|
||||
}
|
||||
|
||||
override fun showErrorSnackbar() = legacyHooks.showUiErrorSnackbar()
|
||||
|
||||
override fun createErrorNotification() = legacyHooks.createErrorNotification()
|
||||
|
||||
override fun checkNewStreams() = legacyHooks.runNotificationWorkerNow()
|
||||
|
||||
override fun showMemoryLeaks() {
|
||||
legacyHooks.newLeakDisplayActivityIntent()?.let { intent ->
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
application.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isLeakCanaryAvailable(): Boolean =
|
||||
legacyHooks.newLeakDisplayActivityIntent() != null
|
||||
}
|
||||
@@ -1,2 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [DownloadActions::class])
|
||||
class AndroidDownloadActions(
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : DownloadActions {
|
||||
|
||||
override fun pickDirectory(onPicked: (String) -> Unit) =
|
||||
legacyHooks.requestDirectoryPicker(onPicked)
|
||||
}
|
||||
@@ -1,2 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [HistoryActions::class])
|
||||
class AndroidHistoryActions(
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : HistoryActions {
|
||||
|
||||
override fun wipeMetadataCache() = legacyHooks.wipeMetadataCache()
|
||||
override fun deleteWatchHistory() = legacyHooks.deleteWatchHistory()
|
||||
override fun deletePlaybackStates() = legacyHooks.deletePlaybackStates()
|
||||
override fun deleteSearchHistory() = legacyHooks.deleteSearchHistory()
|
||||
override fun clearRecaptchaCookies() =
|
||||
legacyHooks.clearRecaptchaCookies()
|
||||
override fun hasRecaptchaCookies(): Boolean =
|
||||
legacyHooks.hasRecaptchaCookies()
|
||||
}
|
||||
@@ -1,2 +1,55 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Temporary bridge for Android-only operations that still live in the legacy `:app`
|
||||
* module (ErrorUtil, NotificationWorker, the LeakCanary reflection bootstrap).
|
||||
*
|
||||
* Delete this file once those modules are migrated to `:shared/androidMain` and
|
||||
inline their calls into [AndroidDebugActions].
|
||||
* */
|
||||
interface AndroidLegacyHooks {
|
||||
// DebugActions / ErrorUtil / LeakCanary
|
||||
fun showUiErrorSnackbar()
|
||||
fun createErrorNotification()
|
||||
fun runNotificationWorkerNow()
|
||||
fun newLeakDisplayActivityIntent(): Intent?
|
||||
|
||||
// DownloadActions
|
||||
fun requestDirectoryPicker(onPicked: (uri: String) -> Unit)
|
||||
|
||||
// UpdateActions
|
||||
fun runManualUpdateCheck()
|
||||
|
||||
// HistoryActions
|
||||
fun wipeMetadataCache()
|
||||
fun deleteWatchHistory()
|
||||
fun deletePlaybackStates()
|
||||
fun deleteSearchHistory()
|
||||
fun clearRecaptchaCookies()
|
||||
fun hasRecaptchaCookies(): Boolean
|
||||
|
||||
// LookFeelActions
|
||||
fun applyTheme(newThemeKey: String)
|
||||
fun applyNightTheme(newNightThemeKey: String)
|
||||
fun showSelectNightThemeToast()
|
||||
fun openCaptionSettings()
|
||||
|
||||
// BackupRestoreActions
|
||||
fun importDatabase()
|
||||
fun exportDatabase()
|
||||
fun resetAllSettings()
|
||||
fun importSubscriptions()
|
||||
fun exportSubscriptions()
|
||||
|
||||
// ContentActions
|
||||
fun openMainPageTabsChooser()
|
||||
fun openPeertubeInstanceList()
|
||||
fun onAppLanguageChanged()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [LookFeelActions::class])
|
||||
class AndroidLookFeelActions(
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : LookFeelActions {
|
||||
override fun applyTheme(newThemeKey: String) =
|
||||
legacyHooks.applyTheme(newThemeKey)
|
||||
override fun applyNightTheme(newNightThemeKey: String) =
|
||||
legacyHooks.applyNightTheme(newNightThemeKey)
|
||||
override fun showSelectNightThemeToast() =
|
||||
legacyHooks.showSelectNightThemeToast()
|
||||
override fun openCaptionSettings() =
|
||||
legacyHooks.openCaptionSettings()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [UpdateActions::class])
|
||||
class AndroidUpdateActions(
|
||||
private val legacyHooks: AndroidLegacyHooks
|
||||
) : UpdateActions {
|
||||
override fun runManualCheck() = legacyHooks.runManualUpdateCheck()
|
||||
}
|
||||
+55
-2
@@ -1,4 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
class DefaultAndroidLegacyHooks {
|
||||
}
|
||||
import android.content.Intent
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* Placeholder [AndroidLegacyHooks] binding that satisfies Koin's compile-time
|
||||
* graph verification.
|
||||
*
|
||||
* At runtime the host activity (`NewPipeComposeActivity` in `:app`) registers
|
||||
* the real [AndroidLegacyHooks] implementation (`AppLegacyHooks`) through
|
||||
* `platformModules`. Koin's default override behavior means the host's
|
||||
* registration replaces this stub — so every method below should never be
|
||||
* called in a properly configured run.
|
||||
*
|
||||
* Delete this class once the legacy `:app` modules (`ErrorUtil`,
|
||||
* `NotificationWorker`, `HistoryRecordManager`, `NewVersionWorker`, etc.)
|
||||
* migrate into `:shared/androidMain` and a real implementation can live here.
|
||||
*/
|
||||
@Singleton(binds = [AndroidLegacyHooks::class])
|
||||
internal class DefaultAndroidLegacyHooks : AndroidLegacyHooks {
|
||||
|
||||
override fun showUiErrorSnackbar() = unsupported("showUiErrorSnackbar")
|
||||
override fun createErrorNotification() = unsupported("createErrorNotification")
|
||||
override fun runNotificationWorkerNow() = unsupported("runNotificationWorkerNow")
|
||||
override fun newLeakDisplayActivityIntent(): Intent? = null
|
||||
override fun requestDirectoryPicker(onPicked: (String) -> Unit) = unsupported("requestDirectoryPicker")
|
||||
override fun runManualUpdateCheck() = unsupported("runManualUpdateCheck")
|
||||
override fun wipeMetadataCache() = unsupported("wipeMetadataCache")
|
||||
override fun deleteWatchHistory() = unsupported("deleteWatchHistory")
|
||||
override fun deletePlaybackStates() = unsupported("deletePlaybackStates")
|
||||
override fun deleteSearchHistory() = unsupported("deleteSearchHistory")
|
||||
override fun clearRecaptchaCookies() = unsupported("clearRecaptchaCookies")
|
||||
override fun hasRecaptchaCookies(): Boolean = false
|
||||
override fun applyTheme(newThemeKey: String) = unsupported("applyTheme")
|
||||
override fun applyNightTheme(newNightThemeKey: String) = unsupported("applyNightTheme")
|
||||
override fun showSelectNightThemeToast() = unsupported("showSelectNightThemeToast")
|
||||
override fun openCaptionSettings() = unsupported("openCaptionSettings")
|
||||
override fun importDatabase() = unsupported("importDatabase")
|
||||
override fun exportDatabase() = unsupported("exportDatabase")
|
||||
override fun resetAllSettings() = unsupported("resetAllSettings")
|
||||
override fun importSubscriptions() = unsupported("importSubscriptions")
|
||||
override fun exportSubscriptions() = unsupported("exportSubscriptions")
|
||||
override fun openMainPageTabsChooser() = unsupported("openMainPageTabsChooser")
|
||||
override fun openPeertubeInstanceList() = unsupported("openPeertubeInstanceList")
|
||||
override fun onAppLanguageChanged() = unsupported("onAppLanguageChanged")
|
||||
|
||||
private fun unsupported(method: String): Nothing = error(
|
||||
"AndroidLegacyHooks.$method called without a host binding — " +
|
||||
"NewPipeComposeActivity.platformModules() must register AppLegacyHooks."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/defaultIconTint"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M480,840Q405,840 339.5,811.5Q274,783 225.5,734.5Q177,686 148.5,620.5Q120,555 120,480Q120,405 148.5,339.5Q177,274 225.5,225.5Q274,177 339.5,148.5Q405,120 480,120Q562,120 635.5,155Q709,190 760,254L760,160L840,160L840,400L600,400L600,320L710,320Q669,264 609,232Q549,200 480,200Q363,200 281.5,281.5Q200,363 200,480Q200,597 281.5,678.5Q363,760 480,760Q585,760 663.5,692Q742,624 756,520L838,520Q823,657 720.5,748.5Q618,840 480,840ZM592,648L440,496L440,280L520,280L520,464L648,592L592,648Z"/>
|
||||
</vector>
|
||||
@@ -34,4 +34,288 @@
|
||||
<string name="open_in_browser">Open in browser</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="not_available">Not available</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_category_player_title">Player</string>
|
||||
<string name="settings_category_player_behavior_title">Behavior</string>
|
||||
<string name="settings_category_downloads_title">Downloads</string>
|
||||
<string name="settings_category_look_and_feel_title">Look and feel</string>
|
||||
<string name="settings_category_history_title">History & cache</string>
|
||||
<string name="settings_category_content_title">Content</string>
|
||||
<string name="settings_category_feed_title">Feed</string>
|
||||
<string name="settings_category_services_title">Services</string>
|
||||
<string name="settings_category_language_title">Language</string>
|
||||
<string name="settings_category_backup_restore_title">Backup & restore</string>
|
||||
<string name="settings_category_updates_title">Updates</string>
|
||||
<string name="settings_category_debug_title">Debug</string>
|
||||
<string name="settings_layout_redesign">Enable the Redesigned Settings page</string>
|
||||
<string name="leakcanary">LeakCanary</string>
|
||||
<string name="enable_leak_canary_summary">Allow heap dumping</string>
|
||||
<string name="leak_canary_not_available">LeakCanary is not available in this build</string>
|
||||
<string name="show_memory_leaks">Show memory leaks</string>
|
||||
<string name="enable_disposed_exceptions_title">Enable disposed exceptions</string>
|
||||
<string name="enable_disposed_exceptions_summary">Throw on RxJava onErrorNotImplemented after disposal</string>
|
||||
<string name="show_original_time_ago_title">Show original time-ago</string>
|
||||
<string name="show_original_time_ago_summary">Disable smoothing of upload dates</string>
|
||||
<string name="show_crash_the_player_title">Show "crash the player" option</string>
|
||||
<string name="show_crash_the_player_summary">Adds an option to the player menu to forcibly crash it</string>
|
||||
<string name="check_new_streams">Check new streams now</string>
|
||||
<string name="crash_the_app">Crash the app</string>
|
||||
<string name="show_error_snackbar">Show error snackbar</string>
|
||||
<string name="create_error_notification">Create error notification</string>
|
||||
<string name="downloads_storage_ask_title">Ask where to download</string>
|
||||
<string name="downloads_storage_ask_summary">You will be asked where to save each download.\nEnable the system folder picker (SAF) if you want to download to an external SD card</string>
|
||||
<string name="downloads_storage_use_saf_title">Use system folder picker (SAF)</string>
|
||||
<string name="downloads_storage_use_saf_summary">The \'Storage Access Framework\' allows downloads to an external SD card</string>
|
||||
<string name="download_path_title">Video download folder</string>
|
||||
<string name="download_path_summary">Downloaded video files are stored here</string>
|
||||
<string name="download_path_audio_title">Audio download folder</string>
|
||||
<string name="download_path_audio_summary">Downloaded audio files are stored here</string>
|
||||
|
||||
<!-- HistorySettingsScreen -->
|
||||
<string name="enable_watch_history_title">Watch history</string>
|
||||
<string name="enable_watch_history_summary">Keep track of watched videos</string>
|
||||
<string name="enable_playback_resume_title">Resume playback</string>
|
||||
<string name="enable_playback_resume_summary">Restore last playback position</string>
|
||||
<string name="enable_playback_state_lists_title">Positions in lists</string>
|
||||
<string name="enable_playback_state_lists_summary">Show playback position indicators in lists</string>
|
||||
<string name="enable_search_history_title">Search history</string>
|
||||
<string name="enable_search_history_summary">Store search queries locally</string>
|
||||
<string name="settings_category_clear_data_title">Clear data</string>
|
||||
<string name="metadata_cache_wipe_title">Wipe cached metadata</string>
|
||||
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
|
||||
<string name="clear_views_history_title">Clear watch history</string>
|
||||
<string name="clear_views_history_summary">Deletes the history of played streams and the playback positions</string>
|
||||
<string name="clear_playback_states_title">Delete playback positions</string>
|
||||
<string name="clear_playback_states_summary">Deletes all playback positions</string>
|
||||
<string name="clear_search_history_title">Clear search history</string>
|
||||
<string name="clear_search_history_summary">Deletes history of search keywords</string>
|
||||
<string name="clear_cookie_title">Clear reCAPTCHA cookies</string>
|
||||
<string name="clear_cookie_summary">Clear cookies that NewPipe stores when you solve a reCAPTCHA</string>
|
||||
|
||||
<!-- Generic action labels -->
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="delete">Delete</string>
|
||||
|
||||
<!-- PlayerSettingsScreen -->
|
||||
<string name="settings_category_video_audio_title">Video & Audio</string>
|
||||
<string name="default_resolution_title">Default resolution</string>
|
||||
<string name="default_popup_resolution_title">Default popup resolution</string>
|
||||
<string name="limit_mobile_data_usage_title">Mobile data limit</string>
|
||||
<string name="show_higher_resolutions_title">Show higher resolutions</string>
|
||||
<string name="show_higher_resolutions_summary">Only some devices can play 2K/4K videos</string>
|
||||
<string name="default_video_format_title">Default video format</string>
|
||||
<string name="default_audio_format_title">Default audio format</string>
|
||||
<string name="prefer_original_audio_title">Prefer original audio</string>
|
||||
<string name="prefer_original_audio_summary">Pick the audio language marked as original by the uploader, when available</string>
|
||||
<string name="prefer_descriptive_audio_title">Prefer descriptive audio</string>
|
||||
<string name="prefer_descriptive_audio_summary">Pick the audio track that describes what is happening on screen, when available</string>
|
||||
<string name="settings_category_exoplayer_title">ExoPlayer settings</string>
|
||||
<string name="settings_category_exoplayer_summary">Configure ExoPlayer (the underlying video player)</string>
|
||||
<string name="use_external_video_player_title">Use external video player</string>
|
||||
<string name="use_external_video_player_summary">Some formats are unavailable when this is on</string>
|
||||
<string name="use_external_audio_player_title">Use external audio player</string>
|
||||
<string name="show_play_with_kodi_title">Show \"Play with Kodi\" option</string>
|
||||
<string name="show_play_with_kodi_summary">Show an option to play a video via Kodi</string>
|
||||
<string name="seekbar_preview_thumbnail_title">Seekbar preview thumbnails</string>
|
||||
|
||||
<!-- Resolution labels -->
|
||||
<string name="best_resolution">Best resolution</string>
|
||||
<string name="resolution_2160p">2160p</string>
|
||||
<string name="resolution_1440p">1440p</string>
|
||||
<string name="resolution_1080p60">1080p60</string>
|
||||
<string name="resolution_1080p">1080p</string>
|
||||
<string name="resolution_720p60">720p60</string>
|
||||
<string name="resolution_720p">720p</string>
|
||||
<string name="resolution_480p">480p</string>
|
||||
<string name="resolution_360p">360p</string>
|
||||
<string name="resolution_240p">240p</string>
|
||||
<string name="resolution_144p">144p</string>
|
||||
|
||||
<!-- Mobile data limit labels -->
|
||||
<string name="limit_data_usage_none">No limit</string>
|
||||
<string name="limit_data_usage_wifi_only">Restrict to Wi-Fi</string>
|
||||
|
||||
<!-- Format labels -->
|
||||
<string name="video_format_mp4">MP4</string>
|
||||
<string name="video_format_webm">WebM</string>
|
||||
<string name="video_format_3gp">3GP</string>
|
||||
<string name="audio_format_m4a">M4A</string>
|
||||
<string name="audio_format_webm">WebM (Opus)</string>
|
||||
<string name="audio_format_ogg">OGG (Vorbis)</string>
|
||||
|
||||
<!-- Seekbar preview thumbnail labels -->
|
||||
<string name="seekbar_preview_thumbnail_high">High quality</string>
|
||||
<string name="seekbar_preview_thumbnail_low">Low quality</string>
|
||||
<string name="seekbar_preview_thumbnail_off">Off</string>
|
||||
|
||||
<!-- ExoPlayerSettingsScreen -->
|
||||
<string name="progressive_load_interval_title">Playback load interval size</string>
|
||||
<string name="progressive_load_interval_summary">Change the load interval size on progressive contents. A lower value may speed up their initial loading</string>
|
||||
<string name="progressive_load_interval_1_kib">1 KiB</string>
|
||||
<string name="progressive_load_interval_16_kib">16 KiB</string>
|
||||
<string name="progressive_load_interval_64_kib">64 KiB</string>
|
||||
<string name="progressive_load_interval_256_kib">256 KiB</string>
|
||||
<string name="progressive_load_interval_exoplayer_default">ExoPlayer default</string>
|
||||
<string name="use_exoplayer_decoder_fallback_title">Use ExoPlayer\'s decoder fallback feature</string>
|
||||
<string name="use_exoplayer_decoder_fallback_summary">Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders</string>
|
||||
<string name="disable_media_tunneling_title">Disable media tunneling</string>
|
||||
<string name="disable_media_tunneling_summary">Disable media tunneling if you experience a black screen or stuttering on video playback.</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_title">Always use ExoPlayer\'s video output surface setting workaround</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_summary">This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen</string>
|
||||
|
||||
<!-- Generic shared labels (reused by ConfirmDialog & legacy parity) -->
|
||||
<string name="ok">OK</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
|
||||
<!-- UpdatesSettingsScreen -->
|
||||
<string name="updates_setting_title">Updates</string>
|
||||
<string name="updates_setting_description">Show a notification to prompt app update when a new version is available</string>
|
||||
<string name="check_for_updates">Check for updates</string>
|
||||
<string name="manual_update_description">Manually check for new versions</string>
|
||||
|
||||
<!-- LookFeel / AppearanceSettingsScreen -->
|
||||
<string name="settings_category_appearance_title">Appearance</string>
|
||||
<string name="theme_title">Theme</string>
|
||||
<string name="theme_light">Light theme</string>
|
||||
<string name="theme_dark">Dark theme</string>
|
||||
<string name="theme_black">Black theme</string>
|
||||
<string name="theme_auto">Automatic (device theme)</string>
|
||||
<string name="night_theme_title">Night theme</string>
|
||||
<string name="night_theme_summary">Select your favorite night theme</string>
|
||||
<string name="night_theme_available">This option is only available if %s is selected for Theme</string>
|
||||
<string name="select_night_theme_toast">You can select your favorite night theme below</string>
|
||||
<string name="show_hold_to_append_title">Show \"Hold to enqueue\" tip</string>
|
||||
<string name="show_hold_to_append_summary">Show tip when pressing the background or the popup button in video details</string>
|
||||
<string name="tablet_mode_title">Tablet mode</string>
|
||||
<string name="tablet_mode_auto">Automatic</string>
|
||||
<string name="tablet_mode_on">On</string>
|
||||
<string name="tablet_mode_off">Off</string>
|
||||
<string name="list_view_mode_title">List view mode</string>
|
||||
<string name="list_view_mode_auto">Automatic</string>
|
||||
<string name="list_view_mode_list">List</string>
|
||||
<string name="list_view_mode_grid">Grid</string>
|
||||
<string name="list_view_mode_card">Card</string>
|
||||
<string name="caption_setting_title">Captions</string>
|
||||
<string name="caption_setting_description">Modify player caption text scale and background styles. Requires app restart to take effect</string>
|
||||
<string name="main_tabs_position_title">Main tabs position</string>
|
||||
<string name="main_tabs_position_summary">Move main tab selector to the bottom</string>
|
||||
|
||||
<!-- BackupRestoreSettingsScreen -->
|
||||
<string name="import_data_title">Import database</string>
|
||||
<string name="import_data_summary">Overrides your current history, subscriptions, playlists and (optionally) settings</string>
|
||||
<string name="export_data_title">Export database</string>
|
||||
<string name="export_data_summary">Export history, subscriptions, playlists and settings</string>
|
||||
<string name="reset_settings_title">Reset settings</string>
|
||||
<string name="reset_settings_summary">Reset all settings to their default values</string>
|
||||
<string name="reset_all_settings">Resetting all settings will discard all of your preferred settings and restart the app.\n\nAre you sure you want to proceed?</string>
|
||||
<string name="import_subscriptions_title">Import subscriptions</string>
|
||||
<string name="import_subscriptions_summary">Import subscriptions from a previous .json export</string>
|
||||
<string name="export_subscriptions_title">Export subscriptions</string>
|
||||
<string name="export_subscriptions_summary">Export your subscriptions to a .json file</string>
|
||||
<string name="override_current_data">This will override your current setup.</string>
|
||||
<string name="import_settings">Do you want to also import settings?</string>
|
||||
<string name="export_complete_toast">Exported</string>
|
||||
<string name="no_valid_zip_file">No valid ZIP file</string>
|
||||
<string name="import_settings_vulnerable_format">The settings being imported use a vulnerable format deprecated since NewPipe 0.27.0. Make sure the export is from a trusted source.</string>
|
||||
|
||||
<!-- ContentSettingsScreen -->
|
||||
<string name="app_language_title">App language</string>
|
||||
<string name="content">Content</string>
|
||||
<string name="content_language_title">Default content language</string>
|
||||
<string name="default_content_country_title">Default content country</string>
|
||||
<string name="main_page_content">Content of main page</string>
|
||||
<string name="main_page_content_summary">What tabs are shown on the main page</string>
|
||||
<string name="show_channel_tabs">Channel tabs</string>
|
||||
<string name="show_channel_tabs_summary">What tabs are shown on the channel pages</string>
|
||||
<string name="peertube_instance_url_title">PeerTube instances</string>
|
||||
<string name="peertube_instance_url_summary">Select your favorite PeerTube instances</string>
|
||||
<string name="show_age_restricted_content_title">Show age restricted content</string>
|
||||
<string name="show_age_restricted_content_summary">Show content possibly unsuitable for children because it has an age limit (like 18+)</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Turn on YouTube\'s \"Restricted Mode\"</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">YouTube provides a \"Restricted Mode\" which hides potentially mature content</string>
|
||||
<string name="show_search_suggestions_title">Search suggestions</string>
|
||||
<string name="show_search_suggestions_summary">Choose the suggestions to show when searching</string>
|
||||
<string name="image_quality_title">Image quality</string>
|
||||
<string name="image_quality_summary">Choose the quality of images and whether to load images at all, to reduce data and memory usage</string>
|
||||
<string name="image_quality_low">Low</string>
|
||||
<string name="image_quality_medium">Medium</string>
|
||||
<string name="image_quality_high">High</string>
|
||||
<string name="show_comments_title">Show comments</string>
|
||||
<string name="show_comments_summary">Turn off to hide comments</string>
|
||||
<string name="show_next_and_similar_title">Show \'Next\' and \'Similar\' videos</string>
|
||||
<string name="show_description_title">Show description</string>
|
||||
<string name="show_description_summary">Turn off to hide video description and additional information</string>
|
||||
<string name="show_meta_info_title">Show meta info</string>
|
||||
<string name="show_meta_info_summary">Turn off to hide meta info boxes with additional information</string>
|
||||
|
||||
<!-- Feed sub-category -->
|
||||
<string name="feed_update_threshold_title">Feed update threshold</string>
|
||||
<string name="feed_update_threshold_summary">Time after last update before a subscription is considered outdated</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">Fetch from dedicated feed when available</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Available in some services, usually much faster but may return limited info</string>
|
||||
<string name="feed_fetch_channel_tabs">Fetch channel tabs</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Tabs to fetch when updating the feed. No effect if fast mode is used.</string>
|
||||
|
||||
<!-- Feed threshold value labels -->
|
||||
<string name="threshold_immediate">Immediately</string>
|
||||
<string name="threshold_5_min">After 5 minutes</string>
|
||||
<string name="threshold_15_min">After 15 minutes</string>
|
||||
<string name="threshold_1_hour">After 1 hour</string>
|
||||
<string name="threshold_6_hours">After 6 hours</string>
|
||||
<string name="threshold_12_hours">After 12 hours</string>
|
||||
<string name="threshold_1_day">After 1 day</string>
|
||||
|
||||
<!-- Channel-tab labels -->
|
||||
<string name="channel_tab_videos">Videos</string>
|
||||
<string name="channel_tab_tracks">Tracks</string>
|
||||
<string name="channel_tab_shorts">Shorts</string>
|
||||
<string name="channel_tab_livestreams">Livestreams</string>
|
||||
<string name="channel_tab_channels">Channels</string>
|
||||
<string name="channel_tab_playlists">Playlists</string>
|
||||
<string name="channel_tab_albums">Albums</string>
|
||||
<string name="channel_tab_likes">Likes</string>
|
||||
<string name="channel_tab_about">About</string>
|
||||
|
||||
<!-- Search-suggestion labels -->
|
||||
<string name="local_search_suggestions">Local</string>
|
||||
<string name="remote_search_suggestions">Remote</string>
|
||||
|
||||
<!-- Localization labels -->
|
||||
<string name="language_system">System default</string>
|
||||
<string name="language_en">English</string>
|
||||
<string name="language_es">Spanish</string>
|
||||
<string name="language_fr">French</string>
|
||||
<string name="language_de">German</string>
|
||||
<string name="language_pt">Portuguese</string>
|
||||
<string name="language_ru">Russian</string>
|
||||
<string name="language_zh">Chinese</string>
|
||||
<string name="language_ja">Japanese</string>
|
||||
<string name="language_ko">Korean</string>
|
||||
<string name="language_it">Italian</string>
|
||||
<string name="language_nl">Dutch</string>
|
||||
<string name="language_pl">Polish</string>
|
||||
<string name="language_tr">Turkish</string>
|
||||
<string name="language_ar">Arabic</string>
|
||||
<string name="language_hi">Hindi</string>
|
||||
|
||||
<string name="country_system">System default</string>
|
||||
<string name="country_us">United States</string>
|
||||
<string name="country_gb">United Kingdom</string>
|
||||
<string name="country_de">Germany</string>
|
||||
<string name="country_fr">France</string>
|
||||
<string name="country_es">Spain</string>
|
||||
<string name="country_it">Italy</string>
|
||||
<string name="country_br">Brazil</string>
|
||||
<string name="country_jp">Japan</string>
|
||||
<string name="country_kr">South Korea</string>
|
||||
<string name="country_in">India</string>
|
||||
<string name="country_ru">Russia</string>
|
||||
<string name="country_cn">China</string>
|
||||
<string name="country_au">Australia</string>
|
||||
<string name="country_ca">Canada</string>
|
||||
<string name="country_mx">Mexico</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -12,24 +12,31 @@ import net.newpipe.app.navigation.NavDisplay
|
||||
import net.newpipe.app.navigation.navModule
|
||||
import net.newpipe.app.theme.AppTheme
|
||||
import org.koin.compose.KoinApplication
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.plugin.module.dsl.koinConfiguration
|
||||
|
||||
/**
|
||||
* Entry point for the multiplatform compose application
|
||||
* @param startDestination Starting destination for the app; defaults to about
|
||||
* @param onCloseRequest Callback to close the app
|
||||
* @param withKoin Additional logic to execute after initialising Koin and setting content
|
||||
* Entry point for the multiplatform compose application.
|
||||
*
|
||||
* @param startDestination Starting destination for the app; defaults to About.
|
||||
* @param onCloseRequest Callback to close the app.
|
||||
* @param platformModules Extra Koin modules supplied by the host platform.
|
||||
* Android passes a module that registers the running Application/Context;
|
||||
* iOS / desktop pass an empty list.
|
||||
* @param withKoin Extra composable content rendered inside the Koin context.
|
||||
*/
|
||||
@Composable
|
||||
fun App(
|
||||
startDestination: Destination = Destination.About,
|
||||
onCloseRequest: () -> Unit,
|
||||
platformModules: List<Module> = emptyList(),
|
||||
withKoin: @Composable () -> Unit = {}
|
||||
) {
|
||||
KoinApplication(
|
||||
configuration = koinConfiguration<KoinApp>(
|
||||
appDeclaration = {
|
||||
modules(navModule())
|
||||
modules(platformModules)
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -1,2 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
|
||||
/**
|
||||
* Generic yes/no confirmation dialog.
|
||||
*
|
||||
* @param title Dialog title.
|
||||
* @param message Body text shown above the buttons.
|
||||
* @param confirmLabel Label for the confirm button (defaults to "OK"-style).
|
||||
* @param dismissLabel Label for the dismiss button.
|
||||
* @param onConfirm Called when the user accepts.
|
||||
* @param onDismiss Called when the user dismisses or taps outside.
|
||||
*/
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
confirmLabel: String,
|
||||
dismissLabel: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = title) },
|
||||
text = { Text(text = message) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = confirmLabel, color =
|
||||
MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(text = dismissLabel) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ConfirmDialogPreview() {
|
||||
ConfirmDialog(
|
||||
title = "Clear watch history",
|
||||
message = "Deletes the history of played streams and the playback positions.", confirmLabel = "Delete",
|
||||
dismissLabel = "Cancel",
|
||||
onConfirm = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ConfirmDialogResetSettingsPreview() {
|
||||
ConfirmDialog(
|
||||
title = "Reset settings",
|
||||
message = "Resetting all settings will discard all of your preferred settings and " + "restart the app.\n\nAre you sure you want to proceed?",
|
||||
confirmLabel = "OK",
|
||||
dismissLabel = "Cancel",
|
||||
onConfirm = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,161 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.theme.spaceSmall
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.cancel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* One entry in a [ListPreference].
|
||||
* @param value Persisted to [com.russhwolf.settings.ObservableSettings].
|
||||
* @param label Display label shown in the dialog and as the row summary.
|
||||
*/
|
||||
data class ListPreferenceOption(
|
||||
val value: String,
|
||||
val label: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Row that opens a radio-button dialog for picking a single value from
|
||||
* [options].
|
||||
*
|
||||
* @param summary Optional static summary. When `null`, the row's summary
|
||||
* shows the label of the currently selected option (legacy parity with
|
||||
* `useSimpleSummaryProvider="true"`). Pass a non-null string to mirror the
|
||||
* legacy `android:summary="…"` static-description behavior.
|
||||
*/
|
||||
@Composable
|
||||
fun ListPreference(
|
||||
title: String,
|
||||
options: List<ListPreferenceOption>,
|
||||
selectedValue: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
summary: String? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
var dialogOpen by rememberSaveable { mutableStateOf(false) }
|
||||
val effectiveSummary = summary ?: options.firstOrNull { it.value == selectedValue }?.label
|
||||
|
||||
TextPreference(
|
||||
title = title,
|
||||
summary = effectiveSummary,
|
||||
onClick = { dialogOpen = true },
|
||||
modifier = modifier,
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
if (dialogOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogOpen = false },
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Column {
|
||||
options.forEach { option ->
|
||||
val isSelected = option.value == selectedValue
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
onValueChange(option.value)
|
||||
dialogOpen = false
|
||||
}
|
||||
)
|
||||
.padding(vertical = spaceSmall),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null
|
||||
)
|
||||
Text(
|
||||
text = option.label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = spaceSmall)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { dialogOpen = false }) {
|
||||
Text(text = stringResource(Res.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val previewOptions = listOf(
|
||||
ListPreferenceOption("best_resolution", "Best resolution"),
|
||||
ListPreferenceOption("1080p", "1080p"),
|
||||
ListPreferenceOption("720p", "720p"),
|
||||
ListPreferenceOption("480p", "480p")
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ListPreferenceSelectedLabelSummaryPreview() {
|
||||
ListPreference(
|
||||
title = "Default resolution",
|
||||
options = previewOptions,
|
||||
selectedValue = "720p",
|
||||
onValueChange = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ListPreferenceStaticSummaryPreview() {
|
||||
ListPreference(
|
||||
title = "Image quality",
|
||||
summary = "Choose the quality of images and whether to load images at all", options = previewOptions,
|
||||
selectedValue = "best_resolution",
|
||||
onValueChange = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ListPreferenceDisabledPreview() {
|
||||
ListPreference(
|
||||
title = "Night theme",
|
||||
options = previewOptions,
|
||||
selectedValue = "1080p",
|
||||
onValueChange = {},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.theme.spaceSmall
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.cancel
|
||||
import newpipe.shared.generated.resources.ok
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* One option in a [MultiSelectListPreference].
|
||||
**/
|
||||
data class MultiSelectListPreferenceOption(
|
||||
val value: String,
|
||||
val label: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Row that opens a checkbox dialog letting the user pick zero or more values
|
||||
* from [options]. Pending selection is held locally so Cancel reverts.
|
||||
*/
|
||||
@Composable
|
||||
fun MultiSelectListPreference(
|
||||
title: String,
|
||||
options: List<MultiSelectListPreferenceOption>,
|
||||
selectedValues: Set<String>,
|
||||
onValuesChange: (Set<String>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
summary: String? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
var dialogOpen by rememberSaveable { mutableStateOf(false) }
|
||||
val effectiveSummary = summary ?: options
|
||||
.filter { it.value in selectedValues }
|
||||
.joinToString(", ") { it.label }
|
||||
.ifEmpty { null }
|
||||
|
||||
TextPreference(
|
||||
title = title,
|
||||
summary = effectiveSummary,
|
||||
onClick = { dialogOpen = true },
|
||||
modifier = modifier,
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
if (dialogOpen) {
|
||||
var pending by remember { mutableStateOf(selectedValues) }
|
||||
LaunchedEffect(selectedValues) { pending = selectedValues }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogOpen = false },
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Column(modifier =
|
||||
Modifier.verticalScroll(rememberScrollState())) {
|
||||
options.forEach { option ->
|
||||
val checked = option.value in pending
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = checked,
|
||||
onValueChange = { isChecked ->
|
||||
pending = if (isChecked) pending +
|
||||
option.value
|
||||
else pending - option.value
|
||||
}
|
||||
)
|
||||
.padding(vertical = spaceSmall),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(checked = checked, onCheckedChange = null)
|
||||
Text(
|
||||
text = option.label,
|
||||
style =
|
||||
MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start =
|
||||
spaceSmall)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValuesChange(pending)
|
||||
dialogOpen = false
|
||||
}) {
|
||||
Text(text = stringResource(Res.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { dialogOpen = false }) {
|
||||
Text(text = stringResource(Res.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun MultiSelectListPreferencePreview() {
|
||||
val options = listOf(
|
||||
MultiSelectListPreferenceOption("a", "Apple"),
|
||||
MultiSelectListPreferenceOption("b", "Banana"),
|
||||
MultiSelectListPreferenceOption("c", "Cherry")
|
||||
)
|
||||
MultiSelectListPreference(
|
||||
title = "Pick fruits",
|
||||
options = options,
|
||||
selectedValues = setOf("a", "c"),
|
||||
onValuesChange = {}
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,47 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.theme.spaceLarge
|
||||
import net.newpipe.app.theme.spaceSmall
|
||||
|
||||
/**
|
||||
* Section header used to group preference rows.
|
||||
*
|
||||
* Maps to `<PreferenceCategory android:title="…">` in the legacy XML
|
||||
* preference system.
|
||||
*/
|
||||
@Composable
|
||||
fun PreferenceCategoryHeader(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = spaceLarge, vertical = spaceSmall)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun PreferenceCategoryHeaderPreview() {
|
||||
PreferenceCategoryHeader(title = "Clear data")
|
||||
}
|
||||
@@ -1,2 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
|
||||
private const val DISABLED_ALPHA = 0.38f
|
||||
|
||||
@Composable
|
||||
internal fun PreferenceText(
|
||||
title: String,
|
||||
summary: String?,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Start,
|
||||
color = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurface.copy(alpha = DISABLED_ALPHA)
|
||||
)
|
||||
summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Start,
|
||||
color = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = DISABLED_ALPHA)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun PreferenceTextEnabledPreview() {
|
||||
PreferenceText(
|
||||
title = "Default resolution",
|
||||
summary = "Best resolution",
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun PreferenceTextDisabledPreview() {
|
||||
PreferenceText(
|
||||
title = "Resume playback",
|
||||
summary = "Restore last playback position",
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun PreferenceTextNoSummaryPreview() {
|
||||
PreferenceText(
|
||||
title = "Crash the app",
|
||||
summary = null,
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,102 @@
|
||||
/*
|
||||
* 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 net.newpipe.app.composable
|
||||
|
||||
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.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.theme.spaceSmall
|
||||
import net.newpipe.app.theme.spaceXSmall
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
title: String,
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
summary: String? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(spaceSmall)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
PreferenceText(title = title, summary = summary, enabled = enabled)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(spaceXSmall))
|
||||
Switch(
|
||||
checked = isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SwitchPreferenceCheckedPreview() {
|
||||
SwitchPreference(
|
||||
title = "Watch history",
|
||||
summary = "Keep track of watched videos",
|
||||
isChecked = true,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SwitchPreferenceUncheckedPreview() {
|
||||
SwitchPreference(
|
||||
title = "Use external video player",
|
||||
summary = "Some formats are unavailable when this is on",
|
||||
isChecked = false,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SwitchPreferenceNoSummaryPreview() {
|
||||
SwitchPreference(
|
||||
title = "Use external audio player",
|
||||
isChecked = false,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SwitchPreferenceDisabledPreview() {
|
||||
SwitchPreference(
|
||||
title = "Resume playback",
|
||||
summary = "Restore last playback position",
|
||||
isChecked = true,
|
||||
onCheckedChange = {},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,117 @@
|
||||
/*
|
||||
* 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 net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.theme.preferenceMinHeight
|
||||
import net.newpipe.app.theme.spaceSmall
|
||||
import net.newpipe.app.theme.spaceXSmall
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.ic_settings
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
private const val DISABLED_ALPHA = 0.38f
|
||||
|
||||
@Composable
|
||||
fun TextPreference(
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter? = null,
|
||||
summary: String? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(spaceSmall)
|
||||
.defaultMinSize(minHeight = preferenceMinHeight)
|
||||
.clickable(enabled = enabled) { onClick() }
|
||||
) {
|
||||
icon?.let {
|
||||
Icon(
|
||||
painter = it,
|
||||
contentDescription = null,
|
||||
tint = if (enabled) Color.Unspecified else MaterialTheme.colorScheme.onSurface.copy(alpha = DISABLED_ALPHA)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(spaceXSmall))
|
||||
}
|
||||
Column {
|
||||
PreferenceText(title = title, summary = summary, enabled = enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TextPreferenceWithIconPreview() {
|
||||
TextPreference(
|
||||
title = "Player",
|
||||
summary = "Resolution, format, external player…",
|
||||
icon = painterResource(Res.drawable.ic_settings),
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TextPreferenceNoIconPreview() {
|
||||
TextPreference(
|
||||
title = "Reset settings",
|
||||
summary = "Reset all settings to their default values",
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TextPreferenceTitleOnlyPreview() {
|
||||
TextPreference(
|
||||
title = "Crash the app",
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TextPreferenceDisabledPreview() {
|
||||
TextPreference(
|
||||
title = "Show memory leaks",
|
||||
summary = "LeakCanary is not available in this build",
|
||||
onClick = {},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ package net.newpipe.app.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.newpipe.app.model.License
|
||||
|
||||
/**
|
||||
* Destinations for navigation in compose
|
||||
@@ -17,4 +16,51 @@ sealed interface Destination : NavKey {
|
||||
|
||||
@Serializable
|
||||
data object About : Destination
|
||||
@Serializable
|
||||
sealed interface Settings : Destination {
|
||||
|
||||
@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
|
||||
|
||||
@Serializable
|
||||
data object ExoPlayer : Settings
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,16 @@ package net.newpipe.app.navigation
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import co.touchlab.kermit.Logger
|
||||
import net.newpipe.app.screen.about.AboutScreen
|
||||
import net.newpipe.app.screen.settings.backuprestore.BackupRestoreSettingsScreen
|
||||
import net.newpipe.app.screen.settings.content.ContentSettingsScreen
|
||||
import net.newpipe.app.screen.settings.debug.DebugScreen
|
||||
import net.newpipe.app.screen.settings.download.DownloadSettingsScreen
|
||||
import net.newpipe.app.screen.settings.exoplayer.ExoPlayerSettingsScreen
|
||||
import net.newpipe.app.screen.settings.history.HistorySettingsScreen
|
||||
import net.newpipe.app.screen.settings.home.SettingsHomeScreen
|
||||
import net.newpipe.app.screen.settings.lookfeel.LookFeelSettingsScreen
|
||||
import net.newpipe.app.screen.settings.player.PlayerSettingsScreen
|
||||
import net.newpipe.app.screen.settings.updates.UpdatesSettingsScreen
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.core.annotation.Provided
|
||||
import org.koin.core.annotation.Singleton
|
||||
@@ -27,6 +37,47 @@ fun navModule() = module {
|
||||
navigation<Destination.About> {
|
||||
AboutScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.Home> {
|
||||
SettingsHomeScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.Debug> {
|
||||
DebugScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.Player> {
|
||||
PlayerSettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.Download> {
|
||||
DownloadSettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.HistoryCache> {
|
||||
HistorySettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.ExoPlayer> {
|
||||
ExoPlayerSettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.Updates> {
|
||||
UpdatesSettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.LookFeel> {
|
||||
LookFeelSettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.BackupRestore> {
|
||||
BackupRestoreSettingsScreen()
|
||||
}
|
||||
|
||||
navigation<Destination.Settings.Content> {
|
||||
ContentSettingsScreen()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side-effects triggered from the Backup & Restore settings screen.
|
||||
*
|
||||
* Android: each action opens the appropriate SAF picker via launchers
|
||||
* registered by the host activity, then runs the corresponding flow
|
||||
* (DB checkpoint, ZIP I/O, optional settings import, app restart) via the
|
||||
* legacy [ImportExportManager] in `:app`.
|
||||
*
|
||||
*/
|
||||
interface BackupRestoreActions {
|
||||
/** Open file picker for a NewPipe ZIP export and import it (DB + optional settings). */
|
||||
fun importDatabase()
|
||||
|
||||
/** Open save-as picker and export DB + settings into a NewPipe ZIP. */
|
||||
fun exportDatabase()
|
||||
|
||||
/** Clear all SharedPreferences after the screen's confirm dialog accepts, then restart. */
|
||||
fun resetAllSettings()
|
||||
|
||||
/** Open file picker for a subscriptions .json and import them. */
|
||||
fun importSubscriptions()
|
||||
|
||||
/** Open save-as picker and export subscriptions to .json. */
|
||||
fun exportSubscriptions()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side-effects triggered from the Content settings screen.
|
||||
*
|
||||
* Android: opens the legacy ChooseTabsFragment / PeertubeInstanceListFragment
|
||||
* via [SettingsActivity], and restarts the app when the app-language changes
|
||||
* (so locale takes effect).
|
||||
*
|
||||
*/
|
||||
interface ContentActions {
|
||||
fun openMainPageTabsChooser()
|
||||
fun openPeertubeInstanceList()
|
||||
fun onAppLanguageChanged()
|
||||
}
|
||||
@@ -1,2 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side effects triggered from the debug settings screen.
|
||||
* Implemented on Android against ErrorUtil / NotificationWorker / LeakCanary.
|
||||
* */
|
||||
interface DebugActions {
|
||||
fun crashTheApp()
|
||||
fun showErrorSnackbar()
|
||||
fun createErrorNotification()
|
||||
fun checkNewStreams()
|
||||
fun showMemoryLeaks()
|
||||
|
||||
fun isLeakCanaryAvailable(): Boolean
|
||||
}
|
||||
@@ -1,2 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side-effects triggered from download-related settings.
|
||||
*/
|
||||
interface DownloadActions {
|
||||
fun pickDirectory(onPicked: (uri: String) -> Unit)
|
||||
}
|
||||
@@ -1,2 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side-effects triggered from the History & Cache settings screen.
|
||||
*/
|
||||
interface HistoryActions {
|
||||
fun wipeMetadataCache()
|
||||
fun deleteWatchHistory()
|
||||
fun deletePlaybackStates()
|
||||
fun deleteSearchHistory()
|
||||
|
||||
fun clearRecaptchaCookies()
|
||||
fun hasRecaptchaCookies(): Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side-effects triggered from the LookFeel/Appearance settings screen.
|
||||
*
|
||||
* Android: applies day/night mode and recreates the current Activity so the
|
||||
* theme change takes effect immediately. Also opens the system captioning
|
||||
* intent.
|
||||
*
|
||||
*/
|
||||
interface LookFeelActions {
|
||||
/**
|
||||
* Apply the new app theme (one of the theme values from
|
||||
* `app/src/main/res/values/settings_keys.xml`: light, dark, black,
|
||||
* auto_device_theme) and recreate the current Activity so the change
|
||||
* takes effect immediately.
|
||||
*/
|
||||
fun applyTheme(newThemeKey: String)
|
||||
|
||||
/**
|
||||
* Apply the new night theme (only the "dark" / "black" values, used when
|
||||
* the main theme is "auto_device_theme"). Same recreate behavior.
|
||||
*/
|
||||
fun applyNightTheme(newNightThemeKey: String)
|
||||
|
||||
/**
|
||||
* Show a short toast/snackbar with the localized message
|
||||
* "You can select your favorite night theme below". Matches the legacy
|
||||
* fragment's behavior when the user picks the auto-device theme.
|
||||
*/
|
||||
fun showSelectNightThemeToast()
|
||||
|
||||
/** Open the system captioning settings screen (Android only; no-op elsewhere). */
|
||||
fun openCaptionSettings()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
/**
|
||||
* Platform side-effects triggered from the Updates settings screen.
|
||||
*
|
||||
* Android: calls into the legacy NewVersionWorker via [AndroidLegacyHooks].
|
||||
*/
|
||||
interface UpdateActions {
|
||||
/** Trigger an immediate update check; shows a "checking…" toast on Android. */
|
||||
fun runManualCheck()
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.backuprestore
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.ConfirmDialog
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.backuprestore.BackupRestoreSettingsViewModel
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.cancel
|
||||
import newpipe.shared.generated.resources.export_data_summary
|
||||
import newpipe.shared.generated.resources.export_data_title
|
||||
import newpipe.shared.generated.resources.export_subscriptions_summary
|
||||
import newpipe.shared.generated.resources.export_subscriptions_title
|
||||
import newpipe.shared.generated.resources.import_data_summary
|
||||
import newpipe.shared.generated.resources.import_data_title
|
||||
import newpipe.shared.generated.resources.import_subscriptions_summary
|
||||
import newpipe.shared.generated.resources.import_subscriptions_title
|
||||
import newpipe.shared.generated.resources.ok
|
||||
import newpipe.shared.generated.resources.reset_all_settings
|
||||
import newpipe.shared.generated.resources.reset_settings_summary
|
||||
import newpipe.shared.generated.resources.reset_settings_title
|
||||
import newpipe.shared.generated.resources.settings_category_backup_restore_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun BackupRestoreSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: BackupRestoreSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
BackupRestoreSettingsContent(
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onImportDatabase = viewModel::importDatabase,
|
||||
onExportDatabase = viewModel::exportDatabase,
|
||||
onResetSettings = viewModel::resetAllSettings,
|
||||
onImportSubscriptions = viewModel::importSubscriptions,
|
||||
onExportSubscriptions = viewModel::exportSubscriptions
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupRestoreSettingsContent(
|
||||
onNavigateUp: () -> Unit,
|
||||
onImportDatabase: () -> Unit,
|
||||
onExportDatabase: () -> Unit,
|
||||
onResetSettings: () -> Unit,
|
||||
onImportSubscriptions: () -> Unit,
|
||||
onExportSubscriptions: () -> Unit
|
||||
) {
|
||||
var showResetConfirm by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showResetConfirm) {
|
||||
ConfirmDialog(
|
||||
title = stringResource(Res.string.reset_settings_title),
|
||||
message = stringResource(Res.string.reset_all_settings),
|
||||
confirmLabel = stringResource(Res.string.ok),
|
||||
dismissLabel = stringResource(Res.string.cancel),
|
||||
onConfirm = {
|
||||
showResetConfirm = false
|
||||
onResetSettings()
|
||||
},
|
||||
onDismiss = { showResetConfirm = false }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title =
|
||||
stringResource(Res.string.settings_category_backup_restore_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.import_data_title),
|
||||
summary = stringResource(Res.string.import_data_summary),
|
||||
onClick = onImportDatabase
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.export_data_title),
|
||||
summary = stringResource(Res.string.export_data_summary),
|
||||
onClick = onExportDatabase
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.reset_settings_title),
|
||||
summary =
|
||||
stringResource(Res.string.reset_settings_summary),
|
||||
onClick = { showResetConfirm = true }
|
||||
)
|
||||
TextPreference(
|
||||
title =
|
||||
stringResource(Res.string.export_subscriptions_title),
|
||||
summary =
|
||||
stringResource(Res.string.export_subscriptions_summary),
|
||||
onClick = onExportSubscriptions
|
||||
)
|
||||
TextPreference(
|
||||
title =
|
||||
stringResource(Res.string.import_subscriptions_title),
|
||||
summary =
|
||||
stringResource(Res.string.import_subscriptions_summary),
|
||||
onClick = onImportSubscriptions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun BackupRestoreSettingsScreenPreview() {
|
||||
BackupRestoreSettingsContent(
|
||||
onNavigateUp = {},
|
||||
onImportDatabase = {},
|
||||
onExportDatabase = {},
|
||||
onResetSettings = {},
|
||||
onImportSubscriptions = {},
|
||||
onExportSubscriptions = {}
|
||||
)
|
||||
}
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.content
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.ListPreference
|
||||
import net.newpipe.app.composable.ListPreferenceOption
|
||||
import net.newpipe.app.composable.MultiSelectListPreference
|
||||
import net.newpipe.app.composable.MultiSelectListPreferenceOption
|
||||
import net.newpipe.app.composable.PreferenceCategoryHeader
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_ABOUT
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_ALBUMS
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_CHANNELS
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_LIKES
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_LIVESTREAMS
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_PLAYLISTS
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_SHORTS
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_TRACKS
|
||||
import net.newpipe.app.viewmodel.settings.content.CHANNEL_TAB_VIDEOS
|
||||
import net.newpipe.app.viewmodel.settings.content.ContentSettingsViewModel
|
||||
import net.newpipe.app.viewmodel.settings.content.FETCH_TAB_LIKES
|
||||
import net.newpipe.app.viewmodel.settings.content.FETCH_TAB_LIVESTREAMS
|
||||
import net.newpipe.app.viewmodel.settings.content.FETCH_TAB_SHORTS
|
||||
import net.newpipe.app.viewmodel.settings.content.FETCH_TAB_TRACKS
|
||||
import net.newpipe.app.viewmodel.settings.content.FETCH_TAB_VIDEOS
|
||||
import net.newpipe.app.viewmodel.settings.content.IMAGE_QUALITY_HIGH
|
||||
import net.newpipe.app.viewmodel.settings.content.IMAGE_QUALITY_LOW
|
||||
import net.newpipe.app.viewmodel.settings.content.IMAGE_QUALITY_MEDIUM
|
||||
import net.newpipe.app.viewmodel.settings.content.LOCALIZATION_SYSTEM
|
||||
import net.newpipe.app.viewmodel.settings.content.SUGGESTIONS_LOCAL
|
||||
import net.newpipe.app.viewmodel.settings.content.SUGGESTIONS_REMOTE
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_12_HOURS
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_15_MIN
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_1_DAY
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_1_HOUR
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_5_MIN
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_6_HOURS
|
||||
import net.newpipe.app.viewmodel.settings.content.THRESHOLD_IMMEDIATE
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.app_language_title
|
||||
import newpipe.shared.generated.resources.channel_tab_about
|
||||
import newpipe.shared.generated.resources.channel_tab_albums
|
||||
import newpipe.shared.generated.resources.channel_tab_channels
|
||||
import newpipe.shared.generated.resources.channel_tab_likes
|
||||
import newpipe.shared.generated.resources.channel_tab_livestreams
|
||||
import newpipe.shared.generated.resources.channel_tab_playlists
|
||||
import newpipe.shared.generated.resources.channel_tab_shorts
|
||||
import newpipe.shared.generated.resources.channel_tab_tracks
|
||||
import newpipe.shared.generated.resources.channel_tab_videos
|
||||
import newpipe.shared.generated.resources.content
|
||||
import newpipe.shared.generated.resources.content_language_title
|
||||
import newpipe.shared.generated.resources.country_au
|
||||
import newpipe.shared.generated.resources.country_br
|
||||
import newpipe.shared.generated.resources.country_ca
|
||||
import newpipe.shared.generated.resources.country_cn
|
||||
import newpipe.shared.generated.resources.country_de
|
||||
import newpipe.shared.generated.resources.country_es
|
||||
import newpipe.shared.generated.resources.country_fr
|
||||
import newpipe.shared.generated.resources.country_gb
|
||||
import newpipe.shared.generated.resources.country_in
|
||||
import newpipe.shared.generated.resources.country_it
|
||||
import newpipe.shared.generated.resources.country_jp
|
||||
import newpipe.shared.generated.resources.country_kr
|
||||
import newpipe.shared.generated.resources.country_mx
|
||||
import newpipe.shared.generated.resources.country_ru
|
||||
import newpipe.shared.generated.resources.country_system
|
||||
import newpipe.shared.generated.resources.country_us
|
||||
import newpipe.shared.generated.resources.default_content_country_title
|
||||
import newpipe.shared.generated.resources.feed_fetch_channel_tabs
|
||||
import newpipe.shared.generated.resources.feed_fetch_channel_tabs_summary
|
||||
import newpipe.shared.generated.resources.feed_update_threshold_summary
|
||||
import newpipe.shared.generated.resources.feed_update_threshold_title
|
||||
import newpipe.shared.generated.resources.feed_use_dedicated_fetch_method_summary
|
||||
import newpipe.shared.generated.resources.feed_use_dedicated_fetch_method_title
|
||||
import newpipe.shared.generated.resources.image_quality_high
|
||||
import newpipe.shared.generated.resources.image_quality_low
|
||||
import newpipe.shared.generated.resources.image_quality_medium
|
||||
import newpipe.shared.generated.resources.image_quality_summary
|
||||
import newpipe.shared.generated.resources.image_quality_title
|
||||
import newpipe.shared.generated.resources.language_ar
|
||||
import newpipe.shared.generated.resources.language_de
|
||||
import newpipe.shared.generated.resources.language_en
|
||||
import newpipe.shared.generated.resources.language_es
|
||||
import newpipe.shared.generated.resources.language_fr
|
||||
import newpipe.shared.generated.resources.language_hi
|
||||
import newpipe.shared.generated.resources.language_it
|
||||
import newpipe.shared.generated.resources.language_ja
|
||||
import newpipe.shared.generated.resources.language_ko
|
||||
import newpipe.shared.generated.resources.language_nl
|
||||
import newpipe.shared.generated.resources.language_pl
|
||||
import newpipe.shared.generated.resources.language_pt
|
||||
import newpipe.shared.generated.resources.language_ru
|
||||
import newpipe.shared.generated.resources.language_system
|
||||
import newpipe.shared.generated.resources.language_tr
|
||||
import newpipe.shared.generated.resources.language_zh
|
||||
import newpipe.shared.generated.resources.local_search_suggestions
|
||||
import newpipe.shared.generated.resources.main_page_content
|
||||
import newpipe.shared.generated.resources.main_page_content_summary
|
||||
import newpipe.shared.generated.resources.peertube_instance_url_summary
|
||||
import newpipe.shared.generated.resources.peertube_instance_url_title
|
||||
import newpipe.shared.generated.resources.remote_search_suggestions
|
||||
import newpipe.shared.generated.resources.settings_category_feed_title
|
||||
import newpipe.shared.generated.resources.show_age_restricted_content_summary
|
||||
import newpipe.shared.generated.resources.show_age_restricted_content_title
|
||||
import newpipe.shared.generated.resources.show_channel_tabs
|
||||
import newpipe.shared.generated.resources.show_channel_tabs_summary
|
||||
import newpipe.shared.generated.resources.show_comments_summary
|
||||
import newpipe.shared.generated.resources.show_comments_title
|
||||
import newpipe.shared.generated.resources.show_description_summary
|
||||
import newpipe.shared.generated.resources.show_description_title
|
||||
import newpipe.shared.generated.resources.show_meta_info_summary
|
||||
import newpipe.shared.generated.resources.show_meta_info_title
|
||||
import newpipe.shared.generated.resources.show_next_and_similar_title
|
||||
import newpipe.shared.generated.resources.show_search_suggestions_summary
|
||||
import newpipe.shared.generated.resources.show_search_suggestions_title
|
||||
import newpipe.shared.generated.resources.threshold_12_hours
|
||||
import newpipe.shared.generated.resources.threshold_15_min
|
||||
import newpipe.shared.generated.resources.threshold_1_day
|
||||
import newpipe.shared.generated.resources.threshold_1_hour
|
||||
import newpipe.shared.generated.resources.threshold_5_min
|
||||
import newpipe.shared.generated.resources.threshold_6_hours
|
||||
import newpipe.shared.generated.resources.threshold_immediate
|
||||
import newpipe.shared.generated.resources.youtube_restricted_mode_enabled_summary
|
||||
import newpipe.shared.generated.resources.youtube_restricted_mode_enabled_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun ContentSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: ContentSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val appLanguage by viewModel.appLanguage.collectAsState()
|
||||
val contentLanguage by viewModel.contentLanguage.collectAsState()
|
||||
val contentCountry by viewModel.contentCountry.collectAsState()
|
||||
val showChannelTabs by viewModel.showChannelTabs.collectAsState()
|
||||
val showSearchSuggestions by
|
||||
viewModel.showSearchSuggestions.collectAsState()
|
||||
val imageQuality by viewModel.imageQuality.collectAsState()
|
||||
val showAgeRestricted by viewModel.showAgeRestricted.collectAsState()
|
||||
val youtubeRestrictedMode by
|
||||
viewModel.youtubeRestrictedMode.collectAsState()
|
||||
val showComments by viewModel.showComments.collectAsState()
|
||||
val showNextVideo by viewModel.showNextVideo.collectAsState()
|
||||
val showDescription by viewModel.showDescription.collectAsState()
|
||||
val showMetaInfo by viewModel.showMetaInfo.collectAsState()
|
||||
val feedUpdateThreshold by
|
||||
viewModel.feedUpdateThreshold.collectAsState()
|
||||
val feedUseDedicatedFetch by
|
||||
viewModel.feedUseDedicatedFetch.collectAsState()
|
||||
val feedFetchChannelTabs by
|
||||
viewModel.feedFetchChannelTabs.collectAsState()
|
||||
|
||||
ContentSettingsContent(
|
||||
appLanguage = appLanguage,
|
||||
contentLanguage = contentLanguage,
|
||||
contentCountry = contentCountry,
|
||||
showChannelTabs = showChannelTabs,
|
||||
showSearchSuggestions = showSearchSuggestions,
|
||||
imageQuality = imageQuality,
|
||||
showAgeRestricted = showAgeRestricted,
|
||||
youtubeRestrictedMode = youtubeRestrictedMode,
|
||||
showComments = showComments,
|
||||
showNextVideo = showNextVideo,
|
||||
showDescription = showDescription,
|
||||
showMetaInfo = showMetaInfo,
|
||||
feedUpdateThreshold = feedUpdateThreshold,
|
||||
feedUseDedicatedFetch = feedUseDedicatedFetch,
|
||||
feedFetchChannelTabs = feedFetchChannelTabs,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onSetAppLanguage = viewModel::setAppLanguage,
|
||||
onSetContentLanguage = viewModel::setContentLanguage,
|
||||
onSetContentCountry = viewModel::setContentCountry,
|
||||
onOpenMainPageTabs = viewModel::openMainPageTabs,
|
||||
onSetShowChannelTabs = viewModel::setShowChannelTabs,
|
||||
onOpenPeertubeInstances = viewModel::openPeertubeInstances,
|
||||
onToggleShowAgeRestricted = viewModel::toggleShowAgeRestricted,
|
||||
onToggleYoutubeRestrictedMode =
|
||||
viewModel::toggleYoutubeRestrictedMode,
|
||||
onSetShowSearchSuggestions = viewModel::setShowSearchSuggestions,
|
||||
onSetImageQuality = viewModel::setImageQuality,
|
||||
onToggleShowComments = viewModel::toggleShowComments,
|
||||
onToggleShowNextVideo = viewModel::toggleShowNextVideo,
|
||||
onToggleShowDescription = viewModel::toggleShowDescription,
|
||||
onToggleShowMetaInfo = viewModel::toggleShowMetaInfo,
|
||||
onSetFeedUpdateThreshold = viewModel::setFeedUpdateThreshold,
|
||||
onToggleFeedUseDedicatedFetch =
|
||||
viewModel::toggleFeedUseDedicatedFetch,
|
||||
onSetFeedFetchChannelTabs = viewModel::setFeedFetchChannelTabs
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
private fun ContentSettingsContent(
|
||||
appLanguage: String,
|
||||
contentLanguage: String,
|
||||
contentCountry: String,
|
||||
showChannelTabs: Set<String>,
|
||||
showSearchSuggestions: Set<String>,
|
||||
imageQuality: String,
|
||||
showAgeRestricted: Boolean,
|
||||
youtubeRestrictedMode: Boolean,
|
||||
showComments: Boolean,
|
||||
showNextVideo: Boolean,
|
||||
showDescription: Boolean,
|
||||
showMetaInfo: Boolean,
|
||||
feedUpdateThreshold: String,
|
||||
feedUseDedicatedFetch: Boolean,
|
||||
feedFetchChannelTabs: Set<String>,
|
||||
onNavigateUp: () -> Unit,
|
||||
onSetAppLanguage: (String) -> Unit,
|
||||
onSetContentLanguage: (String) -> Unit,
|
||||
onSetContentCountry: (String) -> Unit,
|
||||
onOpenMainPageTabs: () -> Unit,
|
||||
onSetShowChannelTabs: (Set<String>) -> Unit,
|
||||
onOpenPeertubeInstances: () -> Unit,
|
||||
onToggleShowAgeRestricted: (Boolean) -> Unit,
|
||||
onToggleYoutubeRestrictedMode: (Boolean) -> Unit,
|
||||
onSetShowSearchSuggestions: (Set<String>) -> Unit,
|
||||
onSetImageQuality: (String) -> Unit,
|
||||
onToggleShowComments: (Boolean) -> Unit,
|
||||
onToggleShowNextVideo: (Boolean) -> Unit,
|
||||
onToggleShowDescription: (Boolean) -> Unit,
|
||||
onToggleShowMetaInfo: (Boolean) -> Unit,
|
||||
onSetFeedUpdateThreshold: (String) -> Unit,
|
||||
onToggleFeedUseDedicatedFetch: (Boolean) -> Unit,
|
||||
onSetFeedFetchChannelTabs: (Set<String>) -> Unit
|
||||
) {
|
||||
val languageOptions = languageOptions()
|
||||
val countryOptions = countryOptions()
|
||||
val channelTabOptions = channelTabOptions()
|
||||
val suggestionOptions = suggestionOptions()
|
||||
val imageQualityOptions = imageQualityOptions()
|
||||
val thresholdOptions = thresholdOptions()
|
||||
val fetchTabOptions = fetchTabOptions()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.content),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.app_language_title),
|
||||
options = languageOptions,
|
||||
selectedValue = appLanguage,
|
||||
onValueChange = onSetAppLanguage
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.content_language_title),
|
||||
options = languageOptions,
|
||||
selectedValue = contentLanguage,
|
||||
onValueChange = onSetContentLanguage
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.default_content_country_title),
|
||||
options = countryOptions,
|
||||
selectedValue = contentCountry,
|
||||
onValueChange = onSetContentCountry
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.main_page_content),
|
||||
summary = stringResource(Res.string.main_page_content_summary),
|
||||
onClick = onOpenMainPageTabs
|
||||
)
|
||||
MultiSelectListPreference(
|
||||
title = stringResource(Res.string.show_channel_tabs),
|
||||
summary = stringResource(Res.string.show_channel_tabs_summary),
|
||||
options = channelTabOptions,
|
||||
selectedValues = showChannelTabs,
|
||||
onValuesChange = onSetShowChannelTabs
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.peertube_instance_url_title),
|
||||
summary = stringResource(Res.string.peertube_instance_url_summary),
|
||||
onClick = onOpenPeertubeInstances
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_age_restricted_content_title),
|
||||
summary = stringResource(Res.string.show_age_restricted_content_summary),
|
||||
isChecked = showAgeRestricted,
|
||||
onCheckedChange = onToggleShowAgeRestricted
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.youtube_restricted_mode_enabled_title),
|
||||
summary = stringResource(Res.string.youtube_restricted_mode_enabled_summary),
|
||||
isChecked = youtubeRestrictedMode,
|
||||
onCheckedChange = onToggleYoutubeRestrictedMode
|
||||
)
|
||||
MultiSelectListPreference(
|
||||
title = stringResource(Res.string.show_search_suggestions_title),
|
||||
summary = stringResource(Res.string.show_search_suggestions_summary),
|
||||
options = suggestionOptions,
|
||||
selectedValues = showSearchSuggestions,
|
||||
onValuesChange = onSetShowSearchSuggestions
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.image_quality_title),
|
||||
summary = stringResource(Res.string.image_quality_summary),
|
||||
options = imageQualityOptions,
|
||||
selectedValue = imageQuality,
|
||||
onValueChange = onSetImageQuality
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_comments_title),
|
||||
summary = stringResource(Res.string.show_comments_summary),
|
||||
isChecked = showComments,
|
||||
onCheckedChange = onToggleShowComments
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_next_and_similar_title),
|
||||
isChecked = showNextVideo,
|
||||
onCheckedChange = onToggleShowNextVideo
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_description_title),
|
||||
summary = stringResource(Res.string.show_description_summary),
|
||||
isChecked = showDescription,
|
||||
onCheckedChange = onToggleShowDescription
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_meta_info_title),
|
||||
summary = stringResource(Res.string.show_meta_info_summary),
|
||||
isChecked = showMetaInfo,
|
||||
onCheckedChange = onToggleShowMetaInfo
|
||||
)
|
||||
|
||||
PreferenceCategoryHeader(
|
||||
title = stringResource(Res.string.settings_category_feed_title)
|
||||
)
|
||||
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.feed_update_threshold_title),
|
||||
summary = stringResource(Res.string.feed_update_threshold_summary),
|
||||
options = thresholdOptions,
|
||||
selectedValue = feedUpdateThreshold,
|
||||
onValueChange = onSetFeedUpdateThreshold
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.feed_use_dedicated_fetch_method_title),
|
||||
summary = stringResource(Res.string.feed_use_dedicated_fetch_method_summary),
|
||||
isChecked = feedUseDedicatedFetch,
|
||||
onCheckedChange = onToggleFeedUseDedicatedFetch
|
||||
)
|
||||
MultiSelectListPreference(
|
||||
title = stringResource(Res.string.feed_fetch_channel_tabs),
|
||||
summary = stringResource(Res.string.feed_fetch_channel_tabs_summary),
|
||||
options = fetchTabOptions,
|
||||
selectedValues = feedFetchChannelTabs,
|
||||
onValuesChange = onSetFeedFetchChannelTabs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Option lists (curated; see TODO at file top)
|
||||
|
||||
@Composable
|
||||
private fun languageOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(LOCALIZATION_SYSTEM, stringResource(Res.string.language_system)),
|
||||
ListPreferenceOption("en", stringResource(Res.string.language_en)),
|
||||
ListPreferenceOption("es", stringResource(Res.string.language_es)),
|
||||
ListPreferenceOption("fr", stringResource(Res.string.language_fr)),
|
||||
ListPreferenceOption("de", stringResource(Res.string.language_de)),
|
||||
ListPreferenceOption("pt", stringResource(Res.string.language_pt)),
|
||||
ListPreferenceOption("ru", stringResource(Res.string.language_ru)),
|
||||
ListPreferenceOption("zh", stringResource(Res.string.language_zh)),
|
||||
ListPreferenceOption("ja", stringResource(Res.string.language_ja)),
|
||||
ListPreferenceOption("ko", stringResource(Res.string.language_ko)),
|
||||
ListPreferenceOption("it", stringResource(Res.string.language_it)),
|
||||
ListPreferenceOption("nl", stringResource(Res.string.language_nl)),
|
||||
ListPreferenceOption("pl", stringResource(Res.string.language_pl)),
|
||||
ListPreferenceOption("tr", stringResource(Res.string.language_tr)),
|
||||
ListPreferenceOption("ar", stringResource(Res.string.language_ar)),
|
||||
ListPreferenceOption("hi", stringResource(Res.string.language_hi))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun countryOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(LOCALIZATION_SYSTEM, stringResource(Res.string.country_system)),
|
||||
ListPreferenceOption("US", stringResource(Res.string.country_us)),
|
||||
ListPreferenceOption("GB", stringResource(Res.string.country_gb)),
|
||||
ListPreferenceOption("DE", stringResource(Res.string.country_de)),
|
||||
ListPreferenceOption("FR", stringResource(Res.string.country_fr)),
|
||||
ListPreferenceOption("ES", stringResource(Res.string.country_es)),
|
||||
ListPreferenceOption("IT", stringResource(Res.string.country_it)),
|
||||
ListPreferenceOption("BR", stringResource(Res.string.country_br)),
|
||||
ListPreferenceOption("JP", stringResource(Res.string.country_jp)),
|
||||
ListPreferenceOption("KR", stringResource(Res.string.country_kr)),
|
||||
ListPreferenceOption("IN", stringResource(Res.string.country_in)),
|
||||
ListPreferenceOption("RU", stringResource(Res.string.country_ru)),
|
||||
ListPreferenceOption("CN", stringResource(Res.string.country_cn)),
|
||||
ListPreferenceOption("AU", stringResource(Res.string.country_au)),
|
||||
ListPreferenceOption("CA", stringResource(Res.string.country_ca)),
|
||||
ListPreferenceOption("MX", stringResource(Res.string.country_mx))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun channelTabOptions(): List<MultiSelectListPreferenceOption> =
|
||||
listOf(
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_VIDEOS, stringResource(Res.string.channel_tab_videos)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_TRACKS, stringResource(Res.string.channel_tab_tracks)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_SHORTS, stringResource(Res.string.channel_tab_shorts)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_LIVESTREAMS, stringResource(Res.string.channel_tab_livestreams)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_CHANNELS, stringResource(Res.string.channel_tab_channels)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_PLAYLISTS, stringResource(Res.string.channel_tab_playlists)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_ALBUMS, stringResource(Res.string.channel_tab_albums)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_LIKES, stringResource(Res.string.channel_tab_likes)),
|
||||
MultiSelectListPreferenceOption(CHANNEL_TAB_ABOUT, stringResource(Res.string.channel_tab_about))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun suggestionOptions(): List<MultiSelectListPreferenceOption> =
|
||||
listOf(
|
||||
MultiSelectListPreferenceOption(SUGGESTIONS_LOCAL, stringResource(Res.string.local_search_suggestions)),
|
||||
MultiSelectListPreferenceOption(SUGGESTIONS_REMOTE, stringResource(Res.string.remote_search_suggestions))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun imageQualityOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(IMAGE_QUALITY_LOW, stringResource(Res.string.image_quality_low)),
|
||||
ListPreferenceOption(IMAGE_QUALITY_MEDIUM, stringResource(Res.string.image_quality_medium)),
|
||||
ListPreferenceOption(IMAGE_QUALITY_HIGH, stringResource(Res.string.image_quality_high))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun thresholdOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(THRESHOLD_IMMEDIATE, stringResource(Res.string.threshold_immediate)),
|
||||
ListPreferenceOption(THRESHOLD_5_MIN, stringResource(Res.string.threshold_5_min)),
|
||||
ListPreferenceOption(THRESHOLD_15_MIN, stringResource(Res.string.threshold_15_min)),
|
||||
ListPreferenceOption(THRESHOLD_1_HOUR, stringResource(Res.string.threshold_1_hour)),
|
||||
ListPreferenceOption(THRESHOLD_6_HOURS, stringResource(Res.string.threshold_6_hours)),
|
||||
ListPreferenceOption(THRESHOLD_12_HOURS, stringResource(Res.string.threshold_12_hours)),
|
||||
ListPreferenceOption(THRESHOLD_1_DAY, stringResource(Res.string.threshold_1_day))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun fetchTabOptions(): List<MultiSelectListPreferenceOption> =
|
||||
listOf(
|
||||
MultiSelectListPreferenceOption(FETCH_TAB_VIDEOS, stringResource(Res.string.channel_tab_videos)),
|
||||
MultiSelectListPreferenceOption(FETCH_TAB_TRACKS, stringResource(Res.string.channel_tab_tracks)),
|
||||
MultiSelectListPreferenceOption(FETCH_TAB_SHORTS, stringResource(Res.string.channel_tab_shorts)),
|
||||
MultiSelectListPreferenceOption(FETCH_TAB_LIVESTREAMS, stringResource(Res.string.channel_tab_livestreams)),
|
||||
MultiSelectListPreferenceOption(FETCH_TAB_LIKES, stringResource(Res.string.channel_tab_likes))
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ContentSettingsScreenPreview() {
|
||||
ContentSettingsContent(
|
||||
appLanguage = LOCALIZATION_SYSTEM,
|
||||
contentLanguage = LOCALIZATION_SYSTEM,
|
||||
contentCountry = LOCALIZATION_SYSTEM,
|
||||
showChannelTabs = setOf(CHANNEL_TAB_VIDEOS, CHANNEL_TAB_TRACKS, CHANNEL_TAB_PLAYLISTS),
|
||||
showSearchSuggestions = setOf(SUGGESTIONS_LOCAL, SUGGESTIONS_REMOTE),
|
||||
imageQuality = IMAGE_QUALITY_MEDIUM,
|
||||
showAgeRestricted = false,
|
||||
youtubeRestrictedMode = false,
|
||||
showComments = true,
|
||||
showNextVideo = true,
|
||||
showDescription = true,
|
||||
showMetaInfo = true,
|
||||
feedUpdateThreshold = THRESHOLD_5_MIN,
|
||||
feedUseDedicatedFetch = false,
|
||||
feedFetchChannelTabs = setOf(FETCH_TAB_VIDEOS, FETCH_TAB_LIVESTREAMS),
|
||||
onNavigateUp = {},
|
||||
onSetAppLanguage = {},
|
||||
onSetContentLanguage = {},
|
||||
onSetContentCountry = {},
|
||||
onOpenMainPageTabs = {},
|
||||
onSetShowChannelTabs = {},
|
||||
onOpenPeertubeInstances = {},
|
||||
onToggleShowAgeRestricted = {},
|
||||
onToggleYoutubeRestrictedMode = {},
|
||||
onSetShowSearchSuggestions = {},
|
||||
onSetImageQuality = {},
|
||||
onToggleShowComments = {},
|
||||
onToggleShowNextVideo = {},
|
||||
onToggleShowDescription = {},
|
||||
onToggleShowMetaInfo = {},
|
||||
onSetFeedUpdateThreshold = {},
|
||||
onToggleFeedUseDedicatedFetch = {},
|
||||
onSetFeedFetchChannelTabs = {}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.debug
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.SettingsViewModel
|
||||
import net.newpipe.app.viewmodel.settings.debug.DebugSettingsViewModel
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.check_new_streams
|
||||
import newpipe.shared.generated.resources.crash_the_app
|
||||
import newpipe.shared.generated.resources.create_error_notification
|
||||
import newpipe.shared.generated.resources.enable_disposed_exceptions_summary
|
||||
import newpipe.shared.generated.resources.enable_disposed_exceptions_title
|
||||
import newpipe.shared.generated.resources.enable_leak_canary_summary
|
||||
import newpipe.shared.generated.resources.leak_canary_not_available
|
||||
import newpipe.shared.generated.resources.leakcanary
|
||||
import newpipe.shared.generated.resources.settings_category_debug_title
|
||||
import newpipe.shared.generated.resources.settings_layout_redesign
|
||||
import newpipe.shared.generated.resources.show_crash_the_player_summary
|
||||
import newpipe.shared.generated.resources.show_crash_the_player_title
|
||||
import newpipe.shared.generated.resources.show_error_snackbar
|
||||
import newpipe.shared.generated.resources.show_memory_leaks
|
||||
import newpipe.shared.generated.resources.show_original_time_ago_summary
|
||||
import newpipe.shared.generated.resources.show_original_time_ago_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun DebugScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
debugVm: DebugSettingsViewModel = koinViewModel(),
|
||||
settingsVm: SettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val settingsLayoutRedesign by settingsVm.settingsLayoutRedesign.collectAsState()
|
||||
val allowHeapDumping by debugVm.allowHeapDumping.collectAsState()
|
||||
val allowDisposedExceptions by debugVm.allowDisposedExceptions.collectAsState()
|
||||
val showOriginalTimeAgo by debugVm.showOriginalTimeAgo.collectAsState()
|
||||
val showCrashThePlayer by debugVm.showCrashThePlayer.collectAsState()
|
||||
|
||||
DebugScreenContent(
|
||||
settingsLayoutRedesign = settingsLayoutRedesign,
|
||||
isLeakCanaryAvailable = debugVm.isLeakCanaryAvailable,
|
||||
allowHeapDumping = allowHeapDumping,
|
||||
allowDisposedExceptions = allowDisposedExceptions,
|
||||
showOriginalTimeAgo = showOriginalTimeAgo,
|
||||
showCrashThePlayer = showCrashThePlayer,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onToggleHeapDumping = debugVm::toggleAllowHeapDumping,
|
||||
onShowMemoryLeaks = debugVm::showMemoryLeaks,
|
||||
onToggleDisposedExceptions = debugVm::toggleAllowDisposedExceptions,
|
||||
onToggleOriginalTimeAgo = debugVm::toggleShowOriginalTimeAgo,
|
||||
onToggleCrashThePlayer = debugVm::toggleShowCrashThePlayer,
|
||||
onCheckNewStreams = debugVm::checkNewStreams,
|
||||
onCrashTheApp = debugVm::crashTheApp,
|
||||
onShowErrorSnackbar = debugVm::showErrorSnackbar,
|
||||
onCreateErrorNotification = debugVm::createErrorNotification,
|
||||
onToggleSettingsLayoutRedesign = settingsVm::toggleSettingsLayoutRedesign
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugScreenContent(
|
||||
settingsLayoutRedesign: Boolean,
|
||||
isLeakCanaryAvailable: Boolean,
|
||||
allowHeapDumping: Boolean,
|
||||
allowDisposedExceptions: Boolean,
|
||||
showOriginalTimeAgo: Boolean,
|
||||
showCrashThePlayer: Boolean,
|
||||
onNavigateUp: () -> Unit,
|
||||
onToggleHeapDumping: (Boolean) -> Unit,
|
||||
onShowMemoryLeaks: () -> Unit,
|
||||
onToggleDisposedExceptions: (Boolean) -> Unit,
|
||||
onToggleOriginalTimeAgo: (Boolean) -> Unit,
|
||||
onToggleCrashThePlayer: (Boolean) -> Unit,
|
||||
onCheckNewStreams: () -> Unit,
|
||||
onCrashTheApp: () -> Unit,
|
||||
onShowErrorSnackbar: () -> Unit,
|
||||
onCreateErrorNotification: () -> Unit,
|
||||
onToggleSettingsLayoutRedesign: (Boolean) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings_category_debug_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.leakcanary),
|
||||
summary = stringResource(
|
||||
if (isLeakCanaryAvailable)
|
||||
Res.string.enable_leak_canary_summary
|
||||
else Res.string.leak_canary_not_available
|
||||
),
|
||||
isChecked = allowHeapDumping,
|
||||
onCheckedChange = onToggleHeapDumping,
|
||||
enabled = isLeakCanaryAvailable
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.show_memory_leaks),
|
||||
onClick = onShowMemoryLeaks,
|
||||
enabled = isLeakCanaryAvailable
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.enable_disposed_exceptions_title),
|
||||
summary = stringResource(Res.string.enable_disposed_exceptions_summary),
|
||||
isChecked = allowDisposedExceptions,
|
||||
onCheckedChange = onToggleDisposedExceptions
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_original_time_ago_title),
|
||||
summary = stringResource(Res.string.show_original_time_ago_summary),
|
||||
isChecked = showOriginalTimeAgo,
|
||||
onCheckedChange = onToggleOriginalTimeAgo
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_crash_the_player_title),
|
||||
summary = stringResource(Res.string.show_crash_the_player_summary),
|
||||
isChecked = showCrashThePlayer,
|
||||
onCheckedChange = onToggleCrashThePlayer
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.check_new_streams),
|
||||
onClick = onCheckNewStreams
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.crash_the_app),
|
||||
onClick = onCrashTheApp
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.show_error_snackbar),
|
||||
onClick = onShowErrorSnackbar
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.create_error_notification),
|
||||
onClick = onCreateErrorNotification
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.settings_layout_redesign),
|
||||
isChecked = settingsLayoutRedesign,
|
||||
onCheckedChange = onToggleSettingsLayoutRedesign
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DebugScreenPreview() {
|
||||
DebugScreenContent(
|
||||
settingsLayoutRedesign = true,
|
||||
isLeakCanaryAvailable = true,
|
||||
allowHeapDumping = false,
|
||||
allowDisposedExceptions = false,
|
||||
showOriginalTimeAgo = false,
|
||||
showCrashThePlayer = false,
|
||||
onNavigateUp = {},
|
||||
onToggleHeapDumping = {},
|
||||
onShowMemoryLeaks = {},
|
||||
onToggleDisposedExceptions = {},
|
||||
onToggleOriginalTimeAgo = {},
|
||||
onToggleCrashThePlayer = {},
|
||||
onCheckNewStreams = {},
|
||||
onCrashTheApp = {},
|
||||
onShowErrorSnackbar = {},
|
||||
onCreateErrorNotification = {},
|
||||
onToggleSettingsLayoutRedesign = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DebugScreenNoLeakCanaryPreview() {
|
||||
DebugScreenContent(
|
||||
settingsLayoutRedesign = false,
|
||||
isLeakCanaryAvailable = false,
|
||||
allowHeapDumping = false,
|
||||
allowDisposedExceptions = false,
|
||||
showOriginalTimeAgo = false,
|
||||
showCrashThePlayer = false,
|
||||
onNavigateUp = {},
|
||||
onToggleHeapDumping = {},
|
||||
onShowMemoryLeaks = {},
|
||||
onToggleDisposedExceptions = {},
|
||||
onToggleOriginalTimeAgo = {},
|
||||
onToggleCrashThePlayer = {},
|
||||
onCheckNewStreams = {},
|
||||
onCrashTheApp = {},
|
||||
onShowErrorSnackbar = {},
|
||||
onCreateErrorNotification = {},
|
||||
onToggleSettingsLayoutRedesign = {}
|
||||
)
|
||||
}
|
||||
+122
@@ -1,2 +1,124 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.download
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.download.DownloadSettingsViewModel
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.download_path_audio_title
|
||||
import newpipe.shared.generated.resources.download_path_title
|
||||
import newpipe.shared.generated.resources.downloads_storage_ask_summary
|
||||
import newpipe.shared.generated.resources.downloads_storage_ask_title
|
||||
import newpipe.shared.generated.resources.downloads_storage_use_saf_summary
|
||||
import newpipe.shared.generated.resources.downloads_storage_use_saf_title
|
||||
import newpipe.shared.generated.resources.settings_category_downloads_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun DownloadSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
vm: DownloadSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val storageAsk by vm.storageAsk.collectAsState()
|
||||
val useSaf by vm.useSaf.collectAsState()
|
||||
val videoPath by vm.videoPath.collectAsState()
|
||||
val audioPath by vm.audioPath.collectAsState()
|
||||
|
||||
DownloadSettingsContent(
|
||||
storageAsk = storageAsk,
|
||||
useSaf = useSaf,
|
||||
videoPath = videoPath,
|
||||
audioPath = audioPath,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onToggleStorageAsk = vm::toggleStorageAsk,
|
||||
onToggleUseSaf = vm::toggleUseSaf,
|
||||
onPickVideoPath = vm::pickVideoPath,
|
||||
onPickAudioPath = vm::pickAudioPath
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadSettingsContent(
|
||||
storageAsk: Boolean,
|
||||
useSaf: Boolean,
|
||||
videoPath: String,
|
||||
audioPath: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onToggleStorageAsk: (Boolean) -> Unit,
|
||||
onToggleUseSaf: (Boolean) -> Unit,
|
||||
onPickVideoPath: () -> Unit,
|
||||
onPickAudioPath: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title =
|
||||
stringResource(Res.string.settings_category_downloads_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.downloads_storage_ask_title),
|
||||
summary = stringResource(Res.string.downloads_storage_ask_summary),
|
||||
isChecked = storageAsk,
|
||||
onCheckedChange = onToggleStorageAsk
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.downloads_storage_use_saf_title),
|
||||
summary = stringResource(Res.string.downloads_storage_use_saf_summary),
|
||||
isChecked = useSaf,
|
||||
onCheckedChange = onToggleUseSaf
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.download_path_title),
|
||||
summary = videoPath.takeIf { it.isNotEmpty() },
|
||||
onClick = onPickVideoPath
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.download_path_audio_title),
|
||||
summary = audioPath.takeIf { it.isNotEmpty() },
|
||||
onClick = onPickAudioPath
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DownloadSettingsScreenPreview() {
|
||||
DownloadSettingsContent(
|
||||
storageAsk = false,
|
||||
useSaf = true,
|
||||
videoPath = "content://com.android.externalstorage/tree/primary%3AMovies",
|
||||
audioPath = "",
|
||||
onNavigateUp = {},
|
||||
onToggleStorageAsk = {},
|
||||
onToggleUseSaf = {},
|
||||
onPickVideoPath = {},
|
||||
onPickAudioPath = {}
|
||||
)
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.exoplayer
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.ListPreference
|
||||
import net.newpipe.app.composable.ListPreferenceOption
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.exoplayer.ExoPlayerSettingsViewModel
|
||||
import net.newpipe.app.viewmodel.settings.exoplayer.PROGRESSIVE_LOAD_INTERVAL_1
|
||||
import net.newpipe.app.viewmodel.settings.exoplayer.PROGRESSIVE_LOAD_INTERVAL_16
|
||||
import net.newpipe.app.viewmodel.settings.exoplayer.PROGRESSIVE_LOAD_INTERVAL_256
|
||||
import net.newpipe.app.viewmodel.settings.exoplayer.PROGRESSIVE_LOAD_INTERVAL_64
|
||||
import net.newpipe.app.viewmodel.settings.exoplayer.PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.always_use_exoplayer_set_output_surface_workaround_summary
|
||||
import newpipe.shared.generated.resources.always_use_exoplayer_set_output_surface_workaround_title
|
||||
import newpipe.shared.generated.resources.disable_media_tunneling_summary
|
||||
import newpipe.shared.generated.resources.disable_media_tunneling_title
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_1_kib
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_16_kib
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_256_kib
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_64_kib
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_exoplayer_default
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_summary
|
||||
import newpipe.shared.generated.resources.progressive_load_interval_title
|
||||
import newpipe.shared.generated.resources.settings_category_exoplayer_title
|
||||
import newpipe.shared.generated.resources.use_exoplayer_decoder_fallback_summary
|
||||
import newpipe.shared.generated.resources.use_exoplayer_decoder_fallback_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun ExoPlayerSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: ExoPlayerSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val progressiveLoadInterval by viewModel.progressiveLoadInterval.collectAsState()
|
||||
val useDecoderFallback by viewModel.useDecoderFallback.collectAsState()
|
||||
val disableMediaTunneling by viewModel.disableMediaTunneling.collectAsState()
|
||||
val alwaysUseSetOutputSurfaceWorkaround by
|
||||
viewModel.alwaysUseSetOutputSurfaceWorkaround.collectAsState()
|
||||
|
||||
ExoPlayerSettingsContent(
|
||||
progressiveLoadInterval = progressiveLoadInterval,
|
||||
useDecoderFallback = useDecoderFallback,
|
||||
disableMediaTunneling = disableMediaTunneling,
|
||||
alwaysUseSetOutputSurfaceWorkaround = alwaysUseSetOutputSurfaceWorkaround,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onSetProgressiveLoadInterval = viewModel::setProgressiveLoadInterval,
|
||||
onToggleUseDecoderFallback = viewModel::toggleUseDecoderFallback,
|
||||
onToggleDisableMediaTunneling = viewModel::toggleDisableMediaTunneling,
|
||||
onToggleAlwaysUseSetOutputSurfaceWorkaround =
|
||||
viewModel::toggleAlwaysUseSetOutputSurfaceWorkaround
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExoPlayerSettingsContent(
|
||||
progressiveLoadInterval: String,
|
||||
useDecoderFallback: Boolean,
|
||||
disableMediaTunneling: Boolean,
|
||||
alwaysUseSetOutputSurfaceWorkaround: Boolean,
|
||||
onNavigateUp: () -> Unit,
|
||||
onSetProgressiveLoadInterval: (String) -> Unit,
|
||||
onToggleUseDecoderFallback: (Boolean) -> Unit,
|
||||
onToggleDisableMediaTunneling: (Boolean) -> Unit,
|
||||
onToggleAlwaysUseSetOutputSurfaceWorkaround: (Boolean) -> Unit
|
||||
) {
|
||||
val intervalOptions = progressiveLoadIntervalOptions()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings_category_exoplayer_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.progressive_load_interval_title),
|
||||
options = intervalOptions,
|
||||
selectedValue = progressiveLoadInterval,
|
||||
onValueChange = onSetProgressiveLoadInterval
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_exoplayer_decoder_fallback_title),
|
||||
summary = stringResource(Res.string.use_exoplayer_decoder_fallback_summary),
|
||||
isChecked = useDecoderFallback,
|
||||
onCheckedChange = onToggleUseDecoderFallback
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.disable_media_tunneling_title),
|
||||
summary = stringResource(Res.string.disable_media_tunneling_summary),
|
||||
isChecked = disableMediaTunneling,
|
||||
onCheckedChange = onToggleDisableMediaTunneling
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(
|
||||
Res.string.always_use_exoplayer_set_output_surface_workaround_title
|
||||
),
|
||||
summary = stringResource(
|
||||
Res.string.always_use_exoplayer_set_output_surface_workaround_summary
|
||||
),
|
||||
isChecked = alwaysUseSetOutputSurfaceWorkaround,
|
||||
onCheckedChange = onToggleAlwaysUseSetOutputSurfaceWorkaround
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun progressiveLoadIntervalOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(
|
||||
PROGRESSIVE_LOAD_INTERVAL_1,
|
||||
stringResource(Res.string.progressive_load_interval_1_kib)
|
||||
),
|
||||
ListPreferenceOption(
|
||||
PROGRESSIVE_LOAD_INTERVAL_16,
|
||||
stringResource(Res.string.progressive_load_interval_16_kib)
|
||||
),
|
||||
ListPreferenceOption(
|
||||
PROGRESSIVE_LOAD_INTERVAL_64,
|
||||
stringResource(Res.string.progressive_load_interval_64_kib)
|
||||
),
|
||||
ListPreferenceOption(
|
||||
PROGRESSIVE_LOAD_INTERVAL_256,
|
||||
stringResource(Res.string.progressive_load_interval_256_kib)
|
||||
),
|
||||
ListPreferenceOption(
|
||||
PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT,
|
||||
stringResource(Res.string.progressive_load_interval_exoplayer_default)
|
||||
)
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ExoPlayerSettingsScreenPreview() {
|
||||
ExoPlayerSettingsContent(
|
||||
progressiveLoadInterval = PROGRESSIVE_LOAD_INTERVAL_64,
|
||||
useDecoderFallback = false,
|
||||
disableMediaTunneling = false,
|
||||
alwaysUseSetOutputSurfaceWorkaround = false,
|
||||
onNavigateUp = {},
|
||||
onSetProgressiveLoadInterval = {},
|
||||
onToggleUseDecoderFallback = {},
|
||||
onToggleDisableMediaTunneling = {},
|
||||
onToggleAlwaysUseSetOutputSurfaceWorkaround = {}
|
||||
)
|
||||
}
|
||||
+261
@@ -1,2 +1,263 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.history
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.ConfirmDialog
|
||||
import net.newpipe.app.composable.PreferenceCategoryHeader
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.history.HistorySettingsViewModel
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.cancel
|
||||
import newpipe.shared.generated.resources.clear_cookie_summary
|
||||
import newpipe.shared.generated.resources.clear_cookie_title
|
||||
import newpipe.shared.generated.resources.clear_playback_states_summary
|
||||
import newpipe.shared.generated.resources.clear_playback_states_title
|
||||
import newpipe.shared.generated.resources.clear_search_history_summary
|
||||
import newpipe.shared.generated.resources.clear_search_history_title
|
||||
import newpipe.shared.generated.resources.clear_views_history_summary
|
||||
import newpipe.shared.generated.resources.clear_views_history_title
|
||||
import newpipe.shared.generated.resources.delete
|
||||
import newpipe.shared.generated.resources.enable_playback_resume_summary
|
||||
import newpipe.shared.generated.resources.enable_playback_resume_title
|
||||
import newpipe.shared.generated.resources.enable_playback_state_lists_summary
|
||||
import newpipe.shared.generated.resources.enable_playback_state_lists_title
|
||||
import newpipe.shared.generated.resources.enable_search_history_summary
|
||||
import newpipe.shared.generated.resources.enable_search_history_title
|
||||
import newpipe.shared.generated.resources.enable_watch_history_summary
|
||||
import newpipe.shared.generated.resources.enable_watch_history_title
|
||||
import newpipe.shared.generated.resources.metadata_cache_wipe_summary
|
||||
import newpipe.shared.generated.resources.metadata_cache_wipe_title
|
||||
import newpipe.shared.generated.resources.settings_category_clear_data_title
|
||||
import newpipe.shared.generated.resources.settings_category_history_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
private enum class PendingDelete { WATCH, PLAYBACK_STATES, SEARCH }
|
||||
|
||||
@Composable
|
||||
fun HistorySettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: HistorySettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val watchHistory by viewModel.watchHistory.collectAsState()
|
||||
val playbackResume by viewModel.playbackResume.collectAsState()
|
||||
val playbackStateLists by viewModel.playbackStateLists.collectAsState()
|
||||
val searchHistory by viewModel.searchHistory.collectAsState()
|
||||
val hasCookies by viewModel.hasRecaptchaCookies.collectAsState()
|
||||
|
||||
HistorySettingsContent(
|
||||
watchHistory = watchHistory,
|
||||
playbackResume = playbackResume,
|
||||
playbackStateLists = playbackStateLists,
|
||||
searchHistory = searchHistory,
|
||||
hasRecaptchaCookies = hasCookies,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onToggleWatchHistory = viewModel::toggleWatchHistory,
|
||||
onTogglePlaybackResume = viewModel::togglePlaybackResume,
|
||||
onTogglePlaybackStateLists = viewModel::togglePlaybackStateLists,
|
||||
onToggleSearchHistory = viewModel::toggleSearchHistory,
|
||||
onWipeMetadataCache = viewModel::wipeMetadataCache,
|
||||
onDeleteWatchHistory = viewModel::deleteWatchHistory,
|
||||
onDeletePlaybackStates = viewModel::deletePlaybackStates,
|
||||
onDeleteSearchHistory = viewModel::deleteSearchHistory,
|
||||
onClearCookies = viewModel::clearRecaptchaCookies
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistorySettingsContent(
|
||||
watchHistory: Boolean,
|
||||
playbackResume: Boolean,
|
||||
playbackStateLists: Boolean,
|
||||
searchHistory: Boolean,
|
||||
hasRecaptchaCookies: Boolean,
|
||||
onNavigateUp: () -> Unit,
|
||||
onToggleWatchHistory: (Boolean) -> Unit,
|
||||
onTogglePlaybackResume: (Boolean) -> Unit,
|
||||
onTogglePlaybackStateLists: (Boolean) -> Unit,
|
||||
onToggleSearchHistory: (Boolean) -> Unit,
|
||||
onWipeMetadataCache: () -> Unit,
|
||||
onDeleteWatchHistory: () -> Unit,
|
||||
onDeletePlaybackStates: () -> Unit,
|
||||
onDeleteSearchHistory: () -> Unit,
|
||||
onClearCookies: () -> Unit
|
||||
) {
|
||||
var pending by rememberSaveable { mutableStateOf<PendingDelete?>(null)
|
||||
}
|
||||
|
||||
pending?.let { which ->
|
||||
val (title, message, action) = when (which) {
|
||||
PendingDelete.WATCH -> Triple(
|
||||
stringResource(Res.string.clear_views_history_title),
|
||||
stringResource(Res.string.clear_views_history_summary),
|
||||
onDeleteWatchHistory
|
||||
)
|
||||
PendingDelete.PLAYBACK_STATES -> Triple(
|
||||
stringResource(Res.string.clear_playback_states_title),
|
||||
stringResource(Res.string.clear_playback_states_summary),
|
||||
onDeletePlaybackStates
|
||||
)
|
||||
PendingDelete.SEARCH -> Triple(
|
||||
stringResource(Res.string.clear_search_history_title),
|
||||
stringResource(Res.string.clear_search_history_summary),
|
||||
onDeleteSearchHistory
|
||||
)
|
||||
}
|
||||
ConfirmDialog(
|
||||
title = title,
|
||||
message = message,
|
||||
confirmLabel = stringResource(Res.string.delete),
|
||||
dismissLabel = stringResource(Res.string.cancel),
|
||||
onConfirm = {
|
||||
action()
|
||||
pending = null
|
||||
},
|
||||
onDismiss = { pending = null }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings_category_history_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.enable_watch_history_title),
|
||||
summary = stringResource(Res.string.enable_watch_history_summary),
|
||||
isChecked = watchHistory,
|
||||
onCheckedChange = onToggleWatchHistory
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.enable_playback_resume_title),
|
||||
summary = stringResource(Res.string.enable_playback_resume_summary),
|
||||
isChecked = playbackResume,
|
||||
onCheckedChange = onTogglePlaybackResume,
|
||||
enabled = watchHistory
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.enable_playback_state_lists_title),
|
||||
summary = stringResource(Res.string.enable_playback_state_lists_summary),
|
||||
isChecked = playbackStateLists,
|
||||
onCheckedChange = onTogglePlaybackStateLists,
|
||||
enabled = watchHistory
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.enable_search_history_title),
|
||||
summary = stringResource(Res.string.enable_search_history_summary),
|
||||
isChecked = searchHistory,
|
||||
onCheckedChange = onToggleSearchHistory
|
||||
)
|
||||
|
||||
PreferenceCategoryHeader(
|
||||
title = stringResource(Res.string.settings_category_clear_data_title)
|
||||
)
|
||||
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.metadata_cache_wipe_title),
|
||||
summary = stringResource(Res.string.metadata_cache_wipe_summary),
|
||||
onClick = onWipeMetadataCache
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.clear_views_history_title),
|
||||
summary = stringResource(Res.string.clear_views_history_summary),
|
||||
onClick = { pending = PendingDelete.WATCH }
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.clear_playback_states_title),
|
||||
summary = stringResource(Res.string.clear_playback_states_summary),
|
||||
onClick = { pending = PendingDelete.PLAYBACK_STATES }
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.clear_search_history_title),
|
||||
summary = stringResource(Res.string.clear_search_history_summary),
|
||||
onClick = { pending = PendingDelete.SEARCH }
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.clear_cookie_title),
|
||||
summary = stringResource(Res.string.clear_cookie_summary),
|
||||
onClick = onClearCookies,
|
||||
enabled = hasRecaptchaCookies
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun HistorySettingsScreenPreview() {
|
||||
HistorySettingsContent(
|
||||
watchHistory = true,
|
||||
playbackResume = true,
|
||||
playbackStateLists = false,
|
||||
searchHistory = true,
|
||||
hasRecaptchaCookies = true,
|
||||
onNavigateUp = {},
|
||||
onToggleWatchHistory = {},
|
||||
onTogglePlaybackResume = {},
|
||||
onTogglePlaybackStateLists = {},
|
||||
onToggleSearchHistory = {},
|
||||
onWipeMetadataCache = {},
|
||||
onDeleteWatchHistory = {},
|
||||
onDeletePlaybackStates = {},
|
||||
onDeleteSearchHistory = {},
|
||||
onClearCookies = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun HistorySettingsScreenWatchHistoryOffPreview() {
|
||||
HistorySettingsContent(
|
||||
watchHistory = false,
|
||||
playbackResume = true,
|
||||
playbackStateLists = false,
|
||||
searchHistory = true,
|
||||
hasRecaptchaCookies = false,
|
||||
onNavigateUp = {},
|
||||
onToggleWatchHistory = {},
|
||||
onTogglePlaybackResume = {},
|
||||
onTogglePlaybackStateLists = {},
|
||||
onToggleSearchHistory = {},
|
||||
onWipeMetadataCache = {},
|
||||
onDeleteWatchHistory = {},
|
||||
onDeletePlaybackStates = {},
|
||||
onDeleteSearchHistory = {},
|
||||
onClearCookies = {}
|
||||
)
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package net.newpipe.app.screen.settings.home
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.BuildConfig
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Destination
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.ic_backup
|
||||
import newpipe.shared.generated.resources.ic_bug_report
|
||||
import newpipe.shared.generated.resources.ic_file_download
|
||||
import newpipe.shared.generated.resources.ic_history
|
||||
import newpipe.shared.generated.resources.ic_language
|
||||
import newpipe.shared.generated.resources.ic_newpipe_update
|
||||
import newpipe.shared.generated.resources.ic_palette
|
||||
import newpipe.shared.generated.resources.ic_play_arrow
|
||||
import newpipe.shared.generated.resources.ic_rss_feed
|
||||
import newpipe.shared.generated.resources.ic_settings
|
||||
import newpipe.shared.generated.resources.ic_subscriptions
|
||||
import newpipe.shared.generated.resources.ic_tv
|
||||
import newpipe.shared.generated.resources.settings
|
||||
import newpipe.shared.generated.resources.settings_category_backup_restore_title
|
||||
import newpipe.shared.generated.resources.settings_category_content_title
|
||||
import newpipe.shared.generated.resources.settings_category_debug_title
|
||||
import newpipe.shared.generated.resources.settings_category_downloads_title
|
||||
import newpipe.shared.generated.resources.settings_category_feed_title
|
||||
import newpipe.shared.generated.resources.settings_category_history_title
|
||||
import newpipe.shared.generated.resources.settings_category_language_title
|
||||
import newpipe.shared.generated.resources.settings_category_look_and_feel_title
|
||||
import newpipe.shared.generated.resources.settings_category_player_behavior_title
|
||||
import newpipe.shared.generated.resources.settings_category_player_title
|
||||
import newpipe.shared.generated.resources.settings_category_services_title
|
||||
import newpipe.shared.generated.resources.settings_category_updates_title
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
/**
|
||||
* One row on the Settings home screen.
|
||||
*
|
||||
* @param destination Where tapping the row navigates to. Used as theLazyColumn
|
||||
* key — must be unique within the rendered list.
|
||||
*/
|
||||
internal data class SettingsHomeRow(
|
||||
val title: StringResource,
|
||||
val icon: DrawableResource,
|
||||
val destination: Destination.Settings
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SettingsHomeScreen(navigator: Navigator = koinInject()) {
|
||||
val rows = remember { defaultSettingsHomeRows(isDebugBuild = BuildConfig.DEBUG) }
|
||||
SettingsHomeScreenContent(
|
||||
rows = rows,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onNavigateTo = navigator::navigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsHomeScreenContent(
|
||||
rows: List<SettingsHomeRow>,
|
||||
onNavigateUp: () -> Unit,
|
||||
onNavigateTo: (Destination.Settings) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
items(items = rows, key = { it.destination.toString() }) { row
|
||||
-> TextPreference(
|
||||
title = stringResource(row.title),
|
||||
icon = painterResource(row.icon),
|
||||
onClick = { onNavigateTo(row.destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default rows shown on the Settings home screen.
|
||||
*
|
||||
* Updates is omitted in debug builds (in-app updates ship only viarelease
|
||||
* APKs); Debug is added only in debug builds.
|
||||
*/
|
||||
private fun defaultSettingsHomeRows(isDebugBuild: Boolean):
|
||||
List<SettingsHomeRow> = buildList {
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_player_title,
|
||||
Res.drawable.ic_play_arrow,
|
||||
Destination.Settings.Player
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_player_behavior_title,
|
||||
Res.drawable.ic_settings,
|
||||
Destination.Settings.Behaviour
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_downloads_title,
|
||||
Res.drawable.ic_file_download,
|
||||
Destination.Settings.Download
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_look_and_feel_title,
|
||||
Res.drawable.ic_palette,
|
||||
Destination.Settings.LookFeel
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_history_title,
|
||||
Res.drawable.ic_history,
|
||||
Destination.Settings.HistoryCache
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_content_title,
|
||||
Res.drawable.ic_tv,
|
||||
Destination.Settings.Content
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_feed_title,
|
||||
Res.drawable.ic_rss_feed,
|
||||
Destination.Settings.Feed
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_services_title,
|
||||
Res.drawable.ic_subscriptions,
|
||||
Destination.Settings.Services
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_language_title,
|
||||
Res.drawable.ic_language,
|
||||
Destination.Settings.Language
|
||||
))
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_backup_restore_title,
|
||||
Res.drawable.ic_backup,
|
||||
Destination.Settings.BackupRestore
|
||||
))
|
||||
if (!isDebugBuild) {
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_updates_title,
|
||||
Res.drawable.ic_newpipe_update,
|
||||
Destination.Settings.Updates
|
||||
))
|
||||
}
|
||||
if (isDebugBuild) {
|
||||
add(SettingsHomeRow(
|
||||
Res.string.settings_category_debug_title,
|
||||
Res.drawable.ic_bug_report,
|
||||
Destination.Settings.Debug
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SettingsHomeScreenPreview() {
|
||||
SettingsHomeScreenContent(
|
||||
rows = defaultSettingsHomeRows(isDebugBuild = false),
|
||||
onNavigateUp = {},
|
||||
onNavigateTo = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SettingsHomeScreenDebugBuildPreview() {
|
||||
SettingsHomeScreenContent(
|
||||
rows = defaultSettingsHomeRows(isDebugBuild = true),
|
||||
onNavigateUp = {},
|
||||
onNavigateTo = {}
|
||||
)
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.lookfeel
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.ListPreference
|
||||
import net.newpipe.app.composable.ListPreferenceOption
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.LIST_VIEW_AUTO
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.LIST_VIEW_CARD
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.LIST_VIEW_GRID
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.LIST_VIEW_LIST
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.LookFeelSettingsViewModel
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.TABLET_AUTO
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.TABLET_OFF
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.TABLET_ON
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.THEME_AUTO
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.THEME_BLACK
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.THEME_DARK
|
||||
import net.newpipe.app.viewmodel.settings.lookfeel.THEME_LIGHT
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.caption_setting_description
|
||||
import newpipe.shared.generated.resources.caption_setting_title
|
||||
import newpipe.shared.generated.resources.list_view_mode_auto
|
||||
import newpipe.shared.generated.resources.list_view_mode_card
|
||||
import newpipe.shared.generated.resources.list_view_mode_grid
|
||||
import newpipe.shared.generated.resources.list_view_mode_list
|
||||
import newpipe.shared.generated.resources.list_view_mode_title
|
||||
import newpipe.shared.generated.resources.main_tabs_position_summary
|
||||
import newpipe.shared.generated.resources.main_tabs_position_title
|
||||
import newpipe.shared.generated.resources.night_theme_available
|
||||
import newpipe.shared.generated.resources.night_theme_summary
|
||||
import newpipe.shared.generated.resources.night_theme_title
|
||||
import newpipe.shared.generated.resources.settings_category_appearance_title
|
||||
import newpipe.shared.generated.resources.show_hold_to_append_summary
|
||||
import newpipe.shared.generated.resources.show_hold_to_append_title
|
||||
import newpipe.shared.generated.resources.tablet_mode_auto
|
||||
import newpipe.shared.generated.resources.tablet_mode_off
|
||||
import newpipe.shared.generated.resources.tablet_mode_on
|
||||
import newpipe.shared.generated.resources.tablet_mode_title
|
||||
import newpipe.shared.generated.resources.theme_auto
|
||||
import newpipe.shared.generated.resources.theme_black
|
||||
import newpipe.shared.generated.resources.theme_dark
|
||||
import newpipe.shared.generated.resources.theme_light
|
||||
import newpipe.shared.generated.resources.theme_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun LookFeelSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: LookFeelSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val theme by viewModel.theme.collectAsState()
|
||||
val nightTheme by viewModel.nightTheme.collectAsState()
|
||||
val showHoldToAppend by viewModel.showHoldToAppend.collectAsState()
|
||||
val tabletMode by viewModel.tabletMode.collectAsState()
|
||||
val listViewMode by viewModel.listViewMode.collectAsState()
|
||||
val mainTabsPosition by viewModel.mainTabsPosition.collectAsState()
|
||||
|
||||
LookFeelSettingsContent(
|
||||
theme = theme,
|
||||
nightTheme = nightTheme,
|
||||
showHoldToAppend = showHoldToAppend,
|
||||
tabletMode = tabletMode,
|
||||
listViewMode = listViewMode,
|
||||
mainTabsPosition = mainTabsPosition,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onSetTheme = viewModel::setTheme,
|
||||
onSetNightTheme = viewModel::setNightTheme,
|
||||
onToggleShowHoldToAppend = viewModel::toggleShowHoldToAppend,
|
||||
onSetTabletMode = viewModel::setTabletMode,
|
||||
onSetListViewMode = viewModel::setListViewMode,
|
||||
onOpenCaptionSettings = viewModel::openCaptionSettings,
|
||||
onToggleMainTabsPosition = viewModel::toggleMainTabsPosition
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LookFeelSettingsContent(
|
||||
theme: String,
|
||||
nightTheme: String,
|
||||
showHoldToAppend: Boolean,
|
||||
tabletMode: String,
|
||||
listViewMode: String,
|
||||
mainTabsPosition: Boolean,
|
||||
onNavigateUp: () -> Unit,
|
||||
onSetTheme: (String) -> Unit,
|
||||
onSetNightTheme: (String) -> Unit,
|
||||
onToggleShowHoldToAppend: (Boolean) -> Unit,
|
||||
onSetTabletMode: (String) -> Unit,
|
||||
onSetListViewMode: (String) -> Unit,
|
||||
onOpenCaptionSettings: () -> Unit,
|
||||
onToggleMainTabsPosition: (Boolean) -> Unit
|
||||
) {
|
||||
val themeOptions = themeOptions()
|
||||
val nightThemeOptions = nightThemeOptions()
|
||||
val tabletOptions = tabletModeOptions()
|
||||
val listViewOptions = listViewModeOptions()
|
||||
val nightThemeEnabled = theme == THEME_AUTO
|
||||
val autoLabel = stringResource(Res.string.theme_auto)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings_category_appearance_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.theme_title),
|
||||
options = themeOptions,
|
||||
selectedValue = theme,
|
||||
onValueChange = onSetTheme
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.night_theme_title),
|
||||
options = nightThemeOptions,
|
||||
selectedValue = nightTheme,
|
||||
onValueChange = onSetNightTheme,
|
||||
enabled = nightThemeEnabled
|
||||
)
|
||||
if (!nightThemeEnabled) {
|
||||
// Disabled-row summary parity with legacy.
|
||||
TextPreference(
|
||||
title = "",
|
||||
summary = stringResource(Res.string.night_theme_available, autoLabel),
|
||||
onClick = {},
|
||||
enabled = false
|
||||
)
|
||||
} else {
|
||||
TextPreference(
|
||||
title = "",
|
||||
summary = stringResource(Res.string.night_theme_summary),
|
||||
onClick = {},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_hold_to_append_title),
|
||||
summary = stringResource(Res.string.show_hold_to_append_summary),
|
||||
isChecked = showHoldToAppend,
|
||||
onCheckedChange = onToggleShowHoldToAppend
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.tablet_mode_title),
|
||||
options = tabletOptions,
|
||||
selectedValue = tabletMode,
|
||||
onValueChange = onSetTabletMode
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.list_view_mode_title),
|
||||
options = listViewOptions,
|
||||
selectedValue = listViewMode,
|
||||
onValueChange = onSetListViewMode
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.caption_setting_title),
|
||||
summary = stringResource(Res.string.caption_setting_description),
|
||||
onClick = onOpenCaptionSettings
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.main_tabs_position_title),
|
||||
summary = stringResource(Res.string.main_tabs_position_summary),
|
||||
isChecked = mainTabsPosition,
|
||||
onCheckedChange = onToggleMainTabsPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun themeOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(THEME_LIGHT, stringResource(Res.string.theme_light)),
|
||||
ListPreferenceOption(THEME_DARK, stringResource(Res.string.theme_dark)),
|
||||
ListPreferenceOption(THEME_BLACK, stringResource(Res.string.theme_black)),
|
||||
ListPreferenceOption(THEME_AUTO, stringResource(Res.string.theme_auto))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun nightThemeOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(THEME_DARK, stringResource(Res.string.theme_dark)),
|
||||
ListPreferenceOption(THEME_BLACK, stringResource(Res.string.theme_black))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun tabletModeOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(TABLET_AUTO, stringResource(Res.string.tablet_mode_auto)),
|
||||
ListPreferenceOption(TABLET_ON, stringResource(Res.string.tablet_mode_on)),
|
||||
ListPreferenceOption(TABLET_OFF, stringResource(Res.string.tablet_mode_off))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun listViewModeOptions(): List<ListPreferenceOption> =
|
||||
listOf(
|
||||
ListPreferenceOption(LIST_VIEW_AUTO, stringResource(Res.string.list_view_mode_auto)),
|
||||
ListPreferenceOption(LIST_VIEW_LIST, stringResource(Res.string.list_view_mode_list)),
|
||||
ListPreferenceOption(LIST_VIEW_GRID, stringResource(Res.string.list_view_mode_grid)),
|
||||
ListPreferenceOption(LIST_VIEW_CARD, stringResource(Res.string.list_view_mode_card))
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun LookFeelSettingsScreenPreview() {
|
||||
LookFeelSettingsContent(
|
||||
theme = THEME_AUTO,
|
||||
nightTheme = THEME_DARK,
|
||||
showHoldToAppend = true,
|
||||
tabletMode = TABLET_AUTO,
|
||||
listViewMode = LIST_VIEW_AUTO,
|
||||
mainTabsPosition = false,
|
||||
onNavigateUp = {},
|
||||
onSetTheme = {},
|
||||
onSetNightTheme = {},
|
||||
onToggleShowHoldToAppend = {},
|
||||
onSetTabletMode = {},
|
||||
onSetListViewMode = {},
|
||||
onOpenCaptionSettings = {},
|
||||
onToggleMainTabsPosition = {}
|
||||
)
|
||||
}
|
||||
+361
@@ -1,2 +1,363 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.player
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.ListPreference
|
||||
import net.newpipe.app.composable.ListPreferenceOption
|
||||
import net.newpipe.app.composable.PreferenceCategoryHeader
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Destination
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.player.AUDIO_FORMAT_M4A
|
||||
import net.newpipe.app.viewmodel.settings.player.AUDIO_FORMAT_OGG
|
||||
import net.newpipe.app.viewmodel.settings.player.AUDIO_FORMAT_WEBM
|
||||
import net.newpipe.app.viewmodel.settings.player.LIMIT_DATA_OFF
|
||||
import net.newpipe.app.viewmodel.settings.player.LIMIT_DATA_WIFI_ONLY
|
||||
import net.newpipe.app.viewmodel.settings.player.PlayerSettingsViewModel
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_1080
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_1080_60
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_144
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_1440
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_2160
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_240
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_360
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_480
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_720
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_720_60
|
||||
import net.newpipe.app.viewmodel.settings.player.RESOLUTION_BEST
|
||||
import net.newpipe.app.viewmodel.settings.player.THUMBNAIL_HIGH
|
||||
import net.newpipe.app.viewmodel.settings.player.THUMBNAIL_LOW
|
||||
import net.newpipe.app.viewmodel.settings.player.THUMBNAIL_OFF
|
||||
import net.newpipe.app.viewmodel.settings.player.VIDEO_FORMAT_3GP
|
||||
import net.newpipe.app.viewmodel.settings.player.VIDEO_FORMAT_MP4
|
||||
import net.newpipe.app.viewmodel.settings.player.VIDEO_FORMAT_WEBM
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.audio_format_m4a
|
||||
import newpipe.shared.generated.resources.audio_format_ogg
|
||||
import newpipe.shared.generated.resources.audio_format_webm
|
||||
import newpipe.shared.generated.resources.best_resolution
|
||||
import newpipe.shared.generated.resources.default_audio_format_title
|
||||
import newpipe.shared.generated.resources.default_popup_resolution_title
|
||||
import newpipe.shared.generated.resources.default_resolution_title
|
||||
import newpipe.shared.generated.resources.default_video_format_title
|
||||
import newpipe.shared.generated.resources.limit_data_usage_none
|
||||
import newpipe.shared.generated.resources.limit_data_usage_wifi_only
|
||||
import newpipe.shared.generated.resources.limit_mobile_data_usage_title
|
||||
import newpipe.shared.generated.resources.prefer_descriptive_audio_summary
|
||||
import newpipe.shared.generated.resources.prefer_descriptive_audio_title
|
||||
import newpipe.shared.generated.resources.prefer_original_audio_summary
|
||||
import newpipe.shared.generated.resources.prefer_original_audio_title
|
||||
import newpipe.shared.generated.resources.resolution_1080p
|
||||
import newpipe.shared.generated.resources.resolution_1080p60
|
||||
import newpipe.shared.generated.resources.resolution_1440p
|
||||
import newpipe.shared.generated.resources.resolution_144p
|
||||
import newpipe.shared.generated.resources.resolution_2160p
|
||||
import newpipe.shared.generated.resources.resolution_240p
|
||||
import newpipe.shared.generated.resources.resolution_360p
|
||||
import newpipe.shared.generated.resources.resolution_480p
|
||||
import newpipe.shared.generated.resources.resolution_720p
|
||||
import newpipe.shared.generated.resources.resolution_720p60
|
||||
import newpipe.shared.generated.resources.seekbar_preview_thumbnail_high
|
||||
import newpipe.shared.generated.resources.seekbar_preview_thumbnail_low
|
||||
import newpipe.shared.generated.resources.seekbar_preview_thumbnail_off
|
||||
import newpipe.shared.generated.resources.seekbar_preview_thumbnail_title
|
||||
import newpipe.shared.generated.resources.settings_category_exoplayer_summary
|
||||
import newpipe.shared.generated.resources.settings_category_exoplayer_title
|
||||
import newpipe.shared.generated.resources.settings_category_player_title
|
||||
import newpipe.shared.generated.resources.settings_category_video_audio_title
|
||||
import newpipe.shared.generated.resources.show_higher_resolutions_summary
|
||||
import newpipe.shared.generated.resources.show_higher_resolutions_title
|
||||
import newpipe.shared.generated.resources.show_play_with_kodi_summary
|
||||
import newpipe.shared.generated.resources.show_play_with_kodi_title
|
||||
import newpipe.shared.generated.resources.use_external_audio_player_title
|
||||
import newpipe.shared.generated.resources.use_external_video_player_summary
|
||||
import newpipe.shared.generated.resources.use_external_video_player_title
|
||||
import newpipe.shared.generated.resources.video_format_3gp
|
||||
import newpipe.shared.generated.resources.video_format_mp4
|
||||
import newpipe.shared.generated.resources.video_format_webm
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun PlayerSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: PlayerSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val defaultResolution by viewModel.defaultResolution.collectAsState()
|
||||
val defaultPopupResolution by viewModel.defaultPopupResolution.collectAsState()
|
||||
val limitMobileDataUsage by viewModel.limitMobileDataUsage.collectAsState()
|
||||
val showHigherResolutions by viewModel.showHigherResolutions.collectAsState()
|
||||
val defaultVideoFormat by viewModel.defaultVideoFormat.collectAsState()
|
||||
val defaultAudioFormat by viewModel.defaultAudioFormat.collectAsState()
|
||||
val preferOriginalAudio by viewModel.preferOriginalAudio.collectAsState()
|
||||
val preferDescriptiveAudio by viewModel.preferDescriptiveAudio.collectAsState()
|
||||
val useExternalVideoPlayer by viewModel.useExternalVideoPlayer.collectAsState()
|
||||
val useExternalAudioPlayer by viewModel.useExternalAudioPlayer.collectAsState()
|
||||
val showPlayWithKodi by viewModel.showPlayWithKodi.collectAsState()
|
||||
val seekbarThumbnail by viewModel.seekbarThumbnail.collectAsState()
|
||||
|
||||
PlayerSettingsContent(
|
||||
defaultResolution = defaultResolution,
|
||||
defaultPopupResolution = defaultPopupResolution,
|
||||
limitMobileDataUsage = limitMobileDataUsage,
|
||||
showHigherResolutions = showHigherResolutions,
|
||||
defaultVideoFormat = defaultVideoFormat,
|
||||
defaultAudioFormat = defaultAudioFormat,
|
||||
preferOriginalAudio = preferOriginalAudio,
|
||||
preferDescriptiveAudio = preferDescriptiveAudio,
|
||||
useExternalVideoPlayer = useExternalVideoPlayer,
|
||||
useExternalAudioPlayer = useExternalAudioPlayer,
|
||||
showPlayWithKodi = showPlayWithKodi,
|
||||
seekbarThumbnail = seekbarThumbnail,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onSetDefaultResolution = viewModel::setDefaultResolution,
|
||||
onSetDefaultPopupResolution = viewModel::setDefaultPopupResolution,
|
||||
onSetLimitMobileDataUsage = viewModel::setLimitMobileDataUsage,
|
||||
onToggleShowHigherResolutions = viewModel::toggleShowHigherResolutions,
|
||||
onSetDefaultVideoFormat = viewModel::setDefaultVideoFormat,
|
||||
onSetDefaultAudioFormat = viewModel::setDefaultAudioFormat,
|
||||
onTogglePreferOriginalAudio = viewModel::togglePreferOriginalAudio,
|
||||
onTogglePreferDescriptiveAudio = viewModel::togglePreferDescriptiveAudio,
|
||||
onExoPlayerSettings = { navigator.navigateTo(Destination.Settings.ExoPlayer) },
|
||||
onToggleUseExternalVideoPlayer = viewModel::toggleUseExternalVideoPlayer,
|
||||
onToggleUseExternalAudioPlayer = viewModel::toggleUseExternalAudioPlayer,
|
||||
onToggleShowPlayWithKodi = viewModel::toggleShowPlayWithKodi,
|
||||
onSetSeekbarThumbnail = viewModel::setSeekbarThumbnail
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlayerSettingsContent(
|
||||
defaultResolution: String,
|
||||
defaultPopupResolution: String,
|
||||
limitMobileDataUsage: String,
|
||||
showHigherResolutions: Boolean,
|
||||
defaultVideoFormat: String,
|
||||
defaultAudioFormat: String,
|
||||
preferOriginalAudio: Boolean,
|
||||
preferDescriptiveAudio: Boolean,
|
||||
useExternalVideoPlayer: Boolean,
|
||||
useExternalAudioPlayer: Boolean,
|
||||
showPlayWithKodi: Boolean,
|
||||
seekbarThumbnail: String,
|
||||
onNavigateUp: () -> Unit,
|
||||
onSetDefaultResolution: (String) -> Unit,
|
||||
onSetDefaultPopupResolution: (String) -> Unit,
|
||||
onSetLimitMobileDataUsage: (String) -> Unit,
|
||||
onToggleShowHigherResolutions: (Boolean) -> Unit,
|
||||
onSetDefaultVideoFormat: (String) -> Unit,
|
||||
onSetDefaultAudioFormat: (String) -> Unit,
|
||||
onTogglePreferOriginalAudio: (Boolean) -> Unit,
|
||||
onTogglePreferDescriptiveAudio: (Boolean) -> Unit,
|
||||
onExoPlayerSettings: () -> Unit,
|
||||
onToggleUseExternalVideoPlayer: (Boolean) -> Unit,
|
||||
onToggleUseExternalAudioPlayer: (Boolean) -> Unit,
|
||||
onToggleShowPlayWithKodi: (Boolean) -> Unit,
|
||||
onSetSeekbarThumbnail: (String) -> Unit
|
||||
) {
|
||||
val resolutionOptions = resolutionOptions(showHigherResolutions)
|
||||
val limitDataOptions = limitDataOptions()
|
||||
val videoFormatOptions = videoFormatOptions()
|
||||
val audioFormatOptions = audioFormatOptions()
|
||||
val thumbnailOptions = thumbnailOptions()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings_category_video_audio_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.default_resolution_title),
|
||||
options = resolutionOptions,
|
||||
selectedValue = defaultResolution,
|
||||
onValueChange = onSetDefaultResolution
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.default_popup_resolution_title),
|
||||
options = resolutionOptions,
|
||||
selectedValue = defaultPopupResolution,
|
||||
onValueChange = onSetDefaultPopupResolution
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.limit_mobile_data_usage_title),
|
||||
options = limitDataOptions,
|
||||
selectedValue = limitMobileDataUsage,
|
||||
onValueChange = onSetLimitMobileDataUsage
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_higher_resolutions_title),
|
||||
summary = stringResource(Res.string.show_higher_resolutions_summary),
|
||||
isChecked = showHigherResolutions,
|
||||
onCheckedChange = onToggleShowHigherResolutions
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.default_video_format_title),
|
||||
options = videoFormatOptions,
|
||||
selectedValue = defaultVideoFormat,
|
||||
onValueChange = onSetDefaultVideoFormat
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.default_audio_format_title),
|
||||
options = audioFormatOptions,
|
||||
selectedValue = defaultAudioFormat,
|
||||
onValueChange = onSetDefaultAudioFormat
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.prefer_original_audio_title),
|
||||
summary = stringResource(Res.string.prefer_original_audio_summary),
|
||||
isChecked = preferOriginalAudio,
|
||||
onCheckedChange = onTogglePreferOriginalAudio
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.prefer_descriptive_audio_title),
|
||||
summary = stringResource(Res.string.prefer_descriptive_audio_summary),
|
||||
isChecked = preferDescriptiveAudio,
|
||||
onCheckedChange = onTogglePreferDescriptiveAudio
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.settings_category_exoplayer_title),
|
||||
summary = stringResource(Res.string.settings_category_exoplayer_summary),
|
||||
onClick = onExoPlayerSettings
|
||||
)
|
||||
|
||||
PreferenceCategoryHeader(
|
||||
title = stringResource(Res.string.settings_category_player_title)
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_external_video_player_title),
|
||||
summary = stringResource(Res.string.use_external_video_player_summary),
|
||||
isChecked = useExternalVideoPlayer,
|
||||
onCheckedChange = onToggleUseExternalVideoPlayer
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_external_audio_player_title),
|
||||
isChecked = useExternalAudioPlayer,
|
||||
onCheckedChange = onToggleUseExternalAudioPlayer
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.show_play_with_kodi_title),
|
||||
summary = stringResource(Res.string.show_play_with_kodi_summary),
|
||||
isChecked = showPlayWithKodi,
|
||||
onCheckedChange = onToggleShowPlayWithKodi
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(Res.string.seekbar_preview_thumbnail_title),
|
||||
options = thumbnailOptions,
|
||||
selectedValue = seekbarThumbnail,
|
||||
onValueChange = onSetSeekbarThumbnail
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun resolutionOptions(showHigh: Boolean): List<ListPreferenceOption> {
|
||||
val base = listOf(
|
||||
ListPreferenceOption(RESOLUTION_BEST, stringResource(Res.string.best_resolution)),
|
||||
ListPreferenceOption(RESOLUTION_1080_60, stringResource(Res.string.resolution_1080p60)),
|
||||
ListPreferenceOption(RESOLUTION_1080, stringResource(Res.string.resolution_1080p)),
|
||||
ListPreferenceOption(RESOLUTION_720_60, stringResource(Res.string.resolution_720p60)),
|
||||
ListPreferenceOption(RESOLUTION_720, stringResource(Res.string.resolution_720p)),
|
||||
ListPreferenceOption(RESOLUTION_480, stringResource(Res.string.resolution_480p)),
|
||||
ListPreferenceOption(RESOLUTION_360, stringResource(Res.string.resolution_360p)),
|
||||
ListPreferenceOption(RESOLUTION_240, stringResource(Res.string.resolution_240p)),
|
||||
ListPreferenceOption(RESOLUTION_144, stringResource(Res.string.resolution_144p))
|
||||
)
|
||||
val high = listOf(
|
||||
ListPreferenceOption(RESOLUTION_2160, stringResource(Res.string.resolution_2160p)),
|
||||
ListPreferenceOption(RESOLUTION_1440, stringResource(Res.string.resolution_1440p))
|
||||
)
|
||||
return if (showHigh) listOf(base.first()) + high + base.drop(1) else base
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun limitDataOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(LIMIT_DATA_OFF, stringResource(Res.string.limit_data_usage_none)),
|
||||
ListPreferenceOption(LIMIT_DATA_WIFI_ONLY, stringResource(Res.string.limit_data_usage_wifi_only))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun videoFormatOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(VIDEO_FORMAT_MP4, stringResource(Res.string.video_format_mp4)),
|
||||
ListPreferenceOption(VIDEO_FORMAT_WEBM, stringResource(Res.string.video_format_webm)),
|
||||
ListPreferenceOption(VIDEO_FORMAT_3GP, stringResource(Res.string.video_format_3gp))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun audioFormatOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(AUDIO_FORMAT_M4A, stringResource(Res.string.audio_format_m4a)),
|
||||
ListPreferenceOption(AUDIO_FORMAT_WEBM, stringResource(Res.string.audio_format_webm)),
|
||||
ListPreferenceOption(AUDIO_FORMAT_OGG, stringResource(Res.string.audio_format_ogg))
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun thumbnailOptions(): List<ListPreferenceOption> = listOf(
|
||||
ListPreferenceOption(THUMBNAIL_HIGH, stringResource(Res.string.seekbar_preview_thumbnail_high)),
|
||||
ListPreferenceOption(THUMBNAIL_LOW, stringResource(Res.string.seekbar_preview_thumbnail_low)),
|
||||
ListPreferenceOption(THUMBNAIL_OFF, stringResource(Res.string.seekbar_preview_thumbnail_off))
|
||||
)
|
||||
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun PlayerSettingsScreenPreview() {
|
||||
PlayerSettingsContent(
|
||||
defaultResolution = RESOLUTION_BEST,
|
||||
defaultPopupResolution = RESOLUTION_720,
|
||||
limitMobileDataUsage = LIMIT_DATA_OFF,
|
||||
showHigherResolutions = false,
|
||||
defaultVideoFormat = VIDEO_FORMAT_MP4,
|
||||
defaultAudioFormat = AUDIO_FORMAT_M4A,
|
||||
preferOriginalAudio = true,
|
||||
preferDescriptiveAudio = false,
|
||||
useExternalVideoPlayer = false,
|
||||
useExternalAudioPlayer = false,
|
||||
showPlayWithKodi = false,
|
||||
seekbarThumbnail = THUMBNAIL_HIGH,
|
||||
onNavigateUp = {},
|
||||
onSetDefaultResolution = {},
|
||||
onSetDefaultPopupResolution = {},
|
||||
onSetLimitMobileDataUsage = {},
|
||||
onToggleShowHigherResolutions = {},
|
||||
onSetDefaultVideoFormat = {},
|
||||
onSetDefaultAudioFormat = {},
|
||||
onTogglePreferOriginalAudio = {},
|
||||
onTogglePreferDescriptiveAudio = {},
|
||||
onExoPlayerSettings = {},
|
||||
onToggleUseExternalVideoPlayer = {},
|
||||
onToggleUseExternalAudioPlayer = {},
|
||||
onToggleShowPlayWithKodi = {},
|
||||
onSetSeekbarThumbnail = {}
|
||||
)
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings.updates
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import net.newpipe.app.composable.SwitchPreference
|
||||
import net.newpipe.app.composable.TextPreference
|
||||
import net.newpipe.app.composable.TopAppBar
|
||||
import net.newpipe.app.navigation.Navigator
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.updates.UpdatesSettingsViewModel
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.check_for_updates
|
||||
import newpipe.shared.generated.resources.manual_update_description
|
||||
import newpipe.shared.generated.resources.settings_category_updates_title
|
||||
import newpipe.shared.generated.resources.updates_setting_description
|
||||
import newpipe.shared.generated.resources.updates_setting_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun UpdatesSettingsScreen(
|
||||
navigator: Navigator = koinInject(),
|
||||
viewModel: UpdatesSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val updateApp by viewModel.updateApp.collectAsState()
|
||||
|
||||
UpdatesSettingsContent(
|
||||
updateApp = updateApp,
|
||||
onNavigateUp = navigator::navigateUp,
|
||||
onToggleUpdateApp = viewModel::toggleUpdateApp,
|
||||
onManualCheck = viewModel::runManualCheck
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdatesSettingsContent(
|
||||
updateApp: Boolean,
|
||||
onNavigateUp: () -> Unit,
|
||||
onToggleUpdateApp: (Boolean) -> Unit,
|
||||
onManualCheck: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(Res.string.settings_category_updates_title),
|
||||
onNavigateUp = onNavigateUp
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.updates_setting_title),
|
||||
summary = stringResource(Res.string.updates_setting_description),
|
||||
isChecked = updateApp,
|
||||
onCheckedChange = onToggleUpdateApp
|
||||
)
|
||||
TextPreference(
|
||||
title = stringResource(Res.string.check_for_updates),
|
||||
summary = stringResource(Res.string.manual_update_description),
|
||||
onClick = onManualCheck
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun UpdatesSettingsScreenPreview() {
|
||||
UpdatesSettingsContent(
|
||||
updateApp = false,
|
||||
onNavigateUp = {},
|
||||
onToggleUpdateApp = {},
|
||||
onManualCheck = {}
|
||||
)
|
||||
}
|
||||
@@ -22,3 +22,5 @@ val iconHDPI = 72.dp
|
||||
val iconXHDPI = 96.dp
|
||||
val iconXXHDPI = 144.dp
|
||||
val iconXXXHDPI = 192.dp
|
||||
|
||||
val preferenceMinHeight = 48.dp
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import com.russhwolf.settings.ExperimentalSettingsApi
|
||||
|
||||
/**
|
||||
* Encapsulates a boolean preference backed by the multiplatform
|
||||
@@ -21,6 +22,7 @@ string-resource key).
|
||||
* @param scope Scope to keep the underlying flow alive (typically the
|
||||
ViewModel scope).
|
||||
*/
|
||||
@OptIn(ExperimentalSettingsApi::class)
|
||||
internal class BooleanPreference(
|
||||
private val key: String,
|
||||
private val defaultValue: Boolean,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
private const val SETTINGS_LAYOUT_REDESIGN_KEY = "settings_layout_redesign_key"
|
||||
|
||||
@KoinViewModel
|
||||
class SettingsViewModel(
|
||||
settings: ObservableSettings
|
||||
) : ViewModel() {
|
||||
|
||||
private val settingsLayoutRedesignPref =
|
||||
BooleanPreference(
|
||||
key = SETTINGS_LAYOUT_REDESIGN_KEY,
|
||||
defaultValue = false,
|
||||
settings = settings,
|
||||
scope = viewModelScope
|
||||
)
|
||||
|
||||
val settingsLayoutRedesign = settingsLayoutRedesignPref.state
|
||||
|
||||
fun toggleSettingsLayoutRedesign(newValue: Boolean) =
|
||||
settingsLayoutRedesignPref.toggle(newValue)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
import com.russhwolf.settings.ExperimentalSettingsApi
|
||||
|
||||
/**
|
||||
* String-valued analogue of [BooleanPreference]. Backs [ListPreference]
|
||||
* composables — anything that stores a small set of opaque string values.
|
||||
@@ -16,6 +18,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
* @param settings The shared, observable key-value store.
|
||||
* @param scope Scope that keeps the underlying flow alive (typically viewModelScope).
|
||||
*/
|
||||
@OptIn(ExperimentalSettingsApi::class)
|
||||
internal class StringPreference(
|
||||
private val key: String,
|
||||
private val defaultValue: String,
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings
|
||||
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import com.russhwolf.settings.coroutines.getStringFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
import com.russhwolf.settings.ExperimentalSettingsApi
|
||||
|
||||
/**
|
||||
* Stores a `Set<String>` as a comma-delimited [String] under [key].
|
||||
*
|
||||
* Used by multi-select preferences. NOTE: this does **not** share format with
|
||||
* Android's `MultiSelectListPreference` (which uses `getStringSet`). Until the
|
||||
* legacy XML screens are retired, multi-select prefs migrated to Compose will
|
||||
* appear "reset" to default when viewed in the legacy UI and vice-versa.
|
||||
*
|
||||
* @param defaultValue values applied when the key is missing.
|
||||
*/
|
||||
@OptIn(ExperimentalSettingsApi::class)
|
||||
internal class StringSetPreference(
|
||||
private val key: String,
|
||||
private val defaultValue: Set<String>,
|
||||
private val settings: ObservableSettings,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val state: StateFlow<Set<String>> = settings
|
||||
.getStringFlow(key, defaultValue.joinToString(","))
|
||||
.map { it.split(",").filter { piece -> piece.isNotEmpty()
|
||||
}.toSet() }
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = decode(settings.getString(key,
|
||||
defaultValue.joinToString(",")))
|
||||
)
|
||||
|
||||
fun set(newValue: Set<String>) {
|
||||
settings.putString(key, newValue.joinToString(","))
|
||||
}
|
||||
|
||||
private fun decode(raw: String): Set<String> =
|
||||
raw.split(",").filter { it.isNotEmpty() }.toSet()
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings.backuprestore
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import net.newpipe.app.platform.BackupRestoreActions
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
@KoinViewModel
|
||||
class BackupRestoreSettingsViewModel(
|
||||
private val actions: BackupRestoreActions
|
||||
) : ViewModel() {
|
||||
fun importDatabase() = actions.importDatabase()
|
||||
fun exportDatabase() = actions.exportDatabase()
|
||||
fun resetAllSettings() = actions.resetAllSettings()
|
||||
fun importSubscriptions() = actions.importSubscriptions()
|
||||
fun exportSubscriptions() = actions.exportSubscriptions()
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings.content
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import net.newpipe.app.platform.ContentActions
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.StringPreference
|
||||
import net.newpipe.app.viewmodel.settings.StringSetPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
// Keys mirror app/src/main/res/values/settings_keys.xml verbatim.
|
||||
private const val APP_LANGUAGE_KEY = "app_language_key"
|
||||
private const val CONTENT_LANGUAGE_KEY = "content_language"
|
||||
private const val CONTENT_COUNTRY_KEY = "content_country"
|
||||
private const val SHOW_CHANNEL_TABS_KEY = "channel_tabs"
|
||||
private const val SHOW_AGE_RESTRICTED_KEY = "show_age_restricted_content"
|
||||
private const val YOUTUBE_RESTRICTED_MODE_KEY =
|
||||
"youtube_restricted_mode_enabled"
|
||||
private const val SHOW_SEARCH_SUGGESTIONS_KEY = "show_search_suggestions"
|
||||
private const val IMAGE_QUALITY_KEY = "image_quality_key"
|
||||
private const val SHOW_COMMENTS_KEY = "show_comments"
|
||||
private const val SHOW_NEXT_VIDEO_KEY = "show_next_video"
|
||||
private const val SHOW_DESCRIPTION_KEY = "show_description"
|
||||
private const val SHOW_META_INFO_KEY = "show_meta_info"
|
||||
private const val FEED_UPDATE_THRESHOLD_KEY = "feed_update_threshold_key"
|
||||
private const val FEED_USE_DEDICATED_FETCH_KEY =
|
||||
"feed_use_dedicated_fetch_method"
|
||||
private const val FEED_FETCH_CHANNEL_TABS_KEY = "feed_fetch_channel_tabs"
|
||||
|
||||
// Default localization
|
||||
internal const val LOCALIZATION_SYSTEM = "system"
|
||||
|
||||
// Image quality values (match settings_keys.xml verbatim)
|
||||
internal const val IMAGE_QUALITY_LOW = "image_quality_low"
|
||||
internal const val IMAGE_QUALITY_MEDIUM = "image_quality_medium"
|
||||
internal const val IMAGE_QUALITY_HIGH = "image_quality_high"
|
||||
|
||||
// Channel tab values
|
||||
internal const val CHANNEL_TAB_VIDEOS = "show_channel_tabs_videos"
|
||||
internal const val CHANNEL_TAB_TRACKS = "show_channel_tabs_tracks"
|
||||
internal const val CHANNEL_TAB_SHORTS = "show_channel_tabs_shorts"
|
||||
internal const val CHANNEL_TAB_LIVESTREAMS =
|
||||
"show_channel_tabs_livestreams"
|
||||
internal const val CHANNEL_TAB_CHANNELS = "show_channel_tabs_channels"
|
||||
internal const val CHANNEL_TAB_PLAYLISTS = "show_channel_tabs_playlists"
|
||||
internal const val CHANNEL_TAB_ALBUMS = "show_channel_tabs_albums"
|
||||
internal const val CHANNEL_TAB_LIKES = "show_channel_tabs_likes"
|
||||
internal const val CHANNEL_TAB_ABOUT = "show_channel_tabs_about"
|
||||
|
||||
private val DEFAULT_CHANNEL_TABS = setOf(
|
||||
CHANNEL_TAB_VIDEOS, CHANNEL_TAB_TRACKS, CHANNEL_TAB_SHORTS,
|
||||
CHANNEL_TAB_LIVESTREAMS, CHANNEL_TAB_CHANNELS, CHANNEL_TAB_PLAYLISTS,
|
||||
CHANNEL_TAB_ALBUMS, CHANNEL_TAB_LIKES, CHANNEL_TAB_ABOUT
|
||||
)
|
||||
|
||||
// Search-suggestion values
|
||||
internal const val SUGGESTIONS_LOCAL = "show_local_search_suggestions"
|
||||
internal const val SUGGESTIONS_REMOTE = "show_remote_search_suggestions"
|
||||
|
||||
private val DEFAULT_SUGGESTIONS = setOf(SUGGESTIONS_LOCAL,
|
||||
SUGGESTIONS_REMOTE)
|
||||
|
||||
// Feed update threshold values (seconds, stored as Strings)
|
||||
internal const val THRESHOLD_IMMEDIATE = "0"
|
||||
internal const val THRESHOLD_5_MIN = "300"
|
||||
internal const val THRESHOLD_15_MIN = "900"
|
||||
internal const val THRESHOLD_1_HOUR = "3600"
|
||||
internal const val THRESHOLD_6_HOURS = "21600"
|
||||
internal const val THRESHOLD_12_HOURS = "43200"
|
||||
internal const val THRESHOLD_1_DAY = "86400"
|
||||
|
||||
// Feed fetch channel-tab values
|
||||
internal const val FETCH_TAB_VIDEOS = "fetch_channel_tabs_videos"
|
||||
internal const val FETCH_TAB_TRACKS = "fetch_channel_tabs_tracks"
|
||||
internal const val FETCH_TAB_SHORTS = "fetch_channel_tabs_shorts"
|
||||
internal const val FETCH_TAB_LIVESTREAMS = "fetch_channel_tabs_livestreams"
|
||||
internal const val FETCH_TAB_LIKES = "fetch_channel_tabs_likes"
|
||||
|
||||
private val DEFAULT_FETCH_TABS = setOf(
|
||||
FETCH_TAB_VIDEOS, FETCH_TAB_TRACKS, FETCH_TAB_SHORTS, FETCH_TAB_LIVESTREAMS, FETCH_TAB_LIKES
|
||||
)
|
||||
|
||||
@KoinViewModel
|
||||
class ContentSettingsViewModel(
|
||||
settings: ObservableSettings,
|
||||
private val contentActions: ContentActions
|
||||
) : ViewModel() {
|
||||
|
||||
// ListPreferences
|
||||
private val appLanguagePref = StringPreference(
|
||||
APP_LANGUAGE_KEY, LOCALIZATION_SYSTEM, settings, viewModelScope
|
||||
)
|
||||
private val contentLanguagePref = StringPreference(
|
||||
CONTENT_LANGUAGE_KEY, LOCALIZATION_SYSTEM, settings, viewModelScope
|
||||
)
|
||||
private val contentCountryPref = StringPreference(
|
||||
CONTENT_COUNTRY_KEY, LOCALIZATION_SYSTEM, settings, viewModelScope
|
||||
)
|
||||
private val imageQualityPref = StringPreference(
|
||||
IMAGE_QUALITY_KEY, IMAGE_QUALITY_MEDIUM, settings, viewModelScope
|
||||
)
|
||||
private val feedUpdateThresholdPref = StringPreference(
|
||||
FEED_UPDATE_THRESHOLD_KEY, THRESHOLD_5_MIN, settings, viewModelScope
|
||||
)
|
||||
|
||||
val appLanguage: StateFlow<String> = appLanguagePref.state
|
||||
val contentLanguage: StateFlow<String> = contentLanguagePref.state
|
||||
val contentCountry: StateFlow<String> = contentCountryPref.state
|
||||
val imageQuality: StateFlow<String> = imageQualityPref.state
|
||||
val feedUpdateThreshold: StateFlow<String> = feedUpdateThresholdPref.state
|
||||
|
||||
fun setAppLanguage(value: String) {
|
||||
appLanguagePref.set(value)
|
||||
contentActions.onAppLanguageChanged()
|
||||
}
|
||||
fun setContentLanguage(value: String) = contentLanguagePref.set(value)
|
||||
fun setContentCountry(value: String) = contentCountryPref.set(value)
|
||||
fun setImageQuality(value: String) = imageQualityPref.set(value)
|
||||
fun setFeedUpdateThreshold(value: String) = feedUpdateThresholdPref.set(value)
|
||||
|
||||
// ── Switches
|
||||
private val showAgeRestrictedPref = BooleanPreference(
|
||||
SHOW_AGE_RESTRICTED_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val youtubeRestrictedModePref = BooleanPreference(
|
||||
YOUTUBE_RESTRICTED_MODE_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val showCommentsPref = BooleanPreference(
|
||||
SHOW_COMMENTS_KEY, true, settings, viewModelScope
|
||||
)
|
||||
private val showNextVideoPref = BooleanPreference(
|
||||
SHOW_NEXT_VIDEO_KEY, true, settings, viewModelScope
|
||||
)
|
||||
private val showDescriptionPref = BooleanPreference(
|
||||
SHOW_DESCRIPTION_KEY, true, settings, viewModelScope
|
||||
)
|
||||
private val showMetaInfoPref = BooleanPreference(
|
||||
SHOW_META_INFO_KEY, true, settings, viewModelScope
|
||||
)
|
||||
private val feedUseDedicatedFetchPref = BooleanPreference(
|
||||
FEED_USE_DEDICATED_FETCH_KEY, false, settings, viewModelScope
|
||||
)
|
||||
|
||||
val showAgeRestricted: StateFlow<Boolean> = showAgeRestrictedPref.state
|
||||
val youtubeRestrictedMode: StateFlow<Boolean> = youtubeRestrictedModePref.state
|
||||
val showComments: StateFlow<Boolean> = showCommentsPref.state
|
||||
val showNextVideo: StateFlow<Boolean> = showNextVideoPref.state
|
||||
val showDescription: StateFlow<Boolean> = showDescriptionPref.state
|
||||
val showMetaInfo: StateFlow<Boolean> = showMetaInfoPref.state
|
||||
val feedUseDedicatedFetch: StateFlow<Boolean> = feedUseDedicatedFetchPref.state
|
||||
|
||||
fun toggleShowAgeRestricted(v: Boolean) = showAgeRestrictedPref.toggle(v)
|
||||
fun toggleYoutubeRestrictedMode(v: Boolean) = youtubeRestrictedModePref.toggle(v)
|
||||
fun toggleShowComments(v: Boolean) = showCommentsPref.toggle(v)
|
||||
fun toggleShowNextVideo(v: Boolean) = showNextVideoPref.toggle(v)
|
||||
fun toggleShowDescription(v: Boolean) = showDescriptionPref.toggle(v)
|
||||
fun toggleShowMetaInfo(v: Boolean) = showMetaInfoPref.toggle(v)
|
||||
fun toggleFeedUseDedicatedFetch(v: Boolean) = feedUseDedicatedFetchPref.toggle(v)
|
||||
|
||||
// MultiSelect
|
||||
private val showChannelTabsPref = StringSetPreference(
|
||||
SHOW_CHANNEL_TABS_KEY, DEFAULT_CHANNEL_TABS, settings, viewModelScope
|
||||
)
|
||||
private val showSearchSuggestionsPref = StringSetPreference(
|
||||
SHOW_SEARCH_SUGGESTIONS_KEY, DEFAULT_SUGGESTIONS, settings, viewModelScope
|
||||
)
|
||||
private val feedFetchChannelTabsPref = StringSetPreference(
|
||||
FEED_FETCH_CHANNEL_TABS_KEY, DEFAULT_FETCH_TABS, settings, viewModelScope
|
||||
)
|
||||
|
||||
val showChannelTabs: StateFlow<Set<String>> = showChannelTabsPref.state
|
||||
val showSearchSuggestions: StateFlow<Set<String>> = showSearchSuggestionsPref.state
|
||||
val feedFetchChannelTabs: StateFlow<Set<String>> = feedFetchChannelTabsPref.state
|
||||
|
||||
fun setShowChannelTabs(values: Set<String>) = showChannelTabsPref.set(values)
|
||||
fun setShowSearchSuggestions(values: Set<String>) = showSearchSuggestionsPref.set(values)
|
||||
fun setFeedFetchChannelTabs(values: Set<String>) = feedFetchChannelTabsPref.set(values)
|
||||
|
||||
// Sub-screen launchers
|
||||
fun openMainPageTabs() = contentActions.openMainPageTabsChooser()
|
||||
fun openPeertubeInstances() = contentActions.openPeertubeInstanceList()
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings.debug
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import net.newpipe.app.platform.DebugActions
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
private const val ALLOW_HEAP_DUMPING_KEY = "allow_heap_dumping_key"
|
||||
private const val ALLOW_DISPOSED_EXCEPTIONS_KEY = "allow_disposed_exceptions_key"
|
||||
private const val SHOW_ORIGINAL_TIME_AGO_KEY = "show_original_time_ago_key"
|
||||
private const val SHOW_CRASH_THE_PLAYER_KEY = "show_crash_the_player_key"
|
||||
|
||||
@KoinViewModel
|
||||
class DebugSettingsViewModel(
|
||||
settings: ObservableSettings,
|
||||
private val debugActions: DebugActions
|
||||
) : ViewModel() {
|
||||
|
||||
private val allowHeapDumpingPref = BooleanPreference(
|
||||
ALLOW_HEAP_DUMPING_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val allowDisposedExceptionsPref = BooleanPreference(
|
||||
ALLOW_DISPOSED_EXCEPTIONS_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val showOriginalTimeAgoPref = BooleanPreference(
|
||||
SHOW_ORIGINAL_TIME_AGO_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val showCrashThePlayerPref = BooleanPreference(
|
||||
SHOW_CRASH_THE_PLAYER_KEY, false, settings, viewModelScope
|
||||
)
|
||||
|
||||
val isLeakCanaryAvailable: Boolean = debugActions.isLeakCanaryAvailable()
|
||||
val allowHeapDumping = allowHeapDumpingPref.state
|
||||
val allowDisposedExceptions = allowDisposedExceptionsPref.state
|
||||
val showOriginalTimeAgo = showOriginalTimeAgoPref.state
|
||||
val showCrashThePlayer = showCrashThePlayerPref.state
|
||||
|
||||
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 crashTheApp() = debugActions.crashTheApp()
|
||||
fun showErrorSnackbar() = debugActions.showErrorSnackbar()
|
||||
|
||||
fun createErrorNotification() = debugActions.createErrorNotification()
|
||||
fun checkNewStreams() = debugActions.checkNewStreams()
|
||||
fun showMemoryLeaks() = debugActions.showMemoryLeaks()
|
||||
}
|
||||
+21
-22
@@ -8,11 +8,10 @@ package net.newpipe.app.viewmodel.settings.download
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import net.newpipe.app.platform.DownloadActions
|
||||
import net.newpipe.app.screen.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.StringPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
// Keys mirror app/src/main/res/values/settings_keys.xml — keep in sync.
|
||||
@@ -23,31 +22,31 @@ private const val DOWNLOAD_PATH_AUDIO_KEY = "download_path_audio_key"
|
||||
|
||||
@KoinViewModel
|
||||
class DownloadSettingsViewModel(
|
||||
private val settings: ObservableSettings,
|
||||
settings: ObservableSettings,
|
||||
private val actions: DownloadActions
|
||||
) : ViewModel() {
|
||||
|
||||
private val askPref = BooleanPreference(DOWNLOADS_STORAGE_ASK_KEY, false, settings, viewModelScope)
|
||||
private val useSafPref = BooleanPreference(STORAGE_USE_SAF_KEY, true, settings, viewModelScope)
|
||||
private val askPref = BooleanPreference(
|
||||
DOWNLOADS_STORAGE_ASK_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val useSafPref = BooleanPreference(
|
||||
STORAGE_USE_SAF_KEY, true, settings, viewModelScope
|
||||
)
|
||||
private val videoPathPref = StringPreference(
|
||||
DOWNLOAD_PATH_VIDEO_KEY, "", settings, viewModelScope
|
||||
)
|
||||
private val audioPathPref = StringPreference(
|
||||
DOWNLOAD_PATH_AUDIO_KEY, "", settings, viewModelScope
|
||||
)
|
||||
|
||||
private val _videoPath = MutableStateFlow(settings.getStringOrNull(DOWNLOAD_PATH_VIDEO_KEY).orEmpty())
|
||||
private val _audioPath = MutableStateFlow(settings.getStringOrNull(DOWNLOAD_PATH_AUDIO_KEY).orEmpty())
|
||||
|
||||
val storageAsk = askPref.state
|
||||
val useSaf = useSafPref.state
|
||||
val videoPath: StateFlow<String> = _videoPath.asStateFlow()
|
||||
val audioPath: StateFlow<String> = _audioPath.asStateFlow()
|
||||
val storageAsk: StateFlow<Boolean> = askPref.state
|
||||
val useSaf: StateFlow<Boolean> = useSafPref.state
|
||||
val videoPath: StateFlow<String> = videoPathPref.state
|
||||
val audioPath: StateFlow<String> = audioPathPref.state
|
||||
|
||||
fun toggleStorageAsk(v: Boolean) = askPref.toggle(v)
|
||||
fun toggleUseSaf(v: Boolean) = useSafPref.toggle(v)
|
||||
|
||||
fun pickVideoPath() = actions.pickDirectory { uri ->
|
||||
settings.putString(DOWNLOAD_PATH_VIDEO_KEY, uri)
|
||||
_videoPath.value = uri
|
||||
}
|
||||
|
||||
fun pickAudioPath() = actions.pickDirectory { uri ->
|
||||
settings.putString(DOWNLOAD_PATH_AUDIO_KEY, uri)
|
||||
_audioPath.value = uri
|
||||
}
|
||||
fun pickVideoPath() = actions.pickDirectory(videoPathPref::set)
|
||||
fun pickAudioPath() = actions.pickDirectory(audioPathPref::set)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings.exoplayer
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.StringPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
// Keys mirror app/src/main/res/values/settings_keys.xml verbatim so the
|
||||
// Compose screen and the legacy fragment share SharedPreferences.
|
||||
private const val PROGRESSIVE_LOAD_INTERVAL_KEY = "progressive_load_interval"
|
||||
private const val USE_EXOPLAYER_DECODER_FALLBACK_KEY = "use_exoplayer_decoder_fallback_key"
|
||||
private const val DISABLE_MEDIA_TUNNELING_KEY = "disable_media_tunneling_key"
|
||||
private const val ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND_KEY =
|
||||
"always_use_exoplayer_set_output_surface_workaround_key"
|
||||
|
||||
internal const val PROGRESSIVE_LOAD_INTERVAL_1 = "1"
|
||||
internal const val PROGRESSIVE_LOAD_INTERVAL_16 = "16"
|
||||
internal const val PROGRESSIVE_LOAD_INTERVAL_64 = "64"
|
||||
internal const val PROGRESSIVE_LOAD_INTERVAL_256 = "256"
|
||||
internal const val PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT = "exoplayer_default"
|
||||
|
||||
@KoinViewModel
|
||||
class ExoPlayerSettingsViewModel(
|
||||
settings: ObservableSettings
|
||||
) : ViewModel() {
|
||||
|
||||
private val progressiveLoadIntervalPref = StringPreference(
|
||||
PROGRESSIVE_LOAD_INTERVAL_KEY, PROGRESSIVE_LOAD_INTERVAL_64, settings, viewModelScope
|
||||
)
|
||||
private val useDecoderFallbackPref = BooleanPreference(
|
||||
USE_EXOPLAYER_DECODER_FALLBACK_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val disableMediaTunnelingPref = BooleanPreference(
|
||||
DISABLE_MEDIA_TUNNELING_KEY, false, settings, viewModelScope
|
||||
)
|
||||
private val alwaysUseSetOutputSurfaceWorkaroundPref = BooleanPreference(
|
||||
ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND_KEY, false, settings, viewModelScope
|
||||
)
|
||||
|
||||
val progressiveLoadInterval: StateFlow<String> = progressiveLoadIntervalPref.state
|
||||
val useDecoderFallback: StateFlow<Boolean> = useDecoderFallbackPref.state
|
||||
val disableMediaTunneling: StateFlow<Boolean> = disableMediaTunnelingPref.state
|
||||
val alwaysUseSetOutputSurfaceWorkaround: StateFlow<Boolean> =
|
||||
alwaysUseSetOutputSurfaceWorkaroundPref.state
|
||||
|
||||
fun setProgressiveLoadInterval(value: String) = progressiveLoadIntervalPref.set(value)
|
||||
fun toggleUseDecoderFallback(value: Boolean) = useDecoderFallbackPref.toggle(value)
|
||||
fun toggleDisableMediaTunneling(value: Boolean) = disableMediaTunnelingPref.toggle(value)
|
||||
fun toggleAlwaysUseSetOutputSurfaceWorkaround(value: Boolean) =
|
||||
alwaysUseSetOutputSurfaceWorkaroundPref.toggle(value)
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ import com.russhwolf.settings.ObservableSettings
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import net.newpipe.app.platform.HistoryActions
|
||||
import net.newpipe.app.screen.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
// Mirrors app/src/main/res/values/settings_keys.xml — keep byte-identical so
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings.lookfeel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import net.newpipe.app.platform.LookFeelActions
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.StringPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
// Keys mirror app/src/main/res/values/settings_keys.xml verbatim so the Compose
|
||||
// screen and the legacy fragment share SharedPreferences.
|
||||
private const val THEME_KEY = "theme"
|
||||
private const val NIGHT_THEME_KEY = "night_theme"
|
||||
private const val SHOW_HOLD_TO_APPEND_KEY = "show_hold_to_append"
|
||||
private const val TABLET_MODE_KEY = "tablet_mode"
|
||||
private const val LIST_VIEW_MODE_KEY = "list_view_mode"
|
||||
private const val MAIN_TABS_POSITION_KEY = "main_tabs_position"
|
||||
|
||||
// Theme values
|
||||
internal const val THEME_LIGHT = "light_theme"
|
||||
internal const val THEME_DARK = "dark_theme"
|
||||
internal const val THEME_BLACK = "black_theme"
|
||||
internal const val THEME_AUTO = "auto_device_theme"
|
||||
|
||||
// Tablet mode values
|
||||
internal const val TABLET_AUTO = "auto"
|
||||
internal const val TABLET_ON = "on"
|
||||
internal const val TABLET_OFF = "off"
|
||||
|
||||
// List view mode values
|
||||
internal const val LIST_VIEW_AUTO = "auto"
|
||||
internal const val LIST_VIEW_LIST = "list"
|
||||
internal const val LIST_VIEW_GRID = "grid"
|
||||
internal const val LIST_VIEW_CARD = "card"
|
||||
|
||||
@KoinViewModel
|
||||
class LookFeelSettingsViewModel(
|
||||
settings: ObservableSettings,
|
||||
private val lookFeelActions: LookFeelActions
|
||||
) : ViewModel() {
|
||||
|
||||
private val themePref = StringPreference(THEME_KEY, THEME_AUTO,
|
||||
settings, viewModelScope)
|
||||
private val nightThemePref = StringPreference(
|
||||
NIGHT_THEME_KEY, THEME_DARK, settings, viewModelScope
|
||||
)
|
||||
private val showHoldToAppendPref = BooleanPreference(
|
||||
SHOW_HOLD_TO_APPEND_KEY, true, settings, viewModelScope
|
||||
)
|
||||
private val tabletModePref = StringPreference(
|
||||
TABLET_MODE_KEY, TABLET_AUTO, settings, viewModelScope
|
||||
)
|
||||
private val listViewModePref = StringPreference(
|
||||
LIST_VIEW_MODE_KEY, LIST_VIEW_AUTO, settings, viewModelScope
|
||||
)
|
||||
private val mainTabsPositionPref = BooleanPreference(
|
||||
MAIN_TABS_POSITION_KEY, false, settings, viewModelScope
|
||||
)
|
||||
|
||||
val theme: StateFlow<String> = themePref.state
|
||||
val nightTheme: StateFlow<String> = nightThemePref.state
|
||||
val showHoldToAppend: StateFlow<Boolean> =
|
||||
showHoldToAppendPref.state
|
||||
val tabletMode: StateFlow<String> = tabletModePref.state
|
||||
val listViewMode: StateFlow<String> = listViewModePref.state
|
||||
val mainTabsPosition: StateFlow<Boolean> =
|
||||
mainTabsPositionPref.state
|
||||
|
||||
fun setTheme(newValue: String) {
|
||||
themePref.set(newValue)
|
||||
if (newValue == THEME_AUTO) {
|
||||
lookFeelActions.showSelectNightThemeToast()
|
||||
}
|
||||
lookFeelActions.applyTheme(newValue)
|
||||
}
|
||||
|
||||
fun setNightTheme(newValue: String) {
|
||||
nightThemePref.set(newValue)
|
||||
lookFeelActions.applyNightTheme(newValue)
|
||||
}
|
||||
|
||||
fun toggleShowHoldToAppend(value: Boolean) =
|
||||
showHoldToAppendPref.toggle(value)
|
||||
fun setTabletMode(value: String) = tabletModePref.set(value)
|
||||
fun setListViewMode(value: String) = listViewModePref.set(value)
|
||||
fun toggleMainTabsPosition(value: Boolean) =
|
||||
mainTabsPositionPref.toggle(value)
|
||||
|
||||
fun openCaptionSettings() = lookFeelActions.openCaptionSettings()
|
||||
}
|
||||
+2
-2
@@ -9,8 +9,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import net.newpipe.app.screen.settings.BooleanPreference
|
||||
import net.newpipe.app.screen.settings.StringPreference
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import net.newpipe.app.viewmodel.settings.StringPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
// Keys mirror app/src/main/res/values/settings_keys.xml verbatim so the
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.viewmodel.settings.updates
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import net.newpipe.app.platform.UpdateActions
|
||||
import net.newpipe.app.viewmodel.settings.BooleanPreference
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
private const val UPDATE_APP_KEY = "update_app_key"
|
||||
|
||||
@KoinViewModel
|
||||
class UpdatesSettingsViewModel(
|
||||
settings: ObservableSettings,
|
||||
private val updateActions: UpdateActions
|
||||
) : ViewModel() {
|
||||
|
||||
private val updateAppPref = BooleanPreference(
|
||||
UPDATE_APP_KEY, false, settings, viewModelScope
|
||||
)
|
||||
|
||||
val updateApp = updateAppPref.state
|
||||
|
||||
fun toggleUpdateApp(newValue: Boolean) {
|
||||
updateAppPref.toggle(newValue)
|
||||
if (newValue) {
|
||||
updateActions.runManualCheck()
|
||||
}
|
||||
}
|
||||
|
||||
fun runManualCheck() = updateActions.runManualCheck()
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.di.settings
|
||||
|
||||
import com.russhwolf.settings.NSUserDefaultsSettings
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.annotation.Singleton
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
/**
|
||||
* Settings for iOS based on UserDefaultsSettings
|
||||
*/
|
||||
@Singleton
|
||||
fun provideSettings(): Settings = NSUserDefaultsSettings(NSUserDefaults())
|
||||
@Singleton(binds = [ObservableSettings::class, Settings::class])
|
||||
fun provideSettings(): ObservableSettings =
|
||||
NSUserDefaultsSettings(NSUserDefaults())
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [BackupRestoreActions::class])
|
||||
class IOSBackupRestoreActions : BackupRestoreActions {
|
||||
override fun importDatabase() = log("importDatabase")
|
||||
override fun exportDatabase() = log("exportDatabase")
|
||||
override fun resetAllSettings() = log("resetAllSettings")
|
||||
override fun importSubscriptions() = log("importSubscriptions")
|
||||
override fun exportSubscriptions() = log("exportSubscriptions")
|
||||
private fun log(name: String) =
|
||||
Logger.i(messageString = "$name not implemented on iOS")
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [ContentActions::class])
|
||||
class IOSContentActions : ContentActions {
|
||||
override fun openMainPageTabsChooser() =
|
||||
log("openMainPageTabsChooser")
|
||||
override fun openPeertubeInstanceList() =
|
||||
log("openPeertubeInstanceList")
|
||||
override fun onAppLanguageChanged() = log("onAppLanguageChanged")
|
||||
private fun log(name: String) =
|
||||
Logger.i(messageString = "$name not implemented on iOS")
|
||||
}
|
||||
@@ -1,2 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [DebugActions::class])
|
||||
class IOSDebugActions : DebugActions {
|
||||
override fun crashTheApp() { throw RuntimeException("Dummy") }
|
||||
override fun showErrorSnackbar() { /* no-op */ }
|
||||
override fun createErrorNotification() { /* no-op */ }
|
||||
override fun checkNewStreams() { /* no-op */ }
|
||||
override fun showMemoryLeaks() { /* no-op */ }
|
||||
override fun isLeakCanaryAvailable(): Boolean = false
|
||||
}
|
||||
@@ -1,2 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* iOS implementation of [DownloadActions].
|
||||
*
|
||||
*/
|
||||
@Singleton(binds = [DownloadActions::class])
|
||||
class IOSDownloadActions : DownloadActions {
|
||||
override fun pickDirectory(onPicked: (String) -> Unit) {
|
||||
Logger.w(messageString = "pickDirectory not yet implemented on iOS")
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* iOS implementation of [HistoryActions].
|
||||
*
|
||||
* Real impl will hook into the iOS-side history store once that lands; for
|
||||
now
|
||||
* every method is a logged no-op so the iOS target still compiles.
|
||||
*/
|
||||
@Singleton(binds = [HistoryActions::class])
|
||||
class IOSHistoryActions : HistoryActions {
|
||||
override fun wipeMetadataCache() {
|
||||
Logger.i(messageString = "wipeMetadataCache not implemented on iOS")
|
||||
}
|
||||
override fun deleteWatchHistory() {
|
||||
Logger.i(messageString = "deleteWatchHistory not implemented on iOS")
|
||||
}
|
||||
override fun deletePlaybackStates() {
|
||||
Logger.i(messageString = "deletePlaybackStates not implemented on iOS")
|
||||
}
|
||||
override fun deleteSearchHistory() {
|
||||
Logger.i(messageString = "deleteSearchHistory not implemented on iOS")
|
||||
}
|
||||
override fun clearRecaptchaCookies() {
|
||||
Logger.i(messageString = "clearRecaptchaCookies not implemented on iOS")
|
||||
}
|
||||
override fun hasRecaptchaCookies(): Boolean = false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [LookFeelActions::class])
|
||||
class IOSLookFeelActions : LookFeelActions {
|
||||
override fun applyTheme(newThemeKey: String) {
|
||||
Logger.i(messageString = "applyTheme not implemented on iOS")
|
||||
}
|
||||
override fun applyNightTheme(newNightThemeKey: String) {
|
||||
Logger.i(messageString = "applyNightTheme not implemented on iOS")
|
||||
}
|
||||
override fun showSelectNightThemeToast() {
|
||||
Logger.i(messageString = "showSelectNightThemeToast not implemented on iOS")
|
||||
}
|
||||
override fun openCaptionSettings() {
|
||||
Logger.i(messageString = "openCaptionSettings not implemented on iOS")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [UpdateActions::class])
|
||||
class IOSUpdateActions : UpdateActions {
|
||||
override fun runManualCheck() {
|
||||
Logger.i(messageString = "runManualCheck not implemented on iOS")
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.di.settings
|
||||
|
||||
import com.russhwolf.settings.ObservableSettings
|
||||
import com.russhwolf.settings.PreferencesSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import java.util.prefs.Preferences
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* Settings for JVM devices based on Java Preferences
|
||||
*/
|
||||
@Singleton
|
||||
fun provideSettings(): Settings = PreferencesSettings(Preferences.userRoot())
|
||||
@Singleton(binds = [ObservableSettings::class, Settings::class])
|
||||
fun provideSettings(): ObservableSettings =
|
||||
PreferencesSettings(Preferences.userRoot())
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [BackupRestoreActions::class])
|
||||
class JVMBackupRestoreActions : BackupRestoreActions {
|
||||
override fun importDatabase() = log("importDatabase")
|
||||
override fun exportDatabase() = log("exportDatabase")
|
||||
override fun resetAllSettings() = log("resetAllSettings")
|
||||
override fun importSubscriptions() = log("importSubscriptions")
|
||||
override fun exportSubscriptions() = log("exportSubscriptions")
|
||||
private fun log(name: String) =
|
||||
Logger.i(messageString = "$name not implemented on JVM")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [ContentActions::class])
|
||||
class JVMContentActions : ContentActions {
|
||||
override fun openMainPageTabsChooser() =
|
||||
log("openMainPageTabsChooser")
|
||||
override fun openPeertubeInstanceList() =
|
||||
log("openPeertubeInstanceList")
|
||||
override fun onAppLanguageChanged() = log("onAppLanguageChanged")
|
||||
private fun log(name: String) =
|
||||
Logger.i(messageString = "$name not implemented on JVM")
|
||||
}
|
||||
@@ -1,2 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [DebugActions::class])
|
||||
class JVMDebugActions : DebugActions {
|
||||
override fun crashTheApp() = throw RuntimeException("Dummy")
|
||||
override fun showErrorSnackbar() { /* no-op */ }
|
||||
override fun createErrorNotification() { /* no-op */ }
|
||||
override fun checkNewStreams() { /* no-op */ }
|
||||
override fun showMemoryLeaks() { /* no-op */ }
|
||||
override fun isLeakCanaryAvailable(): Boolean = false
|
||||
}
|
||||
@@ -1,2 +1,33 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import javax.swing.JFileChooser
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
/**
|
||||
* JVM (desktop) implementation of [DownloadActions].
|
||||
*
|
||||
* Uses Swing's [JFileChooser] in directories-only mode.
|
||||
* Returns the absolute file path as the URI string.
|
||||
*/
|
||||
@Singleton(binds = [DownloadActions::class])
|
||||
class JVMDownloadActions : DownloadActions {
|
||||
override fun pickDirectory(onPicked: (String) -> Unit) {
|
||||
val chooser = JFileChooser().apply {
|
||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
dialogTitle = "Choose download folder"
|
||||
}
|
||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
val file = chooser.selectedFile ?: run {
|
||||
Logger.w(messageString = "JFileChooser approved but returned null file")
|
||||
return
|
||||
}
|
||||
onPicked(file.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [HistoryActions::class])
|
||||
class JVMHistoryActions : HistoryActions {
|
||||
override fun wipeMetadataCache() {
|
||||
Logger.i(messageString = "wipeMetadataCache not implemented on JVM")
|
||||
}
|
||||
override fun deleteWatchHistory() {
|
||||
Logger.i(messageString = "deleteWatchHistory not implemented on JVM")
|
||||
}
|
||||
override fun deletePlaybackStates() {
|
||||
Logger.i(messageString = "deletePlaybackStates not implemented on JVM")
|
||||
}
|
||||
override fun deleteSearchHistory() {
|
||||
Logger.i(messageString = "deleteSearchHistory not implemented on JVM")
|
||||
}
|
||||
override fun clearRecaptchaCookies() {
|
||||
Logger.i(messageString = "clearRecaptchaCookies not implemented on JVM")
|
||||
}
|
||||
override fun hasRecaptchaCookies(): Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [LookFeelActions::class])
|
||||
class JVMLookFeelActions : LookFeelActions {
|
||||
override fun applyTheme(newThemeKey: String) {
|
||||
Logger.i(messageString = "applyTheme not implemented on JVM")
|
||||
}
|
||||
override fun applyNightTheme(newNightThemeKey: String) {
|
||||
Logger.i(messageString = "applyNightTheme not implemented on JVM")
|
||||
}
|
||||
override fun showSelectNightThemeToast() {
|
||||
Logger.i(messageString = "showSelectNightThemeToast not implemented on JVM")
|
||||
}
|
||||
override fun openCaptionSettings() {
|
||||
Logger.i(messageString = "openCaptionSettings not implemented on JVM")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.platform
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Singleton
|
||||
|
||||
@Singleton(binds = [UpdateActions::class])
|
||||
class JVMUpdateActions : UpdateActions {
|
||||
override fun runManualCheck() {
|
||||
Logger.i(messageString = "runManualCheck not implemented on JVM")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user