nuvio-tizen 1.1.0 → 1.3.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 +9 -1
- package/css/style.css +21 -0
- package/index.html +1 -0
- package/js/app.js +12 -4
- package/js/player.js +62 -7
- package/js/stremio.js +7 -1
- package/js/views.js +175 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,11 +19,19 @@ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
|
|
|
19
19
|
- **Stream aggregation** across all installed stream addons.
|
|
20
20
|
- **Subtitles from addons**: fetched per title/episode, SRT + VTT parsed and rendered as a
|
|
21
21
|
timed overlay; press **UP** during playback to pick a track (or turn them Off).
|
|
22
|
+
Configurable in Settings: **default language** (auto-loads a matching track),
|
|
23
|
+
**size** (S/M/L/XL) and **style** (white+shadow / black box / yellow).
|
|
24
|
+
- **Live streams** (sports / IPTV): live HLS and Stremio `tv`-type sources are detected
|
|
25
|
+
(unknown/infinite duration), shown with a **● LIVE** badge, and correctly skip
|
|
26
|
+
seek / resume / progress-tracking. Stream-only live addons with no `meta` resource still
|
|
27
|
+
open (the catalog entry is used as a fallback).
|
|
22
28
|
- Playback via **AVPlay** (HLS/DASH/MP4/progressive) with an HTML5 `<video>` fallback
|
|
23
29
|
when run in a desktop browser for development.
|
|
24
30
|
- OSD with seek (±10s / ±30s), resume, and per-title progress saved to `localStorage`.
|
|
25
31
|
- Full **D-pad + media-key** remote navigation (custom spatial focus engine).
|
|
26
|
-
- Settings
|
|
32
|
+
- Settings: add addons on a **dedicated page** (field kept above the on-screen keyboard;
|
|
33
|
+
nothing is saved unless you press Confirm), remove with a confirmation prompt, and
|
|
34
|
+
**Reset to default addons**.
|
|
27
35
|
|
|
28
36
|
> **Note on WatchHub:** it's a "where to watch" addon — its results are Rent/Buy/Subscription
|
|
29
37
|
> *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 --- */
|
|
@@ -118,6 +129,16 @@ html, body {
|
|
|
118
129
|
.search-results .card { margin: 0 22px 24px 0; }
|
|
119
130
|
.search-empty { font-size: 24px; color: #8b93a5; margin-top: 30px; }
|
|
120
131
|
|
|
132
|
+
/* --- Confirm modal --- */
|
|
133
|
+
.modal-overlay { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 45; background: rgba(0,0,0,0.72); display: flex; align-items: center; justify-content: center; }
|
|
134
|
+
.modal-box { width: 820px; background: #161b26; border: 2px solid #2a3346; border-radius: 16px; padding: 44px 48px; text-align: center; }
|
|
135
|
+
.modal-msg { font-size: 30px; line-height: 1.4; color: #e6e9ef; margin-bottom: 36px; }
|
|
136
|
+
.modal-row { display: flex; justify-content: center; }
|
|
137
|
+
.modal-btn { min-width: 200px; text-align: center; }
|
|
138
|
+
.pill.danger { background: #3a2130; }
|
|
139
|
+
.pill.danger.focusable.focused { background: #ef4444; color: #fff; }
|
|
140
|
+
.reset-item .a-name { color: #f0a5a5; }
|
|
141
|
+
|
|
121
142
|
/* --- Toast + spinner --- */
|
|
122
143
|
#toast { position: absolute; bottom: 60px; left: 50%; margin-left: -300px; width: 600px; text-align: center; z-index: 40; background: rgba(0,0,0,0.85); padding: 18px 24px; border-radius: 10px; font-size: 22px; }
|
|
123
144
|
#spinner { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 50; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.3); }
|
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) {
|
|
20
20
|
if (!id) { return; }
|
|
21
21
|
pushFrame();
|
|
22
22
|
showScreen('detail');
|
|
23
|
-
Views.renderDetail(type, id);
|
|
23
|
+
Views.renderDetail(type, id, preview);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function openSettings() {
|
|
@@ -35,8 +35,15 @@ 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; }
|
|
46
|
+
if (Views.isModalOpen()) { Views.closeModal(); return; }
|
|
40
47
|
if (_current === 'detail' && Views.backInDetail()) { return; }
|
|
41
48
|
var f = _stack.pop();
|
|
42
49
|
if (!f) { tryExitApp(); return; }
|
|
@@ -77,7 +84,8 @@ var App = (function () {
|
|
|
77
84
|
|
|
78
85
|
return {
|
|
79
86
|
start: start, back: back,
|
|
80
|
-
openDetail: openDetail, openSettings: openSettings, openSearch: openSearch
|
|
87
|
+
openDetail: openDetail, openSettings: openSettings, openSearch: openSearch,
|
|
88
|
+
openAddAddon: openAddAddon
|
|
81
89
|
};
|
|
82
90
|
})();
|
|
83
91
|
|
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';
|
|
161
|
+
var base = _isLive ? 'OK: Play/Pause RETURN: Back' : 'OK: Play/Pause ←/→: Seek RETURN: Back';
|
|
119
162
|
if (_subTracks.length) { base += ' UP: Subtitles'; }
|
|
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';
|
|
@@ -361,6 +415,7 @@ var Player = (function () {
|
|
|
361
415
|
|
|
362
416
|
return {
|
|
363
417
|
init: init, play: play, handleKey: handleKey, isActive: isActive,
|
|
364
|
-
exit: exit, continueList: continueList, getResume: getResume
|
|
418
|
+
exit: exit, continueList: continueList, getResume: getResume,
|
|
419
|
+
getSubCfg: getSubCfg, setSubCfg: setSubCfg
|
|
365
420
|
};
|
|
366
421
|
})();
|
package/js/stremio.js
CHANGED
|
@@ -51,6 +51,12 @@ var Stremio = (function () {
|
|
|
51
51
|
_loaded = null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Clears the stored list so the fresh-install DEFAULT_ADDONS apply again.
|
|
55
|
+
function resetAddons() {
|
|
56
|
+
U.store.remove(ADDONS_KEY);
|
|
57
|
+
_loaded = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
// ---- Manifest loading (cached in-memory per session) ----
|
|
55
61
|
var _loaded = null;
|
|
56
62
|
|
|
@@ -268,7 +274,7 @@ var Stremio = (function () {
|
|
|
268
274
|
|
|
269
275
|
return {
|
|
270
276
|
DEFAULT_ADDONS: DEFAULT_ADDONS,
|
|
271
|
-
getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon,
|
|
277
|
+
getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon, resetAddons: resetAddons,
|
|
272
278
|
loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
|
|
273
279
|
getMeta: getMeta, getStreams: getStreams, getSubtitles: getSubtitles,
|
|
274
280
|
getSearchCatalogs: getSearchCatalogs, search: search,
|
package/js/views.js
CHANGED
|
@@ -5,6 +5,52 @@ var Views = (function () {
|
|
|
5
5
|
|
|
6
6
|
function scr(name) { return U.byId('screen-' + name); }
|
|
7
7
|
|
|
8
|
+
// ---------- confirm modal (D-pad friendly) ----------
|
|
9
|
+
var _modalOpen = false;
|
|
10
|
+
var _modalPrevFocus = null;
|
|
11
|
+
var _modalScope = null;
|
|
12
|
+
|
|
13
|
+
function confirm(message, confirmLabel, onYes) {
|
|
14
|
+
// defensive: never stack modals
|
|
15
|
+
var existing = document.querySelector('.modal-overlay');
|
|
16
|
+
if (existing) { existing.parentNode.removeChild(existing); }
|
|
17
|
+
var overlay = U.el('div', 'modal-overlay');
|
|
18
|
+
var box = U.el('div', 'modal-box');
|
|
19
|
+
box.appendChild(U.el('div', 'modal-msg', message));
|
|
20
|
+
var row = U.el('div', 'modal-row');
|
|
21
|
+
var no = U.el('div', 'pill focusable modal-btn', 'Cancel');
|
|
22
|
+
var yes = U.el('div', 'pill focusable modal-btn danger', confirmLabel || 'Confirm');
|
|
23
|
+
no.__onselect = function () { closeModal(); };
|
|
24
|
+
yes.__onselect = function () { closeModal(); onYes(); };
|
|
25
|
+
row.appendChild(no);
|
|
26
|
+
row.appendChild(yes);
|
|
27
|
+
box.appendChild(row);
|
|
28
|
+
overlay.appendChild(box);
|
|
29
|
+
document.body.appendChild(overlay);
|
|
30
|
+
|
|
31
|
+
_modalPrevFocus = Focus.current();
|
|
32
|
+
_modalScope = _current(); // remember which screen is under the modal
|
|
33
|
+
_modalOpen = true;
|
|
34
|
+
Focus.setScope(overlay, no); // default to Cancel (safe)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function closeModal() {
|
|
38
|
+
var o = document.querySelector('.modal-overlay');
|
|
39
|
+
if (o) { document.body.removeChild(o); }
|
|
40
|
+
_modalOpen = false;
|
|
41
|
+
if (_modalScope) { Focus.setScope(_modalScope, _modalPrevFocus); }
|
|
42
|
+
_modalScope = null; _modalPrevFocus = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _current() {
|
|
46
|
+
var names = ['home', 'detail', 'search', 'settings'];
|
|
47
|
+
for (var i = 0; i < names.length; i++) {
|
|
48
|
+
var e = scr(names[i]);
|
|
49
|
+
if (e && e.className.indexOf('hidden') === -1) { return e; }
|
|
50
|
+
}
|
|
51
|
+
return scr('settings');
|
|
52
|
+
}
|
|
53
|
+
|
|
8
54
|
// ---------- shared bits ----------
|
|
9
55
|
function header(withGear, withSearch) {
|
|
10
56
|
var h = U.el('div', 'header');
|
|
@@ -38,7 +84,7 @@ var Views = (function () {
|
|
|
38
84
|
c.appendChild(U.el('div', 'fallback', meta.name || meta.title || ''));
|
|
39
85
|
}
|
|
40
86
|
c.appendChild(U.el('div', 'cap', meta.name || meta.title || ''));
|
|
41
|
-
c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id); };
|
|
87
|
+
c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id, meta); };
|
|
42
88
|
return c;
|
|
43
89
|
}
|
|
44
90
|
|
|
@@ -116,8 +162,8 @@ var Views = (function () {
|
|
|
116
162
|
// ================= DETAIL =================
|
|
117
163
|
var _d = null; // { type, id, meta, mode, season }
|
|
118
164
|
|
|
119
|
-
function renderDetail(type, id) {
|
|
120
|
-
_d = { type: type, id: id, meta: null, mode: 'info', season: null };
|
|
165
|
+
function renderDetail(type, id, preview) {
|
|
166
|
+
_d = { type: type, id: id, meta: null, mode: 'info', season: null, preview: preview || null };
|
|
121
167
|
var s = scr('detail');
|
|
122
168
|
U.clear(s);
|
|
123
169
|
s.appendChild(U.el('div', 'detail-hero'));
|
|
@@ -128,8 +174,15 @@ var Views = (function () {
|
|
|
128
174
|
drawDetail();
|
|
129
175
|
}, function (err) {
|
|
130
176
|
U.spinner(false);
|
|
131
|
-
|
|
132
|
-
|
|
177
|
+
// Fallback: stream-only addons (e.g. live TV/sports) may expose no meta
|
|
178
|
+
// resource. Use the catalog preview so the title still opens & plays.
|
|
179
|
+
if (_d.preview) {
|
|
180
|
+
_d.meta = Object.assign({ id: id, type: type }, _d.preview);
|
|
181
|
+
drawDetail();
|
|
182
|
+
} else {
|
|
183
|
+
U.toast('Could not load details');
|
|
184
|
+
App.back();
|
|
185
|
+
}
|
|
133
186
|
});
|
|
134
187
|
}
|
|
135
188
|
|
|
@@ -345,12 +398,104 @@ var Views = (function () {
|
|
|
345
398
|
});
|
|
346
399
|
}
|
|
347
400
|
|
|
401
|
+
// ================= ADD ADDON (dedicated page) =================
|
|
402
|
+
function renderAddAddon() {
|
|
403
|
+
var s = scr('addaddon');
|
|
404
|
+
U.clear(s);
|
|
405
|
+
s.appendChild(header(false, false));
|
|
406
|
+
|
|
407
|
+
var wrap = U.el('div', 'add-wrap');
|
|
408
|
+
wrap.appendChild(U.el('h2', null, 'Add addon'));
|
|
409
|
+
wrap.appendChild(U.el('div', 'add-hint', 'Type a Stremio addon manifest URL, then choose Confirm. RETURN cancels — nothing is saved unless you confirm.'));
|
|
410
|
+
|
|
411
|
+
var input = document.createElement('input');
|
|
412
|
+
input.id = 'addaddon-input';
|
|
413
|
+
input.type = 'text';
|
|
414
|
+
input.className = 'focusable add-input';
|
|
415
|
+
input.value = 'https://';
|
|
416
|
+
input.placeholder = 'https://addon.example/manifest.json';
|
|
417
|
+
input.__onselect = function () {
|
|
418
|
+
input.focus();
|
|
419
|
+
var n = input.value.length;
|
|
420
|
+
try { input.setSelectionRange(n, n); } catch (e) {}
|
|
421
|
+
};
|
|
422
|
+
input.onkeydown = function (ev) {
|
|
423
|
+
if (ev.keyCode === 13) { if (ev.stopPropagation) { ev.stopPropagation(); } doConfirm(); }
|
|
424
|
+
};
|
|
425
|
+
wrap.appendChild(input);
|
|
426
|
+
|
|
427
|
+
var row = U.el('div', 'add-btn-row');
|
|
428
|
+
var confirmBtn = U.el('div', 'pill focusable', 'Confirm');
|
|
429
|
+
var cancelBtn = U.el('div', 'pill focusable', 'Cancel');
|
|
430
|
+
confirmBtn.__onselect = doConfirm;
|
|
431
|
+
cancelBtn.__onselect = function () { App.back(); };
|
|
432
|
+
row.appendChild(confirmBtn);
|
|
433
|
+
row.appendChild(cancelBtn);
|
|
434
|
+
wrap.appendChild(row);
|
|
435
|
+
|
|
436
|
+
s.appendChild(wrap);
|
|
437
|
+
Focus.setScope(s, input);
|
|
438
|
+
|
|
439
|
+
function doConfirm() {
|
|
440
|
+
var v = ('' + input.value).trim();
|
|
441
|
+
if (!v || v === 'https://' || v.indexOf('://') === -1 || v.length < 12) {
|
|
442
|
+
U.toast('Enter a full manifest URL');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
Stremio.addAddon(v);
|
|
446
|
+
U.toast('Addon added');
|
|
447
|
+
App.back();
|
|
448
|
+
renderSettings();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ================= SUBTITLE CONFIG =================
|
|
453
|
+
var SUB_LANGS = [['off', 'Off'], ['eng', 'English'], ['spa', 'Spanish'], ['fre', 'French'],
|
|
454
|
+
['ger', 'German'], ['ita', 'Italian'], ['por', 'Portuguese'], ['dut', 'Dutch'],
|
|
455
|
+
['rus', 'Russian'], ['ara', 'Arabic'], ['tur', 'Turkish'], ['pol', 'Polish']];
|
|
456
|
+
var SUB_SIZES = [['small', 'Small'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
|
|
457
|
+
var SUB_STYLES = [['shadow', 'White + shadow'], ['box', 'White on black box'], ['yellow', 'Yellow']];
|
|
458
|
+
|
|
459
|
+
function labelFor(list, v) {
|
|
460
|
+
for (var i = 0; i < list.length; i++) { if (list[i][0] === v) { return list[i][1]; } }
|
|
461
|
+
return v;
|
|
462
|
+
}
|
|
463
|
+
function nextVal(list, v) {
|
|
464
|
+
var i = 0;
|
|
465
|
+
for (; i < list.length; i++) { if (list[i][0] === v) { break; } }
|
|
466
|
+
return list[(i + 1) % list.length][0];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function subCycleRow(caption, list, key) {
|
|
470
|
+
var row = U.el('div', 'addon-item focusable cfg-row');
|
|
471
|
+
var name = U.el('div', 'a-name');
|
|
472
|
+
function render() { var cfg = Player.getSubCfg(); name.textContent = caption + ': ' + labelFor(list, cfg[key]); }
|
|
473
|
+
render();
|
|
474
|
+
row.appendChild(name);
|
|
475
|
+
row.appendChild(U.el('div', 'a-url', 'Press OK to change'));
|
|
476
|
+
row.__onselect = function () {
|
|
477
|
+
var cfg = Player.getSubCfg();
|
|
478
|
+
cfg[key] = nextVal(list, cfg[key]);
|
|
479
|
+
Player.setSubCfg(cfg);
|
|
480
|
+
render();
|
|
481
|
+
};
|
|
482
|
+
return row;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function buildSubtitleSection(wrap) {
|
|
486
|
+
wrap.appendChild(U.el('h2', null, 'Subtitles'));
|
|
487
|
+
wrap.appendChild(subCycleRow('Default language', SUB_LANGS, 'lang'));
|
|
488
|
+
wrap.appendChild(subCycleRow('Size', SUB_SIZES, 'size'));
|
|
489
|
+
wrap.appendChild(subCycleRow('Style', SUB_STYLES, 'style'));
|
|
490
|
+
}
|
|
491
|
+
|
|
348
492
|
// ================= SETTINGS =================
|
|
349
493
|
function renderSettings() {
|
|
350
494
|
var s = scr('settings');
|
|
351
495
|
U.clear(s);
|
|
352
496
|
s.appendChild(header(false));
|
|
353
497
|
var wrap = U.el('div', 'settings-wrap');
|
|
498
|
+
buildSubtitleSection(wrap);
|
|
354
499
|
wrap.appendChild(U.el('h2', null, 'Addons'));
|
|
355
500
|
|
|
356
501
|
Stremio.loadAddons().then(function (addons) {
|
|
@@ -360,33 +505,35 @@ var Views = (function () {
|
|
|
360
505
|
var item = U.el('div', 'addon-item focusable');
|
|
361
506
|
item.appendChild(U.el('div', 'a-name', loaded ? (loaded.manifest.name || 'Addon') : '(failed to load)'));
|
|
362
507
|
item.appendChild(U.el('div', 'a-url', url));
|
|
508
|
+
var name = loaded ? (loaded.manifest.name || 'Addon') : 'this addon';
|
|
363
509
|
item.__onselect = function () {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
510
|
+
confirm('Remove "' + name + '"?', 'Remove', function () {
|
|
511
|
+
Stremio.removeAddon(url);
|
|
512
|
+
U.toast('Removed');
|
|
513
|
+
setTimeout(renderSettings, 300);
|
|
514
|
+
});
|
|
367
515
|
};
|
|
368
516
|
wrap.appendChild(item);
|
|
369
517
|
});
|
|
370
518
|
|
|
371
|
-
//
|
|
372
|
-
var
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (v && v !== 'https://') { Stremio.addAddon(v); U.toast('Addon added'); setTimeout(renderSettings, 500); }
|
|
382
|
-
};
|
|
383
|
-
addRow.appendChild(input);
|
|
384
|
-
addRow.__onselect = function () {
|
|
385
|
-
input.focus();
|
|
386
|
-
// place cursor at the end so the user types after https://
|
|
387
|
-
var n = input.value.length;
|
|
388
|
-
try { input.setSelectionRange(n, n); } catch (e) {}
|
|
519
|
+
// Reset to defaults
|
|
520
|
+
var reset = U.el('div', 'addon-item focusable reset-item');
|
|
521
|
+
reset.appendChild(U.el('div', 'a-name', '↺ Reset to default addons'));
|
|
522
|
+
reset.appendChild(U.el('div', 'a-url', 'Removes all addons and restores Cinemeta, WatchHub, OpenSubtitles v3'));
|
|
523
|
+
reset.__onselect = function () {
|
|
524
|
+
confirm('Reset to default addons? This removes all addons you’ve added.', 'Reset', function () {
|
|
525
|
+
Stremio.resetAddons();
|
|
526
|
+
U.toast('Addons reset to default');
|
|
527
|
+
setTimeout(renderSettings, 300);
|
|
528
|
+
});
|
|
389
529
|
};
|
|
530
|
+
wrap.appendChild(reset);
|
|
531
|
+
|
|
532
|
+
// Add-addon (opens a dedicated page — keeps the field clear of the keyboard)
|
|
533
|
+
var addRow = U.el('div', 'addon-item focusable add-row');
|
|
534
|
+
addRow.appendChild(U.el('div', 'a-name', '+ Add addon'));
|
|
535
|
+
addRow.appendChild(U.el('div', 'a-url', 'Enter a Stremio addon manifest URL on its own page'));
|
|
536
|
+
addRow.__onselect = function () { App.openAddAddon(); };
|
|
390
537
|
wrap.appendChild(addRow);
|
|
391
538
|
|
|
392
539
|
wrap.appendChild(U.el('div', 'hint',
|
|
@@ -398,6 +545,7 @@ var Views = (function () {
|
|
|
398
545
|
|
|
399
546
|
return {
|
|
400
547
|
renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
|
|
401
|
-
renderSearch: renderSearch, backInDetail: backInDetail
|
|
548
|
+
renderSearch: renderSearch, renderAddAddon: renderAddAddon, backInDetail: backInDetail,
|
|
549
|
+
isModalOpen: function () { return _modalOpen; }, closeModal: closeModal
|
|
402
550
|
};
|
|
403
551
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuvio-tizen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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
|
+
}
|