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.Inject2122232425262728293031@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 _isMultiSelectMode = MutableStateFlow(false)53 val isMultiSelectMode: StateFlow<Boolean> = _isMultiSelectMode.asStateFlow()5455 private val _selectedIds = MutableStateFlow<Set<String>>(emptySet())56 val selectedIds: StateFlow<Set<String>> = _selectedIds.asStateFlow()5758 private val _savedPulse = MutableStateFlow(0L)59 60 val savedPulse: StateFlow<Long> = _savedPulse.asStateFlow()6162 private var saveJob: Job? = null63 private val untouchedDraftIds = mutableSetOf<String>()6465 66 fun onContentChanged(plainText: String) {67 if (plainText.isNotBlank()) _activeNoteId.value?.let(untouchedDraftIds::remove)68 scheduleSave { it.copy(notes = plainText.ifBlank { null }) }69 }7071 72737475 fun onTitleChanged(title: String) {76 if (title.isNotBlank()) _activeNoteId.value?.let(untouchedDraftIds::remove)77 scheduleSave { it.copy(title = title) }78 }7980 81 fun saveTitleNow(title: String) {82 saveJob?.cancel()83 commit { it.copy(title = title) }84 }8586 fun openNote(noteId: String) {87 saveJob?.cancel()88 exitMultiSelect()89 _activeNoteId.value = noteId90 }9192 fun showToc() {93 saveJob?.cancel()94 exitMultiSelect()95 _activeNoteId.value = null96 }9798 fun createNote() {99 saveJob?.cancel()100 exitMultiSelect()101 viewModelScope.launch {102 val created = vault.addEntry(103 VaultEntry(104 id = "",105 type = "note",106 title = "",107 notes = null,108 createdAt = 0L,109 modifiedAt = 0L,110 ),111 )112 untouchedDraftIds += created.id113 _activeNoteId.value = created.id114 }115 }116117 fun finishNote(title: String, plainText: String, onDone: () -> Unit) {118 saveJob?.cancel()119 val activeId = _activeNoteId.value120 if (shouldDiscardUntouchedNote(activeId in untouchedDraftIds, title, plainText)) {121 viewModelScope.launch {122 activeId?.let { runCatching { vault.deleteEntry(it) } }123 untouchedDraftIds.remove(activeId)124 onDone()125 }126 } else {127 saveNow(title, plainText)128 onDone()129 }130 }131132 133 fun saveNow(title: String, plainText: String) {134 saveJob?.cancel()135 commit {136 it.copy(137 title = title.ifBlank { "Untitled" },138 notes = plainText.ifBlank { null },139 )140 }141 }142143 fun delete(onDone: () -> Unit) {144 viewModelScope.launch {145 _activeNoteId.value?.let { runCatching { vault.deleteEntry(it) } }146 _activeNoteId.value = null147 onDone()148 }149 }150151 fun enterMultiSelect(noteId: String) {152 _isMultiSelectMode.value = true153 _selectedIds.value = setOf(noteId)154 }155156 fun toggleSelected(noteId: String) {157 val next = _selectedIds.value.toMutableSet()158 if (!next.add(noteId)) next.remove(noteId)159 _selectedIds.value = next160 }161162 fun exitMultiSelect() {163 _isMultiSelectMode.value = false164 _selectedIds.value = emptySet()165 }166167 fun batchDelete() {168 val ids = _selectedIds.value.toList()169 if (ids.isEmpty()) return170 viewModelScope.launch {171 ids.forEach { id -> runCatching { vault.deleteEntry(id) } }172 exitMultiSelect()173 }174 }175176 fun lock() = vault.lock()177178 private fun scheduleSave(patch: (VaultEntry) -> VaultEntry) {179 saveJob?.cancel()180 saveJob = viewModelScope.launch {181 delay(800)182 commit(patch)183 }184 }185186 private fun commit(patch: (VaultEntry) -> VaultEntry) {187 val current = entry.value ?: return188 viewModelScope.launch {189 runCatching { vault.updateEntry(patch(current)) }190 .onSuccess { _savedPulse.value = System.currentTimeMillis() }191 }192 }193}194195internal fun shouldDiscardUntouchedNote(isUntouchedDraft: Boolean, title: String, body: String): Boolean =196 isUntouchedDraft && title.isBlank() && body.isBlank()