Skip to content

Commit

Permalink
feat: add favorites and tab based navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
4gray committed Jul 18, 2020
1 parent 20e0a40 commit 8d333d9
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 99 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,68 @@
<mat-list id="top-panel">
<mat-list-item>
<mat-form-field class="full-width">
<input matInput placeholder="Search channel ({{channelList?.length}})" [(ngModel)]="searchTerm.name">
</mat-form-field>
<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">
<mat-accordion class="example-headers-align" multi>
<ng-container *ngFor="let groups of channelList | keyvalue">
<mat-expansion-panel *ngIf="groups.value.length > 0">
<mat-expansion-panel-header>
{{groups.key || 'Ungrouped'}} ({{groups.value.length}})
</mat-expansion-panel-header>

<mat-list-item
*ngFor="let channel of groups.value | filterBy: searchTerm; index as i"
<mat-tab-group>
<mat-tab label="All channels">
<mat-list id="top-panel">
<mat-list-item>
<mat-form-field class="full-width">
<input matInput placeholder="Search channel ({{_channelList?.length}})"
[(ngModel)]="searchTerm.name">
</mat-form-field>
<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">
<mat-accordion multi>
<mat-list-item *ngFor="let channel of _channelList | filterBy: searchTerm; index as i"
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)">
{{i+1 + '. ' + channel?.name || 'Unnamed Channel'}}
<p matLine>
{{ 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>
</mat-expansion-panel>

</ng-container>
</mat-accordion>
</mat-nav-list>
</mat-accordion>
</mat-nav-list>
</mat-tab>

<mat-tab label="Groups">
<mat-nav-list id="channels-list">
<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'}} ({{groups.value.length}})
</mat-expansion-panel-header>
<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' }}
</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>
</mat-expansion-panel>
</ng-container>
</mat-accordion>
</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)">
<p matLine>
{{ 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>
</mat-nav-list>
</mat-tab>
</mat-tab-group>
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@
background: #eee;
}

#channels-list {
overflow-y: auto;
height: calc(100% - 65px);
}

.active {
background: #ddd;
}
Expand All @@ -22,3 +17,10 @@
::ng-deep .mat-expansion-panel-body {
padding: 0 !important;
}

::ng-deep.mat-tab-label,
::ng-deep.mat-tab-label-active {
min-width: 0 !important;
padding: 3px 10px !important;
margin: 3px 10px !important;
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Channel } from 'src/app/state';
import { Channel, ChannelStore, ChannelQuery } from 'src/app/state';
import { NgxIndexedDBService } from 'ngx-indexed-db';
import { Playlist } from '../playlist-uploader/playlist-uploader.component';
import * as _ from 'lodash';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
selector: 'app-channel-list-container',
templateUrl: './channel-list-container.component.html',
styleUrls: ['./channel-list-container.component.css'],
styleUrls: ['./channel-list-container.component.scss'],
})
export class ChannelListContainerComponent {
/**
* Channels array
* Create local copy of the store for local manipulations without updates in the store
*/
@Input() channelList: Channel[];
_channelList: Channel[];
get channelList(): Channel[] {
return this._channelList;
}

/**
* Selected channel
*/
@Input('channelList')
set channelList(value: Channel[]) {
// deep copy
this._channelList = JSON.parse(JSON.stringify(value));
this.groupedChannels = _.groupBy(this._channelList, 'group.title');
}

/** Object with channels sorted by grouped */
groupedChannels: { [key: string]: Channel[] };

/** Selected channel */
selected: Channel;

/**
* Emits on channel change
*/
@Output() changeChannel: EventEmitter<{
url: string;
name: string;
}> = new EventEmitter();
/** Emits on channel change */
@Output() changeChannel: EventEmitter<Channel> = new EventEmitter();

/** List with favorited channels */
favs: Channel[] = [];

/**
* Search term for channel filter
Expand All @@ -32,12 +46,65 @@ export class ChannelListContainerComponent {
name: '',
};

/**
* Creates an instance of ChannelListContainerComponent
* @param channelQuery akita's channel query
* @param channelStore akita's channel store
* @param dbService service to work with indexed db
* @param snackBar service to push snackbar notifications
*/
constructor(
private channelQuery: ChannelQuery,
private channelStore: ChannelStore,
private dbService: NgxIndexedDBService,
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
* @param channel selected channel
*/
selectChannel(channel: Channel): void {
this.selected = channel;
this.changeChannel.emit({ url: channel.url, name: channel.name });
this.changeChannel.emit(channel);
}

/**
* Toggles favorite flag for the given channel
* @param channel channel to update
* @param clickEvent mouse click event
*/
favChannel(channel: Channel, clickEvent: MouseEvent): void {
clickEvent.stopPropagation();
channel.fav = !channel.fav;
this.snackBar.open('Favorites were updated!');

this.channelStore.update(channel.id, { fav: channel.fav });
// update channels fav flag in the indexed db
this.channelStore.update((store) => {
const favorites = channel.fav
? [...store.favorites, channel.id]
: [...store.favorites.filter((favId) => channel.id !== favId)];
this.dbService
.getByID('playlists', store.playlistId)
.then((dataset: Playlist) => {
this.dbService.update('playlists', {
...dataset,
favorites,
});
});

return {
favorites,
};
});
}
}
71 changes: 42 additions & 29 deletions src/app/components/playlist-uploader/playlist-uploader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@ import {
humanizeBytes,
UploaderOptions,
} from 'ngx-uploader';
import { ChannelStore, createChannel, Channel } from 'src/app/state';
import { ChannelStore, createChannel } from 'src/app/state';
import { M3uService } from 'src/app/services/m3u-service.service';
import { Router } from '@angular/router';
import { NgxIndexedDBService } from 'ngx-indexed-db';
import { PLAYLISTS_STORE } from 'src/app/db.config';
import { MatSnackBar } from '@angular/material/snack-bar';
import { guid, ID } from '@datorama/akita';

/**
* Describes playlist interface
*/
export interface Playlist {
id?: number;
id?: ID;
title: string;
filename: string;
playlist: any;
importDate: number;
lastUsage: number;
favorites: string[];
}

@Component({
Expand All @@ -46,6 +48,7 @@ export class PlaylistUploaderComponent {
* @param dbService indexeddb service
* @param m3uService m3u service
* @param router angulars router
* @param snackBar snackbars with notification messages
*/
constructor(
private channelStore: ChannelStore,
Expand Down Expand Up @@ -74,16 +77,8 @@ export class PlaylistUploaderComponent {
this.isLoading = true;
if (this.files.length > 0) {
const fileReader = new FileReader();
fileReader.onload = (fileLoadedEvent) => {
const result = (fileLoadedEvent.target as FileReader)
.result;

const array = (result as string).split('\n');
const playlist = this.m3uService.parsePlaylist(array);
this.savePlaylist(this.files[0].name, playlist);

this.setPlaylist(playlist);
};
fileReader.onload = (fileLoadedEvent) =>
this.handlePlaylist(fileLoadedEvent);
fileReader.readAsText(this.files[0].nativeFile);
}
} else if (
Expand Down Expand Up @@ -119,6 +114,18 @@ export class PlaylistUploaderComponent {
}
}

/**
* Parse and store uploaded playlist
* @param fileLoadedEvent
*/
handlePlaylist(fileLoadedEvent: any): void {
const result = (fileLoadedEvent.target as FileReader).result;
const array = (result as string).split('\n');
const playlist = this.m3uService.parsePlaylist(array);
const playlistObject = this.savePlaylist(this.files[0].name, playlist);
this.setPlaylist(playlistObject);
}

/**
* Navigates to the video player route
*/
Expand All @@ -132,18 +139,20 @@ export class PlaylistUploaderComponent {
* @param name name of the playlist
* @param playlist playlist to save
*/
savePlaylist(name: string, playlist: any): void {
this.dbService
.add<Playlist>('playlists', {
filename: name,
title: name,
playlist,
importDate: new Date().getMilliseconds(),
lastUsage: new Date().getMilliseconds(),
})
.then(() => {
console.log('playlist saved!');
});
savePlaylist(name: string, playlist: any): Playlist {
const playlistObject = {
id: guid(),
filename: name,
title: name,
playlist,
importDate: new Date().getMilliseconds(),
lastUsage: new Date().getMilliseconds(),
favorites: [],
};
this.dbService.add<Playlist>('playlists', playlistObject).then(() => {
console.log('playlist saved!');
});
return playlistObject;
}

/**
Expand All @@ -160,15 +169,19 @@ export class PlaylistUploaderComponent {

/**
* Sets the given playlist as active for the current session
* @param playlist m3u playlist
* // TODO: use typings from iptv-parser lib
* @param playlist playlist object
*/
setPlaylist(playlist: { header: any; items: Channel[] }): void {
setPlaylist(playlist: Playlist): void {
this.channelStore.reset();
const channels = playlist.items.map((element) =>
createChannel(element)
const favorites = playlist.favorites || [];
const channels = playlist.playlist.items.map((element) =>
createChannel(element, favorites)
);
this.channelStore.upsertMany(channels);
this.channelStore.update(() => ({
favorites,
playlistId: playlist.id,
}));
this.navigateToPlayer();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<mat-nav-list>
<div mat-subheader>Recently added playlists ({{playlists.length}})</div>
<a mat-list-item *ngFor="let item of playlists; last as last" (click)="playlistClicked.emit(item.playlist);">
<a mat-list-item *ngFor="let item of playlists; last as last" (click)="playlistClicked.emit(item)">
<mat-icon mat-list-icon>folder</mat-icon>
<div mat-line>{{ item.title || item.filename }}</div>
<div mat-line>Channels: {{ item.playlist?.items?.length }}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class RecentPlaylistsComponent {
@Input() playlists: Playlist[];

/** Emits on playlist selection */
@Output() playlistClicked: EventEmitter<any> = new EventEmitter();
@Output() playlistClicked: EventEmitter<Playlist> = new EventEmitter();

/** Emits on playlist remove click */
@Output() removeClicked: EventEmitter<Playlist> = new EventEmitter();
Expand Down
Loading

0 comments on commit 8d333d9

Please sign in to comment.