diff --git a/src/components/BackupFilesList.vue b/src/components/BackupFilesList.vue new file mode 100644 index 00000000..d1501d5d --- /dev/null +++ b/src/components/BackupFilesList.vue @@ -0,0 +1,597 @@ +/** + * 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 + * + * ``` + */ + + + + \ No newline at end of file diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index 5ec3a43e..949a8481 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -1,7 +1,8 @@ /** * Data Export Section Component * * Provides UI and functionality for exporting user data and backing up identifier seeds. * Includes buttons for seed -backup and database export, with platform-specific download instructions. * * -@component * @displayName DataExportSection * @example * ```vue * +backup and database export, with platform-specific download instructions. * Also +displays a list of backup files with options to open them in the device's file +explorer. * * @component * @displayName DataExportSection * @example * ```vue * * ``` */ @@ -43,18 +44,21 @@ backup and database export, with platform-specific download instructions. * * v-if="platformCapabilities.isIOS" class="list-disc list-outside ml-4" > - On iOS: You will be prompted to choose a location to save your backup - file. + On iOS: Files are saved to Documents folder (accessible via Files app) and persist between app installations.
  • - On Android: You will be prompted to choose a location to save your - backup file. + On Android: Files are saved to Downloads/TimeSafari or external storage (accessible via file managers) and persist between app installations.
  • + + +
    + +
    @@ -65,20 +69,21 @@ import { AppString, NotificationIface } from "../constants/app"; import { Contact } from "../db/tables/contacts"; import * as databaseUtil from "../db/databaseUtil"; -import { logger } from "../utils/logger"; +import { logger, getTimestampForFilename } from "../utils/logger"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformService, PlatformCapabilities, } from "../services/PlatformService"; import { contactsToExportJson } from "../libs/util"; +import BackupFilesList from "./BackupFilesList.vue"; /** * @vue-component * Data Export Section Component * Handles database export and seed backup functionality with platform-specific behavior */ -@Component +@Component({ components: { BackupFilesList } }) export default class DataExportSection extends Vue { /** * Notification function injected by Vue @@ -151,7 +156,9 @@ export default class DataExportSection extends Vue { const jsonStr = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonStr], { type: "application/json" }); - const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`; + // Create timestamped filename + const timestamp = getTimestampForFilename(); + const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`; if (this.platformCapabilities.hasFileDownload) { // Web platform: Use download link @@ -188,10 +195,16 @@ export default class DataExportSection extends Vue { title: "Export Successful", text: this.platformCapabilities.hasFileDownload ? "See your downloads directory for the backup." - : "Backup saved to multiple locations. Use the share dialog to access your file and choose where to save it permanently.", + : "Backup saved to persistent storage that survives app installations. Use the share dialog to access your file and choose where to save it permanently.", }, 5000, ); + + // Refresh the backup files list + const backupFilesList = this.$refs.backupFilesList as any; + if (backupFilesList && typeof backupFilesList.refreshAfterSave === 'function') { + await backupFilesList.refreshAfterSave(); + } } catch (error) { logger.error("Export Error:", error); this.$notify( @@ -225,5 +238,15 @@ export default class DataExportSection extends Vue { hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload, }; } + + async mounted() { + // Ensure permissions are requested and refresh backup files list on mount + if (this.platformCapabilities.hasFileSystem) { + const backupFilesList = this.$refs.backupFilesList as any; + if (backupFilesList && typeof backupFilesList.refreshFiles === 'function') { + await backupFilesList.refreshFiles(); + } + } + } } diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 7ccf6e30..c611a170 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -268,7 +268,7 @@ import { } from "../constants/app"; import { retrieveSettingsForActiveAccount } from "../db/index"; import { accessToken } from "../libs/crypto"; -import { logger } from "../utils/logger"; +import { logger, getTimestampForFilename } from "../utils/logger"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import * as databaseUtil from "../db/databaseUtil"; @@ -576,7 +576,7 @@ export default class ImageMethodDialog extends Vue { (blob) => { if (blob) { this.blob = blob; - this.fileName = `photo_${Date.now()}.jpg`; + this.fileName = `photo-${getTimestampForFilename()}.jpg`; this.showRetry = true; this.stopCameraPreview(); } diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index b9b64d91..03ab39fd 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -127,7 +127,7 @@ import { import * as databaseUtil from "../db/databaseUtil"; import { retrieveSettingsForActiveAccount } from "../db/index"; import { accessToken } from "../libs/crypto"; -import { logger } from "../utils/logger"; +import { logger, getTimestampForFilename } from "../utils/logger"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; @Component({ components: { VuePictureCropper } }) @@ -393,7 +393,7 @@ export default class PhotoDialog extends Vue { (blob) => { if (blob) { this.blob = blob; - this.fileName = `photo_${Date.now()}.jpg`; + this.fileName = `photo-${getTimestampForFilename()}.jpg`; this.stopCameraPreview(); } }, diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index e91f9711..21b5deb9 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -125,6 +125,18 @@ export interface PlatformService { */ testListUserFiles(): Promise; + /** + * Tests listing backup files specifically saved by the app. + * @returns Promise resolving to a test result message + */ + testBackupFiles(): Promise; + + /** + * Tests opening the backup directory in the device's file explorer. + * @returns Promise resolving to a test result message + */ + testOpenBackupDirectory(): Promise; + // Camera operations /** * Activates the device camera to take a picture. @@ -172,4 +184,34 @@ export interface PlatformService { sql: string, params?: unknown[], ): Promise<{ changes: number; lastId?: number }>; + + /** + * Lists user-accessible files saved by the app. + * Returns files from Downloads (Android) or Documents (iOS) directories. + * @returns Promise resolving to array of file information + */ + listUserAccessibleFiles(): Promise>; + + /** + * Lists backup files specifically saved by the app. + * Filters for files that appear to be TimeSafari backups. + * @returns Promise resolving to array of backup file information + */ + listBackupFiles(): Promise>; + + /** + * Opens a file in the device's default file viewer/app. + * Uses the native share dialog to provide options for opening the file. + * @param fileUri - URI of the file to open + * @param fileName - Name of the file (for display purposes) + * @returns Promise resolving to success status + */ + openFile(fileUri: string, fileName: string): Promise<{ success: boolean; error?: string }>; + + /** + * Opens the directory containing backup files in the device's file explorer. + * Uses the native share dialog to provide options for accessing the directory. + * @returns Promise resolving to success status + */ + openBackupDirectory(): Promise<{ success: boolean; error?: string }>; } diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 2bcdb6bc..066eb2f3 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -22,7 +22,7 @@ import { PlatformService, PlatformCapabilities, } from "../PlatformService"; -import { logger } from "../../utils/logger"; +import { logger, getTimestampForFilename } from "../../utils/logger"; interface QueuedOperation { type: "run" | "query"; @@ -406,8 +406,8 @@ export class CapacitorPlatformService implements PlatformService { // Determine save strategy based on options and platform if (options.allowLocationSelection) { - // Use enhanced location selection with multiple save options - fileUri = await this.saveWithLocationOptions(fileName, content, options.mimeType, options.showLocationSelectionDialog); + // Use true user choice for file location + fileUri = await this.saveWithUserChoice(fileName, content, options.mimeType); saved = true; } else if (options.saveToPrivateStorage) { // Save to app-private storage (for sensitive data) @@ -570,7 +570,7 @@ export class CapacitorPlatformService implements PlatformService { }); // First, save the file to a temporary location - const tempFileName = `temp_${Date.now()}_${fileName}`; + const tempFileName = `temp-${getTimestampForFilename()}-${fileName}`; const tempResult = await Filesystem.writeFile({ path: tempFileName, data: content, @@ -634,7 +634,7 @@ export class CapacitorPlatformService implements PlatformService { locations.push(primaryLocation); // Save to app data directory as backup - const backupFileName = `backup_${Date.now()}_${fileName}`; + const backupFileName = `backup-${getTimestampForFilename()}-${fileName}`; const backupResult = await Filesystem.writeFile({ path: backupFileName, data: content, @@ -689,8 +689,9 @@ export class CapacitorPlatformService implements PlatformService { } /** - * Saves a file directly to the Downloads folder (Android) or Documents (iOS). - * These locations are user-accessible through file managers and the app. + * Saves a file directly to user-accessible storage that persists between installations. + * On Android: Saves to external storage (Downloads or app-specific directory) + * On iOS: Saves to Documents directory (accessible via Files app) * @param fileName - Name of the file to save * @param content - File content * @returns Promise resolving to the saved file URI @@ -698,7 +699,7 @@ export class CapacitorPlatformService implements PlatformService { private async saveToDownloads(fileName: string, content: string): Promise { try { if (this.getCapabilities().isIOS) { - // iOS: Save to Documents directory (user accessible) + // iOS: Save to Documents directory (persists between installations, accessible via Files app) const result = await Filesystem.writeFile({ path: fileName, data: content, @@ -706,40 +707,66 @@ export class CapacitorPlatformService implements PlatformService { encoding: Encoding.UTF8, }); - logger.log("[CapacitorPlatformService] File saved to iOS Documents:", { + logger.log("[CapacitorPlatformService] File saved to iOS Documents (persistent):", { uri: result.uri, fileName, + note: "File persists between app installations and is accessible via Files app", timestamp: new Date().toISOString(), }); return result.uri; } else { - // Android: Save to app's external storage (accessible via file managers) - // Due to Android 11+ restrictions, we can't directly write to public Downloads - // Users can access files through file managers or use share dialog to save to Downloads - const downloadsPath = `TimeSafari/${fileName}`; - - const result = await Filesystem.writeFile({ - path: downloadsPath, - data: content, - directory: Directory.External, // App's external storage (accessible via file managers) - encoding: Encoding.UTF8, - recursive: true, - }); - - logger.log("[CapacitorPlatformService] File saved to Android external storage:", { - uri: result.uri, - fileName, - downloadsPath, - note: "File is accessible via file managers. Use share dialog to save to Downloads.", - timestamp: new Date().toISOString(), - }); - - return result.uri; + // Android: Save to external storage that persists between installations + // Try to save to Downloads first, then fallback to app's external storage + try { + // Attempt to save to Downloads directory (most accessible) + const downloadsPath = `Download/TimeSafari/${fileName}`; + + const result = await Filesystem.writeFile({ + path: downloadsPath, + data: content, + directory: Directory.ExternalStorage, // External storage (persists between installations) + encoding: Encoding.UTF8, + recursive: true, + }); + + logger.log("[CapacitorPlatformService] File saved to Android Downloads (persistent):", { + uri: result.uri, + fileName, + downloadsPath, + note: "File persists between app installations and is accessible via file managers", + timestamp: new Date().toISOString(), + }); + + return result.uri; + } catch (downloadsError) { + logger.warn("[CapacitorPlatformService] Could not save to Downloads, using app external storage:", downloadsError); + + // Fallback: Save to app's external storage directory + const appStoragePath = `TimeSafari/${fileName}`; + + const result = await Filesystem.writeFile({ + path: appStoragePath, + data: content, + directory: Directory.ExternalStorage, // External storage (persists between installations) + encoding: Encoding.UTF8, + recursive: true, + }); + + logger.log("[CapacitorPlatformService] File saved to Android app external storage (persistent):", { + uri: result.uri, + fileName, + appStoragePath, + note: "File persists between app installations and is accessible via file managers", + timestamp: new Date().toISOString(), + }); + + return result.uri; + } } } catch (error) { - logger.error("[CapacitorPlatformService] Save to downloads failed:", error); - throw new Error(`Failed to save to downloads: ${error}`); + logger.error("[CapacitorPlatformService] Save to persistent storage failed:", error); + throw new Error(`Failed to save to persistent storage: ${error}`); } } @@ -772,41 +799,78 @@ export class CapacitorPlatformService implements PlatformService { } /** - * Lists user-accessible files saved by the app. - * Returns files from Downloads (Android) or Documents (iOS) directories. + * Lists user-accessible files saved by the app from persistent storage locations. + * Returns files from persistent storage that survive app installations. * @returns Promise resolving to array of file information */ async listUserAccessibleFiles(): Promise> { try { if (this.getCapabilities().isIOS) { - // iOS: List files in Documents directory + // iOS: List files in Documents directory (persistent, accessible via Files app) const result = await Filesystem.readdir({ path: ".", 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(), + }); return result.files.map((file) => ({ name: typeof file === "string" ? file : file.name, uri: `file://${file.uri || file}`, size: typeof file === "string" ? undefined : file.size, })); } else { - // Android: List files in app's external storage (TimeSafari subdirectory) + // Android: List files from persistent storage locations + const allFiles: Array<{name: string, uri: string, size?: number}> = []; + // Try to list files from Downloads/TimeSafari directory try { - const result = await Filesystem.readdir({ - path: "TimeSafari", - directory: Directory.External, + const downloadsResult = await Filesystem.readdir({ + path: "Download/TimeSafari", + directory: Directory.ExternalStorage, }); - - return result.files.map((file) => ({ + logger.log("[CapacitorPlatformService] Files in Downloads/TimeSafari:", { + files: downloadsResult.files.map((file) => (typeof file === "string" ? file : file.name)), + timestamp: new Date().toISOString(), + }); + 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, })); + allFiles.push(...downloadFiles); } catch (downloadsError) { - logger.warn("[CapacitorPlatformService] Could not read external storage directory:", downloadsError); - return []; + logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari directory:", downloadsError); + } + // Try to list files from app's external storage directory + try { + const appStorageResult = await Filesystem.readdir({ + path: "TimeSafari", + directory: Directory.ExternalStorage, + }); + logger.log("[CapacitorPlatformService] Files in TimeSafari (external storage):", { + files: appStorageResult.files.map((file) => (typeof file === "string" ? file : file.name)), + timestamp: new Date().toISOString(), + }); + const appStorageFiles = appStorageResult.files.map((file) => ({ + name: typeof file === "string" ? file : file.name, + uri: `file://${file.uri || file}`, + size: typeof file === "string" ? undefined : file.size, + })); + allFiles.push(...appStorageFiles); + } catch (appStorageError) { + logger.warn("[CapacitorPlatformService] Could not read app external storage directory:", appStorageError); } + // Remove duplicates based on filename + const uniqueFiles = allFiles.filter((file, index, self) => + index === self.findIndex(f => f.name === file.name) + ); + logger.log("[CapacitorPlatformService] Total unique files found in persistent storage:", { + total: uniqueFiles.length, + files: uniqueFiles.map(f => f.name), + timestamp: new Date().toISOString(), + }); + return uniqueFiles; } } catch (error) { logger.error("[CapacitorPlatformService] Failed to list user accessible files:", error); @@ -814,6 +878,49 @@ export class CapacitorPlatformService implements PlatformService { } } + /** + * Test method: List all files in backup directories for debugging + */ + async testListAllBackupFiles(): Promise { + try { + let output = ''; + if (this.getCapabilities().isIOS) { + const result = await Filesystem.readdir({ + path: ".", + directory: Directory.Documents, + }); + output += `iOS Documents files:\n`; + output += result.files.map((file) => (typeof file === "string" ? file : file.name)).join("\n"); + } else { + output += `Android Downloads/TimeSafari files:\n`; + try { + const downloadsResult = await Filesystem.readdir({ + path: "Download/TimeSafari", + directory: Directory.ExternalStorage, + }); + output += downloadsResult.files.map((file) => (typeof file === "string" ? file : file.name)).join("\n"); + } catch (e) { + output += "(Could not read Downloads/TimeSafari)\n"; + } + output += `\nAndroid TimeSafari (external storage) files:\n`; + try { + const appStorageResult = await Filesystem.readdir({ + path: "TimeSafari", + directory: Directory.ExternalStorage, + }); + output += appStorageResult.files.map((file) => (typeof file === "string" ? file : file.name)).join("\n"); + } catch (e) { + output += "(Could not read TimeSafari external storage)\n"; + } + } + logger.log("[CapacitorPlatformService] testListAllBackupFiles output:\n" + output); + return output; + } catch (error) { + logger.error("[CapacitorPlatformService] testListAllBackupFiles error:", error); + return `Error: ${error}`; + } + } + /** * Tests the file sharing functionality by creating and sharing a test file. * @returns Promise resolving to a test result message @@ -827,7 +934,7 @@ export class CapacitorPlatformService implements PlatformService { test: true }; - const fileName = `timesafari-test-${Date.now()}.json`; + const fileName = `timesafari-test-${getTimestampForFilename()}.json`; const content = JSON.stringify(testContent, null, 2); const result = await this.writeAndShareFile(fileName, content, { @@ -859,7 +966,7 @@ export class CapacitorPlatformService implements PlatformService { saveOnly: true }; - const fileName = `timesafari-save-only-${Date.now()}.json`; + const fileName = `timesafari-save-only-${getTimestampForFilename()}.json`; const content = JSON.stringify(testContent, null, 2); const result = await this.writeAndShareFile(fileName, content, { @@ -892,7 +999,7 @@ export class CapacitorPlatformService implements PlatformService { locationSelection: true }; - const fileName = `timesafari-location-test-${Date.now()}.json`; + const fileName = `timesafari-location-test-${getTimestampForFilename()}.json`; const content = JSON.stringify(testContent, null, 2); // Use the FilePicker to let user choose where to save the file @@ -920,7 +1027,7 @@ export class CapacitorPlatformService implements PlatformService { silent: true }; - const fileName = `timesafari-silent-location-test-${Date.now()}.json`; + const fileName = `timesafari-silent-location-test-${getTimestampForFilename()}.json`; const content = JSON.stringify(testContent, null, 2); const result = await this.writeAndShareFile(fileName, content, { @@ -959,7 +1066,7 @@ export class CapacitorPlatformService implements PlatformService { const blob = await this.processImageData(image.base64String); return { blob, - fileName: `photo_${Date.now()}.${image.format || "jpg"}`, + fileName: `photo-${getTimestampForFilename()}.${image.format || "jpg"}`, }; } catch (error) { logger.error("Error taking picture with Capacitor:", error); @@ -985,7 +1092,7 @@ export class CapacitorPlatformService implements PlatformService { const blob = await this.processImageData(image.base64String); return { blob, - fileName: `photo_${Date.now()}.${image.format || "jpg"}`, + fileName: `photo-${getTimestampForFilename()}.${image.format || "jpg"}`, }; } catch (error) { logger.error("Error picking image with Capacitor:", error); @@ -1120,6 +1227,39 @@ export class CapacitorPlatformService implements PlatformService { } } + /** + * Saves a file with true user choice of location using FilePicker + * This gives users real control over where their files are saved + * @param fileName - Name of the file to save + * @param content - File content + * @param mimeType - MIME type of the file + * @returns Promise resolving to the saved file URI + */ + private async saveWithUserChoice(fileName: string, content: string, mimeType: string = "application/json"): Promise { + try { + logger.log("[CapacitorPlatformService] Providing true user choice for file location:", { + fileName, + mimeType, + platform: this.getCapabilities().isIOS ? "iOS" : "Android", + timestamp: new Date().toISOString(), + }); + + if (this.getCapabilities().isIOS) { + // iOS: Use the native share dialog which includes "Save to Files" options + // This allows users to choose where to save using the iOS Files app + return await this.saveFileWithPicker(fileName, content, mimeType); + } else { + // Android: Use FilePicker to let user choose the exact directory + return await this.saveFileWithLocationPicker(fileName, content, mimeType); + } + } catch (error) { + logger.error("[CapacitorPlatformService] User choice save failed, falling back to default location:", error); + + // Fallback to default location if user choice fails + return await this.saveToDownloads(fileName, content); + } + } + /** * Saves a file using the FilePicker to let user choose the save location. * This provides true location selection rather than using a share dialog. @@ -1219,4 +1359,588 @@ export class CapacitorPlatformService implements PlatformService { return `❌ Failed to list user files: ${err.message}`; } } + + /** + * Tests listing backup files specifically saved by the app. + * @returns Promise resolving to a test result message + */ + async testBackupFiles(): Promise { + try { + const files = await this.listBackupFiles(); + + if (files.length === 0) { + return `📁 No backup files found. Try creating some backups first using the export functions.`; + } + + const fileList = files.map(file => { + const pathInfo = file.path && !this.getCapabilities().isIOS ? ` (📁 ${file.path})` : ''; + return `- ${file.name} (${file.type}) (${file.size ? `${file.size} bytes` : 'size unknown'})${pathInfo}`; + }).join('\n'); + + return `📁 Found ${files.length} backup file(s):\n${fileList}`; + } catch (error) { + const err = error as Error; + return `❌ Failed to list backup files: ${err.message}`; + } + } + + /** + * Tests opening the backup directory in the device's file explorer. + * @returns Promise resolving to a test result message + */ + async testOpenBackupDirectory(): Promise { + try { + const result = await this.openBackupDirectory(); + + if (result.success) { + return `✅ Backup directory access dialog opened successfully. Use the share options to access your backup files.`; + } else { + return `❌ Failed to open backup directory: ${result.error}`; + } + } catch (error) { + const err = error as Error; + return `❌ Failed to open backup directory: ${err.message}`; + } + } + + /** + * Lists backup files specifically saved by the app. + * Filters for files that appear to be TimeSafari backups. + * @returns Promise resolving to array of backup file information + */ + async listBackupFiles(): Promise> { + try { + // Use enhanced file discovery to find files regardless of where users saved them + const allFiles = await this.listUserAccessibleFilesEnhanced(); + + logger.log("[CapacitorPlatformService] All user accessible files found (enhanced):", { + total: allFiles.length, + files: allFiles.map(f => ({ name: f.name, path: f.path, size: f.size })), + platform: this.getCapabilities().isIOS ? "iOS" : "Android", + timestamp: new Date().toISOString(), + }); + + // Show ALL JSON files and any files with backup-related keywords + const backupFiles = allFiles + .filter(file => { + const name = file.name.toLowerCase(); + // Exclude directory-access notification files + if (name.startsWith('timesafari-directory-access-') && name.endsWith('.txt')) { + logger.log("[CapacitorPlatformService] Excluding directory access file:", file.name); + return false; + } + + // Check if file matches any 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'); + + const isBackupFile = isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData; + + if (!isBackupFile) { + logger.log("[CapacitorPlatformService] Excluding file (no backup keywords):", { + name: file.name, + path: file.path, + isJson, + hasTimeSafari, + hasBackup, + hasContacts, + hasSeed, + hasExport, + hasData + }); + } + + return isBackupFile; + }) + .map(file => { + const name = file.name.toLowerCase(); + let type: 'contacts' | 'seed' | 'other' = 'other'; + + // Categorize files based on content + 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')) { + // All JSON files are considered backup files + type = 'other'; + } + + return { + ...file, + type + }; + }); + + logger.log("[CapacitorPlatformService] Found backup files (enhanced discovery):", { + total: backupFiles.length, + files: backupFiles.map(f => ({ + name: f.name, + type: f.type, + path: f.path + })), + platform: this.getCapabilities().isIOS ? "iOS" : "Android", + timestamp: new Date().toISOString(), + }); + + return backupFiles; + } catch (error) { + logger.error("[CapacitorPlatformService] Failed to list backup files:", error); + return []; + } + } + + /** + * Opens the directory containing backup files in the device's file explorer. + * Uses the native share dialog to provide options for accessing the directory. + * @returns Promise resolving to success status + */ + async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { + try { + logger.log("[CapacitorPlatformService] User requested to open backup directory:", { + platform: this.getCapabilities().isIOS ? "iOS" : "Android", + timestamp: new Date().toISOString(), + }); + // Instead of creating a .txt file, just notify the user + if (this.getCapabilities().isIOS) { + // iOS: Instruct user to open Files app > Documents > TimeSafari + alert("To access your backups, open the Files app and navigate to the Documents folder. Look for the TimeSafari folder or files."); + } else { + // Android: Instruct user to open Downloads/TimeSafari or TimeSafari in their file manager + alert("To access your backups, open your file manager and navigate to Downloads/TimeSafari or TimeSafari in external storage."); + } + return { success: true }; + } catch (error) { + const err = error as Error; + logger.error("[CapacitorPlatformService] Failed to open backup directory:", { + error: err.message, + timestamp: new Date().toISOString(), + }); + return { success: false, error: err.message }; + } + } + + /** + * Opens a file in the device's default file viewer/app. + * Uses the native share dialog to provide options for opening the file. + * @param fileUri - URI of the file to open + * @param fileName - Name of the file (for display purposes) + * @returns Promise resolving to success status + */ + async openFile(fileUri: string, fileName: string): Promise<{ success: boolean; error?: string }> { + try { + logger.log("[CapacitorPlatformService] Opening file:", { + uri: fileUri, + fileName, + timestamp: new Date().toISOString(), + }); + + // Use the share dialog to provide options for opening the file + await this.handleShareDialog({ + title: "Open TimeSafari File", + text: `Open ${fileName} with your preferred app`, + url: fileUri, + dialogTitle: "Choose how to open your file", + }); + + logger.log("[CapacitorPlatformService] File open dialog completed successfully"); + return { success: true }; + } catch (error) { + const err = error as Error; + logger.error("[CapacitorPlatformService] Failed to open file:", { + error: err.message, + uri: fileUri, + fileName, + timestamp: new Date().toISOString(), + }); + + // Check if it's a user cancellation + if (err.message.includes("cancel") || + err.message.includes("dismiss") || + err.message.includes("timeout")) { + logger.log("[CapacitorPlatformService] User cancelled file open dialog"); + return { success: true }; // Don't treat cancellation as error + } + + return { success: false, error: err.message }; + } + } + + /** + * Debug method: List all files without filtering for troubleshooting + * @returns Promise resolving to all files found in user accessible locations + */ + async debugListAllFiles(): Promise> { + try { + const allFiles = await this.listUserAccessibleFiles(); + + logger.log("[CapacitorPlatformService] DEBUG: All files found (no filtering):", { + total: allFiles.length, + files: allFiles.map(f => ({ + name: f.name, + size: f.size, + uri: f.uri + })), + platform: this.getCapabilities().isIOS ? "iOS" : "Android", + timestamp: new Date().toISOString(), + }); + + // Add path information for Android + const filesWithPath = allFiles.map(file => { + let path: string | undefined; + if (!this.getCapabilities().isIOS) { + try { + const uri = file.uri; + if (uri.startsWith('file://')) { + const decodedPath = decodeURIComponent(uri.substring(7)); + if (decodedPath.includes('Download/TimeSafari')) { + const timeSafariIndex = decodedPath.indexOf('Download/TimeSafari'); + path = decodedPath.substring(timeSafariIndex); + } else if (decodedPath.includes('TimeSafari')) { + const timeSafariIndex = decodedPath.indexOf('TimeSafari'); + path = decodedPath.substring(timeSafariIndex); + } else if (decodedPath.includes('Download')) { + const downloadIndex = decodedPath.indexOf('Download'); + path = decodedPath.substring(downloadIndex); + } else if (decodedPath.includes('Android')) { + const androidIndex = decodedPath.indexOf('Android'); + path = decodedPath.substring(androidIndex); + } else { + path = `./${file.name}`; + } + } else { + path = `./${file.name}`; + } + } catch (pathError) { + path = `./${file.name}`; + } + } + + return { + ...file, + path + }; + }); + + return filesWithPath; + } catch (error) { + logger.error("[CapacitorPlatformService] DEBUG: Failed to list all files:", error); + return []; + } + } + + /** + * Comprehensive debug method to test file discovery step by step + * @returns Promise resolving to detailed debug information + */ + async debugFileDiscoveryStepByStep(): Promise { + try { + let debugOutput = ''; + debugOutput += `=== TimeSafari File Discovery Debug ===\n`; + debugOutput += `Platform: ${this.getCapabilities().isIOS ? "iOS" : "Android"}\n`; + debugOutput += `Timestamp: ${new Date().toISOString()}\n\n`; + + // Step 1: Test basic file system access + debugOutput += `1. Testing basic file system access...\n`; + try { + await this.checkStoragePermissions(); + debugOutput += `✅ Storage permissions OK\n`; + } catch (error) { + debugOutput += `❌ Storage permissions failed: ${error}\n`; + return debugOutput; + } + + // Step 2: List all user accessible files + debugOutput += `\n2. Listing all user accessible files...\n`; + const allFiles = await this.listUserAccessibleFiles(); + debugOutput += `Found ${allFiles.length} total files:\n`; + allFiles.forEach((file, index) => { + debugOutput += ` ${index + 1}. ${file.name} (${file.size || 'unknown'} bytes)\n`; + }); + + // Step 3: Test backup file filtering + debugOutput += `\n3. Testing backup file filtering...\n`; + const backupFiles = await this.listBackupFiles(); + debugOutput += `Found ${backupFiles.length} backup files:\n`; + backupFiles.forEach((file, index) => { + debugOutput += ` ${index + 1}. ${file.name} (${file.type}) (${file.size || 'unknown'} bytes)\n`; + }); + + // Step 4: Test individual directory access + debugOutput += `\n4. Testing individual directory access...\n`; + + if (this.getCapabilities().isIOS) { + try { + const iosResult = await Filesystem.readdir({ + path: ".", + directory: Directory.Documents, + }); + debugOutput += `iOS Documents directory: ${iosResult.files.length} files\n`; + iosResult.files.forEach((file, index) => { + const fileName = typeof file === "string" ? file : file.name; + debugOutput += ` ${index + 1}. ${fileName}\n`; + }); + } catch (error) { + debugOutput += `❌ iOS Documents directory access failed: ${error}\n`; + } + } else { + // Android: Test both directories + try { + const downloadsResult = await Filesystem.readdir({ + path: "Download/TimeSafari", + directory: Directory.ExternalStorage, + }); + debugOutput += `Android Downloads/TimeSafari: ${downloadsResult.files.length} files\n`; + downloadsResult.files.forEach((file, index) => { + const fileName = typeof file === "string" ? file : file.name; + debugOutput += ` ${index + 1}. ${fileName}\n`; + }); + } catch (error) { + debugOutput += `❌ Android Downloads/TimeSafari access failed: ${error}\n`; + } + + try { + const appStorageResult = await Filesystem.readdir({ + path: "TimeSafari", + directory: Directory.ExternalStorage, + }); + debugOutput += `Android TimeSafari (external storage): ${appStorageResult.files.length} files\n`; + appStorageResult.files.forEach((file, index) => { + const fileName = typeof file === "string" ? file : file.name; + debugOutput += ` ${index + 1}. ${fileName}\n`; + }); + } catch (error) { + debugOutput += `❌ Android TimeSafari external storage access failed: ${error}\n`; + } + } + + // Step 5: Test file filtering criteria + debugOutput += `\n5. Testing file filtering criteria...\n`; + allFiles.forEach((file, index) => { + const name = file.name.toLowerCase(); + 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'); + const isExcluded = name.startsWith('timesafari-directory-access-') && name.endsWith('.txt'); + + const isBackupFile = isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData; + const shouldInclude = isBackupFile && !isExcluded; + + debugOutput += ` ${index + 1}. ${file.name}:\n`; + debugOutput += ` - isJson: ${isJson}\n`; + debugOutput += ` - hasTimeSafari: ${hasTimeSafari}\n`; + debugOutput += ` - hasBackup: ${hasBackup}\n`; + debugOutput += ` - hasContacts: ${hasContacts}\n`; + debugOutput += ` - hasSeed: ${hasSeed}\n`; + debugOutput += ` - hasExport: ${hasExport}\n`; + debugOutput += ` - hasData: ${hasData}\n`; + debugOutput += ` - isExcluded: ${isExcluded}\n`; + debugOutput += ` - isBackupFile: ${isBackupFile}\n`; + debugOutput += ` - shouldInclude: ${shouldInclude}\n`; + debugOutput += ` - actuallyIncluded: ${backupFiles.some(bf => bf.name === file.name)}\n`; + }); + + debugOutput += `\n=== Debug Complete ===\n`; + + logger.log("[CapacitorPlatformService] File discovery debug output:\n" + debugOutput); + return debugOutput; + } catch (error) { + const errorMsg = `❌ Debug failed: ${error}`; + logger.error("[CapacitorPlatformService] File discovery debug failed:", error); + return errorMsg; + } + } + + /** + * Enhanced file discovery that searches multiple user-accessible locations + * This helps find files regardless of where users chose to save them + * @returns Promise resolving to array of file information + */ + async listUserAccessibleFilesEnhanced(): Promise> { + try { + const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = []; + + if (this.getCapabilities().isIOS) { + // iOS: List files in Documents directory + const result = await Filesystem.readdir({ + path: ".", + 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) => ({ + 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: Search multiple locations where users might have saved files + + // 1. App's default locations (for backward compatibility) + 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 { + const appStorageResult = await Filesystem.readdir({ + path: "TimeSafari", + directory: Directory.ExternalStorage, + }); + const appStorageFiles = appStorageResult.files.map((file) => ({ + name: typeof file === "string" ? file : file.name, + uri: `file://${file.uri || file}`, + size: typeof file === "string" ? undefined : file.size, + path: "TimeSafari" + })); + allFiles.push(...appStorageFiles); + } catch (error) { + logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error); + } + + // 2. Common user-chosen locations (if accessible) + 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 + })); + + if (relevantFiles.length > 0) { + logger.log(`[CapacitorPlatformService] Found ${relevantFiles.length} relevant files in ${path}:`, { + files: relevantFiles.map(f => f.name), + timestamp: new Date().toISOString(), + }); + allFiles.push(...relevantFiles); + } + } catch (error) { + // Silently skip inaccessible directories + logger.debug(`[CapacitorPlatformService] Could not access ${path}:`, error); + } + } + } + + // Remove duplicates based on filename and path + const uniqueFiles = allFiles.filter((file, index, self) => + index === self.findIndex(f => f.name === file.name && f.path === file.path) + ); + + logger.log("[CapacitorPlatformService] Enhanced file discovery results:", { + total: uniqueFiles.length, + files: uniqueFiles.map(f => ({ name: f.name, path: f.path })), + platform: this.getCapabilities().isIOS ? "iOS" : "Android", + timestamp: new Date().toISOString(), + }); + + return uniqueFiles; + } catch (error) { + logger.error("[CapacitorPlatformService] Enhanced file discovery failed:", error); + return []; + } + } + + /** + * Lists files and folders in a given directory, with type detection. + * Supports folder navigation for the backup browser UI. + * @param path - Directory path to list (relative to root or external storage) + * @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> { + try { + logger.log('[DEBUG] Reading directory:', path); + const entries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = []; + const directory = this.getCapabilities().isIOS ? Directory.Documents : Directory.ExternalStorage; + const result = await Filesystem.readdir({ path, directory }); + logger.log('[DEBUG] Raw readdir result:', result.files); + for (const entry of result.files) { + const name = typeof entry === 'string' ? entry : entry.name; + const entryPath = path === '.' ? name : `${path}/${name}`; + let type: 'file' | 'folder' = 'file'; + let size: number | undefined = undefined; + let uri: string = ''; + if (debugShowAll) { + // Forcibly treat all as files for debugging + type = 'file'; + uri = `file://${entryPath}`; + logger.log('[DEBUG] Forcing file type for entry:', { entryPath, type }); + } else { + try { + const stat = await Filesystem.stat({ path: entryPath, directory }); + if (stat.type === 'directory') { + type = 'folder'; + uri = ''; + } else { + type = 'file'; + size = stat.size; + uri = stat.uri ? stat.uri : `file://${entryPath}`; + } + logger.log('[DEBUG] Stat for entry:', { entryPath, stat, type }); + } catch (e) { + // If stat fails, assume file + type = 'file'; + uri = `file://${entryPath}`; + logger.warn('[DEBUG] Stat failed for entry, assuming file:', { entryPath, error: e }); + } + } + entries.push({ name, uri, size, path: entryPath, type }); + } + // 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('[DEBUG] Final directoryEntries:', entries); + return entries; + } catch (error) { + logger.error('[CapacitorPlatformService] Failed to list files in directory:', { path, error }); + return []; + } + } } diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 7ae9d737..0db6f57a 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -413,4 +413,58 @@ export class ElectronPlatformService implements PlatformService { async testListUserFiles(): Promise { return "File listing not available in Electron platform - not implemented"; } + + /** + * Tests listing backup files specifically saved by the app. + * @returns Promise resolving to a test result message + */ + async testBackupFiles(): Promise { + return "Backup file listing not available in Electron platform - not implemented"; + } + + /** + * Tests opening the backup directory in the device's file explorer. + * @returns Promise resolving to a test result message + */ + async testOpenBackupDirectory(): Promise { + return "Directory access not available in Electron platform - not implemented"; + } + + /** + * Lists user-accessible files saved by the app. + * Not implemented in Electron platform. + * @returns Promise resolving to empty array + */ + async listUserAccessibleFiles(): Promise> { + return []; + } + + /** + * Lists backup files specifically saved by the app. + * Not implemented in Electron platform. + * @returns Promise resolving to empty array + */ + async listBackupFiles(): Promise> { + return []; + } + + /** + * Opens a file in the device's default file viewer/app. + * Not implemented in Electron platform. + * @param _fileUri - URI of the file to open + * @param _fileName - Name of the file (for display purposes) + * @returns Promise resolving to error status + */ + async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> { + return { success: false, error: "File opening not implemented in Electron platform" }; + } + + /** + * Opens the directory containing backup files in the device's file explorer. + * Not implemented in Electron platform. + * @returns Promise resolving to error status + */ + async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { + return { success: false, error: "Directory access not implemented in Electron platform" }; + } } diff --git a/src/services/platforms/PyWebViewPlatformService.ts b/src/services/platforms/PyWebViewPlatformService.ts index 5caf7b3a..ef27c3f4 100644 --- a/src/services/platforms/PyWebViewPlatformService.ts +++ b/src/services/platforms/PyWebViewPlatformService.ts @@ -32,6 +32,7 @@ export class PyWebViewPlatformService implements PlatformService { isIOS: false, hasFileDownload: false, // Not implemented yet needsFileHandlingInstructions: false, + isNativeApp: true, }; } @@ -122,14 +123,127 @@ export class PyWebViewPlatformService implements PlatformService { } /** - * Should write and share a file using the Python backend. - * @param _fileName - Name of the file to write and share - * @param _content - Content to write to the file - * @throws Error with "Not implemented" message - * @todo Implement file writing and sharing through pywebview's Python-JavaScript bridge + * Writes content to a file at the specified path and shares it. + * Not implemented in PyWebView platform. + * @param _fileName - The filename of the file to write + * @param _content - The content to write to the file + * @param _options - Optional parameters for file saving behavior + * @returns Promise that resolves to save/share result */ - async writeAndShareFile(_fileName: string, _content: string): Promise { - logger.error("writeAndShareFile not implemented in PyWebView platform"); - throw new Error("Not implemented"); + async writeAndShareFile( + _fileName: string, + _content: string, + _options?: { + allowLocationSelection?: boolean; + saveToDownloads?: boolean; + saveToPrivateStorage?: boolean; + mimeType?: string; + showShareDialog?: boolean; + showLocationSelectionDialog?: boolean; + } + ): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> { + return { saved: false, shared: false, error: "File sharing not implemented in PyWebView platform" }; + } + + /** + * Lists user-accessible files saved by the app. + * Not implemented in PyWebView platform. + * @returns Promise resolving to empty array + */ + async listUserAccessibleFiles(): Promise> { + return []; + } + + /** + * Lists backup files specifically saved by the app. + * Not implemented in PyWebView platform. + * @returns Promise resolving to empty array + */ + async listBackupFiles(): Promise> { + return []; + } + + /** + * Opens a file in the device's default file viewer/app. + * Not implemented in PyWebView platform. + * @param _fileUri - URI of the file to open + * @param _fileName - Name of the file (for display purposes) + * @returns Promise resolving to error status + */ + async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> { + return { success: false, error: "File opening not implemented in PyWebView platform" }; + } + + /** + * Opens the directory containing backup files in the device's file explorer. + * Not implemented in PyWebView platform. + * @returns Promise resolving to error status + */ + async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { + return { success: false, error: "Directory access not implemented in PyWebView platform" }; + } + + /** + * Tests listing user-accessible files saved by the app. + * @returns Promise resolving to a test result message + */ + async testListUserFiles(): Promise { + return "File listing not available in PyWebView platform - not implemented"; + } + + /** + * Tests listing backup files specifically saved by the app. + * @returns Promise resolving to a test result message + */ + async testBackupFiles(): Promise { + return "Backup file listing not available in PyWebView platform - not implemented"; + } + + /** + * Tests opening the backup directory in the device's file explorer. + * @returns Promise resolving to a test result message + */ + async testOpenBackupDirectory(): Promise { + return "Directory access not available in PyWebView platform - not implemented"; + } + + /** + * Tests the file sharing functionality. + * @returns Promise resolving to a test result message + */ + async testFileSharing(): Promise { + return "File sharing not available in PyWebView platform - not implemented"; + } + + /** + * Tests saving a file without showing the share dialog. + * @returns Promise resolving to a test result message + */ + async testFileSaveOnly(): Promise { + return "File saving not available in PyWebView platform - not implemented"; + } + + /** + * Tests the location selection functionality. + * @returns Promise resolving to a test result message + */ + async testLocationSelection(): Promise { + return "Location selection not available in PyWebView platform - not implemented"; + } + + /** + * Tests location selection without showing the dialog. + * @returns Promise resolving to a test result message + */ + async testLocationSelectionSilent(): Promise { + return "Silent location selection not available in PyWebView platform - not implemented"; + } + + /** + * Rotates the camera between front and back. + * Not implemented in PyWebView platform. + */ + async rotateCamera(): Promise { + // Not implemented } } diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 01a52d78..bec2e6cb 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -3,7 +3,7 @@ import { PlatformService, PlatformCapabilities, } from "../PlatformService"; -import { logger } from "../../utils/logger"; +import { logger, getTimestampForFilename } from "../../utils/logger"; import { QueryExecResult } from "@/interfaces/database"; import databaseService from "../AbsurdSqlDatabaseService"; @@ -193,7 +193,7 @@ export class WebPlatformService implements PlatformService { if (blob) { resolve({ blob, - fileName: `photo_${Date.now()}.jpg`, + fileName: `photo-${getTimestampForFilename()}.jpg`, }); } else { reject(new Error("Failed to capture image from webcam")); @@ -450,6 +450,60 @@ export class WebPlatformService implements PlatformService { return "File listing not available in web platform - files are downloaded directly"; } + /** + * Tests listing backup files specifically saved by the app. + * @returns Promise resolving to a test result message + */ + async testBackupFiles(): Promise { + return "Backup file listing not available in web platform - files are downloaded directly"; + } + + /** + * Tests opening the backup directory in the device's file explorer. + * @returns Promise resolving to a test result message + */ + async testOpenBackupDirectory(): Promise { + return "Directory access not available in web platform - files are downloaded directly"; + } + + /** + * Lists user-accessible files saved by the app. + * Not supported in web platform. + * @returns Promise resolving to empty array + */ + async listUserAccessibleFiles(): Promise> { + return []; + } + + /** + * Lists backup files specifically saved by the app. + * Not supported in web platform. + * @returns Promise resolving to empty array + */ + async listBackupFiles(): Promise> { + return []; + } + + /** + * Opens a file in the device's default file viewer/app. + * Not supported in web platform. + * @param _fileUri - URI of the file to open + * @param _fileName - Name of the file (for display purposes) + * @returns Promise resolving to error status + */ + async openFile(_fileUri: string, _fileName: string): Promise<{ success: boolean; error?: string }> { + return { success: false, error: "File opening not available in web platform" }; + } + + /** + * Opens the directory containing backup files in the device's file explorer. + * Not supported in web platform. + * @returns Promise resolving to error status + */ + async openBackupDirectory(): Promise<{ success: boolean; error?: string }> { + return { success: false, error: "Directory access not available in web platform" }; + } + /** * Rotates the camera between front and back cameras. * Not supported in web platform. diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2cbb228b..0e7014c0 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -79,3 +79,22 @@ if (typeof module !== "undefined" && module.exports) { // Add default export for ESM export default { logger }; + +/** + * Formats current timestamp for use in filenames. + * Returns ISO string with colons and periods replaced with hyphens, truncated to seconds. + * Format: 2024-01-15T14-30-45 + * @returns Formatted timestamp string safe for filenames + */ +export function getTimestampForFilename(): string { + return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); +} + +/** + * Formats current timestamp for use in filenames with date only. + * Format: 2024-01-15 + * @returns Date-only timestamp string safe for filenames + */ +export function getDateForFilename(): string { + return new Date().toISOString().slice(0, 10); +} diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 1675ac83..2e72295e 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -249,6 +249,24 @@ > List User Files + + +
    Result: {{ fileSharingResult }}
    @@ -763,5 +781,69 @@ export default class Help extends Vue { ); } } + + public async testBackupFiles() { + const platformService = PlatformServiceFactory.getInstance(); + try { + const result = await platformService.testBackupFiles(); + this.fileSharingResult = result; + logger.log("Backup Files Test Result:", this.fileSharingResult); + } catch (error) { + logger.error("Backup Files Test Error:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Backup Files Error", + text: error instanceof Error ? error.message : String(error), + }, + 5000, + ); + } + } + + public async testOpenBackupDirectory() { + const platformService = PlatformServiceFactory.getInstance(); + try { + const result = await platformService.testOpenBackupDirectory(); + this.fileSharingResult = result; + logger.log("Open Backup Directory Test Result:", this.fileSharingResult); + } catch (error) { + logger.error("Open Backup Directory Test Error:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Open Backup Directory Error", + text: error instanceof Error ? error.message : String(error), + }, + 5000, + ); + } + } + + public async testFileDiscoveryDebug() { + const platformService = PlatformServiceFactory.getInstance(); + try { + if ('debugFileDiscoveryStepByStep' in platformService) { + const result = await (platformService as any).debugFileDiscoveryStepByStep(); + this.fileSharingResult = result; + logger.log("File Discovery Debug Test Result:", this.fileSharingResult); + } else { + this.fileSharingResult = "Debug method not available on this platform"; + } + } catch (error) { + logger.error("File Discovery Debug Test Error:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "File Discovery Debug Error", + text: error instanceof Error ? error.message : String(error), + }, + 5000, + ); + } + } }