refactor(alarms): point the editor at the moved ringtone picker
Completes the move: the old ui/alarms copies are gone and the editor imports the shared one. Timers pick a sound the same way alarms do.
This commit is contained in:
@@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
||||
import de.jeanlucmakiola.clockula.ui.common.RingtonePickerScreen
|
||||
import de.jeanlucmakiola.clockula.ui.common.rememberAlarmTimeFormatter
|
||||
import de.jeanlucmakiola.clockula.ui.shell.LocalLivePillInset
|
||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||
|
||||
@@ -14,6 +14,8 @@ import de.jeanlucmakiola.clockula.domain.ClockDefaults
|
||||
import de.jeanlucmakiola.clockula.domain.DismissChallenge
|
||||
import de.jeanlucmakiola.clockula.domain.Ringtones
|
||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
||||
import de.jeanlucmakiola.clockula.ui.common.RingtonePicker
|
||||
import de.jeanlucmakiola.clockula.ui.common.RingtonePickerUiState
|
||||
import de.jeanlucmakiola.floret.di.ApplicationScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
package de.jeanlucmakiola.clockula.ui.alarms
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.Ringtones
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.floret.components.GroupedListInset
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.floret.components.SelectedCheck
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
|
||||
/**
|
||||
* The alarm sound picker: App default, Silent, the user's own file when the
|
||||
* device does not list it, and the device's own alarm tones — over a
|
||||
* `LazyColumn`, because a device's tone list can be hundreds of rows (M5 D17).
|
||||
*
|
||||
* "Choose from files…" is an action row rather than an option, so the option
|
||||
* list stays a list of selectable states. Each unselected row can be
|
||||
* auditioned through the play button; taking a row stops the preview and
|
||||
* closes the picker (D21).
|
||||
*
|
||||
* The SAF launcher is **not** registered here: this screen only exists while
|
||||
* the ViewModel says the picker is open, and that flag does not survive process
|
||||
* death — a result restored into a fresh process would have nowhere to land.
|
||||
* [onPickFile] comes from the host, which registers it unconditionally (D20).
|
||||
*/
|
||||
@Composable
|
||||
internal fun RingtonePickerScreen(
|
||||
state: RingtonePickerUiState,
|
||||
defaultUri: String?,
|
||||
onChoose: (String?) -> Unit,
|
||||
onPreview: (String?) -> Unit,
|
||||
onPickFile: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
FullScreenPicker(
|
||||
title = stringResource(R.string.ringtone_picker_title),
|
||||
onDismiss = onDismiss,
|
||||
scrollable = false,
|
||||
) {
|
||||
val rows = state.options.size + 1
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
if (state.deviceHasNoSounds) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.ringtone_none_available),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = GroupedListInset,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.loading) {
|
||||
item {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(GroupedListInset),
|
||||
)
|
||||
}
|
||||
}
|
||||
ringtoneOptions(state, rows, onChoose, onPreview, defaultUri)
|
||||
item {
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.ringtone_pick_file),
|
||||
position = positionOf(rows - 1, rows),
|
||||
onClick = onPickFile,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.ringtoneOptions(
|
||||
state: RingtonePickerUiState,
|
||||
rows: Int,
|
||||
onChoose: (String?) -> Unit,
|
||||
onPreview: (String?) -> Unit,
|
||||
defaultUri: String?,
|
||||
) {
|
||||
state.options.forEachIndexed { index, option ->
|
||||
item(key = optionKey(option)) {
|
||||
RingtoneOptionRow(
|
||||
option = option,
|
||||
position = positionOf(index, rows),
|
||||
selected = RingtonePicker.isSelected(option, state.selectedUri),
|
||||
defaultUri = defaultUri,
|
||||
defaultTitle = state.defaultTitle,
|
||||
onChoose = onChoose,
|
||||
onPreview = onPreview,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun optionKey(option: RingtoneChoice): String = when (option) {
|
||||
RingtoneChoice.AppDefault -> "app-default"
|
||||
RingtoneChoice.Silent -> "silent"
|
||||
is RingtoneChoice.Sound -> option.uri
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RingtoneOptionRow(
|
||||
option: RingtoneChoice,
|
||||
position: Position,
|
||||
selected: Boolean,
|
||||
defaultUri: String?,
|
||||
defaultTitle: String?,
|
||||
onChoose: (String?) -> Unit,
|
||||
onPreview: (String?) -> Unit,
|
||||
) {
|
||||
val unknown = stringResource(R.string.ringtone_unknown)
|
||||
val deviceDefault = stringResource(R.string.ringtone_device_default)
|
||||
val silentTitle = stringResource(R.string.ringtone_silent)
|
||||
val title = when (option) {
|
||||
RingtoneChoice.AppDefault -> stringResource(R.string.override_app_default)
|
||||
RingtoneChoice.Silent -> stringResource(R.string.ringtone_silent)
|
||||
is RingtoneChoice.Sound -> option.title.ifBlank { unknown }
|
||||
}
|
||||
val summary = when (option) {
|
||||
// What "App default" currently resolves to, so inheriting is not blind.
|
||||
// The title is the *resolved* one, not a scan of the device's tone list:
|
||||
// a SAF-picked default is absent from that list, and the row would then
|
||||
// claim the device default instead (M5 D16).
|
||||
RingtoneChoice.AppDefault -> when {
|
||||
defaultUri == null -> deviceDefault
|
||||
Ringtones.isSilent(defaultUri) -> silentTitle
|
||||
else -> defaultTitle ?: unknown
|
||||
}
|
||||
RingtoneChoice.Silent -> stringResource(R.string.ringtone_silent_summary)
|
||||
is RingtoneChoice.Sound -> null
|
||||
}
|
||||
val previewUri = when (option) {
|
||||
RingtoneChoice.AppDefault -> defaultUri
|
||||
RingtoneChoice.Silent -> null
|
||||
is RingtoneChoice.Sound -> option.uri
|
||||
}
|
||||
val chosenUri = when (option) {
|
||||
RingtoneChoice.AppDefault -> null
|
||||
RingtoneChoice.Silent -> Ringtones.SILENT_URI
|
||||
is RingtoneChoice.Sound -> option.uri
|
||||
}
|
||||
|
||||
GroupedRow(
|
||||
title = title,
|
||||
summary = summary,
|
||||
position = position,
|
||||
selected = selected,
|
||||
trailing = when {
|
||||
selected -> {
|
||||
{ SelectedCheck() }
|
||||
}
|
||||
previewUri != null -> {
|
||||
{
|
||||
IconButton(onClick = { onPreview(previewUri) }) {
|
||||
Icon(Icons.Rounded.PlayArrow, contentDescription = title)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
onClick = { onChoose(chosenUri) },
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package de.jeanlucmakiola.clockula.ui.alarms
|
||||
|
||||
import de.jeanlucmakiola.clockula.domain.RingtoneOption
|
||||
import de.jeanlucmakiola.clockula.domain.Ringtones
|
||||
|
||||
sealed interface RingtoneChoice {
|
||||
data object AppDefault : RingtoneChoice
|
||||
data object Silent : RingtoneChoice
|
||||
data class Sound(val uri: String, val title: String) : RingtoneChoice
|
||||
}
|
||||
|
||||
data class RingtonePickerUiState(
|
||||
val loading: Boolean,
|
||||
val options: List<RingtoneChoice>,
|
||||
/** The alarm's stored override: null = App default, sentinel = Silent. */
|
||||
val selectedUri: String?,
|
||||
/** The device reported no alarm sounds at all (M5 D19). */
|
||||
val deviceHasNoSounds: Boolean,
|
||||
/**
|
||||
* The app default's own resolved title, or null when nothing named it — so
|
||||
* the "App default" row can say what inheriting means even when the default
|
||||
* is a SAF-picked file the device's tone list does not contain (M5 D16).
|
||||
*/
|
||||
val defaultTitle: String?,
|
||||
)
|
||||
|
||||
internal object RingtonePicker {
|
||||
/** Pure. App default, Silent, the stored sound when absent from [deviceTones], then [deviceTones]. */
|
||||
fun optionsFor(
|
||||
deviceTones: List<RingtoneOption>,
|
||||
storedUri: String?,
|
||||
storedTitle: String?,
|
||||
): List<RingtoneChoice> {
|
||||
val stored = Ringtones.normalise(storedUri)
|
||||
// The user's own sound sits directly under Silent when the device does
|
||||
// not list it — a SAF-picked file would otherwise be invisible, and so
|
||||
// uncheckable, in the picker that chose it (M5 D17).
|
||||
val own = stored
|
||||
?.takeUnless { Ringtones.isSilent(it) }
|
||||
?.takeUnless { uri -> deviceTones.any { it.uri == uri } }
|
||||
?.let { RingtoneChoice.Sound(it, storedTitle.orEmpty()) }
|
||||
|
||||
val sounds = (listOfNotNull(own) + deviceTones.map { RingtoneChoice.Sound(it.uri, it.title) })
|
||||
.distinctBy { it.uri }
|
||||
return listOf(RingtoneChoice.AppDefault, RingtoneChoice.Silent) + sounds
|
||||
}
|
||||
|
||||
fun isSelected(option: RingtoneChoice, storedUri: String?): Boolean = when (option) {
|
||||
RingtoneChoice.AppDefault -> storedUri == null
|
||||
RingtoneChoice.Silent -> Ringtones.isSilent(storedUri)
|
||||
is RingtoneChoice.Sound -> option.uri == storedUri
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user