diff --git a/public/css/main.css b/public/css/main.css index c133a42..573a399 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -1163,6 +1163,74 @@ kbd { 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-group { margin-bottom: var(--space-md); diff --git a/public/index.html b/public/index.html index fad8a46..8b412b8 100644 --- a/public/index.html +++ b/public/index.html @@ -381,7 +381,6 @@
-
diff --git a/public/js/components/ChannelList.js b/public/js/components/ChannelList.js index 143af70..1c8f853 100644 --- a/public/js/components/ChannelList.js +++ b/public/js/components/ChannelList.js @@ -116,10 +116,18 @@ class ChannelList { } // 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 => { - 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 @@ -852,6 +860,7 @@ class ChannelList { this.contextMenu.dataset.type = type; this.contextMenu.dataset.sourceId = data.sourceId; 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.top = `${e.clientY}px`; @@ -870,7 +879,7 @@ class ChannelList { */ async handleContextAction(e) { const action = e.target.dataset.action; - const { type, sourceId, itemId } = this.contextMenu.dataset; + const { type, sourceId, itemId, streamId } = this.contextMenu.dataset; switch (action) { case 'play': @@ -882,19 +891,104 @@ class ChannelList { } break; case 'hide': - await API.channels.hide(parseInt(sourceId), type, itemId); - this.hiddenItems.add(`${type}:${sourceId}:${itemId}`); + // Use streamId for hiding Xtream channels (raw ID, not composite) + // 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(); break; case 'epg': // Show EPG info modal - this.showEpgInfo(itemId); + this.showEpgInfo(sourceId, itemId, streamId); break; } 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 = '

No EPG data available for this channel.

'; + + 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 = '
'; + + // 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 += ` +
+
${startTime} - ${endTime}
+
▶ ${this.escapeHtml(currentProgram.title)}
+ ${currentProgram.description ? `
${this.escapeHtml(currentProgram.description)}
` : ''} +
+ `; + } + + // 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 += ` +
+
${startTime} - ${endTime}
+
${this.escapeHtml(prog.title)}
+
+ `; + }); + + programsHtml += '
'; + } + } + + modalBody.innerHTML = ` +
+
+ +
+

Group: ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}

+

Source: ${channel.sourceType}

+ ${channel.tvgId ? `

TVG ID: ${this.escapeHtml(channel.tvgId)}

` : ''} +
+
+

Program Schedule

+ ${programsHtml} +
+ `; + + modal.classList.add('active'); + } + /** * Sync favorite status from external source (e.g. EPG) without API call */