Browse Source

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.
pull/130/head
Matthew Raymer 1 month ago
parent
commit
d03fa55001
  1. BIN
      android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  2. BIN
      android/.gradle/file-system.probe
  3. 2
      android/app/src/main/assets/public/index.html
  4. 38
      src/components/DataExportSection.vue
  5. 55
      src/services/PlatformService.ts
  6. 49
      src/services/platforms/CapacitorPlatformService.ts
  7. 49
      src/services/platforms/ElectronPlatformService.ts
  8. 49
      src/services/platforms/PyWebViewPlatformService.ts
  9. 17
      src/services/platforms/WebPlatformService.ts

BIN
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

BIN
android/.gradle/file-system.probe

Binary file not shown.

2
android/app/src/main/assets/public/index.html

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<title>TimeSafari</title> <title>TimeSafari</title>
<script type="module" crossorigin src="/assets/index-DtktPhxR.js"></script> <script type="module" crossorigin src="/assets/index-BX6tAjMT.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>

38
src/components/DataExportSection.vue

@ -42,17 +42,17 @@
> >
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 class="mt-4" v-if="platformCapabilities.needsFileHandlingInstructions">
<p> <p>
After the download, you can save the file in your preferred storage After the download, you can save the file in your preferred storage
location. location.
</p> </p>
<ul> <ul>
<li v-if="platformService.isCapacitor() && isIOS" class="list-disc list-outside ml-4"> <li v-if="platformCapabilities.isIOS" class="list-disc list-outside ml-4">
On iOS: Choose "More..." and select a place in iCloud, or go "Back" On iOS: Choose "More..." and select a place in iCloud, or go "Back"
and save to another location. and save to another location.
</li> </li>
<li v-if="platformService.isCapacitor() && !isIOS" class="list-disc list-outside ml-4"> <li v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS" class="list-disc list-outside ml-4">
On Android: Choose "Open" and then share On Android: Choose "Open" and then share
<font-awesome icon="share-nodes" class="fa-fw" /> <font-awesome icon="share-nodes" class="fa-fw" />
to your prefered place. to your prefered place.
@ -68,7 +68,7 @@ import { NotificationIface } from "../constants/app";
import { db } from "../db/index"; import { db } from "../db/index";
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"; import { PlatformService, PlatformCapabilities } from "../services/PlatformService";
/** /**
* @vue-component * @vue-component
@ -103,17 +103,10 @@ export default class DataExportSection extends Vue {
private platformService: PlatformService = PlatformServiceFactory.getInstance(); private platformService: PlatformService = PlatformServiceFactory.getInstance();
/** /**
* Whether the current platform is iOS * Platform capabilities for the current platform
*/ */
private get isIOS(): boolean { private get platformCapabilities(): PlatformCapabilities {
return /iPad|iPhone|iPod/.test(navigator.userAgent); return this.platformService.getCapabilities();
}
/**
* Whether to show platform-specific instructions
*/
private get showPlatformInstructions(): boolean {
return this.platformService.isCapacitor();
} }
/** /**
@ -121,7 +114,7 @@ export default class DataExportSection extends Vue {
* 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.platformCapabilities.hasFileDownload) {
URL.revokeObjectURL(this.downloadUrl); URL.revokeObjectURL(this.downloadUrl);
} }
} }
@ -139,7 +132,7 @@ export default class DataExportSection extends Vue {
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`;
if (this.platformService.isWeb()) { if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link // Web platform: Use download link
this.downloadUrl = URL.createObjectURL(blob); this.downloadUrl = URL.createObjectURL(blob);
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
@ -147,13 +140,10 @@ export default class DataExportSection extends Vue {
downloadAnchor.download = fileName; downloadAnchor.download = fileName;
downloadAnchor.click(); downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformService.isCapacitor()) { } else if (this.platformCapabilities.hasFileSystem) {
// Capacitor platform: Write to app directory // Native platform: Write to app directory
const content = await blob.text(); const content = await blob.text();
await this.platformService.writeFile(fileName, content); await this.platformService.writeFile(fileName, content);
} else {
// Other platforms: Use platform service
await this.platformService.writeFile(fileName, await blob.text());
} }
this.$notify( this.$notify(
@ -161,7 +151,7 @@ export default class DataExportSection extends Vue {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Export Successful", title: "Export Successful",
text: this.platformService.isWeb() 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.", : "The backup has been saved to your device.",
}, },
@ -187,7 +177,7 @@ export default class DataExportSection extends Vue {
*/ */
public computedStartDownloadLinkClassNames() { public computedStartDownloadLinkClassNames() {
return { 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() { public computedDownloadLinkClassNames() {
return { return {
hidden: !this.downloadUrl || !this.platformService.isWeb(), hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
}; };
} }
} }

55
src/services/PlatformService.ts

@ -9,13 +9,38 @@ export interface ImageResult {
fileName: string; 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. * Platform-agnostic interface for handling platform-specific operations.
* Provides a common API for file system operations, camera interactions, * Provides a common API for file system operations, camera interactions,
* platform detection, and deep linking across different platforms * and platform detection across different platforms (web, mobile, desktop).
* (web, mobile, desktop).
*/ */
export interface PlatformService { 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 // File system operations
/** /**
* Reads the contents of a file at the specified path. * Reads the contents of a file at the specified path.
@ -59,32 +84,6 @@ export interface PlatformService {
*/ */
pickImage(): Promise<ImageResult>; pickImage(): Promise<ImageResult>;
// 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. * Handles deep link URLs for the application.
* @param url - The deep link URL to handle * @param url - The deep link URL to handle

49
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 { Filesystem, Directory } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
@ -11,6 +11,21 @@ import { logger } from "../../utils/logger";
* - Platform-specific features * - Platform-specific features
*/ */
export class CapacitorPlatformService implements PlatformService { 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. * Reads a file from the app's data directory.
* @param path - Relative path to the file in 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" }); 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. * Handles deep link URLs for the application.
* Note: Capacitor handles deep links automatically. * Note: Capacitor handles deep links automatically.

49
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"; import { logger } from "../../utils/logger";
/** /**
@ -14,6 +14,21 @@ import { logger } from "../../utils/logger";
* - System-level features * - System-level features
*/ */
export class ElectronPlatformService implements PlatformService { 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. * Reads a file from the filesystem.
* @param _path - Path to the file to read * @param _path - Path to the file to read
@ -79,38 +94,6 @@ export class ElectronPlatformService implements PlatformService {
throw new Error("Not implemented"); 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. * Should handle deep link URLs for the desktop application.
* @param _url - The deep link URL to handle * @param _url - The deep link URL to handle

49
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"; import { logger } from "../../utils/logger";
/** /**
@ -15,6 +15,21 @@ import { logger } from "../../utils/logger";
* - Python-JavaScript bridge functionality * - Python-JavaScript bridge functionality
*/ */
export class PyWebViewPlatformService implements PlatformService { 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. * Reads a file using the Python backend.
* @param _path - Path to the file to read * @param _path - Path to the file to read
@ -80,38 +95,6 @@ export class PyWebViewPlatformService implements PlatformService {
throw new Error("Not implemented"); 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. * Should handle deep link URLs through the Python backend.
* @param _url - The deep link URL to handle * @param _url - The deep link URL to handle

17
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"; import { logger } from "../../utils/logger";
/** /**
@ -15,6 +15,21 @@ import { logger } from "../../utils/logger";
* due to browser security restrictions. These methods throw appropriate errors. * due to browser security restrictions. These methods throw appropriate errors.
*/ */
export class WebPlatformService implements PlatformService { 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. * Not supported in web platform.
* @param _path - Unused path parameter * @param _path - Unused path parameter

Loading…
Cancel
Save