nuvio-tizen 1.8.4 → 1.8.11

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/config.xml CHANGED
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
3
- id="http://nuvio.tizen/NuvioTizen" version="1.0.0" viewmodes="maximized">
3
+ id="http://nuvio.tizen/NuvioTizen" version="1.8.11" viewmodes="maximized">
4
4
  <tizen:application id="Nuvio00001.Nuvio" package="Nuvio00001" required_version="2.4"/>
5
5
  <content src="index.html"/>
6
6
  <feature name="http://tizen.org/feature/screen.size.all"/>
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; }
@@ -180,6 +191,14 @@ html, body {
180
191
  .modal-msg { font-size: 30px; line-height: 1.4; color: #e6e9ef; margin-bottom: 36px; }
181
192
  .modal-row { display: flex; justify-content: center; }
182
193
  .modal-btn { min-width: 200px; text-align: center; }
194
+ /* list picker (single choice) */
195
+ .pick-box { width: 720px; text-align: left; }
196
+ .pick-box .modal-msg { text-align: left; margin-bottom: 20px; }
197
+ .pick-list { max-height: 620px; overflow: hidden; }
198
+ .pick-item { padding: 14px 20px; margin-bottom: 8px; border-radius: 8px; background: #0e1117; border: 4px solid transparent; font-size: 24px; }
199
+ .pick-item.sel { background: #2a3346; }
200
+ .pick-item.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; }
201
+
183
202
  .pill.danger { background: #3a2130; }
184
203
  .pill.danger.focusable.focused { background: #ef4444; color: #fff; }
185
204
  .reset-item .a-name { color: #f0a5a5; }
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.4">
7
+ <link rel="stylesheet" href="css/style.css?v=1.8.11">
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.4"></script>
51
- <script src="js/util.js?v=1.8.4"></script>
52
- <script src="js/stremio.js?v=1.8.4"></script>
53
- <script src="js/keys.js?v=1.8.4"></script>
54
- <script src="js/focus.js?v=1.8.4"></script>
55
- <script src="js/player.js?v=1.8.4"></script>
56
- <script src="js/views.js?v=1.8.4"></script>
57
- <script src="js/app.js?v=1.8.4"></script>
50
+ <script src="js/polyfills.js?v=1.8.11"></script>
51
+ <script src="js/util.js?v=1.8.11"></script>
52
+ <script src="js/stremio.js?v=1.8.11"></script>
53
+ <script src="js/keys.js?v=1.8.11"></script>
54
+ <script src="js/focus.js?v=1.8.11"></script>
55
+ <script src="js/player.js?v=1.8.11"></script>
56
+ <script src="js/views.js?v=1.8.11"></script>
57
+ <script src="js/app.js?v=1.8.11"></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,42 @@ 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, direction-aware:
89
+ // - Vertical (UP/DOWN): first restrict to the NEAREST row (min primary =
90
+ // vertical distance), then pick the horizontally closest (min cross).
91
+ // - Horizontal (LEFT/RIGHT): first restrict to the SAME row (min cross =
92
+ // vertical offset), then pick the horizontally closest (min primary).
93
+ // This stops vertical moves skipping a short row, and stops horizontal moves
94
+ // jumping into another row (e.g. Settings -> LEFT should land on Search).
95
+ var vertical = (dir === 'UP' || dir === 'DOWN');
96
+ var best = null, bestSel = Infinity, bestTie = Infinity;
97
+
98
+ if (vertical) {
99
+ // nearest row first (min primary band), then closest horizontally
100
+ var BAND = 120, minP = Infinity;
101
+ for (var j = 0; j < cands.length; j++) { if (cands[j].primary < minP) { minP = cands[j].primary; } }
102
+ for (var k = 0; k < cands.length; k++) {
103
+ var c = cands[k];
104
+ if (c.primary > minP + BAND) { continue; }
105
+ if (c.cross < bestSel || (c.cross === bestSel && c.primary < bestTie)) {
106
+ best = c.node; bestSel = c.cross; bestTie = c.primary;
107
+ }
108
+ }
109
+ } else {
110
+ // same row only (absolute vertical tolerance), then nearest horizontally.
111
+ // No jump to another row/header at a row end — just don't move.
112
+ var ROW_TOL = 140;
113
+ for (var m = 0; m < cands.length; m++) {
114
+ var d = cands[m];
115
+ if (d.cross > ROW_TOL) { continue; }
116
+ if (d.primary < bestSel || (d.primary === bestSel && d.cross < bestTie)) {
117
+ best = d.node; bestSel = d.primary; bestTie = d.cross;
118
+ }
119
+ }
74
120
  }
75
121
  if (best) { setFocus(best); }
76
122
  }
package/js/stremio.js CHANGED
@@ -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) {
@@ -450,6 +513,8 @@ var Stremio = (function () {
450
513
  tmdbFindTvId: tmdbFindTvId, tmdbSeasonEpisodes: tmdbSeasonEpisodes, tmdbImg: tmdbImg,
451
514
  omdbSeasonRatings: omdbSeasonRatings,
452
515
  loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
516
+ catalogsForHome: catalogsForHome, listCatalogsOrdered: listCatalogsOrdered,
517
+ catalogKey: catalogKey, moveFeed: moveFeed, toggleFeed: toggleFeed, isFeedHidden: isFeedHidden,
453
518
  getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
454
519
  getSearchCatalogs: getSearchCatalogs, search: search,
455
520
  isPlayable: isPlayable, streamLabel: streamLabel, streamName: streamName,
package/js/views.js CHANGED
@@ -42,6 +42,31 @@ var Views = (function () {
42
42
  _modalScope = null; _modalPrevFocus = null;
43
43
  }
44
44
 
45
+ // A scrollable single-choice list overlay (D-pad). options = [[value,label],...]
46
+ function pickList(title, options, current, onPick) {
47
+ var existing = document.querySelector('.modal-overlay');
48
+ if (existing) { existing.parentNode.removeChild(existing); }
49
+ var overlay = U.el('div', 'modal-overlay');
50
+ var box = U.el('div', 'modal-box pick-box');
51
+ box.appendChild(U.el('div', 'modal-msg', title));
52
+ var listEl = U.el('div', 'pick-list');
53
+ var pref = null;
54
+ options.forEach(function (opt) {
55
+ var it = U.el('div', 'pick-item focusable' + (opt[0] === current ? ' sel' : ''), opt[1]);
56
+ it.__onselect = function () { closeModal(); onPick(opt[0]); };
57
+ if (opt[0] === current) { pref = it; }
58
+ listEl.appendChild(it);
59
+ });
60
+ box.appendChild(listEl);
61
+ overlay.appendChild(box);
62
+ document.body.appendChild(overlay);
63
+
64
+ _modalPrevFocus = Focus.current();
65
+ _modalScope = _current();
66
+ _modalOpen = true;
67
+ Focus.setScope(overlay, pref);
68
+ }
69
+
45
70
  function _current() {
46
71
  var names = ['home', 'detail', 'search', 'settings', 'addaddon'];
47
72
  for (var i = 0; i < names.length; i++) {
@@ -59,14 +84,14 @@ var Views = (function () {
59
84
  h.appendChild(brand);
60
85
  h.appendChild(U.el('div', 'spacer'));
61
86
  if (withSearch) {
62
- var srch = U.el('div', 'gear focusable', '🔍');
63
- srch.style.marginRight = '24px';
87
+ var srch = U.el('div', 'gear hchip focusable', 'Search 🔍');
88
+ srch.style.marginRight = '20px';
64
89
  srch.__onselect = function () { App.openSearch(); };
65
90
  srch.__navDOWN = focusFirstRowItem;
66
91
  h.appendChild(srch);
67
92
  }
68
93
  if (withGear) {
69
- var g = U.el('div', 'gear focusable', '⚙');
94
+ var g = U.el('div', 'gear hchip focusable', 'Settings ⚙');
70
95
  g.__onselect = function () { App.openSettings(); };
71
96
  g.__navDOWN = focusFirstRowItem;
72
97
  h.appendChild(g);
@@ -163,8 +188,8 @@ var Views = (function () {
163
188
  }
164
189
 
165
190
  U.spinner(true);
166
- Stremio.listCatalogs().then(function (cats) {
167
- var pick = cats.slice(0, 10);
191
+ Stremio.catalogsForHome().then(function (cats) {
192
+ var pick = cats.slice(0, 15);
168
193
  var rowNodes = pick.map(function (cat) { var ph = buildRow(cat.name, []); rows.appendChild(ph); return ph; });
169
194
  focusHome(s);
170
195
  U.spinner(false);
@@ -282,6 +307,7 @@ var Views = (function () {
282
307
  }
283
308
  Focus.setScope(s, _d._epFocus);
284
309
  _d.target = null; // consume: don't re-jump after the user navigates
310
+ _d._focusSeason = null;
285
311
  }
286
312
 
287
313
  function movieActions() {
@@ -310,7 +336,9 @@ var Views = (function () {
310
336
  var bar = U.el('div', 'season-bar');
311
337
  ss.forEach(function (sn) {
312
338
  var p = U.el('div', 'pill focusable' + (sn === _d.season ? ' season-sel' : ''), 'Season ' + sn);
313
- p.__onselect = function () { _d.season = sn; drawDetail(); };
339
+ p.__onselect = function () { _d.season = sn; _d._focusSeason = sn; drawDetail(); };
340
+ // keep focus on the season pill the user just picked
341
+ if (_d._focusSeason === sn) { _d._epFocus = p; }
314
342
  bar.appendChild(p);
315
343
  });
316
344
  panel.appendChild(bar);
@@ -332,8 +360,9 @@ var Views = (function () {
332
360
 
333
361
  var body = U.el('div', 'ep-body');
334
362
  var line = U.el('div', 'ep-line');
335
- line.appendChild(document.createTextNode((watched ? '✓ ' : '') + 'E' + (v.episode || '?') + ' ' + (v.title || v.name || '')));
363
+ var title = U.el('span', 'ep-title', (watched ? '✓ ' : '') + 'E' + (v.episode || '?') + ' ' + (v.title || v.name || ''));
336
364
  var rating = U.el('span', 'ep-rating', '');
365
+ line.appendChild(title);
337
366
  line.appendChild(rating);
338
367
  body.appendChild(line);
339
368
  var ov = U.el('span', 'ep-ov', v.overview || '');
@@ -366,7 +395,7 @@ var Views = (function () {
366
395
  if (!map || _d.season !== seasonAtFetch) { return; }
367
396
  epRefs.forEach(function (r) {
368
397
  var rt = map[r.num];
369
- if (rt) { r.rating.textContent = ' ★ ' + rt + ' IMDb'; }
398
+ if (rt) { r.rating.textContent = '★ ' + rt + ' IMDb'; }
370
399
  });
371
400
  });
372
401
  }
@@ -379,8 +408,9 @@ var Views = (function () {
379
408
  var ep = map[r.num];
380
409
  if (!ep) { return; }
381
410
  if (ep.still && !r.thumb.style.backgroundImage) { r.thumb.style.backgroundImage = 'url("' + ep.still + '")'; }
382
- // TMDB rating only when OMDb isn't the configured source.
383
- if (ep.rating && !integ.omdb) { r.rating.textContent = ' ★ ' + (Math.round(ep.rating * 10) / 10); }
411
+ // Fill the rating only if OMDb didn't provide one for this episode
412
+ // (OMDb's dataset is missing IMDb ratings for some episodes).
413
+ if (ep.rating && !r.rating.textContent) { r.rating.textContent = '★ ' + (Math.round(ep.rating * 10) / 10) + ' TMDb'; }
384
414
  if (ep.overview && !r.ov.textContent) { r.ov.textContent = ep.overview; }
385
415
  });
386
416
  });
@@ -555,6 +585,10 @@ var Views = (function () {
555
585
  var go = U.el('div', 'pill focusable search-go', 'Search');
556
586
  go.__onselect = function () { runSearch(input.value); };
557
587
  bar.appendChild(go);
588
+
589
+ var back = U.el('div', 'pill focusable search-go', '← Back');
590
+ back.__onselect = function () { App.back(); };
591
+ bar.appendChild(back);
558
592
  wrap.appendChild(bar);
559
593
 
560
594
  var results = U.el('div', 'search-results');
@@ -681,6 +715,23 @@ var Views = (function () {
681
715
  return row;
682
716
  }
683
717
 
718
+ // Like subCycleRow but OK opens a scrollable list picker.
719
+ function subPickRow(caption, list, key) {
720
+ var row = U.el('div', 'addon-item focusable cfg-row');
721
+ var name = U.el('div', 'a-name');
722
+ function render() { name.textContent = caption + ': ' + labelFor(list, Player.getSubCfg()[key]); }
723
+ render();
724
+ row.appendChild(name);
725
+ row.appendChild(U.el('div', 'a-url', 'Press OK to choose'));
726
+ row.__onselect = function () {
727
+ pickList(caption, list, Player.getSubCfg()[key], function (v) {
728
+ var cfg = Player.getSubCfg(); cfg[key] = v; Player.setSubCfg(cfg);
729
+ render();
730
+ });
731
+ };
732
+ return row;
733
+ }
734
+
684
735
  // ================= SETTINGS (sectioned) =================
685
736
  var _settingsSection = 'menu';
686
737
 
@@ -701,6 +752,7 @@ var Views = (function () {
701
752
  if (_settingsSection === 'addons') { settingsAddons(wrap); }
702
753
  else if (_settingsSection === 'integrations') { settingsIntegrations(wrap); }
703
754
  else if (_settingsSection === 'subtitles') { settingsSubtitles(wrap); }
755
+ else if (_settingsSection === 'feeds') { settingsFeeds(wrap); }
704
756
  else if (_settingsSection === 'display') { settingsDisplay(wrap); }
705
757
  else { settingsMenu(wrap); }
706
758
  }
@@ -713,13 +765,20 @@ var Views = (function () {
713
765
  return r;
714
766
  }
715
767
 
768
+ function backRow() {
769
+ var r = U.el('div', 'addon-item focusable back-row', '← Back');
770
+ r.__onselect = function () { App.back(); };
771
+ return r;
772
+ }
773
+
716
774
  function settingsMenu(wrap) {
717
775
  wrap.appendChild(U.el('h2', null, 'Settings'));
718
776
  wrap.appendChild(sectionRow('Addons', 'Catalog, stream & subtitle addons', 'addons'));
719
- wrap.appendChild(sectionRow('Integrations', 'API keys (TMDB, MDBList)', 'integrations'));
777
+ wrap.appendChild(sectionRow('Integrations', 'API keys (OMDb, TMDB, MDBList)', 'integrations'));
720
778
  wrap.appendChild(sectionRow('Subtitle Options', 'Default language, size, style', 'subtitles'));
779
+ wrap.appendChild(sectionRow('Home Feeds', 'Order & show/hide the home rows', 'feeds'));
721
780
  wrap.appendChild(sectionRow('Display', 'Poster orientation', 'display'));
722
- wrap.appendChild(U.el('div', 'hint', 'Press RETURN to go back.'));
781
+ wrap.appendChild(backRow());
723
782
  Focus.setScope(scr('settings'));
724
783
  }
725
784
 
@@ -772,7 +831,8 @@ var Views = (function () {
772
831
  wrap.appendChild(addRow);
773
832
 
774
833
  wrap.appendChild(U.el('div', 'hint',
775
- 'Nuvio hosts no content — add Stremio addon manifest URLs for catalogs, streams and subtitles. RETURN goes back.'));
834
+ 'Nuvio hosts no content — add Stremio addon manifest URLs for catalogs, streams and subtitles.'));
835
+ wrap.appendChild(backRow());
776
836
  Focus.setScope(scr('settings'));
777
837
  });
778
838
  }
@@ -800,16 +860,17 @@ var Views = (function () {
800
860
  wrap.appendChild(keyRow('OMDb API key', 'omdb', 'OMDb API key', Stremio.omdbValidate));
801
861
  wrap.appendChild(keyRow('TMDB API key (v3)', 'tmdb', 'TMDB v3 API key', Stremio.tmdbValidate));
802
862
  wrap.appendChild(keyRow('MDBList API key', 'mdblist', 'MDBList API key', Stremio.mdblistValidate));
803
- 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. RETURN goes back.'));
863
+ 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.'));
864
+ wrap.appendChild(backRow());
804
865
  Focus.setScope(scr('settings'));
805
866
  }
806
867
 
807
868
  function settingsSubtitles(wrap) {
808
869
  wrap.appendChild(U.el('h2', null, 'Subtitle Options'));
809
- wrap.appendChild(subCycleRow('Default language', SUB_LANGS, 'lang'));
870
+ wrap.appendChild(subPickRow('Default language', SUB_LANGS, 'lang'));
810
871
  wrap.appendChild(subCycleRow('Size', SUB_SIZES, 'size'));
811
872
  wrap.appendChild(subCycleRow('Style', SUB_STYLES, 'style'));
812
- wrap.appendChild(U.el('div', 'hint', 'RETURN goes back.'));
873
+ wrap.appendChild(backRow());
813
874
  Focus.setScope(scr('settings'));
814
875
  }
815
876
 
@@ -828,10 +889,56 @@ var Views = (function () {
828
889
  render();
829
890
  };
830
891
  wrap.appendChild(row);
831
- wrap.appendChild(U.el('div', 'hint', 'Landscape uses wide artwork when available (falls back to the poster). RETURN goes back.'));
892
+ wrap.appendChild(U.el('div', 'hint', 'Landscape uses wide artwork when available (falls back to the poster).'));
893
+ wrap.appendChild(backRow());
832
894
  Focus.setScope(scr('settings'));
833
895
  }
834
896
 
897
+ // ----- Home Feeds ordering -----
898
+ var _feedFocus = null;
899
+ function refeed(key, ctl) { _feedFocus = { key: key, ctl: ctl }; markHomeDirty(); clearCatalogCache(); renderSettings(); }
900
+
901
+ function settingsFeeds(wrap) {
902
+ wrap.appendChild(U.el('h2', null, 'Home Feeds'));
903
+ wrap.appendChild(U.el('div', 'hint', '▲/▼ reorder a feed · OK on the name shows/hides it.'));
904
+ Stremio.listCatalogsOrdered().then(function (cats) {
905
+ cats.forEach(function (cat) {
906
+ var key = Stremio.catalogKey(cat);
907
+ var hidden = Stremio.isFeedHidden(key);
908
+ var frow = U.el('div', 'feed-row' + (hidden ? ' feed-off' : ''));
909
+
910
+ var up = U.el('div', 'feed-btn focusable', '▲');
911
+ up.setAttribute('data-fk', key); up.setAttribute('data-ctl', 'up');
912
+ up.__onselect = function () { Stremio.moveFeed(key, -1).then(function () { refeed(key, 'up'); }); };
913
+
914
+ var down = U.el('div', 'feed-btn focusable', '▼');
915
+ down.setAttribute('data-fk', key); down.setAttribute('data-ctl', 'down');
916
+ down.__onselect = function () { Stremio.moveFeed(key, 1).then(function () { refeed(key, 'down'); }); };
917
+
918
+ var name = U.el('div', 'feed-name focusable', cat.name + (hidden ? ' (hidden)' : ''));
919
+ name.setAttribute('data-fk', key); name.setAttribute('data-ctl', 'name');
920
+ name.__onselect = function () { Stremio.toggleFeed(key); refeed(key, 'name'); };
921
+
922
+ frow.appendChild(up);
923
+ frow.appendChild(down);
924
+ frow.appendChild(name);
925
+ wrap.appendChild(frow);
926
+ });
927
+ wrap.appendChild(backRow());
928
+
929
+ // restore focus to the control that triggered the last change
930
+ var pref = null;
931
+ if (_feedFocus) {
932
+ var nodes = wrap.querySelectorAll('.feed-btn, .feed-name');
933
+ for (var i = 0; i < nodes.length; i++) {
934
+ if (nodes[i].getAttribute('data-fk') === _feedFocus.key && nodes[i].getAttribute('data-ctl') === _feedFocus.ctl) { pref = nodes[i]; break; }
935
+ }
936
+ _feedFocus = null;
937
+ }
938
+ Focus.setScope(scr('settings'), pref);
939
+ });
940
+ }
941
+
835
942
  return {
836
943
  renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
837
944
  settingsRoot: settingsRoot, backInSettings: backInSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.8.4",
3
+ "version": "1.8.11",
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",