dynamic player settings ui per platform

This commit is contained in:
Ida Delphine
2026-10-07 16:59:43 +08:00
committed by Aayush Gupta
parent 793a6bc337
commit 4cad0293ca
15 changed files with 285 additions and 188 deletions
+1 -1
View File
@@ -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"
@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* 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()
}
}
@@ -0,0 +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 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()
}
@@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* 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() }
}
@@ -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
@@ -44,7 +44,7 @@ fun navModule() = module {
VideoAudioSettingsScreen()
}
navigation<Destination.ExoPlayerSettings> {
navigation<Destination.PlayerSettings> {
PlayerSettingsScreen()
}
}
@@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* 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()
}
@@ -1,7 +1,7 @@
/*
* 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.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()
}
@@ -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(
@@ -1,67 +0,0 @@
/*
* 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.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<Settings> { 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()
}
)
}
}
@@ -70,7 +70,7 @@ class VideoAudioSettingsScreenTest {
}
@Test
fun rendersExoPlayerSettingsLink() = runComposeUiTest {
fun rendersPlayerSettingsLink() = runComposeUiTest {
withKoin(
modules = listOf(emptySettings),
content = { VideoAudioSettingsScreenContent() },
@@ -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 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
}
@@ -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 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
}