forked from trent_larson/crowd-funder-for-time-pwa
**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.
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -40,11 +40,6 @@
|
||||
}"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
/>
|
||||
<!-- This gives a round cropper.
|
||||
:presetMode="{
|
||||
mode: 'round',
|
||||
}"
|
||||
-->
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex justify-center">
|
||||
@@ -74,87 +69,67 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="cameraContainer">
|
||||
<!--
|
||||
Camera "resolution" doesn't change how it shows on screen but rather stretches the result,
|
||||
eg. the following which just stretches it vertically:
|
||||
:resolution="{ width: 375, height: 812 }"
|
||||
-->
|
||||
<camera
|
||||
ref="camera"
|
||||
facing-mode="environment"
|
||||
autoplay
|
||||
@started="cameraStarted()"
|
||||
>
|
||||
<div
|
||||
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
|
||||
<div v-else>
|
||||
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="takePhoto"
|
||||
>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="takeImage()"
|
||||
>
|
||||
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
|
||||
<font-awesome icon="camera" class="w-[1em]" />
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="pickPhoto"
|
||||
>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="swapMirrorClass()"
|
||||
>
|
||||
<font-awesome icon="left-right" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="switchCamera()"
|
||||
>
|
||||
<font-awesome icon="rotate" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</camera>
|
||||
<font-awesome icon="image" class="w-[1em]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* PhotoDialog.vue - Cross-platform photo capture and selection component
|
||||
*
|
||||
* This component provides a unified interface for taking photos and selecting images
|
||||
* across different platforms using the PlatformService.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @file PhotoDialog.vue
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
|
||||
@Component({ components: { Camera, VuePictureCropper } })
|
||||
@Component({ components: { VuePictureCropper } })
|
||||
export default class PhotoDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
activeDeviceNumber = 0;
|
||||
activeDid = "";
|
||||
blob?: Blob;
|
||||
claimType = "";
|
||||
crop = false;
|
||||
fileName?: string;
|
||||
mirror = false;
|
||||
numDevices = 0;
|
||||
setImageCallback: (arg: string) => void = () => {};
|
||||
showRetry = true;
|
||||
uploading = false;
|
||||
visible = false;
|
||||
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
URL = window.URL || window.webkitURL;
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
@@ -173,7 +148,7 @@ export default class PhotoDialog extends Vue {
|
||||
setImageFn: (arg: string) => void,
|
||||
claimType: string,
|
||||
crop?: boolean,
|
||||
blob?: Blob, // for image upload, just to use the cropping function
|
||||
blob?: Blob,
|
||||
inputFileName?: string,
|
||||
) {
|
||||
this.visible = true;
|
||||
@@ -187,7 +162,6 @@ export default class PhotoDialog extends Vue {
|
||||
if (blob) {
|
||||
this.blob = blob;
|
||||
this.fileName = inputFileName;
|
||||
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
|
||||
this.showRetry = false;
|
||||
} else {
|
||||
this.blob = undefined;
|
||||
@@ -205,85 +179,35 @@ export default class PhotoDialog extends Vue {
|
||||
this.blob = undefined;
|
||||
}
|
||||
|
||||
async cameraStarted() {
|
||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||
if (cameraComponent) {
|
||||
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
|
||||
this.mirror = cameraComponent.facingMode === "user";
|
||||
// figure out which device is active
|
||||
const currentDeviceId = cameraComponent.currentDeviceID();
|
||||
const devices = await cameraComponent.devices(["videoinput"]);
|
||||
this.activeDeviceNumber = devices.findIndex(
|
||||
(device) => device.deviceId === currentDeviceId,
|
||||
);
|
||||
async takePhoto() {
|
||||
try {
|
||||
const result = await this.platformService.takePicture();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to take picture. Please try again.",
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async switchCamera() {
|
||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
|
||||
const devices = await cameraComponent?.devices(["videoinput"]);
|
||||
await cameraComponent?.changeCamera(
|
||||
devices[this.activeDeviceNumber].deviceId,
|
||||
);
|
||||
}
|
||||
|
||||
async takeImage(/* payload: MouseEvent */) {
|
||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||
|
||||
/**
|
||||
* This logic to set the image height & width correctly.
|
||||
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
|
||||
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
|
||||
* Now that I've done it, I can't explain why it works.
|
||||
*/
|
||||
let imageHeight = cameraComponent?.resolution?.height;
|
||||
let imageWidth = cameraComponent?.resolution?.width;
|
||||
const initialImageRatio = imageWidth / imageHeight;
|
||||
const windowRatio = window.innerWidth / window.innerHeight;
|
||||
if (initialImageRatio > 1 && windowRatio < 1) {
|
||||
// the image is wider than it is tall, and the window is taller than it is wide
|
||||
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
|
||||
// We're gonna force it opposite.
|
||||
imageHeight = cameraComponent?.resolution?.width;
|
||||
imageWidth = cameraComponent?.resolution?.height;
|
||||
} else if (initialImageRatio < 1 && windowRatio > 1) {
|
||||
// the image is taller than it is wide, and the window is wider than it is tall
|
||||
// Haven't seen this happen, but we'll do it just in case.
|
||||
imageHeight = cameraComponent?.resolution?.width;
|
||||
imageWidth = cameraComponent?.resolution?.height;
|
||||
}
|
||||
const newImageRatio = imageWidth / imageHeight;
|
||||
if (newImageRatio < windowRatio) {
|
||||
// the image is a taller ratio than the window, so fit the height first
|
||||
imageHeight = window.innerHeight / 2;
|
||||
imageWidth = imageHeight * newImageRatio;
|
||||
} else {
|
||||
// the image is a wider ratio than the window, so fit the width first
|
||||
imageWidth = window.innerWidth / 2;
|
||||
imageHeight = imageWidth / newImageRatio;
|
||||
}
|
||||
|
||||
// The resolution is only necessary because of that mobile portrait-orientation case.
|
||||
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
|
||||
this.blob =
|
||||
(await cameraComponent?.snapshot({
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
})) || undefined;
|
||||
// png is default
|
||||
this.fileName = "snapshot.png";
|
||||
if (!this.blob) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error taking the picture. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
return;
|
||||
async pickPhoto() {
|
||||
try {
|
||||
const result = await this.platformService.pickImage();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
} catch (error) {
|
||||
logger.error("Error picking image:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to pick image. Please try again.",
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,51 +219,6 @@ export default class PhotoDialog extends Vue {
|
||||
this.blob = undefined;
|
||||
}
|
||||
|
||||
/****
|
||||
|
||||
Here's an approach to photo capture without a library. It has similar quirks.
|
||||
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
|
||||
|
||||
<button id="start-camera" @click="cameraClicked">Start Camera</button>
|
||||
<video id="video" width="320" height="240" autoplay></video>
|
||||
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
|
||||
<canvas id="canvas" width="320" height="240"></canvas>
|
||||
|
||||
async cameraClicked() {
|
||||
const video = document.querySelector("#video");
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false,
|
||||
});
|
||||
if (video instanceof HTMLVideoElement) {
|
||||
video.srcObject = stream;
|
||||
}
|
||||
}
|
||||
photoSnapped() {
|
||||
const video = document.querySelector("#video");
|
||||
const canvas = document.querySelector("#canvas");
|
||||
if (
|
||||
canvas instanceof HTMLCanvasElement &&
|
||||
video instanceof HTMLVideoElement
|
||||
) {
|
||||
canvas
|
||||
?.getContext("2d")
|
||||
?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
// ... or set the blob:
|
||||
// canvas?.toBlob(
|
||||
// (blob) => {
|
||||
// this.blob = blob;
|
||||
// },
|
||||
// "image/jpeg",
|
||||
// 1,
|
||||
// );
|
||||
|
||||
// data url of the image
|
||||
const image_data_url = canvas?.toDataURL("image/jpeg");
|
||||
}
|
||||
}
|
||||
****/
|
||||
|
||||
async uploadImage() {
|
||||
this.uploading = true;
|
||||
|
||||
@@ -350,11 +229,9 @@ export default class PhotoDialog extends Vue {
|
||||
const token = await accessToken(this.activeDid);
|
||||
const headers = {
|
||||
Authorization: "Bearer " + token,
|
||||
// axios fills in Content-Type of multipart/form-data
|
||||
};
|
||||
const formData = new FormData();
|
||||
if (!this.blob) {
|
||||
// yeah, this should never happen, but it helps with subsequent type checking
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -367,7 +244,7 @@ export default class PhotoDialog extends Vue {
|
||||
this.uploading = false;
|
||||
return;
|
||||
}
|
||||
formData.append("image", this.blob, this.fileName || "snapshot.png");
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
formData.append("claimType", this.claimType);
|
||||
try {
|
||||
if (
|
||||
@@ -402,17 +279,6 @@ export default class PhotoDialog extends Vue {
|
||||
this.blob = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
swapMirrorClass() {
|
||||
this.mirror = !this.mirror;
|
||||
if (this.mirror) {
|
||||
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
|
||||
} else {
|
||||
(this.$refs.cameraContainer as HTMLElement).classList.remove(
|
||||
"mirror-video",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export interface ImageResult {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface PlatformService {
|
||||
// File system operations
|
||||
readFile(path: string): Promise<string>;
|
||||
@@ -6,8 +11,8 @@ export interface PlatformService {
|
||||
listFiles(directory: string): Promise<string[]>;
|
||||
|
||||
// Camera operations
|
||||
takePicture(): Promise<string>;
|
||||
pickImage(): Promise<string>;
|
||||
takePicture(): Promise<ImageResult>;
|
||||
pickImage(): Promise<ImageResult>;
|
||||
|
||||
// Platform specific features
|
||||
isCapacitor(): boolean;
|
||||
|
||||
@@ -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<string> {
|
||||
@@ -36,24 +37,64 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
return result.files;
|
||||
}
|
||||
|
||||
async takePicture(): Promise<string> {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Uri,
|
||||
source: CameraSource.Camera,
|
||||
});
|
||||
return image.webPath || "";
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
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<string> {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Uri,
|
||||
source: CameraSource.Photos,
|
||||
});
|
||||
return image.webPath || "";
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
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<Blob> {
|
||||
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 {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { PlatformService } from '../PlatformService';
|
||||
import { PlatformService } from "../PlatformService";
|
||||
|
||||
export class ElectronPlatformService implements PlatformService {
|
||||
async readFile(path: string): Promise<string> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async deleteFile(path: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async listFiles(directory: string): Promise<string[]> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async takePicture(): Promise<string> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async pickImage(): Promise<string> {
|
||||
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<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { PlatformService } from '../PlatformService';
|
||||
import { PlatformService } from "../PlatformService";
|
||||
|
||||
export class PyWebViewPlatformService implements PlatformService {
|
||||
async readFile(path: string): Promise<string> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async deleteFile(path: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async listFiles(directory: string): Promise<string[]> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async takePicture(): Promise<string> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async pickImage(): Promise<string> {
|
||||
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<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
@@ -17,23 +18,28 @@ export class WebPlatformService implements PlatformService {
|
||||
throw new Error("File system access not available in web platform");
|
||||
}
|
||||
|
||||
async takePicture(): Promise<string> {
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
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<string> {
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
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<Blob> {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user