You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
8.3 KiB
281 lines
8.3 KiB
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<string> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string[]> {
|
|
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<ImageResult> {
|
|
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<ImageResult> {
|
|
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<Blob> {
|
|
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<void> {
|
|
// 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<void> {
|
|
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<string> {
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|