diff --git a/package.json b/package.json index dbda488..dd54c9d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.0.68", + "version": "1.0.69", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/src/lib/quickCapture.ts b/src/lib/quickCapture.ts new file mode 100644 index 0000000..16a2a50 --- /dev/null +++ b/src/lib/quickCapture.ts @@ -0,0 +1,70 @@ +/** + * 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 { + 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' }); +} diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 7bb8c85..a56257e 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -81,6 +81,7 @@ import { SpotChat } from '@/components/SpotChat'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; +import { isNativeCapture, captureFromNativeCamera } from '@/lib/quickCapture'; type ViewMode = 'map' | 'list'; @@ -604,10 +605,10 @@ export default function MapPage() { // Mobile layers popover const [layersOpen, setLayersOpen] = useState(false); - // Quick Snap — uses the native OS camera/gallery picker via a file input. - // This is more robust than a custom in-app camera (no Capacitor camera - // permission plumbing) and works identically on web and native. A single - // photo is all a spot snap needs. + // Quick Snap — camera-first. On native (the APK) we open the OS camera + // directly via the runtime Camera plugin (with its built-in preview/retake). + // On web there's no reliable way to force the camera from JS, so we fall back + // to a file input hinting the rear camera. A single photo is all a spot needs. const quickSnapInputRef = useRef(null); const { mutateAsync: uploadFile, isPending: isUploadingQuickPhoto } = useUploadFile(); const { toast } = useToast(); @@ -762,7 +763,27 @@ export default function MapPage() { navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}`); }; - // Native file-input change → hand the chosen photo to the upload flow. + // Quick Snap entry point — camera-first on native, file input on web. + const handleQuickSnap = async () => { + setAddDialOpen(false); + if (isNativeCapture()) { + try { + const file = await captureFromNativeCamera(); + if (file) handleQuickCameraCapture(file); + return; // handled natively + } catch (err) { + console.warn('[QuickSnap] native camera failed, falling back to file input:', err); + toast({ + title: 'Camera unavailable', + description: 'Opening the photo picker instead.', + }); + // fall through to the web/file-input path below + } + } + quickSnapInputRef.current?.click(); + }; + + // Web file-input change → hand the chosen photo to the upload flow. const handleQuickSnapInput = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; // Reset the input so picking the same file again still fires onChange. @@ -1709,9 +1730,9 @@ export default function MapPage() { ))} - {/* Quick Snap — opens the native camera/gallery picker */} + {/* Quick Snap — camera-first (native) / file-input (web) */}