diff --git a/src/app/services/playlists.service.ts b/src/app/services/playlists.service.ts index b4041eb17..c6fbda466 100644 --- a/src/app/services/playlists.service.ts +++ b/src/app/services/playlists.service.ts @@ -14,6 +14,8 @@ import { createFavoritesPlaylist, createPlaylistObject, } from '../../../shared/playlist.utils'; +import { XtreamItem } from '../../../shared/xtream-item.interface'; +import { XtreamSerieItem } from '../../../shared/xtream-serie-item.interface'; import { DbStores } from '../indexed-db.config'; import { PlaylistMeta } from '../shared/playlist-meta.type'; @@ -118,6 +120,43 @@ export class PlaylistsService { ); } + getPortalFavorites(portalId: string) { + return this.dbService + .getByID<{ favorites: Partial[] }>( + DbStores.Playlists, + portalId + ) + .pipe(map((item) => item.favorites ?? [])); + } + + addPortalFavorite(portalId: string, item: any) { + return this.getPlaylistById(portalId).pipe( + switchMap((portal) => + this.dbService.update(DbStores.Playlists, { + ...portal, + favorites: [...(portal.favorites ?? []), item], + }) + ) + ); + } + + removeFromPortalFavorites(portalId: string, favoriteId: number) { + return this.getPlaylistById(portalId).pipe( + switchMap((portal) => + this.dbService.update(DbStores.Playlists, { + ...portal, + favorites: portal.favorites.filter( + (i) => + (i as Partial).stream_id !== + favoriteId && + (i as Partial).series_id !== + favoriteId + ), + }) + ) + ); + } + updatePlaylistPositions( positionUpdates: { id: string; diff --git a/src/app/xtream/category-content-view/category-content-view.component.html b/src/app/xtream/category-content-view/category-content-view.component.html index 1ad2ffcf3..a5a4444d6 100644 --- a/src/app/xtream/category-content-view/category-content-view.component.html +++ b/src/app/xtream/category-content-view/category-content-view.component.html @@ -1,4 +1,4 @@ - +
{{ item.name }}
+
+ search +
Nothing found, try to change you search request
+
diff --git a/src/app/xtream/category-content-view/category-content-view.component.scss b/src/app/xtream/category-content-view/category-content-view.component.scss index 9870a9a85..0df8317a9 100644 --- a/src/app/xtream/category-content-view/category-content-view.component.scss +++ b/src/app/xtream/category-content-view/category-content-view.component.scss @@ -1,12 +1,12 @@ :host { display: block; - padding: 10px; + padding: 20px; } .grid { display: flex; flex-wrap: wrap; - gap: 10px; + gap: 30px; justify-content: center; mat-card { @@ -25,13 +25,13 @@ .stream-icon-placeholder { width: 100%; - background: #ccc; + background: #999; height: 300px; &::after { content: 'No cover'; - position: absolute; - top: 130px; + position: relative; + top: 140px; left: 70px; } } @@ -52,3 +52,13 @@ width: 64px; } } + +mat-card { + overflow: hidden; + + transition: all 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +mat-card:hover { + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); +} diff --git a/src/app/xtream/category-view/category-view.component.ts b/src/app/xtream/category-view/category-view.component.ts index ed8234210..12654e509 100644 --- a/src/app/xtream/category-view/category-view.component.ts +++ b/src/app/xtream/category-view/category-view.component.ts @@ -26,6 +26,16 @@ import { PortalStore } from '../portal.store'; {{ item.category_name }} +
+ search +
Nothing found, try to change you search request
+
@@ -41,7 +51,7 @@ import { PortalStore } from '../portal.store'; styles: [ ` :host { - margin: 10px; + margin: 20px; display: block; } diff --git a/src/app/xtream/detail-view.scss b/src/app/xtream/detail-view.scss index 0e47a6251..1d93e486e 100644 --- a/src/app/xtream/detail-view.scss +++ b/src/app/xtream/detail-view.scss @@ -1,7 +1,12 @@ +:host { + width: 100%; +} + .container { display: grid; grid-gap: 10px; grid-template-columns: 350px 450px; + justify-content: center; &::before { content: ''; @@ -19,6 +24,19 @@ .image { width: 300px; padding: 10px; + + .placeholder-cover { + width: 300px; + height: 500px; + background-color: #999; + + &::after { + content: 'No cover'; + position: relative; + top: 230px; + left: 115px; + } + } } .image img { diff --git a/src/app/xtream/navigation-bar/navigation-bar.component.html b/src/app/xtream/navigation-bar/navigation-bar.component.html index 04385a4f3..c7798b890 100644 --- a/src/app/xtream/navigation-bar/navigation-bar.component.html +++ b/src/app/xtream/navigation-bar/navigation-bar.component.html @@ -1,6 +1,6 @@
diff --git a/src/app/xtream/navigation-bar/navigation-bar.component.scss b/src/app/xtream/navigation-bar/navigation-bar.component.scss index f3b0d8433..9768e23fc 100644 --- a/src/app/xtream/navigation-bar/navigation-bar.component.scss +++ b/src/app/xtream/navigation-bar/navigation-bar.component.scss @@ -1,3 +1,16 @@ +::ng-deep { + .dark-theme { + #navigation, + #sub-panel { + background: black; + } + + #sub-panel { + border-bottom: 1px solid #333; + } + } +} + :host { position: sticky; top: 0; @@ -6,19 +19,21 @@ #navigation { height: 70px; - background: black; + background: white; display: flex; justify-content: space-between; align-items: center; + padding: 0 10px; } #sub-panel { - background: black; - border-bottom: 1px solid #333; + background: white; + border-bottom: 1px solid #ccc; padding: 0 10px 5px; display: flex; justify-content: space-between; align-items: center; + height: 30px; .breadcrumbs { span { @@ -39,8 +54,8 @@ text-align: center; .search-input { - padding: 8px 8px 8px 40px; - width: 300px; + padding: 6px 8px 6px 40px; + width: 250px; border-radius: 5px; border: 1px solid #333; color: #000; diff --git a/src/app/xtream/navigation-bar/navigation-bar.component.ts b/src/app/xtream/navigation-bar/navigation-bar.component.ts index cf68d3b7a..3d00134c9 100644 --- a/src/app/xtream/navigation-bar/navigation-bar.component.ts +++ b/src/app/xtream/navigation-bar/navigation-bar.component.ts @@ -31,6 +31,7 @@ export class NavigationBarComponent { @Output() contentTypeChanged = new EventEmitter(); @Output() breadcrumbClicked = new EventEmitter(); + @Output() favoritesClicked = new EventEmitter(); ContentTypeEnum = ContentType; portalStore = inject(PortalStore); diff --git a/src/app/xtream/serial-details/serial-details.component.html b/src/app/xtream/serial-details/serial-details.component.html index 308360c52..47f1d9700 100644 --- a/src/app/xtream/serial-details/serial-details.component.html +++ b/src/app/xtream/serial-details/serial-details.component.html @@ -40,6 +40,26 @@
Episode run time:
{{ item.info.episode_run_time }} +
+ + + + + +
diff --git a/src/app/xtream/serial-details/serial-details.component.ts b/src/app/xtream/serial-details/serial-details.component.ts index 656e6ff06..4606bea4f 100644 --- a/src/app/xtream/serial-details/serial-details.component.ts +++ b/src/app/xtream/serial-details/serial-details.component.ts @@ -1,11 +1,14 @@ import { JsonPipe, NgIf } from '@angular/common'; import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { XtreamSerieDetails, XtreamSerieEpisode, } from '../../../../shared/xtream-serie-details.interface'; +import { PlaylistsService } from '../../services/playlists.service'; import { selectCurrentPlaylist } from '../../state/selectors'; import { PlayerDialogComponent } from '../player-dialog/player-dialog.component'; import { SeasonContainerComponent } from '../season-container/season-container.component'; @@ -18,6 +21,7 @@ import { SeasonContainerComponent } from '../season-container/season-container.c imports: [ JsonPipe, MatButtonModule, + MatIconModule, NgIf, SeasonContainerComponent, PlayerDialogComponent, @@ -25,9 +29,44 @@ import { SeasonContainerComponent } from '../season-container/season-container.c }) export class SerialDetailsComponent { @Input({ required: true }) item: XtreamSerieDetails; + @Input() seriesId: number; + @Output() addToFavoritesClicked = new EventEmitter(); @Output() playClicked = new EventEmitter(); + @Output() removeFromFavoritesClicked = new EventEmitter(); store = inject(Store); currentPlaylist = this.store.selectSignal(selectCurrentPlaylist); + private playlistService = inject(PlaylistsService); + private route = inject(ActivatedRoute); + private portalId = this.route.snapshot.paramMap.get('id'); + + isFavorite = false; + + ngOnInit(): void { + this.checkFavoriteStatus(); + } + + checkFavoriteStatus() { + this.playlistService + .getPortalFavorites(this.portalId) + .subscribe((favorites) => { + this.isFavorite = favorites.some( + (i) => (i as any).series_id === this.seriesId + ); + }); + } + + toggleFavorite() { + if (this.isFavorite) { + this.removeFromFavoritesClicked.emit(this.seriesId); + } else { + this.addToFavoritesClicked.emit({ + name: this.item.info.name, + series_id: this.seriesId, + cover: this.item.info.cover, + }); + } + this.isFavorite = !this.isFavorite; + } } diff --git a/src/app/xtream/vod-details/vod-details.component.html b/src/app/xtream/vod-details/vod-details.component.html index 0c9202a67..2bbfb866f 100644 --- a/src/app/xtream/vod-details/vod-details.component.html +++ b/src/app/xtream/vod-details/vod-details.component.html @@ -9,7 +9,13 @@ " >
- + + +
+

{{ item.info.name }}

@@ -46,8 +52,26 @@ color="accent" (click)="playClicked.emit(item)" > - Play + Play  + + + + +
diff --git a/src/app/xtream/vod-details/vod-details.component.ts b/src/app/xtream/vod-details/vod-details.component.ts index 8feeaf5f4..5aa0b3f5a 100644 --- a/src/app/xtream/vod-details/vod-details.component.ts +++ b/src/app/xtream/vod-details/vod-details.component.ts @@ -1,17 +1,66 @@ import { JsonPipe, NgIf } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + inject, +} from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { ActivatedRoute } from '@angular/router'; import { XtreamVodDetails } from '../../../../shared/xtream-vod-details.interface'; +import { PlaylistsService } from '../../services/playlists.service'; @Component({ selector: 'app-vod-details', templateUrl: './vod-details.component.html', styleUrls: ['../detail-view.scss'], standalone: true, - imports: [JsonPipe, MatButtonModule, NgIf], + imports: [JsonPipe, MatButtonModule, NgIf, MatIconModule], }) -export class VodDetailsComponent { +export class VodDetailsComponent implements OnInit { @Input({ required: true }) item: XtreamVodDetails; + @Output() addToFavoritesClicked = new EventEmitter(); @Output() playClicked = new EventEmitter(); + @Output() removeFromFavoritesClicked = new EventEmitter(); + + private playlistService = inject(PlaylistsService); + private route = inject(ActivatedRoute); + private portalId = this.route.snapshot.paramMap.get('id'); + + isFavorite = false; + + ngOnInit(): void { + this.checkFavoriteStatus(); + } + + checkFavoriteStatus() { + this.playlistService + .getPortalFavorites(this.portalId) + .subscribe((favorites) => { + this.isFavorite = favorites.some( + (i) => i.stream_id === this.item.movie_data.stream_id + ); + }); + } + + toggleFavorite() { + if (this.isFavorite) { + this.removeFromFavoritesClicked.emit( + this.item.movie_data.stream_id + ); + } else { + this.addToFavoritesClicked.emit({ + name: this.item.movie_data.name, + stream_id: this.item.movie_data.stream_id, + container_extension: this.item.movie_data.container_extension, + cover: this.item.info.movie_image, + stream_type: 'movie', + }); + } + this.isFavorite = !this.isFavorite; + } } diff --git a/src/app/xtream/xtream-main-container.component.html b/src/app/xtream/xtream-main-container.component.html index 1e96caa27..e48a0c633 100644 --- a/src/app/xtream/xtream-main-container.component.html +++ b/src/app/xtream/xtream-main-container.component.html @@ -4,6 +4,7 @@ (breadcrumbClicked)="breadcrumbClicked($event)" (contentTypeChanged)="changeContentType($event)" (searchTextChanged)="setSearchPhrase($event)" + (favoritesClicked)="favoritesClicked()" [searchVisible]=" currentLayout === 'category' || currentLayout === 'category_content' " @@ -30,12 +31,23 @@ *ngIf="currentLayout === 'vod-details'" [item]="vodDetails" (playClicked)="playVod($event)" + (addToFavoritesClicked)="addToFavorites($event)" + (removeFromFavoritesClicked)="removeFromFavorites($event)" /> + + diff --git a/src/app/xtream/xtream-main-container.component.ts b/src/app/xtream/xtream-main-container.component.ts index 8e1863622..8edb4343e 100644 --- a/src/app/xtream/xtream-main-container.component.ts +++ b/src/app/xtream/xtream-main-container.component.ts @@ -1,4 +1,11 @@ -import { JsonPipe, KeyValuePipe, NgFor, NgIf, NgSwitch } from '@angular/common'; +import { + AsyncPipe, + JsonPipe, + KeyValuePipe, + NgFor, + NgIf, + NgSwitch, +} from '@angular/common'; import { Component, NgZone, @@ -39,10 +46,12 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { MatDialog } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { StorageMap } from '@ngx-pwa/local-storage'; +import { Observable } from 'rxjs'; import { XtreamSerieDetails, XtreamSerieEpisode, } from '../../../shared/xtream-serie-details.interface'; +import { PlaylistsService } from '../services/playlists.service'; import { Settings, VideoPlayer } from '../settings/settings.interface'; import { STORE_KEY } from '../shared/enums/store-keys.enum'; import { Breadcrumb } from './breadcrumb.interface'; @@ -75,7 +84,8 @@ type LayoutView = | 'category_content' | 'vod-details' | 'player' - | 'serie-details'; + | 'serie-details' + | 'favorites'; @Component({ selector: 'app-xtream-main-container', @@ -100,18 +110,21 @@ type LayoutView = SerialDetailsComponent, PlayerDialogComponent, MatProgressSpinnerModule, + AsyncPipe, ], }) export class XtreamMainContainerComponent implements OnInit { dataService = inject(DataService); dialog = inject(MatDialog); ngZone = inject(NgZone); + playlistService = inject(PlaylistsService); portalStore = inject(PortalStore); snackBar = inject(MatSnackBar); storage = inject(StorageMap); store = inject(Store); currentPlaylist = this.store.selectSignal(selectCurrentPlaylist); + favorites$: Observable; breadcrumbs: Breadcrumb[] = []; items = []; listeners = []; @@ -123,6 +136,7 @@ export class XtreamMainContainerComponent implements OnInit { ) as Signal; isLoading = true; searchPhrase = this.portalStore.searchPhrase(); + contentId: number; commandsList = [ new IpcCommand(XTREAM_RESPONSE, (response: XtreamResponse) => @@ -143,6 +157,10 @@ export class XtreamMainContainerComponent implements OnInit { ngOnInit() { this.setInitialBreadcrumb(); + console.log(this.currentPlaylist()._id); + this.favorites$ = this.playlistService.getPortalFavorites( + this.currentPlaylist()._id + ); this.commandsList.forEach((command) => { if (this.dataService.isElectron) { @@ -229,6 +247,7 @@ export class XtreamMainContainerComponent implements OnInit { if (item.stream_type && item.stream_type === 'movie') { action = XtreamCodeActions.GetVodInfo; this.breadcrumbs.push({ title: item.name, action }); + this.contentId = item.stream_id; this.sendRequest({ action, vod_id: item.stream_id }); } else if (item.stream_type && item.stream_type === 'live') { this.breadcrumbs.push({ @@ -239,6 +258,7 @@ export class XtreamMainContainerComponent implements OnInit { } else if (item.series_id) { action = XtreamCodeActions.GetSeriesInfo; this.breadcrumbs.push({ title: item.name, action }); + this.contentId = item.series_id; this.sendRequest({ action, series_id: item.series_id }); } } @@ -368,4 +388,29 @@ export class XtreamMainContainerComponent implements OnInit { console.log(searchPhrase); this.searchPhrase = searchPhrase; } + + addToFavorites(item: any) { + this.playlistService + .addPortalFavorite(this.currentPlaylist()._id, item) + .subscribe(() => { + this.snackBar.open('Added to favorites', null, { + duration: 1000, + }); + }); + } + + removeFromFavorites(favoriteId: number) { + this.playlistService + .removeFromPortalFavorites(this.currentPlaylist()._id, favoriteId) + .subscribe(() => { + this.snackBar.open('Removed from favorites', null, { + duration: 1000, + }); + }); + } + + favoritesClicked() { + this.currentLayout = 'favorites'; + this.setInitialBreadcrumb(); + } }