nuvio-tizen 1.0.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/js/views.js ADDED
@@ -0,0 +1,330 @@
1
+ /* Screen rendering. Global namespace: Views
2
+ Screens: home (#screen-home), detail (#screen-detail), settings (#screen-settings). */
3
+ var Views = (function () {
4
+ 'use strict';
5
+
6
+ function scr(name) { return U.byId('screen-' + name); }
7
+
8
+ // ---------- shared bits ----------
9
+ function header(withGear) {
10
+ var h = U.el('div', 'header');
11
+ var brand = U.el('div', 'brand');
12
+ brand.innerHTML = 'NUVIO<span class="dot">.</span>';
13
+ h.appendChild(brand);
14
+ h.appendChild(U.el('div', 'spacer'));
15
+ if (withGear) {
16
+ var g = U.el('div', 'gear focusable', '⚙');
17
+ g.__onselect = function () { App.openSettings(); };
18
+ h.appendChild(g);
19
+ }
20
+ return h;
21
+ }
22
+
23
+ function posterCard(meta, fallbackType) {
24
+ var c = U.el('div', 'card focusable');
25
+ var poster = meta.poster || '';
26
+ if (poster) {
27
+ var img = U.el('img', 'poster');
28
+ img.onerror = function () { img.style.display = 'none'; c.appendChild(U.el('div', 'fallback', meta.name || meta.title || '')); };
29
+ img.src = poster;
30
+ c.appendChild(img);
31
+ } else {
32
+ c.appendChild(U.el('div', 'fallback', meta.name || meta.title || ''));
33
+ }
34
+ c.appendChild(U.el('div', 'cap', meta.name || meta.title || ''));
35
+ c.__onselect = function () { App.openDetail(meta.type || fallbackType, meta.id); };
36
+ return c;
37
+ }
38
+
39
+ // ================= HOME =================
40
+ function renderHome() {
41
+ var s = scr('home');
42
+ U.clear(s);
43
+ s.appendChild(header(true));
44
+ var rows = U.el('div', 'rows');
45
+ s.appendChild(rows);
46
+
47
+ // Continue watching
48
+ var cont = Player.continueList();
49
+ if (cont.length) {
50
+ rows.appendChild(buildRow('Continue Watching', cont.map(function (e) {
51
+ var c = posterCard({ id: e.id.split(':')[0], type: e.type, name: e.name, poster: e.poster }, e.type);
52
+ if (e.duration) {
53
+ var p = U.el('div', 'prog');
54
+ p.style.width = Math.min(100, (e.position / e.duration * 100)) + '%';
55
+ c.appendChild(p);
56
+ }
57
+ return c;
58
+ })));
59
+ }
60
+
61
+ U.spinner(true);
62
+ Stremio.listCatalogs().then(function (cats) {
63
+ var pick = cats.slice(0, 10);
64
+ // Render placeholder rows in order, fill as data arrives.
65
+ var rowNodes = pick.map(function (cat) {
66
+ var placeholder = buildRow(cat.name, []);
67
+ rows.appendChild(placeholder);
68
+ return placeholder;
69
+ });
70
+ Focus.setScope(s);
71
+ U.spinner(false);
72
+
73
+ var tasks = pick.map(function (cat, i) {
74
+ return function () {
75
+ return Stremio.getCatalog(cat).then(function (metas) {
76
+ fillRow(rowNodes[i], metas.slice(0, 24), cat.type);
77
+ });
78
+ };
79
+ });
80
+ U.settleAll(tasks, 3).then(function () {
81
+ // Remove empty rows, refresh focus scope.
82
+ for (var i = rowNodes.length - 1; i >= 0; i--) {
83
+ if (!rowNodes[i].getAttribute('data-filled')) { rows.removeChild(rowNodes[i]); }
84
+ }
85
+ if (!Focus.current()) { Focus.setScope(s); }
86
+ });
87
+ }, function (err) {
88
+ U.spinner(false);
89
+ U.toast('Failed to load addons. Check network / Settings.');
90
+ Focus.setScope(s);
91
+ });
92
+ }
93
+
94
+ function buildRow(title, cards) {
95
+ var row = U.el('div', 'row');
96
+ row.appendChild(U.el('div', 'row-title', title));
97
+ var scroller = U.el('div', 'row-scroller');
98
+ cards.forEach(function (c) { scroller.appendChild(c); });
99
+ row.appendChild(scroller);
100
+ return row;
101
+ }
102
+
103
+ function fillRow(rowNode, metas, fallbackType) {
104
+ if (!metas.length) { return; }
105
+ var scroller = rowNode.querySelector('.row-scroller');
106
+ metas.forEach(function (m) { scroller.appendChild(posterCard(m, fallbackType)); });
107
+ rowNode.setAttribute('data-filled', '1');
108
+ }
109
+
110
+ // ================= DETAIL =================
111
+ var _d = null; // { type, id, meta, mode, season }
112
+
113
+ function renderDetail(type, id) {
114
+ _d = { type: type, id: id, meta: null, mode: 'info', season: null };
115
+ var s = scr('detail');
116
+ U.clear(s);
117
+ s.appendChild(U.el('div', 'detail-hero'));
118
+ U.spinner(true);
119
+ Stremio.getMeta(type, id).then(function (meta) {
120
+ _d.meta = meta;
121
+ U.spinner(false);
122
+ drawDetail();
123
+ }, function (err) {
124
+ U.spinner(false);
125
+ U.toast('Could not load details');
126
+ App.back();
127
+ });
128
+ }
129
+
130
+ function drawDetail() {
131
+ var s = scr('detail');
132
+ U.clear(s);
133
+ var meta = _d.meta;
134
+
135
+ var hero = U.el('div', 'detail-hero');
136
+ var bg = meta.background || meta.poster;
137
+ if (bg) { hero.style.backgroundImage = 'url("' + bg + '")'; }
138
+ hero.appendChild(U.el('div', 'shade'));
139
+ s.appendChild(hero);
140
+
141
+ var body = U.el('div', 'detail-body');
142
+ body.appendChild(U.el('div', 'detail-title', meta.name || meta.title || ''));
143
+ var bits = [];
144
+ if (meta.releaseInfo) { bits.push(meta.releaseInfo); }
145
+ else if (meta.year) { bits.push('' + meta.year); }
146
+ if (meta.runtime) { bits.push(meta.runtime); }
147
+ if (meta.imdbRating) { bits.push('★ ' + meta.imdbRating); }
148
+ if (meta.genres && meta.genres.length) { bits.push(meta.genres.slice(0, 3).join(', ')); }
149
+ body.appendChild(U.el('div', 'detail-meta', bits.join(' · ')));
150
+ body.appendChild(U.el('div', 'detail-desc', meta.description || ''));
151
+ s.appendChild(body);
152
+
153
+ if (_d.mode === 'streams') {
154
+ body.appendChild(streamActionsBar());
155
+ s.appendChild(streamsPanel());
156
+ } else if (_d.type === 'series' && meta.videos && meta.videos.length) {
157
+ s.appendChild(seriesPanel());
158
+ } else {
159
+ body.appendChild(movieActions());
160
+ }
161
+ Focus.setScope(s);
162
+ }
163
+
164
+ function movieActions() {
165
+ var row = U.el('div', 'btn-row');
166
+ var play = U.el('div', 'pill focusable', '▶ Play');
167
+ play.__onselect = function () { openStreams(_d.id, _d.meta.name); };
168
+ row.appendChild(play);
169
+ return row;
170
+ }
171
+
172
+ // ----- series episodes -----
173
+ function seasons(meta) {
174
+ var set = {};
175
+ meta.videos.forEach(function (v) { var s = (v.season === undefined ? 1 : v.season); set[s] = true; });
176
+ var arr = Object.keys(set).map(function (k) { return parseInt(k, 10); });
177
+ arr.sort(function (a, b) { return a - b; });
178
+ return arr;
179
+ }
180
+
181
+ function seriesPanel() {
182
+ var meta = _d.meta;
183
+ var ss = seasons(meta);
184
+ if (_d.season === null) { _d.season = ss.indexOf(1) !== -1 ? 1 : ss[0]; }
185
+
186
+ var panel = U.el('div', 'list-panel');
187
+ var bar = U.el('div', 'season-bar');
188
+ ss.forEach(function (sn) {
189
+ var p = U.el('div', 'pill focusable', 'Season ' + sn);
190
+ if (sn === _d.season) { p.style.background = '#2a3346'; }
191
+ p.__onselect = function () { _d.season = sn; drawDetail(); };
192
+ bar.appendChild(p);
193
+ });
194
+ panel.appendChild(bar);
195
+
196
+ var listScroll = U.el('div', 'list-scroll');
197
+ listScroll.style.position = 'relative';
198
+ listScroll.style.top = '60px';
199
+ listScroll.style.height = '340px';
200
+ var eps = meta.videos.filter(function (v) { return (v.season === undefined ? 1 : v.season) === _d.season; });
201
+ eps.sort(function (a, b) { return (a.episode || 0) - (b.episode || 0); });
202
+ eps.forEach(function (v) {
203
+ var e = U.el('div', 'ep focusable');
204
+ e.appendChild(document.createTextNode('E' + (v.episode || '?') + ' ' + (v.title || v.name || '')));
205
+ if (v.overview) { e.appendChild(U.el('span', 'ep-sub', v.overview)); }
206
+ e.__onselect = function () {
207
+ openStreams(v.id, _d.meta.name + ' S' + _d.season + 'E' + (v.episode || ''));
208
+ };
209
+ listScroll.appendChild(e);
210
+ });
211
+ panel.appendChild(listScroll);
212
+ return panel;
213
+ }
214
+
215
+ // ----- streams -----
216
+ function openStreams(videoId, label) {
217
+ _d.mode = 'streams';
218
+ _d.streamId = videoId;
219
+ _d.streamLabel = label;
220
+ _d.streams = null;
221
+ drawDetail();
222
+ U.spinner(true);
223
+ Stremio.getStreams(_d.type, videoId).then(function (list) {
224
+ _d.streams = list;
225
+ U.spinner(false);
226
+ if (_d.mode === 'streams') { drawDetail(); }
227
+ }, function () {
228
+ _d.streams = [];
229
+ U.spinner(false);
230
+ if (_d.mode === 'streams') { drawDetail(); }
231
+ });
232
+ }
233
+
234
+ function streamActionsBar() {
235
+ var row = U.el('div', 'btn-row');
236
+ row.appendChild(U.el('div', 'detail-meta', 'Sources for ' + (_d.streamLabel || '')));
237
+ return row;
238
+ }
239
+
240
+ function streamsPanel() {
241
+ var panel = U.el('div', 'list-panel');
242
+ var listScroll = U.el('div', 'list-scroll');
243
+ if (_d.streams === null) {
244
+ panel.appendChild(U.el('div', 'detail-meta', 'Loading sources…'));
245
+ return panel;
246
+ }
247
+ if (!_d.streams.length) {
248
+ panel.appendChild(U.el('div', 'detail-meta', 'No sources found. Add a stream addon in Settings.'));
249
+ return panel;
250
+ }
251
+ _d.streams.forEach(function (stream) {
252
+ var playable = Stremio.isPlayable(stream);
253
+ var node = U.el('div', 'stream focusable');
254
+ node.appendChild(document.createTextNode(Stremio.streamLabel(stream)));
255
+ var sub = (stream._addon || '') + (playable ? '' : ' — needs torrent/debrid client (not supported)');
256
+ node.appendChild(U.el('span', 's-sub', sub));
257
+ if (!playable) { node.style.opacity = '0.55'; }
258
+ node.__onselect = function () {
259
+ if (!playable) { U.toast('This source can’t play directly on the TV.'); return; }
260
+ Player.play({
261
+ url: stream.url, type: _d.type, id: _d.streamId,
262
+ name: _d.streamLabel || _d.meta.name, poster: _d.meta.poster
263
+ }, function () { drawDetail(); });
264
+ };
265
+ listScroll.appendChild(node);
266
+ });
267
+ panel.appendChild(listScroll);
268
+ return panel;
269
+ }
270
+
271
+ // Return true if BACK was handled inside detail (streams -> info).
272
+ function backInDetail() {
273
+ if (_d && _d.mode === 'streams') {
274
+ _d.mode = 'info';
275
+ drawDetail();
276
+ return true;
277
+ }
278
+ return false;
279
+ }
280
+
281
+ // ================= SETTINGS =================
282
+ function renderSettings() {
283
+ var s = scr('settings');
284
+ U.clear(s);
285
+ s.appendChild(header(false));
286
+ var wrap = U.el('div', 'settings-wrap');
287
+ wrap.appendChild(U.el('h2', null, 'Addons'));
288
+
289
+ Stremio.loadAddons().then(function (addons) {
290
+ var urls = Stremio.getAddonUrls();
291
+ urls.forEach(function (url) {
292
+ var loaded = addons.find(function (a) { return a.url === url; });
293
+ var item = U.el('div', 'addon-item focusable');
294
+ item.appendChild(U.el('div', 'a-name', loaded ? (loaded.manifest.name || 'Addon') : '(failed to load)'));
295
+ item.appendChild(U.el('div', 'a-url', url));
296
+ item.__onselect = function () {
297
+ U.toast('Removed. Reopening…');
298
+ Stremio.removeAddon(url);
299
+ setTimeout(renderSettings, 500);
300
+ };
301
+ wrap.appendChild(item);
302
+ });
303
+
304
+ // Add-addon input
305
+ var addRow = U.el('div', 'addon-item focusable');
306
+ addRow.appendChild(U.el('div', 'a-name', '+ Add addon (focus, then type a manifest URL)'));
307
+ var input = document.createElement('input');
308
+ input.type = 'text';
309
+ input.placeholder = 'https://addon.example/manifest.json';
310
+ input.style.cssText = 'width:100%;margin-top:10px;padding:12px;font-size:20px;background:#0e1117;color:#fff;border:2px solid #2a3346;border-radius:6px;';
311
+ input.onchange = function () {
312
+ var v = input.value;
313
+ if (v) { Stremio.addAddon(v); U.toast('Addon added'); setTimeout(renderSettings, 500); }
314
+ };
315
+ addRow.appendChild(input);
316
+ addRow.__onselect = function () { input.focus(); };
317
+ wrap.appendChild(addRow);
318
+
319
+ wrap.appendChild(U.el('div', 'hint',
320
+ 'Select an addon to remove it. Nuvio hosts no content — catalogs & metadata come from Cinemeta; add your own Stremio stream addon URLs above to get playable sources. Press RETURN to go back.'));
321
+ s.appendChild(wrap);
322
+ Focus.setScope(s);
323
+ });
324
+ }
325
+
326
+ return {
327
+ renderHome: renderHome, renderDetail: renderDetail, renderSettings: renderSettings,
328
+ backInDetail: backInDetail
329
+ };
330
+ })();
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "nuvio-tizen",
3
+ "version": "1.0.0",
4
+ "description": "Nuvio-style Stremio-addon streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
+ "packageType": "app",
6
+ "appName": "Nuvio",
7
+ "appPath": "index.html",
8
+ "files": [
9
+ "index.html",
10
+ "css",
11
+ "js",
12
+ "icon.png",
13
+ "config.xml",
14
+ "README.md"
15
+ ],
16
+ "keys": [
17
+ "MediaPlay",
18
+ "MediaPause",
19
+ "MediaPlayPause",
20
+ "MediaStop",
21
+ "MediaRewind",
22
+ "MediaFastForward",
23
+ "MediaTrackPrevious",
24
+ "MediaTrackNext"
25
+ ],
26
+ "evaluateScriptOnDocumentStart": false,
27
+ "license": "MIT"
28
+ }