⬇️ Added downloading features

This commit is contained in:
corner 2019-10-05 11:12:36 +02:00
parent b10e7ce8e1
commit 7a55a6b17f
18 changed files with 255 additions and 17 deletions

View File

@ -4,6 +4,8 @@ import { HomeComponent } from './components/home/home.component';
import { ArtistsComponent } from './components/artists/artists.component'; import { ArtistsComponent } from './components/artists/artists.component';
import { AlbumsComponent } from './components/albums/albums.component'; import { AlbumsComponent } from './components/albums/albums.component';
import { SearchComponent } from './components/search/search.component'; import { SearchComponent } from './components/search/search.component';
import { AddsongComponent } from './components/addsong/addsong.component';
import { DownloadsComponent } from './components/downloads/downloads.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: HomeComponent }, { path: '', component: HomeComponent },
@ -11,7 +13,9 @@ const routes: Routes = [
{ path: 'artists', component: ArtistsComponent }, { path: 'artists', component: ArtistsComponent },
{ path: 'albums/:id', component: AlbumsComponent }, { path: 'albums/:id', component: AlbumsComponent },
{ path: 'albums', component: AlbumsComponent }, { path: 'albums', component: AlbumsComponent },
{ path: 'search/:query', component: SearchComponent } { path: 'search/:query', component: SearchComponent },
{ path: 'add/song', component: AddsongComponent },
{ path: 'downloads', component: DownloadsComponent }
]; ];
@NgModule({ @NgModule({

View File

@ -7,8 +7,8 @@
Explore Explore
<fa-icon [icon]="faGlobeEurope" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="faGlobeEurope" [fixedWidth]="true"></fa-icon>
</a> </a>
<a routerLink="songs" class="list-group-item list-group-item-action bg-light d-flex justify-content-between align-items-center"> <a routerLink="/add/song" class="list-group-item list-group-item-action bg-light d-flex justify-content-between align-items-center">
Songs Add Song
<fa-icon [icon]="faMusic" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="faMusic" [fixedWidth]="true"></fa-icon>
</a> </a>
<a routerLink="albums" class="list-group-item list-group-item-action bg-light d-flex justify-content-between align-items-center"> <a routerLink="albums" class="list-group-item list-group-item-action bg-light d-flex justify-content-between align-items-center">
@ -42,5 +42,22 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</div> </div>
<div style="position: absolute; top: 0; right: 0;">
<div *ngFor="let item of notification.notificationStack" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<img src="..." class="rounded mr-2" alt="...">
<strong class="mr-auto">{{ item.user }}</strong>
<small>11 mins ago</small>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
{{ item.message }}
</div>
</div>
</div>
<!-- Music controls --> <!-- Music controls -->
<app-controls class="d-flex"></app-controls> <app-controls class="d-flex"></app-controls>

View File

@ -1,4 +1,4 @@
// Main // Main
body { body {
overflow-x: hidden; overflow-x: hidden;
} }
@ -15,14 +15,14 @@ app-controls {
// Sidebar // Sidebar
#sidebar-wrapper { #sidebar-wrapper {
min-height: inherit; min-height: inherit;
margin-left: -15rem; margin-left: -15rem;
-webkit-transition: margin .25s ease-out; -webkit-transition: margin .25s ease-out;
-moz-transition: margin .25s ease-out; -moz-transition: margin .25s ease-out;
-o-transition: margin .25s ease-out; -o-transition: margin .25s ease-out;
transition: margin .25s ease-out; transition: margin .25s ease-out;
} }
#sidebar-wrapper .sidebar-heading { #sidebar-wrapper .sidebar-heading {
padding: 0.875rem 1.25rem; padding: 0.875rem 1.25rem;
font-size: 1.2rem; font-size: 1.2rem;
@ -31,7 +31,7 @@ app-controls {
.sidebar-heading > img { .sidebar-heading > img {
width: 64px; width: 64px;
} }
#sidebar-wrapper .list-group { #sidebar-wrapper .list-group {
width: 15rem; width: 15rem;
} }
@ -48,7 +48,7 @@ app-controls {
#navbar-search { #navbar-search {
display: none; display: none;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
#sidebar-wrapper { #sidebar-wrapper {
margin-left: 0; margin-left: 0;
@ -62,8 +62,12 @@ app-controls {
min-width: 0; min-width: 0;
width: 100%; width: 100%;
} }
#wrapper.toggled #sidebar-wrapper { #wrapper.toggled #sidebar-wrapper {
margin-left: -15rem; margin-left: -15rem;
} }
} }
.toast {
opacity: 1;
}

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { faGlobeEurope, faMusic, faCompactDisc, faUsers, faCog, faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { faGlobeEurope, faMusic, faCompactDisc, faUsers, faCog, faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { NotificationService } from './services/notification.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -26,7 +27,8 @@ export class AppComponent implements OnInit {
constructor( constructor(
public router: Router, public router: Router,
private fb: FormBuilder private fb: FormBuilder,
public notification: NotificationService
) { } ) { }
ngOnInit() { ngOnInit() {

View File

@ -13,6 +13,8 @@ import { AlbumsComponent } from './components/albums/albums.component';
import { ControlsComponent } from './components/controls/controls.component'; import { ControlsComponent } from './components/controls/controls.component';
import { SonglistComponent } from './components/songlist/songlist.component'; import { SonglistComponent } from './components/songlist/songlist.component';
import { SearchComponent } from './components/search/search.component'; import { SearchComponent } from './components/search/search.component';
import { AddsongComponent } from './components/addsong/addsong.component';
import { DownloadsComponent } from './components/downloads/downloads.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -22,7 +24,9 @@ import { SearchComponent } from './components/search/search.component';
AlbumsComponent, AlbumsComponent,
ControlsComponent, ControlsComponent,
SonglistComponent, SonglistComponent,
SearchComponent SearchComponent,
AddsongComponent,
DownloadsComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,4 @@
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input type="text" formControlName="id">
<input type="submit">
</form>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AddsongComponent } from './addsong.component';
describe('AddsongComponent', () => {
let component: AddsongComponent;
let fixture: ComponentFixture<AddsongComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddsongComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddsongComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { DownloadService } from 'src/app/services/download.service';
import { FormBuilder } from '@angular/forms';
@Component({
selector: 'app-addsong',
templateUrl: './addsong.component.html',
styleUrls: ['./addsong.component.scss']
})
export class AddsongComponent implements OnInit {
form = this.fb.group({
id: ''
});
constructor(
private download: DownloadService,
private fb: FormBuilder
) { }
ngOnInit() { }
onSubmit() {
this.download.addQueueItem(this.form.value.id);
}
}

View File

@ -0,0 +1,9 @@
<div class="container mt-2" *ngIf="downloadQueue as queue">
<div *ngFor="let item of queue.data.result">
{{ item.info.title }}
<div class="progress">
<div class="progress-bar" role="progressbar" [style.width]="getProgress(download.convertTimeToSeconds(item.progress.timemark) / item.info.length_seconds * 100)" aria-valuenow="25" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DownloadsComponent } from './downloads.component';
describe('DownloadsComponent', () => {
let component: DownloadsComponent;
let fixture: ComponentFixture<DownloadsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DownloadsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DownloadsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { DownloadService } from 'src/app/services/download.service';
import { AxiosResponse } from 'axios';
import { ApiData } from 'src/app/services/data.service';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-downloads',
templateUrl: './downloads.component.html',
styleUrls: ['./downloads.component.scss']
})
export class DownloadsComponent implements OnInit {
downloadQueue: AxiosResponse<ApiData>;
constructor(
public download: DownloadService,
private sanitizer: DomSanitizer
) { }
ngOnInit() {
this.getDownloadQueue();
setInterval(() => {
this.getDownloadQueue();
}, 1000);
}
getDownloadQueue() {
this.download.getDownloadQueue().then(res => {
this.downloadQueue = res;
});
}
getProgress(val) {
return this.sanitizer.bypassSecurityTrustStyle(`${val}%`);
}
}

View File

@ -11,7 +11,6 @@ import { ApiService } from 'src/app/services/api.service';
export class HomeComponent implements OnInit { export class HomeComponent implements OnInit {
songs: Promise<Song[]>; songs: Promise<Song[]>;
artists: Artist[] = [];
constructor( constructor(
private api: ApiService, private api: ApiService,

View File

@ -24,9 +24,7 @@ export class DataService {
private axiosInstance: AxiosInstance; private axiosInstance: AxiosInstance;
constructor() { constructor() {
this.axiosInstance = axios.create({ this.axiosInstance = axios.create();
timeout: 3000
});
} }
/** /**
* @param type The type of the thing you want to get. The type is a string containing either 'artist', 'album' or 'song'. * @param type The type of the thing you want to get. The type is a string containing either 'artist', 'album' or 'song'.
@ -85,4 +83,12 @@ export class DataService {
search(query: string): Promise<AxiosResponse<ApiData>> { search(query: string): Promise<AxiosResponse<ApiData>> {
return this.axiosInstance.get(`${this.apiUrl}/search/${query}`); return this.axiosInstance.get(`${this.apiUrl}/search/${query}`);
} }
download(id: string): Promise<AxiosResponse<ApiData>> {
return this.axiosInstance.get(`${this.apiUrl}/download/${id}`);
}
downloadqueue(): Promise<AxiosResponse<ApiData>> {
return this.axiosInstance.get(`${this.apiUrl}/downloadqueue`);
}
} }

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { DownloadService } from './download.service';
describe('DownloadService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: DownloadService = TestBed.get(DownloadService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { DataService } from './data.service';
import { AxiosResponse } from 'axios';
import { NotificationService } from './notification.service';
@Injectable({
providedIn: 'root'
})
export class DownloadService {
constructor(
private data: DataService,
private notification: NotificationService
) { }
downloadQueue: Promise<AxiosResponse>[] = [];
addQueueItem(id: string) {
const index = this.downloadQueue.push(this.data.download(id)) - 1;
this.downloadQueue[index].then(res => {
// tslint:disable-next-line: max-line-length
this.notification.newNotification({ user: 'Download manager', message: `The song ${res.data.result.title} is downloaded successfully.`}, 100000);
this.downloadQueue.splice(index, 1);
});
}
getDownloadQueue() {
return this.data.downloadqueue();
}
// Input like: "00:00:54.43"
convertTimeToSeconds(input: string): number {
if (typeof input === 'string') {
const parts = input.split(/[\:\.]/);
let total = 0;
total += Number(parts[0]) * 60 * 60; // Hours
total += Number(parts[1]) * 60; // Minutes
total += Number(parts[2]); // Seconds
total += Number(parts[3]) / 100; // Centiseconds
return total;
} else {
return;
}
}
}

View File

@ -1,9 +1,24 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
interface Notification {
user: string;
message: string;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class NotificationService { export class NotificationService {
constructor() { } constructor() {
}
notificationStack: Notification[] = [];
newNotification(notification: Notification, timeout: number = 3000) {
const index = this.notificationStack.push(notification);
setTimeout(() => this.notificationStack.splice(index - 1, 1), timeout);
}
} }