diff --git a/src/app/components/FileSelector/component.tsx b/src/app/components/FileSelector/component.tsx index 8c6666f..c96b2c8 100644 --- a/src/app/components/FileSelector/component.tsx +++ b/src/app/components/FileSelector/component.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTodoContext } from '../../context/TodoContext'; +import fileService from '../../services/FileService'; import './style.css'; const FileSelector: React.FC = () => { @@ -12,7 +13,7 @@ const FileSelector: React.FC = () => { } = useTodoContext(); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); - const isFileSystemAccessSupported = 'showOpenFilePicker' in window && 'showSaveFilePicker' in window; + const isFileSystemAccessSupported = fileService.isUsingFileSystemAccess(); useEffect(() => { const timer = setTimeout(() => { @@ -23,10 +24,6 @@ const FileSelector: React.FC = () => { const handleCreateNew = async () => { try { - if (!isFileSystemAccessSupported) { - setError('Your browser does not support the required File System Access API. Please use Chrome, Edge or another compatible browser.'); - return; - } const success = await createNewFile(); if (!success) { setError('Failed to create new file or user cancelled'); @@ -41,10 +38,6 @@ const FileSelector: React.FC = () => { const handleOpenExisting = async () => { try { - if (!isFileSystemAccessSupported) { - setError('Your browser does not support the required File System Access API. Please use Chrome, Edge or another compatible browser.'); - return; - } const success = await openExistingFile(); if (!success) { setError('Failed to open file or user cancelled'); @@ -71,11 +64,21 @@ const FileSelector: React.FC = () => { } }; + const handleDownloadFile = () => { + try { + fileService.downloadCurrentFile(); + setError(null); + } catch (err) { + setError('An error occurred while downloading the file'); + console.error(err); + } + }; + return (
{!isFileSystemAccessSupported && ( -
-

Warning: This app requires the File System Access API which is not supported in your browser. Please use Chrome, Edge, or another compatible browser.

+
+

Compatibility Mode: Using traditional file operations for broader browser compatibility. Files will be downloaded to your default download folder.

)} {error &&
{error}
} @@ -91,14 +94,12 @@ const FileSelector: React.FC = () => { @@ -108,14 +109,15 @@ const FileSelector: React.FC = () => {
- ✓ Auto-saving enabled for + + {isFileSystemAccessSupported ? '✓ Auto-saving enabled for ' : '📁 Working with '} + {currentFileName}
@@ -126,6 +128,15 @@ const FileSelector: React.FC = () => { > Change current file + {!isFileSystemAccessSupported && ( + + )}
diff --git a/src/app/components/FileSelector/style.css b/src/app/components/FileSelector/style.css index 2d295c3..b7ffc7c 100644 --- a/src/app/components/FileSelector/style.css +++ b/src/app/components/FileSelector/style.css @@ -11,7 +11,8 @@ .create-file-button, .change-file-button, -.open-file-button { +.open-file-button, +.download-file-button { padding: 8px 16px; border-radius: 4px; cursor: pointer; @@ -34,9 +35,15 @@ color: var(--primary-color); } +.download-file-button { + background-color: #28a745; + color: white; +} + .create-file-button:hover, .change-file-button:hover, -.open-file-button:hover { +.open-file-button:hover, +.download-file-button:hover { opacity: 0.9; } @@ -55,24 +62,25 @@ font-size: 0.9rem; } -.browser-warning { - background-color: #fff3cd; - color: #856404; - padding: 5px; +.browser-info { + background-color: #d4edda; + color: #155724; + padding: 8px; border-radius: 4px; margin-bottom: 10px; + border: 1px solid #c3e6cb; } .error-message { background-color: #f8d7da; color: #721c24; - padding: 5px; + padding: 8px; border-radius: 4px; margin-bottom: 10px; + border: 1px solid #f5c6cb; } -.file-selected { - display: flex; - align-items: center; - justify-content: space-between; +.loading-message { + color: var(--text-color); + font-style: italic; } diff --git a/src/app/services/FileService.ts b/src/app/services/FileService.ts index 00bb702..012e09a 100644 --- a/src/app/services/FileService.ts +++ b/src/app/services/FileService.ts @@ -1,10 +1,17 @@ +import { downloadFile, readFile, createFileInput, isFileSystemAccessSupported } from '../utils/FileUtils'; + export class FileService { private static instance: FileService; private static readonly STORAGE_KEY = 'actionhub-file-handle'; private fileHandle: FileSystemFileHandle | null = null; private autoSaveEnabled = false; + private currentFileName: string = ''; + private currentFileData: any = null; + private useFileSystemAccess: boolean = false; - private constructor() {} + private constructor() { + this.useFileSystemAccess = isFileSystemAccessSupported(); + } public static getInstance(): FileService { if (!FileService.instance) { @@ -14,6 +21,8 @@ export class FileService { } private async storeFileHandle(fileHandle: FileSystemFileHandle): Promise { + if (!this.useFileSystemAccess) return; + try { console.log("Attempting to store file handle for:", fileHandle.name); const db = await this.openDatabase(); @@ -51,6 +60,8 @@ export class FileService { } private async requestPersistPermission(): Promise { + if (!this.useFileSystemAccess) return; + try { if ((navigator as any).permissions) { const permission = await (navigator as any).permissions.query({ @@ -86,6 +97,8 @@ export class FileService { } public async tryLoadSavedFile(): Promise { + if (!this.useFileSystemAccess) return false; + try { const db = await this.openDatabase(); const transaction = db.transaction(['fileHandles'], 'readonly'); @@ -101,6 +114,7 @@ export class FileService { try { await (fileHandle as any).requestPermission({ mode: 'readwrite' }); this.fileHandle = fileHandle; + this.currentFileName = fileHandle.name; this.autoSaveEnabled = true; resolve(true); } catch (err) { @@ -120,6 +134,14 @@ export class FileService { } public async createNewFile(): Promise { + if (this.useFileSystemAccess) { + return this.createNewFileModern(); + } else { + return this.createNewFileFallback(); + } + } + + private async createNewFileModern(): Promise { try { console.log("Creating new file..."); const fileHandle = await (window as any).showSaveFilePicker({ @@ -133,6 +155,7 @@ export class FileService { }); console.log("File handle created:", fileHandle.name); this.fileHandle = fileHandle; + this.currentFileName = fileHandle.name; const initialData = { todos: [], nextNumber: 1, @@ -157,7 +180,28 @@ export class FileService { } } + private async createNewFileFallback(): Promise { + console.log("Creating new file (fallback mode)..."); + this.currentFileName = 'todolist.actionhub'; + const initialData = { + todos: [], + nextNumber: 1, + availableNumbers: [] + }; + this.currentFileData = initialData; + this.setAutoSave(true); + return true; + } + public async openExistingFile(): Promise { + if (this.useFileSystemAccess) { + return this.openExistingFileModern(); + } else { + return this.openExistingFileFallback(); + } + } + + private async openExistingFileModern(): Promise { try { console.log("Opening existing file..."); const [fileHandle] = await (window as any).showOpenFilePicker({ @@ -170,6 +214,7 @@ export class FileService { }); console.log("File handle obtained:", fileHandle.name); this.fileHandle = fileHandle; + this.currentFileName = fileHandle.name; this.setAutoSave(true); try { await this.storeFileHandle(fileHandle); @@ -184,7 +229,40 @@ export class FileService { } } + private async openExistingFileFallback(): Promise { + try { + console.log("Opening existing file (fallback mode)..."); + const fileList = await createFileInput('.actionhub,application/json', false); + if (!fileList || fileList.length === 0) { + console.log("No file selected"); + return false; + } + + const file = fileList[0]; + const content = await readFile(file); + const data = JSON.parse(content); + + this.currentFileName = file.name; + this.currentFileData = data; + this.setAutoSave(true); + + console.log("File loaded successfully:", file.name); + return true; + } catch (err) { + console.error('Error opening file:', err); + return false; + } + } + public async changeFile(): Promise { + if (this.useFileSystemAccess) { + return this.changeFileModern(); + } else { + return this.openExistingFileFallback(); // Same as opening in fallback mode + } + } + + private async changeFileModern(): Promise { try { console.log("Changing file..."); const [fileHandle] = await (window as any).showOpenFilePicker({ @@ -201,6 +279,7 @@ export class FileService { } console.log("New file handle obtained:", fileHandle.name); this.fileHandle = fileHandle; + this.currentFileName = fileHandle.name; this.setAutoSave(true); try { await this.storeFileHandle(fileHandle); @@ -217,7 +296,7 @@ export class FileService { } public shouldAutoSave(): boolean { - return this.autoSaveEnabled && this.fileHandle !== null; + return this.autoSaveEnabled && (this.useFileSystemAccess ? this.fileHandle !== null : this.currentFileData !== null); } public setAutoSave(enabled: boolean): void { @@ -225,6 +304,14 @@ export class FileService { } public async saveTaskData(data: any): Promise { + if (this.useFileSystemAccess) { + return this.saveTaskDataModern(data); + } else { + return this.saveTaskDataFallback(data); + } + } + + private async saveTaskDataModern(data: any): Promise { if (!this.fileHandle) { console.warn('No file handle available for saving'); return false; @@ -240,8 +327,29 @@ export class FileService { return false; } } - + + private async saveTaskDataFallback(data: any): Promise { + try { + this.currentFileData = data; + // In fallback mode, we just store the data in memory + // The user can manually download it when needed + console.log('Data saved in memory (fallback mode)'); + return true; + } catch (err) { + console.error('Error saving data in fallback mode:', err); + return false; + } + } + public async loadTaskData(): Promise { + if (this.useFileSystemAccess) { + return this.loadTaskDataModern(); + } else { + return this.loadTaskDataFallback(); + } + } + + private async loadTaskDataModern(): Promise { if (!this.fileHandle) { console.warn('No file handle available for loading'); return null; @@ -258,13 +366,32 @@ export class FileService { return null; } } + + private async loadTaskDataFallback(): Promise { + return this.currentFileData; + } public isFileSelected(): boolean { - return this.fileHandle !== null; + return this.useFileSystemAccess ? this.fileHandle !== null : this.currentFileData !== null; } public getFileName(): string { - return this.fileHandle ? this.fileHandle.name : ''; + return this.currentFileName || ''; + } + + public isUsingFileSystemAccess(): boolean { + return this.useFileSystemAccess; + } + + public downloadCurrentFile(): void { + if (!this.currentFileData) { + console.error('No data available for download'); + return; + } + + const jsonData = JSON.stringify(this.currentFileData, null, 2); + const filename = this.currentFileName || 'todolist.actionhub'; + downloadFile(jsonData, filename); } } diff --git a/src/app/services/FileService.ts.backup b/src/app/services/FileService.ts.backup new file mode 100644 index 0000000..00bb702 --- /dev/null +++ b/src/app/services/FileService.ts.backup @@ -0,0 +1,272 @@ +export class FileService { + private static instance: FileService; + private static readonly STORAGE_KEY = 'actionhub-file-handle'; + private fileHandle: FileSystemFileHandle | null = null; + private autoSaveEnabled = false; + + private constructor() {} + + public static getInstance(): FileService { + if (!FileService.instance) { + FileService.instance = new FileService(); + } + return FileService.instance; + } + + private async storeFileHandle(fileHandle: FileSystemFileHandle): Promise { + try { + console.log("Attempting to store file handle for:", fileHandle.name); + const db = await this.openDatabase(); + return new Promise((resolve, reject) => { + try { + const transaction = db.transaction(['fileHandles'], 'readwrite'); + const store = transaction.objectStore('fileHandles'); + const request = store.put(fileHandle, 'currentFile'); + request.onsuccess = () => { + console.log("Successfully stored file handle in IndexedDB"); + this.requestPersistPermission(); + resolve(); + }; + + request.onerror = () => { + console.error("Failed to store file handle:", request.error); + reject(request.error); + }; + + transaction.oncomplete = () => { + console.log("File handle storage transaction completed"); + }; + + transaction.onerror = () => { + console.error("Transaction error while storing file handle:", transaction.error); + }; + } catch (err) { + console.error("Error in IndexedDB transaction:", err); + reject(err); + } + }); + } catch (err) { + console.error('Error storing file handle:', err); + } + } + + private async requestPersistPermission(): Promise { + try { + if ((navigator as any).permissions) { + const permission = await (navigator as any).permissions.query({ + name: 'persistent-storage' + }); + if (permission.state !== 'granted') { + console.log("Requesting persistent storage permission"); + if ((navigator as any).storage && (navigator as any).storage.persist) { + const isPersisted = await (navigator as any).storage.persist(); + console.log("Persisted storage granted:", isPersisted); + } + } else { + console.log("Persistent storage permission already granted"); + } + } + } catch (err) { + console.error("Error requesting persistence:", err); + } + } + + private async openDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open('ActionHubStorage', 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains('fileHandles')) { + db.createObjectStore('fileHandles'); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + public async tryLoadSavedFile(): Promise { + try { + const db = await this.openDatabase(); + const transaction = db.transaction(['fileHandles'], 'readonly'); + const store = transaction.objectStore('fileHandles'); + const request = store.get('currentFile'); + return new Promise((resolve) => { + request.onsuccess = async () => { + const fileHandle = request.result as FileSystemFileHandle; + if (!fileHandle) { + resolve(false); + return; + } + try { + await (fileHandle as any).requestPermission({ mode: 'readwrite' }); + this.fileHandle = fileHandle; + this.autoSaveEnabled = true; + resolve(true); + } catch (err) { + console.error('Error accessing saved file:', err); + resolve(false); + } + }; + request.onerror = () => { + console.error('Error retrieving file handle:', request.error); + resolve(false); + }; + }); + } catch (err) { + console.error('Error trying to load saved file:', err); + return false; + } + } + + public async createNewFile(): Promise { + try { + console.log("Creating new file..."); + const fileHandle = await (window as any).showSaveFilePicker({ + suggestedName: 'todolist.actionhub', + types: [{ + description: 'ActionHub Todo List', + accept: { + 'application/json': ['.actionhub'] + } + }] + }); + console.log("File handle created:", fileHandle.name); + this.fileHandle = fileHandle; + const initialData = { + todos: [], + nextNumber: 1, + availableNumbers: [] + }; + const saveResult = await this.saveTaskData(initialData); + if (!saveResult) { + console.error("Failed to save initial data"); + } + this.setAutoSave(true); + try { + await this.storeFileHandle(fileHandle); + console.log("File handle stored successfully"); + } catch (storeErr) { + console.error("Failed to store file handle:", storeErr); + } + + return true; + } catch (err) { + console.error('User cancelled file creation or error occurred:', err); + return false; + } + } + + public async openExistingFile(): Promise { + try { + console.log("Opening existing file..."); + const [fileHandle] = await (window as any).showOpenFilePicker({ + types: [{ + description: 'ActionHub Todo List', + accept: { + 'application/json': ['.actionhub'] + } + }] + }); + console.log("File handle obtained:", fileHandle.name); + this.fileHandle = fileHandle; + this.setAutoSave(true); + try { + await this.storeFileHandle(fileHandle); + console.log("File handle stored successfully"); + } catch (storeErr) { + console.error("Failed to store file handle:", storeErr); + } + return true; + } catch (err) { + console.error('User cancelled file selection or error occurred:', err); + return false; + } + } + + public async changeFile(): Promise { + try { + console.log("Changing file..."); + const [fileHandle] = await (window as any).showOpenFilePicker({ + types: [{ + description: 'ActionHub Todo List', + accept: { + 'application/json': ['.actionhub'] + } + }] + }); + if (this.fileHandle && this.fileHandle.name === fileHandle.name) { + console.log("Same file selected, no change needed"); + return true; + } + console.log("New file handle obtained:", fileHandle.name); + this.fileHandle = fileHandle; + this.setAutoSave(true); + try { + await this.storeFileHandle(fileHandle); + console.log("New file handle stored successfully"); + } catch (storeErr) { + console.error("Failed to store new file handle:", storeErr); + } + + return true; + } catch (err) { + console.error('User cancelled file change or error occurred:', err); + return false; + } + } + + public shouldAutoSave(): boolean { + return this.autoSaveEnabled && this.fileHandle !== null; + } + + public setAutoSave(enabled: boolean): void { + this.autoSaveEnabled = enabled; + } + + public async saveTaskData(data: any): Promise { + if (!this.fileHandle) { + console.warn('No file handle available for saving'); + return false; + } + try { + const writable = await (this.fileHandle as any).createWritable(); + const jsonData = JSON.stringify(data, null, 2); + await writable.write(jsonData); + await writable.close(); + return true; + } catch (err) { + console.error('Error saving data:', err); + return false; + } + } + + public async loadTaskData(): Promise { + if (!this.fileHandle) { + console.warn('No file handle available for loading'); + return null; + } + try { + const file = await this.fileHandle.getFile(); + const contents = await file.text(); + if (!contents || contents.trim() === '') { + return null; + } + return JSON.parse(contents); + } catch (err) { + console.warn('Error loading data:', err); + return null; + } + } + + public isFileSelected(): boolean { + return this.fileHandle !== null; + } + + public getFileName(): string { + return this.fileHandle ? this.fileHandle.name : ''; + } +} + +const fileService = FileService.getInstance(); +export default fileService; \ No newline at end of file diff --git a/src/app/utils/FileUtils.ts b/src/app/utils/FileUtils.ts new file mode 100644 index 0000000..bd5f274 --- /dev/null +++ b/src/app/utils/FileUtils.ts @@ -0,0 +1,56 @@ +// Utility functions for file operations that work across all browsers + +export const downloadFile = (content: string, filename: string, mimeType: string = 'application/json') => { + const blob = new Blob([content], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); +}; + +export const readFile = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target?.result) { + resolve(e.target.result as string); + } else { + reject(new Error('Failed to read file')); + } + }; + reader.onerror = () => reject(new Error('Error reading file')); + reader.readAsText(file); + }); +}; + +export const createFileInput = (accept: string, multiple: boolean = false): Promise => { + return new Promise((resolve) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = accept; + input.multiple = multiple; + input.style.display = 'none'; + + input.onchange = (e) => { + const target = e.target as HTMLInputElement; + resolve(target.files); + document.body.removeChild(input); + }; + + input.oncancel = () => { + resolve(null); + document.body.removeChild(input); + }; + + document.body.appendChild(input); + input.click(); + }); +}; + +export const isFileSystemAccessSupported = (): boolean => { + return 'showOpenFilePicker' in window && 'showSaveFilePicker' in window; +}; \ No newline at end of file