import { ImageResult, PlatformService } from "../PlatformService"; import { Filesystem, Directory } from "@capacitor/filesystem"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { logger } from "../../utils/logger"; import { Share } from "@capacitor/share"; /** * Platform service implementation for Capacitor (mobile) platform. * Provides native mobile functionality through Capacitor plugins for: * - File system operations * - Camera and image picker * - Platform-specific features */ export class CapacitorPlatformService implements PlatformService { /** * Reads a file from the app's data directory. * @param path - Relative path to the file in the app's data directory * @returns Promise resolving to the file contents as string * @throws Error if file cannot be read or doesn't exist */ async readFile(path: string): Promise { const file = await Filesystem.readFile({ path, directory: Directory.Data, }); if (file.data instanceof Blob) { return await file.data.text(); } return file.data; } /** * Writes content to a file in the app's data directory. * @param path - Relative path where to write the file * @param content - Content to write to the file * @throws Error if write operation fails */ async writeFile(path: string, content: string): Promise { await Filesystem.writeFile({ path, data: content, directory: Directory.Data, }); } /** * Deletes a file from the app's data directory. * @param path - Relative path to the file to delete * @throws Error if deletion fails or file doesn't exist */ async deleteFile(path: string): Promise { await Filesystem.deleteFile({ path, directory: Directory.Data, }); } /** * Lists files in the specified directory within app's data directory. * @param directory - Relative path to the directory to list * @returns Promise resolving to array of filenames * @throws Error if directory cannot be read or doesn't exist */ async listFiles(directory: string): Promise { const result = await Filesystem.readdir({ path: directory, directory: Directory.Data, }); return result.files.map((file) => typeof file === "string" ? file : file.name, ); } /** * Opens the device camera to take a picture. * Configures camera for high quality images with editing enabled. * @returns Promise resolving to the captured image data * @throws Error if camera access fails or user cancels */ async takePicture(): Promise { try { const image = await Camera.getPhoto({ quality: 90, allowEditing: true, resultType: CameraResultType.Base64, source: CameraSource.Camera, }); 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"); } } /** * Opens the device photo gallery to pick an existing image. * Configures picker for high quality images with editing enabled. * @returns Promise resolving to the selected image data * @throws Error if gallery access fails or user cancels */ async pickImage(): Promise { try { const image = await Camera.getPhoto({ quality: 90, allowEditing: true, resultType: CameraResultType.Base64, source: CameraSource.Photos, }); 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"); } } /** * Converts base64 image data to a Blob. * @param base64String - Base64 encoded image data * @returns Promise resolving to image Blob * @throws Error if conversion fails */ private async processImageData(base64String?: string): Promise { 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" }); } /** * Checks if running on Capacitor platform. * @returns true, as this is the Capacitor implementation */ isCapacitor(): boolean { return true; } /** * Checks if running on Electron platform. * @returns false, as this is not Electron */ isElectron(): boolean { return false; } /** * Checks if running on PyWebView platform. * @returns false, as this is not PyWebView */ isPyWebView(): boolean { return false; } /** * Checks if running on web platform. * @returns false, as this is not web */ isWeb(): boolean { return false; } /** * Handles deep link URLs for the application. * Note: Capacitor handles deep links automatically. * @param _url - The deep link URL (unused) */ async handleDeepLink(_url: string): Promise { // Capacitor handles deep links automatically // This is just a placeholder for the interface return Promise.resolve(); } getExportInstructions(): string[] { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); if (isIOS) { return [ "On iOS: Choose 'More...' and select a place in iCloud, or go 'Back' and save to another location.", ]; } else { return [ "On Android: Choose 'Open' and then share to your preferred place.", ]; } } getExportSuccessMessage(): string { return "The backup has been saved to your device."; } needsSecondaryDownloadLink(): boolean { return false; } needsDownloadCleanup(): boolean { return false; } async exportDatabase(blob: Blob, fileName: string): Promise { logger.log("Starting database export on Capacitor platform:", { fileName, blobSize: `${blob.size} bytes`, }); // Create a File object from the Blob const file = new File([blob], fileName, { type: "application/json" }); try { logger.log("Attempting to use native share sheet"); // Use the native share sheet await navigator.share({ files: [file], title: fileName, }); logger.log("Database export completed via native share sheet"); } catch (error) { logger.log("Native share failed, falling back to Capacitor Share API"); // Fallback to Capacitor Share API if Web Share API fails // First save to temporary file const base64Data = await this.blobToBase64(blob); const result = await Filesystem.writeFile({ path: fileName, data: base64Data, directory: Directory.Cache, // Use Cache instead of Documents for temporary files recursive: true, }); logger.log("Temporary file created for sharing:", result.uri); // Then share using Capacitor Share API await Share.share({ title: fileName, url: result.uri, }); logger.log("Database export completed via Capacitor Share API"); // Clean up the temporary file try { await Filesystem.deleteFile({ path: fileName, directory: Directory.Cache, }); logger.log("Temporary file cleaned up successfully"); } catch (cleanupError) { logger.warn("Failed to clean up temporary file:", cleanupError); } } } private async blobToBase64(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const base64data = reader.result as string; resolve(base64data.split(",")[1]); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } }