feat(xtream): add live tv channel sorting functionality

This commit is contained in:
4gray committed 2026-02-13 00:17:17 +01:00
1 parent ba3247b782
commit e3bc450803
5 files changed
+82 -4

No files matched your search

@@ -14,7 +14,37 @@
<button mat-icon-button (click)="backToCategories()">
<mat-icon>chevron_left</mat-icon>
</button>
<h2>{{ selectedCategoryInfo()?.name }} ({{ selectedCategoryInfo()?.count }})</h2>
<h2 class="category-title">
{{ selectedCategoryInfo()?.name }} ({{ selectedCategoryInfo()?.count }})
</h2>
<button
mat-icon-button
[matMenuTriggerFor]="channelSortMenu"
[matTooltip]="'Sort: ' + liveChannelSortLabel()"
aria-label="Sort channels"
>
<mat-icon>sort_by_alpha</mat-icon>
</button>
<mat-menu #channelSortMenu="matMenu">
<button mat-menu-item (click)="setLiveChannelSortMode('server')">
<span>Server Order</span>
@if (liveChannelSortMode() === 'server') {
<mat-icon>check</mat-icon>
}
</button>
<button mat-menu-item (click)="setLiveChannelSortMode('name-asc')">
<span>Name A-Z</span>
@if (liveChannelSortMode() === 'name-asc') {
<mat-icon>check</mat-icon>
}
</button>
<button mat-menu-item (click)="setLiveChannelSortMode('name-desc')">
<span>Name Z-A</span>
@if (liveChannelSortMode() === 'name-desc') {
<mat-icon>check</mat-icon>
}
</button>
</mat-menu>
} @else {
<app-playlist-switcher
class="switcher"
@@ -32,7 +62,10 @@
}
</div>
@if (selectedCategoryId()) {
<app-portal-channels-list (playClicked)="playLive($event)" />
<app-portal-channels-list
[sortMode]="liveChannelSortMode()"
(playClicked)="playLive($event)"
/>
} @else {
<app-category-view
[items]="categories()"
@@ -70,6 +70,11 @@
}
}
.category-title {
flex: 1;
margin: 0;
}
.no-channel-selected {
padding: 16px;
margin: 0;
@@ -7,6 +7,7 @@ import {
inject,
Injector,
OnInit,
signal,
viewChild,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
@@ -16,6 +17,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
@@ -34,6 +36,8 @@ import { PortalChannelsListComponent } from '../portal-channels-list/portal-chan
import { FavoritesService } from '../services/favorites.service';
import { XtreamStore } from '../stores/xtream.store';
type LiveChannelSortMode = 'server' | 'name-asc' | 'name-desc';
@Component({
selector: 'app-live-stream-layout',
templateUrl: './live-stream-layout.component.html',
@@ -47,6 +51,7 @@ import { XtreamStore } from '../stores/xtream.store';
MatIconButton,
MatInputModule,
MatListModule,
MatMenuModule,
MatTooltipModule,
/* MpvPlayerBarComponent, */
PlaylistSwitcherComponent,
@@ -69,6 +74,13 @@ export class LiveStreamLayoutComponent implements OnInit {
readonly currentPlaylist = this.xtreamStore.currentPlaylist;
readonly epgItems = this.xtreamStore.epgItems;
readonly selectedCategoryId = this.xtreamStore.selectedCategoryId;
readonly liveChannelSortMode = signal<LiveChannelSortMode>('server');
readonly liveChannelSortLabel = computed(() => {
const mode = this.liveChannelSortMode();
if (mode === 'name-asc') return 'Name A-Z';
if (mode === 'name-desc') return 'Name Z-A';
return 'Server Order';
});
readonly selectedCategoryInfo = computed(() => {
const categoryId = this.selectedCategoryId();
@@ -173,4 +185,8 @@ export class LiveStreamLayoutComponent implements OnInit {
}
});
}
setLiveChannelSortMode(mode: LiveChannelSortMode): void {
this.liveChannelSortMode.set(mode);
}
}
@@ -18,7 +18,7 @@
>
<div
*cdkVirtualFor="
let item of channels() | filterBy: searchString() : 'title';
let item of sortedChannels() | filterBy: searchString() : 'title';
trackBy: trackBy;
templateCacheSize: 0
"
@@ -7,7 +7,9 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
computed,
inject,
input,
OnDestroy,
output,
signal,
@@ -37,6 +39,8 @@ interface EpgProgram {
stop_timestamp: string;
}
type LiveChannelSortMode = 'server' | 'name-asc' | 'name-desc';
@Component({
selector: 'app-portal-channels-list',
templateUrl: './portal-channels-list.component.html',
@@ -54,12 +58,32 @@ interface EpgProgram {
})
export class PortalChannelsListComponent implements AfterViewInit, OnDestroy {
readonly playClicked = output<any>();
readonly sortMode = input<LiveChannelSortMode>('server');
readonly xtreamStore = inject(XtreamStore);
private readonly favoritesService = inject(FavoritesService);
private readonly epgQueueService = inject(EpgQueueService);
private readonly route = inject(ActivatedRoute);
readonly channels = this.xtreamStore.selectItemsFromSelectedCategory;
readonly sortedChannels = computed(() => {
const mode = this.sortMode();
const channels = this.channels();
if (mode === 'server') {
return channels;
}
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base',
});
return [...channels].sort((a: any, b: any) => {
const titleA = a.title ?? a.name ?? '';
const titleB = b.title ?? b.name ?? '';
const result = collator.compare(titleA, titleB);
return mode === 'name-asc' ? result : -result;
});
});
favorites = new Map<number, boolean>();
searchString = signal<string>('');
@@ -117,7 +141,7 @@ export class PortalChannelsListComponent implements AfterViewInit, OnDestroy {
vp.renderedRangeStream
.pipe(debounceTime(300))
.subscribe((range) => {
const visibleChannels = this.channels().slice(
const visibleChannels = this.sortedChannels().slice(
range.start,
range.end
);