diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanFlowScreen.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanFlowScreen.kt index 2729bad..b485fce 100644 --- a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanFlowScreen.kt +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanFlowScreen.kt @@ -2,11 +2,13 @@ package com.vaultdrop.mobile.ui.scan import android.Manifest.permission.CAMERA import android.content.pm.PackageManager +import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold @@ -80,6 +82,36 @@ fun ScanFlowScreen( } } + // Confirmation avant de quitter le scanner quand des pages sont en cours : + // la flèche CAMERA et le back système feront tous deux valider par un + // dialogue (sinon abandon = suppression des pages capturées). + var showExitConfirm by remember { mutableStateOf(false) } + + val requestExit: () -> Unit = { + if (pages.isNotEmpty()) { + showExitConfirm = true + } else { + viewModel.abandon() + onBack() + } + } + + val confirmExit: () -> Unit = { + showExitConfirm = false + viewModel.abandon() + onBack() + } + + // Retour système : étage courant (CROP/SESSION) → reprise dans le scanner ; + // seul CAMERA quitte l'écran (avec confirmation si pages présentes). + BackHandler { + when (stage) { + ScanStage.CAMERA -> requestExit() + ScanStage.CROP -> viewModel.retake() + ScanStage.SESSION -> viewModel.openCamera() + } + } + // Retour arrière contextuel (géré par BackHandler dans le content). val title = when (stage) { ScanStage.CAMERA -> stringResource(R.string.scan_title) @@ -94,7 +126,7 @@ fun ScanFlowScreen( navigationIcon = { IconButton(onClick = { when (stage) { - ScanStage.CAMERA -> { viewModel.abandon(); onBack() } + ScanStage.CAMERA -> requestExit() ScanStage.CROP -> viewModel.retake() ScanStage.SESSION -> viewModel.openCamera() } @@ -145,6 +177,23 @@ fun ScanFlowScreen( ) } } + if (showExitConfirm) { + AlertDialog( + onDismissRequest = { showExitConfirm = false }, + title = { Text(stringResource(R.string.scan_abandon_title)) }, + text = { Text(stringResource(R.string.scan_abandon_message)) }, + confirmButton = { + TextButton(onClick = confirmExit) { + Text(stringResource(R.string.scan_confirm)) + } + }, + dismissButton = { + TextButton(onClick = { showExitConfirm = false }) { + Text(stringResource(R.string.scan_cancel)) + } + }, + ) + } } } diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanViewModel.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanViewModel.kt index 44431cd..167c35d 100644 --- a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanViewModel.kt +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/scan/ScanViewModel.kt @@ -26,7 +26,9 @@ import com.vaultdrop.mobile.features.scan.ScanQuad import com.vaultdrop.mobile.features.scan.ScanRenderMode import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -96,6 +98,13 @@ class ScanViewModel @Inject constructor( private val _pages = MutableStateFlow>(emptyList()) val pages: StateFlow> = _pages.asStateFlow() + /** + * Scope de nettoyage indépendant du ViewModel : la suppression des fichiers + * d'une session abandonnée est lancée puis on quitte l'écran, ce qui détruit + * le ViewModel — `viewModelScope` serait annulé en plein nettoyage. + */ + private val cleanupScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + init { viewModelScope.launch { val rootId = defaultRootStore.get() @@ -303,7 +312,7 @@ class ScanViewModel @Inject constructor( /** Annule la session : suppression des fichiers temp + fermeture. */ fun abandon() { val currentSession = _session.value ?: return - viewModelScope.launch(Dispatchers.IO) { + cleanupScope.launch { scanRepository.getPages(currentSession.id).forEach { File(it.tempUri).delete() } sessionDir(currentSession.id)?.delete() _pages.value = emptyList()