SkateSpots/src/lib/quickCapture.ts
shakespeare.diy 6d0480961c Quick Snap: camera-first on native (runtime Camera plugin), file input on web
The previous file-input-only approach fixed the crash but opened the gallery
on native because capture="environment" is only a hint the OS can ignore.

New platform-aware flow (lib/quickCapture.ts):
- Native (APK): open the OS camera directly via the runtime-injected
  window.Capacitor.Plugins.Camera with source:'CAMERA' — forces the real
  camera and gives built-in preview/retake. Accessed off the runtime object,
  so no '@capacitor/camera' specifier in the bundle (keeps the white-screen
  fix intact). User cancel is treated as a no-op.
- Web: falls back to the file input with capture="environment".
- If the native camera errors, gracefully falls back to the file input.

MapPage Quick Snap now routes through handleQuickSnap() → the captured photo
feeds the existing upload → create-spot flow unchanged. Bump 1.0.69.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 19:38:46 +07:00

70 lines
2.5 KiB
TypeScript

/**
* Quick Snap capture helper — platform-aware, camera-first.
*
* NATIVE (Capacitor APK): uses the runtime-injected Camera plugin
* (window.Capacitor.Plugins.Camera) with source:'CAMERA', which forces the
* real OS camera and ships its own preview + retake UI. We read the plugin
* off the runtime object rather than importing '@capacitor/camera', so no
* bare specifier ends up in the bundle (that was the white-screen cause).
*
* WEB: there is no reliable way to force the camera from JS, so the caller
* falls back to a file input with capture="environment" (a hint the browser
* may honor). This module only handles the native path; isNativeCapture()
* tells the caller which route to take.
*/
interface CapacitorCameraPlugin {
getPhoto(options: {
resultType: string;
source: string;
quality: number;
correctOrientation: boolean;
}): Promise<{ dataUrl?: string }>;
}
function getNativeCamera(): CapacitorCameraPlugin | null {
const cap = (window as unknown as {
Capacitor?: {
isNativePlatform?: () => boolean;
Plugins?: { Camera?: CapacitorCameraPlugin };
};
}).Capacitor;
if (!cap?.isNativePlatform?.()) return null;
return cap.Plugins?.Camera ?? null;
}
/** True when the native Camera plugin is available (i.e. inside the app). */
export function isNativeCapture(): boolean {
return getNativeCamera() !== null;
}
/**
* Open the native camera and return the captured photo as a File.
* Returns null if the user cancels. Throws if the plugin is unavailable or
* errors (caller should fall back to the web file input).
*/
export async function captureFromNativeCamera(): Promise<File | null> {
const Camera = getNativeCamera();
if (!Camera) throw new Error('Native camera unavailable');
let photo: { dataUrl?: string };
try {
photo = await Camera.getPhoto({
resultType: 'dataUrl', // CameraResultType.DataUrl
source: 'CAMERA', // CameraSource.Camera — forces the camera, not the gallery
quality: 90,
correctOrientation: true,
});
} catch (err) {
// Capacitor throws with a "cancel" message when the user backs out — treat
// that as a benign null rather than an error.
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
if (msg.includes('cancel')) return null;
throw err;
}
if (!photo.dataUrl) return null;
const res = await fetch(photo.dataUrl);
const blob = await res.blob();
return new File([blob], `skate-spot-${Date.now()}.jpg`, { type: 'image/jpeg' });
}