forked from jsnbuchanan/crowd-funder-for-time-pwa
Feature: front/back camera toggle
- Added to gifting and profile dialog camera for now. Toggle button is hidden in desktop. - WIP: same feature for QR scanner camera. - WIP: ability to specify default camera depending on where it's called.
This commit is contained in:
@@ -119,12 +119,21 @@
|
|||||||
playsinline
|
playsinline
|
||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
<button
|
<div class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4">
|
||||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
<button
|
||||||
@click="capturePhoto"
|
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
>
|
@click="capturePhoto"
|
||||||
<font-awesome icon="camera" class="w-[1em]" />
|
>
|
||||||
</button>
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="platformCapabilities.isMobile"
|
||||||
|
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
|
@click="rotateCamera"
|
||||||
|
>
|
||||||
|
<font-awesome icon="rotate" class="w-[1em]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -303,6 +312,9 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
/** Camera stream reference */
|
/** Camera stream reference */
|
||||||
private cameraStream: MediaStream | null = null;
|
private cameraStream: MediaStream | null = null;
|
||||||
|
|
||||||
|
/** Current camera facing mode */
|
||||||
|
private currentFacingMode: 'environment' | 'user' = 'environment';
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
@@ -478,7 +490,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: "environment" },
|
video: { facingMode: this.currentFacingMode },
|
||||||
});
|
});
|
||||||
logger.debug("Camera access granted");
|
logger.debug("Camera access granted");
|
||||||
this.cameraStream = stream;
|
this.cameraStream = stream;
|
||||||
@@ -503,15 +515,17 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
let errorMessage =
|
let errorMessage =
|
||||||
error instanceof Error ? error.message : "Failed to access camera";
|
error instanceof Error ? error.message : "Failed to access camera";
|
||||||
if (
|
if (
|
||||||
|
error instanceof Error && (
|
||||||
error.name === "NotReadableError" ||
|
error.name === "NotReadableError" ||
|
||||||
error.name === "TrackStartError"
|
error.name === "TrackStartError"
|
||||||
) {
|
)) {
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
||||||
} else if (
|
} else if (
|
||||||
|
error instanceof Error && (
|
||||||
error.name === "NotAllowedError" ||
|
error.name === "NotAllowedError" ||
|
||||||
error.name === "PermissionDeniedError"
|
error.name === "PermissionDeniedError"
|
||||||
) {
|
)) {
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Camera access was denied. Please allow camera access in your browser settings.";
|
"Camera access was denied. Please allow camera access in your browser settings.";
|
||||||
}
|
}
|
||||||
@@ -579,6 +593,42 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rotateCamera() {
|
||||||
|
if (this.platformCapabilities.isNativeApp) {
|
||||||
|
try {
|
||||||
|
await this.platformService.rotateCamera();
|
||||||
|
// Take a new picture with the rotated camera
|
||||||
|
const result = await this.platformService.takePicture();
|
||||||
|
this.blob = result.blob;
|
||||||
|
this.fileName = result.fileName;
|
||||||
|
this.showRetry = true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error rotating camera:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to rotate camera. Please try again.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For web browsers, toggle between front and back cameras
|
||||||
|
this.currentFacingMode = this.currentFacingMode === 'environment' ? 'user' : 'environment';
|
||||||
|
|
||||||
|
// Stop current stream
|
||||||
|
if (this.cameraStream) {
|
||||||
|
this.cameraStream.getTracks().forEach(track => track.stop());
|
||||||
|
this.cameraStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new stream with updated facing mode
|
||||||
|
await this.startCameraPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private createBlobURL(blob: Blob): string {
|
private createBlobURL(blob: Blob): string {
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface PlatformCapabilities {
|
|||||||
hasFileDownload: boolean;
|
hasFileDownload: boolean;
|
||||||
/** Whether the platform requires special file handling instructions */
|
/** Whether the platform requires special file handling instructions */
|
||||||
needsFileHandlingInstructions: boolean;
|
needsFileHandlingInstructions: boolean;
|
||||||
|
/** Whether the platform is a native app (Capacitor, Electron, etc.) */
|
||||||
|
isNativeApp: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +94,12 @@ export interface PlatformService {
|
|||||||
*/
|
*/
|
||||||
pickImage(): Promise<ImageResult>;
|
pickImage(): Promise<ImageResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the camera between front and back cameras.
|
||||||
|
* @returns Promise that resolves when the camera is rotated
|
||||||
|
*/
|
||||||
|
rotateCamera(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deep link URLs for the application.
|
* Handles deep link URLs for the application.
|
||||||
* @param url - The deep link URL to handle
|
* @param url - The deep link URL to handle
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
import { Camera, CameraResultType, CameraSource, CameraDirection } from "@capacitor/camera";
|
||||||
import { Share } from "@capacitor/share";
|
import { Share } from "@capacitor/share";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ import { logger } from "../../utils/logger";
|
|||||||
* - Platform-specific features
|
* - Platform-specific features
|
||||||
*/
|
*/
|
||||||
export class CapacitorPlatformService implements PlatformService {
|
export class CapacitorPlatformService implements PlatformService {
|
||||||
|
/** Current camera direction */
|
||||||
|
private currentDirection: CameraDirection = 'BACK';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the capabilities of the Capacitor platform
|
* Gets the capabilities of the Capacitor platform
|
||||||
* @returns Platform capabilities object
|
* @returns Platform capabilities object
|
||||||
@@ -28,6 +31,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||||
hasFileDownload: false,
|
hasFileDownload: false,
|
||||||
needsFileHandlingInstructions: true,
|
needsFileHandlingInstructions: true,
|
||||||
|
isNativeApp: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +405,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
allowEditing: true,
|
allowEditing: true,
|
||||||
resultType: CameraResultType.Base64,
|
resultType: CameraResultType.Base64,
|
||||||
source: CameraSource.Camera,
|
source: CameraSource.Camera,
|
||||||
|
direction: this.currentDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
const blob = await this.processImageData(image.base64String);
|
const blob = await this.processImageData(image.base64String);
|
||||||
@@ -466,6 +471,15 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
return new Blob(byteArrays, { type: "image/jpeg" });
|
return new Blob(byteArrays, { type: "image/jpeg" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the camera between front and back cameras.
|
||||||
|
* @returns Promise that resolves when the camera is rotated
|
||||||
|
*/
|
||||||
|
async rotateCamera(): Promise<void> {
|
||||||
|
this.currentDirection = this.currentDirection === 'BACK' ? 'FRONT' : 'BACK';
|
||||||
|
logger.debug(`Camera rotated to ${this.currentDirection} camera`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deep link URLs for the application.
|
* Handles deep link URLs for the application.
|
||||||
* Note: Capacitor handles deep links automatically.
|
* Note: Capacitor handles deep links automatically.
|
||||||
|
|||||||
Reference in New Issue
Block a user