polish stream quality settings

- label the rows 'Stream quality: Wi-Fi' / 'Stream quality: Mobile'
- default mobile to 192kbps/mp3 while Wi-Fi stays unlimited/server default
- apply the selection only when Done is pressed, cancelling discards the draft
- fade the bottom of the dialog list so the remaining options are visible as scrollable
This commit is contained in:
austinried
2026-10-04 00:29:59 +00:00
parent 9e25a971e6
commit 95216143dc
3 changed files with 65 additions and 31 deletions
@@ -56,7 +56,9 @@ class UserPreferences(
fun streamQuality(mode: NetworkMode): Flow<StreamQuality> = fun streamQuality(mode: NetworkMode): Flow<StreamQuality> =
store.data.map { prefs -> store.data.map { prefs ->
prefs[streamQualityKey(mode)]?.let(::decodeStreamQuality) ?: legacyStreamQuality(prefs) prefs[streamQualityKey(mode)]?.let(::decodeStreamQuality)
?: legacyStreamQuality(prefs)
?: defaultStreamQuality(mode)
} }
suspend fun setStreamQuality( suspend fun setStreamQuality(
@@ -82,11 +84,21 @@ class UserPreferences(
) )
} }
private fun legacyStreamQuality(prefs: Preferences): StreamQuality = private fun legacyStreamQuality(prefs: Preferences): StreamQuality? {
StreamQuality( val bitrate = prefs[LEGACY_BITRATE]
maxBitrate = prefs[LEGACY_BITRATE] ?: 0, val format = prefs[LEGACY_FORMAT]
format = prefs[LEGACY_FORMAT]?.takeIf { it.isNotEmpty() }, if (bitrate == null && format == null) return null
return StreamQuality(
maxBitrate = bitrate ?: 0,
format = format?.takeIf { it.isNotEmpty() },
) )
}
private fun defaultStreamQuality(mode: NetworkMode): StreamQuality =
when (mode) {
NetworkMode.Wifi -> StreamQuality()
NetworkMode.Mobile -> StreamQuality(maxBitrate = 192, format = "mp3")
}
private fun decode( private fun decode(
stored: String?, stored: String?,
@@ -1,7 +1,9 @@
package com.subtracks.ui.settings package com.subtracks.ui.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -39,6 +41,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -150,7 +153,7 @@ fun SettingsScreen(
item { SectionHeader("Network") } item { SectionHeader("Network") }
item { item {
ListItem( ListItem(
headlineContent = { Text("Wi-Fi") }, headlineContent = { Text("Stream quality: Wi-Fi") },
supportingContent = { Text(qualityLabel(wifiQuality)) }, supportingContent = { Text(qualityLabel(wifiQuality)) },
modifier = Modifier.clickable { dialog = SettingsDialog.WifiQuality }, modifier = Modifier.clickable { dialog = SettingsDialog.WifiQuality },
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
@@ -158,7 +161,7 @@ fun SettingsScreen(
} }
item { item {
ListItem( ListItem(
headlineContent = { Text("Mobile data") }, headlineContent = { Text("Stream quality: Mobile") },
supportingContent = { Text(qualityLabel(mobileQuality)) }, supportingContent = { Text(qualityLabel(mobileQuality)) },
modifier = Modifier.clickable { dialog = SettingsDialog.MobileQuality }, modifier = Modifier.clickable { dialog = SettingsDialog.MobileQuality },
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
@@ -171,7 +174,7 @@ fun SettingsScreen(
when (dialog) { when (dialog) {
SettingsDialog.WifiQuality -> { SettingsDialog.WifiQuality -> {
QualityDialog( QualityDialog(
title = "Wi-Fi", title = "Stream quality: Wi-Fi",
quality = wifiQuality, quality = wifiQuality,
onSelect = onWifiQualityChange, onSelect = onWifiQualityChange,
onDismiss = { dialog = null }, onDismiss = { dialog = null },
@@ -180,7 +183,7 @@ fun SettingsScreen(
SettingsDialog.MobileQuality -> { SettingsDialog.MobileQuality -> {
QualityDialog( QualityDialog(
title = "Mobile data", title = "Stream quality: Mobile",
quality = mobileQuality, quality = mobileQuality,
onSelect = onMobileQualityChange, onSelect = onMobileQualityChange,
onDismiss = { dialog = null }, onDismiss = { dialog = null },
@@ -205,34 +208,53 @@ private fun QualityDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
var draft by remember(quality) { mutableStateOf(quality) } var draft by remember(quality) { mutableStateOf(quality) }
val scrollState = rememberScrollState()
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(title) }, title = { Text(title) },
text = { text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Box {
ChoiceGroup( Column(modifier = Modifier.verticalScroll(scrollState)) {
header = "Maximum bitrate", ChoiceGroup(
options = bitrateOptions.map { it to bitrateLabel(it) }, header = "Maximum bitrate",
selected = draft.maxBitrate, options = bitrateOptions.map { it to bitrateLabel(it) },
onSelect = { selected = draft.maxBitrate,
draft = draft.copy(maxBitrate = it) onSelect = { draft = draft.copy(maxBitrate = it) },
onSelect(draft) )
}, Spacer(Modifier.height(16.dp))
) ChoiceGroup(
Spacer(Modifier.height(16.dp)) header = "Preferred format",
ChoiceGroup( options = streamFormats.map { it to (it ?: "Use server default") },
header = "Preferred format", selected = draft.format,
options = streamFormats.map { it to (it ?: "Use server default") }, onSelect = { draft = draft.copy(format = it) },
selected = draft.format, )
onSelect = { }
draft = draft.copy(format = it) if (scrollState.canScrollForward) {
onSelect(draft) Box(
}, modifier =
) Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(32.dp)
.background(
Brush.verticalGradient(
listOf(Color.Transparent, MaterialTheme.colorScheme.surfaceContainerHigh),
),
),
)
}
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onDismiss) { Text("Done") } TextButton(
onClick = {
onSelect(draft)
onDismiss()
},
) { Text("Done") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
}, },
) )
} }
@@ -50,7 +50,7 @@ class UserPreferencesTest {
val prefs = UserPreferences(store) val prefs = UserPreferences(store)
assertEquals(StreamQuality(), prefs.streamQuality(NetworkMode.Wifi).first()) assertEquals(StreamQuality(), prefs.streamQuality(NetworkMode.Wifi).first())
assertEquals(StreamQuality(), prefs.streamQuality(NetworkMode.Mobile).first()) assertEquals(StreamQuality(192, "mp3"), prefs.streamQuality(NetworkMode.Mobile).first())
prefs.setStreamQuality(NetworkMode.Wifi, StreamQuality(320, null)) prefs.setStreamQuality(NetworkMode.Wifi, StreamQuality(320, null))
prefs.setStreamQuality(NetworkMode.Mobile, StreamQuality(96, "opus")) prefs.setStreamQuality(NetworkMode.Mobile, StreamQuality(96, "opus"))