nuvio-tizen 1.2.0 → 1.4.0
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/README.md +16 -3
- package/css/style.css +11 -0
- package/index.html +1 -0
- package/js/app.js +17 -5
- package/js/player.js +161 -42
- package/js/stremio.js +44 -2
- package/js/views.js +160 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,15 +15,28 @@ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
|
|
|
15
15
|
**Cinemeta** (catalogs + metadata), **WatchHub** (streams), **OpenSubtitles v3** (subtitles).
|
|
16
16
|
- Home screen with catalog rows + **Continue Watching**.
|
|
17
17
|
- **Search** (🔍 in the header) across every addon catalog that supports the `search` extra.
|
|
18
|
-
- Detail pages: movies (Play) and series (season/episode browser).
|
|
19
|
-
|
|
18
|
+
- Detail pages: movies (Play) and series (season/episode browser). **Continue Watching**
|
|
19
|
+
resumes a series on the exact season + episode you left off.
|
|
20
|
+
- **Stream aggregation** across all installed stream addons, each source showing
|
|
21
|
+
**size + languages/description** (from `behaviorHints.videoSize` and the addon's own text,
|
|
22
|
+
e.g. MediaFusion / Torrentio).
|
|
23
|
+
- **In-player menu** (press **UP**): *Select source* (switch stream mid-playback, keeping
|
|
24
|
+
your position) and *Configure subtitles* → Language / Size / Style.
|
|
20
25
|
- **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
|
|
21
26
|
timed overlay; press **UP** during playback to pick a track (or turn them Off).
|
|
27
|
+
Configurable in Settings: **default language** (auto-loads a matching track),
|
|
28
|
+
**size** (S/M/L/XL) and **style** (white+shadow / black box / yellow).
|
|
29
|
+
- **Live streams** (sports / IPTV): live HLS and Stremio `tv`-type sources are detected
|
|
30
|
+
(unknown/infinite duration), shown with a **● LIVE** badge, and correctly skip
|
|
31
|
+
seek / resume / progress-tracking. Stream-only live addons with no `meta` resource still
|
|
32
|
+
open (the catalog entry is used as a fallback).
|
|
22
33
|
- Playback via **AVPlay** (HLS/DASH/MP4/progressive) with an HTML5 `<video>` fallback
|
|
23
34
|
when run in a desktop browser for development.
|
|
24
35
|
- OSD with seek (±10s / ±30s), resume, and per-title progress saved to `localStorage`.
|
|
25
36
|
- Full **D-pad + media-key** remote navigation (custom spatial focus engine).
|
|
26
|
-
- Settings
|
|
37
|
+
- Settings: add addons on a **dedicated page** (field kept above the on-screen keyboard;
|
|
38
|
+
nothing is saved unless you press Confirm), remove with a confirmation prompt, and
|
|
39
|
+
**Reset to default addons**.
|
|
27
40
|
|
|
28
41
|
> **Note on WatchHub:** it's a "where to watch" addon — its results are Rent/Buy/Subscription
|
|
29
42
|
> *links to external services*, not direct video URLs, so they show as non-playable. Add your
|
package/css/style.css
CHANGED
|
@@ -89,6 +89,16 @@ html, body {
|
|
|
89
89
|
.addon-item .a-url { font-size: 15px; color: #8b93a5; }
|
|
90
90
|
.hint { color: #8b93a5; font-size: 18px; margin-top: 20px; line-height: 1.5; }
|
|
91
91
|
|
|
92
|
+
/* --- Add-addon page (field kept high, above the on-screen keyboard) --- */
|
|
93
|
+
.add-wrap { position: absolute; top: 120px; left: 60px; right: 60px; }
|
|
94
|
+
.add-wrap h2 { font-size: 36px; margin-bottom: 14px; }
|
|
95
|
+
.add-hint { font-size: 22px; color: #8b93a5; margin-bottom: 28px; line-height: 1.4; max-width: 1200px; }
|
|
96
|
+
.add-input { width: 100%; padding: 20px 24px; font-size: 32px; background: #0e1117; color: #fff; border: 4px solid #2a3346; border-radius: 12px; }
|
|
97
|
+
.add-input.focused { border-color: #818cf8; box-shadow: 0 0 0 2px rgba(129,140,248,0.5); }
|
|
98
|
+
.add-btn-row { margin-top: 34px; }
|
|
99
|
+
.add-btn-row .pill { min-width: 220px; text-align: center; }
|
|
100
|
+
.cfg-row .a-url { color: #6f7788; }
|
|
101
|
+
|
|
92
102
|
/* --- Player OSD --- */
|
|
93
103
|
#player-osd { position: absolute; left: 0; right: 0; bottom: 0; height: 220px; z-index: 20; padding: 40px 60px; background: linear-gradient(transparent, rgba(0,0,0,0.9)); }
|
|
94
104
|
#osd-title { font-size: 30px; font-weight: bold; margin-bottom: 20px; }
|
|
@@ -96,6 +106,7 @@ html, body {
|
|
|
96
106
|
#osd-buffer { position: absolute; top: 0; left: 0; height: 8px; background: rgba(255,255,255,0.35); border-radius: 4px; width: 0; }
|
|
97
107
|
#osd-progress { position: absolute; top: 0; left: 0; height: 8px; background: #818cf8; border-radius: 4px; width: 0; z-index: 2; }
|
|
98
108
|
#osd-times { display: flex; justify-content: space-between; margin-top: 10px; font-size: 20px; color: #cfd3dc; }
|
|
109
|
+
#osd-cur.live { color: #ff4d4d; font-weight: bold; letter-spacing: 1px; }
|
|
99
110
|
#osd-hint { margin-top: 14px; font-size: 18px; color: #8b93a5; }
|
|
100
111
|
|
|
101
112
|
/* --- Subtitles --- */
|
package/index.html
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<div id="screen-detail" class="screen hidden"></div>
|
|
18
18
|
<div id="screen-search" class="screen hidden"></div>
|
|
19
19
|
<div id="screen-settings" class="screen hidden"></div>
|
|
20
|
+
<div id="screen-addaddon" class="screen hidden"></div>
|
|
20
21
|
</div>
|
|
21
22
|
|
|
22
23
|
<!-- Subtitle overlay (over the video plane, under the OSD) -->
|
package/js/app.js
CHANGED
|
@@ -8,7 +8,7 @@ var App = (function () {
|
|
|
8
8
|
function screenEl(name) { return U.byId('screen-' + name); }
|
|
9
9
|
|
|
10
10
|
function showScreen(name) {
|
|
11
|
-
['home', 'detail', 'search', 'settings'].forEach(function (n) {
|
|
11
|
+
['home', 'detail', 'search', 'settings', 'addaddon'].forEach(function (n) {
|
|
12
12
|
screenEl(n).className = 'screen' + (n === name ? '' : ' hidden');
|
|
13
13
|
});
|
|
14
14
|
_current = name;
|
|
@@ -16,11 +16,11 @@ var App = (function () {
|
|
|
16
16
|
|
|
17
17
|
function pushFrame() { _stack.push({ screen: _current, focus: Focus.current() }); }
|
|
18
18
|
|
|
19
|
-
function openDetail(type, id) {
|
|
19
|
+
function openDetail(type, id, preview, target) {
|
|
20
20
|
if (!id) { return; }
|
|
21
21
|
pushFrame();
|
|
22
22
|
showScreen('detail');
|
|
23
|
-
Views.renderDetail(type, id);
|
|
23
|
+
Views.renderDetail(type, id, preview, target);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function openSettings() {
|
|
@@ -35,6 +35,12 @@ var App = (function () {
|
|
|
35
35
|
Views.renderSearch();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function openAddAddon() {
|
|
39
|
+
pushFrame();
|
|
40
|
+
showScreen('addaddon');
|
|
41
|
+
Views.renderAddAddon();
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
function back() {
|
|
39
45
|
if (Player.isActive()) { Player.exit(); return; }
|
|
40
46
|
if (Views.isModalOpen()) { Views.closeModal(); return; }
|
|
@@ -61,7 +67,12 @@ var App = (function () {
|
|
|
61
67
|
case 'LEFT': Focus.move('LEFT'); return true;
|
|
62
68
|
case 'RIGHT': Focus.move('RIGHT'); return true;
|
|
63
69
|
case 'UP': Focus.move('UP'); return true;
|
|
64
|
-
case 'DOWN':
|
|
70
|
+
case 'DOWN': {
|
|
71
|
+
var c = Focus.current();
|
|
72
|
+
if (c && c.__navDOWN && c.__navDOWN() !== false) { return true; }
|
|
73
|
+
Focus.move('DOWN');
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
65
76
|
case 'OK': Focus.clickCurrent(); return true;
|
|
66
77
|
case 'BACK': back(); return true;
|
|
67
78
|
default: return true;
|
|
@@ -78,7 +89,8 @@ var App = (function () {
|
|
|
78
89
|
|
|
79
90
|
return {
|
|
80
91
|
start: start, back: back,
|
|
81
|
-
openDetail: openDetail, openSettings: openSettings, openSearch: openSearch
|
|
92
|
+
openDetail: openDetail, openSettings: openSettings, openSearch: openSearch,
|
|
93
|
+
openAddAddon: openAddAddon
|
|
82
94
|
};
|
|
83
95
|
})();
|
|
84
96
|
|
package/js/player.js
CHANGED
|
@@ -4,6 +4,26 @@ var Player = (function () {
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
var PROGRESS_KEY = 'nuvio.progress';
|
|
7
|
+
var SUBCFG_KEY = 'nuvio.subcfg';
|
|
8
|
+
var SIZE_PX = { small: 28, medium: 36, large: 46, xlarge: 58 };
|
|
9
|
+
|
|
10
|
+
function getSubCfg() {
|
|
11
|
+
var d = U.store.get(SUBCFG_KEY, {}) || {};
|
|
12
|
+
return { lang: d.lang || 'off', size: d.size || 'medium', style: d.style || 'shadow' };
|
|
13
|
+
}
|
|
14
|
+
function setSubCfg(cfg) { U.store.set(SUBCFG_KEY, cfg); }
|
|
15
|
+
|
|
16
|
+
function styleSpan(span) {
|
|
17
|
+
var cfg = getSubCfg();
|
|
18
|
+
span.style.fontSize = (SIZE_PX[cfg.size] || 36) + 'px';
|
|
19
|
+
if (cfg.style === 'box') {
|
|
20
|
+
span.style.background = 'rgba(0,0,0,0.9)'; span.style.color = '#fff'; span.style.textShadow = 'none';
|
|
21
|
+
} else if (cfg.style === 'yellow') {
|
|
22
|
+
span.style.background = 'rgba(0,0,0,0.4)'; span.style.color = '#ffe14d'; span.style.textShadow = '0 2px 3px #000';
|
|
23
|
+
} else { // shadow (default)
|
|
24
|
+
span.style.background = 'rgba(0,0,0,0.55)'; span.style.color = '#fff'; span.style.textShadow = '0 2px 3px #000';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
var avplay = (window.webapis && webapis.avplay) ? webapis.avplay : null;
|
|
9
29
|
var useAV = !!avplay;
|
|
@@ -18,6 +38,7 @@ var Player = (function () {
|
|
|
18
38
|
var _ctx = null; // { id, type, name, poster, resumeSec }
|
|
19
39
|
var _dur = 0; // seconds
|
|
20
40
|
var _cur = 0; // seconds
|
|
41
|
+
var _isLive = false; // live stream (sports/IPTV): no fixed duration
|
|
21
42
|
var _ticker = null;
|
|
22
43
|
var _osdTimer = null;
|
|
23
44
|
var _seekPending = 0;
|
|
@@ -44,7 +65,7 @@ var Player = (function () {
|
|
|
44
65
|
function progressKey(ctx) { return ctx.type + ':' + ctx.id; }
|
|
45
66
|
|
|
46
67
|
function saveProgress() {
|
|
47
|
-
if (!_ctx || !_dur) { return; }
|
|
68
|
+
if (!_ctx || !_dur || _isLive) { return; }
|
|
48
69
|
var all = progressAll();
|
|
49
70
|
var k = progressKey(_ctx);
|
|
50
71
|
if (_cur / _dur > 0.95 || _cur < 5) {
|
|
@@ -79,6 +100,16 @@ var Player = (function () {
|
|
|
79
100
|
|
|
80
101
|
function renderOSD() {
|
|
81
102
|
U.byId('osd-title').textContent = _ctx ? _ctx.name : '';
|
|
103
|
+
if (_isLive) {
|
|
104
|
+
U.byId('osd-bar').style.display = 'none';
|
|
105
|
+
var cur = U.byId('osd-cur');
|
|
106
|
+
cur.textContent = '● LIVE';
|
|
107
|
+
cur.className = 'live';
|
|
108
|
+
U.byId('osd-dur').textContent = '';
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
U.byId('osd-bar').style.display = '';
|
|
112
|
+
U.byId('osd-cur').className = '';
|
|
82
113
|
var pct = _dur > 0 ? (_cur / _dur * 100) : 0;
|
|
83
114
|
U.byId('osd-progress').style.width = pct + '%';
|
|
84
115
|
U.byId('osd-cur').textContent = U.fmtTime(_cur);
|
|
@@ -89,6 +120,8 @@ var Player = (function () {
|
|
|
89
120
|
function play(ctx, onExit) {
|
|
90
121
|
_ctx = ctx; _onExit = onExit;
|
|
91
122
|
_active = true; _cur = 0; _dur = 0;
|
|
123
|
+
// Stremio "tv" type is a strong live hint; refined from duration after prepare.
|
|
124
|
+
_isLive = (ctx.type === 'tv') || !!ctx.isLive;
|
|
92
125
|
_ctx.resumeSec = ctx.resumeSec || getResume(ctx.type, ctx.id);
|
|
93
126
|
|
|
94
127
|
// reset subtitle state
|
|
@@ -110,17 +143,33 @@ var Player = (function () {
|
|
|
110
143
|
if (!_active) { return; }
|
|
111
144
|
_subTracks = tracks || [];
|
|
112
145
|
updateHint();
|
|
113
|
-
if (_subTracks.length) {
|
|
146
|
+
if (!_subTracks.length) { return; }
|
|
147
|
+
var cfg = getSubCfg();
|
|
148
|
+
var match = null;
|
|
149
|
+
if (cfg.lang && cfg.lang !== 'off') {
|
|
150
|
+
match = _subTracks.find(function (t) {
|
|
151
|
+
var l = ('' + (t.lang || '')).toLowerCase();
|
|
152
|
+
return l && (l.indexOf(cfg.lang) === 0 || cfg.lang.indexOf(l) === 0);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (match) { loadTrack(match); }
|
|
156
|
+
else { U.toast(_subTracks.length + ' subtitle track(s) — press UP', 3000); }
|
|
114
157
|
}, function () {});
|
|
115
158
|
}
|
|
116
159
|
|
|
117
160
|
function updateHint() {
|
|
118
|
-
var base = 'OK: Play/Pause ←/→: Seek RETURN: Back';
|
|
119
|
-
|
|
161
|
+
var base = _isLive ? 'OK: Play/Pause RETURN: Back' : 'OK: Play/Pause ←/→: Seek RETURN: Back';
|
|
162
|
+
base += ' UP: Menu';
|
|
120
163
|
var h = U.byId('osd-hint');
|
|
121
164
|
if (h) { h.textContent = base; }
|
|
122
165
|
}
|
|
123
166
|
|
|
167
|
+
// Decides live vs VOD from the reported duration (0 / non-finite = live).
|
|
168
|
+
function markLiveFromDuration() {
|
|
169
|
+
if (_ctx && _ctx.type === 'tv') { _isLive = true; }
|
|
170
|
+
if (!_dur || _dur <= 0 || !isFinite(_dur)) { _isLive = true; }
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
function startAV(url) {
|
|
125
174
|
objEl.className = 'active';
|
|
126
175
|
try {
|
|
@@ -141,7 +190,9 @@ var Player = (function () {
|
|
|
141
190
|
try { avplay.setDisplayRect(0, 0, 1920, 1080); } catch (e1) {}
|
|
142
191
|
try { avplay.setDisplayMethod('PLAYER_DISPLAY_MODE_LETTER_BOX'); } catch (e2) {}
|
|
143
192
|
try { _dur = avplay.getDuration() / 1000; } catch (e3) { _dur = 0; }
|
|
144
|
-
|
|
193
|
+
markLiveFromDuration();
|
|
194
|
+
updateHint();
|
|
195
|
+
if (!_isLive && _ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
|
|
145
196
|
avplay.play();
|
|
146
197
|
renderOSD();
|
|
147
198
|
startTicker();
|
|
@@ -159,7 +210,9 @@ var Player = (function () {
|
|
|
159
210
|
videoEl.src = url;
|
|
160
211
|
videoEl.onloadedmetadata = function () {
|
|
161
212
|
_dur = videoEl.duration || 0;
|
|
162
|
-
|
|
213
|
+
markLiveFromDuration();
|
|
214
|
+
updateHint();
|
|
215
|
+
if (!_isLive && _ctx.resumeSec > 5) { try { videoEl.currentTime = _ctx.resumeSec; } catch (e) {} }
|
|
163
216
|
U.spinner(false); showOSD(false);
|
|
164
217
|
};
|
|
165
218
|
videoEl.ontimeupdate = function () { _cur = videoEl.currentTime; renderOSD(); renderSubs(); };
|
|
@@ -197,6 +250,7 @@ var Player = (function () {
|
|
|
197
250
|
}
|
|
198
251
|
|
|
199
252
|
function seekBy(delta) {
|
|
253
|
+
if (_isLive) { showOSD(false); U.toast('Live — seeking unavailable', 1500); return; }
|
|
200
254
|
var target = Math.max(0, Math.min((_dur || 1e9), _cur + delta));
|
|
201
255
|
_cur = target;
|
|
202
256
|
renderOSD();
|
|
@@ -243,7 +297,7 @@ var Player = (function () {
|
|
|
243
297
|
if (show) {
|
|
244
298
|
subsEl.className = '';
|
|
245
299
|
var span = subsEl.firstChild;
|
|
246
|
-
if (!span) { span = document.createElement('span'); subsEl.appendChild(span); }
|
|
300
|
+
if (!span) { span = document.createElement('span'); subsEl.appendChild(span); styleSpan(span); }
|
|
247
301
|
if (span.textContent !== show) { span.textContent = show; }
|
|
248
302
|
} else {
|
|
249
303
|
subsEl.className = 'hidden';
|
|
@@ -266,55 +320,119 @@ var Player = (function () {
|
|
|
266
320
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
267
321
|
}
|
|
268
322
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
323
|
+
function applyStyleNow() {
|
|
324
|
+
var span = subsEl.firstChild;
|
|
325
|
+
if (span) { styleSpan(span); }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Switch to a different source mid-playback, keeping position (VOD).
|
|
329
|
+
function switchSource(s) {
|
|
330
|
+
if (!s || !s.url) { return; }
|
|
331
|
+
_ctx.url = s.url;
|
|
332
|
+
_ctx.resumeSec = _isLive ? 0 : _cur;
|
|
333
|
+
U.toast('Switching source…', 1500);
|
|
334
|
+
U.spinner(true);
|
|
335
|
+
if (useAV) { startAV(s.url); } else { startHTML5(s.url); }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------- in-player menu (stack of levels) ----------
|
|
339
|
+
var _menuStack = [];
|
|
340
|
+
|
|
341
|
+
function topMenu() { return _menuStack[_menuStack.length - 1]; }
|
|
342
|
+
|
|
343
|
+
function openMenu() {
|
|
272
344
|
_menuOpen = true;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
renderSubMenu();
|
|
345
|
+
_menuStack = [rootMenu()];
|
|
346
|
+
renderMenu();
|
|
276
347
|
subMenuEl.className = '';
|
|
277
348
|
showOSD(true);
|
|
278
349
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
_menuOpen = false;
|
|
350
|
+
function closeMenu() {
|
|
351
|
+
_menuOpen = false; _menuStack = [];
|
|
282
352
|
subMenuEl.className = 'hidden';
|
|
283
353
|
showOSD(false);
|
|
284
354
|
}
|
|
355
|
+
function pushMenu(desc) { _menuStack.push(desc); renderMenu(); }
|
|
356
|
+
function popMenu() { _menuStack.pop(); if (!_menuStack.length) { closeMenu(); } else { renderMenu(); } }
|
|
357
|
+
function replaceTop(desc) { _menuStack[_menuStack.length - 1] = desc; renderMenu(); }
|
|
285
358
|
|
|
286
|
-
function
|
|
359
|
+
function renderMenu() {
|
|
360
|
+
var m = topMenu();
|
|
287
361
|
U.clear(subMenuEl);
|
|
288
|
-
subMenuEl.appendChild(U.el('div', 'sm-title',
|
|
362
|
+
subMenuEl.appendChild(U.el('div', 'sm-title', m.title));
|
|
289
363
|
var list = U.el('div', 'sm-list');
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
items.push({ label: (tr.lang || 'unknown') + ' · ' + (tr._addon || '') + mark, track: tr });
|
|
294
|
-
});
|
|
295
|
-
_menuItems = items;
|
|
296
|
-
items.forEach(function (it, i) {
|
|
297
|
-
var node = U.el('div', 'sm-item' + (i === _menuIdx ? ' sel' : ''), it.label);
|
|
298
|
-
list.appendChild(node);
|
|
364
|
+
m.items.forEach(function (it, i) {
|
|
365
|
+
var label = it.label + (it.sel ? ' ✓' : '');
|
|
366
|
+
list.appendChild(U.el('div', 'sm-item' + (i === m.idx ? ' sel' : ''), label));
|
|
299
367
|
});
|
|
300
368
|
subMenuEl.appendChild(list);
|
|
301
|
-
|
|
302
|
-
var sel = list.children[_menuIdx];
|
|
369
|
+
var sel = list.children[m.idx];
|
|
303
370
|
if (sel && sel.scrollIntoView) { try { sel.scrollIntoView(false); } catch (e) {} }
|
|
304
371
|
}
|
|
305
372
|
|
|
306
|
-
|
|
373
|
+
function rootMenu() {
|
|
374
|
+
var items = [];
|
|
375
|
+
if (_ctx.streams && _ctx.streams.length) {
|
|
376
|
+
items.push({ label: 'Select source', run: function () { pushMenu(sourceMenu()); } });
|
|
377
|
+
}
|
|
378
|
+
items.push({ label: 'Configure subtitles', run: function () { pushMenu(subConfigMenu()); } });
|
|
379
|
+
return { title: 'Menu', items: items, idx: 0 };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sourceMenu() {
|
|
383
|
+
var items = _ctx.streams.map(function (s) {
|
|
384
|
+
return { label: s.label || 'Source', sel: (s.url === _ctx.url),
|
|
385
|
+
run: function () { switchSource(s); closeMenu(); } };
|
|
386
|
+
});
|
|
387
|
+
return { title: 'Select source', items: items, idx: 0 };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function subConfigMenu() {
|
|
391
|
+
return { title: 'Configure subtitles', idx: 0, items: [
|
|
392
|
+
{ label: 'Language', run: function () { pushMenu(langMenu()); } },
|
|
393
|
+
{ label: 'Size', run: function () { pushMenu(sizeMenu()); } },
|
|
394
|
+
{ label: 'Style', run: function () { pushMenu(styleMenu()); } }
|
|
395
|
+
] };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function langMenu() {
|
|
399
|
+
var items = [{ label: 'Off', sel: !_subOn, run: function () { subsOff(); closeMenu(); } }];
|
|
400
|
+
_subTracks.forEach(function (tr) {
|
|
401
|
+
items.push({ label: (tr.lang || 'unknown') + ' · ' + (tr._addon || ''),
|
|
402
|
+
sel: (_subOn && _subLabel === (tr.lang || 'on')),
|
|
403
|
+
run: function () { loadTrack(tr); closeMenu(); } });
|
|
404
|
+
});
|
|
405
|
+
return { title: 'Subtitle language', items: items, idx: 0 };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function optionMenu(title, key, opts) {
|
|
409
|
+
var cfg = getSubCfg();
|
|
410
|
+
var idx = 0;
|
|
411
|
+
var items = opts.map(function (p, i) {
|
|
412
|
+
if (cfg[key] === p[0]) { idx = i; }
|
|
413
|
+
return { label: p[1], sel: (cfg[key] === p[0]), run: function () {
|
|
414
|
+
var c = getSubCfg(); c[key] = p[0]; setSubCfg(c); applyStyleNow();
|
|
415
|
+
replaceTop(optionMenu(title, key, opts)); // refresh checkmark
|
|
416
|
+
} };
|
|
417
|
+
});
|
|
418
|
+
return { title: title, items: items, idx: idx };
|
|
419
|
+
}
|
|
420
|
+
function sizeMenu() {
|
|
421
|
+
return optionMenu('Subtitle size', 'size',
|
|
422
|
+
[['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']]);
|
|
423
|
+
}
|
|
424
|
+
function styleMenu() {
|
|
425
|
+
return optionMenu('Subtitle style', 'style',
|
|
426
|
+
[['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']]);
|
|
427
|
+
}
|
|
307
428
|
|
|
308
429
|
function menuKey(action) {
|
|
309
|
-
|
|
310
|
-
if (action === '
|
|
311
|
-
if (action === '
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (action === 'BACK' || action === 'STOP') { closeSubMenu(); return; }
|
|
430
|
+
var m = topMenu();
|
|
431
|
+
if (action === 'UP') { m.idx = Math.max(0, m.idx - 1); renderMenu(); return; }
|
|
432
|
+
if (action === 'DOWN') { m.idx = Math.min(m.items.length - 1, m.idx + 1); renderMenu(); return; }
|
|
433
|
+
if (action === 'OK') { var it = m.items[m.idx]; if (it && it.run) { it.run(); } return; }
|
|
434
|
+
if (action === 'LEFT' || action === 'BACK') { popMenu(); return; }
|
|
435
|
+
if (action === 'STOP') { closeMenu(); return; }
|
|
318
436
|
}
|
|
319
437
|
|
|
320
438
|
function handleKey(action) {
|
|
@@ -326,7 +444,7 @@ var Player = (function () {
|
|
|
326
444
|
case 'PAUSE': if (useAV) { try { avplay.pause(); } catch (e) {} } else { videoEl.pause(); } return true;
|
|
327
445
|
case 'RIGHT': case 'FF': seekBy(action === 'FF' ? 30 : 10); return true;
|
|
328
446
|
case 'LEFT': case 'REW': seekBy(action === 'REW' ? -30 : -10); return true;
|
|
329
|
-
case 'UP':
|
|
447
|
+
case 'UP': openMenu(); return true;
|
|
330
448
|
case 'DOWN': showOSD(false); return true;
|
|
331
449
|
case 'STOP': case 'BACK': exit(); return true;
|
|
332
450
|
}
|
|
@@ -341,7 +459,7 @@ var Player = (function () {
|
|
|
341
459
|
if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
|
|
342
460
|
hideOSD();
|
|
343
461
|
subsEl.className = 'hidden'; U.clear(subsEl);
|
|
344
|
-
subMenuEl.className = 'hidden'; _menuOpen = false;
|
|
462
|
+
subMenuEl.className = 'hidden'; _menuOpen = false; _menuStack = [];
|
|
345
463
|
_subOn = false; _cues = null; _subTracks = [];
|
|
346
464
|
if (useAV) {
|
|
347
465
|
try { avplay.stop(); } catch (e) {}
|
|
@@ -361,6 +479,7 @@ var Player = (function () {
|
|
|
361
479
|
|
|
362
480
|
return {
|
|
363
481
|
init: init, play: play, handleKey: handleKey, isActive: isActive,
|
|
364
|
-
exit: exit, continueList: continueList, getResume: getResume
|
|
482
|
+
exit: exit, continueList: continueList, getResume: getResume,
|
|
483
|
+
getSubCfg: getSubCfg, setSubCfg: setSubCfg
|
|
365
484
|
};
|
|
366
485
|
})();
|
package/js/stremio.js
CHANGED
|
@@ -112,6 +112,12 @@ var Stremio = (function () {
|
|
|
112
112
|
|
|
113
113
|
// ---- Catalogs ----
|
|
114
114
|
// Returns list of {addon, type, id, name} across all addons.
|
|
115
|
+
var TYPE_LABELS = { movie: 'Movies', series: 'Series', tv: 'Live TV', channel: 'Channels' };
|
|
116
|
+
function prettyType(t) {
|
|
117
|
+
if (TYPE_LABELS[t]) { return TYPE_LABELS[t]; }
|
|
118
|
+
return t ? (t.charAt(0).toUpperCase() + t.slice(1)) : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
function listCatalogs() {
|
|
116
122
|
return loadAddons().then(function (addons) {
|
|
117
123
|
var cats = [];
|
|
@@ -120,8 +126,10 @@ var Stremio = (function () {
|
|
|
120
126
|
cs.forEach(function (c) {
|
|
121
127
|
if (!c.type || !c.id) { return; }
|
|
122
128
|
var label = c.name || c.id;
|
|
129
|
+
// Include the type so per-type catalogs sharing a name (e.g. Cinemeta's
|
|
130
|
+
// "Popular" for both movie & series) don't look like duplicates.
|
|
123
131
|
cats.push({ addon: addon, type: c.type, id: c.id,
|
|
124
|
-
name:
|
|
132
|
+
name: prettyType(c.type) + ' · ' + label + ' · ' + (addon.manifest.name || 'Addon') });
|
|
125
133
|
});
|
|
126
134
|
});
|
|
127
135
|
return cats;
|
|
@@ -272,12 +280,46 @@ var Stremio = (function () {
|
|
|
272
280
|
return ('' + t).replace(/\n/g, ' ');
|
|
273
281
|
}
|
|
274
282
|
|
|
283
|
+
// Short first-line label (quality / addon tag), e.g. "MediaFusion 2160p".
|
|
284
|
+
function streamName(stream) {
|
|
285
|
+
var t = stream.name || stream.title || stream.description || 'Source';
|
|
286
|
+
return ('' + t).split('\n')[0].trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function humanSize(bytes) {
|
|
290
|
+
if (!bytes || bytes <= 0) { return ''; }
|
|
291
|
+
var u = ['B', 'KB', 'MB', 'GB', 'TB'], i = 0, n = bytes;
|
|
292
|
+
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
|
293
|
+
return (n >= 10 ? Math.round(n) : n.toFixed(1)) + ' ' + u[i];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// File size from behaviorHints, or parsed from the title/description text.
|
|
297
|
+
function streamSize(stream) {
|
|
298
|
+
var b = stream.behaviorHints && stream.behaviorHints.videoSize;
|
|
299
|
+
if (b) { return humanSize(b); }
|
|
300
|
+
var txt = ('' + (stream.title || '') + ' ' + (stream.description || ''));
|
|
301
|
+
var m = txt.match(/(\d+(?:[.,]\d+)?)\s?(TB|GB|MB)\b/i);
|
|
302
|
+
return m ? (m[1].replace(',', '.') + ' ' + m[2].toUpperCase()) : '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The descriptive block (size, languages, seeds, etc. as the addon formats it).
|
|
306
|
+
// Kept multi-line so details like MediaFusion's language list survive.
|
|
307
|
+
function streamDescription(stream) {
|
|
308
|
+
var d = '' + (stream.description || stream.title || '');
|
|
309
|
+
if (!stream.name) {
|
|
310
|
+
var parts = d.split('\n');
|
|
311
|
+
if (parts.length > 1) { d = parts.slice(1).join('\n'); }
|
|
312
|
+
}
|
|
313
|
+
return d.trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
275
316
|
return {
|
|
276
317
|
DEFAULT_ADDONS: DEFAULT_ADDONS,
|
|
277
318
|
getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
|
|
278
319
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
279
320
|
getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
|
|
280
321
|
getSearchCatalogs: getSearchCatalogs, search: search,
|
|
281
|
-
isPlayable: isPlayable, streamLabel: streamLabel
|
|
322
|
+
isPlayable: isPlayable, streamLabel: streamLabel, streamName: streamName,
|
|
323
|
+
streamSize: streamSize, streamDescription: streamDescription
|
|
282
324
|
};
|
|
283
325
|
})();
|
package/js/views.js
CHANGED
|
@@ -62,16 +62,28 @@ var Views = (function () {
|
|
|
62
62
|
var srch = U.el('div', 'gear focusable', '🔍');
|
|
63
63
|
srch.style.marginRight = '24px';
|
|
64
64
|
srch.__onselect = function () { App.openSearch(); };
|
|
65
|
+
srch.__navDOWN = focusFirstRowItem;
|
|
65
66
|
h.appendChild(srch);
|
|
66
67
|
}
|
|
67
68
|
if (withGear) {
|
|
68
69
|
var g = U.el('div', 'gear focusable', '⚙');
|
|
69
70
|
g.__onselect = function () { App.openSettings(); };
|
|
71
|
+
g.__navDOWN = focusFirstRowItem;
|
|
70
72
|
h.appendChild(g);
|
|
71
73
|
}
|
|
72
74
|
return h;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// DOWN from the header lands on the first item of the first row
|
|
78
|
+
// (Continue Watching when present), not the geometrically-nearest card.
|
|
79
|
+
function focusFirstRowItem() {
|
|
80
|
+
var rows = scr('home').querySelector('.rows');
|
|
81
|
+
if (!rows) { return false; }
|
|
82
|
+
var first = rows.querySelector('.focusable');
|
|
83
|
+
if (first) { Focus.setFocus(first); return true; }
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
function posterCard(meta, fallbackType) {
|
|
76
88
|
var c = U.el('div', 'card focusable');
|
|
77
89
|
var poster = meta.poster || '';
|
|
@@ -84,7 +96,7 @@ var Views = (function () {
|
|
|
84
96
|
c.appendChild(U.el('div', 'fallback', meta.name || meta.title || ''));
|
|
85
97
|
}
|
|
86
98
|
c.appendChild(U.el('div', 'cap', meta.name || meta.title || ''));
|
|
87
|
-
c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id); };
|
|
99
|
+
c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id, meta); };
|
|
88
100
|
return c;
|
|
89
101
|
}
|
|
90
102
|
|
|
@@ -101,6 +113,13 @@ var Views = (function () {
|
|
|
101
113
|
if (cont.length) {
|
|
102
114
|
rows.appendChild(buildRow('Continue Watching', cont.map(function (e) {
|
|
103
115
|
var c = posterCard({ id: e.id.split(':')[0], type: e.type, name: e.name, poster: e.poster }, e.type);
|
|
116
|
+
// For a series, keep the full episode id so detail opens on the right
|
|
117
|
+
// season and focuses the exact episode being resumed.
|
|
118
|
+
c.__onselect = function () {
|
|
119
|
+
App.openDetail(e.type, e.id.split(':')[0],
|
|
120
|
+
{ name: e.name, poster: e.poster, type: e.type },
|
|
121
|
+
e.type === 'series' ? e.id : null);
|
|
122
|
+
};
|
|
104
123
|
if (e.duration) {
|
|
105
124
|
var p = U.el('div', 'prog');
|
|
106
125
|
p.style.width = Math.min(100, (e.position / e.duration * 100)) + '%';
|
|
@@ -162,8 +181,8 @@ var Views = (function () {
|
|
|
162
181
|
// ================= DETAIL =================
|
|
163
182
|
var _d = null; // { type, id, meta, mode, season }
|
|
164
183
|
|
|
165
|
-
function renderDetail(type, id) {
|
|
166
|
-
_d = { type: type, id: id, meta: null, mode: 'info', season: null };
|
|
184
|
+
function renderDetail(type, id, preview, target) {
|
|
185
|
+
_d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null, target: target || null };
|
|
167
186
|
var s = scr('detail');
|
|
168
187
|
U.clear(s);
|
|
169
188
|
s.appendChild(U.el('div', 'detail-hero'));
|
|
@@ -174,8 +193,15 @@ var Views = (function () {
|
|
|
174
193
|
drawDetail();
|
|
175
194
|
}, function (err) {
|
|
176
195
|
U.spinner(false);
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
// Fallback: stream-only addons (e.g. live TV/sports) may expose no meta
|
|
197
|
+
// resource. Use the catalog preview so the title still opens & plays.
|
|
198
|
+
if (_d.preview) {
|
|
199
|
+
_d.meta = Object.assign({ id: id, type: type }, _d.preview);
|
|
200
|
+
drawDetail();
|
|
201
|
+
} else {
|
|
202
|
+
U.toast('Could not load details');
|
|
203
|
+
App.back();
|
|
204
|
+
}
|
|
179
205
|
});
|
|
180
206
|
}
|
|
181
207
|
|
|
@@ -202,6 +228,13 @@ var Views = (function () {
|
|
|
202
228
|
body.appendChild(U.el('div', 'detail-desc', meta.description || ''));
|
|
203
229
|
s.appendChild(body);
|
|
204
230
|
|
|
231
|
+
// If resuming a series episode, pre-select its season.
|
|
232
|
+
if (_d.type === 'series' && _d.target && _d.season === null && meta.videos) {
|
|
233
|
+
var tv = meta.videos.find(function (v) { return v.id === _d.target; });
|
|
234
|
+
if (tv) { _d.season = (tv.season === undefined ? 1 : tv.season); }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_d._epFocus = null;
|
|
205
238
|
if (_d.mode === 'streams') {
|
|
206
239
|
body.appendChild(streamActionsBar());
|
|
207
240
|
s.appendChild(streamsPanel());
|
|
@@ -210,7 +243,8 @@ var Views = (function () {
|
|
|
210
243
|
} else {
|
|
211
244
|
body.appendChild(movieActions());
|
|
212
245
|
}
|
|
213
|
-
Focus.setScope(s);
|
|
246
|
+
Focus.setScope(s, _d._epFocus);
|
|
247
|
+
_d.target = null; // consume: don't re-jump after the user navigates
|
|
214
248
|
}
|
|
215
249
|
|
|
216
250
|
function movieActions() {
|
|
@@ -258,6 +292,7 @@ var Views = (function () {
|
|
|
258
292
|
e.__onselect = function () {
|
|
259
293
|
openStreams(v.id, _d.meta.name + ' S' + _d.season + 'E' + (v.episode || ''));
|
|
260
294
|
};
|
|
295
|
+
if (_d.target && v.id === _d.target) { _d._epFocus = e; }
|
|
261
296
|
listScroll.appendChild(e);
|
|
262
297
|
});
|
|
263
298
|
panel.appendChild(listScroll);
|
|
@@ -303,21 +338,34 @@ var Views = (function () {
|
|
|
303
338
|
_d.streams.forEach(function (stream) {
|
|
304
339
|
var playable = Stremio.isPlayable(stream);
|
|
305
340
|
var node = U.el('div', 'stream focusable');
|
|
306
|
-
node.appendChild(document.createTextNode(Stremio.
|
|
307
|
-
|
|
341
|
+
node.appendChild(document.createTextNode(Stremio.streamName(stream)));
|
|
342
|
+
|
|
343
|
+
// Detail block: size + the addon's own description (languages, seeds, etc.)
|
|
344
|
+
var lines = [];
|
|
345
|
+
var size = Stremio.streamSize(stream);
|
|
346
|
+
var desc = Stremio.streamDescription(stream);
|
|
347
|
+
if (size && !/(TB|GB|MB)\b/i.test(desc)) { lines.push('💾 ' + size); }
|
|
348
|
+
if (desc) { lines.push(desc); }
|
|
349
|
+
if (stream._addon) { lines.push('⚙️ ' + stream._addon); }
|
|
308
350
|
if (!playable) {
|
|
309
|
-
if (stream.infoHash) {
|
|
310
|
-
else if (stream.externalUrl) {
|
|
311
|
-
else {
|
|
351
|
+
if (stream.infoHash) { lines.push('— torrent, needs a debrid/torrent client'); }
|
|
352
|
+
else if (stream.externalUrl) { lines.push('— opens in an external app (not playable here)'); }
|
|
353
|
+
else { lines.push('— not directly playable on the TV'); }
|
|
312
354
|
}
|
|
313
|
-
var sub = (
|
|
314
|
-
|
|
355
|
+
var sub = U.el('span', 's-sub', lines.join('\n'));
|
|
356
|
+
sub.style.whiteSpace = 'pre-line';
|
|
357
|
+
node.appendChild(sub);
|
|
315
358
|
if (!playable) { node.style.opacity = '0.55'; }
|
|
316
359
|
node.__onselect = function () {
|
|
317
360
|
if (!playable) { U.toast('This source can’t play directly on the TV.'); return; }
|
|
361
|
+
var sources = _d.streams.filter(Stremio.isPlayable).map(function (st) {
|
|
362
|
+
var sz = Stremio.streamSize(st);
|
|
363
|
+
return { url: st.url, label: (st._addon ? st._addon + ' · ' : '') + Stremio.streamName(st) + (sz ? ' · ' + sz : '') };
|
|
364
|
+
});
|
|
318
365
|
Player.play({
|
|
319
366
|
url: stream.url, type: _d.type, id: _d.streamId,
|
|
320
|
-
name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster
|
|
367
|
+
name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster,
|
|
368
|
+
streams: sources
|
|
321
369
|
}, function () { drawDetail(); });
|
|
322
370
|
};
|
|
323
371
|
listScroll.appendChild(node);
|
|
@@ -391,12 +439,104 @@ var Views = (function () {
|
|
|
391
439
|
});
|
|
392
440
|
}
|
|
393
441
|
|
|
442
|
+
// ================= ADD ADDON (dedicated page) =================
|
|
443
|
+
function renderAddAddon() {
|
|
444
|
+
var s = scr('addaddon');
|
|
445
|
+
U.clear(s);
|
|
446
|
+
s.appendChild(header(false, false));
|
|
447
|
+
|
|
448
|
+
var wrap = U.el('div', 'add-wrap');
|
|
449
|
+
wrap.appendChild(U.el('h2', null, 'Add addon'));
|
|
450
|
+
wrap.appendChild(U.el('div', 'add-hint', 'Type a Stremio addon manifest URL, then choose Confirm. RETURN cancels — nothing is saved unless you confirm.'));
|
|
451
|
+
|
|
452
|
+
var input = document.createElement('input');
|
|
453
|
+
input.id = 'addaddon-input';
|
|
454
|
+
input.type = 'text';
|
|
455
|
+
input.className = 'focusable add-input';
|
|
456
|
+
input.value = 'https://';
|
|
457
|
+
input.placeholder = 'https://addon.example/manifest.json';
|
|
458
|
+
input.__onselect = function () {
|
|
459
|
+
input.focus();
|
|
460
|
+
var n = input.value.length;
|
|
461
|
+
try { input.setSelectionRange(n, n); } catch (e) {}
|
|
462
|
+
};
|
|
463
|
+
input.onkeydown = function (ev) {
|
|
464
|
+
if (ev.keyCode === 13) { if (ev.stopPropagation) { ev.stopPropagation(); } doConfirm(); }
|
|
465
|
+
};
|
|
466
|
+
wrap.appendChild(input);
|
|
467
|
+
|
|
468
|
+
var row = U.el('div', 'add-btn-row');
|
|
469
|
+
var confirmBtn = U.el('div', 'pill focusable', 'Confirm');
|
|
470
|
+
var cancelBtn = U.el('div', 'pill focusable', 'Cancel');
|
|
471
|
+
confirmBtn.__onselect = doConfirm;
|
|
472
|
+
cancelBtn.__onselect = function () { App.back(); };
|
|
473
|
+
row.appendChild(confirmBtn);
|
|
474
|
+
row.appendChild(cancelBtn);
|
|
475
|
+
wrap.appendChild(row);
|
|
476
|
+
|
|
477
|
+
s.appendChild(wrap);
|
|
478
|
+
Focus.setScope(s, input);
|
|
479
|
+
|
|
480
|
+
function doConfirm() {
|
|
481
|
+
var v = ('' + input.value).trim();
|
|
482
|
+
if (!v || v === 'https://' || v.indexOf('://') === -1 || v.length < 12) {
|
|
483
|
+
U.toast('Enter a full manifest URL');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
Stremio.addAddon(v);
|
|
487
|
+
U.toast('Addon added');
|
|
488
|
+
App.back();
|
|
489
|
+
renderSettings();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ================= SUBTITLE CONFIG =================
|
|
494
|
+
var SUB_LANGS = [['off', 'Off'], ['eng', 'English'], ['spa', 'Spanish'], ['fre', 'French'],
|
|
495
|
+
['ger', 'German'], ['ita', 'Italian'], ['por', 'Portuguese'], ['dut', 'Dutch'],
|
|
496
|
+
['rus', 'Russian'], ['ara', 'Arabic'], ['tur', 'Turkish'], ['pol', 'Polish']];
|
|
497
|
+
var SUB_SIZES = [['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
|
|
498
|
+
var SUB_STYLES = [['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']];
|
|
499
|
+
|
|
500
|
+
function labelFor(list, v) {
|
|
501
|
+
for (var i = 0; i < list.length; i++) { if (list[i][0] === v) { return list[i][1]; } }
|
|
502
|
+
return v;
|
|
503
|
+
}
|
|
504
|
+
function nextVal(list, v) {
|
|
505
|
+
var i = 0;
|
|
506
|
+
for (; i < list.length; i++) { if (list[i][0] === v) { break; } }
|
|
507
|
+
return list[(i + 1) % list.length][0];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function subCycleRow(caption, list, key) {
|
|
511
|
+
var row = U.el('div', 'addon-item focusable cfg-row');
|
|
512
|
+
var name = U.el('div', 'a-name');
|
|
513
|
+
function render() { var cfg = Player.getSubCfg(); name.textContent = caption + ': ' + labelFor(list, cfg[key]); }
|
|
514
|
+
render();
|
|
515
|
+
row.appendChild(name);
|
|
516
|
+
row.appendChild(U.el('div', 'a-url', 'Press OK to change'));
|
|
517
|
+
row.__onselect = function () {
|
|
518
|
+
var cfg = Player.getSubCfg();
|
|
519
|
+
cfg[key] = nextVal(list, cfg[key]);
|
|
520
|
+
Player.setSubCfg(cfg);
|
|
521
|
+
render();
|
|
522
|
+
};
|
|
523
|
+
return row;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function buildSubtitleSection(wrap) {
|
|
527
|
+
wrap.appendChild(U.el('h2', null, 'Subtitles'));
|
|
528
|
+
wrap.appendChild(subCycleRow('Default language', SUB_LANGS, 'lang'));
|
|
529
|
+
wrap.appendChild(subCycleRow('Size', SUB_SIZES, 'size'));
|
|
530
|
+
wrap.appendChild(subCycleRow('Style', SUB_STYLES, 'style'));
|
|
531
|
+
}
|
|
532
|
+
|
|
394
533
|
// ================= SETTINGS =================
|
|
395
534
|
function renderSettings() {
|
|
396
535
|
var s = scr('settings');
|
|
397
536
|
U.clear(s);
|
|
398
537
|
s.appendChild(header(false));
|
|
399
538
|
var wrap = U.el('div', 'settings-wrap');
|
|
539
|
+
buildSubtitleSection(wrap);
|
|
400
540
|
wrap.appendChild(U.el('h2', null, 'Addons'));
|
|
401
541
|
|
|
402
542
|
Stremio.loadAddons().then(function (addons) {
|
|
@@ -430,25 +570,11 @@ var Views = (function () {
|
|
|
430
570
|
};
|
|
431
571
|
wrap.appendChild(reset);
|
|
432
572
|
|
|
433
|
-
// Add-addon
|
|
434
|
-
var addRow = U.el('div', 'addon-item focusable');
|
|
435
|
-
addRow.appendChild(U.el('div', 'a-name', '+
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
input.value = 'https://';
|
|
439
|
-
input.placeholder = 'https://addon.example/manifest.json';
|
|
440
|
-
input.style.cssText = 'width:100%;margin-top:10px;padding:12px;font-size:20px;background:#0e1117;color:#fff;border:2px solid #2a3346;border-radius:6px;';
|
|
441
|
-
input.onchange = function () {
|
|
442
|
-
var v = ('' + input.value).trim();
|
|
443
|
-
if (v && v !== 'https://') { Stremio.addAddon(v); U.toast('Addon added'); setTimeout(renderSettings, 500); }
|
|
444
|
-
};
|
|
445
|
-
addRow.appendChild(input);
|
|
446
|
-
addRow.__onselect = function () {
|
|
447
|
-
input.focus();
|
|
448
|
-
// place cursor at the end so the user types after https://
|
|
449
|
-
var n = input.value.length;
|
|
450
|
-
try { input.setSelectionRange(n, n); } catch (e) {}
|
|
451
|
-
};
|
|
573
|
+
// Add-addon (opens a dedicated page — keeps the field clear of the keyboard)
|
|
574
|
+
var addRow = U.el('div', 'addon-item focusable add-row');
|
|
575
|
+
addRow.appendChild(U.el('div', 'a-name', '+ Add addon'));
|
|
576
|
+
addRow.appendChild(U.el('div', 'a-url', 'Enter a Stremio addon manifest URL on its own page'));
|
|
577
|
+
addRow.__onselect = function () { App.openAddAddon(); };
|
|
452
578
|
wrap.appendChild(addRow);
|
|
453
579
|
|
|
454
580
|
wrap.appendChild(U.el('div', 'hint',
|
|
@@ -460,7 +586,7 @@ var Views = (function () {
|
|
|
460
586
|
|
|
461
587
|
return {
|
|
462
588
|
renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
|
|
463
|
-
renderSearch: renderSearch, backInDetail: backInDetail,
|
|
589
|
+
renderSearch: renderSearch, renderAddAddon: renderAddAddon, backInDetail: backInDetail,
|
|
464
590
|
isModalOpen: function () { return _modalOpen; }, closeModal: closeModal
|
|
465
591
|
};
|
|
466
592
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuvio-tizen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
@@ -25,4 +25,4 @@
|
|
|
25
25
|
],
|
|
26
26
|
"evaluateScriptOnDocumentStart": false,
|
|
27
27
|
"license": "MIT"
|
|
28
|
-
}
|
|
28
|
+
}
|