mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #4158 from vitorpamplona/claude/scanner-and-cache-log-fixes
fix: stop the QR scanner pumping the zoom in and out
This commit is contained in:
-45
@@ -106,11 +106,9 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.min
|
||||
|
||||
/** What the caller did with a decoded payload — and therefore what the scanner does next. */
|
||||
enum class ScanOutcome {
|
||||
@@ -350,8 +348,6 @@ private fun QrCameraScanner(
|
||||
snapshotFlow { state.zoomRatio }.collect { runCatching { control.setZoomRatio(it) } }
|
||||
}
|
||||
|
||||
AutoZoomSweep(state = state, enabled = camera != null)
|
||||
|
||||
LaunchedEffect(state.notice) {
|
||||
if (state.notice != null) {
|
||||
delay(NOTICE_DURATION_MS)
|
||||
@@ -394,7 +390,6 @@ private fun QrCameraScanner(
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures { _, _, zoom, _ ->
|
||||
if (zoom != 1f) {
|
||||
state.onPinch()
|
||||
state.zoomRatio = (state.zoomRatio * zoom).coerceIn(1f, maxOf(1f, state.maxZoomRatio))
|
||||
}
|
||||
}
|
||||
@@ -503,44 +498,6 @@ private fun ScanOverlayCanvas(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps the zoom while nothing is decoding.
|
||||
*
|
||||
* A code too small in frame to resolve is the single most common reason a scan fails, and no
|
||||
* amount of decoder tuning fixes it — there are not enough pixels per module to read. Rather than
|
||||
* leave the user to work that out and walk closer, the camera pushes in and back out on its own.
|
||||
* It stops for good once the user pinches: they have taken over.
|
||||
*/
|
||||
@Composable
|
||||
private fun AutoZoomSweep(
|
||||
state: QrScannerState,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
LaunchedEffect(enabled, state.autoZoomEnabled) {
|
||||
if (!enabled || !state.autoZoomEnabled) return@LaunchedEffect
|
||||
|
||||
var sweep = 0f
|
||||
while (isActive) {
|
||||
delay(AUTO_ZOOM_TICK_MS)
|
||||
|
||||
if (state.msSinceLastDetection < QrScannerState.AUTO_ZOOM_AFTER_MS) {
|
||||
if (sweep != 0f) {
|
||||
sweep = 0f
|
||||
state.zoomRatio = 1f
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
val ceiling = min(state.maxZoomRatio, QrScannerState.AUTO_ZOOM_MAX)
|
||||
if (ceiling <= 1.01f) continue
|
||||
|
||||
sweep = (sweep + AUTO_ZOOM_TICK_MS.toFloat() / AUTO_ZOOM_PERIOD_MS) % 1f
|
||||
val triangle = if (sweep < 0.5f) sweep * 2f else (1f - sweep) * 2f
|
||||
state.zoomRatio = 1f + triangle * (ceiling - 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The visible code nearest the tap, or null when the tap was not on one. */
|
||||
private fun pickCandidateAt(
|
||||
tap: Offset,
|
||||
@@ -620,8 +577,6 @@ private val ANALYSIS_RESOLUTION =
|
||||
ResolutionStrategy(Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER),
|
||||
).build()
|
||||
|
||||
private const val AUTO_ZOOM_TICK_MS = 100L
|
||||
private const val AUTO_ZOOM_PERIOD_MS = 3_000f
|
||||
private const val NOTICE_DURATION_MS = 3_000L
|
||||
private const val FOCUS_RING_DURATION_MS = 800L
|
||||
private const val FOCUS_AUTO_CANCEL_SECONDS = 4L
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ fun QrScannerControls(
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.autoZoomEnabled && state.zoomRatio > 1.05f) {
|
||||
if (state.zoomRatio > 1.05f) {
|
||||
ZoomChip(
|
||||
zoomRatio = state.zoomRatio,
|
||||
onReset = { state.resetZoom() },
|
||||
|
||||
-32
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@@ -63,13 +62,6 @@ class QrScannerState {
|
||||
var zoomRatio by mutableFloatStateOf(1f)
|
||||
var maxZoomRatio by mutableFloatStateOf(1f)
|
||||
|
||||
/**
|
||||
* Auto-zoom stops for good the first time the user pinches. Someone who has framed the shot
|
||||
* themselves does not want the camera arguing about it.
|
||||
*/
|
||||
var autoZoomEnabled by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
/** A payload we decoded but the caller could not use; drives the explain-what-happened sheet. */
|
||||
var rejected by mutableStateOf<ScannedPayload?>(null)
|
||||
|
||||
@@ -89,16 +81,6 @@ class QrScannerState {
|
||||
private var lastSubmittedAt = 0L
|
||||
private var darkSinceMs = 0L
|
||||
|
||||
/** Milliseconds since anything at all was decoded — what auto-zoom watches. */
|
||||
var msSinceLastDetection by mutableLongStateOf(0L)
|
||||
private set
|
||||
|
||||
private var lastDetectionMs = 0L
|
||||
|
||||
fun onPinch() {
|
||||
autoZoomEnabled = false
|
||||
}
|
||||
|
||||
fun resetZoom() {
|
||||
zoomRatio = 1f
|
||||
}
|
||||
@@ -121,11 +103,6 @@ class QrScannerState {
|
||||
// whether or not the camera is busy reading something else.
|
||||
if (sequence.dropIfStale(nowMs)) sequenceProgress = null
|
||||
|
||||
// Seed the clock on the first frame. Left at zero, the very first empty frame would read
|
||||
// as "nothing decoded since the epoch" and send auto-zoom hunting before the user has had
|
||||
// a chance to aim.
|
||||
if (lastDetectionMs == 0L) lastDetectionMs = nowMs
|
||||
|
||||
// Nothing is decided while the "we can't open this" sheet is up: the user is reading it,
|
||||
// and the offending code is very probably still sitting in front of the lens.
|
||||
if (rejected != null) {
|
||||
@@ -136,12 +113,9 @@ class QrScannerState {
|
||||
val found = scan.results.distinctBy { it.text }
|
||||
if (found.isEmpty()) {
|
||||
candidates = emptyList()
|
||||
msSinceLastDetection = nowMs - lastDetectionMs
|
||||
return null
|
||||
}
|
||||
|
||||
lastDetectionMs = nowMs
|
||||
msSinceLastDetection = 0
|
||||
candidates = found
|
||||
|
||||
// A multi-part code is never complete on its first part, so it can't be a single answer.
|
||||
@@ -232,11 +206,5 @@ class QrScannerState {
|
||||
|
||||
/** Don't offer the torch for a thumb over the lens or a moment of shadow. */
|
||||
const val DARK_DWELL_MS = 1_000L
|
||||
|
||||
/** How long with nothing decoded before auto-zoom starts hunting. */
|
||||
const val AUTO_ZOOM_AFTER_MS = 1_200L
|
||||
|
||||
/** Ceiling for the auto-zoom sweep — past this, focus and shake beat the extra reach. */
|
||||
const val AUTO_ZOOM_MAX = 2.5f
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user