Browse Source

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.
pull/135/head
Jose Olarte III 2 weeks ago
parent
commit
28634839ec
  1. 68
      src/components/ImageMethodDialog.vue
  2. 8
      src/services/PlatformService.ts
  3. 16
      src/services/platforms/CapacitorPlatformService.ts

68
src/components/ImageMethodDialog.vue

@ -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);
} }

8
src/services/PlatformService.ts

@ -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

16
src/services/platforms/CapacitorPlatformService.ts

@ -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.

Loading…
Cancel
Save