// loader.js — fetches directory entries for either source mode. // // Server mode: GET with Accept: application/json. zddc-server // (and Caddy's built-in browse, which we mirror) returns an array of // FileInfo {name, size, url, mod_time, mode, is_dir, is_symlink}. // // FS-API mode: enumerate a FileSystemDirectoryHandle's children. No // network involved; works on local folders the user picked. (function () { 'use strict'; var state = window.app.state; function splitExt(name) { var i = name.lastIndexOf('.'); if (i <= 0 || i === name.length - 1) return ''; return name.substring(i + 1).toLowerCase(); } // Build a raw entry from the server's FileInfo shape. function fromServerEntry(e) { // Server returns directory names with a trailing "/". Strip // it for display; the is_dir flag is the canonical signal. var displayName = e.is_dir ? e.name.replace(/\/$/, '') : e.name; return { name: displayName, isDir: e.is_dir, size: e.size || 0, modTime: e.mod_time ? new Date(e.mod_time) : null, ext: e.is_dir ? '' : splitExt(displayName), url: e.url || null, // FS-API specific (null in server mode): handle: null }; } // Build a raw entry from a FileSystemHandle. async function fromHandle(handle) { var name = handle.name; var isDir = handle.kind === 'directory'; var size = 0; var modTime = null; if (!isDir) { try { var f = await handle.getFile(); size = f.size; modTime = new Date(f.lastModified); } catch (_e) { // permission lost; leave size/modTime defaults } } return { name: name, isDir: isDir, size: size, modTime: modTime, ext: isDir ? '' : splitExt(name), url: null, handle: handle }; } // Fetch children of a directory in server mode. // path must end with '/' so the request hits the directory route. async function fetchServerChildren(path) { if (!path.endsWith('/')) path += '/'; var resp = await fetch(path, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }); if (!resp.ok) { throw new Error('HTTP ' + resp.status + ' fetching ' + path); } var data = await resp.json(); if (!Array.isArray(data)) { throw new Error('Unexpected response shape from ' + path); } return data.map(fromServerEntry); } // Enumerate a FileSystemDirectoryHandle's immediate children. async function fetchFsChildren(dirHandle) { var entries = []; for await (var [_name, handle] of dirHandle.entries()) { entries.push(await fromHandle(handle)); } return entries; } // Probe whether THIS page is being served by zddc-server (or any // server that responds to JSON listing requests). If so, switch to // server mode automatically and load the current directory. async function autoDetectServerMode() { // Only attempt when running over http(s) and the location's // path looks like a directory. Probing on file:// is pointless. if (location.protocol !== 'http:' && location.protocol !== 'https:') { return false; } // Strip any /.html from the path to get the directory. var path = location.pathname; // If the URL points at the browse.html itself, the directory // is the parent. If it's a directory ending in '/', use it. var dirPath; if (path.endsWith('/')) { dirPath = path; } else { // e.g. '/some/dir/browse.html' → '/some/dir/' var slash = path.lastIndexOf('/'); dirPath = slash >= 0 ? path.substring(0, slash + 1) : '/'; } try { var entries = await fetchServerChildren(dirPath); state.source = 'server'; state.currentPath = dirPath; return { entries: entries, path: dirPath }; } catch (_e) { // Not a server-backed page (e.g. opened via file://). return null; } } // CDN library loader. Idempotent — multiple callers share the // same in-flight Promise. Used by ZIP expansion + the file // preview popup. var libCache = new Map(); function loadScript(url) { if (libCache.has(url)) return libCache.get(url); var p = new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = url; s.onload = function () { resolve(); }; s.onerror = function () { reject(new Error('Failed to load: ' + url)); }; document.head.appendChild(s); }); libCache.set(url, p); return p; } function ensureJSZip() { if (window.JSZip) return Promise.resolve(); return loadScript('https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js'); } // Public API window.app.modules.loader = { fetchServerChildren: fetchServerChildren, fetchFsChildren: fetchFsChildren, autoDetectServerMode: autoDetectServerMode, splitExt: splitExt, ensureJSZip: ensureJSZip, loadScript: loadScript }; })();