mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:47:41 -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-item>
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Search channel ({{_channelList?.length}})"
|
||||
[(ngModel)]="searchTerm.name">
|
||||
<input
|
||||
matInput
|
||||
placeholder="Search channel ({{
|
||||
_channelList?.length
|
||||
}})"
|
||||
[(ngModel)]="searchTerm.name"
|
||||
/>
|
||||
</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>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<mat-nav-list id="channels-list">
|
||||
<cdk-virtual-scroll-viewport itemSize="50" class="scroll-viewport">
|
||||
<mat-list-item *cdkVirtualFor="let channel of _channelList | filterBy: searchTerm; index as i"
|
||||
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)">
|
||||
<mat-list-item
|
||||
*cdkVirtualFor="
|
||||
let channel of _channelList | filterBy: searchTerm;
|
||||
index as i
|
||||
"
|
||||
[class.active]="selected?.id === channel.id"
|
||||
(click)="selectChannel(channel)"
|
||||
>
|
||||
<p matLine>
|
||||
{{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }}
|
||||
{{ i + 1 + '. ' + channel?.name || 'Unnamed Channel' }}
|
||||
</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-list-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
@@ -32,18 +44,25 @@
|
||||
<mat-accordion multi>
|
||||
<ng-container *ngFor="let groups of groupedChannels | keyvalue">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header *ngIf="groups.value.length > 0">
|
||||
{{(groups.key || 'Ungrouped') | titlecase}} ({{groups.value.length}})
|
||||
<mat-expansion-panel-header
|
||||
*ngIf="groups.value.length > 0"
|
||||
>
|
||||
{{ groups.key || 'Ungrouped' | titlecase }} ({{
|
||||
groups.value.length
|
||||
}})
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<mat-list-item *ngFor="let channel of groups.value; index as i"
|
||||
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)">
|
||||
<mat-list-item
|
||||
*ngFor="let channel of groups.value; index as i"
|
||||
[class.active]="selected?.id === channel.id"
|
||||
(click)="selectChannel(channel)"
|
||||
>
|
||||
<p matLine>
|
||||
{{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }}
|
||||
{{
|
||||
i + 1 + '. ' + channel?.name ||
|
||||
'Unnamed Channel'
|
||||
}}
|
||||
</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-list-item>
|
||||
</ng-template>
|
||||
@@ -53,18 +72,29 @@
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<!-- <mat-tab label="Favorites">
|
||||
<mat-nav-list>
|
||||
<mat-list-item *ngFor="let channel of favs | filterBy: searchTerm; index as i"
|
||||
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)">
|
||||
<mat-tab label="Favorites">
|
||||
<mat-nav-list *ngIf="favorites$ | async as favorites">
|
||||
<mat-list-item
|
||||
*ngFor="
|
||||
let channel of favorites | filterBy: searchTerm;
|
||||
index as i
|
||||
"
|
||||
[class.active]="selected?.id === channel.id"
|
||||
(click)="selectChannel(channel)"
|
||||
>
|
||||
<p matLine>
|
||||
{{ i+1 + '. ' + channel?.name || 'Unnamed Channel' }}
|
||||
{{ i + 1 + '. ' + channel?.name || 'Unnamed Channel' }}
|
||||
</p>
|
||||
<button mat-icon-button color="primary" (click)="favChannel(channel, $event)">
|
||||
<mat-icon>star{{ channel.fav ? '' : '_outline' }}</mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
matTooltip="Remove from favorites"
|
||||
(click)="toggleFavoriteChannel(channel, $event)"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
</mat-tab> -->
|
||||
</mat-tab-group>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Channel> = new EventEmitter();
|
||||
|
||||
/** 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 */
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
</div>
|
||||
<div fxFlex="100" fxLayoutAlign="start center">
|
||||
<div fxLayout="column">
|
||||
<div fxFlex class="title">{{title}}</div>
|
||||
<div class="subtitle">{{subtitle}}</div>
|
||||
<div fxFlex class="title">{{ title }}</div>
|
||||
<div class="subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div fxLayout="column" fxFlex="50px" fxLayoutAlign="center center">
|
||||
<button mat-icon-button (click)="openSettings()">
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<Channel> {
|
||||
@@ -11,7 +12,11 @@ export interface ChannelState extends EntityState<Channel> {
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@StoreConfig({ name: 'channel', resettable: true })
|
||||
export class ChannelStore extends EntityStore<ChannelState> {
|
||||
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<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-content>
|
||||
<!-- toolbar with drawer icon -->
|
||||
<mat-toolbar color="primary">
|
||||
<button
|
||||
mat-icon-button
|
||||
mat-toolbar="Add to favorites"
|
||||
(click)="drawer.toggle()"
|
||||
matTooltip="Open channels list"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
{{ (activeChannel$ | async)?.name }}
|
||||
<div class="spacer"></div>
|
||||
<button mat-icon-button (click)="openAbout()">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
<!-- <mat-icon>star_outline</mat-icon> -->
|
||||
</mat-toolbar>
|
||||
<ng-container *ngIf="activeChannel$ | async as activeChannel">
|
||||
<!-- toolbar with drawer icon -->
|
||||
<mat-toolbar color="primary">
|
||||
<button
|
||||
mat-icon-button
|
||||
mat-toolbar="Add to favorites"
|
||||
(click)="drawer.toggle()"
|
||||
matTooltip="Open channels list"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="addToFavorites(activeChannel)"
|
||||
matTooltip="Toggle favorite flag"
|
||||
>
|
||||
<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
|
||||
*ngIf="
|
||||
player === 'videojs' &&
|
||||
(activeChannel$ | async) as activeChannel
|
||||
"
|
||||
>
|
||||
<app-vjs-player
|
||||
[options]="{
|
||||
sources: [
|
||||
{
|
||||
src: activeChannel.url,
|
||||
type: 'application/x-mpegURL'
|
||||
}
|
||||
]
|
||||
}"
|
||||
></app-vjs-player>
|
||||
<ng-container *ngIf="player === 'videojs'">
|
||||
<app-vjs-player
|
||||
[options]="{
|
||||
sources: [
|
||||
{
|
||||
src: activeChannel.url,
|
||||
type: 'application/x-mpegURL'
|
||||
}
|
||||
]
|
||||
}"
|
||||
></app-vjs-player>
|
||||
</ng-container>
|
||||
|
||||
<!-- player component -->
|
||||
<video
|
||||
*ngIf="player === 'html5'"
|
||||
id="video-player"
|
||||
autoplay="true"
|
||||
controls="true"
|
||||
muted="muted"
|
||||
></video>
|
||||
</ng-container>
|
||||
|
||||
<!-- player component -->
|
||||
<video
|
||||
*ngIf="player === 'html5'"
|
||||
id="video-player"
|
||||
autoplay="true"
|
||||
controls="true"
|
||||
muted="muted"
|
||||
></video>
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
||||
@@ -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<Channel[]>;
|
||||
channels$: Observable<Channel[]> = this.channelQuery.selectAll();
|
||||
|
||||
/** Favorites list */
|
||||
favorites$: Observable<string[]> = 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user