remove swipe action
This commit is contained in:
@@ -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) }
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user