aboutsummaryrefslogtreecommitdiff
path: root/ui/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/app')
-rw-r--r--ui/src/app/app.component.html74
-rw-r--r--ui/src/app/app.component.sass34
-rw-r--r--ui/src/app/app.component.ts72
-rw-r--r--ui/src/app/app.module.ts5
-rw-r--r--ui/src/app/downloads.service.ts62
-rw-r--r--ui/src/app/master-checkbox.component.ts53
6 files changed, 219 insertions, 81 deletions
diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html
index 3796b72..b05aee7 100644
--- a/ui/src/app/app.component.html
+++ b/ui/src/app/app.component.html
@@ -1,10 +1,9 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" href="#">MeTube</a>
+ <!--
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsDefault" aria-controls="navbarsDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
-
- <!--
<div class="collapse navbar-collapse" id="navbarsDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
@@ -29,47 +28,66 @@
</form>
<p *ngIf="downloads.loading">Loading...</p>
- <div *ngIf="!downloads.loading">
- <div *ngIf="downloads.empty()" class="jumbotron jumbotron-fluid px-4">
- <div class="container text-center">
- <h1 class="display-4">Welcome to MeTube!</h1>
- <p class="lead">Please add some downloads via the URL box above.</p>
- </div>
- </div>
- <table *ngIf="!downloads.empty()" class="table">
- <thead>
+ <div class="metube-section-header">Downloading</div>
+ <table class="table">
+ <thead>
<tr>
- <th scope="col" style="width: 1rem; vertical-align: middle;">
- <div class="custom-control custom-checkbox">
- <input type="checkbox" class="custom-control-input" id="select-all" #masterCheckbox [(ngModel)]="masterSelected" (change)="checkUncheckAll()">
- <label class="custom-control-label" for="select-all"></label>
- </div>
+ <th scope="col" style="width: 1rem;">
+ <app-master-checkbox #queueMasterCheckbox [id]="'queue'" [list]="downloads.queue" (changed)="queueSelectionChanged($event)"></app-master-checkbox>
</th>
<th scope="col">
- <button type="button" class="btn btn-link px-0" disabled #delSelected (click)="delSelectedDownloads()"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Clear selected</button>
+ <button type="button" class="btn btn-link px-0 mr-4" disabled #queueDelSelected (click)="delSelectedDownloads('queue')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Cancel selected</button>
</th>
<th scope="col" style="width: 14rem;"></th>
<th scope="col" style="width: 8rem;">Speed</th>
<th scope="col" style="width: 7rem;">ETA</th>
<th scope="col" style="width: 2rem;"></th>
</tr>
- </thead>
- <tbody>
- <tr *ngFor="let download of downloads.downloads | keyvalue: asIsOrder" [class.disabled]='download.value.deleting'>
+ </thead>
+ <tbody>
+ <tr *ngFor="let download of downloads.queue | keyvalue: asIsOrder" [class.disabled]='download.value.deleting'>
<td>
- <div class="custom-control custom-checkbox">
- <input type="checkbox" class="custom-control-input" id="select-{{download.key}}" [(ngModel)]="download.value.checked" (change)="selectionChanged()">
- <label class="custom-control-label" for="select-{{download.key}}"></label>
- </div>
+ <app-slave-checkbox [id]="download.key" [master]="queueMasterCheckbox" [checkable]="download.value"></app-slave-checkbox>
</td>
<td>{{ download.value.title }}</td>
<td><ngb-progressbar height="1.5rem" [showValue]="download.value.status != 'preparing'" [striped]="download.value.status == 'preparing'" [animated]="download.value.status == 'preparing'" type="success" [value]="download.value.status == 'preparing' ? 100 : download.value.percent | number:'1.0-0'"></ngb-progressbar></td>
<td>{{ download.value.speed | speed }}</td>
<td>{{ download.value.eta | eta }}</td>
- <td><button type="button" class="btn btn-link" (click)="delDownload(download.key)"><fa-icon [icon]="faTrashAlt"></fa-icon></button></td>
+ <td><button type="button" class="btn btn-link" (click)="delDownload('queue', download.key)"><fa-icon [icon]="faTrashAlt"></fa-icon></button></td>
</tr>
- </tbody>
- </table>
- </div>
+ </tbody>
+ </table>
+
+ <div class="metube-section-header">Completed</div>
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col" style="width: 1rem;">
+ <app-master-checkbox #doneMasterCheckbox [id]="'done'" [list]="downloads.done" (changed)="doneSelectionChanged($event)"></app-master-checkbox>
+ </th>
+ <th scope="col">
+ <button type="button" class="btn btn-link px-0 mr-4" disabled #doneDelSelected (click)="delSelectedDownloads('done')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Clear selected</button>
+ <button type="button" class="btn btn-link px-0 mr-4" disabled #doneClearCompleted (click)="clearCompletedDownloads()"><fa-icon [icon]="faCheckCircle"></fa-icon>&nbsp; Clear completed</button>
+ <button type="button" class="btn btn-link px-0 mr-4" disabled #doneClearFailed (click)="clearFailedDownloads()"><fa-icon [icon]="faTimesCircle"></fa-icon>&nbsp; Clear failed</button>
+ </th>
+ <th scope="col" style="width: 2rem;"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let download of downloads.done | keyvalue: asIsOrder" [class.disabled]='download.value.deleting'>
+ <td>
+ <app-slave-checkbox [id]="download.key" [master]="doneMasterCheckbox" [checkable]="download.value"></app-slave-checkbox>
+ </td>
+ <td>
+ <div style="display: inline-block; width: 1.3rem;">
+ <fa-icon *ngIf="download.value.status == 'finished'" [icon]="faCheckCircle" style="color: green;"></fa-icon>
+ <fa-icon *ngIf="download.value.status == 'error'" [icon]="faTimesCircle" style="color: red;"></fa-icon>
+ </div>
+ {{ download.value.title }}
+ </td>
+ <td><button type="button" class="btn btn-link" (click)="delDownload('done', download.key)"><fa-icon [icon]="faTrashAlt"></fa-icon></button></td>
+ </tr>
+ </tbody>
+ </table>
</main><!-- /.container -->
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass
index 847fc30..71b07f8 100644
--- a/ui/src/app/app.component.sass
+++ b/ui/src/app/app.component.sass
@@ -1,15 +1,37 @@
.add-url-box
- padding: 5rem 0
max-width: 720px
- margin: auto
+ margin: 4rem auto
-.rounded-box
- border: 1px solid rgba(0,0,0,.125)
- border-radius: .25rem
+$metube-section-color-bg: rgba(0,0,0,.07)
+
+.metube-section-header
+ font-size: 1.8rem
+ font-weight: 300
+ position: relative
+ background: $metube-section-color-bg
+ padding: 0.5rem 0
+ margin-top: 3.5rem
+
+.metube-section-header:before
+ content: ""
+ position: absolute
+ top: 0
+ bottom: 0
+ left: -9999px
+ right: 0
+ border-left: 9999px solid $metube-section-color-bg
+ box-shadow: 9999px 0 0 $metube-section-color-bg
+
+button:hover
+ text-decoration: none
th
border-top: 0
- border-bottom: 3px solid #dee2e6 !important
+ border-bottom-width: 3px !important
+ vertical-align: middle !important
+
+td
+ vertical-align: middle
.disabled
opacity: 0.5
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 53d3a57..e0961ce 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -1,23 +1,48 @@
-import { Component, ViewChild, ElementRef } from '@angular/core';
-import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
+import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
+import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons';
import { DownloadsService, Status } from './downloads.service';
+import { MasterCheckboxComponent } from './master-checkbox.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass']
})
-export class AppComponent {
+export class AppComponent implements AfterViewInit {
addUrl: string;
addInProgress = false;
+
+ @ViewChild('queueMasterCheckbox', {static: false}) queueMasterCheckbox: MasterCheckboxComponent;
+ @ViewChild('queueDelSelected', {static: false}) queueDelSelected: ElementRef;
+ @ViewChild('doneMasterCheckbox', {static: false}) doneMasterCheckbox: MasterCheckboxComponent;
+ @ViewChild('doneDelSelected', {static: false}) doneDelSelected: ElementRef;
+ @ViewChild('doneClearCompleted', {static: false}) doneClearCompleted: ElementRef;
+ @ViewChild('doneClearFailed', {static: false}) doneClearFailed: ElementRef;
+
faTrashAlt = faTrashAlt;
- masterSelected: boolean;
- @ViewChild('masterCheckbox', {static: false}) masterCheckbox: ElementRef;
- @ViewChild('delSelected', {static: false}) delSelected: ElementRef;
+ faCheckCircle = faCheckCircle;
+ faTimesCircle = faTimesCircle;
constructor(public downloads: DownloadsService) {
- this.downloads.dlChanges.subscribe(() => this.selectionChanged());
+ }
+
+ ngAfterViewInit() {
+ this.downloads.queueChanged.subscribe(() => {
+ this.queueMasterCheckbox.selectionChanged();
+ });
+ this.downloads.doneChanged.subscribe(() => {
+ this.doneMasterCheckbox.selectionChanged();
+ let completed: number = 0, failed: number = 0;
+ this.downloads.done.forEach(dl => {
+ if (dl.status === 'finished')
+ completed++;
+ else if (dl.status === 'error')
+ failed++;
+ });
+ this.doneClearCompleted.nativeElement.disabled = completed === 0;
+ this.doneClearFailed.nativeElement.disabled = failed === 0;
+ });
}
// workaround to allow fetching of Map values in the order they were inserted
@@ -26,19 +51,12 @@ export class AppComponent {
return 1;
}
- checkUncheckAll() {
- this.downloads.downloads.forEach(dl => dl.checked = this.masterSelected);
- this.selectionChanged();
+ queueSelectionChanged(checked: number) {
+ this.queueDelSelected.nativeElement.disabled = checked == 0;
}
- selectionChanged() {
- if (!this.masterCheckbox)
- return;
- let checked: number = 0;
- this.downloads.downloads.forEach(dl => { if(dl.checked) checked++ });
- this.masterSelected = checked > 0 && checked == this.downloads.downloads.size;
- this.masterCheckbox.nativeElement.indeterminate = checked > 0 && checked < this.downloads.downloads.size;
- this.delSelected.nativeElement.disabled = checked == 0;
+ doneSelectionChanged(checked: number) {
+ this.doneDelSelected.nativeElement.disabled = checked == 0;
}
addDownload() {
@@ -53,13 +71,19 @@ export class AppComponent {
});
}
- delDownload(id: string) {
- this.downloads.del([id]).subscribe();
+ delDownload(where: string, id: string) {
+ this.downloads.delById(where, [id]).subscribe();
+ }
+
+ delSelectedDownloads(where: string) {
+ this.downloads.delByFilter(where, dl => dl.checked).subscribe();
+ }
+
+ clearCompletedDownloads() {
+ this.downloads.delByFilter('done', dl => dl.status === 'finished').subscribe();
}
- delSelectedDownloads() {
- let ids: string[] = [];
- this.downloads.downloads.forEach(dl => { if(dl.checked) ids.push(dl.id) });
- this.downloads.del(ids).subscribe();
+ clearFailedDownloads() {
+ this.downloads.delByFilter('done', dl => dl.status === 'error').subscribe();
}
}
diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts
index a169f14..126de30 100644
--- a/ui/src/app/app.module.ts
+++ b/ui/src/app/app.module.ts
@@ -8,6 +8,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { AppComponent } from './app.component';
import { EtaPipe, SpeedPipe } from './downloads.pipe';
+import { MasterCheckboxComponent, SlaveCheckboxComponent } from './master-checkbox.component';
const config: SocketIoConfig = { url: '', options: {} };
@@ -15,7 +16,9 @@ const config: SocketIoConfig = { url: '', options: {} };
declarations: [
AppComponent,
EtaPipe,
- SpeedPipe
+ SpeedPipe,
+ MasterCheckboxComponent,
+ SlaveCheckboxComponent
],
imports: [
BrowserModule,
diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts
index cae16f3..6e19a7a 100644
--- a/ui/src/app/downloads.service.ts
+++ b/ui/src/app/downloads.service.ts
@@ -26,39 +26,51 @@ interface Download {
})
export class DownloadsService {
loading = true;
- downloads = new Map<string, Download>();
- dlChanges = new Subject();
+ queue = new Map<string, Download>();
+ done = new Map<string, Download>();
+ queueChanged = new Subject();
+ doneChanged = new Subject();
constructor(private http: HttpClient, private socket: Socket) {
- socket.fromEvent('queue').subscribe((strdata: string) => {
+ socket.fromEvent('all').subscribe((strdata: string) => {
this.loading = false;
- this.downloads.clear();
- let data: [[string, Download]] = JSON.parse(strdata);
- data.forEach(entry => this.downloads.set(...entry));
- this.dlChanges.next();
+ let data: [[[string, Download]], [[string, Download]]] = JSON.parse(strdata);
+ this.queue.clear();
+ data[0].forEach(entry => this.queue.set(...entry));
+ this.done.clear();
+ data[1].forEach(entry => this.done.set(...entry));
+ this.queueChanged.next();
+ this.doneChanged.next();
});
socket.fromEvent('added').subscribe((strdata: string) => {
let data: Download = JSON.parse(strdata);
- this.downloads.set(data.id, data);
- this.dlChanges.next();
+ this.queue.set(data.id, data);
+ this.queueChanged.next();
});
socket.fromEvent('updated').subscribe((strdata: string) => {
let data: Download = JSON.parse(strdata);
- let dl: Download = this.downloads.get(data.id);
+ let dl: Download = this.queue.get(data.id);
data.checked = dl.checked;
data.deleting = dl.deleting;
- this.downloads.set(data.id, data);
- this.dlChanges.next();
+ this.queue.set(data.id, data);
});
- socket.fromEvent('deleted').subscribe((strdata: string) => {
+ socket.fromEvent('completed').subscribe((strdata: string) => {
+ let data: Download = JSON.parse(strdata);
+ this.queue.delete(data.id);
+ this.done.set(data.id, data);
+ this.queueChanged.next();
+ this.doneChanged.next();
+ });
+ socket.fromEvent('canceled').subscribe((strdata: string) => {
let data: string = JSON.parse(strdata);
- this.downloads.delete(data);
- this.dlChanges.next();
+ this.queue.delete(data);
+ this.queueChanged.next();
+ });
+ socket.fromEvent('cleared').subscribe((strdata: string) => {
+ let data: string = JSON.parse(strdata);
+ this.done.delete(data);
+ this.doneChanged.next();
});
- }
-
- empty() {
- return this.downloads.size == 0;
}
handleHTTPError(error: HttpErrorResponse) {
@@ -72,8 +84,14 @@ export class DownloadsService {
);
}
- public del(ids: string[]) {
- ids.forEach(id => this.downloads.get(id).deleting = true);
- return this.http.post('delete', {ids: ids});
+ public delById(where: string, ids: string[]) {
+ ids.forEach(id => this[where].get(id).deleting = true);
+ return this.http.post('delete', {where: where, ids: ids});
+ }
+
+ public delByFilter(where: string, filter: (dl: Download) => boolean) {
+ let ids: string[] = [];
+ this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.id) });
+ return this.delById(where, ids);
}
}
diff --git a/ui/src/app/master-checkbox.component.ts b/ui/src/app/master-checkbox.component.ts
new file mode 100644
index 0000000..683ea71
--- /dev/null
+++ b/ui/src/app/master-checkbox.component.ts
@@ -0,0 +1,53 @@
+import { Component, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core';
+
+interface Checkable {
+ checked: boolean;
+}
+
+@Component({
+ selector: 'app-master-checkbox',
+ template: `
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox" class="custom-control-input" id="{{id}}-select-all" #masterCheckbox [(ngModel)]="selected" (change)="clicked()">
+ <label class="custom-control-label" for="{{id}}-select-all"></label>
+ </div>
+`
+})
+export class MasterCheckboxComponent {
+ @Input() id: string;
+ @Input() list: Map<String, Checkable>;
+ @Output() changed = new EventEmitter<number>();
+
+ @ViewChild('masterCheckbox', {static: false}) masterCheckbox: ElementRef;
+ selected: boolean;
+
+ clicked() {
+ this.list.forEach(item => item.checked = this.selected);
+ this.selectionChanged();
+ }
+
+ selectionChanged() {
+ if (!this.masterCheckbox)
+ return;
+ let checked: number = 0;
+ this.list.forEach(item => { if(item.checked) checked++ });
+ this.selected = checked > 0 && checked == this.list.size;
+ this.masterCheckbox.nativeElement.indeterminate = checked > 0 && checked < this.list.size;
+ this.changed.emit(checked);
+ }
+}
+
+@Component({
+ selector: 'app-slave-checkbox',
+ template: `
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox" class="custom-control-input" id="{{master.id}}-{{id}}-select" [(ngModel)]="checkable.checked" (change)="master.selectionChanged()">
+ <label class="custom-control-label" for="{{master.id}}-{{id}}-select"></label>
+ </div>
+`
+})
+export class SlaveCheckboxComponent {
+ @Input() id: string;
+ @Input() master: MasterCheckboxComponent;
+ @Input() checkable: Checkable;
+}
bgstack15