stremiok 2.1.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/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 › Home tabs) aren't shown; Watching always is.
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
- // Which cached catalogs belong to a tab. Sports, radio and YouTube have dedicated tabs;
57
- // IPTV is the catch-all for every other live source (tv, …).
58
- function catsForTab(key) {
59
- return (_homeCatalogs || []).filter(function (c) {
60
- if (key === 'youtube') { return isYouTubeCat(c); }
61
- if (isYouTubeCat(c)) { return false; }
62
- if (key === 'movie') { return c.type === 'movie'; }
63
- if (key === 'series') { return c.type === 'series'; }
64
- if (key === 'sport') { return c.type === 'sport'; }
65
- if (key === 'radio') { return c.type === 'radio'; }
66
- if (key === 'tv') {
67
- return c.type !== 'movie' && c.type !== 'series' &&
68
- c.type !== 'sport' && c.type !== 'radio';
69
- }
70
- return false;
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({ name: c.name, type: c.type, metas: metas }); }
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 { name: c.name, type: c.type, metas: c.metas.slice(0, SNAP_CARDS).map(function (m) {
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
- if (again || sameItem) { Focus.setScope(s, again || sameItem); } else { landed = focusHome(s, !firstCard && inMenu); }
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 = m.description || '';
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
- var acc = cats.map(function (cat) { return { name: cat.name, type: cat.type, metas: [] }; });
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 : { name: c.name, type: c.type, metas: [], loading: true }; });
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({ name: c.name, type: c.type, metas: [], loading: true }); } return; } // (a placeholder: still loading)
1379
- if (c.type !== 'other') { out.push({ name: c.name, type: c.type, metas: c.metas.slice(0, ROW_CARDS) }); return; }
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;
@@ -17,7 +17,6 @@
17
17
  function markHomeDirty() { return V.markHomeDirty.apply(null, arguments); } // home
18
18
  function onModalClose() { return V.onModalClose.apply(null, arguments); } // core
19
19
  function pickList() { return V.pickList.apply(null, arguments); } // core
20
- function toggleList() { return V.toggleList.apply(null, arguments); } // core
21
20
  function playlistActions() { return V.playlistActions.apply(null, arguments); } // core
22
21
  function scr() { return V.scr.apply(null, arguments); } // core
23
22
  function showModal() { return V.showModal.apply(null, arguments); } // core
@@ -207,14 +206,16 @@
207
206
  get: function () { return { spoilers: Stremio.getPrefs().spoilers }; },
208
207
  set: function (cfg) { Stremio.setPref('spoilers', cfg.spoilers); }
209
208
  }));
210
- wrap.appendChild(homeTabsRow());
209
+ var menu = sectionRow('Menu ›', 'Order the top menu, and show or hide its tabs', 'menuorder');
210
+ menu.className += ' cfg-row';
211
+ wrap.appendChild(menu);
211
212
  var feeds = sectionRow('Home feeds ›', 'Order the Home rows, and show or hide each one', 'feeds');
212
213
  feeds.className += ' cfg-row';
213
214
  wrap.appendChild(feeds);
214
215
  var sfeeds = sectionRow('Search results ›', 'Order the Search rows (one per catalog that can search), and show or hide each one', 'searchfeeds');
215
216
  sfeeds.className += ' cfg-row';
216
217
  wrap.appendChild(sfeeds);
217
- var back = _focusChild === 'feeds' ? feeds : _focusChild === 'searchfeeds' ? sfeeds : null;
218
+ var back = _focusChild === 'menuorder' ? menu : _focusChild === 'feeds' ? feeds : _focusChild === 'searchfeeds' ? sfeeds : null;
218
219
  _focusChild = null;
219
220
  if (back) { Focus.setScope(scr('settings'), back); } else { focusSettings(); }
220
221
  }
@@ -337,7 +338,7 @@
337
338
  // BACK goes up one level, onto the row it came from: from a section to its group (on
338
339
  // that section, not the first one), from Home feeds / Search results to Appearance,
339
340
  // from a group to the Settings list (on that group).
