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 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 { color: #17203a; }
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.2">
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.2"></script>
51
- <script src="js/util.js?v=1.8.2"></script>
52
- <script src="js/stremio.js?v=1.8.2"></script>
53
- <script src="js/keys.js?v=1.8.2"></script>
54
- <script src="js/focus.js?v=1.8.2"></script>
55
- <script src="js/player.js?v=1.8.2"></script>
56
- <script src="js/views.js?v=1.8.2"></script>
57
- <script src="js/app.js?v=1.8.2"></script>
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
- Focus.setScope(screenEl(f.screen), f.focus);
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(function (e) {
116
- var c = posterCard({ id: e.id.split(':')[0], type: e.type, name: e.name, poster: e.poster }, e.type);
117
- // For a series, keep the full episode id so detail opens on the right
118
- // season and focuses the exact episode being resumed.
119
- c.__onselect = function () {
120
- App.openDetail(e.type, e.id.split(':')[0],
121
- { name: e.name, poster: e.poster, type: e.type },
122
- e.type === 'series' ? e.id : null);
123
- };
124
- // Long-press (hold OK) removes the item from Continue Watching.
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
- })));
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
- // Render placeholder rows in order, fill as data arrives.
145
- var rowNodes = pick.map(function (cat) {
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
- fillRow(rowNodes[i], metas.slice(0, 24), cat.type);
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
- if (!Focus.current()) { Focus.setScope(s); }
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
- Focus.setScope(s);
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
- e.appendChild(document.createTextNode((watched ? '✓ ' : '') + 'E' + (v.episode || '?') + ' ' + (v.title || v.name || '')));
307
- if (v.overview) { e.appendChild(U.el('span', 'ep-sub', v.overview)); }
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; // keep focus on this episode after re-render
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 it.',
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('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.'));
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
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
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",