diff --git a/public/js/components/ChannelList.js b/public/js/components/ChannelList.js index 92f37a7..d99c3c1 100644 --- a/public/js/components/ChannelList.js +++ b/public/js/components/ChannelList.js @@ -20,6 +20,7 @@ class ChannelList { this.currentChannel = null; this.sources = []; this.isLoading = false; + this.renderedChannels = []; this.loadCollapsedState(); this.init(); @@ -256,6 +257,28 @@ class ChannelList { this.groupedChannels = groupedChannels; this.showHidden = showHidden; + // Build rendered channel list for navigation (matches visual order) + this.renderedChannels = []; + this.sortedGroups.forEach(groupName => { + const channels = this.groupedChannels[groupName]; + const isFavoritesGroup = groupName === 'Favorites'; + + const visibleChannels = channels.filter(channel => { + if (isFavoritesGroup) return true; + const rawChannelId = channel.streamId || channel.id; + const channelHidden = this.isHidden('channel', channel.sourceId, rawChannelId); + return !channelHidden || this.showHidden; + }); + + // Assign unique render IDs for linear navigation + visibleChannels.forEach(ch => { + // We clone the object for the rendered list to attach the unique ID + // ensuring no side effects on the main channel object + const renderedCh = { ...ch, _renderId: `rid_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` }; + this.renderedChannels.push(renderedCh); + }); + }); + // Empty State if (this.sortedGroups.length === 0) { this.container.innerHTML = ` @@ -308,6 +331,8 @@ class ChannelList { this.loader.style.opacity = '1'; let html = ''; + let renderIndex = start; // Keep track of global index for mapping to renderedChannels + for (const groupName of groupsToRender) { const channels = this.groupedChannels[groupName]; if (channels.length === 0) continue; @@ -342,15 +367,21 @@ class ChannelList { const channelHidden = !isFavoritesGroup && this.isHidden('channel', channel.sourceId, rawChannelId); const isActive = this.currentChannel?.id === channel.id; + // Check if this specific instance is the "active" one for navigation purposes + const isRenderActive = this.currentRenderId && this.renderedChannels[renderIndex]?._renderId === this.currentRenderId; + const isFavorite = this.isFavorite(channel.sourceId, channel.id); + const renderId = this.renderedChannels[renderIndex]?._renderId || ''; + renderIndex++; html += ` -
+ data-url="${channel.url || ''}" + data-render-id="${renderId}">
@@ -458,6 +489,7 @@ class ChannelList { async loadChannels() { if (this.isLoading) return; this.isLoading = true; + this.currentRenderId = null; // Reset render tracking const sourceValue = this.sourceSelect.value; const self = this; @@ -813,7 +845,8 @@ class ChannelList { // Attach listeners div.addEventListener('click', (e) => { if (e.target.closest('.favorite-btn')) return; - this.selectChannel(div.dataset); + // Pass the render ID from the dataset + this.selectChannel({ ...div.dataset, renderId: div.dataset.renderId }); }); div.addEventListener('contextmenu', (e) => this.showContextMenu(e, 'channel', div.dataset)); @@ -836,14 +869,35 @@ class ChannelList { if (!channel) return; this.currentChannel = channel; + this.currentRenderId = dataset.renderId; // Track which visual instance is active - // Update active state in DOM without full re-render + // Update active state in DOM this.container.querySelectorAll('.channel-item.active').forEach(el => { el.classList.remove('active'); + el.classList.remove('nav-active'); }); - const activeItem = this.container.querySelector(`[data-channel-id="${channel.id}"]`); + + // Try to find specific render instance first + let activeItem; + if (this.currentRenderId) { + activeItem = this.container.querySelector(`[data-render-id="${this.currentRenderId}"]`); + } + + // Fallback to ID match if render ID not found (e.g. initial load or not rendered yet) + if (!activeItem) { + activeItem = this.container.querySelector(`[data-channel-id="${channel.id}"]`); + // If we fell back, update currentRenderId to match what we found + if (activeItem) { + this.currentRenderId = activeItem.dataset.renderId; + } + } + if (activeItem) { activeItem.classList.add('active'); + activeItem.classList.add('nav-active'); // Add specific class for navigation tracking + + // Ensure visible (scroll into view if needed) + activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } // Get stream URL @@ -1041,23 +1095,34 @@ class ChannelList { * Select next channel in the current list */ selectNextChannel() { - if (!this.currentChannel || !this.filteredChannels || this.filteredChannels.length === 0) return; + if (!this.currentChannel || !this.renderedChannels || this.renderedChannels.length === 0) return; - const currentIndex = this.filteredChannels.findIndex(c => - c.id === this.currentChannel.id && c.sourceId === this.currentChannel.sourceId - ); + let currentIndex = -1; + + // Try to find by render ID first (strict visual order) + if (this.currentRenderId) { + currentIndex = this.renderedChannels.findIndex(c => c._renderId === this.currentRenderId); + } + + // Fallback: Find first matching channel ID (if render ID lost or invalid) + if (currentIndex === -1) { + currentIndex = this.renderedChannels.findIndex(c => + c.id === this.currentChannel.id && c.sourceId === this.currentChannel.sourceId + ); + } if (currentIndex === -1) return; - const nextIndex = (currentIndex + 1) % this.filteredChannels.length; - const nextChannel = this.filteredChannels[nextIndex]; + const nextIndex = (currentIndex + 1) % this.renderedChannels.length; + const nextChannel = this.renderedChannels[nextIndex]; this.selectChannel({ channelId: nextChannel.id, sourceId: nextChannel.sourceId, sourceType: nextChannel.sourceType, streamId: nextChannel.streamId, - url: nextChannel.url + url: nextChannel.url, + renderId: nextChannel._renderId // Pass the unique render ID }); } @@ -1065,23 +1130,32 @@ class ChannelList { * Select previous channel in the current list */ selectPrevChannel() { - if (!this.currentChannel || !this.filteredChannels || this.filteredChannels.length === 0) return; + if (!this.currentChannel || !this.renderedChannels || this.renderedChannels.length === 0) return; - const currentIndex = this.filteredChannels.findIndex(c => - c.id === this.currentChannel.id && c.sourceId === this.currentChannel.sourceId - ); + let currentIndex = -1; + + if (this.currentRenderId) { + currentIndex = this.renderedChannels.findIndex(c => c._renderId === this.currentRenderId); + } + + if (currentIndex === -1) { + currentIndex = this.renderedChannels.findIndex(c => + c.id === this.currentChannel.id && c.sourceId === this.currentChannel.sourceId + ); + } if (currentIndex === -1) return; - const prevIndex = (currentIndex - 1 + this.filteredChannels.length) % this.filteredChannels.length; - const prevChannel = this.filteredChannels[prevIndex]; + const prevIndex = (currentIndex - 1 + this.renderedChannels.length) % this.renderedChannels.length; + const prevChannel = this.renderedChannels[prevIndex]; this.selectChannel({ channelId: prevChannel.id, sourceId: prevChannel.sourceId, sourceType: prevChannel.sourceType, streamId: prevChannel.streamId, - url: prevChannel.url + url: prevChannel.url, + renderId: prevChannel._renderId }); }