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
This commit is contained in:
Christian Semmler 2026-01-31 15:22:21 -08:00
parent 89f30aff8c
commit a71693436c
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C

View File

@ -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<FileSystemDirectoryHandle|null>}
*/
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<FileSystemFileHandle|null>}
*/
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<boolean>}
*/
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<ArrayBuffer|null>} - 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<boolean>} - 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<boolean>} - 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<string[]>} - 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<FileSystemDirectoryHandle|null>}
*/
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<boolean>}
*/
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<ArrayBuffer|null>} - 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<boolean>} - 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<string[]>} - 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);
}