340
- var SECTION_PARENT = { feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons' };
341
+ var SECTION_PARENT = { menuorder: 'appearance', feeds: 'appearance', searchfeeds: 'appearance', aiomanager: 'addons' };
341
342
  Object.keys(SETTINGS_GROUPS).forEach(function (g) {
342
343
  SETTINGS_GROUPS[g].rows.forEach(function (row) { SECTION_PARENT[row[2]] = g; });
343
344
  });
@@ -366,6 +367,7 @@
366
367
  else if (_settingsSection === 'subtitles') { settingsSubtitles(wrap); }
367
368
  else if (_settingsSection === 'skipping') { settingsSkipping(wrap); }
368
369
  else if (_settingsSection === 'seeking') { settingsSeeking(wrap); }
370
+ else if (_settingsSection === 'menuorder') { settingsFeeds(wrap, 'menu'); }
369
371
  else if (_settingsSection === 'feeds') { settingsFeeds(wrap, 'home'); }
370
372
  else if (_settingsSection === 'searchfeeds') { settingsFeeds(wrap, 'search'); }
371
373
  else if (_settingsSection === 'ranking') { settingsFeeds(wrap, 'ranking'); }
@@ -867,6 +869,15 @@
867
869
  var _iptvFocus = null; // { id, ctl } to focus once the page is drawn again
868
870
  function two(n) { return (n < 10 ? '0' : '') + n; }
869
871
  function thousands(n) { return ('' + n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
872
+ // The playlist rows IPTV Settings drew last: their guide lines kept current by one listener
873
+ // (one added each time it was drawn used to pile up, each holding its rows).
874
+ var _guideRows = [];
875
+ if (typeof EPG !== 'undefined' && EPG.onChange) {
876
+ EPG.onChange(function () {
877
+ _guideRows = _guideRows.filter(function (r) { return document.documentElement.contains(r.info); });
878
+ _guideRows.forEach(function (r) { r.info.textContent = playlistInfo(r.p); });
879
+ });
880
+ }
870
881
  function playlistInfo(p) {
871
882
  if (p.off) { return 'Off — its channels aren’t shown'; }
872
883
  var st = Stremio.iptvStatus(p.id), parts = [];
@@ -875,13 +886,65 @@
875
886
  if (st.fetchedAt) { var d = new Date(st.fetchedAt); parts.push('downloaded ' + U.dayLabel(st.fetchedAt) + ' ' + two(d.getHours()) + ':' + two(d.getMinutes())); }
876
887
  if (st.dropped) { parts.push(thousands(st.dropped) + ' more left out (a playlist keeps its first 30,000)'); }
877
888
  if (st.uncached) { parts.push('too large to save on the TV: downloaded each launch'); }
889
+ var g = guideInfo(p);
890
+ if (g) { parts.push(g); }
878
891
  return parts.join(' · ');
879
892
  }
893
+ // Its TV guide (js/epg.js): being read, read (for how many of its channels), or why not.
894
+ function guideInfo(p) {
895
+ var g = typeof EPG !== 'undefined' && EPG.status ? EPG.status(p.id) : null;
896
+ if (!g) { return typeof EPG !== 'undefined' && EPG.available && EPG.available() ? 'no TV guide' : ''; }
897
+ if (g.state === 'ready') {
898
+ var d = new Date(g.at);
899
+ return 'TV guide: ' + thousands(g.channels) + ' channel' + (g.channels === 1 ? '' : 's') + ', ' + thousands(g.programmes) + ' programs' + (g.partial ? ' (part of it: too large)' : '') +
900
+ (g.at ? ', read ' + U.dayLabel(g.at) + ' ' + two(d.getHours()) + ':' + two(d.getMinutes()) : '') + (g.error ? ' (the last read failed: ' + g.error + ')' : '');
901
+ }
902
+ if (g.state === 'failed') { return '⚠ TV guide: ' + (g.error || 'couldn’t be read'); }
903
+ return 'TV guide: reading…';
904
+ }
880
905
  function playlistProblem(p) {
881
906
  var st = Stremio.iptvStatus(p.id);
882
907
  if (p.off || !st.error) { return ''; }
883
908
  return '⚠ ' + (st.stale ? 'Couldn’t refresh it (showing the saved copy): ' : '') + st.error;
884
909
  }
910
+ // + Add Xtream account: its server, username and password, a page each (a whole Xtream link
911
+ // in the server's fills the rest); checked with the server, then added (Stremio.iptvAddXtream).
912
+ function addXtream() {
913
+ function done(base, user, pass) {
914
+ U.toast('Checking the account…', 15000);
915
+ IPTV.xtreamCheck(base, user, pass).then(function (r) {
916
+ if (r.unreachable) { U.toast('Couldn’t reach the server: check its address (nothing was saved)', 5000); return; }
917
+ if (r.refused) { U.toast('The server refused that username and password' + (r.status ? ' (' + r.status + ')' : '') + ': nothing was saved', 5000); return; }
918
+ Stremio.iptvAddXtream(base, user, pass);
919
+ refreshAddonRows('local:iptv');
920
+ U.toast('Xtream account added: reading its channels and TV guide…', 4000);
921
+ renderSettings();
922
+ });
923
+ }
924
+ App.openEntry({
925
+ title: 'Xtream account: server', phone: 'playlist',
926
+ hint: 'The server address your provider gave you (http://line.example.com:8080), or a whole Xtream link. RETURN cancels.',
927
+ value: 'http://', placeholder: 'http://line.example.com:8080',
928
+ validate: function (v) { return IPTV.xtreamParts(v) ? null : 'That isn’t a server address'; },
929
+ onConfirm: function (v) {
930
+ var x = IPTV.xtreamParts(v);
931
+ App.back();
932
+ if (x.user && x.pass) { done(x.base, x.user, x.pass); return; }
933
+ App.openEntry({
934
+ title: 'Xtream account: username', hint: 'Your account’s username. RETURN cancels.', value: x.user, placeholder: 'username',
935
+ validate: function (u) { return ('' + u).trim() ? null : 'Type the username'; },
936
+ onConfirm: function (u) {
937
+ App.back();
938
+ App.openEntry({
939
+ title: 'Xtream account: password', hint: 'Your account’s password (kept on the TV, in the playlist’s link). RETURN cancels.', value: '', placeholder: 'password', secret: true,
940
+ validate: function (p) { return ('' + p) ? null : 'Type the password'; },
941
+ onConfirm: function (p) { App.back(); done(x.base, ('' + u).trim(), '' + p); }
942
+ });
943
+ }
944
+ });
945
+ }
946
+ });
947
+ }
885
948
  function settingsIptv(wrap) {
886
949
  wrap.appendChild(U.el('h2', null, 'IPTV'));
887
950
  var playlists = Stremio.iptvGetPlaylists ? Stremio.iptvGetPlaylists() : [];
@@ -925,6 +988,7 @@
925
988
  rows.push({ p: playlist, info: info, problem: problem, idx: idx });
926
989
  });
927
990
 
991
+ _guideRows = rows; // (their guide lines kept current as guides are read: _guideRows)
928
992
  if (statusEl) {
929
993
  Stremio.iptvLoad().then(function () {
930
994
  var channels = 0, groups = 0, failed = 0, on = 0;
@@ -962,6 +1026,11 @@
962
1026
  });
963
1027
  };
964
1028
  wrap.appendChild(add);
1029
+ var xt = U.el('div', 'addon-item focusable add-row');
1030
+ xt.appendChild(U.el('div', 'a-name', '+ Add Xtream account'));
1031
+ xt.appendChild(U.el('div', 'a-url', 'Its server, username and password: its channels and TV guide added'));
1032
+ xt.__onselect = addXtream;
1033
+ wrap.appendChild(xt);
965
1034
 
966
1035
  wrap.appendChild(U.el('div', 'hint',
967
1036
  'Channels are grouped into IPTV and Radio rows on Home. ▲ / ▼ set the playlists\' order: a channel in two playlists comes from the first.'));
@@ -1386,34 +1455,6 @@
1386
1455
  focusSettings();
1387
1456
  }
1388
1457
 
1389
- // Home tabs: hide the ones you never use (Watching always stays: it's where Home opens). A
1390
- // row like the ones above it; OK opens the tabs, OK on one shows or hides it (the list
1391
- // stays open). A hidden tab's rows are back when it's shown again.
1392
- function homeTabsRow() {
1393
- function hiddenNow() { return Stremio.getPrefs().hiddenTabs || []; }
1394
- var r = U.el('div', 'addon-item focusable cfg-row'), name = U.el('div', 'a-name');
1395
- var tabs = (V.HOME_TABS || []).filter(function (t) { return t.key !== 'continue'; });
1396
- function draw() {
1397
- var hidden = hiddenNow().filter(function (k) { return tabs.some(function (t) { return t.key === k; }); });
1398
- name.textContent = 'Home tabs: ' + (hidden.length ? hidden.length + ' hidden' : 'All shown');
1399
- }
1400
- draw();
1401
- r.appendChild(name);
1402
- r.appendChild(U.el('div', 'a-url', 'Press OK to choose'));
1403
- r.__onselect = function () {
1404
- var hidden = hiddenNow();
1405
- toggleList('Home tabs', tabs.map(function (t) { return [t.key, t.label + (t.key === 'local' ? ' (with a USB drive in)' : ''), hidden.indexOf(t.key) === -1]; }),
1406
- { on: 'Shown', off: 'Hidden' }, function (key, on) {
1407
- var now = hiddenNow().filter(function (k) { return k !== key; });
1408
- if (!on) { now.push(key); }
1409
- Stremio.setPref('hiddenTabs', now);
1410
- markHomeDirty();
1411
- draw();
1412
- });
1413
- };
1414
- return r;
1415
- }
1416
-
1417
1458
  var POSTER_SHAPES = [['portrait', 'Portrait'], ['landscape', 'Landscape']];
1418
1459
  // ----- Diagnostics › Logs: the recent problems U.log kept (in memory, URLs masked), and
1419
1460
  // how long launches took -----
@@ -1630,14 +1671,17 @@
1630
1671
  if (!inf.entries.length) { line(dbOn ? 'Nothing in the TV’s database.' : 'The TV’s database isn’t used (turned off above).'); }
1631
1672
  line('Settings storage: ' + fmtBytes(inf.ls) + ' (progress, watched marks, settings…)' + (inf.lsTop.length ? '; the largest:' : ''));
1632
1673
  inf.lsTop.forEach(function (e) { line(' ' + fmtBytes(e.bytes) + ' ' + e.key); });
1674
+ if (inf.guides && inf.guides.count) { line('Saved TV guides (in StremioK’s folder): ' + fmtBytes(inf.guides.bytes) + ' in ' + inf.guides.count + (inf.guides.count === 1 ? ' file' : ' files')); }
1633
1675
  if (inf.disk) { line('The TV’s own disk: ' + fmtBytes(inf.disk.free) + ' free of ' + fmtBytes(inf.disk.total) + ' (what the TV allows StremioK follows it: freeing space, unused apps for one, raises it)'); }
1634
1676
  }
