From a71693436c2d4c015342eb277edb7f8fd8b8e53f Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sat, 31 Jan 2026 15:22:21 -0800 Subject: [PATCH] Refactor opfs.js to reduce code duplication - Consolidate getFileHandle to use getOpfsRoot internally - Add writeTextFile helper that uses writeBinaryFile - Extract showToast helper for toast notifications - Simplify saveConfig to use writeTextFile instead of duplicate worker - Simplify fileExists and readBinaryFile to use getFileHandle --- src/core/opfs.js | 373 ++++++++++++++++++++++------------------------- 1 file changed, 172 insertions(+), 201 deletions(-) diff --git a/src/core/opfs.js b/src/core/opfs.js index fc7c4b6..b3d3979 100644 --- a/src/core/opfs.js +++ b/src/core/opfs.js @@ -4,18 +4,185 @@ import { configToastVisible, configToastMessage } from '../stores.js'; const CONFIG_FILE = 'isle.ini'; let toastTimeout = null; -export async function getFileHandle() { +// ============================================================================ +// Core OPFS Operations +// ============================================================================ + +/** + * Get OPFS root directory + * @returns {Promise} + */ +export async function getOpfsRoot() { try { - const root = await navigator.storage.getDirectory(); - return await root.getFileHandle(CONFIG_FILE, { create: true }); + return await navigator.storage.getDirectory(); } catch (e) { console.error("OPFS not available or permission denied.", e); return null; } } +/** + * Get a file handle from OPFS + * @param {string} filename - Name of the file + * @param {boolean} create - Whether to create the file if it doesn't exist + * @returns {Promise} + */ +export async function getFileHandle(filename = CONFIG_FILE, create = true) { + try { + const root = await getOpfsRoot(); + if (!root) return null; + return await root.getFileHandle(filename, { create }); + } catch (e) { + if (e.name === 'NotFoundError') return null; + console.error("Failed to get file handle:", e); + return null; + } +} + +/** + * Check if a file exists in OPFS + * @param {string} filename - Name of the file + * @returns {Promise} + */ +export async function fileExists(filename) { + const handle = await getFileHandle(filename, false); + return handle !== null; +} + +/** + * Read a binary file from OPFS + * @param {string} filename - Name of the file + * @returns {Promise} - File contents or null if not found + */ +export async function readBinaryFile(filename) { + try { + const handle = await getFileHandle(filename, false); + if (!handle) return null; + + const file = await handle.getFile(); + return await file.arrayBuffer(); + } catch (e) { + console.error('Failed to read binary file:', e); + return null; + } +} + +/** + * Write a binary file to OPFS using Web Worker pattern (Safari-compatible) + * @param {string} filename - Name of the file + * @param {ArrayBuffer|Uint8Array} data - Binary data to write + * @param {boolean} silent - If true, don't show toast notification + * @param {string} toastMsg - Custom toast message (default: 'Settings saved') + * @returns {Promise} - True if successful + */ +export async function writeBinaryFile(filename, data, silent = false, toastMsg = 'Settings saved') { + const workerCode = ` + self.onmessage = async (e) => { + try { + const root = await navigator.storage.getDirectory(); + const handle = await root.getFileHandle(e.data.filename, { create: true }); + const accessHandle = await handle.createSyncAccessHandle(); + const bytes = new Uint8Array(e.data.buffer); + + accessHandle.truncate(0); + accessHandle.write(bytes, { at: 0 }); + accessHandle.flush(); + accessHandle.close(); + + self.postMessage({ status: 'success', message: 'File saved: ' + e.data.filename }); + } catch (err) { + self.postMessage({ status: 'error', message: 'Failed to save file: ' + err.message }); + } + }; + `; + + const blob = new Blob([workerCode], { type: 'application/javascript' }); + const workerUrl = URL.createObjectURL(blob); + const worker = new Worker(workerUrl); + + return new Promise((resolve) => { + worker.postMessage({ filename, buffer: data }); + + worker.onmessage = (e) => { + console.log(e.data.message); + URL.revokeObjectURL(workerUrl); + worker.terminate(); + + if (e.data.status === 'success') { + if (!silent) { + showToast(toastMsg); + } + resolve(true); + } else { + resolve(false); + } + }; + + worker.onerror = (e) => { + console.error('An error occurred in the file-saving worker:', e.message); + URL.revokeObjectURL(workerUrl); + worker.terminate(); + resolve(false); + }; + }); +} + +/** + * Write a text file to OPFS + * @param {string} filename - Name of the file + * @param {string} content - Text content to write + * @param {boolean} silent - If true, don't show toast notification + * @param {string} toastMsg - Custom toast message + * @returns {Promise} - True if successful + */ +export async function writeTextFile(filename, content, silent = false, toastMsg = 'Settings saved') { + const encoder = new TextEncoder(); + const data = encoder.encode(content); + return writeBinaryFile(filename, data.buffer, silent, toastMsg); +} + +/** + * List all files in OPFS matching a pattern + * @param {RegExp} pattern - Regular expression to match filenames + * @returns {Promise} - Array of matching filenames + */ +export async function listFiles(pattern) { + try { + const root = await getOpfsRoot(); + if (!root) return []; + + const files = []; + for await (const entry of root.values()) { + if (entry.kind === 'file' && pattern.test(entry.name)) { + files.push(entry.name); + } + } + return files; + } catch (e) { + console.error('Failed to list files:', e); + return []; + } +} + +/** + * Show a toast notification + * @param {string} message - Message to display + */ +function showToast(message) { + if (toastTimeout) { + clearTimeout(toastTimeout); + } + configToastMessage.set(message); + configToastVisible.set(true); + toastTimeout = setTimeout(() => configToastVisible.set(false), 2000); +} + +// ============================================================================ +// Config File Operations +// ============================================================================ + export async function loadConfig(form) { - const handle = await getFileHandle(); + const handle = await getFileHandle(CONFIG_FILE, true); if (!handle) return null; try { @@ -142,201 +309,5 @@ export async function saveConfig(form, getSiFiles, silent = false) { iniContent += `directives=${directives.join(",\\\n")}\n`; } - // Use inline Web Worker for Safari compatibility - const workerCode = ` - self.onmessage = async (e) => { - if (e.data.action === 'save') { - try { - const root = await navigator.storage.getDirectory(); - const handle = await root.getFileHandle(e.data.filePath, { create: true }); - const accessHandle = await handle.createSyncAccessHandle(); - const encoder = new TextEncoder(); - const encodedData = encoder.encode(e.data.content); - - accessHandle.truncate(0); - accessHandle.write(encodedData, { at: 0 }); - accessHandle.flush(); - accessHandle.close(); - - self.postMessage({ status: 'success', message: 'Config saved to ' + e.data.filePath }); - } catch (err) { - self.postMessage({ status: 'error', message: 'Failed to save config: ' + err.message }); - } - } - }; - `; - - const blob = new Blob([workerCode], { type: 'application/javascript' }); - const workerUrl = URL.createObjectURL(blob); - const worker = new Worker(workerUrl); - - worker.postMessage({ - action: 'save', - content: iniContent, - filePath: CONFIG_FILE - }); - - worker.onmessage = (e) => { - console.log(e.data.message); - URL.revokeObjectURL(workerUrl); - worker.terminate(); - if (e.data.status === 'success' && !silent) { - if (toastTimeout) { - clearTimeout(toastTimeout); - } - configToastVisible.set(true); - toastTimeout = setTimeout(() => configToastVisible.set(false), 2000); - } - }; - - worker.onerror = (e) => { - console.error('An error occurred in the config-saving worker:', e.message); - URL.revokeObjectURL(workerUrl); - worker.terminate(); - }; -} - -// ============================================================================ -// Binary File Operations for Save Game Editor -// ============================================================================ - -/** - * Get OPFS root directory - * @returns {Promise} - */ -export async function getOpfsRoot() { - try { - return await navigator.storage.getDirectory(); - } catch (e) { - console.error("OPFS not available or permission denied.", e); - return null; - } -} - -/** - * Check if a file exists in OPFS - * @param {string} filename - Name of the file - * @returns {Promise} - */ -export async function fileExists(filename) { - try { - const root = await getOpfsRoot(); - if (!root) return false; - await root.getFileHandle(filename, { create: false }); - return true; - } catch (e) { - if (e.name === 'NotFoundError') return false; - console.error('Error checking file existence:', e); - return false; - } -} - -/** - * Read a binary file from OPFS - * @param {string} filename - Name of the file - * @returns {Promise} - File contents or null if not found - */ -export async function readBinaryFile(filename) { - try { - const root = await getOpfsRoot(); - if (!root) return null; - - const handle = await root.getFileHandle(filename, { create: false }); - const file = await handle.getFile(); - return await file.arrayBuffer(); - } catch (e) { - if (e.name === 'NotFoundError') { - return null; - } - console.error('Failed to read binary file:', e); - return null; - } -} - -/** - * Write a binary file to OPFS using Web Worker pattern (Safari-compatible) - * @param {string} filename - Name of the file - * @param {ArrayBuffer} data - Binary data to write - * @param {boolean} silent - If true, don't show toast notification - * @param {string} toastMsg - Custom toast message (default: 'Settings saved') - * @returns {Promise} - True if successful - */ -export async function writeBinaryFile(filename, data, silent = false, toastMsg = 'Settings saved') { - const workerCode = ` - self.onmessage = async (e) => { - try { - const root = await navigator.storage.getDirectory(); - const handle = await root.getFileHandle(e.data.filename, { create: true }); - const accessHandle = await handle.createSyncAccessHandle(); - const bytes = new Uint8Array(e.data.buffer); - - accessHandle.truncate(0); - accessHandle.write(bytes, { at: 0 }); - accessHandle.flush(); - accessHandle.close(); - - self.postMessage({ status: 'success', message: 'File saved: ' + e.data.filename }); - } catch (err) { - self.postMessage({ status: 'error', message: 'Failed to save file: ' + err.message }); - } - }; - `; - - const blob = new Blob([workerCode], { type: 'application/javascript' }); - const workerUrl = URL.createObjectURL(blob); - const worker = new Worker(workerUrl); - - return new Promise((resolve) => { - worker.postMessage({ filename, buffer: data }); - - worker.onmessage = (e) => { - console.log(e.data.message); - URL.revokeObjectURL(workerUrl); - worker.terminate(); - - if (e.data.status === 'success') { - if (!silent) { - if (toastTimeout) { - clearTimeout(toastTimeout); - } - configToastMessage.set(toastMsg); - configToastVisible.set(true); - toastTimeout = setTimeout(() => configToastVisible.set(false), 2000); - } - resolve(true); - } else { - resolve(false); - } - }; - - worker.onerror = (e) => { - console.error('An error occurred in the binary-saving worker:', e.message); - URL.revokeObjectURL(workerUrl); - worker.terminate(); - resolve(false); - }; - }); -} - -/** - * List all files in OPFS matching a pattern - * @param {RegExp} pattern - Regular expression to match filenames - * @returns {Promise} - Array of matching filenames - */ -export async function listFiles(pattern) { - try { - const root = await getOpfsRoot(); - if (!root) return []; - - const files = []; - for await (const entry of root.values()) { - if (entry.kind === 'file' && pattern.test(entry.name)) { - files.push(entry.name); - } - } - return files; - } catch (e) { - console.error('Failed to list files:', e); - return []; - } + return writeTextFile(CONFIG_FILE, iniContent, silent); }