Implemented focus for active channel in channel list

This commit is contained in:
Trevor Mears
2025-12-28 20:15:26 -08:00
parent e8bfb56c8b
commit 44365c4dad
3 changed files with 73 additions and 17 deletions
+61 -9
View File
@@ -274,7 +274,11 @@ class ChannelList {
visibleChannels.forEach(ch => { visibleChannels.forEach(ch => {
// We clone the object for the rendered list to attach the unique ID // We clone the object for the rendered list to attach the unique ID
// ensuring no side effects on the main channel object // ensuring no side effects on the main channel object
const renderedCh = { ...ch, _renderId: `rid_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` }; const renderedCh = {
...ch,
_renderId: `rid_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
_renderGroup: groupName // Track visual group for navigation
};
this.renderedChannels.push(renderedCh); this.renderedChannels.push(renderedCh);
}); });
}); });
@@ -633,7 +637,10 @@ class ChannelList {
const channelList = data.channels.map(ch => ({ const channelList = data.channels.map(ch => ({
...ch, ...ch,
// Use the stable id provided by the server (from tvgId or hash) // Use the stable id provided by the server (from tvgId or hash)
id: ch.id, // Prefix with sourceId to ensure global uniqueness across multiple M3U sources
id: `m3u_${sourceId}_${ch.id}`,
// Keep original ID as streamId if needed (or just use original ID for reference)
streamId: ch.id,
groupId: `m3u_${sourceId}_group_${data.groups.findIndex(g => g.name === ch.groupTitle)}`, groupId: `m3u_${sourceId}_group_${data.groups.findIndex(g => g.name === ch.groupTitle)}`,
sourceId, sourceId,
sourceType: 'm3u' sourceType: 'm3u'
@@ -879,15 +886,26 @@ class ChannelList {
// Try to find specific render instance first // Try to find specific render instance first
let activeItem; let activeItem;
if (this.currentRenderId) { activeItem = this.container.querySelector(`[data-render-id="${this.currentRenderId}"]`);
activeItem = this.container.querySelector(`[data-render-id="${this.currentRenderId}"]`);
// If not found in DOM, it might be in a future batch not yet rendered
// Render batches until we find it or run out
if (!activeItem && this.renderedChannels.length > 0) {
let safety = 0;
while (!activeItem && this.currentBatch * this.batchSize < this.sortedGroups.length && safety < 20) {
this.renderNextBatch();
if (this.currentRenderId) {
activeItem = this.container.querySelector(`[data-render-id="${this.currentRenderId}"]`);
}
safety++;
}
} }
// Fallback to ID match if render ID not found (e.g. initial load or not rendered yet) // Fallback checks if still not found
if (!activeItem) { if (!activeItem) {
activeItem = this.container.querySelector(`[data-channel-id="${channel.id}"]`); activeItem = this.container.querySelector(`[data-channel-id="${channel.id}"]`);
// If we fell back, update currentRenderId to match what we found // If we fell back to channel ID, update currentRenderId to match what we found
if (activeItem) { if (activeItem && activeItem.dataset.renderId) {
this.currentRenderId = activeItem.dataset.renderId; this.currentRenderId = activeItem.dataset.renderId;
} }
} }
@@ -896,8 +914,42 @@ class ChannelList {
activeItem.classList.add('active'); activeItem.classList.add('active');
activeItem.classList.add('nav-active'); // Add specific class for navigation tracking activeItem.classList.add('nav-active'); // Add specific class for navigation tracking
// Ensure visible (scroll into view if needed) // Handle Group Expansion & Scrolling (Focus Mode)
activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); const groupHeader = activeItem.closest('.channel-group')?.querySelector('.group-header');
if (groupHeader) {
const groupName = groupHeader.dataset.group;
// 1. Expand current group if needed
if (this.collapsedGroups.has(groupName)) {
this.collapsedGroups.delete(groupName);
// Update DOM directly for immediate feedback
groupHeader.classList.remove('collapsed');
this.saveCollapsedState();
}
// 2. Collapse ALL other groups (Focus Mode)
document.querySelectorAll('.group-header').forEach(header => {
if (header !== groupHeader && !header.classList.contains('collapsed')) {
const otherGroup = header.dataset.group;
this.collapsedGroups.add(otherGroup);
header.classList.add('collapsed');
}
});
this.saveCollapsedState();
// 3. Scroll Group to Top
// Use a small timeout to allow layout updates (e.g. collapse animations) to start
setTimeout(() => {
groupHeader.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Ensure active item is visible within the group
setTimeout(() => {
activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 50);
}, 50);
} else {
// Fallback for non-grouped items or flat list
activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} }
// Get stream URL // Get stream URL
+6 -8
View File
@@ -464,20 +464,18 @@ class VideoPlayer {
this.video.muted = !this.video.muted; this.video.muted = !this.video.muted;
break; break;
case 'ArrowUp': case 'ArrowUp':
e.preventDefault(); if (!this.settings.arrowKeysChangeChannel) {
if (this.settings.arrowKeysChangeChannel) { e.preventDefault();
this.channelUp();
} else {
this.video.volume = Math.min(1, this.video.volume + 0.1); this.video.volume = Math.min(1, this.video.volume + 0.1);
} }
// If arrowKeysChangeChannel is true, let HomePage handle it
break; break;
case 'ArrowDown': case 'ArrowDown':
e.preventDefault(); if (!this.settings.arrowKeysChangeChannel) {
if (this.settings.arrowKeysChangeChannel) { e.preventDefault();
this.channelDown();
} else {
this.video.volume = Math.max(0, this.video.volume - 0.1); this.video.volume = Math.max(0, this.video.volume - 0.1);
} }
// If arrowKeysChangeChannel is true, let HomePage handle it
break; break;
case 'ArrowLeft': case 'ArrowLeft':
e.preventDefault(); e.preventDefault();
+6
View File
@@ -75,10 +75,16 @@ class HomePage {
switch (e.key) { switch (e.key) {
case 'ArrowUp': case 'ArrowUp':
// Check if player handles arrows for volume
if (this.app.player && !this.app.player.settings.arrowKeysChangeChannel) return;
e.preventDefault(); e.preventDefault();
this.app.channelList.selectPrevChannel(); this.app.channelList.selectPrevChannel();
break; break;
case 'ArrowDown': case 'ArrowDown':
// Check if player handles arrows for volume
if (this.app.player && !this.app.player.settings.arrowKeysChangeChannel) return;
e.preventDefault(); e.preventDefault();
this.app.channelList.selectNextChannel(); this.app.channelList.selectNextChannel();
break; break;