mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-03-01 06:17:38 +00:00
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:
parent
89f30aff8c
commit
a71693436c
373
src/core/opfs.js
373
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<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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user