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.
 
 
 
 
 
 

1026 lines
33 KiB

import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import {
Camera,
CameraResultType,
CameraSource,
CameraDirection,
} from "@capacitor/camera";
import { Capacitor } from "@capacitor/core";
import { Share } from "@capacitor/share";
import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
capSQLiteChanges,
DBSQLiteValues,
} from "@capacitor-community/sqlite";
import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database";
import {
ImageResult,
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
interface QueuedOperation {
type: "run" | "query";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
}
/**
* 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
* - SQLite database operations
*/
export class CapacitorPlatformService implements PlatformService {
/** Current camera direction */
private currentDirection: CameraDirection = CameraDirection.Rear;
private sqlite: SQLiteConnection;
private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.sqlite";
private initialized = false;
private initializationPromise: Promise<void> | null = null;
private operationQueue: Array<QueuedOperation> = [];
private isProcessingQueue: boolean = false;
constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
}
private async initializeDatabase(): Promise<void> {
// If already initialized, return immediately
if (this.initialized) {
return;
}
// If initialization is in progress, wait for it
if (this.initializationPromise) {
return this.initializationPromise;
}
// Start initialization
this.initializationPromise = this._initialize();
try {
await this.initializationPromise;
} catch (error) {
logger.error(
"[CapacitorPlatformService] Initialize method failed:",
error,
);
this.initializationPromise = null; // Reset on failure
throw error;
}
}
private async _initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Create/Open database
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
false,
);
await this.db.open();
// Set journal mode to WAL for better performance
// await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations
await this.runCapacitorMigrations();
this.initialized = true;
logger.log(
"[CapacitorPlatformService] SQLite database initialized successfully",
);
// Start processing the queue after initialization
this.processQueue();
} catch (error) {
logger.error(
"[CapacitorPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[CapacitorPlatformService] Failed to initialize database",
);
}
}
private async processQueue(): Promise<void> {
if (this.isProcessingQueue || !this.initialized || !this.db) {
return;
}
this.isProcessingQueue = true;
while (this.operationQueue.length > 0) {
const operation = this.operationQueue.shift();
if (!operation) continue;
try {
let result: unknown;
switch (operation.type) {
case "run": {
const runResult = await this.db.run(
operation.sql,
operation.params,
);
result = {
changes: runResult.changes?.changes || 0,
lastId: runResult.changes?.lastId,
};
break;
}
case "query": {
const queryResult = await this.db.query(
operation.sql,
operation.params,
);
result = {
columns: Object.keys(queryResult.values?.[0] || {}),
values: (queryResult.values || []).map((row) =>
Object.values(row),
),
};
break;
}
}
operation.resolve(result);
} catch (error) {
logger.error(
"[CapacitorPlatformService] Error while processing SQL queue:",
error,
);
operation.reject(error);
}
}
this.isProcessingQueue = false;
}
private async queueOperation<R>(
type: QueuedOperation["type"],
sql: string,
params: unknown[] = [],
): Promise<R> {
// Convert parameters to SQLite-compatible types
const convertedParams = params.map((param) => {
if (param === null || param === undefined) {
return null;
}
if (typeof param === "object" && param !== null) {
// Convert objects and arrays to JSON strings
return JSON.stringify(param);
}
if (typeof param === "boolean") {
// Convert boolean to integer (0 or 1)
return param ? 1 : 0;
}
// Numbers, strings, bigints, and buffers are already supported
return param;
});
return new Promise<R>((resolve, reject) => {
const operation: QueuedOperation = {
type,
sql,
params: convertedParams,
resolve: (value: unknown) => resolve(value as R),
reject,
};
this.operationQueue.push(operation);
// If we're already initialized, start processing the queue
if (this.initialized && this.db) {
this.processQueue();
}
});
}
private async waitForInitialization(): Promise<void> {
// If we have an initialization promise, wait for it
if (this.initializationPromise) {
await this.initializationPromise;
return;
}
// If not initialized and no promise, start initialization
if (!this.initialized) {
await this.initializeDatabase();
return;
}
// If initialized but no db, something went wrong
if (!this.db) {
logger.error(
"[CapacitorPlatformService] Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null",
);
throw new Error(
"[CapacitorPlatformService] The database could not be initialized. We recommend you restart or reinstall.",
);
}
}
/**
* Execute database migrations for the Capacitor platform
*
* This method orchestrates the database migration process specifically for
* Capacitor-based platforms (mobile and Electron). It provides the platform-specific
* SQL execution functions to the migration service and handles Capacitor SQLite
* plugin integration.
*
* ## Migration Process:
*
* 1. **SQL Execution Setup**: Creates platform-specific SQL execution functions
* that properly handle the Capacitor SQLite plugin's API
*
* 2. **Parameter Handling**: Ensures proper parameter binding for prepared statements
* using the correct Capacitor SQLite methods (run vs execute)
*
* 3. **Result Parsing**: Provides extraction functions that understand the
* Capacitor SQLite result format
*
* 4. **Migration Execution**: Delegates to the migration service for the actual
* migration logic and tracking
*
* 5. **Integrity Verification**: Runs post-migration integrity checks to ensure
* the database is in the expected state
*
* ## Error Handling:
*
* The method includes comprehensive error handling for:
* - Database connection issues
* - SQL execution failures
* - Migration tracking problems
* - Schema validation errors
*
* Even if migrations fail, the integrity check still runs to assess the
* current database state and provide debugging information.
*
* ## Logging:
*
* Detailed logging is provided throughout the process using emoji-tagged
* console messages that appear in the Electron DevTools console. This
* includes:
* - SQL statement execution details
* - Parameter values for debugging
* - Migration success/failure status
* - Database integrity check results
*
* @throws {Error} If database is not initialized or migrations fail critically
* @private Internal method called during database initialization
*
* @example
* ```typescript
* // Called automatically during platform service initialization
* await this.runCapacitorMigrations();
* ```
*/
private async runCapacitorMigrations(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
/**
* SQL execution function for Capacitor SQLite plugin
*
* This function handles the execution of SQL statements (INSERT, UPDATE, CREATE, etc.)
* through the Capacitor SQLite plugin. It automatically chooses the appropriate
* method based on whether parameters are provided.
*
* @param sql - SQL statement to execute
* @param params - Optional parameters for prepared statements
* @returns Promise resolving to execution results
*/
const sqlExec = async (sql: string, params?: unknown[]): Promise<capSQLiteChanges> => {
console.log(`🔧 [CapacitorMigration] Executing SQL:`, sql);
console.log(`📋 [CapacitorMigration] With params:`, params);
if (params && params.length > 0) {
// Use run method for parameterized queries (prepared statements)
// This is essential for proper parameter binding and SQL injection prevention
const result = await this.db!.run(sql, params);
console.log(`✅ [CapacitorMigration] Run result:`, result);
return result;
} else {
// Use execute method for non-parameterized queries
// This is more efficient for simple DDL statements
const result = await this.db!.execute(sql);
console.log(`✅ [CapacitorMigration] Execute result:`, result);
return result;
}
};
/**
* SQL query function for Capacitor SQLite plugin
*
* This function handles the execution of SQL queries (SELECT statements)
* through the Capacitor SQLite plugin. It returns the raw result data
* that can be processed by the migration service.
*
* @param sql - SQL query to execute
* @param params - Optional parameters for prepared statements
* @returns Promise resolving to query results
*/
const sqlQuery = async (sql: string, params?: unknown[]): Promise<DBSQLiteValues> => {
console.log(`🔍 [CapacitorMigration] Querying SQL:`, sql);
console.log(`📋 [CapacitorMigration] With params:`, params);
const result = await this.db!.query(sql, params);
console.log(`📊 [CapacitorMigration] Query result:`, result);
return result;
};
/**
* Extract migration names from Capacitor SQLite query results
*
* This function parses the result format returned by the Capacitor SQLite
* plugin and extracts migration names. It handles the specific data structure
* used by the plugin, which can vary between different result formats.
*
* ## Result Format Handling:
*
* The Capacitor SQLite plugin can return results in different formats:
* - Object format: `{ name: "migration_name" }`
* - Array format: `["migration_name", "timestamp"]`
*
* This function handles both formats to ensure robust migration name extraction.
*
* @param result - Query result from Capacitor SQLite plugin
* @returns Set of migration names found in the result
*/
const extractMigrationNames = (result: DBSQLiteValues): Set<string> => {
console.log(`🔍 [CapacitorMigration] Extracting migration names from:`, result);
// Handle the Capacitor SQLite result format
const names = result.values?.map((row: any) => {
// The row could be an object with 'name' property or an array where name is first element
if (typeof row === 'object' && row.name !== undefined) {
return row.name;
} else if (Array.isArray(row) && row.length > 0) {
return row[0];
}
return null;
}).filter(name => name !== null) || [];
console.log(`📋 [CapacitorMigration] Extracted names:`, names);
return new Set(names);
};
try {
// Execute the migration process
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
// After migrations, run integrity check to verify database state
await this.verifyDatabaseIntegrity();
} catch (error) {
console.error(`❌ [CapacitorMigration] Migration failed:`, error);
// Still try to verify what we have for debugging purposes
await this.verifyDatabaseIntegrity();
throw error;
}
}
/**
* Verify database integrity and migration status
*
* This method performs comprehensive validation of the database structure
* and migration state. It's designed to help identify issues with the
* migration process and provide detailed debugging information.
*
* ## Validation Steps:
*
* 1. **Migration Records**: Checks which migrations are recorded as applied
* 2. **Table Existence**: Verifies all expected core tables exist
* 3. **Schema Validation**: Checks table schemas including column presence
* 4. **Data Integrity**: Validates basic data counts and structure
*
* ## Core Tables Validated:
*
* - `accounts`: User identity and cryptographic keys
* - `secret`: Application secrets and encryption keys
* - `settings`: Configuration and user preferences
* - `contacts`: Contact network and trust relationships
* - `logs`: Application event logging
* - `temp`: Temporary data storage
*
* ## Schema Checks:
*
* For critical tables like `contacts`, the method validates:
* - Table structure using `PRAGMA table_info`
* - Presence of important columns (e.g., `iViewContent`)
* - Column data types and constraints
*
* ## Error Handling:
*
* This method is designed to never throw errors - it captures and logs
* all validation issues for debugging purposes. This ensures that even
* if integrity checks fail, they don't prevent the application from starting.
*
* ## Logging Output:
*
* The method produces detailed console output with emoji tags:
* - `✅` for successful validations
* - `❌` for validation failures
* - `📊` for data summaries
* - `🔍` for investigation steps
*
* @private Internal method called after migrations
*
* @example
* ```typescript
* // Called automatically after migration completion
* await this.verifyDatabaseIntegrity();
* ```
*/
private async verifyDatabaseIntegrity(): Promise<void> {
if (!this.db) {
console.error(`❌ [DB-Integrity] Database not initialized`);
return;
}
console.log(`🔍 [DB-Integrity] Starting database integrity check...`);
try {
// Step 1: Check migrations table and applied migrations
const migrationsResult = await this.db.query("SELECT name, applied_at FROM migrations ORDER BY applied_at");
console.log(`📊 [DB-Integrity] Applied migrations:`, migrationsResult);
// Step 2: Verify core tables exist
const coreTableNames = ['accounts', 'secret', 'settings', 'contacts', 'logs', 'temp'];
const existingTables: string[] = [];
for (const tableName of coreTableNames) {
try {
const tableCheck = await this.db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`);
if (tableCheck.values && tableCheck.values.length > 0) {
existingTables.push(tableName);
console.log(`✅ [DB-Integrity] Table ${tableName} exists`);
} else {
console.error(`❌ [DB-Integrity] Table ${tableName} missing`);
}
} catch (error) {
console.error(`❌ [DB-Integrity] Error checking table ${tableName}:`, error);
}
}
// Step 3: Check contacts table schema (including iViewContent column)
if (existingTables.includes('contacts')) {
try {
const contactsSchema = await this.db.query("PRAGMA table_info(contacts)");
console.log(`📊 [DB-Integrity] Contacts table schema:`, contactsSchema);
// Check for iViewContent column specifically
const hasIViewContent = contactsSchema.values?.some((col: any) =>
(col.name === 'iViewContent') || (Array.isArray(col) && col[1] === 'iViewContent')
);
if (hasIViewContent) {
console.log(`✅ [DB-Integrity] iViewContent column exists in contacts table`);
} else {
console.error(`❌ [DB-Integrity] iViewContent column missing from contacts table`);
}
} catch (error) {
console.error(`❌ [DB-Integrity] Error checking contacts schema:`, error);
}
}
// Step 4: Check for basic data integrity
try {
const accountCount = await this.db.query("SELECT COUNT(*) as count FROM accounts");
const settingsCount = await this.db.query("SELECT COUNT(*) as count FROM settings");
const contactsCount = await this.db.query("SELECT COUNT(*) as count FROM contacts");
console.log(`📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`);
} catch (error) {
console.error(`❌ [DB-Integrity] Error checking data counts:`, error);
}
console.log(`✅ [DB-Integrity] Database integrity check completed`);
} catch (error) {
console.error(`❌ [DB-Integrity] Database integrity check failed:`, error);
}
}
/**
* Gets the capabilities of the Capacitor platform
* @returns Platform capabilities object
*/
getCapabilities(): PlatformCapabilities {
const platform = Capacitor.getPlatform();
return {
hasFileSystem: true,
hasCamera: true,
isMobile: true, // Capacitor is always mobile
isIOS: platform === "ios",
hasFileDownload: false, // Mobile platforms need sharing
needsFileHandlingInstructions: true, // Mobile needs instructions
isNativeApp: true,
};
}
/**
* Checks and requests storage permissions if needed
* @returns Promise that resolves when permissions are granted
* @throws Error if permissions are denied
*/
private async checkStoragePermissions(): Promise<void> {
try {
const logData = {
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
};
logger.log(
"Checking storage permissions",
JSON.stringify(logData, null, 2),
);
if (this.getCapabilities().isIOS) {
// iOS uses different permission model
return;
}
// Try to access a test directory to check permissions
try {
await Filesystem.stat({
path: "/storage/emulated/0/Download",
directory: Directory.Documents,
});
logger.log(
"Storage permissions already granted",
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
);
return;
} catch (error: unknown) {
const err = error as Error;
const errorLogData = {
error: {
message: err.message,
name: err.name,
stack: err.stack,
},
timestamp: new Date().toISOString(),
};
// "File does not exist" is expected and not a permission error
if (err.message === "File does not exist") {
logger.log(
"Directory does not exist (expected), proceeding with write",
JSON.stringify(errorLogData, null, 2),
);
return;
}
// Check for actual permission errors
if (
err.message.includes("permission") ||
err.message.includes("access")
) {
logger.log(
"Permission check failed, requesting permissions",
JSON.stringify(errorLogData, null, 2),
);
// The Filesystem plugin will automatically request permissions when needed
// We just need to try the operation again
try {
await Filesystem.stat({
path: "/storage/emulated/0/Download",
directory: Directory.Documents,
});
logger.log(
"Storage permissions granted after request",
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
);
return;
} catch (retryError: unknown) {
const retryErr = retryError as Error;
throw new Error(
`Failed to obtain storage permissions: ${retryErr.message}`,
);
}
}
// For any other error, log it but don't treat as permission error
logger.log(
"Unexpected error during permission check",
JSON.stringify(errorLogData, null, 2),
);
return;
}
} catch (error: unknown) {
const err = error as Error;
const errorLogData = {
error: {
message: err.message,
name: err.name,
stack: err.stack,
},
timestamp: new Date().toISOString(),
};
logger.error(
"Error checking/requesting permissions",
JSON.stringify(errorLogData, null, 2),
);
throw new Error(`Failed to obtain storage permissions: ${err.message}`);
}
}
/**
* 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 safe storage and offers sharing.
*
* Platform-specific behavior:
* - Saves to app's Documents directory
* - Offers sharing functionality to move file elsewhere
*
* The method handles:
* 1. Writing to app-safe storage
* 2. Sharing the file with user's preferred app
* 3. Error handling and logging
*
* @param fileName - The name of the file to create (e.g. "backup.json")
* @param content - The content to write to the file
*
* @throws Error if:
* - File writing fails
* - Sharing fails
*
* @example
* ```typescript
* // Save and share a JSON file
* await platformService.writeFile(
* "backup.json",
* JSON.stringify(data)
* );
* ```
*/
async writeFile(fileName: string, content: string): Promise<void> {
try {
// Check storage permissions before proceeding
await this.checkStoragePermissions();
const logData = {
targetFileName: fileName,
contentLength: content.length,
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
};
logger.log(
"Starting writeFile operation",
JSON.stringify(logData, null, 2),
);
// For Android, we need to handle content URIs differently
if (this.getCapabilities().isIOS) {
// Write to app's Documents directory for iOS
const writeResult = await Filesystem.writeFile({
path: fileName,
data: content,
directory: Directory.Data,
encoding: Encoding.UTF8,
});
const writeSuccessLogData = {
path: writeResult.uri,
timestamp: new Date().toISOString(),
};
logger.log(
"File write successful",
JSON.stringify(writeSuccessLogData, null, 2),
);
// Offer to share the file
try {
await Share.share({
title: "TimeSafari Backup",
text: "Here is your TimeSafari backup file.",
url: writeResult.uri,
dialogTitle: "Share your backup",
});
logger.log(
"Share dialog shown",
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
);
} catch (shareError) {
// Log share error but don't fail the operation
logger.error(
"Share dialog failed",
JSON.stringify(
{
error: shareError,
timestamp: new Date().toISOString(),
},
null,
2,
),
);
}
} else {
// For Android, first write to app's Documents directory
const writeResult = await Filesystem.writeFile({
path: fileName,
data: content,
directory: Directory.Data,
encoding: Encoding.UTF8,
});
const writeSuccessLogData = {
path: writeResult.uri,
timestamp: new Date().toISOString(),
};
logger.log(
"File write successful to app storage",
JSON.stringify(writeSuccessLogData, null, 2),
);
// Then share the file to let user choose where to save it
try {
await Share.share({
title: "TimeSafari Backup",
text: "Here is your TimeSafari backup file.",
url: writeResult.uri,
dialogTitle: "Save your backup",
});
logger.log(
"Share dialog shown for Android",
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
);
} catch (shareError) {
// Log share error but don't fail the operation
logger.error(
"Share dialog failed for Android",
JSON.stringify(
{
error: shareError,
timestamp: new Date().toISOString(),
},
null,
2,
),
);
}
}
} catch (error: unknown) {
const err = error as Error;
const finalErrorLogData = {
error: {
message: err.message,
name: err.name,
stack: err.stack,
},
timestamp: new Date().toISOString(),
};
logger.error(
"Error in writeFile operation:",
JSON.stringify(finalErrorLogData, null, 2),
);
throw new Error(`Failed to save file: ${err.message}`);
}
}
/**
* Writes content to a file in the device's app-private storage.
* Then shares the file using the system share dialog.
*
* Works on both Android and iOS without needing external storage permissions.
*
* @param fileName - The name of the file to create (e.g. "backup.json")
* @param content - The content to write to the file
*/
async writeAndShareFile(fileName: string, content: string): Promise<void> {
const timestamp = new Date().toISOString();
const logData = {
action: "writeAndShareFile",
fileName,
contentLength: content.length,
timestamp,
};
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
try {
// Check storage permissions before proceeding
await this.checkStoragePermissions();
const { uri } = await Filesystem.writeFile({
path: fileName,
data: content,
directory: Directory.Data,
encoding: Encoding.UTF8,
recursive: true,
});
logger.log("[CapacitorPlatformService] File write successful:", {
uri,
timestamp: new Date().toISOString(),
});
await Share.share({
title: "TimeSafari Backup",
text: "Here is your backup file.",
url: uri,
dialogTitle: "Share your backup file",
});
} catch (error) {
const err = error as Error;
const errLog = {
message: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
};
logger.error(
"[CapacitorPlatformService] Error writing or sharing file:",
JSON.stringify(errLog, null, 2),
);
throw new Error(`Failed to write or share file: ${err.message}`);
}
}
/**
* 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,
direction: this.currentDirection,
});
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" });
}
/**
* 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 === CameraDirection.Rear
? CameraDirection.Front
: CameraDirection.Rear;
logger.debug(`Camera rotated to ${this.currentDirection} camera`);
}
/**
* 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();
}
/**
* @see PlatformService.dbQuery
*/
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
await this.waitForInitialization();
return this.queueOperation<QueryExecResult>("query", sql, params || []);
}
/**
* @see PlatformService.dbExec
*/
async dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
await this.waitForInitialization();
return this.queueOperation<{ changes: number; lastId?: number }>(
"run",
sql,
params || [],
);
}
}