mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(xtream): add recently added views
This commit is contained in:
1 parent
16469256df
commit
c173553d78
10 files changed
+245
-33
No files matched your search
@@ -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;
|
||||
|
||||
+20
-8
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,18 @@
|
||||
{{ 'PORTALS.SIDEBAR.LIBRARY' | translate }}
|
||||
</div> -->
|
||||
<mat-nav-list>
|
||||
@if (!isStalkerPlaylist()) {
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="./recently-added"
|
||||
routerLinkActive="active"
|
||||
[matTooltip]="'PORTALS.SIDEBAR.RECENTLY_ADDED' | translate"
|
||||
matTooltipPosition="right"
|
||||
(click)="pageSwitch('recently-added')"
|
||||
>
|
||||
<mat-icon matListItemIcon>new_releases</mat-icon>
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="./search"
|
||||
|
||||
@@ -37,7 +37,7 @@ export class NavigationComponent {
|
||||
readonly selectedContentType = input<string>();
|
||||
|
||||
readonly categoryClick = output<string>();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="recently-added-page">
|
||||
<h2>{{ 'PORTALS.SIDEBAR.RECENTLY_ADDED' | translate }}</h2>
|
||||
|
||||
@if (recentlyAddedVod().length > 0) {
|
||||
<div class="section">
|
||||
<h3>{{ 'PORTALS.SIDEBAR.MOVIES' | translate }}</h3>
|
||||
<div class="carousel">
|
||||
@for (item of recentlyAddedVod(); track item.xtream_id) {
|
||||
<app-search-result-item
|
||||
class="carousel-item"
|
||||
[title]="item.title || item.name"
|
||||
[description]="getDate(item) | date : 'mediumDate'"
|
||||
[posterUrl]="item.poster_url || item.stream_icon"
|
||||
[type]="'movie'"
|
||||
(itemClick)="openItem(item, 'vod')"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recentlyAddedSeries().length > 0) {
|
||||
<div class="section">
|
||||
<h3>{{ 'PORTALS.SIDEBAR.SERIES' | translate }}</h3>
|
||||
<div class="carousel">
|
||||
@for (item of recentlyAddedSeries(); track item.xtream_id) {
|
||||
<app-search-result-item
|
||||
class="carousel-item"
|
||||
[title]="item.title || item.name"
|
||||
[description]="getDate(item) | date : 'mediumDate'"
|
||||
[posterUrl]="item.poster_url || item.cover"
|
||||
[type]="'series'"
|
||||
(itemClick)="openItem(item, 'series')"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (recentlyAddedLive().length > 0) {
|
||||
<div class="section">
|
||||
<h3>{{ 'PORTALS.SIDEBAR.LIVE_TV' | translate }}</h3>
|
||||
<div class="carousel">
|
||||
@for (item of recentlyAddedLive(); track item.xtream_id) {
|
||||
<app-search-result-item
|
||||
class="carousel-item"
|
||||
[title]="item.title || item.name"
|
||||
[description]="getDate(item) | date : 'mediumDate'"
|
||||
[posterUrl]="item.poster_url || item.stream_icon"
|
||||
[type]="'live'"
|
||||
(itemClick)="openItem(item, 'live')"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Reference in new issue
Block a user