phimpal-tv 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/mod.js +155 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,27 @@ Three TV-specific behaviours live in the engine itself:
|
|
|
100
100
|
site fades it in only while the element really matches `:hover`, which never happens
|
|
101
101
|
with a remote — so the module reveals it itself.
|
|
102
102
|
|
|
103
|
+
## If it freezes on the TV
|
|
104
|
+
|
|
105
|
+
Every key is intercepted before it is handled, so an exception inside the module used to
|
|
106
|
+
swallow all input — that looked like a frozen cursor with a dead Return key. Since 0.2.1:
|
|
107
|
+
|
|
108
|
+
- **Return ×3 within 1.5 s** always resets the module (mode, menus, seek cursor) — the
|
|
109
|
+
universal escape hatch.
|
|
110
|
+
- any exception is caught, the state is reset automatically, and a **red box bottom-left**
|
|
111
|
+
shows the error for 10 s. Photograph it: that text is the bug report. The last one is also
|
|
112
|
+
kept in `localStorage['tv-lasterror']`.
|
|
113
|
+
- page scans are memoised (a TV CPU was re-computing the same styles hundreds of times per
|
|
114
|
+
key), held keys are coalesced, and carousels are recognised structurally so paging also
|
|
115
|
+
works on Tizen ≤ 6.5, whose Chromium ignores `overflow-x: clip`.
|
|
116
|
+
- (0.2.2) a **black box**: the last 40 steps (key, mode, focus, scope, paging…) are written to
|
|
117
|
+
`localStorage['tv-trace']` as they happen. If a session ends in the middle of a key, the
|
|
118
|
+
**next launch** shows the last 12 steps in the same red box for 25 s — so after a freeze,
|
|
119
|
+
relaunch the module and photograph that box.
|
|
120
|
+
- (0.2.2) a navigation scope (dialog/menu) now has to be on screen **and** contain something
|
|
121
|
+
focusable; an element merely carrying `role="dialog"` cannot trap the keys any more, and a
|
|
122
|
+
scope that refuses to close is ignored afterwards.
|
|
123
|
+
|
|
103
124
|
## Known limits
|
|
104
125
|
|
|
105
126
|
- The per-track timing button (clock icon) in the subtitle panel is not reachable with the
|
package/mod.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// PhimPal TV v0.2.
|
|
1
|
+
// PhimPal TV v0.2.2 — generated by build.js from phimpal-tv.user.js, do not edit.
|
|
2
2
|
// TizenBrew site-modification module for https://phimpal.com/
|
|
3
3
|
(function registerRemoteKeys() {
|
|
4
4
|
try {
|
|
@@ -50,6 +50,45 @@
|
|
|
50
50
|
// ------------------------------------------------------------------ dom helpers
|
|
51
51
|
const $ = (s, r = document) => r.querySelector(s);
|
|
52
52
|
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
|
|
53
|
+
|
|
54
|
+
// ------------------------------------------------------------------ error reporting
|
|
55
|
+
// On the TV there is no console: any exception in our code is shown on screen (even with
|
|
56
|
+
// the debug badge off) and kept in localStorage['tv-lasterror'] for the bug report.
|
|
57
|
+
let errTimer = null;
|
|
58
|
+
function report(where, e) {
|
|
59
|
+
const msg = where + ': ' + ((e && e.message) || e) + '\n' + (((e && e.stack) || '').split('\n').slice(1, 3).join('\n'));
|
|
60
|
+
try { localStorage.setItem('tv-lasterror', new Date().toISOString() + ' ' + msg); } catch (x) { /* ignore */ }
|
|
61
|
+
try {
|
|
62
|
+
let b = $('#tv-error');
|
|
63
|
+
if (!b) { b = document.createElement('pre'); b.id = 'tv-error'; document.body.appendChild(b); }
|
|
64
|
+
b.textContent = 'phimpal-tv error\n' + msg;
|
|
65
|
+
b.style.opacity = '1';
|
|
66
|
+
clearTimeout(errTimer); errTimer = setTimeout(() => { b.style.opacity = '0'; }, 10000);
|
|
67
|
+
} catch (x) { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
const safe = (where, fn) => function () { try { return fn.apply(this, arguments); } catch (e) { report(where, e); } };
|
|
70
|
+
|
|
71
|
+
// Black box: the last steps are written to localStorage as they happen. If the previous
|
|
72
|
+
// session died mid-key (frozen page, app killed), the next launch shows that tail on screen.
|
|
73
|
+
const TRACE_KEY = 'tv-trace', TRACE_MAX = 40;
|
|
74
|
+
let traceBuf = [];
|
|
75
|
+
function trace(msg) {
|
|
76
|
+
traceBuf.push((Date.now() % 100000) + ' ' + msg);
|
|
77
|
+
if (traceBuf.length > TRACE_MAX) traceBuf.shift();
|
|
78
|
+
try { localStorage.setItem(TRACE_KEY, traceBuf.join('\n')); } catch (e) { /* ignore */ }
|
|
79
|
+
}
|
|
80
|
+
function showPreviousTrace() {
|
|
81
|
+
let prev = null; try { prev = localStorage.getItem(TRACE_KEY); } catch (e) { /* ignore */ }
|
|
82
|
+
if (!prev || !/\bkey \d+ start\b(?![\s\S]*\bkey end\b)/.test(prev)) return; // previous session ended cleanly
|
|
83
|
+
const tail = prev.split('\n').slice(-12).join('\n');
|
|
84
|
+
try {
|
|
85
|
+
let b = $('#tv-error');
|
|
86
|
+
if (!b) { b = document.createElement('pre'); b.id = 'tv-error'; document.body.appendChild(b); }
|
|
87
|
+
b.textContent = 'phimpal-tv — previous session ended mid-key:\n' + tail;
|
|
88
|
+
b.style.opacity = '1';
|
|
89
|
+
clearTimeout(errTimer); errTimer = setTimeout(() => { b.style.opacity = '0'; }, 25000);
|
|
90
|
+
} catch (e) { /* ignore */ }
|
|
91
|
+
}
|
|
53
92
|
const rectOf = (el) => el.getBoundingClientRect();
|
|
54
93
|
const isWatch = () => /^\/watch\//.test(location.pathname);
|
|
55
94
|
const isHome = () => location.pathname === '/';
|
|
@@ -65,29 +104,47 @@
|
|
|
65
104
|
const EXCL_SELF = 'button[aria-label="Previous"], button[aria-label="Next"], button[aria-label^="Go to page"]';
|
|
66
105
|
const POPUP = ':scope > .group-hover\\:block';
|
|
67
106
|
|
|
107
|
+
// getComputedStyle is the expensive call on a TV CPU; a scan of the page asks for the
|
|
108
|
+
// same ancestors hundreds of times, so results are memoised for the duration of one scan.
|
|
109
|
+
let csCache = null, clipCache = null;
|
|
110
|
+
const cs = (el) => {
|
|
111
|
+
if (!csCache) return getComputedStyle(el);
|
|
112
|
+
let v = csCache.get(el); if (!v) { v = getComputedStyle(el); csCache.set(el, v); }
|
|
113
|
+
return v;
|
|
114
|
+
};
|
|
115
|
+
function withScanCache(fn) {
|
|
116
|
+
const outer = !!csCache;
|
|
117
|
+
if (!outer) { csCache = new Map(); clipCache = new Map(); }
|
|
118
|
+
try { return fn(); } finally { if (!outer) { csCache = null; clipCache = null; } }
|
|
119
|
+
}
|
|
68
120
|
function styleVisible(el) {
|
|
69
|
-
const
|
|
70
|
-
return
|
|
121
|
+
const c = cs(el);
|
|
122
|
+
return c.visibility !== 'hidden' && c.display !== 'none' && parseFloat(c.opacity) > 0 && c.pointerEvents !== 'none';
|
|
71
123
|
}
|
|
72
124
|
function ancestorsOpaque(el, depth = 4) {
|
|
73
125
|
let n = el.parentElement;
|
|
74
126
|
while (n && n !== document.body && depth-- > 0) {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
127
|
+
const c = cs(n);
|
|
128
|
+
if (c.visibility === 'hidden' || parseFloat(c.opacity) === 0) return false;
|
|
77
129
|
n = n.parentElement;
|
|
78
130
|
}
|
|
79
131
|
return true;
|
|
80
132
|
}
|
|
81
133
|
// nearest ancestor that clips or scrolls horizontally
|
|
82
134
|
function clipAncestor(el) {
|
|
83
|
-
|
|
135
|
+
if (clipCache && clipCache.has(el)) return clipCache.get(el);
|
|
136
|
+
let n = el.parentElement, res = null;
|
|
84
137
|
while (n && n !== document.body) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
138
|
+
// the site's rows use `overflow-x: clip`, a value Chromium < 90 (Tizen ≤ 6.5) does not
|
|
139
|
+
// parse — recognise the row by its class too, so paging works on older TVs
|
|
140
|
+
if (n.classList.contains('overflow-x-clip')) { res = { el: n, kind: 'clip' }; break; }
|
|
141
|
+
const ox = cs(n).overflowX;
|
|
142
|
+
if (ox === 'clip' || ox === 'hidden') { res = { el: n, kind: 'clip' }; break; }
|
|
143
|
+
if (ox === 'auto' || ox === 'scroll') { res = { el: n, kind: 'scroll' }; break; }
|
|
88
144
|
n = n.parentElement;
|
|
89
145
|
}
|
|
90
|
-
|
|
146
|
+
if (clipCache) clipCache.set(el, res);
|
|
147
|
+
return res;
|
|
91
148
|
}
|
|
92
149
|
function horizVisible(el, r) {
|
|
93
150
|
const c = clipAncestor(el);
|
|
@@ -152,21 +209,31 @@
|
|
|
152
209
|
}
|
|
153
210
|
const hoverMenuOpen = () => !!S.hoverTrigger && !!currentScope(); // aria-expanded goes stale, the rendered menu doesn't
|
|
154
211
|
|
|
155
|
-
// dialogs / menus that should capture navigation
|
|
212
|
+
// dialogs / menus that should capture navigation.
|
|
213
|
+
// A scope must be on screen AND contain something to focus: an element that merely
|
|
214
|
+
// carries role="dialog" (a toast, an off-screen drawer, a banner) would otherwise trap
|
|
215
|
+
// every key — arrows find nothing inside it and Back cannot close it. Scopes that refuse
|
|
216
|
+
// to close are blacklisted for the rest of the page.
|
|
156
217
|
const subPanel = () => $('.vjs-subtitle-panel.is-open');
|
|
218
|
+
const deadScopes = new WeakSet();
|
|
157
219
|
function currentScope() {
|
|
158
220
|
const list = $$('.vjs-modal-dialog:not(.vjs-hidden), [role="dialog"], [role="alertdialog"], [role="menu"], [data-scope="menu"][data-part="content"], [data-scope="popover"][data-part="content"], [data-scope="dialog"][data-part="content"]');
|
|
159
221
|
const dialog = list.find((e) => {
|
|
222
|
+
if (deadScopes.has(e)) return false;
|
|
160
223
|
if (e.closest('.video-js') && !e.classList.contains('vjs-modal-dialog')) return false;
|
|
224
|
+
if (e.closest('aside')) return false; // mobile drawer, parked off-screen with a transform
|
|
161
225
|
const r = rectOf(e);
|
|
162
|
-
|
|
226
|
+
if (!(r.width > 0 && r.height > 0 && styleVisible(e))) return false;
|
|
227
|
+
if (r.right <= 0 || r.bottom <= 0 || r.left >= innerWidth || r.top >= innerHeight) return false; // off-screen
|
|
228
|
+
return $$(CAND, e).some((c) => c !== e && isVisibleBlock(c)); // has something to focus
|
|
163
229
|
});
|
|
164
230
|
if (dialog) return dialog; // a caption-settings modal opens on top of the subtitle panel
|
|
165
231
|
const sp = subPanel();
|
|
166
232
|
return (sp && rectOf(sp).height > 0) ? sp : null;
|
|
167
233
|
}
|
|
168
234
|
|
|
169
|
-
function collectBlocks(scope) {
|
|
235
|
+
function collectBlocks(scope) { return withScanCache(() => collectBlocksRaw(scope)); }
|
|
236
|
+
function collectBlocksRaw(scope) {
|
|
170
237
|
const root = scope || document;
|
|
171
238
|
const seen = new Set(), dupes = new Map(), out = [];
|
|
172
239
|
for (const el of $$(CAND, root)) {
|
|
@@ -253,15 +320,19 @@
|
|
|
253
320
|
if (!canClick(btn)) return false;
|
|
254
321
|
const item = block.closest('.shrink-0') || block;
|
|
255
322
|
const sib = dir === A.RIGHT ? item.nextElementSibling : item.previousElementSibling;
|
|
323
|
+
trace('page click ' + dir);
|
|
256
324
|
btn.click();
|
|
257
|
-
|
|
325
|
+
trace('page clicked');
|
|
326
|
+
setTimeout(safe('page', () => {
|
|
258
327
|
let t = sib && (sib.matches('.group') ? sib : sib.querySelector('.group'));
|
|
259
328
|
if (!(t && isVisibleBlock(t))) {
|
|
260
|
-
const vis = $$('.group', car.clip).filter(isVisibleBlock);
|
|
329
|
+
const vis = withScanCache(() => $$('.group', car.clip).filter(isVisibleBlock));
|
|
261
330
|
t = dir === A.RIGHT ? vis[0] : vis[vis.length - 1];
|
|
262
331
|
}
|
|
263
|
-
|
|
264
|
-
|
|
332
|
+
trace('page settled target=' + describe(t));
|
|
333
|
+
if (t) setFocus(t); else badge('page: no card');
|
|
334
|
+
trace('page done');
|
|
335
|
+
}), CFG.carouselAnimMs);
|
|
265
336
|
return true;
|
|
266
337
|
}
|
|
267
338
|
function rowsOnPage() {
|
|
@@ -290,6 +361,7 @@
|
|
|
290
361
|
}
|
|
291
362
|
function setFocus(block, opts = {}) {
|
|
292
363
|
if (!block) return;
|
|
364
|
+
trace('focus ' + describe(block));
|
|
293
365
|
if (S.focused !== block) clearFocus();
|
|
294
366
|
S.focused = block;
|
|
295
367
|
if (isCard(block)) { block.classList.add('tv-focus'); (popupOf(block) || block).classList.add('tv-focus-ring'); }
|
|
@@ -312,8 +384,15 @@
|
|
|
312
384
|
function ensureVisible(el) {
|
|
313
385
|
if (el.closest('header')) return;
|
|
314
386
|
const r = rectOf(el);
|
|
387
|
+
const c = clipAncestor(el);
|
|
388
|
+
if (c && c.kind === 'clip') {
|
|
389
|
+
// never let the browser scroll a carousel row itself (that would fight its translateX
|
|
390
|
+
// paging); only move the page vertically
|
|
391
|
+
if (r.top < 90 || r.bottom > innerHeight - 30) window.scrollBy({ top: r.top + r.height / 2 - innerHeight / 2 });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
315
394
|
if (r.top < 90 || r.bottom > innerHeight - 30) el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
316
|
-
else
|
|
395
|
+
else if (c && c.kind === 'scroll') el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
317
396
|
}
|
|
318
397
|
|
|
319
398
|
// per-URL focus memory (survives the full reload of the watch page)
|
|
@@ -323,8 +402,10 @@
|
|
|
323
402
|
function hrefOf(b) { const a = b.tagName === 'A' ? b : (isCard(b) ? posterOf(b) : null); return a ? a.getAttribute('href') : null; }
|
|
324
403
|
function remember(block) {
|
|
325
404
|
const m = loadMem();
|
|
326
|
-
const
|
|
327
|
-
|
|
405
|
+
const href = hrefOf(block);
|
|
406
|
+
// the index needs a full page scan — only pay for it when there is no href to key on
|
|
407
|
+
const idx = href ? null : (() => { const sc = currentScope(); return collectBlocks(sc && sc.contains(block) ? sc : null).indexOf(block); })();
|
|
408
|
+
m[memKey()] = { href, idx };
|
|
328
409
|
saveMem(m);
|
|
329
410
|
}
|
|
330
411
|
function initialFocus() {
|
|
@@ -652,7 +733,8 @@
|
|
|
652
733
|
const scope = currentScope();
|
|
653
734
|
const blocks = collectBlocks(scope);
|
|
654
735
|
let t = pickTarget(f, dir, blocks);
|
|
655
|
-
|
|
736
|
+
trace('move ' + dir + ' scope=' + describe(scope) + ' blocks=' + blocks.length + ' target=' + describe(t));
|
|
737
|
+
if (!t && (dir === A.LEFT || dir === A.RIGHT) && pageCarousel(f, dir)) { trace('paging ' + dir); return; }
|
|
656
738
|
if (!t && dir === A.UP && !scope && scrollY > 0) {
|
|
657
739
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
658
740
|
setTimeout(() => { const t2 = pickTarget(S.focused, A.UP, collectBlocks()); if (t2) setFocus(t2); }, 450);
|
|
@@ -670,7 +752,9 @@
|
|
|
670
752
|
if (scope && scope.classList.contains('vjs-subtitle-panel')) return closeSubPanel();
|
|
671
753
|
const hoverT = S.hoverTrigger;
|
|
672
754
|
const finish = () => {
|
|
673
|
-
|
|
755
|
+
const still = currentScope();
|
|
756
|
+
if (still === scope) { deadScopes.add(scope); trace('scope refused to close, ignoring it'); } // never trap the user
|
|
757
|
+
else if (still) return; // a different dialog is open, stay in it
|
|
674
758
|
if (S.returnMode) { const m = S.returnMode; S.returnMode = null; return m === 'PLAYER' ? enterPlayer() : enterBar(); }
|
|
675
759
|
const back = S.beforeScope || hoverT; S.beforeScope = null;
|
|
676
760
|
if (back && isVisibleBlock(back)) return setFocus(back, { noHover: true }); // don't re-open a hover menu
|
|
@@ -712,12 +796,41 @@
|
|
|
712
796
|
|
|
713
797
|
// ------------------------------------------------------------------ key dispatch
|
|
714
798
|
function consume(e) { e.preventDefault(); e.stopImmediatePropagation(); }
|
|
799
|
+
|
|
800
|
+
// Whatever happens inside a key handler must never leave the module deaf: every key is
|
|
801
|
+
// consumed before it is handled, so an exception would otherwise swallow all input.
|
|
802
|
+
let lastKeyAt = 0, backTaps = [];
|
|
803
|
+
function hardReset(why) {
|
|
804
|
+
try { clearSeek(); hoverClose(); } catch (e) { /* ignore */ }
|
|
805
|
+
S.sub = null; S.menu = null; S.menuItem = null; S.returnMode = null; S.beforeScope = null; S.seekReturn = null;
|
|
806
|
+
S.focused = null;
|
|
807
|
+
if (isWatch() && player()) enterPlayer(); else { S.mode = 'NAV'; initialFocus(); }
|
|
808
|
+
badge('reset: ' + why, true);
|
|
809
|
+
}
|
|
810
|
+
function onKeySafe(e) {
|
|
811
|
+
const known = TIZEN_KEYS[e.keyCode] || (CFG.keyboardEmulation && KB_KEYS[e.keyCode]);
|
|
812
|
+
if (known) trace('key ' + e.keyCode + ' start mode=' + S.mode + ' sub=' + S.sub + ' focus=' + describe(S.focused));
|
|
813
|
+
try { onKey(e); }
|
|
814
|
+
catch (err) { report('key ' + e.keyCode, err); try { hardReset('error'); } catch (x) { report('reset', x); } }
|
|
815
|
+
if (known) trace('key end mode=' + S.mode + ' focus=' + describe(S.focused));
|
|
816
|
+
}
|
|
817
|
+
function describe(el) {
|
|
818
|
+
if (!el) return 'none';
|
|
819
|
+
const a = el.tagName === 'A' ? el : (el.querySelector && el.querySelector('a[href]'));
|
|
820
|
+
return el.tagName + (el.id ? '#' + el.id : '') + '.' + (el.className + '').trim().split(/\s+/).slice(0, 2).join('.') + (a && a.getAttribute('href') ? '[' + a.getAttribute('href').slice(0, 30) + ']' : '');
|
|
821
|
+
}
|
|
715
822
|
function onKey(e) {
|
|
716
823
|
if (e.__tv) return;
|
|
717
824
|
let act = TIZEN_KEYS[e.keyCode];
|
|
718
825
|
if (!act && CFG.keyboardEmulation) act = KB_KEYS[e.keyCode];
|
|
719
826
|
if (!act) return;
|
|
720
827
|
if (e.repeat && (act === A.OK || act === A.BACK || act === A.PLAY)) return consume(e);
|
|
828
|
+
// a held D-pad fires faster than a TV can re-scan the page: coalesce repeats
|
|
829
|
+
const now = Date.now();
|
|
830
|
+
if (e.repeat && now - lastKeyAt < 120) return consume(e);
|
|
831
|
+
lastKeyAt = now;
|
|
832
|
+
// escape hatch: Return three times within 1.5 s always resets the module
|
|
833
|
+
if (act === A.BACK) { backTaps = backTaps.filter((t) => now - t < 1500).concat(now); if (backTaps.length >= 3) { backTaps = []; consume(e); return hardReset('3× back'); } }
|
|
721
834
|
|
|
722
835
|
// Back on the home page belongs to the platform: TizenBrew uses it to leave the mod,
|
|
723
836
|
// so let it bubble instead of swallowing it (the user would be stuck otherwise).
|
|
@@ -744,6 +857,7 @@
|
|
|
744
857
|
if (S.mode !== 'NAV') { S.returnMode = S.mode === 'SEEK' ? 'PLAYER' : S.mode; clearSeek(); S.mode = 'NAV'; S.sub = null; S.menu = null; }
|
|
745
858
|
if (!S.focused || !scope.contains(S.focused)) {
|
|
746
859
|
const bl = collectBlocks(scope);
|
|
860
|
+
trace('scope grabbed key: ' + describe(scope) + ' blocks=' + bl.length);
|
|
747
861
|
if (bl.length) { S.beforeScope = S.focused; setFocus(bl[0]); if (act !== A.BACK) return; }
|
|
748
862
|
}
|
|
749
863
|
} else if (S.returnMode) {
|
|
@@ -764,8 +878,8 @@
|
|
|
764
878
|
const t0 = Date.now();
|
|
765
879
|
(function poll() {
|
|
766
880
|
let v = null; try { v = cond(); } catch (e) { /* ignore */ }
|
|
767
|
-
if (v) return ok(v);
|
|
768
|
-
if (Date.now() - t0 > timeout) return fail && fail();
|
|
881
|
+
if (v) return safe('waitFor', ok)(v);
|
|
882
|
+
if (Date.now() - t0 > timeout) return fail && safe('waitFor:fail', fail)();
|
|
769
883
|
setTimeout(poll, 100);
|
|
770
884
|
})();
|
|
771
885
|
}
|
|
@@ -843,14 +957,21 @@
|
|
|
843
957
|
.video-js.tv-fs .vjs-tech { width: 100% !important; height: 100% !important; object-fit: contain; }
|
|
844
958
|
html.tv-fs-on, html.tv-fs-on body { overflow: hidden !important; }
|
|
845
959
|
html.tv-fs-on header, html.tv-fs-on footer { display: none !important; }
|
|
960
|
+
#tv-error { position: fixed; left: 16px; bottom: 16px; max-width: 70vw; z-index: 2147483647; margin: 0; white-space: pre-wrap;
|
|
961
|
+
font: 15px/1.35 ui-monospace, monospace; color: #fff; background: rgba(160,0,0,.92); padding: 10px 14px; border-radius: 6px;
|
|
962
|
+
pointer-events: none; opacity: 0; transition: opacity .3s; }
|
|
846
963
|
#tv-badge { position: fixed; top: 12px; right: 12px; z-index: 2147483647; font: 13px/1.2 ui-monospace, monospace; padding: 6px 10px;
|
|
847
964
|
background: rgba(0,0,0,.75); color: #fff; border-radius: 6px; pointer-events: none; opacity: 0; transition: opacity .3s; white-space: pre; }
|
|
848
965
|
` + (CFG.hideCursor ? '\n html, body, * { cursor: none !important; }' : '');
|
|
966
|
+
// Chromium < 90 ignores `overflow-x: clip`, which would spill every carousel across the
|
|
967
|
+
// page; `hidden` is the closest thing it understands.
|
|
968
|
+
let clipOk = true; try { clipOk = CSS.supports('overflow-x', 'clip'); } catch (e) { clipOk = false; }
|
|
969
|
+
if (!clipOk) st.textContent += '\n .overflow-x-clip { overflow-x: hidden !important; }';
|
|
849
970
|
document.head.appendChild(st);
|
|
850
971
|
}
|
|
851
972
|
let badgeTimer = null;
|
|
852
|
-
function badge(extra) {
|
|
853
|
-
if (!CFG.debug) return;
|
|
973
|
+
function badge(extra, force) {
|
|
974
|
+
if (!CFG.debug && !force) return;
|
|
854
975
|
let b = $('#tv-badge');
|
|
855
976
|
if (!b) { b = document.createElement('div'); b.id = 'tv-badge'; document.body.appendChild(b); }
|
|
856
977
|
const fs = (S.mode !== 'NAV' && isFs()) ? ' FS' : '';
|
|
@@ -863,16 +984,20 @@
|
|
|
863
984
|
function init() {
|
|
864
985
|
if (window.__tvNav && window.__tvNav.destroy) window.__tvNav.destroy(); // hot re-install (dev)
|
|
865
986
|
injectCSS();
|
|
866
|
-
|
|
987
|
+
showPreviousTrace();
|
|
988
|
+
trace('init ' + location.pathname + ' ' + innerWidth + 'x' + innerHeight);
|
|
989
|
+
addEventListener('keydown', onKeySafe, true);
|
|
867
990
|
patchHistory();
|
|
868
|
-
|
|
869
|
-
|
|
991
|
+
const onRouteSafe = safe('route', checkRoute);
|
|
992
|
+
addEventListener('tv:route', onRouteSafe);
|
|
993
|
+
const onMutateSafe = safe('mutation', onMutate);
|
|
994
|
+
const mo = new MutationObserver(() => { clearTimeout(moTimer); moTimer = setTimeout(onMutateSafe, 200); });
|
|
870
995
|
mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
|
|
871
996
|
onRoute();
|
|
872
997
|
window.__tvNav = {
|
|
873
998
|
S, CFG, collectBlocks, setFocus, initialFocus, enterPlayer, enterBar, isVisibleBlock, pickTarget, clipAncestor, currentScope,
|
|
874
999
|
destroy() {
|
|
875
|
-
removeEventListener('keydown',
|
|
1000
|
+
removeEventListener('keydown', onKeySafe, true); removeEventListener('tv:route', onRouteSafe);
|
|
876
1001
|
mo.disconnect(); clearInterval(keepAlive); clearTimeout(moTimer); clearFocus();
|
|
877
1002
|
const b = $('#tv-badge'); if (b) b.remove(); const st = $('#tv-nav-style'); if (st) st.remove();
|
|
878
1003
|
},
|
package/package.json
CHANGED