From c173553d78bf936d4cc374cd81a2fa83d7b84039 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:54:00 +0100 Subject: [PATCH] feat(xtream): add recently added views --- .../events/database/recently-viewed.events.ts | 6 +- .../category-content-view.component.ts | 28 ++++++--- .../navigation/navigation.component.html | 12 ++++ .../navigation/navigation.component.ts | 4 +- .../recently-added.component.html | 57 ++++++++++++++++++ .../recently-added.component.scss | 37 ++++++++++++ .../recently-added.component.ts | 59 +++++++++++++++++++ .../xtream-main-container.component.ts | 42 ++++++++----- .../xtream-shell/xtream-shell.component.ts | 12 ++-- .../web/src/app/xtream-tauri/xtream.routes.ts | 21 +++++++ 10 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html create mode 100644 apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss create mode 100644 apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts diff --git a/apps/electron-backend/src/app/events/database/recently-viewed.events.ts b/apps/electron-backend/src/app/events/database/recently-viewed.events.ts index 9fdf2232e..38d47b769 100644 --- a/apps/electron-backend/src/app/events/database/recently-viewed.events.ts +++ b/apps/electron-backend/src/app/events/database/recently-viewed.events.ts @@ -87,11 +87,7 @@ ipcMain.handle('DB_GET_RECENT_ITEMS', async (event, playlistId: string) => { schema.content, eq(schema.recentlyViewed.contentId, schema.content.id) ) - .innerJoin( - schema.categories, - eq(schema.content.categoryId, schema.categories.id) - ) - .where(eq(schema.categories.playlistId, playlistId)) + .where(eq(schema.recentlyViewed.playlistId, playlistId)) .orderBy(desc(schema.recentlyViewed.viewedAt)) .limit(100); return result; diff --git a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts index 116d891aa..79f550092 100644 --- a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts @@ -53,11 +53,15 @@ export class CategoryContentViewComponent implements OnInit { // Only set category if it's different from the currently selected one // This preserves the page state when navigating back from detail view - if ( - categoryId && - this.store.selectedCategoryId() !== Number(categoryId) - ) { - this.store.setSelectedCategory(categoryId); + if (categoryId) { + if (this.store.selectedCategoryId() !== Number(categoryId)) { + this.store.setSelectedCategory(categoryId); + } + } else { + // No categoryId in route means "All Items" + if (this.store.selectedCategoryId() !== null) { + this.store.setSelectedCategory(null); + } } } @@ -99,9 +103,17 @@ export class CategoryContentViewComponent implements OnInit { if (this.isStalker) { this.store.setSelectedItem(selectedItem); } else { - this.router.navigate([item.xtream_id], { - relativeTo: this.activatedRoute, - }); + // When viewing "Recently Added" (no category selected), include category_id in path + const categoryId = this.store.selectedCategoryId(); + if (categoryId) { + this.router.navigate([item.xtream_id], { + relativeTo: this.activatedRoute, + }); + } else { + this.router.navigate([item.category_id, item.xtream_id], { + relativeTo: this.activatedRoute, + }); + } } } diff --git a/apps/web/src/app/xtream-tauri/navigation/navigation.component.html b/apps/web/src/app/xtream-tauri/navigation/navigation.component.html index f611d27b4..a27f7f634 100644 --- a/apps/web/src/app/xtream-tauri/navigation/navigation.component.html +++ b/apps/web/src/app/xtream-tauri/navigation/navigation.component.html @@ -61,6 +61,18 @@ {{ 'PORTALS.SIDEBAR.LIBRARY' | translate }} --> + @if (!isStalkerPlaylist()) { + + new_releases + + } (); readonly categoryClick = output(); - readonly pageClicked = output<'search' | 'recent' | 'favorites'>(); + readonly pageClicked = output<'search' | 'recent' | 'favorites' | 'recently-added'>(); readonly currentPlaylist = this.store.selectSignal( selectPlaylistById(this.activatedRoute.snapshot.params.id) @@ -113,7 +113,7 @@ export class NavigationComponent { }); } - pageSwitch(page: 'search' | 'recent' | 'favorites') { + pageSwitch(page: 'search' | 'recent' | 'favorites' | 'recently-added') { this.pageClicked.emit(page); this.xtreamStore.setSelectedContentType(undefined); } diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html new file mode 100644 index 000000000..906c9983c --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html @@ -0,0 +1,57 @@ +
+

{{ 'PORTALS.SIDEBAR.RECENTLY_ADDED' | translate }}

+ + @if (recentlyAddedVod().length > 0) { +
+

{{ 'PORTALS.SIDEBAR.MOVIES' | translate }}

+ +
+ } + + @if (recentlyAddedSeries().length > 0) { +
+

{{ 'PORTALS.SIDEBAR.SERIES' | translate }}

+ +
+ } + + @if (recentlyAddedLive().length > 0) { +
+

{{ 'PORTALS.SIDEBAR.LIVE_TV' | translate }}

+ +
+ } +
diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss new file mode 100644 index 000000000..a1e88cdfe --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss @@ -0,0 +1,37 @@ +:host { + display: block; + height: 100%; + overflow-y: auto; +} + +.recently-added-page { + padding: 20px; +} + +h2 { + margin-bottom: 24px; +} + +.section { + margin-bottom: 32px; + + h3 { + margin-bottom: 16px; + opacity: 0.8; + } +} + +.carousel { + display: flex; + overflow-x: auto; + gap: 16px; + padding-bottom: 16px; + + scrollbar-width: thin; + + .carousel-item { + min-width: 160px; + width: 160px; + flex-shrink: 0; + } +} diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts new file mode 100644 index 000000000..d7dd99abf --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts @@ -0,0 +1,59 @@ +import { DatePipe } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component'; +import { ContentType } from '../xtream-state'; +import { XtreamStore } from '../stores/xtream.store'; + +@Component({ + selector: 'app-recently-added', + templateUrl: './recently-added.component.html', + styleUrls: ['./recently-added.component.scss'], + imports: [DatePipe, SearchResultItemComponent, TranslatePipe], +}) +export class RecentlyAddedComponent { + private readonly xtreamStore = inject(XtreamStore); + private readonly router = inject(Router); + private readonly activatedRoute = inject(ActivatedRoute); + + readonly recentlyAddedLive = computed(() => + this.getRecentlyAdded(this.xtreamStore.liveStreams()) + ); + readonly recentlyAddedVod = computed(() => + this.getRecentlyAdded(this.xtreamStore.vodStreams()) + ); + readonly recentlyAddedSeries = computed(() => + this.getRecentlyAdded(this.xtreamStore.serialStreams(), true) + ); + + private getRecentlyAdded(items: any[], isSeries = false) { + return [...items] + .sort((a, b) => { + const dateA = parseInt(isSeries ? a.last_modified : a.added) || 0; + const dateB = parseInt(isSeries ? b.last_modified : b.added) || 0; + return dateB - dateA; + }) + .slice(0, 20); + } + + getDate(item: any): number { + const timestamp = item.added || item.last_modified; + return parseInt(timestamp) * 1000; + } + + openItem(item: any, type: ContentType) { + this.xtreamStore.setSelectedContentType(type); + + if (type === 'live') { + this.router.navigate(['..', type, item.category_id], { + relativeTo: this.activatedRoute, + }); + } else { + const itemId = item.xtream_id || (type === 'series' ? item.series_id : item.stream_id); + this.router.navigate(['..', type, item.category_id, itemId], { + relativeTo: this.activatedRoute, + }); + } + } +} diff --git a/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts b/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts index 197fee986..de8ad55e9 100644 --- a/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts +++ b/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts @@ -60,30 +60,43 @@ export class XtreamMainContainerComponent implements OnInit { } getContentLabel(): string { - if ( - this.xtreamStore.getSelectedCategory() === null || - this.xtreamStore.getSelectedCategory() === undefined - ) { - return this.translateService.instant('PORTALS.SELECT_CATEGORY'); - } else { - const selectedCategory = this.xtreamStore.getSelectedCategory(); - const categoryName = selectedCategory - ? (selectedCategory as any).name - : 'Category Content'; + const selectedCategoryId = this.xtreamStore.selectedCategoryId(); - // Show page number when viewing category content (not detail view) + // When no category is selected, show "Recently Added" + if (selectedCategoryId === null || selectedCategoryId === undefined) { + const recentlyAddedLabel = this.translateService.instant('PORTALS.SIDEBAR.RECENTLY_ADDED'); + + // Show page number when viewing recently added (not detail view) if ( !this.xtreamStore.selectedItem() && this.xtreamStore.getTotalPages() > 1 ) { - const currentPage = this.xtreamStore.page() + 1; // +1 because page is 0-indexed + const currentPage = this.xtreamStore.page() + 1; const totalPages = this.xtreamStore.getTotalPages(); const pageLabel = this.translateService.instant('PORTALS.PAGE'); - return `${categoryName} (${pageLabel} ${currentPage}/${totalPages})`; + return `${recentlyAddedLabel} (${pageLabel} ${currentPage}/${totalPages})`; } - return `Content for ${categoryName}`; + return recentlyAddedLabel; } + + const selectedCategory = this.xtreamStore.getSelectedCategory(); + const categoryName = selectedCategory + ? (selectedCategory as any).name + : 'Category Content'; + + // Show page number when viewing category content (not detail view) + if ( + !this.xtreamStore.selectedItem() && + this.xtreamStore.getTotalPages() > 1 + ) { + const currentPage = this.xtreamStore.page() + 1; // +1 because page is 0-indexed + const totalPages = this.xtreamStore.getTotalPages(); + const pageLabel = this.translateService.instant('PORTALS.PAGE'); + return `${categoryName} (${pageLabel} ${currentPage}/${totalPages})`; + } + + return categoryName; } historyBack() { @@ -105,6 +118,7 @@ export class XtreamMainContainerComponent implements OnInit { data: { playlistId, contentType, + itemCounts: this.categoryItemCounts(), }, width: '500px', maxHeight: '80vh', diff --git a/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts b/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts index 9583e1d5d..9d2fc9433 100644 --- a/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts +++ b/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts @@ -57,11 +57,16 @@ export class XtreamShellComponent { this.route.params.pipe(takeUntilDestroyed()).subscribe(async (params) => { const newPlaylistId = params['id']; + // Skip if playlist ID hasn't changed (after first load) + if (!this.isFirstLoad && this.currentPlaylistId === newPlaylistId) { + return; + } + if (this.isFirstLoad) { // First load - just set the playlist ID without resetting this.xtreamStore.setPlaylistId(newPlaylistId); this.isFirstLoad = false; - } else if (this.currentPlaylistId !== newPlaylistId) { + } else { // Switching to a different playlist - reset and set new ID this.xtreamStore.resetStore(newPlaylistId); } @@ -85,7 +90,6 @@ export class XtreamShellComponent { this.xtreamStore.initializeContent(); } }, - { allowSignalWrites: true } ); } @@ -96,10 +100,10 @@ export class XtreamShellComponent { }); } - handlePageClick(page: 'search' | 'recent' | 'favorites') { + handlePageClick(page: 'search' | 'recent' | 'favorites' | 'recently-added') { this.xtreamStore.setSelectedContentType(undefined); this.router.navigate([page], { relativeTo: this.route, }); } -} +} \ No newline at end of file diff --git a/apps/web/src/app/xtream-tauri/xtream.routes.ts b/apps/web/src/app/xtream-tauri/xtream.routes.ts index 591297b57..df8db0228 100644 --- a/apps/web/src/app/xtream-tauri/xtream.routes.ts +++ b/apps/web/src/app/xtream-tauri/xtream.routes.ts @@ -36,6 +36,13 @@ export const xtreamRoutes: Route[] = [ (c) => c.XtreamMainContainerComponent ), children: [ + { + path: '', + loadComponent: () => + import( + './category-content-view/category-content-view.component' + ).then((c) => c.CategoryContentViewComponent), + }, { path: ':categoryId', loadComponent: () => @@ -59,6 +66,13 @@ export const xtreamRoutes: Route[] = [ (c) => c.XtreamMainContainerComponent ), children: [ + { + path: '', + loadComponent: () => + import( + './category-content-view/category-content-view.component' + ).then((c) => c.CategoryContentViewComponent), + }, { path: ':categoryId', loadComponent: () => @@ -96,6 +110,13 @@ export const xtreamRoutes: Route[] = [ (c) => c.SearchResultsComponent ), }, + { + path: 'recently-added', + loadComponent: () => + import('./recently-added/recently-added.component').then( + (c) => c.RecentlyAddedComponent + ), + }, ], }, ];