mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-03-01 14:27: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';
|
const CONFIG_FILE = 'isle.ini';
|
||||||
let toastTimeout = null;
|
let toastTimeout = null;
|
||||||
|
|
||||||
export async function getFileHandle() {
|
// ============================================================================
|
||||||
|
// Core OPFS Operations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OPFS root directory
|
||||||
|
* @returns {Promise<FileSystemDirectoryHandle|null>}
|
||||||
|
*/
|
||||||
|
export async function getOpfsRoot() {
|
||||||
try {
|
try {
|
||||||
const root = await navigator.storage.getDirectory();
|
return await navigator.storage.getDirectory();
|
||||||
return await root.getFileHandle(CONFIG_FILE, { create: true });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("OPFS not available or permission denied.", e);
|
console.error("OPFS not available or permission denied.", e);
|
||||||
return null;
|
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) {
|
export async function loadConfig(form) {
|
||||||
const handle = await getFileHandle();
|
const handle = await getFileHandle(CONFIG_FILE, true);
|
||||||
if (!handle) return null;
|
if (!handle) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -142,201 +309,5 @@ export async function saveConfig(form, getSiFiles, silent = false) {
|
|||||||
iniContent += `directives=${directives.join(",\\\n")}\n`;
|
iniContent += `directives=${directives.join(",\\\n")}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use inline Web Worker for Safari compatibility
|
return writeTextFile(CONFIG_FILE, iniContent, silent);
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user