From 4cad0293ca3b11807b99e93c08ba633ea2ab9d4a Mon Sep 17 00:00:00 2001 From: Ida Delphine Date: Tue, 6 Oct 2026 02:33:06 -0700 Subject: [PATCH] dynamic player settings ui per platform --- gradle/libs.versions.toml | 2 +- .../screen/ExoPlayerSettingsSectionsTest.kt | 72 +++++++++++ .../platform/AndroidPlayerSettingsSections.kt | 18 +++ .../app/preferences/ExoPlayerPreferences.kt | 0 .../app/screen/ExoPlayerSettingsSections.kt | 116 ++++++++++++++++++ .../settings/PlayerSettingsViewModel.kt | 0 .../net/newpipe/app/navigation/Destination.kt | 3 +- .../net/newpipe/app/navigation/NavModule.kt | 2 +- .../app/platform/PlayerSettingsSections.kt | 19 +++ .../screen/settings/PlayerSettingsScreen.kt | 115 ++--------------- .../settings/VideoAudioSettingsScreen.kt | 23 ++-- .../settings/ExoPlayerSettingsScreenTest.kt | 67 ---------- .../settings/VideoAudioSettingsScreenTest.kt | 2 +- .../app/platform/IOSPlayerSettingsSections.kt | 17 +++ .../app/platform/JVMPlayerSettingsSections.kt | 17 +++ 15 files changed, 285 insertions(+), 188 deletions(-) create mode 100644 shared/src/androidDeviceTest/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSectionsTest.kt create mode 100644 shared/src/androidMain/kotlin/net/newpipe/app/platform/AndroidPlayerSettingsSections.kt rename shared/src/{commonMain => androidMain}/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt (100%) create mode 100644 shared/src/androidMain/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSections.kt rename shared/src/{commonMain => androidMain}/kotlin/net/newpipe/app/viewmodel/settings/PlayerSettingsViewModel.kt (100%) create mode 100644 shared/src/commonMain/kotlin/net/newpipe/app/platform/PlayerSettingsSections.kt delete mode 100644 shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt create mode 100644 shared/src/iosMain/kotlin/net/newpipe/app/platform/IOSPlayerSettingsSections.kt create mode 100644 shared/src/jvmMain/kotlin/net/newpipe/app/platform/JVMPlayerSettingsSections.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61e15ceca..cca581bc0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ about-libraries = "15.1.1" acra = "5.13.1" activity = "1.13.0" -agp = "9.4.0" +agp = "9.4.1" appcompat = "1.8.0" assertj = "3.27.7" autoservice-google = "1.1.1" diff --git a/shared/src/androidDeviceTest/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSectionsTest.kt b/shared/src/androidDeviceTest/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSectionsTest.kt new file mode 100644 index 000000000..e04ac6590 --- /dev/null +++ b/shared/src/androidDeviceTest/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSectionsTest.kt @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.v2.runComposeUiTest +import kotlin.test.Test +import newpipe.shared.generated.resources.Res +import newpipe.shared.generated.resources.disable_media_tunneling_automatic_info +import newpipe.shared.generated.resources.disable_media_tunneling_summary +import newpipe.shared.generated.resources.progressive_load_interval_summary +import newpipe.shared.generated.resources.progressive_load_interval_title +import org.jetbrains.compose.resources.getString + +@Composable +private fun Hosted(content: @Composable () -> Unit) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { content() } +} + +@OptIn(ExperimentalTestApi::class) +class ExoPlayerSettingsSectionsTest { + + @Test + fun rendersLoadIntervalRow() = runComposeUiTest { + setContent { Hosted { ExoPlayerSettingsSectionsContent() } } + + onNodeWithText(getString(Res.string.progressive_load_interval_title)).assertIsDisplayed() + } + + @Test + fun loadIntervalSummaryShowsSelectedEntryLabel() = runComposeUiTest { + setContent { Hosted { ExoPlayerSettingsSectionsContent() } } + + onNodeWithText(getString(Res.string.progressive_load_interval_summary, "64 KiB")) + .assertIsDisplayed() + } + + @Test + fun mediaTunnelingInfoShownWhenAutoDisabled() = runComposeUiTest { + setContent { + Hosted { ExoPlayerSettingsSectionsContent(mediaTunnelingAutoDisabled = true) } + } + + onNodeWithText(getString(Res.string.disable_media_tunneling_automatic_info), substring = true) + .performScrollTo() + .assertIsDisplayed() + } + + @Test + fun mediaTunnelingInfoHiddenWhenNotAutoDisabled() = runComposeUiTest { + setContent { + Hosted { ExoPlayerSettingsSectionsContent(mediaTunnelingAutoDisabled = false) } + } + + onNodeWithText(getString(Res.string.disable_media_tunneling_summary), substring = true) + .performScrollTo() + .assertIsDisplayed() + onNodeWithText(getString(Res.string.disable_media_tunneling_automatic_info), substring = true) + .assertDoesNotExist() + } +} diff --git a/shared/src/androidMain/kotlin/net/newpipe/app/platform/AndroidPlayerSettingsSections.kt b/shared/src/androidMain/kotlin/net/newpipe/app/platform/AndroidPlayerSettingsSections.kt new file mode 100644 index 000000000..2d971f558 --- /dev/null +++ b/shared/src/androidMain/kotlin/net/newpipe/app/platform/AndroidPlayerSettingsSections.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import androidx.compose.runtime.Composable +import net.newpipe.app.screen.ExoPlayerSettingsSections +import org.koin.core.annotation.Singleton + +@Singleton(binds = [PlayerSettingsSections::class]) +class AndroidPlayerSettingsSections : PlayerSettingsSections { + override val isAvailable = true + + @Composable + override fun Render() = ExoPlayerSettingsSections() +} diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt b/shared/src/androidMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt similarity index 100% rename from shared/src/commonMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt rename to shared/src/androidMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt diff --git a/shared/src/androidMain/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSections.kt b/shared/src/androidMain/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSections.kt new file mode 100644 index 000000000..9a1e25133 --- /dev/null +++ b/shared/src/androidMain/kotlin/net/newpipe/app/screen/ExoPlayerSettingsSections.kt @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import net.newpipe.app.composable.ListPreference +import net.newpipe.app.composable.ListPreferenceEntry +import net.newpipe.app.composable.SwitchPreference +import net.newpipe.app.preferences.ExoPlayerPreferences +import net.newpipe.app.preview.ThemePreviewProvider +import net.newpipe.app.viewmodel.settings.PlayerSettingsViewModel +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_automatic_info +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_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.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.viewmodel.koinViewModel + +@Composable +fun ExoPlayerSettingsSections(viewModel: PlayerSettingsViewModel = koinViewModel()) { + val progressiveLoadInterval by viewModel.progressiveLoadInterval.collectAsStateWithLifecycle() + val useExoplayerDecoderFallback by viewModel.useExoplayerDecoderFallback.collectAsStateWithLifecycle() + val disableMediaTunneling by viewModel.disableMediaTunneling.collectAsStateWithLifecycle() + val mediaTunnelingAutoDisabled by viewModel.mediaTunnelingAutoDisabled.collectAsStateWithLifecycle() + val alwaysUseSetOutputSurfaceWorkaround by viewModel.alwaysUseSetOutputSurfaceWorkaround.collectAsStateWithLifecycle() + + ExoPlayerSettingsSectionsContent( + progressiveLoadInterval = progressiveLoadInterval, + useExoplayerDecoderFallback = useExoplayerDecoderFallback, + disableMediaTunneling = disableMediaTunneling, + mediaTunnelingAutoDisabled = mediaTunnelingAutoDisabled, + alwaysUseSetOutputSurfaceWorkaround = alwaysUseSetOutputSurfaceWorkaround, + onProgressiveLoadIntervalChange = viewModel::setProgressiveLoadInterval, + onUseExoplayerDecoderFallbackChange = viewModel::setUseExoplayerDecoderFallback, + onDisableMediaTunnelingChange = viewModel::setDisableMediaTunneling, + onAlwaysUseSetOutputSurfaceWorkaroundChange = viewModel::setAlwaysUseSetOutputSurfaceWorkaround + ) +} + +@Composable +fun ExoPlayerSettingsSectionsContent( + progressiveLoadInterval: String = ExoPlayerPreferences.DEFAULT_PROGRESSIVE_LOAD_INTERVAL, + useExoplayerDecoderFallback: Boolean = ExoPlayerPreferences.DEFAULT_USE_EXOPLAYER_DECODER_FALLBACK, + disableMediaTunneling: Boolean = ExoPlayerPreferences.DEFAULT_DISABLE_MEDIA_TUNNELING, + mediaTunnelingAutoDisabled: Boolean = false, + alwaysUseSetOutputSurfaceWorkaround: Boolean = ExoPlayerPreferences.DEFAULT_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND, + onProgressiveLoadIntervalChange: (String) -> Unit = {}, + onUseExoplayerDecoderFallbackChange: (Boolean) -> Unit = {}, + onDisableMediaTunnelingChange: (Boolean) -> Unit = {}, + onAlwaysUseSetOutputSurfaceWorkaroundChange: (Boolean) -> Unit = {} +) { + val exoPlayerDefaultLabel = stringResource(Res.string.progressive_load_interval_exoplayer_default) + + val loadIntervalEntries = ExoPlayerPreferences.PROGRESSIVE_LOAD_INTERVALS.map { value -> + ListPreferenceEntry( + value = value, + title = if (value == ExoPlayerPreferences.PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT) exoPlayerDefaultLabel else "$value KiB" + ) + } + val selectedLoadIntervalTitle = loadIntervalEntries.firstOrNull { it.value == progressiveLoadInterval }?.title.orEmpty() + + val tunnelingBaseSummary = stringResource(Res.string.disable_media_tunneling_summary) + val tunnelingSummary = if (mediaTunnelingAutoDisabled) { + tunnelingBaseSummary + " " + stringResource(Res.string.disable_media_tunneling_automatic_info) + } else { + tunnelingBaseSummary + } + + ListPreference( + title = stringResource(Res.string.progressive_load_interval_title), + entries = loadIntervalEntries, + selectedValue = progressiveLoadInterval, + onValueSelected = onProgressiveLoadIntervalChange, + summary = stringResource(Res.string.progressive_load_interval_summary, selectedLoadIntervalTitle) + ) + SwitchPreference( + title = stringResource(Res.string.use_exoplayer_decoder_fallback_title), + summary = stringResource(Res.string.use_exoplayer_decoder_fallback_summary), + checked = useExoplayerDecoderFallback, + onCheckedChange = onUseExoplayerDecoderFallbackChange + ) + SwitchPreference( + title = stringResource(Res.string.disable_media_tunneling_title), + summary = tunnelingSummary, + checked = disableMediaTunneling, + onCheckedChange = onDisableMediaTunnelingChange + ) + 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), + checked = alwaysUseSetOutputSurfaceWorkaround, + onCheckedChange = onAlwaysUseSetOutputSurfaceWorkaroundChange + ) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@PreviewLightDark +@Composable +private fun ExoPlayerSettingsSectionsPreview() { + Column { ExoPlayerSettingsSectionsContent() } +} diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/viewmodel/settings/PlayerSettingsViewModel.kt b/shared/src/androidMain/kotlin/net/newpipe/app/viewmodel/settings/PlayerSettingsViewModel.kt similarity index 100% rename from shared/src/commonMain/kotlin/net/newpipe/app/viewmodel/settings/PlayerSettingsViewModel.kt rename to shared/src/androidMain/kotlin/net/newpipe/app/viewmodel/settings/PlayerSettingsViewModel.kt diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt index 4290abadb..f7a33b8f8 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt @@ -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 @@ -16,7 +15,7 @@ import net.newpipe.app.model.License sealed interface Destination : NavKey { @Serializable - data object ExoPlayerSettings : Destination + data object PlayerSettings : Destination @Serializable data object VideoAudioSettings : Destination diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt index a885b8f8d..267befd74 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt @@ -44,7 +44,7 @@ fun navModule() = module { VideoAudioSettingsScreen() } - navigation { + navigation { PlayerSettingsScreen() } } diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/platform/PlayerSettingsSections.kt b/shared/src/commonMain/kotlin/net/newpipe/app/platform/PlayerSettingsSections.kt new file mode 100644 index 000000000..58120fc89 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/newpipe/app/platform/PlayerSettingsSections.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import androidx.compose.runtime.Composable + +/** + * Platform-specific contents of the player settings screen: ExoPlayer settings on Android, + * and each other platform's own player settings once it has any. + */ +interface PlayerSettingsSections { + val isAvailable: Boolean + + @Composable + fun Render() +} diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/PlayerSettingsScreen.kt b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/PlayerSettingsScreen.kt index 007981b08..a75a5fe6c 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/PlayerSettingsScreen.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/PlayerSettingsScreen.kt @@ -1,7 +1,7 @@ /* -* SPDX-FileCopyrightText: 2026 NewPipe e.V. -* SPDX-License-Identifier: GPL-3.0-or-later -*/ + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package net.newpipe.app.screen.settings @@ -15,95 +15,25 @@ 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.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.ListPreference -import net.newpipe.app.composable.ListPreferenceEntry -import net.newpipe.app.composable.SwitchPreference import net.newpipe.app.composable.TopAppBar import net.newpipe.app.navigation.Navigator -import net.newpipe.app.preferences.ExoPlayerPreferences -import net.newpipe.app.preview.ThemePreviewProvider -import net.newpipe.app.viewmodel.settings.PlayerSettingsViewModel +import net.newpipe.app.platform.PlayerSettingsSections 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_automatic_info -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_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 PlayerSettingsScreen( navigator: Navigator = koinInject(), - viewModel: PlayerSettingsViewModel = koinViewModel() + sections: PlayerSettingsSections = koinInject() ) { - val progressiveLoadInterval by viewModel.progressiveLoadInterval.collectAsStateWithLifecycle() - val useExoplayerDecoderFallback by viewModel.useExoplayerDecoderFallback.collectAsStateWithLifecycle() - val disableMediaTunneling by viewModel.disableMediaTunneling.collectAsStateWithLifecycle() - val mediaTunnelingAutoDisabled by viewModel.mediaTunnelingAutoDisabled.collectAsStateWithLifecycle() - val alwaysUseSetOutputSurfaceWorkaround by viewModel.alwaysUseSetOutputSurfaceWorkaround.collectAsStateWithLifecycle() - - PlayerSettingsScreenContent( - progressiveLoadInterval = progressiveLoadInterval, - useExoplayerDecoderFallback = useExoplayerDecoderFallback, - disableMediaTunneling = disableMediaTunneling, - mediaTunnelingAutoDisabled = mediaTunnelingAutoDisabled, - alwaysUseSetOutputSurfaceWorkaround = alwaysUseSetOutputSurfaceWorkaround, - onProgressiveLoadIntervalChange = viewModel::setProgressiveLoadInterval, - onUseExoplayerDecoderFallbackChange = viewModel::setUseExoplayerDecoderFallback, - onDisableMediaTunnelingChange = viewModel::setDisableMediaTunneling, - onAlwaysUseSetOutputSurfaceWorkaroundChange = viewModel::setAlwaysUseSetOutputSurfaceWorkaround, - onNavigateUp = { navigator.navigateUp() } - ) -} - -@Composable -fun PlayerSettingsScreenContent( - progressiveLoadInterval: String = ExoPlayerPreferences.DEFAULT_PROGRESSIVE_LOAD_INTERVAL, - useExoplayerDecoderFallback: Boolean = ExoPlayerPreferences.DEFAULT_USE_EXOPLAYER_DECODER_FALLBACK, - disableMediaTunneling: Boolean = ExoPlayerPreferences.DEFAULT_DISABLE_MEDIA_TUNNELING, - mediaTunnelingAutoDisabled: Boolean = false, - alwaysUseSetOutputSurfaceWorkaround: Boolean = ExoPlayerPreferences.DEFAULT_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND, - onProgressiveLoadIntervalChange: (String) -> Unit = {}, - onUseExoplayerDecoderFallbackChange: (Boolean) -> Unit = {}, - onDisableMediaTunnelingChange: (Boolean) -> Unit = {}, - onAlwaysUseSetOutputSurfaceWorkaroundChange: (Boolean) -> Unit = {}, - onNavigateUp: () -> Unit = {} -) { - val exoPlayerDefaultLabel = stringResource(Res.string.progressive_load_interval_exoplayer_default) - - val loadIntervalEntries = ExoPlayerPreferences.PROGRESSIVE_LOAD_INTERVALS.map { value -> - ListPreferenceEntry( - value = value, - title = if (value == ExoPlayerPreferences.PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT) exoPlayerDefaultLabel else "$value KiB" - ) - } - val selectedLoadIntervalTitle = loadIntervalEntries.firstOrNull { it.value == progressiveLoadInterval }?.title.orEmpty() - - val tunnelingBaseSummary = stringResource(Res.string.disable_media_tunneling_summary) - val tunnelingSummary = if (mediaTunnelingAutoDisabled) { - tunnelingBaseSummary + " " + stringResource(Res.string.disable_media_tunneling_automatic_info) - } else { - tunnelingBaseSummary - } - Scaffold( topBar = { TopAppBar( title = stringResource(Res.string.settings_category_exoplayer_title), - onNavigateUp = onNavigateUp + onNavigateUp = { navigator.navigateUp() } ) } ) { paddingValues -> @@ -114,38 +44,7 @@ fun PlayerSettingsScreenContent( .verticalScroll(rememberScrollState()) .padding(WindowInsets.navigationBars.asPaddingValues()) ) { - ListPreference( - title = stringResource(Res.string.progressive_load_interval_title), - entries = loadIntervalEntries, - selectedValue = progressiveLoadInterval, - onValueSelected = onProgressiveLoadIntervalChange, - summary = stringResource(Res.string.progressive_load_interval_summary, selectedLoadIntervalTitle) - ) - SwitchPreference( - title = stringResource(Res.string.use_exoplayer_decoder_fallback_title), - summary = stringResource(Res.string.use_exoplayer_decoder_fallback_summary), - checked = useExoplayerDecoderFallback, - onCheckedChange = onUseExoplayerDecoderFallbackChange - ) - SwitchPreference( - title = stringResource(Res.string.disable_media_tunneling_title), - summary = tunnelingSummary, - checked = disableMediaTunneling, - onCheckedChange = onDisableMediaTunnelingChange - ) - 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), - checked = alwaysUseSetOutputSurfaceWorkaround, - onCheckedChange = onAlwaysUseSetOutputSurfaceWorkaroundChange - ) + sections.Render() } } } - -@PreviewWrapper(ThemePreviewProvider::class) -@PreviewLightDark -@Composable -private fun PlayerSettingsScreenPreview() { - PlayerSettingsScreenContent() -} diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt index dbfdd2e63..c910b6966 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt @@ -33,6 +33,7 @@ import net.newpipe.app.composable.SwitchPreference import net.newpipe.app.composable.TopAppBar import net.newpipe.app.navigation.Destination import net.newpipe.app.navigation.Navigator +import net.newpipe.app.platform.PlayerSettingsSections import net.newpipe.app.preferences.VideoAudioPreferences import net.newpipe.app.preview.ThemePreviewProvider import net.newpipe.app.viewmodel.settings.VideoAudioSettingsViewModel @@ -117,7 +118,8 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun VideoAudioSettingsScreen( navigator: Navigator = koinInject(), - viewModel: VideoAudioSettingsViewModel = koinViewModel() + viewModel: VideoAudioSettingsViewModel = koinViewModel(), + playerSettings: PlayerSettingsSections = koinInject() ) { val defaultResolution by viewModel.defaultResolution.collectAsStateWithLifecycle() val defaultPopupResolution by viewModel.defaultPopupResolution.collectAsStateWithLifecycle() @@ -144,6 +146,7 @@ fun VideoAudioSettingsScreen( val seekDuration by viewModel.seekDuration.collectAsStateWithLifecycle() val clearQueueConfirmation by viewModel.clearQueueConfirmation.collectAsStateWithLifecycle() val ignoreHardwareMediaButtons by viewModel.ignoreHardwareMediaButtons.collectAsStateWithLifecycle() + val playerSettingsAvailable = remember { playerSettings.isAvailable } // Option lists are pure functions of their toggles, so derive them instead of storing them. val resolutionValues = remember(showHigherResolutions) { @@ -210,7 +213,8 @@ fun VideoAudioSettingsScreen( onSeekDurationChange = viewModel::setSeekDuration, onClearQueueConfirmationChange = viewModel::setClearQueueConfirmation, onIgnoreHardwareMediaButtonsChange = viewModel::setIgnoreHardwareMediaButtons, - onOpenExoPlayerSettings = { navigator.navigateTo(Destination.ExoPlayerSettings) }, + playerSettingsAvailable = playerSettingsAvailable, + onOpenPlayerSettings = { navigator.navigateTo(Destination.PlayerSettings) }, onNavigateUp = { navigator.navigateUp() } ) } @@ -270,7 +274,8 @@ fun VideoAudioSettingsScreenContent( onSeekDurationChange: (String) -> Unit = {}, onClearQueueConfirmationChange: (Boolean) -> Unit = {}, onIgnoreHardwareMediaButtonsChange: (Boolean) -> Unit = {}, - onOpenExoPlayerSettings: () -> Unit = {}, + playerSettingsAvailable: Boolean = true, + onOpenPlayerSettings: () -> Unit = {}, onNavigateUp: () -> Unit = {} ) { val scope = rememberCoroutineScope() @@ -399,11 +404,13 @@ fun VideoAudioSettingsScreenContent( onCheckedChange = onPreferDescriptiveAudioChange ) - PreferenceRow( - title = stringResource(Res.string.settings_category_exoplayer_title), - summary = stringResource(Res.string.settings_category_exoplayer_summary), - onClick = onOpenExoPlayerSettings - ) + if (playerSettingsAvailable) { + PreferenceRow( + title = stringResource(Res.string.settings_category_exoplayer_title), + summary = stringResource(Res.string.settings_category_exoplayer_summary), + onClick = onOpenPlayerSettings + ) + } PreferenceCategoryTitle(title = stringResource(Res.string.settings_category_player_title)) SwitchPreference( diff --git a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt deleted file mode 100644 index de2883917..000000000 --- a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 NewPipe e.V. - * 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.performScrollTo -import androidx.compose.ui.test.v2.runComposeUiTest -import com.russhwolf.settings.MapSettings -import com.russhwolf.settings.Settings -import kotlin.test.Test -import net.newpipe.app.extensions.withKoin -import newpipe.shared.generated.resources.Res -import newpipe.shared.generated.resources.disable_media_tunneling_automatic_info -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 org.jetbrains.compose.resources.getString -import org.koin.dsl.module - -@OptIn(ExperimentalTestApi::class) -class ExoPlayerSettingsScreenTest { - - private val emptySettings = module { - single { MapSettings() } - } - - @Test - fun rendersTitleAndLoadIntervalRow() = runComposeUiTest { - withKoin( - modules = listOf(emptySettings), - content = { PlayerSettingsScreenContent() }, - onContent = { - onNodeWithText(getString(Res.string.settings_category_exoplayer_title)).assertIsDisplayed() - onNodeWithText(getString(Res.string.progressive_load_interval_title)).assertIsDisplayed() - } - ) - } - - @Test - fun loadIntervalSummaryShowsSelectedEntryLabel() = runComposeUiTest { - withKoin( - modules = listOf(emptySettings), - content = { PlayerSettingsScreenContent() }, - onContent = { - onNodeWithText(getString(Res.string.progressive_load_interval_summary, "64 KiB")).assertIsDisplayed() - } - ) - } - - @Test - fun mediaTunnelingInfoShownOnlyWhenAutoDisabled() = runComposeUiTest { - withKoin( - modules = listOf(emptySettings), - content = { PlayerSettingsScreenContent(mediaTunnelingAutoDisabled = true) }, - onContent = { - onNodeWithText(getString(Res.string.disable_media_tunneling_automatic_info), substring = true) - .performScrollTo() - .assertIsDisplayed() - } - ) - } -} diff --git a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt index 755f10aa4..55949336a 100644 --- a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt +++ b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt @@ -70,7 +70,7 @@ class VideoAudioSettingsScreenTest { } @Test - fun rendersExoPlayerSettingsLink() = runComposeUiTest { + fun rendersPlayerSettingsLink() = runComposeUiTest { withKoin( modules = listOf(emptySettings), content = { VideoAudioSettingsScreenContent() }, diff --git a/shared/src/iosMain/kotlin/net/newpipe/app/platform/IOSPlayerSettingsSections.kt b/shared/src/iosMain/kotlin/net/newpipe/app/platform/IOSPlayerSettingsSections.kt new file mode 100644 index 000000000..383b5f38e --- /dev/null +++ b/shared/src/iosMain/kotlin/net/newpipe/app/platform/IOSPlayerSettingsSections.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import androidx.compose.runtime.Composable +import org.koin.core.annotation.Singleton + +@Singleton(binds = [PlayerSettingsSections::class]) +class IOSPlayerSettingsSections : PlayerSettingsSections { + override val isAvailable = false + + @Composable + override fun Render() = Unit +} diff --git a/shared/src/jvmMain/kotlin/net/newpipe/app/platform/JVMPlayerSettingsSections.kt b/shared/src/jvmMain/kotlin/net/newpipe/app/platform/JVMPlayerSettingsSections.kt new file mode 100644 index 000000000..351df70a4 --- /dev/null +++ b/shared/src/jvmMain/kotlin/net/newpipe/app/platform/JVMPlayerSettingsSections.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import androidx.compose.runtime.Composable +import org.koin.core.annotation.Singleton + +@Singleton(binds = [PlayerSettingsSections::class]) +class JVMPlayerSettingsSections : PlayerSettingsSections { + override val isAvailable = false + + @Composable + override fun Render() = Unit +}