nuvio-tizen 1.2.0 → 1.3.0

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
@@ -19,11 +19,19 @@ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
19
19
  - **Stream aggregation** across all installed stream addons.
20
20
  - **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
21
21
  timed overlay; press **UP** during playback to pick a track (or turn them Off).
22
+ Configurable in Settings: **default language** (auto-loads a matching track),
23
+ **size** (S/M/L/XL) and **style** (white+shadow / black box / yellow).
24
+ - **Live streams** (sports / IPTV): live HLS and Stremio `tv`-type sources are detected
25
+ (unknown/infinite duration), shown with a **● LIVE** badge, and correctly skip
26
+ seek / resume / progress-tracking. Stream-only live addons with no `meta` resource still
27
+ open (the catalog entry is used as a fallback).
22
28
  - Playback via **AVPlay** (HLS/DASH/MP4/progressive) with an HTML5 `<video>` fallback
23
29
  when run in a desktop browser for development.
24
30
  - OSD with seek (±10s / ±30s), resume, and per-title progress saved to `localStorage`.
25
31
  - Full **D-pad + media-key** remote navigation (custom spatial focus engine).
26
- - Settings screen to add/remove addon manifest URLs (the add field is prefilled with `https://`).
32
+ - Settings: add addons on a **dedicated page** (field kept above the on-screen keyboard;
33
+ nothing is saved unless you press Confirm), remove with a confirmation prompt, and
34
+ **Reset to default addons**.
27
35
 
28
36
  > **Note on WatchHub:** it's a "where to watch" addon — its results are Rent/Buy/Subscription
29
37
  > *links to external services*, not direct video URLs, so they show as non-playable. Add your
