import type { File, CreateFileData, Result } from './types'; import type { FileDatabase } from './db'; import { compressFiles, decompressFiles, filesToShareUrl, filesFromShareUrl } from './compression'; import { loadSystemFiles, isSystemFileId, getSystemFile } from './system-files'; async function wrapResult(fn: () => Promise, errorMsg: string): Promise> { try { const data = await fn(); return { success: true, data }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(errorMsg), }; } } /** * Generate a unique ID */ function generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } type FileChangeListener = () => void; /** * File Manager - Main API for managing files */ export class FileManager { private db: FileDatabase; private changeListeners: Set = new Set(); constructor(db: FileDatabase) { this.db = db; } /** * Subscribe to file changes */ onFilesChanged(listener: FileChangeListener): () => void { this.changeListeners.add(listener); return () => this.changeListeners.delete(listener); } /** * Notify all listeners that files have changed */ private notifyChange(): void { this.changeListeners.forEach(listener => listener()); } /** * Initialize the file manager (initializes database) */ async init(): Promise { await this.db.init(); } /** * Create a new file */ async createFile(data: CreateFileData): Promise> { try { const file: File = { id: generateId(), title: data.title, content: data.content || '', }; await this.db.put(file); this.notifyChange(); return { success: true, data: file }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to create file'), }; } } /** * Get a file by ID (checks both user and system files) */ async getFile(id: string): Promise> { try { if (isSystemFileId(id)) { const systemFile = getSystemFile(id); if (systemFile) { return { success: true, data: systemFile }; } } const file = await this.db.get(id); if (!file) { return { success: false, error: new Error(`File not found: ${id}`), }; } return { success: true, data: file }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to get file'), }; } } /** * Get all files (user files + system files) */ async getAllFiles(): Promise> { try { const userFiles = await this.db.getAll(); const systemFiles = await loadSystemFiles(); return { success: true, data: [...systemFiles, ...userFiles], }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to get files'), }; } } /** * Update a file (system files cannot be updated directly) */ async updateFile(id: string, updates: Partial>): Promise> { try { if (isSystemFileId(id)) { return { success: false, error: new Error('Cannot update system files directly. Use "Save As" to create a copy.'), }; } const existingFile = await this.db.get(id); if (!existingFile) { return { success: false, error: new Error(`File not found: ${id}`), }; } const updatedFile: File = { ...existingFile, ...(updates.title !== undefined && { title: updates.title }), ...(updates.content !== undefined && { content: updates.content }), }; await this.db.put(updatedFile); this.notifyChange(); return { success: true, data: updatedFile }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to update file'), }; } } /** * Delete a file (system files cannot be deleted) */ async deleteFile(id: string): Promise> { if (isSystemFileId(id)) { return { success: false, error: new Error('Cannot delete system files'), }; } const result = await wrapResult(() => this.db.delete(id), 'Failed to delete file'); if (result.success) { this.notifyChange(); } return result; } /** * Duplicate a file (works with both user and system files) */ async duplicateFile(id: string): Promise> { try { const result = await this.getFile(id); if (!result.success) { return result; } const originalFile = result.data; const duplicatedFile: File = { id: generateId(), title: `${originalFile.title} (copy)`, content: originalFile.content, }; await this.db.put(duplicatedFile); this.notifyChange(); return { success: true, data: duplicatedFile }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to duplicate file'), }; } } /** * Generate unique untitled filename */ async generateUntitledFilename(): Promise { const result = await this.getAllFiles(); if (!result.success) { return 'Untitled-1.orc'; } const files = result.data; const untitledPattern = /^Untitled-(\d+)\.orc$/; let maxNum = 0; files.forEach(file => { const match = file.title.match(untitledPattern); if (match) { const num = parseInt(match[1], 10); if (num > maxNum) { maxNum = num; } } }); return `Untitled-${maxNum + 1}.orc`; } /** * Export files to a shareable URL */ async exportFilesToUrl(fileIds: string[], baseUrl?: string): Promise> { try { const files: File[] = []; for (const id of fileIds) { const file = await this.db.get(id); if (file) { files.push(file); } } if (files.length === 0) { return { success: false, error: new Error('No files to export'), }; } const url = filesToShareUrl(files, baseUrl); return { success: true, data: url }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to export files'), }; } } /** * Import files from a URL */ async importFilesFromUrl(url: string): Promise> { try { const files = filesFromShareUrl(url); // Generate new IDs for imported files const importedFiles = files.map(file => ({ ...file, id: generateId(), })); for (const file of importedFiles) { await this.db.put(file); } this.notifyChange(); return { success: true, data: importedFiles }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to import files'), }; } } /** * Clear all files (use with caution!) */ async clearAllFiles(): Promise> { const result = await wrapResult(() => this.db.clear(), 'Failed to clear files'); if (result.success) { this.notifyChange(); } return result; } }