NotesModeViewModel.kt

2.9 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    private fun scheduleSave(patch: (VaultEntry) -> VaultEntry) {64        saveJob?.cancel()65        saveJob = viewModelScope.launch {66            delay(800)67            commit(patch)68        }69    }7071    private fun commit(patch: (VaultEntry) -> VaultEntry) {72        val current = entry.value ?: return73        viewModelScope.launch {74            runCatching { vault.updateEntry(patch(current)) }75                .onSuccess { _savedPulse.value = System.currentTimeMillis() }76        }77    }78}