1package com.peachpasswords.ui.vault23import androidx.compose.foundation.background4import androidx.compose.foundation.border5import androidx.compose.foundation.clickable6import androidx.compose.foundation.layout.Arrangement7import androidx.compose.foundation.layout.Box8import androidx.compose.foundation.layout.Column9import androidx.compose.foundation.layout.Row10import androidx.compose.foundation.layout.Spacer11import androidx.compose.foundation.layout.fillMaxSize12import androidx.compose.foundation.layout.fillMaxWidth13import androidx.compose.foundation.layout.padding14import androidx.compose.foundation.layout.size15import androidx.compose.foundation.layout.systemBarsPadding16import androidx.compose.foundation.layout.width17import androidx.compose.foundation.lazy.LazyColumn18import androidx.compose.foundation.lazy.items19import androidx.compose.foundation.shape.RoundedCornerShape20import androidx.compose.material.icons.Icons21import androidx.compose.material.icons.automirrored.rounded.ArrowBack22import androidx.compose.material.icons.outlined.DeleteOutline23import androidx.compose.material3.Icon24import androidx.compose.material3.Text25import androidx.compose.runtime.Composable26import androidx.compose.runtime.collectAsState27import androidx.compose.runtime.getValue28import androidx.compose.runtime.mutableStateOf29import androidx.compose.runtime.remember30import androidx.compose.runtime.setValue31import androidx.compose.ui.Alignment32import androidx.compose.ui.Modifier33import androidx.compose.ui.draw.clip34import androidx.compose.ui.text.font.FontStyle35import androidx.compose.ui.text.style.TextAlign36import androidx.compose.ui.text.style.TextDecoration37import androidx.compose.ui.unit.dp38import androidx.compose.ui.unit.sp39import androidx.hilt.navigation.compose.hiltViewModel40import androidx.lifecycle.ViewModel41import androidx.lifecycle.viewModelScope42import com.peachpasswords.ui.components.FaviconInitials43import com.peachpasswords.ui.theme.LocalPeachAccent44import com.peachpasswords.ui.theme.peachColors45import com.peachpasswords.ui.theme.PeachType46import com.peachpasswords.ui.theme.ShellBackground47import com.peachpasswords.vault.VaultEntry48import com.peachpasswords.vault.VaultRepository49import com.peachpasswords.vault.domainKey50import dagger.hilt.android.lifecycle.HiltViewModel51import kotlinx.coroutines.flow.MutableStateFlow52import kotlinx.coroutines.flow.StateFlow53import kotlinx.coroutines.flow.asStateFlow54import kotlinx.coroutines.launch55import javax.inject.Inject5657@HiltViewModel58class TrashViewModel @Inject constructor(59 private val vault: VaultRepository,60) : ViewModel() {61 private val _deleted = MutableStateFlow(vault.getDeletedEntries())62 val deleted: StateFlow<List<VaultEntry>> = _deleted.asStateFlow()6364 65 val retentionDays: Int? get() = vault.settings.value.trashRetentionDays?.takeIf { it > 0 }6667 private fun refresh() { _deleted.value = vault.getDeletedEntries() }6869 fun restore(id: String) = viewModelScope.launch { vault.restoreEntry(id); refresh() }70 fun purge(id: String) = viewModelScope.launch { vault.purgeEntry(id); refresh() }71 fun emptyTrash() = viewModelScope.launch { vault.purgeDeletedEntries(); refresh() }72}7374@Composable75fun TrashScreen(76 onBack: () -> Unit,77 vm: TrashViewModel = hiltViewModel(),78) {79 val deleted by vm.deleted.collectAsState()80 var confirmEmptyAll by remember { mutableStateOf(false) }81 var confirmPurge by remember { mutableStateOf<VaultEntry?>(null) }8283 ShellBackground {84 Column(85 modifier = Modifier86 .fillMaxSize()87 .systemBarsPadding()88 .padding(horizontal = 20.dp, vertical = 16.dp),89 ) {90 91 Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {92 Icon(93 Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0,94 modifier = Modifier.size(22.dp).clip(RoundedCornerShape(8.dp)).clickable(onClick = onBack),95 )96 Spacer(Modifier.width(14.dp))97 Text(98 "Trash",99 color = peachColors.T0,100 style = PeachType.Display.let {101 androidx.compose.ui.text.TextStyle(102 fontFamily = it, fontStyle = FontStyle.Italic, fontSize = 22.sp,103 )104 },105 )106 Spacer(Modifier.weight(1f))107 if (deleted.isNotEmpty()) {108 GhostPill("Empty trash", peachColors.Breach) { confirmEmptyAll = true }109 }110 }111112 Spacer(Modifier.size(18.dp))113114 if (deleted.isEmpty()) {115 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {116 Text(117 "Trash is empty.",118 color = peachColors.T1,119 style = androidx.compose.ui.text.TextStyle(120 fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp,121 ),122 )123 }124 } else {125 LazyColumn(modifier = Modifier.weight(1f)) {126 items(deleted, key = { it.id }) { entry ->127 DeletedRow(128 entry = entry,129 onRestore = { vm.restore(entry.id) },130 onPurge = { confirmPurge = entry },131 )132 }133 }134 val days = vm.retentionDays135 if (days != null) {136 Text(137 "Entries are permanently deleted after $days days",138 color = peachColors.T2,139 style = PeachType.labelMono,140 textAlign = TextAlign.Center,141 modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),142 )143 }144 }145 }146 }147148 if (confirmEmptyAll) {149 ConfirmOverlay(150 message = "Permanently delete all ${deleted.size} entries? This cannot be undone.",151 confirmLabel = "Empty trash",152 onConfirm = { vm.emptyTrash(); confirmEmptyAll = false },153 onCancel = { confirmEmptyAll = false },154 )155 }156 confirmPurge?.let { target ->157 ConfirmOverlay(158 message = "Permanently delete ${target.title}? This cannot be undone.",159 confirmLabel = "Delete",160 onConfirm = { vm.purge(target.id); confirmPurge = null },161 onCancel = { confirmPurge = null },162 )163 }164}165166@Composable167private fun DeletedRow(entry: VaultEntry, onRestore: () -> Unit, onPurge: () -> Unit) {168 Row(169 verticalAlignment = Alignment.CenterVertically,170 modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),171 ) {172 FaviconInitials(source = entry.url ?: entry.title, size = 26.dp)173 Spacer(Modifier.width(12.dp))174 Column(Modifier.weight(1f)) {175 Text(176 entry.title,177 color = peachColors.T1,178 style = PeachType.entryNameList.copy(textDecoration = TextDecoration.LineThrough),179 )180 val domain = entry.domainKey()181 if (!domain.isNullOrBlank()) {182 Text(domain, color = peachColors.T2, style = PeachType.tinyMono)183 }184 }185 GhostPill("Restore", LocalPeachAccent.current, onRestore)186 Spacer(Modifier.width(8.dp))187 Icon(188 Icons.Outlined.DeleteOutline, contentDescription = "delete forever", tint = peachColors.Breach,189 modifier = Modifier.size(20.dp).clip(RoundedCornerShape(6.dp)).clickable(onClick = onPurge),190 )191 }192}193194@Composable195private fun GhostPill(label: String, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {196 Box(197 modifier = Modifier198 .clip(RoundedCornerShape(999.dp))199 .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))200 .clickable(onClick = onClick)201 .padding(horizontal = 12.dp, vertical = 5.dp),202 ) {203 Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 10.sp))204 }205}206207@Composable208private fun ConfirmOverlay(209 message: String,210 confirmLabel: String,211 onConfirm: () -> Unit,212 onCancel: () -> Unit,213) {214 Box(215 modifier = Modifier216 .fillMaxSize()217 .background(androidx.compose.ui.graphics.Color(0xCC000000))218 .clickable(onClick = onCancel),219 contentAlignment = Alignment.Center,220 ) {221 Column(222 modifier = Modifier223 .padding(32.dp)224 .clip(RoundedCornerShape(18.dp))225 .background(peachColors.S2)226 .border(1.dp, peachColors.T3, RoundedCornerShape(18.dp))227 .clickable(enabled = false) {}228 .padding(22.dp),229 ) {230 Text(message, color = peachColors.T0, style = PeachType.body)231 Spacer(Modifier.size(18.dp))232 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {233 GhostPill("Cancel", peachColors.T1, onCancel)234 Spacer(Modifier.width(10.dp))235 GhostPill(confirmLabel, peachColors.Breach, onConfirm)236 }237 }238 }239}