feat(android): implement file picker for data export
- Add @capawesome/capacitor-file-picker dependency - Update DataExportSection UI text to reflect new file picker behavior - Implement file picker in CapacitorPlatformService - Add debug logging for path handling - Fix logger to show messages in Capacitor environment WIP: File path handling still needs refinement
This commit is contained in:
@@ -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
|
gradle.version=8.11.1
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ dependencies {
|
|||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-camera')
|
implementation project(':capacitor-camera')
|
||||||
implementation project(':capacitor-filesystem')
|
implementation project(':capacitor-filesystem')
|
||||||
|
implementation project(':capawesome-capacitor-file-picker')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,9 @@
|
|||||||
{
|
{
|
||||||
"pkg": "@capacitor/filesystem",
|
"pkg": "@capacitor/filesystem",
|
||||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capawesome/capacitor-file-picker",
|
||||||
|
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/c
|
|||||||
|
|
||||||
include ':capacitor-filesystem'
|
include ':capacitor-filesystem'
|
||||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
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')
|
||||||
|
|||||||
@@ -45,5 +45,15 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
|
<key>UISupportsDocumentBrowser</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>This app needs access to save exported files to your photo library.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>This app needs access to save exported files to your photo library.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@capacitor/core": "^6.2.0",
|
"@capacitor/core": "^6.2.0",
|
||||||
"@capacitor/filesystem": "^6.0.0",
|
"@capacitor/filesystem": "^6.0.0",
|
||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
@@ -2907,6 +2908,25 @@
|
|||||||
"@capacitor/core": "^6.2.0"
|
"@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": {
|
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"@capacitor/core": "^6.2.0",
|
"@capacitor/core": "^6.2.0",
|
||||||
"@capacitor/filesystem": "^6.0.0",
|
"@capacitor/filesystem": "^6.0.0",
|
||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
|
|||||||
@@ -45,16 +45,15 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
v-if="platformCapabilities.isIOS"
|
v-if="platformCapabilities.isIOS"
|
||||||
class="list-disc list-outside ml-4"
|
class="list-disc list-outside ml-4"
|
||||||
>
|
>
|
||||||
On iOS: Choose "More..." and select a place in iCloud, or go "Back"
|
On iOS: You will be prompted to choose a location to save your backup
|
||||||
and save to another location.
|
file.
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
||||||
class="list-disc list-outside ml-4"
|
class="list-disc list-outside ml-4"
|
||||||
>
|
>
|
||||||
On Android: Choose "Open" and then share
|
On Android: You will be prompted to choose a location to save your
|
||||||
<font-awesome icon="share-nodes" class="fa-fw" />
|
backup file.
|
||||||
to your prefered place.
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +155,7 @@ export default class DataExportSection extends Vue {
|
|||||||
title: "Export Successful",
|
title: "Export Successful",
|
||||||
text: this.platformCapabilities.hasFileDownload
|
text: this.platformCapabilities.hasFileDownload
|
||||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
? "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,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { Filesystem, Directory } from "@capacitor/filesystem";
|
import { Filesystem, Directory } from "@capacitor/filesystem";
|
||||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||||
|
import { FilePicker } from "@capawesome/capacitor-file-picker";
|
||||||
import { logger } from "../../utils/logger";
|
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.
|
* Writes content to a file in the user-selected directory.
|
||||||
* @param path - Relative path where to write the file
|
* Opens a directory picker for the user to choose where to save.
|
||||||
|
* @param path - Suggested filename
|
||||||
* @param content - Content to write to the file
|
* @param content - Content to write to the file
|
||||||
* @throws Error if write operation fails
|
* @throws Error if write operation fails
|
||||||
*/
|
*/
|
||||||
async writeFile(path: string, content: string): Promise<void> {
|
async writeFile(path: string, content: string): Promise<void> {
|
||||||
await Filesystem.writeFile({
|
try {
|
||||||
path,
|
// Let user pick save location first
|
||||||
data: content,
|
const result = await FilePicker.pickDirectory();
|
||||||
directory: Directory.Data,
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function safeStringify(obj: unknown) {
|
|||||||
|
|
||||||
export const logger = {
|
export const logger = {
|
||||||
log: (message: string, ...args: unknown[]) => {
|
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
|
// eslint-disable-next-line no-console
|
||||||
console.log(message, ...args);
|
console.log(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
@@ -29,7 +29,7 @@ export const logger = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
warn: (message: string, ...args: unknown[]) => {
|
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
|
// eslint-disable-next-line no-console
|
||||||
console.warn(message, ...args);
|
console.warn(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user