From a0eff4d96d275f874f79d1951f844e4507a5ce1c Mon Sep 17 00:00:00 2001 From: Isaac Grynsztein Date: Sun, 23 Feb 2020 03:18:26 -0500 Subject: [PATCH] images on file cards now load when the accordion is hovered over to increase responsiveness. images are loading maybe a second before clicking so hopefully they're done by the time the expansion finishes added the ability to create playlists in the gui through a new dialog reloading mp3s/mp4s doesn't cause an image refresh anymore when the list is unchanged fixed loading spinner of available formats so it now only shows when it is loading the current url file card images now don't show when errored or thumbnailurl doesn't exist --- src/app/app.module.ts | 17 +++- .../create-playlist.component.html | 19 ++++ .../create-playlist.component.scss | 0 .../create-playlist.component.spec.ts | 25 +++++ .../create-playlist.component.ts | 58 +++++++++++ src/app/file-card/file-card.component.html | 4 +- src/app/file-card/file-card.component.ts | 22 ++++- src/app/main/main.component.css | 4 + src/app/main/main.component.html | 36 ++++--- src/app/main/main.component.ts | 97 +++++++++++++++---- 10 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 src/app/create-playlist/create-playlist.component.html create mode 100644 src/app/create-playlist/create-playlist.component.scss create mode 100644 src/app/create-playlist/create-playlist.component.spec.ts create mode 100644 src/app/create-playlist/create-playlist.component.ts 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!', ''); + } + }); + } +}