feat(player): таймер сна с плавным затуханием (fade-out)
P0-фича из спеки. PlayerController: startSleepTimer/cancelSleepTimer — в последние 20с экспоненциальный fade-out громкости (frac^2), затем пауза + возврат громкости. В плеере — пилюля «Таймер сна» (иконка Moon): при активном показывает остаток M:SS акцентом. Шторка с интервалами 15/30/45/60/90/120 мин + «Выключить». Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,17 @@ import androidx.media3.exoplayer.audio.DefaultAudioSink
|
||||
import androidx.media3.exoplayer.audio.TeeAudioProcessor
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||
import android.os.SystemClock
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -67,6 +74,18 @@ class PlayerController @Inject constructor(
|
||||
private val _icyTitle = MutableStateFlow<String?>(null)
|
||||
val icyTitle: StateFlow<String?> = _icyTitle.asStateFlow()
|
||||
|
||||
// ── Таймер сна ──
|
||||
// Оставшееся время в мс (null = таймер выключен). В последние FADE_MS звук
|
||||
// плавно затухает (экспоненциальная кривая), затем пауза.
|
||||
private val _sleepRemainingMs = MutableStateFlow<Long?>(null)
|
||||
val sleepRemainingMs: StateFlow<Long?> = _sleepRemainingMs.asStateFlow()
|
||||
private val timerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var sleepJob: Job? = null
|
||||
|
||||
private companion object {
|
||||
const val FADE_MS = 20_000L // длительность плавного затухания в конце таймера
|
||||
}
|
||||
|
||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
var onSkipToNext: (() -> Unit)? = null
|
||||
@@ -227,6 +246,43 @@ class PlayerController @Inject constructor(
|
||||
exoPlayer.replaceMediaItem(0, updatedMediaItem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить таймер сна на [durationMs] мс. В последние FADE_MS звук плавно
|
||||
* затухает (экспоненциально, чтобы спад воспринимался естественно), затем пауза.
|
||||
*/
|
||||
fun startSleepTimer(durationMs: Long) {
|
||||
sleepJob?.cancel()
|
||||
exoPlayer.volume = 1f
|
||||
sleepJob = timerScope.launch {
|
||||
val end = SystemClock.elapsedRealtime() + durationMs
|
||||
while (true) {
|
||||
val remaining = end - SystemClock.elapsedRealtime()
|
||||
if (remaining <= 0L) break
|
||||
_sleepRemainingMs.value = remaining
|
||||
if (remaining <= FADE_MS) {
|
||||
// frac: 1 → 0; экспонента (frac^2) — громкость падает резче к концу.
|
||||
val frac = remaining.toFloat() / FADE_MS
|
||||
exoPlayer.volume = (frac * frac).coerceIn(0f, 1f)
|
||||
delay(150)
|
||||
} else {
|
||||
delay(1_000)
|
||||
}
|
||||
}
|
||||
exoPlayer.pause()
|
||||
exoPlayer.volume = 1f
|
||||
_sleepRemainingMs.value = null
|
||||
sleepJob = null
|
||||
}
|
||||
}
|
||||
|
||||
/** Отменить таймер сна и вернуть полную громкость. */
|
||||
fun cancelSleepTimer() {
|
||||
sleepJob?.cancel()
|
||||
sleepJob = null
|
||||
exoPlayer.volume = 1f
|
||||
_sleepRemainingMs.value = null
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user