nuvio-tizen 1.3.0 β†’ 1.5.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
@@ -15,8 +15,18 @@ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
15
15
  **Cinemeta** (catalogs + metadata), **WatchHub** (streams), **OpenSubtitles v3** (subtitles).
16
16
  - Home screen with catalog rows + **Continue Watching**.
17
17
  - **Search** (πŸ” in the header) across every addon catalog that supports the `search` extra.
18
- - Detail pages: movies (Play) and series (season/episode browser).
19
- - **Stream aggregation** across all installed stream addons.
18
+ - Detail pages: movies (Play) and series (season/episode browser). **Continue Watching**
19
+ resumes a series on the exact season + episode you left off.
20
+ - **Stream aggregation** across all installed stream addons, each source showing
21
+ **size + languages/description** (from `behaviorHints.videoSize` and the addon's own text,
22
+ e.g. MediaFusion / Torrentio).
23
+ - **In-player menu** (press **UP**):
24
+ - *Select source* β€” switch stream mid-playback (keeps your position), each source shown
25
+ on two lines (name + size / seeds / languages).
26
+ - *Audio track* β€” shown only when the stream has more than one audio track
27
+ (AVPlay `getTotalTrackInfo` / `setSelectTrack`).
28
+ - *Configure subtitles* β†’ *Language* (2 levels: language β†’ the tracks within it),
29
+ *Size*, *Style*.
20
30
  - **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
21
31
  timed overlay; press **UP** during playback to pick a track (or turn them Off).
22
32
  Configurable in Settings: **default language** (auto-loads a matching track),
