Fire the LED on the 0.5x still
0.5x is a bare Camera2 session, so CameraX never saw the flash setting and the ultra-wide still came off unlit. The native module now reads FLASH_INFO_AVAILABLE off the lens, maps off/auto/on to CONTROL_AE_MODE plus FLASH_MODE_SINGLE, and starts an AE precapture so the HAL can meter the flash before the frame. The viewfinder hands its flashMode down beside the shutter, with a Zap control that only appears where a unit exists.
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, Image, StyleSheet } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Image as ImageIcon, Eye, Download, Camera, Settings, RefreshCw } from 'lucide-react-native';
|
||||
import { Image as ImageIcon, Eye, Download, Camera, Settings, RefreshCw, Zap, ZapOff } from 'lucide-react-native';
|
||||
import type { FlashMode } from '../types';
|
||||
|
||||
// "A" badge tucked into the corner of the flash button's AUTO state.
|
||||
const FLASH_A_BADGE = {
|
||||
position: 'absolute' as const,
|
||||
bottom: 6,
|
||||
right: 12,
|
||||
fontSize: 9,
|
||||
fontWeight: '700' as const,
|
||||
color: '#f4f4f5',
|
||||
};
|
||||
|
||||
interface CameraControlsProps {
|
||||
lastPhotoUri: string | null;
|
||||
@@ -13,6 +24,10 @@ interface CameraControlsProps {
|
||||
onPickImage: () => void;
|
||||
onOpenPreview: () => void;
|
||||
onOpenSettings: () => void;
|
||||
// LED flash state for the next still. Absent = no flash control to show
|
||||
// (library mode, or a lens with no flash unit).
|
||||
flashMode?: FlashMode;
|
||||
onCycleFlash?: () => void;
|
||||
onFlipCamera?: () => void;
|
||||
isLibraryMode: boolean;
|
||||
onSwitchToCamera?: () => void;
|
||||
@@ -26,6 +41,8 @@ export default function CameraControls({
|
||||
onPickImage,
|
||||
onOpenPreview,
|
||||
onOpenSettings,
|
||||
flashMode,
|
||||
onCycleFlash,
|
||||
onFlipCamera,
|
||||
isLibraryMode,
|
||||
onSwitchToCamera,
|
||||
@@ -33,100 +50,132 @@ export default function CameraControls({
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ paddingBottom: insets.bottom + 10 }}
|
||||
className="flex-row items-center px-7 pt-2"
|
||||
>
|
||||
{/* Left cluster: Pick Image + gear (gear sits between library & shutter).
|
||||
flex-1 side zones are equal width, so the shutter between them is
|
||||
exactly centered on the screen regardless of button counts. */}
|
||||
<View className="flex-1 flex-row items-center gap-x-3">
|
||||
<TouchableOpacity
|
||||
onPress={onPickImage}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<ImageIcon size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onOpenSettings}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Settings size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Dynamic Center Button: Shutter or Save */}
|
||||
<TouchableOpacity
|
||||
onPress={onCapture}
|
||||
// Pressability drops onPress once onLongPress has fired, so releasing a
|
||||
// hold never appends a stray single shot after the burst.
|
||||
onLongPress={onBurstStart}
|
||||
onPressOut={onBurstStop}
|
||||
delayLongPress={300}
|
||||
className="relative items-center justify-center"
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLibraryMode ? (
|
||||
<View className="w-20 h-20 rounded-full bg-black/25 items-center justify-center">
|
||||
<View className="w-16 h-16 rounded-full bg-amber-500 items-center justify-center border-2 border-black/20">
|
||||
<Download size={28} color="#000000" />
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-20 h-20 rounded-full bg-black/25 items-center justify-center">
|
||||
<View className="w-14 h-14 rounded-full bg-white items-center justify-center border-2 border-black/10">
|
||||
<View className="w-11 h-11 rounded-full bg-zinc-100 border border-zinc-300" />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Right cluster: flip camera (between shutter & preview) + preview.
|
||||
Preview doubles as "back to camera" while in library mode; the flip
|
||||
control is hidden then. Anchored to the right edge inside the flex-1
|
||||
side zone; small gap keeps it clear of the preview thumbnail without
|
||||
drifting toward the shutter.
|
||||
|
||||
Exports land in the session library, so once library mode has one its
|
||||
thumbnail takes the slot the flip control would have used — that keeps
|
||||
"back to camera" untouched while the finished photo stays reachable
|
||||
(review or trash) after the auto-opened viewer is closed. */}
|
||||
<View className="flex-1 flex-row items-center justify-end gap-x-3">
|
||||
{isLibraryMode ? (
|
||||
lastPhotoUri ? (
|
||||
<TouchableOpacity
|
||||
onPress={onOpenPreview}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center overflow-hidden"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Image source={{ uri: lastPhotoUri }} style={StyleSheet.absoluteFill} className="object-cover" />
|
||||
</TouchableOpacity>
|
||||
) : null
|
||||
) : onFlipCamera ? (
|
||||
<View>
|
||||
{/* Capture-time LED flash: a button of its own above the control row
|
||||
rather than a third button inside it. The two side zones are sized to
|
||||
hold two 48dp buttons next to the shutter — a third would slide under
|
||||
the shutter's hit area on a 360dp screen. Absent on a lens with no
|
||||
flash unit, so the front camera shows no dead control. */}
|
||||
{flashMode && onCycleFlash ? (
|
||||
<View className="px-7 pb-3">
|
||||
<TouchableOpacity
|
||||
onPress={onFlipCamera}
|
||||
onPress={onCycleFlash}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Flash ${flashMode}`}
|
||||
accessibilityHint="Cycles flash off, auto and on"
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<RefreshCw size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
{flashMode === 'off' ? (
|
||||
<ZapOff size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
) : (
|
||||
// AUTO keeps the bolt dim and adds the badge below — a lit bolt
|
||||
// alone would read as ON.
|
||||
<Zap size={20} color={flashMode === 'on' ? '#fbbf24' : '#e4e4e7'} strokeWidth={1.75} />
|
||||
)}
|
||||
{flashMode === 'auto' ? (
|
||||
<Text style={FLASH_A_BADGE}>A</Text>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{ paddingBottom: insets.bottom + 10 }}
|
||||
className="flex-row items-center px-7 pt-2"
|
||||
>
|
||||
{/* Left cluster: Pick Image + gear (gear sits between library & shutter).
|
||||
flex-1 side zones are equal width, so the shutter between them is
|
||||
exactly centered on the screen regardless of button counts. */}
|
||||
<View className="flex-1 flex-row items-center gap-x-3">
|
||||
<TouchableOpacity
|
||||
onPress={onPickImage}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<ImageIcon size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onOpenSettings}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Settings size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
</TouchableOpacity>
|
||||
|
||||
</View>
|
||||
|
||||
{/* Dynamic Center Button: Shutter or Save */}
|
||||
<TouchableOpacity
|
||||
onPress={isLibraryMode && onSwitchToCamera ? onSwitchToCamera : onOpenPreview}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center overflow-hidden"
|
||||
activeOpacity={0.7}
|
||||
onPress={onCapture}
|
||||
// Pressability drops onPress once onLongPress has fired, so releasing a
|
||||
// hold never appends a stray single shot after the burst.
|
||||
onLongPress={onBurstStart}
|
||||
onPressOut={onBurstStop}
|
||||
delayLongPress={300}
|
||||
className="relative items-center justify-center"
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLibraryMode ? (
|
||||
<Camera size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
) : lastPhotoUri ? (
|
||||
<Image source={{ uri: lastPhotoUri }} style={StyleSheet.absoluteFill} className="object-cover" />
|
||||
<View className="w-20 h-20 rounded-full bg-black/25 items-center justify-center">
|
||||
<View className="w-16 h-16 rounded-full bg-amber-500 items-center justify-center border-2 border-black/20">
|
||||
<Download size={28} color="#000000" />
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Eye size={20} color="#a1a1aa" strokeWidth={1.75} />
|
||||
<View className="w-20 h-20 rounded-full bg-black/25 items-center justify-center">
|
||||
<View className="w-14 h-14 rounded-full bg-white items-center justify-center border-2 border-black/10">
|
||||
<View className="w-11 h-11 rounded-full bg-zinc-100 border border-zinc-300" />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Right cluster: flip camera (between shutter & preview) + preview.
|
||||
Preview doubles as "back to camera" while in library mode; the flip
|
||||
control is hidden then. Anchored to the right edge inside the flex-1
|
||||
side zone; small gap keeps it clear of the preview thumbnail without
|
||||
drifting toward the shutter.
|
||||
|
||||
Exports land in the session library, so once library mode has one its
|
||||
thumbnail takes the slot the flip control would have used — that keeps
|
||||
"back to camera" untouched while the finished photo stays reachable
|
||||
(review or trash) after the auto-opened viewer is closed. */}
|
||||
<View className="flex-1 flex-row items-center justify-end gap-x-3">
|
||||
{isLibraryMode ? (
|
||||
lastPhotoUri ? (
|
||||
<TouchableOpacity
|
||||
onPress={onOpenPreview}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center overflow-hidden"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Image source={{ uri: lastPhotoUri }} style={StyleSheet.absoluteFill} className="object-cover" />
|
||||
</TouchableOpacity>
|
||||
) : null
|
||||
) : onFlipCamera ? (
|
||||
<TouchableOpacity
|
||||
onPress={onFlipCamera}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<RefreshCw size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
onPress={isLibraryMode && onSwitchToCamera ? onSwitchToCamera : onOpenPreview}
|
||||
className="w-12 h-12 rounded-full bg-black/40 border border-zinc-700/70 flex items-center justify-center overflow-hidden"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isLibraryMode ? (
|
||||
<Camera size={20} color="#e4e4e7" strokeWidth={1.75} />
|
||||
) : lastPhotoUri ? (
|
||||
<Image source={{ uri: lastPhotoUri }} style={StyleSheet.absoluteFill} className="object-cover" />
|
||||
) : (
|
||||
<Eye size={20} color="#a1a1aa" strokeWidth={1.75} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user