diff --git a/api.ts b/api.ts index eef587988..30551f489 100644 --- a/api.ts +++ b/api.ts @@ -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'); + } + }); } /** diff --git a/src/app/channel-list-container/channel-list-container.component.html b/src/app/channel-list-container/channel-list-container.component.html index 03b98b8cf..b0130ebed 100644 --- a/src/app/channel-list-container/channel-list-container.component.html +++ b/src/app/channel-list-container/channel-list-container.component.html @@ -3,24 +3,36 @@ - + - + create_new_folder - + - {{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} + {{ i + 1 + '. ' + channel?.name || 'Unnamed Channel' }} - @@ -32,18 +44,25 @@ - 0"> - {{(groups.key || 'Ungrouped') | titlecase}} ({{groups.value.length}}) + 0" + > + {{ groups.key || 'Ungrouped' | titlecase }} ({{ + groups.value.length + }}) - + - {{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} + {{ + i + 1 + '. ' + channel?.name || + 'Unnamed Channel' + }} - @@ -53,18 +72,29 @@ - - \ No newline at end of file + + diff --git a/src/app/channel-list-container/channel-list-container.component.scss b/src/app/channel-list-container/channel-list-container.component.scss index 57fe7f7a8..0eaf237d5 100644 --- a/src/app/channel-list-container/channel-list-container.component.scss +++ b/src/app/channel-list-container/channel-list-container.component.scss @@ -25,4 +25,10 @@ .mat-expansion-panel { border-radius: 0 !important; } + + .mat-tab-label, .mat-tab-label-active { + min-width: 0!important; + padding: 15px!important; + margin: 5px!important; + } } \ No newline at end of file diff --git a/src/app/channel-list-container/channel-list-container.component.ts b/src/app/channel-list-container/channel-list-container.component.ts index ce0254331..dec30f002 100644 --- a/src/app/channel-list-container/channel-list-container.component.ts +++ b/src/app/channel-list-container/channel-list-container.component.ts @@ -3,7 +3,7 @@ import { Channel, ChannelStore, ChannelQuery } from '../state'; import * as _ from 'lodash'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Store } from '@datorama/akita'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-channel-list-container', @@ -22,8 +22,7 @@ export class ChannelListContainerComponent { @Input('channelList') set channelList(value: Channel[]) { - // deep copy - this._channelList = value; //JSON.parse(JSON.stringify(value)); + this._channelList = value; this.groupedChannels = _.groupBy(value, 'group.title'); } @@ -37,7 +36,11 @@ export class ChannelListContainerComponent { @Output() changeChannel: EventEmitter = new EventEmitter(); /** List with favorited channels */ - favs: Channel[] = []; + favorites$: Observable = this.channelQuery.select((store) => + this.channelQuery + .getAll() + .filter((channel) => store.favorites.includes(channel.id)) + ); /** Search term for channel filter */ searchTerm: any = { @@ -54,15 +57,7 @@ export class ChannelListContainerComponent { private channelQuery: ChannelQuery, private channelStore: ChannelStore, 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 @@ -82,21 +77,9 @@ export class ChannelListContainerComponent { * @param channel channel to update * @param clickEvent mouse click event */ - favChannel(channel: Channel, clickEvent: MouseEvent): void { + toggleFavoriteChannel(channel: Channel, clickEvent: MouseEvent): void { clickEvent.stopPropagation(); - channel.fav = !channel.fav; this.snackBar.open('Favorites were updated!', null, { duration: 2000 }); - - 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, - }; - }); + this.channelStore.updateFavorite(channel); } } diff --git a/src/app/shared/components/header/header.component.html b/src/app/shared/components/header/header.component.html index b5f7877a4..09406d1a8 100644 --- a/src/app/shared/components/header/header.component.html +++ b/src/app/shared/components/header/header.component.html @@ -3,12 +3,12 @@ - {{title}} - {{subtitle}} + {{ title }} + {{ subtitle }} settings - \ No newline at end of file + diff --git a/src/app/state/channel.store.ts b/src/app/state/channel.store.ts index a2b11332b..194dddb69 100644 --- a/src/app/state/channel.store.ts +++ b/src/app/state/channel.store.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { ElectronService } from 'app/services/electron.service'; import { Channel } from './channel.model'; export interface ChannelState extends EntityState { @@ -11,7 +12,11 @@ export interface ChannelState extends EntityState { @Injectable({ providedIn: 'root' }) @StoreConfig({ name: 'channel', resettable: true }) export class ChannelStore extends EntityStore { - constructor() { + /** + * Creates an instance of ChannelStore + * @param channelStore channels store + */ + constructor(private electronService: ElectronService) { super({ favorites: [], playlistId: '', @@ -19,7 +24,25 @@ export class ChannelStore extends EntityStore { }); } - 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 }; + }); } } diff --git a/src/app/video-player/video-player.component.html b/src/app/video-player/video-player.component.html index 8866ef58f..14d37e509 100644 --- a/src/app/video-player/video-player.component.html +++ b/src/app/video-player/video-player.component.html @@ -17,49 +17,58 @@ - - - - menu - - {{ (activeChannel$ | async)?.name }} - - - info - - - + + + + + menu + + + + {{ + (favorites$ | async).includes(activeChannel.id) + ? 'star' + : 'star_outline' + }} + + {{ activeChannel.name }} + + + info + + - - + + + + + + - - - diff --git a/src/app/video-player/video-player.component.ts b/src/app/video-player/video-player.component.ts index 50734acc6..b4b24597d 100644 --- a/src/app/video-player/video-player.component.ts +++ b/src/app/video-player/video-player.component.ts @@ -1,11 +1,12 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import * as Hls from 'hls.js'; -import { ChannelQuery, Channel } from '../state'; +import { ChannelQuery, Channel, ChannelStore } from '../state'; import { Observable } from 'rxjs'; import { MatSidenav } from '@angular/material/sidenav'; import { ElectronService } from '../services/electron.service'; import { StorageMap } from '@ngx-pwa/local-storage'; import { Settings, VideoPlayerType } from '../settings/settings.interface'; +import { MatSnackBar } from '@angular/material/snack-bar'; /** Settings key in storage */ export const SETTINGS_STORE_KEY = 'settings'; @@ -17,7 +18,12 @@ export const SETTINGS_STORE_KEY = 'settings'; }) export class VideoPlayerComponent implements OnInit { /** Channels list */ - channels$: Observable; + channels$: Observable = this.channelQuery.selectAll(); + + /** Favorites list */ + favorites$: Observable = this.channelQuery.select( + (store) => store.favorites + ); /** Video player DOM element */ videoPlayer: HTMLVideoElement; @@ -40,12 +46,16 @@ export class VideoPlayerComponent implements OnInit { /** * Creates an instance of VideoPlayerComponent * @param channelQuery akita's channel query + * @param channelStore akita's channel store * @param electronService electron service * @param storage browser storage service + * @param snackBar service to push snackbar notifications */ constructor( private channelQuery: ChannelQuery, + private channelStore: ChannelStore, private electronService: ElectronService, + private snackBar: MatSnackBar, private storage: StorageMap ) {} @@ -53,10 +63,7 @@ export class VideoPlayerComponent implements OnInit { * Sets video player and subscribes to channel list from the store */ ngOnInit(): void { - this.channels$ = this.channelQuery.selectAll(); - this.activeChannel$ = this.channelQuery.select((state) => state.active); - this.applySettings(); } @@ -109,4 +116,13 @@ export class VideoPlayerComponent implements OnInit { openAbout(): void { 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); + } }
- {{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} + {{ i + 1 + '. ' + channel?.name || 'Unnamed Channel' }}
- {{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }} + {{ + i + 1 + '. ' + channel?.name || + 'Unnamed Channel' + }}