Compare commits
11 Commits
master
...
capacitor-
Author | SHA1 | Date |
---|---|---|
|
a1b6add178 | 11 hours ago |
|
d3a26a54d4 | 12 hours ago |
|
122b5b1a06 | 12 hours ago |
|
9e8f08aa49 | 12 hours ago |
|
1529cc9689 | 13 hours ago |
|
f7ed05d13f | 13 hours ago |
|
7c8a6d0666 | 13 hours ago |
|
1aa285be55 | 1 day ago |
|
2635c22c33 | 2 days ago |
|
2d516b90b0 | 3 days ago |
|
7a1329e1a4 | 4 days ago |
15 changed files with 6011 additions and 592 deletions
@ -0,0 +1,533 @@ |
|||
# TimeSafari Contact Backup System |
|||
|
|||
## Overview |
|||
|
|||
The TimeSafari application implements a comprehensive contact backup and listing system that works across multiple platforms (Web, iOS, Android, Desktop). This document breaks down how contacts are saved, exported, and listed as backups. |
|||
|
|||
## Architecture Components |
|||
|
|||
### 1. Database Layer |
|||
|
|||
#### Contact Data Structure |
|||
```typescript |
|||
interface Contact { |
|||
did: string; // Decentralized Identifier (primary key) |
|||
contactMethods?: ContactMethod[]; // Array of contact methods (EMAIL, SMS, etc.) |
|||
name?: string; // Display name |
|||
nextPubKeyHashB64?: string; // Base64 hash of next public key |
|||
notes?: string; // User notes |
|||
profileImageUrl?: string; // Profile image URL |
|||
publicKeyBase64?: string; // Base64 encoded public key |
|||
seesMe?: boolean; // Visibility setting |
|||
registered?: boolean; // Registration status |
|||
} |
|||
|
|||
interface ContactMethod { |
|||
label: string; // Display label |
|||
type: string; // Type (EMAIL, SMS, WHATSAPP, etc.) |
|||
value: string; // Contact value |
|||
} |
|||
``` |
|||
|
|||
#### Database Schema |
|||
```sql |
|||
CREATE TABLE contacts ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
did TEXT NOT NULL, -- Decentralized Identifier |
|||
name TEXT, -- Display name |
|||
contactMethods TEXT, -- JSON string of contact methods |
|||
nextPubKeyHashB64 TEXT, -- Next public key hash |
|||
notes TEXT, -- User notes |
|||
profileImageUrl TEXT, -- Profile image URL |
|||
publicKeyBase64 TEXT, -- Public key |
|||
seesMe BOOLEAN, -- Visibility flag |
|||
registered BOOLEAN -- Registration status |
|||
); |
|||
|
|||
CREATE INDEX idx_contacts_did ON contacts(did); |
|||
CREATE INDEX idx_contacts_name ON contacts(name); |
|||
``` |
|||
|
|||
### 2. Contact Saving Operations |
|||
|
|||
#### A. Adding New Contacts |
|||
|
|||
**1. QR Code Scanning (`ContactQRScanFullView.vue`)** |
|||
```typescript |
|||
async addNewContact(contact: Contact) { |
|||
// Check for existing contact |
|||
const existingContacts = await platformService.dbQuery( |
|||
"SELECT * FROM contacts WHERE did = ?", [contact.did] |
|||
); |
|||
|
|||
if (existingContact) { |
|||
// Handle duplicate |
|||
return; |
|||
} |
|||
|
|||
// Convert contactMethods to JSON string for storage |
|||
contact.contactMethods = JSON.stringify( |
|||
parseJsonField(contact.contactMethods, []) |
|||
); |
|||
|
|||
// Insert into database |
|||
const { sql, params } = databaseUtil.generateInsertStatement( |
|||
contact as unknown as Record<string, unknown>, "contacts" |
|||
); |
|||
await platformService.dbExec(sql, params); |
|||
} |
|||
``` |
|||
|
|||
**2. Manual Contact Addition (`ContactsView.vue`)** |
|||
```typescript |
|||
private async addContact(newContact: Contact) { |
|||
// Validate DID format |
|||
if (!isDid(newContact.did)) { |
|||
throw new Error("Invalid DID format"); |
|||
} |
|||
|
|||
// Generate and execute INSERT statement |
|||
const { sql, params } = databaseUtil.generateInsertStatement( |
|||
newContact as unknown as Record<string, unknown>, "contacts" |
|||
); |
|||
await platformService.dbExec(sql, params); |
|||
} |
|||
``` |
|||
|
|||
**3. Contact Import (`ContactImportView.vue`)** |
|||
```typescript |
|||
async importContacts() { |
|||
for (const contact of selectedContacts) { |
|||
const contactToStore = contactToDbRecord(contact); |
|||
|
|||
if (existingContact) { |
|||
// Update existing contact |
|||
const { sql, params } = databaseUtil.generateUpdateStatement( |
|||
contactToStore, "contacts", "did = ?", [contact.did] |
|||
); |
|||
await platformService.dbExec(sql, params); |
|||
} else { |
|||
// Add new contact |
|||
const { sql, params } = databaseUtil.generateInsertStatement( |
|||
contactToStore, "contacts" |
|||
); |
|||
await platformService.dbExec(sql, params); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### B. Updating Existing Contacts |
|||
|
|||
**Contact Editing (`ContactEditView.vue`)** |
|||
```typescript |
|||
async saveEdit() { |
|||
// Normalize contact methods |
|||
const contactMethods = this.contactMethods.map(method => ({ |
|||
...method, |
|||
type: method.type.toUpperCase() |
|||
})); |
|||
|
|||
// Update database |
|||
const contactMethodsString = JSON.stringify(contactMethods); |
|||
await platformService.dbExec( |
|||
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?", |
|||
[this.contactName, this.contactNotes, contactMethodsString, this.contact?.did] |
|||
); |
|||
} |
|||
``` |
|||
|
|||
### 3. Contact Export/Backup System |
|||
|
|||
#### A. Export Process (`DataExportSection.vue`) |
|||
|
|||
#### 1. Data Retrieval |
|||
|
|||
```typescript |
|||
async exportDatabase() { |
|||
// Query all contacts from database |
|||
const result = await platformService.dbQuery("SELECT * FROM contacts"); |
|||
const allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[]; |
|||
|
|||
// Convert to export format |
|||
const exportData = contactsToExportJson(allContacts); |
|||
const jsonStr = JSON.stringify(exportData, null, 2); |
|||
} |
|||
``` |
|||
|
|||
#### 2. Export Format Conversion (`libs/util.ts`) |
|||
|
|||
```typescript |
|||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => { |
|||
const rows = contacts.map((contact) => ({ |
|||
did: contact.did, |
|||
name: contact.name || null, |
|||
contactMethods: contact.contactMethods |
|||
? JSON.stringify(parseJsonField(contact.contactMethods, [])) |
|||
: null, |
|||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null, |
|||
notes: contact.notes || null, |
|||
profileImageUrl: contact.profileImageUrl || null, |
|||
publicKeyBase64: contact.publicKeyBase64 || null, |
|||
seesMe: contact.seesMe || false, |
|||
registered: contact.registered || false, |
|||
})); |
|||
|
|||
return { |
|||
data: { |
|||
data: [{ tableName: "contacts", rows }] |
|||
} |
|||
}; |
|||
}; |
|||
``` |
|||
|
|||
#### 3. File Generation |
|||
|
|||
```typescript |
|||
// Create timestamped filename |
|||
const timestamp = getTimestampForFilename(); |
|||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`; |
|||
|
|||
// Create blob and save |
|||
const blob = new Blob([jsonStr], { type: "application/json" }); |
|||
``` |
|||
|
|||
#### B. Platform-Specific File Saving |
|||
|
|||
##### 1. Web Platform (`WebPlatformService.ts`)** |
|||
|
|||
```typescript |
|||
// Uses browser download API |
|||
const downloadUrl = URL.createObjectURL(blob); |
|||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; |
|||
downloadAnchor.href = downloadUrl; |
|||
downloadAnchor.download = fileName; |
|||
downloadAnchor.click(); |
|||
``` |
|||
|
|||
##### 2. Mobile Platforms (`CapacitorPlatformService.ts`) |
|||
|
|||
```typescript |
|||
async writeAndShareFile(fileName: string, content: string, options = {}) { |
|||
let fileUri: string; |
|||
|
|||
if (options.allowLocationSelection) { |
|||
// User chooses location |
|||
fileUri = await this.saveWithUserChoice(fileName, content, options.mimeType); |
|||
} else if (options.saveToPrivateStorage) { |
|||
// Save to app-private storage |
|||
const result = await Filesystem.writeFile({ |
|||
path: fileName, |
|||
data: content, |
|||
directory: Directory.Data, |
|||
encoding: Encoding.UTF8, |
|||
recursive: true, |
|||
}); |
|||
fileUri = result.uri; |
|||
} else { |
|||
// Save to user-accessible location (Downloads/Documents) |
|||
fileUri = await this.saveToDownloads(fileName, content); |
|||
} |
|||
|
|||
// Share the file |
|||
return await this.shareFile(fileUri, fileName); |
|||
} |
|||
``` |
|||
|
|||
##### 3. Desktop Platforms (`ElectronPlatformService.ts`, `PyWebViewPlatformService.ts`) |
|||
|
|||
```typescript |
|||
// Not implemented - returns empty results |
|||
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { |
|||
return []; |
|||
} |
|||
``` |
|||
|
|||
### 4. Backup File Listing System |
|||
|
|||
#### A. File Discovery (`CapacitorPlatformService.ts`) |
|||
|
|||
##### 1. Enhanced File Discovery |
|||
|
|||
```typescript |
|||
async listUserAccessibleFilesEnhanced(): Promise<Array<{name: string, uri: string, size?: number, path?: string}>> { |
|||
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = []; |
|||
|
|||
if (this.getCapabilities().isIOS) { |
|||
// iOS: Documents directory |
|||
const result = await Filesystem.readdir({ |
|||
path: ".", |
|||
directory: Directory.Documents, |
|||
}); |
|||
const files = result.files.map((file) => ({ |
|||
name: typeof file === "string" ? file : file.name, |
|||
uri: `file://${file.uri || file}`, |
|||
size: typeof file === "string" ? undefined : file.size, |
|||
path: "Documents" |
|||
})); |
|||
allFiles.push(...files); |
|||
} else { |
|||
// Android: Multiple locations |
|||
const commonPaths = ["Download", "Documents", "Backups", "TimeSafari", "Data"]; |
|||
|
|||
for (const path of commonPaths) { |
|||
try { |
|||
const result = await Filesystem.readdir({ |
|||
path: path, |
|||
directory: Directory.ExternalStorage, |
|||
}); |
|||
|
|||
// Filter for TimeSafari-related files |
|||
const relevantFiles = result.files |
|||
.filter(file => { |
|||
const fileName = typeof file === "string" ? file : file.name; |
|||
const name = fileName.toLowerCase(); |
|||
return name.includes('timesafari') || |
|||
name.includes('backup') || |
|||
name.includes('contacts') || |
|||
name.endsWith('.json'); |
|||
}) |
|||
.map((file) => ({ |
|||
name: typeof file === "string" ? file : file.name, |
|||
uri: `file://${file.uri || file}`, |
|||
size: typeof file === "string" ? undefined : file.size, |
|||
path: path |
|||
})); |
|||
|
|||
allFiles.push(...relevantFiles); |
|||
} catch (error) { |
|||
// Silently skip inaccessible directories |
|||
} |
|||
} |
|||
} |
|||
|
|||
return allFiles; |
|||
} |
|||
``` |
|||
|
|||
**2. Backup File Filtering** |
|||
```typescript |
|||
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { |
|||
const allFiles = await this.listUserAccessibleFilesEnhanced(); |
|||
|
|||
const backupFiles = allFiles |
|||
.filter(file => { |
|||
const name = file.name.toLowerCase(); |
|||
|
|||
// Exclude directory-access notification files |
|||
if (name.startsWith('timesafari-directory-access-') && name.endsWith('.txt')) { |
|||
return false; |
|||
} |
|||
|
|||
// Check backup criteria |
|||
const isJson = name.endsWith('.json'); |
|||
const hasTimeSafari = name.includes('timesafari'); |
|||
const hasBackup = name.includes('backup'); |
|||
const hasContacts = name.includes('contacts'); |
|||
const hasSeed = name.includes('seed'); |
|||
const hasExport = name.includes('export'); |
|||
const hasData = name.includes('data'); |
|||
|
|||
return isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData; |
|||
}) |
|||
.map(file => { |
|||
const name = file.name.toLowerCase(); |
|||
let type: 'contacts' | 'seed' | 'other' = 'other'; |
|||
|
|||
// Categorize files |
|||
if (name.includes('contacts') || (name.includes('timesafari') && name.includes('backup'))) { |
|||
type = 'contacts'; |
|||
} else if (name.includes('seed') || name.includes('mnemonic') || name.includes('private')) { |
|||
type = 'seed'; |
|||
} else if (name.endsWith('.json')) { |
|||
type = 'other'; |
|||
} |
|||
|
|||
return { ...file, type }; |
|||
}); |
|||
|
|||
return backupFiles; |
|||
} |
|||
``` |
|||
|
|||
#### B. UI Components (`BackupFilesList.vue`) |
|||
|
|||
**1. File Display** |
|||
```typescript |
|||
@Component |
|||
export default class BackupFilesList extends Vue { |
|||
backupFiles: Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}> = []; |
|||
selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all'; |
|||
isLoading = false; |
|||
|
|||
async refreshFiles() { |
|||
this.isLoading = true; |
|||
try { |
|||
this.backupFiles = await this.platformService.listBackupFiles(); |
|||
|
|||
// 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 |
|||
}; |
|||
} catch (error) { |
|||
// Handle error |
|||
} finally { |
|||
this.isLoading = false; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**2. File Operations** |
|||
```typescript |
|||
async openFile(fileUri: string, fileName: string) { |
|||
const result = await this.platformService.openFile(fileUri, fileName); |
|||
if (!result.success) { |
|||
throw new Error(result.error || "Failed to open file"); |
|||
} |
|||
} |
|||
|
|||
async openBackupDirectory() { |
|||
const result = await this.platformService.openBackupDirectory(); |
|||
if (!result.success) { |
|||
throw new Error(result.error || "Failed to open backup directory"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 5. Platform-Specific Storage Locations |
|||
|
|||
#### A. iOS Platform |
|||
- **Primary Location**: Documents folder (accessible via Files app) |
|||
- **Persistence**: Survives app installations |
|||
- **Access**: Through iOS Files app |
|||
- **File Format**: JSON with timestamped filenames |
|||
|
|||
#### B. Android Platform |
|||
- **Primary Locations**: |
|||
- `Download/TimeSafari/` (external storage) |
|||
- `TimeSafari/` (external storage) |
|||
- User-chosen locations via file picker |
|||
- **Persistence**: Survives app installations |
|||
- **Access**: Through file managers |
|||
- **File Format**: JSON with timestamped filenames |
|||
|
|||
#### C. Web Platform |
|||
- **Primary Location**: Browser downloads folder |
|||
- **Persistence**: Depends on browser settings |
|||
- **Access**: Through browser download manager |
|||
- **File Format**: JSON with timestamped filenames |
|||
|
|||
#### D. Desktop Platforms (Electron/PyWebView) |
|||
- **Status**: Not implemented |
|||
- **Fallback**: Returns empty arrays for file operations |
|||
|
|||
### 6. File Naming Convention |
|||
|
|||
#### A. Contact Backup Files |
|||
``` |
|||
TimeSafari-backup-contacts-YYYY-MM-DD-HH-MM-SS.json |
|||
``` |
|||
|
|||
#### B. File Content Structure |
|||
```json |
|||
{ |
|||
"data": { |
|||
"data": [ |
|||
{ |
|||
"tableName": "contacts", |
|||
"rows": [ |
|||
{ |
|||
"did": "did:ethr:0x...", |
|||
"name": "Contact Name", |
|||
"contactMethods": "[{\"type\":\"EMAIL\",\"value\":\"email@example.com\"}]", |
|||
"notes": "User notes", |
|||
"profileImageUrl": "https://...", |
|||
"publicKeyBase64": "base64...", |
|||
"seesMe": true, |
|||
"registered": false |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 7. Error Handling and Logging |
|||
|
|||
#### A. Comprehensive Logging |
|||
```typescript |
|||
logger.log("[CapacitorPlatformService] File write successful:", { |
|||
uri: fileUri, |
|||
saved, |
|||
timestamp: new Date().toISOString(), |
|||
}); |
|||
|
|||
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(), |
|||
}); |
|||
``` |
|||
|
|||
#### B. Error Recovery |
|||
```typescript |
|||
try { |
|||
// File operations |
|||
} catch (error) { |
|||
logger.error("[CapacitorPlatformService] Failed to list backup files:", error); |
|||
return []; |
|||
} |
|||
``` |
|||
|
|||
### 8. Security Considerations |
|||
|
|||
#### A. Data Privacy |
|||
- Contact data is stored locally on device |
|||
- No cloud synchronization of contact data |
|||
- User controls visibility settings per contact |
|||
- Backup files contain only user-authorized data |
|||
|
|||
#### B. File Access |
|||
- Platform-specific permission handling |
|||
- User choice for file locations |
|||
- Secure storage options for sensitive data |
|||
- Proper error handling for access failures |
|||
|
|||
### 9. Performance Optimizations |
|||
|
|||
#### A. Database Operations |
|||
- Indexed queries on `did` and `name` fields |
|||
- Batch operations for multiple contacts |
|||
- Efficient JSON serialization/deserialization |
|||
- Connection pooling and reuse |
|||
|
|||
#### B. File Operations |
|||
- Asynchronous file I/O |
|||
- Efficient file discovery algorithms |
|||
- Caching of file lists |
|||
- Background refresh operations |
|||
|
|||
## Summary |
|||
|
|||
The TimeSafari contact backup system provides: |
|||
|
|||
1. **Robust Data Storage**: SQLite-based contact storage with proper indexing |
|||
2. **Cross-Platform Compatibility**: Works on web, iOS, Android, and desktop |
|||
3. **Flexible Export Options**: Multiple file formats and storage locations |
|||
4. **Intelligent File Discovery**: Finds backup files regardless of user-chosen locations |
|||
5. **User-Friendly Interface**: Clear categorization and easy file management |
|||
6. **Comprehensive Logging**: Detailed tracking for debugging and monitoring |
|||
7. **Security-First Design**: Privacy-preserving with user-controlled data access |
|||
|
|||
The system ensures that users can reliably backup and restore their contact data across different platforms while maintaining data integrity and user privacy. |
@ -0,0 +1,894 @@ |
|||
/** * 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> |
File diff suppressed because it is too large
Loading…
Reference in new issue