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 1 week 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
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);
}

8
src/services/PlatformService.ts

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

16
src/services/platforms/CapacitorPlatformService.ts

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

Loading…
Cancel
Save