feat: Scanning #6

Merged
elvis merged 24 commits from feat/scanning into master 2026-06-24 15:27:30 +00:00
Owner
No description provided.
Add CameraX (1.3.4) and ML Kit text recognition (19.0.0) libraries to
enable camera-based Pokemon card scanning. Also add CAMERA permission
to AndroidManifest with optional hardware feature declaration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The BlendMode.Clear operation on hardware-accelerated surfaces requires an
offscreen compositing layer to properly clear to transparent instead of black.
Added graphicsLayer modifier with CompositingStrategy.Offscreen to the Canvas
in CardAlignmentOverlay to ensure the transparent hole punch renders correctly.

Also upgraded Compose BOM to 2025.01.00 to support CompositingStrategy API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Screen.Scan route to sealed class in AppNavGraph.kt
- Add ScanScreen composable to NavHost with onCardConfirmed lambda navigating to CardDetail
- Add Scan tab to BottomNavBar with CameraAlt icon
- Navigation wiring verified with successful build and all tests passing

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- CardTextAnalyzer implements AutoCloseable; ScanScreen closes it via DisposableEffect
- NAME_REGEX now accepts dots, colons, and gender symbols (Mr. Mime, Type: Null, Nidoran♀/♂)
- searchByNameAndNumber uses COLLATE NOCASE so OCR case differences don't cause misses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements TDD approach with failing tests followed by implementation:
- CardDao.searchBySetAndNumber(setId, number): CardEntity?
  Query to find a card by set ID and number, case-insensitive number match
- CardRepository.searchByNumberInSet(number, setId): CardEntity?
  Cache-first lookup with API fallback using combined query filter

Tests added for cache hits, API fetches, and error handling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SessionCard data class for in-memory session tracking
- Expand ScanUiState with AddingToBinder variant
- Add binderRepository dependency to ScanViewModel
- Implement session management: addToSession (with dedup), updateSessionCardQuantity,
  removeFromSession, replaceSessionCard
- Implement set lock: lockSet/unlockSet, routes onTextDetected through
  searchByNumberInSet when a set is locked
- Implement binder operations: onAddToBinder, addCardToBinder,
  createBinderAndAddCard, addSessionToBinder, createBinderAndAddSession
- Expose sets, binders, manualSearchQuery, manualSearchResults, snackbarMessage StateFlows
- Add exhaustive AddingToBinder branch to ScanScreen when expression
- Add 11 new TDD tests covering all new behaviour (15 total, all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates SessionCardsSheet composable with manual card search, per-card quantity
controls, replace/delete actions, and batch "Add All to Binder" flow via
AddToBinderSheet overlay. Wires the sheet into ScanScreen using the existing
showSessionCards state variable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root-cause fixes:
- CardTextAnalyzer no longer silently aborts when the name regex fails; it
  now passes name="" so the set-detection path can still fire.
- ScanViewModel now extracts the card's printed total (e.g. "042/185" → 185),
  looks up the matching set via printedTotal, and calls searchByNumberInSet
  instead of the OCR-name-dependent searchByNameAndNumber. Falls back to
  name+number when total matches 0 or 2+ sets.

Also adds a temporary yellow debug overlay on ScanScreen showing the raw OCR
output (name/number/total) for in-device debugging.

DB schema bumped to v4: adds printedTotal column to the sets table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trainer, Item, Supporter, Stadium, etc. are printed prominently on cards
and matched by NAME_REGEX, causing them to be selected over the actual
card name. A BLOCKED_NAMES set filters them out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Older XY/SM-era Trainer cards print "TRAINER" in all-caps, which did not
match the title-case "Trainer" entry in BLOCKED_NAMES. Also sort ML Kit
text blocks by bounding box top coordinate so the topmost block (where the
card name lives) is always evaluated first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use Camera2Interop to set CONTROL_AF_MODE_CONTINUOUS_PICTURE on the
  Preview use case, preventing the camera from hunting/cycling focus
- Move ImageAnalysis analyzer to a dedicated background thread instead
  of running on the main executor
- Raise ImageAnalysis target resolution to 1280x720 so small card
  numbers at the bottom of the card are readable from a normal distance
- Switch name detection from textBlocks to textBlocks.flatMap(lines) so
  ML Kit blocks that group the card name and type label on adjacent lines
  no longer cause the name to be skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the blocking ScanResultSheet bottom sheet with a 1.5-second inline
"Added: {name}" banner, matching the existing NotFound overlay pattern.
Cards are now auto-added to the session the moment they are confirmed, with
duplicate scans incrementing quantity rather than creating new entries.
Removes the full ScanResultSheet composable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Camera freeze root cause: AndroidView.update called provider.unbindAll()
on every recomposition, restarting the camera session 3× per scan
(Idle→Analyzing→Confirmed→Idle). Fix: guard with lastBoundCameraIndex
so the camera only rebinds when selectedCameraIndex actually changes.

Secondary fix: pass analysisExecutor to CardTextAnalyzer so ML Kit's
addOnSuccessListener and addOnCompleteListener callbacks — including
imageProxy.close() — run on the background analysis thread instead of
the main thread, avoiding blocked frame delivery during Compose redraws.

Also replace the plain white "Added:" text with a green rounded-corner
chip containing a checkmark icon and the card name for better visibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Track lastScannedCard in ViewModel; scanning the same card again is
  a no-op (prevents accidental re-scan of an already-captured card)
- Replace transient 1.5s confirmation chip with a persistent pill at
  the bottom showing the last scanned card name, current session qty,
  and a + button to manually increment without re-scanning
- Vibrate (HapticFeedbackType.LongPress) each time a new distinct card
  is successfully added to the session
- Remove ScanUiState.Confirmed and ScanUiState.NotFound — both are now
  handled by returning directly to Idle; "not found" is silent
- Clear lastScannedCard when the session is committed to a binder
- Update ScanViewModelTest to reflect new state machine and add tests
  for duplicate prevention and incrementLastScanned

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLite's INSERT OR REPLACE deletes the existing row before inserting, which
triggered the ON DELETE CASCADE on binder_cards, silently removing cards from
binders whenever CardDetailViewModel refreshed card data from the network.

@Upsert (INSERT OR IGNORE + UPDATE) never deletes existing rows, so the
foreign key cascade never fires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
elvis merged commit c3718ea526 into master 2026-06-24 15:27:30 +00:00
elvis deleted branch feat/scanning 2026-06-24 15:27:30 +00:00
elvis referenced this pull request from a commit 2026-06-24 15:27:31 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
elvis/pokebox!6
No description provided.