feat: favorite channels list

This commit is contained in:
4gray committed 2020-11-24 20:10:50 +01:00
1 parent 44a4c438e2
commit f02bbe39a6
8 files changed
+183 -106

No files matched your search

+10
View File
@@ -97,6 +97,16 @@ export class Api {
}); });
}); });
}); });
ipcMain.on('update-favorites', async (event, args) => {
const updated = await db.update(
{ id: args.id },
{ $set: { favorites: args.favorites } }
);
if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Favorites were not updated');
}
});
} }
/** /**
@@ -3,24 +3,36 @@
<mat-list id="top-panel"> <mat-list id="top-panel">
<mat-list-item> <mat-list-item>
<mat-form-field class="full-width"> <mat-form-field class="full-width">
<input matInput placeholder="Search channel ({{_channelList?.length}})" <input
[(ngModel)]="searchTerm.name"> matInput
placeholder="Search channel ({{
_channelList?.length
}})"
[(ngModel)]="searchTerm.name"
/>
</mat-form-field> </mat-form-field>
<button mat-icon-button [routerLink]="'/'" matTooltip="Upload or select other playlist"> <button
mat-icon-button
[routerLink]="'/'"
matTooltip="Upload or select other playlist"
>
<mat-icon>create_new_folder</mat-icon> <mat-icon>create_new_folder</mat-icon>
</button> </button>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
<mat-nav-list id="channels-list"> <mat-nav-list id="channels-list">
<cdk-virtual-scroll-viewport itemSize="50" class="scroll-viewport"> <cdk-virtual-scroll-viewport itemSize="50" class="scroll-viewport">
<mat-list-item *cdkVirtualFor="let channel of _channelList | filterBy: searchTerm; index as i" <mat-list-item
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)"> *cdkVirtualFor="
let channel of _channelList | filterBy: searchTerm;
index as i
"
[class.active]="selected?.id === channel.id"
(click)="selectChannel(channel)"
>
<p matLine> <p matLine>
{{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} {{ i + 1 + '. ' + channel?.name || 'Unnamed Channel' }}
</p> </p>
<!-- <button mat-icon-button color="primary" (click)="favChannel(channel, $event)">
<mat-icon>star{{ channel.fav ? '' : '_outline' }}</mat-icon>
</button> -->
<mat-divider></mat-divider> <mat-divider></mat-divider>
</mat-list-item> </mat-list-item>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>
@@ -32,18 +44,25 @@
<mat-accordion multi> <mat-accordion multi>
<ng-container *ngFor="let groups of groupedChannels | keyvalue"> <ng-container *ngFor="let groups of groupedChannels | keyvalue">
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header *ngIf="groups.value.length > 0"> <mat-expansion-panel-header
{{(groups.key || 'Ungrouped') | titlecase}} ({{groups.value.length}}) *ngIf="groups.value.length > 0"
>
{{ groups.key || 'Ungrouped' | titlecase }} ({{
groups.value.length
}})
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<mat-list-item *ngFor="let channel of groups.value; index as i" <mat-list-item
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)"> *ngFor="let channel of groups.value; index as i"
[class.active]="selected?.id === channel.id"
(click)="selectChannel(channel)"
>
<p matLine> <p matLine>
{{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} {{
i + 1 + '. ' + channel?.name ||
'Unnamed Channel'
}}
</p> </p>
<!-- <button mat-icon-button color="primary" (click)="favChannel(channel, $event)">
<mat-icon>star{{ channel.fav ? '' : '_outline' }}</mat-icon>
</button> -->
<mat-divider></mat-divider> <mat-divider></mat-divider>
</mat-list-item> </mat-list-item>
</ng-template> </ng-template>
@@ -53,18 +72,29 @@
</mat-nav-list> </mat-nav-list>
</mat-tab> </mat-tab>
<!-- <mat-tab label="Favorites"> <mat-tab label="Favorites">
<mat-nav-list> <mat-nav-list *ngIf="favorites$ | async as favorites">
<mat-list-item *ngFor="let channel of favs | filterBy: searchTerm; index as i" <mat-list-item
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)"> *ngFor="
let channel of favorites | filterBy: searchTerm;
index as i
"
[class.active]="selected?.id === channel.id"
(click)="selectChannel(channel)"
>
<p matLine> <p matLine>
{{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} {{ i + 1 + '. ' + channel?.name || 'Unnamed Channel' }}
</p> </p>
<button mat-icon-button color="primary" (click)="favChannel(channel, $event)"> <button
<mat-icon>star{{ channel.fav ? '' : '_outline' }}</mat-icon> mat-icon-button
color="primary"
matTooltip="Remove from favorites"
(click)="toggleFavoriteChannel(channel, $event)"
>
<mat-icon>star</mat-icon>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
</mat-list-item> </mat-list-item>
</mat-nav-list> </mat-nav-list>
</mat-tab> --> </mat-tab>
</mat-tab-group> </mat-tab-group>
@@ -25,4 +25,10 @@
.mat-expansion-panel { .mat-expansion-panel {
border-radius: 0 !important; border-radius: 0 !important;
} }
.mat-tab-label, .mat-tab-label-active {
min-width: 0!important;
padding: 15px!important;
margin: 5px!important;
}
} }
@@ -3,7 +3,7 @@ import { Channel, ChannelStore, ChannelQuery } from '../state';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@datorama/akita'; import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-channel-list-container', selector: 'app-channel-list-container',
@@ -22,8 +22,7 @@ export class ChannelListContainerComponent {
@Input('channelList') @Input('channelList')
set channelList(value: Channel[]) { set channelList(value: Channel[]) {
// deep copy this._channelList = value;
this._channelList = value; //JSON.parse(JSON.stringify(value));
this.groupedChannels = _.groupBy(value, 'group.title'); this.groupedChannels = _.groupBy(value, 'group.title');
} }
@@ -37,7 +36,11 @@ export class ChannelListContainerComponent {
@Output() changeChannel: EventEmitter<Channel> = new EventEmitter(); @Output() changeChannel: EventEmitter<Channel> = new EventEmitter();
/** List with favorited channels */ /** List with favorited channels */
favs: Channel[] = []; favorites$: Observable<Channel[]> = this.channelQuery.select((store) =>
this.channelQuery
.getAll()
.filter((channel) => store.favorites.includes(channel.id))
);
/** Search term for channel filter */ /** Search term for channel filter */
searchTerm: any = { searchTerm: any = {
@@ -54,15 +57,7 @@ export class ChannelListContainerComponent {
private channelQuery: ChannelQuery, private channelQuery: ChannelQuery,
private channelStore: ChannelStore, private channelStore: ChannelStore,
private snackBar: MatSnackBar private snackBar: MatSnackBar
) { ) {}
this.channelQuery
.selectAll({
filterBy: (entity) => entity.fav === true,
})
.subscribe(
(favs) => (this.favs = JSON.parse(JSON.stringify(favs)))
);
}
/** /**
* Sets clicked channel as selected and emits them to the parent component * Sets clicked channel as selected and emits them to the parent component
@@ -82,21 +77,9 @@ export class ChannelListContainerComponent {
* @param channel channel to update * @param channel channel to update
* @param clickEvent mouse click event * @param clickEvent mouse click event
*/ */
favChannel(channel: Channel, clickEvent: MouseEvent): void { toggleFavoriteChannel(channel: Channel, clickEvent: MouseEvent): void {
clickEvent.stopPropagation(); clickEvent.stopPropagation();
channel.fav = !channel.fav;
this.snackBar.open('Favorites were updated!', null, { duration: 2000 }); this.snackBar.open('Favorites were updated!', null, { duration: 2000 });
this.channelStore.updateFavorite(channel);
this.channelStore.update(channel.id, { fav: channel.fav });
// update channels fav flag in the database
this.channelStore.update((store) => {
const favorites = channel.fav
? [...store.favorites, channel.id]
: [...store.favorites.filter((favId) => channel.id !== favId)];
return {
favorites,
};
});
} }
} }
@@ -3,12 +3,12 @@
</div> </div>
<div fxFlex="100" fxLayoutAlign="start center"> <div fxFlex="100" fxLayoutAlign="start center">
<div fxLayout="column"> <div fxLayout="column">
<div fxFlex class="title">{{title}}</div> <div fxFlex class="title">{{ title }}</div>
<div class="subtitle">{{subtitle}}</div> <div class="subtitle">{{ subtitle }}</div>
</div> </div>
</div> </div>
<div fxLayout="column" fxFlex="50px" fxLayoutAlign="center center"> <div fxLayout="column" fxFlex="50px" fxLayoutAlign="center center">
<button mat-icon-button (click)="openSettings()"> <button mat-icon-button (click)="openSettings()">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
</button> </button>
</div> </div>
+26 -3
View File
@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { ElectronService } from 'app/services/electron.service';
import { Channel } from './channel.model'; import { Channel } from './channel.model';
export interface ChannelState extends EntityState<Channel> { export interface ChannelState extends EntityState<Channel> {
@@ -11,7 +12,11 @@ export interface ChannelState extends EntityState<Channel> {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'channel', resettable: true }) @StoreConfig({ name: 'channel', resettable: true })
export class ChannelStore extends EntityStore<ChannelState> { export class ChannelStore extends EntityStore<ChannelState> {
constructor() { /**
* Creates an instance of ChannelStore
* @param channelStore channels store
*/
constructor(private electronService: ElectronService) {
super({ super({
favorites: [], favorites: [],
playlistId: '', playlistId: '',
@@ -19,7 +24,25 @@ export class ChannelStore extends EntityStore<ChannelState> {
}); });
} }
setChannels(channels: Channel[]): void { /**
this.upsertMany(channels); * Adds/removes the given channel from the favorites list
* @param channel channel to add/remove
*/
updateFavorite(channel: Channel): void {
let favorites;
this.update((store) => {
if (store.favorites.includes(channel.id)) {
favorites = [
...store.favorites.filter((id) => id !== channel.id),
];
} else {
favorites = [...store.favorites, channel.id];
}
this.electronService.ipcRenderer.send('update-favorites', {
id: store.playlistId,
favorites,
});
return { favorites };
});
} }
} }
@@ -17,49 +17,58 @@
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<!-- toolbar with drawer icon --> <ng-container *ngIf="activeChannel$ | async as activeChannel">
<mat-toolbar color="primary"> <!-- toolbar with drawer icon -->
<button <mat-toolbar color="primary">
mat-icon-button <button
mat-toolbar="Add to favorites" mat-icon-button
(click)="drawer.toggle()" mat-toolbar="Add to favorites"
matTooltip="Open channels list" (click)="drawer.toggle()"
> matTooltip="Open channels list"
<mat-icon>menu</mat-icon> >
</button> <mat-icon>menu</mat-icon>
{{ (activeChannel$ | async)?.name }} </button>
<div class="spacer"></div> <button
<button mat-icon-button (click)="openAbout()"> mat-icon-button
<mat-icon>info</mat-icon> (click)="addToFavorites(activeChannel)"
</button> matTooltip="Toggle favorite flag"
<!-- <mat-icon>star_outline</mat-icon> --> >
</mat-toolbar> <mat-icon>
{{
(favorites$ | async).includes(activeChannel.id)
? 'star'
: 'star_outline'
}}</mat-icon
>
</button>
{{ activeChannel.name }}
<div class="spacer"></div>
<button mat-icon-button (click)="openAbout()">
<mat-icon>info</mat-icon>
</button>
</mat-toolbar>
<ng-container <ng-container *ngIf="player === 'videojs'">
*ngIf=" <app-vjs-player
player === 'videojs' && [options]="{
(activeChannel$ | async) as activeChannel sources: [
" {
> src: activeChannel.url,
<app-vjs-player type: 'application/x-mpegURL'
[options]="{ }
sources: [ ]
{ }"
src: activeChannel.url, ></app-vjs-player>
type: 'application/x-mpegURL' </ng-container>
}
] <!-- player component -->
}" <video
></app-vjs-player> *ngIf="player === 'html5'"
id="video-player"
autoplay="true"
controls="true"
muted="muted"
></video>
</ng-container> </ng-container>
<!-- player component -->
<video
*ngIf="player === 'html5'"
id="video-player"
autoplay="true"
controls="true"
muted="muted"
></video>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>
+21 -5
View File
@@ -1,11 +1,12 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import * as Hls from 'hls.js'; import * as Hls from 'hls.js';
import { ChannelQuery, Channel } from '../state'; import { ChannelQuery, Channel, ChannelStore } from '../state';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { MatSidenav } from '@angular/material/sidenav'; import { MatSidenav } from '@angular/material/sidenav';
import { ElectronService } from '../services/electron.service'; import { ElectronService } from '../services/electron.service';
import { StorageMap } from '@ngx-pwa/local-storage'; import { StorageMap } from '@ngx-pwa/local-storage';
import { Settings, VideoPlayerType } from '../settings/settings.interface'; import { Settings, VideoPlayerType } from '../settings/settings.interface';
import { MatSnackBar } from '@angular/material/snack-bar';
/** Settings key in storage */ /** Settings key in storage */
export const SETTINGS_STORE_KEY = 'settings'; export const SETTINGS_STORE_KEY = 'settings';
@@ -17,7 +18,12 @@ export const SETTINGS_STORE_KEY = 'settings';
}) })
export class VideoPlayerComponent implements OnInit { export class VideoPlayerComponent implements OnInit {
/** Channels list */ /** Channels list */
channels$: Observable<Channel[]>; channels$: Observable<Channel[]> = this.channelQuery.selectAll();
/** Favorites list */
favorites$: Observable<string[]> = this.channelQuery.select(
(store) => store.favorites
);
/** Video player DOM element */ /** Video player DOM element */
videoPlayer: HTMLVideoElement; videoPlayer: HTMLVideoElement;
@@ -40,12 +46,16 @@ export class VideoPlayerComponent implements OnInit {
/** /**
* Creates an instance of VideoPlayerComponent * Creates an instance of VideoPlayerComponent
* @param channelQuery akita's channel query * @param channelQuery akita's channel query
* @param channelStore akita's channel store
* @param electronService electron service * @param electronService electron service
* @param storage browser storage service * @param storage browser storage service
* @param snackBar service to push snackbar notifications
*/ */
constructor( constructor(
private channelQuery: ChannelQuery, private channelQuery: ChannelQuery,
private channelStore: ChannelStore,
private electronService: ElectronService, private electronService: ElectronService,
private snackBar: MatSnackBar,
private storage: StorageMap private storage: StorageMap
) {} ) {}
@@ -53,10 +63,7 @@ export class VideoPlayerComponent implements OnInit {
* Sets video player and subscribes to channel list from the store * Sets video player and subscribes to channel list from the store
*/ */
ngOnInit(): void { ngOnInit(): void {
this.channels$ = this.channelQuery.selectAll();
this.activeChannel$ = this.channelQuery.select((state) => state.active); this.activeChannel$ = this.channelQuery.select((state) => state.active);
this.applySettings(); this.applySettings();
} }
@@ -109,4 +116,13 @@ export class VideoPlayerComponent implements OnInit {
openAbout(): void { openAbout(): void {
this.electronService.ipcRenderer.send('show-about'); this.electronService.ipcRenderer.send('show-about');
} }
/**
* Adds/removes a given channel to the favorites list
* @param channel channel to add
*/
addToFavorites(channel: Channel): void {
this.snackBar.open('Favorites were updated!', null, { duration: 2000 });
this.channelStore.updateFavorite(channel);
}
} }