diff --git a/android/.gradle/buildOutputCleanup/cache.properties b/android/.gradle/buildOutputCleanup/cache.properties
index 2cb07cfc..55624b6e 100644
--- a/android/.gradle/buildOutputCleanup/cache.properties
+++ b/android/.gradle/buildOutputCleanup/cache.properties
@@ -1,2 +1,2 @@
-#Tue Apr 08 11:03:40 UTC 2025
+#Wed Apr 09 09:01:13 UTC 2025
gradle.version=8.11.1
diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index d4eda79d..a196dce3 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(':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 d0a6315f..d6020933 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -10,5 +10,9 @@
{
"pkg": "@capacitor/filesystem",
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
+ },
+ {
+ "pkg": "@capawesome/capacitor-file-picker",
+ "classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
}
]
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index 82d81801..bccee664 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -10,3 +10,6 @@ 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 ':capawesome-capacitor-file-picker'
+project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist
index 2a84039a..0776c2e9 100644
--- a/ios/App/App/Info.plist
+++ b/ios/App/App/Info.plist
@@ -45,5 +45,15 @@
UIViewControllerBasedStatusBarAppearance
+ UIFileSharingEnabled
+
+ LSSupportsOpeningDocumentsInPlace
+
+ UISupportsDocumentBrowser
+
+ NSPhotoLibraryAddUsageDescription
+ This app needs access to save exported files to your photo library.
+ NSPhotoLibraryUsageDescription
+ This app needs access to save exported files to your photo library.
diff --git a/package-lock.json b/package-lock.json
index c96becf4..57cde520 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",
+ "@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
"@ethersproject/hdnode": "^5.7.0",
@@ -2907,6 +2908,25 @@
"@capacitor/core": "^6.2.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",
+ "integrity": "sha512-ZgXbC3qOyKJrQh2bQIOLcjAYOdS8+ii1V0zaV56pMAw2i/pitdayvdBs7Da3tTw/eMdUNZ0lBYZcLN6NPQMIvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/capawesome-team/"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/capawesome"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": "^6.0.0"
+ }
+ },
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
diff --git a/package.json b/package.json
index 78e4eaec..2a7af222 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0",
+ "@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
"@ethersproject/hdnode": "^5.7.0",
diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue
index d07a91b5..54a35e2c 100644
--- a/src/components/DataExportSection.vue
+++ b/src/components/DataExportSection.vue
@@ -45,16 +45,15 @@ backup and database export, with platform-specific download instructions. * *
v-if="platformCapabilities.isIOS"
class="list-disc list-outside ml-4"
>
- On iOS: Choose "More..." and select a place in iCloud, or go "Back"
- and save to another location.
+ On iOS: You will be prompted to choose a location to save your backup
+ file.
- On Android: Choose "Open" and then share
-
- to your prefered place.
+ On Android: You will be prompted to choose a location to save your
+ backup file.
@@ -156,7 +155,7 @@ export default class DataExportSection extends Vue {
title: "Export Successful",
text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format."
- : "The backup has been saved to your device.",
+ : "Please choose a location to save your backup file.",
},
-1,
);
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index c416ddb4..7a185017 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -5,6 +5,7 @@ import {
} from "../PlatformService";
import { Filesystem, Directory } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
+import { FilePicker } from "@capawesome/capacitor-file-picker";
import { logger } from "../../utils/logger";
/**
@@ -48,17 +49,57 @@ export class CapacitorPlatformService implements PlatformService {
}
/**
- * Writes content to a file in the app's data directory.
- * @param path - Relative path where to write the file
+ * 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
*/
async writeFile(path: string, content: string): Promise {
- await Filesystem.writeFile({
- path,
- data: content,
- directory: Directory.Data,
- });
+ try {
+ // Let user pick save location first
+ const result = await FilePicker.pickDirectory();
+ logger.log("FilePicker result path:", result.path);
+
+ // Handle paths based on platform
+ let cleanPath = result.path;
+ if (this.getCapabilities().isIOS) {
+ // For iOS, keep content: prefix
+ cleanPath = result.path;
+ } else {
+ // For Android, extract the actual path from the content URI
+ const pathMatch = result.path.match(/tree\/(.*?)(?:\/|$)/);
+ logger.log("Path match result:", pathMatch);
+ if (pathMatch) {
+ const decodedPath = decodeURIComponent(pathMatch[1]);
+ logger.log("Decoded path:", decodedPath);
+ // Convert primary:Download to /storage/emulated/0/Download
+ cleanPath = decodedPath.replace('primary:', '/storage/emulated/0/');
+ }
+ }
+
+ // For Android, ensure we're using the correct external storage path
+ if (this.getCapabilities().isMobile && !this.getCapabilities().isIOS) {
+ logger.log("Before Android path conversion:", cleanPath);
+ cleanPath = cleanPath.replace('primary:', '/storage/emulated/0/');
+ logger.log("After Android path conversion:", cleanPath);
+ }
+
+ const finalPath = `${cleanPath}/${path}`;
+ logger.log("Final path for writeFile:", finalPath);
+
+ // Write to the selected directory
+ await Filesystem.writeFile({
+ path: finalPath,
+ data: content,
+ directory: Directory.External,
+ recursive: true,
+ });
+
+ } catch (error) {
+ logger.error("Error saving file:", error);
+ throw new Error("Failed to save file to selected location");
+ }
}
/**
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index fcf0847a..a0921b40 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -21,7 +21,7 @@ function safeStringify(obj: unknown) {
export const logger = {
log: (message: string, ...args: unknown[]) => {
- if (process.env.NODE_ENV !== "production") {
+ if (process.env.NODE_ENV !== "production" || process.env.VITE_PLATFORM === "capacitor") {
// eslint-disable-next-line no-console
console.log(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
@@ -29,7 +29,7 @@ export const logger = {
}
},
warn: (message: string, ...args: unknown[]) => {
- if (process.env.NODE_ENV !== "production") {
+ if (process.env.NODE_ENV !== "production" || process.env.VITE_PLATFORM === "capacitor") {
// eslint-disable-next-line no-console
console.warn(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";