Capture feedback (shutter sound + flash) and Paperless UX; v0.1.1
All checks were successful
Build APK / build (push) Successful in 5m40s
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>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user