package/css/style.css CHANGED
@@ -116,8 +116,11 @@ html, body {
116
116
  #sub-menu { position: absolute; right: 60px; bottom: 250px; width: 560px; max-height: 620px; overflow: hidden; z-index: 30; background: rgba(20,25,36,0.98); border-radius: 12px; padding: 18px; }
117
117
  #sub-menu .sm-title { font-size: 24px; font-weight: bold; margin-bottom: 12px; color: #c7cbd4; }
118
118
  #sub-menu .sm-list { max-height: 520px; overflow: hidden; }
119
- #sub-menu .sm-item { padding: 12px 16px; font-size: 22px; border-radius: 8px; border: 3px solid transparent; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
119
+ #sub-menu .sm-item { padding: 12px 16px; border-radius: 8px; border: 3px solid transparent; }
120
+ #sub-menu .sm-item .sm-main { font-size: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
121
+ #sub-menu .sm-item .sm-detail { font-size: 16px; color: #9aa2b1; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
120
122
  #sub-menu .sm-item.sel { background: #818cf8; color: #0e1117; }
123
+ #sub-menu .sm-item.sel .sm-detail { color: #26304a; }
121
124
 
122
125
  /* --- Search --- */
123
126
  .search-wrap { position: absolute; top: 120px; left: 60px; right: 60px; bottom: 40px; }
package/js/app.js CHANGED
@@ -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, preview) {
19
+ function openDetail(type, id, preview, target) {
20
20
  if (!id) { return; }
21
21
  pushFrame();
22
22
  showScreen('detail');
23
- Views.renderDetail(type, id, preview);
23
+ Views.renderDetail(type, id, preview, target);
24
24
  }
25
25
 
26
26
  function openSettings() {
@@ -67,7 +67,12 @@ var App = (function () {
67
67
  case 'LEFT': Focus.move('LEFT'); return true;
68
68
  case 'RIGHT': Focus.move('RIGHT'); return true;
69
69
  case 'UP': Focus.move('UP'); return true;
70
- case 'DOWN': Focus.move('DOWN'); return true;
70
+ case 'DOWN': {
71
+ var c = Focus.current();
72
+ if (c && c.__navDOWN && c.__navDOWN() !== false) { return true; }
73
+ Focus.move('DOWN');
74
+ return true;
75
+ }
71
76
  case 'OK': Focus.clickCurrent(); return true;
72
77
  case 'BACK': back(); return true;
73
78
  default: return true;
package/js/player.js CHANGED
@@ -43,13 +43,16 @@ var Player = (function () {
43
43
  var _osdTimer = null;
44
44
  var _seekPending = 0;
45
45
 
46
+ // audio
47
+ var _audioIdx = null; // currently selected audio track index
48
+
46
49
  // subtitles
47
50
  var _subTracks = []; // [{lang,url,_addon}]
48
51
  var _cues = null; // [{start,end,text}]
49
52
  var _subOn = false;
50
53
  var _subLabel = '';
54
+ var _subTrackUrl = '';
51
55
  var _menuOpen = false;
52
- var _menuIdx = 0;
53
56
 
54
57
  function init() {
55
58
  objEl = U.byId('av-player');
@@ -124,9 +127,10 @@ var Player = (function () {
124
127
  _isLive = (ctx.type === 'tv') || !!ctx.isLive;
125
128
  _ctx.resumeSec = ctx.resumeSec || getResume(ctx.type, ctx.id);
126
129
 
127
- // reset subtitle state
128
- _subTracks = []; _cues = null; _subOn = false; _subLabel = '';
129
- _menuOpen = false; _menuIdx = 0;
130
+ // reset subtitle + audio state
131
+ _subTracks = []; _cues = null; _subOn = false; _subLabel = ''; _subTrackUrl = '';
132
+ _audioIdx = null;
133
+ _menuOpen = false;
130
134
  subsEl.className = 'hidden'; U.clear(subsEl);
131
135
  subMenuEl.className = 'hidden';
132
136
  updateHint();
@@ -159,7 +163,7 @@ var Player = (function () {
159
163
 
160
164
  function updateHint() {
161
165
  var base = _isLive ? 'OK: Play/Pause RETURN: Back' : 'OK: Play/Pause ←/β†’: Seek RETURN: Back';
162
- if (_subTracks.length) { base += ' UP: Subtitles'; }
166
+ base += ' UP: Menu';
163
167
  var h = U.byId('osd-hint');
164
168
  if (h) { h.textContent = base; }
165
169
  }
@@ -192,6 +196,7 @@ var Player = (function () {
192
196
  try { _dur = avplay.getDuration() / 1000; } catch (e3) { _dur = 0; }
193
197
  markLiveFromDuration();
194
198
  updateHint();
199
+ try { var atr = getAudioTracks(); _audioIdx = atr.length ? atr[0].index : null; } catch (eA) {}
195
200
  if (!_isLive && _ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
196
201
  avplay.play();
197
202
  renderOSD();
@@ -309,66 +314,213 @@ var Player = (function () {
309
314
  U.getText(track.url, 15000).then(function (text) {
310
315
  var cues = parseCues(text);
311
316
  if (!cues.length) { U.toast('Could not parse subtitles'); return; }
312
- _cues = cues; _subOn = true; _subLabel = track.lang || 'on';
317
+ _cues = cues; _subOn = true; _subLabel = track.lang || 'on'; _subTrackUrl = track.url;
313
318
  renderSubs();
314
319
  U.toast('Subtitles: ' + (track.lang || 'on'));
315
320
  }, function () { U.toast('Failed to download subtitles'); });
316
321
  }
317
322
 
318
323
  function subsOff() {
319
- _subOn = false; _cues = null; _subLabel = '';
324
+ _subOn = false; _cues = null; _subLabel = ''; _subTrackUrl = '';
320
325
  subsEl.className = 'hidden'; U.clear(subsEl);
321
326
  }
322
327
 
323
- // ---------- subtitle menu ----------
324
- function openSubMenu() {
325
- if (!_subTracks.length) { return; }
328
+ // ---------- audio tracks ----------
329
+ function getAudioTracks() {
330
+ if (useAV) {
331
+ try {
332
+ var all = avplay.getTotalTrackInfo() || [];
333
+ var out = [];
334
+ for (var i = 0; i < all.length; i++) {
335
+ if (('' + all[i].type).toUpperCase() === 'AUDIO') { out.push(all[i]); }
336
+ }
337
+ return out;
338
+ } catch (e) { return []; }
339
+ }
340
+ try {
341
+ var at = videoEl.audioTracks, arr = [];
342
+ if (at) {
343
+ for (var j = 0; j < at.length; j++) {
344
+ arr.push({ index: j, extra_info: JSON.stringify({ language: at[j].language || at[j].label }) });
345
+ }
346
+ }
347
+ return arr;
348
+ } catch (e2) { return []; }
349
+ }
350
+
351
+ function audioLabel(tr, n) {
352
+ var info = tr.extra_info;
353
+ try { if (typeof info === 'string') { info = JSON.parse(info); } } catch (e) { info = null; }
354
+ var lang = info && (info.language || info.lang);
355
+ var ch = info && (info.channels || info.channel);
356
+ var parts = [];
357
+ parts.push((lang && lang !== 'und' && lang !== '') ? lang : ('Audio ' + (n + 1)));
358
+ if (ch) { parts.push(ch + 'ch'); }
359
+ return parts.join(' Β· ');
360
+ }
361
+
362
+ function selectAudio(index) {
363
+ if (useAV) {
364
+ try { avplay.setSelectTrack('AUDIO', index); }
365
+ catch (e) { try { avplay.setSelectTrack('AUDIO', '' + index); } catch (e2) {} }
366
+ } else {
367
+ try { var at = videoEl.audioTracks; for (var i = 0; i < at.length; i++) { at[i].enabled = (i === index); } } catch (e3) {}
368
+ }
369
+ _audioIdx = index;
370
+ U.toast('Audio track changed');
371
+ }
372
+
373
+ function applyStyleNow() {
374
+ var span = subsEl.firstChild;
375
+ if (span) { styleSpan(span); }
376
+ }
377
+
378
+ // Switch to a different source mid-playback, keeping position (VOD).
379
+ function switchSource(s) {
380
+ if (!s || !s.url) { return; }
381
+ _ctx.url = s.url;
382
+ _ctx.resumeSec = _isLive ? 0 : _cur;
383
+ U.toast('Switching source…', 1500);
384
+ U.spinner(true);
385
+ if (useAV) { startAV(s.url); } else { startHTML5(s.url); }
386
+ }
387
+
388
+ // ---------- in-player menu (stack of levels) ----------
389
+ var _menuStack = [];
390
+
391
+ function topMenu() { return _menuStack[_menuStack.length - 1]; }
392
+
393
+ function openMenu() {
326
394
  _menuOpen = true;
327
- // index 0 = Off; tracks start at 1
328
- _menuIdx = 0;
329
- renderSubMenu();
395
+ _menuStack = [rootMenu()];
396
+ renderMenu();
330
397
  subMenuEl.className = '';
331
398
  showOSD(true);
332
399
  }
333
-
334
- function closeSubMenu() {
335
- _menuOpen = false;
400
+ function closeMenu() {
401
+ _menuOpen = false; _menuStack = [];
336
402
  subMenuEl.className = 'hidden';
337
403
  showOSD(false);
338
404
  }
405
+ function pushMenu(desc) { _menuStack.push(desc); renderMenu(); }
406
+ function popMenu() { _menuStack.pop(); if (!_menuStack.length) { closeMenu(); } else { renderMenu(); } }
407
+ function replaceTop(desc) { _menuStack[_menuStack.length - 1] = desc; renderMenu(); }
339
408
 
340
- function renderSubMenu() {
409
+ function renderMenu() {
410
+ var m = topMenu();
341
411
  U.clear(subMenuEl);
342
- subMenuEl.appendChild(U.el('div', 'sm-title', 'Subtitles'));
412
+ subMenuEl.appendChild(U.el('div', 'sm-title', m.title));
343
413
  var list = U.el('div', 'sm-list');
344
- var items = [{ label: 'Off' + (_subOn ? '' : ' βœ“'), track: null }];
345
- _subTracks.forEach(function (tr) {
346
- var mark = (_subOn && _subLabel === (tr.lang || 'on')) ? ' βœ“' : '';
347
- items.push({ label: (tr.lang || 'unknown') + ' Β· ' + (tr._addon || '') + mark, track: tr });
348
- });
349
- _menuItems = items;
350
- items.forEach(function (it, i) {
351
- var node = U.el('div', 'sm-item' + (i === _menuIdx ? ' sel' : ''), it.label);
414
+ m.items.forEach(function (it, i) {
415
+ var node = U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''));
416
+ node.appendChild(U.el('div', 'sm-main', it.label + (it.sel ? ' βœ“' : '')));
417
+ if (it.detail) { node.appendChild(U.el('div', 'sm-detail', it.detail)); }
352
418
  list.appendChild(node);
353
419
  });
354
420
  subMenuEl.appendChild(list);
355
- // keep selected item visible
356
- var sel = list.children[_menuIdx];
421
+ var sel = list.children[m.idx];
357
422
  if (sel && sel.scrollIntoView) { try { sel.scrollIntoView(false); } catch (e) {} }
358
423
  }
359
424
 
360
- var _menuItems = [];
425
+ function rootMenu() {
426
+ var items = [];
427
+ if (_ctx.streams && _ctx.streams.length) {
428
+ items.push({ label: 'Select source', run: function () { pushMenu(sourceMenu()); } });
429
+ }
430
+ if (getAudioTracks().length > 1) {
431
+ items.push({ label: 'Audio track', run: function () { pushMenu(audioMenu()); } });
432
+ }
433
+ items.push({ label: 'Configure subtitles', run: function () { pushMenu(subConfigMenu()); } });
434
+ return { title: 'Menu', items: items, idx: 0 };
435
+ }
436
+
437
+ function sourceMenu() {
438
+ var idx = 0;
439
+ var items = _ctx.streams.map(function (s, i) {
440
+ if (s.url === _ctx.url) { idx = i; }
441
+ return { label: s.label || 'Source', detail: s.detail || '', sel: (s.url === _ctx.url),
442
+ run: function () { switchSource(s); closeMenu(); } };
443
+ });
444
+ return { title: 'Select source', items: items, idx: idx };
445
+ }
446
+
447
+ function audioMenu() {
448
+ var tracks = getAudioTracks();
449
+ var idx = 0;
450
+ var items = tracks.map(function (tr, i) {
451
+ if (tr.index === _audioIdx) { idx = i; }
452
+ return { label: audioLabel(tr, i), sel: (tr.index === _audioIdx),
453
+ run: function () { selectAudio(tr.index); closeMenu(); } };
454
+ });
455
+ return { title: 'Audio track', items: items, idx: idx };
456
+ }
457
+
458
+ function subConfigMenu() {
459
+ return { title: 'Configure subtitles', idx: 0, items: [
460
+ { label: 'Language', run: function () { pushMenu(langMenu()); } },
461
+ { label: 'Size', run: function () { pushMenu(sizeMenu()); } },
462
+ { label: 'Style', run: function () { pushMenu(styleMenu()); } }
463
+ ] };
464
+ }
465
+
466
+ // Level 1: Off + one row per language (with option count).
467
+ function langMenu() {
468
+ var byLang = {}, order = [];
469
+ _subTracks.forEach(function (tr) {
470
+ var l = tr.lang || 'unknown';
471
+ if (!byLang[l]) { byLang[l] = []; order.push(l); }
472
+ byLang[l].push(tr);
473
+ });
474
+ var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
475
+ order.forEach(function (l) {
476
+ var n = byLang[l].length;
477
+ items.push({ label: l, detail: n + ' option' + (n > 1 ? 's' : ''),
478
+ sel: (_subOn && _subLabel === l),
479
+ run: function () { pushMenu(langTracksMenu(l, byLang[l])); } });
480
+ });
481
+ return { title: 'Subtitle language', items: items, idx: 0 };
482
+ }
483
+
484
+ // Level 2: the individual tracks within one language.
485
+ function langTracksMenu(lang, tracks) {
486
+ var idx = 0;
487
+ var items = tracks.map(function (tr, i) {
488
+ if (_subTrackUrl === tr.url) { idx = i; }
489
+ return { label: (tr._addon || 'Source') + ' Β· option ' + (i + 1),
490
+ sel: (_subTrackUrl === tr.url),
491
+ run: function () { loadTrack(tr); closeMenu(); } };
492
+ });
493
+ return { title: lang + ' subtitles', items: items, idx: idx };
494
+ }
495
+
496
+ function optionMenu(title, key, opts) {
497
+ var cfg = getSubCfg();
498
+ var idx = 0;
499
+ var items = opts.map(function (p, i) {
500
+ if (cfg[key] === p[0]) { idx = i; }
501
+ return { label: p[1], sel: (cfg[key] === p[0]), run: function () {
502
+ var c = getSubCfg(); c[key] = p[0]; setSubCfg(c); applyStyleNow();
503
+ replaceTop(optionMenu(title, key, opts)); // refresh checkmark
504
+ } };
505
+ });
506
+ return { title: title, items: items, idx: idx };
507
+ }
508
+ function sizeMenu() {
509
+ return optionMenu('Subtitle size', 'size',
510
+ [['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']]);
511
+ }
512
+ function styleMenu() {
513
+ return optionMenu('Subtitle style', 'style',
514
+ [['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']]);
515
+ }
361
516
 
362
517
  function menuKey(action) {
363
- if (action === 'UP') { _menuIdx = Math.max(0, _menuIdx - 1); renderSubMenu(); return; }
364
- if (action === 'DOWN') { _menuIdx = Math.min(_menuItems.length - 1, _menuIdx + 1); renderSubMenu(); return; }
365
- if (action === 'OK') {
366
- var it = _menuItems[_menuIdx];
367
- if (!it || it.track === null) { subsOff(); } else { loadTrack(it.track); }
368
- closeSubMenu();
369
- return;
370
- }
371
- if (action === 'BACK' || action === 'STOP') { closeSubMenu(); return; }
518
+ var m = topMenu();
519
+ if (action === 'UP') { m.idx = Math.max(0, m.idx - 1); renderMenu(); return; }
520
+ if (action === 'DOWN') { m.idx = Math.min(m.items.length - 1, m.idx + 1); renderMenu(); return; }
521
+ if (action === 'OK') { var it = m.items[m.idx]; if (it && it.run) { it.run(); } return; }
522
+ if (action === 'LEFT' || action === 'BACK') { popMenu(); return; }
523
+ if (action === 'STOP') { closeMenu(); return; }
372
524
  }
373
525
 
374
526
  function handleKey(action) {
@@ -380,7 +532,7 @@ var Player = (function () {
380
532
  case 'PAUSE': if (useAV) { try { avplay.pause(); } catch (e) {} } else { videoEl.pause(); } return true;
381
533
  case 'RIGHT': case 'FF': seekBy(action === 'FF' ? 30 : 10); return true;
382
534
  case 'LEFT': case 'REW': seekBy(action === 'REW' ? -30 : -10); return true;
383
- case 'UP': if (_subTracks.length) { openSubMenu(); } return true;
535
+ case 'UP': openMenu(); return true;
384
536
  case 'DOWN': showOSD(false); return true;
385
537
  case 'STOP': case 'BACK': exit(); return true;
386
538
  }
@@ -395,7 +547,7 @@ var Player = (function () {
395
547
  if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
396
548
  hideOSD();
397
549
  subsEl.className = 'hidden'; U.clear(subsEl);
398
- subMenuEl.className = 'hidden'; _menuOpen = false;
550
+ subMenuEl.className = 'hidden'; _menuOpen = false; _menuStack = [];
399
551
  _subOn = false; _cues = null; _subTracks = [];
400
552
  if (useAV) {
401
553
  try { avplay.stop(); } catch (e) {}
package/js/stremio.js CHANGED
@@ -112,6 +112,12 @@ var Stremio = (function () {
112
112
 
113
113
  // ---- Catalogs ----
114
114
  // Returns list of {addon, type, id, name} across all addons.
115
+ var TYPE_LABELS = { movie: 'Movies', series: 'Series', tv: 'Live TV', channel: 'Channels' };
116
+ function prettyType(t) {
117
+ if (TYPE_LABELS[t]) { return TYPE_LABELS[t]; }
118
+ return t ? (t.charAt(0).toUpperCase() + t.slice(1)) : '';
119
+ }
120
+
115
121
  function listCatalogs() {
116
122
  return loadAddons().then(function (addons) {
117
123
  var cats = [];
@@ -120,8 +126,10 @@ var Stremio = (function () {
120
126
  cs.forEach(function (c) {
121
127
  if (!c.type || !c.id) { return; }
122
128
  var label = c.name || c.id;
129
+ // Include the type so per-type catalogs sharing a name (e.g. Cinemeta's
130
+ // "Popular" for both movie & series) don't look like duplicates.
123
131
  cats.push({ addon: addon, type: c.type, id: c.id,
124
- name: label + ' Β· ' + (addon.manifest.name || 'Addon') });
132
+ name: prettyType(c.type) + ' Β· ' + label + ' Β· ' + (addon.manifest.name || 'Addon') });
125
133
  });
126
134
  });
127
135
  return cats;
@@ -272,12 +280,46 @@ var Stremio = (function () {
272
280
  return ('' + t).replace(/\n/g, ' ');
273
281
  }
274
282
 
283
+ // Short first-line label (quality / addon tag), e.g. "MediaFusion 2160p".
284
+ function streamName(stream) {
285
+ var t = stream.name || stream.title || stream.description || 'Source';
286
+ return ('' + t).split('\n')[0].trim();
287
+ }
288
+
289
+ function humanSize(bytes) {
290
+ if (!bytes || bytes <= 0) { return ''; }
291
+ var u = ['B', 'KB', 'MB', 'GB', 'TB'], i = 0, n = bytes;
292
+ while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
293
+ return (n >= 10 ? Math.round(n) : n.toFixed(1)) + ' ' + u[i];
294
+ }
295
+
296
+ // File size from behaviorHints, or parsed from the title/description text.
297
+ function streamSize(stream) {
298
+ var b = stream.behaviorHints && stream.behaviorHints.videoSize;
299
+ if (b) { return humanSize(b); }
300
+ var txt = ('' + (stream.title || '') + ' ' + (stream.description || ''));
301
+ var m = txt.match(/(\d+(?:[.,]\d+)?)\s?(TB|GB|MB)\b/i);
302
+ return m ? (m[1].replace(',', '.') + ' ' + m[2].toUpperCase()) : '';
303
+ }
304
+
305
+ // The descriptive block (size, languages, seeds, etc. as the addon formats it).
306
+ // Kept multi-line so details like MediaFusion's language list survive.
307
+ function streamDescription(stream) {
308
+ var d = '' + (stream.description || stream.title || '');
309
+ if (!stream.name) {
310
+ var parts = d.split('\n');
311
+ if (parts.length > 1) { d = parts.slice(1).join('\n'); }
312
+ }
313
+ return d.trim();
314
+ }
315
+
275
316
  return {
276
317
  DEFAULT_ADDONS: DEFAULT_ADDONS,
277
318
  getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
278
319
  loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
279
320
  getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
280
321
  getSearchCatalogs: getSearchCatalogs, search: search,
281
- isPlayable: isPlayable, streamLabel: streamLabel
322
+ isPlayable: isPlayable, streamLabel: streamLabel, streamName: streamName,
323
+ streamSize: streamSize, streamDescription: streamDescription
282
324
  };
283
325
  })();
package/js/views.js CHANGED
@@ -62,16 +62,28 @@ var Views = (function () {
62
62
  var srch = U.el('div', 'gear focusable', 'πŸ”');
63
63
  srch.style.marginRight = '24px';
64
64
  srch.__onselect = function () { App.openSearch(); };
65
+ srch.__navDOWN = focusFirstRowItem;
65
66
  h.appendChild(srch);
66
67
  }
67
68
  if (withGear) {
68
69
  var g = U.el('div', 'gear focusable', 'βš™');
69
70
  g.__onselect = function () { App.openSettings(); };
71
+ g.__navDOWN = focusFirstRowItem;
70
72
  h.appendChild(g);
71
73
  }
72
74
  return h;
73
75
  }
74
76
 
77
+ // DOWN from the header lands on the first item of the first row
78
+ // (Continue Watching when present), not the geometrically-nearest card.
79
+ function focusFirstRowItem() {
80
+ var rows = scr('home').querySelector('.rows');
81
+ if (!rows) { return false; }
82
+ var first = rows.querySelector('.focusable');
83
+ if (first) { Focus.setFocus(first); return true; }
84
+ return false;
85
+ }
86
+
75
87
  function posterCard(meta, fallbackType) {
76
88
  var c = U.el('div', 'card focusable');
77
89
  var poster = meta.poster || '';
@@ -101,6 +113,13 @@ var Views = (function () {
101
113
  if (cont.length) {
102
114
  rows.appendChild(buildRow('Continue Watching', cont.map(function (e) {
103
115
  var c = posterCard({ id: e.id.split(':')[0], type: e.type, name: e.name, poster: e.poster }, e.type);
116
+ // For a series, keep the full episode id so detail opens on the right
117
+ // season and focuses the exact episode being resumed.
118
+ c.__onselect = function () {
119
+ App.openDetail(e.type, e.id.split(':')[0],
120
+ { name: e.name, poster: e.poster, type: e.type },
121
+ e.type === 'series' ? e.id : null);
122
+ };
104
123
  if (e.duration) {
105
124
  var p = U.el('div', 'prog');
106
125
  p.style.width = Math.min(100, (e.position / e.duration * 100)) + '%';
@@ -162,8 +181,8 @@ var Views = (function () {
162
181
  // ================= DETAIL =================
163
182
  var _d = null; // { type, id, meta, mode, season }
164
183
 
165
- function renderDetail(type, id, preview) {
166
- _d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null };
184
+ function renderDetail(type, id, preview, target) {
185
+ _d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null, target: target || null };
167
186
  var s = scr('detail');
168
187
  U.clear(s);
169
188
  s.appendChild(U.el('div', 'detail-hero'));
@@ -209,6 +228,13 @@ var Views = (function () {
209
228
  body.appendChild(U.el('div', 'detail-desc', meta.description || ''));
210
229
  s.appendChild(body);
211
230
 
231
+ // If resuming a series episode, pre-select its season.
232
+ if (_d.type === 'series' && _d.target && _d.season === null && meta.videos) {
233
+ var tv = meta.videos.find(function (v) { return v.id === _d.target; });
234
+ if (tv) { _d.season = (tv.season === undefined ? 1 : tv.season); }
235
+ }
236
+
237
+ _d._epFocus = null;
212
238
  if (_d.mode === 'streams') {
213
239
  body.appendChild(streamActionsBar());
214
240
  s.appendChild(streamsPanel());
@@ -217,7 +243,8 @@ var Views = (function () {
217
243
  } else {
218
244
  body.appendChild(movieActions());
219
245
  }
220
- Focus.setScope(s);
246
+ Focus.setScope(s, _d._epFocus);
247
+ _d.target = null; // consume: don't re-jump after the user navigates
221
248
  }
222
249
 
223
250
  function movieActions() {
@@ -265,6 +292,7 @@ var Views = (function () {
265
292
  e.__onselect = function () {
266
293
  openStreams(v.id, _d.meta.name + ' S' + _d.season + 'E' + (v.episode || ''));
267
294
  };
295
+ if (_d.target && v.id === _d.target) { _d._epFocus = e; }
268
296
  listScroll.appendChild(e);
269
297
  });
270
298
  panel.appendChild(listScroll);
@@ -310,21 +338,42 @@ var Views = (function () {
310
338
  _d.streams.forEach(function (stream) {
311
339
  var playable = Stremio.isPlayable(stream);
312
340
  var node = U.el('div', 'stream focusable');
313
- node.appendChild(document.createTextNode(Stremio.streamLabel(stream)));
314
- var why = '';
341
+ node.appendChild(document.createTextNode(Stremio.streamName(stream)));
342
+
343
+ // Detail block: size + the addon's own description (languages, seeds, etc.)
344
+ var lines = [];
345
+ var size = Stremio.streamSize(stream);
346
+ var desc = Stremio.streamDescription(stream);
347
+ if (size && !/(TB|GB|MB)\b/i.test(desc)) { lines.push('πŸ’Ύ ' + size); }
348
+ if (desc) { lines.push(desc); }
349
+ if (stream._addon) { lines.push('βš™οΈ ' + stream._addon); }
315
350
  if (!playable) {
316
- if (stream.infoHash) { why = ' β€” torrent, needs a debrid/torrent client'; }
317
- else if (stream.externalUrl) { why = ' β€” opens in an external app (not playable here)'; }
318
- else { why = ' β€” not directly playable on the TV'; }
351
+ if (stream.infoHash) { lines.push('β€” torrent, needs a debrid/torrent client'); }
352
+ else if (stream.externalUrl) { lines.push('β€” opens in an external app (not playable here)'); }
353
+ else { lines.push('β€” not directly playable on the TV'); }
319
354
  }
320
- var sub = (stream._addon || '') + why;
321
- node.appendChild(U.el('span', 's-sub', sub));
355
+ var sub = U.el('span', 's-sub', lines.join('\n'));
356
+ sub.style.whiteSpace = 'pre-line';
357
+ node.appendChild(sub);
322
358
  if (!playable) { node.style.opacity = '0.55'; }
323
359
  node.__onselect = function () {
324
360
  if (!playable) { U.toast('This source can’t play directly on the TV.'); return; }
361
+ var sources = _d.streams.filter(Stremio.isPlayable).map(function (st) {
362
+ var sz = Stremio.streamSize(st);
363
+ var desc = Stremio.streamDescription(st).replace(/\n/g, ' ');
364
+ var dparts = [];
365
+ if (sz && !/(TB|GB|MB)\b/i.test(desc)) { dparts.push('πŸ’Ύ ' + sz); }
366
+ if (desc) { dparts.push(desc); }
367
+ return {
368
+ url: st.url,
369
+ label: (st._addon ? st._addon + ' Β· ' : '') + Stremio.streamName(st),
370
+ detail: dparts.join(' ')
371
+ };
372
+ });
325
373
  Player.play({
326
374
  url: stream.url, type: _d.type, id: _d.streamId,
327
- name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster
375
+ name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster,
376
+ streams: sources
328
377
  }, function () { drawDetail(); });
329
378
  };
330
379
  listScroll.appendChild(node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",