If no download happened yet, click again here to download now.
-
-
- After the download, you can save the file in your preferred storage
- location.
+
+
+ {{ instruction }}
-
- -
- On iOS: Choose "More..." and select a place in iCloud, or go "Back"
- and save to another location.
-
- -
- On Android: Choose "Open" and then share
-
- to your prefered place.
-
-
@@ -100,60 +89,72 @@ export default class DataExportSection extends Vue {
/**
* Platform service instance for platform-specific operations
*/
- private platformService: PlatformService = PlatformServiceFactory.getInstance();
-
- /**
- * Whether the current platform is iOS
- */
- private get isIOS(): boolean {
- return /iPad|iPhone|iPod/.test(navigator.userAgent);
- }
-
- /**
- * Whether to show platform-specific instructions
- */
- private get showPlatformInstructions(): boolean {
- return this.platformService.isCapacitor();
- }
+ private platformService?: PlatformService;
/**
* Lifecycle hook to clean up resources
* Revokes object URL when component is unmounted (web platform only)
*/
beforeUnmount() {
- if (this.downloadUrl && this.platformService.isWeb()) {
+ if (this.downloadUrl && this.platformService?.needsDownloadCleanup()) {
URL.revokeObjectURL(this.downloadUrl);
}
}
+ async mounted() {
+ this.platformService = await PlatformServiceFactory.getInstance();
+ logger.log(
+ "DataExportSection mounted on platform:",
+ process.env.VITE_PLATFORM,
+ );
+ }
+
/**
* Exports the database to a JSON file
* Uses platform-specific methods for saving the exported data
* Shows success/error notifications to user
- *
+ *
* @throws {Error} If export fails
* @emits {Notification} Success or error notification
*/
public async exportDatabase() {
try {
+ if (!this.platformService) {
+ this.platformService = await PlatformServiceFactory.getInstance();
+ }
+ logger.log(
+ "Starting database export on platform:",
+ process.env.VITE_PLATFORM,
+ );
+
const blob = await db.export({ prettyJson: true });
const fileName = `${db.name}-backup.json`;
+ logger.log("Database export details:", {
+ fileName,
+ blobSize: `${blob.size} bytes`,
+ platform: process.env.VITE_PLATFORM,
+ });
await this.platformService.exportDatabase(blob, fileName);
+ logger.log("Database export completed successfully:", {
+ fileName,
+ platform: process.env.VITE_PLATFORM,
+ });
this.$notify(
{
group: "alert",
type: "success",
title: "Export Successful",
- text: this.platformService.isWeb()
- ? "See your downloads directory for the backup. It is in the Dexie format."
- : "The backup has been saved to your device.",
+ text: this.platformService.getExportSuccessMessage(),
},
-1,
);
} catch (error) {
- logger.error("Export Error:", error);
+ logger.error("Database export failed:", {
+ error,
+ platform: process.env.VITE_PLATFORM,
+ });
this.$notify(
{
group: "alert",
@@ -172,7 +173,8 @@ export default class DataExportSection extends Vue {
*/
public computedStartDownloadLinkClassNames() {
return {
- hidden: this.downloadUrl && this.platformService.isWeb(),
+ hidden:
+ this.downloadUrl && this.platformService?.needsSecondaryDownloadLink(),
};
}
@@ -182,7 +184,9 @@ export default class DataExportSection extends Vue {
*/
public computedDownloadLinkClassNames() {
return {
- hidden: !this.downloadUrl || !this.platformService.isWeb(),
+ hidden:
+ !this.downloadUrl ||
+ !this.platformService?.needsSecondaryDownloadLink(),
};
}
}
diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue
index 561c96ee..15a93f43 100644
--- a/src/components/PhotoDialog.vue
+++ b/src/components/PhotoDialog.vue
@@ -108,6 +108,7 @@ import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
+import { PlatformService } from "../services/PlatformService";
@Component({ components: { VuePictureCropper } })
export default class PhotoDialog extends Vue {
@@ -123,13 +124,14 @@ export default class PhotoDialog extends Vue {
uploading = false;
visible = false;
- private platformService = PlatformServiceFactory.getInstance();
+ private platformService?: PlatformService;
URL = window.URL || window.webkitURL;
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
+ this.platformService = await PlatformServiceFactory.getInstance();
} catch (err: unknown) {
logger.error("Error retrieving settings from database:", err);
this.$notify(
@@ -137,7 +139,10 @@ export default class PhotoDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
- text: err.message || "There was an error retrieving your settings.",
+ text:
+ err instanceof Error
+ ? err.message
+ : "There was an error retrieving your settings.",
},
-1,
);
@@ -181,6 +186,9 @@ export default class PhotoDialog extends Vue {
async takePhoto() {
try {
+ if (!this.platformService) {
+ this.platformService = await PlatformServiceFactory.getInstance();
+ }
const result = await this.platformService.takePicture();
this.blob = result.blob;
this.fileName = result.fileName;
@@ -200,6 +208,9 @@ export default class PhotoDialog extends Vue {
async pickPhoto() {
try {
+ if (!this.platformService) {
+ this.platformService = await PlatformServiceFactory.getInstance();
+ }
const result = await this.platformService.pickImage();
this.blob = result.blob;
this.fileName = result.fileName;
diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts
index c58f6d71..20dff449 100644
--- a/src/services/PlatformService.ts
+++ b/src/services/PlatformService.ts
@@ -100,4 +100,28 @@ export interface PlatformService {
* @returns Promise that resolves when the deep link has been handled
*/
handleDeepLink(url: string): Promise
;
+
+ /**
+ * Gets platform-specific instructions for saving exported files
+ * @returns Array of instruction strings for the current platform
+ */
+ getExportInstructions(): string[];
+
+ /**
+ * Gets the success message for database export
+ * @returns Success message appropriate for the current platform
+ */
+ getExportSuccessMessage(): string;
+
+ /**
+ * Checks if the platform requires a secondary download link
+ * @returns true if platform needs a secondary download link
+ */
+ needsSecondaryDownloadLink(): boolean;
+
+ /**
+ * Checks if the platform needs cleanup after download
+ * @returns true if platform needs cleanup after download
+ */
+ needsDownloadCleanup(): boolean;
}
diff --git a/src/services/PlatformServiceFactory.ts b/src/services/PlatformServiceFactory.ts
index 6dca11b8..42e15121 100644
--- a/src/services/PlatformServiceFactory.ts
+++ b/src/services/PlatformServiceFactory.ts
@@ -1,20 +1,17 @@
import { PlatformService } from "./PlatformService";
import { WebPlatformService } from "./platforms/WebPlatformService";
-import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
-import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
-import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
/**
* Factory class for creating platform-specific service implementations.
* Implements the Singleton pattern to ensure only one instance of PlatformService exists.
- *
+ *
* The factory determines which platform implementation to use based on the VITE_PLATFORM
* environment variable. Supported platforms are:
* - capacitor: Mobile platform using Capacitor
* - electron: Desktop platform using Electron
* - pywebview: Python WebView implementation
* - web: Default web platform (fallback)
- *
+ *
* @example
* ```typescript
* const platformService = PlatformServiceFactory.getInstance();
@@ -27,32 +24,46 @@ export class PlatformServiceFactory {
/**
* Gets or creates the singleton instance of PlatformService.
* Creates the appropriate platform-specific implementation based on environment.
- *
+ *
* @returns {PlatformService} The singleton instance of PlatformService
*/
- public static getInstance(): PlatformService {
+ public static async getInstance(): Promise {
if (PlatformServiceFactory.instance) {
return PlatformServiceFactory.instance;
}
const platform = process.env.VITE_PLATFORM || "web";
+ let service: PlatformService;
switch (platform) {
- case "capacitor":
- PlatformServiceFactory.instance = new CapacitorPlatformService();
+ case "capacitor": {
+ const { CapacitorPlatformService } = await import(
+ "./platforms/CapacitorPlatformService"
+ );
+ service = new CapacitorPlatformService();
break;
- case "electron":
- PlatformServiceFactory.instance = new ElectronPlatformService();
+ }
+ case "electron": {
+ const { ElectronPlatformService } = await import(
+ "./platforms/ElectronPlatformService"
+ );
+ service = new ElectronPlatformService();
break;
- case "pywebview":
- PlatformServiceFactory.instance = new PyWebViewPlatformService();
+ }
+ case "pywebview": {
+ const { PyWebViewPlatformService } = await import(
+ "./platforms/PyWebViewPlatformService"
+ );
+ service = new PyWebViewPlatformService();
break;
+ }
case "web":
default:
- PlatformServiceFactory.instance = new WebPlatformService();
+ service = new WebPlatformService();
break;
}
- return PlatformServiceFactory.instance;
+ PlatformServiceFactory.instance = service;
+ return service;
}
}
diff --git a/src/services/api.ts b/src/services/api.ts
index 5869abf8..3235100e 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -1,7 +1,7 @@
/**
* API error handling utilities for the application.
* Provides centralized error handling for API requests with platform-specific logging.
- *
+ *
* @module api
*/
@@ -10,12 +10,12 @@ import { logger } from "../utils/logger";
/**
* Handles API errors with platform-specific logging and error processing.
- *
+ *
* @param error - The Axios error object from the failed request
* @param endpoint - The API endpoint that was called
* @returns null for rate limit errors (400), throws the error otherwise
* @throws The original error for non-rate-limit cases
- *
+ *
* @remarks
* Special handling includes:
* - Enhanced logging for Capacitor platform
@@ -25,7 +25,7 @@ import { logger } from "../utils/logger";
* - HTTP status
* - Response data
* - Request configuration (URL, method, headers)
- *
+ *
* @example
* ```typescript
* try {
diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts
index 4681019a..20e8cce0 100644
--- a/src/services/deepLinks.ts
+++ b/src/services/deepLinks.ts
@@ -25,7 +25,7 @@
*
* Deep Link Format:
* timesafari://[/][?queryParam1=value1&queryParam2=value2]
- *
+ *
* Supported Routes:
* - user-profile: View user profile
* - project-details: View project details
@@ -73,7 +73,7 @@ export class DeepLinkHandler {
/**
* Parses deep link URL into path, params and query components.
* Validates URL structure using Zod schemas.
- *
+ *
* @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid
* @returns Parsed URL components (path, params, query)
@@ -111,7 +111,7 @@ export class DeepLinkHandler {
/**
* Processes incoming deep links and routes them appropriately.
* Handles validation, error handling, and routing to the correct view.
- *
+ *
* @param url - The deep link URL to process
* @throws {DeepLinkError} If URL processing fails
*/
@@ -142,7 +142,7 @@ export class DeepLinkHandler {
/**
* Routes the deep link to appropriate view with validated parameters.
* Validates route and parameters using Zod schemas before routing.
- *
+ *
* @param path - The route path from the deep link
* @param params - URL parameters
* @param query - Query string parameters
diff --git a/src/services/plan.ts b/src/services/plan.ts
index a730d63a..1419b22c 100644
--- a/src/services/plan.ts
+++ b/src/services/plan.ts
@@ -1,7 +1,7 @@
/**
* Plan service module for handling plan and claim data loading.
* Provides functionality to load plans with retry mechanism and error handling.
- *
+ *
* @module plan
*/
@@ -26,11 +26,11 @@ interface PlanResponse {
/**
* Loads a plan with automatic retry mechanism.
* Attempts to load the plan multiple times in case of failure.
- *
+ *
* @param handle - The unique identifier for the plan or claim
* @param retries - Number of retry attempts (default: 3)
* @returns Promise resolving to PlanResponse
- *
+ *
* @remarks
* - Implements exponential backoff with 1 second delay between retries
* - Provides detailed logging of each attempt and any errors
@@ -39,7 +39,7 @@ interface PlanResponse {
* - HTTP status and headers
* - Response data
* - Request configuration
- *
+ *
* @example
* ```typescript
* const response = await loadPlanWithRetry('plan-123');
@@ -104,11 +104,11 @@ export const loadPlanWithRetry = async (
/**
* Makes a single API request to load a plan or claim.
* Determines the appropriate endpoint based on the handle.
- *
+ *
* @param handle - The unique identifier for the plan or claim
* @returns Promise resolving to PlanResponse
* @throws Will throw an error if the API request fails
- *
+ *
* @remarks
* - Automatically detects claim vs plan endpoints based on handle
* - Uses axios for HTTP requests
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index b3bcffa8..b2f62088 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -66,7 +66,9 @@ export class CapacitorPlatformService implements PlatformService {
path: directory,
directory: Directory.Data,
});
- return result.files.map(file => typeof file === 'string' ? file : file.name);
+ return result.files.map((file) =>
+ typeof file === "string" ? file : file.name,
+ );
}
/**
@@ -190,17 +192,50 @@ export class CapacitorPlatformService implements PlatformService {
return Promise.resolve();
}
+ getExportInstructions(): string[] {
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+ if (isIOS) {
+ return [
+ "On iOS: Choose 'More...' and select a place in iCloud, or go 'Back' and save to another location.",
+ ];
+ } else {
+ return [
+ "On Android: Choose 'Open' and then share to your preferred place.",
+ ];
+ }
+ }
+
+ getExportSuccessMessage(): string {
+ return "The backup has been saved to your device.";
+ }
+
+ needsSecondaryDownloadLink(): boolean {
+ return false;
+ }
+
+ needsDownloadCleanup(): boolean {
+ return false;
+ }
+
async exportDatabase(blob: Blob, fileName: string): Promise {
+ logger.log("Starting database export on Capacitor platform:", {
+ fileName,
+ blobSize: `${blob.size} bytes`,
+ });
+
// Create a File object from the Blob
- const file = new File([blob], fileName, { type: 'application/json' });
-
+ const file = new File([blob], fileName, { type: "application/json" });
+
try {
+ logger.log("Attempting to use native share sheet");
// Use the native share sheet
await navigator.share({
files: [file],
title: fileName,
});
+ logger.log("Database export completed via native share sheet");
} catch (error) {
+ logger.log("Native share failed, falling back to Capacitor Share API");
// Fallback to Capacitor Share API if Web Share API fails
// First save to temporary file
const base64Data = await this.blobToBase64(blob);
@@ -208,23 +243,26 @@ export class CapacitorPlatformService implements PlatformService {
path: fileName,
data: base64Data,
directory: Directory.Cache, // Use Cache instead of Documents for temporary files
- recursive: true
+ recursive: true,
});
-
+ logger.log("Temporary file created for sharing:", result.uri);
+
// Then share using Capacitor Share API
await Share.share({
title: fileName,
- url: result.uri
+ url: result.uri,
});
-
+ logger.log("Database export completed via Capacitor Share API");
+
// Clean up the temporary file
try {
await Filesystem.deleteFile({
path: fileName,
- directory: Directory.Cache
+ directory: Directory.Cache,
});
+ logger.log("Temporary file cleaned up successfully");
} catch (cleanupError) {
- logger.warn('Failed to clean up temporary file:', cleanupError);
+ logger.warn("Failed to clean up temporary file:", cleanupError);
}
}
}
@@ -234,7 +272,7 @@ export class CapacitorPlatformService implements PlatformService {
const reader = new FileReader();
reader.onloadend = () => {
const base64data = reader.result as string;
- resolve(base64data.split(',')[1]);
+ resolve(base64data.split(",")[1]);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts
index 0a99cd43..b7d4955b 100644
--- a/src/services/platforms/ElectronPlatformService.ts
+++ b/src/services/platforms/ElectronPlatformService.ts
@@ -5,7 +5,7 @@ import { logger } from "../../utils/logger";
* Platform service implementation for Electron (desktop) platform.
* Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
- *
+ *
* @remarks
* This service is intended for desktop application functionality through Electron.
* Future implementations should provide:
@@ -121,4 +121,16 @@ export class ElectronPlatformService implements PlatformService {
logger.error("handleDeepLink not implemented in Electron platform");
throw new Error("Not implemented");
}
+
+ /**
+ * Exports a database blob to a file.
+ * @param _blob - The database blob to export
+ * @param _fileName - The name of the file to save
+ * @throws Error with "Not implemented" message
+ * @todo Implement file export using Electron's file system API
+ */
+ async exportDatabase(_blob: Blob, _fileName: string): Promise {
+ logger.error("exportDatabase not implemented in Electron platform");
+ throw new Error("Not implemented");
+ }
}
diff --git a/src/services/platforms/PyWebViewPlatformService.ts b/src/services/platforms/PyWebViewPlatformService.ts
index 0e118297..a4832d48 100644
--- a/src/services/platforms/PyWebViewPlatformService.ts
+++ b/src/services/platforms/PyWebViewPlatformService.ts
@@ -5,7 +5,7 @@ import { logger } from "../../utils/logger";
* Platform service implementation for PyWebView platform.
* Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
- *
+ *
* @remarks
* This service is intended for Python-based desktop applications using pywebview.
* Future implementations should provide:
@@ -122,4 +122,16 @@ export class PyWebViewPlatformService implements PlatformService {
logger.error("handleDeepLink not implemented in PyWebView platform");
throw new Error("Not implemented");
}
+
+ /**
+ * Exports a database blob to a file using the Python backend.
+ * @param _blob - The database blob to export
+ * @param _fileName - The name of the file to save
+ * @throws Error with "Not implemented" message
+ * @todo Implement file export through pywebview's Python-JavaScript bridge
+ */
+ async exportDatabase(_blob: Blob, _fileName: string): Promise {
+ logger.error("exportDatabase not implemented in PyWebView platform");
+ throw new Error("Not implemented");
+ }
}
diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts
index 7e94357f..883904fd 100644
--- a/src/services/platforms/WebPlatformService.ts
+++ b/src/services/platforms/WebPlatformService.ts
@@ -4,13 +4,13 @@ import { logger } from "../../utils/logger";
/**
* Platform service implementation for web browser platform.
* Implements the PlatformService interface with web-specific functionality.
- *
+ *
* @remarks
* This service provides web-based implementations for:
* - Image capture using the browser's file input
* - Image selection from local filesystem
* - Image processing and conversion
- *
+ *
* Note: File system operations are not available in the web platform
* due to browser security restrictions. These methods throw appropriate errors.
*/
@@ -55,10 +55,10 @@ export class WebPlatformService implements PlatformService {
/**
* Opens a file input dialog configured for camera capture.
* Creates a temporary file input element to access the device camera.
- *
+ *
* @returns Promise resolving to the captured image data
* @throws Error if image capture fails or no image is selected
- *
+ *
* @remarks
* Uses the 'capture' attribute to prefer the device camera.
* Falls back to file selection if camera is not available.
@@ -96,10 +96,10 @@ export class WebPlatformService implements PlatformService {
/**
* Opens a file input dialog for selecting an image file.
* Creates a temporary file input element to access local files.
- *
+ *
* @returns Promise resolving to the selected image data
* @throws Error if image processing fails or no image is selected
- *
+ *
* @remarks
* Allows selection of any image file type.
* Processes the selected image to ensure consistent format.
@@ -135,11 +135,11 @@ export class WebPlatformService implements PlatformService {
/**
* Processes an image file to ensure consistent format.
* Converts the file to a data URL and then to a Blob.
- *
+ *
* @param file - The image File object to process
* @returns Promise resolving to processed image Blob
* @throws Error if file reading or conversion fails
- *
+ *
* @remarks
* This method ensures consistent image format across different
* input sources by converting through data URL to Blob.
@@ -201,7 +201,7 @@ export class WebPlatformService implements PlatformService {
/**
* Handles deep link URLs in the web platform.
* Deep links are handled through URL parameters in the web environment.
- *
+ *
* @param _url - The deep link URL to handle (unused in web implementation)
* @returns Promise that resolves immediately as web handles URLs naturally
*/
@@ -210,12 +210,52 @@ export class WebPlatformService implements PlatformService {
return Promise.resolve();
}
+ getExportInstructions(): string[] {
+ return [
+ "After the download, you can save the file in your preferred storage location.",
+ ];
+ }
+
+ getExportSuccessMessage(): string {
+ return "See your downloads directory for the backup. It is in the Dexie format.";
+ }
+
+ needsSecondaryDownloadLink(): boolean {
+ return true;
+ }
+
+ needsDownloadCleanup(): boolean {
+ return true;
+ }
+
async exportDatabase(blob: Blob, fileName: string): Promise {
- const downloadUrl = URL.createObjectURL(blob);
- const downloadAnchor = document.createElement('a');
- downloadAnchor.href = downloadUrl;
- downloadAnchor.download = fileName;
- downloadAnchor.click();
- setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
+ logger.log("Starting database export on web platform:", {
+ fileName,
+ blobSize: `${blob.size} bytes`,
+ });
+
+ try {
+ logger.log("Creating download link for database export");
+ // Create a download link
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = fileName;
+
+ logger.log("Triggering download");
+ // Trigger the download
+ document.body.appendChild(a);
+ a.click();
+
+ logger.log("Cleaning up download link");
+ // Clean up
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ logger.log("Database export completed successfully");
+ } catch (error) {
+ logger.error("Failed to export database:", error);
+ throw error;
+ }
}
}
diff --git a/vite.config.common.mts b/vite.config.common.mts
index 1e288f5f..9c459932 100644
--- a/vite.config.common.mts
+++ b/vite.config.common.mts
@@ -36,7 +36,21 @@ export async function createBuildConfig(mode: string) {
assetsDir: 'assets',
chunkSizeWarningLimit: 1000,
rollupOptions: {
- external: isCapacitor ? ['@capacitor/app'] : []
+ external: isCapacitor
+ ? [
+ '@capacitor/app',
+ '@capacitor/share',
+ '@capacitor/filesystem',
+ '@capacitor/camera',
+ '@capacitor/core'
+ ]
+ : [
+ '@capacitor/app',
+ '@capacitor/share',
+ '@capacitor/filesystem',
+ '@capacitor/camera',
+ '@capacitor/core'
+ ]
}
},
define: {