From 2c84bb50b36ee52db97c120e63463b4d579f7644 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 6 Apr 2025 13:04:26 +0000 Subject: [PATCH] **refactor(PhotoDialog, PlatformService): Implement cross-platform photo capture and encapsulated image processing** - Replace direct camera library with platform-agnostic `PlatformService` - Move platform-specific image processing logic to respective platform implementations - Introduce `ImageResult` interface for consistent image handling across platforms - Add support for native camera and image picker across all platforms - Simplify `PhotoDialog` by removing platform-specific logic - Maintain existing cropping and upload functionality - Improve error handling and logging throughout - Clean up UI for better user experience - Add comprehensive documentation for usage and architecture **BREAKING CHANGE:** Removes direct camera library dependency in favor of `PlatformService` This change improves separation of concerns, enhances maintainability, and standardizes cross-platform image handling. --- .../buildOutputCleanup.lock | Bin 17 -> 17 bytes android/.gradle/file-system.probe | Bin 8 -> 8 bytes src/components/PhotoDialog.vue | 250 ++++-------------- src/services/PlatformService.ts | 9 +- .../platforms/CapacitorPlatformService.ts | 75 ++++-- .../platforms/ElectronPlatformService.ts | 18 +- .../platforms/PyWebViewPlatformService.ts | 18 +- src/services/platforms/WebPlatformService.ts | 67 +++-- 8 files changed, 187 insertions(+), 250 deletions(-) diff --git a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 6f5da8a4ca559c926011acca45cde951d01726cd..c730091308eb1abd471b4912b5ddddd035b1516a 100644 GIT binary patch literal 17 VcmZQ>duy#wxduy#wx( diff --git a/android/.gradle/file-system.probe b/android/.gradle/file-system.probe index d0da61435c89283dd5c3fdbf3e2a207341b09998..d83886692946aaa057ab4c5c6541d6109500ca1a 100644 GIT binary patch literal 8 PcmZQzV4TMF;!z0z2owU~ literal 8 PcmZQzV4S*rmY^E|2nhl| diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index 76fd948a..9ffba49e 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -40,11 +40,6 @@ }" class="max-h-[90vh] max-w-[90vw] object-contain" /> -
@@ -74,87 +69,67 @@
-
- - -
+
+ -
-
+ + -
-
- -
- + + +
@@ -438,12 +304,4 @@ export default class PhotoDialog extends Vue { width: 100%; max-width: 700px; } - -.mirror-video { - transform: scaleX(-1); - -webkit-transform: scaleX(-1); /* For Safari */ - -moz-transform: scaleX(-1); /* For Firefox */ - -ms-transform: scaleX(-1); /* For IE */ - -o-transform: scaleX(-1); /* For Opera */ -} diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 8cb6c8e4..ab3c0a60 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -1,3 +1,8 @@ +export interface ImageResult { + blob: Blob; + fileName: string; +} + export interface PlatformService { // File system operations readFile(path: string): Promise; @@ -6,8 +11,8 @@ export interface PlatformService { listFiles(directory: string): Promise; // Camera operations - takePicture(): Promise; - pickImage(): Promise; + takePicture(): Promise; + pickImage(): Promise; // Platform specific features isCapacitor(): boolean; diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index ae1aa2ab..ad232f3a 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -1,8 +1,9 @@ -import { PlatformService } from "../PlatformService"; +import { ImageResult, PlatformService } from "../PlatformService"; import { Capacitor } from "@capacitor/core"; import { Filesystem, Directory } from "@capacitor/filesystem"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { App } from "@capacitor/app"; +import { logger } from "../../utils/logger"; export class CapacitorPlatformService implements PlatformService { async readFile(path: string): Promise { @@ -36,24 +37,64 @@ export class CapacitorPlatformService implements PlatformService { return result.files; } - async takePicture(): Promise { - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: true, - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - }); - return image.webPath || ""; + async takePicture(): Promise { + try { + const image = await Camera.getPhoto({ + quality: 90, + allowEditing: true, + resultType: CameraResultType.Base64, + source: CameraSource.Camera, + }); + + const blob = await this.processImageData(image.base64String); + return { + blob, + fileName: `photo_${Date.now()}.${image.format || 'jpg'}` + }; + } catch (error) { + logger.error("Error taking picture with Capacitor:", error); + throw new Error("Failed to take picture"); + } } - async pickImage(): Promise { - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: true, - resultType: CameraResultType.Uri, - source: CameraSource.Photos, - }); - return image.webPath || ""; + async pickImage(): Promise { + try { + const image = await Camera.getPhoto({ + quality: 90, + allowEditing: true, + resultType: CameraResultType.Base64, + source: CameraSource.Photos, + }); + + const blob = await this.processImageData(image.base64String); + return { + blob, + fileName: `photo_${Date.now()}.${image.format || 'jpg'}` + }; + } catch (error) { + logger.error("Error picking image with Capacitor:", error); + throw new Error("Failed to pick image"); + } + } + + private async processImageData(base64String?: string): Promise { + if (!base64String) { + throw new Error("No image data received"); + } + + // Convert base64 to blob + const byteCharacters = atob(base64String); + const byteArrays = []; + for (let offset = 0; offset < byteCharacters.length; offset += 512) { + const slice = byteCharacters.slice(offset, offset + 512); + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + return new Blob(byteArrays, { type: 'image/jpeg' }); } isCapacitor(): boolean { diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 8595d390..00d41174 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -1,28 +1,28 @@ -import { PlatformService } from '../PlatformService'; +import { PlatformService } from "../PlatformService"; export class ElectronPlatformService implements PlatformService { async readFile(path: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async writeFile(path: string, content: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async deleteFile(path: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async listFiles(directory: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async takePicture(): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async pickImage(): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } isCapacitor(): boolean { @@ -42,6 +42,6 @@ export class ElectronPlatformService implements PlatformService { } async handleDeepLink(url: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } -} \ No newline at end of file +} diff --git a/src/services/platforms/PyWebViewPlatformService.ts b/src/services/platforms/PyWebViewPlatformService.ts index 4d10a285..907241fe 100644 --- a/src/services/platforms/PyWebViewPlatformService.ts +++ b/src/services/platforms/PyWebViewPlatformService.ts @@ -1,28 +1,28 @@ -import { PlatformService } from '../PlatformService'; +import { PlatformService } from "../PlatformService"; export class PyWebViewPlatformService implements PlatformService { async readFile(path: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async writeFile(path: string, content: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async deleteFile(path: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async listFiles(directory: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async takePicture(): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } async pickImage(): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } isCapacitor(): boolean { @@ -42,6 +42,6 @@ export class PyWebViewPlatformService implements PlatformService { } async handleDeepLink(url: string): Promise { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } -} \ No newline at end of file +} diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index befb7cbc..9ae1de83 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -1,4 +1,5 @@ -import { PlatformService } from "../PlatformService"; +import { ImageResult, PlatformService } from "../PlatformService"; +import { logger } from "../../utils/logger"; export class WebPlatformService implements PlatformService { async readFile(path: string): Promise { @@ -17,23 +18,28 @@ export class WebPlatformService implements PlatformService { throw new Error("File system access not available in web platform"); } - async takePicture(): Promise { + async takePicture(): Promise { return new Promise((resolve, reject) => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.capture = "environment"; - input.onchange = (e) => { + input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { - const reader = new FileReader(); - reader.onload = (event) => { - resolve(event.target?.result as string); - }; - reader.readAsDataURL(file); + try { + const blob = await this.processImageFile(file); + resolve({ + blob, + fileName: file.name || "photo.jpg" + }); + } catch (error) { + logger.error("Error processing camera image:", error); + reject(new Error("Failed to process camera image")); + } } else { - reject(new Error("No file selected")); + reject(new Error("No image captured")); } }; @@ -41,22 +47,27 @@ export class WebPlatformService implements PlatformService { }); } - async pickImage(): Promise { + async pickImage(): Promise { return new Promise((resolve, reject) => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; - input.onchange = (e) => { + input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { - const reader = new FileReader(); - reader.onload = (event) => { - resolve(event.target?.result as string); - }; - reader.readAsDataURL(file); + try { + const blob = await this.processImageFile(file); + resolve({ + blob, + fileName: file.name || "photo.jpg" + }); + } catch (error) { + logger.error("Error processing picked image:", error); + reject(new Error("Failed to process picked image")); + } } else { - reject(new Error("No file selected")); + reject(new Error("No image selected")); } }; @@ -64,6 +75,28 @@ export class WebPlatformService implements PlatformService { }); } + private async processImageFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + const dataUrl = event.target?.result as string; + // Convert to blob to ensure consistent format + fetch(dataUrl) + .then(res => res.blob()) + .then(blob => resolve(blob)) + .catch(error => { + logger.error("Error converting data URL to blob:", error); + reject(error); + }); + }; + reader.onerror = (error) => { + logger.error("Error reading file:", error); + reject(error); + }; + reader.readAsDataURL(file); + }); + } + isCapacitor(): boolean { return false; }