@hatfiller/tizenpstream 1.0.0 → 1.0.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 CHANGED
@@ -5,7 +5,8 @@ TizenBrew **site modification** module for [P-Stream](https://pstream.mov) on Sa
5
5
  ## Install
6
6
 
7
7
  1. Install [TizenBrew](https://github.com/reisxd/TizenBrew) on your Samsung Tizen TV.
8
- 2. In TizenBrew’s module manager, add `@hatfiller/tizenpstream` as an NPM module.
8
+ 2. **App (launcher):** In the module manager add **`tizenpstream-app`** (no @). This gives you a **PStream** tile; opening it launches pstream.mov in the app view so you can navigate with the remote like a normal Tizen app.
9
+ 3. **Mod (navigation + keys):** Add **`@hatfiller/tizenpstream`** so D-pad, Enter, Back, and media keys work on pstream.mov. Enter exactly **`@hatfiller/tizenpstream`** (no `npm/` in front). If you see “unknown module”, try **`hatfiller/tizenpstream`** (without `@`).
9
10
 
10
11
  ## What it does
11
12
 
@@ -16,10 +17,20 @@ TizenBrew **site modification** module for [P-Stream](https://pstream.mov) on Sa
16
17
 
17
18
  The script is written in ES5-style JS for compatibility with older Tizen WebKit.
18
19
 
20
+ ### See your TV’s Tizen / device info (debug)
21
+
22
+ - **Once on load:** A small debug overlay appears for about 6 seconds with your TV’s user agent and Tizen build info (if available).
23
+ - **Anytime:** Press the **0** key on the remote to show or hide the same overlay (UA, Tizen API, build). Press **0** or **Back** again to close it.
24
+
19
25
  ## Module type
20
26
 
21
27
  This is a **site modification** module (`packageType: "mods"`). It injects `mods/userScript.js` (or `dist/userScript.js`) into `https://pstream.mov` when you open it via TizenBrew.
22
28
 
29
+ ## Repo layout
30
+
31
+ - **Mod (this package):** `@hatfiller/tizenpstream` — injects into pstream.mov for remote navigation and debug overlay.
32
+ - **App launcher:** `tizen-pstream-app/` — package **`tizenpstream-app`** (unscoped); add it in TizenBrew for a PStream tile. Publish from that folder with `npm publish --access public`.
33
+
23
34
  ## Build
24
35
 
25
36
  ```bash
@@ -16,8 +16,40 @@
16
16
  /** @type {HTMLVideoElement | null} */
17
17
  var lastKnownVideo = null;
18
18
 
19
+ var FOCUS_CLASS = 'tizenpstream-focus';
20
+ var lastFocused = null;
21
+
22
+ function injectFocusStyles() {
23
+ if (document.getElementById('tizenpstream-focus-styles')) return;
24
+ var style = document.createElement('style');
25
+ style.id = 'tizenpstream-focus-styles';
26
+ style.textContent = [
27
+ '*:focus { outline: 4px solid #00d4ff !important; outline-offset: 2px !important; box-shadow: 0 0 0 6px rgba(0,212,255,0.4) !important; }',
28
+ '.' + FOCUS_CLASS + ' { outline: 4px solid #00d4ff !important; outline-offset: 2px !important; box-shadow: 0 0 0 6px rgba(0,212,255,0.4) !important; }',
29
+ 'video.' + FOCUS_CLASS + ' { outline: 4px solid #00d4ff !important; }'
30
+ ].join('\n');
31
+ (document.head || document.documentElement).appendChild(style);
32
+ }
33
+
34
+ function makeFocusable(el) {
35
+ if (el && !el.disabled && el.getAttribute('tabindex') === null) {
36
+ try { el.setAttribute('tabindex', '0'); } catch (err) {}
37
+ }
38
+ }
39
+
40
+ function ensureClickablesFocusable() {
41
+ var sel = '[onclick], [role="button"], [role="link"], [role="option"], [data-href], [data-link], [data-action], a, button, [tabindex]';
42
+ var nodes = document.querySelectorAll(sel);
43
+ for (var i = 0; i < nodes.length; i++) {
44
+ var n = nodes[i];
45
+ if (n.offsetParent !== null && (n.getAttribute('tabindex') === null || n.getAttribute('tabindex') === '')) {
46
+ makeFocusable(n);
47
+ }
48
+ }
49
+ }
50
+
19
51
  function getFocusableSelector() {
20
- return 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select, textarea, [onclick], video';
52
+ return 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select, textarea, [onclick], [role="button"], [role="link"], [role="option"], [data-href], video';
21
53
  }
22
54
 
23
55
  function getFocusables(container) {
@@ -26,13 +58,29 @@
26
58
  var list = [];
27
59
  for (var i = 0; i < nodes.length; i++) {
28
60
  var n = nodes[i];
29
- if (n.offsetParent !== null && !n.disabled) {
61
+ if (n.offsetParent !== null && !n.disabled && n.offsetWidth > 0 && n.offsetHeight > 0) {
30
62
  list.push(n);
31
63
  }
32
64
  }
33
65
  return list;
34
66
  }
35
67
 
68
+ function setFocusRing(el) {
69
+ if (lastFocused && lastFocused !== el) {
70
+ lastFocused.classList.remove(FOCUS_CLASS);
71
+ }
72
+ lastFocused = el;
73
+ if (el) {
74
+ el.classList.add(FOCUS_CLASS);
75
+ el.focus();
76
+ try {
77
+ el.scrollIntoView({ block: 'nearest', behavior: 'auto', inline: 'nearest' });
78
+ } catch (err) {
79
+ el.scrollIntoView(true);
80
+ }
81
+ }
82
+ }
83
+
36
84
  function getVisibleCenter(el) {
37
85
  var r = el.getBoundingClientRect();
38
86
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
@@ -62,10 +110,98 @@
62
110
  best = cand;
63
111
  }
64
112
  }
65
- return best;
113
+ if (best) return best;
114
+ var idx = focusables.indexOf(current);
115
+ if (idx === -1) return focusables[0] || null;
116
+ if (dir === 'right' || dir === 'down') return focusables[idx + 1] || focusables[0] || null;
117
+ if (dir === 'left' || dir === 'up') return focusables[idx - 1] || focusables[focusables.length - 1] || null;
118
+ return null;
119
+ }
120
+
121
+ function registerTVKeys() {
122
+ try {
123
+ if (typeof tizen !== 'undefined' && tizen.tvinputdevice) {
124
+ var keys = [
125
+ 'MediaPlayPause', 'MediaPlay', 'MediaPause', 'MediaStop',
126
+ 'MediaFastForward', 'MediaRewind', 'MediaTrackNext', 'MediaTrackPrevious'
127
+ ];
128
+ for (var i = 0; i < keys.length; i++) {
129
+ try {
130
+ tizen.tvinputdevice.registerKey(keys[i]);
131
+ } catch (err) {}
132
+ }
133
+ }
134
+ } catch (err) {}
135
+ }
136
+
137
+ function trackVideo() {
138
+ var v = document.querySelector('video');
139
+ if (v && v !== lastKnownVideo) {
140
+ lastKnownVideo = v;
141
+ v.addEventListener('play', function () {
142
+ lastKnownVideo = v;
143
+ });
144
+ }
145
+ }
146
+
147
+ var KEY_0 = 48;
148
+ var debugOverlay = null;
149
+
150
+ function getTizenDebugInfo(cb) {
151
+ var out = { userAgent: navigator.userAgent, tizen: 'no' };
152
+ try {
153
+ if (typeof tizen !== 'undefined') {
154
+ out.tizen = 'yes';
155
+ if (tizen.systeminfo && tizen.systeminfo.getPropertyValue) {
156
+ tizen.systeminfo.getPropertyValue('BUILD', function (build) {
157
+ out.build = typeof build === 'object' ? JSON.stringify(build) : String(build);
158
+ cb(out);
159
+ }, function () { cb(out); });
160
+ return;
161
+ }
162
+ }
163
+ } catch (e) {
164
+ out.error = String(e.message || e);
165
+ }
166
+ cb(out);
167
+ }
168
+
169
+ function showDebugOverlay() {
170
+ getTizenDebugInfo(function (info) {
171
+ if (debugOverlay && debugOverlay.parentNode) {
172
+ debugOverlay.parentNode.removeChild(debugOverlay);
173
+ debugOverlay = null;
174
+ return;
175
+ }
176
+ var lines = [
177
+ 'PStream mod debug',
178
+ 'UA: ' + (info.userAgent || '').substring(0, 80),
179
+ 'Tizen: ' + info.tizen,
180
+ info.build ? 'Build: ' + info.build : '',
181
+ info.error ? 'Err: ' + info.error : ''
182
+ ].filter(Boolean);
183
+ var div = document.createElement('div');
184
+ div.id = 'tizenpstream-debug-overlay';
185
+ div.style.cssText = 'position:fixed;top:20px;left:20px;right:20px;max-width:90%;background:rgba(0,0,0,0.9);color:#0f0;font:14px monospace;padding:12px;z-index:999999;border:2px solid #0f0;white-space:pre-wrap;word-break:break-all;';
186
+ div.textContent = lines.join('\n');
187
+ div.addEventListener('keydown', function (e) {
188
+ if (e.keyCode === KEY_0 || e.keyCode === KEY_BACK) {
189
+ if (div.parentNode) div.parentNode.removeChild(div);
190
+ debugOverlay = null;
191
+ }
192
+ });
193
+ document.body.appendChild(div);
194
+ debugOverlay = div;
195
+ div.focus();
196
+ });
66
197
  }
67
198
 
68
199
  function handleKeydown(e) {
200
+ if (e.keyCode === KEY_0) {
201
+ showDebugOverlay();
202
+ e.preventDefault();
203
+ return false;
204
+ }
69
205
  var key = e.keyCode;
70
206
  var dir = null;
71
207
  if (key === KEY_LEFT) dir = 'left';
@@ -76,12 +212,12 @@
76
212
  if (dir) {
77
213
  var focusables = getFocusables();
78
214
  var current = document.activeElement;
79
- if (!current || current === document.body) {
215
+ if (!current || current === document.body || focusables.indexOf(current) === -1) {
80
216
  current = focusables[0] || null;
81
217
  }
82
218
  var next = findNeighbor(current, focusables, dir);
83
219
  if (next) {
84
- next.focus();
220
+ setFocusRing(next);
85
221
  e.preventDefault();
86
222
  e.stopPropagation();
87
223
  return false;
@@ -90,12 +226,25 @@
90
226
 
91
227
  if (key === KEY_ENTER) {
92
228
  var target = document.activeElement;
93
- if (target && target.tagName === 'VIDEO') {
229
+ if (!target || target === document.body) {
230
+ var focusables = getFocusables();
231
+ if (focusables[0]) setFocusRing(focusables[0]);
232
+ e.preventDefault();
233
+ return false;
234
+ }
235
+ if (target.tagName === 'VIDEO') {
94
236
  if (target.paused) target.play();
95
237
  else target.pause();
96
238
  e.preventDefault();
97
239
  return false;
98
240
  }
241
+ if (target.tagName === 'A' || target.getAttribute('role') === 'button' || target.getAttribute('role') === 'link' || target.onclick || target.getAttribute('onclick')) {
242
+ try {
243
+ target.click();
244
+ } catch (err) {}
245
+ e.preventDefault();
246
+ return false;
247
+ }
99
248
  }
100
249
 
101
250
  if (key === KEY_BACK) {
@@ -109,37 +258,31 @@
109
258
  return true;
110
259
  }
111
260
 
112
- function registerTVKeys() {
113
- try {
114
- if (typeof tizen !== 'undefined' && tizen.tvinputdevice) {
115
- var keys = [
116
- 'MediaPlayPause', 'MediaPlay', 'MediaPause', 'MediaStop',
117
- 'MediaFastForward', 'MediaRewind', 'MediaTrackNext', 'MediaTrackPrevious'
118
- ];
119
- for (var i = 0; i < keys.length; i++) {
120
- try {
121
- tizen.tvinputdevice.registerKey(keys[i]);
122
- } catch (err) {}
123
- }
124
- }
125
- } catch (err) {}
126
- }
127
-
128
- function trackVideo() {
129
- var v = document.querySelector('video');
130
- if (v && v !== lastKnownVideo) {
131
- lastKnownVideo = v;
132
- v.addEventListener('play', function () {
133
- lastKnownVideo = v;
134
- });
135
- }
136
- }
137
-
261
+ var debugShownOnce = false;
138
262
  function init() {
263
+ injectFocusStyles();
264
+ ensureClickablesFocusable();
139
265
  registerTVKeys();
140
266
  document.addEventListener('keydown', handleKeydown, true);
141
267
  trackVideo();
142
268
  setInterval(trackVideo, 2000);
269
+ setInterval(ensureClickablesFocusable, 3000);
270
+ setTimeout(function () {
271
+ var focusables = getFocusables();
272
+ if (focusables[0]) setFocusRing(focusables[0]);
273
+ }, 500);
274
+ if (!debugShownOnce) {
275
+ debugShownOnce = true;
276
+ setTimeout(function () {
277
+ showDebugOverlay();
278
+ setTimeout(function () {
279
+ if (debugOverlay && debugOverlay.parentNode) {
280
+ debugOverlay.parentNode.removeChild(debugOverlay);
281
+ debugOverlay = null;
282
+ }
283
+ }, 6000);
284
+ }, 1500);
285
+ }
143
286
  }
144
287
 
145
288
  if (document.readyState === 'loading') {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hatfiller/tizenpstream",
3
3
  "appName": "PStream",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "description": "TizenBrew module for P-Stream (pstream.mov) on Samsung Tizen TVs — TV remote support and focus handling for pre-2022 and older Tizen.",
6
6
  "packageType": "mods",
7
7
  "websiteURL": "https://pstream.mov",
@@ -15,6 +15,7 @@
15
15
  "type": "git",
16
16
  "url": "git+https://github.com/perlytiara/TizenPStream.git"
17
17
  },
18
+ "keywords": ["tizenbrew", "tizen", "pstream", "samsung-tv"],
18
19
  "keys": [
19
20
  "MediaPlayPause",
20
21
  "MediaPlay",