nuvio-tizen 1.8.3 → 1.8.9

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/css/style.css CHANGED
@@ -36,6 +36,8 @@ html, body {
36
36
  .brand .dot { color: #e6e9ef; }
37
37
  .header .spacer { flex: 1; }
38
38
  .header .gear { font-size: 30px; opacity: 0.7; }
39
+ .header .hchip { font-size: 24px; opacity: 0.9; padding: 10px 24px; border-radius: 30px; background: #1a1f2b; border: 4px solid transparent; }
40
+ .header .hchip.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; opacity: 1; }
39
41
 
40
42
  /* --- Rows --- */
41
43
  .rows { position: absolute; top: 110px; left: 0; right: 0; bottom: 0; overflow: hidden; }
@@ -104,13 +106,22 @@ html, body {
104
106
  .ep-row { display: flex; align-items: flex-start; padding: 12px 16px; }
105
107
  .ep-thumb { width: 120px; height: 68px; flex: 0 0 120px; margin-right: 18px; border-radius: 6px; background-color: #0e1117; background-size: cover; background-position: center; }
106
108
  .ep-body { flex: 1; min-width: 0; }
107
- .ep-line { font-size: 22px; }
108
- .ep-rating { color: #ffd257; font-size: 19px; }
109
+ .ep-line { display: flex; align-items: flex-start; font-size: 22px; }
110
+ .ep-title { flex: 1 1 auto; min-width: 0; }
111
+ .ep-rating { flex: 0 0 auto; margin-left: 14px; color: #ffd257; font-size: 19px; white-space: nowrap; }
112
+ .ep-rating:empty { margin-left: 0; }
109
113
  .ep-ov { display: block; font-size: 17px; color: #9aa2b1; margin-top: 6px; max-height: 44px; overflow: hidden; }
110
114
  .list-scroll { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; }
111
115
 
112
116
  /* Settings */
113
- .settings-wrap { position: absolute; top: 120px; left: 60px; right: 60px; }
117
+ .settings-wrap { position: absolute; top: 120px; left: 60px; right: 60px; bottom: 40px; overflow: hidden; }
118
+ .back-row { color: #c7cbd4; }
119
+ /* Home Feeds ordering */
120
+ .feed-row { display: flex; align-items: center; margin-bottom: 8px; }
121
+ .feed-btn { width: 60px; text-align: center; padding: 14px 0; margin-right: 8px; background: #161b26; border-radius: 8px; font-size: 22px; border: 4px solid transparent; }
122
+ .feed-name { flex: 1; padding: 14px 18px; background: #161b26; border-radius: 8px; font-size: 22px; border: 4px solid transparent; white-space: normal; }
123
+ .feed-off .feed-name { opacity: 0.5; }
124
+ .feed-btn.focusable.focused, .feed-name.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; }
114
125
  .settings-wrap h2 { font-size: 30px; margin-bottom: 20px; }
115
126
  .addon-item { padding: 16px 20px; margin-bottom: 10px; background: #161b26; border-radius: 8px; font-size: 20px; border: 4px solid transparent; word-break: break-all; white-space: normal; }
116
127
  .addon-item .a-name { font-weight: bold; }
package/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=1920, height=1080, user-scalable=no">
6
6
  <title>Nuvio</title>
7
- <link rel="stylesheet" href="css/style.css?v=1.8.3">
7
+ <link rel="stylesheet" href="css/style.css?v=1.8.9">
8
8
  </head>
9
9
  <body>
10
10
  <!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
@@ -47,13 +47,13 @@
47
47
 
48
48
  <!-- Load order matters: polyfills -> util -> data -> input -> player -> views -> app -->
49
49
  <!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
50
- <script src="js/polyfills.js?v=1.8.3"></script>
51
- <script src="js/util.js?v=1.8.3"></script>
52
- <script src="js/stremio.js?v=1.8.3"></script>
53
- <script src="js/keys.js?v=1.8.3"></script>
54
- <script src="js/focus.js?v=1.8.3"></script>
55
- <script src="js/player.js?v=1.8.3"></script>
56
- <script src="js/views.js?v=1.8.3"></script>
57
- <script src="js/app.js?v=1.8.3"></script>
50
+ <script src="js/polyfills.js?v=1.8.9"></script>
51
+ <script src="js/util.js?v=1.8.9"></script>
52
+ <script src="js/stremio.js?v=1.8.9"></script>
53
+ <script src="js/keys.js?v=1.8.9"></script>
54
+ <script src="js/focus.js?v=1.8.9"></script>
55
+ <script src="js/player.js?v=1.8.9"></script>
56
+ <script src="js/views.js?v=1.8.9"></script>
57
+ <script src="js/app.js?v=1.8.9"></script>
58
58
  </body>
59
59
  </html>
package/js/focus.js CHANGED
@@ -20,6 +20,16 @@ var Focus = (function () {
20
20
 
21
21
  function setFocus(node) {
22
22
  if (!node) { return; }
23
+ // Moving to a non-input control must blur any text field that still holds
24
+ // DOM keyboard focus — otherwise the remote's OK/Enter is delivered to the
25
+ // input (e.g. re-running a search) instead of the visually-focused element.
26
+ try {
27
+ var ae = document.activeElement;
28
+ if (ae && ae !== node && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA') &&
29
+ node.tagName !== 'INPUT' && node.tagName !== 'TEXTAREA') {
30
+ ae.blur();
31
+ }
32
+ } catch (e) {}
23
33
  if (_current && _current !== node && _current.classList) { _current.classList.remove('focused'); }
24
34
  _current = node;
25
35
  if (node.classList) { node.classList.add('focused'); }
@@ -56,8 +66,10 @@ var Focus = (function () {
56
66
 
57
67
  var r = _current.getBoundingClientRect();
58
68
  var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
59
- var best = null, bestScore = Infinity;
60
69
 
70
+ // Collect candidates in the pressed direction with their primary (distance
71
+ // along the axis of travel) and cross (offset on the other axis).
72
+ var cands = [];
61
73
  for (var i = 0; i < list.length; i++) {
62
74
  var node = list[i];
63
75
  if (node === _current) { continue; }
@@ -69,8 +81,23 @@ var Focus = (function () {
69
81
  else if (dir === 'RIGHT') { if (dx < 1) { continue; } primary = dx; cross = Math.abs(dy); }
70
82
  else if (dir === 'UP') { if (dy > -1) { continue; } primary = -dy; cross = Math.abs(dx); }
71
83
  else { if (dy < 1) { continue; } primary = dy; cross = Math.abs(dx); }
72
- var score = primary + cross * 2.2;
73
- if (score < bestScore) { bestScore = score; best = node; }
84
+ cands.push({ node: node, primary: primary, cross: cross });
85
+ }
86
+ if (!cands.length) { return; }
87
+
88
+ // Two-phase: land in the NEAREST row/column first (so vertical moves never
89
+ // skip a row just because a farther element is better horizontally aligned),
90
+ // then pick the closest within that band.
91
+ var minP = Infinity;
92
+ for (var j = 0; j < cands.length; j++) { if (cands[j].primary < minP) { minP = cands[j].primary; } }
93
+ var BAND = 120;
94
+ var best = null, bestCross = Infinity, bestP = Infinity;
95
+ for (var k = 0; k < cands.length; k++) {
96
+ var c = cands[k];
97
+ if (c.primary > minP + BAND) { continue; }
98
+ if (c.cross < bestCross || (c.cross === bestCross && c.primary < bestP)) {
99
+ best = c.node; bestCross = c.cross; bestP = c.primary;
100
+ }
74
101
  }
75
102
  if (best) { setFocus(best); }
76
103
  }
package/js/stremio.js CHANGED
@@ -63,7 +63,7 @@ var Stremio = (function () {
63
63
 
64
64
  function getIntegrations() {
65
65
  var o = U.store.get(INTEG_KEY, {}) || {};
66
- return { tmdb: o.tmdb || '', mdblist: o.mdblist || '', theintrodb: o.theintrodb || '' };
66
+ return { omdb: o.omdb || '', tmdb: o.tmdb || '', mdblist: o.mdblist || '', theintrodb: o.theintrodb || '' };
67
67
  }
68
68
  function setIntegration(key, val) {
69
69
  var o = getIntegrations();
@@ -178,6 +178,69 @@ var Stremio = (function () {
178
178
  .then(function (res) { return (res && res.metas) ? res.metas : []; });
179
179
  }
180
180
 
181
+ // ---- Home feed ordering / visibility ----
182
+ var FEEDS_KEY = 'nuvio.feeds';
183
+ function catalogKey(cat) {
184
+ var aid = (cat.addon.manifest && cat.addon.manifest.id) || cat.addon.url;
185
+ return cat.type + '|' + cat.id + '|' + aid;
186
+ }
187
+ function getFeedCfg() {
188
+ var f = U.store.get(FEEDS_KEY, {}) || {};
189
+ return { order: f.order || [], hidden: f.hidden || [] };
190
+ }
191
+ function setFeedCfg(cfg) { U.store.set(FEEDS_KEY, cfg); }
192
+
193
+ function sortCatalogsByPref(cats) {
194
+ var order = getFeedCfg().order;
195
+ var pos = {};
196
+ order.forEach(function (k, i) { pos[k] = i; });
197
+ var withIdx = cats.map(function (c, i) { return { c: c, i: i, k: catalogKey(c) }; });
198
+ withIdx.sort(function (a, b) {
199
+ var pa = (pos[a.k] !== undefined) ? pos[a.k] : (100000 + a.i);
200
+ var pb = (pos[b.k] !== undefined) ? pos[b.k] : (100000 + b.i);
201
+ return pa - pb;
202
+ });
203
+ return withIdx.map(function (x) { return x.c; });
204
+ }
205
+
206
+ // All catalogs in the user's order (for the ordering settings page).
207
+ function listCatalogsOrdered() {
208
+ return listCatalogs().then(function (cats) { return sortCatalogsByPref(cats); });
209
+ }
210
+
211
+ // Ordered + visible catalogs (for the home page).
212
+ function catalogsForHome() {
213
+ return listCatalogs().then(function (cats) {
214
+ var hidden = {};
215
+ getFeedCfg().hidden.forEach(function (k) { hidden[k] = 1; });
216
+ return sortCatalogsByPref(cats).filter(function (c) { return !hidden[catalogKey(c)]; });
217
+ });
218
+ }
219
+
220
+ function moveFeed(key, dir) {
221
+ return listCatalogs().then(function (cats) {
222
+ var ordered = sortCatalogsByPref(cats).map(catalogKey);
223
+ var i = ordered.indexOf(key);
224
+ if (i < 0) { return; }
225
+ var j = i + dir;
226
+ if (j < 0 || j >= ordered.length) { return; }
227
+ var t = ordered[i]; ordered[i] = ordered[j]; ordered[j] = t;
228
+ var cfg = getFeedCfg(); cfg.order = ordered; setFeedCfg(cfg);
229
+ });
230
+ }
231
+
232
+ function toggleFeed(key) {
233
+ var cfg = getFeedCfg();
234
+ var h = {};
235
+ cfg.hidden.forEach(function (k) { h[k] = 1; });
236
+ if (h[key]) { delete h[key]; } else { h[key] = 1; }
237
+ cfg.hidden = Object.keys(h);
238
+ setFeedCfg(cfg);
239
+ }
240
+ function isFeedHidden(key) {
241
+ return getFeedCfg().hidden.indexOf(key) !== -1;
242
+ }
243
+
181
244
  // ---- Meta ----
182
245
  function getMeta(type, id) {
183
246
  return loadAddons().then(function (addons) {
@@ -301,6 +364,36 @@ var Stremio = (function () {
301
364
  }, function () { _tmdbSeasonCache[ck] = null; return null; });
302
365
  }
303
366
 
367
+ // ---- OMDb (real IMDb per-episode ratings) ----
368
+ var _omdbCache = {};
369
+ function omdbKey() { return getIntegrations().omdb; }
370
+
371
+ function omdbValidate(key) {
372
+ if (!key) { return Promise.resolve(false); }
373
+ return U.getJSON('https://www.omdbapi.com/?apikey=' + encodeURIComponent(key) + '&i=tt0111161', 8000)
374
+ .then(function (r) { return !!(r && r.Response === 'True'); }, function () { return false; });
375
+ }
376
+
377
+ // One request per season → { episodeNumber: imdbRatingString }.
378
+ function omdbSeasonRatings(seriesImdb, season) {
379
+ var key = omdbKey();
380
+ if (!key || !seriesImdb) { return Promise.resolve(null); }
381
+ var ck = seriesImdb + ':' + season;
382
+ if (_omdbCache[ck] !== undefined) { return Promise.resolve(_omdbCache[ck]); }
383
+ return U.getJSON('https://www.omdbapi.com/?apikey=' + encodeURIComponent(key) +
384
+ '&i=' + encodeURIComponent(seriesImdb) + '&Season=' + encodeURIComponent(season), 12000)
385
+ .then(function (r) {
386
+ var map = {};
387
+ if (r && r.Episodes) {
388
+ r.Episodes.forEach(function (ep) {
389
+ var n = parseInt(ep.Episode, 10);
390
+ if (!isNaN(n) && ep.imdbRating && ep.imdbRating !== 'N/A') { map[n] = ep.imdbRating; }
391
+ });
392
+ }
393
+ _omdbCache[ck] = map; return map;
394
+ }, function () { _omdbCache[ck] = null; return null; });
395
+ }
396
+
304
397
  // ---- Skip segments (TheIntroDB) ----
305
398
  // GET https://api.theintrodb.org/v3/media?imdb_id=tt..[&season=&episode=][&duration_ms=]
306
399
  // Returns { intro, recap, credits, preview } arrays of {start,end} in seconds (null = open).
@@ -416,9 +509,12 @@ var Stremio = (function () {
416
509
  getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
417
510
  getIntegrations: getIntegrations, setIntegration: setIntegration, getPrefs: getPrefs, setPref: setPref,
418
511
  getIntroSegments: getIntroSegments,
419
- tmdbValidate: tmdbValidate, mdblistValidate: mdblistValidate,
512
+ tmdbValidate: tmdbValidate, mdblistValidate: mdblistValidate, omdbValidate: omdbValidate,
420
513
  tmdbFindTvId: tmdbFindTvId, tmdbSeasonEpisodes: tmdbSeasonEpisodes, tmdbImg: tmdbImg,
514
+ omdbSeasonRatings: omdbSeasonRatings,
421
515
  loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
516
+ catalogsForHome: catalogsForHome, listCatalogsOrdered: listCatalogsOrdered,
517
+ catalogKey: catalogKey, moveFeed: moveFeed, toggleFeed: toggleFeed, isFeedHidden: isFeedHidden,
422
518
  getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
423
519
  getSearchCatalogs: getSearchCatalogs, search: search,
424
520
  isPlayable: isPlayable, streamLabel: streamLabel, streamName: streamName,
package/js/views.js CHANGED
@@ -59,14 +59,14 @@ var Views = (function () {
59
59
  h.appendChild(brand);
60
60
  h.appendChild(U.el('div', 'spacer'));
61
61
  if (withSearch) {
62
- var srch = U.el('div', 'gear focusable', '🔍');
63
- srch.style.marginRight = '24px';
62
+ var srch = U.el('div', 'gear hchip focusable', 'Search 🔍');
63
+ srch.style.marginRight = '20px';
64
64
  srch.__onselect = function () { App.openSearch(); };
65
65
  srch.__navDOWN = focusFirstRowItem;
66
66
  h.appendChild(srch);
67
67
  }
68
68
  if (withGear) {
69
- var g = U.el('div', 'gear focusable', '⚙');
69
+ var g = U.el('div', 'gear hchip focusable', 'Settings ⚙');
70
70
  g.__onselect = function () { App.openSettings(); };
71
71
  g.__navDOWN = focusFirstRowItem;
72
72
  h.appendChild(g);
@@ -163,8 +163,8 @@ var Views = (function () {
163
163
  }
164
164
 
165
165
  U.spinner(true);
166
- Stremio.listCatalogs().then(function (cats) {
167
- var pick = cats.slice(0, 10);
166
+ Stremio.catalogsForHome().then(function (cats) {
167
+ var pick = cats.slice(0, 15);
168
168
  var rowNodes = pick.map(function (cat) { var ph = buildRow(cat.name, []); rows.appendChild(ph); return ph; });
169
169
  focusHome(s);
170
170
  U.spinner(false);
@@ -282,6 +282,7 @@ var Views = (function () {
282
282
  }
283
283
  Focus.setScope(s, _d._epFocus);
284
284
  _d.target = null; // consume: don't re-jump after the user navigates
285
+ _d._focusSeason = null;
285
286
  }
286
287
 
287
288
  function movieActions() {
@@ -310,7 +311,9 @@ var Views = (function () {
310
311
  var bar = U.el('div', 'season-bar');
311
312
  ss.forEach(function (sn) {
312
313
  var p = U.el('div', 'pill focusable' + (sn === _d.season ? ' season-sel' : ''), 'Season ' + sn);
313
- p.__onselect = function () { _d.season = sn; drawDetail(); };
314
+ p.__onselect = function () { _d.season = sn; _d._focusSeason = sn; drawDetail(); };
315
+ // keep focus on the season pill the user just picked
316
+ if (_d._focusSeason === sn) { _d._epFocus = p; }
314
317
  bar.appendChild(p);
315
318
  });
316
319
  panel.appendChild(bar);
@@ -332,8 +335,9 @@ var Views = (function () {
332
335
 
333
336
  var body = U.el('div', 'ep-body');
334
337
  var line = U.el('div', 'ep-line');
335
- line.appendChild(document.createTextNode((watched ? '✓ ' : '') + 'E' + (v.episode || '?') + ' ' + (v.title || v.name || '')));
338
+ var title = U.el('span', 'ep-title', (watched ? '✓ ' : '') + 'E' + (v.episode || '?') + ' ' + (v.title || v.name || ''));
336
339
  var rating = U.el('span', 'ep-rating', '');
340
+ line.appendChild(title);
337
341
  line.appendChild(rating);
338
342
  body.appendChild(line);
339
343
  var ov = U.el('span', 'ep-ov', v.overview || '');
@@ -356,9 +360,21 @@ var Views = (function () {
356
360
  });
357
361
  panel.appendChild(listScroll);
358
362
 
359
- // Enrich with TMDB episode images + ratings (one request per season) if a key is set.
360
- if (Stremio.getIntegrations().tmdb) {
361
- var seasonAtFetch = _d.season;
363
+ // Enrich episodes (one request per season each). Rating source priority:
364
+ // OMDb (real IMDb) when its key is set, else TMDB vote_average. Images come from TMDB.
365
+ var integ = Stremio.getIntegrations();
366
+ var seasonAtFetch = _d.season;
367
+
368
+ if (integ.omdb) {
369
+ Stremio.omdbSeasonRatings(_d.id, seasonAtFetch).then(function (map) {
370
+ if (!map || _d.season !== seasonAtFetch) { return; }
371
+ epRefs.forEach(function (r) {
372
+ var rt = map[r.num];
373
+ if (rt) { r.rating.textContent = '★ ' + rt + ' IMDb'; }
374
+ });
375
+ });
376
+ }
377
+ if (integ.tmdb) {
362
378
  Stremio.tmdbFindTvId(_d.id).then(function (tvId) {
363
379
  if (!tvId || _d.season !== seasonAtFetch) { return; }
364
380
  return Stremio.tmdbSeasonEpisodes(tvId, seasonAtFetch).then(function (map) {
@@ -367,7 +383,9 @@ var Views = (function () {
367
383
  var ep = map[r.num];
368
384
  if (!ep) { return; }
369
385
  if (ep.still && !r.thumb.style.backgroundImage) { r.thumb.style.backgroundImage = 'url("' + ep.still + '")'; }
370
- if (ep.rating) { r.rating.textContent = ' ★ ' + (Math.round(ep.rating * 10) / 10); }
386
+ // Fill the rating only if OMDb didn't provide one for this episode
387
+ // (OMDb's dataset is missing IMDb ratings for some episodes).
388
+ if (ep.rating && !r.rating.textContent) { r.rating.textContent = '★ ' + (Math.round(ep.rating * 10) / 10) + ' TMDb'; }
371
389
  if (ep.overview && !r.ov.textContent) { r.ov.textContent = ep.overview; }
372
390
  });
373
391
  });
@@ -542,6 +560,10 @@ var Views = (function () {
542
560
  var go = U.el('div', 'pill focusable search-go', 'Search');
543
561
  go.__onselect = function () { runSearch(input.value); };
544
562
  bar.appendChild(go);
563
+
564
+ var back = U.el('div', 'pill focusable search-go', '← Back');
565
+ back.__onselect = function () { App.back(); };
566
+ bar.appendChild(back);
545
567
  wrap.appendChild(bar);
546
568
 
547
569
  var results = U.el('div', 'search-results');
@@ -688,6 +710,7 @@ var Views = (function () {
688
710
  if (_settingsSection === 'addons') { settingsAddons(wrap); }
689
711
  else if (_settingsSection === 'integrations') { settingsIntegrations(wrap); }
690
712
  else if (_settingsSection === 'subtitles') { settingsSubtitles(wrap); }
713
+ else if (_settingsSection === 'feeds') { settingsFeeds(wrap); }
691
714
  else if (_settingsSection === 'display') { settingsDisplay(wrap); }
692
715
  else { settingsMenu(wrap); }
693
716
  }
@@ -700,13 +723,20 @@ var Views = (function () {
700
723
  return r;
701
724
  }
702
725
 
726
+ function backRow() {
727
+ var r = U.el('div', 'addon-item focusable back-row', '← Back');
728
+ r.__onselect = function () { App.back(); };
729
+ return r;
730
+ }
731
+
703
732
  function settingsMenu(wrap) {
704
733
  wrap.appendChild(U.el('h2', null, 'Settings'));
705
734
  wrap.appendChild(sectionRow('Addons', 'Catalog, stream & subtitle addons', 'addons'));
706
- wrap.appendChild(sectionRow('Integrations', 'API keys (TMDB, MDBList)', 'integrations'));
735
+ wrap.appendChild(sectionRow('Integrations', 'API keys (OMDb, TMDB, MDBList)', 'integrations'));
707
736
  wrap.appendChild(sectionRow('Subtitle Options', 'Default language, size, style', 'subtitles'));
737
+ wrap.appendChild(sectionRow('Home Feeds', 'Order & show/hide the home rows', 'feeds'));
708
738
  wrap.appendChild(sectionRow('Display', 'Poster orientation', 'display'));
709
- wrap.appendChild(U.el('div', 'hint', 'Press RETURN to go back.'));
739
+ wrap.appendChild(backRow());
710
740
  Focus.setScope(scr('settings'));
711
741
  }
712
742
 
@@ -759,7 +789,8 @@ var Views = (function () {
759
789
  wrap.appendChild(addRow);
760
790
 
761
791
  wrap.appendChild(U.el('div', 'hint',
762
- 'Nuvio hosts no content — add Stremio addon manifest URLs for catalogs, streams and subtitles. RETURN goes back.'));
792
+ 'Nuvio hosts no content — add Stremio addon manifest URLs for catalogs, streams and subtitles.'));
793
+ wrap.appendChild(backRow());
763
794
  Focus.setScope(scr('settings'));
764
795
  });
765
796
  }
@@ -784,9 +815,11 @@ var Views = (function () {
784
815
 
785
816
  function settingsIntegrations(wrap) {
786
817
  wrap.appendChild(U.el('h2', null, 'Integrations'));
818
+ wrap.appendChild(keyRow('OMDb API key', 'omdb', 'OMDb API key', Stremio.omdbValidate));
787
819
  wrap.appendChild(keyRow('TMDB API key (v3)', 'tmdb', 'TMDB v3 API key', Stremio.tmdbValidate));
788
820
  wrap.appendChild(keyRow('MDBList API key', 'mdblist', 'MDBList API key', Stremio.mdblistValidate));
789
- wrap.appendChild(U.el('div', 'hint', 'Keys are stored on this TV only. TMDB enables episode images & ratings. RETURN goes back.'));
821
+ wrap.appendChild(U.el('div', 'hint', 'Keys are stored on this TV only. OMDb gives real IMDb per-episode ratings (used first when set); TMDB adds episode images & ratings.'));
822
+ wrap.appendChild(backRow());
790
823
  Focus.setScope(scr('settings'));
791
824
  }
792
825
 
@@ -795,7 +828,7 @@ var Views = (function () {
795
828
  wrap.appendChild(subCycleRow('Default language', SUB_LANGS, 'lang'));
796
829
  wrap.appendChild(subCycleRow('Size', SUB_SIZES, 'size'));
797
830
  wrap.appendChild(subCycleRow('Style', SUB_STYLES, 'style'));
798
- wrap.appendChild(U.el('div', 'hint', 'RETURN goes back.'));
831
+ wrap.appendChild(backRow());
799
832
  Focus.setScope(scr('settings'));
800
833
  }
801
834
 
@@ -814,10 +847,56 @@ var Views = (function () {
814
847
  render();
815
848
  };
816
849
  wrap.appendChild(row);
817
- wrap.appendChild(U.el('div', 'hint', 'Landscape uses wide artwork when available (falls back to the poster). RETURN goes back.'));
850
+ wrap.appendChild(U.el('div', 'hint', 'Landscape uses wide artwork when available (falls back to the poster).'));
851
+ wrap.appendChild(backRow());
818
852
  Focus.setScope(scr('settings'));
819
853
  }
820
854
 
855
+ // ----- Home Feeds ordering -----
856
+ var _feedFocus = null;
857
+ function refeed(key, ctl) { _feedFocus = { key: key, ctl: ctl }; markHomeDirty(); clearCatalogCache(); renderSettings(); }
858
+
859
+ function settingsFeeds(wrap) {
860
+ wrap.appendChild(U.el('h2', null, 'Home Feeds'));
861
+ wrap.appendChild(U.el('div', 'hint', '▲/▼ reorder a feed · OK on the name shows/hides it.'));
862
+ Stremio.listCatalogsOrdered().then(function (cats) {
863
+ cats.forEach(function (cat) {
864
+ var key = Stremio.catalogKey(cat);
865
+ var hidden = Stremio.isFeedHidden(key);
866
+ var frow = U.el('div', 'feed-row' + (hidden ? ' feed-off' : ''));
867
+
868
+ var up = U.el('div', 'feed-btn focusable', '▲');
869
+ up.setAttribute('data-fk', key); up.setAttribute('data-ctl', 'up');
870
+ up.__onselect = function () { Stremio.moveFeed(key, -1).then(function () { refeed(key, 'up'); }); };
871
+
872
+ var down = U.el('div', 'feed-btn focusable', '▼');
873
+ down.setAttribute('data-fk', key); down.setAttribute('data-ctl', 'down');
874
+ down.__onselect = function () { Stremio.moveFeed(key, 1).then(function () { refeed(key, 'down'); }); };
875
+
876
+ var name = U.el('div', 'feed-name focusable', cat.name + (hidden ? ' (hidden)' : ''));
877
+ name.setAttribute('data-fk', key); name.setAttribute('data-ctl', 'name');
878
+ name.__onselect = function () { Stremio.toggleFeed(key); refeed(key, 'name'); };
879
+
880
+ frow.appendChild(up);
881
+ frow.appendChild(down);
882
+ frow.appendChild(name);
883
+ wrap.appendChild(frow);
884
+ });
885
+ wrap.appendChild(backRow());
886
+
887
+ // restore focus to the control that triggered the last change
888
+ var pref = null;
889
+ if (_feedFocus) {
890
+ var nodes = wrap.querySelectorAll('.feed-btn, .feed-name');
891
+ for (var i = 0; i < nodes.length; i++) {
892
+ if (nodes[i].getAttribute('data-fk') === _feedFocus.key && nodes[i].getAttribute('data-ctl') === _feedFocus.ctl) { pref = nodes[i]; break; }
893
+ }
894
+ _feedFocus = null;
895
+ }
896
+ Focus.setScope(scr('settings'), pref);
897
+ });
898
+ }
899
+
821
900
  return {
822
901
  renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
823
902
  settingsRoot: settingsRoot, backInSettings: backInSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.8.3",
3
+ "version": "1.8.9",
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",