feat(xtream): add category search functionality and update selection state

This commit is contained in:
4gray committed 2026-02-13 00:45:42 +01:00
1 parent bcdaff368a
commit f752ba2b85
4 files changed
+101 -11

No files matched your search

@@ -24,6 +24,7 @@ export interface SelectionState {
page: number;
limit: number;
contentSortMode: XtreamCategorySortMode;
categorySearchTerm: string;
isLoadingDetails: boolean;
detailsError: string | null;
}
@@ -38,6 +39,7 @@ const initialSelectionState: SelectionState = {
page: 0,
limit: Number(localStorage.getItem('xtream-page-size') ?? 25),
contentSortMode: 'date-desc',
categorySearchTerm: '',
isLoadingDetails: false,
detailsError: null,
};
@@ -88,6 +90,18 @@ export function withSelection() {
});
};
const filterBySearchTerm = (items: any[], searchTerm: string): any[] => {
const normalized = searchTerm.trim().toLocaleLowerCase();
if (!normalized) {
return items;
}
return items.filter((item: any) => {
const title = (item.title ?? item.name ?? '').toString();
return title.toLocaleLowerCase().includes(normalized);
});
};
// Memoized sorted content - only recalculates when content/type changes
const sortedContent = computed(() => {
const categoryType = store.selectedContentType();
@@ -166,6 +180,7 @@ export function withSelection() {
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const sortMode = store.contentSortMode();
const searchTerm = store.categorySearchTerm();
// Access parent store content (from withContent)
const storeAny = store as any;
@@ -183,6 +198,10 @@ export function withSelection() {
Number(item.category_id) === categoryId
);
if (categoryType === 'vod' || categoryType === 'series') {
filteredContent = filterBySearchTerm(
filteredContent,
searchTerm
);
filteredContent = sortByMode(
filteredContent,
sortMode,
@@ -203,6 +222,7 @@ export function withSelection() {
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const sortMode = store.contentSortMode();
const searchTerm = store.categorySearchTerm();
// Access parent store content (from withContent)
const storeAny = store as any;
@@ -222,7 +242,7 @@ export function withSelection() {
);
if (categoryType === 'vod' || categoryType === 'series') {
return sortByMode(
filteredContent,
filterBySearchTerm(filteredContent, searchTerm),
sortMode,
categoryType
);
@@ -237,6 +257,7 @@ export function withSelection() {
getTotalPages: computed(() => {
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const searchTerm = store.categorySearchTerm();
// Access parent store content (from withContent)
const storeAny = store as any;
@@ -249,10 +270,14 @@ export function withSelection() {
let totalItems = 0;
if (categoryId) {
totalItems = content.filter(
let filtered = content.filter(
(item: any) =>
Number(item.category_id) === categoryId
).length;
);
if (categoryType === 'vod' || categoryType === 'series') {
filtered = filterBySearchTerm(filtered, searchTerm);
}
totalItems = filtered.length;
} else {
totalItems = content.length;
}
@@ -320,6 +345,7 @@ export function withSelection() {
selectedContentType: type,
selectedCategoryId: null,
page: 0,
categorySearchTerm: '',
});
},
@@ -337,6 +363,7 @@ export function withSelection() {
patchState(store, {
selectedCategoryId: newCategoryId,
page: 0,
categorySearchTerm: '',
});
}
},
@@ -387,6 +414,16 @@ export function withSelection() {
});
},
/**
* Set selected category search term
*/
setCategorySearchTerm(term: string): void {
patchState(store, {
categorySearchTerm: term,
page: 0,
});
},
/**
* Reset selection state
*/
@@ -42,14 +42,41 @@
</h2>
</div>
@if (canShowSortMenu()) {
<button
mat-icon-button
[matMenuTriggerFor]="xtreamSortMenu"
[matTooltip]="'Sort: ' + contentSortLabel()"
aria-label="Sort content"
>
<mat-icon>sort</mat-icon>
</button>
<div class="header-actions">
<mat-form-field
class="category-search"
appearance="outline"
subscriptSizing="dynamic"
>
<mat-icon matPrefix>search</mat-icon>
<input
matInput
type="search"
spellcheck="false"
[ngModel]="categorySearchTerm()"
(ngModelChange)="onCategorySearchChange($event)"
placeholder="Search in category"
/>
@if (categorySearchTerm()) {
<button
mat-icon-button
matSuffix
aria-label="Clear search"
(click)="clearCategorySearch()"
>
<mat-icon>close</mat-icon>
</button>
}
</mat-form-field>
<button
mat-icon-button
[matMenuTriggerFor]="xtreamSortMenu"
[matTooltip]="'Sort: ' + contentSortLabel()"
aria-label="Sort content"
>
<mat-icon>sort</mat-icon>
</button>
</div>
<mat-menu #xtreamSortMenu="matMenu">
<button mat-menu-item (click)="setContentSortMode('date-desc')">
<span>Date Added (Latest First)</span>
@@ -53,6 +53,17 @@
min-width: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
app-region: no-drag;
}
.category-search {
width: 240px;
}
.scrollable-content {
flex: 1;
overflow-y: auto;
@@ -1,7 +1,10 @@
import { Component, computed, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatIconButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
@@ -24,8 +27,11 @@ const XTREAM_CATEGORY_SORT_STORAGE_KEY = 'xtream-category-sort-mode';
styleUrls: ['./xtream-main-container.component.scss', './sidebar.scss'],
imports: [
CategoryViewComponent,
FormsModule,
MatFormFieldModule,
MatIcon,
MatIconButton,
MatInputModule,
MatMenuModule,
MatTooltipModule,
PlaylistSwitcherComponent,
@@ -46,6 +52,7 @@ export class XtreamMainContainerComponent implements OnInit {
readonly currentPlaylist = this.xtreamStore.currentPlaylist;
readonly selectedCategoryId = this.xtreamStore.selectedCategoryId;
readonly contentSortMode = this.xtreamStore.contentSortMode;
readonly categorySearchTerm = this.xtreamStore.categorySearchTerm;
readonly canShowSortMenu = computed(() => {
const hasCategorySelected = this.selectedCategoryId() != null;
const type = this.xtreamStore.selectedContentType();
@@ -165,4 +172,12 @@ export class XtreamMainContainerComponent implements OnInit {
this.xtreamStore.setContentSortMode(mode);
localStorage.setItem(XTREAM_CATEGORY_SORT_STORAGE_KEY, mode);
}
onCategorySearchChange(term: string): void {
this.xtreamStore.setCategorySearchTerm(term);
}
clearCategorySearch(): void {
this.xtreamStore.setCategorySearchTerm('');
}
}