Browse Source

chore: linting; more non-errors need fixing

capacitor-local-save
Matthew Raymer 1 day ago
parent
commit
a1b6add178
  1. 387
      src/components/BackupFilesList.vue
  2. 26
      src/components/DataExportSection.vue
  3. 41
      src/services/PlatformService.ts
  4. 1682
      src/services/platforms/CapacitorPlatformService.ts
  5. 63
      src/services/platforms/ElectronPlatformService.ts
  6. 67
      src/services/platforms/PyWebViewPlatformService.ts
  7. 70
      src/services/platforms/WebPlatformService.ts
  8. 2
      src/utils/logger.ts
  9. 14
      src/views/TestView.vue

387
src/components/BackupFilesList.vue

@ -1,18 +1,10 @@
/**
* Backup Files List Component
*
* Displays a list of backup files saved by 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 backup directory in the device's file explorer
*
* @component
* @displayName BackupFilesList
* @example
* ```vue
* <BackupFilesList />
* ```
*/
/** * Backup Files List Component * * Displays a list of backup files saved by
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
backup directory in the device's file explorer * * @component * @displayName
BackupFilesList * @example * ```vue *
<BackupFilesList />
* ``` */
<template>
<div class="backup-files-list">
@ -21,9 +13,9 @@
<div class="flex gap-2">
<button
v-if="platformCapabilities.hasFileSystem"
@click="refreshFiles()"
class="text-sm bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"
:disabled="isLoading"
@click="refreshFiles()"
>
<font-awesome
icon="refresh"
@ -34,34 +26,34 @@
</button>
<button
v-if="platformCapabilities.hasFileSystem"
@click="openBackupDirectory()"
class="text-sm bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded"
:disabled="isLoading"
@click="openBackupDirectory()"
>
<font-awesome icon="folder-open" class="fa-fw" />
Open Directory
</button>
<button
v-if="platformCapabilities.hasFileSystem && isDevelopment"
@click="debugFileDiscovery()"
class="text-sm bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded"
:disabled="isLoading"
title="Debug file discovery (development only)"
@click="debugFileDiscovery()"
>
<font-awesome icon="bug" class="fa-fw" />
Debug
</button>
<button
@click="createTestBackup"
:disabled="isLoading"
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
</button>
<button
@click="testDirectoryContexts"
:disabled="isLoading"
class="px-3 py-1 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 disabled:opacity-50"
@click="testDirectoryContexts"
>
Test Contexts
</button>
@ -73,21 +65,40 @@
<p class="mt-2">Loading backup files...</p>
</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" />
<p>No backup files found</p>
<p class="text-sm mt-1">Create backups using the export functions above</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>
<p class="text-sm mt-1">
Create backups using the export functions above
</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">
<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> Backup files are saved to persistent storage that survives app 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>
Backup files are saved to persistent storage that survives app
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 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>
</ul>
</div>
@ -99,18 +110,20 @@
<button
v-for="type in ['all', 'contacts', 'seed', 'other'] as const"
:key="type"
@click="selectedType = type"
:class="[
'text-sm px-3 py-1 rounded border',
selectedType === type
? '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">
({{ getFileCountByType(type) }})
</span>
{{
type === "all"
? "All"
: type.charAt(0).toUpperCase() + type.slice(1)
}}
<span class="ml-1 text-xs"> ({{ getFileCountByType(type) }}) </span>
</button>
</div>
@ -118,9 +131,9 @@
<div class="flex items-center gap-2 mb-2">
<span v-for="(crumb, idx) in breadcrumbs" :key="idx">
<span
v-if="idx < breadcrumbs.length - 1"
class="text-blue-600 cursor-pointer underline"
@click="goToBreadcrumb(idx)"
v-if="idx < breadcrumbs.length - 1"
>
{{ crumb }}
</span>
@ -129,14 +142,23 @@
</span>
</div>
<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 class="mb-2">
<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>
</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 class="space-y-2 max-h-64 overflow-y-auto">
<div
@ -148,7 +170,10 @@
<div class="flex items-center gap-2">
<font-awesome icon="folder" class="fa-fw text-yellow-500" />
<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
@ -164,14 +189,18 @@
<div class="text-sm text-gray-500 mt-1">
<span v-if="entry.size">{{ formatFileSize(entry.size) }}</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 class="flex gap-2 ml-3">
<button
@click="openFile(entry.uri, entry.name)"
class="text-blue-500 hover:text-blue-700 p-1"
title="Open file"
@click="openFile(entry.uri, entry.name)"
>
<font-awesome icon="external-link-alt" class="fa-fw" />
</button>
@ -181,15 +210,28 @@
<!-- Summary -->
<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 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">
<li v-if="platformCapabilities.isIOS">iOS: Documents folder (accessible via Files app)</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>
<li v-if="platformCapabilities.isIOS">
iOS: Documents folder (accessible via Files app)
</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>
</div>
</div>
@ -222,7 +264,8 @@ export default class BackupFilesList extends Vue {
/**
* Platform service instance for platform-specific operations
*/
private platformService: PlatformService = PlatformServiceFactory.getInstance();
private platformService: PlatformService =
PlatformServiceFactory.getInstance();
/**
* Platform capabilities for the current platform
@ -234,12 +277,18 @@ export default class BackupFilesList extends Vue {
/**
* 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
*/
selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all';
selectedType: "all" | "contacts" | "seed" | "other" = "all";
/**
* Loading state for file operations
@ -259,7 +308,13 @@ export default class BackupFilesList extends Vue {
/**
* 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
@ -271,25 +326,34 @@ export default class BackupFilesList extends Vue {
* Returns true if permission is granted, false otherwise.
*/
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;
// Only relevant for native platforms (Android/iOS)
const platformService = this.platformService as any;
if (typeof platformService.checkStoragePermissions === 'function') {
if (typeof platformService.checkStoragePermissions === "function") {
try {
await platformService.checkStoragePermissions();
logger.log('[BackupFilesList] Storage permission granted.');
logger.log("[BackupFilesList] Storage permission granted.");
return true;
} catch (error) {
logger.error('[BackupFilesList] Storage permission denied:', error);
logger.error("[BackupFilesList] Storage permission denied:", error);
// Get specific guidance for the platform
let guidance = "This app needs permission to access your files to list and restore backups.";
if (typeof platformService.getStoragePermissionGuidance === 'function') {
let guidance =
"This app needs permission to access your files to list and restore backups.";
if (
typeof platformService.getStoragePermissionGuidance === "function"
) {
try {
guidance = await platformService.getStoragePermissionGuidance();
} catch (guidanceError) {
logger.warn('[BackupFilesList] Could not get permission guidance:', guidanceError);
logger.warn(
"[BackupFilesList] Could not get permission guidance:",
guidanceError,
);
}
}
@ -312,21 +376,27 @@ export default class BackupFilesList extends Vue {
* Lifecycle hook to load backup files when component is 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) {
// Check/request permission before loading
const hasPermission = await this.ensureStoragePermission();
if (hasPermission) {
// Set default root path
if (this.platformCapabilities.isIOS) {
this.currentPath = ['.'];
this.currentPath = ["."];
} else {
this.currentPath = ['Download', 'TimeSafari'];
this.currentPath = ["Download", "TimeSafari"];
}
await this.loadDirectory();
this.refreshInterval = window.setInterval(() => {
this.loadDirectory();
}, 5 * 60 * 1000);
this.refreshInterval = window.setInterval(
() => {
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.
*/
get filteredFiles() {
if (this.selectedType === 'all') {
logger.log('[BackupFilesList] filteredFiles (All):', this.backupFiles);
if (this.selectedType === "all") {
logger.log("[BackupFilesList] filteredFiles (All):", this.backupFiles);
return this.backupFiles;
}
const filtered = this.backupFiles.filter(file => file.type === this.selectedType);
logger.log(`[BackupFilesList] filteredFiles (${this.selectedType}):`, filtered);
const filtered = this.backupFiles.filter(
(file) => file.type === this.selectedType,
);
logger.log(
`[BackupFilesList] filteredFiles (${this.selectedType}):`,
filtered,
);
return filtered;
}
@ -369,11 +444,18 @@ export default class BackupFilesList extends Vue {
if (!this.platformCapabilities.hasFileSystem) return;
this.isLoading = true;
try {
const path = this.currentPath.join('/') || (this.platformCapabilities.isIOS ? '.' : 'Download/TimeSafari');
this.directoryEntries = await (this.platformService as any).listFilesInDirectory(path, this.debugShowAll);
logger.log('[BackupFilesList] Loaded directory:', { path, entries: this.directoryEntries });
const path =
this.currentPath.join("/") ||
(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) {
logger.error('[BackupFilesList] Failed to load directory:', error);
logger.error("[BackupFilesList] Failed to load directory:", error);
this.directoryEntries = [];
} finally {
this.isLoading = false;
@ -417,17 +499,17 @@ export default class BackupFilesList extends Vue {
* Computed property for showing files and folders
*/
get folders() {
return this.directoryEntries.filter(e => e.type === 'folder');
return this.directoryEntries.filter((e) => e.type === "folder");
}
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
*/
async refreshFiles() {
logger.log('[BackupFilesList] refreshFiles called.');
logger.log("[BackupFilesList] refreshFiles called.");
if (!this.platformCapabilities.hasFileSystem) {
return;
}
@ -441,29 +523,32 @@ export default class BackupFilesList extends Vue {
this.isLoading = true;
try {
this.backupFiles = await this.platformService.listBackupFiles();
logger.log('[BackupFilesList] Refreshed backup files:', {
logger.log("[BackupFilesList] Refreshed backup files:", {
count: this.backupFiles.length,
files: this.backupFiles.map(f => ({
files: this.backupFiles.map((f) => ({
name: f.name,
type: f.type,
path: f.path,
size: f.size
size: f.size,
})),
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
});
// Debug: Log file type distribution
const typeCounts = {
contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
seed: this.backupFiles.filter(f => f.type === 'seed').length,
other: this.backupFiles.filter(f => f.type === 'other').length,
total: this.backupFiles.length
contacts: this.backupFiles.filter((f) => f.type === "contacts").length,
seed: this.backupFiles.filter((f) => f.type === "seed").length,
other: this.backupFiles.filter((f) => f.type === "other").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
logger.log('[BackupFilesList] backupFiles array for All tab:', this.backupFiles);
logger.log(
"[BackupFilesList] backupFiles array for All tab:",
this.backupFiles,
);
} catch (error) {
logger.error('[BackupFilesList] Failed to refresh backup files:', error);
logger.error("[BackupFilesList] Failed to refresh backup files:", error);
this.$notify(
{
group: "alert",
@ -484,12 +569,12 @@ export default class BackupFilesList extends Vue {
async createTestBackup() {
try {
this.isLoading = true;
logger.log('[BackupFilesList] Creating test backup file');
logger.log("[BackupFilesList] Creating test backup file");
const result = await this.platformService.createTestBackupFile();
if (result.success) {
logger.log('[BackupFilesList] Test backup file created successfully:', {
logger.log("[BackupFilesList] Test backup file created successfully:", {
fileName: result.fileName,
uri: result.uri,
timestamp: new Date().toISOString(),
@ -511,7 +596,10 @@ export default class BackupFilesList extends Vue {
throw new Error(result.error || "Failed to create test backup file");
}
} catch (error) {
logger.error('[BackupFilesList] Failed to create test backup file:', error);
logger.error(
"[BackupFilesList] Failed to create test backup file:",
error,
);
this.$notify(
{
group: "alert",
@ -532,11 +620,14 @@ export default class BackupFilesList extends Vue {
async testDirectoryContexts() {
try {
this.isLoading = true;
logger.log('[BackupFilesList] Testing directory contexts');
logger.log("[BackupFilesList] Testing directory contexts");
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
this.$notify(
@ -550,12 +641,14 @@ export default class BackupFilesList extends Vue {
);
// Also log the full output to console for easy access
console.log("=== Directory Context Test Results ===");
console.log(debugOutput);
console.log("=== End Test Results ===");
logger.log("=== Directory Context Test Results ===");
logger.log(debugOutput);
logger.log("=== End Test Results ===");
} catch (error) {
logger.error('[BackupFilesList] Failed to test directory contexts:', error);
logger.error(
"[BackupFilesList] Failed to test directory contexts:",
error,
);
this.$notify(
{
group: "alert",
@ -575,7 +668,7 @@ export default class BackupFilesList extends Vue {
* This method can be called from parent components
*/
async refreshAfterSave() {
logger.log('[BackupFilesList] refreshAfterSave called');
logger.log("[BackupFilesList] refreshAfterSave called");
await this.refreshFiles();
}
@ -642,14 +735,18 @@ export default class BackupFilesList extends Vue {
* Gets the count of files for a specific type
* 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;
if (type === 'all') {
if (type === "all") {
count = this.backupFiles.length;
logger.log('[BackupFilesList] getFileCountByType (All):', count, this.backupFiles);
logger.log(
"[BackupFilesList] getFileCountByType (All):",
count,
this.backupFiles,
);
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);
return count;
}
@ -659,14 +756,14 @@ export default class BackupFilesList extends Vue {
* @param type - File type
* @returns FontAwesome icon name
*/
getFileIcon(type: 'contacts' | 'seed' | 'other'): string {
getFileIcon(type: "contacts" | "seed" | "other"): string {
switch (type) {
case 'contacts':
return 'address-book';
case 'seed':
return 'key';
case "contacts":
return "address-book";
case "seed":
return "key";
default:
return 'file-alt';
return "file-alt";
}
}
@ -675,14 +772,14 @@ export default class BackupFilesList extends Vue {
* @param type - File type
* @returns CSS color class
*/
getFileIconColor(type: 'contacts' | 'seed' | 'other'): string {
getFileIconColor(type: "contacts" | "seed" | "other"): string {
switch (type) {
case 'contacts':
return 'text-blue-500';
case 'seed':
return 'text-orange-500';
case "contacts":
return "text-blue-500";
case "seed":
return "text-orange-500";
default:
return 'text-gray-500';
return "text-gray-500";
}
}
@ -691,14 +788,14 @@ export default class BackupFilesList extends Vue {
* @param type - File type
* @returns CSS color class
*/
getTypeBadgeColor(type: 'contacts' | 'seed' | 'other'): string {
getTypeBadgeColor(type: "contacts" | "seed" | "other"): string {
switch (type) {
case 'contacts':
return 'bg-blue-100 text-blue-800';
case 'seed':
return 'bg-orange-100 text-orange-800';
case "contacts":
return "bg-blue-100 text-blue-800";
case "seed":
return "bg-orange-100 text-orange-800";
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
*/
formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
if (bytes === 0) return "0 Bytes";
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));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
@ -730,38 +827,56 @@ export default class BackupFilesList extends Vue {
// Test listing all user files
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
const backupFilesResult = await platformService.testBackupFiles();
logger.log("[BackupFilesList] Backup files test result:", backupFilesResult);
logger.log(
"[BackupFilesList] Backup files test result:",
backupFilesResult,
);
// Test listing all backup files (if available)
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)
if ('debugListAllFiles' in platformService) {
const debugAllFiles = await (platformService as any).debugListAllFiles();
if ("debugListAllFiles" in platformService) {
const debugAllFiles = await (
platformService as any
).debugListAllFiles();
logger.log("[BackupFilesList] Debug all files (no filtering):", {
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)
if ('debugFileDiscoveryStepByStep' in platformService) {
const stepByStepDebug = await (platformService as any).debugFileDiscoveryStepByStep();
logger.log("[BackupFilesList] Step-by-step debug output:", stepByStepDebug);
if ("debugFileDiscoveryStepByStep" in platformService) {
const stepByStepDebug = await (
platformService as any
).debugFileDiscoveryStepByStep();
logger.log(
"[BackupFilesList] Step-by-step debug output:",
stepByStepDebug,
);
}
return {
allFiles: allFilesResult,
backupFiles: backupFilesResult,
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) {
logger.error("[BackupFilesList] Debug file discovery failed:", error);
@ -769,7 +884,7 @@ export default class BackupFilesList extends Vue {
}
}
@Watch('platformCapabilities.hasFileSystem', { immediate: true })
@Watch("platformCapabilities.hasFileSystem", { immediate: true })
async onFileSystemCapabilityChanged(newVal: boolean) {
if (newVal) {
await this.refreshFiles();

26
src/components/DataExportSection.vue

@ -44,19 +44,25 @@ explorer. * * @component * @displayName DataExportSection * @example * ```vue *
v-if="platformCapabilities.isIOS"
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
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
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>
</ul>
</div>
<!-- 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" />
</div>
</div>
@ -176,8 +182,8 @@ export default class DataExportSection extends Vue {
{
allowLocationSelection: true,
showLocationSelectionDialog: true,
mimeType: "application/json"
}
mimeType: "application/json",
},
);
// Handle the result
@ -202,7 +208,10 @@ export default class DataExportSection extends Vue {
// Refresh the backup files list
const backupFilesList = this.$refs.backupFilesList as any;
if (backupFilesList && typeof backupFilesList.refreshAfterSave === 'function') {
if (
backupFilesList &&
typeof backupFilesList.refreshAfterSave === "function"
) {
await backupFilesList.refreshAfterSave();
}
} catch (error) {
@ -243,7 +252,10 @@ export default class DataExportSection extends Vue {
// Ensure permissions are requested and refresh backup files list on mount
if (this.platformCapabilities.hasFileSystem) {
const backupFilesList = this.$refs.backupFilesList as any;
if (backupFilesList && typeof backupFilesList.refreshFiles === 'function') {
if (
backupFilesList &&
typeof backupFilesList.refreshFiles === "function"
) {
await backupFilesList.refreshFiles();
}
}

41
src/services/PlatformService.ts

@ -78,7 +78,7 @@ export interface PlatformService {
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
}
},
): 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 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.
* Filters for files that appear to be TimeSafari backups.
* @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.
@ -206,7 +216,10 @@ export interface PlatformService {
* @param fileName - Name of the file (for display purposes)
* @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.
@ -220,7 +233,12 @@ export interface PlatformService {
* This is useful for debugging file visibility issues.
* @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.
@ -235,7 +253,18 @@ export interface PlatformService {
* @param debugShowAll - Debug flag to treat all entries as files
* @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

1682
src/services/platforms/CapacitorPlatformService.ts

File diff suppressed because it is too large

63
src/services/platforms/ElectronPlatformService.ts

@ -249,12 +249,17 @@ export class ElectronPlatformService implements PlatformService {
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
}
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
},
): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return {
saved: 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.
* @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 [];
}
@ -444,7 +451,15 @@ export class ElectronPlatformService implements PlatformService {
* Not implemented for Electron platform.
* @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 [];
}
@ -455,8 +470,14 @@ export class ElectronPlatformService implements PlatformService {
* @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status
*/
async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "File opening not implemented in Electron platform" };
async openFile(
_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
*/
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.
* @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 [];
}
@ -491,10 +526,16 @@ export class ElectronPlatformService implements PlatformService {
* Not implemented for Electron platform.
* @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 {
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.",
};
}

67
src/services/platforms/PyWebViewPlatformService.ts

@ -140,9 +140,18 @@ export class PyWebViewPlatformService implements PlatformService {
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
}
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
return { saved: false, shared: false, error: "File sharing not implemented in PyWebView platform" };
},
): Promise<{
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.
* @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 [];
}
@ -159,7 +170,15 @@ export class PyWebViewPlatformService implements PlatformService {
* Not implemented for PyWebView platform.
* @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 [];
}
@ -170,8 +189,14 @@ export class PyWebViewPlatformService implements PlatformService {
* @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status
*/
async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "File opening not implemented in PyWebView platform" };
async openFile(
_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
*/
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.
* @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 [];
}
@ -270,10 +309,16 @@ export class PyWebViewPlatformService implements PlatformService {
* Not implemented for PyWebView platform.
* @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 {
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.",
};
}

70
src/services/platforms/WebPlatformService.ts

@ -29,9 +29,10 @@ export class WebPlatformService implements PlatformService {
return {
hasFileSystem: false,
hasCamera: true, // Through file input with capture
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
),
isMobile:
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
),
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: true,
needsFileHandlingInstructions: false,
@ -372,12 +373,17 @@ export class WebPlatformService implements PlatformService {
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
}
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
},
): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return {
saved: 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.
* @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 [];
}
@ -480,7 +488,15 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform.
* @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 [];
}
@ -491,8 +507,14 @@ export class WebPlatformService implements PlatformService {
* @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status
*/
async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "File opening not available in web platform" };
async openFile(
_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
*/
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.
* @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 [];
}
@ -537,10 +573,16 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform.
* @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 {
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
*/
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">
<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>
<button
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 {
const result = await platformService.testLocationSelectionSilent();
this.fileSharingResult = result;
logger.log("Silent Location Selection Test Result:", this.fileSharingResult);
logger.log(
"Silent Location Selection Test Result:",
this.fileSharingResult,
);
} catch (error) {
logger.error("Silent Location Selection Test Error:", error);
this.$notify(
@ -825,8 +829,10 @@ export default class Help extends Vue {
public async testFileDiscoveryDebug() {
const platformService = PlatformServiceFactory.getInstance();
try {
if ('debugFileDiscoveryStepByStep' in platformService) {
const result = await (platformService as any).debugFileDiscoveryStepByStep();
if ("debugFileDiscoveryStepByStep" in platformService) {
const result = await (
platformService as any
).debugFileDiscoveryStepByStep();
this.fileSharingResult = result;
logger.log("File Discovery Debug Test Result:", this.fileSharingResult);
} else {

Loading…
Cancel
Save