remove swipe action

This commit is contained in:
m
2026-09-12 20:58:27 +02:00
parent 7cc8c6eb24
commit 86006c285e
2 changed files with 10 additions and 125 deletions
@@ -1,14 +1,8 @@
package com.vaultdrop.mobile.ui.review package com.vaultdrop.mobile.ui.review
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -24,26 +18,16 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.data.local.entity.FileEntity import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.ui.components.FileCategoryIcon import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.document.content.DocumentContentViewer import com.vaultdrop.mobile.ui.document.content.DocumentContentViewer
import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -51,34 +35,27 @@ import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
/** /**
* Pile de cartes « tinder » pour traiter les nouveaux documents. * Pile de cartes du mode review : la carte du dessus affiche le contenu réel
* * du document ; les cartes du dessous ne sont que des emplacements simplifiés
* La carte du dessus affiche le contenu réel du document et glisse à * (nom + catégorie), le contenu lourd n'étant chargé que pour la carte
* gauche (supprimer) ou à droite (garder) dès qu'elle dépasse le seuil ; * visible. Les actions (garder/supprimer) sont pilotées par les boutons de
* les cartes du dessous ne sont que des emplacements simplifiés (nom + * l'écran, pas par le geste.
* catégorie), le contenu lourd n'étant chargé que pour la carte visible.
*/ */
@Composable @Composable
fun SwipeCardDeck( fun SwipeCardDeck(
cards: List<FileEntity>, cards: List<FileEntity>,
onKeep: (FileEntity) -> Unit,
onDelete: (FileEntity) -> Unit,
onOpenExternalFailed: () -> Unit, onOpenExternalFailed: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BoxWithConstraints(modifier = modifier) { Box(modifier = modifier) {
val widthPx = with(LocalDensity.current) { maxWidth.toPx() }
val visible = cards.take(MAX_STACK) val visible = cards.take(MAX_STACK)
visible.asReversed().forEach { file -> visible.asReversed().forEach { file ->
val depth = visible.indexOf(file) val depth = visible.indexOf(file)
key(file.resourceId) { key(file.resourceId) {
if (depth == 0) { if (depth == 0) {
SwipeableTopCard( TopCard(
file = file, file = file,
widthPx = widthPx,
onKeep = { onKeep(file) },
onDelete = { onDelete(file) },
onOpenExternalFailed = onOpenExternalFailed, onOpenExternalFailed = onOpenExternalFailed,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
@@ -94,57 +71,17 @@ fun SwipeCardDeck(
} }
} }
/** Carte du dessus : contenu réel du document + barre méta. */
@Composable @Composable
private fun SwipeableTopCard( private fun TopCard(
file: FileEntity, file: FileEntity,
widthPx: Float,
onKeep: () -> Unit,
onDelete: () -> Unit,
onOpenExternalFailed: () -> Unit, onOpenExternalFailed: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val offsetX = remember(file.resourceId) { Animatable(0f) }
val scope = rememberCoroutineScope()
val threshold = widthPx * SWIPE_THRESHOLD
Box( Box(
modifier = modifier modifier = modifier
.graphicsLayer {
translationX = offsetX.value
rotationZ = (offsetX.value / widthPx) * MAX_ROTATION_DEG
}
.clip(CardShape) .clip(CardShape)
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface),
.pointerInput(file.resourceId) {
detectHorizontalDragGestures(
onHorizontalDrag = { change, dragAmount ->
change.consume()
scope.launch {
offsetX.snapTo((offsetX.value + dragAmount).coerceIn(-widthPx, widthPx))
}
},
onDragEnd = {
scope.launch {
when {
offsetX.value >= threshold -> {
offsetX.animateTo(widthPx * 1.7f, tween(DRAG_OUT_MS))
onKeep()
}
offsetX.value <= -threshold -> {
offsetX.animateTo(-widthPx * 1.7f, tween(DRAG_OUT_MS))
onDelete()
}
else -> offsetX.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow))
}
}
},
onDragCancel = {
scope.launch { offsetX.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow)) }
},
)
},
) { ) {
DocumentContentViewer( DocumentContentViewer(
file = file, file = file,
@@ -158,26 +95,6 @@ private fun SwipeableTopCard(
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth(), .fillMaxWidth(),
) )
val progress = (offsetX.value / threshold).coerceIn(-1f, 1f)
ReviewSwipeLabel(
text = stringResource(R.string.review_keep),
color = KeepColor,
visible = progress > 0f,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp, top = 72.dp)
.alpha(progress),
)
ReviewSwipeLabel(
text = stringResource(R.string.review_delete),
color = DeleteColor,
visible = progress < 0f,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 16.dp, top = 72.dp)
.alpha(-progress),
)
} }
} }
@@ -246,31 +163,6 @@ private fun ReviewCardMetaBar(file: FileEntity, modifier: Modifier = Modifier) {
} }
} }
/** Badge « GARDER » / « SUPPRIMER » affiché pendant le drag. */
@Composable
private fun ReviewSwipeLabel(
text: String,
color: Color,
visible: Boolean,
modifier: Modifier = Modifier,
) {
if (!visible) return
Surface(
modifier = modifier,
color = color,
shape = RoundedCornerShape(8.dp),
) {
Text(
text = text,
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}
private fun formatSize(bytes: Long): String { private fun formatSize(bytes: Long): String {
val kb = 1024.0 val kb = 1024.0
val mb = kb * 1024 val mb = kb * 1024
@@ -289,11 +181,6 @@ private fun formatDate(file: FileEntity): String {
} }
private val CardShape = RoundedCornerShape(20.dp) private val CardShape = RoundedCornerShape(20.dp)
private val KeepColor = Color(0xFF2E7D32)
private val DeleteColor = Color(0xFFC62828)
private const val MAX_STACK = 3 private const val MAX_STACK = 3
private const val SWIPE_THRESHOLD = 0.3f
private const val MAX_ROTATION_DEG = 12f
private const val DRAG_OUT_MS = 220
private const val STACK_SHRINK = 0.045f private const val STACK_SHRINK = 0.045f
private const val STACK_OFFSET_DP = 18f private const val STACK_OFFSET_DP = 18f
@@ -128,8 +128,6 @@ fun SwipeReviewScreen(
) { ) {
SwipeCardDeck( SwipeCardDeck(
cards = state.cards, cards = state.cards,
onKeep = viewModel::keep,
onDelete = viewModel::delete,
onOpenExternalFailed = { onOpenExternalFailed = {
scope.launch { snackbarHostState.showSnackbar(openExternalErrorMsg) } scope.launch { snackbarHostState.showSnackbar(openExternalErrorMsg) }
}, },