forked from trent_larson/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
|
||||
muted
|
||||
></video>
|
||||
<button
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||
@click="capturePhoto"
|
||||
>
|
||||
<font-awesome icon="camera" class="w-[1em]" />
|
||||
</button>
|
||||
<div class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4">
|
||||
<button
|
||||
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||
@click="capturePhoto"
|
||||
>
|
||||
<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
|
||||
@@ -303,6 +312,9 @@ export default class ImageMethodDialog extends Vue {
|
||||
/** Camera stream reference */
|
||||
private cameraStream: MediaStream | null = null;
|
||||
|
||||
/** Current camera facing mode */
|
||||
private currentFacingMode: 'environment' | 'user' = 'environment';
|
||||
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
URL = window.URL || window.webkitURL;
|
||||
|
||||
@@ -478,7 +490,7 @@ export default class ImageMethodDialog extends Vue {
|
||||
await this.$nextTick();
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment" },
|
||||
video: { facingMode: this.currentFacingMode },
|
||||
});
|
||||
logger.debug("Camera access granted");
|
||||
this.cameraStream = stream;
|
||||
@@ -503,15 +515,17 @@ export default class ImageMethodDialog extends Vue {
|
||||
let errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to access camera";
|
||||
if (
|
||||
error instanceof Error && (
|
||||
error.name === "NotReadableError" ||
|
||||
error.name === "TrackStartError"
|
||||
) {
|
||||
)) {
|
||||
errorMessage =
|
||||
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
||||
} else if (
|
||||
error instanceof Error && (
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
) {
|
||||
)) {
|
||||
errorMessage =
|
||||
"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 {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface PlatformCapabilities {
|
||||
hasFileDownload: boolean;
|
||||
/** Whether the platform requires special file handling instructions */
|
||||
needsFileHandlingInstructions: boolean;
|
||||
/** Whether the platform is a native app (Capacitor, Electron, etc.) */
|
||||
isNativeApp: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +94,12 @@ export interface PlatformService {
|
||||
*/
|
||||
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.
|
||||
* @param url - The deep link URL to handle
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
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 { logger } from "../../utils/logger";
|
||||
|
||||
@@ -16,6 +16,9 @@ import { logger } from "../../utils/logger";
|
||||
* - Platform-specific features
|
||||
*/
|
||||
export class CapacitorPlatformService implements PlatformService {
|
||||
/** Current camera direction */
|
||||
private currentDirection: CameraDirection = 'BACK';
|
||||
|
||||
/**
|
||||
* Gets the capabilities of the Capacitor platform
|
||||
* @returns Platform capabilities object
|
||||
@@ -28,6 +31,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
hasFileDownload: false,
|
||||
needsFileHandlingInstructions: true,
|
||||
isNativeApp: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -401,6 +405,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Base64,
|
||||
source: CameraSource.Camera,
|
||||
direction: this.currentDirection,
|
||||
});
|
||||
|
||||
const blob = await this.processImageData(image.base64String);
|
||||
@@ -466,6 +471,15 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
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.
|
||||
* Note: Capacitor handles deep links automatically.
|
||||
|
||||
Reference in New Issue
Block a user