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
pull/11/head
Isaac Grynsztein 5 years ago
parent b87b49d77b
commit a0eff4d96d

@ -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<any>) {
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]

@ -0,0 +1,19 @@
<form>
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div>
<mat-form-field color="accent">
<mat-label>{{(type === 'audio') ? 'Audio files' : 'Videos'}}</mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</form>
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button>

@ -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<CreatePlaylistComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CreatePlaylistComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreatePlaylistComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<CreatePlaylistComponent>) { }
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;
}
}

@ -5,8 +5,8 @@
<br/>
<span class="max-two-lines">ID: {{name}}</span>
<div *ngIf="isPlaylist">Count: {{count}}</div>
<div class="img-div">
<img class="image" [lazyLoad]="thumbnailURL" (onLoad)="imageLoaded($event)" alt="Thumbnail">
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
<span *ngIf="!image_loaded">
<ngx-content-loading [width]="500" [height]="360">
<svg:g ngx-rect width="500" height="360" y="0" x="0" rx="4" ry="4"></svg:g>

@ -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;
}

@ -112,3 +112,7 @@ mat-form-field.mat-form-field {
bottom: 0px;
width: 150px;
}
.add-playlist-button {
float: right;
}

@ -11,7 +11,7 @@
<div class="row">
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
<mat-form-field color="accent" class="example-full-width">
<input style="padding-right: 25px;" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" required #urlinput>
<input style="padding-right: 25px;" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" required #urlinput>
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
</mat-form-field>
@ -21,12 +21,12 @@
<mat-label>Quality</mat-label>
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url] && cachedAvailableFormats[url][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
{{option.label}}
</mat-option>
</ng-container>
</mat-select>
<div class="spinner-div" *ngIf="formats_loading && !cachedAvailableFormats[url]">
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-form-field>
@ -80,7 +80,7 @@
</ng-template>
<div style="margin: 20px" *ngIf="fileManagerEnabled">
<mat-accordion>
<mat-expansion-panel class="big">
<mat-expansion-panel (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
<mat-expansion-panel-header>
<mat-panel-title>
Audio
@ -92,26 +92,30 @@
<div *ngIf="mp3s.length > 0;else nomp3s">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
<app-file-card (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="true"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<mat-divider *ngIf="playlists.audio.length > 0"></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.audio.length > 0">
<mat-divider></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;">
<h6>Playlists</h6>
</div>
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
<app-file-card (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('audio')" mat-fab><mat-icon>add</mat-icon></button></div>
<div *ngIf="playlists.audio.length === 0">
No playlists available. Create one from your downloading audio files by clicking the blue plus button.
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel class="big">
<mat-expansion-panel (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" class="big">
<mat-expansion-panel-header>
<mat-panel-title>
Video
@ -123,23 +127,29 @@
<div *ngIf="mp4s.length > 0;else nomp4s">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="false"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<mat-divider *ngIf="playlists.video.length > 0"></mat-divider>
<mat-divider></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.video.length > 0">
<div style="width: 100%; text-align: center; margin-top: 10px;">
<h6>Playlists</h6>
</div>
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
<app-file-card (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<!-- Add video playlist button -->
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
<div *ngIf="playlists.video.length === 0">
No playlists available. Create one from your downloading video files by clicking the blue plus button.
</div>
</div>
</mat-expansion-panel>
</mat-accordion>

@ -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<FileCardComponent>;
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
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!', '');
}
});
}
}

Loading…
Cancel
Save