M2: passläge, offline-först med synk-kö, vilotimer med inställningar
Some checks failed
release / build-release (push) Has been cancelled
Some checks failed
release / build-release (push) Has been cancelled
- 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
This commit is contained in:
102
app/src/main/java/eu/brassepc/fitnessdroid/data/RestTimer.kt
Normal file
102
app/src/main/java/eu/brassepc/fitnessdroid/data/RestTimer.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user