diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 301c99b71..1d9fb0d64 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,6 +16,9 @@ import { MaterialModule } from './material.module'; import { RecentPlaylistsComponent } from './components/recent-playlists/recent-playlists.component'; import { FilterPipeModule } from 'ngx-filter-pipe'; import { FormsModule } from '@angular/forms'; +import { NgxIndexedDBModule } from 'ngx-indexed-db'; +import { dbConfig } from './db.config'; + @NgModule({ declarations: [ @@ -33,6 +36,7 @@ import { FormsModule } from '@angular/forms'; FilterPipeModule, FormsModule, HttpClientModule, + NgxIndexedDBModule.forRoot(dbConfig), MaterialModule, NgxUploaderModule, ServiceWorkerModule.register('ngsw-worker.js', { diff --git a/src/app/components/channel-list-container/channel-list-container.component.css b/src/app/components/channel-list-container/channel-list-container.component.css index 06bb81697..1506753a7 100644 --- a/src/app/components/channel-list-container/channel-list-container.component.css +++ b/src/app/components/channel-list-container/channel-list-container.component.css @@ -2,6 +2,15 @@ width: 100%; } -mat-list { - margin-top: 10px; +#top-panel { + background: #eee; } + +#channels-list { + overflow-y: auto; + height: calc(100% - 65px); +} + +.active { + background: #ddd; +} \ No newline at end of file diff --git a/src/app/components/channel-list-container/channel-list-container.component.html b/src/app/components/channel-list-container/channel-list-container.component.html index 14d5cd18a..2a8dfcadf 100644 --- a/src/app/components/channel-list-container/channel-list-container.component.html +++ b/src/app/components/channel-list-container/channel-list-container.component.html @@ -1,13 +1,14 @@ - + - + - - - - + + + + {{i+1 + '. ' + channel?.inf?.title || 'Unnamed Channel'}} + + + diff --git a/src/app/components/channel-list-container/channel-list-container.component.ts b/src/app/components/channel-list-container/channel-list-container.component.ts index d32e8926f..43b607d32 100644 --- a/src/app/components/channel-list-container/channel-list-container.component.ts +++ b/src/app/components/channel-list-container/channel-list-container.component.ts @@ -1,4 +1,5 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Channel } from 'src/app/state'; @Component({ selector: 'app-channel-list-container', @@ -9,7 +10,12 @@ export class ChannelListContainerComponent { /** * Channels array */ - @Input() channelList; + @Input() channelList: Channel[]; + + /** + * Selected channel + */ + selected: Channel; /** * Emits on channel change @@ -27,4 +33,13 @@ export class ChannelListContainerComponent { title: '', }, }; + + /** + * 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, title: channel.inf.title}); + } } diff --git a/src/app/components/playlist-uploader/playlist-uploader.component.css b/src/app/components/playlist-uploader/playlist-uploader.component.css index 93050ab60..d6ea15ea9 100644 --- a/src/app/components/playlist-uploader/playlist-uploader.component.css +++ b/src/app/components/playlist-uploader/playlist-uploader.component.css @@ -1,3 +1,9 @@ +:host { + max-width: 800px; + margin: 0 auto; + display: block; +} + .drop-container { height: 200px; border: 2px dashed #ccc; @@ -8,7 +14,7 @@ color: #666; } -input { +#file input { border-radius: 4px; border: 1px solid #ccc; padding: 5px; @@ -18,7 +24,7 @@ input { font-size: 1.25em; } -.inputfile { +.input-file { width: 0.1px; height: 0.1px; opacity: 0; @@ -27,7 +33,7 @@ input { z-index: -1; } -.inputfile + label { +.input-file + label { font-size: 1.25em; font-weight: 700; color: white; @@ -35,12 +41,12 @@ input { display: inline-block; } -.inputfile:focus + label, -.inputfile + label:hover { +.input-file:focus + label, +.input-file + label:hover { background-color: red; } -.inputfile + label { +.input-file + label { cursor: pointer; line-height: 35px; padding: 3px 10px; diff --git a/src/app/components/playlist-uploader/playlist-uploader.component.html b/src/app/components/playlist-uploader/playlist-uploader.component.html index 374aec1fa..95aaeba6b 100644 --- a/src/app/components/playlist-uploader/playlist-uploader.component.html +++ b/src/app/components/playlist-uploader/playlist-uploader.component.html @@ -1,3 +1,10 @@ + + + + + + +
Add playlist

or

-
-
- - - Recent playlists - - - - - - + \ No newline at end of file diff --git a/src/app/components/playlist-uploader/playlist-uploader.component.ts b/src/app/components/playlist-uploader/playlist-uploader.component.ts index a51a77e8d..f616ba0d3 100644 --- a/src/app/components/playlist-uploader/playlist-uploader.component.ts +++ b/src/app/components/playlist-uploader/playlist-uploader.component.ts @@ -9,6 +9,21 @@ import { 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'; + +/** + * Describes playlist interface + */ +export interface Playlist { + id?: number; + title: string; + filename: string; + playlist: any; + importDate: number; + lastUsage: number; +} @Component({ selector: 'app-playlist-uploader', @@ -22,18 +37,22 @@ export class PlaylistUploaderComponent { humanizeBytes: Function; dragOver: boolean; options: UploaderOptions; - playlists: any; + playlists: Playlist[] = []; + isLoading = false; /** * Creates an instanceof PlaylistUploaderComponent * @param channelStore channels store + * @param dbService indexeddb service * @param m3uService m3u service * @param router angulars router */ constructor( private channelStore: ChannelStore, + private dbService: NgxIndexedDBService, private m3uService: M3uService, - private router: Router + private router: Router, + private snackBar: MatSnackBar ) { this.getPlaylists(); @@ -52,6 +71,7 @@ export class PlaylistUploaderComponent { */ onUploadOutput(output: UploadOutput): void { if (output.type === 'allAddedToQueue') { + this.isLoading = true; if (this.files.length > 0) { const fileReader = new FileReader(); fileReader.onload = fileLoadedEvent => { @@ -64,7 +84,7 @@ export class PlaylistUploaderComponent { ); this.savePlaylist( this.files[0].name, - JSON.stringify(playlist) + playlist ); this.setPlaylist(playlist); @@ -108,6 +128,7 @@ export class PlaylistUploaderComponent { * Navigates to the video player route */ navigateToPlayer(): void { + this.isLoading = false; this.router.navigateByUrl('/iptv', { skipLocationChange: true }); } @@ -117,21 +138,21 @@ export class PlaylistUploaderComponent { * @param playlist playlist to save */ savePlaylist(name: string, playlist: any): void { - localStorage.setItem(name, playlist); + this.dbService.add('playlists', { filename: name, title: name, playlist, importDate: new Date().getMilliseconds(), lastUsage: new Date().getMilliseconds() }).then(() => { + console.log('playlist saved!') + }); } /** - * Reads all saved playlists from the localStorage + * Reads all saved playlists from the browser store */ getPlaylists(): void { - this.playlists = { ...localStorage }; - - this.playlists = Object.keys(this.playlists) - .filter(key => key.includes('.m3u')) - .reduce((obj, key) => { - obj[key] = JSON.parse(this.playlists[key]); - return obj; - }, {}); + this.dbService.getAll('playlists').then( + (playlists: Playlist[]) => this.playlists = playlists, + error => { + console.error(error); + } + ); } /** @@ -139,9 +160,19 @@ export class PlaylistUploaderComponent { * @param playlist m3u playlist */ setPlaylist(playlist: any): void { - playlist.segments.forEach(element => { - this.channelStore.add(createChannel(element)); - this.navigateToPlayer(); - }); + this.channelStore.reset(); + const channels = playlist.segments.map(element => createChannel(element)); + this.channelStore.upsertMany(channels); + this.navigateToPlayer(); + } + + /** + * Removes the provided playlist from the indexedDb + * @param playlist playlist to remove + */ + removePlaylist(playlist: Playlist) { + this.dbService.delete(PLAYLISTS_STORE, playlist.id); + this.getPlaylists(); + this.snackBar.open('Done! Playlist was removed.', null,{ duration: 2000 }); } } diff --git a/src/app/components/recent-playlists/recent-playlists.component.html b/src/app/components/recent-playlists/recent-playlists.component.html index 396a34e29..92da2c9f4 100644 --- a/src/app/components/recent-playlists/recent-playlists.component.html +++ b/src/app/components/recent-playlists/recent-playlists.component.html @@ -1,3 +1,15 @@ - \ No newline at end of file + +
Recently added playlists
+ + folder +
{{ item.title || item.filename }}
+
Channels: {{ item.playlist?.segments.length }}
+ + + +
+
\ No newline at end of file diff --git a/src/app/components/recent-playlists/recent-playlists.component.scss b/src/app/components/recent-playlists/recent-playlists.component.scss index 88254dbbc..c07b49bc0 100644 --- a/src/app/components/recent-playlists/recent-playlists.component.scss +++ b/src/app/components/recent-playlists/recent-playlists.component.scss @@ -1,3 +1,3 @@ -button { - margin: 5px; -} +mat-list-item { + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/components/recent-playlists/recent-playlists.component.ts b/src/app/components/recent-playlists/recent-playlists.component.ts index fdee9f98a..5bb584227 100644 --- a/src/app/components/recent-playlists/recent-playlists.component.ts +++ b/src/app/components/recent-playlists/recent-playlists.component.ts @@ -1,4 +1,5 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Playlist } from '../playlist-uploader/playlist-uploader.component'; @Component({ selector: 'app-recent-playlists', @@ -6,13 +7,15 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; styleUrls: ['./recent-playlists.component.scss'], }) export class RecentPlaylistsComponent { - /** - * Playlist object - */ - @Input() playlists: any; + /** All available playlists */ + @Input() playlists: Playlist[]; - /** - * Emits on playlist click - */ + /** Emits on playlist selection */ @Output() playlistClicked: EventEmitter = new EventEmitter(); + + /** Emits on playlist remove click */ + @Output() removeClicked: EventEmitter = new EventEmitter(); + + /** Emits on playlist rename click */ + @Output() renameClicked: EventEmitter = new EventEmitter(); } diff --git a/src/app/components/video-player/video-player.component.html b/src/app/components/video-player/video-player.component.html index 22240a972..7209284ba 100644 --- a/src/app/components/video-player/video-player.component.html +++ b/src/app/components/video-player/video-player.component.html @@ -6,6 +6,7 @@ diff --git a/src/app/db.config.ts b/src/app/db.config.ts new file mode 100644 index 000000000..ac2cb8b9c --- /dev/null +++ b/src/app/db.config.ts @@ -0,0 +1,19 @@ +import { DBConfig } from 'ngx-indexed-db'; + +export const PLAYLISTS_STORE = 'playlists'; + +export const dbConfig: DBConfig = { + name: 'MyIptvDb', + version: 1, + objectStoresMeta: [{ + store: PLAYLISTS_STORE, + storeConfig: { keyPath: 'id', autoIncrement: true }, + storeSchema: [ + { name: 'title', keypath: 'title', options: { unique: false } }, + { name: 'filename', keypath: 'filename', options: { unique: false } }, + { name: 'playlist', keypath: 'playlist', options: { unique: false } }, + { name: 'importDate', keypath: 'importDate', options: { unique: false } }, + { name: 'lastUsage', keypath: 'lastUsage', options: { unique: false } }, + ] + }] +}; \ No newline at end of file diff --git a/src/app/material.module.ts b/src/app/material.module.ts index 1eaf13ae9..fe6ccdafb 100644 --- a/src/app/material.module.ts +++ b/src/app/material.module.ts @@ -4,8 +4,10 @@ import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; @NgModule({ exports: [ @@ -14,7 +16,9 @@ import { MatToolbarModule } from '@angular/material/toolbar'; MatIconModule, MatInputModule, MatListModule, + MatProgressBarModule, MatSidenavModule, + MatSnackBarModule, MatToolbarModule, ], })