Migrate from .NET MAUI to Expo + Convex
Complete rewrite of Scry using TypeScript stack:
- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)
Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support
Removes all .NET/MAUI code (src/, test/, tools/).
💘 Generated with Crush
Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
This commit is contained in:
parent
56499d5af9
commit
83ab4df537
138 changed files with 19136 additions and 7681 deletions
48
components/camera/ExpoCamera.tsx
Normal file
48
components/camera/ExpoCamera.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Camera component using expo-camera (works in Expo Go).
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { CameraView } from "expo-camera";
|
||||
|
||||
export interface CameraHandle {
|
||||
takePhoto: () => Promise<{ uri: string }>;
|
||||
}
|
||||
|
||||
interface ExpoCameraProps {
|
||||
flashEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const ExpoCamera = forwardRef<CameraHandle, ExpoCameraProps>(
|
||||
({ flashEnabled = false }, ref) => {
|
||||
const cameraRef = useRef<CameraView>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
takePhoto: async () => {
|
||||
if (!cameraRef.current) {
|
||||
throw new Error("Camera not ready");
|
||||
}
|
||||
const photo = await cameraRef.current.takePictureAsync({
|
||||
quality: 0.8,
|
||||
base64: false,
|
||||
});
|
||||
if (!photo) {
|
||||
throw new Error("Failed to capture photo");
|
||||
}
|
||||
return { uri: photo.uri };
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<CameraView
|
||||
ref={cameraRef}
|
||||
style={StyleSheet.absoluteFill}
|
||||
facing="back"
|
||||
flash={flashEnabled ? "on" : "off"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExpoCamera.displayName = "ExpoCamera";
|
||||
53
components/camera/VisionCamera.tsx
Normal file
53
components/camera/VisionCamera.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Camera component using react-native-vision-camera (production builds).
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Camera, useCameraDevice } from "react-native-vision-camera";
|
||||
|
||||
export interface CameraHandle {
|
||||
takePhoto: () => Promise<{ uri: string }>;
|
||||
}
|
||||
|
||||
interface VisionCameraProps {
|
||||
flashEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const VisionCamera = forwardRef<CameraHandle, VisionCameraProps>(
|
||||
({ flashEnabled = false }, ref) => {
|
||||
const device = useCameraDevice("back");
|
||||
const cameraRef = useRef<Camera>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
takePhoto: async () => {
|
||||
if (!cameraRef.current) {
|
||||
throw new Error("Camera not ready");
|
||||
}
|
||||
const photo = await cameraRef.current.takePhoto({
|
||||
flash: flashEnabled ? "on" : "off",
|
||||
enableShutterSound: false,
|
||||
});
|
||||
return { uri: photo.path };
|
||||
},
|
||||
}));
|
||||
|
||||
if (!device) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
style={StyleSheet.absoluteFill}
|
||||
device={device}
|
||||
isActive={true}
|
||||
photo={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
VisionCamera.displayName = "VisionCamera";
|
||||
|
||||
export { useCameraDevice };
|
||||
64
components/camera/index.tsx
Normal file
64
components/camera/index.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Adaptive camera component that uses expo-camera in Expo Go
|
||||
* and react-native-vision-camera in production builds.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, lazy, Suspense } from "react";
|
||||
import { View, ActivityIndicator, StyleSheet, Text } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
export interface CameraHandle {
|
||||
takePhoto: () => Promise<{ uri: string }>;
|
||||
}
|
||||
|
||||
interface AdaptiveCameraProps {
|
||||
flashEnabled?: boolean;
|
||||
}
|
||||
|
||||
// Detect if running in Expo Go
|
||||
const isExpoGo = Constants.appOwnership === "expo";
|
||||
|
||||
// Lazy load the appropriate camera component
|
||||
const ExpoCamera = lazy(() =>
|
||||
import("./ExpoCamera").then((m) => ({ default: m.ExpoCamera }))
|
||||
);
|
||||
const VisionCamera = lazy(() =>
|
||||
import("./VisionCamera").then((m) => ({ default: m.VisionCamera }))
|
||||
);
|
||||
|
||||
const CameraLoading = () => (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<Text style={styles.loadingText}>Initializing camera...</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const AdaptiveCamera = forwardRef<CameraHandle, AdaptiveCameraProps>(
|
||||
(props, ref) => {
|
||||
const CameraComponent = isExpoGo ? ExpoCamera : VisionCamera;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<CameraLoading />}>
|
||||
<CameraComponent ref={ref} {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AdaptiveCamera.displayName = "AdaptiveCamera";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loading: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: "#000",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
color: "#888",
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export { isExpoGo };
|
||||
Loading…
Add table
Add a link
Reference in a new issue