From 08fcccc528e9b7f29bcfa2c2446d3499745af2fb Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 8 Apr 2025 11:29:39 +0000 Subject: [PATCH] refactor(platform): replace platform checks with capability-based system - Add PlatformCapabilities interface to define available features - Remove isWeb(), isCapacitor(), isElectron(), isPyWebView() methods - Update platform services to implement getCapabilities() - Refactor DataExportSection to use capability checks instead of platform checks - Improve platform abstraction and separation of concerns - Make platform-specific logic more maintainable and extensible This change decouples components from specific platform implementations, making the codebase more maintainable and easier to extend with new platforms. --- .../buildOutputCleanup.lock | Bin 17 -> 17 bytes android/.gradle/file-system.probe | Bin 8 -> 8 bytes src/components/DataExportSection.vue | 38 +++++------- src/services/PlatformService.ts | 55 +++++++++--------- .../platforms/CapacitorPlatformService.ts | 49 +++++----------- .../platforms/ElectronPlatformService.ts | 49 +++++----------- .../platforms/PyWebViewPlatformService.ts | 49 +++++----------- src/services/platforms/WebPlatformService.ts | 17 +++++- 8 files changed, 105 insertions(+), 152 deletions(-) diff --git a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock index af452334b0ea08f2ce4a4670c56381b07edd37e9..16c60848e71561ea28ebba0b2e207f330bbf78bc 100644 GIT binary patch literal 17 VcmZQ>duy#wxduy#wx If no download happened yet, click again here to download now. -
+

