mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
feat(xtream): implement content sorting functionality with user-selectable sort modes
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user