feat(memory-lane): edit/delete capsules, custom unlock date picker, error snackbar
This commit is contained in:
parent
9710bbc438
commit
755077c7ba
|
|
@ -115,4 +115,24 @@ class FirestoreCapsuleDataSource @Inject constructor(
|
|||
suspend fun unlockCapsule(coupleId: String, capsuleId: String) {
|
||||
col(coupleId).document(capsuleId).update("status", "unlocked").await()
|
||||
}
|
||||
|
||||
suspend fun updateCapsule(capsule: TimeCapsule) {
|
||||
val aead = encryptionManager.aeadFor(capsule.coupleId)
|
||||
?: throw IllegalStateException("Encryption key not ready. Please try again in a moment.")
|
||||
val encTitle = fieldEncryptor.encrypt(capsule.title, aead, capsule.coupleId)
|
||||
val encContent = fieldEncryptor.encrypt(capsule.content, aead, capsule.coupleId)
|
||||
val encPrompt = fieldEncryptor.encryptNullable(capsule.promptUsed, aead, capsule.coupleId)
|
||||
col(capsule.coupleId).document(capsule.id).update(
|
||||
mapOf(
|
||||
"title" to encTitle,
|
||||
"content" to encContent,
|
||||
"promptUsed" to encPrompt,
|
||||
"unlockAt" to capsule.unlockAt
|
||||
)
|
||||
).await()
|
||||
}
|
||||
|
||||
suspend fun deleteCapsule(coupleId: String, capsuleId: String) {
|
||||
col(coupleId).document(capsuleId).delete().await()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,19 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import app.closer.ui.components.CloserHeartLoader
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -41,13 +47,18 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -100,7 +111,7 @@ private val capsulePrompts = listOf(
|
|||
|
||||
// ── ViewModel ──────────────────────────────────────────────────────────────────
|
||||
|
||||
enum class MemoryLanePhase { LOADING, LIST, CREATE, DETAIL, ERROR }
|
||||
enum class MemoryLanePhase { LOADING, LIST, CREATE, EDIT, DETAIL, ERROR }
|
||||
|
||||
data class MemoryLaneUiState(
|
||||
val phase: MemoryLanePhase = MemoryLanePhase.LOADING,
|
||||
|
|
@ -113,7 +124,11 @@ data class MemoryLaneUiState(
|
|||
val createContent: String = "",
|
||||
val selectedPreset: UnlockPreset = UnlockPreset.ONE_YEAR,
|
||||
val selectedPrompt: String? = null,
|
||||
val customUnlockMs: Long? = null,
|
||||
val showDatePicker: Boolean = false,
|
||||
val showDeleteConfirm: Boolean = false,
|
||||
val isSaving: Boolean = false,
|
||||
val isDeleting: Boolean = false,
|
||||
val error: String? = null,
|
||||
val hasPremium: Boolean = true,
|
||||
val navigateTo: String? = null
|
||||
|
|
@ -184,6 +199,8 @@ class MemoryLaneViewModel @Inject constructor(
|
|||
createContent = "",
|
||||
selectedPreset = UnlockPreset.ONE_YEAR,
|
||||
selectedPrompt = null,
|
||||
customUnlockMs = null,
|
||||
showDatePicker = false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
|
@ -195,7 +212,10 @@ class MemoryLaneViewModel @Inject constructor(
|
|||
|
||||
fun updateTitle(v: String) = _uiState.update { it.copy(createTitle = v) }
|
||||
fun updateContent(v: String) = _uiState.update { it.copy(createContent = v) }
|
||||
fun selectPreset(p: UnlockPreset) = _uiState.update { it.copy(selectedPreset = p) }
|
||||
fun selectPreset(p: UnlockPreset) = _uiState.update { it.copy(selectedPreset = p, customUnlockMs = null) }
|
||||
fun showDatePicker() = _uiState.update { it.copy(showDatePicker = true) }
|
||||
fun hideDatePicker() = _uiState.update { it.copy(showDatePicker = false) }
|
||||
fun selectCustomDate(epochMs: Long) = _uiState.update { it.copy(customUnlockMs = epochMs, showDatePicker = false) }
|
||||
fun selectPrompt(p: String?) = _uiState.update {
|
||||
it.copy(selectedPrompt = p, createContent = if (p != null) "" else it.createContent)
|
||||
}
|
||||
|
|
@ -220,7 +240,7 @@ class MemoryLaneViewModel @Inject constructor(
|
|||
title = state.createTitle.trim(),
|
||||
content = state.createContent.trim(),
|
||||
promptUsed = state.selectedPrompt,
|
||||
unlockAt = now + state.selectedPreset.ms,
|
||||
unlockAt = state.customUnlockMs ?: (now + state.selectedPreset.ms),
|
||||
createdAt = now,
|
||||
status = "sealed"
|
||||
)
|
||||
|
|
@ -235,6 +255,67 @@ class MemoryLaneViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun openEdit(capsule: TimeCapsule) = _uiState.update {
|
||||
it.copy(
|
||||
phase = MemoryLanePhase.EDIT,
|
||||
selectedCapsule = capsule,
|
||||
createTitle = capsule.title,
|
||||
createContent = capsule.content,
|
||||
selectedPrompt = capsule.promptUsed,
|
||||
customUnlockMs = capsule.unlockAt,
|
||||
showDatePicker = false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
||||
fun saveEdit() {
|
||||
val state = _uiState.value
|
||||
val capsule = state.selectedCapsule ?: return
|
||||
val coupleId = state.coupleId ?: return
|
||||
if (state.createTitle.isBlank()) {
|
||||
_uiState.update { it.copy(error = "Give your capsule a title.") }
|
||||
return
|
||||
}
|
||||
if (state.createContent.isBlank()) {
|
||||
_uiState.update { it.copy(error = "Write something inside.") }
|
||||
return
|
||||
}
|
||||
_uiState.update { it.copy(isSaving = true, error = null) }
|
||||
val updated = capsule.copy(
|
||||
title = state.createTitle.trim(),
|
||||
content = state.createContent.trim(),
|
||||
promptUsed = state.selectedPrompt,
|
||||
unlockAt = state.customUnlockMs ?: capsule.unlockAt
|
||||
)
|
||||
viewModelScope.launch {
|
||||
runCatching { capsuleDataSource.updateCapsule(updated) }
|
||||
.onSuccess { _uiState.update { it.copy(isSaving = false, phase = MemoryLanePhase.LIST, selectedCapsule = null) } }
|
||||
.onFailure { e ->
|
||||
Log.w(TAG, "Could not update capsule", e)
|
||||
val msg = if (e is IllegalStateException) e.message ?: "Encryption not ready. Try again." else "Could not save. Check your connection."
|
||||
_uiState.update { s -> s.copy(isSaving = false, error = msg) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showDeleteConfirm() = _uiState.update { it.copy(showDeleteConfirm = true) }
|
||||
fun dismissDeleteConfirm() = _uiState.update { it.copy(showDeleteConfirm = false) }
|
||||
|
||||
fun deleteCapsule() {
|
||||
val state = _uiState.value
|
||||
val capsule = state.selectedCapsule ?: return
|
||||
val coupleId = state.coupleId ?: return
|
||||
_uiState.update { it.copy(showDeleteConfirm = false, isDeleting = true) }
|
||||
viewModelScope.launch {
|
||||
runCatching { capsuleDataSource.deleteCapsule(coupleId, capsule.id) }
|
||||
.onSuccess { _uiState.update { it.copy(isDeleting = false, phase = MemoryLanePhase.LIST, selectedCapsule = null) } }
|
||||
.onFailure { e ->
|
||||
Log.w(TAG, "Could not delete capsule", e)
|
||||
_uiState.update { s -> s.copy(isDeleting = false, error = "Could not delete. Check your connection and try again.") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissError() = _uiState.update { it.copy(error = null) }
|
||||
|
||||
companion object {
|
||||
|
|
@ -250,52 +331,97 @@ fun MemoryLaneScreen(
|
|||
viewModel: MemoryLaneViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(state.navigateTo) {
|
||||
state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() }
|
||||
}
|
||||
// Show snackbar only for DETAIL-phase errors (create/edit have inline error display).
|
||||
LaunchedEffect(state.error, state.phase) {
|
||||
val msg = state.error
|
||||
if (state.phase == MemoryLanePhase.DETAIL && msg != null) {
|
||||
snackbarHostState.showSnackbar(msg)
|
||||
viewModel.dismissError()
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(closerBackgroundBrush())
|
||||
) {
|
||||
when (state.phase) {
|
||||
MemoryLanePhase.LOADING -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CloserHeartLoader()
|
||||
}
|
||||
MemoryLanePhase.LIST -> if (!state.hasPremium) {
|
||||
MemoryLaneLockedScreen(
|
||||
onBack = { onNavigate(AppRoute.PLAY) },
|
||||
onUnlock = { onNavigate(AppRoute.PAYWALL) }
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
containerColor = Color.Transparent
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.background(closerBackgroundBrush())
|
||||
) {
|
||||
when (state.phase) {
|
||||
MemoryLanePhase.LOADING -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CloserHeartLoader()
|
||||
}
|
||||
MemoryLanePhase.LIST -> if (!state.hasPremium) {
|
||||
MemoryLaneLockedScreen(
|
||||
onBack = { onNavigate(AppRoute.PLAY) },
|
||||
onUnlock = { onNavigate(AppRoute.PAYWALL) }
|
||||
)
|
||||
} else {
|
||||
CapsuleListScreen(
|
||||
capsules = state.capsules,
|
||||
onBack = { onNavigate(AppRoute.PLAY) },
|
||||
onNew = { viewModel.openCreate() },
|
||||
onOpen = { viewModel.openDetail(it) }
|
||||
)
|
||||
}
|
||||
MemoryLanePhase.CREATE -> CapsuleCreateScreen(
|
||||
state = state,
|
||||
isEditing = false,
|
||||
onBack = { viewModel.backToList() },
|
||||
onTitleChange = { viewModel.updateTitle(it) },
|
||||
onContentChange = { viewModel.updateContent(it) },
|
||||
onPresetSelect = { viewModel.selectPreset(it) },
|
||||
onPromptSelect = { viewModel.selectPrompt(it) },
|
||||
onShowDatePicker = { viewModel.showDatePicker() },
|
||||
onDatePicked = { viewModel.selectCustomDate(it) },
|
||||
onHideDatePicker = { viewModel.hideDatePicker() },
|
||||
onSave = { viewModel.saveCapsule() }
|
||||
)
|
||||
} else {
|
||||
CapsuleListScreen(
|
||||
capsules = state.capsules,
|
||||
MemoryLanePhase.EDIT -> CapsuleCreateScreen(
|
||||
state = state,
|
||||
isEditing = true,
|
||||
onBack = { viewModel.backToList() },
|
||||
onTitleChange = { viewModel.updateTitle(it) },
|
||||
onContentChange = { viewModel.updateContent(it) },
|
||||
onPresetSelect = { viewModel.selectPreset(it) },
|
||||
onPromptSelect = { viewModel.selectPrompt(it) },
|
||||
onShowDatePicker = { viewModel.showDatePicker() },
|
||||
onDatePicked = { viewModel.selectCustomDate(it) },
|
||||
onHideDatePicker = { viewModel.hideDatePicker() },
|
||||
onSave = { viewModel.saveEdit() }
|
||||
)
|
||||
MemoryLanePhase.DETAIL -> {
|
||||
val capsule = state.selectedCapsule!!
|
||||
CapsuleDetailScreen(
|
||||
capsule = capsule,
|
||||
userId = state.userId ?: "",
|
||||
onBack = { viewModel.backToList() },
|
||||
onEdit = if (!capsule.isUnlocked && capsule.authorId == state.userId) {
|
||||
{ viewModel.openEdit(capsule) }
|
||||
} else null,
|
||||
onDelete = if (capsule.authorId == state.userId) {
|
||||
{ viewModel.showDeleteConfirm() }
|
||||
} else null,
|
||||
showDeleteConfirm = state.showDeleteConfirm,
|
||||
isDeleting = state.isDeleting,
|
||||
onConfirmDelete = { viewModel.deleteCapsule() },
|
||||
onDismissDelete = { viewModel.dismissDeleteConfirm() }
|
||||
)
|
||||
}
|
||||
MemoryLanePhase.ERROR -> MemoryLaneErrorScreen(
|
||||
message = state.error ?: "Something went wrong.",
|
||||
onBack = { onNavigate(AppRoute.PLAY) },
|
||||
onNew = { viewModel.openCreate() },
|
||||
onOpen = { viewModel.openDetail(it) }
|
||||
onRetry = { viewModel.retry() }
|
||||
)
|
||||
}
|
||||
MemoryLanePhase.CREATE -> CapsuleCreateScreen(
|
||||
state = state,
|
||||
onBack = { viewModel.backToList() },
|
||||
onTitleChange = { viewModel.updateTitle(it) },
|
||||
onContentChange = { viewModel.updateContent(it) },
|
||||
onPresetSelect = { viewModel.selectPreset(it) },
|
||||
onPromptSelect = { viewModel.selectPrompt(it) },
|
||||
onSave = { viewModel.saveCapsule() }
|
||||
)
|
||||
MemoryLanePhase.DETAIL -> CapsuleDetailScreen(
|
||||
capsule = state.selectedCapsule!!,
|
||||
userId = state.userId ?: "",
|
||||
onBack = { viewModel.backToList() }
|
||||
)
|
||||
MemoryLanePhase.ERROR -> MemoryLaneErrorScreen(
|
||||
message = state.error ?: "Something went wrong.",
|
||||
onBack = { onNavigate(AppRoute.PLAY) },
|
||||
onRetry = { viewModel.retry() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -475,17 +601,39 @@ private fun CapsuleCard(capsule: TimeCapsule, onClick: () -> Unit) {
|
|||
|
||||
// ── Create ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CapsuleCreateScreen(
|
||||
state: MemoryLaneUiState,
|
||||
isEditing: Boolean,
|
||||
onBack: () -> Unit,
|
||||
onTitleChange: (String) -> Unit,
|
||||
onContentChange: (String) -> Unit,
|
||||
onPresetSelect: (UnlockPreset) -> Unit,
|
||||
onPromptSelect: (String?) -> Unit,
|
||||
onShowDatePicker: () -> Unit,
|
||||
onDatePicked: (Long) -> Unit,
|
||||
onHideDatePicker: () -> Unit,
|
||||
onSave: () -> Unit
|
||||
) {
|
||||
if (state.showDatePicker) {
|
||||
val pickerState = rememberDatePickerState(
|
||||
initialSelectedDateMillis = state.customUnlockMs ?: (System.currentTimeMillis() + UnlockPreset.ONE_YEAR.ms)
|
||||
)
|
||||
DatePickerDialog(
|
||||
onDismissRequest = onHideDatePicker,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val ms = pickerState.selectedDateMillis
|
||||
if (ms != null && ms > System.currentTimeMillis()) onDatePicked(ms)
|
||||
else onHideDatePicker()
|
||||
}) { Text("OK") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = onHideDatePicker) { Text("Cancel") } }
|
||||
) {
|
||||
DatePicker(state = pickerState)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -501,7 +649,7 @@ private fun CapsuleCreateScreen(
|
|||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("New Capsule", style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onBackground)
|
||||
Text(if (isEditing) "Edit Capsule" else "New Capsule", style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
|
||||
// Title field
|
||||
|
|
@ -558,13 +706,13 @@ private fun CapsuleCreateScreen(
|
|||
)
|
||||
)
|
||||
|
||||
// Unlock date presets
|
||||
// Unlock date presets + custom date
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Open on", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
UnlockPreset.entries.forEach { preset ->
|
||||
FilterChip(
|
||||
selected = state.selectedPreset == preset,
|
||||
selected = state.customUnlockMs == null && state.selectedPreset == preset,
|
||||
onClick = { onPresetSelect(preset) },
|
||||
label = { Text(preset.label, style = MaterialTheme.typography.labelSmall) },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
|
|
@ -573,9 +721,24 @@ private fun CapsuleCreateScreen(
|
|||
)
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = state.customUnlockMs != null,
|
||||
onClick = onShowDatePicker,
|
||||
label = {
|
||||
Text(
|
||||
if (state.customUnlockMs != null) formatDate(state.customUnlockMs) else "Custom date",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = CloserPalette.PurpleDeep.copy(alpha = 0.15f),
|
||||
selectedLabelColor = CloserPalette.PurpleDeep
|
||||
)
|
||||
)
|
||||
}
|
||||
val previewMs = state.customUnlockMs ?: (System.currentTimeMillis() + state.selectedPreset.ms)
|
||||
Text(
|
||||
text = "Will open on ${formatDate(System.currentTimeMillis() + state.selectedPreset.ms)}",
|
||||
text = "Will open on ${formatDate(previewMs)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
|
@ -595,7 +758,7 @@ private fun CapsuleCreateScreen(
|
|||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
||||
) {
|
||||
if (state.isSaving) CloserHeartLoader(size = 22.dp)
|
||||
else Text("Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge)
|
||||
else Text(if (isEditing) "Save changes" else "Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
|
@ -608,8 +771,28 @@ private fun CapsuleCreateScreen(
|
|||
private fun CapsuleDetailScreen(
|
||||
capsule: TimeCapsule,
|
||||
userId: String,
|
||||
onBack: () -> Unit
|
||||
onBack: () -> Unit,
|
||||
onEdit: (() -> Unit)?,
|
||||
onDelete: (() -> Unit)?,
|
||||
showDeleteConfirm: Boolean,
|
||||
isDeleting: Boolean,
|
||||
onConfirmDelete: () -> Unit,
|
||||
onDismissDelete: () -> Unit
|
||||
) {
|
||||
if (showDeleteConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissDelete,
|
||||
title = { Text("Delete capsule?") },
|
||||
text = { Text("This cannot be undone. The capsule and its contents will be permanently removed.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirmDelete) { Text("Delete", color = MaterialTheme.colorScheme.error) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissDelete) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val unlocked = capsule.isUnlocked
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
|
|
@ -633,6 +816,17 @@ private fun CapsuleDetailScreen(
|
|||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (onEdit != null) {
|
||||
IconButton(onClick = onEdit, enabled = !isDeleting) {
|
||||
Icon(Icons.Filled.Edit, contentDescription = "Edit", tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
if (onDelete != null) {
|
||||
IconButton(onClick = onDelete, enabled = !isDeleting) {
|
||||
if (isDeleting) CloserHeartLoader(size = 20.dp)
|
||||
else Icon(Icons.Filled.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f), modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue