1package com.peachpasswords.ui.vault23import android.content.Context4import com.peachpasswords.shared.PeachClipboard5import androidx.lifecycle.SavedStateHandle6import androidx.lifecycle.ViewModel7import androidx.lifecycle.viewModelScope8import com.peachpasswords.ui.PeachNav9import com.peachpasswords.ui.vault.entryTapDestination10import com.peachpasswords.vault.LastViewedEntryStore11import com.peachpasswords.vault.TOTPGenerator12import com.peachpasswords.vault.VaultEntry13import com.peachpasswords.vault.VaultRepository14import dagger.hilt.android.lifecycle.HiltViewModel15import dagger.hilt.android.qualifiers.ApplicationContext16import kotlinx.coroutines.flow.MutableStateFlow17import kotlinx.coroutines.flow.SharingStarted18import kotlinx.coroutines.flow.StateFlow19import kotlinx.coroutines.flow.asStateFlow20import kotlinx.coroutines.flow.map21import kotlinx.coroutines.flow.stateIn22import kotlinx.coroutines.launch23import javax.inject.Inject2425@HiltViewModel26class EntryDetailViewModel @Inject constructor(27 private val vault: VaultRepository,28 @ApplicationContext private val appContext: Context,29 private val lastViewed: LastViewedEntryStore,30 savedStateHandle: SavedStateHandle,31) : ViewModel() {32 private val id: String = savedStateHandle.get<String>("id").orEmpty()3334 init {35 36 37 if (id.isNotBlank() && vault.getEntry(id) != null) lastViewed.set(id)38 }3940 val entry: StateFlow<VaultEntry?> =41 vault.entries.map { list -> list.firstOrNull { it.id == id } }42 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getEntry(id))4344 val activeEntries: StateFlow<List<VaultEntry>> =45 vault.entries.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getActiveEntries())4647 48 val identitySuggestions: StateFlow<List<String>> =49 vault.entries50 .map { vault.getDistinctFieldValues("username").take(4) }51 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())5253 val allTags: StateFlow<List<String>> =54 vault.entries55 .map { vault.getDistinctTags() }56 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())5758 private val _revealed = MutableStateFlow(false)59 val revealed: StateFlow<Boolean> = _revealed.asStateFlow()6061 private val _copiedField = MutableStateFlow<String?>(null)62 val copiedField: StateFlow<String?> = _copiedField.asStateFlow()6364 65 fun reveal() {66 _revealed.value = true67 viewModelScope.launch {68 kotlinx.coroutines.delay(10_000)69 _revealed.value = false70 }71 }7273 fun mask() { _revealed.value = false }7475 private val _cvcRevealed = MutableStateFlow(false)76 val cvcRevealed: StateFlow<Boolean> = _cvcRevealed.asStateFlow()7778 79 fun revealCvc() {80 _cvcRevealed.value = true81 viewModelScope.launch {82 kotlinx.coroutines.delay(10_000)83 _cvcRevealed.value = false84 }85 }8687 fun maskCvc() { _cvcRevealed.value = false }8889 fun copy(field: String, value: String?) {90 val v = value ?: return91 PeachClipboard.copy(appContext, "peach", v)92 viewModelScope.launch {93 _copiedField.value = field94 kotlinx.coroutines.delay(1200)95 if (_copiedField.value == field) _copiedField.value = null96 }97 }9899 100 fun copyTotp() {101 val totp = entry.value?.totp ?: return102 copy("totp", TOTPGenerator.generateCode(totp))103 }104105 fun adjacentDestination(offset: Int): PeachNav? {106 val list = activeEntries.value107 if (list.isEmpty()) return null108 val currentIndex = list.indexOfFirst { it.id == id }109 if (currentIndex < 0) return null110 val nextIndex = (currentIndex + offset + list.size) % list.size111 return entryTapDestination(list[nextIndex])112 }113114 115116117118 fun applyGeneratedPassword(newPassword: String, onDone: () -> Unit = {}) {119 val current = entry.value ?: return120 if (newPassword.isEmpty() || newPassword == current.password) { onDone(); return }121 viewModelScope.launch {122 runCatching { vault.updateEntry(current.copy(password = newPassword)) }123 onDone()124 }125 }126127 128129130131 fun updateField(field: String, value: String) {132 val current = entry.value ?: return133 val patch = when (field) {134 "title" -> current.copy(title = value.trim().ifBlank { current.title })135 "url" -> current.copy(url = value.ifBlank { null })136 "username" -> current.copy(username = value.ifBlank { null })137 "email" -> current.copy(email = value.ifBlank { null })138 "password" -> current.copy(password = value.ifBlank { null })139 "notes" -> current.copy(notes = value.ifBlank { null })140 "totp" -> current.copy(totp = value.ifBlank { null })141 else -> return142 }143 viewModelScope.launch {144 runCatching { vault.updateEntry(patch) }145 }146 }147148 fun updateAdditionalUrl(index: Int, value: String) {149 val current = entry.value ?: return150 val urls = current.additionalUrls.orEmpty().toMutableList()151 if (index !in urls.indices) return152 urls[index] = value153 val patch = current.copy(154 additionalUrls = urls.map { it.trim() }.filter { it.isNotBlank() }.ifEmpty { null },155 )156 viewModelScope.launch {157 runCatching { vault.updateEntry(patch) }158 }159 }160161 fun addAdditionalUrl(value: String) {162 val current = entry.value ?: return163 val next = (current.additionalUrls.orEmpty() + value)164 .map { it.trim() }165 .filter { it.isNotBlank() }166 .distinct()167 .ifEmpty { null }168 viewModelScope.launch {169 runCatching { vault.updateEntry(current.copy(additionalUrls = next)) }170 }171 }172173 fun addTag(raw: String) {174 val current = entry.value ?: return175 val tag = normalizeTag(raw)176 if (tag.isEmpty()) return177 val next = (current.tags.orEmpty() + tag).distinct().take(10)178 viewModelScope.launch {179 runCatching { vault.updateEntry(current.copy(tags = next.ifEmpty { null })) }180 }181 }182183 fun removeTag(tag: String) {184 val current = entry.value ?: return185 val next = current.tags.orEmpty().filterNot { it == tag }186 viewModelScope.launch {187 runCatching { vault.updateEntry(current.copy(tags = next.ifEmpty { null })) }188 }189 }190191 fun updateCard(cardNickname: String, cardNumber: String, cardholderName: String, expiry: String, cvc: String) {192 val current = entry.value ?: return193 val digits = cardNumber.filter { it.isDigit() }194 val expDigits = expiry.filter { it.isDigit() }195 val network = CardFieldUtils.detectNetwork(digits).takeIf { it != "unknown" }196 viewModelScope.launch {197 runCatching {198 vault.updateEntry(199 current.copy(200 cardNickname = cardNickname.ifBlank { null },201 cardNumber = digits.ifBlank { null },202 cardholderName = cardholderName.ifBlank { null },203 expMonth = expDigits.take(2).ifBlank { null },204 expYear = expDigits.drop(2).take(2).ifBlank { null },205 cvc = cvc.ifBlank { null },206 cardNetwork = network,207 ),208 )209 }210 }211 }212213 214215216217 fun restorePassword(entryId: String, oldPassword: String) {218 viewModelScope.launch {219 val current = vault.getEntry(entryId) ?: return@launch220 if (current.password == oldPassword) return@launch221 runCatching { vault.updateEntry(current.copy(password = oldPassword)) }222 }223 }224225 fun delete(onDone: () -> Unit) {226 viewModelScope.launch {227 runCatching { vault.deleteEntry(id) }228 onDone()229 }230 }231}