Updated channel list context menu
This commit is contained in:
@@ -1163,6 +1163,74 @@ kbd {
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* EPG Info Modal */
|
||||||
|
.epg-info-modal .channel-details {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
padding-bottom: var(--space-md);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-info-modal .channel-logo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
object-fit: contain;
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-info-modal .channel-meta p {
|
||||||
|
margin: var(--space-xs) 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-info-modal h4 {
|
||||||
|
margin: var(--space-md) 0 var(--space-sm);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-program-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-program {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-program.current {
|
||||||
|
background: var(--color-primary-bg);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-program-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-program-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epg-program-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-programs {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
/* Form Elements */
|
/* Form Elements */
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
|
|||||||
@@ -381,7 +381,6 @@
|
|||||||
<div id="context-menu" class="context-menu">
|
<div id="context-menu" class="context-menu">
|
||||||
<button class="context-item" data-action="play">▶ Play</button>
|
<button class="context-item" data-action="play">▶ Play</button>
|
||||||
<button class="context-item" data-action="hide">👁 Hide</button>
|
<button class="context-item" data-action="hide">👁 Hide</button>
|
||||||
<button class="context-item" data-action="epg">📋 EPG Info</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JavaScript -->
|
<!-- JavaScript -->
|
||||||
|
|||||||
@@ -116,10 +116,18 @@ class ChannelList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Context menu handlers
|
// Context menu handlers
|
||||||
document.addEventListener('click', () => this.hideContextMenu());
|
document.addEventListener('click', (e) => {
|
||||||
|
// Don't close if clicking inside context menu
|
||||||
|
if (!this.contextMenu.contains(e.target)) {
|
||||||
|
this.hideContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.contextMenu.querySelectorAll('.context-item').forEach(item => {
|
this.contextMenu.querySelectorAll('.context-item').forEach(item => {
|
||||||
item.addEventListener('click', (e) => this.handleContextAction(e));
|
item.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation(); // Prevent document click from firing
|
||||||
|
this.handleContextAction(e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Intersection Observer for lazy loading
|
// Intersection Observer for lazy loading
|
||||||
@@ -852,6 +860,7 @@ class ChannelList {
|
|||||||
this.contextMenu.dataset.type = type;
|
this.contextMenu.dataset.type = type;
|
||||||
this.contextMenu.dataset.sourceId = data.sourceId;
|
this.contextMenu.dataset.sourceId = data.sourceId;
|
||||||
this.contextMenu.dataset.itemId = type === 'group' ? data.group : data.channelId;
|
this.contextMenu.dataset.itemId = type === 'group' ? data.group : data.channelId;
|
||||||
|
this.contextMenu.dataset.streamId = data.streamId || '';
|
||||||
|
|
||||||
this.contextMenu.style.left = `${e.clientX}px`;
|
this.contextMenu.style.left = `${e.clientX}px`;
|
||||||
this.contextMenu.style.top = `${e.clientY}px`;
|
this.contextMenu.style.top = `${e.clientY}px`;
|
||||||
@@ -870,7 +879,7 @@ class ChannelList {
|
|||||||
*/
|
*/
|
||||||
async handleContextAction(e) {
|
async handleContextAction(e) {
|
||||||
const action = e.target.dataset.action;
|
const action = e.target.dataset.action;
|
||||||
const { type, sourceId, itemId } = this.contextMenu.dataset;
|
const { type, sourceId, itemId, streamId } = this.contextMenu.dataset;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'play':
|
case 'play':
|
||||||
@@ -882,19 +891,104 @@ class ChannelList {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hide':
|
case 'hide':
|
||||||
await API.channels.hide(parseInt(sourceId), type, itemId);
|
// Use streamId for hiding Xtream channels (raw ID, not composite)
|
||||||
this.hiddenItems.add(`${type}:${sourceId}:${itemId}`);
|
// Server expects 'channel' type, not 'live'
|
||||||
|
const hideId = streamId || itemId;
|
||||||
|
await API.channels.hide(parseInt(sourceId), 'channel', hideId);
|
||||||
|
this.hiddenItems.add(`channel:${sourceId}:${hideId}`);
|
||||||
this.render();
|
this.render();
|
||||||
break;
|
break;
|
||||||
case 'epg':
|
case 'epg':
|
||||||
// Show EPG info modal
|
// Show EPG info modal
|
||||||
this.showEpgInfo(itemId);
|
this.showEpgInfo(sourceId, itemId, streamId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hideContextMenu();
|
this.hideContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show EPG info for a channel
|
||||||
|
*/
|
||||||
|
showEpgInfo(sourceId, channelId, streamId) {
|
||||||
|
const channel = this.channels.find(c => c.id === channelId);
|
||||||
|
if (!channel) {
|
||||||
|
alert('Channel not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById('modal');
|
||||||
|
const modalTitle = document.getElementById('modal-title');
|
||||||
|
const modalBody = document.getElementById('modal-body');
|
||||||
|
|
||||||
|
if (!modal || !modalTitle || !modalBody) return;
|
||||||
|
|
||||||
|
modalTitle.textContent = `📋 ${channel.name} - EPG Info`;
|
||||||
|
|
||||||
|
// Get current and upcoming programs
|
||||||
|
let programsHtml = '<p class="no-programs">No EPG data available for this channel.</p>';
|
||||||
|
|
||||||
|
if (window.app?.epgGuide) {
|
||||||
|
const tvgKey = channel.tvgId || channel.name;
|
||||||
|
const currentProgram = window.app.epgGuide.getCurrentProgram(channel.tvgId, channel.name);
|
||||||
|
const programs = window.app.epgGuide.getChannelPrograms?.(tvgKey) || [];
|
||||||
|
|
||||||
|
if (currentProgram || programs.length > 0) {
|
||||||
|
programsHtml = '<div class="epg-program-list">';
|
||||||
|
|
||||||
|
// Show current program
|
||||||
|
if (currentProgram) {
|
||||||
|
const startTime = new Date(currentProgram.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
const endTime = new Date(currentProgram.end).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
programsHtml += `
|
||||||
|
<div class="epg-program current">
|
||||||
|
<div class="epg-program-time">${startTime} - ${endTime}</div>
|
||||||
|
<div class="epg-program-title">▶ ${this.escapeHtml(currentProgram.title)}</div>
|
||||||
|
${currentProgram.description ? `<div class="epg-program-desc">${this.escapeHtml(currentProgram.description)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show upcoming programs (next 5)
|
||||||
|
const now = Date.now();
|
||||||
|
const upcoming = programs
|
||||||
|
.filter(p => new Date(p.start).getTime() > now)
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
upcoming.forEach(prog => {
|
||||||
|
const startTime = new Date(prog.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
const endTime = new Date(prog.end).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
programsHtml += `
|
||||||
|
<div class="epg-program">
|
||||||
|
<div class="epg-program-time">${startTime} - ${endTime}</div>
|
||||||
|
<div class="epg-program-title">${this.escapeHtml(prog.title)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
programsHtml += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="epg-info-modal">
|
||||||
|
<div class="channel-details">
|
||||||
|
<img class="channel-logo" src="${channel.tvgLogo || '/img/placeholder.png'}"
|
||||||
|
onerror="this.onerror=null;this.src='/img/placeholder.png'" />
|
||||||
|
<div class="channel-meta">
|
||||||
|
<p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p>
|
||||||
|
<p><strong>Source:</strong> ${channel.sourceType}</p>
|
||||||
|
${channel.tvgId ? `<p><strong>TVG ID:</strong> ${this.escapeHtml(channel.tvgId)}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4>Program Schedule</h4>
|
||||||
|
${programsHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync favorite status from external source (e.g. EPG) without API call
|
* Sync favorite status from external source (e.g. EPG) without API call
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user