diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html index 77e70e8b5..3b3b9764f 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html @@ -23,7 +23,7 @@
} @@ -140,6 +142,7 @@ " [isDraggable]="true" [logo]="channel?.tvg?.logo" + [showEpg]="shouldShowEpg" [epgProgram]="getEpgForChannel(channel)" (clicked)="selectChannel(channel)" [selected]="activeChannel()?.url === channel?.url" diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index 83af308a9..496e4643c 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -25,6 +25,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTabsModule } from '@angular/material/tabs'; import { FilterPipe } from '@iptvnator/pipes'; import { Store } from '@ngrx/store'; +import { StorageMap } from '@ngx-pwa/local-storage'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import * as _ from 'lodash'; import * as PlaylistActions from 'm3u-state'; @@ -33,9 +34,16 @@ import { selectActivePlaylistId, selectFavorites, } from 'm3u-state'; -import { BehaviorSubject, combineLatest, map, skipWhile } from 'rxjs'; +import { + BehaviorSubject, + combineLatest, + map, + skipWhile, + Subject, + takeUntil, +} from 'rxjs'; import { EpgService } from 'services'; -import { Channel, EpgProgram } from 'shared-interfaces'; +import { Channel, EpgProgram, Settings, STORE_KEY } from 'shared-interfaces'; import { ChannelListItemComponent } from './channel-list-item/channel-list-item.component'; @Component({ @@ -62,6 +70,7 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item. export class ChannelListContainerComponent implements OnInit, OnDestroy { private readonly epgService = inject(EpgService); private readonly snackBar = inject(MatSnackBar); + private readonly storage = inject(StorageMap); private readonly store = inject(Store); private readonly translateService = inject(TranslateService); @@ -71,6 +80,16 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { /** Interval for refreshing EPG data */ private epgRefreshInterval?: number; + private destroy$ = new Subject(); + + /** Whether to show EPG data in channel items (false in PWA mode or when EPG is not configured) */ + shouldShowEpg = false; + + /** Item size for virtual scroll - compact when no EPG */ + get itemSize(): number { + return this.shouldShowEpg ? 68 : 48; + } + /** * Channels array * Create local copy of the store for local manipulations without updates in the store @@ -128,7 +147,12 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { this.channelList$, ]).pipe( map(([favoriteChannelIds, channelList]) => { - console.log('[ChannelList] favorites$ emit - IDs:', favoriteChannelIds.length, 'Channels:', channelList.length); + console.log( + '[ChannelList] favorites$ emit - IDs:', + favoriteChannelIds.length, + 'Channels:', + channelList.length + ); const favorites = favoriteChannelIds .map((favoriteChannelId) => channelList.find( @@ -136,7 +160,11 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { ) ) .filter((channel): channel is Channel => channel !== undefined); // Filter out undefined channels - console.log('[ChannelList] favorites$ result:', favorites.length, 'favorites'); + console.log( + '[ChannelList] favorites$ result:', + favorites.length, + 'favorites' + ); return favorites; }) ); @@ -185,6 +213,25 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { } ngOnInit(): void { + // Check if EPG should be shown (only in Electron with configured EPG URL) + const isElectron = !!window['electron']; + if (isElectron) { + this.storage + .get(STORE_KEY.Settings) + .subscribe((settings: unknown) => { + if ( + settings && + Object.keys(settings as Settings).length > 0 + ) { + const epgUrl = (settings as Settings).epgUrl; + this.shouldShowEpg = !!(epgUrl && epgUrl.length > 0); + } + }); + } else { + // PWA mode - don't show EPG + this.shouldShowEpg = false; + } + // Set up EPG refresh interval (every 60 seconds) this.epgRefreshInterval = window.setInterval(() => { this.fetchEpgForChannels(this._channelList); @@ -198,6 +245,8 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { clearInterval(this.epgRefreshInterval); } + this.destroy$.next(); + this.destroy$.complete(); // Clean up BehaviorSubject this.channelList$.complete(); } @@ -213,19 +262,16 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { // Get channel IDs (prefer tvg-id, fallback to name) const channelIds = channels - .map(channel => channel?.tvg?.id?.trim() || channel?.name?.trim()) - .filter(id => !!id); - - console.log(`[EPG] Fetching EPG for ${channelIds.length} channels`); + .map((channel) => channel?.tvg?.id?.trim() || channel?.name?.trim()) + .filter((id) => !!id); // Batch fetch EPG programs - this.epgService.getCurrentProgramsForChannels(channelIds).subscribe( - (epgMap) => { - console.log(`[EPG] Received EPG data for ${epgMap.size} channels`); - console.log('[EPG] Sample data:', Array.from(epgMap.entries()).slice(0, 3)); + this.epgService + .getCurrentProgramsForChannels(channelIds) + .pipe(takeUntil(this.destroy$)) + .subscribe((epgMap) => { this.channelEpgMap = epgMap; - } - ); + }); } /** diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss index 838b9d0c8..f64284e0d 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss @@ -19,6 +19,12 @@ &.active { background-color: rgba(0, 0, 0, 0.08); } + + // Compact mode when EPG is not shown + &.compact { + height: 48px; + padding: 6px 12px; + } } // Dark theme support @@ -57,6 +63,12 @@ border-radius: 4px; object-fit: contain; flex-shrink: 0; + + // Smaller logo in compact mode + .compact & { + width: 32px; + height: 32px; + } } // Channel details (name + EPG) @@ -83,9 +95,9 @@ // EPG title (program name) .epg-title { - font-family: 'Crimson Pro', serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 0.8rem; - font-weight: 600; + font-weight: 500; opacity: 0.85; overflow: hidden; text-overflow: ellipsis; diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts index 3692c925c..43523658c 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts @@ -26,6 +26,7 @@ import { EpgProgram } from 'shared-interfaces'; changeDetection: ChangeDetectionStrategy.OnPush, template: `