react-native-image-stitcher 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/README.md +28 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +3 -2
- package/dist/camera/ARCameraView.d.ts +10 -0
- package/dist/camera/ARCameraView.js +1 -0
- package/dist/camera/Camera.d.ts +191 -0
- package/dist/camera/Camera.js +250 -9
- package/dist/camera/OrientationDriftModal.d.ts +83 -0
- package/dist/camera/OrientationDriftModal.js +159 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +13 -1
- package/dist/camera/PanoramaBandOverlay.js +106 -45
- package/dist/camera/PanoramaSettingsModal.js +15 -1
- package/dist/camera/ViewportCropOverlay.d.ts +35 -31
- package/dist/camera/ViewportCropOverlay.js +39 -30
- package/dist/camera/useDeviceOrientation.d.ts +18 -9
- package/dist/camera/useDeviceOrientation.js +18 -9
- package/dist/camera/useOrientationDrift.d.ts +104 -0
- package/dist/camera/useOrientationDrift.js +120 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -1
- package/dist/stitching/incremental.d.ts +5 -3
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +7 -1
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +4 -3
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +9 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -7
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +18 -1
- package/src/camera/Camera.tsx +639 -21
- package/src/camera/OrientationDriftModal.tsx +224 -0
- package/src/camera/PanoramaBandOverlay.tsx +135 -49
- package/src/camera/PanoramaSettingsModal.tsx +14 -0
- package/src/camera/ViewportCropOverlay.tsx +52 -30
- package/src/camera/__tests__/useOrientationDrift.test.ts +169 -0
- package/src/camera/useDeviceOrientation.ts +18 -9
- package/src/camera/useOrientationDrift.ts +172 -0
- package/src/index.ts +13 -0
- package/src/stitching/incremental.ts +5 -3
package/src/camera/Camera.tsx
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
StyleSheet,
|
|
53
53
|
Text,
|
|
54
54
|
View,
|
|
55
|
+
useWindowDimensions,
|
|
55
56
|
type StyleProp,
|
|
56
57
|
type ViewStyle,
|
|
57
58
|
} from 'react-native';
|
|
@@ -66,13 +67,21 @@ import { useARSession } from '../ar/useARSession';
|
|
|
66
67
|
import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
|
|
67
68
|
import { CameraShutter } from './CameraShutter';
|
|
68
69
|
import { CameraView } from './CameraView';
|
|
70
|
+
import { CaptureHeader, type CaptureHeaderProps } from './CaptureHeader';
|
|
71
|
+
import { CapturePreview, type CapturePreviewAction } from './CapturePreview';
|
|
72
|
+
import {
|
|
73
|
+
CaptureThumbnailStrip,
|
|
74
|
+
type CaptureThumbnailItem,
|
|
75
|
+
} from './CaptureThumbnailStrip';
|
|
69
76
|
import { CaptureStatusOverlay, type CaptureStatusPhase } from './CaptureStatusOverlay';
|
|
70
77
|
import { CaptureDebugOverlay } from './CaptureDebugOverlay';
|
|
71
78
|
import { CaptureMemoryPill } from './CaptureMemoryPill';
|
|
72
79
|
import { CaptureKeyframePill } from './CaptureKeyframePill';
|
|
73
80
|
import { CaptureOrientationPill } from './CaptureOrientationPill';
|
|
74
81
|
import { CaptureStitchStatsToast, useStitchStatsToast } from './CaptureStitchStatsToast';
|
|
82
|
+
import { IncrementalPanGuide } from './IncrementalPanGuide';
|
|
75
83
|
import { PanoramaBandOverlay } from './PanoramaBandOverlay';
|
|
84
|
+
import { PanoramaGuidance } from './PanoramaGuidance';
|
|
76
85
|
import { type PanoramaSettings } from './PanoramaSettings';
|
|
77
86
|
import { panoramaSettingsToNativeConfig } from './PanoramaSettingsBridge';
|
|
78
87
|
import { PanoramaSettingsModal } from './PanoramaSettingsModal';
|
|
@@ -82,7 +91,9 @@ import {
|
|
|
82
91
|
} from './buildPanoramaInitialSettings';
|
|
83
92
|
import { isLowMemDevice } from './lowMemDevice';
|
|
84
93
|
import { useCapture } from './useCapture';
|
|
85
|
-
import { useDeviceOrientation } from './useDeviceOrientation';
|
|
94
|
+
import { useDeviceOrientation, type DeviceOrientation } from './useDeviceOrientation';
|
|
95
|
+
import { useOrientationDrift } from './useOrientationDrift';
|
|
96
|
+
import { OrientationDriftModal } from './OrientationDriftModal';
|
|
86
97
|
import {
|
|
87
98
|
getIncrementalNativeModule,
|
|
88
99
|
incrementalStitcherIsAvailable,
|
|
@@ -293,6 +304,212 @@ export interface CameraProps {
|
|
|
293
304
|
onFramesDropped?: (info: FramesDroppedInfo) => void;
|
|
294
305
|
onError?: (err: CameraError) => void;
|
|
295
306
|
|
|
307
|
+
/**
|
|
308
|
+
* v0.12.0 — fires when the SDK auto-abandons an in-progress
|
|
309
|
+
* capture without producing output. `reason` is a string union
|
|
310
|
+
* so future reasons (network loss, low memory, etc.) can be added
|
|
311
|
+
* without breaking the callback signature.
|
|
312
|
+
*
|
|
313
|
+
* Currently the only reason in v0.12 is `'orientation-drift'`:
|
|
314
|
+
* the user rotated the device between Mode A (landscape + vertical
|
|
315
|
+
* pan) and Mode B (portrait + horizontal pan) mid-capture. The
|
|
316
|
+
* engine docstring at `incremental.ts:373-403` is explicit that
|
|
317
|
+
* cross-mode capture is "best-effort, not supported," so the SDK
|
|
318
|
+
* decisively cancels the capture (`incremental.cancel()`) and
|
|
319
|
+
* surfaces `OrientationDriftModal` to explain what happened.
|
|
320
|
+
*
|
|
321
|
+
* Hosts use this callback to clean up their own state (e.g., reset
|
|
322
|
+
* a wizard step, log telemetry, surface their own retry UX in
|
|
323
|
+
* addition to the SDK's built-in modal). No `onCapture` will fire
|
|
324
|
+
* for an abandoned capture.
|
|
325
|
+
*/
|
|
326
|
+
onCaptureAbandoned?: (reason: 'orientation-drift') => void;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
|
|
330
|
+
*
|
|
331
|
+
* - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
|
|
332
|
+
* state internally. Tapping the built-in flash button toggles
|
|
333
|
+
* it on/off. `onFlashChange` (if supplied) fires for telemetry.
|
|
334
|
+
* - **Controlled** (supply `flash`): the parent owns the state.
|
|
335
|
+
* The built-in button still renders and fires `onFlashChange`
|
|
336
|
+
* on press, but it's a no-op unless the parent updates `flash`
|
|
337
|
+
* in response.
|
|
338
|
+
*
|
|
339
|
+
* Both shapes coexist with the v0.13 "flash button is on by default"
|
|
340
|
+
* built-in (see the bottom-left bar slot in the JSX). Hosts that
|
|
341
|
+
* want their own flash chrome can opt out via `showFlashButton={false}`
|
|
342
|
+
* and drive the underlying torch by controlling `flash` directly.
|
|
343
|
+
*
|
|
344
|
+
* ## AR-mode behaviour
|
|
345
|
+
*
|
|
346
|
+
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
|
|
347
|
+
* ARKit / ARCore own the `AVCaptureDevice` and don't expose the
|
|
348
|
+
* torch through vision-camera's pipeline. The built-in flash
|
|
349
|
+
* button renders as visibly disabled (a11y label "Flash unavailable
|
|
350
|
+
* in AR mode") and `flash` is forced to `'off'` regardless of
|
|
351
|
+
* controlled/uncontrolled state. Hosts that need flash should
|
|
352
|
+
* toggle to non-AR before enabling.
|
|
353
|
+
*/
|
|
354
|
+
flash?: 'on' | 'off';
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* v0.13.0 — fires when the user taps the built-in flash button.
|
|
358
|
+
* In uncontrolled mode, the internal state has already flipped
|
|
359
|
+
* (single render delay). In controlled mode, the parent must
|
|
360
|
+
* update the `flash` prop in response or the visual toggle is
|
|
361
|
+
* a no-op. Useful in either mode for telemetry.
|
|
362
|
+
*/
|
|
363
|
+
onFlashChange?: (next: 'on' | 'off') => void;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* v0.13.0 — show the built-in flash button in the bottom-left
|
|
367
|
+
* slot. Defaults to `true`. Hosts that render their own flash
|
|
368
|
+
* chrome (and drive the underlying torch via the controlled
|
|
369
|
+
* `flash` prop) can opt out by setting this to `false`.
|
|
370
|
+
*/
|
|
371
|
+
showFlashButton?: boolean;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* v0.13.0 — show the built-in IncrementalPanGuide ("keep the
|
|
375
|
+
* arrow on the line" drift marker) while recording. Defaults
|
|
376
|
+
* to `true`. The guide is gyroscope-driven and only active
|
|
377
|
+
* during the recording phase (no idle sensor cost). Hosts that
|
|
378
|
+
* want their own pan-guide chrome can opt out via `false`.
|
|
379
|
+
*/
|
|
380
|
+
panGuide?: boolean;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
|
|
384
|
+
* ("Pan slowly" / "Slow down" / "Too fast") while recording.
|
|
385
|
+
* Defaults to `true`. Gyroscope-driven, only active during
|
|
386
|
+
* recording. Hosts that want their own speed chrome can opt
|
|
387
|
+
* out via `false`.
|
|
388
|
+
*/
|
|
389
|
+
panoramaGuidance?: boolean;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
|
|
393
|
+
* renders a top-of-screen header showing this title (centred)
|
|
394
|
+
* with an optional back affordance + guidance subtitle + the
|
|
395
|
+
* existing settings gear absorbed into the header's right side.
|
|
396
|
+
*
|
|
397
|
+
* When `headerTitle` is undefined the header is not rendered
|
|
398
|
+
* (matches pre-v0.13 behaviour: top of preview is bare except
|
|
399
|
+
* for the standalone settings gear gated on `showSettingsButton`).
|
|
400
|
+
*
|
|
401
|
+
* Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
|
|
402
|
+
* and `headerColors` to customise the rest of the header. Hosts
|
|
403
|
+
* that need richer header chrome can omit `headerTitle` and
|
|
404
|
+
* compose their own `<CaptureHeader>` above `<Camera>`.
|
|
405
|
+
*/
|
|
406
|
+
headerTitle?: string;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* v0.13.0 — header back-button callback. When supplied (and
|
|
410
|
+
* `headerTitle` is set), the header renders a back affordance
|
|
411
|
+
* on the left. Omitted ⇒ no back button (the title stays
|
|
412
|
+
* centred).
|
|
413
|
+
*/
|
|
414
|
+
onHeaderBack?: () => void;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* v0.13.0 — header back-button label. Defaults to "‹ Back".
|
|
418
|
+
* No effect unless `headerTitle` and `onHeaderBack` are both set.
|
|
419
|
+
*/
|
|
420
|
+
headerBackLabel?: string;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* v0.13.0 — optional second-line subtitle shown below the
|
|
424
|
+
* header title. E.g. "Photograph the promotional cola end cap."
|
|
425
|
+
* Renders nothing when undefined. No effect unless `headerTitle`
|
|
426
|
+
* is set.
|
|
427
|
+
*/
|
|
428
|
+
headerGuidance?: string;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* v0.13.0 — colour overrides for the built-in header. Defaults
|
|
432
|
+
* are white-on-black to stay legible over the camera preview.
|
|
433
|
+
* No effect unless `headerTitle` is set.
|
|
434
|
+
*/
|
|
435
|
+
headerColors?: CaptureHeaderProps['colors'];
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
|
|
439
|
+
* built-in `CaptureThumbnailStrip` above the bottom controls
|
|
440
|
+
* showing the host's capture history. Each item is a plain
|
|
441
|
+
* `{ id, uri, width?, height? }` object; the strip handles
|
|
442
|
+
* aspect-ratio rendering, tap-to-preview, and the count line.
|
|
443
|
+
*
|
|
444
|
+
* Omit (`undefined`) to skip the strip entirely. Hosts using
|
|
445
|
+
* the strip independently (e.g. on a non-camera screen) can keep
|
|
446
|
+
* importing `CaptureThumbnailStrip` directly from the library —
|
|
447
|
+
* the prop here is the convenience wiring for in-`<Camera>` use.
|
|
448
|
+
*
|
|
449
|
+
* Captures emitted by `<Camera>`'s `onCapture` are NOT added to
|
|
450
|
+
* this array automatically — the host owns the canonical list
|
|
451
|
+
* (typically persisted to its own DB) and updates the prop in
|
|
452
|
+
* response. This matches the SDK's "Camera owns runtime state,
|
|
453
|
+
* host persists" pattern.
|
|
454
|
+
*/
|
|
455
|
+
thumbnails?: CaptureThumbnailItem[];
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* v0.13.0 — minimum-photos hint for the count line. Renders
|
|
459
|
+
* "n / minPhotos min" with the success colour when reached,
|
|
460
|
+
* warning colour otherwise.
|
|
461
|
+
*/
|
|
462
|
+
thumbnailsMin?: number;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* v0.13.0 — maximum-photos hint for the count line. Renders
|
|
466
|
+
* "· maxPhotos max" suffix. No enforcement — the host decides
|
|
467
|
+
* what to do at the cap.
|
|
468
|
+
*/
|
|
469
|
+
thumbnailsMax?: number;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* v0.13.0 — tap handler for thumbnails. When set, replaces the
|
|
473
|
+
* strip's built-in tap-to-preview modal; the host shows its own
|
|
474
|
+
* preview UI (e.g. with delete / recapture buttons gated on
|
|
475
|
+
* sync state). Omit to use the built-in preview.
|
|
476
|
+
*/
|
|
477
|
+
onThumbnailPress?: (item: CaptureThumbnailItem) => void;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
|
|
481
|
+
* modal as `visible`. Use this for post-stitch confirmation:
|
|
482
|
+
* after `onCapture` emits, the host stores the result and sets
|
|
483
|
+
* `capturePreview` to the new image, with `capturePreviewActions`
|
|
484
|
+
* = `[Discard, Save]` (or similar). Setting `undefined` hides
|
|
485
|
+
* the modal.
|
|
486
|
+
*
|
|
487
|
+
* Hosts using the modal for thumbnail tap-to-preview can leave
|
|
488
|
+
* this undefined and let the built-in strip's preview handle
|
|
489
|
+
* that case.
|
|
490
|
+
*/
|
|
491
|
+
capturePreview?: {
|
|
492
|
+
imageUri: string;
|
|
493
|
+
imageWidth?: number;
|
|
494
|
+
imageHeight?: number;
|
|
495
|
+
title?: string;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* v0.13.0 — action buttons rendered along the bottom of the
|
|
500
|
+
* `CapturePreview` modal. Empty array (or undefined) renders
|
|
501
|
+
* no buttons, only the close affordance.
|
|
502
|
+
*/
|
|
503
|
+
capturePreviewActions?: CapturePreviewAction[];
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* v0.13.0 — fires when the user dismisses the `capturePreview`
|
|
507
|
+
* modal (tap close, backdrop tap, hardware back on Android).
|
|
508
|
+
* The host is expected to clear the `capturePreview` prop in
|
|
509
|
+
* response.
|
|
510
|
+
*/
|
|
511
|
+
onCapturePreviewClose?: () => void;
|
|
512
|
+
|
|
296
513
|
/**
|
|
297
514
|
* Optional host-supplied vision-camera frame processor.
|
|
298
515
|
*
|
|
@@ -656,17 +873,49 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
656
873
|
onLensChange,
|
|
657
874
|
onFramesDropped,
|
|
658
875
|
onError,
|
|
876
|
+
onCaptureAbandoned,
|
|
877
|
+
flash: controlledFlash,
|
|
878
|
+
onFlashChange,
|
|
879
|
+
showFlashButton = true,
|
|
880
|
+
panGuide = true,
|
|
881
|
+
panoramaGuidance = true,
|
|
882
|
+
headerTitle,
|
|
883
|
+
onHeaderBack,
|
|
884
|
+
headerBackLabel,
|
|
885
|
+
headerGuidance,
|
|
886
|
+
headerColors,
|
|
887
|
+
thumbnails,
|
|
888
|
+
thumbnailsMin,
|
|
889
|
+
thumbnailsMax,
|
|
890
|
+
onThumbnailPress,
|
|
891
|
+
capturePreview,
|
|
892
|
+
capturePreviewActions,
|
|
893
|
+
onCapturePreviewClose,
|
|
659
894
|
frameProcessor: hostFrameProcessor,
|
|
660
895
|
engine = 'batch-keyframe',
|
|
661
896
|
} = props;
|
|
662
897
|
|
|
663
898
|
const insets = useSafeAreaInsets();
|
|
899
|
+
// v0.12.0 — JS-layout orientation independent of device-physical.
|
|
900
|
+
// `useWindowDimensions().width > height` tells us if the OS
|
|
901
|
+
// rotated the framebuffer (only happens for non-locked hosts in
|
|
902
|
+
// device-landscape). Combined with `useDeviceOrientation()` to
|
|
903
|
+
// pick the JS edge corresponding to the home-indicator side of
|
|
904
|
+
// the device — see `homeIndicatorEdge` below.
|
|
905
|
+
const jsWindow = useWindowDimensions();
|
|
906
|
+
const jsLandscape = jsWindow.width > jsWindow.height;
|
|
664
907
|
|
|
665
908
|
// ── State ───────────────────────────────────────────────────────
|
|
666
909
|
const [arPreference, setArPreference] = useState(
|
|
667
910
|
defaultCaptureSource === 'ar',
|
|
668
911
|
);
|
|
669
912
|
const [lens, setLens] = useState<CameraLens>(defaultLens);
|
|
913
|
+
// v0.13.0 — flash state. Controlled by `controlledFlash` when the
|
|
914
|
+
// host supplies the `flash` prop; otherwise owned internally and
|
|
915
|
+
// toggled by the built-in flash button. `effectiveFlash` below
|
|
916
|
+
// also forces 'off' in AR mode (ARKit / ARCore own the device's
|
|
917
|
+
// torch and don't surface it through vision-camera's pipeline).
|
|
918
|
+
const [internalFlash, setInternalFlash] = useState<'on' | 'off'>('off');
|
|
670
919
|
const [settings, setSettings] = useState<PanoramaSettings>(() =>
|
|
671
920
|
buildPanoramaInitialSettings(
|
|
672
921
|
extractPanoramaOverrides(props),
|
|
@@ -857,6 +1106,64 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
857
1106
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
858
1107
|
useEffect(() => () => { fpDriver.stop(); }, []);
|
|
859
1108
|
|
|
1109
|
+
// ── v0.12.0 — Orientation drift detection + auto-abandon ────────
|
|
1110
|
+
//
|
|
1111
|
+
// The incremental engine supports both portrait (Mode B, horizontal
|
|
1112
|
+
// pan) and landscape (Mode A, vertical pan) capture as first-class,
|
|
1113
|
+
// but the docstring at `incremental.ts:373-403` is explicit that
|
|
1114
|
+
// mixing them mid-capture is "best-effort, not supported" — the
|
|
1115
|
+
// output rotation becomes ambiguous and the stitched panorama is
|
|
1116
|
+
// malformed. v0.12 protects against this by snapshotting the
|
|
1117
|
+
// orientation at `start()` and auto-cancelling the capture the
|
|
1118
|
+
// instant the user rotates to a different orientation mid-flight.
|
|
1119
|
+
//
|
|
1120
|
+
// The modal is informational only — by the time it renders, the
|
|
1121
|
+
// capture is already stopped. No Continue/Resume affordance per
|
|
1122
|
+
// the engine spec.
|
|
1123
|
+
const drift = useOrientationDrift(statusPhase === 'recording');
|
|
1124
|
+
const [driftModalDismissed, setDriftModalDismissed] = useState(false);
|
|
1125
|
+
// Reset the dismissed flag when a new capture starts (or any non-
|
|
1126
|
+
// recording state) so the next drift event surfaces a fresh modal.
|
|
1127
|
+
useEffect(() => {
|
|
1128
|
+
if (statusPhase !== 'recording') setDriftModalDismissed(false);
|
|
1129
|
+
}, [statusPhase]);
|
|
1130
|
+
|
|
1131
|
+
useEffect(() => {
|
|
1132
|
+
if (!drift.drifted || statusPhase !== 'recording') return;
|
|
1133
|
+
// Auto-abandon the in-flight capture. Order matches handleHoldEnd's
|
|
1134
|
+
// "stitch" path but skips finalize:
|
|
1135
|
+
// 1. Stop pumping frames so no new keyframes arrive mid-cancel.
|
|
1136
|
+
// 2. Tell the native engine to drop accumulated state
|
|
1137
|
+
// (`incremental.cancel()`).
|
|
1138
|
+
// 3. Reset statusPhase back to idle.
|
|
1139
|
+
// 4. Notify the host via `onCaptureAbandoned`.
|
|
1140
|
+
//
|
|
1141
|
+
// Wrapped in an IIFE because useEffect callbacks can't be async
|
|
1142
|
+
// directly. Errors from `incremental.cancel()` are caught + sent
|
|
1143
|
+
// through `onError` — abandonment must succeed even if the engine
|
|
1144
|
+
// is in a weird state.
|
|
1145
|
+
void (async () => {
|
|
1146
|
+
fpDriver.stop();
|
|
1147
|
+
try {
|
|
1148
|
+
await incremental.cancel();
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1151
|
+
onError?.(new CameraError(
|
|
1152
|
+
'PANORAMA_FINALIZE_FAILED',
|
|
1153
|
+
`cancel after orientation drift failed: ${message}`,
|
|
1154
|
+
err,
|
|
1155
|
+
));
|
|
1156
|
+
} finally {
|
|
1157
|
+
setStatusPhase('idle');
|
|
1158
|
+
setRecordingStartedAt(null);
|
|
1159
|
+
onCaptureAbandoned?.('orientation-drift');
|
|
1160
|
+
}
|
|
1161
|
+
})();
|
|
1162
|
+
// Deps: re-run whenever drift latches OR recording state changes.
|
|
1163
|
+
// Other deps are stable refs / setters.
|
|
1164
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1165
|
+
}, [drift.drifted, statusPhase]);
|
|
1166
|
+
|
|
860
1167
|
// v0.8.0 Phase 5 / v0.11.0 — frameProcessor prop semantics:
|
|
861
1168
|
//
|
|
862
1169
|
// - Host supplied? → use host's processor. The host's worklet
|
|
@@ -991,7 +1298,14 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
991
1298
|
// ARCameraView writes to its own tmp location; relocate to
|
|
992
1299
|
// photoOutputPath via the native FileBridge so both branches
|
|
993
1300
|
// return paths under the same dir.
|
|
994
|
-
|
|
1301
|
+
// v0.12.0 — pass deviceOrientation so the AR takePhoto's
|
|
1302
|
+
// native CIImage rotation matches the user's view. Pre-
|
|
1303
|
+
// v0.12 the native side hardcoded portrait, so landscape
|
|
1304
|
+
// photos came out sideways.
|
|
1305
|
+
const photo = await arViewRef.current.takePhoto({
|
|
1306
|
+
quality: 90,
|
|
1307
|
+
orientation: deviceOrientation,
|
|
1308
|
+
});
|
|
995
1309
|
try {
|
|
996
1310
|
await moveFile(photo.path, photoOutputPath);
|
|
997
1311
|
} catch (moveErr) {
|
|
@@ -1251,6 +1565,22 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1251
1565
|
setArPreference((prev) => !prev);
|
|
1252
1566
|
}, []);
|
|
1253
1567
|
|
|
1568
|
+
// ── v0.13.0 — Flash control ─────────────────────────────────────
|
|
1569
|
+
//
|
|
1570
|
+
// `flashRequested` is what the host / built-in button asks for.
|
|
1571
|
+
// `effectiveFlash` is what we actually drive into vision-camera —
|
|
1572
|
+
// AR mode forces 'off' because ARKit / ARCore own AVCaptureDevice
|
|
1573
|
+
// and the torch isn't exposed. This way the button's visual state
|
|
1574
|
+
// (a11y, styling) tracks `flashRequested` while the underlying
|
|
1575
|
+
// camera always sees the correct value.
|
|
1576
|
+
const flashRequested: 'on' | 'off' = controlledFlash ?? internalFlash;
|
|
1577
|
+
const effectiveFlash: 'on' | 'off' = isAR ? 'off' : flashRequested;
|
|
1578
|
+
const toggleFlash = useCallback(() => {
|
|
1579
|
+
const next: 'on' | 'off' = flashRequested === 'on' ? 'off' : 'on';
|
|
1580
|
+
if (controlledFlash == null) setInternalFlash(next);
|
|
1581
|
+
onFlashChange?.(next);
|
|
1582
|
+
}, [flashRequested, controlledFlash, onFlashChange]);
|
|
1583
|
+
|
|
1254
1584
|
// ── JSX ─────────────────────────────────────────────────────────
|
|
1255
1585
|
|
|
1256
1586
|
return (
|
|
@@ -1282,7 +1612,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1282
1612
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
1283
1613
|
// which has run on `video` (true) for months without issue.
|
|
1284
1614
|
video
|
|
1285
|
-
flash=
|
|
1615
|
+
flash={effectiveFlash}
|
|
1286
1616
|
style={StyleSheet.absoluteFill}
|
|
1287
1617
|
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
1288
1618
|
// the camera producer thread for every frame. Only wired
|
|
@@ -1318,6 +1648,20 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1318
1648
|
recordingStartedAt={recordingStartedAt ?? undefined}
|
|
1319
1649
|
/>
|
|
1320
1650
|
|
|
1651
|
+
{/* v0.13.0 — built-in pan guidance overlays. Both sit on top
|
|
1652
|
+
of the camera preview but under the controls. Each is
|
|
1653
|
+
gyroscope-driven and only subscribes while `active` is
|
|
1654
|
+
true — flipping `active` false on capture-end tears the
|
|
1655
|
+
subscription down so the sensor isn't running idle. Hosts
|
|
1656
|
+
can opt out per overlay via the `panGuide` / `panoramaGuidance`
|
|
1657
|
+
boolean props (both default true). */}
|
|
1658
|
+
{panGuide && (
|
|
1659
|
+
<IncrementalPanGuide active={statusPhase === 'recording'} />
|
|
1660
|
+
)}
|
|
1661
|
+
{panoramaGuidance && (
|
|
1662
|
+
<PanoramaGuidance active={statusPhase === 'recording'} />
|
|
1663
|
+
)}
|
|
1664
|
+
|
|
1321
1665
|
{/*
|
|
1322
1666
|
2026-05-22 (audit F9 + F3) — debug UI suite, all gated on
|
|
1323
1667
|
settings.debug. Mounts in <Camera> automatically; Layer-2
|
|
@@ -1361,39 +1705,111 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1361
1705
|
topInset={insets.top}
|
|
1362
1706
|
/>
|
|
1363
1707
|
|
|
1364
|
-
{/*
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1708
|
+
{/* v0.13.0 — built-in CaptureHeader, gated on `headerTitle`.
|
|
1709
|
+
When the header is mounted, it absorbs the settings gear
|
|
1710
|
+
on its right side (avoids stacking with the standalone
|
|
1711
|
+
gear). Hosts that DON'T set `headerTitle` get the legacy
|
|
1712
|
+
standalone gear, still gated on `showSettingsButton`. */}
|
|
1713
|
+
{headerTitle != null ? (
|
|
1714
|
+
<View style={styles.headerWrap} pointerEvents="box-none">
|
|
1715
|
+
<CaptureHeader
|
|
1716
|
+
title={headerTitle}
|
|
1717
|
+
onBack={onHeaderBack}
|
|
1718
|
+
backLabel={headerBackLabel}
|
|
1719
|
+
guidance={headerGuidance}
|
|
1720
|
+
colors={headerColors}
|
|
1721
|
+
topInset={insets.top}
|
|
1722
|
+
onSettingsPress={
|
|
1723
|
+
showSettingsButton
|
|
1724
|
+
? () => setSettingsModalVisible(true)
|
|
1725
|
+
: undefined
|
|
1726
|
+
}
|
|
1727
|
+
/>
|
|
1728
|
+
</View>
|
|
1729
|
+
) : (
|
|
1730
|
+
showSettingsButton && (
|
|
1731
|
+
<SettingsButton
|
|
1732
|
+
topInset={insets.top}
|
|
1733
|
+
onPress={() => setSettingsModalVisible(true)}
|
|
1734
|
+
/>
|
|
1735
|
+
)
|
|
1736
|
+
)}
|
|
1737
|
+
|
|
1738
|
+
{/* v0.13.0 — built-in capture-history thumbnail strip. Renders
|
|
1739
|
+
when the host supplies a `thumbnails` array (even empty),
|
|
1740
|
+
hidden during recording so it doesn't overlap the band
|
|
1741
|
+
overlay. Sits above the bottom controls in JS-bottom
|
|
1742
|
+
coordinates; landscape/non-locked layouts get the strip in
|
|
1743
|
+
the same place (no orientation-aware repositioning for now —
|
|
1744
|
+
the strip is intrinsically horizontal). */}
|
|
1745
|
+
{thumbnails != null && statusPhase !== 'recording' && (
|
|
1746
|
+
<View style={styles.thumbnailStripWrap} pointerEvents="box-none">
|
|
1747
|
+
<CaptureThumbnailStrip
|
|
1748
|
+
items={thumbnails}
|
|
1749
|
+
minPhotos={thumbnailsMin}
|
|
1750
|
+
maxPhotos={thumbnailsMax}
|
|
1751
|
+
onItemPress={onThumbnailPress}
|
|
1752
|
+
/>
|
|
1753
|
+
</View>
|
|
1370
1754
|
)}
|
|
1371
1755
|
|
|
1372
1756
|
{/*
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1757
|
+
v0.12.0 — Orientation-aware bottom controls anchored to the
|
|
1758
|
+
physical home-indicator edge. The shutter follows the home-
|
|
1759
|
+
indicator regardless of host portrait-lock state:
|
|
1760
|
+
- locked + any device → JS-bottom (locked
|
|
1761
|
+
framebuffer maps device-bottom to JS-bottom always)
|
|
1762
|
+
- non-locked + device-portrait → JS-bottom
|
|
1763
|
+
- non-locked + device-landscape-L → JS-right
|
|
1764
|
+
- non-locked + device-landscape-R → JS-left
|
|
1765
|
+
Computed in `homeIndicatorEdge` which combines `jsLandscape`
|
|
1766
|
+
(from window dims) with `deviceOrientation` (sensor).
|
|
1380
1767
|
*/}
|
|
1381
1768
|
<View
|
|
1382
1769
|
pointerEvents="box-none"
|
|
1383
|
-
style={
|
|
1770
|
+
style={bottomAreaStyleForEdge(
|
|
1771
|
+
homeIndicatorEdge(jsLandscape, deviceOrientation),
|
|
1772
|
+
insets.bottom + 12,
|
|
1773
|
+
insets.top + 12,
|
|
1774
|
+
)}
|
|
1384
1775
|
>
|
|
1385
|
-
{/* Live-frame band — only visible while recording.
|
|
1776
|
+
{/* Live-frame band — only visible while recording. `vertical`
|
|
1777
|
+
is true when the home-indicator anchor is on a side edge
|
|
1778
|
+
(left or right), in which case the band is a vertical
|
|
1779
|
+
column. Otherwise it's a horizontal strip. */}
|
|
1386
1780
|
{statusPhase === 'recording' && (
|
|
1387
1781
|
<PanoramaBandOverlay
|
|
1388
1782
|
state={incrementalState}
|
|
1389
1783
|
frameUris={batchKeyframeThumbnails}
|
|
1390
1784
|
captureOrientation={deviceOrientation}
|
|
1785
|
+
vertical={isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation))}
|
|
1391
1786
|
/>
|
|
1392
1787
|
)}
|
|
1393
1788
|
|
|
1394
|
-
{/* Shutter row
|
|
1395
|
-
|
|
1396
|
-
|
|
1789
|
+
{/* Shutter row. Horizontal row when home-indicator is on
|
|
1790
|
+
top/bottom (lens left / shutter center / AR right);
|
|
1791
|
+
vertical column when on left/right (slots stack along
|
|
1792
|
+
the narrow strip). Touch targets stay axis-aligned. */}
|
|
1793
|
+
<View style={bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation))}>
|
|
1794
|
+
<View style={styles.bottomBarLeft}>
|
|
1795
|
+
{showFlashButton && (
|
|
1796
|
+
<Pressable
|
|
1797
|
+
onPress={isAR ? undefined : toggleFlash}
|
|
1798
|
+
accessibilityRole="button"
|
|
1799
|
+
accessibilityLabel={isAR ? 'Flash unavailable in AR mode' : `Flash ${flashRequested === 'on' ? 'on' : 'off'}`}
|
|
1800
|
+
accessibilityState={{ selected: flashRequested === 'on', disabled: isAR }}
|
|
1801
|
+
disabled={isAR}
|
|
1802
|
+
hitSlop={8}
|
|
1803
|
+
style={[
|
|
1804
|
+
styles.flashButton,
|
|
1805
|
+
flashRequested === 'on' && !isAR && styles.flashButtonActive,
|
|
1806
|
+
isAR && styles.flashButtonDisabled,
|
|
1807
|
+
]}
|
|
1808
|
+
>
|
|
1809
|
+
<Text style={styles.flashIcon}>⚡</Text>
|
|
1810
|
+
</Pressable>
|
|
1811
|
+
)}
|
|
1812
|
+
</View>
|
|
1397
1813
|
<View style={styles.bottomBarCenter}>
|
|
1398
1814
|
<LensChip
|
|
1399
1815
|
lens={lens}
|
|
@@ -1425,6 +1841,34 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1425
1841
|
onChange={setSettings}
|
|
1426
1842
|
onClose={() => setSettingsModalVisible(false)}
|
|
1427
1843
|
/>
|
|
1844
|
+
|
|
1845
|
+
{/* v0.12.0 — Orientation drift modal. Shows AFTER the SDK has
|
|
1846
|
+
auto-abandoned the capture (the useEffect above stops the
|
|
1847
|
+
engine + transitions to idle + fires onCaptureAbandoned).
|
|
1848
|
+
Modal exists purely to explain WHY the capture was
|
|
1849
|
+
cancelled. Single OK button (no Continue) per the engine
|
|
1850
|
+
spec on cross-mode capture being best-effort, not supported. */}
|
|
1851
|
+
<OrientationDriftModal
|
|
1852
|
+
visible={drift.drifted && !driftModalDismissed}
|
|
1853
|
+
captureOrientation={drift.captureOrientation}
|
|
1854
|
+
currentOrientation={drift.currentOrientation}
|
|
1855
|
+
onAcknowledge={() => setDriftModalDismissed(true)}
|
|
1856
|
+
/>
|
|
1857
|
+
|
|
1858
|
+
{/* v0.13.0 — built-in post-stitch / tap-to-preview modal.
|
|
1859
|
+
Visible when the host supplies `capturePreview`. When
|
|
1860
|
+
undefined the modal stays hidden (visible=false) so it
|
|
1861
|
+
doesn't intercept touches. Host is expected to clear
|
|
1862
|
+
`capturePreview` via `onCapturePreviewClose` on dismiss. */}
|
|
1863
|
+
<CapturePreview
|
|
1864
|
+
visible={capturePreview != null}
|
|
1865
|
+
imageUri={capturePreview?.imageUri ?? ''}
|
|
1866
|
+
imageWidth={capturePreview?.imageWidth}
|
|
1867
|
+
imageHeight={capturePreview?.imageHeight}
|
|
1868
|
+
title={capturePreview?.title}
|
|
1869
|
+
actions={capturePreviewActions}
|
|
1870
|
+
onClose={onCapturePreviewClose ?? noop}
|
|
1871
|
+
/>
|
|
1428
1872
|
</View>
|
|
1429
1873
|
);
|
|
1430
1874
|
}
|
|
@@ -1435,6 +1879,148 @@ function noop(): void {
|
|
|
1435
1879
|
}
|
|
1436
1880
|
|
|
1437
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* v0.12.0 — JS edge corresponding to the physical home-indicator
|
|
1884
|
+
* side of the device. This is where the shutter + controls anchor
|
|
1885
|
+
* to so they're always within thumb reach of the user's grip
|
|
1886
|
+
* (matching iOS Camera's behaviour).
|
|
1887
|
+
*
|
|
1888
|
+
* Combines two signals:
|
|
1889
|
+
* - `jsLandscape`: whether the OS rotated the framebuffer. True
|
|
1890
|
+
* only for non-locked hosts in device-landscape.
|
|
1891
|
+
* - `deviceOrient`: physical device orientation from the sensor.
|
|
1892
|
+
*
|
|
1893
|
+
* Truth table:
|
|
1894
|
+
* | jsLandscape | deviceOrient | edge |
|
|
1895
|
+
* |--- |--- |--- |
|
|
1896
|
+
* | false | any | bottom | (portrait JS coords —
|
|
1897
|
+
* | | | | device-bottom = JS-bottom
|
|
1898
|
+
* | | | | in both locked and
|
|
1899
|
+
* | | | | non-locked-portrait)
|
|
1900
|
+
* | true | landscape-left | right | (screen rotated, home
|
|
1901
|
+
* | | | | indicator on user-right)
|
|
1902
|
+
* | true | landscape-right | left | (mirror)
|
|
1903
|
+
*
|
|
1904
|
+
* Caveats:
|
|
1905
|
+
* - Non-locked + upside-down doesn't surface JS-top here because
|
|
1906
|
+
* upside-down doesn't change window dimensions; we can't
|
|
1907
|
+
* distinguish locked-portrait-with-device-flipped from
|
|
1908
|
+
* non-locked-portrait-with-screen-flipped-180°. Defaults to
|
|
1909
|
+
* JS-bottom which matches the more common locked case. Add
|
|
1910
|
+
* handling here when a host needs upside-down support.
|
|
1911
|
+
* - jsLandscape=true with non-landscape device shouldn't happen
|
|
1912
|
+
* in steady state — only during a transition mid-rotation.
|
|
1913
|
+
* Falls through to 'right' as a defensive default.
|
|
1914
|
+
*/
|
|
1915
|
+
type HomeIndicatorEdge = 'bottom' | 'top' | 'left' | 'right';
|
|
1916
|
+
|
|
1917
|
+
function homeIndicatorEdge(
|
|
1918
|
+
jsLandscape: boolean,
|
|
1919
|
+
deviceOrient: DeviceOrientation,
|
|
1920
|
+
): HomeIndicatorEdge {
|
|
1921
|
+
if (!jsLandscape) return 'bottom';
|
|
1922
|
+
if (deviceOrient === 'landscape-left') return 'right';
|
|
1923
|
+
if (deviceOrient === 'landscape-right') return 'left';
|
|
1924
|
+
return 'right';
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* v0.12.0 — true when the anchor edge is on a side (left/right), so
|
|
1930
|
+
* the band + shutter row need to be vertical strips. Top/bottom
|
|
1931
|
+
* anchors yield horizontal strips.
|
|
1932
|
+
*/
|
|
1933
|
+
function isSideEdge(edge: HomeIndicatorEdge): boolean {
|
|
1934
|
+
return edge === 'left' || edge === 'right';
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* v0.12.0 — bottom-controls outer container positioning. Anchors
|
|
1940
|
+
* to the home-indicator JS edge with the appropriate flex direction
|
|
1941
|
+
* so the band sits on the viewport side of the shutter (toward the
|
|
1942
|
+
* camera preview centre).
|
|
1943
|
+
*/
|
|
1944
|
+
function bottomAreaStyleForEdge(
|
|
1945
|
+
edge: HomeIndicatorEdge,
|
|
1946
|
+
bottomInsetPx: number,
|
|
1947
|
+
topInsetPx: number,
|
|
1948
|
+
): ViewStyle {
|
|
1949
|
+
switch (edge) {
|
|
1950
|
+
case 'bottom':
|
|
1951
|
+
// Band above shutter row, both at JS-bottom. JSX order
|
|
1952
|
+
// [band, shutter] + flexDirection 'column' = band at top of
|
|
1953
|
+
// stack (closer to screen centre), shutter at JS-bottom.
|
|
1954
|
+
return {
|
|
1955
|
+
position: 'absolute',
|
|
1956
|
+
left: 0,
|
|
1957
|
+
right: 0,
|
|
1958
|
+
bottom: 0,
|
|
1959
|
+
flexDirection: 'column',
|
|
1960
|
+
alignItems: 'stretch',
|
|
1961
|
+
paddingBottom: bottomInsetPx,
|
|
1962
|
+
};
|
|
1963
|
+
case 'top':
|
|
1964
|
+
// Mirror of bottom. column-reverse so JSX [band, shutter]
|
|
1965
|
+
// renders [shutter, band] in JS, shutter at JS-top, band
|
|
1966
|
+
// below it (toward screen centre).
|
|
1967
|
+
return {
|
|
1968
|
+
position: 'absolute',
|
|
1969
|
+
left: 0,
|
|
1970
|
+
right: 0,
|
|
1971
|
+
top: 0,
|
|
1972
|
+
flexDirection: 'column-reverse',
|
|
1973
|
+
alignItems: 'stretch',
|
|
1974
|
+
paddingTop: topInsetPx,
|
|
1975
|
+
};
|
|
1976
|
+
case 'right':
|
|
1977
|
+
// Band to the left of shutter column, both at JS-right.
|
|
1978
|
+
// flexDirection 'row' + JSX [band, shutter] = band at JS-left
|
|
1979
|
+
// of container (screen centre side), shutter at JS-right.
|
|
1980
|
+
return {
|
|
1981
|
+
position: 'absolute',
|
|
1982
|
+
top: 0,
|
|
1983
|
+
bottom: 0,
|
|
1984
|
+
right: 0,
|
|
1985
|
+
flexDirection: 'row',
|
|
1986
|
+
alignItems: 'stretch',
|
|
1987
|
+
paddingRight: 12,
|
|
1988
|
+
};
|
|
1989
|
+
case 'left':
|
|
1990
|
+
// Mirror of right. row-reverse so JSX [band, shutter] gives
|
|
1991
|
+
// band at JS-right (screen centre side), shutter at JS-left.
|
|
1992
|
+
return {
|
|
1993
|
+
position: 'absolute',
|
|
1994
|
+
top: 0,
|
|
1995
|
+
bottom: 0,
|
|
1996
|
+
left: 0,
|
|
1997
|
+
flexDirection: 'row-reverse',
|
|
1998
|
+
alignItems: 'stretch',
|
|
1999
|
+
paddingLeft: 12,
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* v0.12.0 — inner shutter-row flex direction. Horizontal row for
|
|
2007
|
+
* top/bottom anchors; vertical column for left/right anchors so
|
|
2008
|
+
* the three slots (lens / shutter / AR) stack along the narrow
|
|
2009
|
+
* side strip. Buttons don't rotate — touch targets and text
|
|
2010
|
+
* orient correctly via either (a) un-rotated framebuffer under
|
|
2011
|
+
* portrait-lock or (b) OS-rotated framebuffer under non-locked.
|
|
2012
|
+
*/
|
|
2013
|
+
function bottomBarStyleForEdge(edge: HomeIndicatorEdge): ViewStyle {
|
|
2014
|
+
const vertical = isSideEdge(edge);
|
|
2015
|
+
return {
|
|
2016
|
+
flexDirection: vertical ? 'column' : 'row',
|
|
2017
|
+
paddingHorizontal: vertical ? 0 : 18,
|
|
2018
|
+
paddingVertical: vertical ? 18 : 0,
|
|
2019
|
+
alignItems: 'center',
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
|
|
1438
2024
|
const styles = StyleSheet.create({
|
|
1439
2025
|
container: {
|
|
1440
2026
|
flex: 1,
|
|
@@ -1464,6 +2050,8 @@ const styles = StyleSheet.create({
|
|
|
1464
2050
|
},
|
|
1465
2051
|
bottomBarLeft: {
|
|
1466
2052
|
flex: 1,
|
|
2053
|
+
alignItems: 'flex-start',
|
|
2054
|
+
justifyContent: 'flex-end',
|
|
1467
2055
|
},
|
|
1468
2056
|
bottomBarCenter: {
|
|
1469
2057
|
flex: 1,
|
|
@@ -1477,4 +2065,34 @@ const styles = StyleSheet.create({
|
|
|
1477
2065
|
shutterWrap: {
|
|
1478
2066
|
marginTop: 12,
|
|
1479
2067
|
},
|
|
2068
|
+
headerWrap: {
|
|
2069
|
+
position: 'absolute',
|
|
2070
|
+
top: 0,
|
|
2071
|
+
left: 0,
|
|
2072
|
+
right: 0,
|
|
2073
|
+
},
|
|
2074
|
+
thumbnailStripWrap: {
|
|
2075
|
+
position: 'absolute',
|
|
2076
|
+
left: 0,
|
|
2077
|
+
right: 0,
|
|
2078
|
+
bottom: 160,
|
|
2079
|
+
},
|
|
2080
|
+
flashButton: {
|
|
2081
|
+
width: 44,
|
|
2082
|
+
height: 44,
|
|
2083
|
+
borderRadius: 22,
|
|
2084
|
+
alignItems: 'center',
|
|
2085
|
+
justifyContent: 'center',
|
|
2086
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
2087
|
+
},
|
|
2088
|
+
flashButtonActive: {
|
|
2089
|
+
backgroundColor: '#ffd34d',
|
|
2090
|
+
},
|
|
2091
|
+
flashButtonDisabled: {
|
|
2092
|
+
opacity: 0.35,
|
|
2093
|
+
},
|
|
2094
|
+
flashIcon: {
|
|
2095
|
+
fontSize: 20,
|
|
2096
|
+
color: '#ffffff',
|
|
2097
|
+
},
|
|
1480
2098
|
});
|