add video audio screen and viewmodel

This commit is contained in:
Ida Delphine
2026-10-07 16:59:43 +08:00
committed by Aayush Gupta
parent 1baecc704b
commit ac3fb1d8ab
4 changed files with 369 additions and 0 deletions
@@ -74,4 +74,19 @@
<string name="card">Card</string>
<string name="cancel">Cancel</string>
<!-- Video/Audio Settings -->
<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">Limit resolution when using mobile data</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">Select the original audio track regardless of the language</string>
<string name="prefer_descriptive_audio_title">Prefer descriptive audio</string>
<string name="prefer_descriptive_audio_summary">Select an audio track with descriptions for visually impaired people if available</string>
<string name="best_resolution">Best resolution</string>
<string name="limit_data_usage_none_description">No limit</string>
</resources>
@@ -16,6 +16,7 @@ object VideoAudioPreferences {
const val KEY_SHOW_HIGHER_RESOLUTIONS = "show_higher_resolutions"
const val KEY_DEFAULT_VIDEO_FORMAT = "default_video_format"
const val KEY_DEFAULT_AUDIO_FORMAT = "default_audio_format"
const val KEY_PREFER_ORIGINAL_AUDIO = "prefer_original_audio"
const val KEY_PREFER_DESCRIPTIVE_AUDIO = "prefer_descriptive_audio"
// Special resolution values that need a localized label
@@ -0,0 +1,205 @@
/*
* 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.Column
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.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.VideoAudioPreferences
import net.newpipe.app.preview.ThemePreviewProvider
import net.newpipe.app.viewmodel.settings.VideoAudioSettingsViewModel
import newpipe.shared.generated.resources.Res
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_description
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.settings_category_video_audio_title
import newpipe.shared.generated.resources.show_higher_resolutions_summary
import newpipe.shared.generated.resources.show_higher_resolutions_title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun VideoAudioSettingsScreen(
navigator: Navigator = koinInject(),
viewModel: VideoAudioSettingsViewModel = koinViewModel()
) {
val defaultResolution by viewModel.defaultResolution.collectAsStateWithLifecycle()
val defaultPopupResolution by viewModel.defaultPopupResolution.collectAsStateWithLifecycle()
val mobileDataResolution by viewModel.mobileDataResolution.collectAsStateWithLifecycle()
val showHigherResolutions by viewModel.showHigherResolutions.collectAsStateWithLifecycle()
val resolutionValues by viewModel.resolutionValues.collectAsStateWithLifecycle()
val mobileDataResolutionValues by viewModel.mobileDataResolutionValues.collectAsStateWithLifecycle()
val videoFormat by viewModel.videoFormat.collectAsStateWithLifecycle()
val audioFormat by viewModel.audioFormat.collectAsStateWithLifecycle()
val preferOriginalAudio by viewModel.preferOriginalAudio.collectAsStateWithLifecycle()
val preferDescriptiveAudio by viewModel.preferDescriptiveAudio.collectAsStateWithLifecycle()
VideoAudioSettingsScreenContent(
defaultResolution = defaultResolution,
defaultPopupResolution = defaultPopupResolution,
mobileDataResolution = mobileDataResolution,
showHigherResolutions = showHigherResolutions,
resolutionValues = resolutionValues,
mobileDataResolutionValues = mobileDataResolutionValues,
videoFormat = videoFormat,
audioFormat = audioFormat,
preferOriginalAudio = preferOriginalAudio,
preferDescriptiveAudio = preferDescriptiveAudio,
onDefaultResolutionChange = viewModel::setDefaultResolution,
onDefaultPopupResolutionChange = viewModel::setDefaultPopupResolution,
onMobileDataResolutionChange = viewModel::setMobileDataResolution,
onShowHigherResolutionsChange = viewModel::setShowHigherResolutions,
onVideoFormatChange = viewModel::setVideoFormat,
onAudioFormatChange = viewModel::setAudioFormat,
onPreferOriginalAudioChange = viewModel::setPreferOriginalAudio,
onPreferDescriptiveAudioChange = viewModel::setPreferDescriptiveAudio,
onNavigateUp = { navigator.navigateUp() }
)
}
@Composable
fun VideoAudioSettingsScreenContent(
defaultResolution: String = VideoAudioPreferences.DEFAULT_RESOLUTION,
defaultPopupResolution: String = VideoAudioPreferences.DEFAULT_POPUP_RESOLUTION,
mobileDataResolution: String = VideoAudioPreferences.DEFAULT_LIMIT_MOBILE_DATA_USAGE,
showHigherResolutions: Boolean = VideoAudioPreferences.DEFAULT_SHOW_HIGHER_RESOLUTIONS,
resolutionValues: List<String> = VideoAudioPreferences.BASE_RESOLUTIONS,
mobileDataResolutionValues: List<String> = VideoAudioPreferences.BASE_MOBILE_DATA_RESOLUTIONS,
videoFormat: String = VideoAudioPreferences.DEFAULT_VIDEO_FORMAT,
audioFormat: String = VideoAudioPreferences.DEFAULT_AUDIO_FORMAT,
preferOriginalAudio: Boolean = VideoAudioPreferences.DEFAULT_PREFER_ORIGINAL_AUDIO,
preferDescriptiveAudio: Boolean = VideoAudioPreferences.DEFAULT_PREFER_DESCRIPTIVE_AUDIO,
onDefaultResolutionChange: (String) -> Unit = {},
onDefaultPopupResolutionChange: (String) -> Unit = {},
onMobileDataResolutionChange: (String) -> Unit = {},
onShowHigherResolutionsChange: (Boolean) -> Unit = {},
onVideoFormatChange: (String) -> Unit = {},
onAudioFormatChange: (String) -> Unit = {},
onPreferOriginalAudioChange: (Boolean) -> Unit = {},
onPreferDescriptiveAudioChange: (Boolean) -> Unit = {},
onNavigateUp: () -> Unit = {}
) {
val bestResolutionLabel = stringResource(Res.string.best_resolution)
val noLimitLabel = stringResource(Res.string.limit_data_usage_none_description)
fun resolutionEntry(value: String) = ListPreferenceEntry(
value = value,
title = if (value == VideoAudioPreferences.BEST_RESOLUTION) bestResolutionLabel else value
)
fun mobileDataEntry(value: String) = ListPreferenceEntry(
value = value,
title = if (value == VideoAudioPreferences.LIMIT_DATA_USAGE_NONE) noLimitLabel else value
)
val resolutionEntries = resolutionValues.map(::resolutionEntry)
val mobileDataEntries = mobileDataResolutionValues.map(::mobileDataEntry)
val videoFormatEntries = VideoAudioPreferences.VIDEO_FORMATS.map { (value, label) ->
ListPreferenceEntry(value, label)
}
val audioFormatEntries = VideoAudioPreferences.AUDIO_FORMATS.map { (value, label) ->
ListPreferenceEntry(value, label)
}
Scaffold(
topBar = {
TopAppBar(
title = stringResource(Res.string.settings_category_video_audio_title),
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
.verticalScroll(rememberScrollState())
.padding(WindowInsets.navigationBars.asPaddingValues())
) {
ListPreference(
title = stringResource(Res.string.default_resolution_title),
entries = resolutionEntries,
selectedValue = defaultResolution,
onValueSelected = onDefaultResolutionChange
)
ListPreference(
title = stringResource(Res.string.default_popup_resolution_title),
entries = resolutionEntries,
selectedValue = defaultPopupResolution,
onValueSelected = onDefaultPopupResolutionChange
)
ListPreference(
title = stringResource(Res.string.limit_mobile_data_usage_title),
entries = mobileDataEntries,
selectedValue = mobileDataResolution,
onValueSelected = onMobileDataResolutionChange
)
SwitchPreference(
title = stringResource(Res.string.show_higher_resolutions_title),
summary = stringResource(Res.string.show_higher_resolutions_summary),
checked = showHigherResolutions,
onCheckedChange = onShowHigherResolutionsChange
)
ListPreference(
title = stringResource(Res.string.default_video_format_title),
entries = videoFormatEntries,
selectedValue = videoFormat,
onValueSelected = onVideoFormatChange
)
ListPreference(
title = stringResource(Res.string.default_audio_format_title),
entries = audioFormatEntries,
selectedValue = audioFormat,
onValueSelected = onAudioFormatChange
)
SwitchPreference(
title = stringResource(Res.string.prefer_original_audio_title),
summary = stringResource(Res.string.prefer_original_audio_summary),
checked = preferOriginalAudio,
onCheckedChange = onPreferOriginalAudioChange
)
SwitchPreference(
title = stringResource(Res.string.prefer_descriptive_audio_title),
summary = stringResource(Res.string.prefer_descriptive_audio_summary),
checked = preferDescriptiveAudio,
onCheckedChange = onPreferDescriptiveAudioChange
)
}
}
}
@PreviewWrapper(ThemePreviewProvider::class)
@PreviewLightDark
@Composable
private fun VideoAudioSettingsScreenPreview() {
VideoAudioSettingsScreenContent()
}
@@ -0,0 +1,148 @@
/*
* 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 com.russhwolf.settings.Settings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import net.newpipe.app.preferences.VideoAudioPreferences
import org.koin.core.annotation.KoinViewModel
@KoinViewModel
class VideoAudioSettingsViewModel(private val settings: Settings) : ViewModel() {
val defaultResolution: StateFlow<String>
field = MutableStateFlow(
settings.getString(
VideoAudioPreferences.KEY_DEFAULT_RESOLUTION,
VideoAudioPreferences.DEFAULT_RESOLUTION
)
)
val defaultPopupResolution: StateFlow<String>
field = MutableStateFlow(
settings.getString(
VideoAudioPreferences.KEY_DEFAULT_POPUP_RESOLUTION,
VideoAudioPreferences.DEFAULT_POPUP_RESOLUTION
)
)
val mobileDataResolution: StateFlow<String>
field = MutableStateFlow(
settings.getString(
VideoAudioPreferences.KEY_LIMIT_MOBILE_DATA_USAGE,
VideoAudioPreferences.DEFAULT_LIMIT_MOBILE_DATA_USAGE
)
)
val showHigherResolutions: StateFlow<Boolean>
field = MutableStateFlow(
settings.getBoolean(
VideoAudioPreferences.KEY_SHOW_HIGHER_RESOLUTIONS,
VideoAudioPreferences.DEFAULT_SHOW_HIGHER_RESOLUTIONS
)
)
/** Resolution values available for the default & popup pickers; grows with [showHigherResolutions]. */
val resolutionValues: StateFlow<List<String>>
field = MutableStateFlow(VideoAudioPreferences.resolutions(showHigherResolutions.value))
/** Resolution values for the mobile-data limit picker; grows with [showHigherResolutions]. */
val mobileDataResolutionValues: StateFlow<List<String>>
field = MutableStateFlow(VideoAudioPreferences.mobileDataResolutions(showHigherResolutions.value))
val videoFormat: StateFlow<String>
field = MutableStateFlow(
settings.getString(
VideoAudioPreferences.KEY_DEFAULT_VIDEO_FORMAT,
VideoAudioPreferences.DEFAULT_VIDEO_FORMAT
)
)
val audioFormat: StateFlow<String>
field = MutableStateFlow(
settings.getString(
VideoAudioPreferences.KEY_DEFAULT_AUDIO_FORMAT,
VideoAudioPreferences.DEFAULT_AUDIO_FORMAT
)
)
val preferOriginalAudio: StateFlow<Boolean>
field = MutableStateFlow(
settings.getBoolean(
VideoAudioPreferences.KEY_PREFER_ORIGINAL_AUDIO,
VideoAudioPreferences.DEFAULT_PREFER_ORIGINAL_AUDIO
)
)
val preferDescriptiveAudio: StateFlow<Boolean>
field = MutableStateFlow(
settings.getBoolean(
VideoAudioPreferences.KEY_PREFER_DESCRIPTIVE_AUDIO,
VideoAudioPreferences.DEFAULT_PREFER_DESCRIPTIVE_AUDIO
)
)
fun setDefaultResolution(value: String) {
settings.putString(VideoAudioPreferences.KEY_DEFAULT_RESOLUTION, value)
defaultResolution.value = value
}
fun setDefaultPopupResolution(value: String) {
settings.putString(VideoAudioPreferences.KEY_DEFAULT_POPUP_RESOLUTION, value)
defaultPopupResolution.value = value
}
fun setMobileDataResolution(value: String) {
settings.putString(VideoAudioPreferences.KEY_LIMIT_MOBILE_DATA_USAGE, value)
mobileDataResolution.value = value
}
/**
* Toggling higher resolutions rebuilds the pickers' option lists. When turning the option
* off, any currently selected high resolution is reset to the top entry, matching the legacy
* screen (which called `setValueIndex(0)`).
*/
fun setShowHigherResolutions(value: Boolean) {
settings.putBoolean(VideoAudioPreferences.KEY_SHOW_HIGHER_RESOLUTIONS, value)
showHigherResolutions.value = value
resolutionValues.value = VideoAudioPreferences.resolutions(value)
mobileDataResolutionValues.value = VideoAudioPreferences.mobileDataResolutions(value)
if (!value) {
if (defaultResolution.value in VideoAudioPreferences.HIGH_RESOLUTIONS) {
setDefaultResolution(VideoAudioPreferences.BEST_RESOLUTION)
}
if (defaultPopupResolution.value in VideoAudioPreferences.HIGH_RESOLUTIONS) {
setDefaultPopupResolution(VideoAudioPreferences.BEST_RESOLUTION)
}
if (mobileDataResolution.value in VideoAudioPreferences.HIGH_RESOLUTIONS) {
setMobileDataResolution(VideoAudioPreferences.LIMIT_DATA_USAGE_NONE)
}
}
}
fun setVideoFormat(value: String) {
settings.putString(VideoAudioPreferences.KEY_DEFAULT_VIDEO_FORMAT, value)
videoFormat.value = value
}
fun setAudioFormat(value: String) {
settings.putString(VideoAudioPreferences.KEY_DEFAULT_AUDIO_FORMAT, value)
audioFormat.value = value
}
fun setPreferOriginalAudio(value: Boolean) {
settings.putBoolean(VideoAudioPreferences.KEY_PREFER_ORIGINAL_AUDIO, value)
preferOriginalAudio.value = value
}
fun setPreferDescriptiveAudio(value: Boolean) {
settings.putBoolean(VideoAudioPreferences.KEY_PREFER_DESCRIPTIVE_AUDIO, value)
preferDescriptiveAudio.value = value
}
}