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">
<link rel="icon" href="/favicon.ico">
<title>TimeSafari</title>
<script type="module" crossorigin src="/assets/index-DtktPhxR.js"></script>
<script type="module" crossorigin src="/assets/index-BX6tAjMT.js"></script>
</head>
<body>
<noscript>

38
src/components/DataExportSection.vue

@ -42,17 +42,17 @@
>
If no download happened yet, click again here to download now.
</a>
<div class="mt-4" v-if="showPlatformInstructions">
<div class="mt-4" v-if="platformCapabilities.needsFileHandlingInstructions">
<p>
After the download, you can save the file in your preferred storage
location.
</p>
<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"
and save to another location.
</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
<font-awesome icon="share-nodes" class="fa-fw" />
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,
};
}
}

55
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<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.
* @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 { 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.

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";
/**
@ -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

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";
/**
@ -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

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";
/**
@ -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

Loading…
Cancel
Save