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 +14 -3
- package/index.html +9 -9
- package/js/focus.js +30 -3
- package/js/stremio.js +98 -2
- package/js/views.js +96 -17
- package/package.json +1 -1
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-
|
|
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.
|
|
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.
|
|
51
|
-
<script src="js/util.js?v=1.8.
|
|
52
|
-
<script src="js/stremio.js?v=1.8.
|
|
53
|
-
<script src="js/keys.js?v=1.8.
|
|
54
|
-
<script src="js/focus.js?v=1.8.
|
|
55
|
-
<script src="js/player.js?v=1.8.
|
|
56
|
-
<script src="js/views.js?v=1.8.
|
|
57
|
-
<script src="js/app.js?v=1.8.
|
|
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
|
-
|
|
73
|
-
|
|
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 = '
|
|
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.
|
|
167
|
-
var pick = cats.slice(0,
|
|
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
|
-
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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(
|
|
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).
|
|
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,
|