Pith - kiln
kiln/app/src/main/java/com/vgmlr/kiln/KilnViewModel.kt [8.9 kb]
Modified: 18:59:25 154 026 (20 Aug 026)
3 Days Ago
package com.vgmlr.kiln

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.time.LocalDate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.StateFlow
import android.net.Uri
import androidx.room.withTransaction
import kotlinx.coroutines.withContext

data class AccountSummary(
    val kilnAccount: KilnAccount,
    val daysToPreMenstrual: Long?,
    val todayHue: Float,
    val isDefault: Boolean
)

@OptIn(ExperimentalCoroutinesApi::class)
class KilnViewModel(app: Application) : AndroidViewModel(app) {
    private val db = KilnDb.get(app)

    private val dao = db.dayRecordDao()

    private val accountDao = db.accountDao()

    private val buttonDao = db.buttonDao()

    private val prefs = KilnPrefs.get(app)

    val defaultAccountId = prefs.defaultAccountId
    val loadDefaultOnStart = prefs.loadDefaultOnStart

    val accounts = accountDao.observeAll()
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    val allRecords = dao.observeEvery()
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    private val allButtons = buttonDao.observeEvery()
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    val summaries = combine(
        accounts, allRecords, allButtons, defaultAccountId
    ) { accts, recs, btns, defId ->
        val byAccount = recs.groupBy { it.accountId }
        val buttonsByAccount = btns.groupBy { it.accountId }
        val today = LocalDate.now()
        accts.map { a ->
            val r = byAccount[a.id].orEmpty()
            val set = KilnButtonSet(buttonsByAccount[a.id].orEmpty())
            val starts = KilnCyclePredictor.periodStarts(r, set)
            val cycle = KilnCyclePredictor.avgCycleLength(starts)
            AccountSummary(
                a,
                KilnDayStats.preMenstrualIn(today, r, set),
                KilnGradientPalette.lookup(r, set, starts, cycle)(today),
                a.id == defId
            )
        }
    }.flowOn(Dispatchers.Default)
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    private val activeId = MutableStateFlow(KilnPrefs.NO_ACCOUNT)

    val activeKilnAccount: StateFlow<KilnAccount?> =
        combine(accounts, activeId) { list, id -> list.firstOrNull { it.id == id } }
            .stateIn(viewModelScope, SharingStarted.Eagerly, null)

    val selectedDate = MutableStateFlow(LocalDate.now())

    val records = activeId
        .flatMapLatest { id ->
            if (id == KilnPrefs.NO_ACCOUNT) flowOf(emptyList()) else dao.observeAll(id)
        }
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    val buttons = activeId
        .flatMapLatest { id ->
            if (id == KilnPrefs.NO_ACCOUNT) flowOf(emptyList()) else buttonDao.observeAll(id)
        }
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    val selectedRecord = combine(activeId, selectedDate) { id, d -> id to d }
        .flatMapLatest { (id, d) ->
            if (id == KilnPrefs.NO_ACCOUNT) flowOf(null) else dao.observeDay(id, d.toEpochDay())
        }
        .stateIn(viewModelScope, SharingStarted.Eagerly, null)

    fun open(kilnAccount: KilnAccount) {
        activeId.value = kilnAccount.id
        selectedDate.value = LocalDate.now()
    }

    fun addAccount(name: String) {
        viewModelScope.launch {
            val id = accountDao.insert(KilnAccount(name = name.trim()))
            buttonDao.insertAll(KilnDefaultButtons.forAccount(id))
            if (prefs.defaultAccountId.value == KilnPrefs.NO_ACCOUNT) prefs.setDefaultAccount(id)
        }
    }

    fun deleteActiveAccount() {
        val account = activeKilnAccount.value ?: return
        activeId.value = KilnPrefs.NO_ACCOUNT
        if (prefs.defaultAccountId.value == account.id) {
            prefs.setDefaultAccount(KilnPrefs.NO_ACCOUNT)
        }
        viewModelScope.launch { accountDao.delete(account) }
    }

    fun setHair(hair: KilnHair) = updateAccount { it.copy(hair = hair.name) }

    fun setPmsAlert(on: Boolean) = updateAccount { it.copy(pmsAlert = on) }

    fun setPmsMessage(text: String) = updateAccount { it.copy(pmsMessage = text) }

    fun setMenstruationAlert(on: Boolean) = updateAccount {
        it.copy(menstruationAlert = on, menstruationReminder = it.menstruationReminder && on)
    }

    fun setMenstruationMessage(text: String) = updateAccount {
        it.copy(menstruationMessage = text)
    }

