feat(xtream): add favorites list

This commit is contained in:
4gray committed 2023-07-19 00:09:46 +02:00
1 parent d57fd52ac5
commit 3e008aa3f2
14 files changed
+312 -23

No files matched your search

+39
View File
@@ -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<XtreamItem>[] }>(
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<XtreamItem>).stream_id !==
favoriteId &&
(i as Partial<XtreamSerieItem>).series_id !==
favoriteId
),
})
)
);
}
updatePlaylistPositions(
positionUpdates: {
id: string;
@@ -1,4 +1,4 @@
<ng-container *ngIf="items.length > 0; else noItems">
<ng-container *ngIf="items?.length > 0; else noItems">
<div class="grid">
<mat-card
*ngFor="
@@ -25,6 +25,13 @@
<div class="title">{{ item.name }}</div>
</mat-card-content>
</mat-card>
<div
class="no-content"
*ngIf="!(items | filterBy : searchPhrase() : 'name')?.length"
>
<mat-icon class="icon">search</mat-icon>
<div>Nothing found, try to change you search request</div>
</div>
</div>
</ng-container>
<ng-template #noItems>
@@ -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);
}
@@ -26,6 +26,16 @@ import { PortalStore } from '../portal.store';
{{ item.category_name }}
</mat-card-content>
</mat-card>
<div
class="no-content"
*ngIf="
!(items | filterBy : searchText() : 'category_name')
?.length
"
>
<mat-icon class="icon">search</mat-icon>
<div>Nothing found, try to change you search request</div>
</div>
</div>
</ng-container>
<ng-template #noItems>
@@ -41,7 +51,7 @@ import { PortalStore } from '../portal.store';
styles: [
`
:host {
margin: 10px;
margin: 20px;
display: block;
}
+18
View File
@@ -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 {
@@ -1,6 +1,6 @@
<div id="navigation">
<button mat-icon-button routerLink="/">
<mat-icon>home</mat-icon>
<button mat-button routerLink="/">
<mat-icon>home</mat-icon> Playlists
</button>
<div>
@@ -22,8 +22,8 @@
</mat-button-toggle-group>
</div>
<button mat-icon-button>
<mat-icon>info</mat-icon>
<button mat-button (click)="favoritesClicked.emit()">
<mat-icon>star</mat-icon> Favorites
</button>
</div>
<div id="sub-panel">
@@ -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;
@@ -31,6 +31,7 @@ export class NavigationBarComponent {
@Output() contentTypeChanged = new EventEmitter<ContentType>();
@Output() breadcrumbClicked = new EventEmitter<Breadcrumb>();
@Output() favoritesClicked = new EventEmitter<void>();
ContentTypeEnum = ContentType;
portalStore = inject(PortalStore);
@@ -40,6 +40,26 @@
<div class="label">Episode run time:</div>
{{ item.info.episode_run_time }}
</label>
<div class="action-buttons">
<button
*ngIf="!isFavorite; else removeFromFavoritesButton"
mat-stroked-button
color="accent"
(click)="toggleFavorite()"
>
<mat-icon>star_outline</mat-icon> Add to favorites
</button>
<ng-template #removeFromFavoritesButton>
<button
mat-flat-button
color="accent"
(click)="toggleFavorite()"
>
<mat-icon>star</mat-icon>Remove from favorites
</button>
</ng-template>
</div>
</div>
</div>
@@ -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<any>();
@Output() playClicked = new EventEmitter<XtreamSerieEpisode>();
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
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;
}
}
@@ -9,7 +9,13 @@
"
>
<div class="image">
<img *ngIf="item.info.movie_image" [src]="item.info.movie_image" />
<img
*ngIf="item.info.movie_image; else placeholderCover"
[src]="item.info.movie_image"
/>
<ng-template #placeholderCover>
<div class="placeholder-cover"></div>
</ng-template>
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
@@ -46,8 +52,26 @@
color="accent"
(click)="playClicked.emit(item)"
>
Play
Play</button
>&nbsp;
<button
*ngIf="!isFavorite; else removeFromFavoritesButton"
mat-stroked-button
color="accent"
(click)="toggleFavorite()"
>
<mat-icon>star_outline</mat-icon> Add to favorites
</button>
<ng-template #removeFromFavoritesButton>
<button
mat-stroked-button
color="accent"
(click)="toggleFavorite()"
>
<mat-icon>star</mat-icon>Remove from favorites
</button>
</ng-template>
</div>
</div>
</div>
@@ -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<any>();
@Output() playClicked = new EventEmitter<XtreamVodDetails>();
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
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;
}
}
@@ -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)"
/>
<app-serial-details
*ngIf="currentLayout === 'serie-details'"
[item]="vodDetails"
[seriesId]="contentId"
(playClicked)="playEpisode($event)"
(addToFavoritesClicked)="addToFavorites($event)"
(removeFromFavoritesClicked)="removeFromFavorites($event)"
/>
<app-category-content-view
*ngIf="currentLayout === 'favorites'"
[items]="favorites$ | async"
(itemClicked)="itemClicked($event)"
/>
<app-player-view *ngIf="currentLayout === 'player'" />
@@ -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<any>;
breadcrumbs: Breadcrumb[] = [];
items = [];
listeners = [];
@@ -123,6 +136,7 @@ export class XtreamMainContainerComponent implements OnInit {
) as Signal<Settings>;
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();
}
}