Browse Source

chore: linting; more non-errors need fixing

capacitor-local-save
Matthew Raymer 1 day ago
parent
commit
a1b6add178
  1. 439
      src/components/BackupFilesList.vue
  2. 30
      src/components/DataExportSection.vue
  3. 4
      src/libs/capacitor/app.ts
  4. 43
      src/services/PlatformService.ts
  5. 2008
      src/services/platforms/CapacitorPlatformService.ts
  6. 65
      src/services/platforms/ElectronPlatformService.ts
  7. 69
      src/services/platforms/PyWebViewPlatformService.ts
  8. 72
      src/services/platforms/WebPlatformService.ts
  9. 2
      src/utils/logger.ts
  10. 14
      src/views/TestView.vue

439
src/components/BackupFilesList.vue

@ -1,18 +1,10 @@
/** /** * Backup Files List Component * * Displays a list of backup files saved by
* Backup Files List Component the app and provides options to: * - View backup files by type (contacts, seed,
* other) * - Open individual files in the device's file viewer * - Access the
* Displays a list of backup files saved by the app and provides options to: backup directory in the device's file explorer * * @component * @displayName
* - View backup files by type (contacts, seed, other) BackupFilesList * @example * ```vue *
* - Open individual files in the device's file viewer <BackupFilesList />
* - Access the backup directory in the device's file explorer * ``` */
*
* @component
* @displayName BackupFilesList
* @example
* ```vue
* <BackupFilesList />
* ```
*/
<template> <template>
<div class="backup-files-list"> <div class="backup-files-list">
@ -21,47 +13,47 @@
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
v-if="platformCapabilities.hasFileSystem" v-if="platformCapabilities.hasFileSystem"
@click="refreshFiles()"
class="text-sm bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded" class="text-sm bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"
:disabled="isLoading" :disabled="isLoading"
@click="refreshFiles()"
> >
<font-awesome <font-awesome
icon="refresh" icon="refresh"
class="fa-fw" class="fa-fw"
:class="{ 'animate-spin': isLoading }" :class="{ 'animate-spin': isLoading }"
/> />
Refresh Refresh
</button> </button>
<button <button
v-if="platformCapabilities.hasFileSystem" v-if="platformCapabilities.hasFileSystem"
@click="openBackupDirectory()"
class="text-sm bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded" class="text-sm bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded"
:disabled="isLoading" :disabled="isLoading"
@click="openBackupDirectory()"
> >
<font-awesome icon="folder-open" class="fa-fw" /> <font-awesome icon="folder-open" class="fa-fw" />
Open Directory Open Directory
</button> </button>
<button <button
v-if="platformCapabilities.hasFileSystem && isDevelopment" v-if="platformCapabilities.hasFileSystem && isDevelopment"
@click="debugFileDiscovery()"
class="text-sm bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded" class="text-sm bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded"
:disabled="isLoading" :disabled="isLoading"
title="Debug file discovery (development only)" title="Debug file discovery (development only)"
@click="debugFileDiscovery()"
> >
<font-awesome icon="bug" class="fa-fw" /> <font-awesome icon="bug" class="fa-fw" />
Debug Debug
</button> </button>
<button <button
@click="createTestBackup"
:disabled="isLoading" :disabled="isLoading"
class="px-3 py-1 bg-green-500 text-white rounded text-sm hover:bg-green-600 disabled:opacity-50" class="px-3 py-1 bg-green-500 text-white rounded text-sm hover:bg-green-600 disabled:opacity-50"
@click="createTestBackup"
> >
Create Test Backup Create Test Backup
</button> </button>
<button <button
@click="testDirectoryContexts"
:disabled="isLoading" :disabled="isLoading"
class="px-3 py-1 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 disabled:opacity-50" class="px-3 py-1 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 disabled:opacity-50"
@click="testDirectoryContexts"
> >
Test Contexts Test Contexts
</button> </button>
@ -73,21 +65,40 @@
<p class="mt-2">Loading backup files...</p> <p class="mt-2">Loading backup files...</p>
</div> </div>
<div v-else-if="backupFiles.length === 0" class="text-center py-4 text-gray-500"> <div
v-else-if="backupFiles.length === 0"
class="text-center py-4 text-gray-500"
>
<font-awesome icon="folder-open" class="fa-2x mb-2" /> <font-awesome icon="folder-open" class="fa-2x mb-2" />
<p>No backup files found</p> <p>No backup files found</p>
<p class="text-sm mt-1">Create backups using the export functions above</p> <p class="text-sm mt-1">
<div class="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg text-left"> Create backups using the export functions above
<p class="text-sm font-medium text-blue-800 mb-2">💡 How to create backup files:</p> </p>
<div
class="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg text-left"
>
<p class="text-sm font-medium text-blue-800 mb-2">
💡 How to create backup files:
</p>
<ul class="text-xs text-blue-700 space-y-1"> <ul class="text-xs text-blue-700 space-y-1">
<li> Use the "Export Contacts" button above to create contact backups</li> <li>
Use the "Export Contacts" button above to create contact backups
</li>
<li> Use the "Export Seed" button to backup your recovery phrase</li> <li> Use the "Export Seed" button to backup your recovery phrase</li>
<li> Backup files are saved to persistent storage that survives app installations</li> <li>
<li v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS" class="text-orange-700"> Backup files are saved to persistent storage that survives app
On Android: Files are saved to Downloads/TimeSafari or app data directory installations
</li>
<li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
class="text-orange-700"
>
On Android: Files are saved to Downloads/TimeSafari or app data
directory
</li> </li>
<li v-if="platformCapabilities.isIOS" class="text-orange-700"> <li v-if="platformCapabilities.isIOS" class="text-orange-700">
On iOS: Files are saved to Documents folder (accessible via Files app) On iOS: Files are saved to Documents folder (accessible via Files
app)
</li> </li>
</ul> </ul>
</div> </div>
@ -99,18 +110,20 @@
<button <button
v-for="type in ['all', 'contacts', 'seed', 'other'] as const" v-for="type in ['all', 'contacts', 'seed', 'other'] as const"
:key="type" :key="type"
@click="selectedType = type"
:class="[ :class="[
'text-sm px-3 py-1 rounded border', 'text-sm px-3 py-1 rounded border',
selectedType === type selectedType === type
? 'bg-blue-500 text-white border-blue-500' ? 'bg-blue-500 text-white border-blue-500'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
]" ]"
@click="selectedType = type"
> >
{{ type === 'all' ? 'All' : type.charAt(0).toUpperCase() + type.slice(1) }} {{
<span class="ml-1 text-xs"> type === "all"
({{ getFileCountByType(type) }}) ? "All"
</span> : type.charAt(0).toUpperCase() + type.slice(1)
}}
<span class="ml-1 text-xs"> ({{ getFileCountByType(type) }}) </span>
</button> </button>
</div> </div>
@ -118,9 +131,9 @@
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<span v-for="(crumb, idx) in breadcrumbs" :key="idx"> <span v-for="(crumb, idx) in breadcrumbs" :key="idx">
<span <span
v-if="idx < breadcrumbs.length - 1"
class="text-blue-600 cursor-pointer underline" class="text-blue-600 cursor-pointer underline"
@click="goToBreadcrumb(idx)" @click="goToBreadcrumb(idx)"
v-if="idx < breadcrumbs.length - 1"
> >
{{ crumb }} {{ crumb }}
</span> </span>
@ -129,14 +142,23 @@
</span> </span>
</div> </div>
<div v-if="currentPath.length > 1" class="mb-2"> <div v-if="currentPath.length > 1" class="mb-2">
<button @click="goUp" class="text-xs text-blue-500 underline"> Up</button> <button class="text-xs text-blue-500 underline" @click="goUp">
Up
</button>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" v-model="debugShowAll" @change="loadDirectory" class="mr-2" /> <input
v-model="debugShowAll"
type="checkbox"
class="mr-2"
@change="loadDirectory"
/>
<span class="text-xs">Debug: Show all entries as files</span> <span class="text-xs">Debug: Show all entries as files</span>
</label> </label>
<span v-if="debugShowAll" class="text-xs text-red-600 ml-2">[Debug mode: forcibly treating all entries as files]</span> <span v-if="debugShowAll" class="text-xs text-red-600 ml-2"
>[Debug mode: forcibly treating all entries as files]</span
>
</div> </div>
<div class="space-y-2 max-h-64 overflow-y-auto"> <div class="space-y-2 max-h-64 overflow-y-auto">
<div <div
@ -148,7 +170,10 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<font-awesome icon="folder" class="fa-fw text-yellow-500" /> <font-awesome icon="folder" class="fa-fw text-yellow-500" />
<span class="font-medium">{{ entry.name }}</span> <span class="font-medium">{{ entry.name }}</span>
<span class="text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full ml-2">Folder</span> <span
class="text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full ml-2"
>Folder</span
>
</div> </div>
</div> </div>
<div <div
@ -164,14 +189,18 @@
<div class="text-sm text-gray-500 mt-1"> <div class="text-sm text-gray-500 mt-1">
<span v-if="entry.size">{{ formatFileSize(entry.size) }}</span> <span v-if="entry.size">{{ formatFileSize(entry.size) }}</span>
<span v-else>Size unknown</span> <span v-else>Size unknown</span>
<span v-if="entry.path && !platformCapabilities.isIOS" class="ml-2 text-xs text-blue-600">📁 {{ entry.path }}</span> <span
v-if="entry.path && !platformCapabilities.isIOS"
class="ml-2 text-xs text-blue-600"
>📁 {{ entry.path }}</span
>
</div> </div>
</div> </div>
<div class="flex gap-2 ml-3"> <div class="flex gap-2 ml-3">
<button <button
@click="openFile(entry.uri, entry.name)"
class="text-blue-500 hover:text-blue-700 p-1" class="text-blue-500 hover:text-blue-700 p-1"
title="Open file" title="Open file"
@click="openFile(entry.uri, entry.name)"
> >
<font-awesome icon="external-link-alt" class="fa-fw" /> <font-awesome icon="external-link-alt" class="fa-fw" />
</button> </button>
@ -181,15 +210,28 @@
<!-- Summary --> <!-- Summary -->
<div class="text-sm text-gray-500 mt-3 pt-3 border-t"> <div class="text-sm text-gray-500 mt-3 pt-3 border-t">
Showing {{ filteredFiles.length }} of {{ backupFiles.length }} backup files Showing {{ filteredFiles.length }} of {{ backupFiles.length }} backup
files
</div> </div>
<div class="text-sm text-gray-600 mb-2"> <div class="text-sm text-gray-600 mb-2">
<p>📁 Backup files are saved to persistent storage that survives app installations:</p> <p>
📁 Backup files are saved to persistent storage that survives app
installations:
</p>
<ul class="list-disc list-inside ml-2 mt-1 text-xs"> <ul class="list-disc list-inside ml-2 mt-1 text-xs">
<li v-if="platformCapabilities.isIOS">iOS: Documents folder (accessible via Files app)</li> <li v-if="platformCapabilities.isIOS">
<li v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS">Android: Downloads/TimeSafari or external storage (accessible via file managers)</li> iOS: Documents folder (accessible via Files app)
<li v-if="!platformCapabilities.isMobile">Desktop: User's download directory</li> </li>
<li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
>
Android: Downloads/TimeSafari or external storage (accessible via
file managers)
</li>
<li v-if="!platformCapabilities.isMobile">
Desktop: User's download directory
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -222,7 +264,8 @@ export default class BackupFilesList extends Vue {
/** /**
* Platform service instance for platform-specific operations * Platform service instance for platform-specific operations
*/ */
private platformService: PlatformService = PlatformServiceFactory.getInstance(); private platformService: PlatformService =
PlatformServiceFactory.getInstance();
/** /**
* Platform capabilities for the current platform * Platform capabilities for the current platform
@ -234,12 +277,18 @@ export default class BackupFilesList extends Vue {
/** /**
* List of backup files found on the device * List of backup files found on the device
*/ */
backupFiles: Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}> = []; backupFiles: Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}> = [];
/** /**
* Currently selected file type filter * Currently selected file type filter
*/ */
selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all'; selectedType: "all" | "contacts" | "seed" | "other" = "all";
/** /**
* Loading state for file operations * Loading state for file operations
@ -259,7 +308,13 @@ export default class BackupFilesList extends Vue {
/** /**
* List of files/folders in the current directory * List of files/folders in the current directory
*/ */
directoryEntries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = []; directoryEntries: Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}> = [];
/** /**
* Temporary debug mode to show all entries as files * Temporary debug mode to show all entries as files
@ -271,28 +326,37 @@ export default class BackupFilesList extends Vue {
* Returns true if permission is granted, false otherwise. * Returns true if permission is granted, false otherwise.
*/ */
private async ensureStoragePermission(): Promise<boolean> { private async ensureStoragePermission(): Promise<boolean> {
logger.log('[BackupFilesList] ensureStoragePermission called. platformCapabilities:', this.platformCapabilities); logger.log(
"[BackupFilesList] ensureStoragePermission called. platformCapabilities:",
this.platformCapabilities,
);
if (!this.platformCapabilities.hasFileSystem) return true; if (!this.platformCapabilities.hasFileSystem) return true;
// Only relevant for native platforms (Android/iOS) // Only relevant for native platforms (Android/iOS)
const platformService = this.platformService as any; const platformService = this.platformService as any;
if (typeof platformService.checkStoragePermissions === 'function') { if (typeof platformService.checkStoragePermissions === "function") {
try { try {
await platformService.checkStoragePermissions(); await platformService.checkStoragePermissions();
logger.log('[BackupFilesList] Storage permission granted.'); logger.log("[BackupFilesList] Storage permission granted.");
return true; return true;
} catch (error) { } catch (error) {
logger.error('[BackupFilesList] Storage permission denied:', error); logger.error("[BackupFilesList] Storage permission denied:", error);
// Get specific guidance for the platform // Get specific guidance for the platform
let guidance = "This app needs permission to access your files to list and restore backups."; let guidance =
if (typeof platformService.getStoragePermissionGuidance === 'function') { "This app needs permission to access your files to list and restore backups.";
if (
typeof platformService.getStoragePermissionGuidance === "function"
) {
try { try {
guidance = await platformService.getStoragePermissionGuidance(); guidance = await platformService.getStoragePermissionGuidance();
} catch (guidanceError) { } catch (guidanceError) {
logger.warn('[BackupFilesList] Could not get permission guidance:', guidanceError); logger.warn(
"[BackupFilesList] Could not get permission guidance:",
guidanceError,
);
} }
} }
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -312,21 +376,27 @@ export default class BackupFilesList extends Vue {
* Lifecycle hook to load backup files when component is mounted * Lifecycle hook to load backup files when component is mounted
*/ */
async mounted() { async mounted() {
logger.log('[BackupFilesList] mounted hook called. platformCapabilities:', this.platformCapabilities); logger.log(
"[BackupFilesList] mounted hook called. platformCapabilities:",
this.platformCapabilities,
);
if (this.platformCapabilities.hasFileSystem) { if (this.platformCapabilities.hasFileSystem) {
// Check/request permission before loading // Check/request permission before loading
const hasPermission = await this.ensureStoragePermission(); const hasPermission = await this.ensureStoragePermission();
if (hasPermission) { if (hasPermission) {
// Set default root path // Set default root path
if (this.platformCapabilities.isIOS) { if (this.platformCapabilities.isIOS) {
this.currentPath = ['.']; this.currentPath = ["."];
} else { } else {
this.currentPath = ['Download', 'TimeSafari']; this.currentPath = ["Download", "TimeSafari"];
} }
await this.loadDirectory(); await this.loadDirectory();
this.refreshInterval = window.setInterval(() => { this.refreshInterval = window.setInterval(
this.loadDirectory(); () => {
}, 5 * 60 * 1000); this.loadDirectory();
},
5 * 60 * 1000,
);
} }
} }
} }
@ -346,12 +416,17 @@ export default class BackupFilesList extends Vue {
* Note: The 'All' tab count is sometimes too small. Logging for debugging. * Note: The 'All' tab count is sometimes too small. Logging for debugging.
*/ */
get filteredFiles() { get filteredFiles() {
if (this.selectedType === 'all') { if (this.selectedType === "all") {
logger.log('[BackupFilesList] filteredFiles (All):', this.backupFiles); logger.log("[BackupFilesList] filteredFiles (All):", this.backupFiles);
return this.backupFiles; return this.backupFiles;
} }
const filtered = this.backupFiles.filter(file => file.type === this.selectedType); const filtered = this.backupFiles.filter(
logger.log(`[BackupFilesList] filteredFiles (${this.selectedType}):`, filtered); (file) => file.type === this.selectedType,
);
logger.log(
`[BackupFilesList] filteredFiles (${this.selectedType}):`,
filtered,
);
return filtered; return filtered;
} }
@ -369,11 +444,18 @@ export default class BackupFilesList extends Vue {
if (!this.platformCapabilities.hasFileSystem) return; if (!this.platformCapabilities.hasFileSystem) return;
this.isLoading = true; this.isLoading = true;
try { try {
const path = this.currentPath.join('/') || (this.platformCapabilities.isIOS ? '.' : 'Download/TimeSafari'); const path =
this.directoryEntries = await (this.platformService as any).listFilesInDirectory(path, this.debugShowAll); this.currentPath.join("/") ||
logger.log('[BackupFilesList] Loaded directory:', { path, entries: this.directoryEntries }); (this.platformCapabilities.isIOS ? "." : "Download/TimeSafari");
this.directoryEntries = await (
this.platformService as PlatformService
).listFilesInDirectory(path, this.debugShowAll);
logger.log("[BackupFilesList] Loaded directory:", {
path,
entries: this.directoryEntries,
});
} catch (error) { } catch (error) {
logger.error('[BackupFilesList] Failed to load directory:', error); logger.error("[BackupFilesList] Failed to load directory:", error);
this.directoryEntries = []; this.directoryEntries = [];
} finally { } finally {
this.isLoading = false; this.isLoading = false;
@ -417,17 +499,17 @@ export default class BackupFilesList extends Vue {
* Computed property for showing files and folders * Computed property for showing files and folders
*/ */
get folders() { get folders() {
return this.directoryEntries.filter(e => e.type === 'folder'); return this.directoryEntries.filter((e) => e.type === "folder");
} }
get files() { get files() {
return this.directoryEntries.filter(e => e.type === 'file'); return this.directoryEntries.filter((e) => e.type === "file");
} }
/** /**
* Refreshes the list of backup files from the device * Refreshes the list of backup files from the device
*/ */
async refreshFiles() { async refreshFiles() {
logger.log('[BackupFilesList] refreshFiles called.'); logger.log("[BackupFilesList] refreshFiles called.");
if (!this.platformCapabilities.hasFileSystem) { if (!this.platformCapabilities.hasFileSystem) {
return; return;
} }
@ -441,29 +523,32 @@ export default class BackupFilesList extends Vue {
this.isLoading = true; this.isLoading = true;
try { try {
this.backupFiles = await this.platformService.listBackupFiles(); this.backupFiles = await this.platformService.listBackupFiles();
logger.log('[BackupFilesList] Refreshed backup files:', { logger.log("[BackupFilesList] Refreshed backup files:", {
count: this.backupFiles.length, count: this.backupFiles.length,
files: this.backupFiles.map(f => ({ files: this.backupFiles.map((f) => ({
name: f.name, name: f.name,
type: f.type, type: f.type,
path: f.path, path: f.path,
size: f.size size: f.size,
})), })),
platform: this.platformCapabilities.isIOS ? "iOS" : "Android", platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
// Debug: Log file type distribution // Debug: Log file type distribution
const typeCounts = { const typeCounts = {
contacts: this.backupFiles.filter(f => f.type === 'contacts').length, contacts: this.backupFiles.filter((f) => f.type === "contacts").length,
seed: this.backupFiles.filter(f => f.type === 'seed').length, seed: this.backupFiles.filter((f) => f.type === "seed").length,
other: this.backupFiles.filter(f => f.type === 'other').length, other: this.backupFiles.filter((f) => f.type === "other").length,
total: this.backupFiles.length total: this.backupFiles.length,
}; };
logger.log('[BackupFilesList] File type distribution:', typeCounts); logger.log("[BackupFilesList] File type distribution:", typeCounts);
// Log the full backupFiles array for debugging the 'All' tab count // Log the full backupFiles array for debugging the 'All' tab count
logger.log('[BackupFilesList] backupFiles array for All tab:', this.backupFiles); logger.log(
"[BackupFilesList] backupFiles array for All tab:",
this.backupFiles,
);
} catch (error) { } catch (error) {
logger.error('[BackupFilesList] Failed to refresh backup files:', error); logger.error("[BackupFilesList] Failed to refresh backup files:", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -484,17 +569,17 @@ export default class BackupFilesList extends Vue {
async createTestBackup() { async createTestBackup() {
try { try {
this.isLoading = true; this.isLoading = true;
logger.log('[BackupFilesList] Creating test backup file'); logger.log("[BackupFilesList] Creating test backup file");
const result = await this.platformService.createTestBackupFile(); const result = await this.platformService.createTestBackupFile();
if (result.success) { if (result.success) {
logger.log('[BackupFilesList] Test backup file created successfully:', { logger.log("[BackupFilesList] Test backup file created successfully:", {
fileName: result.fileName, fileName: result.fileName,
uri: result.uri, uri: result.uri,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -504,14 +589,17 @@ export default class BackupFilesList extends Vue {
}, },
5000, 5000,
); );
// Refresh the file list to show the new test file // Refresh the file list to show the new test file
await this.refreshFiles(); await this.refreshFiles();
} else { } else {
throw new Error(result.error || "Failed to create test backup file"); throw new Error(result.error || "Failed to create test backup file");
} }
} catch (error) { } catch (error) {
logger.error('[BackupFilesList] Failed to create test backup file:', error); logger.error(
"[BackupFilesList] Failed to create test backup file:",
error,
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -532,12 +620,15 @@ export default class BackupFilesList extends Vue {
async testDirectoryContexts() { async testDirectoryContexts() {
try { try {
this.isLoading = true; this.isLoading = true;
logger.log('[BackupFilesList] Testing directory contexts'); logger.log("[BackupFilesList] Testing directory contexts");
const debugOutput = await this.platformService.testDirectoryContexts(); const debugOutput = await this.platformService.testDirectoryContexts();
logger.log('[BackupFilesList] Directory context test results:', debugOutput); logger.log(
"[BackupFilesList] Directory context test results:",
debugOutput,
);
// Show the debug output in a notification or alert // Show the debug output in a notification or alert
this.$notify( this.$notify(
{ {
@ -548,14 +639,16 @@ export default class BackupFilesList extends Vue {
}, },
5000, 5000,
); );
// Also log the full output to console for easy access // Also log the full output to console for easy access
console.log("=== Directory Context Test Results ==="); logger.log("=== Directory Context Test Results ===");
console.log(debugOutput); logger.log(debugOutput);
console.log("=== End Test Results ==="); logger.log("=== End Test Results ===");
} catch (error) { } catch (error) {
logger.error('[BackupFilesList] Failed to test directory contexts:', error); logger.error(
"[BackupFilesList] Failed to test directory contexts:",
error,
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -575,7 +668,7 @@ export default class BackupFilesList extends Vue {
* This method can be called from parent components * This method can be called from parent components
*/ */
async refreshAfterSave() { async refreshAfterSave() {
logger.log('[BackupFilesList] refreshAfterSave called'); logger.log("[BackupFilesList] refreshAfterSave called");
await this.refreshFiles(); await this.refreshFiles();
} }
@ -587,7 +680,7 @@ export default class BackupFilesList extends Vue {
async openFile(fileUri: string, fileName: string) { async openFile(fileUri: string, fileName: string) {
try { try {
const result = await this.platformService.openFile(fileUri, fileName); const result = await this.platformService.openFile(fileUri, fileName);
if (result.success) { if (result.success) {
logger.log("[BackupFilesList] File opened successfully:", { logger.log("[BackupFilesList] File opened successfully:", {
fileName, fileName,
@ -616,7 +709,7 @@ export default class BackupFilesList extends Vue {
async openBackupDirectory() { async openBackupDirectory() {
try { try {
const result = await this.platformService.openBackupDirectory(); const result = await this.platformService.openBackupDirectory();
if (result.success) { if (result.success) {
logger.log("[BackupFilesList] Backup directory opened successfully:", { logger.log("[BackupFilesList] Backup directory opened successfully:", {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -642,14 +735,18 @@ export default class BackupFilesList extends Vue {
* Gets the count of files for a specific type * Gets the count of files for a specific type
* Note: The 'All' tab count is sometimes too small. Logging for debugging. * Note: The 'All' tab count is sometimes too small. Logging for debugging.
*/ */
getFileCountByType(type: 'all' | 'contacts' | 'seed' | 'other'): number { getFileCountByType(type: "all" | "contacts" | "seed" | "other"): number {
let count; let count;
if (type === 'all') { if (type === "all") {
count = this.backupFiles.length; count = this.backupFiles.length;
logger.log('[BackupFilesList] getFileCountByType (All):', count, this.backupFiles); logger.log(
"[BackupFilesList] getFileCountByType (All):",
count,
this.backupFiles,
);
return count; return count;
} }
count = this.backupFiles.filter(file => file.type === type).length; count = this.backupFiles.filter((file) => file.type === type).length;
logger.log(`[BackupFilesList] getFileCountByType (${type}):`, count); logger.log(`[BackupFilesList] getFileCountByType (${type}):`, count);
return count; return count;
} }
@ -659,14 +756,14 @@ export default class BackupFilesList extends Vue {
* @param type - File type * @param type - File type
* @returns FontAwesome icon name * @returns FontAwesome icon name
*/ */
getFileIcon(type: 'contacts' | 'seed' | 'other'): string { getFileIcon(type: "contacts" | "seed" | "other"): string {
switch (type) { switch (type) {
case 'contacts': case "contacts":
return 'address-book'; return "address-book";
case 'seed': case "seed":
return 'key'; return "key";
default: default:
return 'file-alt'; return "file-alt";
} }
} }
@ -675,14 +772,14 @@ export default class BackupFilesList extends Vue {
* @param type - File type * @param type - File type
* @returns CSS color class * @returns CSS color class
*/ */
getFileIconColor(type: 'contacts' | 'seed' | 'other'): string { getFileIconColor(type: "contacts" | "seed" | "other"): string {
switch (type) { switch (type) {
case 'contacts': case "contacts":
return 'text-blue-500'; return "text-blue-500";
case 'seed': case "seed":
return 'text-orange-500'; return "text-orange-500";
default: default:
return 'text-gray-500'; return "text-gray-500";
} }
} }
@ -691,14 +788,14 @@ export default class BackupFilesList extends Vue {
* @param type - File type * @param type - File type
* @returns CSS color class * @returns CSS color class
*/ */
getTypeBadgeColor(type: 'contacts' | 'seed' | 'other'): string { getTypeBadgeColor(type: "contacts" | "seed" | "other"): string {
switch (type) { switch (type) {
case 'contacts': case "contacts":
return 'bg-blue-100 text-blue-800'; return "bg-blue-100 text-blue-800";
case 'seed': case "seed":
return 'bg-orange-100 text-orange-800'; return "bg-orange-100 text-orange-800";
default: default:
return 'bg-gray-100 text-gray-800'; return "bg-gray-100 text-gray-800";
} }
} }
@ -708,13 +805,13 @@ export default class BackupFilesList extends Vue {
* @returns Formatted file size string * @returns Formatted file size string
*/ */
formatFileSize(bytes: number): string { formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB']; const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
} }
/** /**
@ -724,44 +821,62 @@ export default class BackupFilesList extends Vue {
public async debugFileDiscovery() { public async debugFileDiscovery() {
try { try {
logger.log("[BackupFilesList] Starting debug file discovery..."); logger.log("[BackupFilesList] Starting debug file discovery...");
// Test the platform service's test methods // Test the platform service's test methods
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
// Test listing all user files // Test listing all user files
const allFilesResult = await platformService.testListUserFiles(); const allFilesResult = await platformService.testListUserFiles();
logger.log("[BackupFilesList] All user files test result:", allFilesResult); logger.log(
"[BackupFilesList] All user files test result:",
allFilesResult,
);
// Test listing backup files specifically // Test listing backup files specifically
const backupFilesResult = await platformService.testBackupFiles(); const backupFilesResult = await platformService.testBackupFiles();
logger.log("[BackupFilesList] Backup files test result:", backupFilesResult); logger.log(
"[BackupFilesList] Backup files test result:",
// Test listing all backup files (if available) backupFilesResult,
if ('testListAllBackupFiles' in platformService) { );
const allBackupFilesResult = await (platformService as any).testListAllBackupFiles();
logger.log("[BackupFilesList] All backup files test result:", allBackupFilesResult); // Note: testListAllBackupFiles method is not part of the PlatformService interface
} // It exists only in CapacitorPlatformService implementation
// If needed, this could be added to the interface or called via type assertion
// Test debug listing all files without filtering (if available) // Test debug listing all files without filtering (if available)
if ('debugListAllFiles' in platformService) { if ("debugListAllFiles" in platformService) {
const debugAllFiles = await (platformService as any).debugListAllFiles(); const debugAllFiles = await (
platformService as any
).debugListAllFiles();
logger.log("[BackupFilesList] Debug all files (no filtering):", { logger.log("[BackupFilesList] Debug all files (no filtering):", {
count: debugAllFiles.length, count: debugAllFiles.length,
files: debugAllFiles.map((f: any) => ({ name: f.name, path: f.path, size: f.size })) files: debugAllFiles.map((f: any) => ({
name: f.name,
path: f.path,
size: f.size,
})),
}); });
} }
// Test comprehensive step-by-step debug (if available) // Test comprehensive step-by-step debug (if available)
if ('debugFileDiscoveryStepByStep' in platformService) { if ("debugFileDiscoveryStepByStep" in platformService) {
const stepByStepDebug = await (platformService as any).debugFileDiscoveryStepByStep(); const stepByStepDebug = await (
logger.log("[BackupFilesList] Step-by-step debug output:", stepByStepDebug); platformService as any
).debugFileDiscoveryStepByStep();
logger.log(
"[BackupFilesList] Step-by-step debug output:",
stepByStepDebug,
);
} }
return { return {
allFiles: allFilesResult, allFiles: allFilesResult,
backupFiles: backupFilesResult, backupFiles: backupFilesResult,
currentBackupFiles: this.backupFiles, currentBackupFiles: this.backupFiles,
debugAllFiles: 'debugListAllFiles' in platformService ? await (platformService as any).debugListAllFiles() : null debugAllFiles:
"debugListAllFiles" in platformService
? await (platformService as any).debugListAllFiles()
: null,
}; };
} catch (error) { } catch (error) {
logger.error("[BackupFilesList] Debug file discovery failed:", error); logger.error("[BackupFilesList] Debug file discovery failed:", error);
@ -769,11 +884,11 @@ export default class BackupFilesList extends Vue {
} }
} }
@Watch('platformCapabilities.hasFileSystem', { immediate: true }) @Watch("platformCapabilities.hasFileSystem", { immediate: true })
async onFileSystemCapabilityChanged(newVal: boolean) { async onFileSystemCapabilityChanged(newVal: boolean) {
if (newVal) { if (newVal) {
await this.refreshFiles(); await this.refreshFiles();
} }
} }
} }
</script> </script>

30
src/components/DataExportSection.vue

@ -44,19 +44,25 @@ explorer. * * @component * @displayName DataExportSection * @example * ```vue *
v-if="platformCapabilities.isIOS" v-if="platformCapabilities.isIOS"
class="list-disc list-outside ml-4" class="list-disc list-outside ml-4"
> >
On iOS: Files are saved to Documents folder (accessible via Files app) and persist between app installations. On iOS: Files are saved to Documents folder (accessible via Files app)
and persist between app installations.
</li> </li>
<li <li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS" v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
class="list-disc list-outside ml-4" class="list-disc list-outside ml-4"
> >
On Android: Files are saved to Downloads/TimeSafari or external storage (accessible via file managers) and persist between app installations. On Android: Files are saved to Downloads/TimeSafari or external
storage (accessible via file managers) and persist between app
installations.
</li> </li>
</ul> </ul>
</div> </div>
<!-- Backup Files List --> <!-- Backup Files List -->
<div v-if="platformCapabilities.hasFileSystem" class="mt-6 pt-6 border-t border-gray-300"> <div
v-if="platformCapabilities.hasFileSystem"
class="mt-6 pt-6 border-t border-gray-300"
>
<BackupFilesList ref="backupFilesList" /> <BackupFilesList ref="backupFilesList" />
</div> </div>
</div> </div>
@ -171,15 +177,15 @@ export default class DataExportSection extends Vue {
} else if (this.platformCapabilities.hasFileSystem) { } else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to user-accessible location and share // Native platform: Write to user-accessible location and share
const result = await this.platformService.writeAndShareFile( const result = await this.platformService.writeAndShareFile(
fileName, fileName,
jsonStr, jsonStr,
{ {
allowLocationSelection: true, allowLocationSelection: true,
showLocationSelectionDialog: true, showLocationSelectionDialog: true,
mimeType: "application/json" mimeType: "application/json",
} },
); );
// Handle the result // Handle the result
if (!result.saved) { if (!result.saved) {
throw new Error(result.error || "Failed to save file"); throw new Error(result.error || "Failed to save file");
@ -202,7 +208,10 @@ export default class DataExportSection extends Vue {
// Refresh the backup files list // Refresh the backup files list
const backupFilesList = this.$refs.backupFilesList as any; const backupFilesList = this.$refs.backupFilesList as any;
if (backupFilesList && typeof backupFilesList.refreshAfterSave === 'function') { if (
backupFilesList &&
typeof backupFilesList.refreshAfterSave === "function"
) {
await backupFilesList.refreshAfterSave(); await backupFilesList.refreshAfterSave();
} }
} catch (error) { } catch (error) {
@ -243,7 +252,10 @@ export default class DataExportSection extends Vue {
// Ensure permissions are requested and refresh backup files list on mount // Ensure permissions are requested and refresh backup files list on mount
if (this.platformCapabilities.hasFileSystem) { if (this.platformCapabilities.hasFileSystem) {
const backupFilesList = this.$refs.backupFilesList as any; const backupFilesList = this.$refs.backupFilesList as any;
if (backupFilesList && typeof backupFilesList.refreshFiles === 'function') { if (
backupFilesList &&
typeof backupFilesList.refreshFiles === "function"
) {
await backupFilesList.refreshFiles(); await backupFilesList.refreshFiles();
} }
} }

4
src/libs/capacitor/app.ts

@ -15,7 +15,7 @@ interface AppInterface {
/** /**
* Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to * Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to
* exit the app when navigation is complete. * exit the app when navigation is complete.
* *
* @returns Promise that resolves when the app has been exited * @returns Promise that resolves when the app has been exited
*/ */
exitApp(): Promise<void>; exitApp(): Promise<void>;
@ -52,7 +52,7 @@ export const App: AppInterface = {
/** /**
* Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to * Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to
* exit the app when navigation is complete. * exit the app when navigation is complete.
* *
* @returns Promise that resolves when the app has been exited * @returns Promise that resolves when the app has been exited
*/ */
exitApp(): Promise<void> { exitApp(): Promise<void> {

43
src/services/PlatformService.ts

@ -69,7 +69,7 @@ export interface PlatformService {
* @returns Promise that resolves to save/share result * @returns Promise that resolves to save/share result
*/ */
writeAndShareFile( writeAndShareFile(
fileName: string, fileName: string,
content: string, content: string,
options?: { options?: {
allowLocationSelection?: boolean; allowLocationSelection?: boolean;
@ -78,7 +78,7 @@ export interface PlatformService {
mimeType?: string; mimeType?: string;
showShareDialog?: boolean; showShareDialog?: boolean;
showLocationSelectionDialog?: boolean; showLocationSelectionDialog?: boolean;
} },
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }>; ): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }>;
/** /**
@ -190,14 +190,24 @@ export interface PlatformService {
* Returns files from Downloads (Android) or Documents (iOS) directories. * Returns files from Downloads (Android) or Documents (iOS) directories.
* @returns Promise resolving to array of file information * @returns Promise resolving to array of file information
*/ */
listUserAccessibleFiles(): Promise<Array<{name: string, uri: string, size?: number}>>; listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
>;
/** /**
* Lists backup files specifically saved by the app. * Lists backup files specifically saved by the app.
* Filters for files that appear to be TimeSafari backups. * Filters for files that appear to be TimeSafari backups.
* @returns Promise resolving to array of backup file information * @returns Promise resolving to array of backup file information
*/ */
listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>>; listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
>;
/** /**
* Opens a file in the device's default file viewer/app. * Opens a file in the device's default file viewer/app.
@ -206,7 +216,10 @@ export interface PlatformService {
* @param fileName - Name of the file (for display purposes) * @param fileName - Name of the file (for display purposes)
* @returns Promise resolving to success status * @returns Promise resolving to success status
*/ */
openFile(fileUri: string, fileName: string): Promise<{ success: boolean; error?: string }>; openFile(
fileUri: string,
fileName: string,
): Promise<{ success: boolean; error?: string }>;
/** /**
* Opens the directory containing backup files in the device's file explorer. * Opens the directory containing backup files in the device's file explorer.
@ -220,7 +233,12 @@ export interface PlatformService {
* This is useful for debugging file visibility issues. * This is useful for debugging file visibility issues.
* @returns Promise resolving to success status and file information * @returns Promise resolving to success status and file information
*/ */
createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }>; createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}>;
/** /**
* Tests different directory contexts to see what files are available. * Tests different directory contexts to see what files are available.
@ -235,7 +253,18 @@ export interface PlatformService {
* @param debugShowAll - Debug flag to treat all entries as files * @param debugShowAll - Debug flag to treat all entries as files
* @returns Promise resolving to array of directory entries * @returns Promise resolving to array of directory entries
*/ */
listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>>; listFilesInDirectory(
path: string,
debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
>;
/** /**
* Debug method to check what's actually in the TimeSafari directory * Debug method to check what's actually in the TimeSafari directory

2008
src/services/platforms/CapacitorPlatformService.ts

File diff suppressed because it is too large

65
src/services/platforms/ElectronPlatformService.ts

@ -240,7 +240,7 @@ export class ElectronPlatformService implements PlatformService {
* @todo Implement using Electron's dialog and file system APIs * @todo Implement using Electron's dialog and file system APIs
*/ */
async writeAndShareFile( async writeAndShareFile(
_fileName: string, _fileName: string,
_content: string, _content: string,
_options?: { _options?: {
allowLocationSelection?: boolean; allowLocationSelection?: boolean;
@ -249,12 +249,17 @@ export class ElectronPlatformService implements PlatformService {
mimeType?: string; mimeType?: string;
showShareDialog?: boolean; showShareDialog?: boolean;
showLocationSelectionDialog?: boolean; showLocationSelectionDialog?: boolean;
} },
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> { ): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return { return {
saved: false, saved: false,
shared: false, shared: false,
error: "Not implemented in Electron platform" error: "Not implemented in Electron platform",
}; };
} }
@ -435,7 +440,9 @@ export class ElectronPlatformService implements PlatformService {
* Not implemented in Electron platform. * Not implemented in Electron platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listUserAccessibleFiles(): Promise<Array<{name: string, uri: string, size?: number}>> { async listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
> {
return []; return [];
} }
@ -444,7 +451,15 @@ export class ElectronPlatformService implements PlatformService {
* Not implemented for Electron platform. * Not implemented for Electron platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { async listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
> {
return []; return [];
} }
@ -455,8 +470,14 @@ export class ElectronPlatformService implements PlatformService {
* @param _fileName - Name of the file (for display purposes) * @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> { async openFile(
return { success: false, error: "File opening not implemented in Electron platform" }; _fileUri: string,
_fileName: string,
): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "File opening not implemented in Electron platform",
};
} }
/** /**
@ -465,7 +486,10 @@ export class ElectronPlatformService implements PlatformService {
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "Directory access not implemented in Electron platform" }; return {
success: false,
error: "Directory access not implemented in Electron platform",
};
} }
/** /**
@ -473,7 +497,18 @@ export class ElectronPlatformService implements PlatformService {
* Not implemented for Electron platform. * Not implemented for Electron platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> { async listFilesInDirectory(
_path: string,
_debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
> {
return []; return [];
} }
@ -491,10 +526,16 @@ export class ElectronPlatformService implements PlatformService {
* Not implemented for Electron platform. * Not implemented for Electron platform.
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> { async createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}> {
return { return {
success: false, success: false,
error: "Electron platform does not support file system access for creating test backup files." error:
"Electron platform does not support file system access for creating test backup files.",
}; };
} }

69
src/services/platforms/PyWebViewPlatformService.ts

@ -131,7 +131,7 @@ export class PyWebViewPlatformService implements PlatformService {
* @returns Promise that resolves to save/share result * @returns Promise that resolves to save/share result
*/ */
async writeAndShareFile( async writeAndShareFile(
_fileName: string, _fileName: string,
_content: string, _content: string,
_options?: { _options?: {
allowLocationSelection?: boolean; allowLocationSelection?: boolean;
@ -140,9 +140,18 @@ export class PyWebViewPlatformService implements PlatformService {
mimeType?: string; mimeType?: string;
showShareDialog?: boolean; showShareDialog?: boolean;
showLocationSelectionDialog?: boolean; showLocationSelectionDialog?: boolean;
} },
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> { ): Promise<{
return { saved: false, shared: false, error: "File sharing not implemented in PyWebView platform" }; saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return {
saved: false,
shared: false,
error: "File sharing not implemented in PyWebView platform",
};
} }
/** /**
@ -150,7 +159,9 @@ export class PyWebViewPlatformService implements PlatformService {
* Not implemented in PyWebView platform. * Not implemented in PyWebView platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listUserAccessibleFiles(): Promise<Array<{name: string, uri: string, size?: number}>> { async listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
> {
return []; return [];
} }
@ -159,7 +170,15 @@ export class PyWebViewPlatformService implements PlatformService {
* Not implemented for PyWebView platform. * Not implemented for PyWebView platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { async listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
> {
return []; return [];
} }
@ -170,8 +189,14 @@ export class PyWebViewPlatformService implements PlatformService {
* @param _fileName - Name of the file (for display purposes) * @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> { async openFile(
return { success: false, error: "File opening not implemented in PyWebView platform" }; _fileUri: string,
_fileName: string,
): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "File opening not implemented in PyWebView platform",
};
} }
/** /**
@ -180,7 +205,10 @@ export class PyWebViewPlatformService implements PlatformService {
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "Directory access not implemented in PyWebView platform" }; return {
success: false,
error: "Directory access not implemented in PyWebView platform",
};
} }
/** /**
@ -252,7 +280,18 @@ export class PyWebViewPlatformService implements PlatformService {
* Not implemented for PyWebView platform. * Not implemented for PyWebView platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> { async listFilesInDirectory(
_path: string,
_debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
> {
return []; return [];
} }
@ -270,10 +309,16 @@ export class PyWebViewPlatformService implements PlatformService {
* Not implemented for PyWebView platform. * Not implemented for PyWebView platform.
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> { async createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}> {
return { return {
success: false, success: false,
error: "PyWebView platform does not support file system access for creating test backup files." error:
"PyWebView platform does not support file system access for creating test backup files.",
}; };
} }

72
src/services/platforms/WebPlatformService.ts

@ -29,9 +29,10 @@ export class WebPlatformService implements PlatformService {
return { return {
hasFileSystem: false, hasFileSystem: false,
hasCamera: true, // Through file input with capture hasCamera: true, // Through file input with capture
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( isMobile:
navigator.userAgent, /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
), navigator.userAgent,
),
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent), isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: true, hasFileDownload: true,
needsFileHandlingInstructions: false, needsFileHandlingInstructions: false,
@ -363,7 +364,7 @@ export class WebPlatformService implements PlatformService {
* @returns Promise that resolves to a failure result * @returns Promise that resolves to a failure result
*/ */
async writeAndShareFile( async writeAndShareFile(
_fileName: string, _fileName: string,
_content: string, _content: string,
_options?: { _options?: {
allowLocationSelection?: boolean; allowLocationSelection?: boolean;
@ -372,12 +373,17 @@ export class WebPlatformService implements PlatformService {
mimeType?: string; mimeType?: string;
showShareDialog?: boolean; showShareDialog?: boolean;
showLocationSelectionDialog?: boolean; showLocationSelectionDialog?: boolean;
} },
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> { ): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return { return {
saved: false, saved: false,
shared: false, shared: false,
error: "File system access not available in web platform" error: "File system access not available in web platform",
}; };
} }
@ -471,7 +477,9 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform. * Not supported in web platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listUserAccessibleFiles(): Promise<Array<{name: string, uri: string, size?: number}>> { async listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
> {
return []; return [];
} }
@ -480,7 +488,15 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform. * Not supported in web platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { async listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
> {
return []; return [];
} }
@ -491,8 +507,14 @@ export class WebPlatformService implements PlatformService {
* @param _fileName - Name of the file (for display purposes) * @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> { async openFile(
return { success: false, error: "File opening not available in web platform" }; _fileUri: string,
_fileName: string,
): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "File opening not available in web platform",
};
} }
/** /**
@ -501,7 +523,10 @@ export class WebPlatformService implements PlatformService {
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "Directory access not available in web platform" }; return {
success: false,
error: "Directory access not available in web platform",
};
} }
/** /**
@ -519,7 +544,18 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform. * Not supported in web platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> { async listFilesInDirectory(
_path: string,
_debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
> {
return []; return [];
} }
@ -537,10 +573,16 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform. * Not supported in web platform.
* @returns Promise resolving to error status * @returns Promise resolving to error status
*/ */
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> { async createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}> {
return { return {
success: false, success: false,
error: "Web platform does not support file system access for creating test backup files." error:
"Web platform does not support file system access for creating test backup files.",
}; };
} }

2
src/utils/logger.ts

@ -87,7 +87,7 @@ export default { logger };
* @returns Formatted timestamp string safe for filenames * @returns Formatted timestamp string safe for filenames
*/ */
export function getTimestampForFilename(): string { export function getTimestampForFilename(): string {
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
} }
/** /**

14
src/views/TestView.vue

@ -217,7 +217,8 @@
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-bold mb-4">File Sharing Test</h2> <h2 class="text-xl font-bold mb-4">File Sharing Test</h2>
Test the new file sharing functionality that saves to user-accessible locations. Test the new file sharing functionality that saves to user-accessible
locations.
<div> <div>
<button <button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@ -747,7 +748,10 @@ export default class Help extends Vue {
try { try {
const result = await platformService.testLocationSelectionSilent(); const result = await platformService.testLocationSelectionSilent();
this.fileSharingResult = result; this.fileSharingResult = result;
logger.log("Silent Location Selection Test Result:", this.fileSharingResult); logger.log(
"Silent Location Selection Test Result:",
this.fileSharingResult,
);
} catch (error) { } catch (error) {
logger.error("Silent Location Selection Test Error:", error); logger.error("Silent Location Selection Test Error:", error);
this.$notify( this.$notify(
@ -825,8 +829,10 @@ export default class Help extends Vue {
public async testFileDiscoveryDebug() { public async testFileDiscoveryDebug() {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
try { try {
if ('debugFileDiscoveryStepByStep' in platformService) { if ("debugFileDiscoveryStepByStep" in platformService) {
const result = await (platformService as any).debugFileDiscoveryStepByStep(); const result = await (
platformService as any
).debugFileDiscoveryStepByStep();
this.fileSharingResult = result; this.fileSharingResult = result;
logger.log("File Discovery Debug Test Result:", this.fileSharingResult); logger.log("File Discovery Debug Test Result:", this.fileSharingResult);
} else { } else {

Loading…
Cancel
Save