stremiok 2.0.0 → 2.2.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/CHANGELOG.md +82 -0
- package/README.md +10 -8
- package/config.xml +1 -1
- package/css/style.css +37 -2
- package/index.html +34 -31
- package/js/app.js +19 -7
- package/js/downloads.js +17 -5
- package/js/epg.js +320 -0
- package/js/hubpreview.js +15 -5
- package/js/iptv.js +202 -29
- package/js/keys.js +2 -0
- package/js/player-controls.js +55 -8
- package/js/player-core.js +204 -31
- package/js/player-lang.js +1 -1
- package/js/player-quality.js +15 -7
- package/js/player-store.js +28 -4
- package/js/player-subs.js +45 -11
- package/js/player.js +1 -1
- package/js/stremio-account.js +3 -0
- package/js/stremio.js +157 -28
- package/js/trakt.js +61 -9
- package/js/util.js +36 -10
- package/js/view-core.js +139 -55
- package/js/view-detail.js +5 -4
- package/js/view-guide.js +332 -0
- package/js/view-home.js +168 -36
- package/js/view-settings.js +170 -80
- package/js/views.js +3 -2
- package/js/youtube.js +1 -3
- package/js/ytplay.js +62 -5
- package/package.json +2 -1
- package/service/index.js +1131 -108
- package/service/preview.js +14 -4
package/js/view-home.js
CHANGED
|
@@ -26,40 +26,116 @@
|
|
|
26
26
|
{ key: 'movie', icon: '🎬', label: 'Movies' },
|
|
27
27
|
{ key: 'series', icon: '📺', label: 'Series' },
|
|
28
28
|
{ key: 'tv', icon: '📡', label: 'IPTV' },
|
|
29
|
+
{ key: 'guide', icon: '☰', label: 'Guide' }, // (the IPTV channels' TV guide as a grid, once one is read: see js/view-guide.js)
|
|
29
30
|
{ key: 'radio', icon: '📻', label: 'Radio' },
|
|
30
31
|
{ key: 'sport', icon: '⚽', label: 'Sports' },
|
|
32
|
+
{ key: 'youtube', icon: '📹', label: 'YouTube' }, // (StremioK's YouTube catalogs, an addon's YouTube / channel ones: see isYouTubeCat)
|
|
31
33
|
{ key: 'local', icon: '💾', label: 'Local' } // (a USB drive plugged in: see LocalMedia)
|
|
32
34
|
];
|
|
33
35
|
function hasLocal() { return typeof LocalMedia !== 'undefined' && LocalMedia.present(); }
|
|
36
|
+
function hasGuide() { return typeof EPG !== 'undefined' && !!EPG.hasAny && EPG.hasAny(); }
|
|
37
|
+
|
|
38
|
+
// The top menu's order (Settings › Appearance › Menu): Watching first and Settings last, the
|
|
39
|
+
// rest (the tabs, Discover, Search) in yours. A saved order is kept as far as it goes: an
|
|
40
|
+
// item it doesn't have (one added since) goes after the one it usually follows, and one
|
|
41
|
+
// that no longer exists is dropped.
|
|
42
|
+
var MENU_ITEMS = HOME_TABS.filter(function (t) { return t.key !== 'continue'; }).map(function (t) { return t.key; }).concat(['discover', 'search']);
|
|
43
|
+
function menuOrder() {
|
|
44
|
+
var out = [];
|
|
45
|
+
(Stremio.getPrefs().menuOrder || []).forEach(function (k) { if (MENU_ITEMS.indexOf(k) !== -1 && out.indexOf(k) === -1) { out.push(k); } });
|
|
46
|
+
MENU_ITEMS.forEach(function (k, i) {
|
|
47
|
+
if (out.indexOf(k) !== -1) { return; }
|
|
48
|
+
var at = 0;
|
|
49
|
+
for (var j = i - 1; j >= 0; j--) { var p = out.indexOf(MENU_ITEMS[j]); if (p !== -1) { at = p + 1; break; } }
|
|
50
|
+
out.splice(at, 0, k);
|
|
51
|
+
});
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function moveMenuItem(key, dir) {
|
|
55
|
+
var o = menuOrder();
|
|
56
|
+
if (!U.moveIn(o, o.indexOf(key), dir)) { return false; }
|
|
57
|
+
Stremio.setPref('menuOrder', o);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// What the top menu shows, left to right, before Settings: Watching, then in your order the
|
|
61
|
+
// tabs showing (availableTabs), Discover and Search. [{ key, tab }] (tab: a Home tab's entry).
|
|
62
|
+
function menuItems() {
|
|
63
|
+
var tabs = availableTabs(), byKey = {}, out = [];
|
|
64
|
+
tabs.forEach(function (t) { byKey[t.key] = t; });
|
|
65
|
+
if (byKey['continue']) { out.push({ key: 'continue', tab: byKey['continue'] }); }
|
|
66
|
+
menuOrder().forEach(function (k) {
|
|
67
|
+
if (k === 'discover' || k === 'search') { out.push({ key: k }); }
|
|
68
|
+
else if (byKey[k]) { out.push({ key: k, tab: byKey[k] }); }
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
34
72
|
|
|
35
73
|
// Which tabs to show in the top menu (needs the home catalogs; falls back to the
|
|
36
74
|
// standard set before they've loaded so the menu is consistent everywhere).
|
|
37
|
-
// Tabs you hid (Settings › Appearance ›
|
|
75
|
+
// Tabs you hid (Settings › Appearance › Menu) aren't shown; Watching always is.
|
|
38
76
|
function availableTabs() {
|
|
39
77
|
var hidden = Stremio.getPrefs().hiddenTabs || [];
|
|
40
|
-
function shown(t) { return t.key === 'continue' || hidden.indexOf(t.key) === -1; }
|
|
78
|
+
function shown(t) { return t.key === 'continue' || (hidden.indexOf(t.key) === -1 && (t.key !== 'guide' || hasGuide())); }
|
|
41
79
|
if (_homeCatalogs === null || _homeFirstLoad) { // first load still running
|
|
42
80
|
return HOME_TABS.filter(function (t) { return shown(t) && (t.key !== 'local' || hasLocal()); });
|
|
43
81
|
}
|
|
44
82
|
return HOME_TABS.filter(function (t) {
|
|
45
|
-
return shown(t) && (t.key === 'continue' || t.key === 'calendar' || (t.key === 'local' ? hasLocal() : catsForTab(t.key).length > 0));
|
|
83
|
+
return shown(t) && (t.key === 'continue' || t.key === 'calendar' || t.key === 'guide' || (t.key === 'local' ? hasLocal() : catsForTab(t.key).length > 0));
|
|
46
84
|
});
|
|
47
85
|
}
|
|
48
86
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
// YouTube's tab: StremioK's own YouTube catalogs (whatever type you gave one: "Podcasts"),
|
|
88
|
+
// and another addon's YouTube or channel catalogs (YouTubio's "YouTube", Stremio's "channel").
|
|
89
|
+
// (Home's copies of a catalog, and the ones saved for the next launch, keep only its name,
|
|
90
|
+
// type and titles: a YouTube one carries yt, as its addon isn't kept with them)
|
|
91
|
+
function isYouTubeCat(c) {
|
|
92
|
+
var m = c.addon && c.addon.manifest;
|
|
93
|
+
return !!c.yt || !!(m && m.id === 'org.stremiok.youtube') || /^(youtube|channel)$/i.test('' + c.type);
|
|
94
|
+
}
|
|
95
|
+
function copyCat(c, metas, more) { // (name, type, titles: and yt when it's one)
|
|
96
|
+
var o = { name: c.name, type: c.type, metas: metas };
|
|
97
|
+
if (c.yt) { o.yt = true; }
|
|
98
|
+
if (more) { Object.keys(more).forEach(function (k) { o[k] = more[k]; }); }
|
|
99
|
+
return o;
|
|
100
|
+
}
|
|
101
|
+
// Which cached catalogs belong to a tab (tabOf). Movies, series, sports, radio and YouTube go
|
|
102
|
+
// to their own tabs, live TV (LIVE_TYPES, IPTV playlists) to IPTV. Another type (a debrid
|
|
103
|
+
// library's "RealDebrid", "other") goes by what it lists (byItems); anything still unclear
|
|
104
|
+
// isn't on Home ('': Discover has it). ("anime" never gets here: see Stremio's discoverOnly.)
|
|
105
|
+
var LIVE_TYPES = { tv: 1, events: 1, event: 1, live: 1, livetv: 1, iptv: 1 };
|
|
106
|
+
var TITLE_IDS = /^(tt\d+|tmdb:|tvdb:|kitsu:|mal:|anilist:|anidb:)/i;
|
|
107
|
+
function tabOf(c) {
|
|
108
|
+
var n = (c.metas || []).length;
|
|
109
|
+
if (c._tab !== undefined && c._tabN === n) { return c._tab; } // (worked out again once it has titles)
|
|
110
|
+
var t = ('' + (c.type || '')).toLowerCase(), tab;
|
|
111
|
+
if (isYouTubeCat(c)) { tab = 'youtube'; }
|
|
112
|
+
else if (t === 'movie' || t === 'series' || t === 'radio') { tab = t; }
|
|
113
|
+
else if (t === 'sport' || t === 'sports') { tab = 'sport'; }
|
|
114
|
+
else if (LIVE_TYPES[t]) { tab = 'tv'; }
|
|
115
|
+
else { tab = byItems(c); }
|
|
116
|
+
c._tab = tab; c._tabN = n;
|
|
117
|
+
return tab;
|
|
118
|
+
}
|
|
119
|
+
// Mostly films or series (60% of its first 50 titles): that tab. Title ids, and "anime",
|
|
120
|
+
// "series" or "show" in its name or type: Series ("movie" / "film": Movies). Else ''.
|
|
121
|
+
function byItems(c) {
|
|
122
|
+
var ms = (c.metas || []).slice(0, 50), mv = 0, sr = 0, ids = 0;
|
|
123
|
+
if (!ms.length) { return ''; }
|
|
124
|
+
ms.forEach(function (m) {
|
|
125
|
+
var mt = ('' + ((m && m.type) || '')).toLowerCase();
|
|
126
|
+
if (mt === 'movie') { mv++; } else if (mt === 'series') { sr++; }
|
|
127
|
+
if (TITLE_IDS.test('' + ((m && m.id) || ''))) { ids++; }
|
|
62
128
|
});
|
|
129
|
+
if (mv + sr >= ms.length * 0.6) { return mv > sr ? 'movie' : 'series'; }
|
|
130
|
+
if (ids >= ms.length * 0.6) {
|
|
131
|
+
var hint = (c.type + ' ' + (c.name || '') + ' ' + ((c.def && c.def.name) || '')).toLowerCase();
|
|
132
|
+
if (/movie|film|pel[ií]cula/.test(hint)) { return 'movie'; }
|
|
133
|
+
if (/anime|series|show|serie/.test(hint)) { return 'series'; }
|
|
134
|
+
}
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
function catsForTab(key) {
|
|
138
|
+
return (_homeCatalogs || []).filter(function (c) { return tabOf(c) === key; });
|
|
63
139
|
}
|
|
64
140
|
var _homeCatalogs = null; // [{name,type,metas}] — fetched once per session, filtered per tab
|
|
65
141
|
var _homeTab = null; // active tab key
|
|
@@ -89,8 +165,11 @@
|
|
|
89
165
|
// url: the addon ('local:iptv' for IPTV); none: nothing to fetch (only the set / order
|
|
90
166
|
// of rows changed). Calls before the reload runs add up.
|
|
91
167
|
var _reloadOnly = null; // { addon url: 1 } for the next reload, else a full one
|
|
168
|
+
var _homeAddons = {}; // { addon url: 1 } the addons Home's last load had catalogs from
|
|
92
169
|
function refreshAddonRows(url) {
|
|
93
170
|
if (_homeCatalogs === null) { clearCatalogCache(); return; } // nothing loaded yet: a full load anyway
|
|
171
|
+
// an addon Home didn't load rows from (the first playlist added: IPTV): a full load, its tab with it
|
|
172
|
+
if (url && !_homeAddons[url]) { clearCatalogCache(); return; }
|
|
94
173
|
_reloadOnly = _reloadOnly || {};
|
|
95
174
|
if (url) { _reloadOnly[url] = 1; }
|
|
96
175
|
_homeDirty = true;
|
|
@@ -139,17 +218,17 @@
|
|
|
139
218
|
if (bytes > _homeBytesMax) { return; }
|
|
140
219
|
var metas = c.metas.map(trimMeta);
|
|
141
220
|
metas.forEach(function (m) { bytes += sizeOf(m); });
|
|
142
|
-
if (bytes <= _homeBytesMax) { cats.push(
|
|
221
|
+
if (bytes <= _homeBytesMax) { cats.push(copyCat(c, metas)); }
|
|
143
222
|
});
|
|
144
223
|
if (U.bigStore && !_homeSaveFailed) {
|
|
145
224
|
U.bigStore.set(HOME_CACHE_KEY, { sig: sig, cats: cats }).then(function (ok) { if (ok === false) { _homeSaveFailed = true; } });
|
|
146
225
|
}
|
|
147
226
|
U.store.set(HOME_SNAP_KEY, { sig: sig, cats: cats.slice(0, SNAP_ROWS).map(function (c) {
|
|
148
|
-
return
|
|
227
|
+
return copyCat(c, c.metas.slice(0, SNAP_CARDS).map(function (m) {
|
|
149
228
|
var o = { id: m.id, type: m.type, name: m.name };
|
|
150
229
|
SNAP_FIELDS.forEach(function (k) { if (m[k] != null) { o[k] = m[k]; } });
|
|
151
230
|
return o;
|
|
152
|
-
})
|
|
231
|
+
}));
|
|
153
232
|
}) });
|
|
154
233
|
}
|
|
155
234
|
function useHomeCache() {
|
|
@@ -967,16 +1046,18 @@
|
|
|
967
1046
|
showModal(overlay, details || watched);
|
|
968
1047
|
}
|
|
969
1048
|
|
|
970
|
-
// Local progress merged with Trakt playback (when connected), deduped, newest first
|
|
1049
|
+
// Local progress merged with Trakt playback (when connected), deduped, newest first; a
|
|
1050
|
+
// place kept here that Trakt has you watching past since (another TV) left out.
|
|
971
1051
|
// One card per show: its furthest episode (highest season, then episode), placed by
|
|
972
1052
|
// the show's most recent activity. The others ride along as _dupes so Remove clears
|
|
973
1053
|
// the whole show.
|
|
974
1054
|
// byActivity: the most recent activity first, whatever order you gave the row (Move): the
|
|
975
1055
|
// home bar's tiles (previewTiles).
|
|
976
1056
|
function continueEntries(byActivity) {
|
|
977
|
-
var local = Player.continueList();
|
|
1057
|
+
var local = Player.continueList(), traktOn = typeof Trakt !== 'undefined' && Trakt.isConnected();
|
|
1058
|
+
if (traktOn && Trakt.watchedPast) { local = local.filter(function (e) { return !Trakt.watchedPast(e); }); }
|
|
978
1059
|
var merged = local.slice();
|
|
979
|
-
if (
|
|
1060
|
+
if (traktOn) {
|
|
980
1061
|
var seen = {};
|
|
981
1062
|
local.forEach(function (e) { seen[e.type + ':' + e.id] = 1; });
|
|
982
1063
|
Trakt.continueList().forEach(function (e) {
|
|
@@ -1139,7 +1220,7 @@
|
|
|
1139
1220
|
// Rows for the active tab, from the once-per-session catalog cache. Catalog tabs
|
|
1140
1221
|
// keep their built rows, so switching back doesn't rebuild every card; Continue
|
|
1141
1222
|
// Watching is small and changes after every playback, so it's always rebuilt.
|
|
1142
|
-
var rows = (_homeTab !== 'continue' && _homeTab !== 'local' && _homeTab !== 'calendar') ? _tabRows[_homeTab] : null;
|
|
1223
|
+
var rows = (_homeTab !== 'continue' && _homeTab !== 'local' && _homeTab !== 'calendar' && _homeTab !== 'guide') ? _tabRows[_homeTab] : null;
|
|
1143
1224
|
if (_homeTab !== 'calendar' && _calCancel) { _calCancel.cancel(); _calCancel = null; } // (its titles' details still coming: not wanted)
|
|
1144
1225
|
if (rows) {
|
|
1145
1226
|
s.appendChild(rows);
|
|
@@ -1151,6 +1232,8 @@
|
|
|
1151
1232
|
localRows(rows);
|
|
1152
1233
|
} else if (_homeTab === 'calendar') {
|
|
1153
1234
|
calendarRows(rows);
|
|
1235
|
+
} else if (_homeTab === 'guide') {
|
|
1236
|
+
V.guideGrid(rows);
|
|
1154
1237
|
} else if (_homeTab === 'continue') {
|
|
1155
1238
|
var entries = continueEntries();
|
|
1156
1239
|
if (entries.length) {
|
|
@@ -1183,12 +1266,15 @@
|
|
|
1183
1266
|
var again = keep ? findCard(s, keep) : null;
|
|
1184
1267
|
var landed = false;
|
|
1185
1268
|
var sameItem = (!firstCard && inMenu) ? menuItem(s, cur.textContent) : null; // the menu item focus was on
|
|
1186
|
-
|
|
1269
|
+
var rowsEl = s.querySelector('.rows'), fc = Focus.current();
|
|
1270
|
+
if (_homeTab === 'guide' && fc && rowsEl && rowsEl.contains(fc)) { landed = true; } // (the Guide drawn from what it had put focus back on its program)
|
|
1271
|
+
else if (again || sameItem) { Focus.setScope(s, again || sameItem); } else { landed = focusHome(s, !firstCard && inMenu); }
|
|
1187
1272
|
if (_homeCatalogs === null || homeStale() || _reloadOnly) { loadHomeCatalogs(); }
|
|
1188
1273
|
// No title to focus yet: try again when the rows arrive (Continue Watching and a
|
|
1189
1274
|
// tab with no catalogs have nothing coming, so focus simply stays on the pill).
|
|
1190
|
-
_focusFirstCard = (firstCard && !landed && _homeLoading && _homeTab !== 'continue' && _homeTab !== 'calendar') ? 'pending' : false;
|
|
1275
|
+
_focusFirstCard = (firstCard && !landed && _homeLoading && _homeTab !== 'continue' && _homeTab !== 'calendar' && _homeTab !== 'guide') ? 'pending' : false;
|
|
1191
1276
|
_renderedSig = tabSig();
|
|
1277
|
+
_renderedMenu = menuSig();
|
|
1192
1278
|
if (s.querySelector('.rows .card')) { U.mark('firstCard'); }
|
|
1193
1279
|
// the strip describes the focused title only (empty while focus is in the menu)
|
|
1194
1280
|
clearTimeout(_homeInfoTimer); _homeInfoTimer = null;
|
|
@@ -1203,6 +1289,7 @@
|
|
|
1203
1289
|
function fillInfoBox(box, m) {
|
|
1204
1290
|
if (!box) { return; }
|
|
1205
1291
|
m = m || {};
|
|
1292
|
+
if (!m.logo && /^iptv:/.test(m.id || '') && typeof EPG !== 'undefined' && EPG.logo && EPG.logo(m.id)) { m = Object.assign({}, m, { logo: EPG.logo(m.id) }); } // (a channel: its guide's logo)
|
|
1206
1293
|
var bits = [];
|
|
1207
1294
|
if (m.releaseInfo) { bits.push(m.releaseInfo); } else if (m.year) { bits.push('' + m.year); }
|
|
1208
1295
|
if (m.runtime) { bits.push(m.runtime); }
|
|
@@ -1220,8 +1307,34 @@
|
|
|
1220
1307
|
} else {
|
|
1221
1308
|
t.textContent = name;
|
|
1222
1309
|
}
|
|
1310
|
+
var desc = m.description || '';
|
|
1311
|
+
var g = /^iptv:/.test(m.id || '') && typeof EPG !== 'undefined' ? EPG.now(m.id) : null; // a channel: what's on, from its TV guide
|
|
1312
|
+
if (g && g.now) {
|
|
1313
|
+
bits = ['Now ' + EPG.hhmm(g.now.start) + '–' + EPG.hhmm(g.now.stop) + ' ' + g.now.title];
|
|
1314
|
+
desc = (g.now.desc ? g.now.desc + ' ' : '') + (g.next ? 'Next ' + EPG.hhmm(g.next.start) + ' ' + g.next.title : '');
|
|
1315
|
+
}
|
|
1223
1316
|
box.children[1].textContent = bits.join(' · ');
|
|
1224
|
-
box.children[2].textContent =
|
|
1317
|
+
box.children[2].textContent = desc;
|
|
1318
|
+
// a program's picture from the TV guide (the Guide tab's focused one, a channel's now)
|
|
1319
|
+
var picSrc = m.picture || (g && g.now && g.now.picture) || '', pic = box.querySelector('.home-info-pic');
|
|
1320
|
+
if (!picSrc) { if (pic) { pic.className = 'home-info-pic hidden'; } box.classList.remove('has-pic'); return; }
|
|
1321
|
+
if (!pic) { pic = U.el('img', 'home-info-pic'); pic.onerror = function () { pic.className = 'home-info-pic hidden'; box.classList.remove('has-pic'); }; box.appendChild(pic); }
|
|
1322
|
+
pic.className = 'home-info-pic';
|
|
1323
|
+
box.classList.add('has-pic');
|
|
1324
|
+
if (pic.__src !== picSrc) { pic.__src = picSrc; U.deferImage(pic, U.cardImage(picSrc, 'still'), true); }
|
|
1325
|
+
}
|
|
1326
|
+
if (typeof EPG !== 'undefined') { // (the focused channel's: drawn again when its guide arrives)
|
|
1327
|
+
var _hadGuide = false;
|
|
1328
|
+
EPG.onChange(function () {
|
|
1329
|
+
var n = Focus.current();
|
|
1330
|
+
if (n && n.__meta && /^iptv:/.test(n.__meta.id || '') && scr('home').contains(n)) { fillHomeInfo(n.__meta); }
|
|
1331
|
+
// a guide read (or gone): the Guide tab comes (or goes) in the menu, at once
|
|
1332
|
+
var has = hasGuide();
|
|
1333
|
+
if (has !== _hadGuide) {
|
|
1334
|
+
_hadGuide = has;
|
|
1335
|
+
if (_current() === scr('home') && !isModalOpen() && !Player.isActive()) { renderHome(); }
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1225
1338
|
}
|
|
1226
1339
|
Focus.onFocus(function (node) {
|
|
1227
1340
|
if (!node || !scr('home').contains(node)) { return; }
|
|
@@ -1252,12 +1365,18 @@
|
|
|
1252
1365
|
// Everything the tab on screen shows from the catalogs (which titles, in which rows),
|
|
1253
1366
|
// to tell whether a refresh changed it. Continue Watching shows none of it.
|
|
1254
1367
|
function shownSig() {
|
|
1255
|
-
if (_homeTab === 'continue' || _homeTab === 'calendar') { return _homeTab; }
|
|
1368
|
+
if (_homeTab === 'continue' || _homeTab === 'calendar' || _homeTab === 'guide') { return _homeTab; }
|
|
1256
1369
|
return _homeTab + '#' + catsForTab(_homeTab).map(function (c) {
|
|
1257
1370
|
return c.name + ':' + c.metas.map(function (m) { return m.id + '/' + (m.poster || ''); }).join(',');
|
|
1258
1371
|
}).join('|');
|
|
1259
1372
|
}
|
|
1260
|
-
var _renderedSig = '';
|
|
1373
|
+
var _renderedSig = '', _renderedMenu = '';
|
|
1374
|
+
// The tabs that aren't drawn from catalogs (Watching, Calendar, Guide, Local): catalogs that
|
|
1375
|
+
// arrive don't change them, only the top menu might (a tab coming). Redrawing one rebuilds it
|
|
1376
|
+
// whole (the Guide asks for its programs again): a flicker, so not unless the menu changed.
|
|
1377
|
+
function ownTab(t) { return t === 'continue' || t === 'calendar' || t === 'guide' || t === 'local'; }
|
|
1378
|
+
function menuSig() { return availableTabs().map(function (t) { return t.key; }).join(','); }
|
|
1379
|
+
function ownTabStill() { return ownTab(_homeTab) && menuSig() === _renderedMenu; }
|
|
1261
1380
|
function tabSig() {
|
|
1262
1381
|
return _homeTab + '#' + catsForTab(_homeTab).map(function (c) { return c.name + ':' + c.metas.length; }).join('|');
|
|
1263
1382
|
}
|
|
@@ -1272,6 +1391,8 @@
|
|
|
1272
1391
|
if (typeof IPTV !== 'undefined' && IPTV.onProgress) {
|
|
1273
1392
|
IPTV.onProgress(function (n) { if (_homeFirstLoad) { U.spinner(true, channelsText(n)); } });
|
|
1274
1393
|
}
|
|
1394
|
+
// The IPTV playlists read after your addons loaded (they aren't waited for): their rows join.
|
|
1395
|
+
if (typeof Stremio !== 'undefined' && Stremio.onIptvReady) { Stremio.onIptvReady(function () { refreshAddonRows('local:iptv'); }); }
|
|
1275
1396
|
var PARTIAL_DELAY = 150; // coalesces catalogs that arrive together into one redraw
|
|
1276
1397
|
function loadHomeCatalogs() {
|
|
1277
1398
|
if (_homeLoading) { return; }
|
|
@@ -1295,23 +1416,28 @@
|
|
|
1295
1416
|
_homeLoading = false;
|
|
1296
1417
|
_homeFirstLoad = false;
|
|
1297
1418
|
if (first) { U.spinner(false); }
|
|
1419
|
+
if (_reloadOnly && onHome()) { setTimeout(loadHomeCatalogs, 0); } // (rows asked for meanwhile: the IPTV ones, read after the addons)
|
|
1298
1420
|
}
|
|
1299
1421
|
function onHome() { return _current() === s && !isModalOpen() && !Player.isActive(); }
|
|
1300
1422
|
Stremio.catalogsForHome().then(function (cats) {
|
|
1301
|
-
|
|
1423
|
+
_homeAddons = {};
|
|
1424
|
+
cats.forEach(function (cat) { if (cat.addon && cat.addon.url) { _homeAddons[cat.addon.url] = 1; } });
|
|
1425
|
+
var acc = cats.map(function (cat) { return copyCat({ name: cat.name, type: cat.type, yt: isYouTubeCat(cat) }, []); });
|
|
1302
1426
|
var arrived = cats.map(function () { return false; }), shown = 0, partialTimer = null;
|
|
1303
|
-
// First load: publish
|
|
1304
|
-
// when
|
|
1427
|
+
// First load: publish each row as soon as it arrives, in its own place (a slower one above
|
|
1428
|
+
// it held by a "Loading…" placeholder, so nothing jumps when it comes), and redraw when
|
|
1429
|
+
// that changes the tab on screen.
|
|
1305
1430
|
function partial() {
|
|
1306
|
-
var n =
|
|
1307
|
-
while (n < arrived.length && arrived[n]) { n++; }
|
|
1431
|
+
var n = arrived.filter(Boolean).length, last = arrived.lastIndexOf(true);
|
|
1308
1432
|
if (!first || n <= shown) { return; }
|
|
1309
1433
|
shown = n;
|
|
1310
|
-
|
|
1434
|
+
var now = acc.slice(0, last + 1).map(function (c, i) { return arrived[i] ? c : copyCat(c, [], { loading: true }); });
|
|
1435
|
+
_homeCatalogs = splitOtherCatalogs(now).filter(function (c) { return c.loading || (c.metas && c.metas.length); });
|
|
1311
1436
|
if (partialTimer) { return; }
|
|
1312
1437
|
partialTimer = setTimeout(function () {
|
|
1313
1438
|
partialTimer = null;
|
|
1314
1439
|
if (!_homeFirstLoad) { return; } // everything arrived meanwhile: drawn below
|
|
1440
|
+
if (ownTabStill()) { _tabRows = {}; return; } // (Watching, the Guide…: as they are)
|
|
1315
1441
|
var keep = {};
|
|
1316
1442
|
if (tabSig() === _renderedSig) { keep[_homeTab] = _tabRows[_homeTab]; }
|
|
1317
1443
|
_tabRows = keep; // other tabs rebuild from the new rows when opened
|
|
@@ -1336,7 +1462,7 @@
|
|
|
1336
1462
|
// The tab on screen is redrawn only if what it shows changed: redrawing rebuilds
|
|
1337
1463
|
// every card (and reloads its artwork), which shows as a flicker. Continue
|
|
1338
1464
|
// Watching doesn't come from catalogs at all. Other tabs rebuild when opened.
|
|
1339
|
-
var same = before !== null && shownSig() === before;
|
|
1465
|
+
var same = (before !== null && shownSig() === before) || ownTabStill();
|
|
1340
1466
|
var keep = {};
|
|
1341
1467
|
if (same && _tabRows[_homeTab]) { keep[_homeTab] = _tabRows[_homeTab]; }
|
|
1342
1468
|
_tabRows = keep;
|
|
@@ -1360,7 +1486,8 @@
|
|
|
1360
1486
|
function splitOtherCatalogs(list) {
|
|
1361
1487
|
var out = [];
|
|
1362
1488
|
list.forEach(function (c) {
|
|
1363
|
-
if (c.type !== 'other') { out.push(
|
|
1489
|
+
if (c.loading) { if (c.type !== 'other' || c.yt) { out.push(copyCat(c, [], { loading: true })); } return; } // (a placeholder: still loading)
|
|
1490
|
+
if (c.type !== 'other' || c.yt) { out.push(copyCat(c, c.metas.slice(0, ROW_CARDS))); return; } // (a YouTube one as it is, whatever its type)
|
|
1364
1491
|
var movies = [], series = [];
|
|
1365
1492
|
c.metas.forEach(function (m) { (SERIES_NAME.test(m.name || '') ? series : movies).push(m); });
|
|
1366
1493
|
out.push({ name: otherRowName('Movies', c.name), type: 'movie', metas: movies.slice(0, ROW_CARDS) });
|
|
@@ -1379,6 +1506,7 @@
|
|
|
1379
1506
|
for (var i = from; i < to; i++) {
|
|
1380
1507
|
var c = cats[i];
|
|
1381
1508
|
var row = buildRow(c.name, c.metas.map(function (m) { return posterCard(m, c.type); }));
|
|
1509
|
+
if (c.loading) { row.className += ' row-loading'; row.appendChild(U.el('div', 'row-empty', 'Loading…')); } // (its place kept: nothing to focus)
|
|
1382
1510
|
rows.appendChild(row);
|
|
1383
1511
|
built.push(row);
|
|
1384
1512
|
}
|
|
@@ -1418,6 +1546,10 @@
|
|
|
1418
1546
|
// ----- published for the other parts (and js/views.js) -----
|
|
1419
1547
|
V.fillInfoBox = fillInfoBox;
|
|
1420
1548
|
V.availableTabs = availableTabs;
|
|
1549
|
+
V.menuOrder = menuOrder;
|
|
1550
|
+
V.moveMenuItem = moveMenuItem;
|
|
1551
|
+
V.menuItems = menuItems;
|
|
1552
|
+
V.MENU_ITEMS = MENU_ITEMS;
|
|
1421
1553
|
V.HOME_TABS = HOME_TABS;
|
|
1422
1554
|
V.buildRow = buildRow;
|
|
1423
1555
|
V.clearCatalogCache = clearCatalogCache;
|