mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
feat: favorite channels list
This commit is contained in:
1 parent
44a4c438e2
commit
f02bbe39a6
8 files changed
+183
-106
No files matched your search
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user