NotesModeViewModel.kt

4.7 KB · raw

1package com.peachpasswords.ui.vault23import androidx.lifecycle.SavedStateHandle4import androidx.lifecycle.ViewModel5import androidx.lifecycle.viewModelScope6import com.peachpasswords.vault.VaultEntry7import com.peachpasswords.vault.VaultRepository8import com.peachpasswords.vault.isNote9import dagger.hilt.android.lifecycle.HiltViewModel10import kotlinx.coroutines.Job11import kotlinx.coroutines.delay12import kotlinx.coroutines.flow.MutableStateFlow13import kotlinx.coroutines.flow.SharingStarted14import kotlinx.coroutines.flow.StateFlow15import kotlinx.coroutines.flow.asStateFlow16import kotlinx.coroutines.flow.combine17import kotlinx.coroutines.flow.map18import kotlinx.coroutines.flow.stateIn19import kotlinx.coroutines.launch20import javax.inject.Inject2122/**23 * Backs [NotesModeScreen]. Holds the live note entry, an 800ms-debounced24 * auto-save, and a transient "saved." pulse the screen fades in/out.25 *26 * Title and body are committed independently through [updateEntry] — both27 * funnel through a single debounced job so a rapid title-then-body change28 * coalesces into one write. The repository's updateEntry is a full-entry29 * replace, so each save patches the latest known entry, never a stale copy.30 */31@HiltViewModel32class NotesModeViewModel @Inject constructor(33    private val vault: VaultRepository,34    savedStateHandle: SavedStateHandle,35) : ViewModel() {36    private val id: String = savedStateHandle.get<String>("id").orEmpty()37    private val _activeNoteId = MutableStateFlow(id.takeIf { it.isNotBlank() })3839    val entry: StateFlow<VaultEntry?> =40        combine(vault.entries, _activeNoteId) { list, activeId ->41            activeId?.let { id -> list.firstOrNull { it.id == id && it.isNote() } }42        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getEntry(id)?.takeIf { it.isNote() })4344    val notes: StateFlow<List<VaultEntry>> =45        vault.entries.map { list ->46            list.filter { it.isNote() }47                .sortedByDescending { it.lastUsedAt ?: it.modifiedAt }48        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getEntries().filter { it.isNote() })4950    val activeNoteId: StateFlow<String?> = _activeNoteId.asStateFlow()5152    private val _savedPulse = MutableStateFlow(0L)53    /** Increments on each successful auto-save so the screen can re-trigger its fade. */54    val savedPulse: StateFlow<Long> = _savedPulse.asStateFlow()5556    private var saveJob: Job? = null5758    /** Debounced note-body save. Reschedules on every keystroke; fires 800ms after the last. */59    fun onContentChanged(plainText: String) =60        scheduleSave { it.copy(notes = plainText.ifBlank { null }) }6162    /**63     * Title save. Debounced like the body so blur-after-typing still lands;64     * callers may also force an immediate save on blur via [saveTitleNow].65     */66    fun onTitleChanged(title: String) =67        scheduleSave { it.copy(title = title) }6869    /** Commit the title immediately (e.g. on field blur), bypassing the debounce. */70    fun saveTitleNow(title: String) {71        saveJob?.cancel()72        commit { it.copy(title = title) }73    }7475    fun openNote(noteId: String) {76        saveJob?.cancel()77        _activeNoteId.value = noteId78    }7980    fun showToc() {81        saveJob?.cancel()82        _activeNoteId.value = null83    }8485    fun createNote() {86        saveJob?.cancel()87        viewModelScope.launch {88            val created = vault.addEntry(89                VaultEntry(90                    id = "",91                    type = "note",92                    title = "Untitled",93                    notes = null,94                    createdAt = 0L,95                    modifiedAt = 0L,96                ),97            )98            _activeNoteId.value = created.id99        }100    }101102    /** Browser parity: back navigation commits both title and body before leaving. */103    fun saveNow(title: String, plainText: String) {104        saveJob?.cancel()105        commit {106            it.copy(107                title = title.ifBlank { "Untitled" },108                notes = plainText.ifBlank { null },109            )110        }111    }112113    fun delete(onDone: () -> Unit) {114        viewModelScope.launch {115            _activeNoteId.value?.let { runCatching { vault.deleteEntry(it) } }116            _activeNoteId.value = null117            onDone()118        }119    }120121    private fun scheduleSave(patch: (VaultEntry) -> VaultEntry) {122        saveJob?.cancel()123        saveJob = viewModelScope.launch {124            delay(800)125            commit(patch)126        }127    }128129    private fun commit(patch: (VaultEntry) -> VaultEntry) {130        val current = entry.value ?: return131        viewModelScope.launch {132            runCatching { vault.updateEntry(patch(current)) }133                .onSuccess { _savedPulse.value = System.currentTimeMillis() }134        }135    }136}