Browse Source

WIP: Debug backup file discovery system - Fixed recursive directory search in CapacitorPlatformService to properly search subdirectories instead of excluding them - Added missing listFilesInDirectory method to all platform services for directory browsing functionality - Added debug methods (debugTimeSafariDirectory, createTestBackupFile, testDirectoryContexts) to help diagnose file visibility issues - Enhanced logging for backup file discovery process - Current issue: TimeSafari directory exists in Download but shows 'Directory does not exist' when trying to read contents - Need to investigate why JSON backup files are not being found despite directory existence

capacitor-local-save
Matthew Raymer 1 day ago
parent
commit
1aa285be55
  1. 533
      CONTACT_BACKUP_SYSTEM.md
  2. 205
      src/components/BackupFilesList.vue
  3. 29
      src/services/PlatformService.ts
  4. 500
      src/services/platforms/CapacitorPlatformService.ts
  5. 41
      src/services/platforms/ElectronPlatformService.ts
  6. 41
      src/services/platforms/PyWebViewPlatformService.ts
  7. 39
      src/services/platforms/WebPlatformService.ts

533
CONTACT_BACKUP_SYSTEM.md

@ -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.

205
src/components/BackupFilesList.vue

@ -51,6 +51,20 @@
<font-awesome icon="bug" class="fa-fw" /> <font-awesome icon="bug" class="fa-fw" />
Debug Debug
</button> </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"
>
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"
>
Test Contexts
</button>
</div> </div>
</div> </div>
@ -238,21 +252,57 @@ export default class BackupFilesList extends Vue {
*/ */
debugShowAll = false; 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);
this.$notify(
{
group: "alert",
type: "danger",
title: "Storage Permission Required",
text: "This app needs permission to access your files to list and restore backups. Please grant storage permission and try again.",
},
7000,
);
return false;
}
}
return true;
}
/** /**
* Lifecycle hook to load backup files when component is mounted * Lifecycle hook to load backup files when component is mounted
*/ */
async mounted() { async mounted() {
logger.log('[BackupFilesList] mounted hook called. platformCapabilities:', this.platformCapabilities);
if (this.platformCapabilities.hasFileSystem) { if (this.platformCapabilities.hasFileSystem) {
// Set default root path // Check/request permission before loading
if (this.platformCapabilities.isIOS) { const hasPermission = await this.ensureStoragePermission();
this.currentPath = ['.']; if (hasPermission) {
} else { // Set default root path
this.currentPath = ['Download', 'TimeSafari']; if (this.platformCapabilities.isIOS) {
this.currentPath = ['.'];
} else {
this.currentPath = ['Download', 'TimeSafari'];
}
await this.loadDirectory();
this.refreshInterval = window.setInterval(() => {
this.loadDirectory();
}, 5 * 60 * 1000);
} }
await this.loadDirectory();
this.refreshInterval = window.setInterval(() => {
this.loadDirectory();
}, 5 * 60 * 1000);
} }
} }
@ -268,12 +318,16 @@ export default class BackupFilesList extends Vue {
/** /**
* Computed property for filtered files based on selected type * Computed property for filtered files based on selected type
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
*/ */
get filteredFiles() { get filteredFiles() {
if (this.selectedType === 'all') { if (this.selectedType === 'all') {
logger.log('[BackupFilesList] filteredFiles (All):', this.backupFiles);
return this.backupFiles; return this.backupFiles;
} }
return this.backupFiles.filter(file => file.type === this.selectedType); const filtered = this.backupFiles.filter(file => file.type === this.selectedType);
logger.log(`[BackupFilesList] filteredFiles (${this.selectedType}):`, filtered);
return filtered;
} }
/** /**
@ -348,15 +402,21 @@ export default class BackupFilesList extends Vue {
* Refreshes the list of backup files from the device * Refreshes the list of backup files from the device
*/ */
async refreshFiles() { async refreshFiles() {
logger.log('[BackupFilesList] refreshFiles called.');
if (!this.platformCapabilities.hasFileSystem) { if (!this.platformCapabilities.hasFileSystem) {
return; return;
} }
// Check/request permission before refreshing
const hasPermission = await this.ensureStoragePermission();
if (!hasPermission) {
this.backupFiles = [];
this.isLoading = false;
return;
}
this.isLoading = true; this.isLoading = true;
try { try {
this.backupFiles = await this.platformService.listBackupFiles(); this.backupFiles = await this.platformService.listBackupFiles();
logger.log('[BackupFilesList] Refreshed backup files:', {
logger.log("[BackupFilesList] Refreshed backup files:", {
count: this.backupFiles.length, count: this.backupFiles.length,
files: this.backupFiles.map(f => ({ files: this.backupFiles.map(f => ({
name: f.name, name: f.name,
@ -367,7 +427,6 @@ export default class BackupFilesList extends Vue {
platform: this.platformCapabilities.isIOS ? "iOS" : "Android", platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
// Debug: Log file type distribution // Debug: Log file type distribution
const typeCounts = { const typeCounts = {
contacts: this.backupFiles.filter(f => f.type === 'contacts').length, contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
@ -375,9 +434,11 @@ export default class BackupFilesList extends Vue {
other: this.backupFiles.filter(f => f.type === 'other').length, other: this.backupFiles.filter(f => f.type === 'other').length,
total: this.backupFiles.length total: this.backupFiles.length
}; };
logger.log("[BackupFilesList] File type distribution:", typeCounts); logger.log('[BackupFilesList] File type distribution:', typeCounts);
// Log the full backupFiles array for debugging the 'All' tab count
logger.log('[BackupFilesList] backupFiles array for All tab:', this.backupFiles);
} catch (error) { } catch (error) {
logger.error("[BackupFilesList] Failed to refresh backup files:", error); logger.error('[BackupFilesList] Failed to refresh backup files:', error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -393,11 +454,103 @@ export default class BackupFilesList extends Vue {
} }
/** /**
* Public method to refresh files from external components * Creates a test backup file for debugging purposes
* Used by DataExportSection to refresh after saving new files */
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
console.log("=== Directory Context Test Results ===");
console.log(debugOutput);
console.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
*/ */
public async refreshAfterSave() { async refreshAfterSave() {
logger.log("[BackupFilesList] Refreshing files after save operation"); logger.log('[BackupFilesList] refreshAfterSave called');
await this.refreshFiles(); await this.refreshFiles();
} }
@ -462,14 +615,18 @@ export default class BackupFilesList extends Vue {
/** /**
* Gets the count of files for a specific type * Gets the count of files for a specific type
* @param type - File type to count * Note: The 'All' tab count is sometimes too small. Logging for debugging.
* @returns Number of files of the specified type
*/ */
getFileCountByType(type: 'all' | 'contacts' | 'seed' | 'other'): number { getFileCountByType(type: 'all' | 'contacts' | 'seed' | 'other'): number {
let count;
if (type === 'all') { if (type === 'all') {
return this.backupFiles.length; count = this.backupFiles.length;
logger.log('[BackupFilesList] getFileCountByType (All):', count, this.backupFiles);
return count;
} }
return 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;
} }
/** /**

29
src/services/PlatformService.ts

@ -214,4 +214,33 @@ export interface PlatformService {
* @returns Promise resolving to success status * @returns Promise resolving to success status
*/ */
openBackupDirectory(): Promise<{ success: boolean; error?: string }>; openBackupDirectory(): Promise<{ success: boolean; error?: string }>;
/**
* Creates a test backup file to verify file writing and reading functionality.
* 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 }>;
/**
* Tests different directory contexts to see what files are available.
* This helps debug file visibility issues across different storage contexts.
* @returns Promise resolving to debug information about file discovery across contexts
*/
testDirectoryContexts(): Promise<string>;
/**
* Lists files and folders in a specific directory for directory browsing
* @param path - The directory path to list
* @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'}>>;
/**
* Debug method to check what's actually in the TimeSafari directory
* This helps identify if the directory exists but is empty or has permission issues
* @returns Promise resolving to debug information about the TimeSafari directory
*/
debugTimeSafariDirectory(): Promise<string>;
} }

500
src/services/platforms/CapacitorPlatformService.ts

@ -1770,15 +1770,11 @@ export class CapacitorPlatformService implements PlatformService {
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = []; const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = [];
if (this.getCapabilities().isIOS) { if (this.getCapabilities().isIOS) {
// iOS: List files in Documents directory // iOS: Documents directory
const result = await Filesystem.readdir({ const result = await Filesystem.readdir({
path: ".", path: ".",
directory: Directory.Documents, directory: Directory.Documents,
}); });
logger.log("[CapacitorPlatformService] Files in iOS Documents:", {
files: result.files.map((file) => (typeof file === "string" ? file : file.name)),
timestamp: new Date().toISOString(),
});
const files = result.files.map((file) => ({ const files = result.files.map((file) => ({
name: typeof file === "string" ? file : file.name, name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`, uri: `file://${file.uri || file}`,
@ -1787,30 +1783,32 @@ export class CapacitorPlatformService implements PlatformService {
})); }));
allFiles.push(...files); allFiles.push(...files);
} else { } else {
// Android: Search multiple locations where users might have saved files // Android: Multiple locations with recursive search
// 1. App's default locations (for backward compatibility) // 1. App's external storage directory
try {
const downloadsResult = await Filesystem.readdir({
path: "Download/TimeSafari",
directory: Directory.ExternalStorage,
});
const downloadFiles = downloadsResult.files.map((file) => ({
name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
path: "Download/TimeSafari"
}));
allFiles.push(...downloadFiles);
} catch (error) {
logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari:", error);
}
try { try {
const appStorageResult = await Filesystem.readdir({ const appStorageResult = await Filesystem.readdir({
path: "TimeSafari", path: "TimeSafari",
directory: Directory.ExternalStorage, directory: Directory.ExternalStorage,
}); });
// Log full readdir output for TimeSafari
logger.log("[CapacitorPlatformService] Android TimeSafari readdir full result:", {
path: "TimeSafari",
directory: "ExternalStorage",
files: appStorageResult.files,
fileCount: appStorageResult.files.length,
fileDetails: appStorageResult.files.map((file, index) => ({
index,
name: typeof file === "string" ? file : file.name,
type: typeof file === "string" ? "string" : "object",
hasUri: typeof file === "string" ? false : !!file.uri,
hasSize: typeof file === "string" ? false : !!file.size,
fullObject: file
})),
timestamp: new Date().toISOString(),
});
const appStorageFiles = appStorageResult.files.map((file) => ({ const appStorageFiles = appStorageResult.files.map((file) => ({
name: typeof file === "string" ? file : file.name, name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`, uri: `file://${file.uri || file}`,
@ -1822,7 +1820,7 @@ export class CapacitorPlatformService implements PlatformService {
logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error); logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error);
} }
// 2. Common user-chosen locations (if accessible) // 2. Common user-chosen locations (if accessible) with recursive search
const commonPaths = [ const commonPaths = [
"Download", "Download",
"Documents", "Documents",
@ -1830,7 +1828,7 @@ export class CapacitorPlatformService implements PlatformService {
"TimeSafari", "TimeSafari",
"Data" "Data"
]; ];
for (const path of commonPaths) { for (const path of commonPaths) {
try { try {
const result = await Filesystem.readdir({ const result = await Filesystem.readdir({
@ -1838,22 +1836,94 @@ export class CapacitorPlatformService implements PlatformService {
directory: Directory.ExternalStorage, directory: Directory.ExternalStorage,
}); });
// Filter for TimeSafari-related files // Log full readdir output for debugging
const relevantFiles = result.files logger.log(`[CapacitorPlatformService] Android ${path} readdir full result:`, {
.filter(file => { path: path,
const fileName = typeof file === "string" ? file : file.name; directory: "ExternalStorage",
const name = fileName.toLowerCase(); files: result.files,
return name.includes('timesafari') || fileCount: result.files.length,
fileDetails: result.files.map((file, index) => ({
index,
name: typeof file === "string" ? file : file.name,
type: typeof file === "string" ? "string" : "object",
hasUri: typeof file === "string" ? false : !!file.uri,
hasSize: typeof file === "string" ? false : !!file.size,
fullObject: file
})),
timestamp: new Date().toISOString(),
});
// Process each entry (file or directory)
const relevantFiles = [];
for (const file of result.files) {
const fileName = typeof file === "string" ? file : file.name;
const name = fileName.toLowerCase();
// Check if it's a directory by trying to get file stats
let isDirectory = false;
try {
const stat = await Filesystem.stat({
path: `${path}/${fileName}`,
directory: Directory.ExternalStorage
});
isDirectory = stat.type === 'directory';
} catch (statError) {
// If stat fails, assume it's a file
isDirectory = false;
}
if (isDirectory) {
// RECURSIVELY SEARCH DIRECTORY for backup files
logger.log(`[CapacitorPlatformService] Recursively searching directory: ${fileName} in ${path}`);
try {
const subDirResult = await Filesystem.readdir({
path: `${path}/${fileName}`,
directory: Directory.ExternalStorage,
});
// Process files in subdirectory
for (const subFile of subDirResult.files) {
const subFileName = typeof subFile === "string" ? subFile : subFile.name;
const subName = subFileName.toLowerCase();
// Check if subfile matches backup criteria
const matchesBackupCriteria = subName.includes('timesafari') ||
subName.includes('backup') ||
subName.includes('contacts') ||
subName.endsWith('.json');
if (matchesBackupCriteria) {
relevantFiles.push({
name: subFileName,
uri: `file://${subFile.uri || subFile}`,
size: typeof subFile === "string" ? undefined : subFile.size,
path: `${path}/${fileName}`
});
logger.log(`[CapacitorPlatformService] Found backup file in subdirectory: ${subFileName} in ${path}/${fileName}`);
}
}
} catch (subDirError) {
logger.warn(`[CapacitorPlatformService] Could not read subdirectory ${path}/${fileName}:`, subDirError);
}
} else {
// Check if file matches backup criteria
const matchesBackupCriteria = name.includes('timesafari') ||
name.includes('backup') || name.includes('backup') ||
name.includes('contacts') || name.includes('contacts') ||
name.endsWith('.json'); name.endsWith('.json');
})
.map((file) => ({ if (matchesBackupCriteria) {
name: typeof file === "string" ? file : file.name, relevantFiles.push({
uri: `file://${file.uri || file}`, name: fileName,
size: typeof file === "string" ? undefined : file.size, uri: `file://${file.uri || file}`,
path: path size: typeof file === "string" ? undefined : file.size,
})); path: path
});
} else {
logger.log(`[CapacitorPlatformService] Excluding non-backup file: ${fileName} in ${path}`);
}
}
}
if (relevantFiles.length > 0) { if (relevantFiles.length > 0) {
logger.log(`[CapacitorPlatformService] Found ${relevantFiles.length} relevant files in ${path}:`, { logger.log(`[CapacitorPlatformService] Found ${relevantFiles.length} relevant files in ${path}:`, {
@ -1889,57 +1959,325 @@ export class CapacitorPlatformService implements PlatformService {
} }
/** /**
* Lists files and folders in a given directory, with type detection. * Test method to try different directory contexts and see what files are available
* Supports folder navigation for the backup browser UI. * This helps debug file visibility issues across different storage contexts
* @param path - Directory path to list (relative to root or external storage) * @returns Promise resolving to debug information about file discovery across contexts
* @param debugShowAll - If true, forcibly treat all entries as files (for debugging)
* @returns Promise resolving to array of file/folder info
*/ */
async listFilesInDirectory(path: string = "Download/TimeSafari", debugShowAll: boolean = false): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> { async testDirectoryContexts(): Promise<string> {
try { try {
logger.log('[DEBUG] Reading directory:', path); let debugOutput = "=== Directory Context Testing ===\n\n";
const entries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = [];
const directory = this.getCapabilities().isIOS ? Directory.Documents : Directory.ExternalStorage; if (this.getCapabilities().isIOS) {
const result = await Filesystem.readdir({ path, directory }); debugOutput += "iOS Platform - Testing Documents directory:\n";
logger.log('[DEBUG] Raw readdir result:', result.files); try {
for (const entry of result.files) { const result = await Filesystem.readdir({
const name = typeof entry === 'string' ? entry : entry.name; path: ".",
const entryPath = path === '.' ? name : `${path}/${name}`; directory: Directory.Documents,
let type: 'file' | 'folder' = 'file'; });
let size: number | undefined = undefined; debugOutput += `Documents directory: ${result.files.length} files found\n`;
let uri: string = ''; result.files.forEach((file, index) => {
if (debugShowAll) { const name = typeof file === "string" ? file : file.name;
// Forcibly treat all as files for debugging debugOutput += ` ${index + 1}. ${name}\n`;
type = 'file'; });
uri = `file://${entryPath}`; } catch (error) {
logger.log('[DEBUG] Forcing file type for entry:', { entryPath, type }); debugOutput += `Documents directory error: ${error}\n`;
} else { }
} else {
debugOutput += "Android Platform - Testing multiple directory contexts:\n\n";
const contexts = [
{ name: "Data (App Private)", directory: Directory.Data, path: "." },
{ name: "Documents", directory: Directory.Documents, path: "." },
{ name: "External Storage Root", directory: Directory.ExternalStorage, path: "." },
{ name: "External Storage Downloads", directory: Directory.ExternalStorage, path: "Download" },
{ name: "External Storage TimeSafari", directory: Directory.ExternalStorage, path: "TimeSafari" },
{ name: "External Storage Download/TimeSafari", directory: Directory.ExternalStorage, path: "Download/TimeSafari" },
];
for (const context of contexts) {
debugOutput += `${context.name}:\n`;
try { try {
const stat = await Filesystem.stat({ path: entryPath, directory }); const result = await Filesystem.readdir({
if (stat.type === 'directory') { path: context.path,
type = 'folder'; directory: context.directory,
uri = ''; });
} else { debugOutput += ` Found ${result.files.length} entries\n`;
type = 'file'; result.files.forEach((file, index) => {
size = stat.size; const name = typeof file === "string" ? file : file.name;
uri = stat.uri ? stat.uri : `file://${entryPath}`; debugOutput += ` ${index + 1}. ${name}\n`;
} });
logger.log('[DEBUG] Stat for entry:', { entryPath, stat, type }); } catch (error) {
} catch (e) { debugOutput += ` Error: ${error}\n`;
// If stat fails, assume file }
type = 'file'; debugOutput += "\n";
uri = `file://${entryPath}`; }
logger.warn('[DEBUG] Stat failed for entry, assuming file:', { entryPath, error: e }); }
debugOutput += "=== Context Testing Complete ===\n";
logger.log("[CapacitorPlatformService] Directory context test results:\n" + debugOutput);
return debugOutput;
} catch (error) {
const errorMsg = `❌ Directory context testing failed: ${error}`;
logger.error("[CapacitorPlatformService] Directory context testing failed:", error);
return errorMsg;
}
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* This is useful for debugging file visibility issues.
* @returns Promise resolving to success status and file information
*/
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> {
try {
logger.log("[CapacitorPlatformService] Creating test backup file for debugging");
// Create a simple test backup file
const testData = {
type: "test_backup",
timestamp: new Date().toISOString(),
message: "This is a test backup file created for debugging file visibility issues",
contacts: [
{
did: "did:ethr:0x1234567890abcdef",
name: "Test Contact",
contactMethods: JSON.stringify([{ type: "EMAIL", value: "test@example.com" }]),
notes: "Test contact for debugging"
}
]
};
const content = JSON.stringify(testData, null, 2);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `TimeSafari-test-backup-${timestamp}.json`;
logger.log("[CapacitorPlatformService] Test backup file details:", {
fileName,
contentLength: content.length,
timestamp: new Date().toISOString(),
});
// Save the test file using the same method as regular backups
const result = await this.writeAndShareFile(fileName, content, {
allowLocationSelection: false,
saveToDownloads: true,
showShareDialog: false
});
if (result.saved) {
logger.log("[CapacitorPlatformService] Test backup file created successfully:", {
fileName,
uri: result.uri,
timestamp: new Date().toISOString(),
});
return {
success: true,
fileName,
uri: result.uri
};
} else {
throw new Error(result.error || "Failed to save test backup file");
}
} catch (error) {
const err = error as Error;
logger.error("[CapacitorPlatformService] Failed to create test backup file:", {
error: err.message,
timestamp: new Date().toISOString(),
});
return {
success: false,
error: err.message
};
}
}
/**
* Debug method to check what's actually in the TimeSafari directory
* This helps identify if the directory exists but is empty or has permission issues
* @returns Promise resolving to debug information about the TimeSafari directory
*/
async debugTimeSafariDirectory(): Promise<string> {
try {
let debugOutput = "=== TimeSafari Directory Debug ===\n\n";
if (this.getCapabilities().isIOS) {
debugOutput += "iOS: Checking Documents directory for TimeSafari files\n";
try {
const result = await Filesystem.readdir({
path: ".",
directory: Directory.Documents,
});
debugOutput += `Found ${result.files.length} files in Documents:\n`;
result.files.forEach((file, index) => {
const name = typeof file === "string" ? file : file.name;
debugOutput += ` ${index + 1}. ${name}\n`;
});
} catch (error) {
debugOutput += `Error reading Documents: ${error}\n`;
}
} else {
debugOutput += "Android: Checking multiple TimeSafari locations\n\n";
// Test 1: Check if Download/TimeSafari exists and can be read
debugOutput += "1. Testing Download/TimeSafari directory:\n";
try {
const downloadResult = await Filesystem.readdir({
path: "Download/TimeSafari",
directory: Directory.ExternalStorage,
});
debugOutput += ` ✅ Success! Found ${downloadResult.files.length} files:\n`;
downloadResult.files.forEach((file, index) => {
const name = typeof file === "string" ? file : file.name;
const size = typeof file === "string" ? "unknown" : file.size;
debugOutput += ` ${index + 1}. ${name} (${size} bytes)\n`;
});
} catch (error) {
debugOutput += ` ❌ Error: ${error}\n`;
}
// Test 2: Check if TimeSafari exists in root external storage
debugOutput += "\n2. Testing TimeSafari in root external storage:\n";
try {
const rootResult = await Filesystem.readdir({
path: "TimeSafari",
directory: Directory.ExternalStorage,
});
debugOutput += ` ✅ Success! Found ${rootResult.files.length} files:\n`;
rootResult.files.forEach((file, index) => {
const name = typeof file === "string" ? file : file.name;
const size = typeof file === "string" ? "unknown" : file.size;
debugOutput += ` ${index + 1}. ${name} (${size} bytes)\n`;
});
} catch (error) {
debugOutput += ` ❌ Error: ${error}\n`;
}
// Test 3: Check what's in the Download directory
debugOutput += "\n3. Testing Download directory contents:\n";
try {
const downloadDirResult = await Filesystem.readdir({
path: "Download",
directory: Directory.ExternalStorage,
});
debugOutput += ` ✅ Success! Found ${downloadDirResult.files.length} items in Download:\n`;
downloadDirResult.files.forEach((file, index) => {
const name = typeof file === "string" ? file : file.name;
const size = typeof file === "string" ? "unknown" : file.size;
debugOutput += ` ${index + 1}. ${name} (${size} bytes)\n`;
});
} catch (error) {
debugOutput += ` ❌ Error: ${error}\n`;
}
// Test 4: Try to create a test file in Download/TimeSafari
debugOutput += "\n4. Testing file creation in Download/TimeSafari:\n";
try {
const testResult = await this.createTestBackupFile();
if (testResult.success) {
debugOutput += ` ✅ Successfully created test file: ${testResult.fileName}\n`;
debugOutput += ` URI: ${testResult.uri}\n`;
} else {
debugOutput += ` ❌ Failed to create test file: ${testResult.error}\n`;
} }
} catch (error) {
debugOutput += ` ❌ Error creating test file: ${error}\n`;
}
}
debugOutput += "\n=== TimeSafari Directory Debug Complete ===\n";
logger.log("[CapacitorPlatformService] TimeSafari directory debug results:\n" + debugOutput);
return debugOutput;
} catch (error) {
const errorMsg = `❌ TimeSafari directory debug failed: ${error}`;
logger.error("[CapacitorPlatformService] TimeSafari directory debug failed:", error);
return errorMsg;
}
}
/**
* Lists files and folders in a specific directory for directory browsing
* @param path - The directory path to list
* @param debugShowAll - Debug flag to treat all entries as files
* @returns Promise resolving to array of directory entries
*/
async listFilesInDirectory(path: string, debugShowAll: boolean = false): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> {
try {
logger.log("[CapacitorPlatformService] Listing directory:", { path, debugShowAll });
let directory: Directory;
let actualPath: string;
if (this.getCapabilities().isIOS) {
directory = Directory.Documents;
actualPath = path === '.' ? '.' : path;
} else {
directory = Directory.ExternalStorage;
// Handle nested paths properly - use the full path as provided
actualPath = path;
}
logger.log("[CapacitorPlatformService] Attempting to read directory:", {
actualPath,
directory: directory.toString(),
platform: this.getCapabilities().isIOS ? "iOS" : "Android"
});
const result = await Filesystem.readdir({
path: actualPath,
directory: directory,
});
const entries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = [];
for (const file of result.files) {
const fileName = typeof file === "string" ? file : file.name;
// In debug mode, treat everything as a file
if (debugShowAll) {
entries.push({
name: fileName,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
path: path,
type: 'file'
});
continue;
} }
entries.push({ name, uri, size, path: entryPath, type });
// Check if it's a directory by trying to get file stats
let isDirectory = false;
try {
const stat = await Filesystem.stat({
path: `${actualPath}/${fileName}`,
directory: directory
});
isDirectory = stat.type === 'directory';
} catch (statError) {
// If stat fails, assume it's a file
isDirectory = false;
}
entries.push({
name: fileName,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
path: path,
type: isDirectory ? 'folder' : 'file'
});
} }
// Sort: folders first, then files
entries.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'folder' ? -1 : 1)); logger.log("[CapacitorPlatformService] Directory listing result:", {
logger.log('[DEBUG] Final directoryEntries:', entries); path,
entryCount: entries.length,
folders: entries.filter(e => e.type === 'folder').length,
files: entries.filter(e => e.type === 'file').length
});
return entries; return entries;
} catch (error) { } catch (error) {
logger.error('[CapacitorPlatformService] Failed to list files in directory:', { path, error }); logger.error("[CapacitorPlatformService] Failed to list directory:", { path, error });
return []; return [];
} }
} }

41
src/services/platforms/ElectronPlatformService.ts

@ -441,7 +441,7 @@ export class ElectronPlatformService implements PlatformService {
/** /**
* Lists backup files specifically saved by the app. * Lists backup files specifically saved by the app.
* Not implemented in Electron platform. * Not implemented for Electron platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
@ -467,4 +467,43 @@ export class ElectronPlatformService implements PlatformService {
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return { success: false, error: "Directory access not implemented in Electron platform" }; return { success: false, error: "Directory access not implemented in Electron platform" };
} }
/**
* Lists files and folders in a specific directory for directory browsing.
* 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'}>> {
return [];
}
/**
* Debug method to check what's actually in the TimeSafari directory.
* Not implemented for Electron platform.
* @returns Promise resolving to debug information
*/
async debugTimeSafariDirectory(): Promise<string> {
return "Electron platform does not support file system access for debugging TimeSafari directory.";
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* Not implemented for Electron platform.
* @returns Promise resolving to error status
*/
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."
};
}
/**
* Test method to try different directory contexts and see what files are available.
* Not implemented for Electron platform.
* @returns Promise resolving to debug information
*/
async testDirectoryContexts(): Promise<string> {
return "Electron platform does not support file system access for testing directory contexts.";
}
} }

41
src/services/platforms/PyWebViewPlatformService.ts

@ -156,7 +156,7 @@ export class PyWebViewPlatformService implements PlatformService {
/** /**
* Lists backup files specifically saved by the app. * Lists backup files specifically saved by the app.
* Not implemented in PyWebView platform. * Not implemented for PyWebView platform.
* @returns Promise resolving to empty array * @returns Promise resolving to empty array
*/ */
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> { async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
@ -246,4 +246,43 @@ export class PyWebViewPlatformService implements PlatformService {
async rotateCamera(): Promise<void> { async rotateCamera(): Promise<void> {
// Not implemented // Not implemented
} }
/**
* Lists files and folders in a specific directory for directory browsing.
* 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'}>> {
return [];
}
/**
* Debug method to check what's actually in the TimeSafari directory.
* Not implemented for PyWebView platform.
* @returns Promise resolving to debug information
*/
async debugTimeSafariDirectory(): Promise<string> {
return "PyWebView platform does not support file system access for debugging TimeSafari directory.";
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* Not implemented for PyWebView platform.
* @returns Promise resolving to error status
*/
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."
};
}
/**
* Test method to try different directory contexts and see what files are available.
* Not implemented for PyWebView platform.
* @returns Promise resolving to debug information
*/
async testDirectoryContexts(): Promise<string> {
return "PyWebView platform does not support file system access for testing directory contexts.";
}
} }

39
src/services/platforms/WebPlatformService.ts

@ -513,4 +513,43 @@ export class WebPlatformService implements PlatformService {
// Not supported in web platform // Not supported in web platform
return Promise.resolve(); return Promise.resolve();
} }
/**
* Lists files and folders in a specific directory for directory browsing.
* 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'}>> {
return [];
}
/**
* Debug method to check what's actually in the TimeSafari directory.
* Not supported in web platform.
* @returns Promise resolving to debug information
*/
async debugTimeSafariDirectory(): Promise<string> {
return "Web platform does not support file system access for debugging TimeSafari directory.";
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* Not supported in web platform.
* @returns Promise resolving to error status
*/
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."
};
}
/**
* Test method to try different directory contexts and see what files are available.
* Not supported in web platform.
* @returns Promise resolving to debug information
*/
async testDirectoryContexts(): Promise<string> {
return "Web platform does not support file system access for testing directory contexts.";
}
} }

Loading…
Cancel
Save