Browse Source

refactor: centralize platform-specific behavior in platform services

- Add platform-specific capability methods to PlatformService interface:
  - getExportInstructions()
  - getExportSuccessMessage()
  - needsSecondaryDownloadLink()
  - needsDownloadCleanup()

- Update platform service implementations:
  - WebPlatformService: Implement web-specific export behavior
  - CapacitorPlatformService: Implement mobile-specific export behavior
  - ElectronPlatformService: Add placeholder for export functionality
  - PyWebViewPlatformService: Add placeholder for export functionality

- Refactor DataExportSection component:
  - Remove direct platform checks (isWeb, isCapacitor, etc.)
  - Use platform service capabilities for UI behavior
  - Improve error handling and logging
  - Add proper cleanup for web platform downloads

- Update PlatformServiceFactory:
  - Make getInstance() async to support dynamic imports
  - Improve platform service initialization

- Fix code style and documentation:
  - Update JSDoc comments
  - Fix string quotes consistency
  - Add proper error handling
  - Improve logging messages

- Update Vite config:
  - Add all Capacitor dependencies to external list
  - Ensure consistent handling across platforms
cross-platform-factory
Matthew Raymer 2 months ago
parent
commit
5ee4a7e411
  1. 106
      src/components/DataExportSection.vue
  2. 15
      src/components/PhotoDialog.vue
  3. 24
      src/services/PlatformService.ts
  4. 41
      src/services/PlatformServiceFactory.ts
  5. 8
      src/services/api.ts
  6. 8
      src/services/deepLinks.ts
  7. 12
      src/services/plan.ts
  8. 58
      src/services/platforms/CapacitorPlatformService.ts
  9. 14
      src/services/platforms/ElectronPlatformService.ts
  10. 14
      src/services/platforms/PyWebViewPlatformService.ts
  11. 70
      src/services/platforms/WebPlatformService.ts
  12. 16
      vite.config.common.mts

106
src/components/DataExportSection.vue

