feat(player): звуки для сна (белый/розовый/коричневый шум) + Smart Sleep Fade
SleepSoundPlayer — процедурная генерация цветного шума через AudioTrack (розовый — фильтр Келлета, коричневый — random walk). В таймере сна выбор звука: радио плавно перетекает в выбранный шум (кроссфейд ≤90с), шум играет, к концу затухает — как в спеке («Smart Sleep Fade»). В шторке таймера — чипы выбора звука. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,7 @@ fun PlayerBottomSheet(
|
||||
var showLyrics by remember { mutableStateOf(false) }
|
||||
var showQuality by remember { mutableStateOf(false) }
|
||||
var showSleep by remember { mutableStateOf(false) }
|
||||
var selectedSound by remember { mutableStateOf<com.radiola.service.SleepSound?>(null) }
|
||||
val currentQuality by viewModel.currentQuality.collectAsState()
|
||||
val sleepRemainingMs by viewModel.sleepRemainingMs.collectAsState()
|
||||
val spectrum by viewModel.spectrum.collectAsState()
|
||||
@@ -526,11 +527,23 @@ fun PlayerBottomSheet(
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Музыка плавно затихнет к концу и поставится на паузу.",
|
||||
text = "Музыка плавно затихнет к концу. Можно мягко перейти на звук для сна.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colors.textSecondary,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
// Выбор звука для сна: радио плавно перетечёт в выбранный шум.
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
) {
|
||||
item {
|
||||
SoundChip("Без звука", selectedSound == null) { selectedSound = null }
|
||||
}
|
||||
items(com.radiola.service.SleepSound.entries) { snd ->
|
||||
SoundChip(snd.title, selectedSound == snd) { selectedSound = snd }
|
||||
}
|
||||
}
|
||||
// Если активен — показываем остаток и кнопку отмены
|
||||
if (sleepRemainingMs != null) {
|
||||
SleepRow(
|
||||
@@ -550,7 +563,7 @@ fun PlayerBottomSheet(
|
||||
selected = false,
|
||||
onClick = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
viewModel.startSleepTimer(min)
|
||||
viewModel.startSleepTimer(min, selectedSound)
|
||||
showSleep = false
|
||||
}
|
||||
)
|
||||
@@ -601,6 +614,27 @@ private fun SleepRow(
|
||||
}
|
||||
}
|
||||
|
||||
/** Чип выбора звука для сна. */
|
||||
@Composable
|
||||
private fun SoundChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
val colors = RadiolaTheme.colors
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = if (selected) colors.bgBase else colors.textSecondary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(if (selected) colors.accent else colors.surface2)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 9.dp)
|
||||
)
|
||||
}
|
||||
|
||||
/** Форматирует оставшееся время таймера сна в M:SS / MM:SS. */
|
||||
private fun formatSleep(ms: Long): String {
|
||||
val total = (ms / 1000).coerceAtLeast(0)
|
||||
|
||||
Reference in New Issue
Block a user