react-native-image-stitcher 0.13.0 → 0.14.1
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 +115 -0
- package/README.md +238 -62
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +33 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +73 -1
- package/dist/camera/Camera.d.ts +71 -16
- package/dist/camera/Camera.js +167 -51
- package/dist/camera/CameraView.d.ts +6 -0
- package/dist/camera/CameraView.js +2 -2
- package/dist/camera/CaptureHeader.js +39 -16
- package/dist/camera/CapturePreview.js +13 -1
- package/dist/camera/CaptureThumbnailStrip.d.ts +25 -1
- package/dist/camera/CaptureThumbnailStrip.js +17 -4
- package/dist/camera/PanoramaBandOverlay.d.ts +76 -0
- package/dist/camera/PanoramaBandOverlay.js +90 -33
- package/dist/camera/PanoramaConfirmModal.js +11 -1
- package/dist/camera/selectCaptureDevice.d.ts +93 -0
- package/dist/camera/selectCaptureDevice.js +131 -0
- package/dist/camera/useCapture.d.ts +40 -0
- package/dist/camera/useCapture.js +50 -12
- package/dist/camera/useContentRotation.d.ts +99 -0
- package/dist/camera/useContentRotation.js +124 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +6 -5
- package/package.json +1 -1
- package/src/camera/Camera.tsx +281 -118
- package/src/camera/CameraView.tsx +9 -0
- package/src/camera/CaptureHeader.tsx +39 -16
- package/src/camera/CapturePreview.tsx +12 -0
- package/src/camera/CaptureThumbnailStrip.tsx +44 -4
- package/src/camera/PanoramaBandOverlay.tsx +97 -35
- package/src/camera/PanoramaConfirmModal.tsx +10 -0
- package/src/camera/__tests__/bandThumbRotation.test.ts +120 -0
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +116 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +177 -0
- package/src/camera/__tests__/useContentRotation.test.ts +89 -0
- package/src/camera/selectCaptureDevice.ts +187 -0
- package/src/camera/useCapture.ts +99 -11
- package/src/camera/useContentRotation.ts +149 -0
- package/src/index.ts +6 -2
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -44,7 +44,19 @@ import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-nativ
|
|
|
44
44
|
import { type CaptureHeaderProps } from './CaptureHeader';
|
|
45
45
|
import { type CapturePreviewAction } from './CapturePreview';
|
|
46
46
|
import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
|
|
47
|
+
import { type DeviceOrientation } from './useDeviceOrientation';
|
|
47
48
|
export type CaptureSource = 'ar' | 'non-ar';
|
|
49
|
+
/**
|
|
50
|
+
* v0.13.2 — which capture sources the host ALLOWS. A constraint on top
|
|
51
|
+
* of `defaultCaptureSource` (which picks the initial source within this
|
|
52
|
+
* constraint):
|
|
53
|
+
* 'both' — AR and non-AR both available; AR toggle is shown.
|
|
54
|
+
* 'ar' — AR only; AR toggle hidden (nothing to switch to), and the
|
|
55
|
+
* 0.5× lens chooser is hidden (ARKit/ARCore don't expose the
|
|
56
|
+
* ultra-wide).
|
|
57
|
+
* 'non-ar' — non-AR only; AR toggle hidden.
|
|
58
|
+
*/
|
|
59
|
+
export type CaptureSourcesMode = 'ar' | 'non-ar' | 'both';
|
|
48
60
|
export type CameraLens = '1x' | '0.5x';
|
|
49
61
|
export type StitchMode = 'auto' | 'panorama' | 'scans';
|
|
50
62
|
export type Blender = 'multiband' | 'feather';
|
|
@@ -142,6 +154,19 @@ export interface CameraProps {
|
|
|
142
154
|
enablePhotoMode?: boolean;
|
|
143
155
|
enablePanoramaMode?: boolean;
|
|
144
156
|
showSettingsButton?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* v0.13.2 — which capture sources the host allows (default `'both'`).
|
|
159
|
+
* Constrains both the runtime AR toggle and `defaultCaptureSource`:
|
|
160
|
+
* - `'both'` : AR + non-AR; the AR toggle is shown so the user can
|
|
161
|
+
* switch at runtime.
|
|
162
|
+
* - `'ar'` : AR only. AR toggle hidden (nothing to toggle); the
|
|
163
|
+
* 0.5× lens chooser is also hidden (ARKit/ARCore can't use the
|
|
164
|
+
* ultra-wide), so the camera stays on the AR-capable 1× lens.
|
|
165
|
+
* - `'non-ar'`: non-AR only. AR toggle hidden.
|
|
166
|
+
* When set to a single source, that source wins regardless of
|
|
167
|
+
* `defaultCaptureSource`.
|
|
168
|
+
*/
|
|
169
|
+
captureSources?: CaptureSourcesMode;
|
|
145
170
|
style?: StyleProp<ViewStyle>;
|
|
146
171
|
/**
|
|
147
172
|
* Which incremental stitcher engine to drive. Default
|
|
@@ -256,22 +281,6 @@ export interface CameraProps {
|
|
|
256
281
|
* `flash` prop) can opt out by setting this to `false`.
|
|
257
282
|
*/
|
|
258
283
|
showFlashButton?: boolean;
|
|
259
|
-
/**
|
|
260
|
-
* v0.13.0 — show the built-in IncrementalPanGuide ("keep the
|
|
261
|
-
* arrow on the line" drift marker) while recording. Defaults
|
|
262
|
-
* to `true`. The guide is gyroscope-driven and only active
|
|
263
|
-
* during the recording phase (no idle sensor cost). Hosts that
|
|
264
|
-
* want their own pan-guide chrome can opt out via `false`.
|
|
265
|
-
*/
|
|
266
|
-
panGuide?: boolean;
|
|
267
|
-
/**
|
|
268
|
-
* v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
|
|
269
|
-
* ("Pan slowly" / "Slow down" / "Too fast") while recording.
|
|
270
|
-
* Defaults to `true`. Gyroscope-driven, only active during
|
|
271
|
-
* recording. Hosts that want their own speed chrome can opt
|
|
272
|
-
* out via `false`.
|
|
273
|
-
*/
|
|
274
|
-
panoramaGuidance?: boolean;
|
|
275
284
|
/**
|
|
276
285
|
* v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
|
|
277
286
|
* renders a top-of-screen header showing this title (centred)
|
|
@@ -476,4 +485,50 @@ export interface CameraProps {
|
|
|
476
485
|
* The public `<Camera>` component.
|
|
477
486
|
*/
|
|
478
487
|
export declare function Camera(props: CameraProps): React.JSX.Element;
|
|
488
|
+
/**
|
|
489
|
+
* v0.12.0 — JS edge corresponding to the physical home-indicator
|
|
490
|
+
* side of the device. This is where the shutter + controls anchor
|
|
491
|
+
* to so they're always within thumb reach of the user's grip
|
|
492
|
+
* (matching iOS Camera's behaviour).
|
|
493
|
+
*
|
|
494
|
+
* Combines two signals:
|
|
495
|
+
* - `jsLandscape`: whether the OS rotated the framebuffer. True
|
|
496
|
+
* only for non-locked hosts in device-landscape.
|
|
497
|
+
* - `deviceOrient`: physical device orientation from the sensor.
|
|
498
|
+
*
|
|
499
|
+
* Truth table:
|
|
500
|
+
* | jsLandscape | deviceOrient | edge |
|
|
501
|
+
* |--- |--- |--- |
|
|
502
|
+
* | false | any | bottom | (portrait JS coords —
|
|
503
|
+
* | | | | device-bottom = JS-bottom
|
|
504
|
+
* | | | | in both locked and
|
|
505
|
+
* | | | | non-locked-portrait)
|
|
506
|
+
* | true | landscape-left | right | (screen rotated, home
|
|
507
|
+
* | | | | indicator on user-right)
|
|
508
|
+
* | true | landscape-right | left | (mirror)
|
|
509
|
+
*
|
|
510
|
+
* Caveats:
|
|
511
|
+
* - Non-locked + upside-down doesn't surface JS-top here because
|
|
512
|
+
* upside-down doesn't change window dimensions; we can't
|
|
513
|
+
* distinguish locked-portrait-with-device-flipped from
|
|
514
|
+
* non-locked-portrait-with-screen-flipped-180°. Defaults to
|
|
515
|
+
* JS-bottom which matches the more common locked case. Add
|
|
516
|
+
* handling here when a host needs upside-down support.
|
|
517
|
+
* - jsLandscape=true with non-landscape device shouldn't happen
|
|
518
|
+
* in steady state — only during a transition mid-rotation.
|
|
519
|
+
* Falls through to 'right' as a defensive default.
|
|
520
|
+
*/
|
|
521
|
+
type HomeIndicatorEdge = 'bottom' | 'top' | 'left' | 'right';
|
|
522
|
+
declare function homeIndicatorEdge(jsLandscape: boolean, deviceOrient: DeviceOrientation): HomeIndicatorEdge;
|
|
523
|
+
/**
|
|
524
|
+
* v0.12.0 — true when the anchor edge is on a side (left/right), so
|
|
525
|
+
* the band + shutter row need to be vertical strips. Top/bottom
|
|
526
|
+
* anchors yield horizontal strips.
|
|
527
|
+
*/
|
|
528
|
+
declare function isSideEdge(edge: HomeIndicatorEdge): boolean;
|
|
529
|
+
/** @internal test-only — see `homeIndicatorEdge`. */
|
|
530
|
+
export declare const _homeIndicatorEdgeForTests: typeof homeIndicatorEdge;
|
|
531
|
+
/** @internal test-only — see `isSideEdge`. */
|
|
532
|
+
export declare const _isSideEdgeForTests: typeof isSideEdge;
|
|
533
|
+
export {};
|
|
479
534
|
//# sourceMappingURL=Camera.d.ts.map
|
package/dist/camera/Camera.js
CHANGED
|
@@ -74,7 +74,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
74
74
|
};
|
|
75
75
|
})();
|
|
76
76
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
77
|
-
exports.CameraError = void 0;
|
|
77
|
+
exports._isSideEdgeForTests = exports._homeIndicatorEdgeForTests = exports.CameraError = void 0;
|
|
78
78
|
exports.Camera = Camera;
|
|
79
79
|
const react_1 = __importStar(require("react"));
|
|
80
80
|
const react_native_1 = require("react-native");
|
|
@@ -92,15 +92,14 @@ const CaptureMemoryPill_1 = require("./CaptureMemoryPill");
|
|
|
92
92
|
const CaptureKeyframePill_1 = require("./CaptureKeyframePill");
|
|
93
93
|
const CaptureOrientationPill_1 = require("./CaptureOrientationPill");
|
|
94
94
|
const CaptureStitchStatsToast_1 = require("./CaptureStitchStatsToast");
|
|
95
|
-
const IncrementalPanGuide_1 = require("./IncrementalPanGuide");
|
|
96
95
|
const PanoramaBandOverlay_1 = require("./PanoramaBandOverlay");
|
|
97
|
-
const PanoramaGuidance_1 = require("./PanoramaGuidance");
|
|
98
96
|
const PanoramaSettingsBridge_1 = require("./PanoramaSettingsBridge");
|
|
99
97
|
const PanoramaSettingsModal_1 = require("./PanoramaSettingsModal");
|
|
100
98
|
const buildPanoramaInitialSettings_1 = require("./buildPanoramaInitialSettings");
|
|
101
99
|
const lowMemDevice_1 = require("./lowMemDevice");
|
|
102
100
|
const useCapture_1 = require("./useCapture");
|
|
103
101
|
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
102
|
+
const useContentRotation_1 = require("./useContentRotation");
|
|
104
103
|
const useOrientationDrift_1 = require("./useOrientationDrift");
|
|
105
104
|
const OrientationDriftModal_1 = require("./OrientationDriftModal");
|
|
106
105
|
const incremental_1 = require("../stitching/incremental");
|
|
@@ -118,10 +117,10 @@ class CameraError extends Error {
|
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
119
|
exports.CameraError = CameraError;
|
|
121
|
-
function LensChip({ lens, onChange, has0_5x }) {
|
|
120
|
+
function LensChip({ lens, onChange, has0_5x, contentRotation }) {
|
|
122
121
|
if (!has0_5x) {
|
|
123
122
|
return (react_1.default.createElement(react_native_1.View, { style: [lensChipStyles.container, lensChipStyles.singleLens] },
|
|
124
|
-
react_1.default.createElement(react_native_1.Text, { style: lensChipStyles.label }, "1\u00D7")));
|
|
123
|
+
react_1.default.createElement(react_native_1.Text, { style: [lensChipStyles.label, contentRotation] }, "1\u00D7")));
|
|
125
124
|
}
|
|
126
125
|
return (react_1.default.createElement(react_native_1.View, { style: lensChipStyles.container },
|
|
127
126
|
react_1.default.createElement(react_native_1.Pressable, { onPress: () => onChange('0.5x'), accessibilityRole: "button", accessibilityLabel: "0.5x ultra-wide lens", accessibilityState: { selected: lens === '0.5x' }, style: [
|
|
@@ -131,6 +130,7 @@ function LensChip({ lens, onChange, has0_5x }) {
|
|
|
131
130
|
react_1.default.createElement(react_native_1.Text, { style: [
|
|
132
131
|
lensChipStyles.label,
|
|
133
132
|
lens === '0.5x' && lensChipStyles.labelActive,
|
|
133
|
+
contentRotation,
|
|
134
134
|
] }, "0.5\u00D7")),
|
|
135
135
|
react_1.default.createElement(react_native_1.Pressable, { onPress: () => onChange('1x'), accessibilityRole: "button", accessibilityLabel: "1x wide-angle lens", accessibilityState: { selected: lens === '1x' }, style: [
|
|
136
136
|
lensChipStyles.pill,
|
|
@@ -139,6 +139,7 @@ function LensChip({ lens, onChange, has0_5x }) {
|
|
|
139
139
|
react_1.default.createElement(react_native_1.Text, { style: [
|
|
140
140
|
lensChipStyles.label,
|
|
141
141
|
lens === '1x' && lensChipStyles.labelActive,
|
|
142
|
+
contentRotation,
|
|
142
143
|
] }, "1\u00D7"))));
|
|
143
144
|
}
|
|
144
145
|
const lensChipStyles = react_native_1.StyleSheet.create({
|
|
@@ -171,11 +172,12 @@ const lensChipStyles = react_native_1.StyleSheet.create({
|
|
|
171
172
|
color: '#1a1a1a',
|
|
172
173
|
},
|
|
173
174
|
});
|
|
174
|
-
function ARToggle({ arEnabled, onToggle }) {
|
|
175
|
+
function ARToggle({ arEnabled, onToggle, contentRotation }) {
|
|
175
176
|
return (react_1.default.createElement(react_native_1.Pressable, { onPress: onToggle, accessibilityRole: "switch", accessibilityLabel: `AR mode ${arEnabled ? 'on' : 'off'}`, accessibilityState: { checked: arEnabled }, style: [arToggleStyles.container, arEnabled && arToggleStyles.containerOn] },
|
|
176
177
|
react_1.default.createElement(react_native_1.Text, { style: [
|
|
177
178
|
arToggleStyles.label,
|
|
178
179
|
arEnabled && arToggleStyles.labelOn,
|
|
180
|
+
contentRotation,
|
|
179
181
|
] }, "AR")));
|
|
180
182
|
}
|
|
181
183
|
const arToggleStyles = react_native_1.StyleSheet.create({
|
|
@@ -277,7 +279,14 @@ function extractPanoramaOverrides(props) {
|
|
|
277
279
|
* The public `<Camera>` component.
|
|
278
280
|
*/
|
|
279
281
|
function Camera(props) {
|
|
280
|
-
const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true,
|
|
282
|
+
const { defaultCaptureSource = 'ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
|
|
283
|
+
// v0.13.2 — capture-source constraint (default 'both'). Derives which
|
|
284
|
+
// sources are permitted; `captureSources` overrides any conflicting
|
|
285
|
+
// `defaultCaptureSource`. Used to constrain the initial AR preference
|
|
286
|
+
// and to hide the AR toggle / lens chooser below.
|
|
287
|
+
const arAllowed = captureSources !== 'non-ar';
|
|
288
|
+
const nonArAllowed = captureSources !== 'ar';
|
|
289
|
+
const arOnly = captureSources === 'ar';
|
|
281
290
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
282
291
|
// v0.12.0 — JS-layout orientation independent of device-physical.
|
|
283
292
|
// `useWindowDimensions().width > height` tells us if the OS
|
|
@@ -288,8 +297,13 @@ function Camera(props) {
|
|
|
288
297
|
const jsWindow = (0, react_native_1.useWindowDimensions)();
|
|
289
298
|
const jsLandscape = jsWindow.width > jsWindow.height;
|
|
290
299
|
// ── State ───────────────────────────────────────────────────────
|
|
291
|
-
|
|
292
|
-
|
|
300
|
+
// v0.13.2 — initial AR preference honours `defaultCaptureSource` but
|
|
301
|
+
// is clamped to the `captureSources` constraint: 'ar' forces on,
|
|
302
|
+
// 'non-ar' forces off, 'both' uses the default.
|
|
303
|
+
const [arPreference, setArPreference] = (0, react_1.useState)(!arAllowed ? false : !nonArAllowed ? true : defaultCaptureSource === 'ar');
|
|
304
|
+
// v0.13.2 — `arOnly` forces the 1× lens (the ultra-wide isn't usable
|
|
305
|
+
// in AR), and the lens chooser is hidden in that mode.
|
|
306
|
+
const [lens, setLens] = (0, react_1.useState)(arOnly ? '1x' : defaultLens);
|
|
293
307
|
// v0.13.0 — flash state. Controlled by `controlledFlash` when the
|
|
294
308
|
// host supplies the `flash` prop; otherwise owned internally and
|
|
295
309
|
// toggled by the built-in flash button. `effectiveFlash` below
|
|
@@ -319,6 +333,14 @@ function Camera(props) {
|
|
|
319
333
|
const isAR = effectiveCaptureSource === 'ar';
|
|
320
334
|
const isNonAR = !isAR;
|
|
321
335
|
const deviceOrientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
|
|
336
|
+
// v0.13.1 — counter-rotation for control CONTENT (AR toggle, lens
|
|
337
|
+
// pill, flash icon, thumbnails) so their labels read upright relative
|
|
338
|
+
// to gravity when the device is held landscape under a PORTRAIT-LOCKED
|
|
339
|
+
// host (the recommended config — the JS framebuffer stays portrait, so
|
|
340
|
+
// without this the labels render at 90°). Returns `{}` (no-op) in the
|
|
341
|
+
// common upright cases, including non-locked hosts where the OS already
|
|
342
|
+
// rotated the framebuffer. See `useContentRotation` truth table.
|
|
343
|
+
const contentRotation = (0, useContentRotation_1.useContentRotation)();
|
|
322
344
|
// ── Camera handoff gate ─────────────────────────────────────────
|
|
323
345
|
//
|
|
324
346
|
// The placeholder rendered while the underlying camera identity
|
|
@@ -347,6 +369,33 @@ function Camera(props) {
|
|
|
347
369
|
const inFlightTransition = settledIsARRef.current !== isAR
|
|
348
370
|
|| settledLensRef.current !== lens
|
|
349
371
|
|| cameraTransitioning;
|
|
372
|
+
// ── v0.13.1 — Android portrait lock ─────────────────────────────
|
|
373
|
+
//
|
|
374
|
+
// Android lets a mounted view force its host Activity's orientation,
|
|
375
|
+
// so `<Camera>` guarantees a portrait capture surface regardless of
|
|
376
|
+
// the host app's manifest (even a landscape/unlocked host gets a
|
|
377
|
+
// portrait camera while `<Camera>` is mounted). The lock lives on
|
|
378
|
+
// the Activity via the native `RNSARSession` module, so it covers
|
|
379
|
+
// BOTH the AR (ARCore) and non-AR (vision-camera) capture paths.
|
|
380
|
+
//
|
|
381
|
+
// iOS is intentionally NOT locked here: iOS supported orientations
|
|
382
|
+
// are a static Info.plist declaration the host owns, and we want iOS
|
|
383
|
+
// hosts to be able to support landscape/unlocked capture. Hosts that
|
|
384
|
+
// want a portrait-only iOS app set UISupportedInterfaceOrientations
|
|
385
|
+
// themselves.
|
|
386
|
+
//
|
|
387
|
+
// Empty dep array — lock on mount, restore the host's PRIOR
|
|
388
|
+
// orientation on unmount (the native side captures it).
|
|
389
|
+
(0, react_1.useEffect)(() => {
|
|
390
|
+
if (react_native_1.Platform.OS !== 'android')
|
|
391
|
+
return undefined;
|
|
392
|
+
const arModule = react_native_1.NativeModules
|
|
393
|
+
.RNSARSession;
|
|
394
|
+
arModule?.lockPortrait?.();
|
|
395
|
+
return () => {
|
|
396
|
+
arModule?.unlockOrientation?.();
|
|
397
|
+
};
|
|
398
|
+
}, []);
|
|
350
399
|
// ── Notify parent of capture-source changes ─────────────────────
|
|
351
400
|
const lastEmittedSourceRef = (0, react_1.useRef)(null);
|
|
352
401
|
(0, react_1.useEffect)(() => {
|
|
@@ -355,19 +404,22 @@ function Camera(props) {
|
|
|
355
404
|
onCaptureSourceChange?.(effectiveCaptureSource);
|
|
356
405
|
}
|
|
357
406
|
}, [effectiveCaptureSource, onCaptureSourceChange]);
|
|
358
|
-
// ── Lens chip availability ──────────────────────────────────────
|
|
359
|
-
// TODO follow-up: probe the device's available physical lenses via
|
|
360
|
-
// vision-camera's `useCameraDevices` and surface in
|
|
361
|
-
// `useCapture().availablePhysicalDevices`. For now we assume the
|
|
362
|
-
// 0.5x ultra-wide exists on modern devices. When it doesn't, the
|
|
363
|
-
// lens chip degenerates to a static 1× indicator (see LensChip).
|
|
364
|
-
const has0_5x = true;
|
|
365
407
|
// ── Capture hooks ───────────────────────────────────────────────
|
|
408
|
+
// v0.13.2 — pass the active `lens` so useCapture uses capability-aware
|
|
409
|
+
// selection (multi-cam zoom-switch where available, standalone-ultra-
|
|
410
|
+
// wide swap otherwise). Replaces the old per-lens
|
|
411
|
+
// `preferredPhysicalDevice` request that mis-selected on some phones.
|
|
366
412
|
const capture = (0, useCapture_1.useCapture)({
|
|
367
413
|
cameraPosition: 'back',
|
|
368
414
|
enableQualityChecks: false,
|
|
369
|
-
|
|
415
|
+
lens,
|
|
370
416
|
});
|
|
417
|
+
// ── Lens chip availability ──────────────────────────────────────
|
|
418
|
+
// v0.13.2 — real device capability from `useCapture` (which uses
|
|
419
|
+
// `selectCaptureDevice`). True only when the device actually exposes
|
|
420
|
+
// an ultra-wide reachable via a multi-cam zoom OR a standalone
|
|
421
|
+
// ultra-wide device; false on wide-only hardware (chip hides).
|
|
422
|
+
const has0_5x = capture.has0_5x;
|
|
371
423
|
const incremental = (0, useIncrementalStitcher_1.useIncrementalStitcher)();
|
|
372
424
|
const visionCameraRef = (0, react_1.useRef)(null);
|
|
373
425
|
const arViewRef = (0, react_1.useRef)(null);
|
|
@@ -881,19 +933,41 @@ function Camera(props) {
|
|
|
881
933
|
// ── v0.13.0 — Flash control ─────────────────────────────────────
|
|
882
934
|
//
|
|
883
935
|
// `flashRequested` is what the host / built-in button asks for.
|
|
884
|
-
// `effectiveFlash` is what we
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
936
|
+
// `effectiveFlash` is what we drive into vision-camera (non-AR). AR
|
|
937
|
+
// mode forces 'off' (flash is hidden in AR; ARKit/ARCore own the
|
|
938
|
+
// device) so vision-camera — which isn't the active camera in AR —
|
|
939
|
+
// doesn't fight for it.
|
|
940
|
+
//
|
|
941
|
+
// v0.13.1 — the ACTIVE device's torch capability is the source of
|
|
942
|
+
// truth. The ultra-wide (0.5×) lens has no flash/torch unit on most
|
|
943
|
+
// phones, so vision-camera throws `flash-not-available` if we pass
|
|
944
|
+
// flash="on" while it's selected. `capture.device.hasTorch` (from
|
|
945
|
+
// vision-camera's device list) tells us definitively; we hide the
|
|
946
|
+
// flash control and force 'off' when the device can't flash.
|
|
947
|
+
// v0.13.2 — `capture.deviceHasTorch` reflects the MOUNTED device. In
|
|
948
|
+
// multi-cam mode this is the multi-cam device (has a torch → flash
|
|
949
|
+
// works on both 1× and 0.5× via zoom). In standalone-uw mode on 0.5×
|
|
950
|
+
// the mounted device is the torchless ultra-wide → flash hides.
|
|
951
|
+
const deviceHasTorch = capture.deviceHasTorch;
|
|
889
952
|
const flashRequested = controlledFlash ?? internalFlash;
|
|
890
|
-
const effectiveFlash = isAR ? 'off' : flashRequested;
|
|
953
|
+
const effectiveFlash = isAR || !deviceHasTorch ? 'off' : flashRequested;
|
|
891
954
|
const toggleFlash = (0, react_1.useCallback)(() => {
|
|
892
955
|
const next = flashRequested === 'on' ? 'off' : 'on';
|
|
893
956
|
if (controlledFlash == null)
|
|
894
957
|
setInternalFlash(next);
|
|
895
958
|
onFlashChange?.(next);
|
|
896
959
|
}, [flashRequested, controlledFlash, onFlashChange]);
|
|
960
|
+
// v0.13.1 — top-right control pills (flash + AR) stack vertically
|
|
961
|
+
// UNDER the settings affordance. Anchor depends on what's above:
|
|
962
|
+
// - headerTitle set → pills clear the CaptureHeader bar
|
|
963
|
+
// (title row ≈ topInset + ~36; guidance pill adds ~28 when present)
|
|
964
|
+
// - standalone gear → pills clear the 40px gear at topInset + 8
|
|
965
|
+
// - neither → pills start where the gear would be
|
|
966
|
+
const pillStackTop = headerTitle != null
|
|
967
|
+
? insets.top + (headerGuidance != null ? 72 : 40)
|
|
968
|
+
: showSettingsButton
|
|
969
|
+
? insets.top + 8 + 44
|
|
970
|
+
: insets.top + 8;
|
|
897
971
|
// ── JSX ─────────────────────────────────────────────────────────
|
|
898
972
|
return (react_1.default.createElement(react_native_1.View, { style: [styles.container, style] },
|
|
899
973
|
inFlightTransition ? (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] },
|
|
@@ -905,7 +979,12 @@ function Camera(props) {
|
|
|
905
979
|
// the very first buffered preview frame. Android takeSnapshot
|
|
906
980
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
907
981
|
// which has run on `video` (true) for months without issue.
|
|
908
|
-
video: true, flash: effectiveFlash,
|
|
982
|
+
video: true, flash: effectiveFlash,
|
|
983
|
+
// v0.13.2 — in multi-cam mode the lens is switched via zoom
|
|
984
|
+
// on a single mounted device (0.5× → ultra-wide end, 1× →
|
|
985
|
+
// wide baseline). undefined in standalone/wide-only modes
|
|
986
|
+
// (lens = device identity, no zoom).
|
|
987
|
+
zoom: capture.deviceZoom, style: react_native_1.StyleSheet.absoluteFill,
|
|
909
988
|
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
910
989
|
// the camera producer thread for every frame. Only wired
|
|
911
990
|
// in non-AR mode; AR mode uses ARCameraView which doesn't
|
|
@@ -926,8 +1005,6 @@ function Camera(props) {
|
|
|
926
1005
|
onError?.(new CameraError('VISION_CAMERA_RUNTIME', `${codeStr}: ${msg}`, err));
|
|
927
1006
|
} })),
|
|
928
1007
|
react_1.default.createElement(CaptureStatusOverlay_1.CaptureStatusOverlay, { phase: statusPhase, topInset: insets.top, recordingStartedAt: recordingStartedAt ?? undefined }),
|
|
929
|
-
panGuide && (react_1.default.createElement(IncrementalPanGuide_1.IncrementalPanGuide, { active: statusPhase === 'recording' })),
|
|
930
|
-
panoramaGuidance && (react_1.default.createElement(PanoramaGuidance_1.PanoramaGuidance, { active: statusPhase === 'recording' })),
|
|
931
1008
|
settings.debug && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
932
1009
|
react_1.default.createElement(CaptureOrientationPill_1.CaptureOrientationPill, { orientation: deviceOrientation, topInset: insets.top }),
|
|
933
1010
|
react_1.default.createElement(CaptureKeyframePill_1.CaptureKeyframePill, { state: incrementalState, topInset: insets.top }),
|
|
@@ -938,22 +1015,36 @@ function Camera(props) {
|
|
|
938
1015
|
react_1.default.createElement(CaptureHeader_1.CaptureHeader, { title: headerTitle, onBack: onHeaderBack, backLabel: headerBackLabel, guidance: headerGuidance, colors: headerColors, topInset: insets.top, onSettingsPress: showSettingsButton
|
|
939
1016
|
? () => setSettingsModalVisible(true)
|
|
940
1017
|
: undefined }))) : (showSettingsButton && (react_1.default.createElement(SettingsButton, { topInset: insets.top, onPress: () => setSettingsModalVisible(true) }))),
|
|
941
|
-
thumbnails != null && statusPhase !== 'recording' && (react_1.default.createElement(react_native_1.View, { style: styles.thumbnailStripWrap, pointerEvents: "box-none" },
|
|
942
|
-
react_1.default.createElement(CaptureThumbnailStrip_1.CaptureThumbnailStrip, { items: thumbnails, minPhotos: thumbnailsMin, maxPhotos: thumbnailsMax, onItemPress: onThumbnailPress }))),
|
|
943
1018
|
react_1.default.createElement(react_native_1.View, { pointerEvents: "box-none", style: bottomAreaStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation), insets.bottom + 12, insets.top + 12) },
|
|
944
1019
|
statusPhase === 'recording' && (react_1.default.createElement(PanoramaBandOverlay_1.PanoramaBandOverlay, { state: incrementalState, frameUris: batchKeyframeThumbnails, captureOrientation: deviceOrientation, vertical: isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) })),
|
|
1020
|
+
thumbnails != null && statusPhase !== 'recording' && (react_1.default.createElement(CaptureThumbnailStrip_1.CaptureThumbnailStrip, { items: thumbnails, minPhotos: thumbnailsMin, maxPhotos: thumbnailsMax, onItemPress: onThumbnailPress,
|
|
1021
|
+
// v0.13.1 — stack the idle strip vertically when the
|
|
1022
|
+
// home-indicator anchor is on a side edge (non-locked host
|
|
1023
|
+
// in landscape), matching PanoramaBandOverlay's `vertical`
|
|
1024
|
+
// so the strip rides the home-indicator edge instead of
|
|
1025
|
+
// running horizontally across the rotated screen.
|
|
1026
|
+
vertical: isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)),
|
|
1027
|
+
// v0.13.1 — counter-rotate the thumbnail images so the
|
|
1028
|
+
// captured scene reads upright in portrait-locked landscape.
|
|
1029
|
+
contentRotation: contentRotation })),
|
|
945
1030
|
react_1.default.createElement(react_native_1.View, { style: bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) },
|
|
946
|
-
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft },
|
|
947
|
-
styles.flashButton,
|
|
948
|
-
flashRequested === 'on' && !isAR && styles.flashButtonActive,
|
|
949
|
-
isAR && styles.flashButtonDisabled,
|
|
950
|
-
] },
|
|
951
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.flashIcon }, "\u26A1")))),
|
|
1031
|
+
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft }),
|
|
952
1032
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarCenter },
|
|
953
|
-
react_1.default.createElement(LensChip, { lens: lens, onChange: handleLensChange, has0_5x: has0_5x }),
|
|
1033
|
+
!arOnly && (react_1.default.createElement(LensChip, { lens: lens, onChange: handleLensChange, has0_5x: has0_5x, contentRotation: contentRotation })),
|
|
954
1034
|
react_1.default.createElement(react_native_1.View, { style: styles.shutterWrap },
|
|
955
1035
|
react_1.default.createElement(CameraShutter_1.CameraShutter, { onTap: handleTap, onHoldStart: enablePanoramaMode ? handleHoldStart : noop, onHoldComplete: enablePanoramaMode ? handleHoldEnd : noop, isProcessing: statusPhase === 'stitching', disabled: statusPhase === 'stitching' }))),
|
|
956
|
-
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarRight }
|
|
1036
|
+
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarRight }))),
|
|
1037
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.pillStack, { top: pillStackTop }], pointerEvents: "box-none" },
|
|
1038
|
+
arAllowed && nonArAllowed && lens === '1x' && isARSupportedOnDevice && (react_1.default.createElement(ARToggle, { arEnabled: arPreference, onToggle: handleARToggle, contentRotation: contentRotation })),
|
|
1039
|
+
showFlashButton && !isAR && deviceHasTorch && (react_1.default.createElement(react_native_1.Pressable, { onPress: toggleFlash, accessibilityRole: "button", accessibilityLabel: `Flash ${flashRequested === 'on' ? 'on' : 'off'}`, accessibilityState: { selected: flashRequested === 'on' }, hitSlop: 8, style: [
|
|
1040
|
+
pillStyles.pill,
|
|
1041
|
+
flashRequested === 'on' && pillStyles.pillActive,
|
|
1042
|
+
] },
|
|
1043
|
+
react_1.default.createElement(react_native_1.Text, { style: [
|
|
1044
|
+
pillStyles.flashGlyph,
|
|
1045
|
+
flashRequested === 'on' && pillStyles.glyphActive,
|
|
1046
|
+
contentRotation,
|
|
1047
|
+
] }, "\u26A1")))),
|
|
957
1048
|
react_1.default.createElement(PanoramaSettingsModal_1.PanoramaSettingsModal, { visible: settingsModalVisible, settings: settings, onChange: setSettings, onClose: () => setSettingsModalVisible(false) }),
|
|
958
1049
|
react_1.default.createElement(OrientationDriftModal_1.OrientationDriftModal, { visible: drift.drifted && !driftModalDismissed, captureOrientation: drift.captureOrientation, currentOrientation: drift.currentOrientation, onAcknowledge: () => setDriftModalDismissed(true) }),
|
|
959
1050
|
react_1.default.createElement(CapturePreview_1.CapturePreview, { visible: capturePreview != null, imageUri: capturePreview?.imageUri ?? '', imageWidth: capturePreview?.imageWidth, imageHeight: capturePreview?.imageHeight, title: capturePreview?.title, actions: capturePreviewActions, onClose: onCapturePreviewClose ?? noop })));
|
|
@@ -978,6 +1069,16 @@ function homeIndicatorEdge(jsLandscape, deviceOrient) {
|
|
|
978
1069
|
function isSideEdge(edge) {
|
|
979
1070
|
return edge === 'left' || edge === 'right';
|
|
980
1071
|
}
|
|
1072
|
+
// v0.13.1 — test-only exports of the pure orientation-decision
|
|
1073
|
+
// functions. `homeIndicatorEdge` + `isSideEdge` together produce the
|
|
1074
|
+
// `vertical` flag that drives PanoramaBandOverlay and
|
|
1075
|
+
// CaptureThumbnailStrip layout, so they carry the orientation contract.
|
|
1076
|
+
// Unit-tested via these handles (the lib's jest config is pure-TS and
|
|
1077
|
+
// can't mount <Camera>; see jest.config.js).
|
|
1078
|
+
/** @internal test-only — see `homeIndicatorEdge`. */
|
|
1079
|
+
exports._homeIndicatorEdgeForTests = homeIndicatorEdge;
|
|
1080
|
+
/** @internal test-only — see `isSideEdge`. */
|
|
1081
|
+
exports._isSideEdgeForTests = isSideEdge;
|
|
981
1082
|
/**
|
|
982
1083
|
* v0.12.0 — bottom-controls outer container positioning. Anchors
|
|
983
1084
|
* to the home-indicator JS edge with the appropriate flex direction
|
|
@@ -1106,29 +1207,44 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1106
1207
|
left: 0,
|
|
1107
1208
|
right: 0,
|
|
1108
1209
|
},
|
|
1109
|
-
thumbnailStripWrap
|
|
1210
|
+
// v0.13.1 — `thumbnailStripWrap` removed. The strip now renders
|
|
1211
|
+
// inside the orientation-aware bottomArea container (alongside
|
|
1212
|
+
// PanoramaBandOverlay and the bottom bar) rather than as a
|
|
1213
|
+
// position-absolute overlay at hard-coded `bottom: 160`.
|
|
1214
|
+
//
|
|
1215
|
+
// v0.13.1 — top-right control pill stack (flash + AR). Absolute,
|
|
1216
|
+
// pinned to the right edge under the settings affordance; `top` is
|
|
1217
|
+
// set inline from `pillStackTop`. Column so the pills stack
|
|
1218
|
+
// vertically; gap keeps them from touching.
|
|
1219
|
+
pillStack: {
|
|
1110
1220
|
position: 'absolute',
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1221
|
+
right: 14,
|
|
1222
|
+
alignItems: 'flex-end',
|
|
1223
|
+
gap: 10,
|
|
1114
1224
|
},
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1225
|
+
});
|
|
1226
|
+
// v0.13.1 — shared pill style for the top-right control stack. The
|
|
1227
|
+
// flash pill matches the AR toggle's shape (same padding / radius /
|
|
1228
|
+
// background) so the two read as a set.
|
|
1229
|
+
const pillStyles = react_native_1.StyleSheet.create({
|
|
1230
|
+
pill: {
|
|
1231
|
+
paddingHorizontal: 14,
|
|
1232
|
+
paddingVertical: 8,
|
|
1233
|
+
borderRadius: 16,
|
|
1234
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
1235
|
+
minWidth: 56,
|
|
1119
1236
|
alignItems: 'center',
|
|
1120
1237
|
justifyContent: 'center',
|
|
1121
|
-
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
1122
1238
|
},
|
|
1123
|
-
|
|
1239
|
+
pillActive: {
|
|
1124
1240
|
backgroundColor: '#ffd34d',
|
|
1125
1241
|
},
|
|
1126
|
-
|
|
1127
|
-
opacity: 0.35,
|
|
1128
|
-
},
|
|
1129
|
-
flashIcon: {
|
|
1130
|
-
fontSize: 20,
|
|
1242
|
+
flashGlyph: {
|
|
1131
1243
|
color: '#ffffff',
|
|
1244
|
+
fontSize: 18,
|
|
1245
|
+
},
|
|
1246
|
+
glyphActive: {
|
|
1247
|
+
color: '#1a1a1a',
|
|
1132
1248
|
},
|
|
1133
1249
|
});
|
|
1134
1250
|
//# sourceMappingURL=Camera.js.map
|
|
@@ -25,6 +25,12 @@ export interface CameraViewProps {
|
|
|
25
25
|
device: CameraDevice | null | undefined;
|
|
26
26
|
/** Flash / torch state from ``useCapture().flash``. */
|
|
27
27
|
flash?: 'off' | 'on';
|
|
28
|
+
/**
|
|
29
|
+
* v0.13.2 — zoom factor for the mounted device. Used in multi-cam
|
|
30
|
+
* mode to switch lenses (0.5× ultra-wide ↔ 1× wide) on a single
|
|
31
|
+
* device. `undefined` leaves vision-camera at its default zoom.
|
|
32
|
+
*/
|
|
33
|
+
zoom?: number;
|
|
28
34
|
/** Whether the preview is actively rendering. Defaults to true. */
|
|
29
35
|
isActive?: boolean;
|
|
30
36
|
/**
|
|
@@ -73,7 +73,7 @@ const VC_LIFECYCLE_ERROR_CODES = new Set([
|
|
|
73
73
|
'system/camera-has-been-disconnected', // another app grabbed the camera
|
|
74
74
|
'device/camera-already-in-use', // same class as above
|
|
75
75
|
]);
|
|
76
|
-
exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', isActive = true, video = false, guidance, style, cameraProps, onError, }, ref) {
|
|
76
|
+
exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', zoom, isActive = true, video = false, guidance, style, cameraProps, onError, }, ref) {
|
|
77
77
|
// Error filter — see `VC_LIFECYCLE_ERROR_CODES` for the swallow
|
|
78
78
|
// list rationale. `code` on vision-camera's `CameraRuntimeError`
|
|
79
79
|
// is typed as a string; treat any non-string defensively as a
|
|
@@ -97,7 +97,7 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
|
|
|
97
97
|
react_1.default.createElement(react_native_1.Text, { style: styles.placeholderText }, "Initialising camera\u2026")));
|
|
98
98
|
}
|
|
99
99
|
return (react_1.default.createElement(react_native_1.View, { style: [styles.root, style] },
|
|
100
|
-
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive, photo: true, video: video,
|
|
100
|
+
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive, photo: true, video: video, ...(zoom != null ? { zoom } : {}),
|
|
101
101
|
// Bake the device orientation into the captured pixels.
|
|
102
102
|
// Without this, vision-camera writes the file in the camera
|
|
103
103
|
// sensor's native landscape and relies on EXIF metadata to
|
|
@@ -27,20 +27,26 @@ exports.CaptureHeader = CaptureHeader;
|
|
|
27
27
|
const react_1 = __importDefault(require("react"));
|
|
28
28
|
const react_native_1 = require("react-native");
|
|
29
29
|
function CaptureHeader({ title, onBack, backLabel = '‹ Back', onSettingsPress, guidance, topInset = 0, colors, style, }) {
|
|
30
|
-
|
|
30
|
+
// v0.13.1 — defaults are now transparent over the camera preview
|
|
31
|
+
// (matches the AR toggle / settings gear pill style); hosts using
|
|
32
|
+
// the header outside a camera context can pass solid colours via
|
|
33
|
+
// `colors`. Title + gear get a text shadow for legibility over
|
|
34
|
+
// bright preview content; guidance row keeps a translucent pill
|
|
35
|
+
// background for the same reason.
|
|
36
|
+
const bg = colors?.background ?? 'transparent';
|
|
31
37
|
const titleColor = colors?.title ?? '#ffffff';
|
|
32
38
|
const accent = colors?.accent ?? '#FF9F0A';
|
|
33
|
-
const guidanceBg = colors?.guidanceBackground ?? 'rgba(
|
|
39
|
+
const guidanceBg = colors?.guidanceBackground ?? 'rgba(0,0,0,0.45)';
|
|
34
40
|
const guidanceColor = colors?.guidanceText ?? '#ffffff';
|
|
35
41
|
return (react_1.default.createElement(react_native_1.View, { style: [{ backgroundColor: bg }, style] },
|
|
36
|
-
react_1.default.createElement(react_native_1.View, { style: [styles.titleRow, { paddingTop: topInset +
|
|
42
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.titleRow, { paddingTop: topInset + 4 }] },
|
|
37
43
|
onBack ? (react_1.default.createElement(react_native_1.Pressable, { onPress: onBack, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: "Go back", style: styles.backButton },
|
|
38
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.backText, { color: accent }] }, backLabel))) : (
|
|
44
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.backText, styles.textShadow, { color: accent }] }, backLabel))) : (
|
|
39
45
|
// Empty spacer keeps the title centred even when back is hidden.
|
|
40
46
|
react_1.default.createElement(react_native_1.View, { style: styles.backButton })),
|
|
41
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.title, { color: titleColor }], numberOfLines: 1, accessibilityRole: "header" }, title),
|
|
47
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.title, styles.textShadow, { color: titleColor }], numberOfLines: 1, accessibilityRole: "header" }, title),
|
|
42
48
|
onSettingsPress ? (react_1.default.createElement(react_native_1.Pressable, { onPress: onSettingsPress, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: "Open panorama settings", style: styles.backButton },
|
|
43
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.gearIcon, { color: accent }] }, "\u2699"))) : (react_1.default.createElement(react_native_1.View, { style: styles.backButton }))),
|
|
49
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.gearIcon, styles.textShadow, { color: accent }] }, "\u2699"))) : (react_1.default.createElement(react_native_1.View, { style: styles.backButton }))),
|
|
44
50
|
guidance ? (react_1.default.createElement(react_native_1.View, { style: [styles.guidance, { backgroundColor: guidanceBg }], accessibilityRole: "text" },
|
|
45
51
|
react_1.default.createElement(react_native_1.Text, { style: [styles.guidanceText, { color: guidanceColor }], numberOfLines: 2 }, guidance))) : null));
|
|
46
52
|
}
|
|
@@ -49,33 +55,50 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
49
55
|
flexDirection: 'row',
|
|
50
56
|
alignItems: 'center',
|
|
51
57
|
justifyContent: 'space-between',
|
|
52
|
-
paddingHorizontal:
|
|
53
|
-
paddingBottom:
|
|
58
|
+
paddingHorizontal: 12,
|
|
59
|
+
paddingBottom: 4,
|
|
54
60
|
},
|
|
55
61
|
backButton: {
|
|
56
|
-
minWidth:
|
|
57
|
-
paddingVertical:
|
|
62
|
+
minWidth: 56,
|
|
63
|
+
paddingVertical: 2,
|
|
58
64
|
},
|
|
59
65
|
backText: {
|
|
60
|
-
fontSize:
|
|
66
|
+
fontSize: 14,
|
|
61
67
|
fontWeight: '500',
|
|
62
68
|
},
|
|
63
69
|
title: {
|
|
64
70
|
flex: 1,
|
|
65
71
|
textAlign: 'center',
|
|
66
|
-
fontSize:
|
|
72
|
+
fontSize: 14,
|
|
67
73
|
fontWeight: '600',
|
|
68
74
|
},
|
|
69
75
|
guidance: {
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
// v0.13.1 — guidance row is now a centred pill inset from the
|
|
77
|
+
// edges (matches the AR-toggle / lens-chip pill style) rather
|
|
78
|
+
// than a full-width band. The pill background gives it its
|
|
79
|
+
// own contrast over the preview without forcing a solid bar.
|
|
80
|
+
alignSelf: 'center',
|
|
81
|
+
marginTop: 4,
|
|
82
|
+
paddingHorizontal: 10,
|
|
83
|
+
paddingVertical: 5,
|
|
84
|
+
borderRadius: 12,
|
|
85
|
+
maxWidth: '90%',
|
|
72
86
|
},
|
|
73
87
|
guidanceText: {
|
|
74
|
-
fontSize:
|
|
88
|
+
fontSize: 12,
|
|
89
|
+
textAlign: 'center',
|
|
75
90
|
},
|
|
76
91
|
gearIcon: {
|
|
77
|
-
fontSize:
|
|
92
|
+
fontSize: 20,
|
|
78
93
|
textAlign: 'right',
|
|
79
94
|
},
|
|
95
|
+
// v0.13.1 — subtle text shadow so the (now-transparent) header
|
|
96
|
+
// text stays legible over bright preview content. Same trick
|
|
97
|
+
// iOS Camera uses for the timestamp / mode labels.
|
|
98
|
+
textShadow: {
|
|
99
|
+
textShadowColor: 'rgba(0,0,0,0.65)',
|
|
100
|
+
textShadowOffset: { width: 0, height: 1 },
|
|
101
|
+
textShadowRadius: 2,
|
|
102
|
+
},
|
|
80
103
|
});
|
|
81
104
|
//# sourceMappingURL=CaptureHeader.js.map
|
|
@@ -37,7 +37,19 @@ function CapturePreview({ visible, imageUri, imageWidth, imageHeight, actions, o
|
|
|
37
37
|
? imageWidth / imageHeight
|
|
38
38
|
: 16 / 9;
|
|
39
39
|
const hasActions = actions && actions.length > 0;
|
|
40
|
-
return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "fade", transparent: true, statusBarTranslucent: true,
|
|
40
|
+
return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "fade", transparent: true, statusBarTranslucent: true,
|
|
41
|
+
// v0.13.1 — RN's iOS <Modal> defaults to portrait-only, which
|
|
42
|
+
// pins the stitched-image preview to portrait even when the host
|
|
43
|
+
// app is in landscape (the preview appeared sideways/letterboxed
|
|
44
|
+
// under a non-locked host). Declaring all four keeps the modal
|
|
45
|
+
// aligned with the interface. Mirrors the v0.12 fix already on
|
|
46
|
+
// OrientationDriftModal + PanoramaSettingsModal.
|
|
47
|
+
supportedOrientations: [
|
|
48
|
+
'portrait',
|
|
49
|
+
'portrait-upside-down',
|
|
50
|
+
'landscape-left',
|
|
51
|
+
'landscape-right',
|
|
52
|
+
], onRequestClose: onClose },
|
|
41
53
|
react_1.default.createElement(react_native_1.View, { style: styles.backdrop },
|
|
42
54
|
react_1.default.createElement(react_native_1.View, { style: styles.topBar },
|
|
43
55
|
react_1.default.createElement(react_native_1.View, { style: styles.topBarSpacer }),
|