Implemented focus for active channel in channel list
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user