nuvio-tizen 1.8.2 → 1.8.4
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 +13 -1
- package/index.html +9 -9
- package/js/app.js +6 -1
- package/js/stremio.js +84 -1
- package/js/views.js +136 -51
- package/package.json +1 -1
package/css/style.css
CHANGED
|
@@ -76,7 +76,8 @@ html, body {
|
|
|
76
76
|
/* Filled selection for list rows / buttons */
|
|
77
77
|
.pill.focusable.focused, .stream.focusable.focused, .ep.focusable.focused { background: #818cf8; color: #0e1117; }
|
|
78
78
|
/* Keep secondary lines readable on the bright selected background */
|
|
79
|
-
.stream.focusable.focused .s-sub, .ep.focusable.focused .ep-sub
|
|
79
|
+
.stream.focusable.focused .s-sub, .ep.focusable.focused .ep-sub,
|
|
80
|
+
.ep.focusable.focused .ep-ov, .ep.focusable.focused .ep-rating { color: #17203a; }
|
|
80
81
|
|
|
81
82
|
.gear.focusable.focused { opacity: 1; color: #818cf8; }
|
|
82
83
|
|
|
@@ -89,12 +90,23 @@ html, body {
|
|
|
89
90
|
.detail-desc { font-size: 22px; line-height: 1.45; color: #cfd3dc; max-height: 200px; overflow: hidden; margin-bottom: 26px; }
|
|
90
91
|
.btn-row { margin-top: 10px; }
|
|
91
92
|
.pill { display: inline-block; padding: 14px 30px; margin-right: 16px; border-radius: 30px; background: #1a1f2b; font-size: 22px; border: 4px solid transparent; }
|
|
93
|
+
/* selected season: grey with light text; focus must still win (higher specificity below) */
|
|
94
|
+
.season-bar .pill.season-sel { background: #2a3346; color: #c7cbd4; }
|
|
95
|
+
.season-bar .pill.focusable.focused { background: #818cf8; color: #0e1117; }
|
|
92
96
|
|
|
93
97
|
/* Episode + stream lists */
|
|
94
98
|
.list-panel { position: absolute; top: 640px; left: 60px; right: 60px; bottom: 40px; overflow: hidden; }
|
|
95
99
|
.season-bar { margin-bottom: 14px; white-space: nowrap; }
|
|
96
100
|
.ep, .stream { display: block; padding: 16px 22px; margin-bottom: 10px; background: #161b26; border-radius: 8px; font-size: 22px; border: 4px solid transparent; white-space: normal; }
|
|
97
101
|
.ep .ep-sub, .stream .s-sub { display: block; font-size: 17px; color: #9aa2b1; margin-top: 4px; }
|
|
102
|
+
|
|
103
|
+
/* episode rows with thumbnail + rating */
|
|
104
|
+
.ep-row { display: flex; align-items: flex-start; padding: 12px 16px; }
|
|
105
|
+
.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
|
+
.ep-body { flex: 1; min-width: 0; }
|
|
107
|
+
.ep-line { font-size: 22px; }
|
|
108
|
+
.ep-rating { color: #ffd257; font-size: 19px; }
|
|
109
|
+
.ep-ov { display: block; font-size: 17px; color: #9aa2b1; margin-top: 6px; max-height: 44px; overflow: hidden; }
|
|
98
110
|
.list-scroll { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; }
|
|
99
111
|
|
|
100
112
|
/* Settings */
|
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.4">
|
|
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.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>
|
|
58
58
|
</body>
|
|
59
59
|
</html>
|
package/js/app.js
CHANGED
|
@@ -49,7 +49,12 @@ var App = (function () {
|
|
|
49
49
|
var f = _stack.pop();
|
|
50
50
|
if (!f) { tryExitApp(); return; }
|
|
51
51
|
showScreen(f.screen);
|
|
52
|
-
|
|
52
|
+
// Refresh home when returning if Continue Watching / poster / addons changed.
|
|
53
|
+
if (f.screen === 'home' && Views.homeDirty()) {
|
|
54
|
+
Views.renderHome();
|
|
55
|
+
} else {
|
|
56
|
+
Focus.setScope(screenEl(f.screen), f.focus);
|
|
57
|
+
}
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function tryExitApp() {
|
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();
|
|
@@ -251,6 +251,86 @@ var Stremio = (function () {
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// ---- TMDB / MDBList (uses keys from Integrations) ----
|
|
255
|
+
var TMDB_IMG = 'https://image.tmdb.org/t/p/';
|
|
256
|
+
var _tmdbIdCache = {};
|
|
257
|
+
var _tmdbSeasonCache = {};
|
|
258
|
+
|
|
259
|
+
function tmdbKey() { return getIntegrations().tmdb; }
|
|
260
|
+
function tmdbImg(path, size) { return path ? (TMDB_IMG + (size || 'w300') + path) : ''; }
|
|
261
|
+
|
|
262
|
+
function tmdbValidate(key) {
|
|
263
|
+
if (!key) { return Promise.resolve(false); }
|
|
264
|
+
return U.getJSON('https://api.themoviedb.org/3/configuration?api_key=' + encodeURIComponent(key), 8000)
|
|
265
|
+
.then(function () { return true; }, function () { return false; });
|
|
266
|
+
}
|
|
267
|
+
function mdblistValidate(key) {
|
|
268
|
+
if (!key) { return Promise.resolve(false); }
|
|
269
|
+
return U.getJSON('https://api.mdblist.com/user?apikey=' + encodeURIComponent(key), 8000)
|
|
270
|
+
.then(function (r) { return !!(r && !r.error); }, function () { return false; });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// imdb id (tt..) -> TMDB tv id, cached.
|
|
274
|
+
function tmdbFindTvId(imdbId) {
|
|
275
|
+
var key = tmdbKey();
|
|
276
|
+
if (!key || !imdbId) { return Promise.resolve(null); }
|
|
277
|
+
if (_tmdbIdCache[imdbId] !== undefined) { return Promise.resolve(_tmdbIdCache[imdbId]); }
|
|
278
|
+
return U.getJSON('https://api.themoviedb.org/3/find/' + encodeURIComponent(imdbId) +
|
|
279
|
+
'?api_key=' + encodeURIComponent(key) + '&external_source=imdb_id', 9000)
|
|
280
|
+
.then(function (r) {
|
|
281
|
+
var id = (r && r.tv_results && r.tv_results[0]) ? r.tv_results[0].id : null;
|
|
282
|
+
_tmdbIdCache[imdbId] = id; return id;
|
|
283
|
+
}, function () { _tmdbIdCache[imdbId] = null; return null; });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// One request per season → { episodeNumber: {still, rating, overview, name} }.
|
|
287
|
+
function tmdbSeasonEpisodes(tvId, season) {
|
|
288
|
+
var key = tmdbKey();
|
|
289
|
+
if (!key || !tvId) { return Promise.resolve(null); }
|
|
290
|
+
var ck = tvId + ':' + season;
|
|
291
|
+
if (_tmdbSeasonCache[ck] !== undefined) { return Promise.resolve(_tmdbSeasonCache[ck]); }
|
|
292
|
+
return U.getJSON('https://api.themoviedb.org/3/tv/' + tvId + '/season/' + season +
|
|
293
|
+
'?api_key=' + encodeURIComponent(key), 12000)
|
|
294
|
+
.then(function (r) {
|
|
295
|
+
var map = {};
|
|
296
|
+
((r && r.episodes) || []).forEach(function (ep) {
|
|
297
|
+
map[ep.episode_number] = { still: tmdbImg(ep.still_path, 'w300'),
|
|
298
|
+
rating: ep.vote_average, overview: ep.overview, name: ep.name };
|
|
299
|
+
});
|
|
300
|
+
_tmdbSeasonCache[ck] = map; return map;
|
|
301
|
+
}, function () { _tmdbSeasonCache[ck] = null; return null; });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---- OMDb (real IMDb per-episode ratings) ----
|
|
305
|
+
var _omdbCache = {};
|
|
306
|
+
function omdbKey() { return getIntegrations().omdb; }
|
|
307
|
+
|
|
308
|
+
function omdbValidate(key) {
|
|
309
|
+
if (!key) { return Promise.resolve(false); }
|
|
310
|
+
return U.getJSON('https://www.omdbapi.com/?apikey=' + encodeURIComponent(key) + '&i=tt0111161', 8000)
|
|
311
|
+
.then(function (r) { return !!(r && r.Response === 'True'); }, function () { return false; });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// One request per season → { episodeNumber: imdbRatingString }.
|
|
315
|
+
function omdbSeasonRatings(seriesImdb, season) {
|
|
316
|
+
var key = omdbKey();
|
|
317
|
+
if (!key || !seriesImdb) { return Promise.resolve(null); }
|
|
318
|
+
var ck = seriesImdb + ':' + season;
|
|
319
|
+
if (_omdbCache[ck] !== undefined) { return Promise.resolve(_omdbCache[ck]); }
|
|
320
|
+
return U.getJSON('https://www.omdbapi.com/?apikey=' + encodeURIComponent(key) +
|
|
321
|
+
'&i=' + encodeURIComponent(seriesImdb) + '&Season=' + encodeURIComponent(season), 12000)
|
|
322
|
+
.then(function (r) {
|
|
323
|
+
var map = {};
|
|
324
|
+
if (r && r.Episodes) {
|
|
325
|
+
r.Episodes.forEach(function (ep) {
|
|
326
|
+
var n = parseInt(ep.Episode, 10);
|
|
327
|
+
if (!isNaN(n) && ep.imdbRating && ep.imdbRating !== 'N/A') { map[n] = ep.imdbRating; }
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
_omdbCache[ck] = map; return map;
|
|
331
|
+
}, function () { _omdbCache[ck] = null; return null; });
|
|
332
|
+
}
|
|
333
|
+
|
|
254
334
|
// ---- Skip segments (TheIntroDB) ----
|
|
255
335
|
// GET https://api.theintrodb.org/v3/media?imdb_id=tt..[&season=&episode=][&duration_ms=]
|
|
256
336
|
// Returns { intro, recap, credits, preview } arrays of {start,end} in seconds (null = open).
|
|
@@ -366,6 +446,9 @@ var Stremio = (function () {
|
|
|
366
446
|
getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
|
|
367
447
|
getIntegrations: getIntegrations, setIntegration: setIntegration, getPrefs: getPrefs, setPref: setPref,
|
|
368
448
|
getIntroSegments: getIntroSegments,
|
|
449
|
+
tmdbValidate: tmdbValidate, mdblistValidate: mdblistValidate, omdbValidate: omdbValidate,
|
|
450
|
+
tmdbFindTvId: tmdbFindTvId, tmdbSeasonEpisodes: tmdbSeasonEpisodes, tmdbImg: tmdbImg,
|
|
451
|
+
omdbSeasonRatings: omdbSeasonRatings,
|
|
369
452
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
370
453
|
getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
|
|
371
454
|
getSearchCatalogs: getSearchCatalogs, search: search,
|
package/js/views.js
CHANGED
|
@@ -102,72 +102,94 @@ var Views = (function () {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// ================= HOME =================
|
|
105
|
+
var _catalogCache = null; // [{name,type,metas}] — avoids refetch on re-render
|
|
106
|
+
var _homeDirty = false; // set when Continue Watching / poster / addons changed
|
|
107
|
+
|
|
108
|
+
function markHomeDirty() { _homeDirty = true; }
|
|
109
|
+
function homeDirty() { return _homeDirty; }
|
|
110
|
+
function clearCatalogCache() { _catalogCache = null; _homeDirty = true; }
|
|
111
|
+
|
|
112
|
+
function focusHome(s) {
|
|
113
|
+
var firstCard = s.querySelector('.rows .card.focusable');
|
|
114
|
+
Focus.setScope(s, firstCard || undefined);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function continueCard(e) {
|
|
118
|
+
var c = posterCard({ id: e.id.split(':')[0], type: e.type, name: e.name, poster: e.poster }, e.type);
|
|
119
|
+
// For a series, keep the full episode id so detail opens on the right season/episode.
|
|
120
|
+
c.__onselect = function () {
|
|
121
|
+
App.openDetail(e.type, e.id.split(':')[0],
|
|
122
|
+
{ name: e.name, poster: e.poster, type: e.type },
|
|
123
|
+
e.type === 'series' ? e.id : null);
|
|
124
|
+
};
|
|
125
|
+
c.__onlongpress = function () {
|
|
126
|
+
confirm('Remove “' + (e.name || 'this') + '” from Continue Watching?', 'Remove', function () {
|
|
127
|
+
Player.removeProgress(e.type, e.id);
|
|
128
|
+
U.toast('Removed from Continue Watching');
|
|
129
|
+
renderHome();
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
if (e.duration) {
|
|
133
|
+
var p = U.el('div', 'prog');
|
|
134
|
+
p.style.width = Math.min(100, (e.position / e.duration * 100)) + '%';
|
|
135
|
+
c.appendChild(p);
|
|
136
|
+
}
|
|
137
|
+
return c;
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
function renderHome() {
|
|
141
|
+
_homeDirty = false;
|
|
106
142
|
var s = scr('home');
|
|
107
143
|
U.clear(s);
|
|
108
144
|
s.appendChild(header(true, true));
|
|
109
145
|
var rows = U.el('div', 'rows');
|
|
110
146
|
s.appendChild(rows);
|
|
111
147
|
|
|
112
|
-
// Continue watching
|
|
148
|
+
// Continue watching (always fresh — it's local)
|
|
113
149
|
var cont = Player.continueList();
|
|
114
150
|
if (cont.length) {
|
|
115
|
-
rows.appendChild(buildRow('Continue Watching · hold OK to remove', cont.map(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Player.removeProgress(e.type, e.id);
|
|
128
|
-
U.toast('Removed from Continue Watching');
|
|
129
|
-
renderHome();
|
|
130
|
-
});
|
|
131
|
-
};
|
|
132
|
-
if (e.duration) {
|
|
133
|
-
var p = U.el('div', 'prog');
|
|
134
|
-
p.style.width = Math.min(100, (e.position / e.duration * 100)) + '%';
|
|
135
|
-
c.appendChild(p);
|
|
136
|
-
}
|
|
137
|
-
return c;
|
|
138
|
-
})));
|
|
151
|
+
rows.appendChild(buildRow('Continue Watching · hold OK to remove', cont.map(continueCard)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Catalog rows: use cache for an instant re-render; fetch only when cold.
|
|
155
|
+
if (_catalogCache) {
|
|
156
|
+
_catalogCache.forEach(function (c) {
|
|
157
|
+
var row = buildRow(c.name, c.metas.map(function (m) { return posterCard(m, c.type); }));
|
|
158
|
+
row.setAttribute('data-filled', '1');
|
|
159
|
+
rows.appendChild(row);
|
|
160
|
+
});
|
|
161
|
+
focusHome(s);
|
|
162
|
+
return;
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
U.spinner(true);
|
|
142
166
|
Stremio.listCatalogs().then(function (cats) {
|
|
143
167
|
var pick = cats.slice(0, 10);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
var placeholder = buildRow(cat.name, []);
|
|
147
|
-
rows.appendChild(placeholder);
|
|
148
|
-
return placeholder;
|
|
149
|
-
});
|
|
150
|
-
Focus.setScope(s);
|
|
168
|
+
var rowNodes = pick.map(function (cat) { var ph = buildRow(cat.name, []); rows.appendChild(ph); return ph; });
|
|
169
|
+
focusHome(s);
|
|
151
170
|
U.spinner(false);
|
|
152
171
|
|
|
172
|
+
var cache = pick.map(function (cat) { return { name: cat.name, type: cat.type, metas: [] }; });
|
|
153
173
|
var tasks = pick.map(function (cat, i) {
|
|
154
174
|
return function () {
|
|
155
175
|
return Stremio.getCatalog(cat).then(function (metas) {
|
|
156
|
-
|
|
176
|
+
var slice = metas.slice(0, 24);
|
|
177
|
+
cache[i].metas = slice;
|
|
178
|
+
fillRow(rowNodes[i], slice, cat.type);
|
|
157
179
|
});
|
|
158
180
|
};
|
|
159
181
|
});
|
|
160
182
|
U.settleAll(tasks, 3).then(function () {
|
|
161
|
-
// Remove empty rows, refresh focus scope.
|
|
162
183
|
for (var i = rowNodes.length - 1; i >= 0; i--) {
|
|
163
184
|
if (!rowNodes[i].getAttribute('data-filled')) { rows.removeChild(rowNodes[i]); }
|
|
164
185
|
}
|
|
165
|
-
|
|
186
|
+
_catalogCache = cache.filter(function (c) { return c.metas && c.metas.length; });
|
|
187
|
+
if (!Focus.current()) { focusHome(s); }
|
|
166
188
|
});
|
|
167
189
|
}, function (err) {
|
|
168
190
|
U.spinner(false);
|
|
169
191
|
U.toast('Failed to load addons. Check network / Settings.');
|
|
170
|
-
|
|
192
|
+
focusHome(s);
|
|
171
193
|
});
|
|
172
194
|
}
|
|
173
195
|
|
|
@@ -287,8 +309,7 @@ var Views = (function () {
|
|
|
287
309
|
var panel = U.el('div', 'list-panel');
|
|
288
310
|
var bar = U.el('div', 'season-bar');
|
|
289
311
|
ss.forEach(function (sn) {
|
|
290
|
-
var p = U.el('div', 'pill focusable', 'Season ' + sn);
|
|
291
|
-
if (sn === _d.season) { p.style.background = '#2a3346'; }
|
|
312
|
+
var p = U.el('div', 'pill focusable' + (sn === _d.season ? ' season-sel' : ''), 'Season ' + sn);
|
|
292
313
|
p.__onselect = function () { _d.season = sn; drawDetail(); };
|
|
293
314
|
bar.appendChild(p);
|
|
294
315
|
});
|
|
@@ -300,26 +321,71 @@ var Views = (function () {
|
|
|
300
321
|
listScroll.style.height = '340px';
|
|
301
322
|
var eps = meta.videos.filter(function (v) { return (v.season === undefined ? 1 : v.season) === _d.season; });
|
|
302
323
|
eps.sort(function (a, b) { return (a.episode || 0) - (b.episode || 0); });
|
|
324
|
+
var epRefs = [];
|
|
303
325
|
eps.forEach(function (v) {
|
|
304
326
|
var watched = Player.isWatched('series', v.id);
|
|
305
|
-
var e = U.el('div', 'ep focusable' + (watched ? ' watched' : ''));
|
|
306
|
-
|
|
307
|
-
|
|
327
|
+
var e = U.el('div', 'ep ep-row focusable' + (watched ? ' watched' : ''));
|
|
328
|
+
|
|
329
|
+
var thumb = U.el('div', 'ep-thumb');
|
|
330
|
+
if (v.thumbnail) { thumb.style.backgroundImage = 'url("' + v.thumbnail + '")'; }
|
|
331
|
+
e.appendChild(thumb);
|
|
332
|
+
|
|
333
|
+
var body = U.el('div', 'ep-body');
|
|
334
|
+
var line = U.el('div', 'ep-line');
|
|
335
|
+
line.appendChild(document.createTextNode((watched ? '✓ ' : '') + 'E' + (v.episode || '?') + ' ' + (v.title || v.name || '')));
|
|
336
|
+
var rating = U.el('span', 'ep-rating', '');
|
|
337
|
+
line.appendChild(rating);
|
|
338
|
+
body.appendChild(line);
|
|
339
|
+
var ov = U.el('span', 'ep-ov', v.overview || '');
|
|
340
|
+
body.appendChild(ov);
|
|
341
|
+
e.appendChild(body);
|
|
342
|
+
|
|
308
343
|
e.__onselect = function () {
|
|
309
344
|
openStreams(v.id, _d.meta.name + ' S' + _d.season + 'E' + (v.episode || ''));
|
|
310
345
|
};
|
|
311
|
-
// Long-press toggles watched/unwatched.
|
|
312
346
|
e.__onlongpress = function () {
|
|
313
347
|
var now = !Player.isWatched('series', v.id);
|
|
314
348
|
Player.setWatched('series', v.id, now);
|
|
315
349
|
U.toast(now ? 'Marked watched' : 'Marked unwatched');
|
|
316
|
-
_d.target = v.id;
|
|
350
|
+
_d.target = v.id;
|
|
317
351
|
drawDetail();
|
|
318
352
|
};
|
|
319
353
|
if (_d.target && v.id === _d.target) { _d._epFocus = e; }
|
|
320
354
|
listScroll.appendChild(e);
|
|
355
|
+
epRefs.push({ num: v.episode, thumb: thumb, rating: rating, ov: ov });
|
|
321
356
|
});
|
|
322
357
|
panel.appendChild(listScroll);
|
|
358
|
+
|
|
359
|
+
// Enrich episodes (one request per season each). Rating source priority:
|
|
360
|
+
// OMDb (real IMDb) when its key is set, else TMDB vote_average. Images come from TMDB.
|
|
361
|
+
var integ = Stremio.getIntegrations();
|
|
362
|
+
var seasonAtFetch = _d.season;
|
|
363
|
+
|
|
364
|
+
if (integ.omdb) {
|
|
365
|
+
Stremio.omdbSeasonRatings(_d.id, seasonAtFetch).then(function (map) {
|
|
366
|
+
if (!map || _d.season !== seasonAtFetch) { return; }
|
|
367
|
+
epRefs.forEach(function (r) {
|
|
368
|
+
var rt = map[r.num];
|
|
369
|
+
if (rt) { r.rating.textContent = ' ★ ' + rt + ' IMDb'; }
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (integ.tmdb) {
|
|
374
|
+
Stremio.tmdbFindTvId(_d.id).then(function (tvId) {
|
|
375
|
+
if (!tvId || _d.season !== seasonAtFetch) { return; }
|
|
376
|
+
return Stremio.tmdbSeasonEpisodes(tvId, seasonAtFetch).then(function (map) {
|
|
377
|
+
if (!map || _d.season !== seasonAtFetch) { return; }
|
|
378
|
+
epRefs.forEach(function (r) {
|
|
379
|
+
var ep = map[r.num];
|
|
380
|
+
if (!ep) { return; }
|
|
381
|
+
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); }
|
|
384
|
+
if (ep.overview && !r.ov.textContent) { r.ov.textContent = ep.overview; }
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
}
|
|
323
389
|
return panel;
|
|
324
390
|
}
|
|
325
391
|
|
|
@@ -420,6 +486,7 @@ var Views = (function () {
|
|
|
420
486
|
}
|
|
421
487
|
|
|
422
488
|
function startPlayback(stream, videoId, label) {
|
|
489
|
+
markHomeDirty(); // progress/watched will change → refresh Continue Watching on return
|
|
423
490
|
var ctx = { url: stream.url, type: _d.type, id: videoId, name: label,
|
|
424
491
|
poster: _d.meta.poster, streams: playableSources() };
|
|
425
492
|
if (_d.type === 'series' && _d.meta.videos) {
|
|
@@ -480,8 +547,9 @@ var Views = (function () {
|
|
|
480
547
|
input.className = 'focusable';
|
|
481
548
|
input.placeholder = 'Search movies & series…';
|
|
482
549
|
input.__onselect = function () { input.focus(); };
|
|
550
|
+
// Enter (or the Search button) runs the search. No onchange — it fires on blur
|
|
551
|
+
// when moving to a result and would re-run the search and steal focus.
|
|
483
552
|
input.onkeydown = function (ev) { if (ev.keyCode === 13) { if (ev.stopPropagation) { ev.stopPropagation(); } runSearch(input.value); } };
|
|
484
|
-
input.onchange = function () { runSearch(input.value); };
|
|
485
553
|
bar.appendChild(input);
|
|
486
554
|
|
|
487
555
|
var go = U.el('div', 'pill focusable search-go', 'Search');
|
|
@@ -554,6 +622,18 @@ var Views = (function () {
|
|
|
554
622
|
confirmBtn.__onselect = doConfirm;
|
|
555
623
|
cancelBtn.__onselect = function () { App.back(); };
|
|
556
624
|
row.appendChild(confirmBtn);
|
|
625
|
+
if (cfg.validator) {
|
|
626
|
+
var valBtn = U.el('div', 'pill focusable', 'Validate');
|
|
627
|
+
valBtn.__onselect = function () {
|
|
628
|
+
var v = ('' + input.value).trim();
|
|
629
|
+
if (!v) { U.toast('Enter a key first'); return; }
|
|
630
|
+
U.toast('Validating…', 4000);
|
|
631
|
+
cfg.validator(v).then(function (ok) {
|
|
632
|
+
U.toast(ok ? '✓ Key is valid' : '✗ Key rejected (or unreachable)', 3500);
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
row.appendChild(valBtn);
|
|
636
|
+
}
|
|
557
637
|
row.appendChild(cancelBtn);
|
|
558
638
|
wrap.appendChild(row);
|
|
559
639
|
|
|
@@ -656,6 +736,7 @@ var Views = (function () {
|
|
|
656
736
|
item.__onselect = function () {
|
|
657
737
|
confirm('Remove "' + name + '"?', 'Remove', function () {
|
|
658
738
|
Stremio.removeAddon(url);
|
|
739
|
+
clearCatalogCache();
|
|
659
740
|
U.toast('Removed');
|
|
660
741
|
setTimeout(renderSettings, 300);
|
|
661
742
|
});
|
|
@@ -669,6 +750,7 @@ var Views = (function () {
|
|
|
669
750
|
reset.__onselect = function () {
|
|
670
751
|
confirm('Reset to default addons? This removes all addons you’ve added.', 'Reset', function () {
|
|
671
752
|
Stremio.resetAddons();
|
|
753
|
+
clearCatalogCache();
|
|
672
754
|
U.toast('Addons reset to default');
|
|
673
755
|
setTimeout(renderSettings, 300);
|
|
674
756
|
});
|
|
@@ -684,7 +766,7 @@ var Views = (function () {
|
|
|
684
766
|
hint: 'Type a Stremio addon manifest URL, then Confirm. RETURN cancels — nothing is saved unless you confirm.',
|
|
685
767
|
value: 'https://', placeholder: 'https://addon.example/manifest.json',
|
|
686
768
|
validate: function (v) { return (!v || v === 'https://' || v.indexOf('://') === -1 || v.length < 12) ? 'Enter a full manifest URL' : null; },
|
|
687
|
-
onConfirm: function (v) { Stremio.addAddon(v); U.toast('Addon added'); App.back(); renderSettings(); }
|
|
769
|
+
onConfirm: function (v) { Stremio.addAddon(v); clearCatalogCache(); U.toast('Addon added'); App.back(); renderSettings(); }
|
|
688
770
|
});
|
|
689
771
|
};
|
|
690
772
|
wrap.appendChild(addRow);
|
|
@@ -697,7 +779,7 @@ var Views = (function () {
|
|
|
697
779
|
|
|
698
780
|
function maskKey(v) { v = '' + v; return v.length <= 4 ? '••••' : ('••••' + v.slice(-4)); }
|
|
699
781
|
|
|
700
|
-
function keyRow(title, key, placeholder) {
|
|
782
|
+
function keyRow(title, key, placeholder, validator) {
|
|
701
783
|
var val = Stremio.getIntegrations()[key] || '';
|
|
702
784
|
var r = U.el('div', 'addon-item focusable');
|
|
703
785
|
r.appendChild(U.el('div', 'a-name', title));
|
|
@@ -705,8 +787,8 @@ var Views = (function () {
|
|
|
705
787
|
r.__onselect = function () {
|
|
706
788
|
App.openEntry({
|
|
707
789
|
title: title,
|
|
708
|
-
hint: 'Paste your key, then Confirm. Leave blank and Confirm to clear
|
|
709
|
-
value: val, placeholder: placeholder,
|
|
790
|
+
hint: 'Paste your key, then Confirm. Use Validate to test it. Leave blank and Confirm to clear.',
|
|
791
|
+
value: val, placeholder: placeholder, validator: validator,
|
|
710
792
|
onConfirm: function (v) { Stremio.setIntegration(key, v); U.toast(v ? 'Saved' : 'Cleared'); App.back(); renderSettings(); }
|
|
711
793
|
});
|
|
712
794
|
};
|
|
@@ -715,9 +797,10 @@ var Views = (function () {
|
|
|
715
797
|
|
|
716
798
|
function settingsIntegrations(wrap) {
|
|
717
799
|
wrap.appendChild(U.el('h2', null, 'Integrations'));
|
|
718
|
-
wrap.appendChild(keyRow('
|
|
719
|
-
wrap.appendChild(keyRow('
|
|
720
|
-
wrap.appendChild(
|
|
800
|
+
wrap.appendChild(keyRow('OMDb API key', 'omdb', 'OMDb API key', Stremio.omdbValidate));
|
|
801
|
+
wrap.appendChild(keyRow('TMDB API key (v3)', 'tmdb', 'TMDB v3 API key', Stremio.tmdbValidate));
|
|
802
|
+
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.'));
|
|
721
804
|
Focus.setScope(scr('settings'));
|
|
722
805
|
}
|
|
723
806
|
|
|
@@ -741,6 +824,7 @@ var Views = (function () {
|
|
|
741
824
|
row.appendChild(U.el('div', 'a-url', 'Press OK to change'));
|
|
742
825
|
row.__onselect = function () {
|
|
743
826
|
Stremio.setPref('posterShape', nextVal(POSTER_SHAPES, Stremio.getPrefs().posterShape));
|
|
827
|
+
markHomeDirty(); // home reflects the new shape on return
|
|
744
828
|
render();
|
|
745
829
|
};
|
|
746
830
|
wrap.appendChild(row);
|
|
@@ -752,6 +836,7 @@ var Views = (function () {
|
|
|
752
836
|
renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
|
|
753
837
|
settingsRoot: settingsRoot, backInSettings: backInSettings,
|
|
754
838
|
renderSearch: renderSearch, renderEntry: renderEntry, backInDetail: backInDetail,
|
|
839
|
+
homeDirty: homeDirty,
|
|
755
840
|
isModalOpen: function () { return _modalOpen; }, closeModal: closeModal
|
|
756
841
|
};
|
|
757
842
|
})();
|