Updated update settings form validation.

This commit is contained in:
root 2023-08-26 23:24:34 -05:00
parent 2d73981314
commit d3635e69cb
10 changed files with 606 additions and 1063 deletions

View file

@ -38,3 +38,4 @@ server/RdtClient.Web/wwwroot/
build/
publish/data

3
.gitignore vendored
View file

@ -8,3 +8,6 @@ server/RdtClient.Web/appsettings.Development.json
data
Dockerfile.dev
test.bat
Dockerfile
docker-compose.yml

View file

@ -10,9 +10,11 @@ WORKDIR /appclient
RUN apk add --no-cache git
COPY . /appclient/
RUN \
echo "**** Cloning Source Code ****" && \
git clone https://github.com/rogerfar/rdt-client.git . && \
# echo "**** Cloning Source Code ****" && \
# git clone https://github.com/rogerfar/rdt-client.git . && \
cd client && \
echo "**** Building Code ****" && \
npm ci && \

View file

@ -8,4 +8,4 @@
"secure": false,
"ws": true
}
}
}

View file

@ -3,7 +3,7 @@ import { APP_BASE_HREF } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { curray } from 'curray';
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
@ -47,6 +47,7 @@ curray();
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
NgxFilesizeModule,
FlexLayoutModule,

View file

