|
|
@ -5,7 +5,7 @@ import { |
|
|
|
} from "../PlatformService"; |
|
|
|
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; |
|
|
|
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; |
|
|
|
import { FilePicker } from "@capawesome/capacitor-file-picker"; |
|
|
|
import { Share } from "@capacitor/share"; |
|
|
|
import { logger } from "../../utils/logger"; |
|
|
|
|
|
|
|
/** |
|
|
@ -156,16 +156,37 @@ export class CapacitorPlatformService implements PlatformService { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Writes content to a file in the user-selected directory. |
|
|
|
* Opens a directory picker for the user to choose where to save. |
|
|
|
* @param path - Suggested filename |
|
|
|
* @param content - Content to write to the file |
|
|
|
* @throws Error if write operation fails |
|
|
|
* 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(path: string, content: string): Promise<void> { |
|
|
|
async writeFile(fileName: string, content: string): Promise<void> { |
|
|
|
try { |
|
|
|
const logData = { |
|
|
|
targetPath: path, |
|
|
|
targetFileName: fileName, |
|
|
|
contentLength: content.length, |
|
|
|
platform: this.getCapabilities().isIOS ? "iOS" : "Android", |
|
|
|
timestamp: new Date().toISOString(), |
|
|
@ -175,146 +196,97 @@ export class CapacitorPlatformService implements PlatformService { |
|
|
|
JSON.stringify(logData, null, 2), |
|
|
|
); |
|
|
|
|
|
|
|
// Check and request storage permissions if needed
|
|
|
|
await this.checkStoragePermissions(); |
|
|
|
|
|
|
|
// Let user pick save location first
|
|
|
|
const result = await FilePicker.pickDirectory(); |
|
|
|
const pickerLogData = { |
|
|
|
path: result.path, |
|
|
|
platform: this.getCapabilities().isIOS ? "iOS" : "Android", |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log("FilePicker result:", JSON.stringify(pickerLogData, null, 2)); |
|
|
|
|
|
|
|
// Handle paths based on platform
|
|
|
|
let cleanPath = result.path; |
|
|
|
// For Android, we need to handle content URIs differently
|
|
|
|
if (this.getCapabilities().isIOS) { |
|
|
|
const iosLogData = { |
|
|
|
originalPath: cleanPath, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log("Processing iOS path", JSON.stringify(iosLogData, null, 2)); |
|
|
|
cleanPath = result.path; |
|
|
|
} else { |
|
|
|
const androidLogData = { |
|
|
|
originalPath: cleanPath, |
|
|
|
// 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( |
|
|
|
"Processing Android path", |
|
|
|
JSON.stringify(androidLogData, null, 2), |
|
|
|
"File write successful", |
|
|
|
JSON.stringify(writeSuccessLogData, null, 2), |
|
|
|
); |
|
|
|
|
|
|
|
// For Android, use the content URI directly
|
|
|
|
if (cleanPath.startsWith("content://")) { |
|
|
|
const uriLogData = { |
|
|
|
uri: cleanPath, |
|
|
|
filename: path, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log( |
|
|
|
"Using content URI for Android:", |
|
|
|
JSON.stringify(uriLogData, null, 2), |
|
|
|
); |
|
|
|
|
|
|
|
// Extract the document ID from the content URI
|
|
|
|
const docIdMatch = cleanPath.match(/tree\/(.*?)(?:\/|$)/); |
|
|
|
if (docIdMatch) { |
|
|
|
const docId = docIdMatch[1]; |
|
|
|
const docIdLogData = { |
|
|
|
docId, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log( |
|
|
|
"Extracted document ID:", |
|
|
|
JSON.stringify(docIdLogData, null, 2), |
|
|
|
); |
|
|
|
|
|
|
|
// Use the document ID as the path
|
|
|
|
cleanPath = docId; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const finalPath = cleanPath; |
|
|
|
const finalPathLogData = { |
|
|
|
fullPath: finalPath, |
|
|
|
filename: path, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log( |
|
|
|
"Final path details:", |
|
|
|
JSON.stringify(finalPathLogData, null, 2), |
|
|
|
); |
|
|
|
|
|
|
|
// Write to the selected directory
|
|
|
|
const writeLogData = { |
|
|
|
path: finalPath, |
|
|
|
contentLength: content.length, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log( |
|
|
|
"Attempting file write:", |
|
|
|
JSON.stringify(writeLogData, null, 2), |
|
|
|
); |
|
|
|
|
|
|
|
try { |
|
|
|
if (this.getCapabilities().isIOS) { |
|
|
|
await Filesystem.writeFile({ |
|
|
|
path: finalPath, |
|
|
|
data: content, |
|
|
|
directory: Directory.Documents, |
|
|
|
recursive: true, |
|
|
|
encoding: Encoding.UTF8, |
|
|
|
// 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", |
|
|
|
}); |
|
|
|
} else { |
|
|
|
// For Android, use the content URI directly
|
|
|
|
const androidPath = `Download/${path}`; |
|
|
|
const directoryLogData = { |
|
|
|
path: androidPath, |
|
|
|
directory: Directory.ExternalStorage, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
|
|
|
|
logger.log( |
|
|
|
"Android path configuration:", |
|
|
|
JSON.stringify(directoryLogData, null, 2), |
|
|
|
"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, |
|
|
|
), |
|
|
|
); |
|
|
|
|
|
|
|
await Filesystem.writeFile({ |
|
|
|
path: androidPath, |
|
|
|
data: content, |
|
|
|
directory: Directory.ExternalStorage, |
|
|
|
recursive: true, |
|
|
|
encoding: Encoding.UTF8, |
|
|
|
}); |
|
|
|
} |
|
|
|
} 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: finalPath, |
|
|
|
path: writeResult.uri, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.log( |
|
|
|
"File write successful", |
|
|
|
"File write successful to app storage", |
|
|
|
JSON.stringify(writeSuccessLogData, null, 2), |
|
|
|
); |
|
|
|
} catch (writeError: unknown) { |
|
|
|
const error = writeError as Error; |
|
|
|
const writeErrorLogData = { |
|
|
|
error: { |
|
|
|
message: error.message, |
|
|
|
name: error.name, |
|
|
|
stack: error.stack, |
|
|
|
}, |
|
|
|
path: finalPath, |
|
|
|
contentLength: content.length, |
|
|
|
timestamp: new Date().toISOString(), |
|
|
|
}; |
|
|
|
logger.error( |
|
|
|
"File write failed:", |
|
|
|
JSON.stringify(writeErrorLogData, null, 2), |
|
|
|
); |
|
|
|
throw new Error(`Failed to write file: ${error.message}`); |
|
|
|
|
|
|
|
// 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; |
|
|
@ -330,9 +302,55 @@ export class CapacitorPlatformService implements PlatformService { |
|
|
|
"Error in writeFile operation:", |
|
|
|
JSON.stringify(finalErrorLogData, null, 2), |
|
|
|
); |
|
|
|
throw new Error( |
|
|
|
`Failed to save file to selected location: ${err.message}`, |
|
|
|
); |
|
|
|
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 { |
|
|
|
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}`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|