/** * ZDDC Classifier — persistence for the Classify & Copy map. * * The assignment map and target trees, plus the picked source directory * HANDLE, are stored in IndexedDB (localStorage can't hold a * FileSystemDirectoryHandle; the handle is structured-cloneable, so IndexedDB * can). On reload we re-request permission on the stored handle — a single * click re-grants access, no re-navigation. If that fails (permission denied, * or a different machine), the caller falls back to a fresh pick and the map * re-attaches by relative path. * * NOTE: a stored handle is only valid in the same browser profile on the same * machine. The map keys on source-relative paths, so re-picking the same tree * elsewhere still re-attaches — that's the warning shown to the user on save. */ (function () { 'use strict'; var DB_NAME = 'zddc-classifier'; var STORE = 'kv'; var K_STATE = 'classify-state'; var K_HANDLE = 'source-handle'; var available = typeof indexedDB !== 'undefined'; function openDB() { return new Promise(function (resolve, reject) { if (!available) { reject(new Error('IndexedDB unavailable')); return; } var req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = function () { var db = req.result; if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE); }; req.onsuccess = function () { resolve(req.result); }; req.onerror = function () { reject(req.error); }; }); } function tx(mode, fn) { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var t = db.transaction(STORE, mode); var store = t.objectStore(STORE); var out = fn(store); t.oncomplete = function () { resolve(out && out.result !== undefined ? out.result : out); }; t.onerror = function () { reject(t.error); }; t.onabort = function () { reject(t.error); }; }); }); } function put(key, value) { return tx('readwrite', function (s) { return s.put(value, key); }); } function getValue(key) { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var t = db.transaction(STORE, 'readonly'); var req = t.objectStore(STORE).get(key); req.onsuccess = function () { resolve(req.result); }; req.onerror = function () { reject(req.error); }; }); }); } // ── public API ───────────────────────────────────────────────────────── function saveState(obj) { return put(K_STATE, obj).catch(function (e) { console.warn('persist.saveState', e); }); } function loadState() { return getValue(K_STATE).catch(function () { return null; }); } function saveSourceHandle(handle) { // Real FileSystemDirectoryHandle only; the HTTP polyfill handle is not // worth persisting (server mode re-detects the root on load). if (!handle || handle.isHttp) return Promise.resolve(); return put(K_HANDLE, handle).catch(function (e) { console.warn('persist.saveHandle', e); }); } function loadSourceHandle() { return getValue(K_HANDLE).catch(function () { return null; }); } function clearAll() { return tx('readwrite', function (s) { s.delete(K_STATE); s.delete(K_HANDLE); }) .catch(function (e) { console.warn('persist.clear', e); }); } // Re-acquire read permission on a stored handle. Returns true if usable. function verifyPermission(handle) { if (!handle || typeof handle.queryPermission !== 'function') return Promise.resolve(false); var opts = { mode: 'read' }; return handle.queryPermission(opts).then(function (p) { if (p === 'granted') return true; return handle.requestPermission(opts).then(function (p2) { return p2 === 'granted'; }); }).catch(function () { return false; }); } window.app.modules.persist = { available: available, saveState: saveState, loadState: loadState, saveSourceHandle: saveSourceHandle, loadSourceHandle: loadSourceHandle, verifyPermission: verifyPermission, clearAll: clearAll, }; })();