Merge pull request #836 from Cucumberrbob/chore/angular-v20
update to angular 20 (plus other frontend gardening)
This commit is contained in:
commit
2e8a7ce080
32 changed files with 1517 additions and 1044 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# Stage 1 - Build the frontend
|
||||
FROM node:18-alpine3.18 AS node-build-env
|
||||
FROM node:22.16.0-alpine3.22 AS node-build-env
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
ARG BUILDPLATFORM
|
||||
|
|
|
|||
|
|
@ -116,5 +116,31 @@
|
|||
"cli": {
|
||||
"analytics": false,
|
||||
"schematicCollections": ["@angular-eslint/schematics"]
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1842
client/package-lock.json
generated
1842
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,15 +12,15 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.11",
|
||||
"@angular/animations": "^20.0.2",
|
||||
"@angular/cdk": "^19.2.16",
|
||||
"@angular/common": "^19.2.11",
|
||||
"@angular/compiler": "^19.2.11",
|
||||
"@angular/core": "^19.2.11",
|
||||
"@angular/forms": "^19.2.11",
|
||||
"@angular/platform-browser": "^19.2.11",
|
||||
"@angular/platform-browser-dynamic": "^19.2.11",
|
||||
"@angular/router": "^19.2.11",
|
||||
"@angular/common": "^20.0.2",
|
||||
"@angular/compiler": "^20.0.2",
|
||||
"@angular/core": "^20.0.2",
|
||||
"@angular/forms": "^20.0.2",
|
||||
"@angular/platform-browser": "^20.0.2",
|
||||
"@angular/platform-browser-dynamic": "^20.0.2",
|
||||
"@angular/router": "^20.0.2",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"bulma": "^1.0.4",
|
||||
|
|
@ -37,10 +37,10 @@
|
|||
"@angular-eslint/eslint-plugin-template": "19.4.0",
|
||||
"@angular-eslint/schematics": "19.4.0",
|
||||
"@angular-eslint/template-parser": "19.4.0",
|
||||
"@angular/build": "^19.2.12",
|
||||
"@angular/cli": "^19.2.12",
|
||||
"@angular/compiler-cli": "^19.2.11",
|
||||
"@angular/language-service": "^19.2.11",
|
||||
"@angular/build": "^20.0.1",
|
||||
"@angular/cli": "^20.0.1",
|
||||
"@angular/compiler-cli": "^20.0.2",
|
||||
"@angular/language-service": "^20.0.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/file-saver-es": "^2.0.3",
|
||||
"@types/node": "^22.15.18",
|
||||
|
|
|
|||
|
|
@ -3,21 +3,6 @@
|
|||
width: 24em;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
max-height: 603px;
|
||||
}
|
||||
|
||||
.is-fullwidth-label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -42,7 +27,3 @@
|
|||
.separator:not(:empty)::after {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.strike-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ import { TorrentService } from 'src/app/torrent.service';
|
|||
import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-torrent',
|
||||
templateUrl: './add-new-torrent.component.html',
|
||||
styleUrls: ['./add-new-torrent.component.scss'],
|
||||
standalone: false,
|
||||
imports: [FormsModule, NgClass],
|
||||
standalone: true,
|
||||
})
|
||||
export class AddNewTorrentComponent implements OnInit {
|
||||
public fileName: string;
|
||||
|
|
@ -60,26 +63,25 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
this.settingsService.get().subscribe((settings) => {
|
||||
const providerSetting = settings.first((m) => m.key === 'Provider:Provider');
|
||||
const providerSetting = settings.find((m) => m.key === 'Provider:Provider');
|
||||
this.provider = providerSetting.enumValues[providerSetting.value as number];
|
||||
this.downloadClient = settings.first((m) => m.key === 'DownloadClient:Client')?.value as number;
|
||||
this.downloadClient = settings.find((m) => m.key === 'DownloadClient:Client')?.value as number;
|
||||
|
||||
this.category = settings.first((m) => m.key === 'Gui:Default:Category')?.value as string;
|
||||
this.hostDownloadAction = this.downloadAction = settings.first((m) => m.key === 'Gui:Default:HostDownloadAction')
|
||||
this.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
|
||||
this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
|
||||
?.value as number;
|
||||
this.downloadAction =
|
||||
settings.first((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
||||
this.finishedAction = settings.first((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
||||
this.finishedActionDelay = settings.first((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
||||
this.downloadMinSize = settings.first((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
||||
this.includeRegex = settings.first((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
||||
this.excludeRegex = settings.first((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
||||
this.torrentRetryAttempts = settings.first((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
||||
this.downloadRetryAttempts = settings.first((m) => m.key === 'Gui:Default:DownloadRetryAttempts')
|
||||
?.value as number;
|
||||
this.torrentDeleteOnError = settings.first((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
||||
this.torrentLifetime = settings.first((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
||||
this.priority = settings.first((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
||||
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
||||
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
||||
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
||||
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
||||
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
||||
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
||||
this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
||||
this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')?.value as number;
|
||||
this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
||||
this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
||||
this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
||||
|
||||
this.setFinishAction();
|
||||
});
|
||||
|
|
@ -111,25 +113,6 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
this.checkFiles();
|
||||
}
|
||||
|
||||
public downloadFileChecked(file: string): void {
|
||||
this.downloadFiles[file] = !this.downloadFiles[file];
|
||||
|
||||
this.allSelected = true;
|
||||
this.availableFiles.forEach((file) => {
|
||||
if (!this.downloadFiles[file.filename]) {
|
||||
this.allSelected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public downloadFileCheckedAll(): void {
|
||||
this.allSelected = !this.allSelected;
|
||||
|
||||
this.availableFiles.forEach((file) => {
|
||||
this.downloadFiles[file.filename] = this.allSelected;
|
||||
});
|
||||
}
|
||||
|
||||
public ok(): void {
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
|
|
@ -138,7 +121,7 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
|
||||
if (this.downloadAction === 2) {
|
||||
const selectedFiles = [];
|
||||
for (let filePath in this.downloadFiles) {
|
||||
for (const filePath in this.downloadFiles) {
|
||||
if (this.downloadFiles[filePath] === true) {
|
||||
selectedFiles.push(filePath);
|
||||
}
|
||||
|
|
@ -170,25 +153,21 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
torrent.downloadClient = this.downloadClient;
|
||||
|
||||
if (this.magnetLink) {
|
||||
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe(
|
||||
() => {
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
(err) => {
|
||||
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if (this.selectedFile) {
|
||||
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe(
|
||||
() => {
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
(err) => {
|
||||
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.error = 'No magnet or file uploaded';
|
||||
this.saving = false;
|
||||
|
|
@ -213,8 +192,8 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
this.allSelected = true;
|
||||
|
||||
if (this.magnetLink) {
|
||||
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe(
|
||||
(result) => {
|
||||
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe({
|
||||
next: (result) => {
|
||||
this.saving = false;
|
||||
this.availableFiles = result;
|
||||
this.currentTorrentFile = this.magnetLink;
|
||||
|
|
@ -222,42 +201,30 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
this.downloadFiles[file.filename] = true;
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if (this.selectedFile) {
|
||||
this.torrentService.checkFiles(this.selectedFile).subscribe(
|
||||
(result) => {
|
||||
this.torrentService.checkFiles(this.selectedFile).subscribe({
|
||||
next: (result) => {
|
||||
this.saving = false;
|
||||
this.availableFiles = result;
|
||||
result.forEach((file) => {
|
||||
this.downloadFiles[file.filename] = true;
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isRegexExcluded(file: TorrentFileAvailability): boolean {
|
||||
if (this.regexSelected == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.regexSelected.find((m) => m.filename === file.filename) == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public verifyRegex(): void {
|
||||
this.includeRegexError = null;
|
||||
this.excludeRegexError = null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||
import { AuthResolverService } from './auth-resolver.service';
|
||||
import { authResolver } from './auth-resolver.service';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
||||
import { ProfileComponent } from './profile/profile.component';
|
||||
|
|
@ -23,7 +23,7 @@ const routes: Routes = [
|
|||
path: '',
|
||||
component: MainLayoutComponent,
|
||||
resolve: {
|
||||
isLoggedIn: AuthResolverService,
|
||||
isLoggedIn: authResolver,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<router-outlet></router-outlet>',
|
||||
styles: [],
|
||||
standalone: false,
|
||||
selector: 'app-root',
|
||||
template: '<router-outlet></router-outlet>',
|
||||
styles: [],
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import 'curray';
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AuthInterceptor } from './auth.interceptor';
|
||||
import { DecodeURIPipe } from './decode-uri.pipe';
|
||||
import { DownloadStatusPipe } from './download-status.pipe';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
||||
import { NavbarComponent } from './navbar/navbar.component';
|
||||
import { Nl2BrPipe } from './nl2br.pipe';
|
||||
import { ProfileComponent } from './profile/profile.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { SetupComponent } from './setup/setup.component';
|
||||
import { TorrentStatusPipe } from './torrent-status.pipe';
|
||||
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||
import { TorrentComponent } from './torrent/torrent.component';
|
||||
import { SortPipe } from './sort.pipe';
|
||||
import { FileSizePipe } from './filesize.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
MainLayoutComponent,
|
||||
NavbarComponent,
|
||||
AddNewTorrentComponent,
|
||||
TorrentTableComponent,
|
||||
SettingsComponent,
|
||||
TorrentStatusPipe,
|
||||
DownloadStatusPipe,
|
||||
LoginComponent,
|
||||
SetupComponent,
|
||||
TorrentComponent,
|
||||
DecodeURIPipe,
|
||||
ProfileComponent,
|
||||
Nl2BrPipe,
|
||||
SortPipe,
|
||||
FileSizePipe,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule, AppRoutingModule, FormsModule, ClipboardModule],
|
||||
providers: [
|
||||
FileSizePipe,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { inject } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthResolverService {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
resolve() {
|
||||
return this.authService.isLoggedIn();
|
||||
}
|
||||
}
|
||||
export const authResolver: ResolveFn<boolean> = () => {
|
||||
return inject(AuthService).isLoggedIn();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||
} else if (error && (error.status === 401 || error.status === 403)) {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
return throwError(error);
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'decodeURI',
|
||||
standalone: false,
|
||||
})
|
||||
@Pipe({ name: 'decodeURI', })
|
||||
export class DecodeURIPipe implements PipeTransform {
|
||||
transform(input: any) {
|
||||
return decodeURI(input);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
|||
import { Download } from './models/download.model';
|
||||
import { FileSizePipe } from './filesize.pipe';
|
||||
|
||||
@Pipe({
|
||||
name: 'downloadStatus',
|
||||
standalone: false,
|
||||
})
|
||||
@Pipe({ name: 'downloadStatus' })
|
||||
export class DownloadStatusPipe implements PipeTransform {
|
||||
constructor(private pipe: FileSizePipe) {}
|
||||
|
||||
|
|
@ -27,11 +24,7 @@ export class DownloadStatusPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
if (value.unpackingStarted) {
|
||||
let progress = (value.bytesDone / value.bytesTotal) * 100;
|
||||
|
||||
if (isNaN(progress)) {
|
||||
progress = 0;
|
||||
}
|
||||
const progress = (value.bytesDone / value.bytesTotal || 0) * 100;
|
||||
|
||||
return `Unpacking ${progress.toFixed(2)}%`;
|
||||
}
|
||||
|
|
@ -45,11 +38,7 @@ export class DownloadStatusPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
if (value.downloadStarted) {
|
||||
let progress = (value.bytesDone / value.bytesTotal) * 100;
|
||||
|
||||
if (isNaN(progress)) {
|
||||
progress = 0;
|
||||
}
|
||||
const progress = (value.bytesDone / value.bytesTotal || 0) * 100;
|
||||
|
||||
const speed = this.pipe.transform(value.speed, 'filesize');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
@Pipe({
|
||||
name: 'filesize',
|
||||
standalone: false,
|
||||
})
|
||||
@Pipe({ name: 'filesize', })
|
||||
export class FileSizePipe implements PipeTransform {
|
||||
private static transformOne(value: number, options?: any): string {
|
||||
return filesize(value, options).toString();
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@
|
|||
<div class="box">
|
||||
<form (ngSubmit)="login()">
|
||||
<div class="field">
|
||||
<label for="" class="label">Username</label>
|
||||
<label for="userName" class="label">Username</label>
|
||||
<div class="control has-icons-left">
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
class="input"
|
||||
name="userName"
|
||||
id="userName"
|
||||
(change)="setUserName($event)"
|
||||
required
|
||||
/>
|
||||
|
|
@ -23,9 +24,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="" class="label" name="password">Password</label>
|
||||
<label for="password" class="label">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="password" class="input" required name="password" (change)="setPassword($event)" />
|
||||
<input
|
||||
type="password"
|
||||
class="input"
|
||||
required
|
||||
name="password"
|
||||
id="password"
|
||||
(change)="setPassword($event)"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-lock"></i>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss'],
|
||||
standalone: false,
|
||||
imports: [FormsModule, NgClass],
|
||||
standalone: true,
|
||||
})
|
||||
export class LoginComponent {
|
||||
public userName: string;
|
||||
|
|
@ -30,14 +33,12 @@ export class LoginComponent {
|
|||
public login(): void {
|
||||
this.error = null;
|
||||
this.loggingIn = true;
|
||||
this.authService.login(this.userName, this.password).subscribe(
|
||||
() => {
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
(err) => {
|
||||
this.authService.login(this.userName, this.password).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.loggingIn = false;
|
||||
this.error = err.error;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { NavbarComponent } from '../navbar/navbar.component';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-layout',
|
||||
templateUrl: './main-layout.component.html',
|
||||
styleUrls: ['./main-layout.component.scss'],
|
||||
standalone: false,
|
||||
imports: [NavbarComponent, RouterOutlet],
|
||||
standalone: true,
|
||||
})
|
||||
export class MainLayoutComponent {
|
||||
constructor() {}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { NavigationEnd, Router, RouterLink } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Profile } from '../models/profile.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { NgClass, DatePipe } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrls: ['./navbar.component.scss'],
|
||||
standalone: false,
|
||||
imports: [RouterLink, NgClass, DatePipe],
|
||||
standalone: true,
|
||||
})
|
||||
export class NavbarComponent implements OnInit {
|
||||
public showMobileMenu = false;
|
||||
|
|
@ -58,11 +60,6 @@ export class NavbarComponent implements OnInit {
|
|||
}
|
||||
|
||||
public logout(): void {
|
||||
this.authService.logout().subscribe(
|
||||
() => {
|
||||
this.router.navigate(['/login']);
|
||||
},
|
||||
(err) => {},
|
||||
);
|
||||
this.authService.logout().subscribe({ next: () => this.router.navigate(['/login']), error: console.error });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'nl2br',
|
||||
standalone: false,
|
||||
})
|
||||
@Pipe({ name: 'nl2br', })
|
||||
export class Nl2BrPipe implements PipeTransform {
|
||||
constructor(private sanitizer: DomSanitizer) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
templateUrl: './profile.component.html',
|
||||
styleUrls: ['./profile.component.scss'],
|
||||
standalone: false,
|
||||
imports: [FormsModule, NgClass],
|
||||
standalone: true,
|
||||
})
|
||||
export class ProfileComponent {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
|
@ -22,16 +25,16 @@ export class ProfileComponent {
|
|||
this.error = null;
|
||||
this.saving = true;
|
||||
|
||||
this.authService.update(this.username, this.password).subscribe(
|
||||
() => {
|
||||
this.authService.update(this.username, this.password).subscribe({
|
||||
next: () => {
|
||||
this.success = true;
|
||||
this.saving = false;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.success = false;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { SettingsService } from 'src/app/settings.service';
|
||||
import { Setting } from '../models/setting.model';
|
||||
import { NgClass, KeyValuePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Nl2BrPipe } from '../nl2br.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
standalone: false,
|
||||
imports: [NgClass, FormsModule, KeyValuePipe, Nl2BrPipe, FileSizePipe],
|
||||
standalone: true,
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
public activeTab = 0;
|
||||
|
|
@ -39,10 +44,10 @@ export class SettingsComponent implements OnInit {
|
|||
|
||||
public reset(): void {
|
||||
this.settingsService.get().subscribe((settings) => {
|
||||
this.tabs = settings.where((m) => m.key.indexOf(':') === -1);
|
||||
this.tabs = settings.filter((m) => m.key.indexOf(':') === -1);
|
||||
|
||||
for (let tab of this.tabs) {
|
||||
tab.settings = settings.where((m) => m.key.indexOf(`${tab.key}:`) > -1);
|
||||
tab.settings = settings.filter((m) => m.key.indexOf(`${tab.key}:`) > -1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -50,40 +55,39 @@ export class SettingsComponent implements OnInit {
|
|||
public ok(): void {
|
||||
this.saving = true;
|
||||
|
||||
const settingsToSave = this.tabs.selectMany((m) => m.settings).where((m) => m.type !== 'Object');
|
||||
const settingsToSave = this.tabs.flatMap((m) => m.settings).filter((m) => m.type !== 'Object');
|
||||
|
||||
this.settingsService.update(settingsToSave).subscribe(
|
||||
() => {
|
||||
this.settingsService.update(settingsToSave).subscribe({
|
||||
next: () =>
|
||||
setTimeout(() => {
|
||||
this.saving = false;
|
||||
}, 1000);
|
||||
},
|
||||
(err) => {
|
||||
}, 1000),
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.error = err;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public testDownloadPath(): void {
|
||||
const settingDownloadPath = this.tabs
|
||||
.first((m) => m.key === 'DownloadClient')
|
||||
.settings.first((m) => m.key === 'DownloadClient:DownloadPath').value as string;
|
||||
.find((m) => m.key === 'DownloadClient')
|
||||
.settings.find((m) => m.key === 'DownloadClient:DownloadPath').value as string;
|
||||
|
||||
this.saving = true;
|
||||
this.testPathError = null;
|
||||
this.testPathSuccess = false;
|
||||
|
||||
this.settingsService.testPath(settingDownloadPath).subscribe(
|
||||
() => {
|
||||
this.settingsService.testPath(settingDownloadPath).subscribe({
|
||||
next: () => {
|
||||
this.saving = false;
|
||||
this.testPathSuccess = true;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.testPathError = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public testDownloadSpeed(): void {
|
||||
|
|
@ -91,62 +95,64 @@ export class SettingsComponent implements OnInit {
|
|||
this.testDownloadSpeedError = null;
|
||||
this.testDownloadSpeedSuccess = 0;
|
||||
|
||||
this.settingsService.testDownloadSpeed().subscribe(
|
||||
(result) => {
|
||||
this.settingsService.testDownloadSpeed().subscribe({
|
||||
next: (result) => {
|
||||
this.saving = false;
|
||||
this.testDownloadSpeedSuccess = result;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.testDownloadSpeedError = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
public testWriteSpeed(): void {
|
||||
this.saving = true;
|
||||
this.testWriteSpeedError = null;
|
||||
this.testWriteSpeedSuccess = 0;
|
||||
|
||||
this.settingsService.testWriteSpeed().subscribe(
|
||||
(result) => {
|
||||
this.settingsService.testWriteSpeed().subscribe({
|
||||
next: (result) => {
|
||||
this.saving = false;
|
||||
this.testWriteSpeedSuccess = result;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.testWriteSpeedError = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public testAria2cConnection(): void {
|
||||
const settingAria2cUrl = this.tabs
|
||||
.first((m) => m.key === 'DownloadClient')
|
||||
.settings.first((m) => m.key === 'DownloadClient:Aria2cUrl').value as string;
|
||||
.find((m) => m.key === 'DownloadClient')
|
||||
.settings.find((m) => m.key === 'DownloadClient:Aria2cUrl').value as string;
|
||||
const settingAria2cSecret = this.tabs
|
||||
.first((m) => m.key === 'DownloadClient')
|
||||
.settings.first((m) => m.key === 'DownloadClient:Aria2cSecret').value as string;
|
||||
.find((m) => m.key === 'DownloadClient')
|
||||
.settings.find((m) => m.key === 'DownloadClient:Aria2cSecret').value as string;
|
||||
|
||||
this.saving = true;
|
||||
this.testAria2cConnectionError = null;
|
||||
this.testAria2cConnectionSuccess = null;
|
||||
|
||||
this.settingsService.testAria2cConnection(settingAria2cUrl, settingAria2cSecret).subscribe(
|
||||
(result) => {
|
||||
this.settingsService.testAria2cConnection(settingAria2cUrl, settingAria2cSecret).subscribe({
|
||||
next: (result) => {
|
||||
this.saving = false;
|
||||
this.testAria2cConnectionSuccess = result.version;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.testAria2cConnectionError = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public registerMagnetHandler(): void {
|
||||
try {
|
||||
navigator.registerProtocolHandler('magnet', `${window.location.origin}/add?magnet=%s`);
|
||||
alert('Success! Your browser will now prompt you to confirm and add the client as the default handler for magnet links.');
|
||||
alert(
|
||||
'Success! Your browser will now prompt you to confirm and add the client as the default handler for magnet links.',
|
||||
);
|
||||
} catch (error) {
|
||||
alert('Magnet link registration failed.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,33 @@
|
|||
</div>
|
||||
<form (ngSubmit)="setup()">
|
||||
<div class="field">
|
||||
<label for="" class="label">Username</label>
|
||||
<label for="userName" class="label">Username</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
class="input"
|
||||
name="userName"
|
||||
id="userName"
|
||||
[(ngModel)]="userName"
|
||||
required
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="" class="label" name="password">Password</label>
|
||||
<label for="password" class="label">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||
<input
|
||||
type="password"
|
||||
class="input"
|
||||
required
|
||||
name="password"
|
||||
id="password"
|
||||
[(ngModel)]="password"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-lock"></i>
|
||||
</span>
|
||||
|
|
@ -81,9 +96,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="" class="label">API Private Token</label>
|
||||
<label for="debrid-api-token" class="label">API Private Token</label>
|
||||
<div class="control">
|
||||
<input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required />
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
class="input"
|
||||
name="token"
|
||||
id="debrid-api-token"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@if (provider === 0) {
|
||||
<p class="help">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-setup',
|
||||
templateUrl: './setup.component.html',
|
||||
styleUrls: ['./setup.component.scss'],
|
||||
standalone: false,
|
||||
imports: [FormsModule, NgClass],
|
||||
standalone: true,
|
||||
})
|
||||
export class SetupComponent {
|
||||
public userName: string;
|
||||
|
|
@ -28,29 +31,29 @@ export class SetupComponent {
|
|||
this.error = null;
|
||||
this.working = true;
|
||||
|
||||
this.authService.create(this.userName, this.password).subscribe(
|
||||
() => {
|
||||
this.authService.create(this.userName, this.password).subscribe({
|
||||
next: () => {
|
||||
this.step = 2;
|
||||
this.working = false;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.working = false;
|
||||
this.error = err.error;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public setToken(): void {
|
||||
this.authService.setupProvider(this.provider, this.token).subscribe(
|
||||
() => {
|
||||
this.authService.setupProvider(this.provider, this.token).subscribe({
|
||||
next: () => {
|
||||
this.step = 3;
|
||||
this.working = false;
|
||||
},
|
||||
(err: any) => {
|
||||
error: (err: any) => {
|
||||
this.working = false;
|
||||
this.error = err.error;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'sort',
|
||||
standalone: false,
|
||||
})
|
||||
@Pipe({ name: 'sort', })
|
||||
export class SortPipe implements PipeTransform {
|
||||
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {
|
||||
if (!Array.isArray(array)) {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
|||
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
||||
import { FileSizePipe } from './filesize.pipe';
|
||||
|
||||
@Pipe({
|
||||
name: 'status',
|
||||
standalone: false,
|
||||
})
|
||||
@Pipe({ name: 'status' })
|
||||
export class TorrentStatusPipe implements PipeTransform {
|
||||
constructor(private pipe: FileSizePipe) {}
|
||||
|
||||
|
|
@ -15,59 +12,49 @@ export class TorrentStatusPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
if (torrent.downloads.length > 0) {
|
||||
const allFinished = torrent.downloads.all((m) => m.completed != null);
|
||||
const allFinished = torrent.downloads.every((m) => m.completed != null);
|
||||
|
||||
if (allFinished) {
|
||||
return 'Finished';
|
||||
}
|
||||
|
||||
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
|
||||
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
|
||||
const downloading = torrent.downloads.filter((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
|
||||
const downloaded = torrent.downloads.filter((m) => m.downloadFinished != null);
|
||||
|
||||
if (downloading.length > 0) {
|
||||
const bytesDone = downloading.sum((m) => m.bytesDone);
|
||||
const bytesTotal = downloading.sum((m) => m.bytesTotal);
|
||||
let progress = (bytesDone / bytesTotal) * 100;
|
||||
const bytesDone = downloading.reduce((sum, m) => sum + m.bytesDone, 0);
|
||||
const bytesTotal = downloading.reduce((sum, m) => sum + m.bytesTotal, 0);
|
||||
const progress = (bytesDone / bytesTotal || 0) * 100;
|
||||
|
||||
if (isNaN(progress)) {
|
||||
progress = 0;
|
||||
}
|
||||
const allSpeeds = downloading.reduce((sum, m) => sum + m.speed, 0);
|
||||
|
||||
let allSpeeds = downloading.sum((m) => m.speed);
|
||||
|
||||
let speed: string | string[] = '0';
|
||||
|
||||
speed = this.pipe.transform(allSpeeds, 'filesize');
|
||||
const speed: string | string[] = this.pipe.transform(allSpeeds, 'filesize');
|
||||
|
||||
return `Downloading file ${downloading.length + downloaded.length}/${
|
||||
torrent.downloads.length
|
||||
} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||
}
|
||||
|
||||
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
|
||||
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
|
||||
const unpacking = torrent.downloads.filter((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
|
||||
const unpacked = torrent.downloads.filter((m) => m.unpackingFinished != null);
|
||||
|
||||
if (unpacking.length > 0) {
|
||||
const bytesDone = unpacking.sum((m) => m.bytesDone);
|
||||
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
|
||||
let progress = (bytesDone / bytesTotal) * 100;
|
||||
|
||||
if (isNaN(progress)) {
|
||||
progress = 0;
|
||||
}
|
||||
const bytesDone = unpacking.reduce((sum, m) => sum + m.bytesDone, 0);
|
||||
const bytesTotal = unpacking.reduce((sum, m) => sum + m.bytesTotal, 0);
|
||||
const progress = (bytesDone / bytesTotal || 0) * 100;
|
||||
|
||||
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
|
||||
2,
|
||||
)}%)`;
|
||||
}
|
||||
|
||||
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
|
||||
const queuedForUnpacking = torrent.downloads.filter((m) => m.unpackingQueued && !m.unpackingStarted);
|
||||
|
||||
if (queuedForUnpacking.length > 0) {
|
||||
return `Queued for unpacking`;
|
||||
}
|
||||
|
||||
const queuedForDownload = torrent.downloads.where((m) => !m.downloadStarted && !m.downloadFinished);
|
||||
const queuedForDownload = torrent.downloads.filter((m) => !m.downloadStarted && !m.downloadFinished);
|
||||
|
||||
if (queuedForDownload.length > 0) {
|
||||
return `Queued for downloading`;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelect(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.contains(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.includes(torrent.torrentId)"
|
||||
/>
|
||||
</td>
|
||||
<td (click)="openTorrent(torrent.torrentId)" class="break-all">
|
||||
|
|
|
|||
|
|
@ -3,12 +3,18 @@ import { Router } from '@angular/router';
|
|||
import { Torrent } from '../models/torrent.model';
|
||||
import { TorrentService } from '../torrent.service';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass, DecimalPipe, DatePipe } from '@angular/common';
|
||||
import { TorrentStatusPipe } from '../torrent-status.pipe';
|
||||
import { SortPipe } from '../sort.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent-table',
|
||||
templateUrl: './torrent-table.component.html',
|
||||
styleUrls: ['./torrent-table.component.scss'],
|
||||
standalone: false,
|
||||
imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, SortPipe, FileSizePipe],
|
||||
standalone: true,
|
||||
})
|
||||
export class TorrentTableComponent implements OnInit {
|
||||
public torrents: Torrent[] = [];
|
||||
|
|
@ -48,18 +54,18 @@ export class TorrentTableComponent implements OnInit {
|
|||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.torrentService.getList().subscribe(
|
||||
(result) => {
|
||||
this.torrentService.getList().subscribe({
|
||||
next: (result) => {
|
||||
this.torrents = result;
|
||||
|
||||
this.torrentService.update$.subscribe((result2) => {
|
||||
this.torrents = result2;
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public sort(property: string): void {
|
||||
|
|
@ -107,7 +113,7 @@ export class TorrentTableComponent implements OnInit {
|
|||
public deleteOk(): void {
|
||||
this.deleting = true;
|
||||
|
||||
let calls: Observable<void>[] = [];
|
||||
const calls: Observable<void>[] = [];
|
||||
|
||||
this.selectedTorrents.forEach((torrentId) => {
|
||||
calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
|
||||
|
|
@ -140,7 +146,7 @@ export class TorrentTableComponent implements OnInit {
|
|||
public retryOk(): void {
|
||||
this.retrying = true;
|
||||
|
||||
let calls: Observable<void>[] = [];
|
||||
const calls: Observable<void>[] = [];
|
||||
|
||||
this.selectedTorrents.forEach((torrentId) => {
|
||||
calls.push(this.torrentService.retry(torrentId));
|
||||
|
|
@ -163,30 +169,40 @@ export class TorrentTableComponent implements OnInit {
|
|||
public changeSettingsModal(): void {
|
||||
this.changeSettingsError = null;
|
||||
|
||||
const selectedTorrents = this.torrents.where((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
const selectedTorrents = this.torrents.filter((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
|
||||
this.updateSettingsDownloadClient =
|
||||
selectedTorrents.distinctBy((m) => m.downloadClient).count() == 1 ? selectedTorrents[0].downloadClient : null;
|
||||
this.updateSettingsHostDownloadAction =
|
||||
selectedTorrents.distinctBy((m) => m.hostDownloadAction).count() == 1
|
||||
? selectedTorrents[0].hostDownloadAction
|
||||
: null;
|
||||
this.updateSettingsCategory =
|
||||
selectedTorrents.distinctBy((m) => m.category).count() == 1 ? selectedTorrents[0].category : null;
|
||||
this.updateSettingsPriority =
|
||||
selectedTorrents.distinctBy((m) => m.priority).count() == 1 ? selectedTorrents[0].priority : null;
|
||||
this.updateSettingsDownloadRetryAttempts =
|
||||
selectedTorrents.distinctBy((m) => m.downloadRetryAttempts).count() == 1
|
||||
? selectedTorrents[0].downloadRetryAttempts
|
||||
: null;
|
||||
this.updateSettingsTorrentRetryAttempts =
|
||||
selectedTorrents.distinctBy((m) => m.torrentRetryAttempts).count() == 1
|
||||
? selectedTorrents[0].torrentRetryAttempts
|
||||
: null;
|
||||
this.updateSettingsDeleteOnError =
|
||||
selectedTorrents.distinctBy((m) => m.deleteOnError).count() == 1 ? selectedTorrents[0].deleteOnError : null;
|
||||
this.updateSettingsTorrentLifetime =
|
||||
selectedTorrents.distinctBy((m) => m.lifetime).count() == 1 ? selectedTorrents[0].lifetime : null;
|
||||
this.updateSettingsDownloadClient = selectedTorrents.every(
|
||||
(m, _, arr) => m.downloadClient === arr[0].downloadClient,
|
||||
)
|
||||
? selectedTorrents[0].downloadClient
|
||||
: null;
|
||||
this.updateSettingsHostDownloadAction = selectedTorrents.every(
|
||||
(m, _, arr) => m.hostDownloadAction === arr[0].hostDownloadAction,
|
||||
)
|
||||
? selectedTorrents[0].hostDownloadAction
|
||||
: null;
|
||||
this.updateSettingsCategory = selectedTorrents.every((m, _, arr) => m.category === arr[0].category)
|
||||
? selectedTorrents[0].category
|
||||
: null;
|
||||
this.updateSettingsPriority = selectedTorrents.every((m, _, arr) => m.priority === arr[0].priority)
|
||||
? selectedTorrents[0].priority
|
||||
: null;
|
||||
this.updateSettingsDownloadRetryAttempts = selectedTorrents.every(
|
||||
(m, _, arr) => m.downloadRetryAttempts === arr[0].downloadRetryAttempts,
|
||||
)
|
||||
? selectedTorrents[0].downloadRetryAttempts
|
||||
: null;
|
||||
this.updateSettingsTorrentRetryAttempts = selectedTorrents.every(
|
||||
(m, _, arr) => m.torrentRetryAttempts === arr[0].torrentRetryAttempts,
|
||||
)
|
||||
? selectedTorrents[0].torrentRetryAttempts
|
||||
: null;
|
||||
this.updateSettingsDeleteOnError = selectedTorrents.every((m, _, arr) => m.deleteOnError === arr[0].deleteOnError)
|
||||
? selectedTorrents[0].deleteOnError
|
||||
: null;
|
||||
this.updateSettingsTorrentLifetime = selectedTorrents.every((m, _, arr) => m.lifetime === arr[0].lifetime)
|
||||
? selectedTorrents[0].lifetime
|
||||
: null;
|
||||
|
||||
this.isChangeSettingsModalActive = true;
|
||||
}
|
||||
|
|
@ -198,9 +214,9 @@ export class TorrentTableComponent implements OnInit {
|
|||
public changeSettingsOk(): void {
|
||||
this.changingSettings = true;
|
||||
|
||||
let calls: Observable<void>[] = [];
|
||||
const calls: Observable<void>[] = [];
|
||||
|
||||
const selectedTorrents = this.torrents.where((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
const selectedTorrents = this.torrents.filter((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
|
||||
selectedTorrents.forEach((torrent) => {
|
||||
if (this.updateSettingsDownloadClient != null) {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
.fa-download {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,29 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||
import { saveAs } from 'file-saver-es';
|
||||
import { Torrent } from '../models/torrent.model';
|
||||
import { TorrentService } from '../torrent.service';
|
||||
import { NgClass, DatePipe } from '@angular/common';
|
||||
import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TorrentStatusPipe } from '../torrent-status.pipe';
|
||||
import { DownloadStatusPipe } from '../download-status.pipe';
|
||||
import { DecodeURIPipe } from '../decode-uri.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent',
|
||||
templateUrl: './torrent.component.html',
|
||||
styleUrls: ['./torrent.component.scss'],
|
||||
standalone: false,
|
||||
imports: [
|
||||
NgClass,
|
||||
CdkCopyToClipboard,
|
||||
FormsModule,
|
||||
DatePipe,
|
||||
TorrentStatusPipe,
|
||||
DownloadStatusPipe,
|
||||
DecodeURIPipe,
|
||||
FileSizePipe,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class TorrentComponent implements OnInit {
|
||||
public torrent: Torrent;
|
||||
|
|
@ -59,23 +76,21 @@ export class TorrentComponent implements OnInit {
|
|||
this.activatedRoute.params.subscribe((params) => {
|
||||
const torrentId = params['id'];
|
||||
|
||||
this.torrentService.get(torrentId).subscribe(
|
||||
(torrent) => {
|
||||
this.torrentService.get(torrentId).subscribe({
|
||||
next: (torrent) => {
|
||||
this.torrent = torrent;
|
||||
|
||||
this.torrentService.update$.subscribe((result) => {
|
||||
this.update(result);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
);
|
||||
error: () => this.router.navigate(['/']),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public update(torrents: Torrent[]): void {
|
||||
const updatedTorrent = torrents.firstOrDefault((m) => m.torrentId === this.torrent.torrentId);
|
||||
const updatedTorrent = torrents.find((m) => m.torrentId === this.torrent.torrentId);
|
||||
|
||||
if (updatedTorrent == null) {
|
||||
return;
|
||||
|
|
@ -94,7 +109,7 @@ export class TorrentComponent implements OnInit {
|
|||
}),
|
||||
);
|
||||
|
||||
var blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
|
||||
const blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
|
||||
saveAs(blob, `${this.torrent.rdName}.torrent`);
|
||||
}
|
||||
|
||||
|
|
@ -116,18 +131,18 @@ export class TorrentComponent implements OnInit {
|
|||
|
||||
this.torrentService
|
||||
.delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
|
||||
.subscribe(
|
||||
() => {
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.isDeleteModalActive = false;
|
||||
this.deleting = false;
|
||||
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.deleteError = err.error;
|
||||
this.deleting = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public showRetryModal(): void {
|
||||
|
|
@ -143,18 +158,18 @@ export class TorrentComponent implements OnInit {
|
|||
public retryOk(): void {
|
||||
this.retrying = true;
|
||||
|
||||
this.torrentService.retry(this.torrent.torrentId).subscribe(
|
||||
() => {
|
||||
this.torrentService.retry(this.torrent.torrentId).subscribe({
|
||||
next: () => {
|
||||
this.isRetryModalActive = false;
|
||||
this.retrying = false;
|
||||
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.retryError = err.error;
|
||||
this.retrying = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public showDownloadRetryModal(downloadId: string): void {
|
||||
|
|
@ -171,16 +186,16 @@ export class TorrentComponent implements OnInit {
|
|||
public downloadRetryOk(): void {
|
||||
this.downloadRetrying = true;
|
||||
|
||||
this.torrentService.retryDownload(this.downloadRetryId).subscribe(
|
||||
() => {
|
||||
this.torrentService.retryDownload(this.downloadRetryId).subscribe({
|
||||
next: () => {
|
||||
this.isDownloadRetryModalActive = false;
|
||||
this.downloadRetrying = false;
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.downloadRetryError = err.error;
|
||||
this.downloadRetrying = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public showUpdateSettingsModal(): void {
|
||||
|
|
@ -212,16 +227,16 @@ export class TorrentComponent implements OnInit {
|
|||
this.torrent.deleteOnError = this.updateSettingsDeleteOnError;
|
||||
this.torrent.lifetime = this.updateSettingsTorrentLifetime;
|
||||
|
||||
this.torrentService.update(this.torrent).subscribe(
|
||||
() => {
|
||||
this.torrentService.update(this.torrent).subscribe({
|
||||
next: () => {
|
||||
this.isUpdateSettingsModalActive = false;
|
||||
this.updating = false;
|
||||
},
|
||||
() => {
|
||||
error: () => {
|
||||
this.isUpdateSettingsModalActive = false;
|
||||
this.updating = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
toggleDeleteSelectAllOptions() {
|
||||
this.deleteData = this.deleteSelectAll;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@
|
|||
<base href="/" />
|
||||
<script>
|
||||
(function () {
|
||||
var baseElement = document.querySelector("base");
|
||||
const baseElement = document.querySelector("base");
|
||||
|
||||
if (baseElement) {
|
||||
var hrefValue = baseElement.getAttribute("href");
|
||||
window["_app_base"] = hrefValue;
|
||||
window["_app_base"] = baseElement.getAttribute("href");
|
||||
console.log("setting base href to " + window["_app_base"]);
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,29 @@
|
|||
import { enableProdMode } from '@angular/core';
|
||||
import { enableProdMode, importProvidersFrom } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
import { environment } from './environments/environment';
|
||||
import { FileSizePipe } from './app/filesize.pipe';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { AuthInterceptor } from './app/auth.interceptor';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppRoutingModule } from './app/app-routing.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
importProvidersFrom(BrowserModule, AppRoutingModule, FormsModule, ClipboardModule),
|
||||
FileSizePipe,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
]
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@
|
|||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"strictNullChecks": false,
|
||||
"target": "ES2022",
|
||||
"module": "es2020",
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue