Files
NodecastV2/public/js/pages/Guide.js
Blomios e2249154a5 feat(live): paginate catalog and search server-side (#49)
Load Live channels from a filtered SQLite cursor API, keep navigation and guide consumers working across pages, and retain the QA reproduction coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-22 22:43:29 +02:00

82 lines
3.1 KiB
JavaScript

/**
* Guide Page Controller
*/
class GuidePage {
constructor(app) {
this.app = app;
}
async init() {
// EPG guide will lazy load when shown
}
async loadCatalog() {
const channelList = this.app.channelList;
const revision = API.liveCatalogRevision || 0;
if (channelList.guideCatalog && channelList.guideCatalogRevision === revision) return;
if (this.catalogLoad?.revision === revision) return this.catalogLoad.promise;
const promise = (async () => {
const channels = [];
let cursor;
do {
// Guide ignores group visibility; EpgGuide filters individually hidden channels.
const result = await API.channels.liveCatalog({ limit: 500, cursor, includeHidden: true });
if (revision !== (API.liveCatalogRevision || 0)) throw new Error('Catalogue changed');
channels.push(...result.items);
const next = result.page.hasMore ? result.page.nextCursor : null;
if (result.page.hasMore && (!next || next === cursor)) throw new Error('Invalid catalogue cursor');
cursor = next;
} while (cursor);
await channelList.loadHiddenItems();
if (revision !== (API.liveCatalogRevision || 0)) throw new Error('Catalogue changed');
channelList.guideCatalog = channels;
channelList.guideCatalogRevision = revision;
})();
this.catalogLoad = { revision, promise };
try { await promise; }
finally {
if (this.catalogLoad?.promise === promise) this.catalogLoad = null;
}
}
async show() {
const visit = this.visit = (this.visit || 0) + 1;
const container = this.app.epgGuide.container;
const channelList = this.app.channelList;
const needsCatalog = !channelList.guideCatalog || channelList.guideCatalogRevision !== (API.liveCatalogRevision || 0);
if (needsCatalog) {
container.textContent = 'Loading…';
container.setAttribute('aria-busy', 'true');
}
try {
await this.loadCatalog();
if (visit !== this.visit) return;
if (!this.app.epgGuide.programmes?.length) {
await this.app.epgGuide.loadEpg();
} else {
this.app.epgGuide.render();
}
} catch (err) {
if (visit !== this.visit) return;
container.textContent = 'Error loading channels';
const retry = document.createElement('button');
retry.type = 'button';
retry.className = 'btn btn-sm';
retry.textContent = 'Retry';
retry.addEventListener('click', () => this.show());
container.appendChild(retry);
} finally {
if (visit === this.visit) container.setAttribute('aria-busy', 'false');
}
}
hide() {
this.visit = (this.visit || 0) + 1;
this.app.epgGuide.container.setAttribute('aria-busy', 'false');
}
}
window.GuidePage = GuidePage;