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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
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.common.rememberAlarmTimeFormatter
|
||||||
import de.jeanlucmakiola.clockula.ui.shell.LocalLivePillInset
|
import de.jeanlucmakiola.clockula.ui.shell.LocalLivePillInset
|
||||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
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.DismissChallenge
|
||||||
import de.jeanlucmakiola.clockula.domain.Ringtones
|
import de.jeanlucmakiola.clockula.domain.Ringtones
|
||||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
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 de.jeanlucmakiola.floret.di.ApplicationScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
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