phimpal-tv 0.2.1 → 0.2.3
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 +15 -0
- package/mod.js +69 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,21 @@ swallow all input — that looked like a frozen cursor with a dead Return key. S
|
|
|
113
113
|
- page scans are memoised (a TV CPU was re-computing the same styles hundreds of times per
|
|
114
114
|
key), held keys are coalesced, and carousels are recognised structurally so paging also
|
|
115
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
|
+
- (0.2.3) **low-gfx rendering** on the TV: all CSS transitions, `backdrop-filter`, and the
|
|
124
|
+
filters/shadows on the card popup and the player are removed. A freeze with no error and no
|
|
125
|
+
trace points at the renderer, not at the script — TV compositors are known to stall on
|
|
126
|
+
`backdrop-blur` and on a 500 ms transform over a row of 4K posters. Restore the site's
|
|
127
|
+
effects with `localStorage['tv-gfx'] = 'full'`.
|
|
128
|
+
- (0.2.3) the running **version is announced** bottom-right for 2 s at startup
|
|
129
|
+
(`PhimPal TV 0.2.3 · low-gfx`). If you don't see the version you just published, TizenBrew
|
|
130
|
+
is still serving a cached module: remove and re-add it in the module manager.
|
|
116
131
|
|
|
117
132
|
## Known limits
|
|
118
133
|
|
package/mod.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// PhimPal TV v0.2.
|
|
1
|
+
// PhimPal TV v0.2.3 — 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 {
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
// ------------------------------------------------------------------ config
|
|
15
15
|
const CFG = {
|
|
16
|
+
version: '0.2.3',
|
|
16
17
|
keyboardEmulation: false, // remote key codes only
|
|
17
18
|
debug: (function () { try { return localStorage.getItem('tv-debug') === '1'; } catch (e) { return false; } })(), // badge off unless asked
|
|
18
19
|
hideCursor: true, // the D-pad replaces the pointer
|
|
20
|
+
lowGfx: (function () { try { return localStorage.getItem('tv-gfx') !== 'full'; } catch (e) { return true; } })(), // flat rendering unless tv-gfx=full
|
|
19
21
|
seekSteps: [10, 30, 60, 300], // seconds per press, accelerates while you keep pressing
|
|
20
22
|
seekAccelWindow: 800, // ms between presses to accelerate
|
|
21
23
|
chanSeek: 300, // Channel up/down step inside seek mode, seconds
|
|
@@ -67,6 +69,28 @@
|
|
|
67
69
|
} catch (x) { /* ignore */ }
|
|
68
70
|
}
|
|
69
71
|
const safe = (where, fn) => function () { try { return fn.apply(this, arguments); } catch (e) { report(where, e); } };
|
|
72
|
+
|
|
73
|
+
// Black box: the last steps are written to localStorage as they happen. If the previous
|
|
74
|
+
// session died mid-key (frozen page, app killed), the next launch shows that tail on screen.
|
|
75
|
+
const TRACE_KEY = 'tv-trace', TRACE_MAX = 40;
|
|
76
|
+
let traceBuf = [];
|
|
77
|
+
function trace(msg) {
|
|
78
|
+
traceBuf.push((Date.now() % 100000) + ' ' + msg);
|
|
79
|
+
if (traceBuf.length > TRACE_MAX) traceBuf.shift();
|
|
80
|
+
try { localStorage.setItem(TRACE_KEY, traceBuf.join('\n')); } catch (e) { /* ignore */ }
|
|
81
|
+
}
|
|
82
|
+
function showPreviousTrace() {
|
|
83
|
+
let prev = null; try { prev = localStorage.getItem(TRACE_KEY); } catch (e) { /* ignore */ }
|
|
84
|
+
if (!prev || !/\bkey \d+ start\b(?![\s\S]*\bkey end\b)/.test(prev)) return; // previous session ended cleanly
|
|
85
|
+
const tail = prev.split('\n').slice(-12).join('\n');
|
|
86
|
+
try {
|
|
87
|
+
let b = $('#tv-error');
|
|
88
|
+
if (!b) { b = document.createElement('pre'); b.id = 'tv-error'; document.body.appendChild(b); }
|
|
89
|
+
b.textContent = 'phimpal-tv — previous session ended mid-key:\n' + tail;
|
|
90
|
+
b.style.opacity = '1';
|
|
91
|
+
clearTimeout(errTimer); errTimer = setTimeout(() => { b.style.opacity = '0'; }, 25000);
|
|
92
|
+
} catch (e) { /* ignore */ }
|
|
93
|
+
}
|
|
70
94
|
const rectOf = (el) => el.getBoundingClientRect();
|
|
71
95
|
const isWatch = () => /^\/watch\//.test(location.pathname);
|
|
72
96
|
const isHome = () => location.pathname === '/';
|
|
@@ -187,14 +211,23 @@
|
|
|
187
211
|
}
|
|
188
212
|
const hoverMenuOpen = () => !!S.hoverTrigger && !!currentScope(); // aria-expanded goes stale, the rendered menu doesn't
|
|
189
213
|
|
|
190
|
-
// dialogs / menus that should capture navigation
|
|
214
|
+
// dialogs / menus that should capture navigation.
|
|
215
|
+
// A scope must be on screen AND contain something to focus: an element that merely
|
|
216
|
+
// carries role="dialog" (a toast, an off-screen drawer, a banner) would otherwise trap
|
|
217
|
+
// every key — arrows find nothing inside it and Back cannot close it. Scopes that refuse
|
|
218
|
+
// to close are blacklisted for the rest of the page.
|
|
191
219
|
const subPanel = () => $('.vjs-subtitle-panel.is-open');
|
|
220
|
+
const deadScopes = new WeakSet();
|
|
192
221
|
function currentScope() {
|
|
193
222
|
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"]');
|
|
194
223
|
const dialog = list.find((e) => {
|
|
224
|
+
if (deadScopes.has(e)) return false;
|
|
195
225
|
if (e.closest('.video-js') && !e.classList.contains('vjs-modal-dialog')) return false;
|
|
226
|
+
if (e.closest('aside')) return false; // mobile drawer, parked off-screen with a transform
|
|
196
227
|
const r = rectOf(e);
|
|
197
|
-
|
|
228
|
+
if (!(r.width > 0 && r.height > 0 && styleVisible(e))) return false;
|
|
229
|
+
if (r.right <= 0 || r.bottom <= 0 || r.left >= innerWidth || r.top >= innerHeight) return false; // off-screen
|
|
230
|
+
return $$(CAND, e).some((c) => c !== e && isVisibleBlock(c)); // has something to focus
|
|
198
231
|
});
|
|
199
232
|
if (dialog) return dialog; // a caption-settings modal opens on top of the subtitle panel
|
|
200
233
|
const sp = subPanel();
|
|
@@ -289,15 +322,19 @@
|
|
|
289
322
|
if (!canClick(btn)) return false;
|
|
290
323
|
const item = block.closest('.shrink-0') || block;
|
|
291
324
|
const sib = dir === A.RIGHT ? item.nextElementSibling : item.previousElementSibling;
|
|
325
|
+
trace('page click ' + dir);
|
|
292
326
|
btn.click();
|
|
327
|
+
trace('page clicked');
|
|
293
328
|
setTimeout(safe('page', () => {
|
|
294
329
|
let t = sib && (sib.matches('.group') ? sib : sib.querySelector('.group'));
|
|
295
330
|
if (!(t && isVisibleBlock(t))) {
|
|
296
331
|
const vis = withScanCache(() => $$('.group', car.clip).filter(isVisibleBlock));
|
|
297
332
|
t = dir === A.RIGHT ? vis[0] : vis[vis.length - 1];
|
|
298
333
|
}
|
|
334
|
+
trace('page settled target=' + describe(t));
|
|
299
335
|
if (t) setFocus(t); else badge('page: no card');
|
|
300
|
-
|
|
336
|
+
trace('page done');
|
|
337
|
+
}), CFG.lowGfx ? 80 : CFG.carouselAnimMs); // no transition to wait for in low-gfx mode
|
|
301
338
|
return true;
|
|
302
339
|
}
|
|
303
340
|
function rowsOnPage() {
|
|
@@ -326,6 +363,7 @@
|
|
|
326
363
|
}
|
|
327
364
|
function setFocus(block, opts = {}) {
|
|
328
365
|
if (!block) return;
|
|
366
|
+
trace('focus ' + describe(block));
|
|
329
367
|
if (S.focused !== block) clearFocus();
|
|
330
368
|
S.focused = block;
|
|
331
369
|
if (isCard(block)) { block.classList.add('tv-focus'); (popupOf(block) || block).classList.add('tv-focus-ring'); }
|
|
@@ -697,7 +735,8 @@
|
|
|
697
735
|
const scope = currentScope();
|
|
698
736
|
const blocks = collectBlocks(scope);
|
|
699
737
|
let t = pickTarget(f, dir, blocks);
|
|
700
|
-
|
|
738
|
+
trace('move ' + dir + ' scope=' + describe(scope) + ' blocks=' + blocks.length + ' target=' + describe(t));
|
|
739
|
+
if (!t && (dir === A.LEFT || dir === A.RIGHT) && pageCarousel(f, dir)) { trace('paging ' + dir); return; }
|
|
701
740
|
if (!t && dir === A.UP && !scope && scrollY > 0) {
|
|
702
741
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
703
742
|
setTimeout(() => { const t2 = pickTarget(S.focused, A.UP, collectBlocks()); if (t2) setFocus(t2); }, 450);
|
|
@@ -715,7 +754,9 @@
|
|
|
715
754
|
if (scope && scope.classList.contains('vjs-subtitle-panel')) return closeSubPanel();
|
|
716
755
|
const hoverT = S.hoverTrigger;
|
|
717
756
|
const finish = () => {
|
|
718
|
-
|
|
757
|
+
const still = currentScope();
|
|
758
|
+
if (still === scope) { deadScopes.add(scope); trace('scope refused to close, ignoring it'); } // never trap the user
|
|
759
|
+
else if (still) return; // a different dialog is open, stay in it
|
|
719
760
|
if (S.returnMode) { const m = S.returnMode; S.returnMode = null; return m === 'PLAYER' ? enterPlayer() : enterBar(); }
|
|
720
761
|
const back = S.beforeScope || hoverT; S.beforeScope = null;
|
|
721
762
|
if (back && isVisibleBlock(back)) return setFocus(back, { noHover: true }); // don't re-open a hover menu
|
|
@@ -769,8 +810,16 @@
|
|
|
769
810
|
badge('reset: ' + why, true);
|
|
770
811
|
}
|
|
771
812
|
function onKeySafe(e) {
|
|
813
|
+
const known = TIZEN_KEYS[e.keyCode] || (CFG.keyboardEmulation && KB_KEYS[e.keyCode]);
|
|
814
|
+
if (known) trace('key ' + e.keyCode + ' start mode=' + S.mode + ' sub=' + S.sub + ' focus=' + describe(S.focused));
|
|
772
815
|
try { onKey(e); }
|
|
773
816
|
catch (err) { report('key ' + e.keyCode, err); try { hardReset('error'); } catch (x) { report('reset', x); } }
|
|
817
|
+
if (known) trace('key end mode=' + S.mode + ' focus=' + describe(S.focused));
|
|
818
|
+
}
|
|
819
|
+
function describe(el) {
|
|
820
|
+
if (!el) return 'none';
|
|
821
|
+
const a = el.tagName === 'A' ? el : (el.querySelector && el.querySelector('a[href]'));
|
|
822
|
+
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) + ']' : '');
|
|
774
823
|
}
|
|
775
824
|
function onKey(e) {
|
|
776
825
|
if (e.__tv) return;
|
|
@@ -810,6 +859,7 @@
|
|
|
810
859
|
if (S.mode !== 'NAV') { S.returnMode = S.mode === 'SEEK' ? 'PLAYER' : S.mode; clearSeek(); S.mode = 'NAV'; S.sub = null; S.menu = null; }
|
|
811
860
|
if (!S.focused || !scope.contains(S.focused)) {
|
|
812
861
|
const bl = collectBlocks(scope);
|
|
862
|
+
trace('scope grabbed key: ' + describe(scope) + ' blocks=' + bl.length);
|
|
813
863
|
if (bl.length) { S.beforeScope = S.focused; setFocus(bl[0]); if (act !== A.BACK) return; }
|
|
814
864
|
}
|
|
815
865
|
} else if (S.returnMode) {
|
|
@@ -914,7 +964,15 @@
|
|
|
914
964
|
pointer-events: none; opacity: 0; transition: opacity .3s; }
|
|
915
965
|
#tv-badge { position: fixed; top: 12px; right: 12px; z-index: 2147483647; font: 13px/1.2 ui-monospace, monospace; padding: 6px 10px;
|
|
916
966
|
background: rgba(0,0,0,.75); color: #fff; border-radius: 6px; pointer-events: none; opacity: 0; transition: opacity .3s; white-space: pre; }
|
|
917
|
-
` + (CFG.hideCursor ? '\n html, body, * { cursor: none !important; }' : '')
|
|
967
|
+
` + (CFG.hideCursor ? '\n html, body, * { cursor: none !important; }' : '')
|
|
968
|
+
+ (CFG.lowGfx ? `
|
|
969
|
+
/* TV compositors stall on backdrop-filter, large filters and animated transforms over
|
|
970
|
+
a dozen 4K posters: render everything flat and instant. */
|
|
971
|
+
*, *::before, *::after { transition: none !important; animation-duration: 0s !important;
|
|
972
|
+
backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
|
|
973
|
+
.group.tv-focus .group-hover\\:block, .video-js, .video-js * { filter: none !important; box-shadow: none !important; }
|
|
974
|
+
header { background: rgba(10, 12, 20, .96) !important; }
|
|
975
|
+
.tv-focus-ring { box-shadow: none; }` : '');
|
|
918
976
|
// Chromium < 90 ignores `overflow-x: clip`, which would spill every carousel across the
|
|
919
977
|
// page; `hidden` is the closest thing it understands.
|
|
920
978
|
let clipOk = true; try { clipOk = CSS.supports('overflow-x', 'clip'); } catch (e) { clipOk = false; }
|
|
@@ -936,6 +994,10 @@
|
|
|
936
994
|
function init() {
|
|
937
995
|
if (window.__tvNav && window.__tvNav.destroy) window.__tvNav.destroy(); // hot re-install (dev)
|
|
938
996
|
injectCSS();
|
|
997
|
+
showPreviousTrace();
|
|
998
|
+
trace('init ' + CFG.version + ' ' + location.pathname + ' ' + innerWidth + 'x' + innerHeight);
|
|
999
|
+
// always announce the running build once, so a stale module cache is obvious
|
|
1000
|
+
setTimeout(() => badge('PhimPal TV ' + CFG.version + (CFG.lowGfx ? ' · low-gfx' : ''), true), 800);
|
|
939
1001
|
addEventListener('keydown', onKeySafe, true);
|
|
940
1002
|
patchHistory();
|
|
941
1003
|
const onRouteSafe = safe('route', checkRoute);
|
package/package.json
CHANGED