package/css/style.css CHANGED
@@ -89,6 +89,16 @@ html, body {
89
89
  .addon-item .a-url { font-size: 15px; color: #8b93a5; }
90
90
  .hint { color: #8b93a5; font-size: 18px; margin-top: 20px; line-height: 1.5; }
91
91
 
92
+ /* --- Add-addon page (field kept high, above the on-screen keyboard) --- */
93
+ .add-wrap { position: absolute; top: 120px; left: 60px; right: 60px; }
94
+ .add-wrap h2 { font-size: 36px; margin-bottom: 14px; }
95
+ .add-hint { font-size: 22px; color: #8b93a5; margin-bottom: 28px; line-height: 1.4; max-width: 1200px; }
96
+ .add-input { width: 100%; padding: 20px 24px; font-size: 32px; background: #0e1117; color: #fff; border: 4px solid #2a3346; border-radius: 12px; }
97
+ .add-input.focused { border-color: #818cf8; box-shadow: 0 0 0 2px rgba(129,140,248,0.5); }
98
+ .add-btn-row { margin-top: 34px; }
99
+ .add-btn-row .pill { min-width: 220px; text-align: center; }
100
+ .cfg-row .a-url { color: #6f7788; }
101
+
92
102
  /* --- Player OSD --- */
93
103
  #player-osd { position: absolute; left: 0; right: 0; bottom: 0; height: 220px; z-index: 20; padding: 40px 60px; background: linear-gradient(transparent, rgba(0,0,0,0.9)); }
94
104
  #osd-title { font-size: 30px; font-weight: bold; margin-bottom: 20px; }
@@ -96,6 +106,7 @@ html, body {
96
106
  #osd-buffer { position: absolute; top: 0; left: 0; height: 8px; background: rgba(255,255,255,0.35); border-radius: 4px; width: 0; }
97
107
  #osd-progress { position: absolute; top: 0; left: 0; height: 8px; background: #818cf8; border-radius: 4px; width: 0; z-index: 2; }
98
108
  #osd-times { display: flex; justify-content: space-between; margin-top: 10px; font-size: 20px; color: #cfd3dc; }
109
+ #osd-cur.live { color: #ff4d4d; font-weight: bold; letter-spacing: 1px; }
99
110
  #osd-hint { margin-top: 14px; font-size: 18px; color: #8b93a5; }
100
111
 
101
112
  /* --- Subtitles --- */
package/index.html CHANGED
@@ -17,6 +17,7 @@
17
17
  <div id="screen-detail" class="screen hidden"></div>
18
18
  <div id="screen-search" class="screen hidden"></div>
19
19
  <div id="screen-settings" class="screen hidden"></div>
20
+ <div id="screen-addaddon" class="screen hidden"></div>
20
21
  </div>
21
22
 
22
23
  <!-- Subtitle overlay (over the video plane, under the OSD) -->
package/js/app.js CHANGED
@@ -8,7 +8,7 @@ var App = (function () {
8
8
  function screenEl(name) { return U.byId('screen-' + name); }
9
9
 
10
10
  function showScreen(name) {
11
- ['home', 'detail', 'search', 'settings'].forEach(function (n) {
11
+ ['home', 'detail', 'search', 'settings', 'addaddon'].forEach(function (n) {
12
12
  screenEl(n).className = 'screen' + (n === name ? '' : ' hidden');
13
13
  });
14
14
  _current = name;
@@ -16,11 +16,11 @@ var App = (function () {
16
16
 
17
17
  function pushFrame() { _stack.push({ screen: _current, focus: Focus.current() }); }
18
18
 
19
- function openDetail(type, id) {
19
+ function openDetail(type, id, preview) {
20
20
  if (!id) { return; }
21
21
  pushFrame();
22
22
  showScreen('detail');
23
- Views.renderDetail(type, id);
23
+ Views.renderDetail(type, id, preview);
24
24
  }
25
25
 
26
26
  function openSettings() {
@@ -35,6 +35,12 @@ var App = (function () {
35
35
  Views.renderSearch();
36
36
  }
37
37
 
38
+ function openAddAddon() {
39
+ pushFrame();
40
+ showScreen('addaddon');
41
+ Views.renderAddAddon();
42
+ }
43
+
38
44
  function back() {
39
45
  if (Player.isActive()) { Player.exit(); return; }
40
46
  if (Views.isModalOpen()) { Views.closeModal(); return; }
@@ -78,7 +84,8 @@ var App = (function () {
78
84
 
79
85
  return {
80
86
  start: start, back: back,
81
- openDetail: openDetail, openSettings: openSettings, openSearch: openSearch
87
+ openDetail: openDetail, openSettings: openSettings, openSearch: openSearch,
88
+ openAddAddon: openAddAddon
82
89
  };
83
90
  })();
84
91
 
package/js/player.js CHANGED
@@ -4,6 +4,26 @@ var Player = (function () {
4
4
  'use strict';
5
5
 
6
6
  var PROGRESS_KEY = 'nuvio.progress';
7
+ var SUBCFG_KEY = 'nuvio.subcfg';
8
+ var SIZE_PX = { small: 28, medium: 36, large: 46, xlarge: 58 };
9
+
10
+ function getSubCfg() {
11
+ var d = U.store.get(SUBCFG_KEY, {}) || {};
12
+ return { lang: d.lang || 'off', size: d.size || 'medium', style: d.style || 'shadow' };
13
+ }
14
+ function setSubCfg(cfg) { U.store.set(SUBCFG_KEY, cfg); }
15
+
16
+ function styleSpan(span) {
17
+ var cfg = getSubCfg();
18
+ span.style.fontSize = (SIZE_PX[cfg.size] || 36) + 'px';
19
+ if (cfg.style === 'box') {
20
+ span.style.background = 'rgba(0,0,0,0.9)'; span.style.color = '#fff'; span.style.textShadow = 'none';
21
+ } else if (cfg.style === 'yellow') {
22
+ span.style.background = 'rgba(0,0,0,0.4)'; span.style.color = '#ffe14d'; span.style.textShadow = '0 2px 3px #000';
23
+ } else { // shadow (default)
24
+ span.style.background = 'rgba(0,0,0,0.55)'; span.style.color = '#fff'; span.style.textShadow = '0 2px 3px #000';
25
+ }
26
+ }
7
27
 
8
28
  var avplay = (window.webapis && webapis.avplay) ? webapis.avplay : null;
9
29
  var useAV = !!avplay;
@@ -18,6 +38,7 @@ var Player = (function () {
18
38
  var _ctx = null; // { id, type, name, poster, resumeSec }
19
39
  var _dur = 0; // seconds
20
40
  var _cur = 0; // seconds
41
+ var _isLive = false; // live stream (sports/IPTV): no fixed duration
21
42
  var _ticker = null;
22
43
  var _osdTimer = null;
23
44
  var _seekPending = 0;
@@ -44,7 +65,7 @@ var Player = (function () {
44
65
  function progressKey(ctx) { return ctx.type + ':' + ctx.id; }
45
66
 
46
67
  function saveProgress() {
47
- if (!_ctx || !_dur) { return; }
68
+ if (!_ctx || !_dur || _isLive) { return; }
48
69
  var all = progressAll();
49
70
  var k = progressKey(_ctx);
50
71
  if (_cur / _dur > 0.95 || _cur < 5) {
@@ -79,6 +100,16 @@ var Player = (function () {
79
100
 
80
101
  function renderOSD() {
81
102
  U.byId('osd-title').textContent = _ctx ? _ctx.name : '';
103
+ if (_isLive) {
104
+ U.byId('osd-bar').style.display = 'none';
105
+ var cur = U.byId('osd-cur');
106
+ cur.textContent = '● LIVE';
107
+ cur.className = 'live';
108
+ U.byId('osd-dur').textContent = '';
109
+ return;
110
+ }
111
+ U.byId('osd-bar').style.display = '';
112
+ U.byId('osd-cur').className = '';
82
113
  var pct = _dur > 0 ? (_cur / _dur * 100) : 0;
83
114
  U.byId('osd-progress').style.width = pct + '%';
84
115
  U.byId('osd-cur').textContent = U.fmtTime(_cur);
@@ -89,6 +120,8 @@ var Player = (function () {
89
120
  function play(ctx, onExit) {
90
121
  _ctx = ctx; _onExit = onExit;
91
122
  _active = true; _cur = 0; _dur = 0;
123
+ // Stremio "tv" type is a strong live hint; refined from duration after prepare.
124
+ _isLive = (ctx.type === 'tv') || !!ctx.isLive;
92
125
  _ctx.resumeSec = ctx.resumeSec || getResume(ctx.type, ctx.id);
93
126
 
94
127
  // reset subtitle state
@@ -110,17 +143,33 @@ var Player = (function () {
110
143
  if (!_active) { return; }
111
144
  _subTracks = tracks || [];
112
145
  updateHint();
113
- if (_subTracks.length) { U.toast(_subTracks.length + ' subtitle track(s) — press UP', 3000); }
146
+ if (!_subTracks.length) { return; }
147
+ var cfg = getSubCfg();
148
+ var match = null;
149
+ if (cfg.lang && cfg.lang !== 'off') {
150
+ match = _subTracks.find(function (t) {
151
+ var l = ('' + (t.lang || '')).toLowerCase();
152
+ return l && (l.indexOf(cfg.lang) === 0 || cfg.lang.indexOf(l) === 0);
153
+ });
154
+ }
155
+ if (match) { loadTrack(match); }
156
+ else { U.toast(_subTracks.length + ' subtitle track(s) — press UP', 3000); }
114
157
  }, function () {});
115
158
  }
116
159
 
117
160
  function updateHint() {
118
- var base = 'OK: Play/Pause ←/→: Seek RETURN: Back';
161
+ var base = _isLive ? 'OK: Play/Pause RETURN: Back' : 'OK: Play/Pause ←/→: Seek RETURN: Back';
119
162
  if (_subTracks.length) { base += ' UP: Subtitles'; }
120
163
  var h = U.byId('osd-hint');
121
164
  if (h) { h.textContent = base; }
122
165
  }
123
166
 
167
+ // Decides live vs VOD from the reported duration (0 / non-finite = live).
168
+ function markLiveFromDuration() {
169
+ if (_ctx && _ctx.type === 'tv') { _isLive = true; }
170
+ if (!_dur || _dur <= 0 || !isFinite(_dur)) { _isLive = true; }
171
+ }
172
+
124
173
  function startAV(url) {
125
174
  objEl.className = 'active';
126
175
  try {
@@ -141,7 +190,9 @@ var Player = (function () {
141
190
  try { avplay.setDisplayRect(0, 0, 1920, 1080); } catch (e1) {}
142
191
  try { avplay.setDisplayMethod('PLAYER_DISPLAY_MODE_LETTER_BOX'); } catch (e2) {}
143
192
  try { _dur = avplay.getDuration() / 1000; } catch (e3) { _dur = 0; }
144
- if (_ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
193
+ markLiveFromDuration();
194
+ updateHint();
195
+ if (!_isLive && _ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
145
196
  avplay.play();
146
197
  renderOSD();
147
198
  startTicker();
@@ -159,7 +210,9 @@ var Player = (function () {
159
210
  videoEl.src = url;
160
211
  videoEl.onloadedmetadata = function () {
161
212
  _dur = videoEl.duration || 0;
162
- if (_ctx.resumeSec > 5) { try { videoEl.currentTime = _ctx.resumeSec; } catch (e) {} }
213
+ markLiveFromDuration();
214
+ updateHint();
215
+ if (!_isLive && _ctx.resumeSec > 5) { try { videoEl.currentTime = _ctx.resumeSec; } catch (e) {} }
163
216
  U.spinner(false); showOSD(false);
164
217
  };
165
218
  videoEl.ontimeupdate = function () { _cur = videoEl.currentTime; renderOSD(); renderSubs(); };
@@ -197,6 +250,7 @@ var Player = (function () {
197
250
  }
198
251
 
199
252
  function seekBy(delta) {
253
+ if (_isLive) { showOSD(false); U.toast('Live — seeking unavailable', 1500); return; }
200
254
  var target = Math.max(0, Math.min((_dur || 1e9), _cur + delta));
201
255
  _cur = target;
202
256
  renderOSD();
@@ -243,7 +297,7 @@ var Player = (function () {
243
297
  if (show) {
244
298
  subsEl.className = '';
245
299
  var span = subsEl.firstChild;
246
- if (!span) { span = document.createElement('span'); subsEl.appendChild(span); }
300
+ if (!span) { span = document.createElement('span'); subsEl.appendChild(span); styleSpan(span); }
247
301
  if (span.textContent !== show) { span.textContent = show; }
248
302
  } else {
249
303
  subsEl.className = 'hidden';
@@ -361,6 +415,7 @@ var Player = (function () {
361
415
 
362
416
  return {
363
417
  init: init, play: play, handleKey: handleKey, isActive: isActive,
364
- exit: exit, continueList: continueList, getResume: getResume
418
+ exit: exit, continueList: continueList, getResume: getResume,
419
+ getSubCfg: getSubCfg, setSubCfg: setSubCfg
365
420
  };
366
421
  })();
package/js/views.js CHANGED
@@ -84,7 +84,7 @@ var Views = (function () {
84
84
  c.appendChild(U.el('div', 'fallback', meta.name || meta.title || ''));
85
85
  }
86
86
  c.appendChild(U.el('div', 'cap', meta.name || meta.title || ''));
87
- c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id); };
87
+ c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id, meta); };
88
88
  return c;
89
89
  }
90
90
 
@@ -162,8 +162,8 @@ var Views = (function () {
162
162
  // ================= DETAIL =================
163
163
  var _d = null; // { type, id, meta, mode, season }
164
164
 
165
- function renderDetail(type, id) {
166
- _d = { type: type, id: id, meta: null, mode: 'info', season: null };
165
+ function renderDetail(type, id, preview) {
166
+ _d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null };
167
167
  var s = scr('detail');
168
168
  U.clear(s);
169
169
  s.appendChild(U.el('div', 'detail-hero'));
@@ -174,8 +174,15 @@ var Views = (function () {
174
174
  drawDetail();
175
175
  }, function (err) {
176
176
  U.spinner(false);
177
- U.toast('Could not load details');
178
- App.back();
177
+ // Fallback: stream-only addons (e.g. live TV/sports) may expose no meta
178
+ // resource. Use the catalog preview so the title still opens & plays.
179
+ if (_d.preview) {
180
+ _d.meta = Object.assign({ id: id, type: type }, _d.preview);
181
+ drawDetail();
182
+ } else {
183
+ U.toast('Could not load details');
184
+ App.back();
185
+ }
179
186
  });
180
187
  }
181
188
 
@@ -391,12 +398,104 @@ var Views = (function () {
391
398
  });
392
399
  }
393
400
 
401
+ // ================= ADD ADDON (dedicated page) =================
402
+ function renderAddAddon() {
403
+ var s = scr('addaddon');
404
+ U.clear(s);
405
+ s.appendChild(header(false, false));
406
+
407
+ var wrap = U.el('div', 'add-wrap');
408
+ wrap.appendChild(U.el('h2', null, 'Add addon'));
409
+ wrap.appendChild(U.el('div', 'add-hint', 'Type a Stremio addon manifest URL, then choose Confirm. RETURN cancels — nothing is saved unless you confirm.'));
410
+
411
+ var input = document.createElement('input');
412
+ input.id = 'addaddon-input';
413
+ input.type = 'text';
414
+ input.className = 'focusable add-input';
415
+ input.value = 'https://';
416
+ input.placeholder = 'https://addon.example/manifest.json';
417
+ input.__onselect = function () {
418
+ input.focus();
419
+ var n = input.value.length;
420
+ try { input.setSelectionRange(n, n); } catch (e) {}
421
+ };
422
+ input.onkeydown = function (ev) {
423
+ if (ev.keyCode === 13) { if (ev.stopPropagation) { ev.stopPropagation(); } doConfirm(); }
424
+ };
425
+ wrap.appendChild(input);
426
+
427
+ var row = U.el('div', 'add-btn-row');
428
+ var confirmBtn = U.el('div', 'pill focusable', 'Confirm');
429
+ var cancelBtn = U.el('div', 'pill focusable', 'Cancel');
430
+ confirmBtn.__onselect = doConfirm;
431
+ cancelBtn.__onselect = function () { App.back(); };
432
+ row.appendChild(confirmBtn);
433
+ row.appendChild(cancelBtn);
434
+ wrap.appendChild(row);
435
+
436
+ s.appendChild(wrap);
437
+ Focus.setScope(s, input);
438
+
439
+ function doConfirm() {
440
+ var v = ('' + input.value).trim();
441
+ if (!v || v === 'https://' || v.indexOf('://') === -1 || v.length < 12) {
442
+ U.toast('Enter a full manifest URL');
443
+ return;
444
+ }
445
+ Stremio.addAddon(v);
446
+ U.toast('Addon added');
447
+ App.back();
448
+ renderSettings();
449
+ }
450
+ }
451
+
452
+ // ================= SUBTITLE CONFIG =================
453
+ var SUB_LANGS = [['off', 'Off'], ['eng', 'English'], ['spa', 'Spanish'], ['fre', 'French'],
454
+ ['ger', 'German'], ['ita', 'Italian'], ['por', 'Portuguese'], ['dut', 'Dutch'],
455
+ ['rus', 'Russian'], ['ara', 'Arabic'], ['tur', 'Turkish'], ['pol', 'Polish']];
456
+ var SUB_SIZES = [['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
457
+ var SUB_STYLES = [['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']];
458
+
459
+ function labelFor(list, v) {
460
+ for (var i = 0; i < list.length; i++) { if (list[i][0] === v) { return list[i][1]; } }
461
+ return v;
462
+ }
463
+ function nextVal(list, v) {
464
+ var i = 0;
465
+ for (; i < list.length; i++) { if (list[i][0] === v) { break; } }
466
+ return list[(i + 1) % list.length][0];
467
+ }
468
+
469
+ function subCycleRow(caption, list, key) {
470
+ var row = U.el('div', 'addon-item focusable cfg-row');
471
+ var name = U.el('div', 'a-name');
472
+ function render() { var cfg = Player.getSubCfg(); name.textContent = caption + ': ' + labelFor(list, cfg[key]); }
473
+ render();
474
+ row.appendChild(name);
475
+ row.appendChild(U.el('div', 'a-url', 'Press OK to change'));
476
+ row.__onselect = function () {
477
+ var cfg = Player.getSubCfg();
478
+ cfg[key] = nextVal(list, cfg[key]);
479
+ Player.setSubCfg(cfg);
480
+ render();
481
+ };
482
+ return row;
483
+ }
484
+
485
+ function buildSubtitleSection(wrap) {
486
+ wrap.appendChild(U.el('h2', null, 'Subtitles'));
487
+ wrap.appendChild(subCycleRow('Default language', SUB_LANGS, 'lang'));
488
+ wrap.appendChild(subCycleRow('Size', SUB_SIZES, 'size'));
489
+ wrap.appendChild(subCycleRow('Style', SUB_STYLES, 'style'));
490
+ }
491
+
394
492
  // ================= SETTINGS =================
395
493
  function renderSettings() {
396
494
  var s = scr('settings');
397
495
  U.clear(s);
398
496
  s.appendChild(header(false));
399
497
  var wrap = U.el('div', 'settings-wrap');
498
+ buildSubtitleSection(wrap);
400
499
  wrap.appendChild(U.el('h2', null, 'Addons'));
401
500
 
402
501
  Stremio.loadAddons().then(function (addons) {
@@ -430,25 +529,11 @@ var Views = (function () {
430
529
  };
431
530
  wrap.appendChild(reset);
432
531
 
433
- // Add-addon input
434
- var addRow = U.el('div', 'addon-item focusable');
435
- addRow.appendChild(U.el('div', 'a-name', '+ Add addon (focus, then type a manifest URL)'));
436
- var input = document.createElement('input');
437
- input.type = 'text';
438
- input.value = 'https://';
439
- input.placeholder = 'https://addon.example/manifest.json';
440
- input.style.cssText = 'width:100%;margin-top:10px;padding:12px;font-size:20px;background:#0e1117;color:#fff;border:2px solid #2a3346;border-radius:6px;';
441
- input.onchange = function () {
442
- var v = ('' + input.value).trim();
443
- if (v && v !== 'https://') { Stremio.addAddon(v); U.toast('Addon added'); setTimeout(renderSettings, 500); }
444
- };
445
- addRow.appendChild(input);
446
- addRow.__onselect = function () {
447
- input.focus();
448
- // place cursor at the end so the user types after https://
449
- var n = input.value.length;
450
- try { input.setSelectionRange(n, n); } catch (e) {}
451
- };
532
+ // Add-addon (opens a dedicated page — keeps the field clear of the keyboard)
533
+ var addRow = U.el('div', 'addon-item focusable add-row');
534
+ addRow.appendChild(U.el('div', 'a-name', '+ Add addon'));
535
+ addRow.appendChild(U.el('div', 'a-url', 'Enter a Stremio addon manifest URL on its own page'));
536
+ addRow.__onselect = function () { App.openAddAddon(); };
452
537
  wrap.appendChild(addRow);
453
538
 
454
539
  wrap.appendChild(U.el('div', 'hint',
@@ -460,7 +545,7 @@ var Views = (function () {
460
545
 
461
546
  return {
462
547
  renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
463
- renderSearch: renderSearch, backInDetail: backInDetail,
548
+ renderSearch: renderSearch, renderAddAddon: renderAddAddon, backInDetail: backInDetail,
464
549
  isModalOpen: function () { return _modalOpen; }, closeModal: closeModal
465
550
  };
466
551
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Nuvio-style Stremio-addon streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
5
  "packageType": "app",
6
6
  "appName": "Nuvio",
@@ -25,4 +25,4 @@
25
25
  ],
26
26
  "evaluateScriptOnDocumentStart": false,
27
27
  "license": "MIT"
28
- }
28
+ }