From fb1104f6b7652e6658c861984086ee8f3ddd9c50 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:52:42 +0100 Subject: [PATCH] feat(category-management): show category item counts --- .../category-management-dialog.component.html | 1 + .../category-management-dialog.component.scss | 6 +++ .../category-management-dialog.component.ts | 7 +++ .../category-view/category-view.component.ts | 4 +- .../live-stream-layout.component.ts | 1 + .../stores/features/with-selection.feature.ts | 52 +++++++++++++------ 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html index 781b0e08b..589d8f41e 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html @@ -59,6 +59,7 @@ > {{ category.name }} + ({{ getItemCount(category) }}) } @empty {
diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss index 079ba4d9f..f5026434d 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss @@ -79,6 +79,12 @@ text-overflow: ellipsis; } +.item-count { + opacity: 0.6; + font-size: 12px; + color: var(--mat-sys-on-surface-variant); +} + .empty-message { padding: 24px; text-align: center; diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts index 38ff9f639..373ca23e3 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts @@ -18,6 +18,7 @@ import { DatabaseService, XCategoryFromDb } from 'services'; export interface CategoryManagementDialogData { playlistId: string; contentType: 'live' | 'vod' | 'series'; + itemCounts: Map; } interface CategoryWithSelection extends XCategoryFromDb { @@ -104,6 +105,12 @@ export class CategoryManagementDialogComponent implements OnInit { this.searchTerm.set(''); } + getItemCount(category: CategoryWithSelection): number { + // content.category_id references categories.id (internal DB id), not xtream_id + const categoryId = Number(category.id); + return this.data.itemCounts.get(categoryId) ?? 0; + } + private getDbType(): 'live' | 'movies' | 'series' { switch (this.data.contentType) { case 'live': diff --git a/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts b/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts index 35e73073f..ed9f9eb32 100644 --- a/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts @@ -32,7 +32,9 @@ export class CategoryViewComponent { } getItemCount(item: XtreamCategory): number { - const itemId = Number((item as any).category_id ?? item.id); + // content.category_id references categories.id (internal DB id) + // For DB format categories, use id; for API format, use category_id + const itemId = Number(item.id ?? (item as any).category_id); return this.itemCounts().get(itemId) ?? 0; } } diff --git a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts index b8ff9855c..f86d29464 100644 --- a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts +++ b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts @@ -118,6 +118,7 @@ export class LiveStreamLayoutComponent implements OnInit { data: { playlistId, contentType, + itemCounts: this.categoryItemCounts(), }, width: '500px', maxHeight: '80vh', diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts index d86e80c13..1dc659b59 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts @@ -50,7 +50,9 @@ export function withSelection() { */ getSelectedCategory: computed(() => { const categoryId = store.selectedCategoryId(); - if (!categoryId) return null; + if (!categoryId) { + return { id: 0, name: 'All Items', type: store.selectedContentType() }; + } // Access parent store categories (from withContent) const storeAny = store as any; @@ -98,9 +100,6 @@ export function withSelection() { const startIndex = store.page() * store.limit(); const endIndex = startIndex + store.limit(); const categoryId = store.selectedCategoryId(); - - if (!categoryId) return []; - const categoryType = store.selectedContentType(); // Access parent store content (from withContent) @@ -112,9 +111,20 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; - return content - .filter((item: any) => Number(item.category_id) === categoryId) - .slice(startIndex, endIndex); + let filteredContent = content; + if (categoryId) { + filteredContent = content.filter((item: any) => Number(item.category_id) === categoryId); + } else { + // Filter nothing (all items), sort by added + // Clone to avoid mutation + filteredContent = [...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 filteredContent.slice(startIndex, endIndex); }), /** @@ -124,8 +134,6 @@ export function withSelection() { const categoryId = store.selectedCategoryId(); const categoryType = store.selectedContentType(); - if (!categoryId) return []; - // Access parent store content (from withContent) const storeAny = store as any; const content = @@ -135,6 +143,15 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; + if (!categoryId) { + // Return all items sorted by added + 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 content.filter( (item: any) => Number(item.category_id) === categoryId ); @@ -145,8 +162,6 @@ export function withSelection() { */ getTotalPages: computed(() => { const categoryId = store.selectedCategoryId(); - if (!categoryId) return 0; - const categoryType = store.selectedContentType(); // Access parent store content (from withContent) @@ -158,9 +173,14 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; - const totalItems = content.filter( - (item: any) => Number(item.category_id) === categoryId - ).length; + let totalItems = 0; + if (categoryId) { + totalItems = content.filter( + (item: any) => Number(item.category_id) === categoryId + ).length; + } else { + totalItems = content.length; + } return Math.ceil(totalItems / store.limit()); }), @@ -230,9 +250,9 @@ export function withSelection() { /** * Set the selected category */ - setSelectedCategory(categoryId: number): void { + setSelectedCategory(categoryId: number | null): void { patchState(store, { - selectedCategoryId: Number(categoryId), + selectedCategoryId: categoryId !== null ? Number(categoryId) : null, page: 0, }); },