phimpal-tv 0.2.1 → 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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/mod.js +55 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -113,6 +113,13 @@ 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.
116
123
 
117
124
  ## Known limits
118
125
 
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.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 {
@@ -67,6 +67,28 @@
67
67
  } catch (x) { /* ignore */ }
68
68
  }
69
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
+ }
70
92
  const rectOf = (el) => el.getBoundingClientRect();
71
93
  const isWatch = () => /^\/watch\//.test(location.pathname);
72
94
  const isHome = () => location.pathname === '/';
@@ -187,14 +209,23 @@
187
209
  }
188
210
  const hoverMenuOpen = () => !!S.hoverTrigger && !!currentScope(); // aria-expanded goes stale, the rendered menu doesn't
189
211
 
190
- // 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.
191
217
  const subPanel = () => $('.vjs-subtitle-panel.is-open');
218
+ const deadScopes = new WeakSet();
192
219
  function currentScope() {
193
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"]');
194
221
  const dialog = list.find((e) => {
222
+ if (deadScopes.has(e)) return false;
195
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
196
225
  const r = rectOf(e);
197
- return r.width > 0 && r.height > 0 && styleVisible(e);
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
198
229
  });
199
230
  if (dialog) return dialog; // a caption-settings modal opens on top of the subtitle panel
200
231
  const sp = subPanel();
@@ -289,14 +320,18 @@
289
320
  if (!canClick(btn)) return false;
290
321
  const item = block.closest('.shrink-0') || block;
291
322
  const sib = dir === A.RIGHT ? item.nextElementSibling : item.previousElementSibling;
323
+ trace('page click ' + dir);
292
324
  btn.click();
325
+ trace('page clicked');
293
326
  setTimeout(safe('page', () => {
294
327
  let t = sib && (sib.matches('.group') ? sib : sib.querySelector('.group'));
295
328
  if (!(t && isVisibleBlock(t))) {
296
329
  const vis = withScanCache(() => $$('.group', car.clip).filter(isVisibleBlock));
297
330
  t = dir === A.RIGHT ? vis[0] : vis[vis.length - 1];
298
331
  }
332
+ trace('page settled target=' + describe(t));
299
333
  if (t) setFocus(t); else badge('page: no card');
334
+ trace('page done');
300
335
  }), CFG.carouselAnimMs);
301
336
  return true;
302
337
  }
@@ -326,6 +361,7 @@
326
361
  }
327
362
  function setFocus(block, opts = {}) {
328
363
  if (!block) return;
364
+ trace('focus ' + describe(block));
329
365
  if (S.focused !== block) clearFocus();
330
366
  S.focused = block;
331
367
  if (isCard(block)) { block.classList.add('tv-focus'); (popupOf(block) || block).classList.add('tv-focus-ring'); }
@@ -697,7 +733,8 @@
697
733
  const scope = currentScope();
698
734
  const blocks = collectBlocks(scope);
699
735
  let t = pickTarget(f, dir, blocks);
700
- if (!t && (dir === A.LEFT || dir === A.RIGHT) && pageCarousel(f, dir)) return;
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; }
701
738
  if (!t && dir === A.UP && !scope && scrollY > 0) {
702
739
  window.scrollTo({ top: 0, behavior: 'smooth' });
703
740
  setTimeout(() => { const t2 = pickTarget(S.focused, A.UP, collectBlocks()); if (t2) setFocus(t2); }, 450);
@@ -715,7 +752,9 @@
715
752
  if (scope && scope.classList.contains('vjs-subtitle-panel')) return closeSubPanel();
716
753
  const hoverT = S.hoverTrigger;
717
754
  const finish = () => {
718
- if (currentScope()) return; // still open, stay in scope
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
719
758
  if (S.returnMode) { const m = S.returnMode; S.returnMode = null; return m === 'PLAYER' ? enterPlayer() : enterBar(); }
720
759
  const back = S.beforeScope || hoverT; S.beforeScope = null;
721
760
  if (back && isVisibleBlock(back)) return setFocus(back, { noHover: true }); // don't re-open a hover menu
@@ -769,8 +808,16 @@
769
808
  badge('reset: ' + why, true);
770
809
  }
771
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));
772
813
  try { onKey(e); }
773
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) + ']' : '');
774
821
  }
775
822
  function onKey(e) {
776
823
  if (e.__tv) return;
@@ -810,6 +857,7 @@
810
857
  if (S.mode !== 'NAV') { S.returnMode = S.mode === 'SEEK' ? 'PLAYER' : S.mode; clearSeek(); S.mode = 'NAV'; S.sub = null; S.menu = null; }
811
858
  if (!S.focused || !scope.contains(S.focused)) {
812
859
  const bl = collectBlocks(scope);
860
+ trace('scope grabbed key: ' + describe(scope) + ' blocks=' + bl.length);
813
861
  if (bl.length) { S.beforeScope = S.focused; setFocus(bl[0]); if (act !== A.BACK) return; }
814
862
  }
815
863
  } else if (S.returnMode) {
@@ -936,6 +984,8 @@
936
984
  function init() {
937
985
  if (window.__tvNav && window.__tvNav.destroy) window.__tvNav.destroy(); // hot re-install (dev)
938
986
  injectCSS();
987
+ showPreviousTrace();
988
+ trace('init ' + location.pathname + ' ' + innerWidth + 'x' + innerHeight);
939
989
  addEventListener('keydown', onKeySafe, true);
940
990
  patchHistory();
941
991
  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.2",
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",