feat(xtream): implement content sorting functionality with user-selectable sort modes

This commit is contained in:
4gray committed 2026-02-13 00:42:48 +01:00
1 parent 8ef789ac8a
commit bcdaff368a
4 files changed
+155 -22

No files matched your search

@@ -8,6 +8,12 @@ import {
} from '@ngrx/signals';
import { ContentType } from '../../xtream-state';
export type XtreamCategorySortMode =
| 'date-desc'
| 'date-asc'
| 'name-asc'
| 'name-desc';
/**
* Selection state for managing UI selection and pagination
*/
@@ -17,6 +23,7 @@ export interface SelectionState {
selectedItem: any | null;
page: number;
limit: number;
contentSortMode: XtreamCategorySortMode;
isLoadingDetails: boolean;
detailsError: string | null;
}
@@ -30,6 +37,7 @@ const initialSelectionState: SelectionState = {
selectedItem: null,
page: 0,
limit: Number(localStorage.getItem('xtream-page-size') ?? 25),
contentSortMode: 'date-desc',
isLoadingDetails: false,
detailsError: null,
};
@@ -47,6 +55,39 @@ export function withSelection() {
withState<SelectionState>(initialSelectionState),
withComputed((store) => {
const getItemDate = (item: any, categoryType: ContentType): number => {
const value =
categoryType === 'series'
? item.last_modified ?? item.added
: item.added;
return parseInt(value ?? '', 10) || 0;
};
const sortByMode = (
items: any[],
sortMode: XtreamCategorySortMode,
categoryType: ContentType
): any[] => {
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base',
});
return [...items].sort((a: any, b: any) => {
if (sortMode === 'date-desc') {
return getItemDate(b, categoryType) - getItemDate(a, categoryType);
}
if (sortMode === 'date-asc') {
return getItemDate(a, categoryType) - getItemDate(b, categoryType);
}
const titleA = a.title ?? a.name ?? '';
const titleB = b.title ?? b.name ?? '';
const byName = collator.compare(titleA, titleB);
return sortMode === 'name-asc' ? byName : -byName;
});
};
// Memoized sorted content - only recalculates when content/type changes
const sortedContent = computed(() => {
const categoryType = store.selectedContentType();
@@ -58,21 +99,7 @@ export function withSelection() {
? storeAny.vodStreams?.() || []
: storeAny.serialStreams?.() || [];
return [...content].sort((a: any, b: any) => {
const dateA =
parseInt(
categoryType === 'series'
? a.last_modified
: a.added
) || 0;
const dateB =
parseInt(
categoryType === 'series'
? b.last_modified
: b.added
) || 0;
return dateB - dateA;
});
return sortByMode(content, 'date-desc', categoryType);
});
return {
@@ -138,6 +165,7 @@ export function withSelection() {
const endIndex = startIndex + store.limit();
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const sortMode = store.contentSortMode();
// Access parent store content (from withContent)
const storeAny = store as any;
@@ -154,6 +182,13 @@ export function withSelection() {
(item: any) =>
Number(item.category_id) === categoryId
);
if (categoryType === 'vod' || categoryType === 'series') {
filteredContent = sortByMode(
filteredContent,
sortMode,
categoryType
);
}
} else {
filteredContent = sortedContent();
}
@@ -167,6 +202,7 @@ export function withSelection() {
selectItemsFromSelectedCategory: computed(() => {
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const sortMode = store.contentSortMode();
// Access parent store content (from withContent)
const storeAny = store as any;
@@ -181,9 +217,18 @@ export function withSelection() {
return sortedContent();
}
return content.filter(
const filteredContent = content.filter(
(item: any) => Number(item.category_id) === categoryId
);
if (categoryType === 'vod' || categoryType === 'series') {
return sortByMode(
filteredContent,
sortMode,
categoryType
);
}
return filteredContent;
}),
/**
@@ -332,6 +377,16 @@ export function withSelection() {
localStorage.setItem('xtream-page-size', String(limit));
},
/**
* Set category content sort mode
*/
setContentSortMode(mode: XtreamCategorySortMode): void {
patchState(store, {
contentSortMode: mode,
page: 0,
});
},
/**
* Reset selection state
*/
@@ -33,12 +33,50 @@
<div class="main-content">
@if (!xtreamStore.selectedItem() && !xtreamStore.isLoadingDetails()) {
<div class="content-header">
<h2>
<button mat-icon-button disabled>
<mat-icon>category</mat-icon>
<div class="header-left">
<h2>
<button mat-icon-button disabled>
<mat-icon>category</mat-icon>
</button>
{{ getContentLabel() }}
</h2>
</div>
@if (canShowSortMenu()) {
<button
mat-icon-button
[matMenuTriggerFor]="xtreamSortMenu"
[matTooltip]="'Sort: ' + contentSortLabel()"
aria-label="Sort content"
>
<mat-icon>sort</mat-icon>
</button>
{{ getContentLabel() }}
</h2>
<mat-menu #xtreamSortMenu="matMenu">
<button mat-menu-item (click)="setContentSortMode('date-desc')">
<span>Date Added (Latest First)</span>
@if (contentSortMode() === 'date-desc') {
<mat-icon>check</mat-icon>
}
</button>
<button mat-menu-item (click)="setContentSortMode('date-asc')">
<span>Date Added (Oldest First)</span>
@if (contentSortMode() === 'date-asc') {
<mat-icon>check</mat-icon>
}
</button>
<button mat-menu-item (click)="setContentSortMode('name-asc')">
<span>Name A-Z</span>
@if (contentSortMode() === 'name-asc') {
<mat-icon>check</mat-icon>
}
</button>
<button mat-menu-item (click)="setContentSortMode('name-desc')">
<span>Name Z-A</span>
@if (contentSortMode() === 'name-desc') {
<mat-icon>check</mat-icon>
}
</button>
</mat-menu>
}
</div>
}
<div
@@ -30,6 +30,9 @@
.content-header {
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
justify-content: space-between;
app-region: drag;
h2 {
@@ -46,6 +49,10 @@
}
}
.header-left {
min-width: 0;
}
.scrollable-content {
flex: 1;
overflow-y: auto;
@@ -1,7 +1,8 @@
import { Component, inject, OnInit } from '@angular/core';
import { Component, computed, inject, OnInit } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
@@ -13,6 +14,9 @@ import {
} from './category-management-dialog/category-management-dialog.component';
import { CategoryViewComponent } from './category-view/category-view.component';
import { XtreamStore } from './stores/xtream.store';
import { XtreamCategorySortMode } from './stores/features/with-selection.feature';
const XTREAM_CATEGORY_SORT_STORAGE_KEY = 'xtream-category-sort-mode';
@Component({
selector: 'app-xtream-main-container',
@@ -22,6 +26,7 @@ import { XtreamStore } from './stores/xtream.store';
CategoryViewComponent,
MatIcon,
MatIconButton,
MatMenuModule,
MatTooltipModule,
PlaylistSwitcherComponent,
ResizableDirective,
@@ -40,8 +45,31 @@ export class XtreamMainContainerComponent implements OnInit {
readonly categoryItemCounts = this.xtreamStore.getCategoryItemCounts;
readonly currentPlaylist = this.xtreamStore.currentPlaylist;
readonly selectedCategoryId = this.xtreamStore.selectedCategoryId;
readonly contentSortMode = this.xtreamStore.contentSortMode;
readonly canShowSortMenu = computed(() => {
const hasCategorySelected = this.selectedCategoryId() != null;
const type = this.xtreamStore.selectedContentType();
return hasCategorySelected && (type === 'vod' || type === 'series');
});
readonly contentSortLabel = computed(() => {
const mode = this.contentSortMode();
if (mode === 'date-asc') return 'Date Added (Oldest First)';
if (mode === 'name-asc') return 'Name A-Z';
if (mode === 'name-desc') return 'Name Z-A';
return 'Date Added (Latest First)';
});
ngOnInit(): void {
const savedSortMode = localStorage.getItem(XTREAM_CATEGORY_SORT_STORAGE_KEY);
if (
savedSortMode === 'date-desc' ||
savedSortMode === 'date-asc' ||
savedSortMode === 'name-asc' ||
savedSortMode === 'name-desc'
) {
this.xtreamStore.setContentSortMode(savedSortMode);
}
const { categoryId } = this.route.snapshot.params;
if (categoryId)
this.xtreamStore.setSelectedCategory(Number(categoryId));
@@ -132,4 +160,9 @@ export class XtreamMainContainerComponent implements OnInit {
}
});
}
setContentSortMode(mode: XtreamCategorySortMode): void {
this.xtreamStore.setContentSortMode(mode);
localStorage.setItem(XTREAM_CATEGORY_SORT_STORAGE_KEY, mode);
}
}