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 +1 -1
- package/css/style.css +1 -0
- package/index.html +11 -11
- package/js/app.js +10 -2
- package/js/player.js +6 -4
- package/js/views.js +164 -127
- package/js/ytplay.js +13 -14
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
54
|
-
<script src="js/util.js?v=1.9.
|
|
55
|
-
<script src="js/iptv.js?v=1.9.
|
|
56
|
-
<script src="js/stremio.js?v=1.9.
|
|
57
|
-
<script src="js/keys.js?v=1.9.
|
|
58
|
-
<script src="js/focus.js?v=1.9.
|
|
59
|
-
<script src="js/player.js?v=1.9.
|
|
60
|
-
<script src="js/ytplay.js?v=1.9.
|
|
61
|
-
<script src="js/views.js?v=1.9.
|
|
62
|
-
<script src="js/app.js?v=1.9.
|
|
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,
|
|
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
|
-
|
|
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: '
|
|
616
|
-
if (_ctx.hasNext) { b.push({ label: '
|
|
617
|
-
b.push({ label: '
|
|
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
|
-
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
111
|
-
function
|
|
112
|
-
var
|
|
113
|
-
|
|
114
|
-
var
|
|
115
|
-
|
|
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: '
|
|
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
|
-
//
|
|
215
|
-
var
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (!
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
290
|
+
U.toast('Failed to load addons. Check network / Settings.');
|
|
291
|
+
_homeCatalogs = [];
|
|
288
292
|
});
|
|
289
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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('
|
|
890
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '
|
|
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
|
-
|
|
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://
|
|
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-
|
|
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
|
-
|
|
34
|
-
|
|
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 () {
|
|
40
|
-
function () { poll(40); } // may already be running
|
|
41
|
+
function () {}, function () {}
|
|
41
42
|
);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|