@ -7,31 +7,24 @@
<thead>
<tr>
<th>
<input
type="checkbox"
(click)="toggleSelectAll($event)"
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
/>
</th>
<th>Name</th>
<th>Category</th>
<th>Priority</th>
<th>Files</th>
<th>Downloads</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let torrent of torrents; trackBy: trackByMethod">
<td>
<input
type="checkbox"
(click)="toggleSelect(torrent.torrentId)"
[checked]="selectedTorrents.contains(torrent.torrentId)"
/>
<input type="checkbox" (click)="toggleSelectAll($event)"
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length" />
</th>
<th>Name</th>
<th>Category</th>
<th>Priority</th>
<th>Files</th>
<th>Downloads</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let torrent of torrents; trackBy: trackByMethod">
<td>
<input type="checkbox" (click)="toggleSelect(torrent)" [checked]="getChecked(torrent)" />
</td>
<td (click)="openTorrent(torrent.torrentId)">
<td (click)="openTorrent(torrent)">
{{ torrent.rdName }}
</td>
<td>
@ -59,34 +52,22 @@
<div class="field is-grouped">
<div class="control">
<button
class="button is-danger"
(click)="showDeleteModal()"
[disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0"
>
<button class="button is-danger" (click)="showDeleteModal()" [disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0">
Delete Selected
</button>
</div>
<div class="control">
<button
class="button is-primary"
(click)="showRetryModal()"
[disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0"
>
<button class="button is-primary" (click)="showRetryModal()" [disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0">
Retry Selected
</button>
</div>
<div class="control">
<button
class="button is-light"
(click)="showUpdateSettingsModal()"
[disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0"
>
<button class="button is-light" (click)="showUpdateSettingsModal()" [disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0">
Change Selected's Settings
</button>
</div>
@ -130,12 +111,8 @@
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="deleteOk()"
[disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }"
>
<button class="button is-success" (click)="deleteOk()" [disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }">
Delete selected
</button>
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
@ -153,98 +130,82 @@
<button class="delete" aria-label="close" (click)="updateSettingsCancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Category</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="updateSettingsCategory" />
<form [formGroup]="updateTorrentSettingsForm" (ngSubmit)="updateSettingsOk()" id="updateTorrentSettingsForm">
<div class="field">
<label class="label">Category</label>
<div class="control">
<input class="input" type="text" formControlName="category" name="category" />
</div>
<p class="help">The category becomes a sub-folder in your main download path.</p>
</div>
<p class="help">The category becomes a sub-folder in your main download path.</p>
</div>
<div class="field">
<label class="label">Priority</label>
<div class="control">
<input class="input" type="number" step="1" [(ngModel)]="updateSettingsPriority" />
<div class="field">
<label class="label">Priority</label>
<div class="control">
<input class="input" inputmode="numeric" type="text" pattern="\d*" formControlName="priority"
name="priority" />
</div>
<p class="help">
Set the priority for this torrent where 1 is the highest. When empty it will be assigned the lowest
priority.
</p>
</div>
<p class="help">
Set the priority for this torrent where 1 is the highest. When empty it will be assigned the lowest priority.
</p>
</div>
<div class="field">
<label class="label">Automatic retry downloads</label>
<div class="control">
<input
class="input"
type="number"
max="1000"
min="0"
step="1"
[(ngModel)]="updateSettingsDownloadRetryAttempts"
/>
<div class="field">
<label class="label">Automatic retry downloads</label>
<div class="control">
<input class="input" inputmode="numeric" type="text" pattern="\d*" formControlName="downloadRetryAttempts"
name="downloadRetryAttempts" />
</div>
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
</div>
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
</div>
<div class="field">
<label class="label">Automatic retry torrent</label>
<div class="control">
<input
class="input"
type="number"
max="1000"
min="0"
step="1"
[(ngModel)]="updateSettingsTorrentRetryAttempts"
/>
<div class="field">
<label class="label">Automatic retry torrent</label>
<div class="control">
<input class="input" inputmode="numeric" type="text" pattern="\d*" formControlName="torrentRetryAttempts"
name="torrentRetryAttempts" />
</div>
<p class="help">
When a single download has failed multiple times (see setting above) or when the torrent itself received an
error it will retry the full torrent this many times before marking it failed.
</p>
</div>
<p class="help">
When a single download has failed multiple times (see setting above) or when the torrent itself received an
error it will retry the full torrent this many times before marking it failed.
</p>
</div>
<div class="field">
<label class="label">Delete download when in error</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="updateSettingsDeleteOnError" />
<div class="field">
<label class="label">Delete download when in error</label>
<div class="control">
<input class="input" inputmode="numeric" type="text" pattern="\d*" formControlName="deleteOnError"
name="deleteOnError" />
</div>
<p class="help">
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to
disable.
</p>
</div>
<p class="help">
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to
disable.
</p>
</div>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input
class="input"
type="number"
max="100000"
min="0"
step="1"
[(ngModel)]="updateSettingsTorrentLifetime"
/>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input class="input" inputmode="numeric" type="text" pattern="\d*" formControlName="lifetime"
name="lifetime" />
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
</form>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="updateSettingsOk()"
[disabled]="updating"
[ngClass]="{ 'is-loading': updating }"
>
<button class="button is-success" type="submit" form="updateTorrentSettingsForm"
[disabled]="updateTorrentSettingsForm.pristine || updateTorrentSettingsForm.invalid || updating ? true : false"
[ngClass]="{ 'is-loading': updating }">
Save
</button>
<button
class="button"
(click)="updateSettingsCancel()"
[disabled]="updating"
[ngClass]="{ 'is-loading': updating }"
>
<button class="button" (click)="updateSettingsCancel()" [disabled]="updating" [ngClass]="{ }">
Cancel
</button>
<div style="width: 100%; max-width: 100%;"></div>
<button class="button is-danger" (click)="updateSettingsResetAll()"
[disabled]="updateTorrentSettingsForm.pristine || updating ? true : false" [ngClass]="{ }">
Reset All
</button>
</footer>
</div>
</div>
@ -267,12 +228,8 @@
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="retryOk()"
[disabled]="retrying"
[ngClass]="{ 'is-loading': retrying }"
>
<button class="button is-success" (click)="retryOk()" [disabled]="retrying"
[ngClass]="{ 'is-loading': retrying }">
Retry
</button>
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
@ -280,4 +237,4 @@
</button>
</footer>
</div>
</div>
</div>

View file

@ -3,3 +3,11 @@ table {
cursor: pointer;
}
}
.control > .ng-dirty.ng-valid {
border-color: green;
border-width: 2px;
}
.control > .ng-dirty.ng-invalid {
border-color: red;
border-width: 2px;
}

View file

@ -3,6 +3,7 @@ import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service';
import { forkJoin, Observable } from 'rxjs';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-torrent-table',
@ -12,8 +13,7 @@ import { forkJoin, Observable } from 'rxjs';
export class TorrentTableComponent implements OnInit {
public torrent: Torrent;
public torrents: Torrent[] = [];
public selectedTorrents: string[] = [];
public selectedTorrentsAsObjects: Torrent[] = []
public selectedTorrents: Torrent[] = [];
public error: string;
public isDeleteModalActive: boolean;
@ -22,23 +22,53 @@ export class TorrentTableComponent implements OnInit {
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
public isRetryModalActive: boolean;
public retryError: string;
public retrying: boolean;
public isUpdateSettingsModalActive: boolean;
public updateSettingsCategory: string;
public updateSettingsPriority: number;
public updateSettingsDownloadRetryAttempts: number;
public updateSettingsTorrentRetryAttempts: number;
public updateSettingsDeleteOnError: number;
public updateSettingsTorrentLifetime: number;
public updateError: string;
public updating: boolean;
constructor(private router: Router, private torrentService: TorrentService) {}
public getSameValue(key: string): any {
let previousValue: any = null;
this.selectedTorrents.forEach((torrent: Torrent, i: number) => {
type ObjectKey = keyof typeof torrent;
const str = key as ObjectKey;
const currentValue = torrent[str];
if (i == 0) {
previousValue = currentValue;
} else {
if (previousValue != currentValue) {
previousValue = null;
}
}
});
return previousValue;
}
public updateTorrentSettingsForm: FormGroup = this.fb.group({
category: [this.getSameValue('category') || '', [Validators.maxLength(100)]],
priority: [this.getSameValue('priority') || '', [Validators.min(0), Validators.min(0)]],
downloadRetryAttempts: [
this.getSameValue('downloadRetryAttempts') || '0',
[Validators.required, Validators.min(0), Validators.max(1000)],
],
torrentRetryAttempts: [
this.getSameValue('torrentRetryAttempts') || '0',
[Validators.required, Validators.min(0), Validators.max(1000)],
],
deleteOnError: [
this.getSameValue('deleteOnError') || '0',
[Validators.required, Validators.min(0), Validators.max(1000)],
],
lifetime: [this.getSameValue('lifetime') || '0', [Validators.required, Validators.min(0), Validators.max(100000)]],
});
constructor(private router: Router, private torrentService: TorrentService, private fb: FormBuilder) {}
ngOnInit(): void {
this.torrentService.getList().subscribe(
@ -55,61 +85,71 @@ export class TorrentTableComponent implements OnInit {
);
}
public openTorrent(torrentId: string): void {
this.router.navigate([`/torrent/${torrentId}`]);
public openTorrent(torrent: Torrent): void {
this.router.navigate([`/torrent/${torrent.torrentId}`]);
}
public trackByMethod(index: number, el: Torrent): string {
return el.torrentId;
}
public getChecked(torrent: Torrent) {
const index = this.selectedTorrents.findIndex((selected) => selected.torrentId === torrent.torrentId);
if (index > -1) {
return true;
}
return false;
}
public toggleSelectAll(event: any) {
this.selectedTorrents = [];
if (event.target.checked) {
this.torrents.map((torrent) => {
this.selectedTorrents.push(torrent.torrentId);
this.selectedTorrents.push(torrent);
});
}
}
public toggleSelect(torrentId: string) {
const index = this.selectedTorrents.indexOf(torrentId);
public toggleSelect(torrent: Torrent) {
const index = this.selectedTorrents.findIndex((selected) => selected.torrentId === torrent.torrentId);
if (index > -1) {
this.selectedTorrents.splice(index, 1);
} else {
this.selectedTorrents.push(torrentId);
this.selectedTorrents.push(torrent);
}
}
public showDeleteModal(): void {
this.deleteData = false;
this.deleteRdTorrent = false;
this.deleteLocalFiles = false;
this.deleteError = null;
this.isDeleteModalActive = true;
}
public deleteCancel(): void {
this.isDeleteModalActive = false;
}
public deleteOk(): void {
this.deleting = true;
let calls: Observable<void>[] = [];
this.selectedTorrents.forEach((torrentId) => {
calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
this.selectedTorrents.forEach((torrent) => {
calls.push(
this.torrentService.delete(torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
);
});
forkJoin(calls).subscribe({
complete: () => {
this.isDeleteModalActive = false;
this.deleting = false;
this.selectedTorrents = [];
},
error: (err) => {
@ -121,28 +161,28 @@ export class TorrentTableComponent implements OnInit {
public showRetryModal(): void {
this.retryError = null;
this.isRetryModalActive = true;
}
public retryCancel(): void {
this.isRetryModalActive = false;
}
public retryOk(): void {
this.retrying = true;
let calls: Observable<void>[] = [];
this.selectedTorrents.forEach((torrentId) => {
calls.push(this.torrentService.retry(torrentId));
})
this.selectedTorrents.forEach((torrent) => {
calls.push(this.torrentService.retry(torrent.torrentId));
});
forkJoin(calls).subscribe({
complete: () => {
this.isRetryModalActive = false;
this.retrying = false;
this.selectedTorrents = [];
},
error: (err) => {
@ -151,64 +191,63 @@ export class TorrentTableComponent implements OnInit {
},
});
}
public getSameValueOrNull(key: string): any {
let newValue: any = null
this.selectedTorrentsAsObjects.forEach((t, i) => {
type ObjectKey = keyof typeof t;
const myVar = key as ObjectKey;
const currentValue = t[myVar]
if (i == 0) {
newValue = currentValue
} else {
if (newValue != currentValue) {
newValue = null
}
}
})
return newValue
public updateSettingsResetAll(): void {
this.updateTorrentSettingsForm.reset();
this.updateTorrentSettingsForm.patchValue({
category: this.getSameValue('category'),
priority: this.getSameValue('priority'),
downloadRetryAttempts: this.getSameValue('downloadRetryAttempts'),
torrentRetryAttempts: this.getSameValue('torrentRetryAttempts'),
deleteOnError: this.getSameValue('deleteOnError'),
lifetime: this.getSameValue('lifetime'),
});
}
public showUpdateSettingsModal(): void {
this.selectedTorrentsAsObjects = this.torrents.filter(t => this.selectedTorrents.includes(t.torrentId))
this.updateSettingsCategory = this.getSameValueOrNull('category')
this.updateSettingsPriority = this.getSameValueOrNull('priority');
this.updateSettingsDownloadRetryAttempts = this.getSameValueOrNull('downloadRetryAttempts');
this.updateSettingsTorrentRetryAttempts = this.getSameValueOrNull('torrentRetryAttempts');
this.updateSettingsDeleteOnError = this.getSameValueOrNull('deleteOnError');
this.updateSettingsTorrentLifetime = this.getSameValueOrNull('lifetime');
this.updateSettingsResetAll();
this.isUpdateSettingsModalActive = true;
}
public updateSettingsCancel(): void {
this.isUpdateSettingsModalActive = false;
this.updateTorrentSettingsForm.reset();
}
public getUpdatedValue(original: any, key: string): string {
type ObjectKey = keyof typeof original;
const str: any = key as ObjectKey;
const fieldIsPristine = this.updateTorrentSettingsForm.controls[str].pristine;
const originalValue = original[str];
const updatedValue = this.updateTorrentSettingsForm.value[str];
if (fieldIsPristine) {
return originalValue;
}
return updatedValue;
}
public updateSettingsOk(): void {
this.updating = true;
let calls: Observable<void>[] = [];
this.selectedTorrentsAsObjects.forEach((torrent) => {
torrent.category = this.updateSettingsCategory;
torrent.priority = this.updateSettingsPriority;
torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
torrent.deleteOnError = this.updateSettingsDeleteOnError;
torrent.lifetime = this.updateSettingsTorrentLifetime;
this.selectedTorrents.forEach((torrent) => {
(torrent.category = this.getUpdatedValue(torrent, 'category')),
(torrent.priority = parseInt(this.getUpdatedValue(torrent, 'priority'))),
(torrent.downloadRetryAttempts = parseInt(this.getUpdatedValue(torrent, 'downloadRetryAttempts'))),
(torrent.torrentRetryAttempts = parseInt(this.getUpdatedValue(torrent, 'torrentRetryAttempts'))),
(torrent.deleteOnError = parseInt(this.getUpdatedValue(torrent, 'deleteOnError'))),
(torrent.lifetime = parseInt(this.getUpdatedValue(torrent, 'lifetime')));
calls.push(this.torrentService.update(torrent));
})
});
forkJoin(calls).subscribe({
complete: () => {
this.isUpdateSettingsModalActive = false;
this.updating = false;
this.updateTorrentSettingsForm.reset();
this.selectedTorrents = [];
},

View file

@ -1,28 +1,28 @@
version: '3.3'
version: "3.3"
services:
rdtclient:
image: rogerfar/rdtclient
container_name: rdtclient
# build:
# context: .
# dockerfile: Dockerfile
environment:
- PUID=1001
- PGID=1001
volumes:
- ${PWD}/data/downloads:/data/downloads
- ${PWD}/data/db:/data/db
restart: always
logging:
driver: json-file
options:
max-size: 10m
ports:
- '6500:6500'
healthcheck:
test: curl --fail http://localhost:6500 || exit 1
interval: 30s
retries: 3
start_period: 30s
timeout: 30s
rdtclient:
# image: rogerfar/rdtclient
container_name: rdtclient
build:
context: .
dockerfile: Dockerfile
environment:
- PUID=1001
- PGID=1001
volumes:
- /mnt/internal_1tb/Downloading/Downloads/rdtclient:/mnt/internal_1tb/Downloading/Downloads/rdtclient:ro
- /docker_mnt/rdtclient/data/db:/data/db
restart: always
logging:
driver: json-file
options:
max-size: 10m
ports:
- "6500:6500"
healthcheck:
test: curl --fail http://localhost:6500 || exit 1
interval: 30s
retries: 3
start_period: 30s
timeout: 30s

1172
package-lock.json generated

File diff suppressed because it is too large Load diff