feat: add settings option to hide Live TV categories (#11)

Add a liveChannelListMode setting (default 'grouped') with a Settings
toggle to display the Live TV channel list flat, without category
group sections. When enabled, logical channels are listed directly
with their stream variants still grouped as in #9/#10. Cover the
setting default and flat-list behavior with tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-02 09:57:20 +02:00
parent 451f11f21f
commit e06f4ec2c3
8 changed files with 153 additions and 7 deletions

View File

@ -23,6 +23,7 @@ class ChannelList {
this.sources = [];
this.isLoading = false;
this.renderedChannels = [];
this.listMode = 'grouped';
this.logicalChannels = [];
this.selectedVariants = new Map();
this.expandedVariants = new Set();
@ -189,6 +190,10 @@ class ChannelList {
// Source filter handler
this.sourceSelect.addEventListener('change', () => this.loadChannels());
this.toggleGroupsBtn?.addEventListener('click', () => {
if (this.listMode === 'grouped') this.toggleAllGroups();
});
// Show hidden toggle
if (this.showHiddenCheckbox) {
this.showHiddenCheckbox.addEventListener('change', () => this.render());
@ -212,7 +217,8 @@ class ChannelList {
// Intersection Observer for lazy loading
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.renderNextBatch();
if (this.listMode === 'flat') this.renderNextFlatBatch();
else this.renderNextBatch();
}
}, { rootMargin: '100px' });
@ -312,6 +318,16 @@ class ChannelList {
}
}
setListMode(mode) {
this.listMode = mode === 'flat' ? 'flat' : 'grouped';
if (this.toggleGroupsBtn) {
this.toggleGroupsBtn.hidden = this.listMode === 'flat';
this.toggleGroupsBtn.disabled = this.listMode === 'flat';
this.toggleGroupsBtn.classList.toggle('hidden', this.listMode === 'flat');
}
if (this.channels.length > 0) this.render();
}
escapeHtml(text) {
if (!text) return '';
return text
@ -328,6 +344,12 @@ class ChannelList {
render() {
const searchTerm = this.searchInput.value.toLowerCase();
const showHidden = this.showHiddenCheckbox ? this.showHiddenCheckbox.checked : false;
this.listMode = window.app?.player?.settings?.liveChannelListMode === 'flat' ? 'flat' : 'grouped';
if (this.toggleGroupsBtn) {
this.toggleGroupsBtn.hidden = this.listMode === 'flat';
this.toggleGroupsBtn.disabled = this.listMode === 'flat';
this.toggleGroupsBtn.classList.toggle('hidden', this.listMode === 'flat');
}
this.logicalChannels = ChannelVariantGrouping.groupChannels(this.channels, {
showHidden,
@ -359,6 +381,12 @@ class ChannelList {
let filteredChannels = this.filteredChannels;
this.showHidden = showHidden;
if (this.listMode === 'flat') {
this.renderFlatList(filteredChannels, searchTerm);
return;
}
// 2. Group
filteredChannels.forEach(ch => {
const groupKey = ch.groupTitle || 'Uncategorized';
@ -466,6 +494,57 @@ class ChannelList {
this.observer.observe(this.loader);
}
renderFlatList(channels, searchTerm) {
const orderedChannels = ChannelVariantGrouping.orderFlatChannels(channels);
this.renderedChannels = orderedChannels.map(channel => ({
...channel,
_renderId: `rid_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
_renderGroup: 'flat'
}));
if (this.renderedChannels.length === 0) {
this.container.innerHTML = `
<div class="empty-state">
<p>${searchTerm ? 'No channels match your search' : 'No channels loaded'}</p>
<p class="hint">${searchTerm ? 'Try a different search term' : 'Add a source in Settings to get started'}</p>
</div>`;
return;
}
this.currentBatch = 0;
this.batchSize = 100;
this.listContainer = document.createElement('div');
this.listContainer.className = 'channel-list-content flat-channel-list';
this.container.appendChild(this.listContainer);
this.loader = document.createElement('div');
this.loader.className = 'batch-loader';
this.loader.innerHTML = '<div class="loading-spinner"></div>';
this.container.appendChild(this.loader);
this.renderNextFlatBatch();
this.observer.observe(this.loader);
}
renderNextFlatBatch() {
if (!this.loader || !this.listContainer) return;
const start = this.currentBatch * this.batchSize;
const channels = this.renderedChannels.slice(start, start + this.batchSize);
if (channels.length === 0) {
this.loader.style.display = 'none';
return;
}
const temp = document.createElement('div');
temp.innerHTML = channels.map(channel => this.renderLogicalChannel(
channel,
channel._renderId,
'flat',
this.currentChannel?.id === channel.id,
this.currentRenderId === channel._renderId
)).join('');
this.attachChannelListeners(temp);
while (temp.firstElementChild) this.listContainer.appendChild(temp.firstElementChild);
this.currentBatch++;
if (start + channels.length >= this.renderedChannels.length) this.loader.style.display = 'none';
}
/**
* Render next batch of groups
*/
@ -1002,6 +1081,8 @@ class ChannelList {
* Update Favorites group in DOM and data
*/
updateFavoritesGroup(channel, isAdded) {
if (!ChannelVariantGrouping.shouldUseFavoritesGroup(this.listMode) || !this.groupedChannels) return;
// 1. Update Data
if (!this.groupedChannels['Favorites']) {
this.groupedChannels['Favorites'] = [];
@ -1518,7 +1599,7 @@ class ChannelList {
* Get list of visible (non-hidden) channels in display order
*/
getVisibleChannels() {
return this.logicalChannels;
return this.listMode === 'flat' ? this.renderedChannels : this.logicalChannels;
}
}