diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index a196dce3..a54399fa 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -12,6 +12,7 @@ dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-camera')
implementation project(':capacitor-filesystem')
+ implementation project(':capacitor-share')
implementation project(':capawesome-capacitor-file-picker')
}
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index d6020933..30b5ba98 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -11,6 +11,10 @@
"pkg": "@capacitor/filesystem",
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
},
+ {
+ "pkg": "@capacitor/share",
+ "classpath": "com.capacitorjs.plugins.share.SharePlugin"
+ },
{
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
index bd0c4d80..680bb50e 100644
--- a/android/app/src/main/res/xml/file_paths.xml
+++ b/android/app/src/main/res/xml/file_paths.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index bccee664..736eac60 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -11,5 +11,8 @@ project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/c
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
+include ':capacitor-share'
+project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
+
include ':capawesome-capacitor-file-picker'
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
diff --git a/package-lock.json b/package-lock.json
index cc5e7d01..dd158e30 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0",
+ "@capacitor/share": "^6.0.3",
"@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
@@ -2908,6 +2909,15 @@
"@capacitor/core": "^6.2.0"
}
},
+ "node_modules/@capacitor/share": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-6.0.3.tgz",
+ "integrity": "sha512-BkNM73Ix+yxQ7fkni8CrrGcp1kSl7u+YNoPLwWKQ1MuQ5Uav0d+CT8M67ie+3dc4jASmegnzlC6tkTmFcPTLeA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": "^6.0.0"
+ }
+ },
"node_modules/@capawesome/capacitor-file-picker": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-file-picker/-/capacitor-file-picker-6.2.0.tgz",
diff --git a/package.json b/package.json
index 4d7ce2ea..686caad4 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0",
+ "@capacitor/share": "^6.0.3",
"@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue
index 54a35e2c..647beb19 100644
--- a/src/components/DataExportSection.vue
+++ b/src/components/DataExportSection.vue
@@ -145,7 +145,7 @@ export default class DataExportSection extends Vue {
} else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory
const content = await blob.text();
- await this.platformService.writeFile(fileName, content);
+ await this.platformService.writeAndShareFile(fileName, content);
}
this.$notify(
diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts
index 5a2c9209..574b1a3a 100644
--- a/src/services/PlatformService.ts
+++ b/src/services/PlatformService.ts
@@ -57,6 +57,14 @@ export interface PlatformService {
*/
writeFile(path: string, content: string): Promise;
+ /**
+ * Writes content to a file at the specified path and shares it.
+ * @param fileName - The filename of the file to write
+ * @param content - The content to write to the file
+ * @returns Promise that resolves when the write is complete
+ */
+ writeAndShareFile(fileName: string, content: string): Promise;
+
/**
* Deletes a file at the specified path.
* @param path - The path to the file to delete
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 68ce715d..9c655eb6 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -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 {
+ async writeFile(fileName: string, content: string): Promise {
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 {
+ 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}`);
}
}