NotesModeViewModel.kt

3.4 KB · raw

1package com.peachpasswords.ui.vault23import androidx.lifecycle.SavedStateHandle4import androidx.lifecycle.ViewModel5import androidx.lifecycle.viewModelScope6import com.peachpasswords.vault.VaultEntry7import com.peachpasswords.vault.VaultRepository8import dagger.hilt.android.lifecycle.HiltViewModel9import kotlinx.coroutines.Job10import kotlinx.coroutines.delay11import kotlinx.coroutines.flow.MutableStateFlow12import kotlinx.coroutines.flow.SharingStarted13import kotlinx.coroutines.flow.StateFlow14import kotlinx.coroutines.flow.asStateFlow15import kotlinx.coroutines.flow.map16import kotlinx.coroutines.flow.stateIn17import kotlinx.coroutines.launch18import javax.inject.Inject1920/**21 * Backs [NotesModeScreen]. Holds the live note entry, an 800ms-debounced22 * auto-save, and a transient "saved." pulse the screen fades in/out.23 *24 * Title and body are committed independently through [updateEntry] — both25 * funnel through a single debounced job so a rapid title-then-body change26 * coalesces into one write. The repository's updateEntry is a full-entry27 * replace, so each save patches the latest known entry, never a stale copy.28 */29@HiltViewModel30class NotesModeViewModel @Inject constructor(31    private val vault: VaultRepository,32    savedStateHandle: SavedStateHandle,33) : ViewModel() {34    private val id: String = savedStateHandle.get<String>("id").orEmpty()3536    val entry: StateFlow<VaultEntry?> =37        vault.entries.map { list -> list.firstOrNull { it.id == id } }38            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getEntry(id))3940    private val _savedPulse = MutableStateFlow(0L)41    /** Increments on each successful auto-save so the screen can re-trigger its fade. */42    val savedPulse: StateFlow<Long> = _savedPulse.asStateFlow()4344    private var saveJob: Job? = null4546    /** Debounced note-body save. Reschedules on every keystroke; fires 800ms after the last. */47    fun onContentChanged(plainText: String) =48        scheduleSave { it.copy(notes = plainText.ifBlank { null }) }4950    /**51     * Title save. Debounced like the body so blur-after-typing still lands;52     * callers may also force an immediate save on blur via [saveTitleNow].53     */54    fun onTitleChanged(title: String) =55        scheduleSave { it.copy(title = title) }5657    /** Commit the title immediately (e.g. on field blur), bypassing the debounce. */58    fun saveTitleNow(title: String) {59        saveJob?.cancel()60        commit { it.copy(title = title) }61    }6263    /** Browser parity: back navigation commits both title and body before leaving. */64    fun saveNow(title: String, plainText: String) {65        saveJob?.cancel()66        commit {67            it.copy(68                title = title.ifBlank { "Untitled" },69                notes = plainText.ifBlank { null },70            )71        }72    }7374    fun delete(onDone: () -> Unit) {75        viewModelScope.launch {76            runCatching { vault.deleteEntry(id) }77            onDone()78        }79    }8081    private fun scheduleSave(patch: (VaultEntry) -> VaultEntry) {82        saveJob?.cancel()83        saveJob = viewModelScope.launch {84            delay(800)85            commit(patch)86        }87    }8889    private fun commit(patch: (VaultEntry) -> VaultEntry) {90        val current = entry.value ?: return91        viewModelScope.launch {92            runCatching { vault.updateEntry(patch(current)) }93                .onSuccess { _savedPulse.value = System.currentTimeMillis() }94        }95    }96}