add document scanner

This commit is contained in:
m
2026-09-13 22:05:13 +02:00
parent 612e3b0eff
commit 8a4cd44ef8
2 changed files with 60 additions and 2 deletions
@@ -2,11 +2,13 @@ package com.vaultdrop.mobile.ui.scan
import android.Manifest.permission.CAMERA import android.Manifest.permission.CAMERA
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold 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). // Retour arrière contextuel (géré par BackHandler dans le content).
val title = when (stage) { val title = when (stage) {
ScanStage.CAMERA -> stringResource(R.string.scan_title) ScanStage.CAMERA -> stringResource(R.string.scan_title)
@@ -94,7 +126,7 @@ fun ScanFlowScreen(
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(onClick = {
when (stage) { when (stage) {
ScanStage.CAMERA -> { viewModel.abandon(); onBack() } ScanStage.CAMERA -> requestExit()
ScanStage.CROP -> viewModel.retake() ScanStage.CROP -> viewModel.retake()
ScanStage.SESSION -> viewModel.openCamera() 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))
}
},
)
}
} }
} }
@@ -26,7 +26,9 @@ import com.vaultdrop.mobile.features.scan.ScanQuad
import com.vaultdrop.mobile.features.scan.ScanRenderMode import com.vaultdrop.mobile.features.scan.ScanRenderMode
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -96,6 +98,13 @@ class ScanViewModel @Inject constructor(
private val _pages = MutableStateFlow<List<ScanPageEntity>>(emptyList()) private val _pages = MutableStateFlow<List<ScanPageEntity>>(emptyList())
val pages: StateFlow<List<ScanPageEntity>> = _pages.asStateFlow() val pages: StateFlow<List<ScanPageEntity>> = _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 { init {
viewModelScope.launch { viewModelScope.launch {
val rootId = defaultRootStore.get() val rootId = defaultRootStore.get()
@@ -303,7 +312,7 @@ class ScanViewModel @Inject constructor(
/** Annule la session : suppression des fichiers temp + fermeture. */ /** Annule la session : suppression des fichiers temp + fermeture. */
fun abandon() { fun abandon() {
val currentSession = _session.value ?: return val currentSession = _session.value ?: return
viewModelScope.launch(Dispatchers.IO) { cleanupScope.launch {
scanRepository.getPages(currentSession.id).forEach { File(it.tempUri).delete() } scanRepository.getPages(currentSession.id).forEach { File(it.tempUri).delete() }
sessionDir(currentSession.id)?.delete() sessionDir(currentSession.id)?.delete()
_pages.value = emptyList() _pages.value = emptyList()