Files
solelog/apps/mobile/polyfills/web/camera.web.tsx
Bas van Rossem d94d0b188b Initial commit: code as received (Create/Anything export)
Insole-production time tracker exported from the Create/Anything AI
platform. Baseline snapshot before any reverse-engineering or cleanup.

- apps/mobile: Expo Router app (iOS/Android/web), the only workspace
- publisher/: standalone OpenNext/AWS deploy tooling for the web side
- Backend (/api/tasks, /api/logs + DB) lives remotely, not in this repo
2026-06-17 10:19:33 +02:00

242 lines
5.9 KiB
TypeScript

import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { View, Text, StyleSheet, type ViewStyle } from 'react-native';
export enum CameraType {
front = 'front',
back = 'back',
}
export enum FlashMode {
off = 'off',
on = 'on',
auto = 'auto',
torch = 'torch',
}
export enum CameraMode {
picture = 'picture',
video = 'video',
}
interface CameraViewProps {
style?: ViewStyle;
facing?: 'front' | 'back';
flash?: 'off' | 'on' | 'auto';
mode?: 'picture' | 'video';
zoom?: number;
enableTorch?: boolean;
onCameraReady?: () => void;
onMountError?: (event: { message: string }) => void;
children?: React.ReactNode;
}
interface CameraViewRef {
takePictureAsync: (options?: {
quality?: number;
base64?: boolean;
}) => Promise<{ uri: string; width: number; height: number; base64?: string }>;
recordAsync: (options?: {
maxDuration?: number;
}) => Promise<{ uri: string }>;
stopRecording: () => void;
}
export const CameraView = forwardRef<CameraViewRef, CameraViewProps>(
function CameraView(
{ style, facing = 'back', onCameraReady, onMountError, children },
ref
) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const [hasCamera, setHasCamera] = useState(true);
const startCamera = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: facing === 'front' ? 'user' : 'environment',
},
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
onCameraReady?.();
} catch (err) {
setHasCamera(false);
onMountError?.({
message:
err instanceof Error ? err.message : 'Camera not available',
});
}
}, [facing, onCameraReady, onMountError]);
useEffect(() => {
void startCamera();
return () => {
streamRef.current?.getTracks().forEach((t) => t.stop());
streamRef.current = null;
};
}, [startCamera]);
useImperativeHandle(ref, () => ({
takePictureAsync: async (options) => {
const video = videoRef.current;
if (!video || !streamRef.current) {
throw new Error('Camera is not ready');
}
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(video, 0, 0);
const quality = options?.quality ?? 0.85;
const dataUrl = canvas.toDataURL('image/jpeg', quality);
const result: {
uri: string;
width: number;
height: number;
base64?: string;
} = {
uri: dataUrl,
width: canvas.width,
height: canvas.height,
};
if (options?.base64) {
result.base64 = dataUrl.split(',')[1];
}
return result;
},
recordAsync: async () => {
throw new Error('Video recording is not supported in web preview');
},
stopRecording: () => {},
}));
if (!hasCamera) {
return (
<View style={[styles.container, style]}>
<View style={styles.placeholder}>
<Text style={styles.placeholderText}>Camera</Text>
<Text style={styles.placeholderSubtext}>
Not available in this browser
</Text>
</View>
{children}
</View>
);
}
return (
<View style={[styles.container, style]}>
<video
ref={videoRef}
autoPlay
playsInline
muted
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
{children}
</View>
);
}
);
const grantedPermission = {
status: 'granted' as const,
granted: true,
canAskAgain: true,
expires: 'never' as const,
};
const deniedPermission = {
status: 'denied' as const,
granted: false,
canAskAgain: true,
expires: 'never' as const,
};
export function useCameraPermissions(): [
{ status: string; granted: boolean; canAskAgain: boolean } | null,
() => Promise<{ status: string; granted: boolean; canAskAgain: boolean }>,
] {
const [permission, setPermission] = useState<{
status: string;
granted: boolean;
canAskAgain: boolean;
} | null>(null);
const requestPermission = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach((t) => t.stop());
setPermission(grantedPermission);
return grantedPermission;
} catch {
setPermission(deniedPermission);
return deniedPermission;
}
}, []);
return [permission, requestPermission];
}
export async function requestCameraPermissionsAsync() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach((t) => t.stop());
return grantedPermission;
} catch {
return deniedPermission;
}
}
export async function getCameraPermissionsAsync() {
try {
const result = await navigator.permissions.query({
name: 'camera' as PermissionName,
});
return result.state === 'granted' ? grantedPermission : deniedPermission;
} catch {
return deniedPermission;
}
}
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
backgroundColor: '#000',
},
placeholder: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1a1a1a',
},
placeholderText: {
color: '#999',
fontSize: 18,
fontWeight: '600',
marginBottom: 4,
},
placeholderSubtext: {
color: '#666',
fontSize: 13,
},
});