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.PeachTopBar44import 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 PeachTopBar(95 title = "Trash",96 leading = {97 Icon(98 Icons.AutoMirrored.Rounded.ArrowBack,99 contentDescription = "back",100 tint = peachColors.T0,101 modifier = Modifier102 .size(22.dp)103 .clip(RoundedCornerShape(PeachSpacing.sm))104 .clickable(onClick = onBack),105 )106 },107 actions = {108 if (deleted.isNotEmpty()) {109 GhostPill("Empty trash", peachColors.Breach) { confirmEmptyAll = true }110 }111 },112 )113114 Spacer(Modifier.size(PeachSpacing.lg))115116 if (deleted.isEmpty()) {117 Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {118 PeachEmptyOrEndState(119 message = "Trash is empty.",120 detail = "Deleted entries will wait here before they are gone for good.",121 )122 }123 } else {124 LazyColumn(modifier = Modifier.weight(1f)) {125 items(deleted, key = { it.id }) { entry ->126 DeletedRow(127 entry = entry,128 onRestore = { vm.restore(entry.id) },129 onPurge = { confirmPurge = entry },130 )131 }132 item {133 val days = vm.retentionDays134 if (days != null) {135 Text(136 "Entries are permanently deleted after $days days",137 color = peachColors.T2,138 style = PeachType.labelMono,139 textAlign = TextAlign.Center,140 modifier = Modifier141 .fillMaxWidth()142 .padding(vertical = PeachSpacing.md),143 )144 } else {145 PeachEmptyOrEndState(146 message = "End of trash.",147 detail = "Nothing below this point will be removed automatically.",148 )149 }150 }151 }152 }153 }154 }155156 if (confirmEmptyAll) {157 ConfirmOverlay(158 message = "Permanently delete all ${deleted.size} entries? This cannot be undone.",159 confirmLabel = "Empty trash",160 onConfirm = { vm.emptyTrash(); confirmEmptyAll = false },161 onCancel = { confirmEmptyAll = false },162 )163 }164 confirmPurge?.let { target ->165 ConfirmOverlay(166 message = "Permanently delete ${target.title}? This cannot be undone.",167 confirmLabel = "Delete",168 onConfirm = { vm.purge(target.id); confirmPurge = null },169 onCancel = { confirmPurge = null },170 )171 }172}173174@Composable175private fun DeletedRow(entry: VaultEntry, onRestore: () -> Unit, onPurge: () -> Unit) {176 Row(177 verticalAlignment = Alignment.CenterVertically,178 modifier = Modifier179 .fillMaxWidth()180 .padding(vertical = PeachSpacing.sm),181 ) {182 FaviconInitials(source = entry.url ?: entry.title, size = 26.dp)183 Spacer(Modifier.width(PeachSpacing.sm))184 Column(Modifier.weight(1f)) {185 Text(186 entry.title,187 color = peachColors.T1,188 style = PeachType.entryNameList.copy(textDecoration = TextDecoration.LineThrough),189 )190 val domain = entry.domainKey()191 if (!domain.isNullOrBlank()) {192 Text(domain, color = peachColors.T2, style = PeachType.tinyMono)193 }194 }195 GhostPill("Restore", LocalPeachAccent.current, onRestore)196 Spacer(Modifier.width(PeachSpacing.sm))197 Icon(198 Icons.Outlined.DeleteOutline,199 contentDescription = "delete forever",200 tint = peachColors.Breach,201 modifier = Modifier202 .size(20.dp)203 .clip(RoundedCornerShape(6.dp))204 .clickable(onClick = onPurge),205 )206 }207}208209@Composable210private fun GhostPill(label: String, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {211 Box(212 modifier = Modifier213 .clip(RoundedCornerShape(999.dp))214 .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))215 .clickable(onClick = onClick)216 .padding(horizontal = 12.dp, vertical = 5.dp),217 ) {218 Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 10.sp))219 }220}221222@Composable223private fun ConfirmOverlay(224 message: String,225 confirmLabel: String,226 onConfirm: () -> Unit,227 onCancel: () -> Unit,228) {229 Box(230 modifier = Modifier231 .fillMaxSize()232 .background(androidx.compose.ui.graphics.Color(0xCC000000))233 .clickable(onClick = onCancel),234 contentAlignment = Alignment.Center,235 ) {236 Column(237 modifier = Modifier238 .padding(PeachSpacing.xxl)239 .clip(RoundedCornerShape(PeachSpacing.lg))240 .background(peachColors.S2)241 .border(1.dp, peachColors.T3, RoundedCornerShape(PeachSpacing.lg))242 .clickable(enabled = false) {}243 .padding(22.dp),244 ) {245 Text(message, color = peachColors.T0, style = PeachType.body)246 Spacer(Modifier.size(PeachSpacing.lg))247 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {248 GhostPill("Cancel", peachColors.T1, onCancel)249 Spacer(Modifier.width(PeachSpacing.sm))250 GhostPill(confirmLabel, peachColors.Breach, onConfirm)251 }252 }253 }254}