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/player.js ADDED
@@ -0,0 +1,218 @@
1
+ /* Playback via Tizen AVPlay, with an HTML5 <video> fallback for desktop dev.
2
+ Global namespace: Player */
3
+ var Player = (function () {
4
+ 'use strict';
5
+
6
+ var PROGRESS_KEY = 'nuvio.progress';
7
+
8
+ var avplay = (window.webapis && webapis.avplay) ? webapis.avplay : null;
9
+ var useAV = !!avplay;
10
+
11
+ var videoEl = null; // html5 fallback
12
+ var objEl = null;
13
+
14
+ var _active = false;
15
+ var _onExit = null;
16
+ var _ctx = null; // { id, type, name, poster, resumeSec }
17
+ var _dur = 0; // seconds
18
+ var _cur = 0; // seconds
19
+ var _ticker = null;
20
+ var _osdTimer = null;
21
+ var _seekPending = 0;
22
+
23
+ function init() {
24
+ objEl = U.byId('av-player');
25
+ videoEl = U.byId('html5-player');
26
+ }
27
+
28
+ function isActive() { return _active; }
29
+
30
+ // ---------- progress store ----------
31
+ function progressAll() { return U.store.get(PROGRESS_KEY, {}); }
32
+ function progressKey(ctx) { return ctx.type + ':' + ctx.id; }
33
+
34
+ function saveProgress() {
35
+ if (!_ctx || !_dur) { return; }
36
+ var all = progressAll();
37
+ var k = progressKey(_ctx);
38
+ if (_cur / _dur > 0.95 || _cur < 5) {
39
+ delete all[k];
40
+ } else {
41
+ all[k] = { id: _ctx.id, type: _ctx.type, name: _ctx.name, poster: _ctx.poster,
42
+ position: Math.floor(_cur), duration: Math.floor(_dur), updated: Date.now() };
43
+ }
44
+ U.store.set(PROGRESS_KEY, all);
45
+ }
46
+
47
+ function getResume(type, id) {
48
+ var e = progressAll()[type + ':' + id];
49
+ return e ? e.position : 0;
50
+ }
51
+
52
+ function continueList() {
53
+ var all = progressAll();
54
+ var arr = [];
55
+ for (var k in all) { if (Object.prototype.hasOwnProperty.call(all, k)) { arr.push(all[k]); } }
56
+ arr.sort(function (a, b) { return b.updated - a.updated; });
57
+ return arr;
58
+ }
59
+
60
+ // ---------- OSD ----------
61
+ function showOSD(sticky) {
62
+ U.byId('player-osd').className = '';
63
+ if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
64
+ if (!sticky) { _osdTimer = setTimeout(hideOSD, 4000); }
65
+ }
66
+ function hideOSD() { U.byId('player-osd').className = 'hidden'; }
67
+
68
+ function renderOSD() {
69
+ U.byId('osd-title').textContent = _ctx ? _ctx.name : '';
70
+ var pct = _dur > 0 ? (_cur / _dur * 100) : 0;
71
+ U.byId('osd-progress').style.width = pct + '%';
72
+ U.byId('osd-cur').textContent = U.fmtTime(_cur);
73
+ U.byId('osd-dur').textContent = U.fmtTime(_dur);
74
+ }
75
+
76
+ // ---------- lifecycle ----------
77
+ function play(ctx, onExit) {
78
+ _ctx = ctx; _onExit = onExit;
79
+ _active = true; _cur = 0; _dur = 0;
80
+ _ctx.resumeSec = ctx.resumeSec || getResume(ctx.type, ctx.id);
81
+
82
+ U.byId('app').className = 'hidden';
83
+ document.body.style.background = 'transparent';
84
+ showOSD(true);
85
+ U.spinner(true);
86
+
87
+ if (useAV) { startAV(ctx.url); } else { startHTML5(ctx.url); }
88
+ }
89
+
90
+ function startAV(url) {
91
+ objEl.className = 'active';
92
+ try {
93
+ avplay.close();
94
+ } catch (e) {}
95
+ try {
96
+ avplay.open(url);
97
+ avplay.setListener({
98
+ onbufferingstart: function () { U.spinner(true); },
99
+ onbufferingcomplete: function () { U.spinner(false); },
100
+ onstreamcompleted: function () { _cur = _dur; onEnded(); },
101
+ oncurrentplaytime: function (ms) { _cur = ms / 1000; renderOSD(); },
102
+ onerror: function (err) { U.spinner(false); U.toast('Playback error: ' + err); setTimeout(exit, 1500); }
103
+ });
104
+ try { avplay.setStreamingProperty('ADAPTIVE_INFO', 'BITRATES=1'); } catch (e0) {}
105
+ avplay.prepareAsync(function () {
106
+ U.spinner(false);
107
+ try { avplay.setDisplayRect(0, 0, 1920, 1080); } catch (e1) {}
108
+ try { avplay.setDisplayMethod('PLAYER_DISPLAY_MODE_LETTER_BOX'); } catch (e2) {}
109
+ try { _dur = avplay.getDuration() / 1000; } catch (e3) { _dur = 0; }
110
+ if (_ctx.resumeSec > 5) { try { avplay.seekTo(_ctx.resumeSec * 1000); } catch (e4) {} }
111
+ avplay.play();
112
+ renderOSD();
113
+ startTicker();
114
+ showOSD(false);
115
+ }, function (err) {
116
+ U.spinner(false); U.toast('Could not open stream'); setTimeout(exit, 1500);
117
+ });
118
+ } catch (e) {
119
+ U.spinner(false); U.toast('AVPlay failed: ' + e.message); setTimeout(exit, 1500);
120
+ }
121
+ }
122
+
123
+ function startHTML5(url) {
124
+ videoEl.className = 'active';
125
+ videoEl.src = url;
126
+ videoEl.onloadedmetadata = function () {
127
+ _dur = videoEl.duration || 0;
128
+ if (_ctx.resumeSec > 5) { try { videoEl.currentTime = _ctx.resumeSec; } catch (e) {} }
129
+ U.spinner(false); showOSD(false);
130
+ };
131
+ videoEl.ontimeupdate = function () { _cur = videoEl.currentTime; renderOSD(); };
132
+ videoEl.onended = onEnded;
133
+ videoEl.onerror = function () { U.toast('Playback error'); setTimeout(exit, 1500); };
134
+ var p = videoEl.play();
135
+ if (p && p['catch']) { p['catch'](function () {}); }
136
+ startTicker();
137
+ }
138
+
139
+ function startTicker() {
140
+ if (_ticker) { clearInterval(_ticker); }
141
+ _ticker = setInterval(function () {
142
+ if (useAV) { try { _cur = avplay.getCurrentTime() / 1000; } catch (e) {} renderOSD(); }
143
+ saveProgress();
144
+ }, 5000);
145
+ }
146
+
147
+ function onEnded() { saveProgress(); exit(); }
148
+
149
+ // ---------- controls ----------
150
+ function state() {
151
+ if (useAV) { try { return avplay.getState(); } catch (e) { return ''; } }
152
+ return videoEl.paused ? 'PAUSED' : 'PLAYING';
153
+ }
154
+
155
+ function togglePlay() {
156
+ if (useAV) {
157
+ var s = state();
158
+ if (s === 'PLAYING') { avplay.pause(); } else { avplay.play(); }
159
+ } else {
160
+ if (videoEl.paused) { videoEl.play(); } else { videoEl.pause(); }
161
+ }
162
+ showOSD(false);
163
+ }
164
+
165
+ function seekBy(delta) {
166
+ var target = Math.max(0, Math.min((_dur || 1e9), _cur + delta));
167
+ _cur = target;
168
+ renderOSD();
169
+ showOSD(false);
170
+ if (useAV) {
171
+ try { avplay.seekTo(Math.floor(target * 1000)); } catch (e) {}
172
+ } else {
173
+ try { videoEl.currentTime = target; } catch (e2) {}
174
+ }
175
+ }
176
+
177
+ function handleKey(action) {
178
+ showOSD(false);
179
+ switch (action) {
180
+ case 'OK': case 'PLAYPAUSE': togglePlay(); return true;
181
+ case 'PLAY': if (useAV) { try { avplay.play(); } catch (e) {} } else { videoEl.play(); } return true;
182
+ case 'PAUSE': if (useAV) { try { avplay.pause(); } catch (e) {} } else { videoEl.pause(); } return true;
183
+ case 'RIGHT': case 'FF': seekBy(action === 'FF' ? 30 : 10); return true;
184
+ case 'LEFT': case 'REW': seekBy(action === 'REW' ? -30 : -10); return true;
185
+ case 'UP': case 'DOWN': showOSD(false); return true;
186
+ case 'STOP': case 'BACK': exit(); return true;
187
+ }
188
+ return true;
189
+ }
190
+
191
+ function exit() {
192
+ if (!_active) { return; }
193
+ saveProgress();
194
+ _active = false;
195
+ if (_ticker) { clearInterval(_ticker); _ticker = null; }
196
+ if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
197
+ hideOSD();
198
+ if (useAV) {
199
+ try { avplay.stop(); } catch (e) {}
200
+ try { avplay.close(); } catch (e2) {}
201
+ objEl.className = '';
202
+ } else {
203
+ try { videoEl.pause(); } catch (e3) {}
204
+ videoEl.removeAttribute('src');
205
+ videoEl.className = '';
206
+ }
207
+ document.body.style.background = '';
208
+ U.byId('app').className = '';
209
+ U.spinner(false);
210
+ var cb = _onExit; _onExit = null; _ctx = null;
211
+ if (cb) { cb(); }
212
+ }
213
+
214
+ return {
215
+ init: init, play: play, handleKey: handleKey, isActive: isActive,
216
+ exit: exit, continueList: continueList, getResume: getResume
217
+ };
218
+ })();
@@ -0,0 +1,47 @@
1
+ /* Minimal polyfills for Tizen 3.0 (Chromium ~47). Each is feature-guarded. */
2
+ (function () {
3
+ 'use strict';
4
+
5
+ if (typeof Object.assign !== 'function') {
6
+ Object.assign = function (target) {
7
+ for (var i = 1; i < arguments.length; i++) {
8
+ var src = arguments[i];
9
+ if (src) { for (var k in src) { if (Object.prototype.hasOwnProperty.call(src, k)) { target[k] = src[k]; } } }
10
+ }
11
+ return target;
12
+ };
13
+ }
14
+
15
+ if (!Array.prototype.find) {
16
+ Array.prototype.find = function (fn) {
17
+ for (var i = 0; i < this.length; i++) { if (fn(this[i], i, this)) { return this[i]; } }
18
+ return undefined;
19
+ };
20
+ }
21
+
22
+ if (!Array.prototype.findIndex) {
23
+ Array.prototype.findIndex = function (fn) {
24
+ for (var i = 0; i < this.length; i++) { if (fn(this[i], i, this)) { return i; } }
25
+ return -1;
26
+ };
27
+ }
28
+
29
+ if (!Array.prototype.includes) {
30
+ Array.prototype.includes = function (v) { return this.indexOf(v) !== -1; };
31
+ }
32
+
33
+ if (!String.prototype.includes) {
34
+ String.prototype.includes = function (v) { return this.indexOf(v) !== -1; };
35
+ }
36
+
37
+ if (!Array.from) {
38
+ Array.from = function (arrLike) {
39
+ var out = []; for (var i = 0; i < arrLike.length; i++) { out.push(arrLike[i]); } return out;
40
+ };
41
+ }
42
+
43
+ // requestAnimationFrame fallback (usually present, but be safe)
44
+ if (!window.requestAnimationFrame) {
45
+ window.requestAnimationFrame = function (cb) { return window.setTimeout(function () { cb(Date.now()); }, 16); };
46
+ }
47
+ })();
package/js/stremio.js ADDED
@@ -0,0 +1,198 @@
1
+ /* Stremio addon protocol client. Global namespace: Stremio
2
+ Mirrors how Nuvio sources data: user-configured addons expose
3
+ catalog / meta / stream resources over HTTP JSON. */
4
+ var Stremio = (function () {
5
+ 'use strict';
6
+
7
+ var ADDONS_KEY = 'nuvio.addons';
8
+
9
+ // Default: Cinemeta (official catalog + metadata addon). Stream addons are
10
+ // added by the user in Settings — this app hosts no content itself.
11
+ var DEFAULT_ADDONS = ['https://v3-cinemeta.strem.io/manifest.json'];
12
+
13
+ function normalizeManifestUrl(input) {
14
+ var u = ('' + input).trim();
15
+ if (!u) { return null; }
16
+ if (u.indexOf('://') === -1) { u = 'https://' + u; }
17
+ if (u.slice(-13) === 'manifest.json') { return u; }
18
+ if (u.slice(-1) === '/') { u = u.slice(0, -1); }
19
+ return u + '/manifest.json';
20
+ }
21
+
22
+ function baseOf(manifestUrl) {
23
+ return manifestUrl.replace(/\/manifest\.json.*$/, '');
24
+ }
25
+
26
+ function getAddonUrls() {
27
+ var list = U.store.get(ADDONS_KEY, null);
28
+ if (!list || !list.length) { list = DEFAULT_ADDONS.slice(); }
29
+ return list;
30
+ }
31
+
32
+ function saveAddonUrls(list) { U.store.set(ADDONS_KEY, list); }
33
+
34
+ function addAddon(input) {
35
+ var url = normalizeManifestUrl(input);
36
+ if (!url) { return false; }
37
+ var list = getAddonUrls();
38
+ if (list.indexOf(url) === -1) { list.push(url); saveAddonUrls(list); }
39
+ _loaded = null; // force reload
40
+ return url;
41
+ }
42
+
43
+ function removeAddon(url) {
44
+ var list = getAddonUrls().filter(function (u) { return u !== url; });
45
+ saveAddonUrls(list);
46
+ _loaded = null;
47
+ }
48
+
49
+ // ---- Manifest loading (cached in-memory per session) ----
50
+ var _loaded = null;
51
+
52
+ function loadAddons() {
53
+ if (_loaded) { return Promise.resolve(_loaded); }
54
+ var urls = getAddonUrls();
55
+ var tasks = urls.map(function (url) {
56
+ return function () {
57
+ return U.getJSON(url, 12000).then(function (manifest) {
58
+ return { url: url, base: baseOf(url), manifest: manifest };
59
+ });
60
+ };
61
+ });
62
+ return U.settleAll(tasks, 4).then(function (results) {
63
+ var addons = [];
64
+ results.forEach(function (r) { if (r.ok && r.value.manifest) { addons.push(r.value); } });
65
+ _loaded = addons;
66
+ return addons;
67
+ });
68
+ }
69
+
70
+ // ---- Resource matching ----
71
+ function resourceEntries(addon) {
72
+ var res = (addon.manifest.resources) || [];
73
+ var out = [];
74
+ res.forEach(function (r) {
75
+ if (typeof r === 'string') {
76
+ out.push({ name: r, types: addon.manifest.types || [], idPrefixes: addon.manifest.idPrefixes || null });
77
+ } else if (r && r.name) {
78
+ out.push({ name: r.name, types: r.types || addon.manifest.types || [], idPrefixes: r.idPrefixes || addon.manifest.idPrefixes || null });
79
+ }
80
+ });
81
+ return out;
82
+ }
83
+
84
+ function supports(addon, resourceName, type, id) {
85
+ var entries = resourceEntries(addon);
86
+ for (var i = 0; i < entries.length; i++) {
87
+ var e = entries[i];
88
+ if (e.name !== resourceName) { continue; }
89
+ if (type && e.types && e.types.length && e.types.indexOf(type) === -1) { continue; }
90
+ if (id && e.idPrefixes && e.idPrefixes.length) {
91
+ var ok = false;
92
+ for (var j = 0; j < e.idPrefixes.length; j++) {
93
+ if (id.indexOf(e.idPrefixes[j]) === 0) { ok = true; break; }
94
+ }
95
+ if (!ok) { continue; }
96
+ }
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ // ---- Catalogs ----
103
+ // Returns list of {addon, type, id, name} across all addons.
104
+ function listCatalogs() {
105
+ return loadAddons().then(function (addons) {
106
+ var cats = [];
107
+ addons.forEach(function (addon) {
108
+ var cs = addon.manifest.catalogs || [];
109
+ cs.forEach(function (c) {
110
+ if (!c.type || !c.id) { return; }
111
+ var label = c.name || c.id;
112
+ cats.push({ addon: addon, type: c.type, id: c.id,
113
+ name: label + ' · ' + (addon.manifest.name || 'Addon') });
114
+ });
115
+ });
116
+ return cats;
117
+ });
118
+ }
119
+
120
+ function buildCatalogUrl(addon, type, id, extra) {
121
+ var extras = [];
122
+ if (extra) {
123
+ for (var k in extra) {
124
+ if (Object.prototype.hasOwnProperty.call(extra, k) && extra[k] !== undefined && extra[k] !== '') {
125
+ extras.push(encodeURIComponent(k) + '=' + encodeURIComponent(extra[k]));
126
+ }
127
+ }
128
+ }
129
+ var tail = extras.length ? ('/' + extras.join('&')) : '';
130
+ return addon.base + '/catalog/' + encodeURIComponent(type) + '/' + encodeURIComponent(id) + tail + '.json';
131
+ }
132
+
133
+ function getCatalog(cat, extra) {
134
+ return U.getJSON(buildCatalogUrl(cat.addon, cat.type, cat.id, extra), 12000)
135
+ .then(function (res) { return (res && res.metas) ? res.metas : []; });
136
+ }
137
+
138
+ // ---- Meta ----
139
+ function getMeta(type, id) {
140
+ return loadAddons().then(function (addons) {
141
+ var providers = addons.filter(function (a) { return supports(a, 'meta', type, id); });
142
+ if (!providers.length) { return Promise.reject(new Error('No meta addon for ' + type)); }
143
+ var i = 0;
144
+ function tryNext() {
145
+ if (i >= providers.length) { return Promise.reject(new Error('Meta not found')); }
146
+ var a = providers[i++];
147
+ var url = a.base + '/meta/' + encodeURIComponent(type) + '/' + encodeURIComponent(id) + '.json';
148
+ return U.getJSON(url, 12000).then(function (res) {
149
+ if (res && res.meta) { return res.meta; }
150
+ return tryNext();
151
+ }, function () { return tryNext(); });
152
+ }
153
+ return tryNext();
154
+ });
155
+ }
156
+
157
+ // ---- Streams ----
158
+ // Aggregates stream lists from every addon that supports the (type,id).
159
+ function getStreams(type, id) {
160
+ return loadAddons().then(function (addons) {
161
+ var providers = addons.filter(function (a) { return supports(a, 'stream', type, id); });
162
+ var tasks = providers.map(function (a) {
163
+ return function () {
164
+ var url = a.base + '/stream/' + encodeURIComponent(type) + '/' + encodeURIComponent(id) + '.json';
165
+ return U.getJSON(url, 15000).then(function (res) {
166
+ var arr = (res && res.streams) ? res.streams : [];
167
+ arr.forEach(function (s) { s._addon = a.manifest.name || 'Addon'; });
168
+ return arr;
169
+ });
170
+ };
171
+ });
172
+ return U.settleAll(tasks, 4).then(function (results) {
173
+ var all = [];
174
+ results.forEach(function (r) { if (r.ok) { all = all.concat(r.value); } });
175
+ return all;
176
+ });
177
+ });
178
+ }
179
+
180
+ // A stream is directly playable on the TV only if it exposes an http(s) URL.
181
+ // Torrent (infoHash) / external-only streams need an engine this app doesn't embed.
182
+ function isPlayable(stream) {
183
+ return !!(stream && typeof stream.url === 'string' && /^https?:\/\//i.test(stream.url));
184
+ }
185
+
186
+ function streamLabel(stream) {
187
+ var t = stream.title || stream.name || stream.description || 'Stream';
188
+ return ('' + t).replace(/\n/g, ' ');
189
+ }
190
+
191
+ return {
192
+ DEFAULT_ADDONS: DEFAULT_ADDONS,
193
+ getAddonUrls: getAddonUrls, addAddon: addAddon, removeAddon: removeAddon,
194
+ loadAddons: loadAddons, listCatalogs: listCatalogs, getCatalog: getCatalog,
195
+ getMeta: getMeta, getStreams: getStreams,
196
+ isPlayable: isPlayable, streamLabel: streamLabel
197
+ };
198
+ })();
package/js/util.js ADDED
@@ -0,0 +1,110 @@
1
+ /* Utilities: DOM, HTTP, storage, formatting. Global namespace: U */
2
+ var U = (function () {
3
+ 'use strict';
4
+
5
+ function byId(id) { return document.getElementById(id); }
6
+
7
+ function el(tag, className, text) {
8
+ var node = document.createElement(tag);
9
+ if (className) { node.className = className; }
10
+ if (text !== undefined && text !== null) { node.textContent = text; }
11
+ return node;
12
+ }
13
+
14
+ function clear(node) { while (node.firstChild) { node.removeChild(node.firstChild); } }
15
+
16
+ /* GET JSON via XHR with timeout. Returns a Promise. */
17
+ function getJSON(url, timeoutMs) {
18
+ return new Promise(function (resolve, reject) {
19
+ var xhr = new XMLHttpRequest();
20
+ var done = false;
21
+ try { xhr.open('GET', url, true); } catch (e) { reject(e); return; }
22
+ xhr.timeout = timeoutMs || 15000;
23
+ xhr.onreadystatechange = function () {
24
+ if (xhr.readyState !== 4 || done) { return; }
25
+ done = true;
26
+ if (xhr.status >= 200 && xhr.status < 300) {
27
+ try { resolve(JSON.parse(xhr.responseText)); }
28
+ catch (e2) { reject(new Error('Bad JSON from ' + url)); }
29
+ } else {
30
+ reject(new Error('HTTP ' + xhr.status + ' for ' + url));
31
+ }
32
+ };
33
+ xhr.ontimeout = function () { if (!done) { done = true; reject(new Error('Timeout: ' + url)); } };
34
+ xhr.onerror = function () { if (!done) { done = true; reject(new Error('Network error: ' + url)); } };
35
+ try { xhr.send(); } catch (e3) { reject(e3); }
36
+ });
37
+ }
38
+
39
+ /* localStorage JSON helpers, all guarded (storage can throw / be unavailable). */
40
+ var store = {
41
+ get: function (key, fallback) {
42
+ try {
43
+ var raw = window.localStorage.getItem(key);
44
+ return raw === null ? fallback : JSON.parse(raw);
45
+ } catch (e) { return fallback; }
46
+ },
47
+ set: function (key, value) {
48
+ try { window.localStorage.setItem(key, JSON.stringify(value)); return true; }
49
+ catch (e) { return false; }
50
+ },
51
+ remove: function (key) {
52
+ try { window.localStorage.removeItem(key); } catch (e) {}
53
+ }
54
+ };
55
+
56
+ var _toastTimer = null;
57
+ function toast(msg, ms) {
58
+ var t = byId('toast');
59
+ t.textContent = msg;
60
+ t.className = '';
61
+ if (_toastTimer) { clearTimeout(_toastTimer); }
62
+ _toastTimer = setTimeout(function () { t.className = 'hidden'; }, ms || 2600);
63
+ }
64
+
65
+ function spinner(on) { byId('spinner').className = on ? '' : 'hidden'; }
66
+
67
+ function fmtTime(sec) {
68
+ sec = Math.max(0, Math.floor(sec || 0));
69
+ var h = Math.floor(sec / 3600);
70
+ var m = Math.floor((sec % 3600) / 60);
71
+ var s = sec % 60;
72
+ function p(n) { return n < 10 ? '0' + n : '' + n; }
73
+ return (h > 0 ? h + ':' + p(m) : p(m)) + ':' + p(s);
74
+ }
75
+
76
+ /* Run an array of promise-returning fns with limited concurrency; never rejects.
77
+ Returns Promise of array of {ok, value|error} in original order. */
78
+ function settleAll(tasks, concurrency) {
79
+ concurrency = concurrency || 4;
80
+ return new Promise(function (resolve) {
81
+ var results = new Array(tasks.length);
82
+ var idx = 0, active = 0, finished = 0;
83
+ if (tasks.length === 0) { resolve(results); return; }
84
+ function launch() {
85
+ while (active < concurrency && idx < tasks.length) {
86
+ (function (i) {
87
+ active++;
88
+ tasks[i]().then(function (v) {
89
+ results[i] = { ok: true, value: v };
90
+ }, function (e) {
91
+ results[i] = { ok: false, error: e };
92
+ }).then(function () {
93
+ active--; finished++;
94
+ if (finished === tasks.length) { resolve(results); }
95
+ else { launch(); }
96
+ });
97
+ })(idx);
98
+ idx++;
99
+ }
100
+ }
101
+ launch();
102
+ });
103
+ }
104
+
105
+ return {
106
+ byId: byId, el: el, clear: clear, getJSON: getJSON,
107
+ store: store, toast: toast, spinner: spinner,
108
+ fmtTime: fmtTime, settleAll: settleAll
109
+ };
110
+ })();