Files
FitnessDroid/app/src/main/java/eu/brassepc/fitnessdroid/data/RestTimer.kt
claude 984e2374f8
Some checks failed
release / build-release (push) Has been cancelled
M2: passläge, offline-först med synk-kö, vilotimer med inställningar
- Room-cache: övningar/muskelgrupper/favoritkort + lokal-först-pass
- SyncEngine: FIFO-op-kö mot API:t, retry med backoff, synk-status per set
  (moln-ikon) + global räknare; nätfel loggar inte ut användaren
- Passläge: en övning i taget (svep), ±steppers, RPE, förifyllda värden
  från förra passet, logga set med ett tryck
- Vilotimer: helskärm eller banner (inställning), larm/vibration/visuell/
  inget vid utgång, ±15s, per-övnings vilotid via API:ts defaultRestSeconds
- Övningsväljare: sök + muskelgrupp/muskel-chips med ikoner (API:ts iconKey
  med namnheuristik som fallback), favoriter först, KG/REPS/TID/M-brickor
- Navigation: bottenrad + start-FAB + minispelare, hemskärm med favoritpass
- Inställningsskärm under Profil; historik-lista; version 0.2.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
2026-07-24 13:29:17 +02:00

103 lines
3.4 KiB
Kotlin

package eu.brassepc.fitnessdroid.data
import android.content.Context
import android.media.RingtoneManager
import android.os.VibrationEffect
import android.os.VibratorManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
data class RestState(
val totalSeconds: Int,
val endsAtEpochMs: Long,
val secondsLeft: Int,
val finished: Boolean = false,
)
/**
* Vilotimern. Räknar mot en absolut sluttid (överlever att appen pausas)
* och larmar enligt inställningarna när tiden är ute.
*/
class RestTimerController(
private val context: Context,
private val settingsStore: SettingsStore,
private val scope: CoroutineScope,
) {
private val _state = MutableStateFlow<RestState?>(null)
val state: StateFlow<RestState?> = _state
private var ticker: Job? = null
fun start(seconds: Int? = null) {
scope.launch {
val settings = settingsStore.settings.first()
val total = seconds ?: settings.defaultRestSeconds
_state.value = RestState(
totalSeconds = total,
endsAtEpochMs = System.currentTimeMillis() + total * 1000L,
secondsLeft = total,
)
startTicker()
}
}
fun adjust(deltaSeconds: Int) {
val current = _state.value ?: return
if (current.finished) return
_state.value = current.copy(
endsAtEpochMs = (current.endsAtEpochMs + deltaSeconds * 1000L)
.coerceAtLeast(System.currentTimeMillis()),
totalSeconds = (current.totalSeconds + deltaSeconds).coerceAtLeast(5),
)
}
fun dismiss() {
ticker?.cancel()
_state.value = null
}
private fun startTicker() {
ticker?.cancel()
ticker = scope.launch {
while (true) {
val current = _state.value ?: return@launch
val left = (((current.endsAtEpochMs - System.currentTimeMillis()) + 999) / 1000)
.coerceAtLeast(0).toInt()
if (left != current.secondsLeft) {
_state.value = current.copy(secondsLeft = left)
}
if (left <= 0) {
_state.value = _state.value?.copy(secondsLeft = 0, finished = true)
alert()
// Låt "klart"-läget synas en stund, försvinn sedan självmant.
delay(8_000)
if (_state.value?.finished == true) _state.value = null
return@launch
}
delay(250)
}
}
}
private suspend fun alert() {
when (settingsStore.settings.first().restAlert) {
RestAlert.SOUND -> runCatching {
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
RingtoneManager.getRingtone(context, uri)?.play()
}
RestAlert.VIBRATE -> runCatching {
val vm = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vm.defaultVibrator.vibrate(
VibrationEffect.createWaveform(longArrayOf(0, 350, 150, 350, 150, 500), -1)
)
}
RestAlert.VISUAL, RestAlert.NONE -> Unit
}
}
}