mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-03 09:13:56 +08:00
164 lines
6.5 KiB
JavaScript
164 lines
6.5 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
'require fs-fit as fit';
|
|
|
|
/* The live appliers every page needs (dark mode, layout, auto-collapse, rail) and the localStorage
|
|
* wrappers under them. The other axes, the snapshot and the resets live in fs-axes.js. */
|
|
|
|
/* A browser can refuse storage (blocked cookies, a partitioned WebView, a full quota) and then
|
|
* reads or writes throw: every access is wrapped, and an unreadable preference is a default, not
|
|
* an error. */
|
|
const _mem = new Map();
|
|
let _lsBroken = false;
|
|
function storageBroken() { return _lsBroken; }
|
|
function lsGet(k) { if (_mem.has(k)) return _mem.get(k); try { return localStorage.getItem(k); } catch (e) { _lsBroken = true; return null; } }
|
|
function lsSet(k, v) { try { localStorage.setItem(k, v); _mem.delete(k); } catch (e) { _lsBroken = true; _mem.set(k, String(v)); } }
|
|
function lsDel(k) { _mem.delete(k); try { localStorage.removeItem(k); } catch (e) { _lsBroken = true; } }
|
|
|
|
/* A stored JSON array, or [] — the shape of the two remembered lists (the search palette's recent
|
|
* paths, the menu's open sections). */
|
|
function lsGetArr(k) {
|
|
try {
|
|
const a = JSON.parse(lsGet(k) || '[]');
|
|
return Array.isArray(a) ? a : [];
|
|
} catch (e) { return []; }
|
|
}
|
|
|
|
const RECENT_KEY = 'fs-recent';
|
|
const RECENT_MAX = 8;
|
|
function recentPaths() {
|
|
return lsGetArr(RECENT_KEY).filter((x) => typeof x === 'string').slice(0, RECENT_MAX);
|
|
}
|
|
|
|
/* the router-wide defaults the server stamped (head.ut), read at runtime so current*() reports the
|
|
* effective default when this browser has no localStorage value */
|
|
function sd(k) { return (window.__fsSD || {})[k]; }
|
|
|
|
function setSD(field, val) { (window.__fsSD = window.__fsSD || {})[field] = val; }
|
|
|
|
function modeDefault() {
|
|
const d = sd('darkmode');
|
|
return (d === 'dark' || d === 'light') ? d : 'auto';
|
|
}
|
|
function currentMode() {
|
|
const s = lsGet('fs-darkmode');
|
|
if (s === 'true') return 'dark';
|
|
if (s === 'false') return 'light';
|
|
if (s === null) return modeDefault();
|
|
return 'auto';
|
|
}
|
|
/* Dark mode is announced in three dialects because apps sniff for it: `data-theme="dark"`
|
|
* (luci-app-justclash keys 21 rules off it), Bootstrap's `data-bs-theme` and `data-darkmode`. */
|
|
function stampDark(root, dark) {
|
|
root.setAttribute('data-darkmode', dark ? 'true' : 'false');
|
|
root.setAttribute('data-theme', dark ? 'dark' : 'light');
|
|
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
|
|
}
|
|
|
|
const _mqDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
function intendedDark() {
|
|
const m = currentMode();
|
|
return m === 'dark' || (m === 'auto' && _mqDark.matches);
|
|
}
|
|
|
|
function applyMode(val) {
|
|
const root = document.documentElement;
|
|
/* 'auto' is stored explicitly, so it overrides a router default of dark/light */
|
|
if (val === 'auto') lsSet('fs-darkmode', 'auto');
|
|
else lsSet('fs-darkmode', val === 'dark' ? 'true' : 'false');
|
|
stampDark(root, intendedDark());
|
|
}
|
|
|
|
/* Third parties write the dialects too: luci-app-openclash stamps `data-darkmode="true"` onto
|
|
* :root from seven templates, so the guard re-asserts the stamp. */
|
|
function guardDarkStamp() {
|
|
const root = document.documentElement;
|
|
const check = () => {
|
|
const dark = intendedDark();
|
|
if (root.getAttribute('data-darkmode') === (dark ? 'true' : 'false') &&
|
|
root.getAttribute('data-theme') === (dark ? 'dark' : 'light') &&
|
|
root.getAttribute('data-bs-theme') === (dark ? 'dark' : 'light')) return;
|
|
stampDark(root, dark);
|
|
};
|
|
check();
|
|
new MutationObserver(check).observe(root, {
|
|
attributes: true,
|
|
attributeFilter: [ 'data-darkmode', 'data-theme', 'data-bs-theme' ]
|
|
});
|
|
}
|
|
|
|
/* `<meta name="theme-color">` colours a mobile address bar and an installed PWA's title bar;
|
|
* head.ut ships the default palette's page colour and this tracks the live one. */
|
|
function paintThemeColor() {
|
|
const meta = document.querySelector('meta[name="theme-color"]');
|
|
if (!meta || !document.body) return;
|
|
const bg = getComputedStyle(document.body).backgroundColor;
|
|
/* a transparent body means the sheet has not applied yet; keep the server's value */
|
|
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') meta.setAttribute('content', bg);
|
|
}
|
|
|
|
function watchThemeColor() {
|
|
paintThemeColor();
|
|
new MutationObserver(fit.frame(paintThemeColor)).observe(document.documentElement,
|
|
{ attributes: true, attributeFilter: [ 'style', 'class', 'data-darkmode', 'data-palette', 'data-wallpaper' ] });
|
|
}
|
|
|
|
function currentLayout() {
|
|
return document.documentElement.getAttribute('data-layout') === 'top' ? 'top' : 'sidebar';
|
|
}
|
|
function isTopLayout() {
|
|
return currentLayout() === 'top';
|
|
}
|
|
function applyLayout(val) {
|
|
const layout = (val === 'top') ? 'top' : 'sidebar';
|
|
/* always an explicit value, never a removed attribute: every layout rule matches data-layout
|
|
* positively */
|
|
lsSet('fs-layout', layout);
|
|
document.documentElement.setAttribute('data-layout', layout);
|
|
/* the bar and the column leave the menu different room: re-take the fits-on-one-row
|
|
* measurement */
|
|
fit.schedule();
|
|
}
|
|
|
|
/* Sidebar accordion: auto-collapse on = one section open at a time; off (default) they stack. Only
|
|
* meaningful for the expanded sidebar. */
|
|
function autoCollapseDefault() {
|
|
return sd('autocollapse') === 'on';
|
|
}
|
|
function currentAutoCollapse() {
|
|
const s = lsGet('fs-menu-autocollapse');
|
|
if (s === 'true') return true;
|
|
if (s === 'false') return false;
|
|
if (s === null) return autoCollapseDefault();
|
|
return false;
|
|
}
|
|
function applyAutoCollapse(val) {
|
|
const on = (val === 'on');
|
|
lsSet('fs-menu-autocollapse', on ? 'true' : 'false');
|
|
|
|
/* Switching it on with several sections unfolded leaves the menu in a state the setting says is
|
|
* impossible, so the open ones are folded. */
|
|
document.dispatchEvent(new CustomEvent('fs-autocollapse', { detail: { on } }));
|
|
}
|
|
|
|
/* The sidebar rail's collapsed flag, flipped by fs-chrome.js: a transient chrome collapse, not an
|
|
* axis, so snapshotAxes() and resetToSaved() skip it. */
|
|
function applyRail(on) {
|
|
const root = document.documentElement;
|
|
if (on) { root.setAttribute('data-rail', 'true'); lsSet('fs-rail', 'true'); }
|
|
else { root.removeAttribute('data-rail'); lsDel('fs-rail'); }
|
|
}
|
|
function currentRail() {
|
|
return document.documentElement.getAttribute('data-rail') === 'true';
|
|
}
|
|
|
|
return baseclass.extend({
|
|
/* the storage wrappers and the router-default reader: fs-axes.js is built on these */
|
|
lsGet, lsSet, lsDel, lsGetArr, RECENT_KEY, RECENT_MAX, recentPaths, storageBroken, sd, setSD,
|
|
currentMode, applyMode, modeDefault, guardDarkStamp, watchThemeColor,
|
|
currentLayout, applyLayout, isTopLayout,
|
|
currentAutoCollapse, applyAutoCollapse, autoCollapseDefault,
|
|
currentRail, applyRail
|
|
});
|