Upgrade packages.

Migrate Angular to new injector constructors.
This commit is contained in:
Roger Far 2026-03-14 13:39:51 -06:00
parent 351e1eb7c8
commit 683ac44c12
34 changed files with 1094 additions and 1788 deletions

View file

@ -1,14 +1,14 @@
const minorOnly = ['eslint']; const minorOnly = ["eslint"];
const patchOnly = []; const patchOnly = [];
module.exports = { module.exports = {
target: (name) => { target: (name) => {
if (minorOnly.indexOf(name) > -1) { if (minorOnly.indexOf(name) > -1) {
return 'minor'; return "minor";
} }
if (patchOnly.indexOf(name) > -1) { if (patchOnly.indexOf(name) > -1) {
return 'patch'; return "patch";
} }
return 'latest'; return "latest";
} },
}; };

2548
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,19 +8,20 @@
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk", "update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk",
"lint": "ng lint", "lint": "ng lint",
"prettier": "prettier --write \"./**/*.{ts,html,json}\"" "prettier": "prettier --write .",
"precommit": "npm run prettier && npm run lint"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^21.1.4", "@angular/animations": "^21.2.4",
"@angular/cdk": "^21.1.4", "@angular/cdk": "^21.2.2",
"@angular/common": "^21.1.4", "@angular/common": "^21.2.4",
"@angular/compiler": "^21.1.4", "@angular/compiler": "^21.2.4",
"@angular/core": "^21.1.4", "@angular/core": "^21.2.4",
"@angular/forms": "^21.1.4", "@angular/forms": "^21.2.4",
"@angular/platform-browser": "^21.1.4", "@angular/platform-browser": "^21.2.4",
"@angular/platform-browser-dynamic": "^21.1.4", "@angular/platform-browser-dynamic": "^21.2.4",
"@angular/router": "^21.1.4", "@angular/router": "^21.2.4",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.2.0",
"@microsoft/signalr": "^10.0.0", "@microsoft/signalr": "^10.0.0",
"bulma": "^1.0.4", "bulma": "^1.0.4",
@ -29,24 +30,24 @@
"filesize": "^11.0.13", "filesize": "^11.0.13",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"zone.js": "^0.16.0" "zone.js": "^0.16.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-eslint/builder": "21.2.0", "@angular-eslint/builder": "21.3.0",
"@angular-eslint/eslint-plugin": "21.2.0", "@angular-eslint/eslint-plugin": "21.3.0",
"@angular-eslint/eslint-plugin-template": "21.2.0", "@angular-eslint/eslint-plugin-template": "21.3.0",
"@angular-eslint/schematics": "21.2.0", "@angular-eslint/schematics": "21.3.0",
"@angular-eslint/template-parser": "21.2.0", "@angular-eslint/template-parser": "21.3.0",
"@angular/build": "^21.1.4", "@angular/build": "^21.2.2",
"@angular/cli": "^21.1.4", "@angular/cli": "^21.2.2",
"@angular/compiler-cli": "^21.1.4", "@angular/compiler-cli": "^21.2.4",
"@angular/language-service": "^21.1.4", "@angular/language-service": "^21.2.4",
"@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": "^25.2.3", "@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.55.0", "@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.55.0", "@typescript-eslint/parser": "^8.57.0",
"eslint": "^9.39.2", "eslint": "^9.39.4",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "5.9.3" "typescript": "5.9.3"
} }

View file

