You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
894 lines
26 KiB
894 lines
26 KiB
/** * 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">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-semibold">Backup Files</h3>
|
|
<div class="flex gap-2">
|
|
<button
|
|
v-if="platformCapabilities.hasFileSystem"
|
|
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"
|
|
class="fa-fw"
|
|
:class="{ 'animate-spin': isLoading }"
|
|
/>
|
|
Refresh
|
|
</button>
|
|
<button
|
|
v-if="platformCapabilities.hasFileSystem"
|
|
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"
|
|
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
|
|
: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
|
|
: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>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="text-center py-4">
|
|
<font-awesome icon="spinner" class="animate-spin fa-2x" />
|
|
<p class="mt-2">Loading backup files...</p>
|
|
</div>
|
|
|
|
<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>
|
|
<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 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>
|
|
<li v-if="platformCapabilities.isIOS" class="text-orange-700">
|
|
• On iOS: Files are saved to Documents folder (accessible via Files
|
|
app)
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="space-y-2">
|
|
<!-- File Type Filter -->
|
|
<div class="flex gap-2 mb-3">
|
|
<button
|
|
v-for="type in ['all', 'contacts', 'seed', 'other'] as const"
|
|
:key="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',
|
|
]"
|
|
@click="selectedType = type"
|
|
>
|
|
{{
|
|
type === "all"
|
|
? "All"
|
|
: type.charAt(0).toUpperCase() + type.slice(1)
|
|
}}
|
|
<span class="ml-1 text-xs"> ({{ getFileCountByType(type) }}) </span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Files List -->
|
|
<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)"
|
|
>
|
|
{{ crumb }}
|
|
</span>
|
|
<span v-else class="font-bold">{{ crumb }}</span>
|
|
<span v-if="idx < breadcrumbs.length - 1"> / </span>
|
|
</span>
|
|
</div>
|
|
<div v-if="currentPath.length > 1" class="mb-2">
|
|
<button class="text-xs text-blue-500 underline" @click="goUp">
|
|
⬅ Up
|
|
</button>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="inline-flex items-center">
|
|
<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
|
|
>
|
|
</div>
|
|
<div class="space-y-2 max-h-64 overflow-y-auto">
|
|
<div
|
|
v-for="entry in folders"
|
|
:key="'folder-' + entry.path"
|
|
class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer"
|
|
@click="openFolder(entry)"
|
|
>
|
|
<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
|
|
>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-for="entry in files"
|
|
:key="'file-' + entry.path"
|
|
class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50"
|
|
>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<font-awesome icon="file-alt" class="fa-fw text-gray-500" />
|
|
<span class="font-medium truncate">{{ entry.name }}</span>
|
|
</div>
|
|
<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
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 ml-3">
|
|
<button
|
|
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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary -->
|
|
<div class="text-sm text-gray-500 mt-3 pt-3 border-t">
|
|
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>
|
|
<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>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Vue, Watch } from "vue-facing-decorator";
|
|
import { NotificationIface } from "../constants/app";
|
|
import { logger } from "../utils/logger";
|
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
|
import {
|
|
PlatformService,
|
|
PlatformCapabilities,
|
|
} from "../services/PlatformService";
|
|
|
|
/**
|
|
* @vue-component
|
|
* Backup Files List Component
|
|
* Displays and manages backup files with platform-specific functionality
|
|
*/
|
|
@Component
|
|
export default class BackupFilesList extends Vue {
|
|
/**
|
|
* Notification function injected by Vue
|
|
* Used to show success/error messages to the user
|
|
*/
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
/**
|
|
* Platform service instance for platform-specific operations
|
|
*/
|
|
private platformService: PlatformService =
|
|
PlatformServiceFactory.getInstance();
|
|
|
|
/**
|
|
* Platform capabilities for the current platform
|
|
*/
|
|
private get platformCapabilities(): PlatformCapabilities {
|
|
return this.platformService.getCapabilities();
|
|
}
|
|
|
|
/**
|
|
* List of backup files found on the device
|
|
*/
|
|
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";
|
|
|
|
/**
|
|
* Loading state for file operations
|
|
*/
|
|
isLoading = false;
|
|
|
|
/**
|
|
* Interval for periodic refresh (5 minutes)
|
|
*/
|
|
private refreshInterval: number | null = null;
|
|
|
|
/**
|
|
* Current path for folder navigation (array for breadcrumbs)
|
|
*/
|
|
currentPath: string[] = [];
|
|
|
|
/**
|
|
* List of files/folders in the current directory
|
|
*/
|
|
directoryEntries: Array<{
|
|
name: string;
|
|
uri: string;
|
|
size?: number;
|
|
path: string;
|
|
type: "file" | "folder";
|
|
}> = [];
|
|
|
|
/**
|
|
* Temporary debug mode to show all entries as files
|
|
*/
|
|
debugShowAll = false;
|
|
|
|
/**
|
|
* Checks and requests storage permissions if needed.
|
|
* Returns true if permission is granted, false otherwise.
|
|
*/
|
|
private async ensureStoragePermission(): Promise<boolean> {
|
|
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") {
|
|
try {
|
|
await platformService.checkStoragePermissions();
|
|
logger.log("[BackupFilesList] Storage permission granted.");
|
|
return true;
|
|
} catch (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"
|
|
) {
|
|
try {
|
|
guidance = await platformService.getStoragePermissionGuidance();
|
|
} catch (guidanceError) {
|
|
logger.warn(
|
|
"[BackupFilesList] Could not get permission guidance:",
|
|
guidanceError,
|
|
);
|
|
}
|
|
}
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Storage Permission Required",
|
|
text: guidance,
|
|
},
|
|
10000, // Show for 10 seconds to give user time to read
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Lifecycle hook to load backup files when component is mounted
|
|
*/
|
|
async mounted() {
|
|
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 = ["."];
|
|
} else {
|
|
this.currentPath = ["Download", "TimeSafari"];
|
|
}
|
|
await this.loadDirectory();
|
|
this.refreshInterval = window.setInterval(
|
|
() => {
|
|
this.loadDirectory();
|
|
},
|
|
5 * 60 * 1000,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lifecycle hook to clean up resources when component is unmounted
|
|
*/
|
|
beforeUnmount() {
|
|
if (this.refreshInterval) {
|
|
clearInterval(this.refreshInterval);
|
|
this.refreshInterval = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computed property for filtered files based on selected type
|
|
* 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);
|
|
return this.backupFiles;
|
|
}
|
|
const filtered = this.backupFiles.filter(
|
|
(file) => file.type === this.selectedType,
|
|
);
|
|
logger.log(
|
|
`[BackupFilesList] filteredFiles (${this.selectedType}):`,
|
|
filtered,
|
|
);
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Computed property to check if we're in development mode
|
|
*/
|
|
get isDevelopment(): boolean {
|
|
return import.meta.env.DEV;
|
|
}
|
|
|
|
/**
|
|
* Load the current directory entries
|
|
*/
|
|
async loadDirectory() {
|
|
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 PlatformService
|
|
).listFilesInDirectory(path, this.debugShowAll);
|
|
logger.log("[BackupFilesList] Loaded directory:", {
|
|
path,
|
|
entries: this.directoryEntries,
|
|
});
|
|
} catch (error) {
|
|
logger.error("[BackupFilesList] Failed to load directory:", error);
|
|
this.directoryEntries = [];
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigate into a folder
|
|
*/
|
|
async openFolder(entry: { name: string; path: string }) {
|
|
this.currentPath.push(entry.name);
|
|
await this.loadDirectory();
|
|
}
|
|
|
|
/**
|
|
* Navigate to a breadcrumb
|
|
*/
|
|
async goToBreadcrumb(index: number) {
|
|
this.currentPath = this.currentPath.slice(0, index + 1);
|
|
await this.loadDirectory();
|
|
}
|
|
|
|
/**
|
|
* Go up one directory
|
|
*/
|
|
async goUp() {
|
|
if (this.currentPath.length > 1) {
|
|
this.currentPath.pop();
|
|
await this.loadDirectory();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computed property for breadcrumbs
|
|
*/
|
|
get breadcrumbs() {
|
|
return this.currentPath;
|
|
}
|
|
|
|
/**
|
|
* Computed property for showing files and folders
|
|
*/
|
|
get folders() {
|
|
return this.directoryEntries.filter((e) => e.type === "folder");
|
|
}
|
|
get files() {
|
|
return this.directoryEntries.filter((e) => e.type === "file");
|
|
}
|
|
|
|
/**
|
|
* Refreshes the list of backup files from the device
|
|
*/
|
|
async refreshFiles() {
|
|
logger.log("[BackupFilesList] refreshFiles called.");
|
|
if (!this.platformCapabilities.hasFileSystem) {
|
|
return;
|
|
}
|
|
// Check/request permission before refreshing
|
|
const hasPermission = await this.ensureStoragePermission();
|
|
if (!hasPermission) {
|
|
this.backupFiles = [];
|
|
this.isLoading = false;
|
|
return;
|
|
}
|
|
this.isLoading = true;
|
|
try {
|
|
this.backupFiles = await this.platformService.listBackupFiles();
|
|
logger.log("[BackupFilesList] Refreshed backup files:", {
|
|
count: this.backupFiles.length,
|
|
files: this.backupFiles.map((f) => ({
|
|
name: f.name,
|
|
type: f.type,
|
|
path: f.path,
|
|
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,
|
|
};
|
|
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,
|
|
);
|
|
} catch (error) {
|
|
logger.error("[BackupFilesList] Failed to refresh backup files:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Loading Files",
|
|
text: "Failed to load backup files from your device.",
|
|
},
|
|
5000,
|
|
);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a test backup file for debugging purposes
|
|
*/
|
|
async createTestBackup() {
|
|
try {
|
|
this.isLoading = true;
|
|
logger.log("[BackupFilesList] Creating test backup file");
|
|
|
|
const result = await this.platformService.createTestBackupFile();
|
|
|
|
if (result.success) {
|
|
logger.log("[BackupFilesList] Test backup file created successfully:", {
|
|
fileName: result.fileName,
|
|
uri: result.uri,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Test Backup Created",
|
|
text: `Test backup file "${result.fileName}" created successfully. Refresh the list to see it.`,
|
|
},
|
|
5000,
|
|
);
|
|
|
|
// Refresh the file list to show the new test file
|
|
await this.refreshFiles();
|
|
} else {
|
|
throw new Error(result.error || "Failed to create test backup file");
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
"[BackupFilesList] Failed to create test backup file:",
|
|
error,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Test Backup Failed",
|
|
text: "Failed to create test backup file. Check the console for details.",
|
|
},
|
|
5000,
|
|
);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests different directory contexts to debug file visibility issues
|
|
*/
|
|
async testDirectoryContexts() {
|
|
try {
|
|
this.isLoading = true;
|
|
logger.log("[BackupFilesList] Testing directory contexts");
|
|
|
|
const debugOutput = await this.platformService.testDirectoryContexts();
|
|
|
|
logger.log(
|
|
"[BackupFilesList] Directory context test results:",
|
|
debugOutput,
|
|
);
|
|
|
|
// Show the debug output in a notification or alert
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Directory Context Test",
|
|
text: "Directory context test completed. Check the console for detailed results.",
|
|
},
|
|
5000,
|
|
);
|
|
|
|
// Also log the full output to console for easy access
|
|
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,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Context Test Failed",
|
|
text: "Failed to test directory contexts. Check the console for details.",
|
|
},
|
|
5000,
|
|
);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refreshes the file list after a backup is created
|
|
* This method can be called from parent components
|
|
*/
|
|
async refreshAfterSave() {
|
|
logger.log("[BackupFilesList] refreshAfterSave called");
|
|
await this.refreshFiles();
|
|
}
|
|
|
|
/**
|
|
* Opens a specific file in the device's file viewer
|
|
* @param fileUri - URI of the file to open
|
|
* @param fileName - Name of the file for display
|
|
*/
|
|
async openFile(fileUri: string, fileName: string) {
|
|
try {
|
|
const result = await this.platformService.openFile(fileUri, fileName);
|
|
|
|
if (result.success) {
|
|
logger.log("[BackupFilesList] File opened successfully:", {
|
|
fileName,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
throw new Error(result.error || "Failed to open file");
|
|
}
|
|
} catch (error) {
|
|
logger.error("[BackupFilesList] Failed to open file:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Opening File",
|
|
text: `Failed to open ${fileName}. ${error instanceof Error ? error.message : String(error)}`,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens the backup directory in the device's file explorer
|
|
*/
|
|
async openBackupDirectory() {
|
|
try {
|
|
const result = await this.platformService.openBackupDirectory();
|
|
|
|
if (result.success) {
|
|
logger.log("[BackupFilesList] Backup directory opened successfully:", {
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
throw new Error(result.error || "Failed to open backup directory");
|
|
}
|
|
} catch (error) {
|
|
logger.error("[BackupFilesList] Failed to open backup directory:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Opening Directory",
|
|
text: `Failed to open backup directory. ${error instanceof Error ? error.message : String(error)}`,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
let count;
|
|
if (type === "all") {
|
|
count = this.backupFiles.length;
|
|
logger.log(
|
|
"[BackupFilesList] getFileCountByType (All):",
|
|
count,
|
|
this.backupFiles,
|
|
);
|
|
return count;
|
|
}
|
|
count = this.backupFiles.filter((file) => file.type === type).length;
|
|
logger.log(`[BackupFilesList] getFileCountByType (${type}):`, count);
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Gets the appropriate icon for a file type
|
|
* @param type - File type
|
|
* @returns FontAwesome icon name
|
|
*/
|
|
getFileIcon(type: "contacts" | "seed" | "other"): string {
|
|
switch (type) {
|
|
case "contacts":
|
|
return "address-book";
|
|
case "seed":
|
|
return "key";
|
|
default:
|
|
return "file-alt";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the appropriate icon color for a file type
|
|
* @param type - File type
|
|
* @returns CSS color class
|
|
*/
|
|
getFileIconColor(type: "contacts" | "seed" | "other"): string {
|
|
switch (type) {
|
|
case "contacts":
|
|
return "text-blue-500";
|
|
case "seed":
|
|
return "text-orange-500";
|
|
default:
|
|
return "text-gray-500";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the appropriate badge color for a file type
|
|
* @param type - File type
|
|
* @returns CSS color class
|
|
*/
|
|
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";
|
|
default:
|
|
return "bg-gray-100 text-gray-800";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats file size in human-readable format
|
|
* @param bytes - File size in bytes
|
|
* @returns Formatted file size string
|
|
*/
|
|
formatFileSize(bytes: number): string {
|
|
if (bytes === 0) return "0 Bytes";
|
|
|
|
const k = 1024;
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Debug method to test file discovery
|
|
* Can be called from browser console for troubleshooting
|
|
*/
|
|
public async debugFileDiscovery() {
|
|
try {
|
|
logger.log("[BackupFilesList] Starting debug file discovery...");
|
|
|
|
// Test the platform service's test methods
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
// Test listing all user files
|
|
const allFilesResult = await platformService.testListUserFiles();
|
|
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,
|
|
);
|
|
|
|
// 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();
|
|
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,
|
|
})),
|
|
});
|
|
}
|
|
|
|
// 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,
|
|
);
|
|
}
|
|
|
|
return {
|
|
allFiles: allFilesResult,
|
|
backupFiles: backupFilesResult,
|
|
currentBackupFiles: this.backupFiles,
|
|
debugAllFiles:
|
|
"debugListAllFiles" in platformService
|
|
? await (platformService as any).debugListAllFiles()
|
|
: null,
|
|
};
|
|
} catch (error) {
|
|
logger.error("[BackupFilesList] Debug file discovery failed:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
@Watch("platformCapabilities.hasFileSystem", { immediate: true })
|
|
async onFileSystemCapabilityChanged(newVal: boolean) {
|
|
if (newVal) {
|
|
await this.refreshFiles();
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|