mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 01:25:13 +08:00
✨ Sync 2026-08-13 21:00:03
This commit is contained in:
1 parent
c57dcedc98
commit
29133c66a8
48 files changed
+1339
-1967
No files matched your search
@@ -684,9 +684,9 @@ function takeReturn() {
|
||||
return true;
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
const MARK = 'fs-ap';
|
||||
const MARK = 'fs-ap'; /* the built form's own class; also how mount() knows it is already there */
|
||||
/* how long the stock view gets to render its tabs before a missing group counts as a failure */
|
||||
const TAB_DEADLINE = 5000; /* the built form's own class; also how mount() knows it is already there */
|
||||
const TAB_DEADLINE = 5000;
|
||||
const TAB = 'fs-appearance'; /* the pane's data-tab, which ui.tabs' click handler matches on */
|
||||
|
||||
let _routeObserver = null, _viewObserver = null, _observedView = null, _building = false;
|
||||
|
||||
@@ -23,8 +23,18 @@ const _fitters = [];
|
||||
let _rafPending = false;
|
||||
let _ro = null, _mo = null;
|
||||
/* one canvas for the whole document: wordFloor() measures text with it, and creating one per call
|
||||
* is the expensive half of that measurement */
|
||||
* is the expensive half of that measurement. `null` = not asked yet, `false` = asked and refused —
|
||||
* the same three-state slot fs-widgets.js's rasterCtx() keeps, and for the same reason: a browser
|
||||
* with canvas turned off (an anti-fingerprinting extension, a WebView, memory pressure) answers
|
||||
* null, and retrying that on every fit pass is a cost with no chance of a different answer. */
|
||||
let _cx = null;
|
||||
function textCtx() {
|
||||
if (_cx === null) {
|
||||
try { _cx = document.createElement('canvas').getContext('2d') || false; }
|
||||
catch (e) { _cx = false; }
|
||||
}
|
||||
return _cx;
|
||||
}
|
||||
const SPACE_RE = /\s+/;
|
||||
|
||||
/* Run every fitter NOW, synchronously. A fitter must be idempotent — this fires on every
|
||||
@@ -155,7 +165,15 @@ return baseclass.extend({
|
||||
wordFloor(t) {
|
||||
const rows = t.querySelectorAll('.tr:not(.table-titles):not(.cbi-section-table-titles):not(.placeholder)');
|
||||
if (!rows.length) return 0;
|
||||
if (!_cx) _cx = document.createElement('canvas').getContext('2d');
|
||||
/* No 2D context, no floor to report — and 0 is the honest answer, not a failure: it says
|
||||
* "this test has nothing to add", leaving the other two stack tests (does it overflow, is a
|
||||
* one-token column shredded) to decide. Throwing here would cost far more than the test is
|
||||
* worth: fs-select's fitTables() strips `fs-stacked` BEFORE it measures (rule 1), and this is
|
||||
* the last term of that `||`, so the throw would escape the row walk with the class already
|
||||
* gone — every stackable table left un-stacked, every later table in the pass unfitted, once
|
||||
* a second on a polled page. */
|
||||
const cx = textCtx();
|
||||
if (!cx) return 0;
|
||||
const floors = [], longest = [];
|
||||
let cols = null;
|
||||
for (const row of rows) {
|
||||
@@ -181,8 +199,8 @@ return baseclass.extend({
|
||||
else for (const w of text.split(SPACE_RE)) if (w.length > word.length) word = w;
|
||||
if (!(word.length > (longest[i] || 0))) continue;
|
||||
longest[i] = word.length;
|
||||
_cx.font = col.font;
|
||||
const need = _cx.measureText(word).width + (col.tracking * word.length) + col.pad;
|
||||
cx.font = col.font;
|
||||
const need = cx.measureText(word).width + (col.tracking * word.length) + col.pad;
|
||||
if (!(floors[i] >= need)) floors[i] = need;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,20 +61,39 @@ function clearViewIntervals() {
|
||||
* is the pair disagreeing: a tick that is running while the id it runs on has no name we know.
|
||||
* Then do NOTHING, once, loudly: a view's leftover interval outliving its page costs a wasted
|
||||
* RPC, whereas clearing LuCI's own tick costs every live value on every later page. */
|
||||
/* …and the ALIAS ITSELF is the first thing that can go — it is the deprecation the paragraph
|
||||
* above is about, so this function cannot be the one place that reads it blind. Every other
|
||||
* `L.Poll` read in this file is guarded, and an unguarded one here throws where a throw costs
|
||||
* most: navigate() calls this AFTER #view has been swapped for the "Loading view…" spinner and
|
||||
* BEFORE data-page, pushState and the require, so a TypeError would strand every click on the
|
||||
* spinner with the old URL and the old menu mark — far worse than the leaked interval this
|
||||
* exists to sweep. No alias is the same answer as an unreadable timer: do NOTHING, once,
|
||||
* loudly. */
|
||||
if (!L.Poll) {
|
||||
warnPollUnreadable('footstrap: L.Poll is gone from this luci-base, so LuCI\'s own tick cannot be '
|
||||
+ 'told apart from a view\'s timers — leaving view intervals alone. fs-router.js needs '
|
||||
+ 'updating for this luci-base.');
|
||||
return;
|
||||
}
|
||||
const running = (typeof L.Poll.active === 'function') ? L.Poll.active() : (L.Poll.timer != null);
|
||||
if (running && L.Poll.timer == null) {
|
||||
if (!_pollTimerWarned) {
|
||||
_pollTimerWarned = true;
|
||||
console.error('footstrap: LuCI is polling but L.Poll.timer is not readable — leaving view '
|
||||
+ 'intervals alone rather than risking its tick. fs-router.js needs updating for this '
|
||||
+ 'luci-base.');
|
||||
}
|
||||
warnPollUnreadable('footstrap: LuCI is polling but L.Poll.timer is not readable — leaving view '
|
||||
+ 'intervals alone rather than risking its tick. fs-router.js needs updating for this '
|
||||
+ 'luci-base.');
|
||||
return;
|
||||
}
|
||||
const keep = running ? L.Poll.timer : null;
|
||||
_viewIntervals.forEach((id) => { if (id !== keep) window.clearInterval(id); });
|
||||
}
|
||||
/* ONE line per document whatever went wrong: this runs on every navigation, and a router that
|
||||
* cannot read L.Poll cannot read it on the next click either — a message per click would bury the
|
||||
* console the user is reading it in. */
|
||||
let _pollTimerWarned = false;
|
||||
function warnPollUnreadable(msg) {
|
||||
if (_pollTimerWarned) return;
|
||||
_pollTimerWarned = true;
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
/* --- uci cache teardown for SPA nav ---
|
||||
* `uci.load()` does not answer "is this config present?" — it answers "which of these packages did
|
||||
@@ -806,15 +825,23 @@ function navigate(pathname, push, kbd) {
|
||||
* repairStaleRender() a mess that only exists because a require in flight cannot be stopped. */
|
||||
if (gen !== _navGen) return null;
|
||||
_seen.add(className);
|
||||
/* NAME THE OWNER for the length of this require. On a first visit the require IS the render,
|
||||
* so any <style> the module injects belongs to THIS page — even if a newer navigation has
|
||||
* stamped data-page by the time it lands, which is exactly the case repairStaleRender() below
|
||||
* exists for. Without it fs-sheets credited such a sheet to the page that superseded this one
|
||||
* and bound it there for the life of the document (measured: luci-app-filemanager's
|
||||
* `.cbi-button-save { display: none !important }` disabled on its own page and live on
|
||||
* System -> System, across return visits). */
|
||||
sheets.attributeTo(rsegs);
|
||||
return RT.require(className).finally(() => sheets.attributeTo(null));
|
||||
/* NAME THE OWNER for the length of this require, AND ONLY WHEN THE MODULE HAS YET TO BE
|
||||
* EVALUATED. On a first visit the require IS the render, so any <style> the module injects
|
||||
* belongs to THIS page — even if a newer navigation has stamped data-page by the time it
|
||||
* lands, which is exactly the case repairStaleRender() below exists for. Without it fs-sheets
|
||||
* credited such a sheet to the page that superseded this one and bound it there for the life
|
||||
* of the document (measured: luci-app-filemanager's `.cbi-button-save { display: none
|
||||
* !important }` disabled on its own page and live on System -> System, across return visits).
|
||||
*
|
||||
* `!cached` is what makes the hint survive long enough to be read, and its absence is why the
|
||||
* measured case stopped being covered. A CACHED require injects nothing — the module was
|
||||
* evaluated on the first visit and `require()` hands back the singleton — but it still ran
|
||||
* this line, and its `.finally` fires within a microtask, so the navigation that superseded a
|
||||
* cold require would set the hint to itself and clear it to null while that require was still
|
||||
* in flight. The sheet then landed with no hint at all, i.e. credited to the superseding page,
|
||||
* which is the exact bug. A require that cannot inject has no business naming an owner. */
|
||||
if (!cached) sheets.attributeTo(rsegs, gen);
|
||||
return RT.require(className).finally(() => { if (!cached) sheets.attributeTo(null, gen); });
|
||||
}).then((view) => {
|
||||
if (view == null) return;
|
||||
if (!(view instanceof RT.view))
|
||||
|
||||
@@ -185,8 +185,21 @@ function enhance(sel) {
|
||||
* openclash / justclash / ssclash / dashboard / statistics included, found ZERO. That is the point:
|
||||
* every table anyone here emits already carries the class, so this changes nothing that can be
|
||||
* measured and covers the one shape that cannot be (docs/conventions.md: coverage is a contract). */
|
||||
const FOREIGN_TABLE = '#view .table:not(.cbi-section-table):not(.fs-dt), ' +
|
||||
'#view table:not(.table):not(.cbi-section-table):not(.fs-dt)';
|
||||
/* NO `:not(.fs-dt)`, and that is the difference between claiming a table once and KEEPING it. These
|
||||
* tables are polled: L.ui.Table.update() and every hand-rolled equivalent replace the rows inside
|
||||
* the element they already have. Excluding what we tagged meant the claim ran exactly once per
|
||||
* element — the rows present at that moment were adopted and captioned, and every batch after it
|
||||
* kept neither `.tr`/`.td` nor `data-title`, on a table that by then may carry `.fs-stacked`, where
|
||||
* `#view .table.fs-dt.fs-stacked { overflow: hidden }` clips the lot with no scrollbar. That is the
|
||||
* exact failure adoptMarkup() was written to prevent, arriving one poll later. LuCI's own tables
|
||||
* were never exposed to it (ui.Table writes those class names and the caption itself), so this is
|
||||
* third-party-only — which is the zone this whole selector exists for.
|
||||
*
|
||||
* Re-running is what the two functions below are already written for: both are additive, both skip
|
||||
* what is already done, and neither re-decides anything (adoptMarkup() settles the "is this ours to
|
||||
* rewrite?" question once, at claim time, and remembers the answer on the element). */
|
||||
const FOREIGN_TABLE = '#view .table:not(.cbi-section-table), ' +
|
||||
'#view table:not(.table):not(.cbi-section-table)';
|
||||
|
||||
/* The FOURTH header markup, and the one only a foreign table produces: `<table><tr><th>…`, with no
|
||||
* `<thead>` for the parser to imply and none of LuCI's class names. It is the exact shape the phone
|
||||
@@ -255,9 +268,10 @@ function tagDataTables() {
|
||||
* names: a `<thead>` becomes `.thead` and a plain first row of `<th>` becomes `.tr.table-titles` —
|
||||
* the two names theme/30-tables.css hides when stacked. */
|
||||
function adoptMarkup(t, head) {
|
||||
/* DECIDED ONCE, AT CLAIM TIME, and only for a table that speaks none of this vocabulary. Asking
|
||||
* the question every pass instead would answer "already adopted" the moment we adopted it — and
|
||||
* these tables are polled, so the fresh rows that arrive bare afterwards would never be taken.
|
||||
/* DECIDED ONCE, AT CLAIM TIME, and only for a table that speaks none of this vocabulary — then
|
||||
* READ on every pass, because the caller now revisits a table it has already claimed (see
|
||||
* FOREIGN_TABLE). Asking the question afresh each pass instead would answer "already adopted"
|
||||
* the moment we adopted it, and the fresh rows a poll brings in bare would never be taken.
|
||||
* Asking it at all is what keeps the theme's hands off LuCI's own markup: the apk Software list
|
||||
* heads its table with `.tr.cbi-section-table-titles`, and blindly adding `table-titles` to that
|
||||
* would be the theme rewriting a class LuCI chose. */
|
||||
|
||||
@@ -676,11 +676,30 @@ function appKey(segs) {
|
||||
* stand: the System page came back with no Save button, and stayed that way across return visits).
|
||||
*
|
||||
* So the router names the owner for the duration of such a require (see fs-router.js), and this is
|
||||
* that hint. It is a plain value rather than a stack: two cold requires cannot overlap, because the
|
||||
* second navigation supersedes the first before it starts one. */
|
||||
* that hint.
|
||||
*
|
||||
* ONE SLOT, STAMPED WITH THE NAVIGATION THAT SET IT. The router only names an owner for a require
|
||||
* that has yet to evaluate its module, so the common shape — a cold require in flight, the user
|
||||
* clicking on to a page already in LuCI's class cache — no longer touches this slot at all. Two
|
||||
* COLD requires can still overlap, and then the newer one wins the slot: it is the page the user is
|
||||
* looking at, and crediting ITS sheet to the page it superseded would leave the visible page
|
||||
* unpainted, which is the worse of the two errors. The generation stamp is what keeps that from
|
||||
* getting worse still — the older require's `.finally` must not clear a slot the newer one now
|
||||
* holds, or the newer page's own sheet lands unattributed.
|
||||
*
|
||||
* There is no way to do better from here: LuCI evaluates a view module inside `eval()` in its own
|
||||
* `require()` (luci.js), so nothing observable says WHICH module is running when a <style> appears —
|
||||
* `document.currentScript` is null and the injection is synchronous inside a promise chain we do
|
||||
* not own. The remaining hole is therefore two cold requires overlapping, where the first one's
|
||||
* sheet is credited to the second's page — the pre-existing behaviour, now narrowed to that one
|
||||
* case instead of every navigation away from a cold require. */
|
||||
let _ownerHint = null;
|
||||
function attributeTo(segs) {
|
||||
let _ownerGen = -1;
|
||||
function attributeTo(segs, gen) {
|
||||
/* a stale require letting go of a slot somebody else now holds: leave it alone */
|
||||
if (segs == null && gen !== _ownerGen) return;
|
||||
_ownerHint = (segs == null) ? null : appKey(segs);
|
||||
_ownerGen = (segs == null) ? -1 : gen;
|
||||
}
|
||||
|
||||
function ownerKey() {
|
||||
@@ -723,6 +742,25 @@ function scopeToCurrentPage(segs) {
|
||||
});
|
||||
}
|
||||
|
||||
/* TAKE A SHEET, AND SCOPE IT IN THE SAME BREATH.
|
||||
*
|
||||
* scopeToCurrentPage() runs on NAVIGATION, so it only ever sees sheets that were already here. A
|
||||
* sheet that arrives afterwards is scoped by nobody until the next click — and the one case where
|
||||
* that matters is precisely the case the owner hint exists for: a cold require whose page the user
|
||||
* has already left injects its <style> into a document showing somebody else's page, and the sheet
|
||||
* paints there until the user navigates again. Measured with a view whose module appends
|
||||
* `body { outline: 3px solid rgb(9,9,9) }`: correctly credited to its own page, and still painting
|
||||
* the outline on the page that superseded it.
|
||||
*
|
||||
* So the stamp and the switch are one act. `ownerKey()` is the page the sheet BELONGS to and
|
||||
* `currentKey()` the page on screen; on every ordinary arrival they are the same string and this is
|
||||
* a no-op. */
|
||||
function claimOwner(el) {
|
||||
const key = ownerKey();
|
||||
_owner.set(el, key);
|
||||
if (outlivesPage(el)) setEnabled(el, key === currentKey());
|
||||
}
|
||||
|
||||
function rehostIntoThemeLayer(el, universe) {
|
||||
if (el.dataset.fsLayered) return;
|
||||
|
||||
@@ -735,7 +773,7 @@ function rehostIntoThemeLayer(el, universe) {
|
||||
el.dataset.fsLayered = '1';
|
||||
el.after(s); /* keep source order: ties inside the layer still resolve as they did */
|
||||
silence(el);
|
||||
_owner.set(s, ownerKey()); /* the shim paints; the original is silenced for good */
|
||||
claimOwner(s); /* the shim paints; the original is silenced for good */
|
||||
fenceImported(s, universe.names, Date.now() + 1000); /* a cache hit lands on the first frame */
|
||||
return;
|
||||
}
|
||||
@@ -752,7 +790,6 @@ function rehostIntoThemeLayer(el, universe) {
|
||||
* unpinned all over again and a second pass appends a second fence. The mark is the only thing
|
||||
* that says the work is done, so it has to be set for every path below, wrapped or not. */
|
||||
el.dataset.fsLayered = '1';
|
||||
_owner.set(el, ownerKey()); /* a <style> is re-hosted IN PLACE, so it paints itself */
|
||||
|
||||
/* Wrap only if the text still IS the sheet (see textIsSheet). When it is not, the sheet stays
|
||||
* unlayered — Zone 2 exactly where it already was, which is a trade — rather than lose rules,
|
||||
@@ -769,6 +806,15 @@ function rehostIntoThemeLayer(el, universe) {
|
||||
el.textContent = '@layer theme {\n' + el.textContent + '\n}';
|
||||
}
|
||||
try { if (el.sheet) fenceRules(el.sheet.cssRules, universe.names); } catch (e) { /* unfenced, not broken */ }
|
||||
/* LAST, because the line above may have re-parsed the sheet. `el.disabled` is not a content
|
||||
* attribute — it is the element's view of `el.sheet.disabled`, and assigning textContent throws
|
||||
* the old CSSStyleSheet away and builds a new one, which comes back ENABLED. Claiming before the
|
||||
* wrap therefore switched a sheet off and then switched it back on within the same call, which is
|
||||
* how a sheet belonging to a page the user had left went on painting the page that superseded it
|
||||
* (measured: `body { outline: 3px solid rgb(9,9,9) }` from a still-loading view, live on
|
||||
* System -> System). Ownership is recorded from the same call either way — ownerKey() is read
|
||||
* here, while the router's hint still names the page whose module is evaluating. */
|
||||
claimOwner(el); /* a <style> is re-hosted IN PLACE, so it paints itself */
|
||||
}
|
||||
|
||||
/* Re-hosting needs the theme's own selectors to tell an invasive sheet from an inert one. If
|
||||
|
||||
Reference in new issue
Block a user