@ -87,7 +87,7 @@
<div class="field" style="margin-top: 10px"> <div class="field" style="margin-top: 10px">
<div class="control"> <div class="control">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()"> <button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
@if (type === 'torrent') { @if (type === "torrent") {
<span>Add Torrent</span> <span>Add Torrent</span>
} @else { } @else {
<span>Add NZB</span> <span>Add NZB</span>
@ -109,9 +109,7 @@
<option [ngValue]="3">Synology DownloadStation</option> <option [ngValue]="3">Synology DownloadStation</option>
</select> </select>
</div> </div>
<p class="help"> <p class="help">Select which downloader is used to download this from the debrid provider to your host.</p>
Select which downloader is used to download this from the debrid provider to your host.
</p>
</div> </div>
<div class="field"> <div class="field">
@ -123,8 +121,8 @@
</select> </select>
</div> </div>
<p class="help"> <p class="help">
When a download is finished on the provider, perform this action. Use this setting if you only want When a download is finished on the provider, perform this action. Use this setting if you only want to add files
to add files to your debrid provider but not download them to the host. to your debrid provider but not download them to the host.
</p> </p>
</div> </div>
@ -213,8 +211,8 @@
<input class="input" type="number" [(ngModel)]="finishedActionDelay" /> <input class="input" type="number" [(ngModel)]="finishedActionDelay" />
</div> </div>
<p class="help"> <p class="help">
When a download is finished on the provider, perform this action. Use this setting if you only want When a download is finished on the provider, perform this action. Use this setting if you only want to add files
to add files to your debrid provider but not download them to the host. to your debrid provider but not download them to the host.
</p> </p>
</div> </div>
@ -247,9 +245,7 @@
<div class="control"> <div class="control">
<input class="input" type="number" step="1" [(ngModel)]="priority" /> <input class="input" type="number" step="1" [(ngModel)]="priority" />
</div> </div>
<p class="help"> <p class="help">Set the priority where 1 is the highest. When empty it will be assigned the lowest priority.</p>
Set the priority where 1 is the highest. When empty it will be assigned the lowest priority.
</p>
</div> </div>
</div> </div>
</div> </div>
@ -289,8 +285,8 @@
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" /> <input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" />
</div> </div>
<p class="help"> <p class="help">
The maximum lifetime of a download in minutes. When this time has passed, mark it as error. If the The maximum lifetime of a download in minutes. When this time has passed, mark it as error. If the download is
download is completed, the lifetime setting will not apply. 0 to disable. completed, the lifetime setting will not apply. 0 to disable.
</p> </p>
</div> </div>
</div> </div>
@ -303,4 +299,3 @@
} }
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TorrentService } from 'src/app/torrent.service'; import { TorrentService } from 'src/app/torrent.service';
import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model'; import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model';
@ -15,6 +15,11 @@ import { NgClass } from '@angular/common';
standalone: true, standalone: true,
}) })
export class AddNewTorrentComponent implements OnInit { export class AddNewTorrentComponent implements OnInit {
private router = inject(Router);
private torrentService = inject(TorrentService);
private settingsService = inject(SettingsService);
private activatedRoute = inject(ActivatedRoute);
public type: 'torrent' | 'nzb' = 'torrent'; public type: 'torrent' | 'nzb' = 'torrent';
public fileName: string; public fileName: string;
public magnetLink: string; public magnetLink: string;
@ -53,13 +58,6 @@ export class AddNewTorrentComponent implements OnInit {
private selectedFile: File; private selectedFile: File;
constructor(
private router: Router,
private torrentService: TorrentService,
private settingsService: SettingsService,
private activatedRoute: ActivatedRoute,
) {}
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.queryParams.subscribe((params) => { this.activatedRoute.queryParams.subscribe((params) => {
if (params['type'] === 'nzb') { if (params['type'] === 'nzb') {
@ -89,9 +87,7 @@ export class AddNewTorrentComponent implements OnInit {
.map((c) => c.trim()) .map((c) => c.trim())
.filter((c) => c.length > 0) .filter((c) => c.length > 0)
.filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i); .filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i);
const matchedCategory = this.categories.find( const matchedCategory = this.categories.find((c) => c.toLowerCase() === (this.category ?? '').toLowerCase());
(c) => c.toLowerCase() === (this.category ?? '').toLowerCase(),
);
if (matchedCategory) { if (matchedCategory) {
this.category = matchedCategory; this.category = matchedCategory;
} }

View file

@ -2,9 +2,9 @@ import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; 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: [],
imports: [RouterOutlet], imports: [RouterOutlet],
}) })
export class AppComponent {} export class AppComponent {}

View file

@ -1,12 +1,12 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
constructor(private router: Router) {} private router = inject(Router);
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe( return next.handle(req).pipe(

View file

@ -1,16 +1,14 @@
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthService { export class AuthService {
constructor( private http = inject(HttpClient);
private http: HttpClient, private baseHref = inject(APP_BASE_HREF);
@Inject(APP_BASE_HREF) private baseHref: string,
) {}
public isLoggedIn(): Observable<boolean> { public isLoggedIn(): Observable<boolean> {
return this.http.get<boolean>(`${this.baseHref}Api/Authentication/IsLoggedIn`); return this.http.get<boolean>(`${this.baseHref}Api/Authentication/IsLoggedIn`);

View file

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

View file

@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform, inject } 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({ name: 'downloadStatus' }) @Pipe({ name: 'downloadStatus' })
export class DownloadStatusPipe implements PipeTransform { export class DownloadStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} private pipe = inject(FileSizePipe);
transform(value: Download): string { transform(value: Download): string {
if (!value) { if (!value) {

View file

@ -1,7 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { filesize } from 'filesize'; import { filesize } from 'filesize';
@Pipe({ name: 'filesize', }) @Pipe({ name: 'filesize' })
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

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, inject } 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 { FormsModule } from '@angular/forms';
@ -12,16 +12,14 @@ import { NgClass } from '@angular/common';
standalone: true, standalone: true,
}) })
export class LoginComponent { export class LoginComponent {
private authService = inject(AuthService);
private router = inject(Router);
public userName: string; public userName: string;
public password: string; public password: string;
public error: string; public error: string;
public loggingIn: boolean; public loggingIn: boolean;
constructor(
private authService: AuthService,
private router: Router,
) {}
public setUserName(event: Event): void { public setUserName(event: Event): void {
this.userName = (event.target as any).value; this.userName = (event.target as any).value;
} }

View file

@ -4,4 +4,4 @@
.notification { .notification {
margin-top: 52px; margin-top: 52px;
} }

View file

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { NavigationEnd, Router, RouterLink } 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';
@ -13,17 +13,17 @@ import { NgClass, DatePipe } from '@angular/common';
standalone: true, standalone: true,
}) })
export class NavbarComponent implements OnInit { export class NavbarComponent implements OnInit {
private settingsService = inject(SettingsService);
private authService = inject(AuthService);
private router = inject(Router);
public showMobileMenu = false; public showMobileMenu = false;
public profile: Profile; public profile: Profile;
public providerLink: string; public providerLink: string;
public version: string; public version: string;
constructor( constructor() {
private settingsService: SettingsService,
private authService: AuthService,
private router: Router,
) {
this.router.events.subscribe((event) => { this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
this.showMobileMenu = false; this.showMobileMenu = false;

View file

@ -1,9 +1,9 @@
import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core'; import { Pipe, PipeTransform, SecurityContext, VERSION, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
@Pipe({ name: 'nl2br', }) @Pipe({ name: 'nl2br' })
export class Nl2BrPipe implements PipeTransform { export class Nl2BrPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {} private sanitizer = inject(DomSanitizer);
transform(value: string, sanitizeBeforehand?: boolean): string { transform(value: string, sanitizeBeforehand?: boolean): string {
if (typeof value !== 'string') { if (typeof value !== 'string') {

View file

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NgClass } from '@angular/common'; import { NgClass } from '@angular/common';
@ -11,7 +11,7 @@ import { NgClass } from '@angular/common';
standalone: true, standalone: true,
}) })
export class ProfileComponent { export class ProfileComponent {
constructor(private authService: AuthService) {} private authService = inject(AuthService);
public username: string; public username: string;
public password: string; public password: string;

View file

@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Profile } from './models/profile.model'; import { Profile } from './models/profile.model';
import { Setting } from './models/setting.model'; import { Setting } from './models/setting.model';
@ -10,10 +10,8 @@ import { Version } from './models/version.model';
providedIn: 'root', providedIn: 'root',
}) })
export class SettingsService { export class SettingsService {
constructor( private http = inject(HttpClient);
private http: HttpClient, private baseHref = inject(APP_BASE_HREF);
@Inject(APP_BASE_HREF) private baseHref: string,
) {}
public get(): Observable<Setting[]> { public get(): Observable<Setting[]> {
return this.http.get<Setting[]>(`${this.baseHref}Api/Settings`); return this.http.get<Setting[]>(`${this.baseHref}Api/Settings`);

View file

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, inject } 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 { NgClass, KeyValuePipe } from '@angular/common';
@ -14,6 +14,8 @@ import { FileSizePipe } from '../filesize.pipe';
standalone: true, standalone: true,
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
private settingsService = inject(SettingsService);
public activeTab = 0; public activeTab = 0;
public tabs: Setting[] = []; public tabs: Setting[] = [];
@ -35,8 +37,6 @@ export class SettingsComponent implements OnInit {
public canRegisterMagnetHandler = false; public canRegisterMagnetHandler = false;
constructor(private settingsService: SettingsService) {}
ngOnInit(): void { ngOnInit(): void {
this.reset(); this.reset();
this.canRegisterMagnetHandler = !!(window.isSecureContext && 'registerProtocolHandler' in navigator); this.canRegisterMagnetHandler = !!(window.isSecureContext && 'registerProtocolHandler' in navigator);

View file

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, inject } 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 { NgClass } from '@angular/common';
@ -12,6 +12,9 @@ import { FormsModule } from '@angular/forms';
standalone: true, standalone: true,
}) })
export class SetupComponent { export class SetupComponent {
private authService = inject(AuthService);
private router = inject(Router);
public userName: string; public userName: string;
public password: string; public password: string;
public provider = 0; public provider = 0;
@ -22,11 +25,6 @@ export class SetupComponent {
public step: number = 1; public step: number = 1;
constructor(
private authService: AuthService,
private router: Router,
) {}
public setup(): void { public setup(): void {
this.error = null; this.error = null;
this.working = true; this.working = true;

View file

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'sort', }) @Pipe({ name: 'sort' })
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

@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform, inject } 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({ name: 'status' }) @Pipe({ name: 'status' })
export class TorrentStatusPipe implements PipeTransform { export class TorrentStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} private pipe = inject(FileSizePipe);
transform(torrent: Torrent): string { transform(torrent: Torrent): string {
if (torrent.error) { if (torrent.error) {

View file

@ -10,16 +10,16 @@
<br /> <br />
Available: {{ diskSpaceStatus.availableSpaceGB }} GB | Resume at: {{ diskSpaceStatus.thresholdGB * 2 }} GB Available: {{ diskSpaceStatus.availableSpaceGB }} GB | Resume at: {{ diskSpaceStatus.thresholdGB * 2 }} GB
<br /> <br />
<small>Last check: {{ diskSpaceStatus.lastCheckTime | date: 'short' }}</small> <small>Last check: {{ diskSpaceStatus.lastCheckTime | date: "short" }}</small>
</div> </div>
} }
@if (rateLimitStatus?.nextDequeueTime) { @if (rateLimitStatus?.nextDequeueTime) {
<div class="notification is-warning"> <div class="notification is-warning">
<strong>Debrid provider rate limit reached</strong> <strong>Debrid provider rate limit reached</strong>
<br /> <br />
Processing paused until {{ rateLimitStatus.nextDequeueTime | date: 'medium' }} Processing paused until {{ rateLimitStatus.nextDequeueTime | date: "medium" }}
@if (rateLimitStatus.secondsRemaining > 0) { @if (rateLimitStatus.secondsRemaining > 0) {
({{ rateLimitStatus.secondsRemaining | number: '1.0-0' }} seconds remaining) ({{ rateLimitStatus.secondsRemaining | number: "1.0-0" }} seconds remaining)
} }
</div> </div>
} }
@ -107,10 +107,16 @@
} }
</div> </div>
<div class="mobile-card-details"> <div class="mobile-card-details">
<span class="mobile-card-detail"><span class="detail-label">Size</span> {{ torrent.rdSize | filesize }}</span> <span class="mobile-card-detail"
><span class="detail-label">Size</span> {{ torrent.rdSize | filesize }}</span
>
<span class="mobile-card-detail"><span class="detail-label">Seeders</span> {{ torrent.rdSeeders }}</span> <span class="mobile-card-detail"><span class="detail-label">Seeders</span> {{ torrent.rdSeeders }}</span>
<span class="mobile-card-detail"><span class="detail-label">Files</span> {{ torrent.files.length | number }}</span> <span class="mobile-card-detail"
<span class="mobile-card-detail"><span class="detail-label">Downloads</span> {{ torrent.downloads.length | number }}</span> ><span class="detail-label">Files</span> {{ torrent.files.length | number }}</span
>
<span class="mobile-card-detail"
><span class="detail-label">Downloads</span> {{ torrent.downloads.length | number }}</span
>
</div> </div>
</div> </div>
</div> </div>

View file

@ -32,7 +32,7 @@ table {
flex-shrink: 0; flex-shrink: 0;
padding-top: 0.125rem; padding-top: 0.125rem;
input[type='checkbox'] { input[type="checkbox"] {
width: 1.1rem; width: 1.1rem;
height: 1.1rem; height: 1.1rem;
cursor: pointer; cursor: pointer;

View file

@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model'; import { Torrent } from '../models/torrent.model';
import { DiskSpaceStatus } from '../models/disk-space-status.model'; import { DiskSpaceStatus } from '../models/disk-space-status.model';
@ -19,6 +19,9 @@ import { FileSizePipe } from '../filesize.pipe';
standalone: true, standalone: true,
}) })
export class TorrentTableComponent implements OnInit, OnDestroy { export class TorrentTableComponent implements OnInit, OnDestroy {
private router = inject(Router);
private torrentService = inject(TorrentService);
public torrents: Torrent[] = []; public torrents: Torrent[] = [];
public selectedTorrents: string[] = []; public selectedTorrents: string[] = [];
public error: string; public error: string;
@ -57,11 +60,6 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
private mobileQuery: MediaQueryList; private mobileQuery: MediaQueryList;
private mobileQueryListener: (e: MediaQueryListEvent) => void; private mobileQueryListener: (e: MediaQueryListEvent) => void;
constructor(
private router: Router,
private torrentService: TorrentService,
) {}
ngOnInit(): void { ngOnInit(): void {
this.mobileQuery = window.matchMedia('(max-width: 768px)'); this.mobileQuery = window.matchMedia('(max-width: 768px)');
this.isMobile = this.mobileQuery.matches; this.isMobile = this.mobileQuery.matches;

View file

@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import * as signalR from '@microsoft/signalr'; import * as signalR from '@microsoft/signalr';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { Torrent, TorrentFileAvailability } from './models/torrent.model'; import { Torrent, TorrentFileAvailability } from './models/torrent.model';
@ -11,16 +11,16 @@ import { APP_BASE_HREF } from '@angular/common';
providedIn: 'root', providedIn: 'root',
}) })
export class TorrentService { export class TorrentService {
private http = inject(HttpClient);
private baseHref = inject(APP_BASE_HREF);
public update$: Subject<Torrent[]> = new Subject(); public update$: Subject<Torrent[]> = new Subject();
public diskSpaceStatus$: Subject<DiskSpaceStatus> = new Subject(); public diskSpaceStatus$: Subject<DiskSpaceStatus> = new Subject();
public rateLimitStatus$: Subject<RateLimitStatus> = new Subject(); public rateLimitStatus$: Subject<RateLimitStatus> = new Subject();
private connection: signalR.HubConnection; private connection: signalR.HubConnection;
constructor( constructor() {
private http: HttpClient,
@Inject(APP_BASE_HREF) private baseHref: string,
) {
this.connect(); this.connect();
} }

View file

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; 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';
@ -28,6 +28,10 @@ import { FileSizePipe } from '../filesize.pipe';
standalone: true, standalone: true,
}) })
export class TorrentComponent implements OnInit { export class TorrentComponent implements OnInit {
private activatedRoute = inject(ActivatedRoute);
private router = inject(Router);
private torrentService = inject(TorrentService);
public torrent: Torrent; public torrent: Torrent;
public activeTab: number = 0; public activeTab: number = 0;
@ -66,12 +70,6 @@ export class TorrentComponent implements OnInit {
public updating: boolean; public updating: boolean;
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private torrentService: TorrentService,
) {}
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.params.subscribe((params) => { this.activatedRoute.params.subscribe((params) => {
const torrentId = params['id']; const torrentId = params['id'];

View file

@ -1,7 +1,6 @@
import { enableProdMode, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { enableProdMode, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import { FileSizePipe } from './app/filesize.pipe'; import { FileSizePipe } from './app/filesize.pipe';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
@ -18,12 +17,12 @@ if (environment.production) {
} }
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, {
providers: [ providers: [
provideZoneChangeDetection(),importProvidersFrom(BrowserModule, AppRoutingModule, FormsModule, ClipboardModule), provideZoneChangeDetection(),
FileSizePipe, importProvidersFrom(BrowserModule, AppRoutingModule, FormsModule, ClipboardModule),
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, FileSizePipe,
{ provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
provideHttpClient(withInterceptorsFromDi()), { provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' },
] provideHttpClient(withInterceptorsFromDi()),
}) ],
.catch((err) => console.error(err)); }).catch((err) => console.error(err));

View file

@ -8,11 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -15,9 +15,9 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="SharpCompress" Version="0.42.1" /> <PackageReference Include="SharpCompress" Version="[0.42.1]" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />

View file

@ -330,7 +330,8 @@ public class TorBoxDebridClientTest
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), It.IsAny<Boolean>(), It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), It.IsAny<Boolean>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<UsenetAddResult> { Data = new UsenetAddResult { Hash = "new-hash" } }); .ReturnsAsync(new Response<UsenetAddResult> { Data = new()
{ Hash = "new-hash" } });
// Act // Act
var result = await clientMock.Object.AddNzbFile(bytes, name); var result = await clientMock.Object.AddNzbFile(bytes, name);
@ -605,7 +606,9 @@ public class TorBoxDebridClientTest
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); .ReturnsAsync(new Response<User> { Data = new()
{ Settings = new()
{ SeedTorrents = 5 } } });
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached"));
@ -632,10 +635,12 @@ public class TorBoxDebridClientTest
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); .ReturnsAsync(new Response<User> { Data = new()
{ Settings = new()
{ SeedTorrents = 5 } } });
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("slow_down")); .ThrowsAsync(new("slow_down"));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink)); await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink));
@ -657,7 +662,9 @@ public class TorBoxDebridClientTest
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); .ReturnsAsync(new Response<User> { Data = new()
{ Settings = new()
{ SeedTorrents = 5 } } });
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60))); .ThrowsAsync(new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60)));
@ -683,10 +690,12 @@ public class TorBoxDebridClientTest
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); .ReturnsAsync(new Response<User> { Data = new()
{ Settings = new()
{ SeedTorrents = 5 } } });
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Wrapped", new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60)))); .ThrowsAsync(new("Wrapped", new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60))));
// Act & Assert // Act & Assert
var ex = await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink)); var ex = await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink));
@ -708,7 +717,9 @@ public class TorBoxDebridClientTest
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); .ReturnsAsync(new Response<User> { Data = new()
{ Settings = new()
{ SeedTorrents = 5 } } });
torrentsApiMock.Setup(m => m.AddFileAsync(bytes, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddFileAsync(bytes, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached"));

View file

@ -119,7 +119,7 @@ public static class DiConfig
if (delay >= timeout) if (delay >= timeout)
{ {
return new ValueTask<TimeSpan?>((TimeSpan?)null); return new((TimeSpan?)null);
} }
return new(delay); return new(delay);

View file

@ -12,18 +12,18 @@
<PackageReference Include="AllDebrid.NET" Version="1.0.18" /> <PackageReference Include="AllDebrid.NET" Version="1.0.18" />
<PackageReference Include="Aria2.NET" Version="1.0.6" /> <PackageReference Include="Aria2.NET" Version="1.0.6" />
<PackageReference Include="DebridLinkFr.NET" Version="1.0.4" /> <PackageReference Include="DebridLinkFr.NET" Version="1.0.4" />
<PackageReference Include="Downloader" Version="4.1.1" /> <PackageReference Include="Downloader" Version="5.1.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.3" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.3.0" /> <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.4.0" />
<PackageReference Include="MonoTorrent" Version="3.0.2" /> <PackageReference Include="MonoTorrent" Version="3.0.2" />
<PackageReference Include="Polly" Version="8.6.5" /> <PackageReference Include="Polly" Version="8.6.6" />
<PackageReference Include="Premiumize.NET" Version="1.0.10" /> <PackageReference Include="Premiumize.NET" Version="1.0.10" />
<PackageReference Include="RateLimitHeaders.Polly" Version="1.0.0" /> <PackageReference Include="RateLimitHeaders.Polly" Version="1.0.0" />
<PackageReference Include="RD.NET" Version="2.1.11" /> <PackageReference Include="RD.NET" Version="2.1.11" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SharpCompress" Version="0.42.1" /> <PackageReference Include="SharpCompress" Version="[0.42.1]" />
<PackageReference Include="Synology.Api.Client" Version="0.3.93" /> <PackageReference Include="Synology.Api.Client" Version="[0.3.93]" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />
<PackageReference Include="TorBox.NET" Version="1.6.2" /> <PackageReference Include="TorBox.NET" Version="1.6.2" />

View file

@ -10,9 +10,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="SharpCompress" Version="0.42.1" /> <PackageReference Include="SharpCompress" Version="[0.42.1]" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View file

@ -30,18 +30,18 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.3" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.5" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SharpCompress" Version="0.42.1" /> <PackageReference Include="SharpCompress" Version="[0.42.1]" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>