package com.peachpasswords.ui.vault import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.peachpasswords.ui.components.FaviconInitials import com.peachpasswords.ui.components.PeachEmptyOrEndState import com.peachpasswords.ui.components.PeachTopBar import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.PeachSpacing import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.ui.theme.ShellBackground import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.vault.VaultEntry import com.peachpasswords.vault.VaultRepository import com.peachpasswords.vault.domainKey import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class TrashViewModel @Inject constructor( private val vault: VaultRepository, ) : ViewModel() { private val _deleted = MutableStateFlow(vault.getDeletedEntries()) val deleted: StateFlow> = _deleted.asStateFlow() /** Retention window in days, or null when set to "Never". */ val retentionDays: Int? get() = vault.settings.value.trashRetentionDays?.takeIf { it > 0 } private fun refresh() { _deleted.value = vault.getDeletedEntries() } fun restore(id: String) = viewModelScope.launch { vault.restoreEntry(id); refresh() } fun purge(id: String) = viewModelScope.launch { vault.purgeEntry(id); refresh() } fun emptyTrash() = viewModelScope.launch { vault.purgeDeletedEntries(); refresh() } } @Composable fun TrashScreen( onBack: () -> Unit, vm: TrashViewModel = hiltViewModel(), ) { val deleted by vm.deleted.collectAsState() var confirmEmptyAll by remember { mutableStateOf(false) } var confirmPurge by remember { mutableStateOf(null) } ShellBackground { Column( modifier = Modifier .fillMaxSize() .systemBarsPadding() .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md), ) { PeachTopBar( title = "Trash", leading = { Icon( Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0, modifier = Modifier .size(22.dp) .clip(RoundedCornerShape(PeachSpacing.sm)) .clickable(onClick = onBack), ) }, actions = { if (deleted.isNotEmpty()) { GhostPill("Empty trash", peachColors.Breach) { confirmEmptyAll = true } } }, ) Spacer(Modifier.size(PeachSpacing.lg)) if (deleted.isEmpty()) { Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { PeachEmptyOrEndState( message = "Trash is empty.", detail = "Deleted entries will wait here before they are gone for good.", ) } } else { LazyColumn(modifier = Modifier.weight(1f)) { items(deleted, key = { it.id }) { entry -> DeletedRow( entry = entry, onRestore = { vm.restore(entry.id) }, onPurge = { confirmPurge = entry }, ) } item { val days = vm.retentionDays if (days != null) { Text( "Entries are permanently deleted after $days days", color = peachColors.T2, style = PeachType.labelMono, textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() .padding(vertical = PeachSpacing.md), ) } else { PeachEmptyOrEndState( message = "End of trash.", detail = "Nothing below this point will be removed automatically.", ) } } } } } } if (confirmEmptyAll) { ConfirmOverlay( message = "Permanently delete all ${deleted.size} entries? This cannot be undone.", confirmLabel = "Empty trash", onConfirm = { vm.emptyTrash(); confirmEmptyAll = false }, onCancel = { confirmEmptyAll = false }, ) } confirmPurge?.let { target -> ConfirmOverlay( message = "Permanently delete ${target.title}? This cannot be undone.", confirmLabel = "Delete", onConfirm = { vm.purge(target.id); confirmPurge = null }, onCancel = { confirmPurge = null }, ) } } @Composable private fun DeletedRow(entry: VaultEntry, onRestore: () -> Unit, onPurge: () -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding(vertical = PeachSpacing.sm), ) { FaviconInitials(source = entry.url ?: entry.title, size = 26.dp) Spacer(Modifier.width(PeachSpacing.sm)) Column(Modifier.weight(1f)) { Text( entry.title, color = peachColors.T1, style = PeachType.entryNameList.copy(textDecoration = TextDecoration.LineThrough), ) val domain = entry.domainKey() if (!domain.isNullOrBlank()) { Text(domain, color = peachColors.T2, style = PeachType.tinyMono) } } GhostPill("Restore", LocalPeachAccent.current, onRestore) Spacer(Modifier.width(PeachSpacing.sm)) Icon( Icons.Outlined.DeleteOutline, contentDescription = "delete forever", tint = peachColors.Breach, modifier = Modifier .size(20.dp) .clip(RoundedCornerShape(6.dp)) .clickable(onClick = onPurge), ) } } @Composable private fun GhostPill(label: String, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) { Box( modifier = Modifier .clip(RoundedCornerShape(999.dp)) .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp)) .clickable(onClick = onClick) .padding(horizontal = 12.dp, vertical = 5.dp), ) { Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 10.sp)) } } @Composable private fun ConfirmOverlay( message: String, confirmLabel: String, onConfirm: () -> Unit, onCancel: () -> Unit, ) { Box( modifier = Modifier .fillMaxSize() .background(androidx.compose.ui.graphics.Color(0xCC000000)) .clickable(onClick = onCancel), contentAlignment = Alignment.Center, ) { Column( modifier = Modifier .padding(PeachSpacing.xxl) .clip(RoundedCornerShape(PeachSpacing.lg)) .background(peachColors.S2) .border(1.dp, peachColors.T3, RoundedCornerShape(PeachSpacing.lg)) .clickable(enabled = false) {} .padding(22.dp), ) { Text(message, color = peachColors.T0, style = PeachType.body) Spacer(Modifier.size(PeachSpacing.lg)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { GhostPill("Cancel", peachColors.T1, onCancel) Spacer(Modifier.width(PeachSpacing.sm)) GhostPill(confirmLabel, peachColors.Breach, onConfirm) } } } }