diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fd7b0c7..967e631 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -24,8 +24,15 @@ import {VgControlsModule} from 'videogular2/compiled/controls'; import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play'; import {VgBufferingModule} from 'videogular2/compiled/buffering'; import { InputDialogComponent } from './input-dialog/input-dialog.component'; -import { LazyLoadImageModule } from 'ng-lazyload-image'; +import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image'; import { NgxContentLoadingModule } from 'ngx-content-loading'; +import { audioFilesMouseHovering, videoFilesMouseHovering } from './main/main.component'; +import { Observable } from 'rxjs'; +import { CreatePlaylistComponent } from './create-playlist/create-playlist.component'; + +function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { + return (element.id === 'video' ? videoFilesMouseHovering : audioFilesMouseHovering); +} @NgModule({ declarations: [ @@ -33,7 +40,8 @@ import { NgxContentLoadingModule } from 'ngx-content-loading'; FileCardComponent, MainComponent, PlayerComponent, - InputDialogComponent + InputDialogComponent, + CreatePlaylistComponent ], imports: [ BrowserModule, @@ -64,13 +72,14 @@ import { NgxContentLoadingModule } from 'ngx-content-loading'; VgControlsModule, VgOverlayPlayModule, VgBufferingModule, - LazyLoadImageModule, + LazyLoadImageModule.forRoot({ isVisible }), NgxContentLoadingModule, RouterModule, AppRoutingModule, ], entryComponents: [ - InputDialogComponent + InputDialogComponent, + CreatePlaylistComponent ], providers: [PostsService], bootstrap: [AppComponent] diff --git a/src/app/create-playlist/create-playlist.component.html b/src/app/create-playlist/create-playlist.component.html new file mode 100644 index 0000000..6b75e53 --- /dev/null +++ b/src/app/create-playlist/create-playlist.component.html @@ -0,0 +1,19 @@ + +
+
+ + + +
+
+ + {{(type === 'audio') ? 'Audio files' : 'Videos'}} + + {{file.id}} + + +
+
+ +
+ diff --git a/src/app/create-playlist/create-playlist.component.scss b/src/app/create-playlist/create-playlist.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/create-playlist/create-playlist.component.spec.ts b/src/app/create-playlist/create-playlist.component.spec.ts new file mode 100644 index 0000000..862e64b --- /dev/null +++ b/src/app/create-playlist/create-playlist.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreatePlaylistComponent } from './create-playlist.component'; + +describe('CreatePlaylistComponent', () => { + let component: CreatePlaylistComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CreatePlaylistComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreatePlaylistComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/create-playlist/create-playlist.component.ts b/src/app/create-playlist/create-playlist.component.ts new file mode 100644 index 0000000..b3d04bf --- /dev/null +++ b/src/app/create-playlist/create-playlist.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { FormControl } from '@angular/forms'; +import { PostsService } from 'app/posts.services'; + +@Component({ + selector: 'app-create-playlist', + templateUrl: './create-playlist.component.html', + styleUrls: ['./create-playlist.component.scss'] +}) +export class CreatePlaylistComponent implements OnInit { + // really "createPlaylistDialogComponent" + + filesToSelectFrom = null; + type = null; + filesSelect = new FormControl(); + name = ''; + + create_in_progress = false; + + constructor(@Inject(MAT_DIALOG_DATA) public data: any, + private postsService: PostsService, + public dialogRef: MatDialogRef) { } + + + ngOnInit() { + if (this.data) { + this.filesToSelectFrom = this.data.filesToSelectFrom; + this.type = this.data.type; + } + } + + createPlaylist() { + const thumbnailURL = this.getThumbnailURL(); + this.create_in_progress = true; + this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => { + this.create_in_progress = false; + if (res['success']) { + this.dialogRef.close(true); + } else { + this.dialogRef.close(false); + } + }); + } + + getThumbnailURL() { + for (let i = 0; i < this.filesToSelectFrom.length; i++) { + const file = this.filesToSelectFrom[i]; + if (file.id === this.filesSelect.value[0]) { + // different services store the thumbnail in different places + if (file.thumbnailURL) { return file.thumbnailURL }; + if (file.thumbnail) { return file.thumbnail }; + } + } + return null; + } + +} diff --git a/src/app/file-card/file-card.component.html b/src/app/file-card/file-card.component.html index a3caec2..c40a0ef 100644 --- a/src/app/file-card/file-card.component.html +++ b/src/app/file-card/file-card.component.html @@ -5,8 +5,8 @@
ID: {{name}}
Count: {{count}}
-
- Thumbnail +
+ Thumbnail diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 81541c4..67a9f75 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -3,6 +3,8 @@ import {PostsService} from '../posts.services'; import {MatSnackBar} from '@angular/material'; import {EventEmitter} from '@angular/core'; import { MainComponent } from 'app/main/main.component'; +import { Subject, Observable } from 'rxjs'; +import 'rxjs/add/observable/merge'; @Component({ selector: 'app-file-card', @@ -21,8 +23,18 @@ export class FileCardComponent implements OnInit { @Input() count = null; type; image_loaded = false; + image_errored = false; - constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { } + scrollSubject; + scrollAndLoad; + + constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { + this.scrollSubject = new Subject(); + this.scrollAndLoad = Observable.merge( + Observable.fromEvent(window, 'scroll'), + this.scrollSubject + ); + } ngOnInit() { this.type = this.isAudio ? 'audio' : 'video'; @@ -44,6 +56,14 @@ export class FileCardComponent implements OnInit { } + onImgError(event) { + this.image_errored = true; + } + + onHoverResponse() { + this.scrollSubject.next(); + } + imageLoaded(loaded) { this.image_loaded = true; } diff --git a/src/app/main/main.component.css b/src/app/main/main.component.css index e50149a..5ec8b78 100644 --- a/src/app/main/main.component.css +++ b/src/app/main/main.component.css @@ -111,4 +111,8 @@ mat-form-field.mat-form-field { position: absolute; bottom: 0px; width: 150px; +} + +.add-playlist-button { + float: right; } \ No newline at end of file diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index c281784..63ad205 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -11,7 +11,7 @@
- + Please enter a valid URL! @@ -21,12 +21,12 @@ Quality - + {{option.label}} -
+
@@ -80,7 +80,7 @@
- + Audio @@ -92,26 +92,30 @@
- - -
+ +
Playlists
- +
+
+ No playlists available. Create one from your downloading audio files by clicking the blue plus button. +
- + Video @@ -123,23 +127,29 @@
- - + -
+
Playlists
- + + +
+
+ No playlists available. Create one from your downloading video files by clicking the blue plus button. +
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index a72ebb1..f8866ac 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -1,10 +1,10 @@ -import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; +import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core'; import {PostsService} from '../posts.services'; import {FileCardComponent} from '../file-card/file-card.component'; import { Observable } from 'rxjs/Observable'; import {FormControl, Validators} from '@angular/forms'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatSnackBar} from '@angular/material'; +import {MatSnackBar, MatDialog} from '@angular/material'; import { saveAs } from 'file-saver'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/mapTo'; @@ -16,6 +16,10 @@ import 'rxjs/add/operator/do' import 'rxjs/add/operator/switch' import { YoutubeSearchService, Result } from '../youtube-search.service'; import { Router } from '@angular/router'; +import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component'; + +export let audioFilesMouseHovering = false; +export let videoFilesMouseHovering = false; @Component({ selector: 'app-root', @@ -156,11 +160,13 @@ export class MainComponent implements OnInit { formats_loading = false; @ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef; + @ViewChildren('audiofilecard') audioFileCards: QueryList; + @ViewChildren('videofilecard') videoFileCards: QueryList; last_valid_url = ''; last_url_check = 0; constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar, - private router: Router) { + private router: Router, public dialog: MatDialog) { this.audioOnly = false; @@ -202,7 +208,8 @@ export class MainComponent implements OnInit { this.postsService.getMp3s().subscribe(result => { const mp3s = result['mp3s']; const playlists = result['playlists']; - this.mp3s = mp3s; + // if they are different + if (JSON.stringify(this.mp3s) !== JSON.stringify(mp3s)) { this.mp3s = mp3s }; this.playlists.audio = playlists; // get thumbnail url by using first video. this is a temporary hack @@ -216,7 +223,7 @@ export class MainComponent implements OnInit { } } - this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; + if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; } } }, error => { console.log(error); @@ -227,7 +234,8 @@ export class MainComponent implements OnInit { this.postsService.getMp4s().subscribe(result => { const mp4s = result['mp4s']; const playlists = result['playlists']; - this.mp4s = mp4s; + // if they are different + if (JSON.stringify(this.mp4s) !== JSON.stringify(mp4s)) { this.mp4s = mp4s }; this.playlists.video = playlists; // get thumbnail url by using first video. this is a temporary hack @@ -241,7 +249,7 @@ export class MainComponent implements OnInit { } } - this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; + if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; } } }, error => { @@ -396,9 +404,9 @@ export class MainComponent implements OnInit { let customQualityConfiguration = null; if (this.selectedQuality !== '') { - const cachedFormatsExists = this.cachedAvailableFormats[this.url]; + const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats']; if (cachedFormatsExists) { - const audio_formats = this.cachedAvailableFormats[this.url]['audio']; + const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio']; customQualityConfiguration = audio_formats[this.selectedQuality]['format_id']; } } @@ -415,9 +423,9 @@ export class MainComponent implements OnInit { }); } else { let customQualityConfiguration = null; - const cachedFormatsExists = this.cachedAvailableFormats[this.url]; + const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats']; if (cachedFormatsExists) { - const video_formats = this.cachedAvailableFormats[this.url]['video']; + const video_formats = this.cachedAvailableFormats[this.url]['formats']['video']; if (video_formats['best_audio_format'] && this.selectedQuality !== '') { customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format']; } @@ -524,7 +532,7 @@ export class MainComponent implements OnInit { // tslint:disable-next-line: max-line-length const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/; const reYT = new RegExp(youtubeStrRegex); - const ytValid = reYT.test(str); + const ytValid = true || reYT.test(str); if (valid && ytValid && Date.now() - this.last_url_check > 1000) { if (str !== this.last_valid_url && this.allowQualitySelect) { // get info @@ -543,18 +551,33 @@ export class MainComponent implements OnInit { } getURLInfo(url) { - if (!(this.cachedAvailableFormats[url])) { - this.formats_loading = true; + if (!this.cachedAvailableFormats[url]) { + this.cachedAvailableFormats[url] = {}; + } + if (!(this.cachedAvailableFormats[url] && this.cachedAvailableFormats[url]['formats'])) { + this.cachedAvailableFormats[url]['formats_loading'] = true; this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => { - if (url === this.url) { this.formats_loading = false; } + this.cachedAvailableFormats[url]['formats_loading'] = false; const infos = res['result']; + if (!infos || !infos.formats) { + this.errorFormats(url); + return; + } const parsed_infos = this.getAudioAndVideoFormats(infos.formats); + console.log(parsed_infos); const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]}; - this.cachedAvailableFormats[url] = available_formats; + this.cachedAvailableFormats[url]['formats'] = available_formats; + }, err => { + this.errorFormats(url); }); } } + errorFormats(url) { + this.cachedAvailableFormats[url]['formats_loading'] = false; + console.error('Could not load formats for url ' + url); + } + attachToInput() { Observable.fromEvent(this.urlInput.nativeElement, 'keyup') .map((e: any) => e.target.value) // extract the value of input @@ -653,5 +676,45 @@ export class MainComponent implements OnInit { } return best_audio_format_for_mp4; } -} + accordionEntered(type) { + if (type === 'audio') { + audioFilesMouseHovering = true; + this.audioFileCards.forEach(filecard => { + filecard.onHoverResponse(); + }); + } else if (type === 'video') { + videoFilesMouseHovering = true; + this.videoFileCards.forEach(filecard => { + filecard.onHoverResponse(); + }); + } + } + + accordionLeft(type) { + if (type === 'audio') { + audioFilesMouseHovering = false; + } else if (type === 'video') { + videoFilesMouseHovering = false; + } + } + + // creating a playlist + openCreatePlaylistDialog(type) { + const dialogRef = this.dialog.open(CreatePlaylistComponent, { + data: { + filesToSelectFrom: (type === 'audio') ? this.mp3s : this.mp4s, + type: type + } + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + if (type === 'audio') { this.getMp3s() }; + if (type === 'video') { this.getMp4s() }; + this.openSnackBar('Successfully created playlist!', ''); + } else if (result === false) { + this.openSnackBar('ERROR: failed to create playlist!', ''); + } + }); + } +}