From ed6302c05956365ecc21a385f4d5bd3cee0d4fc8 Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 6 May 2026 00:14:52 +0200 Subject: [PATCH] feat(m3u): add channel list sorting controls Entire-Checkpoint: f957cd9849e0 --- docs/architecture/m3u-playlist-module.md | 27 +++- .../video-player/video-player.component.html | 2 +- .../all-channels-view.component.html | 51 ++++++ .../all-channels-view.component.scss | 41 +++++ .../all-channels-view.component.spec.ts | 150 ++++++++++++++++-- .../all-channels-view.component.ts | 49 ++++-- .../channel-list-container.component.html | 15 +- .../channel-list-sort.util.ts | 59 +++++++ .../groups-view/groups-view.component.html | 24 ++- .../groups-view/groups-view.component.spec.ts | 6 +- .../groups-view/groups-view.component.ts | 26 +-- 11 files changed, 387 insertions(+), 63 deletions(-) create mode 100644 libs/ui/components/src/lib/channel-list-container/channel-list-sort.util.ts diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index 92c36cc86..5b4c848f7 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -9,7 +9,7 @@ The M3U playlist module provides: - Channel list display with virtual scrolling (90,000+ channels support) - EPG (Electronic Program Guide) integration - Favorites management with drag-and-drop reordering -- Channel grouping and search +- Channel grouping, search, and per-list channel sorting - Per-playlist group visibility management in the groups view - Video playback with multiple player backends @@ -209,6 +209,19 @@ channel-list-container/ hidden by separating "playlist has no groups" from "no visible/search-matching groups" empty states. +### Channel Sorting + +- `AllChannelsViewComponent` owns sorting for the all-channels list and + persists the selected mode under `m3u-all-channels-sort-mode`. +- `GroupsViewComponent` owns sorting for the selected group's channel pane and + persists the selected mode under `m3u-groups-channel-sort-mode`. +- Both views support three modes: `Playlist Order`, `Name A-Z`, and `Name Z-A`. + `Playlist Order` is the default and preserves the original channel order from + the M3U playlist. +- Sorting is applied after the current search filter and before virtual-scroll + rendering. Playlist order avoids cloning the full list when no search term is + active. + ### EnrichedChannel Pattern For performance optimization, channels are pre-enriched with EPG data: @@ -252,25 +265,25 @@ EPG lookup keys use the same precedence in both program and icon paths: ### Tab Components -#### AllChannelsTabComponent +#### AllChannelsViewComponent - **Inputs**: `channels`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds` - **Outputs**: `channelSelected`, `favoriteToggled` -- **Features**: Search with 300ms debounce, virtual scrolling, no-results placeholder +- **Features**: Workspace search, persisted channel sorting, virtual scrolling, no-results placeholder -#### GroupsTabComponent +#### GroupsViewComponent - **Inputs**: Same as AllChannelsTab + `groupedChannels` - **Outputs**: `channelSelected`, `favoriteToggled` -- **Features**: Expansion panels, infinite scroll with IntersectionObserver, lazy loading +- **Features**: Resizable groups rail, local group search, group visibility management, persisted selected-group channel sorting -#### FavoritesTabComponent +#### FavoritesViewComponent - **Inputs**: `favorites`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl` - **Outputs**: `channelSelected`, `favoriteToggled`, `favoritesReordered` - **Features**: Drag-and-drop reordering with CDK DragDrop, read-only channel details context menu -#### RecentTabComponent +#### RecentViewComponent - **Inputs**: recent channels, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl` - **Outputs**: `channelSelected`, `favoriteToggled`, `recentItemRemoved` diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html index 947efd1f1..968868227 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html @@ -10,7 +10,7 @@ (widthChange)="onSidebarWidthChange($event)" (resizeEnd)="onSidebarResizeEnd($event)" > - @if (activeView() !== 'groups') { + @if (activeView() !== 'groups' && activeView() !== 'all') { + + + + + + + + + +
+
@if (filteredChannels().length === 0 && searchTerm()) {
diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss index 42081740c..e74c10efb 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss @@ -32,6 +32,47 @@ } } +.all-channels-header { + padding: 12px 16px 8px; + flex-shrink: 0; +} + +.all-channels-header__top { + display: flex; + align-items: center; + gap: 8px; + + h2 { + flex: 1; + min-width: 0; + overflow: hidden; + margin: 0; + color: var(--mat-sys-on-surface); + font-size: 0.94rem; + font-weight: 600; + letter-spacing: 0; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.all-channels-sort-button { + flex: 0 0 auto; +} + +.all-channels-sidebar-toggle { + flex: 0 0 auto; + margin-right: -4px; +} + +.all-channels-divider { + height: 1px; + background: var(--mat-sys-outline-variant); + margin: 0 0 6px; + opacity: 0.6; + flex-shrink: 0; +} + #all-channels { flex: 1; min-height: 0; diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts index 9eec83e75..bf72f5b7f 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts @@ -6,6 +6,8 @@ import { Channel } from 'shared-interfaces'; import { ChannelDetailsDialogComponent } from '../channel-details-dialog/channel-details-dialog.component'; import { AllChannelsViewComponent } from './all-channels-view.component'; +const ALL_CHANNELS_SORT_STORAGE_KEY = 'm3u-all-channels-sort-mode'; + function createChannel(id: string, name: string, url: string): Channel { return { epgParams: '', @@ -47,6 +49,8 @@ describe('AllChannelsViewComponent', () => { ); beforeEach(async () => { + localStorage.removeItem(ALL_CHANNELS_SORT_STORAGE_KEY); + dialog = { open: jest.fn(), }; @@ -71,7 +75,10 @@ describe('AllChannelsViewComponent', () => { fixture.componentRef.setInput('channels', [primaryChannel]); fixture.componentRef.setInput('searchTerm', ''); fixture.componentRef.setInput('channelEpgMap', new Map()); - fixture.componentRef.setInput('channelIconMap', new Map()); + fixture.componentRef.setInput( + 'channelIconMap', + new Map() + ); fixture.componentRef.setInput('progressTick', 0); fixture.componentRef.setInput('shouldShowEpg', false); fixture.componentRef.setInput('itemSize', 48); @@ -79,18 +86,133 @@ describe('AllChannelsViewComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + fixture.destroy(); + localStorage.removeItem(ALL_CHANNELS_SORT_STORAGE_KEY); + }); + + it('defaults to playlist order when no saved sort mode exists', () => { + expect(component.allChannelsSortMode()).toBe('server'); + expect(component.allChannelsSortLabel()).toBe('Playlist Order'); + }); + + it('restores a saved valid sort mode and ignores invalid stored values', () => { + fixture.destroy(); + localStorage.setItem(ALL_CHANNELS_SORT_STORAGE_KEY, 'name-asc'); + + fixture = TestBed.createComponent(AllChannelsViewComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('channels', [primaryChannel]); + fixture.componentRef.setInput('searchTerm', ''); + fixture.componentRef.setInput('channelEpgMap', new Map()); + fixture.componentRef.setInput( + 'channelIconMap', + new Map() + ); + fixture.componentRef.setInput('progressTick', 0); + fixture.componentRef.setInput('shouldShowEpg', false); + fixture.componentRef.setInput('itemSize', 48); + fixture.componentRef.setInput('favoriteIds', new Set()); + fixture.detectChanges(); + + expect(component.allChannelsSortMode()).toBe('name-asc'); + expect(component.allChannelsSortLabel()).toBe('Name A-Z'); + + fixture.destroy(); + localStorage.setItem(ALL_CHANNELS_SORT_STORAGE_KEY, 'invalid'); + + fixture = TestBed.createComponent(AllChannelsViewComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('channels', [primaryChannel]); + fixture.componentRef.setInput('searchTerm', ''); + fixture.componentRef.setInput('channelEpgMap', new Map()); + fixture.componentRef.setInput( + 'channelIconMap', + new Map() + ); + fixture.componentRef.setInput('progressTick', 0); + fixture.componentRef.setInput('shouldShowEpg', false); + fixture.componentRef.setInput('itemSize', 48); + fixture.componentRef.setInput('favoriteIds', new Set()); + fixture.detectChanges(); + + expect(component.allChannelsSortMode()).toBe('server'); + }); + + it('persists sort mode changes', () => { + component.setAllChannelsSortMode('name-desc'); + + expect(component.allChannelsSortMode()).toBe('name-desc'); + expect(localStorage.getItem(ALL_CHANNELS_SORT_STORAGE_KEY)).toBe( + 'name-desc' + ); + }); + + it('sorts all channels by playlist order, name ascending, and name descending', () => { + const alphaSignal = createChannel( + 'sort-1', + 'Alpha Signal', + 'https://example.com/alpha-signal.m3u8' + ); + const zuluVision = createChannel( + 'sort-2', + 'Zulu Vision', + 'https://example.com/zulu-vision.m3u8' + ); + const middleNews = createChannel( + 'sort-3', + 'Middle News', + 'https://example.com/middle-news.m3u8' + ); + + fixture.componentRef.setInput('channels', [ + zuluVision, + alphaSignal, + middleNews, + ]); + fixture.detectChanges(); + + expect( + component.filteredChannels().map((channel) => channel.name) + ).toEqual(['Zulu Vision', 'Alpha Signal', 'Middle News']); + + component.setAllChannelsSortMode('name-asc'); + fixture.detectChanges(); + + expect( + component.filteredChannels().map((channel) => channel.name) + ).toEqual(['Alpha Signal', 'Middle News', 'Zulu Vision']); + + component.setAllChannelsSortMode('name-desc'); + fixture.detectChanges(); + + expect( + component.filteredChannels().map((channel) => channel.name) + ).toEqual(['Zulu Vision', 'Middle News', 'Alpha Signal']); + }); + + it('emits sidebar toggle requests from the inline header action', () => { + const sidebarToggleRequested = jest.fn(); + component.sidebarToggleRequested.subscribe(sidebarToggleRequested); + + const toggleButton = fixture.nativeElement.querySelector( + '.all-channels-sidebar-toggle' + ) as HTMLButtonElement; + + toggleButton.click(); + + expect(sidebarToggleRequested).toHaveBeenCalledTimes(1); + }); + it('stores viewport coordinates for the context menu and opens the dialog for that channel', async () => { const openMenuSpy = jest .spyOn(component.contextMenuTrigger(), 'openMenu') .mockImplementation(); - component.onChannelContextMenu( - primaryChannel, - { - clientX: 144, - clientY: 188, - } as MouseEvent - ); + component.onChannelContextMenu(primaryChannel, { + clientX: 144, + clientY: 188, + } as MouseEvent); await Promise.resolve(); expect(component.contextMenuChannel()).toBe(primaryChannel); @@ -129,9 +251,9 @@ describe('AllChannelsViewComponent', () => { ); fixture.detectChanges(); - expect(component.getLogoForChannel(component.filteredChannels()[0])).toBe( - playlistLogo - ); + expect( + component.getLogoForChannel(component.filteredChannels()[0]) + ).toBe(playlistLogo); }); it('falls back to the EPG icon using tvg-name when tvg-id and playlist logo are missing', () => { @@ -152,8 +274,8 @@ describe('AllChannelsViewComponent', () => { ); fixture.detectChanges(); - expect(component.getLogoForChannel(component.filteredChannels()[0])).toBe( - 'https://example.com/guide-news.png' - ); + expect( + component.getLogoForChannel(component.filteredChannels()[0]) + ).toBe('https://example.com/guide-news.png'); }); }); diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts index 4c6e87b68..d7b5f21f4 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts @@ -10,16 +10,27 @@ import { signal, viewChild, } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslatePipe } from '@ngx-translate/core'; import { resolveChannelEpgLookupKey } from 'm3u-state'; import { Channel, EpgProgram } from 'shared-interfaces'; +import { + PlaylistChannelSortMode, + getPlaylistChannelSortModeLabel, + persistPlaylistChannelSortMode, + restorePlaylistChannelSortMode, + sortPlaylistChannelItems, +} from '../channel-list-sort.util'; import { resolveChannelLogo } from '../channel-logo-fallback.util'; import { ChannelDetailsDialogComponent } from '../channel-details-dialog/channel-details-dialog.component'; import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component'; +const ALL_CHANNELS_SORT_STORAGE_KEY = 'm3u-all-channels-sort-mode'; + /** * Per-channel EPG metadata stored in a side-car map keyed by EPG lookup key. * Replaces the older EnrichedChannel pattern that spread-cloned every channel @@ -37,8 +48,10 @@ export interface ChannelEpgMetadata { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ChannelListItemComponent, + MatButtonModule, MatIconModule, MatMenuModule, + MatTooltipModule, ScrollingModule, TranslatePipe, ], @@ -46,9 +59,8 @@ export interface ChannelEpgMetadata { export class AllChannelsViewComponent { private readonly dialog = inject(MatDialog); - readonly contextMenuTrigger = viewChild.required( - 'contextMenuTrigger' - ); + readonly contextMenuTrigger = + viewChild.required('contextMenuTrigger'); /** All channels (will be filtered by search) */ readonly channels = input.required(); @@ -82,24 +94,36 @@ export class AllChannelsViewComponent { event: MouseEvent; }>(); + /** Emits when the user clicks the inline collapse toggle in the list header */ + readonly sidebarToggleRequested = output(); + readonly contextMenuChannel = signal(null); readonly contextMenuPosition = signal({ x: '0px', y: '0px', }); + readonly allChannelsSortMode = signal( + restorePlaylistChannelSortMode(ALL_CHANNELS_SORT_STORAGE_KEY) + ); + readonly allChannelsSortLabel = computed(() => + getPlaylistChannelSortModeLabel(this.allChannelsSortMode()) + ); /** - * Filtered channels — just a subset reference, no cloning. - * Recomputes only when the source list or the search term changes. + * Filtered and sorted channels. Playlist order keeps the original input + * reference when there is no search term, so large lists avoid cloning. */ readonly filteredChannels = computed(() => { const term = this.searchTerm().trim().toLowerCase(); const channels = this.channels(); - if (!term) { - return channels; - } - return channels.filter((ch) => - ch.name?.toLowerCase().includes(term) + const filteredChannels = term + ? channels.filter((ch) => ch.name?.toLowerCase().includes(term)) + : channels; + + return sortPlaylistChannelItems( + filteredChannels, + this.allChannelsSortMode(), + (channel) => channel?.name ); }); @@ -183,6 +207,11 @@ export class AllChannelsViewComponent { this.favoriteToggled.emit({ channel, event }); } + setAllChannelsSortMode(mode: PlaylistChannelSortMode): void { + this.allChannelsSortMode.set(mode); + persistPlaylistChannelSortMode(ALL_CHANNELS_SORT_STORAGE_KEY, mode); + } + onChannelContextMenu(channel: Channel, event: MouseEvent): void { this.contextMenuChannel.set(channel); this.contextMenuPosition.set({ 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 344f4055b..4d0fb6267 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 @@ -1,16 +1,18 @@ @if (channelsLoading()) { } @else if (displayedChannels().length) { - @if (viewTitle() && activeView() !== 'groups') { + @if (viewTitle() && activeView() !== 'groups' && activeView() !== 'all') {

{{ viewTitle() | translate }}

@if ( - activeView() === 'recent' && - recentChannelItems().length > 0 + activeView() === 'recent' && recentChannelItems().length > 0 ) { }
@@ -32,6 +34,7 @@ [favoriteIds]="favoriteIds()" (channelSelected)="onChannelSelected($event)" (favoriteToggled)="onFavoriteToggled($event)" + (sidebarToggleRequested)="sidebarToggleRequested.emit()" /> } @case ('groups') { @@ -48,9 +51,7 @@ [sidebarWidth]="sidebarWidth()" (channelSelected)="onChannelSelected($event)" (favoriteToggled)="onFavoriteToggled($event)" - (hiddenGroupTitlesChanged)=" - onHiddenGroupTitlesChanged($event) - " + (hiddenGroupTitlesChanged)="onHiddenGroupTitlesChanged($event)" (sidebarWidthRequested)="onSidebarWidthRequested($event)" (sidebarWidthRequestEnded)="onSidebarWidthRequestEnded($event)" (sidebarToggleRequested)="sidebarToggleRequested.emit()" diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-sort.util.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-sort.util.ts new file mode 100644 index 000000000..952cd5b5b --- /dev/null +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-sort.util.ts @@ -0,0 +1,59 @@ +const CHANNEL_LIST_SORT_COLLATOR = new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', +}); + +export type PlaylistChannelSortMode = 'server' | 'name-asc' | 'name-desc'; + +export function isPlaylistChannelSortMode( + value: unknown +): value is PlaylistChannelSortMode { + return value === 'server' || value === 'name-asc' || value === 'name-desc'; +} + +export function restorePlaylistChannelSortMode( + storageKey: string, + fallback: PlaylistChannelSortMode = 'server' +): PlaylistChannelSortMode { + const storedValue = localStorage.getItem(storageKey); + return isPlaylistChannelSortMode(storedValue) ? storedValue : fallback; +} + +export function persistPlaylistChannelSortMode( + storageKey: string, + mode: PlaylistChannelSortMode +): void { + localStorage.setItem(storageKey, mode); +} + +export function getPlaylistChannelSortModeLabel( + mode: PlaylistChannelSortMode +): string { + if (mode === 'name-asc') { + return 'Name A-Z'; + } + + if (mode === 'name-desc') { + return 'Name Z-A'; + } + + return 'Playlist Order'; +} + +export function sortPlaylistChannelItems( + items: readonly T[], + mode: PlaylistChannelSortMode, + getDisplayName: (item: T) => string | null | undefined +): readonly T[] { + if (mode === 'server') { + return items; + } + + return [...items].sort((a, b) => { + const result = CHANNEL_LIST_SORT_COLLATOR.compare( + getDisplayName(a) ?? '', + getDisplayName(b) ?? '' + ); + return mode === 'name-asc' ? result : -result; + }); +} diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html index fc2ac3a05..fdfa6e283 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html @@ -44,7 +44,9 @@ type="button" mat-icon-button class="groups-nav-action groups-nav-action--search" - [matTooltip]="'CHANNELS.SEARCH_GROUPS' | translate" + [matTooltip]=" + 'CHANNELS.SEARCH_GROUPS' | translate + " [attr.aria-pressed]=" isGroupSearchOpen() ? 'true' : 'false' " @@ -59,7 +61,9 @@ type="button" mat-icon-button class="groups-nav-action groups-nav-action--manage" - [matTooltip]="'CHANNELS.MANAGE_GROUPS' | translate" + [matTooltip]=" + 'CHANNELS.MANAGE_GROUPS' | translate + " [attr.aria-label]=" 'CHANNELS.MANAGE_GROUPS' | translate " @@ -139,7 +143,9 @@
{{ - hasSearchQuery() ? 'search_off' : 'folder_off' + hasSearchQuery() + ? 'search_off' + : 'folder_off' }}
@@ -153,7 +159,8 @@ @if (hasSearchQuery()) {
{{ - 'CHANNELS.TRY_DIFFERENT_SEARCH' | translate + 'CHANNELS.TRY_DIFFERENT_SEARCH' + | translate }}
} @@ -206,7 +213,8 @@ mat-menu-item (click)="setGroupChannelSortMode('server')" > - Server Order + drag_indicator + Playlist Order @if (groupChannelSortMode() === 'server') { check } @@ -215,6 +223,7 @@ mat-menu-item (click)="setGroupChannelSortMode('name-asc')" > + sort_by_alpha Name A-Z @if (groupChannelSortMode() === 'name-asc') { check @@ -224,6 +233,7 @@ mat-menu-item (click)="setGroupChannelSortMode('name-desc')" > + sort_by_alpha Name Z-A @if (groupChannelSortMode() === 'name-desc') { check @@ -286,9 +296,7 @@
- {{ - hasSearchQuery() ? 'search_off' : 'visibility_off' - }} + {{ hasSearchQuery() ? 'search_off' : 'visibility_off' }}
{{ diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts index c111d9b06..146204f3c 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts @@ -208,9 +208,9 @@ describe('GroupsViewComponent', () => { ]); }); - it('defaults to server order when no saved sort mode exists', () => { + it('defaults to playlist order when no saved sort mode exists', () => { expect(component.groupChannelSortMode()).toBe('server'); - expect(component.groupChannelSortLabel()).toBe('Server Order'); + expect(component.groupChannelSortLabel()).toBe('Playlist Order'); }); it('restores a saved valid sort mode and ignores invalid stored values', () => { @@ -264,7 +264,7 @@ describe('GroupsViewComponent', () => { ); }); - it('sorts selected group channels by server order, name ascending, and name descending', () => { + it('sorts selected group channels by playlist order, name ascending, and name descending', () => { const alphaSignal = createChannel( 'sort-1', 'Alpha Signal', diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts index fd90b0de5..9ab618ade 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts @@ -21,14 +21,14 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslatePipe } from '@ngx-translate/core'; import { resolveChannelEpgLookupKey } from 'm3u-state'; import { Channel, EpgProgram } from 'shared-interfaces'; -import { - PortalChannelSortMode, - getPortalChannelSortModeLabel, - persistPortalChannelSortMode, - restorePortalChannelSortMode, - sortPortalChannelItems, -} from '@iptvnator/portal/shared/util'; import { ChannelEpgMetadata } from '../all-channels-view/all-channels-view.component'; +import { + PlaylistChannelSortMode, + getPlaylistChannelSortModeLabel, + persistPlaylistChannelSortMode, + restorePlaylistChannelSortMode, + sortPlaylistChannelItems, +} from '../channel-list-sort.util'; import { resolveChannelLogo } from '../channel-logo-fallback.util'; import { ChannelDetailsDialogComponent } from '../channel-details-dialog/channel-details-dialog.component'; import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component'; @@ -125,11 +125,11 @@ export class GroupsViewComponent { readonly isGroupSearchOpen = signal(false); readonly localGroupSearchTerm = signal(''); readonly selectedGroupKey = signal(null); - readonly groupChannelSortMode = signal( - restorePortalChannelSortMode(GROUP_CHANNEL_SORT_STORAGE_KEY) + readonly groupChannelSortMode = signal( + restorePlaylistChannelSortMode(GROUP_CHANNEL_SORT_STORAGE_KEY) ); readonly groupChannelSortLabel = computed(() => - getPortalChannelSortModeLabel(this.groupChannelSortMode()) + getPlaylistChannelSortModeLabel(this.groupChannelSortMode()) ); readonly hasSearchQuery = computed( () => @@ -329,7 +329,7 @@ export class GroupsViewComponent { return []; } - return sortPortalChannelItems( + return sortPlaylistChannelItems( group.channels, sortMode, (channel) => channel?.name @@ -439,9 +439,9 @@ export class GroupsViewComponent { }); } - setGroupChannelSortMode(mode: PortalChannelSortMode): void { + setGroupChannelSortMode(mode: PlaylistChannelSortMode): void { this.groupChannelSortMode.set(mode); - persistPortalChannelSortMode(GROUP_CHANNEL_SORT_STORAGE_KEY, mode); + persistPlaylistChannelSortMode(GROUP_CHANNEL_SORT_STORAGE_KEY, mode); } onGroupsNavResizeStart(): void {