    fun setMenstruationReminder(on: Boolean) = updateAccount { it.copy(menstruationReminder = on) }

    private fun updateAccount(transform: (KilnAccount) -> KilnAccount) {
        val account = activeKilnAccount.value ?: return
        viewModelScope.launch { accountDao.update(transform(account)) }
    }

    fun addButton(label: String, kind: KilnButtonKind) {
        val id = activeKilnAccount.value?.id ?: return
        val sort = (buttons.value.maxOfOrNull { it.sort } ?: -1) + 1
        viewModelScope.launch {
            buttonDao.insert(
                KilnButton(accountId = id, label = label.trim(), kind = kind.value, sort = sort)
            )
        }
    }

    fun updateButton(button: KilnButton, label: String, kind: KilnButtonKind) {
        viewModelScope.launch {
            buttonDao.update(button.copy(label = label.trim(), kind = kind.value))
        }
    }

    fun deleteButton(button: KilnButton) {
        viewModelScope.launch { buttonDao.delete(button) }
    }

    fun moveButton(button: KilnButton, delta: Int) {
        val list = buttons.value.toMutableList()
        val i = list.indexOfFirst { it.id == button.id }
        val j = i + delta
        if (i < 0 || j !in list.indices) return

        val held = list[i]
        list[i] = list[j]
        list[j] = held

        val changed = list.mapIndexedNotNull { k, b -> if (b.sort == k) null else b.copy(sort = k) }
        if (changed.isEmpty()) return
        viewModelScope.launch { buttonDao.updateAll(changed) }
    }

    fun toggle(buttonId: Long) = edit { it.toggled(buttonId) }

    fun setDefaultAccount(id: Long) = prefs.setDefaultAccount(id)

    fun exportActive(uri: Uri) {
        val account = activeKilnAccount.value ?: return
        val btns = buttons.value
        val recs = records.value
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                KilnTransfer.write(
                    getApplication(), uri, KilnTransfer.encode(account, btns, recs)
                )
            }
        }
    }

    fun importFrom(uri: Uri, onResult: (Boolean) -> Unit) {
        val ctx = getApplication<Application>()
        viewModelScope.launch {
            val bundle = withContext(Dispatchers.IO) {
                if (!KilnTransfer.isKilnFile(KilnTransfer.displayName(ctx, uri))) return@withContext null
                KilnTransfer.read(ctx, uri)?.let(KilnTransfer::decode)
            }
            if (bundle == null) {
                onResult(false)
                return@launch
            }
            insertBundle(bundle)
            onResult(true)
        }
    }

    private suspend fun insertBundle(bundle: KilnBundle) = db.withTransaction {
        val id = accountDao.insert(bundle.account)
        val map = HashMap<Long, Long>()
        for (b in bundle.buttons) map[b.id] = buttonDao.insert(b.copy(id = 0, accountId = id))
        dao.upsertAll(
            bundle.records.map { r ->
                r.copy(accountId = id, checked = r.checked.mapNotNullTo(HashSet()) { map[it] })
            }
        )
        if (prefs.defaultAccountId.value == KilnPrefs.NO_ACCOUNT) prefs.setDefaultAccount(id)
    }

    fun setLoadDefaultOnStart(enabled: Boolean) = prefs.setLoadDefaultOnStart(enabled)

    fun select(date: LocalDate) { selectedDate.value = date }

    fun edit(transform: (KilnDayRecord) -> KilnDayRecord) {
        val id = activeKilnAccount.value?.id ?: return
        val day = selectedDate.value.toEpochDay()
        viewModelScope.launch {
            val current = records.value.find { it.epochDay == day } ?: KilnDayRecord(id, day)
            dao.upsert(transform(current))
        }
    }
    
    fun iconRecords(): List<KilnDayRecord> {
        val id = KilnIconSync.iconAccount(accounts.value, prefs.defaultAccountId.value)?.id
            ?: return emptyList()
        return allRecords.value.filter { it.accountId == id }
    }

    fun iconButtons(): KilnButtonSet {
        val id = KilnIconSync.iconAccount(accounts.value, prefs.defaultAccountId.value)?.id
            ?: return KilnButtonSet.EMPTY
        return KilnButtonSet(allButtons.value.filter { it.accountId == id })
    }
}
Updates
Kerf - Android 157.026
Kiln - Android 157.026
Wedge - Android 156.026
Whittle - Linux 155.026
Grit - Firefox/Chrome 154.026

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)
0.00231
257,102 (+356)