⬇️ 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 { AlbumsComponent } from './components/albums/albums.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 = [
|
||||
{ path: '', component: HomeComponent },
|
||||
@ -11,7 +13,9 @@ const routes: Routes = [
|
||||
{ path: 'artists', component: ArtistsComponent },
|
||||
{ path: 'albums/:id', 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({
|
||||
|
@ -7,8 +7,8 @@
|
||||
Explore
|
||||
<fa-icon [icon]="faGlobeEurope" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
<a routerLink="songs" class="list-group-item list-group-item-action bg-light d-flex justify-content-between align-items-center">
|
||||
Songs
|
||||
<a routerLink="/add/song" class="list-group-item list-group-item-action bg-light d-flex justify-content-between align-items-center">
|
||||
Add Song
|
||||
<fa-icon [icon]="faMusic" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
<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>
|
||||
</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 -->
|
||||
<app-controls class="d-flex"></app-controls>
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Main
|
||||
// Main
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -15,14 +15,14 @@ app-controls {
|
||||
|
||||
// Sidebar
|
||||
#sidebar-wrapper {
|
||||
min-height: inherit;
|
||||
min-height: inherit;
|
||||
margin-left: -15rem;
|
||||
-webkit-transition: margin .25s ease-out;
|
||||
-moz-transition: margin .25s ease-out;
|
||||
-o-transition: margin .25s ease-out;
|
||||
transition: margin .25s ease-out;
|
||||
}
|
||||
|
||||
|
||||
#sidebar-wrapper .sidebar-heading {
|
||||
padding: 0.875rem 1.25rem;
|
||||
font-size: 1.2rem;
|
||||
@ -31,7 +31,7 @@ app-controls {
|
||||
.sidebar-heading > img {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
|
||||
#sidebar-wrapper .list-group {
|
||||
width: 15rem;
|
||||
}
|
||||
@ -48,7 +48,7 @@ app-controls {
|
||||
#navbar-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#sidebar-wrapper {
|
||||
margin-left: 0;
|
||||
@ -62,8 +62,12 @@ app-controls {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
#wrapper.toggled #sidebar-wrapper {
|
||||
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 { Router } from '@angular/router';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { NotificationService } from './services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -26,7 +27,8 @@ export class AppComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public router: Router,
|
||||
private fb: FormBuilder
|
||||
private fb: FormBuilder,
|
||||
public notification: NotificationService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -13,6 +13,8 @@ import { AlbumsComponent } from './components/albums/albums.component';
|
||||
import { ControlsComponent } from './components/controls/controls.component';
|
||||
import { SonglistComponent } from './components/songlist/songlist.component';
|
||||
import { SearchComponent } from './components/search/search.component';
|
||||
import { AddsongComponent } from './components/addsong/addsong.component';
|
||||
import { DownloadsComponent } from './components/downloads/downloads.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -22,7 +24,9 @@ import { SearchComponent } from './components/search/search.component';
|
||||
AlbumsComponent,
|
||||
ControlsComponent,
|
||||
SonglistComponent,
|
||||
SearchComponent
|
||||
SearchComponent,
|
||||
AddsongComponent,
|
||||
DownloadsComponent
|
||||
],
|
||||
imports: [
|
||||
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 {
|
||||
|
||||
songs: Promise<Song[]>;
|
||||
artists: Artist[] = [];
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
|
@ -24,9 +24,7 @@ export class DataService {
|
||||
private axiosInstance: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axiosInstance = axios.create({
|
||||
timeout: 3000
|
||||
});
|
||||
this.axiosInstance = axios.create();
|
||||
}
|
||||
/**
|
||||
* @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>> {
|
||||
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';
|
||||
|
||||
interface Notification {
|
||||
user: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
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