Merge pull request #836 from Cucumberrbob/chore/angular-v20

update to angular 20 (plus other frontend gardening)
This commit is contained in:
Roger Far 2025-07-29 13:05:59 -06:00 committed by GitHub
commit 2e8a7ce080
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1517 additions and 1044 deletions

View file

@ -1,5 +1,5 @@
# Stage 1 - Build the frontend # 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 ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG BUILDPLATFORM ARG BUILDPLATFORM

View file

@ -116,5 +116,31 @@
"cli": { "cli": {
"analytics": false, "analytics": false,
"schematicCollections": ["@angular-eslint/schematics"] "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

File diff suppressed because it is too large Load diff

View file

@ -12,15 +12,15 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^19.2.11", "@angular/animations": "^20.0.2",
"@angular/cdk": "^19.2.16", "@angular/cdk": "^19.2.16",
"@angular/common": "^19.2.11", "@angular/common": "^20.0.2",
"@angular/compiler": "^19.2.11", "@angular/compiler": "^20.0.2",
"@angular/core": "^19.2.11", "@angular/core": "^20.0.2",
"@angular/forms": "^19.2.11", "@angular/forms": "^20.0.2",
"@angular/platform-browser": "^19.2.11", "@angular/platform-browser": "^20.0.2",
"@angular/platform-browser-dynamic": "^19.2.11", "@angular/platform-browser-dynamic": "^20.0.2",
"@angular/router": "^19.2.11", "@angular/router": "^20.0.2",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@microsoft/signalr": "^8.0.7", "@microsoft/signalr": "^8.0.7",
"bulma": "^1.0.4", "bulma": "^1.0.4",
@ -37,10 +37,10 @@
"@angular-eslint/eslint-plugin-template": "19.4.0", "@angular-eslint/eslint-plugin-template": "19.4.0",
"@angular-eslint/schematics": "19.4.0", "@angular-eslint/schematics": "19.4.0",
"@angular-eslint/template-parser": "19.4.0", "@angular-eslint/template-parser": "19.4.0",
"@angular/build": "^19.2.12", "@angular/build": "^20.0.1",
"@angular/cli": "^19.2.12", "@angular/cli": "^20.0.1",
"@angular/compiler-cli": "^19.2.11", "@angular/compiler-cli": "^20.0.2",
"@angular/language-service": "^19.2.11", "@angular/language-service": "^20.0.2",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/file-saver-es": "^2.0.3", "@types/file-saver-es": "^2.0.3",
"@types/node": "^22.15.18", "@types/node": "^22.15.18",

View file

@ -3,21 +3,6 @@
width: 24em; 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 { .separator {
display: flex; display: flex;
align-items: center; align-items: center;
@ -42,7 +27,3 @@
.separator:not(:empty)::after { .separator:not(:empty)::after {
margin-left: 0.25em; margin-left: 0.25em;
} }
.strike-through {
text-decoration: line-through;
}

View file

@ -4,12 +4,15 @@ import { TorrentService } from 'src/app/torrent.service';
import { Torrent, TorrentFileAvailability } from '../models/torrent.model'; import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { NgClass } from '@angular/common';
@Component({ @Component({
selector: 'app-add-new-torrent', selector: 'app-add-new-torrent',
templateUrl: './add-new-torrent.component.html', templateUrl: './add-new-torrent.component.html',
styleUrls: ['./add-new-torrent.component.scss'], styleUrls: ['./add-new-torrent.component.scss'],
standalone: false, imports: [FormsModule, NgClass],
standalone: true,
}) })
export class AddNewTorrentComponent implements OnInit { export class AddNewTorrentComponent implements OnInit {
public fileName: string; public fileName: string;
@ -60,26 +63,25 @@ export class AddNewTorrentComponent implements OnInit {
} }
}); });
this.settingsService.get().subscribe((settings) => { 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.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.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
this.hostDownloadAction = this.downloadAction = settings.first((m) => m.key === 'Gui:Default:HostDownloadAction') this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
?.value as number; ?.value as number;
this.downloadAction = this.downloadAction =
settings.first((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0; settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
this.finishedAction = settings.first((m) => m.key === 'Gui:Default:FinishedAction')?.value as number; this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
this.finishedActionDelay = settings.first((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number; this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
this.downloadMinSize = settings.first((m) => m.key === 'Gui:Default:MinFileSize')?.value as number; this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
this.includeRegex = settings.first((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string; this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
this.excludeRegex = settings.first((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string; this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
this.torrentRetryAttempts = settings.first((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number; this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
this.downloadRetryAttempts = settings.first((m) => m.key === 'Gui:Default:DownloadRetryAttempts') this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')?.value as number;
?.value as number; this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
this.torrentDeleteOnError = settings.first((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number; this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
this.torrentLifetime = settings.first((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number; this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
this.priority = settings.first((m) => m.key === 'Gui:Default:Priority')?.value as number;
this.setFinishAction(); this.setFinishAction();
}); });
@ -111,25 +113,6 @@ export class AddNewTorrentComponent implements OnInit {
this.checkFiles(); 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 { public ok(): void {
this.saving = true; this.saving = true;
this.error = null; this.error = null;
@ -138,7 +121,7 @@ export class AddNewTorrentComponent implements OnInit {
if (this.downloadAction === 2) { if (this.downloadAction === 2) {
const selectedFiles = []; const selectedFiles = [];
for (let filePath in this.downloadFiles) { for (const filePath in this.downloadFiles) {
if (this.downloadFiles[filePath] === true) { if (this.downloadFiles[filePath] === true) {
selectedFiles.push(filePath); selectedFiles.push(filePath);
} }
@ -170,25 +153,21 @@ export class AddNewTorrentComponent implements OnInit {
torrent.downloadClient = this.downloadClient; torrent.downloadClient = this.downloadClient;
if (this.magnetLink) { if (this.magnetLink) {
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe( this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({
() => { next: () => this.router.navigate(['/']),
this.router.navigate(['/']); error: (err) => {
},
(err) => {
this.error = err.error; this.error = err.error;
this.saving = false; this.saving = false;
}, },
); });
} else if (this.selectedFile) { } else if (this.selectedFile) {
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe( this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({
() => { next: () => this.router.navigate(['/']),
this.router.navigate(['/']); error: (err) => {
},
(err) => {
this.error = err.error; this.error = err.error;
this.saving = false; this.saving = false;
}, },
); });
} else { } else {
this.error = 'No magnet or file uploaded'; this.error = 'No magnet or file uploaded';
this.saving = false; this.saving = false;
@ -213,8 +192,8 @@ export class AddNewTorrentComponent implements OnInit {
this.allSelected = true; this.allSelected = true;
if (this.magnetLink) { if (this.magnetLink) {
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe( this.torrentService.checkFilesMagnet(this.magnetLink).subscribe({
(result) => { next: (result) => {
this.saving = false; this.saving = false;
this.availableFiles = result; this.availableFiles = result;
this.currentTorrentFile = this.magnetLink; this.currentTorrentFile = this.magnetLink;
@ -222,42 +201,30 @@ export class AddNewTorrentComponent implements OnInit {
this.downloadFiles[file.filename] = true; this.downloadFiles[file.filename] = true;
}); });
}, },
(err) => { error: (err) => {
this.error = err.error; this.error = err.error;
this.saving = false; this.saving = false;
}, },
); });
} else if (this.selectedFile) { } else if (this.selectedFile) {
this.torrentService.checkFiles(this.selectedFile).subscribe( this.torrentService.checkFiles(this.selectedFile).subscribe({
(result) => { next: (result) => {
this.saving = false; this.saving = false;
this.availableFiles = result; this.availableFiles = result;
result.forEach((file) => { result.forEach((file) => {
this.downloadFiles[file.filename] = true; this.downloadFiles[file.filename] = true;
}); });
}, },
(err) => { error: (err) => {
this.error = err.error; this.error = err.error;
this.saving = false; this.saving = false;
}, },
); });
} else { } else {
this.saving = false; 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 { public verifyRegex(): void {
this.includeRegexError = null; this.includeRegexError = null;
this.excludeRegexError = null; this.excludeRegexError = null;

View file

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component'; 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 { LoginComponent } from './login/login.component';
import { MainLayoutComponent } from './main-layout/main-layout.component'; import { MainLayoutComponent } from './main-layout/main-layout.component';
import { ProfileComponent } from './profile/profile.component'; import { ProfileComponent } from './profile/profile.component';
@ -23,7 +23,7 @@ const routes: Routes = [
path: '', path: '',
component: MainLayoutComponent, component: MainLayoutComponent,
resolve: { resolve: {
isLoggedIn: AuthResolverService, isLoggedIn: authResolver,
}, },
children: [ children: [
{ {

View file

@ -1,9 +1,10 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: '<router-outlet></router-outlet>', template: '<router-outlet></router-outlet>',
styles: [], styles: [],
standalone: false, imports: [RouterOutlet],
}) })
export class AppComponent {} export class AppComponent {}

View file

@ -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 {}

View file

@ -1,13 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { ResolveFn } from '@angular/router';
@Injectable({ export const authResolver: ResolveFn<boolean> = () => {
providedIn: 'root', return inject(AuthService).isLoggedIn();
}) };
export class AuthResolverService {
constructor(private authService: AuthService) {}
resolve() {
return this.authService.isLoggedIn();
}
}

View file

@ -16,7 +16,7 @@ export class AuthInterceptor implements HttpInterceptor {
} else if (error && (error.status === 401 || error.status === 403)) { } else if (error && (error.status === 401 || error.status === 403)) {
this.router.navigate(['/login']); this.router.navigate(['/login']);
} }
return throwError(error); return throwError(() => error);
}), }),
); );
} }

View file

@ -1,9 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({ name: 'decodeURI', })
name: 'decodeURI',
standalone: false,
})
export class DecodeURIPipe implements PipeTransform { export class DecodeURIPipe implements PipeTransform {
transform(input: any) { transform(input: any) {
return decodeURI(input); return decodeURI(input);

View file

@ -2,10 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
import { Download } from './models/download.model'; import { Download } from './models/download.model';
import { FileSizePipe } from './filesize.pipe'; import { FileSizePipe } from './filesize.pipe';
@Pipe({ @Pipe({ name: 'downloadStatus' })
name: 'downloadStatus',
standalone: false,
})
export class DownloadStatusPipe implements PipeTransform { export class DownloadStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} constructor(private pipe: FileSizePipe) {}
@ -27,11 +24,7 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.unpackingStarted) { if (value.unpackingStarted) {
let progress = (value.bytesDone / value.bytesTotal) * 100; const progress = (value.bytesDone / value.bytesTotal || 0) * 100;
if (isNaN(progress)) {
progress = 0;
}
return `Unpacking ${progress.toFixed(2)}%`; return `Unpacking ${progress.toFixed(2)}%`;
} }
@ -45,11 +38,7 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.downloadStarted) { if (value.downloadStarted) {
let progress = (value.bytesDone / value.bytesTotal) * 100; const progress = (value.bytesDone / value.bytesTotal || 0) * 100;
if (isNaN(progress)) {
progress = 0;
}
const speed = this.pipe.transform(value.speed, 'filesize'); const speed = this.pipe.transform(value.speed, 'filesize');

View file

@ -1,10 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { filesize } from 'filesize'; import { filesize } from 'filesize';
@Pipe({ @Pipe({ name: 'filesize', })
name: 'filesize',
standalone: false,
})
export class FileSizePipe implements PipeTransform { export class FileSizePipe implements PipeTransform {
private static transformOne(value: number, options?: any): string { private static transformOne(value: number, options?: any): string {
return filesize(value, options).toString(); return filesize(value, options).toString();

View file

@ -7,13 +7,14 @@
<div class="box"> <div class="box">
<form (ngSubmit)="login()"> <form (ngSubmit)="login()">
<div class="field"> <div class="field">
<label for="" class="label">Username</label> <label for="userName" class="label">Username</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input <input
type="text" type="text"
placeholder="" placeholder=""
class="input" class="input"
name="userName" name="userName"
id="userName"
(change)="setUserName($event)" (change)="setUserName($event)"
required required
/> />
@ -23,9 +24,16 @@
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="" class="label" name="password">Password</label> <label for="password" class="label">Password</label>
<div class="control has-icons-left"> <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"> <span class="icon is-small is-left">
<i class="fa fa-lock"></i> <i class="fa fa-lock"></i>
</span> </span>

View file

@ -1,12 +1,15 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { FormsModule } from '@angular/forms';
import { NgClass } from '@angular/common';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
standalone: false, imports: [FormsModule, NgClass],
standalone: true,
}) })
export class LoginComponent { export class LoginComponent {
public userName: string; public userName: string;
@ -30,14 +33,12 @@ export class LoginComponent {
public login(): void { public login(): void {
this.error = null; this.error = null;
this.loggingIn = true; this.loggingIn = true;
this.authService.login(this.userName, this.password).subscribe( this.authService.login(this.userName, this.password).subscribe({
() => { next: () => this.router.navigate(['/']),
this.router.navigate(['/']); error: (err) => {
},
(err) => {
this.loggingIn = false; this.loggingIn = false;
this.error = err.error; this.error = err.error;
}, },
); });
} }
} }

View file

@ -1,10 +1,13 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NavbarComponent } from '../navbar/navbar.component';
import { RouterOutlet } from '@angular/router';
@Component({ @Component({
selector: 'app-main-layout', selector: 'app-main-layout',
templateUrl: './main-layout.component.html', templateUrl: './main-layout.component.html',
styleUrls: ['./main-layout.component.scss'], styleUrls: ['./main-layout.component.scss'],
standalone: false, imports: [NavbarComponent, RouterOutlet],
standalone: true,
}) })
export class MainLayoutComponent { export class MainLayoutComponent {
constructor() {} constructor() {}

View file

@ -1,14 +1,16 @@
import { Component, OnInit } from '@angular/core'; 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 { AuthService } from '../auth.service';
import { Profile } from '../models/profile.model'; import { Profile } from '../models/profile.model';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
import { NgClass, DatePipe } from '@angular/common';
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.scss'], styleUrls: ['./navbar.component.scss'],
standalone: false, imports: [RouterLink, NgClass, DatePipe],
standalone: true,
}) })
export class NavbarComponent implements OnInit { export class NavbarComponent implements OnInit {
public showMobileMenu = false; public showMobileMenu = false;
@ -58,11 +60,6 @@ export class NavbarComponent implements OnInit {
} }
public logout(): void { public logout(): void {
this.authService.logout().subscribe( this.authService.logout().subscribe({ next: () => this.router.navigate(['/login']), error: console.error });
() => {
this.router.navigate(['/login']);
},
(err) => {},
);
} }
} }

View file

@ -1,10 +1,7 @@
import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core'; import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
@Pipe({ @Pipe({ name: 'nl2br', })
name: 'nl2br',
standalone: false,
})
export class Nl2BrPipe implements PipeTransform { export class Nl2BrPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {} constructor(private sanitizer: DomSanitizer) {}

View file

@ -1,11 +1,14 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { FormsModule } from '@angular/forms';
import { NgClass } from '@angular/common';
@Component({ @Component({
selector: 'app-profile', selector: 'app-profile',
templateUrl: './profile.component.html', templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss'], styleUrls: ['./profile.component.scss'],
standalone: false, imports: [FormsModule, NgClass],
standalone: true,
}) })
export class ProfileComponent { export class ProfileComponent {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
@ -22,16 +25,16 @@ export class ProfileComponent {
this.error = null; this.error = null;
this.saving = true; this.saving = true;
this.authService.update(this.username, this.password).subscribe( this.authService.update(this.username, this.password).subscribe({
() => { next: () => {
this.success = true; this.success = true;
this.saving = false; this.saving = false;
}, },
(err) => { error: (err) => {
this.error = err.error; this.error = err.error;
this.success = false; this.success = false;
this.saving = false; this.saving = false;
}, },
); });
} }
} }

View file

@ -1,12 +1,17 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { SettingsService } from 'src/app/settings.service'; import { SettingsService } from 'src/app/settings.service';
import { Setting } from '../models/setting.model'; 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({ @Component({
selector: 'app-settings', selector: 'app-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'], styleUrls: ['./settings.component.scss'],
standalone: false, imports: [NgClass, FormsModule, KeyValuePipe, Nl2BrPipe, FileSizePipe],
standalone: true,
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
public activeTab = 0; public activeTab = 0;
@ -39,10 +44,10 @@ export class SettingsComponent implements OnInit {
public reset(): void { public reset(): void {
this.settingsService.get().subscribe((settings) => { 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) { 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 { public ok(): void {
this.saving = true; 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(() => { setTimeout(() => {
this.saving = false; this.saving = false;
}, 1000); }, 1000),
}, error: (err) => {
(err) => {
this.saving = false; this.saving = false;
this.error = err; this.error = err;
}, },
); });
} }
public testDownloadPath(): void { public testDownloadPath(): void {
const settingDownloadPath = this.tabs const settingDownloadPath = this.tabs
.first((m) => m.key === 'DownloadClient') .find((m) => m.key === 'DownloadClient')
.settings.first((m) => m.key === 'DownloadClient:DownloadPath').value as string; .settings.find((m) => m.key === 'DownloadClient:DownloadPath').value as string;
this.saving = true; this.saving = true;
this.testPathError = null; this.testPathError = null;
this.testPathSuccess = false; this.testPathSuccess = false;
this.settingsService.testPath(settingDownloadPath).subscribe( this.settingsService.testPath(settingDownloadPath).subscribe({
() => { next: () => {
this.saving = false; this.saving = false;
this.testPathSuccess = true; this.testPathSuccess = true;
}, },
(err) => { error: (err) => {
this.testPathError = err.error; this.testPathError = err.error;
this.saving = false; this.saving = false;
}, },
); });
} }
public testDownloadSpeed(): void { public testDownloadSpeed(): void {
@ -91,62 +95,64 @@ export class SettingsComponent implements OnInit {
this.testDownloadSpeedError = null; this.testDownloadSpeedError = null;
this.testDownloadSpeedSuccess = 0; this.testDownloadSpeedSuccess = 0;
this.settingsService.testDownloadSpeed().subscribe( this.settingsService.testDownloadSpeed().subscribe({
(result) => { next: (result) => {
this.saving = false; this.saving = false;
this.testDownloadSpeedSuccess = result; this.testDownloadSpeedSuccess = result;
}, },
(err) => { error: (err) => {
this.testDownloadSpeedError = err.error; this.testDownloadSpeedError = err.error;
this.saving = false; this.saving = false;
}, },
); });
} }
public testWriteSpeed(): void { public testWriteSpeed(): void {
this.saving = true; this.saving = true;
this.testWriteSpeedError = null; this.testWriteSpeedError = null;
this.testWriteSpeedSuccess = 0; this.testWriteSpeedSuccess = 0;
this.settingsService.testWriteSpeed().subscribe( this.settingsService.testWriteSpeed().subscribe({
(result) => { next: (result) => {
this.saving = false; this.saving = false;
this.testWriteSpeedSuccess = result; this.testWriteSpeedSuccess = result;
}, },
(err) => { error: (err) => {
this.testWriteSpeedError = err.error; this.testWriteSpeedError = err.error;
this.saving = false; this.saving = false;
}, },
); });
} }
public testAria2cConnection(): void { public testAria2cConnection(): void {
const settingAria2cUrl = this.tabs const settingAria2cUrl = this.tabs
.first((m) => m.key === 'DownloadClient') .find((m) => m.key === 'DownloadClient')
.settings.first((m) => m.key === 'DownloadClient:Aria2cUrl').value as string; .settings.find((m) => m.key === 'DownloadClient:Aria2cUrl').value as string;
const settingAria2cSecret = this.tabs const settingAria2cSecret = this.tabs
.first((m) => m.key === 'DownloadClient') .find((m) => m.key === 'DownloadClient')
.settings.first((m) => m.key === 'DownloadClient:Aria2cSecret').value as string; .settings.find((m) => m.key === 'DownloadClient:Aria2cSecret').value as string;
this.saving = true; this.saving = true;
this.testAria2cConnectionError = null; this.testAria2cConnectionError = null;
this.testAria2cConnectionSuccess = null; this.testAria2cConnectionSuccess = null;
this.settingsService.testAria2cConnection(settingAria2cUrl, settingAria2cSecret).subscribe( this.settingsService.testAria2cConnection(settingAria2cUrl, settingAria2cSecret).subscribe({
(result) => { next: (result) => {
this.saving = false; this.saving = false;
this.testAria2cConnectionSuccess = result.version; this.testAria2cConnectionSuccess = result.version;
}, },
(err) => { error: (err) => {
this.testAria2cConnectionError = err.error; this.testAria2cConnectionError = err.error;
this.saving = false; this.saving = false;
}, },
); });
} }
public registerMagnetHandler(): void { public registerMagnetHandler(): void {
try { try {
navigator.registerProtocolHandler('magnet', `${window.location.origin}/add?magnet=%s`); 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) { } catch (error) {
alert('Magnet link registration failed.'); alert('Magnet link registration failed.');
} }

View file

@ -11,18 +11,33 @@
</div> </div>
<form (ngSubmit)="setup()"> <form (ngSubmit)="setup()">
<div class="field"> <div class="field">
<label for="" class="label">Username</label> <label for="userName" class="label">Username</label>
<div class="control has-icons-left"> <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"> <span class="icon is-small is-left">
<i class="fa fa-envelope"></i> <i class="fa fa-envelope"></i>
</span> </span>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="" class="label" name="password">Password</label> <label for="password" class="label">Password</label>
<div class="control has-icons-left"> <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"> <span class="icon is-small is-left">
<i class="fa fa-lock"></i> <i class="fa fa-lock"></i>
</span> </span>
@ -81,9 +96,17 @@
</div> </div>
</div> </div>
<div class="field"> <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"> <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> </div>
@if (provider === 0) { @if (provider === 0) {
<p class="help"> <p class="help">

View file

@ -1,12 +1,15 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { NgClass } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({ @Component({
selector: 'app-setup', selector: 'app-setup',
templateUrl: './setup.component.html', templateUrl: './setup.component.html',
styleUrls: ['./setup.component.scss'], styleUrls: ['./setup.component.scss'],
standalone: false, imports: [FormsModule, NgClass],
standalone: true,
}) })
export class SetupComponent { export class SetupComponent {
public userName: string; public userName: string;
@ -28,29 +31,29 @@ export class SetupComponent {
this.error = null; this.error = null;
this.working = true; this.working = true;
this.authService.create(this.userName, this.password).subscribe( this.authService.create(this.userName, this.password).subscribe({
() => { next: () => {
this.step = 2; this.step = 2;
this.working = false; this.working = false;
}, },
(err) => { error: (err) => {
this.working = false; this.working = false;
this.error = err.error; this.error = err.error;
}, },
); });
} }
public setToken(): void { public setToken(): void {
this.authService.setupProvider(this.provider, this.token).subscribe( this.authService.setupProvider(this.provider, this.token).subscribe({
() => { next: () => {
this.step = 3; this.step = 3;
this.working = false; this.working = false;
}, },
(err: any) => { error: (err: any) => {
this.working = false; this.working = false;
this.error = err.error; this.error = err.error;
}, },
); });
} }
public close(): void { public close(): void {

View file

@ -1,9 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({ name: 'sort', })
name: 'sort',
standalone: false,
})
export class SortPipe implements PipeTransform { export class SortPipe implements PipeTransform {
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] { transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {
if (!Array.isArray(array)) { if (!Array.isArray(array)) {

View file

@ -2,10 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
import { RealDebridStatus, Torrent } from './models/torrent.model'; import { RealDebridStatus, Torrent } from './models/torrent.model';
import { FileSizePipe } from './filesize.pipe'; import { FileSizePipe } from './filesize.pipe';
@Pipe({ @Pipe({ name: 'status' })
name: 'status',
standalone: false,
})
export class TorrentStatusPipe implements PipeTransform { export class TorrentStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} constructor(private pipe: FileSizePipe) {}
@ -15,59 +12,49 @@ export class TorrentStatusPipe implements PipeTransform {
} }
if (torrent.downloads.length > 0) { 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) { if (allFinished) {
return 'Finished'; return 'Finished';
} }
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0); const downloading = torrent.downloads.filter((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null); const downloaded = torrent.downloads.filter((m) => m.downloadFinished != null);
if (downloading.length > 0) { if (downloading.length > 0) {
const bytesDone = downloading.sum((m) => m.bytesDone); const bytesDone = downloading.reduce((sum, m) => sum + m.bytesDone, 0);
const bytesTotal = downloading.sum((m) => m.bytesTotal); const bytesTotal = downloading.reduce((sum, m) => sum + m.bytesTotal, 0);
let progress = (bytesDone / bytesTotal) * 100; const progress = (bytesDone / bytesTotal || 0) * 100;
if (isNaN(progress)) { const allSpeeds = downloading.reduce((sum, m) => sum + m.speed, 0);
progress = 0;
}
let allSpeeds = downloading.sum((m) => m.speed); const speed: string | string[] = this.pipe.transform(allSpeeds, 'filesize');
let speed: string | string[] = '0';
speed = this.pipe.transform(allSpeeds, 'filesize');
return `Downloading file ${downloading.length + downloaded.length}/${ return `Downloading file ${downloading.length + downloaded.length}/${
torrent.downloads.length torrent.downloads.length
} (${progress.toFixed(2)}% - ${speed}/s)`; } (${progress.toFixed(2)}% - ${speed}/s)`;
} }
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0); const unpacking = torrent.downloads.filter((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null); const unpacked = torrent.downloads.filter((m) => m.unpackingFinished != null);
if (unpacking.length > 0) { if (unpacking.length > 0) {
const bytesDone = unpacking.sum((m) => m.bytesDone); const bytesDone = unpacking.reduce((sum, m) => sum + m.bytesDone, 0);
const bytesTotal = unpacking.sum((m) => m.bytesTotal); const bytesTotal = unpacking.reduce((sum, m) => sum + m.bytesTotal, 0);
let progress = (bytesDone / bytesTotal) * 100; const progress = (bytesDone / bytesTotal || 0) * 100;
if (isNaN(progress)) {
progress = 0;
}
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed( return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
2, 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) { if (queuedForUnpacking.length > 0) {
return `Queued for unpacking`; 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) { if (queuedForDownload.length > 0) {
return `Queued for downloading`; return `Queued for downloading`;

View file

@ -33,7 +33,7 @@
<input <input
type="checkbox" type="checkbox"
(click)="toggleSelect(torrent.torrentId)" (click)="toggleSelect(torrent.torrentId)"
[checked]="selectedTorrents.contains(torrent.torrentId)" [checked]="selectedTorrents.includes(torrent.torrentId)"
/> />
</td> </td>
<td (click)="openTorrent(torrent.torrentId)" class="break-all"> <td (click)="openTorrent(torrent.torrentId)" class="break-all">

View file

@ -3,12 +3,18 @@ import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model'; import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service'; import { TorrentService } from '../torrent.service';
import { forkJoin, Observable } from 'rxjs'; 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({ @Component({
selector: 'app-torrent-table', selector: 'app-torrent-table',
templateUrl: './torrent-table.component.html', templateUrl: './torrent-table.component.html',
styleUrls: ['./torrent-table.component.scss'], styleUrls: ['./torrent-table.component.scss'],
standalone: false, imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, SortPipe, FileSizePipe],
standalone: true,
}) })
export class TorrentTableComponent implements OnInit { export class TorrentTableComponent implements OnInit {
public torrents: Torrent[] = []; public torrents: Torrent[] = [];
@ -48,18 +54,18 @@ export class TorrentTableComponent implements OnInit {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.torrentService.getList().subscribe( this.torrentService.getList().subscribe({
(result) => { next: (result) => {
this.torrents = result; this.torrents = result;
this.torrentService.update$.subscribe((result2) => { this.torrentService.update$.subscribe((result2) => {
this.torrents = result2; this.torrents = result2;
}); });
}, },
(err) => { error: (err) => {
this.error = err.error; this.error = err.error;
}, },
); });
} }
public sort(property: string): void { public sort(property: string): void {
@ -107,7 +113,7 @@ export class TorrentTableComponent implements OnInit {
public deleteOk(): void { public deleteOk(): void {
this.deleting = true; this.deleting = true;
let calls: Observable<void>[] = []; const calls: Observable<void>[] = [];
this.selectedTorrents.forEach((torrentId) => { this.selectedTorrents.forEach((torrentId) => {
calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)); calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
@ -140,7 +146,7 @@ export class TorrentTableComponent implements OnInit {
public retryOk(): void { public retryOk(): void {
this.retrying = true; this.retrying = true;
let calls: Observable<void>[] = []; const calls: Observable<void>[] = [];
this.selectedTorrents.forEach((torrentId) => { this.selectedTorrents.forEach((torrentId) => {
calls.push(this.torrentService.retry(torrentId)); calls.push(this.torrentService.retry(torrentId));
@ -163,30 +169,40 @@ export class TorrentTableComponent implements OnInit {
public changeSettingsModal(): void { public changeSettingsModal(): void {
this.changeSettingsError = null; 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 = this.updateSettingsDownloadClient = selectedTorrents.every(
selectedTorrents.distinctBy((m) => m.downloadClient).count() == 1 ? selectedTorrents[0].downloadClient : null; (m, _, arr) => m.downloadClient === arr[0].downloadClient,
this.updateSettingsHostDownloadAction = )
selectedTorrents.distinctBy((m) => m.hostDownloadAction).count() == 1 ? selectedTorrents[0].downloadClient
? selectedTorrents[0].hostDownloadAction : null;
: null; this.updateSettingsHostDownloadAction = selectedTorrents.every(
this.updateSettingsCategory = (m, _, arr) => m.hostDownloadAction === arr[0].hostDownloadAction,
selectedTorrents.distinctBy((m) => m.category).count() == 1 ? selectedTorrents[0].category : null; )
this.updateSettingsPriority = ? selectedTorrents[0].hostDownloadAction
selectedTorrents.distinctBy((m) => m.priority).count() == 1 ? selectedTorrents[0].priority : null; : null;
this.updateSettingsDownloadRetryAttempts = this.updateSettingsCategory = selectedTorrents.every((m, _, arr) => m.category === arr[0].category)
selectedTorrents.distinctBy((m) => m.downloadRetryAttempts).count() == 1 ? selectedTorrents[0].category
? selectedTorrents[0].downloadRetryAttempts : null;
: null; this.updateSettingsPriority = selectedTorrents.every((m, _, arr) => m.priority === arr[0].priority)
this.updateSettingsTorrentRetryAttempts = ? selectedTorrents[0].priority
selectedTorrents.distinctBy((m) => m.torrentRetryAttempts).count() == 1 : null;
? selectedTorrents[0].torrentRetryAttempts this.updateSettingsDownloadRetryAttempts = selectedTorrents.every(
: null; (m, _, arr) => m.downloadRetryAttempts === arr[0].downloadRetryAttempts,
this.updateSettingsDeleteOnError = )
selectedTorrents.distinctBy((m) => m.deleteOnError).count() == 1 ? selectedTorrents[0].deleteOnError : null; ? selectedTorrents[0].downloadRetryAttempts
this.updateSettingsTorrentLifetime = : null;
selectedTorrents.distinctBy((m) => m.lifetime).count() == 1 ? selectedTorrents[0].lifetime : 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; this.isChangeSettingsModalActive = true;
} }
@ -198,9 +214,9 @@ export class TorrentTableComponent implements OnInit {
public changeSettingsOk(): void { public changeSettingsOk(): void {
this.changingSettings = true; 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) => { selectedTorrents.forEach((torrent) => {
if (this.updateSettingsDownloadClient != null) { if (this.updateSettingsDownloadClient != null) {

View file

@ -8,10 +8,6 @@ table {
} }
} }
.fa-download {
margin-left: 12px;
}
.flex-container { .flex-container {
display: flex; display: flex;
flex: 1 1 0; flex: 1 1 0;

View file

@ -3,12 +3,29 @@ import { ActivatedRoute, Router } from '@angular/router';
import { saveAs } from 'file-saver-es'; import { saveAs } from 'file-saver-es';
import { Torrent } from '../models/torrent.model'; import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service'; 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({ @Component({
selector: 'app-torrent', selector: 'app-torrent',
templateUrl: './torrent.component.html', templateUrl: './torrent.component.html',
styleUrls: ['./torrent.component.scss'], styleUrls: ['./torrent.component.scss'],
standalone: false, imports: [
NgClass,
CdkCopyToClipboard,
FormsModule,
DatePipe,
TorrentStatusPipe,
DownloadStatusPipe,
DecodeURIPipe,
FileSizePipe,
],
standalone: true,
}) })
export class TorrentComponent implements OnInit { export class TorrentComponent implements OnInit {
public torrent: Torrent; public torrent: Torrent;
@ -59,23 +76,21 @@ export class TorrentComponent implements OnInit {
this.activatedRoute.params.subscribe((params) => { this.activatedRoute.params.subscribe((params) => {
const torrentId = params['id']; const torrentId = params['id'];
this.torrentService.get(torrentId).subscribe( this.torrentService.get(torrentId).subscribe({
(torrent) => { next: (torrent) => {
this.torrent = torrent; this.torrent = torrent;
this.torrentService.update$.subscribe((result) => { this.torrentService.update$.subscribe((result) => {
this.update(result); this.update(result);
}); });
}, },
() => { error: () => this.router.navigate(['/']),
this.router.navigate(['/']); });
},
);
}); });
} }
public update(torrents: Torrent[]): void { 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) { if (updatedTorrent == null) {
return; 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`); saveAs(blob, `${this.torrent.rdName}.torrent`);
} }
@ -116,18 +131,18 @@ export class TorrentComponent implements OnInit {
this.torrentService this.torrentService
.delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles) .delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
.subscribe( .subscribe({
() => { next: () => {
this.isDeleteModalActive = false; this.isDeleteModalActive = false;
this.deleting = false; this.deleting = false;
this.router.navigate(['/']); this.router.navigate(['/']);
}, },
(err) => { error: (err) => {
this.deleteError = err.error; this.deleteError = err.error;
this.deleting = false; this.deleting = false;
}, },
); });
} }
public showRetryModal(): void { public showRetryModal(): void {
@ -143,18 +158,18 @@ export class TorrentComponent implements OnInit {
public retryOk(): void { public retryOk(): void {
this.retrying = true; this.retrying = true;
this.torrentService.retry(this.torrent.torrentId).subscribe( this.torrentService.retry(this.torrent.torrentId).subscribe({
() => { next: () => {
this.isRetryModalActive = false; this.isRetryModalActive = false;
this.retrying = false; this.retrying = false;
this.router.navigate(['/']); this.router.navigate(['/']);
}, },
(err) => { error: (err) => {
this.retryError = err.error; this.retryError = err.error;
this.retrying = false; this.retrying = false;
}, },
); });
} }
public showDownloadRetryModal(downloadId: string): void { public showDownloadRetryModal(downloadId: string): void {
@ -171,16 +186,16 @@ export class TorrentComponent implements OnInit {
public downloadRetryOk(): void { public downloadRetryOk(): void {
this.downloadRetrying = true; this.downloadRetrying = true;
this.torrentService.retryDownload(this.downloadRetryId).subscribe( this.torrentService.retryDownload(this.downloadRetryId).subscribe({
() => { next: () => {
this.isDownloadRetryModalActive = false; this.isDownloadRetryModalActive = false;
this.downloadRetrying = false; this.downloadRetrying = false;
}, },
(err) => { error: (err) => {
this.downloadRetryError = err.error; this.downloadRetryError = err.error;
this.downloadRetrying = false; this.downloadRetrying = false;
}, },
); });
} }
public showUpdateSettingsModal(): void { public showUpdateSettingsModal(): void {
@ -212,16 +227,16 @@ export class TorrentComponent implements OnInit {
this.torrent.deleteOnError = this.updateSettingsDeleteOnError; this.torrent.deleteOnError = this.updateSettingsDeleteOnError;
this.torrent.lifetime = this.updateSettingsTorrentLifetime; this.torrent.lifetime = this.updateSettingsTorrentLifetime;
this.torrentService.update(this.torrent).subscribe( this.torrentService.update(this.torrent).subscribe({
() => { next: () => {
this.isUpdateSettingsModalActive = false; this.isUpdateSettingsModalActive = false;
this.updating = false; this.updating = false;
}, },
() => { error: () => {
this.isUpdateSettingsModalActive = false; this.isUpdateSettingsModalActive = false;
this.updating = false; this.updating = false;
}, },
); });
} }
toggleDeleteSelectAllOptions() { toggleDeleteSelectAllOptions() {
this.deleteData = this.deleteSelectAll; this.deleteData = this.deleteSelectAll;

View file

@ -6,11 +6,10 @@
<base href="/" /> <base href="/" />
<script> <script>
(function () { (function () {
var baseElement = document.querySelector("base"); const baseElement = document.querySelector("base");
if (baseElement) { if (baseElement) {
var hrefValue = baseElement.getAttribute("href"); window["_app_base"] = baseElement.getAttribute("href");
window["_app_base"] = hrefValue;
console.log("setting base href to " + window["_app_base"]); console.log("setting base href to " + window["_app_base"]);
} }
})(); })();

View file

@ -1,13 +1,29 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode, importProvidersFrom } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; 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) { if (environment.production) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic() bootstrapApplication(AppComponent, {
.bootstrapModule(AppModule) 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)); .catch((err) => console.error(err));

View file

@ -14,12 +14,15 @@
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "bundler",
"importHelpers": true, "importHelpers": true,
"strictNullChecks": false, "strictNullChecks": false,
"target": "ES2022", "target": "ES2022",
"module": "es2020", "module": "es2020",
"lib": ["es2020", "dom"], "lib": [
"es2020",
"dom"
],
"useDefineForClassFields": false "useDefineForClassFields": false
}, },
"angularCompilerOptions": { "angularCompilerOptions": {