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.style.TextAlign35import androidx.compose.ui.text.style.TextDecoration36import androidx.compose.ui.unit.dp37import androidx.compose.ui.unit.sp38import androidx.hilt.navigation.compose.hiltViewModel39import androidx.lifecycle.ViewModel40import androidx.lifecycle.viewModelScope41import com.peachpasswords.ui.components.FaviconInitials42import com.peachpasswords.ui.components.PeachEmptyOrEndState43import com.peachpasswords.ui.components.PeachSectionHeader44import com.peachpasswords.ui.theme.LocalPeachAccent45import com.peachpasswords.ui.theme.PeachSpacing46import com.peachpasswords.ui.theme.PeachType47import com.peachpasswords.ui.theme.ShellBackground48import com.peachpasswords.ui.theme.peachColors49import com.peachpasswords.vault.VaultEntry50import com.peachpasswords.vault.VaultRepository51import com.peachpasswords.vault.domainKey52import dagger.hilt.android.lifecycle.HiltViewModel53import kotlinx.coroutines.flow.MutableStateFlow54import kotlinx.coroutines.flow.StateFlow55import kotlinx.coroutines.flow.asStateFlow56import kotlinx.coroutines.launch57import javax.inject.Inject5859@HiltViewModel60class TrashViewModel @Inject constructor(61 private val vault: VaultRepository,62) : ViewModel() {63 private val _deleted = MutableStateFlow(vault.getDeletedEntries())64 val deleted: StateFlow<List<VaultEntry>> = _deleted.asStateFlow()6566 67 val retentionDays: Int? get() = vault.settings.value.trashRetentionDays?.takeIf { it > 0 }6869 private fun refresh() {70 _deleted.value = vault.getDeletedEntries()71 }7273 fun restore(id: String) = viewModelScope.launch { vault.restoreEntry(id); refresh() }74 fun purge(id: String) = viewModelScope.launch { vault.purgeEntry(id); refresh() }75 fun emptyTrash() = viewModelScope.launch { vault.purgeDeletedEntries(); refresh() }76}7778@Composable79fun TrashScreen(80 onBack: () -> Unit,81 vm: TrashViewModel = hiltViewModel(),82) {83 val deleted by vm.deleted.collectAsState()84 var confirmEmptyAll by remember { mutableStateOf(false) }85 var confirmPurge by remember { mutableStateOf<VaultEntry?>(null) }8687 ShellBackground {88 Column(89 modifier = Modifier90 .fillMaxSize()91 .systemBarsPadding()92 .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),93 ) {94 PeachSectionHeader(95 section = "trash",96 onHome = onBack,97 actions = {98 if (deleted.isNotEmpty()) {99 GhostPill("Empty trash", peachColors.Breach) { confirmEmptyAll = true }100 }101 },102 )103104 Spacer(Modifier.size(PeachSpacing.lg))105106 if (deleted.isEmpty()) {107 Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {108 PeachEmptyOrEndState(109 message = "Trash is empty.",110 detail = "Deleted entries will wait here before they are gone for good.",111 )112 }113 } else {114 LazyColumn(modifier = Modifier.weight(1f)) {115 items(deleted, key = { it.id }) { entry ->116 DeletedRow(117 entry = entry,118 onRestore = { vm.restore(entry.id) },119 onPurge = { confirmPurge = entry },120 )121 }122 item {123 val days = vm.retentionDays124 if (days != null) {125 Text(126 "Entries are permanently deleted after $days days",127 color = peachColors.T2,128 style = PeachType.labelMono,129 textAlign = TextAlign.Center,130 modifier = Modifier131 .fillMaxWidth()132 .padding(vertical = PeachSpacing.md),133 )134 } else {135 PeachEmptyOrEndState(136 message = "End of trash.",137 detail = "Nothing below this point will be removed automatically.",138 )139 }140 }141 }142 }143 }144 }145146 if (confirmEmptyAll) {147 ConfirmOverlay(148 message = "Permanently delete all ${deleted.size} entries? This cannot be undone.",149 confirmLabel = "Empty trash",150 onConfirm = { vm.emptyTrash(); confirmEmptyAll = false },151 onCancel = { confirmEmptyAll = false },152 )153 }154 confirmPurge?.let { target ->155 ConfirmOverlay(156 message = "Permanently delete ${target.title}? This cannot be undone.",157 confirmLabel = "Delete",158 onConfirm = { vm.purge(target.id); confirmPurge = null },159 onCancel = { confirmPurge = null },160 )161 }162}163164@Composable165private fun DeletedRow(entry: VaultEntry, onRestore: () -> Unit, onPurge: () -> Unit) {166 Row(167 verticalAlignment = Alignment.CenterVertically,168 modifier = Modifier169 .fillMaxWidth()170 .padding(vertical = PeachSpacing.sm),171 ) {172 FaviconInitials(source = entry.url ?: entry.title, size = 26.dp)173 Spacer(Modifier.width(PeachSpacing.sm))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(PeachSpacing.sm))187 Icon(188 Icons.Outlined.DeleteOutline,189 contentDescription = "delete forever",190 tint = peachColors.Breach,191 modifier = Modifier192 .size(20.dp)193 .clip(RoundedCornerShape(6.dp))194 .clickable(onClick = onPurge),195 )196 }197}198199@Composable200private fun GhostPill(label: String, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {201 Box(202 modifier = Modifier203 .clip(RoundedCornerShape(999.dp))204 .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))205 .clickable(onClick = onClick)206 .padding(horizontal = 12.dp, vertical = 5.dp),207 ) {208 Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 10.sp))209 }210}211212@Composable213private fun ConfirmOverlay(214 message: String,215 confirmLabel: String,216 onConfirm: () -> Unit,217 onCancel: () -> Unit,218) {219 Box(220 modifier = Modifier221 .fillMaxSize()222 .background(androidx.compose.ui.graphics.Color(0xCC000000))223 .clickable(onClick = onCancel),224 contentAlignment = Alignment.Center,225 ) {226 Column(227 modifier = Modifier228 .padding(PeachSpacing.xxl)229 .clip(RoundedCornerShape(PeachSpacing.lg))230 .background(peachColors.S2)231 .border(1.dp, peachColors.T3, RoundedCornerShape(PeachSpacing.lg))232 .clickable(enabled = false) {}233 .padding(22.dp),234 ) {235 Text(message, color = peachColors.T0, style = PeachType.body)236 Spacer(Modifier.size(PeachSpacing.lg))237 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {238 GhostPill("Cancel", peachColors.T1, onCancel)239 Spacer(Modifier.width(PeachSpacing.sm))240 GhostPill(confirmLabel, peachColors.Breach, onConfirm)241 }242 }243 }244}