nuvio-tizen 1.9.4 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.xml CHANGED
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
3
- id="http://nuvio.tizen/NuvioTizen" version="1.9.4" viewmodes="maximized">
3
+ id="http://nuvio.tizen/NuvioTizen" version="1.9.5" viewmodes="maximized">
4
4
  <tizen:application id="Nuvio00001.Nuvio" package="Nuvio00001" required_version="3.0"/>
5
5
  <content src="index.html"/>
6
6
  <feature name="http://tizen.org/feature/screen.size.all"/>
package/css/style.css CHANGED
@@ -220,4 +220,5 @@ html, body {
220
220
  .tab-ic { margin-left: 8px; font-size: 22px; }
221
221
  .tab-pill.active { background: #2a3346; color: #fff; }
222
222
  .tab-pill.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; }
223
+ .home-header .hchip.active { background: #2a3346; color: #fff; opacity: 1; }
223
224
  .row-empty { margin: 50px 60px; font-size: 26px; color: #8b93a3; }
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.9.4">
7
+ <link rel="stylesheet" href="css/style.css?v=1.9.5">
8
8
  </head>
9
9
  <body>
10
10
  <!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
@@ -50,15 +50,15 @@
50
50
 
51
51
  <!-- Load order matters: polyfills -> util -> iptv -> data -> input -> player -> ytplay -> views -> app -->
52
52
  <!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
53
- <script src="js/polyfills.js?v=1.9.4"></script>
54
- <script src="js/util.js?v=1.9.4"></script>
55
- <script src="js/iptv.js?v=1.9.4"></script>
56
- <script src="js/stremio.js?v=1.9.4"></script>
57
- <script src="js/keys.js?v=1.9.4"></script>
58
- <script src="js/focus.js?v=1.9.4"></script>
59
- <script src="js/player.js?v=1.9.4"></script>
60
- <script src="js/ytplay.js?v=1.9.4"></script>
61
- <script src="js/views.js?v=1.9.4"></script>
62
- <script src="js/app.js?v=1.9.4"></script>
53
+ <script src="js/polyfills.js?v=1.9.5"></script>
54
+ <script src="js/util.js?v=1.9.5"></script>
55
+ <script src="js/iptv.js?v=1.9.5"></script>
56
+ <script src="js/stremio.js?v=1.9.5"></script>
57
+ <script src="js/keys.js?v=1.9.5"></script>
58
+ <script src="js/focus.js?v=1.9.5"></script>
59
+ <script src="js/player.js?v=1.9.5"></script>
60
+ <script src="js/ytplay.js?v=1.9.5"></script>
61
+ <script src="js/views.js?v=1.9.5"></script>
62
+ <script src="js/app.js?v=1.9.5"></script>
63
63
  </body>
64
64
  </html>
package/js/app.js CHANGED
@@ -23,6 +23,14 @@ var App = (function () {
23
23
  Views.renderDetail(type, id, preview, target);
24
24
  }
25
25
 
26
+ // Go to a Home tab from the shared top menu (works from any screen).
27
+ function goHome(tab) {
28
+ Views.setHomeTab(tab);
29
+ _stack = []; // Home is the root
30
+ showScreen('home');
31
+ Views.renderHome();
32
+ }
33
+
26
34
  // Live TV: skip the info page — load sources and (if a single one) play immediately.
27
35
  function openLive(type, id, preview) {
28
36
  if (!id) { return; }
@@ -133,8 +141,8 @@ var App = (function () {
133
141
 
134
142
  return {
135
143
  start: start, back: back,
136
- openDetail: openDetail, openLive: openLive, openSettings: openSettings, openSearch: openSearch,
137
- openEntry: openEntry
144
+ openDetail: openDetail, openLive: openLive, goHome: goHome,
145
+ openSettings: openSettings, openSearch: openSearch, openEntry: openEntry
138
146
  };
139
147
  })();
140
148
 
package/js/player.js CHANGED
@@ -610,11 +610,13 @@ var Player = (function () {
610
610
 
611
611
  // ---------- contextual quick-action bar (UP) ----------
612
612
  function barButtons() {
613
- var b = [{ label: isPaused() ? '▶ Play' : '❙❙ Pause', run: togglePlay }];
613
+ // Glyphs kept to the old Geometric-Shapes/Arrows blocks the media-control
614
+ // symbols (⏵ ⏸ ⏮ ⏭ ⤶) don't render on Tizen 3.0's fonts.
615
+ var b = [{ label: isPaused() ? '▶ Play' : '‖ Pause', run: togglePlay }];
614
616
  if (!_isLive) { b.push({ label: '↺ Restart', run: function () { seekTo(0); closeBar(); } }); }
615
- if (_ctx.hasPrev) { b.push({ label: ' Previous', run: function () { closeBar(); _ctx.playPrev(); } }); }
616
- if (_ctx.hasNext) { b.push({ label: ' Next', run: function () { closeBar(); _ctx.playNext(); } }); }
617
- b.push({ label: ' Back', run: function () { exit(); } }); // stop playback, return to the previous page
617
+ if (_ctx.hasPrev) { b.push({ label: '◀◀ Previous', run: function () { closeBar(); _ctx.playPrev(); } }); }
618
+ if (_ctx.hasNext) { b.push({ label: '▶▶ Next', run: function () { closeBar(); _ctx.playNext(); } }); }
619
+ b.push({ label: ' Back', run: function () { exit(); } }); // stop playback, return to the previous page
618
620
  return b;
619
621
  }
620
622
  function openBar() {
package/js/views.js CHANGED
@@ -77,42 +77,70 @@ var Views = (function () {
77
77
  }
78
78
 
79
79
  // ---------- shared bits ----------
80
- function header(withGear, withSearch) {
80
+ // Plain brand-only header, used by the text-entry page. Home/Search/Settings use
81
+ // buildTopMenu() instead (shared top menu with tab pills + Search + Settings).
82
+ function header() {
81
83
  var h = U.el('div', 'header');
82
84
  var brand = U.el('div', 'brand');
83
85
  brand.innerHTML = 'NUVIO<span class="dot">.</span>';
84
86
  h.appendChild(brand);
85
87
  h.appendChild(U.el('div', 'spacer'));
86
- if (withSearch) {
87
- var srch = U.el('div', 'gear hchip focusable', 'Search 🔍');
88
- srch.style.marginRight = '20px';
89
- srch.__onselect = function () { App.openSearch(); };
90
- srch.__navDOWN = focusActiveTabOrRow;
91
- h.appendChild(srch);
92
- }
93
- if (withGear) {
94
- var g = U.el('div', 'gear hchip focusable', 'Settings ⚙');
95
- g.__onselect = function () { App.openSettings(); };
96
- g.__navDOWN = focusActiveTabOrRow;
97
- h.appendChild(g);
98
- }
99
88
  return h;
100
89
  }
101
90
 
102
- // DOWN from the header lands on the active tab pill (then DOWN again the rows).
103
- function focusActiveTabOrRow() {
104
- var home = scr('home');
105
- var pill = home.querySelector('.tab-pill.active');
106
- if (pill) { Focus.setFocus(pill); return true; }
107
- return focusFirstRowItem();
91
+ // The shared top menu, used on Home / Search / Settings so they look and navigate
92
+ // the same: tab pills + Search + Settings on the left, NUVIO on the right.
93
+ // `active` is the key of the current view: a tab key, 'search' or 'settings'.
94
+ function buildTopMenu(active) {
95
+ var head = U.el('div', 'header home-header');
96
+ var tabbar = U.el('div', 'tabbar-inline');
97
+ availableTabs().forEach(function (t) {
98
+ var pill = U.el('div', 'tab-pill focusable' + (t.key === active ? ' active' : ''));
99
+ pill.appendChild(document.createTextNode(t.label));
100
+ pill.appendChild(U.el('span', 'tab-ic', t.icon));
101
+ pill.__onselect = function () { App.goHome(t.key); };
102
+ pill.__navDOWN = focusBelowTop;
103
+ tabbar.appendChild(pill);
104
+ });
105
+ head.appendChild(tabbar);
106
+
107
+ var srch = U.el('div', 'gear hchip focusable' + (active === 'search' ? ' active' : ''), 'Search 🔍');
108
+ srch.__onselect = function () { App.openSearch(); };
109
+ srch.__navDOWN = focusBelowTop;
110
+ head.appendChild(srch);
111
+
112
+ var gear = U.el('div', 'gear hchip focusable' + (active === 'settings' ? ' active' : ''), 'Settings ⚙');
113
+ gear.__onselect = function () { App.openSettings(); };
114
+ gear.__navDOWN = focusBelowTop;
115
+ head.appendChild(gear);
116
+
117
+ head.appendChild(U.el('div', 'spacer'));
118
+ var brand = U.el('div', 'brand');
119
+ brand.innerHTML = 'NUVIO<span class="dot">.</span>';
120
+ head.appendChild(brand);
121
+ return head;
122
+ }
123
+
124
+ // Which tabs to show in the top menu (needs the home catalogs; falls back to the
125
+ // standard set before they've loaded so the menu is consistent everywhere).
126
+ function availableTabs() {
127
+ var cont = Player.continueList();
128
+ if (_homeCatalogs === null) {
129
+ return HOME_TABS.filter(function (t) { return t.key !== 'continue'; });
130
+ }
131
+ return HOME_TABS.filter(function (t) {
132
+ return t.key === 'continue' ? cont.length > 0 : catsForTab(t.key).length > 0;
133
+ });
108
134
  }
109
135
 
110
- // DOWN from a tab pill lands on the first item of the first row.
111
- function focusFirstRowItem() {
112
- var rows = scr('home').querySelector('.rows');
113
- if (!rows) { return false; }
114
- var first = rows.querySelector('.focusable');
115
- if (first) { Focus.setFocus(first); return true; }
136
+ // DOWN from the top menu lands on the first focusable in the content area below it.
137
+ function focusBelowTop() {
138
+ var s = _current();
139
+ var all = s.querySelectorAll('.focusable');
140
+ for (var i = 0; i < all.length; i++) {
141
+ var r = all[i].getBoundingClientRect();
142
+ if (r.height > 0 && r.top > 80) { Focus.setFocus(all[i]); return true; }
143
+ }
116
144
  return false;
117
145
  }
118
146
 
@@ -147,7 +175,7 @@ var Views = (function () {
147
175
  // other three are the catalog rows of that Stremio type, in the user's
148
176
  // Home-Feeds order/visibility. A tab only appears when it has content.
149
177
  var HOME_TABS = [
150
- { key: 'continue', icon: '', label: 'Continue Watching' },
178
+ { key: 'continue', icon: '', label: 'Continue Watching' },
151
179
  { key: 'movie', icon: '🎬', label: 'Movies' },
152
180
  { key: 'series', icon: '📺', label: 'Series' },
153
181
  { key: 'tv', icon: '📡', label: 'Live TV' },
@@ -169,6 +197,7 @@ var Views = (function () {
169
197
  var _homeCatalogs = null; // [{name,type,metas}] — fetched once per session, filtered per tab
170
198
  var _homeTab = null; // active tab key
171
199
  var _homeDirty = false; // set when Continue Watching / poster / addons changed
200
+ var _focusTopPill = false; // next home render should focus the active tab pill
172
201
 
173
202
  function markHomeDirty() { _homeDirty = true; }
174
203
  function homeDirty() { return _homeDirty; }
@@ -206,92 +235,62 @@ var Views = (function () {
206
235
  return c;
207
236
  }
208
237
 
238
+ // Switch the active home tab (from the shared top menu) and show it.
239
+ function setHomeTab(key) { _homeTab = key || _homeTab; _focusTopPill = true; }
240
+
209
241
  function renderHome() {
210
242
  _homeDirty = false;
211
243
  var s = scr('home');
212
244
  U.clear(s);
213
245
 
214
- // Single top row: tab pills + Search + Settings on the left, NUVIO logo top-right.
215
- var head = U.el('div', 'header home-header');
216
- var tabbar = U.el('div', 'tabbar-inline');
217
- head.appendChild(tabbar);
218
- var srch = U.el('div', 'gear hchip focusable', 'Search 🔍');
219
- srch.__onselect = function () { App.openSearch(); };
220
- srch.__navDOWN = focusFirstRowItem;
221
- head.appendChild(srch);
222
- var gear = U.el('div', 'gear hchip focusable', 'Settings ⚙');
223
- gear.__onselect = function () { App.openSettings(); };
224
- gear.__navDOWN = focusFirstRowItem;
225
- head.appendChild(gear);
226
- head.appendChild(U.el('div', 'spacer'));
227
- var brand = U.el('div', 'brand');
228
- brand.innerHTML = 'NUVIO<span class="dot">.</span>';
229
- head.appendChild(brand);
230
- s.appendChild(head);
246
+ // Keep the active tab valid for what's actually available.
247
+ var avail = availableTabs();
248
+ if (!_homeTab || !avail.some(function (t) { return t.key === _homeTab; })) {
249
+ _homeTab = avail.length ? avail[0].key : 'movie';
250
+ }
231
251
 
252
+ s.appendChild(buildTopMenu(_homeTab));
232
253
  var rows = U.el('div', 'rows');
233
254
  s.appendChild(rows);
234
255
 
235
- // Re-render the pills + the active tab's rows from cached data (no refetch).
236
- function paint(preferPill) {
237
- var cont = Player.continueList();
238
- var avail = HOME_TABS.filter(function (t) {
239
- return t.key === 'continue' ? cont.length > 0 : catsForTab(t.key).length > 0;
240
- });
241
- if (!_homeTab || !avail.some(function (t) { return t.key === _homeTab; })) {
242
- _homeTab = avail.length ? avail[0].key : 'movie';
243
- }
244
-
245
- U.clear(tabbar);
246
- avail.forEach(function (t) {
247
- var pill = U.el('div', 'tab-pill focusable' + (t.key === _homeTab ? ' active' : ''));
248
- pill.appendChild(document.createTextNode(t.label));
249
- pill.appendChild(U.el('span', 'tab-ic', t.icon));
250
- pill.__onselect = function () { if (_homeTab !== t.key) { _homeTab = t.key; paint(true); } };
251
- pill.__navDOWN = focusFirstRowItem;
252
- tabbar.appendChild(pill);
253
- });
254
-
255
- U.clear(rows);
256
- if (_homeTab === 'continue') {
257
- rows.appendChild(buildRow('Continue Watching · hold OK to remove', cont.map(continueCard)));
258
- } else {
259
- var cats = catsForTab(_homeTab);
260
- if (!cats.length) {
261
- rows.appendChild(U.el('div', 'row-empty', 'Nothing here yet.'));
262
- } else {
263
- cats.forEach(function (c) {
264
- rows.appendChild(buildRow(c.name, c.metas.map(function (m) { return posterCard(m, c.type); })));
265
- });
266
- }
256
+ // Rows for the active tab, from the once-per-session catalog cache.
257
+ if (_homeTab === 'continue') {
258
+ rows.appendChild(buildRow('Continue Watching · hold OK to remove',
259
+ Player.continueList().map(continueCard)));
260
+ } else {
261
+ var cats = catsForTab(_homeTab);
262
+ if (!cats.length) { rows.appendChild(U.el('div', 'row-empty', 'Nothing here yet.')); }
263
+ else {
264
+ cats.forEach(function (c) {
265
+ rows.appendChild(buildRow(c.name, c.metas.map(function (m) { return posterCard(m, c.type); })));
266
+ });
267
267
  }
268
- focusHome(s, preferPill);
269
268
  }
270
-
271
- // Catalog metas are fetched once per session and cached; tab switches are instant.
272
- if (_homeCatalogs) { paint(false); return; }
273
-
274
- // Paint immediately so Continue Watching / pills show while catalogs load.
275
- paint(false);
276
- U.spinner(true);
277
- Stremio.catalogsForHome().then(function (cats) {
278
- var acc = cats.map(function (cat) { return { name: cat.name, type: cat.type, metas: [] }; });
279
- var tasks = cats.map(function (cat, i) {
280
- return function () {
281
- return Stremio.getCatalog(cat).then(function (metas) { acc[i].metas = metas.slice(0, 24); });
282
- };
283
- });
284
- U.settleAll(tasks, 3).then(function () {
285
- _homeCatalogs = acc.filter(function (c) { return c.metas && c.metas.length; });
269
+ focusHome(s, _focusTopPill);
270
+ _focusTopPill = false;
271
+
272
+ // Cold start: fetch every catalog's metas once, cache, then re-render.
273
+ if (_homeCatalogs === null) {
274
+ _homeCatalogs = []; // guard against re-entry while loading
275
+ U.spinner(true);
276
+ Stremio.catalogsForHome().then(function (cats) {
277
+ var acc = cats.map(function (cat) { return { name: cat.name, type: cat.type, metas: [] }; });
278
+ var tasks = cats.map(function (cat, i) {
279
+ return function () {
280
+ return Stremio.getCatalog(cat).then(function (metas) { acc[i].metas = metas.slice(0, 24); });
281
+ };
282
+ });
283
+ U.settleAll(tasks, 3).then(function () {
284
+ _homeCatalogs = acc.filter(function (c) { return c.metas && c.metas.length; });
285
+ U.spinner(false);
286
+ if (_current() === s) { renderHome(); }
287
+ });
288
+ }, function () {
286
289
  U.spinner(false);
287
- paint(false);
290
+ U.toast('Failed to load addons. Check network / Settings.');
291
+ _homeCatalogs = [];
288
292
  });
289
- }, function () {
290
- U.spinner(false);
291
- U.toast('Failed to load addons. Check network / Settings.');
292
- _homeCatalogs = [];
293
- paint(false);
294
- });
293
+ }
295
294
  }
296
295
 
297
296
  function buildRow(title, cards) {
@@ -329,6 +328,7 @@ var Views = (function () {
329
328
  var playable = list.filter(Stremio.isPlayable);
330
329
  if (!playable.length) { U.toast('No playable source for this channel'); App.back(); return; }
331
330
  if (playable.length === 1) {
331
+ _d.exitToBack = true; // exiting playback returns straight to the grid
332
332
  startPlayback(playable[0], id, _d.streamLabel || _d.meta.name || 'Live');
333
333
  } else {
334
334
  drawDetail(); // multiple sources → straight to the source list
@@ -345,19 +345,26 @@ var Views = (function () {
345
345
  Stremio.getMeta(type, id).then(function (meta) {
346
346
  _d.meta = meta;
347
347
  U.spinner(false);
348
- drawDetail();
348
+ afterMeta();
349
349
  }, function (err) {
350
350
  U.spinner(false);
351
351
  // Fallback: stream-only addons (e.g. live TV/sports) may expose no meta
352
352
  // resource. Use the catalog preview so the title still opens & plays.
353
353
  if (_d.preview) {
354
354
  _d.meta = Object.assign({ id: id, type: type }, _d.preview);
355
- drawDetail();
355
+ afterMeta();
356
356
  } else {
357
357
  U.toast('Could not load details');
358
358
  App.back();
359
359
  }
360
360
  });
361
+
362
+ // On a movie landing page, skip the "Play" button and load sources automatically
363
+ // (series still shows the episode browser; each episode loads its own sources).
364
+ function afterMeta() {
365
+ if (_d.type === 'movie') { openStreams(_d.id, _d.meta.name || _d.meta.title); }
366
+ else { drawDetail(); }
367
+ }
361
368
  }
362
369
 
363
370
  function drawDetail() {
@@ -616,8 +623,14 @@ var Views = (function () {
616
623
 
617
624
  function startPlayback(stream, videoId, label) {
618
625
  markHomeDirty(); // progress/watched will change → refresh Continue Watching on return
626
+ // Exit handler: single-source live channels return straight to the grid (skipping
627
+ // the source/info pages we skipped on the way in); everything else redraws detail.
628
+ var exitToBack = !!(_d && _d.exitToBack);
629
+ // Single-source live: skip the (already-skipped) source/info pages and go
630
+ // straight back to the home grid, on the tab we came from.
631
+ var onExit = function () { if (exitToBack) { App.goHome(); } else { drawDetail(); } };
619
632
  // YouTube-live IPTV channels play through the leanback proxy overlay, not AVPlay.
620
- if (stream._yt) { YTPlay.open(stream, function () { drawDetail(); }); return; }
633
+ if (stream._yt) { YTPlay.open(stream, onExit); return; }
621
634
  var ctx = { url: stream.url, type: _d.type, id: videoId, name: label,
622
635
  poster: _d.meta.poster, streams: playableSources(), _headers: stream._headers };
623
636
  if (_d.type === 'series' && _d.meta.videos) {
@@ -630,7 +643,7 @@ var Views = (function () {
630
643
  ctx.playNext = function () { playEpisode(ord[idx + 1]); };
631
644
  }
632
645
  }
633
- Player.play(ctx, function () { drawDetail(); });
646
+ Player.play(ctx, onExit);
634
647
  }
635
648
 
636
649
  // Load a specific episode's sources and auto-play the first playable one.
@@ -667,7 +680,7 @@ var Views = (function () {
667
680
  function renderSearch() {
668
681
  var s = scr('search');
669
682
  U.clear(s);
670
- s.appendChild(header(false, false));
683
+ s.appendChild(buildTopMenu('search')); // shared top menu (UP reaches it; no Back button)
671
684
 
672
685
  var wrap = U.el('div', 'search-wrap');
673
686
  var bar = U.el('div', 'search-bar');
@@ -686,10 +699,6 @@ var Views = (function () {
686
699
  var go = U.el('div', 'pill focusable search-go', 'Search');
687
700
  go.__onselect = function () { runSearch(input.value); };
688
701
  bar.appendChild(go);
689
-
690
- var back = U.el('div', 'pill focusable search-go', '← Back');
691
- back.__onselect = function () { App.back(); };
692
- bar.appendChild(back);
693
702
  wrap.appendChild(bar);
694
703
 
695
704
  var results = U.el('div', 'search-results');
@@ -844,6 +853,12 @@ var Views = (function () {
844
853
 
845
854
  function settingsRoot() { _settingsSection = 'menu'; renderSettings(); }
846
855
 
856
+ // Focus the first item in the settings content (not the top menu) so UP reaches the menu.
857
+ function focusSettings() {
858
+ var s = scr('settings');
859
+ Focus.setScope(s, s.querySelector('.settings-wrap .focusable') || undefined);
860
+ }
861
+
847
862
  // Return true if BACK stepped a settings section back to the menu.
848
863
  function backInSettings() {
849
864
  if (_settingsSection !== 'menu') { _settingsSection = 'menu'; renderSettings(); return true; }
@@ -853,7 +868,7 @@ var Views = (function () {
853
868
  function renderSettings() {
854
869
  var s = scr('settings');
855
870
  U.clear(s);
856
- s.appendChild(header(false, false));
871
+ s.appendChild(buildTopMenu('settings')); // shared top menu (UP reaches it; no Back button)
857
872
  var wrap = U.el('div', 'settings-wrap');
858
873
  s.appendChild(wrap);
859
874
  if (_settingsSection === 'addons') { settingsAddons(wrap); }
@@ -882,13 +897,12 @@ var Views = (function () {
882
897
  function settingsMenu(wrap) {
883
898
  wrap.appendChild(U.el('h2', null, 'Settings'));
884
899
  wrap.appendChild(sectionRow('Addons', 'Catalog, stream & subtitle addons', 'addons'));
885
- wrap.appendChild(sectionRow('Live TV (IPTV)', 'Load channels from an M3U playlist URL', 'iptv'));
886
900
  wrap.appendChild(sectionRow('Integrations', 'API keys (OMDb, TMDB, MDBList)', 'integrations'));
901
+ wrap.appendChild(sectionRow('Live TV (IPTV)', 'Load channels from an M3U playlist URL', 'iptv'));
887
902
  wrap.appendChild(sectionRow('Subtitle Options', 'Default language, size, style', 'subtitles'));
888
903
  wrap.appendChild(sectionRow('Home Feeds', 'Order & show/hide the home rows', 'feeds'));
889
- wrap.appendChild(sectionRow('Display', 'Poster orientation', 'display'));
890
- wrap.appendChild(backRow());
891
- Focus.setScope(scr('settings'));
904
+ wrap.appendChild(sectionRow('Posters', 'Poster orientation', 'display'));
905
+ focusSettings();
892
906
  }
893
907
 
894
908
  function settingsAddons(wrap) {
@@ -939,10 +953,38 @@ var Views = (function () {
939
953
  };
940
954
  wrap.appendChild(addRow);
941
955
 
956
+ // Add several addons at once from a plain-text list URL — one addon/manifest
957
+ // URL per line. Handy for long URLs that are painful to type on the TV.
958
+ var listRow = U.el('div', 'addon-item focusable add-row');
959
+ listRow.appendChild(U.el('div', 'a-name', '+ Add from list URL'));
960
+ listRow.appendChild(U.el('div', 'a-url', 'Fetch a .txt of addon URLs (one per line) and add them all'));
961
+ listRow.__onselect = function () {
962
+ App.openEntry({
963
+ title: 'Add addons from a list URL',
964
+ hint: 'Enter the URL of a text file with one addon/manifest URL per line (lines starting with # are ignored), then Confirm.',
965
+ value: 'https://', placeholder: 'https://example.com/addons.txt',
966
+ validate: function (v) { return (!v || v === 'https://' || v.indexOf('://') === -1 || v.length < 12) ? 'Enter a full list URL' : null; },
967
+ onConfirm: function (v) {
968
+ U.toast('Fetching addon list…', 5000);
969
+ U.getText(v, 15000).then(function (text) {
970
+ var added = 0;
971
+ text.split(/\r?\n/).forEach(function (line) {
972
+ var u = line.trim();
973
+ if (!u || u.charAt(0) === '#' || u.indexOf('://') === -1) { return; }
974
+ if (Stremio.addAddon(u)) { added++; }
975
+ });
976
+ clearCatalogCache();
977
+ U.toast(added ? ('Added ' + added + ' addon(s)') : 'No addon URLs found in that file', 3500);
978
+ App.back(); renderSettings();
979
+ }, function () { U.toast('Could not fetch that list URL (check the URL/CORS).', 4000); });
980
+ }
981
+ });
982
+ };
983
+ wrap.appendChild(listRow);
984
+
942
985
  wrap.appendChild(U.el('div', 'hint',
943
986
  'Nuvio hosts no content — add Stremio addon manifest URLs for catalogs, streams and subtitles.'));
944
- wrap.appendChild(backRow());
945
- Focus.setScope(scr('settings'));
987
+ focusSettings();
946
988
  });
947
989
  }
948
990
 
@@ -1005,8 +1047,7 @@ var Views = (function () {
1005
1047
  wrap.appendChild(U.el('div', 'hint',
1006
1048
  'Channels are grouped by the playlist\'s group-title into Live TV rows on Home. ' +
1007
1049
  'Direct HLS streams play via AVPlay; YouTube channels play via the built-in leanback proxy.'));
1008
- wrap.appendChild(backRow());
1009
- Focus.setScope(scr('settings'));
1050
+ focusSettings();
1010
1051
  }
1011
1052
 
1012
1053
  function maskKey(v) { v = '' + v; return v.length <= 4 ? '••••' : ('••••' + v.slice(-4)); }
@@ -1033,8 +1074,7 @@ var Views = (function () {
1033
1074
  wrap.appendChild(keyRow('TMDB API key (v3)', 'tmdb', 'TMDB v3 API key', Stremio.tmdbValidate));
1034
1075
  wrap.appendChild(keyRow('MDBList API key', 'mdblist', 'MDBList API key', Stremio.mdblistValidate));
1035
1076
  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.'));
1036
- wrap.appendChild(backRow());
1037
- Focus.setScope(scr('settings'));
1077
+ focusSettings();
1038
1078
  }
1039
1079
 
1040
1080
  function settingsSubtitles(wrap) {
@@ -1042,13 +1082,12 @@ var Views = (function () {
1042
1082
  wrap.appendChild(subPickRow('Default language', SUB_LANGS, 'lang'));
1043
1083
  wrap.appendChild(subCycleRow('Size', SUB_SIZES, 'size'));
1044
1084
  wrap.appendChild(subCycleRow('Style', SUB_STYLES, 'style'));
1045
- wrap.appendChild(backRow());
1046
- Focus.setScope(scr('settings'));
1085
+ focusSettings();
1047
1086
  }
1048
1087
 
1049
1088
  var POSTER_SHAPES = [['portrait', 'Portrait'], ['landscape', 'Landscape']];
1050
1089
  function settingsDisplay(wrap) {
1051
- wrap.appendChild(U.el('h2', null, 'Display'));
1090
+ wrap.appendChild(U.el('h2', null, 'Posters'));
1052
1091
  var row = U.el('div', 'addon-item focusable cfg-row');
1053
1092
  var name = U.el('div', 'a-name');
1054
1093
  function render() { name.textContent = 'Poster orientation: ' + labelFor(POSTER_SHAPES, Stremio.getPrefs().posterShape); }
@@ -1062,8 +1101,7 @@ var Views = (function () {
1062
1101
  };
1063
1102
  wrap.appendChild(row);
1064
1103
  wrap.appendChild(U.el('div', 'hint', 'Landscape uses wide artwork when available (falls back to the poster).'));
1065
- wrap.appendChild(backRow());
1066
- Focus.setScope(scr('settings'));
1104
+ focusSettings();
1067
1105
  }
1068
1106
 
1069
1107
  // ----- Home Feeds ordering -----
@@ -1096,7 +1134,6 @@ var Views = (function () {
1096
1134
  frow.appendChild(name);
1097
1135
  wrap.appendChild(frow);
1098
1136
  });
1099
- wrap.appendChild(backRow());
1100
1137
 
1101
1138
  // restore focus to the control that triggered the last change
1102
1139
  var pref = null;
@@ -1113,7 +1150,7 @@ var Views = (function () {
1113
1150
 
1114
1151
  return {
1115
1152
  renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
1116
- settingsRoot: settingsRoot, backInSettings: backInSettings,
1153
+ settingsRoot: settingsRoot, backInSettings: backInSettings, setHomeTab: setHomeTab,
1117
1154
  renderLive: renderLive,
1118
1155
  renderSearch: renderSearch, renderEntry: renderEntry, backInDetail: backInDetail,
1119
1156
  homeDirty: homeDirty,
package/js/ytplay.js CHANGED
@@ -7,7 +7,7 @@
7
7
  var YTPlay = (function () {
8
8
  'use strict';
9
9
 
10
- var PROXY = 'http://localhost:8099';
10
+ var PROXY = 'http://127.0.0.1:8099'; // 127.0.0.1 (not "localhost") resolves reliably on Tizen
11
11
  var _open = false;
12
12
  var _onExit = null;
13
13
  var _idCache = {}; // ytPage -> { id, ts }
@@ -24,25 +24,26 @@ var YTPlay = (function () {
24
24
  return new Promise(function (resolve, reject) {
25
25
  function poll(left) {
26
26
  U.getText(PROXY + '/health', 2500).then(function () { resolve(); }, function () {
27
- if (left <= 0) { reject(new Error('proxy-timeout')); return; }
27
+ if (left <= 0) { reject(new Error('proxy-down')); return; }
28
28
  setTimeout(function () { poll(left - 1); }, 500);
29
29
  });
30
30
  }
31
31
  // Already running? (warm TV service, or a locally-run proxy in dev)
32
32
  U.getText(PROXY + '/health', 1500).then(function () { resolve(); }, function () {
33
- if (window.tizen && tizen.application) {
34
- try {
33
+ // Best-effort launch of the bundled <tizen:service>, then keep polling.
34
+ var launched = false;
35
+ try {
36
+ if (window.tizen && tizen.application && tizen.application.launchAppControl) {
35
37
  var pkgId = tizen.application.getCurrentApplication().appInfo.packageId;
36
38
  tizen.application.launchAppControl(
37
39
  new tizen.ApplicationControl('http://tizen.org/appcontrol/operation/service'),
38
40
  pkgId + '.NuvioProxy',
39
- function () { poll(40); }, // launched → wait for it to listen (~20s)
40
- function () { poll(40); } // may already be running
41
+ function () {}, function () {}
41
42
  );
42
- } catch (e) { poll(40); }
43
- } else {
44
- reject(new Error('no-service'));
45
- }
43
+ launched = true;
44
+ }
45
+ } catch (e) { /* fall through to polling */ }
46
+ poll(launched ? 40 : 10); // ~20s if we launched it; a short probe otherwise
46
47
  });
47
48
  });
48
49
  }
@@ -116,10 +117,8 @@ var YTPlay = (function () {
116
117
  show(id);
117
118
  }, function (err) {
118
119
  U.spinner(false);
119
- var msg = (err && err.message === 'no-service')
120
- ? 'YouTube needs the leanback proxy. For desktop dev, run: node service/index.js'
121
- : 'Could not reach the YouTube proxy service (port 8099).';
122
- U.toast(msg, 5000);
120
+ U.toast('Couldn\'t reach the YouTube proxy (port 8099). Use the sideloaded Nuvio ' +
121
+ 'build so its background service is installed. (Desktop dev: run node service/index.js.)', 5500);
123
122
  });
124
123
  }
125
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
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",