nuvio-tizen 1.8.2 → 1.8.3
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 +52 -0
- package/js/views.js +122 -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.3">
|
|
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.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>
|
|
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
|
@@ -251,6 +251,56 @@ 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
|
+
|
|
254
304
|
// ---- Skip segments (TheIntroDB) ----
|
|
255
305
|
// GET https://api.theintrodb.org/v3/media?imdb_id=tt..[&season=&episode=][&duration_ms=]
|
|
256
306
|
// Returns { intro, recap, credits, preview } arrays of {start,end} in seconds (null = open).
|
|
@@ -366,6 +416,8 @@ var Stremio = (function () {
|
|
|
366
416
|
getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
|
|
367
417
|
getIntegrations: getIntegrations, setIntegration: setIntegration, getPrefs: getPrefs, setPref: setPref,
|
|
368
418
|
getIntroSegments: getIntroSegments,
|
|
419
|
+
tmdbValidate: tmdbValidate, mdblistValidate: mdblistValidate,
|
|
420
|
+
tmdbFindTvId: tmdbFindTvId, tmdbSeasonEpisodes: tmdbSeasonEpisodes, tmdbImg: tmdbImg,
|
|
369
421
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
370
422
|
getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
|
|
371
423
|
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,58 @@ 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 with TMDB episode images + ratings (one request per season) if a key is set.
|
|
360
|
+
if (Stremio.getIntegrations().tmdb) {
|
|
361
|
+
var seasonAtFetch = _d.season;
|
|
362
|
+
Stremio.tmdbFindTvId(_d.id).then(function (tvId) {
|
|
363
|
+
if (!tvId || _d.season !== seasonAtFetch) { return; }
|
|
364
|
+
return Stremio.tmdbSeasonEpisodes(tvId, seasonAtFetch).then(function (map) {
|
|
365
|
+
if (!map || _d.season !== seasonAtFetch) { return; }
|
|
366
|
+
epRefs.forEach(function (r) {
|
|
367
|
+
var ep = map[r.num];
|
|
368
|
+
if (!ep) { return; }
|
|
369
|
+
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); }
|
|
371
|
+
if (ep.overview && !r.ov.textContent) { r.ov.textContent = ep.overview; }
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
323
376
|
return panel;
|
|
324
377
|
}
|
|
325
378
|
|
|
@@ -420,6 +473,7 @@ var Views = (function () {
|
|
|
420
473
|
}
|
|
421
474
|
|
|
422
475
|
function startPlayback(stream, videoId, label) {
|
|
476
|
+
markHomeDirty(); // progress/watched will change → refresh Continue Watching on return
|
|
423
477
|
var ctx = { url: stream.url, type: _d.type, id: videoId, name: label,
|
|
424
478
|
poster: _d.meta.poster, streams: playableSources() };
|
|
425
479
|
if (_d.type === 'series' && _d.meta.videos) {
|
|
@@ -480,8 +534,9 @@ var Views = (function () {
|
|
|
480
534
|
input.className = 'focusable';
|
|
481
535
|
input.placeholder = 'Search movies & series…';
|
|
482
536
|
input.__onselect = function () { input.focus(); };
|
|
537
|
+
// Enter (or the Search button) runs the search. No onchange — it fires on blur
|
|
538
|
+
// when moving to a result and would re-run the search and steal focus.
|
|
483
539
|
input.onkeydown = function (ev) { if (ev.keyCode === 13) { if (ev.stopPropagation) { ev.stopPropagation(); } runSearch(input.value); } };
|
|
484
|
-
input.onchange = function () { runSearch(input.value); };
|
|
485
540
|
bar.appendChild(input);
|
|
486
541
|
|
|
487
542
|
var go = U.el('div', 'pill focusable search-go', 'Search');
|
|
@@ -554,6 +609,18 @@ var Views = (function () {
|
|
|
554
609
|
confirmBtn.__onselect = doConfirm;
|
|
555
610
|
cancelBtn.__onselect = function () { App.back(); };
|
|
556
611
|
row.appendChild(confirmBtn);
|
|
612
|
+
if (cfg.validator) {
|
|
613
|
+
var valBtn = U.el('div', 'pill focusable', 'Validate');
|
|
614
|
+
valBtn.__onselect = function () {
|
|
615
|
+
var v = ('' + input.value).trim();
|
|
616
|
+
if (!v) { U.toast('Enter a key first'); return; }
|
|
617
|
+
U.toast('Validating…', 4000);
|
|
618
|
+
cfg.validator(v).then(function (ok) {
|
|
619
|
+
U.toast(ok ? '✓ Key is valid' : '✗ Key rejected (or unreachable)', 3500);
|
|
620
|
+
});
|
|
621
|
+
};
|
|
622
|
+
row.appendChild(valBtn);
|
|
623
|
+
}
|
|
557
624
|
row.appendChild(cancelBtn);
|
|
558
625
|
wrap.appendChild(row);
|
|
559
626
|
|
|
@@ -656,6 +723,7 @@ var Views = (function () {
|
|
|
656
723
|
item.__onselect = function () {
|
|
657
724
|
confirm('Remove "' + name + '"?', 'Remove', function () {
|
|
658
725
|
Stremio.removeAddon(url);
|
|
726
|
+
clearCatalogCache();
|
|
659
727
|
U.toast('Removed');
|
|
660
728
|
setTimeout(renderSettings, 300);
|
|
661
729
|
});
|
|
@@ -669,6 +737,7 @@ var Views = (function () {
|
|
|
669
737
|
reset.__onselect = function () {
|
|
670
738
|
confirm('Reset to default addons? This removes all addons you’ve added.', 'Reset', function () {
|
|
671
739
|
Stremio.resetAddons();
|
|
740
|
+
clearCatalogCache();
|
|
672
741
|
U.toast('Addons reset to default');
|
|
673
742
|
setTimeout(renderSettings, 300);
|
|
674
743
|
});
|
|
@@ -684,7 +753,7 @@ var Views = (function () {
|
|
|
684
753
|
hint: 'Type a Stremio addon manifest URL, then Confirm. RETURN cancels — nothing is saved unless you confirm.',
|
|
685
754
|
value: 'https://', placeholder: 'https://addon.example/manifest.json',
|
|
686
755
|
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(); }
|
|
756
|
+
onConfirm: function (v) { Stremio.addAddon(v); clearCatalogCache(); U.toast('Addon added'); App.back(); renderSettings(); }
|
|
688
757
|
});
|
|
689
758
|
};
|
|
690
759
|
wrap.appendChild(addRow);
|
|
@@ -697,7 +766,7 @@ var Views = (function () {
|
|
|
697
766
|
|
|
698
767
|
function maskKey(v) { v = '' + v; return v.length <= 4 ? '••••' : ('••••' + v.slice(-4)); }
|
|
699
768
|
|
|
700
|
-
function keyRow(title, key, placeholder) {
|
|
769
|
+
function keyRow(title, key, placeholder, validator) {
|
|
701
770
|
var val = Stremio.getIntegrations()[key] || '';
|
|
702
771
|
var r = U.el('div', 'addon-item focusable');
|
|
703
772
|
r.appendChild(U.el('div', 'a-name', title));
|
|
@@ -705,8 +774,8 @@ var Views = (function () {
|
|
|
705
774
|
r.__onselect = function () {
|
|
706
775
|
App.openEntry({
|
|
707
776
|
title: title,
|
|
708
|
-
hint: 'Paste your key, then Confirm. Leave blank and Confirm to clear
|
|
709
|
-
value: val, placeholder: placeholder,
|
|
777
|
+
hint: 'Paste your key, then Confirm. Use Validate to test it. Leave blank and Confirm to clear.',
|
|
778
|
+
value: val, placeholder: placeholder, validator: validator,
|
|
710
779
|
onConfirm: function (v) { Stremio.setIntegration(key, v); U.toast(v ? 'Saved' : 'Cleared'); App.back(); renderSettings(); }
|
|
711
780
|
});
|
|
712
781
|
};
|
|
@@ -715,9 +784,9 @@ var Views = (function () {
|
|
|
715
784
|
|
|
716
785
|
function settingsIntegrations(wrap) {
|
|
717
786
|
wrap.appendChild(U.el('h2', null, 'Integrations'));
|
|
718
|
-
wrap.appendChild(keyRow('TMDB API key (v3)', 'tmdb', 'TMDB v3 API key'));
|
|
719
|
-
wrap.appendChild(keyRow('MDBList API key', 'mdblist', 'MDBList API key'));
|
|
720
|
-
wrap.appendChild(U.el('div', 'hint', 'Keys are stored on this TV only. RETURN goes back.'));
|
|
787
|
+
wrap.appendChild(keyRow('TMDB API key (v3)', 'tmdb', 'TMDB v3 API key', Stremio.tmdbValidate));
|
|
788
|
+
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.'));
|
|
721
790
|
Focus.setScope(scr('settings'));
|
|
722
791
|
}
|
|
723
792
|
|
|
@@ -741,6 +810,7 @@ var Views = (function () {
|
|
|
741
810
|
row.appendChild(U.el('div', 'a-url', 'Press OK to change'));
|
|
742
811
|
row.__onselect = function () {
|
|
743
812
|
Stremio.setPref('posterShape', nextVal(POSTER_SHAPES, Stremio.getPrefs().posterShape));
|
|
813
|
+
markHomeDirty(); // home reflects the new shape on return
|
|
744
814
|
render();
|
|
745
815
|
};
|
|
746
816
|
wrap.appendChild(row);
|
|
@@ -752,6 +822,7 @@ var Views = (function () {
|
|
|
752
822
|
renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
|
|
753
823
|
settingsRoot: settingsRoot, backInSettings: backInSettings,
|
|
754
824
|
renderSearch: renderSearch, renderEntry: renderEntry, backInDetail: backInDetail,
|
|
825
|
+
homeDirty: homeDirty,
|
|
755
826
|
isModalOpen: function () { return _modalOpen; }, closeModal: closeModal
|
|
756
827
|
};
|
|
757
828
|
})();
|