Compare commits
24 Commits
Author | SHA1 | Date |
---|---|---|
|
ca455e9593 | 4 weeks ago |
|
5ada70b05e | 1 month ago |
|
4f9b146a66 | 1 month ago |
|
2b638ce2a7 | 1 month ago |
|
0b528af2a6 | 1 month ago |
|
008211bc21 | 1 month ago |
|
6955a36458 | 1 month ago |
|
ba079ea983 | 1 month ago |
|
d7b3c5ec9d | 1 month ago |
|
d83a25f47e | 1 month ago |
|
fb40dc0ff7 | 1 month ago |
|
d03fa55001 | 1 month ago |
|
c8eff4d39e | 1 month ago |
|
b8a7771edf | 1 month ago |
|
5d845fb112 | 1 month ago |
|
660f2170de | 1 month ago |
|
94bd649003 | 1 month ago |
|
b2d628cfeb | 1 month ago |
|
00e52f8dca | 1 month ago |
|
073ce24f43 | 1 month ago |
|
2c84bb50b3 | 1 month ago |
|
abf18835f6 | 1 month ago |
|
f72562804d | 1 month ago |
|
bdc5ffafc1 | 1 month ago |
@ -1,2 +1,2 @@ |
|||
#Fri Mar 21 07:27:50 UTC 2025 |
|||
gradle.version=8.2.1 |
|||
#Wed Apr 09 09:01:13 UTC 2025 |
|||
gradle.version=8.11.1 |
|||
|
@ -1,17 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang=""> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<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-CZMUlUNO.js"></script> |
|||
</head> |
|||
<body> |
|||
<noscript> |
|||
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> |
|||
</noscript> |
|||
<div id="app"></div> |
|||
</body> |
|||
</html> |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 23 KiB |
@ -1,23 +1,16 @@ |
|||
App/build |
|||
App/Pods |
|||
App/output |
|||
App/App/public |
|||
DerivedData |
|||
xcuserdata |
|||
*.xcuserstate |
|||
App/Pods |
|||
|
|||
# Cordova plugins for Capacitor |
|||
capacitor-cordova-ios-plugins |
|||
App/*.xcodeproj/xcuserdata/ |
|||
App/*.xcworkspace/xcuserdata/ |
|||
App/*/public |
|||
|
|||
# Generated Config files |
|||
App/App/capacitor.config.json |
|||
App/App/config.xml |
|||
App/*/capacitor.config.json |
|||
App/*/config.xml |
|||
|
|||
# User-specific Xcode files |
|||
App/App.xcodeproj/xcuserdata/*.xcuserdatad/ |
|||
App/App.xcodeproj/*.xcuserstate |
|||
# Cordova plugins for Capacitor |
|||
capacitor-cordova-ios-plugins |
|||
|
|||
fastlane/report.xml |
|||
fastlane/Preview.html |
|||
fastlane/screenshots |
|||
fastlane/test_output |
|||
DerivedData |
|||
|
@ -1,8 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>IDEDidComputeMac32BitWarning</key> |
|||
<true/> |
|||
</dict> |
|||
</plist> |
@ -1,28 +1,52 @@ |
|||
PODS: |
|||
- Capacitor (6.2.0): |
|||
- Capacitor (6.2.1): |
|||
- CapacitorCordova |
|||
- CapacitorApp (6.0.2): |
|||
- Capacitor |
|||
- CapacitorCordova (6.2.0) |
|||
- CapacitorCamera (6.1.2): |
|||
- Capacitor |
|||
- CapacitorCordova (6.2.1) |
|||
- CapacitorFilesystem (6.0.3): |
|||
- Capacitor |
|||
- CapacitorShare (6.0.3): |
|||
- Capacitor |
|||
- CapawesomeCapacitorFilePicker (6.2.0): |
|||
- Capacitor |
|||
|
|||
DEPENDENCIES: |
|||
- "Capacitor (from `../../node_modules/@capacitor/ios`)" |
|||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)" |
|||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" |
|||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" |
|||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" |
|||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)" |
|||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)" |
|||
|
|||
EXTERNAL SOURCES: |
|||
Capacitor: |
|||
:path: "../../node_modules/@capacitor/ios" |
|||
CapacitorApp: |
|||
:path: "../../node_modules/@capacitor/app" |
|||
CapacitorCamera: |
|||
:path: "../../node_modules/@capacitor/camera" |
|||
CapacitorCordova: |
|||
:path: "../../node_modules/@capacitor/ios" |
|||
CapacitorFilesystem: |
|||
:path: "../../node_modules/@capacitor/filesystem" |
|||
CapacitorShare: |
|||
:path: "../../node_modules/@capacitor/share" |
|||
CapawesomeCapacitorFilePicker: |
|||
:path: "../../node_modules/@capawesome/capacitor-file-picker" |
|||
|
|||
SPEC CHECKSUMS: |
|||
Capacitor: 05d35014f4425b0740fc8776481f6a369ad071bf |
|||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf |
|||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 |
|||
CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9 |
|||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79 |
|||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff |
|||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 |
|||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e |
|||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd |
|||
|
|||
PODFILE CHECKSUM: 4233f5c5f414604460ff96d372542c311b0fb7a8 |
|||
PODFILE CHECKSUM: 1e9280368fd410520414f5741bf8fdfe7847b965 |
|||
|
|||
COCOAPODS: 1.16.2 |
|||
|
@ -0,0 +1,22 @@ |
|||
#!/bin/bash |
|||
|
|||
# Clean the public directory |
|||
rm -rf android/app/src/main/assets/public/* |
|||
|
|||
# Copy web assets |
|||
cp -r dist/* android/app/src/main/assets/public/ |
|||
|
|||
# Ensure the directory structure exists |
|||
mkdir -p android/app/src/main/assets/public/assets |
|||
|
|||
# Copy the main index file |
|||
cp dist/index.html android/app/src/main/assets/public/ |
|||
|
|||
# Copy all assets |
|||
cp -r dist/assets/* android/app/src/main/assets/public/assets/ |
|||
|
|||
# Copy other necessary files |
|||
cp dist/favicon.ico android/app/src/main/assets/public/ |
|||
cp dist/robots.txt android/app/src/main/assets/public/ |
|||
|
|||
echo "Web assets copied successfully!" |
@ -0,0 +1,22 @@ |
|||
#!/bin/bash |
|||
|
|||
# Create directories if they don't exist |
|||
mkdir -p android/app/src/main/res/mipmap-mdpi |
|||
mkdir -p android/app/src/main/res/mipmap-hdpi |
|||
mkdir -p android/app/src/main/res/mipmap-xhdpi |
|||
mkdir -p android/app/src/main/res/mipmap-xxhdpi |
|||
mkdir -p android/app/src/main/res/mipmap-xxxhdpi |
|||
|
|||
# Generate placeholder icons using ImageMagick |
|||
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png |
|||
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png |
|||
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png |
|||
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png |
|||
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png |
|||
|
|||
# Copy to round versions |
|||
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png |
@ -0,0 +1,196 @@ |
|||
/** * Data Export Section Component * * Provides UI and functionality for |
|||
exporting user data and backing up identifier seeds. * Includes buttons for seed |
|||
backup and database export, with platform-specific download instructions. * * |
|||
@component * @displayName DataExportSection * @example * ```vue * |
|||
<DataExportSection :active-did="currentDid" /> |
|||
* ``` */ |
|||
|
|||
<template> |
|||
<div |
|||
id="sectionDataExport" |
|||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" |
|||
> |
|||
<div class="mb-2 font-bold">Data Export</div> |
|||
<router-link |
|||
v-if="activeDid" |
|||
:to="{ name: 'seed-backup' }" |
|||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2" |
|||
> |
|||
Backup Identifier Seed |
|||
</router-link> |
|||
|
|||
<button |
|||
:class="computedStartDownloadLinkClassNames()" |
|||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" |
|||
@click="exportDatabase()" |
|||
> |
|||
Download Settings & Contacts |
|||
<br /> |
|||
(excluding Identifier Data) |
|||
</button> |
|||
<a |
|||
ref="downloadLink" |
|||
: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" |
|||
> |
|||
If no download happened yet, click again here to download now. |
|||
</a> |
|||
<div v-if="platformCapabilities.needsFileHandlingInstructions" class="mt-4"> |
|||
<p> |
|||
After the download, you can save the file in your preferred storage |
|||
location. |
|||
</p> |
|||
<ul> |
|||
<li |
|||
v-if="platformCapabilities.isIOS" |
|||
class="list-disc list-outside ml-4" |
|||
> |
|||
On iOS: You will be prompted to choose a location to save your backup |
|||
file. |
|||
</li> |
|||
<li |
|||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS" |
|||
class="list-disc list-outside ml-4" |
|||
> |
|||
On Android: You will be prompted to choose a location to save your |
|||
backup file. |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Prop, Vue } from "vue-facing-decorator"; |
|||
import { NotificationIface } from "../constants/app"; |
|||
import { db } from "../db/index"; |
|||
import { logger } from "../utils/logger"; |
|||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; |
|||
import { |
|||
PlatformService, |
|||
PlatformCapabilities, |
|||
} from "../services/PlatformService"; |
|||
|
|||
/** |
|||
* @vue-component |
|||
* Data Export Section Component |
|||
* Handles database export and seed backup functionality with platform-specific behavior |
|||
*/ |
|||
@Component |
|||
export default class DataExportSection extends Vue { |
|||
/** |
|||
* Notification function injected by Vue |
|||
* Used to show success/error messages to the user |
|||
*/ |
|||
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|||
|
|||
/** |
|||
* Active DID (Decentralized Identifier) of the user |
|||
* Controls visibility of seed backup option |
|||
* @required |
|||
*/ |
|||
@Prop({ required: true }) readonly activeDid!: string; |
|||
|
|||
/** |
|||
* URL for the database export download |
|||
* Created and revoked dynamically during export process |
|||
* Only used in web platform |
|||
*/ |
|||
downloadUrl = ""; |
|||
|
|||
/** |
|||
* Platform service instance for platform-specific operations |
|||
*/ |
|||
private platformService: PlatformService = |
|||
PlatformServiceFactory.getInstance(); |
|||
|
|||
/** |
|||
* Platform capabilities for the current platform |
|||
*/ |
|||
private get platformCapabilities(): PlatformCapabilities { |
|||
return this.platformService.getCapabilities(); |
|||
} |
|||
|
|||
/** |
|||
* Lifecycle hook to clean up resources |
|||
* Revokes object URL when component is unmounted (web platform only) |
|||
*/ |
|||
beforeUnmount() { |
|||
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) { |
|||
URL.revokeObjectURL(this.downloadUrl); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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 { |
|||
const blob = await db.export({ prettyJson: true }); |
|||
const fileName = `${db.name}-backup.json`; |
|||
|
|||
if (this.platformCapabilities.hasFileDownload) { |
|||
// Web platform: Use download link |
|||
this.downloadUrl = URL.createObjectURL(blob); |
|||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; |
|||
downloadAnchor.href = this.downloadUrl; |
|||
downloadAnchor.download = fileName; |
|||
downloadAnchor.click(); |
|||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); |
|||
} else if (this.platformCapabilities.hasFileSystem) { |
|||
// Native platform: Write to app directory |
|||
const content = await blob.text(); |
|||
await this.platformService.writeAndShareFile(fileName, content); |
|||
} |
|||
|
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Export Successful", |
|||
text: this.platformCapabilities.hasFileDownload |
|||
? "See your downloads directory for the backup. It is in the Dexie format." |
|||
: "Please choose a location to save your backup file.", |
|||
}, |
|||
-1, |
|||
); |
|||
} catch (error) { |
|||
logger.error("Export Error:", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Export Error", |
|||
text: "There was an error exporting the data.", |
|||
}, |
|||
3000, |
|||
); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Computes class names for the initial download button |
|||
* @returns Object with 'hidden' class when download is in progress (web platform only) |
|||
*/ |
|||
public computedStartDownloadLinkClassNames() { |
|||
return { |
|||
hidden: this.downloadUrl && this.platformCapabilities.hasFileDownload, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Computes class names for the secondary download link |
|||
* @returns Object with 'hidden' class when no download is available or not on web platform |
|||
*/ |
|||
public computedDownloadLinkClassNames() { |
|||
return { |
|||
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload, |
|||
}; |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,101 @@ |
|||
/** |
|||
* Represents the result of an image capture or selection operation. |
|||
* Contains both the image data as a Blob and the associated filename. |
|||
*/ |
|||
export interface ImageResult { |
|||
/** The image data as a Blob object */ |
|||
blob: Blob; |
|||
/** The filename associated with the image */ |
|||
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, |
|||
* 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. |
|||
* @param path - The path to the file to read |
|||
* @returns Promise resolving to the file contents as a string |
|||
*/ |
|||
readFile(path: string): Promise<string>; |
|||
|
|||
/** |
|||
* Writes content to a file at the specified path. |
|||
* @param path - The path where the file should be written |
|||
* @param content - The content to write to the file |
|||
* @returns Promise that resolves when the write is complete |
|||
*/ |
|||
writeFile(path: string, content: string): Promise<void>; |
|||
|
|||
/** |
|||
* Writes content to a file at the specified path and shares it. |
|||
* @param fileName - The filename of the file to write |
|||
* @param content - The content to write to the file |
|||
* @returns Promise that resolves when the write is complete |
|||
*/ |
|||
writeAndShareFile(fileName: string, content: string): Promise<void>; |
|||
|
|||
/** |
|||
* Deletes a file at the specified path. |
|||
* @param path - The path to the file to delete |
|||
* @returns Promise that resolves when the deletion is complete |
|||
*/ |
|||
deleteFile(path: string): Promise<void>; |
|||
|
|||
/** |
|||
* Lists all files in the specified directory. |
|||
* @param directory - The directory path to list |
|||
* @returns Promise resolving to an array of filenames |
|||
*/ |
|||
listFiles(directory: string): Promise<string[]>; |
|||
|
|||
// Camera operations
|
|||
/** |
|||
* Activates the device camera to take a picture. |
|||
* @returns Promise resolving to the captured image result |
|||
*/ |
|||
takePicture(): Promise<ImageResult>; |
|||
|
|||
/** |
|||
* Opens a file picker to select an existing image. |
|||
* @returns Promise resolving to the selected image result |
|||
*/ |
|||
pickImage(): Promise<ImageResult>; |
|||
|
|||
/** |
|||
* Handles deep link URLs for the application. |
|||
* @param url - The deep link URL to handle |
|||
* @returns Promise that resolves when the deep link has been handled |
|||
*/ |
|||
handleDeepLink(url: string): Promise<void>; |
|||
} |
@ -0,0 +1,58 @@ |
|||
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(); |
|||
* await platformService.takePicture(); |
|||
* ``` |
|||
*/ |
|||
export class PlatformServiceFactory { |
|||
private static instance: PlatformService | null = null; |
|||
|
|||
/** |
|||
* 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 { |
|||
if (PlatformServiceFactory.instance) { |
|||
return PlatformServiceFactory.instance; |
|||
} |
|||
|
|||
const platform = process.env.VITE_PLATFORM || "web"; |
|||
|
|||
switch (platform) { |
|||
case "capacitor": |
|||
PlatformServiceFactory.instance = new CapacitorPlatformService(); |
|||
break; |
|||
case "electron": |
|||
PlatformServiceFactory.instance = new ElectronPlatformService(); |
|||
break; |
|||
case "pywebview": |
|||
PlatformServiceFactory.instance = new PyWebViewPlatformService(); |
|||
break; |
|||
case "web": |
|||
default: |
|||
PlatformServiceFactory.instance = new WebPlatformService(); |
|||
break; |
|||
} |
|||
|
|||
return PlatformServiceFactory.instance; |
|||
} |
|||
} |
@ -0,0 +1,473 @@ |
|||
import { |
|||
ImageResult, |
|||
PlatformService, |
|||
PlatformCapabilities, |
|||
} from "../PlatformService"; |
|||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; |
|||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; |
|||
import { Share } from "@capacitor/share"; |
|||
import { logger } from "../../utils/logger"; |
|||
|
|||
/** |
|||
* Platform service implementation for Capacitor (mobile) platform. |
|||
* Provides native mobile functionality through Capacitor plugins for: |
|||
* - File system operations |
|||
* - Camera and image picker |
|||
* - 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, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Checks and requests storage permissions if needed |
|||
* @returns Promise that resolves when permissions are granted |
|||
* @throws Error if permissions are denied |
|||
*/ |
|||
private async checkStoragePermissions(): Promise<void> { |
|||
try { |
|||
const logData = { |
|||
platform: this.getCapabilities().isIOS ? "iOS" : "Android", |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.log( |
|||
"Checking storage permissions", |
|||
JSON.stringify(logData, null, 2), |
|||
); |
|||
|
|||
if (this.getCapabilities().isIOS) { |
|||
// iOS uses different permission model
|
|||
return; |
|||
} |
|||
|
|||
// Try to access a test directory to check permissions
|
|||
try { |
|||
await Filesystem.stat({ |
|||
path: "/storage/emulated/0/Download", |
|||
directory: Directory.Documents, |
|||
}); |
|||
logger.log( |
|||
"Storage permissions already granted", |
|||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2), |
|||
); |
|||
return; |
|||
} catch (error: unknown) { |
|||
const err = error as Error; |
|||
const errorLogData = { |
|||
error: { |
|||
message: err.message, |
|||
name: err.name, |
|||
stack: err.stack, |
|||
}, |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
|
|||
// "File does not exist" is expected and not a permission error
|
|||
if (err.message === "File does not exist") { |
|||
logger.log( |
|||
"Directory does not exist (expected), proceeding with write", |
|||
JSON.stringify(errorLogData, null, 2), |
|||
); |
|||
return; |
|||
} |
|||
|
|||
// Check for actual permission errors
|
|||
if ( |
|||
err.message.includes("permission") || |
|||
err.message.includes("access") |
|||
) { |
|||
logger.log( |
|||
"Permission check failed, requesting permissions", |
|||
JSON.stringify(errorLogData, null, 2), |
|||
); |
|||
|
|||
// The Filesystem plugin will automatically request permissions when needed
|
|||
// We just need to try the operation again
|
|||
try { |
|||
await Filesystem.stat({ |
|||
path: "/storage/emulated/0/Download", |
|||
directory: Directory.Documents, |
|||
}); |
|||
logger.log( |
|||
"Storage permissions granted after request", |
|||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2), |
|||
); |
|||
return; |
|||
} catch (retryError: unknown) { |
|||
const retryErr = retryError as Error; |
|||
throw new Error( |
|||
`Failed to obtain storage permissions: ${retryErr.message}`, |
|||
); |
|||
} |
|||
} |
|||
|
|||
// For any other error, log it but don't treat as permission error
|
|||
logger.log( |
|||
"Unexpected error during permission check", |
|||
JSON.stringify(errorLogData, null, 2), |
|||
); |
|||
return; |
|||
} |
|||
} catch (error: unknown) { |
|||
const err = error as Error; |
|||
const errorLogData = { |
|||
error: { |
|||
message: err.message, |
|||
name: err.name, |
|||
stack: err.stack, |
|||
}, |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.error( |
|||
"Error checking/requesting permissions", |
|||
JSON.stringify(errorLogData, null, 2), |
|||
); |
|||
throw new Error(`Failed to obtain storage permissions: ${err.message}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Reads a file from the app's data directory. |
|||
* @param path - Relative path to the file in the app's data directory |
|||
* @returns Promise resolving to the file contents as string |
|||
* @throws Error if file cannot be read or doesn't exist |
|||
*/ |
|||
async readFile(path: string): Promise<string> { |
|||
const file = await Filesystem.readFile({ |
|||
path, |
|||
directory: Directory.Data, |
|||
}); |
|||
if (file.data instanceof Blob) { |
|||
return await file.data.text(); |
|||
} |
|||
return file.data; |
|||
} |
|||
|
|||
/** |
|||
* Writes content to a file in the app's safe storage and offers sharing. |
|||
* |
|||
* Platform-specific behavior: |
|||
* - Saves to app's Documents directory |
|||
* - Offers sharing functionality to move file elsewhere |
|||
* |
|||
* The method handles: |
|||
* 1. Writing to app-safe storage |
|||
* 2. Sharing the file with user's preferred app |
|||
* 3. Error handling and logging |
|||
* |
|||
* @param fileName - The name of the file to create (e.g. "backup.json") |
|||
* @param content - The content to write to the file |
|||
* |
|||
* @throws Error if: |
|||
* - File writing fails |
|||
* - Sharing fails |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* // Save and share a JSON file
|
|||
* await platformService.writeFile( |
|||
* "backup.json", |
|||
* JSON.stringify(data) |
|||
* ); |
|||
* ``` |
|||
*/ |
|||
async writeFile(fileName: string, content: string): Promise<void> { |
|||
try { |
|||
const logData = { |
|||
targetFileName: fileName, |
|||
contentLength: content.length, |
|||
platform: this.getCapabilities().isIOS ? "iOS" : "Android", |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.log( |
|||
"Starting writeFile operation", |
|||
JSON.stringify(logData, null, 2), |
|||
); |
|||
|
|||
// For Android, we need to handle content URIs differently
|
|||
if (this.getCapabilities().isIOS) { |
|||
// Write to app's Documents directory for iOS
|
|||
const writeResult = await Filesystem.writeFile({ |
|||
path: fileName, |
|||
data: content, |
|||
directory: Directory.Data, |
|||
encoding: Encoding.UTF8, |
|||
}); |
|||
|
|||
const writeSuccessLogData = { |
|||
path: writeResult.uri, |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.log( |
|||
"File write successful", |
|||
JSON.stringify(writeSuccessLogData, null, 2), |
|||
); |
|||
|
|||
// Offer to share the file
|
|||
try { |
|||
await Share.share({ |
|||
title: "TimeSafari Backup", |
|||
text: "Here is your TimeSafari backup file.", |
|||
url: writeResult.uri, |
|||
dialogTitle: "Share your backup", |
|||
}); |
|||
|
|||
logger.log( |
|||
"Share dialog shown", |
|||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2), |
|||
); |
|||
} catch (shareError) { |
|||
// Log share error but don't fail the operation
|
|||
logger.error( |
|||
"Share dialog failed", |
|||
JSON.stringify( |
|||
{ |
|||
error: shareError, |
|||
timestamp: new Date().toISOString(), |
|||
}, |
|||
null, |
|||
2, |
|||
), |
|||
); |
|||
} |
|||
} else { |
|||
// For Android, first write to app's Documents directory
|
|||
const writeResult = await Filesystem.writeFile({ |
|||
path: fileName, |
|||
data: content, |
|||
directory: Directory.Data, |
|||
encoding: Encoding.UTF8, |
|||
}); |
|||
|
|||
const writeSuccessLogData = { |
|||
path: writeResult.uri, |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.log( |
|||
"File write successful to app storage", |
|||
JSON.stringify(writeSuccessLogData, null, 2), |
|||
); |
|||
|
|||
// Then share the file to let user choose where to save it
|
|||
try { |
|||
await Share.share({ |
|||
title: "TimeSafari Backup", |
|||
text: "Here is your TimeSafari backup file.", |
|||
url: writeResult.uri, |
|||
dialogTitle: "Save your backup", |
|||
}); |
|||
|
|||
logger.log( |
|||
"Share dialog shown for Android", |
|||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2), |
|||
); |
|||
} catch (shareError) { |
|||
// Log share error but don't fail the operation
|
|||
logger.error( |
|||
"Share dialog failed for Android", |
|||
JSON.stringify( |
|||
{ |
|||
error: shareError, |
|||
timestamp: new Date().toISOString(), |
|||
}, |
|||
null, |
|||
2, |
|||
), |
|||
); |
|||
} |
|||
} |
|||
} catch (error: unknown) { |
|||
const err = error as Error; |
|||
const finalErrorLogData = { |
|||
error: { |
|||
message: err.message, |
|||
name: err.name, |
|||
stack: err.stack, |
|||
}, |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.error( |
|||
"Error in writeFile operation:", |
|||
JSON.stringify(finalErrorLogData, null, 2), |
|||
); |
|||
throw new Error(`Failed to save file: ${err.message}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Writes content to a file in the device's app-private storage. |
|||
* Then shares the file using the system share dialog. |
|||
* |
|||
* Works on both Android and iOS without needing external storage permissions. |
|||
* |
|||
* @param fileName - The name of the file to create (e.g. "backup.json") |
|||
* @param content - The content to write to the file |
|||
*/ |
|||
async writeAndShareFile(fileName: string, content: string): Promise<void> { |
|||
const timestamp = new Date().toISOString(); |
|||
const logData = { |
|||
action: 'writeAndShareFile', |
|||
fileName, |
|||
contentLength: content.length, |
|||
timestamp, |
|||
}; |
|||
logger.log('[CapacitorPlatformService]', JSON.stringify(logData, null, 2)); |
|||
|
|||
try { |
|||
const { uri } = await Filesystem.writeFile({ |
|||
path: fileName, |
|||
data: content, |
|||
directory: Directory.Data, |
|||
encoding: Encoding.UTF8, |
|||
recursive: true, |
|||
}); |
|||
|
|||
logger.log('[CapacitorPlatformService] File write successful:', { uri, timestamp: new Date().toISOString() }); |
|||
|
|||
await Share.share({ |
|||
title: 'TimeSafari Backup', |
|||
text: 'Here is your backup file.', |
|||
url: uri, |
|||
dialogTitle: 'Share your backup file', |
|||
}); |
|||
} catch (error) { |
|||
const err = error as Error; |
|||
const errLog = { |
|||
message: err.message, |
|||
stack: err.stack, |
|||
timestamp: new Date().toISOString(), |
|||
}; |
|||
logger.error('[CapacitorPlatformService] Error writing or sharing file:', JSON.stringify(errLog, null, 2)); |
|||
throw new Error(`Failed to write or share file: ${err.message}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Deletes a file from the app's data directory. |
|||
* @param path - Relative path to the file to delete |
|||
* @throws Error if deletion fails or file doesn't exist |
|||
*/ |
|||
async deleteFile(path: string): Promise<void> { |
|||
await Filesystem.deleteFile({ |
|||
path, |
|||
directory: Directory.Data, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Lists files in the specified directory within app's data directory. |
|||
* @param directory - Relative path to the directory to list |
|||
* @returns Promise resolving to array of filenames |
|||
* @throws Error if directory cannot be read or doesn't exist |
|||
*/ |
|||
async listFiles(directory: string): Promise<string[]> { |
|||
const result = await Filesystem.readdir({ |
|||
path: directory, |
|||
directory: Directory.Data, |
|||
}); |
|||
return result.files.map((file) => |
|||
typeof file === "string" ? file : file.name, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Opens the device camera to take a picture. |
|||
* Configures camera for high quality images with editing enabled. |
|||
* @returns Promise resolving to the captured image data |
|||
* @throws Error if camera access fails or user cancels |
|||
*/ |
|||
async takePicture(): Promise<ImageResult> { |
|||
try { |
|||
const image = await Camera.getPhoto({ |
|||
quality: 90, |
|||
allowEditing: true, |
|||
resultType: CameraResultType.Base64, |
|||
source: CameraSource.Camera, |
|||
}); |
|||
|
|||
const blob = await this.processImageData(image.base64String); |
|||
return { |
|||
blob, |
|||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`, |
|||
}; |
|||
} catch (error) { |
|||
logger.error("Error taking picture with Capacitor:", error); |
|||
throw new Error("Failed to take picture"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Opens the device photo gallery to pick an existing image. |
|||
* Configures picker for high quality images with editing enabled. |
|||
* @returns Promise resolving to the selected image data |
|||
* @throws Error if gallery access fails or user cancels |
|||
*/ |
|||
async pickImage(): Promise<ImageResult> { |
|||
try { |
|||
const image = await Camera.getPhoto({ |
|||
quality: 90, |
|||
allowEditing: true, |
|||
resultType: CameraResultType.Base64, |
|||
source: CameraSource.Photos, |
|||
}); |
|||
|
|||
const blob = await this.processImageData(image.base64String); |
|||
return { |
|||
blob, |
|||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`, |
|||
}; |
|||
} catch (error) { |
|||
logger.error("Error picking image with Capacitor:", error); |
|||
throw new Error("Failed to pick image"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Converts base64 image data to a Blob. |
|||
* @param base64String - Base64 encoded image data |
|||
* @returns Promise resolving to image Blob |
|||
* @throws Error if conversion fails |
|||
*/ |
|||
private async processImageData(base64String?: string): Promise<Blob> { |
|||
if (!base64String) { |
|||
throw new Error("No image data received"); |
|||
} |
|||
|
|||
// Convert base64 to blob
|
|||
const byteCharacters = atob(base64String); |
|||
const byteArrays = []; |
|||
for (let offset = 0; offset < byteCharacters.length; offset += 512) { |
|||
const slice = byteCharacters.slice(offset, offset + 512); |
|||
const byteNumbers = new Array(slice.length); |
|||
for (let i = 0; i < slice.length; i++) { |
|||
byteNumbers[i] = slice.charCodeAt(i); |
|||
} |
|||
const byteArray = new Uint8Array(byteNumbers); |
|||
byteArrays.push(byteArray); |
|||
} |
|||
return new Blob(byteArrays, { type: "image/jpeg" }); |
|||
} |
|||
|
|||
/** |
|||
* Handles deep link URLs for the application. |
|||
* Note: Capacitor handles deep links automatically. |
|||
* @param _url - The deep link URL (unused) |
|||
*/ |
|||
async handleDeepLink(_url: string): Promise<void> { |
|||
// Capacitor handles deep links automatically
|
|||
// This is just a placeholder for the interface
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
@ -0,0 +1,111 @@ |
|||
import { |
|||
ImageResult, |
|||
PlatformService, |
|||
PlatformCapabilities, |
|||
} from "../PlatformService"; |
|||
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: |
|||
* - Native file system access |
|||
* - Desktop camera integration |
|||
* - 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 |
|||
* @returns Promise that should resolve to file contents |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file reading using Electron's file system API |
|||
*/ |
|||
async readFile(_path: string): Promise<string> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Writes content to a file. |
|||
* @param _path - Path where to write the file |
|||
* @param _content - Content to write to the file |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file writing using Electron's file system API |
|||
*/ |
|||
async writeFile(_path: string, _content: string): Promise<void> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Deletes a file from the filesystem. |
|||
* @param _path - Path to the file to delete |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file deletion using Electron's file system API |
|||
*/ |
|||
async deleteFile(_path: string): Promise<void> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Lists files in the specified directory. |
|||
* @param _directory - Path to the directory to list |
|||
* @returns Promise that should resolve to array of filenames |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement directory listing using Electron's file system API |
|||
*/ |
|||
async listFiles(_directory: string): Promise<string[]> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Should open system camera to take a picture. |
|||
* @returns Promise that should resolve to captured image data |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement camera access using Electron's media APIs |
|||
*/ |
|||
async takePicture(): Promise<ImageResult> { |
|||
logger.error("takePicture not implemented in Electron platform"); |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Should open system file picker for selecting an image. |
|||
* @returns Promise that should resolve to selected image data |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file picker using Electron's dialog API |
|||
*/ |
|||
async pickImage(): Promise<ImageResult> { |
|||
logger.error("pickImage not implemented in Electron platform"); |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Should handle deep link URLs for the desktop application. |
|||
* @param _url - The deep link URL to handle |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement deep link handling using Electron's protocol handler |
|||
*/ |
|||
async handleDeepLink(_url: string): Promise<void> { |
|||
logger.error("handleDeepLink not implemented in Electron platform"); |
|||
throw new Error("Not implemented"); |
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
import { |
|||
ImageResult, |
|||
PlatformService, |
|||
PlatformCapabilities, |
|||
} from "../PlatformService"; |
|||
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: |
|||
* - Integration with Python backend file operations |
|||
* - System camera access through Python |
|||
* - Native system dialogs via pywebview |
|||
* - 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 |
|||
* @returns Promise that should resolve to file contents |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file reading through pywebview's Python-JavaScript bridge |
|||
*/ |
|||
async readFile(_path: string): Promise<string> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Writes content to a file using the Python backend. |
|||
* @param _path - Path where to write the file |
|||
* @param _content - Content to write to the file |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file writing through pywebview's Python-JavaScript bridge |
|||
*/ |
|||
async writeFile(_path: string, _content: string): Promise<void> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Deletes a file using the Python backend. |
|||
* @param _path - Path to the file to delete |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file deletion through pywebview's Python-JavaScript bridge |
|||
*/ |
|||
async deleteFile(_path: string): Promise<void> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Lists files in the specified directory using the Python backend. |
|||
* @param _directory - Path to the directory to list |
|||
* @returns Promise that should resolve to array of filenames |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement directory listing through pywebview's Python-JavaScript bridge |
|||
*/ |
|||
async listFiles(_directory: string): Promise<string[]> { |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Should open system camera through Python backend. |
|||
* @returns Promise that should resolve to captured image data |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement camera access using Python's camera libraries |
|||
*/ |
|||
async takePicture(): Promise<ImageResult> { |
|||
logger.error("takePicture not implemented in PyWebView platform"); |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Should open system file picker through pywebview. |
|||
* @returns Promise that should resolve to selected image data |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement file picker using pywebview's file dialog API |
|||
*/ |
|||
async pickImage(): Promise<ImageResult> { |
|||
logger.error("pickImage not implemented in PyWebView platform"); |
|||
throw new Error("Not implemented"); |
|||
} |
|||
|
|||
/** |
|||
* Should handle deep link URLs through the Python backend. |
|||
* @param _url - The deep link URL to handle |
|||
* @throws Error with "Not implemented" message |
|||
* @todo Implement deep link handling using Python's URL handling capabilities |
|||
*/ |
|||
async handleDeepLink(_url: string): Promise<void> { |
|||
logger.error("handleDeepLink not implemented in PyWebView platform"); |
|||
throw new Error("Not implemented"); |
|||
} |
|||
} |
@ -0,0 +1,231 @@ |
|||
import { |
|||
ImageResult, |
|||
PlatformService, |
|||
PlatformCapabilities, |
|||
} from "../PlatformService"; |
|||
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. |
|||
*/ |
|||
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 |
|||
* @throws Error indicating file system access is not available |
|||
*/ |
|||
async readFile(_path: string): Promise<string> { |
|||
throw new Error("File system access not available in web platform"); |
|||
} |
|||
|
|||
/** |
|||
* Not supported in web platform. |
|||
* @param _path - Unused path parameter |
|||
* @param _content - Unused content parameter |
|||
* @throws Error indicating file system access is not available |
|||
*/ |
|||
async writeFile(_path: string, _content: string): Promise<void> { |
|||
throw new Error("File system access not available in web platform"); |
|||
} |
|||
|
|||
/** |
|||
* Not supported in web platform. |
|||
* @param _path - Unused path parameter |
|||
* @throws Error indicating file system access is not available |
|||
*/ |
|||
async deleteFile(_path: string): Promise<void> { |
|||
throw new Error("File system access not available in web platform"); |
|||
} |
|||
|
|||
/** |
|||
* Not supported in web platform. |
|||
* @param _directory - Unused directory parameter |
|||
* @throws Error indicating file system access is not available |
|||
*/ |
|||
async listFiles(_directory: string): Promise<string[]> { |
|||
throw new Error("File system access not available in web platform"); |
|||
} |
|||
|
|||
/** |
|||
* 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. |
|||
* Processes the captured image to ensure consistent format. |
|||
*/ |
|||
async takePicture(): Promise<ImageResult> { |
|||
return new Promise((resolve, reject) => { |
|||
const input = document.createElement("input"); |
|||
input.type = "file"; |
|||
input.accept = "image/*"; |
|||
input.capture = "environment"; |
|||
|
|||
input.onchange = async (e) => { |
|||
const file = (e.target as HTMLInputElement).files?.[0]; |
|||
if (file) { |
|||
try { |
|||
const blob = await this.processImageFile(file); |
|||
resolve({ |
|||
blob, |
|||
fileName: file.name || "photo.jpg", |
|||
}); |
|||
} catch (error) { |
|||
logger.error("Error processing camera image:", error); |
|||
reject(new Error("Failed to process camera image")); |
|||
} |
|||
} else { |
|||
reject(new Error("No image captured")); |
|||
} |
|||
}; |
|||
|
|||
input.click(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 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. |
|||
*/ |
|||
async pickImage(): Promise<ImageResult> { |
|||
return new Promise((resolve, reject) => { |
|||
const input = document.createElement("input"); |
|||
input.type = "file"; |
|||
input.accept = "image/*"; |
|||
|
|||
input.onchange = async (e) => { |
|||
const file = (e.target as HTMLInputElement).files?.[0]; |
|||
if (file) { |
|||
try { |
|||
const blob = await this.processImageFile(file); |
|||
resolve({ |
|||
blob, |
|||
fileName: file.name || "photo.jpg", |
|||
}); |
|||
} catch (error) { |
|||
logger.error("Error processing picked image:", error); |
|||
reject(new Error("Failed to process picked image")); |
|||
} |
|||
} else { |
|||
reject(new Error("No image selected")); |
|||
} |
|||
}; |
|||
|
|||
input.click(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 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. |
|||
*/ |
|||
private async processImageFile(file: File): Promise<Blob> { |
|||
return new Promise((resolve, reject) => { |
|||
const reader = new FileReader(); |
|||
reader.onload = (event) => { |
|||
const dataUrl = event.target?.result as string; |
|||
// Convert to blob to ensure consistent format
|
|||
fetch(dataUrl) |
|||
.then((res) => res.blob()) |
|||
.then((blob) => resolve(blob)) |
|||
.catch((error) => { |
|||
logger.error("Error converting data URL to blob:", error); |
|||
reject(error); |
|||
}); |
|||
}; |
|||
reader.onerror = (error) => { |
|||
logger.error("Error reading file:", error); |
|||
reject(error); |
|||
}; |
|||
reader.readAsDataURL(file); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 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 false, as this is not PyWebView |
|||
*/ |
|||
isPyWebView(): boolean { |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Checks if running on web platform. |
|||
* @returns true, as this is the web implementation |
|||
*/ |
|||
isWeb(): boolean { |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* 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 |
|||
*/ |
|||
async handleDeepLink(_url: string): Promise<void> { |
|||
// Web platform can handle deep links through URL parameters
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
@ -1,35 +1,31 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers |
|||
"useDefineForClassFields": true, |
|||
"module": "ESNext", // Use ES modules |
|||
"strict": true, // Enable all strict type checking options |
|||
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
|||
"skipLibCheck": true, |
|||
"moduleResolution": "bundler", |
|||
"allowImportingTsExtensions": true, |
|||
"resolveJsonModule": true, |
|||
"isolatedModules": true, |
|||
"noEmit": true, |
|||
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler |
|||
"moduleResolution": "node", // Use Node.js style module resolution |
|||
"strict": true, // Enable all strict type checking options |
|||
"noUnusedLocals": true, |
|||
"noUnusedParameters": true, |
|||
"noFallthroughCasesInSwitch": true, |
|||
"baseUrl": ".", |
|||
"experimentalDecorators": true, |
|||
"esModuleInterop": true, // Enables compatibility with CommonJS modules for default imports |
|||
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export |
|||
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file |
|||
"useDefineForClassFields": true, |
|||
"sourceMap": true, |
|||
"baseUrl": "./src", // Base directory to resolve non-relative module names |
|||
"paths": { |
|||
"@/components/*": ["components/*"], |
|||
"@/views/*": ["views/*"], |
|||
"@/db/*": ["db/*"], |
|||
"@/libs/*": ["libs/*"], |
|||
"@/constants/*": ["constants/*"], |
|||
"@/store/*": ["store/*"] |
|||
}, |
|||
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs |
|||
"@/*": ["src/*"] |
|||
} |
|||
}, |
|||
"include": [ |
|||
"src/**/*.ts", |
|||
"src/**/*.d.ts", |
|||
"src/**/*.tsx", |
|||
"src/**/*.vue", |
|||
"test-playwright/**/*.ts", |
|||
"test-playwright/**/*.tsx" |
|||
"src/**/*.vue" |
|||
], |
|||
"exclude": [ |
|||
"node_modules" |
|||
] |
|||
"references": [{ "path": "./tsconfig.node.json" }] |
|||
} |
|||
|
@ -0,0 +1,10 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"composite": true, |
|||
"skipLibCheck": true, |
|||
"module": "ESNext", |
|||
"moduleResolution": "bundler", |
|||
"allowSyntheticDefaultImports": true |
|||
}, |
|||
"include": ["vite.config.*"] |
|||
} |