diff --git a/apps/web/src/app/home/video-player/video-player.component.ts b/apps/web/src/app/home/video-player/video-player.component.ts index a03a0d086..5ce337d7e 100644 --- a/apps/web/src/app/home/video-player/video-player.component.ts +++ b/apps/web/src/app/home/video-player/video-player.component.ts @@ -335,6 +335,13 @@ export class VideoPlayerComponent implements OnInit { selectChannels ) as Observable; + // Pass the active channel's tvg.id for highlighting + this.activeChannel$.pipe(take(1)).subscribe((channel) => { + if (channel) { + componentRef.instance.activeChannelId = (channel as Channel).tvg?.id || null; + } + }); + this.overlayRef.backdropClick().subscribe(() => { this.overlayRef.dispose(); }); diff --git a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.html b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.html index fd1814ded..97af434d0 100644 --- a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.html +++ b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.html @@ -1,60 +1,41 @@
+
+ {{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }} +
-
- {{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }} -
- - -
-
- +
+ @for (item of channels$ | async; track item.id; let i = $index) { - + } -
- +
+ @for (hour of timeHeader; track hour) { @@ -102,12 +83,20 @@ > {{ hour }}:00 + + } @for (item of channels$ | async; track item.id; let i = $index) { - + @for (program of item.programs; track trackByProgram($index, program)) { rect[fill='#000'] { + fill: $bg-surface !important; + } + + g > text { + font-family: 'JetBrains Mono', monospace !important; + font-size: 11px !important; + font-weight: 500; + fill: $text-secondary !important; + letter-spacing: 0.1em; + } + + // Hour separator vertical lines + .hour-separator { + stroke: $border-subtle; + stroke-width: 1px; + pointer-events: none; + } + + // Program blocks + .program { + cursor: pointer; + + rect { + fill: $bg-elevated; + stroke: $border-subtle; + rx: 4; + ry: 4; + transition: all 0.15s ease; + } + + &:hover rect { + fill: $bg-hover; + stroke: rgba(139, 92, 246, 0.4); + filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.2)); + } + } + + // Active channel row highlighting + .active-row .program rect { + fill: rgba(139, 92, 246, 0.15); + stroke: rgba(139, 92, 246, 0.3); + } + + .active-row .program:hover rect { + fill: rgba(139, 92, 246, 0.25); + } + + .program-title { + color: $text-primary; + padding: 6px 10px; + font-size: 11px; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + letter-spacing: 0.01em; + line-height: 1.4; + + // Subtle gradient text for depth + display: flex; + align-items: center; + height: 100%; + } + + // Current time line - glowing effect + line[stroke='red'] { + stroke: $accent-primary !important; + stroke-width: 2px; + filter: drop-shadow(0 0 8px rgba(0, 212, 170, 0.6)) + drop-shadow(0 0 16px rgba(0, 212, 170, 0.3)); + + // Animated pulse + animation: timePulse 2s ease-in-out infinite; + } +} + +// Pulse animation for time line +@keyframes timePulse { + 0%, + 100% { + opacity: 1; + filter: drop-shadow(0 0 8px rgba(0, 212, 170, 0.6)) + drop-shadow(0 0 16px rgba(0, 212, 170, 0.3)); + } + 50% { + opacity: 0.8; + filter: drop-shadow(0 0 12px rgba(0, 212, 170, 0.8)) + drop-shadow(0 0 24px rgba(0, 212, 170, 0.4)); + } +} + +// Legacy classes cleanup .program-item { cursor: pointer; } rect { - stroke: #676767; + stroke: $border-subtle; cursor: pointer; } .channel > rect { - stroke: #676767; -} - -.program-item:hover > rect { - fill: #333; + stroke: $border-subtle; } .program-name, @@ -23,112 +345,121 @@ rect { text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; - padding: 6px 2px; -} - -#epg-container { - width: calc(100vw - 100px); - height: calc(100vh - 42px); - overflow-x: scroll; - overflow-y: hidden; -} - -.parent { - display: flex; - background-color: black; - color: #fff; -} - -#channels-column { - width: 100px; - flex: none; - border-right: 2px solid #ccc; - - .channel { - cursor: pointer; - - rect { - fill: #2d2d2d; - stroke: #3d3d3d; - } - - &:hover rect { - fill: #3d3d3d; - } - } - - .channel-name { - color: white; - padding: 4px 8px; - font-size: 12px; - display: flex; - align-items: center; - height: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - image { - object-fit: contain; - background: #2d2d2d; - } -} - -/* .channel-name { - margin: 0 5px; - text-align: center; -} */ - -#epg-navigation { - display: flex; - background-color: white; - border-bottom: 2px solid #fff; - align-items: center; -} - -#epg-svg { - .program { - cursor: pointer; - - rect { - fill: #2d2d2d; - stroke: #3d3d3d; - } - - &:hover rect { - fill: #3d3d3d; - } - } - - .program-title { - color: white; - padding: 4px 8px; - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - } -} - -.today-date { - flex: 1; - align-items: center; - line-height: 40px; - height: 40px; - padding: 0 20px; + padding: 6px 8px; } #current-time-line { stroke-width: 2px; - stroke: white; + stroke: $accent-primary; } +// Dark theme override (already dark, but ensure consistency) ::ng-deep { .dark-theme { #epg-navigation { - background-color: black; + background: rgba(18, 18, 26, 0.85); + } + } + + // Light theme adjustments + .light-theme { + #epg-navigation { + background: rgba(255, 255, 255, 0.9); + border-bottom-color: rgba(0, 0, 0, 0.1); + + .today-date { + color: #059669; + text-shadow: none; + } + } + + .parent { + background: #f8fafc; + + &::before { + background-image: + linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px), + linear-gradient( + 90deg, + rgba(0, 0, 0, 0.03) 1px, + transparent 1px + ); + } + } + + #channels-column { + background: #ffffff; + border-right-color: rgba(0, 0, 0, 0.1); + box-shadow: 4px 0 20px rgba(0, 0, 0, 0.05); + + .channel rect { + fill: #ffffff; + stroke: rgba(0, 0, 0, 0.08); + } + + .channel:hover rect { + fill: #f1f5f9; + } + + .channel-name { + color: #1e293b; + } + } + + #epg-container { + background: #f8fafc; + } + + #epg-svg { + g > rect[fill='#000'] { + fill: #e2e8f0 !important; + } + + g > text { + fill: #64748b !important; + } + + .program { + rect { + fill: #ffffff; + stroke: rgba(0, 0, 0, 0.08); + } + + &:hover rect { + fill: #f1f5f9; + stroke: rgba(139, 92, 246, 0.4); + } + } + + .program-title { + color: #1e293b; + } + + line[stroke='red'] { + stroke: #059669 !important; + filter: drop-shadow(0 0 6px rgba(5, 150, 105, 0.4)); + } } } } + +// Responsive adjustments +@media (max-width: 768px) { + #channels-column { + width: 100px; + + .channel-name { + font-size: 10px; + padding: 6px 8px; + } + } + + #epg-container { + width: calc(100vw - 100px); + } + + .today-date { + font-size: 12px; + padding: 0 12px; + } +} diff --git a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts index 3dfcc68d1..46a2d3d85 100644 --- a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts +++ b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts @@ -62,12 +62,19 @@ export class MultiEpgContainerComponent if (value) { this.playlistChannelsSubscription = value.subscribe(() => { + // Reset data for new playlist + this.channelsLowerRange = 0; + this.originalEpgData = []; + this.channels$.next([]); + this.isLastPage = false; this.initializeVisibleChannels(); this.requestPrograms(); }); } } + @Input() activeChannelId: string | null = null; + readonly timeHeader = Array.from({ length: 24 }, (_, i) => i); readonly hourWidth$ = new BehaviorSubject(150); readonly barHeight = 50; @@ -85,6 +92,7 @@ export class MultiEpgContainerComponent private playlistChannelsSubscription?: Subscription; isLastPage = false; + isLoading = false; totalChannels = 0; private readonly dialog = inject(MatDialog); @@ -161,6 +169,12 @@ export class MultiEpgContainerComponent return; } + if (this.isLoading || this.isLastPage) { + return; + } + + this.isLoading = true; + try { const response = await window.electron.getEpgChannelsByRange( this.channelsLowerRange, @@ -168,35 +182,35 @@ export class MultiEpgContainerComponent ); if (response && Array.isArray(response)) { - this.originalEpgData = response; + // Append new data to existing data + this.originalEpgData = [...this.originalEpgData, ...response]; this.channels$.next(this.enrichProgramData()); // Update isLastPage based on the number of channels received this.isLastPage = response.length < this.visibleChannels; + // Update range for next fetch + this.channelsLowerRange += response.length; + this.cdr.detectChanges(); } } catch (error) { console.error('Error fetching EPG data:', error); + } finally { + this.isLoading = false; } } - nextChannels(): void { - this.channelsLowerRange += this.visibleChannels; - this.channelsUpperRange = - this.channelsLowerRange + this.visibleChannels; + onScroll(event: Event): void { + const target = event.target as HTMLElement; + const scrollTop = target.scrollTop; + const scrollHeight = target.scrollHeight; + const clientHeight = target.clientHeight; - this.requestPrograms(); - } - - previousChannels(): void { - this.channelsLowerRange = Math.max( - 0, - this.channelsLowerRange - this.visibleChannels - ); - this.channelsUpperRange = - this.channelsLowerRange + this.visibleChannels; - this.requestPrograms(); + // Load more when user scrolls to within 200px of the bottom + if (scrollHeight - scrollTop - clientHeight < 200) { + this.requestPrograms(); + } } private getCachedDate(dateStr: string): Date {