@ -1,16 +1,9 @@
/** /** * Data Export Section Component * * Provides UI and functionality for
* Data Export Section Component exporting user data and backing up identifier seeds. * Includes buttons for seed
* backup and database export, with platform-specific download instructions. * *
* Provides UI and functionality for exporting user data and backing up identifier seeds. @component * @displayName DataExportSection * @example * ```vue *
* Includes buttons for seed backup and database export, with platform-specific download instructions. <DataExportSection :active-did="currentDid" />
* * ``` */
* @component
* @displayName DataExportSection
* @example
* ```vue
* <DataExportSection :active-did="currentDid" />
* ```
*/
<template> <template>
<div <div
@ -36,28 +29,24 @@
(excluding Identifier Data) (excluding Identifier Data)
</button> </button>
<a <a
v-if="platformService?.needsSecondaryDownloadLink()"
ref="downloadLink" ref="downloadLink"
:class="computedDownloadLinkClassNames()" :class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
> >
If no download happened yet, click again here to download now. If no download happened yet, click again here to download now.
</a> </a>
<div class="mt-4" v-if="showPlatformInstructions"> <div
<p> v-if="platformService?.getExportInstructions().length > 0"
After the download, you can save the file in your preferred storage class="mt-4"
location. >
<p
v-for="instruction in platformService?.getExportInstructions()"
:key="instruction"
class="list-disc list-outside ml-4"
>
{{ instruction }}
</p> </p>
<ul>
<li v-if="platformService.isCapacitor() && 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.
</li>
<li v-if="platformService.isCapacitor() && !isIOS" class="list-disc list-outside ml-4">
On Android: Choose "Open" and then share
<font-awesome icon="share-nodes" class="fa-fw" />
to your prefered place.
</li>
</ul>
</div> </div>
</div> </div>
</template> </template>
@ -100,60 +89,72 @@ export default class DataExportSection extends Vue {
/** /**
* Platform service instance for platform-specific operations * Platform service instance for platform-specific operations
*/ */
private platformService: PlatformService = PlatformServiceFactory.getInstance(); private platformService?: PlatformService;
/**
* 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();
}
/** /**
* Lifecycle hook to clean up resources * Lifecycle hook to clean up resources
* Revokes object URL when component is unmounted (web platform only) * Revokes object URL when component is unmounted (web platform only)
*/ */
beforeUnmount() { beforeUnmount() {
if (this.downloadUrl && this.platformService.isWeb()) { if (this.downloadUrl && this.platformService?.needsDownloadCleanup()) {
URL.revokeObjectURL(this.downloadUrl); 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 * Exports the database to a JSON file
* Uses platform-specific methods for saving the exported data * Uses platform-specific methods for saving the exported data
* Shows success/error notifications to user * Shows success/error notifications to user
* *
* @throws {Error} If export fails * @throws {Error} If export fails
* @emits {Notification} Success or error notification * @emits {Notification} Success or error notification
*/ */
public async exportDatabase() { public async exportDatabase() {
try { 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 blob = await db.export({ prettyJson: true });
const fileName = `${db.name}-backup.json`; 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); await this.platformService.exportDatabase(blob, fileName);
logger.log("Database export completed successfully:", {
fileName,
platform: process.env.VITE_PLATFORM,
});
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Export Successful", title: "Export Successful",
text: this.platformService.isWeb() text: this.platformService.getExportSuccessMessage(),
? "See your downloads directory for the backup. It is in the Dexie format."
: "The backup has been saved to your device.",
}, },
-1, -1,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Database export failed:", {
error,
platform: process.env.VITE_PLATFORM,
});
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -172,7 +173,8 @@ export default class DataExportSection extends Vue {
*/ */
public computedStartDownloadLinkClassNames() { public computedStartDownloadLinkClassNames() {
return { 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() { public computedDownloadLinkClassNames() {
return { return {
hidden: !this.downloadUrl || !this.platformService.isWeb(), hidden:
!this.downloadUrl ||
!this.platformService?.needsSecondaryDownloadLink(),
}; };
} }
} }

15
src/components/PhotoDialog.vue

@ -108,6 +108,7 @@ import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto"; import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { PlatformService } from "../services/PlatformService";
@Component({ components: { VuePictureCropper } }) @Component({ components: { VuePictureCropper } })
export default class PhotoDialog extends Vue { export default class PhotoDialog extends Vue {
@ -123,13 +124,14 @@ export default class PhotoDialog extends Vue {
uploading = false; uploading = false;
visible = false; visible = false;
private platformService = PlatformServiceFactory.getInstance(); private platformService?: PlatformService;
URL = window.URL || window.webkitURL; URL = window.URL || window.webkitURL;
async mounted() { async mounted() {
try { try {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.platformService = await PlatformServiceFactory.getInstance();
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Error retrieving settings from database:", err); logger.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
@ -137,7 +139,10 @@ export default class PhotoDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", 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, -1,
); );
@ -181,6 +186,9 @@ export default class PhotoDialog extends Vue {
async takePhoto() { async takePhoto() {
try { try {
if (!this.platformService) {
this.platformService = await PlatformServiceFactory.getInstance();
}
const result = await this.platformService.takePicture(); const result = await this.platformService.takePicture();
this.blob = result.blob; this.blob = result.blob;
this.fileName = result.fileName; this.fileName = result.fileName;
@ -200,6 +208,9 @@ export default class PhotoDialog extends Vue {
async pickPhoto() { async pickPhoto() {
try { try {
if (!this.platformService) {
this.platformService = await PlatformServiceFactory.getInstance();
}
const result = await this.platformService.pickImage(); const result = await this.platformService.pickImage();
this.blob = result.blob; this.blob = result.blob;
this.fileName = result.fileName; this.fileName = result.fileName;

24
src/services/PlatformService.ts

@ -100,4 +100,28 @@ export interface PlatformService {
* @returns Promise that resolves when the deep link has been handled * @returns Promise that resolves when the deep link has been handled
*/ */
handleDeepLink(url: string): Promise<void>; handleDeepLink(url: string): Promise<void>;
/**
* 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;
} }

41
src/services/PlatformServiceFactory.ts

@ -1,20 +1,17 @@
import { PlatformService } from "./PlatformService"; import { PlatformService } from "./PlatformService";
import { WebPlatformService } from "./platforms/WebPlatformService"; 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. * Factory class for creating platform-specific service implementations.
* Implements the Singleton pattern to ensure only one instance of PlatformService exists. * 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 * The factory determines which platform implementation to use based on the VITE_PLATFORM
* environment variable. Supported platforms are: * environment variable. Supported platforms are:
* - capacitor: Mobile platform using Capacitor * - capacitor: Mobile platform using Capacitor
* - electron: Desktop platform using Electron * - electron: Desktop platform using Electron
* - pywebview: Python WebView implementation * - pywebview: Python WebView implementation
* - web: Default web platform (fallback) * - web: Default web platform (fallback)
* *
* @example * @example
* ```typescript * ```typescript
* const platformService = PlatformServiceFactory.getInstance(); * const platformService = PlatformServiceFactory.getInstance();
@ -27,32 +24,46 @@ export class PlatformServiceFactory {
/** /**
* Gets or creates the singleton instance of PlatformService. * Gets or creates the singleton instance of PlatformService.
* Creates the appropriate platform-specific implementation based on environment. * Creates the appropriate platform-specific implementation based on environment.
* *
* @returns {PlatformService} The singleton instance of PlatformService * @returns {PlatformService} The singleton instance of PlatformService
*/ */
public static getInstance(): PlatformService { public static async getInstance(): Promise<PlatformService> {
if (PlatformServiceFactory.instance) { if (PlatformServiceFactory.instance) {
return PlatformServiceFactory.instance; return PlatformServiceFactory.instance;
} }
const platform = process.env.VITE_PLATFORM || "web"; const platform = process.env.VITE_PLATFORM || "web";
let service: PlatformService;
switch (platform) { switch (platform) {
case "capacitor": case "capacitor": {
PlatformServiceFactory.instance = new CapacitorPlatformService(); const { CapacitorPlatformService } = await import(
"./platforms/CapacitorPlatformService"
);
service = new CapacitorPlatformService();
break; break;
case "electron": }
PlatformServiceFactory.instance = new ElectronPlatformService(); case "electron": {
const { ElectronPlatformService } = await import(
"./platforms/ElectronPlatformService"
);
service = new ElectronPlatformService();
break; break;
case "pywebview": }
PlatformServiceFactory.instance = new PyWebViewPlatformService(); case "pywebview": {
const { PyWebViewPlatformService } = await import(
"./platforms/PyWebViewPlatformService"
);
service = new PyWebViewPlatformService();
break; break;
}
case "web": case "web":
default: default:
PlatformServiceFactory.instance = new WebPlatformService(); service = new WebPlatformService();
break; break;
} }
return PlatformServiceFactory.instance; PlatformServiceFactory.instance = service;
return service;
} }
} }

8
src/services/api.ts

@ -1,7 +1,7 @@
/** /**
* API error handling utilities for the application. * API error handling utilities for the application.
* Provides centralized error handling for API requests with platform-specific logging. * Provides centralized error handling for API requests with platform-specific logging.
* *
* @module api * @module api
*/ */
@ -10,12 +10,12 @@ import { logger } from "../utils/logger";
/** /**
* Handles API errors with platform-specific logging and error processing. * Handles API errors with platform-specific logging and error processing.
* *
* @param error - The Axios error object from the failed request * @param error - The Axios error object from the failed request
* @param endpoint - The API endpoint that was called * @param endpoint - The API endpoint that was called
* @returns null for rate limit errors (400), throws the error otherwise * @returns null for rate limit errors (400), throws the error otherwise
* @throws The original error for non-rate-limit cases * @throws The original error for non-rate-limit cases
* *
* @remarks * @remarks
* Special handling includes: * Special handling includes:
* - Enhanced logging for Capacitor platform * - Enhanced logging for Capacitor platform
@ -25,7 +25,7 @@ import { logger } from "../utils/logger";
* - HTTP status * - HTTP status
* - Response data * - Response data
* - Request configuration (URL, method, headers) * - Request configuration (URL, method, headers)
* *
* @example * @example
* ```typescript * ```typescript
* try { * try {

8
src/services/deepLinks.ts

@ -25,7 +25,7 @@
* *
* Deep Link Format: * Deep Link Format:
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2] * timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
* *
* Supported Routes: * Supported Routes:
* - user-profile: View user profile * - user-profile: View user profile
* - project-details: View project details * - project-details: View project details
@ -73,7 +73,7 @@ export class DeepLinkHandler {
/** /**
* Parses deep link URL into path, params and query components. * Parses deep link URL into path, params and query components.
* Validates URL structure using Zod schemas. * Validates URL structure using Zod schemas.
* *
* @param url - The deep link URL to parse (format: scheme://path[?query]) * @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid * @throws {DeepLinkError} If URL format is invalid
* @returns Parsed URL components (path, params, query) * @returns Parsed URL components (path, params, query)
@ -111,7 +111,7 @@ export class DeepLinkHandler {
/** /**
* Processes incoming deep links and routes them appropriately. * Processes incoming deep links and routes them appropriately.
* Handles validation, error handling, and routing to the correct view. * Handles validation, error handling, and routing to the correct view.
* *
* @param url - The deep link URL to process * @param url - The deep link URL to process
* @throws {DeepLinkError} If URL processing fails * @throws {DeepLinkError} If URL processing fails
*/ */
@ -142,7 +142,7 @@ export class DeepLinkHandler {
/** /**
* Routes the deep link to appropriate view with validated parameters. * Routes the deep link to appropriate view with validated parameters.
* Validates route and parameters using Zod schemas before routing. * Validates route and parameters using Zod schemas before routing.
* *
* @param path - The route path from the deep link * @param path - The route path from the deep link
* @param params - URL parameters * @param params - URL parameters
* @param query - Query string parameters * @param query - Query string parameters

12
src/services/plan.ts

@ -1,7 +1,7 @@
/** /**
* Plan service module for handling plan and claim data loading. * Plan service module for handling plan and claim data loading.
* Provides functionality to load plans with retry mechanism and error handling. * Provides functionality to load plans with retry mechanism and error handling.
* *
* @module plan * @module plan
*/ */
@ -26,11 +26,11 @@ interface PlanResponse {
/** /**
* Loads a plan with automatic retry mechanism. * Loads a plan with automatic retry mechanism.
* Attempts to load the plan multiple times in case of failure. * Attempts to load the plan multiple times in case of failure.
* *
* @param handle - The unique identifier for the plan or claim * @param handle - The unique identifier for the plan or claim
* @param retries - Number of retry attempts (default: 3) * @param retries - Number of retry attempts (default: 3)
* @returns Promise resolving to PlanResponse * @returns Promise resolving to PlanResponse
* *
* @remarks * @remarks
* - Implements exponential backoff with 1 second delay between retries * - Implements exponential backoff with 1 second delay between retries
* - Provides detailed logging of each attempt and any errors * - Provides detailed logging of each attempt and any errors
@ -39,7 +39,7 @@ interface PlanResponse {
* - HTTP status and headers * - HTTP status and headers
* - Response data * - Response data
* - Request configuration * - Request configuration
* *
* @example * @example
* ```typescript * ```typescript
* const response = await loadPlanWithRetry('plan-123'); * 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. * Makes a single API request to load a plan or claim.
* Determines the appropriate endpoint based on the handle. * Determines the appropriate endpoint based on the handle.
* *
* @param handle - The unique identifier for the plan or claim * @param handle - The unique identifier for the plan or claim
* @returns Promise resolving to PlanResponse * @returns Promise resolving to PlanResponse
* @throws Will throw an error if the API request fails * @throws Will throw an error if the API request fails
* *
* @remarks * @remarks
* - Automatically detects claim vs plan endpoints based on handle * - Automatically detects claim vs plan endpoints based on handle
* - Uses axios for HTTP requests * - Uses axios for HTTP requests

58
src/services/platforms/CapacitorPlatformService.ts

@ -66,7 +66,9 @@ export class CapacitorPlatformService implements PlatformService {
path: directory, path: directory,
directory: Directory.Data, 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(); 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<void> { async exportDatabase(blob: Blob, fileName: string): Promise<void> {
logger.log("Starting database export on Capacitor platform:", {
fileName,
blobSize: `${blob.size} bytes`,
});
// Create a File object from the Blob // 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 { try {
logger.log("Attempting to use native share sheet");
// Use the native share sheet // Use the native share sheet
await navigator.share({ await navigator.share({
files: [file], files: [file],
title: fileName, title: fileName,
}); });
logger.log("Database export completed via native share sheet");
} catch (error) { } catch (error) {
logger.log("Native share failed, falling back to Capacitor Share API");
// Fallback to Capacitor Share API if Web Share API fails // Fallback to Capacitor Share API if Web Share API fails
// First save to temporary file // First save to temporary file
const base64Data = await this.blobToBase64(blob); const base64Data = await this.blobToBase64(blob);
@ -208,23 +243,26 @@ export class CapacitorPlatformService implements PlatformService {
path: fileName, path: fileName,
data: base64Data, data: base64Data,
directory: Directory.Cache, // Use Cache instead of Documents for temporary files 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 // Then share using Capacitor Share API
await Share.share({ await Share.share({
title: fileName, title: fileName,
url: result.uri url: result.uri,
}); });
logger.log("Database export completed via Capacitor Share API");
// Clean up the temporary file // Clean up the temporary file
try { try {
await Filesystem.deleteFile({ await Filesystem.deleteFile({
path: fileName, path: fileName,
directory: Directory.Cache directory: Directory.Cache,
}); });
logger.log("Temporary file cleaned up successfully");
} catch (cleanupError) { } 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(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
const base64data = reader.result as string; const base64data = reader.result as string;
resolve(base64data.split(',')[1]); resolve(base64data.split(",")[1]);
}; };
reader.onerror = reject; reader.onerror = reject;
reader.readAsDataURL(blob); reader.readAsDataURL(blob);

14
src/services/platforms/ElectronPlatformService.ts

@ -5,7 +5,7 @@ import { logger } from "../../utils/logger";
* Platform service implementation for Electron (desktop) platform. * Platform service implementation for Electron (desktop) platform.
* Note: This is a placeholder implementation with most methods currently unimplemented. * Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations. * Implements the PlatformService interface but throws "Not implemented" errors for most operations.
* *
* @remarks * @remarks
* This service is intended for desktop application functionality through Electron. * This service is intended for desktop application functionality through Electron.
* Future implementations should provide: * Future implementations should provide:
@ -121,4 +121,16 @@ export class ElectronPlatformService implements PlatformService {
logger.error("handleDeepLink not implemented in Electron platform"); logger.error("handleDeepLink not implemented in Electron platform");
throw new Error("Not implemented"); 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<void> {
logger.error("exportDatabase not implemented in Electron platform");
throw new Error("Not implemented");
}
} }

14
src/services/platforms/PyWebViewPlatformService.ts

@ -5,7 +5,7 @@ import { logger } from "../../utils/logger";
* Platform service implementation for PyWebView platform. * Platform service implementation for PyWebView platform.
* Note: This is a placeholder implementation with most methods currently unimplemented. * Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations. * Implements the PlatformService interface but throws "Not implemented" errors for most operations.
* *
* @remarks * @remarks
* This service is intended for Python-based desktop applications using pywebview. * This service is intended for Python-based desktop applications using pywebview.
* Future implementations should provide: * Future implementations should provide:
@ -122,4 +122,16 @@ export class PyWebViewPlatformService implements PlatformService {
logger.error("handleDeepLink not implemented in PyWebView platform"); logger.error("handleDeepLink not implemented in PyWebView platform");
throw new Error("Not implemented"); 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<void> {
logger.error("exportDatabase not implemented in PyWebView platform");
throw new Error("Not implemented");
}
} }

70
src/services/platforms/WebPlatformService.ts

@ -4,13 +4,13 @@ import { logger } from "../../utils/logger";
/** /**
* Platform service implementation for web browser platform. * Platform service implementation for web browser platform.
* Implements the PlatformService interface with web-specific functionality. * Implements the PlatformService interface with web-specific functionality.
* *
* @remarks * @remarks
* This service provides web-based implementations for: * This service provides web-based implementations for:
* - Image capture using the browser's file input * - Image capture using the browser's file input
* - Image selection from local filesystem * - Image selection from local filesystem
* - Image processing and conversion * - Image processing and conversion
* *
* Note: File system operations are not available in the web platform * Note: File system operations are not available in the web platform
* due to browser security restrictions. These methods throw appropriate errors. * 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. * Opens a file input dialog configured for camera capture.
* Creates a temporary file input element to access the device camera. * Creates a temporary file input element to access the device camera.
* *
* @returns Promise resolving to the captured image data * @returns Promise resolving to the captured image data
* @throws Error if image capture fails or no image is selected * @throws Error if image capture fails or no image is selected
* *
* @remarks * @remarks
* Uses the 'capture' attribute to prefer the device camera. * Uses the 'capture' attribute to prefer the device camera.
* Falls back to file selection if camera is not available. * 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. * Opens a file input dialog for selecting an image file.
* Creates a temporary file input element to access local files. * Creates a temporary file input element to access local files.
* *
* @returns Promise resolving to the selected image data * @returns Promise resolving to the selected image data
* @throws Error if image processing fails or no image is selected * @throws Error if image processing fails or no image is selected
* *
* @remarks * @remarks
* Allows selection of any image file type. * Allows selection of any image file type.
* Processes the selected image to ensure consistent format. * 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. * Processes an image file to ensure consistent format.
* Converts the file to a data URL and then to a Blob. * Converts the file to a data URL and then to a Blob.
* *
* @param file - The image File object to process * @param file - The image File object to process
* @returns Promise resolving to processed image Blob * @returns Promise resolving to processed image Blob
* @throws Error if file reading or conversion fails * @throws Error if file reading or conversion fails
* *
* @remarks * @remarks
* This method ensures consistent image format across different * This method ensures consistent image format across different
* input sources by converting through data URL to Blob. * 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. * Handles deep link URLs in the web platform.
* Deep links are handled through URL parameters in the web environment. * Deep links are handled through URL parameters in the web environment.
* *
* @param _url - The deep link URL to handle (unused in web implementation) * @param _url - The deep link URL to handle (unused in web implementation)
* @returns Promise that resolves immediately as web handles URLs naturally * @returns Promise that resolves immediately as web handles URLs naturally
*/ */
@ -210,12 +210,52 @@ export class WebPlatformService implements PlatformService {
return Promise.resolve(); 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<void> { async exportDatabase(blob: Blob, fileName: string): Promise<void> {
const downloadUrl = URL.createObjectURL(blob); logger.log("Starting database export on web platform:", {
const downloadAnchor = document.createElement('a'); fileName,
downloadAnchor.href = downloadUrl; blobSize: `${blob.size} bytes`,
downloadAnchor.download = fileName; });
downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); 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;
}
} }
} }

16
vite.config.common.mts

@ -36,7 +36,21 @@ export async function createBuildConfig(mode: string) {
assetsDir: 'assets', assetsDir: 'assets',
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
rollupOptions: { 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: { define: {

Loading…
Cancel
Save