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"
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
/>
|
/>
|
||||||
<!-- This gives a round cropper.
|
|
||||||
:presetMode="{
|
|
||||||
mode: 'round',
|
|
||||||
}"
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@@ -74,87 +69,67 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else ref="cameraContainer">
|
<div v-else>
|
||||||
<!--
|
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
||||||
Camera "resolution" doesn't change how it shows on screen but rather stretches the result,
|
<button
|
||||||
eg. the following which just stretches it vertically:
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||||
:resolution="{ width: 375, height: 812 }"
|
@click="takePhoto"
|
||||||
-->
|
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
<button
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
</button>
|
||||||
@click="takeImage()"
|
<button
|
||||||
>
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||||
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
|
@click="pickPhoto"
|
||||||
</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"
|
|
||||||
>
|
>
|
||||||
<button
|
<font-awesome icon="image" class="w-[1em]" />
|
||||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
</button>
|
||||||
@click="swapMirrorClass()"
|
</div>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 axios from "axios";
|
||||||
import Camera from "simple-vue-camera";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||||
|
|
||||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { accessToken } from "../libs/crypto";
|
import { accessToken } from "../libs/crypto";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component({ components: { Camera, VuePictureCropper } })
|
@Component({ components: { VuePictureCropper } })
|
||||||
export default class PhotoDialog extends Vue {
|
export default class PhotoDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
activeDeviceNumber = 0;
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
blob?: Blob;
|
blob?: Blob;
|
||||||
claimType = "";
|
claimType = "";
|
||||||
crop = false;
|
crop = false;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
mirror = false;
|
|
||||||
numDevices = 0;
|
|
||||||
setImageCallback: (arg: string) => void = () => {};
|
setImageCallback: (arg: string) => void = () => {};
|
||||||
showRetry = true;
|
showRetry = true;
|
||||||
uploading = false;
|
uploading = false;
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("Error retrieving settings from database:", err);
|
logger.error("Error retrieving settings from database:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -173,7 +148,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
setImageFn: (arg: string) => void,
|
setImageFn: (arg: string) => void,
|
||||||
claimType: string,
|
claimType: string,
|
||||||
crop?: boolean,
|
crop?: boolean,
|
||||||
blob?: Blob, // for image upload, just to use the cropping function
|
blob?: Blob,
|
||||||
inputFileName?: string,
|
inputFileName?: string,
|
||||||
) {
|
) {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
@@ -187,7 +162,6 @@ export default class PhotoDialog extends Vue {
|
|||||||
if (blob) {
|
if (blob) {
|
||||||
this.blob = blob;
|
this.blob = blob;
|
||||||
this.fileName = inputFileName;
|
this.fileName = inputFileName;
|
||||||
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
|
|
||||||
this.showRetry = false;
|
this.showRetry = false;
|
||||||
} else {
|
} else {
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
@@ -205,85 +179,35 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async cameraStarted() {
|
async takePhoto() {
|
||||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
try {
|
||||||
if (cameraComponent) {
|
const result = await this.platformService.takePicture();
|
||||||
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
|
this.blob = result.blob;
|
||||||
this.mirror = cameraComponent.facingMode === "user";
|
this.fileName = result.fileName;
|
||||||
// figure out which device is active
|
} catch (error) {
|
||||||
const currentDeviceId = cameraComponent.currentDeviceID();
|
logger.error("Error taking picture:", error);
|
||||||
const devices = await cameraComponent.devices(["videoinput"]);
|
this.$notify({
|
||||||
this.activeDeviceNumber = devices.findIndex(
|
group: "alert",
|
||||||
(device) => device.deviceId === currentDeviceId,
|
type: "danger",
|
||||||
);
|
title: "Error",
|
||||||
|
text: "Failed to take picture. Please try again.",
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchCamera() {
|
async pickPhoto() {
|
||||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
try {
|
||||||
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
|
const result = await this.platformService.pickImage();
|
||||||
const devices = await cameraComponent?.devices(["videoinput"]);
|
this.blob = result.blob;
|
||||||
await cameraComponent?.changeCamera(
|
this.fileName = result.fileName;
|
||||||
devices[this.activeDeviceNumber].deviceId,
|
} catch (error) {
|
||||||
);
|
logger.error("Error picking image:", error);
|
||||||
}
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
async takeImage(/* payload: MouseEvent */) {
|
type: "danger",
|
||||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
title: "Error",
|
||||||
|
text: "Failed to pick image. Please try again.",
|
||||||
/**
|
}, 5000);
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,51 +219,6 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
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() {
|
async uploadImage() {
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
|
|
||||||
@@ -350,11 +229,9 @@ export default class PhotoDialog extends Vue {
|
|||||||
const token = await accessToken(this.activeDid);
|
const token = await accessToken(this.activeDid);
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: "Bearer " + token,
|
Authorization: "Bearer " + token,
|
||||||
// axios fills in Content-Type of multipart/form-data
|
|
||||||
};
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (!this.blob) {
|
if (!this.blob) {
|
||||||
// yeah, this should never happen, but it helps with subsequent type checking
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -367,7 +244,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formData.append("image", this.blob, this.fileName || "snapshot.png");
|
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||||
formData.append("claimType", this.claimType);
|
formData.append("claimType", this.claimType);
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@@ -402,17 +279,6 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -438,12 +304,4 @@ export default class PhotoDialog extends Vue {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 700px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export interface ImageResult {
|
||||||
|
blob: Blob;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlatformService {
|
export interface PlatformService {
|
||||||
// File system operations
|
// File system operations
|
||||||
readFile(path: string): Promise<string>;
|
readFile(path: string): Promise<string>;
|
||||||
@@ -6,8 +11,8 @@ export interface PlatformService {
|
|||||||
listFiles(directory: string): Promise<string[]>;
|
listFiles(directory: string): Promise<string[]>;
|
||||||
|
|
||||||
// Camera operations
|
// Camera operations
|
||||||
takePicture(): Promise<string>;
|
takePicture(): Promise<ImageResult>;
|
||||||
pickImage(): Promise<string>;
|
pickImage(): Promise<ImageResult>;
|
||||||
|
|
||||||
// Platform specific features
|
// Platform specific features
|
||||||
isCapacitor(): boolean;
|
isCapacitor(): boolean;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { PlatformService } from "../PlatformService";
|
import { ImageResult, PlatformService } from "../PlatformService";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
import { Filesystem, Directory } from "@capacitor/filesystem";
|
import { Filesystem, Directory } from "@capacitor/filesystem";
|
||||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||||
import { App } from "@capacitor/app";
|
import { App } from "@capacitor/app";
|
||||||
|
import { logger } from "../../utils/logger";
|
||||||
|
|
||||||
export class CapacitorPlatformService implements PlatformService {
|
export class CapacitorPlatformService implements PlatformService {
|
||||||
async readFile(path: string): Promise<string> {
|
async readFile(path: string): Promise<string> {
|
||||||
@@ -36,24 +37,64 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
return result.files;
|
return result.files;
|
||||||
}
|
}
|
||||||
|
|
||||||
async takePicture(): Promise<string> {
|
async takePicture(): Promise<ImageResult> {
|
||||||
const image = await Camera.getPhoto({
|
try {
|
||||||
quality: 90,
|
const image = await Camera.getPhoto({
|
||||||
allowEditing: true,
|
quality: 90,
|
||||||
resultType: CameraResultType.Uri,
|
allowEditing: true,
|
||||||
source: CameraSource.Camera,
|
resultType: CameraResultType.Base64,
|
||||||
});
|
source: CameraSource.Camera,
|
||||||
return image.webPath || "";
|
});
|
||||||
|
|
||||||
|
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> {
|
async pickImage(): Promise<ImageResult> {
|
||||||
const image = await Camera.getPhoto({
|
try {
|
||||||
quality: 90,
|
const image = await Camera.getPhoto({
|
||||||
allowEditing: true,
|
quality: 90,
|
||||||
resultType: CameraResultType.Uri,
|
allowEditing: true,
|
||||||
source: CameraSource.Photos,
|
resultType: CameraResultType.Base64,
|
||||||
});
|
source: CameraSource.Photos,
|
||||||
return image.webPath || "";
|
});
|
||||||
|
|
||||||
|
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 {
|
isCapacitor(): boolean {
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { PlatformService } from '../PlatformService';
|
import { PlatformService } from "../PlatformService";
|
||||||
|
|
||||||
export class ElectronPlatformService implements PlatformService {
|
export class ElectronPlatformService implements PlatformService {
|
||||||
async readFile(path: string): Promise<string> {
|
async readFile(path: string): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeFile(path: string, content: string): Promise<void> {
|
async writeFile(path: string, content: string): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(path: string): Promise<void> {
|
async deleteFile(path: string): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFiles(directory: string): Promise<string[]> {
|
async listFiles(directory: string): Promise<string[]> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async takePicture(): Promise<string> {
|
async takePicture(): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async pickImage(): Promise<string> {
|
async pickImage(): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
isCapacitor(): boolean {
|
isCapacitor(): boolean {
|
||||||
@@ -42,6 +42,6 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
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 {
|
export class PyWebViewPlatformService implements PlatformService {
|
||||||
async readFile(path: string): Promise<string> {
|
async readFile(path: string): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeFile(path: string, content: string): Promise<void> {
|
async writeFile(path: string, content: string): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(path: string): Promise<void> {
|
async deleteFile(path: string): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFiles(directory: string): Promise<string[]> {
|
async listFiles(directory: string): Promise<string[]> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async takePicture(): Promise<string> {
|
async takePicture(): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
async pickImage(): Promise<string> {
|
async pickImage(): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
isCapacitor(): boolean {
|
isCapacitor(): boolean {
|
||||||
@@ -42,6 +42,6 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
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 {
|
export class WebPlatformService implements PlatformService {
|
||||||
async readFile(path: string): Promise<string> {
|
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");
|
throw new Error("File system access not available in web platform");
|
||||||
}
|
}
|
||||||
|
|
||||||
async takePicture(): Promise<string> {
|
async takePicture(): Promise<ImageResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "image/*";
|
input.accept = "image/*";
|
||||||
input.capture = "environment";
|
input.capture = "environment";
|
||||||
|
|
||||||
input.onchange = (e) => {
|
input.onchange = async (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
try {
|
||||||
reader.onload = (event) => {
|
const blob = await this.processImageFile(file);
|
||||||
resolve(event.target?.result as string);
|
resolve({
|
||||||
};
|
blob,
|
||||||
reader.readAsDataURL(file);
|
fileName: file.name || "photo.jpg"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error processing camera image:", error);
|
||||||
|
reject(new Error("Failed to process camera image"));
|
||||||
|
}
|
||||||
} else {
|
} 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "image/*";
|
input.accept = "image/*";
|
||||||
|
|
||||||
input.onchange = (e) => {
|
input.onchange = async (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
try {
|
||||||
reader.onload = (event) => {
|
const blob = await this.processImageFile(file);
|
||||||
resolve(event.target?.result as string);
|
resolve({
|
||||||
};
|
blob,
|
||||||
reader.readAsDataURL(file);
|
fileName: file.name || "photo.jpg"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error processing picked image:", error);
|
||||||
|
reject(new Error("Failed to process picked image"));
|
||||||
|
}
|
||||||
} else {
|
} 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 {
|
isCapacitor(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user