After the download, you can save the file in your preferred storage location.

    -
  • +
  • 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. @@ -68,7 +68,7 @@ import { NotificationIface } from "../constants/app"; import { db } from "../db/index"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; -import { PlatformService } from "../services/PlatformService"; +import { PlatformService, PlatformCapabilities } from "../services/PlatformService"; /** * @vue-component @@ -103,17 +103,10 @@ export default class DataExportSection extends Vue { private platformService: PlatformService = PlatformServiceFactory.getInstance(); /** - * Whether the current platform is iOS + * Platform capabilities for the current platform */ - 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 get platformCapabilities(): PlatformCapabilities { + return this.platformService.getCapabilities(); } /** @@ -121,7 +114,7 @@ export default class DataExportSection extends Vue { * Revokes object URL when component is unmounted (web platform only) */ beforeUnmount() { - if (this.downloadUrl && this.platformService.isWeb()) { + if (this.downloadUrl && this.platformCapabilities.hasFileDownload) { URL.revokeObjectURL(this.downloadUrl); } } @@ -139,7 +132,7 @@ export default class DataExportSection extends Vue { const blob = await db.export({ prettyJson: true }); const fileName = `${db.name}-backup.json`; - if (this.platformService.isWeb()) { + if (this.platformCapabilities.hasFileDownload) { // Web platform: Use download link this.downloadUrl = URL.createObjectURL(blob); const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; @@ -147,13 +140,10 @@ export default class DataExportSection extends Vue { downloadAnchor.download = fileName; downloadAnchor.click(); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); - } else if (this.platformService.isCapacitor()) { - // Capacitor platform: Write to app directory + } else if (this.platformCapabilities.hasFileSystem) { + // Native platform: Write to app directory const content = await blob.text(); await this.platformService.writeFile(fileName, content); - } else { - // Other platforms: Use platform service - await this.platformService.writeFile(fileName, await blob.text()); } this.$notify( @@ -161,7 +151,7 @@ export default class DataExportSection extends Vue { group: "alert", type: "success", title: "Export Successful", - text: this.platformService.isWeb() + 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.", }, @@ -187,7 +177,7 @@ export default class DataExportSection extends Vue { */ public computedStartDownloadLinkClassNames() { return { - hidden: this.downloadUrl && this.platformService.isWeb(), + hidden: this.downloadUrl && this.platformCapabilities.hasFileDownload, }; } @@ -197,7 +187,7 @@ export default class DataExportSection extends Vue { */ public computedDownloadLinkClassNames() { return { - hidden: !this.downloadUrl || !this.platformService.isWeb(), + hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload, }; } } diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 821261ee..5a2c9209 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -9,13 +9,38 @@ export interface ImageResult { fileName: string; } +/** + * Platform capabilities interface defining what features are available + * on the current platform implementation + */ +export interface PlatformCapabilities { + /** Whether the platform supports native file system access */ + hasFileSystem: boolean; + /** Whether the platform supports native camera access */ + hasCamera: boolean; + /** Whether the platform is a mobile device */ + isMobile: boolean; + /** Whether the platform is iOS specifically */ + isIOS: boolean; + /** Whether the platform supports native file download */ + hasFileDownload: boolean; + /** Whether the platform requires special file handling instructions */ + needsFileHandlingInstructions: boolean; +} + /** * Platform-agnostic interface for handling platform-specific operations. * Provides a common API for file system operations, camera interactions, - * platform detection, and deep linking across different platforms - * (web, mobile, desktop). + * and platform detection across different platforms (web, mobile, desktop). */ export interface PlatformService { + // Platform capabilities + /** + * Gets the current platform's capabilities + * @returns Object describing what features are available on this platform + */ + getCapabilities(): PlatformCapabilities; + // File system operations /** * Reads the contents of a file at the specified path. @@ -59,32 +84,6 @@ export interface PlatformService { */ pickImage(): Promise; - // Platform specific features - /** - * Checks if the current platform is Capacitor (mobile). - * @returns true if running on Capacitor - */ - isCapacitor(): boolean; - - /** - * Checks if the current platform is Electron (desktop). - * @returns true if running on Electron - */ - isElectron(): boolean; - - /** - * Checks if the current platform is PyWebView. - * @returns true if running on PyWebView - */ - isPyWebView(): boolean; - - /** - * Checks if the current platform is web browser. - * @returns true if running in a web browser - */ - isWeb(): boolean; - - // Deep linking /** * Handles deep link URLs for the application. * @param url - The deep link URL to handle diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index b6ba1198..b5fcd81e 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -1,4 +1,4 @@ -import { ImageResult, PlatformService } from "../PlatformService"; +import { ImageResult, PlatformService, PlatformCapabilities } from "../PlatformService"; import { Filesystem, Directory } from "@capacitor/filesystem"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { logger } from "../../utils/logger"; @@ -11,6 +11,21 @@ import { logger } from "../../utils/logger"; * - Platform-specific features */ export class CapacitorPlatformService implements PlatformService { + /** + * Gets the capabilities of the Capacitor platform + * @returns Platform capabilities object + */ + getCapabilities(): PlatformCapabilities { + return { + hasFileSystem: true, + hasCamera: true, + isMobile: true, + isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent), + hasFileDownload: false, + needsFileHandlingInstructions: true + }; + } + /** * Reads a file from the app's data directory. * @param path - Relative path to the file in the app's data directory @@ -146,38 +161,6 @@ export class CapacitorPlatformService implements PlatformService { return new Blob(byteArrays, { type: "image/jpeg" }); } - /** - * Checks if running on Capacitor platform. - * @returns true, as this is the Capacitor implementation - */ - isCapacitor(): boolean { - return true; - } - - /** - * Checks if running on Electron platform. - * @returns false, as this is not Electron - */ - isElectron(): boolean { - return false; - } - - /** - * Checks if running on PyWebView platform. - * @returns false, as this is not PyWebView - */ - isPyWebView(): boolean { - return false; - } - - /** - * Checks if running on web platform. - * @returns false, as this is not web - */ - isWeb(): boolean { - return false; - } - /** * Handles deep link URLs for the application. * Note: Capacitor handles deep links automatically. diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 0a99cd43..86725881 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -1,4 +1,4 @@ -import { ImageResult, PlatformService } from "../PlatformService"; +import { ImageResult, PlatformService, PlatformCapabilities } from "../PlatformService"; import { logger } from "../../utils/logger"; /** @@ -14,6 +14,21 @@ import { logger } from "../../utils/logger"; * - System-level features */ export class ElectronPlatformService implements PlatformService { + /** + * Gets the capabilities of the Electron platform + * @returns Platform capabilities object + */ + getCapabilities(): PlatformCapabilities { + return { + hasFileSystem: false, // Not implemented yet + hasCamera: false, // Not implemented yet + isMobile: false, + isIOS: false, + hasFileDownload: false, // Not implemented yet + needsFileHandlingInstructions: false + }; + } + /** * Reads a file from the filesystem. * @param _path - Path to the file to read @@ -79,38 +94,6 @@ export class ElectronPlatformService implements PlatformService { throw new Error("Not implemented"); } - /** - * Checks if running on Capacitor platform. - * @returns false, as this is not Capacitor - */ - isCapacitor(): boolean { - return false; - } - - /** - * Checks if running on Electron platform. - * @returns true, as this is the Electron implementation - */ - isElectron(): boolean { - return true; - } - - /** - * Checks if running on PyWebView platform. - * @returns false, as this is not PyWebView - */ - isPyWebView(): boolean { - return false; - } - - /** - * Checks if running on web platform. - * @returns false, as this is not web - */ - isWeb(): boolean { - return false; - } - /** * Should handle deep link URLs for the desktop application. * @param _url - The deep link URL to handle diff --git a/src/services/platforms/PyWebViewPlatformService.ts b/src/services/platforms/PyWebViewPlatformService.ts index 0e118297..7cd5fd53 100644 --- a/src/services/platforms/PyWebViewPlatformService.ts +++ b/src/services/platforms/PyWebViewPlatformService.ts @@ -1,4 +1,4 @@ -import { ImageResult, PlatformService } from "../PlatformService"; +import { ImageResult, PlatformService, PlatformCapabilities } from "../PlatformService"; import { logger } from "../../utils/logger"; /** @@ -15,6 +15,21 @@ import { logger } from "../../utils/logger"; * - Python-JavaScript bridge functionality */ export class PyWebViewPlatformService implements PlatformService { + /** + * Gets the capabilities of the PyWebView platform + * @returns Platform capabilities object + */ + getCapabilities(): PlatformCapabilities { + return { + hasFileSystem: false, // Not implemented yet + hasCamera: false, // Not implemented yet + isMobile: false, + isIOS: false, + hasFileDownload: false, // Not implemented yet + needsFileHandlingInstructions: false + }; + } + /** * Reads a file using the Python backend. * @param _path - Path to the file to read @@ -80,38 +95,6 @@ export class PyWebViewPlatformService implements PlatformService { throw new Error("Not implemented"); } - /** - * Checks if running on Capacitor platform. - * @returns false, as this is not Capacitor - */ - isCapacitor(): boolean { - return false; - } - - /** - * Checks if running on Electron platform. - * @returns false, as this is not Electron - */ - isElectron(): boolean { - return false; - } - - /** - * Checks if running on PyWebView platform. - * @returns true, as this is the PyWebView implementation - */ - isPyWebView(): boolean { - return true; - } - - /** - * Checks if running on web platform. - * @returns false, as this is not web - */ - isWeb(): boolean { - return false; - } - /** * Should handle deep link URLs through the Python backend. * @param _url - The deep link URL to handle diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 9a65278a..bcfdfec1 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -1,4 +1,4 @@ -import { ImageResult, PlatformService } from "../PlatformService"; +import { ImageResult, PlatformService, PlatformCapabilities } from "../PlatformService"; import { logger } from "../../utils/logger"; /** @@ -15,6 +15,21 @@ import { logger } from "../../utils/logger"; * due to browser security restrictions. These methods throw appropriate errors. */ export class WebPlatformService implements PlatformService { + /** + * Gets the capabilities of the web platform + * @returns Platform capabilities object + */ + getCapabilities(): PlatformCapabilities { + return { + hasFileSystem: false, + hasCamera: true, // Through file input with capture + isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), + isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent), + hasFileDownload: true, + needsFileHandlingInstructions: false + }; + } + /** * Not supported in web platform. * @param _path - Unused path parameter