From 752d4972bb8b795c65218dc1be65e4def8d3d7cc Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 13 Jul 2026 14:49:10 +0200 Subject: [PATCH] Capture feedback (shutter sound + flash) and Paperless UX; v0.1.1 - Camera: system shutter sound (only when ringer isn't silenced) + brief white flash overlay on capture, for clear feedback even on silent. - Settings: snackbar confirmation when saving. - Export: explain why the Paperless upload button is disabled when unconfigured. - Bump versionCode 2 / versionName 0.1.1. Fixes #1, #2 Co-Authored-By: Claude Opus 4.8 --- app/build.gradle.kts | 4 +-- .../pdfscan/ui/screens/CameraScreen.kt | 31 +++++++++++++++++++ .../pdfscan/ui/screens/ExportScreen.kt | 5 +++ .../pdfscan/ui/screens/SettingsScreen.kt | 11 ++++++- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef6f7dd..1f2c1b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "eu.geyskens.pdfscan" minSdk = 26 targetSdk = 35 - versionCode = 1 - versionName = "0.1.0" + versionCode = 2 + versionName = "0.1.1" // OpenCV native libs are large; ship only the ABIs real phones use. ndk { diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt index e476f8a..0e5f46b 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt @@ -1,12 +1,16 @@ package eu.geyskens.pdfscan.ui.screens import android.Manifest +import android.media.AudioManager +import android.media.MediaActionSound import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +35,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -98,6 +103,22 @@ private fun CameraContent(vm: ScanViewModel, navController: androidx.navigation. } var capturing by remember { mutableStateOf(false) } + // Capture feedback: system shutter sound + a brief white flash over the preview, + // so there's clear confirmation even when the phone is on silent. + val shutterSound = remember { MediaActionSound().apply { load(MediaActionSound.SHUTTER_CLICK) } } + val audioManager = remember { ContextCompat.getSystemService(context, AudioManager::class.java) } + val flash = remember { Animatable(0f) } + var captureTick by remember { mutableIntStateOf(0) } + androidx.compose.runtime.LaunchedEffect(captureTick) { + if (captureTick > 0) { + flash.snapTo(0.75f) + flash.animateTo(0f, tween(320)) + } + } + androidx.compose.runtime.DisposableEffect(Unit) { + onDispose { shutterSound.release() } + } + val previewView = remember { PreviewView(context).apply { scaleType = PreviewView.ScaleType.FILL_CENTER } } @@ -146,6 +167,11 @@ private fun CameraContent(vm: ScanViewModel, navController: androidx.navigation. onClick = { if (capturing) return@FilledIconButton capturing = true + // Flash always; play the shutter sound only when the ringer isn't silenced. + if (audioManager?.ringerMode == AudioManager.RINGER_MODE_NORMAL) { + shutterSound.play(MediaActionSound.SHUTTER_CLICK) + } + captureTick++ val file = File(vm.capturesDir(), "cap_${System.currentTimeMillis()}.jpg") val options = ImageCapture.OutputFileOptions.Builder(file).build() imageCapture.takePicture( @@ -183,6 +209,11 @@ private fun CameraContent(vm: ScanViewModel, navController: androidx.navigation. ) } } + + // Shutter flash overlay (drawn on top, fades out quickly). + if (flash.value > 0f) { + Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = flash.value))) + } } androidx.compose.runtime.DisposableEffect(Unit) { diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt index 5776876..dcddfb5 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt @@ -155,6 +155,11 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { } if (!settings.paperlessUrl.isNotBlank() || !settings.hasToken) { + Text( + "Configureer eerst Paperless (server-URL + token) in de instellingen om te kunnen uploaden.", + style = androidx.compose.material3.MaterialTheme.typography.bodySmall, + color = Color(0xFF9A9A9A), + ) OutlinedButton( onClick = { navController.navigate(Routes.SETTINGS) }, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt index be5b395..df7b55b 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -51,8 +53,10 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) { var token by remember { mutableStateOf(vm.currentToken().orEmpty()) } var testing by remember { mutableStateOf(false) } var testResult by remember { mutableStateOf(null) } + val snackbarHostState = remember { SnackbarHostState() } Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( title = { Text("Instellingen") }, @@ -116,7 +120,12 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) { ) { Text("Verbinding testen") } Button( - onClick = { scope.launch { vm.savePaperless(url, token) } }, + onClick = { + scope.launch { + vm.savePaperless(url, token) + snackbarHostState.showSnackbar("Instellingen opgeslagen ✓") + } + }, modifier = Modifier.weight(1f), ) { Text("Opslaan") } }