Merge pull request #13652 from Idadelveloper/settings-home

Migrate settings homepage to compose multiplatform
This commit is contained in:
Ida Delphine authored and GitHub committed 2026-07-09 19:05:37 +01:00
commit 6fa36eea2f
26 files changed
+618 -1

No files matched your search

@@ -688,6 +688,7 @@ public final class NavigationHelper {
}
public static void openSettings(final Context context) {
// TODO: Replace with "ContextKt.navigateTo(context, Destination.Settings.INSTANCE);" later
final Intent intent = new Intent(context, SettingsActivity.class);
context.startActivity(intent);
}
+2 -1
View File
@@ -37,7 +37,8 @@ kotlin {
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes"
"-Xexpect-actual-classes",
"-Xexplicit-backing-fields"
)
optIn.addAll(
"androidx.compose.material3.ExperimentalMaterial3Api",
@@ -0,0 +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.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import androidx.core.content.pm.PackageInfoCompat
import co.touchlab.kermit.Logger
import org.koin.core.annotation.Singleton
@Singleton(binds = [BuildInfo::class])
class AndroidBuildInfo(private val context: Context) : BuildInfo {
override val isDebug: Boolean =
context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
@get:SuppressLint("NewApi")
@OptIn(ExperimentalStdlibApi::class)
override val isReleaseApk: Boolean by lazy {
val certificates = mapOf(
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
)
try {
PackageInfoCompat.hasSignatures(
context.packageManager,
context.packageName,
certificates,
false
)
} catch (exception: PackageManager.NameNotFoundException) {
Logger.e(messageString = "Could not find package info", throwable = exception)
false
}
}
companion object {
private const val RELEASE_CERT_PUBLIC_KEY_SHA256 =
"cb84069bd68116bafae5ee4ee5b08a567aa6d898404e7cb12f9e756df5cf5cab"
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5c0,-0.61 -0.23,-1.2 -0.64,-1.67c-0.08,-0.1 -0.13,-0.21 -0.13,-0.33c0,-0.28 0.22,-0.5 0.5,-0.5H16c3.31,0 6,-2.69 6,-6C22,6.04 17.51,2 12,2zM17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M14,12c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2 0.9,2 2,2 2,-0.9 2,-2zM12,3c-4.97,0 -9,4.03 -9,9L0,12l4,4 4,-4L5,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.51,0 -2.91,-0.49 -4.06,-1.3l-1.42,1.44C8.04,20.3 9.94,21 12,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z" />
</vector>
@@ -34,4 +34,17 @@
<string name="open_in_browser">Open in browser</string>
<string name="done">Done</string>
<string name="not_available">Not available</string>
<!-- SettingsHomeScreen -->
<string name="settings">Settings</string>
<string name="search">Search</string>
<string name="content">Content</string>
<string name="notifications">Notifications</string>
<string name="settings_category_video_audio_title">Video and audio</string>
<string name="settings_category_downloads_title">Download</string>
<string name="settings_category_appearance_title">Appearance</string>
<string name="settings_category_history_title">History and cache</string>
<string name="settings_category_updates_title">Updates</string>
<string name="settings_category_backup_restore_title">Backup and restore</string>
<string name="settings_category_debug_title">Debug</string>
</resources>
@@ -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.composable
import androidx.compose.foundation.clickable
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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 newpipe.shared.generated.resources.Res
import newpipe.shared.generated.resources.ic_headset
import newpipe.shared.generated.resources.settings_category_video_audio_title
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
/**
* A row that mirrors the standard AndroidX preference item
*
* @param title Title text of the preference
* @param modifier Modifier applied to the row
* @param icon Leading icon painter; null hides the leading slot
* @param summary Optional secondary line under the title
* @param onClick Action executed when the row is tapped
*/
@Composable
fun PreferenceRow(
title: String,
modifier: Modifier = Modifier,
icon: Painter? = null,
summary: String? = null,
onClick: () -> Unit = {}
) {
ListItem(
modifier = modifier.clickable(onClick = onClick),
headlineContent = { Text(text = title) },
supportingContent = summary?.let { text -> { Text(text = text) } },
leadingContent = icon?.let { painter ->
{ Icon(painter = painter, contentDescription = null) }
}
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@PreviewLightDark
@Composable
private fun PreferenceRowPreview() {
PreferenceRow(
title = stringResource(Res.string.settings_category_video_audio_title),
icon = painterResource(Res.drawable.ic_headset)
)
}
@@ -15,6 +15,9 @@ import net.newpipe.app.model.License
@Serializable
sealed interface Destination : NavKey {
@Serializable
data object Settings : Destination
@Serializable
data object About : Destination
}
@@ -8,6 +8,7 @@ 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.SettingsHomeScreen
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.annotation.Provided
import org.koin.core.annotation.Singleton
@@ -27,6 +28,10 @@ fun navModule() = module {
navigation<Destination.About> {
AboutScreen()
}
navigation<Destination.Settings> {
SettingsHomeScreen()
}
}
/**
@@ -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
/**
* Information about the currently running build.
*/
interface BuildInfo {
val isReleaseApk: Boolean
val isDebug: Boolean
}
@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package net.newpipe.app.screen.settings
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import net.newpipe.app.composable.PreferenceRow
import net.newpipe.app.composable.TopAppBar
import net.newpipe.app.navigation.Navigator
import net.newpipe.app.preview.ThemePreviewProvider
import net.newpipe.app.screen.settings.model.SettingsCategory
import net.newpipe.app.screen.settings.model.SettingsCategoryType
import net.newpipe.app.viewmodel.settings.SettingsViewModel
import newpipe.shared.generated.resources.Res
import newpipe.shared.generated.resources.ic_search
import newpipe.shared.generated.resources.search
import newpipe.shared.generated.resources.settings
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun SettingsHomeScreen(
navigator: Navigator = koinInject(),
viewModel: SettingsViewModel = koinViewModel()
) {
val categories by viewModel.categories.collectAsStateWithLifecycle()
SettingsHomeScreenContent(
categories = categories.map { type ->
SettingsCategory(
title = type.title,
icon = type.icon,
// TODO: Replace with a Destination once sub-screens are migrated
onClick = {}
)
},
onNavigateUp = { navigator.navigateUp() }
)
}
@Composable
fun SettingsHomeScreenContent(
categories: List<SettingsCategory> = SettingsCategoryType.entries.map { type ->
SettingsCategory(title = type.title, icon = type.icon)
},
onNavigateUp: () -> Unit = {},
onSearchClick: () -> Unit = {}
) {
Scaffold(
topBar = {
TopAppBar(
title = stringResource(Res.string.settings),
onNavigateUp = onNavigateUp,
actions = {
IconButton(onClick = onSearchClick) {
Icon(
painter = painterResource(Res.drawable.ic_search),
contentDescription = stringResource(Res.string.search)
)
}
}
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()),
contentPadding = WindowInsets.navigationBars.asPaddingValues()
) {
items(items = categories) { category ->
PreferenceRow(
title = stringResource(category.title),
icon = painterResource(category.icon),
onClick = category.onClick
)
}
}
}
}
@PreviewWrapper(ThemePreviewProvider::class)
@PreviewLightDark
@Composable
private fun SettingsHomeScreenPreview() {
SettingsHomeScreenContent()
}
@@ -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.screen.settings.model
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
data class SettingsCategory(
val title: StringResource,
val icon: DrawableResource,
// TODO: Replace with a Destination once sub-screens are migrated
val onClick: () -> Unit = {}
)
@@ -0,0 +1,41 @@
package net.newpipe.app.screen.settings.model
import newpipe.shared.generated.resources.Res
import newpipe.shared.generated.resources.content
import newpipe.shared.generated.resources.ic_bug_report
import newpipe.shared.generated.resources.ic_cloud_download
import newpipe.shared.generated.resources.ic_file_download
import newpipe.shared.generated.resources.ic_headset
import newpipe.shared.generated.resources.ic_history
import newpipe.shared.generated.resources.ic_language
import newpipe.shared.generated.resources.ic_notifications
import newpipe.shared.generated.resources.ic_palette
import newpipe.shared.generated.resources.ic_settings_backup_restore
import newpipe.shared.generated.resources.notifications
import newpipe.shared.generated.resources.settings_category_appearance_title
import newpipe.shared.generated.resources.settings_category_backup_restore_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_history_title
import newpipe.shared.generated.resources.settings_category_updates_title
import newpipe.shared.generated.resources.settings_category_video_audio_title
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
/**
* Categories on the settings home screen.
*/
enum class SettingsCategoryType(
val title: StringResource,
val icon: DrawableResource
) {
VIDEO_AUDIO(Res.string.settings_category_video_audio_title, Res.drawable.ic_headset),
DOWNLOADS(Res.string.settings_category_downloads_title, Res.drawable.ic_file_download),
APPEARANCE(Res.string.settings_category_appearance_title, Res.drawable.ic_palette),
HISTORY(Res.string.settings_category_history_title, Res.drawable.ic_history),
CONTENT(Res.string.content, Res.drawable.ic_language),
NOTIFICATIONS(Res.string.notifications, Res.drawable.ic_notifications),
UPDATES(Res.string.settings_category_updates_title, Res.drawable.ic_cloud_download),
BACKUP_RESTORE(Res.string.settings_category_backup_restore_title, Res.drawable.ic_settings_backup_restore),
DEBUG(Res.string.settings_category_debug_title, Res.drawable.ic_bug_report)
}
@@ -0,0 +1,30 @@
/*
* 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 androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import net.newpipe.app.platform.BuildInfo
import net.newpipe.app.screen.settings.model.SettingsCategoryType
import org.koin.core.annotation.KoinViewModel
@KoinViewModel
class SettingsViewModel(buildInfo: BuildInfo) : ViewModel() {
val categories: StateFlow<List<SettingsCategoryType>>
field = MutableStateFlow(computeVisible(buildInfo))
private fun computeVisible(buildInfo: BuildInfo): List<SettingsCategoryType> =
SettingsCategoryType.entries.filter { type ->
when (type) {
SettingsCategoryType.UPDATES -> buildInfo.isReleaseApk
SettingsCategoryType.DEBUG -> buildInfo.isDebug
else -> true
}
}
}
@@ -0,0 +1,48 @@
/*
* 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.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest
import kotlin.test.Test
import kotlin.test.assertTrue
@OptIn(ExperimentalTestApi::class)
class PreferenceRowTest {
@Test
fun testPreferenceRowTitleAndClick() = runComposeUiTest {
val title = "Test title"
var clicked = false
setContent {
PreferenceRow(
title = title,
onClick = { clicked = true }
)
}
onNodeWithText(title).apply {
assertIsDisplayed()
performClick()
}
assertTrue(clicked)
}
@Test
fun testPreferenceRowShowsSummary() = runComposeUiTest {
val title = "Test title"
val summary = "Test summary"
setContent {
PreferenceRow(title = title, summary = summary)
}
onNodeWithText(title).assertIsDisplayed()
onNodeWithText(summary).assertIsDisplayed()
}
}
@@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package net.newpipe.app.screen.settings
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest
import com.russhwolf.settings.MapSettings
import com.russhwolf.settings.Settings
import net.newpipe.app.extensions.withKoin
import net.newpipe.app.screen.settings.model.SettingsCategory
import kotlin.test.Test
import kotlin.test.assertTrue
import newpipe.shared.generated.resources.Res
import newpipe.shared.generated.resources.ic_headset
import newpipe.shared.generated.resources.settings
import newpipe.shared.generated.resources.settings_category_video_audio_title
import org.jetbrains.compose.resources.getString
import org.koin.dsl.module
@OptIn(ExperimentalTestApi::class)
class SettingsHomeScreenTest {
private val emptySettings = module {
single<Settings> { MapSettings() }
}
@Test
fun testTitleAndCategoriesRender() = runComposeUiTest {
withKoin(
modules = listOf(emptySettings),
content = { SettingsHomeScreenContent() },
onContent = {
onNodeWithText(getString(Res.string.settings)).assertIsDisplayed()
onNodeWithText(getString(Res.string.settings_category_video_audio_title))
.assertIsDisplayed()
}
)
}
@Test
fun testCategoryClickFires() = runComposeUiTest {
var clicked = false
val categories = listOf(
SettingsCategory(
title = Res.string.settings_category_video_audio_title,
icon = Res.drawable.ic_headset,
onClick = { clicked = true }
)
)
withKoin(
modules = listOf(emptySettings),
content = { SettingsHomeScreenContent(categories = categories) },
onContent = {
onNodeWithText(getString(Res.string.settings_category_video_audio_title))
.performClick()
assertTrue(clicked)
}
)
}
}
@@ -0,0 +1,9 @@
package net.newpipe.app.platform
import org.koin.core.annotation.Singleton
@Singleton(binds = [BuildInfo::class])
class IOSBuildInfo : BuildInfo {
override val isReleaseApk: Boolean = false
override val isDebug: Boolean = false
}
@@ -0,0 +1,9 @@
package net.newpipe.app.platform
import org.koin.core.annotation.Singleton
@Singleton(binds = [BuildInfo::class])
class JVMBuildInfo : BuildInfo {
override val isReleaseApk: Boolean = false
override val isDebug: Boolean = false
}