refactor(favorites): restructure favorites component for improved category handling and UI layout

This commit is contained in:
4gray committed 2025-05-04 23:06:45 +02:00
1 parent 818e729c75
commit a7f4e97f6f
3 files changed
+143 -71

No files matched your search

@@ -1,41 +1,37 @@
<div class="favorites-container"> <div class="sidebar">
<div class="header"> <div class="sidebar-header">
<h2>{{ 'CHANNELS.FAVORITES' | translate }}</h2> <h2>{{ 'PORTALS.ALL_CATEGORIES' | translate }}</h2>
</div> </div>
<app-category-view
[items]="categories()"
[selectedCategoryId]="selectedCategoryId()"
(categoryClicked)="setCategoryId($event.category_id)"
/>
</div>
<mat-tab-group> <div class="main-content">
<mat-tab label="Live TV ({{ liveCount }})"> <div class="content-header">
<ng-container <h2>
*ngTemplateOutlet="itemsGrid; context: { type: 'live' }" <h2>
> {{
</ng-container> selectedCategoryId() +
</mat-tab> ' ' +
<mat-tab label="Movies ({{ moviesCount }})"> ('CHANNELS.FAVORITES' | translate)
<ng-container }}
*ngTemplateOutlet="itemsGrid; context: { type: 'movie' }" </h2>
> </h2>
</ng-container> </div>
</mat-tab> <div class="scrollable-content">
<mat-tab label="Series ({{ seriesCount }})"> @if (favoritesToShow().length) {
<ng-container
*ngTemplateOutlet="itemsGrid; context: { type: 'series' }"
>
</ng-container>
</mat-tab>
</mat-tab-group>
<ng-template #itemsGrid let-type="type">
@if ((favorites$ | async | filterByType: type).length) {
<div class="items-grid"> <div class="items-grid">
@for ( @for (item of favoritesToShow(); track item.content_id) {
item of favorites$ | async | filterByType: type;
track $any(item).id
) {
<mat-card class="item-card" (click)="openItem(item)"> <mat-card class="item-card" (click)="openItem(item)">
<img <img
[src]=" [src]="
item.poster_url || item.poster_url ||
'assets/default-' + type + '.png' 'assets/default-' +
selectedCategoryId() +
'.png'
" "
(error)=" (error)="
$event.target.src = $event.target.src =
@@ -68,8 +64,9 @@
} @else { } @else {
<div class="no-items"> <div class="no-items">
<mat-icon>favorite_border</mat-icon> <mat-icon>favorite_border</mat-icon>
<p>No favorite {{ type }} items</p> <p>No favorite {{ selectedCategoryId() }} items</p>
</div> </div>
} }
</ng-template> </div>
<app-mpv-player-bar />
</div> </div>
@@ -1,9 +1,36 @@
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: row; // Changed from column to row
overflow-y: auto; overflow: hidden;
height: 100%; height: 100%;
width: 100%; width: 100%; // Added to ensure full width
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; // Important for proper flex behavior
}
.content-header {
text-transform: capitalize;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
h2 {
margin: 0;
font-size: 1.2rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
}
}
.scrollable-content {
flex: 1;
overflow-y: auto;
} }
.favorites-container { .favorites-container {
@@ -96,4 +123,4 @@ mat-card-content {
font-size: 1.1rem; font-size: 1.1rem;
margin: 0; margin: 0;
} }
} }
@@ -1,15 +1,16 @@
import { AsyncPipe, DatePipe, NgTemplateOutlet } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core'; import { Component, OnInit, inject, signal } from '@angular/core';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { BehaviorSubject, switchMap } from 'rxjs';
import { FilterByTypePipe } from '../../shared/pipes/filter-by-type.pipe'; import { XtreamCategory } from '../../../../shared/xtream-category.interface';
import { MpvPlayerBarComponent } from '../../shared/components/mpv-player-bar/mpv-player-bar.component';
import { selectActivePlaylist } from '../../state/selectors'; import { selectActivePlaylist } from '../../state/selectors';
import { CategoryViewComponent } from '../category-view/category-view.component';
import { FavoriteItem } from '../services/favorite-item.interface'; import { FavoriteItem } from '../services/favorite-item.interface';
import { FavoritesService } from '../services/favorites.service'; import { FavoritesService } from '../services/favorites.service';
import { XtreamStore } from '../xtream.store'; import { XtreamStore } from '../xtream.store';
@@ -17,18 +18,16 @@ import { XtreamStore } from '../xtream.store';
@Component({ @Component({
selector: 'app-favorites', selector: 'app-favorites',
imports: [ imports: [
AsyncPipe, CategoryViewComponent,
DatePipe, DatePipe,
FilterByTypePipe,
MatCardModule, MatCardModule,
MatIcon, MatIcon,
MatIconButton, MatIconButton,
MatTabsModule, MpvPlayerBarComponent,
NgTemplateOutlet, TranslatePipe,
TranslateModule,
], ],
templateUrl: './favorites.component.html', templateUrl: './favorites.component.html',
styleUrl: './favorites.component.scss' styleUrls: ['./favorites.component.scss', '../sidebar.scss'],
}) })
export class FavoritesComponent implements OnInit { export class FavoritesComponent implements OnInit {
private favoritesService = inject(FavoritesService); private favoritesService = inject(FavoritesService);
@@ -37,27 +36,78 @@ export class FavoritesComponent implements OnInit {
private store = inject(Store); private store = inject(Store);
private xtreamStore = inject(XtreamStore); private xtreamStore = inject(XtreamStore);
favorites$: Observable<FavoriteItem[]>; readonly categories = signal<XtreamCategory[]>([]);
liveCount = 0;
moviesCount = 0; readonly series = signal<FavoriteItem[]>([]);
seriesCount = 0; readonly movies = signal<FavoriteItem[]>([]);
readonly live = signal<FavoriteItem[]>([]);
readonly favoritesToShow = signal<FavoriteItem[]>([]);
readonly selectedCategoryId = signal<string>('movie');
private favoritesRefresh$ = new BehaviorSubject<void>(undefined);
ngOnInit() { ngOnInit() {
this.xtreamStore.setSelectedContentType(undefined);
const playlistId = this.store.selectSignal(selectActivePlaylist)()._id; const playlistId = this.store.selectSignal(selectActivePlaylist)()._id;
this.favorites$ = this.favoritesService.getFavorites(playlistId); this.favoritesRefresh$
.pipe(
switchMap(() => this.favoritesService.getFavorites(playlistId))
)
.subscribe((items) => {
this.movies.set(
items.filter((item) => (item as any).type === 'movie')
);
this.live.set(items.filter((item) => item.type === 'live'));
this.series.set(items.filter((item) => item.type === 'series'));
this.initCategories();
this.setCategoryContent(this.selectedCategoryId());
});
}
// Subscribe to update counts initCategories() {
this.favorites$.subscribe((items) => { this.categories.set([
this.liveCount = items.filter( {
(item) => item.type === 'live' id: 1,
).length; category_id: 'movie',
this.moviesCount = items.filter( category_name: 'Movies' + ' (' + this.movies().length + ')',
(item) => (item as any).type === 'movie' parent_id: 0,
).length; },
this.seriesCount = items.filter( {
(item) => item.type === 'series' id: 2,
).length; category_id: 'live',
}); category_name: 'Live TV' + ' (' + this.live().length + ')',
parent_id: 0,
},
{
id: 3,
category_id: 'series',
category_name: 'Series' + ' (' + this.series().length + ')',
parent_id: 0,
},
]);
}
setCategoryId(categoryId: string) {
this.selectedCategoryId.set(categoryId);
this.setCategoryContent(categoryId);
}
setCategoryContent(categoryId: string) {
switch (categoryId) {
case 'movie':
this.favoritesToShow.set(this.movies());
break;
case 'live':
this.favoritesToShow.set(this.live());
break;
case 'series':
this.favoritesToShow.set(this.series());
break;
default:
this.favoritesToShow.set(this.movies());
break;
}
} }
async removeFromFavorites(item: any) { async removeFromFavorites(item: any) {
@@ -66,17 +116,15 @@ export class FavoritesComponent implements OnInit {
item.playlist_id item.playlist_id
); );
// Refresh favorites after removal // Refresh favorites after removal
const playlistId = this.store.selectSignal(selectActivePlaylist)()._id; this.favoritesRefresh$.next();
this.favorites$ = this.favoritesService.getFavorites(playlistId);
} }
openItem(item: any) { openItem(item: any) {
const type = item.type === 'movie' ? 'vod' : item.type; const type = item.type === 'movie' ? 'vod' : item.type;
this.xtreamStore.setSelectedContentType(type); this.xtreamStore.setSelectedContentType(type);
if (type === 'live') { if (type === 'live') {
this.router.navigate(['..', type, item.category_id], { const streamUrl = this.xtreamStore.constructStreamUrl(item);
relativeTo: this.route, this.xtreamStore.openPlayer(streamUrl, item.title, item.poster_url);
});
} else { } else {
this.router.navigate( this.router.navigate(
['..', type, item.category_id, item.xtream_id], ['..', type, item.category_id, item.xtream_id],