stremiok 2.1.0 → 2.2.1
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 +97 -0
- package/README.md +10 -9
- package/config.xml +1 -1
- package/css/style.css +27 -0
- package/index.html +34 -31
- package/js/app.js +25 -10
- package/js/epg.js +320 -0
- package/js/iptv.js +198 -25
- package/js/keys.js +2 -0
- package/js/player-controls.js +50 -8
- package/js/player-core.js +112 -12
- package/js/player-lang.js +66 -5
- package/js/player-store.js +23 -2
- package/js/player-subs.js +57 -13
- package/js/stremio.js +70 -6
- package/js/trakt.js +1 -0
- package/js/util.js +24 -13
- package/js/view-core.js +128 -55
- package/js/view-detail.js +1 -1
- package/js/view-guide.js +371 -0
- package/js/view-home.js +148 -33
- package/js/view-settings.js +241 -90
- package/js/views.js +4 -3
- package/js/ytplay.js +53 -5
- package/package.json +2 -1
- package/service/index.js +863 -46
package/js/view-home.js
CHANGED
|
@@ -26,49 +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' },
|
|
31
32
|
{ key: 'youtube', icon: '📹', label: 'YouTube' }, // (StremioK's YouTube catalogs, an addon's YouTube / channel ones: see isYouTubeCat)
|
|
32
33
|
{ key: 'local', icon: '💾', label: 'Local' } // (a USB drive plugged in: see LocalMedia)
|
|
33
34
|
];
|
|
34
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
|
+
}
|
|
35
72
|
|
|
36
73
|
// Which tabs to show in the top menu (needs the home catalogs; falls back to the
|
|
37
74
|
// standard set before they've loaded so the menu is consistent everywhere).
|
|
38
|
-
// Tabs you hid (Settings › Appearance ›
|
|
75
|
+
// Tabs you hid (Settings › Appearance › Menu) aren't shown; Watching always is.
|
|
39
76
|
function availableTabs() {
|
|
40
77
|
var hidden = Stremio.getPrefs().hiddenTabs || [];
|
|
41
|
-
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())); }
|
|
42
79
|
if (_homeCatalogs === null || _homeFirstLoad) { // first load still running
|
|
43
80
|
return HOME_TABS.filter(function (t) { return shown(t) && (t.key !== 'local' || hasLocal()); });
|
|
44
81
|
}
|
|
45
82
|
return HOME_TABS.filter(function (t) {
|
|
46
|
-
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));
|
|
47
84
|
});
|
|
48
85
|
}
|
|
49
86
|
|
|
50
87
|
// YouTube's tab: StremioK's own YouTube catalogs (whatever type you gave one: "Podcasts"),
|
|
51
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)
|
|
52
91
|
function isYouTubeCat(c) {
|
|
53
92
|
var m = c.addon && c.addon.manifest;
|
|
54
|
-
return !!(m && m.id === 'org.stremiok.youtube') || /^(youtube|channel)$/i.test('' + c.type);
|
|
93
|
+
return !!c.yt || !!(m && m.id === 'org.stremiok.youtube') || /^(youtube|channel)$/i.test('' + c.type);
|
|
55
94
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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++; }
|
|
71
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; });
|
|
72
139
|
}
|
|
73
140
|
var _homeCatalogs = null; // [{name,type,metas}] — fetched once per session, filtered per tab
|
|
74
141
|
var _homeTab = null; // active tab key
|
|
@@ -98,8 +165,11 @@
|
|
|
98
165
|
// url: the addon ('local:iptv' for IPTV); none: nothing to fetch (only the set / order
|
|
99
166
|
// of rows changed). Calls before the reload runs add up.
|
|
100
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
|
|
101
169
|
function refreshAddonRows(url) {
|
|
102
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; }
|
|
103
173
|
_reloadOnly = _reloadOnly || {};
|
|
104
174
|
if (url) { _reloadOnly[url] = 1; }
|
|
105
175
|
_homeDirty = true;
|
|
@@ -148,17 +218,17 @@
|
|
|
148
218
|
if (bytes > _homeBytesMax) { return; }
|
|
149
219
|
var metas = c.metas.map(trimMeta);
|
|
150
220
|
metas.forEach(function (m) { bytes += sizeOf(m); });
|
|
151
|
-
if (bytes <= _homeBytesMax) { cats.push(
|
|
221
|
+
if (bytes <= _homeBytesMax) { cats.push(copyCat(c, metas)); }
|
|
152
222
|
});
|
|
153
223
|
if (U.bigStore && !_homeSaveFailed) {
|
|
154
224
|
U.bigStore.set(HOME_CACHE_KEY, { sig: sig, cats: cats }).then(function (ok) { if (ok === false) { _homeSaveFailed = true; } });
|
|
155
225
|
}
|
|
156
226
|
U.store.set(HOME_SNAP_KEY, { sig: sig, cats: cats.slice(0, SNAP_ROWS).map(function (c) {
|
|
157
|
-
return
|
|
227
|
+
return copyCat(c, c.metas.slice(0, SNAP_CARDS).map(function (m) {
|
|
158
228
|
var o = { id: m.id, type: m.type, name: m.name };
|
|
159
229
|
SNAP_FIELDS.forEach(function (k) { if (m[k] != null) { o[k] = m[k]; } });
|
|
160
230
|
return o;
|
|
161
|
-
})
|
|
231
|
+
}));
|
|
162
232
|
}) });
|
|
163
233
|
}
|
|
164
234
|
function useHomeCache() {
|
|
@@ -1150,7 +1220,7 @@
|
|
|
1150
1220
|
// Rows for the active tab, from the once-per-session catalog cache. Catalog tabs
|
|
1151
1221
|
// keep their built rows, so switching back doesn't rebuild every card; Continue
|
|
1152
1222
|
// Watching is small and changes after every playback, so it's always rebuilt.
|
|
1153
|
-
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;
|
|
1154
1224
|
if (_homeTab !== 'calendar' && _calCancel) { _calCancel.cancel(); _calCancel = null; } // (its titles' details still coming: not wanted)
|
|
1155
1225
|
if (rows) {
|
|
1156
1226
|
s.appendChild(rows);
|
|
@@ -1162,6 +1232,8 @@
|
|
|
1162
1232
|
localRows(rows);
|
|
1163
1233
|
} else if (_homeTab === 'calendar') {
|
|
1164
1234
|
calendarRows(rows);
|
|
1235
|
+
} else if (_homeTab === 'guide') {
|
|
1236
|
+
V.guideGrid(rows);
|
|
1165
1237
|
} else if (_homeTab === 'continue') {
|
|
1166
1238
|
var entries = continueEntries();
|
|
1167
1239
|
if (entries.length) {
|
|
@@ -1194,12 +1266,15 @@
|
|
|
1194
1266
|
var again = keep ? findCard(s, keep) : null;
|
|
1195
1267
|
var landed = false;
|
|
1196
1268
|
var sameItem = (!firstCard && inMenu) ? menuItem(s, cur.textContent) : null; // the menu item focus was on
|
|
1197
|
-
|
|
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); }
|
|
1198
1272
|
if (_homeCatalogs === null || homeStale() || _reloadOnly) { loadHomeCatalogs(); }
|
|
1199
1273
|
// No title to focus yet: try again when the rows arrive (Continue Watching and a
|
|
1200
1274
|
// tab with no catalogs have nothing coming, so focus simply stays on the pill).
|
|
1201
|
-
_focusFirstCard = (firstCard && !landed && _homeLoading && _homeTab !== 'continue' && _homeTab !== 'calendar') ? 'pending' : false;
|
|
1275
|
+
_focusFirstCard = (firstCard && !landed && _homeLoading && _homeTab !== 'continue' && _homeTab !== 'calendar' && _homeTab !== 'guide') ? 'pending' : false;
|
|
1202
1276
|
_renderedSig = tabSig();
|
|
1277
|
+
_renderedMenu = menuSig();
|
|
1203
1278
|
if (s.querySelector('.rows .card')) { U.mark('firstCard'); }
|
|
1204
1279
|
// the strip describes the focused title only (empty while focus is in the menu)
|
|
1205
1280
|
clearTimeout(_homeInfoTimer); _homeInfoTimer = null;
|
|
@@ -1214,6 +1289,7 @@
|
|
|
1214
1289
|
function fillInfoBox(box, m) {
|
|
1215
1290
|
if (!box) { return; }
|
|
1216
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)
|
|
1217
1293
|
var bits = [];
|
|
1218
1294
|
if (m.releaseInfo) { bits.push(m.releaseInfo); } else if (m.year) { bits.push('' + m.year); }
|
|
1219
1295
|
if (m.runtime) { bits.push(m.runtime); }
|
|
@@ -1231,8 +1307,34 @@
|
|
|
1231
1307
|
} else {
|
|
1232
1308
|
t.textContent = name;
|
|
1233
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
|
+
}
|
|
1234
1316
|
box.children[1].textContent = bits.join(' · ');
|
|
1235
|
-
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
|
+
});
|
|
1236
1338
|
}
|
|
1237
1339
|
Focus.onFocus(function (node) {
|
|
1238
1340
|
if (!node || !scr('home').contains(node)) { return; }
|
|
@@ -1263,12 +1365,18 @@
|
|
|
1263
1365
|
// Everything the tab on screen shows from the catalogs (which titles, in which rows),
|
|
1264
1366
|
// to tell whether a refresh changed it. Continue Watching shows none of it.
|
|
1265
1367
|
function shownSig() {
|
|
1266
|
-
if (_homeTab === 'continue' || _homeTab === 'calendar') { return _homeTab; }
|
|
1368
|
+
if (_homeTab === 'continue' || _homeTab === 'calendar' || _homeTab === 'guide') { return _homeTab; }
|
|
1267
1369
|
return _homeTab + '#' + catsForTab(_homeTab).map(function (c) {
|
|
1268
1370
|
return c.name + ':' + c.metas.map(function (m) { return m.id + '/' + (m.poster || ''); }).join(',');
|
|
1269
1371
|
}).join('|');
|
|
1270
1372
|
}
|
|
1271
|
-
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; }
|
|
1272
1380
|
function tabSig() {
|
|
1273
1381
|
return _homeTab + '#' + catsForTab(_homeTab).map(function (c) { return c.name + ':' + c.metas.length; }).join('|');
|
|
1274
1382
|
}
|
|
@@ -1312,7 +1420,9 @@
|
|
|
1312
1420
|
}
|
|
1313
1421
|
function onHome() { return _current() === s && !isModalOpen() && !Player.isActive(); }
|
|
1314
1422
|
Stremio.catalogsForHome().then(function (cats) {
|
|
1315
|
-
|
|
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) }, []); });
|
|
1316
1426
|
var arrived = cats.map(function () { return false; }), shown = 0, partialTimer = null;
|
|
1317
1427
|
// First load: publish each row as soon as it arrives, in its own place (a slower one above
|
|
1318
1428
|
// it held by a "Loading…" placeholder, so nothing jumps when it comes), and redraw when
|
|
@@ -1321,12 +1431,13 @@
|
|
|
1321
1431
|
var n = arrived.filter(Boolean).length, last = arrived.lastIndexOf(true);
|
|
1322
1432
|
if (!first || n <= shown) { return; }
|
|
1323
1433
|
shown = n;
|
|
1324
|
-
var now = acc.slice(0, last + 1).map(function (c, i) { return arrived[i] ? c :
|
|
1434
|
+
var now = acc.slice(0, last + 1).map(function (c, i) { return arrived[i] ? c : copyCat(c, [], { loading: true }); });
|
|
1325
1435
|
_homeCatalogs = splitOtherCatalogs(now).filter(function (c) { return c.loading || (c.metas && c.metas.length); });
|
|
1326
1436
|
if (partialTimer) { return; }
|
|
1327
1437
|
partialTimer = setTimeout(function () {
|
|
1328
1438
|
partialTimer = null;
|
|
1329
1439
|
if (!_homeFirstLoad) { return; } // everything arrived meanwhile: drawn below
|
|
1440
|
+
if (ownTabStill()) { _tabRows = {}; return; } // (Watching, the Guide…: as they are)
|
|
1330
1441
|
var keep = {};
|
|
1331
1442
|
if (tabSig() === _renderedSig) { keep[_homeTab] = _tabRows[_homeTab]; }
|
|
1332
1443
|
_tabRows = keep; // other tabs rebuild from the new rows when opened
|
|
@@ -1351,7 +1462,7 @@
|
|
|
1351
1462
|
// The tab on screen is redrawn only if what it shows changed: redrawing rebuilds
|
|
1352
1463
|
// every card (and reloads its artwork), which shows as a flicker. Continue
|
|
1353
1464
|
// Watching doesn't come from catalogs at all. Other tabs rebuild when opened.
|
|
1354
|
-
var same = before !== null && shownSig() === before;
|
|
1465
|
+
var same = (before !== null && shownSig() === before) || ownTabStill();
|
|
1355
1466
|
var keep = {};
|
|
1356
1467
|
if (same && _tabRows[_homeTab]) { keep[_homeTab] = _tabRows[_homeTab]; }
|
|
1357
1468
|
_tabRows = keep;
|
|
@@ -1375,8 +1486,8 @@
|
|
|
1375
1486
|
function splitOtherCatalogs(list) {
|
|
1376
1487
|
var out = [];
|
|
1377
1488
|
list.forEach(function (c) {
|
|
1378
|
-
if (c.loading) { if (c.type !== 'other') { out.push(
|
|
1379
|
-
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)
|
|
1380
1491
|
var movies = [], series = [];
|
|
1381
1492
|
c.metas.forEach(function (m) { (SERIES_NAME.test(m.name || '') ? series : movies).push(m); });
|
|
1382
1493
|
out.push({ name: otherRowName('Movies', c.name), type: 'movie', metas: movies.slice(0, ROW_CARDS) });
|
|
@@ -1435,6 +1546,10 @@
|
|
|
1435
1546
|
// ----- published for the other parts (and js/views.js) -----
|
|
1436
1547
|
V.fillInfoBox = fillInfoBox;
|
|
1437
1548
|
V.availableTabs = availableTabs;
|
|
1549
|
+
V.menuOrder = menuOrder;
|
|
1550
|
+
V.moveMenuItem = moveMenuItem;
|
|
1551
|
+
V.menuItems = menuItems;
|
|
1552
|
+
V.MENU_ITEMS = MENU_ITEMS;
|
|
1438
1553
|
V.HOME_TABS = HOME_TABS;
|
|
1439
1554
|
V.buildRow = buildRow;
|
|
1440
1555
|
V.clearCatalogCache = clearCatalogCache;
|