add document scanner
This commit is contained in:
@@ -15,9 +15,18 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.vaultdrop.mobile"
|
applicationId = "com.vaultdrop.mobile"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
// targetSdk 35 exige des .so alignés en 16K ; les libs précompilées
|
||||||
|
// OpenCV/CameraX sont en 4K (warning de compatibilité sur devices
|
||||||
|
// 16K-pages). On reste en 34 tant qu'elles ne sont pas mises à jour.
|
||||||
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.0"
|
||||||
|
|
||||||
|
// OpenCV natif : on conserve uniquement les ABIs ciblés
|
||||||
|
// (arm64 = devices, x86_64 = émulateur) pour garder l'APK raisonnable.
|
||||||
|
ndk {
|
||||||
|
abiFilters += listOf("arm64-v8a", "x86_64")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -73,6 +82,11 @@ dependencies {
|
|||||||
ksp(libs.androidx.hilt.compiler)
|
ksp(libs.androidx.hilt.compiler)
|
||||||
implementation(libs.androidx.work.runtime.ktx)
|
implementation(libs.androidx.work.runtime.ktx)
|
||||||
|
|
||||||
|
implementation(libs.androidx.camera.camera2)
|
||||||
|
implementation(libs.androidx.camera.lifecycle)
|
||||||
|
implementation(libs.androidx.camera.view)
|
||||||
|
implementation(libs.opencv)
|
||||||
|
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
implementation(libs.retrofit.converter.moshi)
|
implementation(libs.retrofit.converter.moshi)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".VaultDropApplication"
|
android:name=".VaultDropApplication"
|
||||||
|
|||||||
@@ -5,24 +5,27 @@ import androidx.room.RoomDatabase
|
|||||||
import com.vaultdrop.mobile.data.local.dao.FileDao
|
import com.vaultdrop.mobile.data.local.dao.FileDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.FolderDao
|
import com.vaultdrop.mobile.data.local.dao.FolderDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
||||||
|
import com.vaultdrop.mobile.data.local.dao.ScanDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.UserPreferenceDao
|
import com.vaultdrop.mobile.data.local.dao.UserPreferenceDao
|
||||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||||
import com.vaultdrop.mobile.data.local.entity.PendingOperationEntity
|
import com.vaultdrop.mobile.data.local.entity.PendingOperationEntity
|
||||||
|
import com.vaultdrop.mobile.data.local.entity.ScanPageEntity
|
||||||
|
import com.vaultdrop.mobile.data.local.entity.ScanSessionEntity
|
||||||
import com.vaultdrop.mobile.data.local.entity.UserPreferenceEntity
|
import com.vaultdrop.mobile.data.local.entity.UserPreferenceEntity
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* DB SQLite locale, `dot.db` (même nom que la version Expo).
|
* DB SQLite locale, `dot.db` (même nom que la version Expo).
|
||||||
* v1: folders ; v2: user_preferences ; v3: files ; v4: category sur files ;
|
* v1: folders ; v2: user_preferences ; v3: files ; v4: category sur files ;
|
||||||
* v5: created_in_app sur folders ; v6: processed sur files (mode review) ;
|
* v5: created_in_app sur folders ; v6: processed sur files (mode review) ;
|
||||||
* v7: pending_operations (outbox).
|
* v7: pending_operations (outbox) ; v8: scan_sessions + scan_pages (scanner).
|
||||||
*/
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
FolderEntity::class, UserPreferenceEntity::class, FileEntity::class,
|
FolderEntity::class, UserPreferenceEntity::class, FileEntity::class,
|
||||||
PendingOperationEntity::class,
|
PendingOperationEntity::class, ScanSessionEntity::class, ScanPageEntity::class,
|
||||||
],
|
],
|
||||||
version = 7,
|
version = 8,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -31,4 +34,5 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun userPreferenceDao(): UserPreferenceDao
|
abstract fun userPreferenceDao(): UserPreferenceDao
|
||||||
abstract fun fileDao(): FileDao
|
abstract fun fileDao(): FileDao
|
||||||
abstract fun pendingOperationDao(): PendingOperationDao
|
abstract fun pendingOperationDao(): PendingOperationDao
|
||||||
|
abstract fun scanDao(): ScanDao
|
||||||
}
|
}
|
||||||
+42
-1
@@ -20,6 +20,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
* v7 : table `pending_operations` (outbox) — file des mutations locales à
|
* v7 : table `pending_operations` (outbox) — file des mutations locales à
|
||||||
* pousser vers `POST /sync/ops` (uuid 32-hex client-generated, cf.
|
* pousser vers `POST /sync/ops` (uuid 32-hex client-generated, cf.
|
||||||
* docs/api-v1.md §6.1).
|
* docs/api-v1.md §6.1).
|
||||||
|
* v8 : tables `scan_sessions` + `scan_pages` (scanner appareil photo) — session
|
||||||
|
* multi-pages persistée pour survivre au process death avant export SAF.
|
||||||
*/
|
*/
|
||||||
object Migrations {
|
object Migrations {
|
||||||
|
|
||||||
@@ -117,7 +119,46 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_7_8 = object : Migration(7, 8) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `scan_sessions` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`resource_id` TEXT NOT NULL,
|
||||||
|
`root_folder_id` TEXT,
|
||||||
|
`status` TEXT NOT NULL,
|
||||||
|
`created_at` INTEGER NOT NULL,
|
||||||
|
`updated_at` INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_scan_sessions_resource_id` ON `scan_sessions` (`resource_id`)")
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `scan_pages` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`resource_id` TEXT NOT NULL,
|
||||||
|
`session_id` INTEGER NOT NULL,
|
||||||
|
`temp_uri` TEXT NOT NULL,
|
||||||
|
`corners_json` TEXT NOT NULL,
|
||||||
|
`width` INTEGER NOT NULL,
|
||||||
|
`height` INTEGER NOT NULL,
|
||||||
|
`sort_order` INTEGER NOT NULL,
|
||||||
|
`status` TEXT NOT NULL,
|
||||||
|
`created_at` INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(`session_id`) REFERENCES `scan_sessions`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_scan_pages_resource_id` ON `scan_pages` (`resource_id`)")
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS `index_scan_pages_session_id` ON `scan_pages` (`session_id`)")
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS `index_scan_pages_sort_order` ON `scan_pages` (`sort_order`)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val ALL: Array<Migration> = arrayOf(
|
val ALL: Array<Migration> = arrayOf(
|
||||||
MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7,
|
MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.vaultdrop.mobile.data.local.AppDatabase
|
|||||||
import com.vaultdrop.mobile.data.local.dao.FileDao
|
import com.vaultdrop.mobile.data.local.dao.FileDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.FolderDao
|
import com.vaultdrop.mobile.data.local.dao.FolderDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
||||||
|
import com.vaultdrop.mobile.data.local.dao.ScanDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.UserPreferenceDao
|
import com.vaultdrop.mobile.data.local.dao.UserPreferenceDao
|
||||||
import com.vaultdrop.mobile.data.local.migration.Migrations
|
import com.vaultdrop.mobile.data.local.migration.Migrations
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -37,4 +38,7 @@ object DatabaseModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providePendingOperationDao(db: AppDatabase): PendingOperationDao = db.pendingOperationDao()
|
fun providePendingOperationDao(db: AppDatabase): PendingOperationDao = db.pendingOperationDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideScanDao(db: AppDatabase): ScanDao = db.scanDao()
|
||||||
}
|
}
|
||||||
@@ -11,11 +11,14 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.DocumentScanner
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -43,6 +46,7 @@ fun DashboardScreen(
|
|||||||
connectionStatusViewModel: ConnectionStatusViewModel,
|
connectionStatusViewModel: ConnectionStatusViewModel,
|
||||||
syncViewModel: SyncViewModel,
|
syncViewModel: SyncViewModel,
|
||||||
onOpenReview: () -> Unit,
|
onOpenReview: () -> Unit,
|
||||||
|
onOpenScan: () -> Unit,
|
||||||
viewModel: DashboardViewModel = hiltViewModel(),
|
viewModel: DashboardViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
|
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
|
||||||
@@ -64,6 +68,18 @@ fun DashboardScreen(
|
|||||||
bottomBar = {
|
bottomBar = {
|
||||||
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
SmallFloatingActionButton(
|
||||||
|
onClick = onOpenScan,
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.DocumentScanner,
|
||||||
|
contentDescription = stringResource(R.string.scan_title),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.vaultdrop.mobile.ui.folderdetail.FolderDetailScreen
|
|||||||
import com.vaultdrop.mobile.ui.folderlist.FolderListScreen
|
import com.vaultdrop.mobile.ui.folderlist.FolderListScreen
|
||||||
import com.vaultdrop.mobile.ui.pdfbuilder.PdfBuilderScreen
|
import com.vaultdrop.mobile.ui.pdfbuilder.PdfBuilderScreen
|
||||||
import com.vaultdrop.mobile.ui.review.SwipeReviewScreen
|
import com.vaultdrop.mobile.ui.review.SwipeReviewScreen
|
||||||
|
import com.vaultdrop.mobile.ui.scan.ScanFlowScreen
|
||||||
import com.vaultdrop.mobile.ui.search.SearchScreen
|
import com.vaultdrop.mobile.ui.search.SearchScreen
|
||||||
import com.vaultdrop.mobile.ui.settings.SettingsScreen
|
import com.vaultdrop.mobile.ui.settings.SettingsScreen
|
||||||
import com.vaultdrop.mobile.ui.watchedfolders.WatchedFoldersScreen
|
import com.vaultdrop.mobile.ui.watchedfolders.WatchedFoldersScreen
|
||||||
@@ -42,6 +43,7 @@ object Routes {
|
|||||||
const val ARG_DOCUMENT = "documentResourceId"
|
const val ARG_DOCUMENT = "documentResourceId"
|
||||||
const val PDF_BUILDER = "pdfbuilder/{ids}"
|
const val PDF_BUILDER = "pdfbuilder/{ids}"
|
||||||
const val ARG_BUILDER_IDS = "ids"
|
const val ARG_BUILDER_IDS = "ids"
|
||||||
|
const val SCAN = "scan"
|
||||||
|
|
||||||
fun folder(folderResourceId: String): String = "folder/$folderResourceId"
|
fun folder(folderResourceId: String): String = "folder/$folderResourceId"
|
||||||
fun document(documentResourceId: String): String = "document/$documentResourceId"
|
fun document(documentResourceId: String): String = "document/$documentResourceId"
|
||||||
@@ -126,6 +128,12 @@ fun NavGraph(
|
|||||||
connectionStatusViewModel = connectionStatusViewModel,
|
connectionStatusViewModel = connectionStatusViewModel,
|
||||||
syncViewModel = syncViewModel,
|
syncViewModel = syncViewModel,
|
||||||
onOpenReview = { navController.navigate(Routes.REVIEW) },
|
onOpenReview = { navController.navigate(Routes.REVIEW) },
|
||||||
|
onOpenScan = { navController.navigate(Routes.SCAN) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(Routes.SCAN) {
|
||||||
|
ScanFlowScreen(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Routes.REVIEW) {
|
composable(Routes.REVIEW) {
|
||||||
|
|||||||
@@ -179,4 +179,37 @@
|
|||||||
<string name="pdf_builder_preview">Aperçu</string>
|
<string name="pdf_builder_preview">Aperçu</string>
|
||||||
<string name="pdf_builder_close_preview">Fermer l\'aperçu</string>
|
<string name="pdf_builder_close_preview">Fermer l\'aperçu</string>
|
||||||
<string name="pdf_generated_folder">PDF générés</string>
|
<string name="pdf_generated_folder">PDF générés</string>
|
||||||
|
|
||||||
|
<!-- Scanner appareil photo -->
|
||||||
|
<string name="scan_title">Scanner</string>
|
||||||
|
<string name="scan_capture">Capturer</string>
|
||||||
|
<string name="scan_pages_count">%1$d page(s)</string>
|
||||||
|
<string name="scan_view_session">Voir les pages</string>
|
||||||
|
<string name="scan_camera_hint">Cadre le document puis capture.</string>
|
||||||
|
<string name="scan_permission_rationale">Autorise la caméra pour scanner des documents.</string>
|
||||||
|
<string name="scan_permission_grant">Autoriser la caméra</string>
|
||||||
|
<string name="scan_crop_title">Recadrer</string>
|
||||||
|
<string name="scan_confirm_page">Valider la page</string>
|
||||||
|
<string name="scan_retake">Reprendre</string>
|
||||||
|
<string name="scan_render_color">Couleur</string>
|
||||||
|
<string name="scan_render_gray">N&B</string>
|
||||||
|
<string name="scan_render_high">N&B contrasté</string>
|
||||||
|
<string name="scan_add_page">Ajouter une page</string>
|
||||||
|
<string name="scan_session_title">Pages scannées</string>
|
||||||
|
<string name="scan_delete_page">Supprimer</string>
|
||||||
|
<string name="scan_move_up">Monter</string>
|
||||||
|
<string name="scan_move_down">Descendre</string>
|
||||||
|
<string name="scan_empty">Aucune page pour le moment.\nCapture ta première page.</string>
|
||||||
|
<string name="scan_export">Exporter vers le dossier VaultDrop</string>
|
||||||
|
<string name="scan_exporting">Export en cours…</string>
|
||||||
|
<string name="scan_abandon">Abandonner</string>
|
||||||
|
<string name="scan_abandon_title">Abandonner le scan ?</string>
|
||||||
|
<string name="scan_abandon_message">Les pages capturées seront supprimées.</string>
|
||||||
|
<string name="scan_confirm">Confirmer</string>
|
||||||
|
<string name="scan_cancel">Annuler</string>
|
||||||
|
<string name="scan_error_session">Session de scan indisponible.</string>
|
||||||
|
<string name="scan_error_capture">Impossible d\'utiliser cette capture.</string>
|
||||||
|
<string name="scan_error_page">Impossible d\'enregistrer la page.</string>
|
||||||
|
<string name="scan_error_export">Impossible d\'exporter les pages.</string>
|
||||||
|
<string name="scan_export_done">Pages exportées</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -17,6 +17,8 @@ moshi = "1.15.1"
|
|||||||
securityCrypto = "1.0.0"
|
securityCrypto = "1.0.0"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
|
cameraX = "1.3.4"
|
||||||
|
opencv = "4.14.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
robolectric = "4.14.1"
|
robolectric = "4.14.1"
|
||||||
androidxTestCore = "1.6.1"
|
androidxTestCore = "1.6.1"
|
||||||
@@ -29,6 +31,10 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
|
|||||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
|
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
|
||||||
|
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
|
||||||
|
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" }
|
||||||
|
opencv = { group = "org.opencv", name = "opencv", version.ref = "opencv" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
|||||||
Reference in New Issue
Block a user