android: scan the web recipe card's QR and import the look it points at

The landing page's QR card now carries a link to `GET
/api/photos/:id/preset.recipe`, so the app can pick a look up off a
screen instead of a file. PRESETS gains a SCAN QR chip next to IMPORT:
the code is decoded, checked against the preset route, fetched, and
stored through the same `storeImportedRecipe` path IMPORT already uses,
so an imported look and a scanned look land in one place.

Scanning runs on `expo-camera`, not the viewfinder's vision-camera:
v5's object output is iOS-only, Android's `createObjectOutput` throws.
That is a second camera library in the app, so the sheet pauses the
viewfinder (`paused` prop) — CameraX will not let two clients hold one
lens. A native rebuild is required for the new module.

The code holds a URL, never the recipe, so anything that is not the
preset route is refused by name rather than silently dropped. On a
release APK the plain-HTTP link is blocked by Android's cleartext
policy; debug builds have it on. Noted in 7_SCAN_QR.md along with the
web side, the regex, and the paused-viewfinder rule.
This commit is contained in:
2026-09-18 17:01:34 +07:00
parent e0526283ca
commit 1e1ead6f54
8 changed files with 392 additions and 9 deletions
+7 -2
View File
@@ -65,6 +65,10 @@ interface ViewfinderProps {
// a nonce so the panel puts the caret back in its input).
onWmEdit?: () => void;
cameraPermissionGranted: boolean;
// Another camera session wants the lens (the QR scanner's own sheet): the
// live session stops until it is up, exactly as it does for the app going
// to the background — two CameraX clients cannot hold one camera.
paused?: boolean;
onRequestCameraPermission: () => void;
photoOutput: CameraPhotoOutput;
// Composition ratio (FULL / 4:3 / 3:2 upright) driving the viewfinder band
@@ -303,6 +307,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
availableHeight,
topInset,
cameraPermissionGranted,
paused = false,
onRequestCameraPermission,
photoOutput,
aspectRatio,
@@ -2871,7 +2876,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
ref={skiaCameraRef}
style={StyleSheet.absoluteFill}
device={device}
isActive={appActive && !ultraWideOn}
isActive={appActive && !ultraWideOn && !paused}
outputs={cameraOutputs}
pixelFormat="yuv"
targetResolution={frameTarget}
@@ -2886,7 +2891,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
turning one on means turning the other off, never both at once. */}
<UltraWidePreview
style={StyleSheet.absoluteFill}
active={appActive && ultraWideOn}
active={appActive && ultraWideOn && !paused}
cover={uwCover}
/>
{/* Gesture layer: tap = AE/AF lock; vertical drag on the image adjusts