diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.ts b/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.ts index 6b9575a63..9ed052b8d 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.ts @@ -139,14 +139,26 @@ export class AllChannelsTabComponent implements OnDestroy { return 0; } - const now = new Date().getTime(); + const now = Date.now(); const start = new Date(epgProgram.start).getTime(); const stop = new Date(epgProgram.stop).getTime(); - const total = stop - start; - const elapsed = now - start; + // Validate start/stop are finite numbers + if (!Number.isFinite(start) || !Number.isFinite(stop)) { + return 0; + } - return Math.min(100, Math.max(0, (elapsed / total) * 100)); + const total = stop - start; + + // Bail out if duration is zero or negative + if (total <= 0) { + return 0; + } + + // Clamp elapsed to [0, total] + const elapsed = Math.min(total, Math.max(0, now - start)); + + return Math.round((elapsed / total) * 100); } trackByFn(_: number, channel: Channel): string { diff --git a/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.html b/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.html index 1e0fd3702..a30bce896 100644 --- a/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.html +++ b/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.html @@ -4,9 +4,9 @@ (cdkDropListDropped)="onDrop($event)" id="favorites-list" > - @if (enrichedFavorites.length > 0) { + @if (enrichedFavorites().length > 0) { @for ( - channel of enrichedFavorites; + channel of enrichedFavorites(); track trackByFn($index, channel); let i = $index ) { diff --git a/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.ts b/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.ts index c345f73b0..ce882a472 100644 --- a/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/favorites-tab/favorites-tab.component.ts @@ -6,24 +6,21 @@ import { import { ChangeDetectionStrategy, Component, + computed, input, output, } from '@angular/core'; import { TranslatePipe } from '@ngx-translate/core'; import { Channel, EpgProgram } from 'shared-interfaces'; -import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component'; import { EnrichedChannel } from '../all-channels-tab/all-channels-tab.component'; +import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component'; @Component({ selector: 'app-favorites-tab', templateUrl: './favorites-tab.component.html', styleUrls: ['./favorites-tab.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - ChannelListItemComponent, - DragDropModule, - TranslatePipe, - ], + imports: [ChannelListItemComponent, DragDropModule, TranslatePipe], }) export class FavoritesTabComponent { /** Favorite channels */ @@ -45,21 +42,24 @@ export class FavoritesTabComponent { readonly channelSelected = output(); /** Emits when favorite is toggled (removed) */ - readonly favoriteToggled = output<{ channel: Channel; event: MouseEvent }>(); + readonly favoriteToggled = output<{ + channel: Channel; + event: MouseEvent; + }>(); /** Emits when favorites order changes via drag-drop */ readonly favoritesReordered = output(); /** - * Gets enriched favorites with EPG data + * Computed signal for enriched favorites with EPG data */ - get enrichedFavorites(): EnrichedChannel[] { + readonly enrichedFavorites = computed(() => { const favorites = this.favorites(); const epgMap = this.channelEpgMap(); // Read progressTick to trigger recalculation this.progressTick(); - return favorites.map(channel => { + return favorites.map((channel) => { const channelId = channel?.tvg?.id?.trim() || channel?.name?.trim(); const epgProgram = channelId ? epgMap.get(channelId) : null; return { @@ -68,12 +68,14 @@ export class FavoritesTabComponent { progressPercentage: this.calculateProgress(epgProgram), } as EnrichedChannel; }); - } + }); /** * Calculates progress percentage for an EPG program */ - private calculateProgress(epgProgram: EpgProgram | null | undefined): number { + private calculateProgress( + epgProgram: EpgProgram | null | undefined + ): number { if (!epgProgram) { return 0; } @@ -103,6 +105,6 @@ export class FavoritesTabComponent { onDrop(event: CdkDragDrop): void { const favorites = [...this.favorites()]; moveItemInArray(favorites, event.previousIndex, event.currentIndex); - this.favoritesReordered.emit(favorites.map(item => item.url)); + this.favoritesReordered.emit(favorites.map((item) => item.url)); } } diff --git a/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.html b/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.html index f5cd906e5..80a8edd61 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.html +++ b/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.html @@ -20,7 +20,7 @@ @let enrichedGroupChannels = - getEnrichedGroupChannels(groups.value); + getEnrichedGroupChannels(groups.value, groups.key); @let limit = getGroupLimit(groups.key);
@for ( diff --git a/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.ts b/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.ts index 3c7266bbb..0c9bef53a 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/groups-tab/groups-tab.component.ts @@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, inject, input, NgZone, @@ -176,13 +177,46 @@ export class GroupsTabComponent implements AfterViewInit, OnDestroy { } /** - * Gets enriched channels for a specific group + * Computed signal that memoizes enriched channels for all groups */ - getEnrichedGroupChannels(channels: Channel[]): EnrichedChannel[] { + private readonly enrichedGroupChannelsMap = computed(() => { + const grouped = this.groupedChannels(); const epgMap = this.channelEpgMap(); // Read progressTick to create dependency for progress refresh this.progressTick(); + const result = new Map(); + for (const [groupKey, channels] of Object.entries(grouped)) { + result.set( + groupKey, + channels.map((channel) => { + const channelId = + channel?.tvg?.id?.trim() || channel?.name?.trim(); + const epgProgram = channelId ? epgMap.get(channelId) : null; + return { + ...channel, + epgProgram, + progressPercentage: this.calculateProgress(epgProgram), + } as EnrichedChannel; + }) + ); + } + return result; + }); + + /** + * Gets enriched channels for a specific group from the memoized map + */ + getEnrichedGroupChannels( + channels: Channel[], + groupKey?: string + ): EnrichedChannel[] { + // If groupKey provided, use memoized map + if (groupKey !== undefined) { + return this.enrichedGroupChannelsMap().get(groupKey) || []; + } + // Fallback for direct channel array (shouldn't happen with proper template usage) + const epgMap = this.channelEpgMap(); return channels.map((channel) => { const channelId = channel?.tvg?.id?.trim() || channel?.name?.trim(); const epgProgram = channelId ? epgMap.get(channelId) : null;