317 lines
7.5 KiB
TypeScript
317 lines
7.5 KiB
TypeScript
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<T>(fn: () => Promise<T>, errorMsg: string): Promise<Result<T>> {
|
|
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<FileChangeListener> = 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<void> {
|
|
await this.db.init();
|
|
}
|
|
|
|
/**
|
|
* Create a new file
|
|
*/
|
|
async createFile(data: CreateFileData): Promise<Result<File>> {
|
|
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<Result<File>> {
|
|
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<Result<File[]>> {
|
|
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<Pick<File, 'title' | 'content'>>): Promise<Result<File>> {
|
|
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<Result<void>> {
|
|
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<Result<File>> {
|
|
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<string> {
|
|
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<Result<string>> {
|
|
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<Result<File[]>> {
|
|
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<Result<void>> {
|
|
const result = await wrapResult(() => this.db.clear(), 'Failed to clear files');
|
|
if (result.success) {
|
|
this.notifyChange();
|
|
}
|
|
return result;
|
|
}
|
|
}
|