1635
1677
  r.__onselect = function () {
1636
1678
  if (!U.bigStore || !U.bigStore.info || _storageInfo === 'busy') { return; }
1637
1679
  _storageInfo = 'busy'; renderSettings();
1638
- Promise.all([U.bigStore.info(), U.diskInfo ? U.diskInfo() : null]).then(function (both) {
1680
+ var guides = (typeof YTPlay !== 'undefined' && YTPlay.epgFiles) ? YTPlay.epgFiles().then(null, function () { return null; }) : null;
1681
+ Promise.all([U.bigStore.info(), U.diskInfo ? U.diskInfo() : null, guides]).then(function (both) {
1639
1682
  var inf = both[0], ls = 0, lsAll = [];
1640
1683
  inf.disk = both[1];
1684
+ inf.guides = both[2]; // ({ count, bytes }: the TV guides StremioK's service saved)
1641
1685
  inf.lsKeys = {};
1642
1686
  try {
1643
1687
  for (var i = 0; i < window.localStorage.length; i++) {
@@ -1669,12 +1713,15 @@
1669
1713
  }
1670
1714
  var clr = U.el('div', 'addon-item focusable');
1671
1715
  clr.appendChild(U.el('div', 'a-name', 'Clear saved copies'));
1672
- clr.appendChild(U.el('div', 'a-url', 'Home’s saved rows, IPTV playlist copies, the USB index: made again when needed (playlists download again)'));
1716
+ clr.appendChild(U.el('div', 'a-url', 'Home’s saved rows, IPTV playlist copies, saved TV guides, the USB index: made again when needed (playlists and guides download again)'));
1673
1717
  clr.__onselect = function () {
1674
- confirm('Clear the copies StremioK can make again (Home rows, playlist copies, USB index)?', 'Clear', function () {
1718
+ confirm('Clear the copies StremioK can make again (Home rows, playlist copies, TV guides, USB index)?', 'Clear', function () {
1675
1719
  U.bigStore.keys().then(function (ks) {
1676
1720
  return Promise.all(ks.filter(function (k) { return /^stremiok\.(homecache|local\.index|iptv\.cache\.)/.test(k); }).map(function (k) { return U.bigStore.remove(k); }));
1677
1721
  }).then(function () {
1722
+ return (typeof YTPlay !== 'undefined' && YTPlay.epgClear) ? YTPlay.epgClear().then(null, function () {}) : null; // (the saved guides: read again as they're needed)
1723
+ }).then(function () {
1724
+ if (typeof EPG !== 'undefined' && EPG.reset) { EPG.reset(); }
1678
1725
  if (typeof LocalMedia !== 'undefined') { LocalMedia.clearIndex(); }
1679
1726
  _storageInfo = null;
1680
1727
  U.toast('Saved copies cleared');
@@ -1846,7 +1893,28 @@
1846
1893
  // ----- Home feeds and Search results (Settings > Appearance): order, show / hide -----
1847
1894
  // kind 'home': the Home rows (every catalog); 'search': the Search rows (every catalog
1848
1895
  // that can search, IPTV as one per type). ▲/▼ moves one; OK on its name shows / hides it.
1896
+ // 'menu': the top menu between Watching and Settings (the tabs, Discover, Search: ViewParts'
1897
+ // menuOrder); OK on a tab shows / hides it, Discover and Search always show.
1898
+ var MENU_FIXED = { discover: 'Discover', search: 'Search' };
1899
+ function menuHidden(k) { return !MENU_FIXED[k] && (Stremio.getPrefs().hiddenTabs || []).indexOf(k) !== -1; }
1849
1900
  var FEED_KINDS = {
1901
+ menu: { section: 'menuorder', title: 'Appearance › Menu', hint: 'Watching comes first and Settings last: ⇈ ▲ ▼ ⇊ put the rest in your order · OK on a tab shows/hides it.',
1902
+ list: function () {
1903
+ return Promise.resolve(V.menuOrder().map(function (k) {
1904
+ var t = (V.HOME_TABS || []).filter(function (x) { return x.key === k; })[0];
1905
+ return { key: k, name: MENU_FIXED[k] || (t ? t.label : k) + (k === 'local' ? ' (with a USB drive in)' : k === 'guide' ? ' (once a TV guide is read)' : '') };
1906
+ }));
1907
+ },
1908
+ keyOf: function (x) { return x.key; },
1909
+ move: function (k, d) { V.moveMenuItem(k, d); markHomeDirty(); return Promise.resolve(); },
1910
+ toggle: function (k) {
1911
+ if (MENU_FIXED[k]) { U.toast(MENU_FIXED[k] + ' is always in the menu'); return; }
1912
+ var now = (Stremio.getPrefs().hiddenTabs || []).filter(function (x) { return x !== k; });
1913
+ if (!menuHidden(k)) { now.push(k); }
1914
+ Stremio.setPref('hiddenTabs', now);
1915
+ markHomeDirty();
1916
+ },
1917
+ hidden: menuHidden },
1850
1918
  home: { section: 'feeds', title: 'Appearance › Home feeds', hint: '▲/▼ reorder a feed · OK on the name shows/hides it on Home.',
1851
1919
  list: function () { return Stremio.listCatalogsOrdered(); }, keyOf: function (c) { return Stremio.catalogKey(c); },
1852
1920
  move: function (k, d) { return Stremio.moveFeed(k, d); }, toggle: function (k) { Stremio.toggleFeed(k); },
package/js/views.js CHANGED
@@ -12,8 +12,9 @@ var Views = (function (V) {
12
12
  // pages, a search's rows) stopped
13
13
  leaveScreen: function (name) { var f = { detail: V.leaveDetail, discover: V.leaveDiscover, search: V.leaveSearch }[name]; if (f) { f(); } }, backInLocal: V.backInLocal, setHomeTab: V.setHomeTab,
14
14
  renderLive: V.renderLive,
15
- renderSearch: V.renderSearch, renderDiscover: V.renderDiscover, renderEntry: V.renderEntry, backInDetail: V.backInDetail,
15
+ renderSearch: V.renderSearch, searchFor: function (q) { V.searchFor(q); }, renderDiscover: V.renderDiscover, renderEntry: V.renderEntry, backInDetail: V.backInDetail,
16
16
  homeDirty: V.homeDirty, refreshHomeIfStale: V.refreshHomeIfStale, alertNewEpisodes: V.alertNewEpisodes, previewTiles: V.previewTiles, withShowInfo: V.withShowInfo,
17
- isModalOpen: V.isModalOpen, closeModal: V.closeModal, moveKey: V.moveKey, availableTabs: V.availableTabs
17
+ isModalOpen: V.isModalOpen, closeModal: V.closeModal, moveKey: V.moveKey, availableTabs: V.availableTabs,
18
+ menuItems: V.menuItems, menuOrder: V.menuOrder, moveMenuItem: V.moveMenuItem
18
19
  };
19
20
  })(ViewParts);