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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/mod.js +69 -7
  3. 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 — generated by build.js from phimpal-tv.user.js, do not edit.
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
- return r.width > 0 && r.height > 0 && styleVisible(e);
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
- }), CFG.carouselAnimMs);
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
- if (!t && (dir === A.LEFT || dir === A.RIGHT) && pageCarousel(f, dir)) return;
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
- if (currentScope()) return; // still open, stay in scope
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phimpal-tv",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "D-pad (TV remote) navigation for phimpal.com on Samsung Tizen TVs, as a TizenBrew site-modification module.",
5
5
  "appName": "PhimPal TV",
6
6
  "packageType": "mods",