⬇️ Added downloading features
This commit is contained in:
parent
b10e7ce8e1
commit
7a55a6b17f
@ -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({
|
||||||
|
@ -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">×</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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
|
4
src/app/components/addsong/addsong.component.html
Normal file
4
src/app/components/addsong/addsong.component.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
<input type="text" formControlName="id">
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
0
src/app/components/addsong/addsong.component.scss
Normal file
0
src/app/components/addsong/addsong.component.scss
Normal file
25
src/app/components/addsong/addsong.component.spec.ts
Normal file
25
src/app/components/addsong/addsong.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
27
src/app/components/addsong/addsong.component.ts
Normal file
27
src/app/components/addsong/addsong.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/app/components/downloads/downloads.component.html
Normal file
9
src/app/components/downloads/downloads.component.html
Normal 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>
|
25
src/app/components/downloads/downloads.component.spec.ts
Normal file
25
src/app/components/downloads/downloads.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
38
src/app/components/downloads/downloads.component.ts
Normal file
38
src/app/components/downloads/downloads.component.ts
Normal 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}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
src/app/services/download.service.spec.ts
Normal file
12
src/app/services/download.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
47
src/app/services/download.service.ts
Normal file
47
src/app/services/download.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user