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) : "";