|
|
|
@@ -41,7 +41,7 @@ interface QueuedOperation {
|
|
|
|
|
*/
|
|
|
|
|
export class CapacitorPlatformService implements PlatformService {
|
|
|
|
|
/** Current camera direction */
|
|
|
|
|
private currentDirection: CameraDirection = "BACK";
|
|
|
|
|
private currentDirection: CameraDirection = "BACK" as CameraDirection;
|
|
|
|
|
|
|
|
|
|
private sqlite: SQLiteConnection;
|
|
|
|
|
private db: SQLiteDBConnection | null = null;
|
|
|
|
@@ -379,174 +379,34 @@ export class CapacitorPlatformService implements PlatformService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Writes content to a file in the app's safe storage and offers sharing.
|
|
|
|
|
* Enhanced file save and share functionality with location selection.
|
|
|
|
|
*
|
|
|
|
|
* 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
|
|
|
|
|
* Provides multiple options for saving files:
|
|
|
|
|
* 1. Save to app-private storage and share (current behavior)
|
|
|
|
|
* 2. Save to device Downloads folder (Android) or Documents (iOS)
|
|
|
|
|
* 3. Allow user to choose save location via file picker
|
|
|
|
|
* 4. Direct share without saving locally
|
|
|
|
|
*
|
|
|
|
|
* @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)
|
|
|
|
|
* );
|
|
|
|
|
* ```
|
|
|
|
|
* @param options - Additional options for file saving behavior
|
|
|
|
|
* @returns Promise resolving to save/share result
|
|
|
|
|
*/
|
|
|
|
|
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> {
|
|
|
|
|
async writeAndShareFile(
|
|
|
|
|
fileName: string,
|
|
|
|
|
content: string,
|
|
|
|
|
options: {
|
|
|
|
|
allowLocationSelection?: boolean;
|
|
|
|
|
saveToDownloads?: boolean;
|
|
|
|
|
mimeType?: string;
|
|
|
|
|
} = {}
|
|
|
|
|
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
const logData = {
|
|
|
|
|
action: "writeAndShareFile",
|
|
|
|
|
fileName,
|
|
|
|
|
contentLength: content.length,
|
|
|
|
|
options,
|
|
|
|
|
timestamp,
|
|
|
|
|
};
|
|
|
|
|
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
|
|
|
|
@@ -555,25 +415,54 @@ export class CapacitorPlatformService implements PlatformService {
|
|
|
|
|
// Check storage permissions before proceeding
|
|
|
|
|
await this.checkStoragePermissions();
|
|
|
|
|
|
|
|
|
|
const { uri } = await Filesystem.writeFile({
|
|
|
|
|
let fileUri: string;
|
|
|
|
|
let saved = false;
|
|
|
|
|
|
|
|
|
|
// Determine save strategy based on options and platform
|
|
|
|
|
if (options.allowLocationSelection) {
|
|
|
|
|
// Use file picker to let user choose location
|
|
|
|
|
fileUri = await this.saveFileWithPicker(fileName, content, options.mimeType);
|
|
|
|
|
saved = true;
|
|
|
|
|
} else if (options.saveToDownloads) {
|
|
|
|
|
// Save directly to Downloads folder
|
|
|
|
|
fileUri = await this.saveToDownloads(fileName, content);
|
|
|
|
|
saved = true;
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to app-private storage (current behavior)
|
|
|
|
|
const result = await Filesystem.writeFile({
|
|
|
|
|
path: fileName,
|
|
|
|
|
data: content,
|
|
|
|
|
directory: Directory.Data,
|
|
|
|
|
encoding: Encoding.UTF8,
|
|
|
|
|
recursive: true,
|
|
|
|
|
});
|
|
|
|
|
fileUri = result.uri;
|
|
|
|
|
saved = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.log("[CapacitorPlatformService] File write successful:", {
|
|
|
|
|
uri,
|
|
|
|
|
uri: fileUri,
|
|
|
|
|
saved,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Share the file
|
|
|
|
|
let shared = false;
|
|
|
|
|
try {
|
|
|
|
|
await Share.share({
|
|
|
|
|
title: "TimeSafari Backup",
|
|
|
|
|
text: "Here is your backup file.",
|
|
|
|
|
url: uri,
|
|
|
|
|
url: fileUri,
|
|
|
|
|
dialogTitle: "Share your backup file",
|
|
|
|
|
});
|
|
|
|
|
shared = true;
|
|
|
|
|
logger.log("[CapacitorPlatformService] File shared successfully");
|
|
|
|
|
} catch (shareError) {
|
|
|
|
|
logger.warn("[CapacitorPlatformService] Share failed, but file was saved:", shareError);
|
|
|
|
|
// Don't throw error if sharing fails, file is still saved
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { saved, uri: fileUri, shared };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const err = error as Error;
|
|
|
|
|
const errLog = {
|
|
|
|
@@ -585,7 +474,74 @@ export class CapacitorPlatformService implements PlatformService {
|
|
|
|
|
"[CapacitorPlatformService] Error writing or sharing file:",
|
|
|
|
|
JSON.stringify(errLog, null, 2),
|
|
|
|
|
);
|
|
|
|
|
throw new Error(`Failed to write or share file: ${err.message}`);
|
|
|
|
|
return { saved: false, shared: false, error: err.message };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Saves a file using the file picker to let user choose location.
|
|
|
|
|
* @param fileName - Name of the file to save
|
|
|
|
|
* @param content - File content
|
|
|
|
|
* @param mimeType - MIME type of the file
|
|
|
|
|
* @returns Promise resolving to the saved file URI
|
|
|
|
|
*/
|
|
|
|
|
private async saveFileWithPicker(
|
|
|
|
|
fileName: string,
|
|
|
|
|
content: string,
|
|
|
|
|
mimeType: string = "application/json"
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
try {
|
|
|
|
|
// For now, fallback to regular save since file picker save API is complex
|
|
|
|
|
// Save to app-private storage and let user share to choose location
|
|
|
|
|
const result = await Filesystem.writeFile({
|
|
|
|
|
path: fileName,
|
|
|
|
|
data: content,
|
|
|
|
|
directory: Directory.Data,
|
|
|
|
|
encoding: Encoding.UTF8,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.log("[CapacitorPlatformService] File saved to app storage for picker fallback:", {
|
|
|
|
|
uri: result.uri,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result.uri;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("[CapacitorPlatformService] File picker save failed:", error);
|
|
|
|
|
throw new Error(`Failed to save file with picker: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Saves a file directly to the Downloads folder (Android) or Documents (iOS).
|
|
|
|
|
* @param fileName - Name of the file to save
|
|
|
|
|
* @param content - File content
|
|
|
|
|
* @returns Promise resolving to the saved file URI
|
|
|
|
|
*/
|
|
|
|
|
private async saveToDownloads(fileName: string, content: string): Promise<string> {
|
|
|
|
|
try {
|
|
|
|
|
if (this.getCapabilities().isIOS) {
|
|
|
|
|
// iOS: Save to Documents directory
|
|
|
|
|
const result = await Filesystem.writeFile({
|
|
|
|
|
path: fileName,
|
|
|
|
|
data: content,
|
|
|
|
|
directory: Directory.Documents,
|
|
|
|
|
encoding: Encoding.UTF8,
|
|
|
|
|
});
|
|
|
|
|
return result.uri;
|
|
|
|
|
} else {
|
|
|
|
|
// Android: Save to Downloads directory
|
|
|
|
|
const result = await Filesystem.writeFile({
|
|
|
|
|
path: fileName,
|
|
|
|
|
data: content,
|
|
|
|
|
directory: Directory.External,
|
|
|
|
|
encoding: Encoding.UTF8,
|
|
|
|
|
});
|
|
|
|
|
return result.uri;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("[CapacitorPlatformService] Save to downloads failed:", error);
|
|
|
|
|
throw new Error(`Failed to save to downloads: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -701,7 +657,9 @@ export class CapacitorPlatformService implements PlatformService {
|
|
|
|
|
* @returns Promise that resolves when the camera is rotated
|
|
|
|
|
*/
|
|
|
|
|
async rotateCamera(): Promise<void> {
|
|
|
|
|
this.currentDirection = this.currentDirection === "BACK" ? "FRONT" : "BACK";
|
|
|
|
|
this.currentDirection = this.currentDirection === "BACK" as CameraDirection
|
|
|
|
|
? "FRONT" as CameraDirection
|
|
|
|
|
: "BACK" as CameraDirection;
|
|
|
|
|
logger.debug(`Camera rotated to ${this.currentDirection} camera`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -738,4 +696,60 @@ export class CapacitorPlatformService implements PlatformService {
|
|
|
|
|
params || [],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Writes content to a file in the app's safe storage and offers sharing.
|
|
|
|
|
* @param path - The path where the file should be written
|
|
|
|
|
* @param content - The content to write to the file
|
|
|
|
|
* @returns Promise that resolves when the write is complete
|
|
|
|
|
*/
|
|
|
|
|
async writeFile(path: string, content: string): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
// Check storage permissions before proceeding
|
|
|
|
|
await this.checkStoragePermissions();
|
|
|
|
|
|
|
|
|
|
const logData = {
|
|
|
|
|
targetPath: path,
|
|
|
|
|
contentLength: content.length,
|
|
|
|
|
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
logger.log(
|
|
|
|
|
"Starting writeFile operation",
|
|
|
|
|
JSON.stringify(logData, null, 2),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Write to app's Data directory
|
|
|
|
|
const writeResult = await Filesystem.writeFile({
|
|
|
|
|
path,
|
|
|
|
|
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),
|
|
|
|
|
);
|
|
|
|
|
} 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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|