1 Commits

Author SHA1 Message Date
sam
752d4972bb Capture feedback (shutter sound + flash) and Paperless UX; v0.1.1
All checks were successful
Build APK / build (push) Successful in 5m40s
- 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 <noreply@anthropic.com>
2026-07-13 14:49:10 +02:00
4 changed files with 48 additions and 3 deletions

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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(),

View File

@@ -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<String?>(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") }
}