General frontend improvements (#252)

This commit is contained in:
Flaminel
2025-08-01 19:45:01 +03:00
committed by GitHub
parent 8b5918d221
commit dca45585ca
6 changed files with 206 additions and 52 deletions

View File

@@ -116,7 +116,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
<small class="form-helper-text">When enabled, private torrents will not be processed</small>
</div>
</div>
@@ -129,7 +129,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
</div>
</div>

View File

@@ -166,27 +166,36 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
effect(() => {
const config = this.contentBlockerConfig();
if (config) {
// Reset form with the config values
// Handle the case where ignorePrivate is true but deletePrivate is also true
// This shouldn't happen, but if it does, correct it
const correctedConfig = { ...config };
// For Content Blocker
if (correctedConfig.ignorePrivate && correctedConfig.deletePrivate) {
correctedConfig.deletePrivate = false;
}
// Reset form with the corrected config values
this.contentBlockerForm.patchValue({
enabled: config.enabled,
useAdvancedScheduling: config.useAdvancedScheduling || false,
cronExpression: config.cronExpression,
jobSchedule: config.jobSchedule || {
enabled: correctedConfig.enabled,
useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false,
cronExpression: correctedConfig.cronExpression,
jobSchedule: correctedConfig.jobSchedule || {
every: 5,
type: ScheduleUnit.Seconds
},
ignorePrivate: config.ignorePrivate,
deletePrivate: config.deletePrivate,
deleteKnownMalware: config.deleteKnownMalware,
sonarr: config.sonarr,
radarr: config.radarr,
lidarr: config.lidarr,
readarr: config.readarr,
whisparr: config.whisparr,
ignorePrivate: correctedConfig.ignorePrivate,
deletePrivate: correctedConfig.deletePrivate,
deleteKnownMalware: correctedConfig.deleteKnownMalware,
sonarr: correctedConfig.sonarr,
radarr: correctedConfig.radarr,
lidarr: correctedConfig.lidarr,
readarr: correctedConfig.readarr,
whisparr: correctedConfig.whisparr,
});
// Update all form control states
this.updateFormControlDisabledStates(config);
this.updateFormControlDisabledStates(correctedConfig);
// Store original values for dirty checking
this.storeOriginalValues();
@@ -247,6 +256,27 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
this.updateMainControlsState(enabled);
});
}
// Add listener for ignorePrivate changes
const ignorePrivateControl = this.contentBlockerForm.get('ignorePrivate');
if (ignorePrivateControl) {
ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((ignorePrivate: boolean) => {
const deletePrivateControl = this.contentBlockerForm.get('deletePrivate');
if (ignorePrivate && deletePrivateControl) {
// If ignoring private, uncheck and disable delete private
deletePrivateControl.setValue(false);
deletePrivateControl.disable({ onlySelf: true });
} else if (!ignorePrivate && deletePrivateControl) {
// If not ignoring private, enable delete private (if main feature is enabled)
const mainEnabled = this.contentBlockerForm.get('enabled')?.value || false;
if (mainEnabled) {
deletePrivateControl.enable({ onlySelf: true });
}
}
});
}
// Listen for changes to the 'useAdvancedScheduling' control
const advancedControl = this.contentBlockerForm.get('useAdvancedScheduling');
@@ -417,8 +447,17 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
// Enable content blocker specific controls
this.contentBlockerForm.get("ignorePrivate")?.enable({ onlySelf: true });
this.contentBlockerForm.get("deletePrivate")?.enable({ onlySelf: true });
this.contentBlockerForm.get("deleteKnownMalware")?.enable({ onlySelf: true });
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.contentBlockerForm.get("ignorePrivate")?.value || false;
const deletePrivateControl = this.contentBlockerForm.get("deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable({ onlySelf: true });
} else if (deletePrivateControl) {
deletePrivateControl.disable({ onlySelf: true });
}
// Enable blocklist settings for each Arr
this.contentBlockerForm.get("sonarr.enabled")?.enable({ onlySelf: true });

View File

@@ -274,6 +274,7 @@
</div>
<small *ngIf="hasError('unlinkedTargetCategory', 'required')" class="p-error">Target category is required</small>
<small class="form-helper-text">Category to move unlinked downloads to</small>
<small class="form-helper-text">You have to create a seeding rule for this category if you want to remove the downloads</small>
</div>
</div>

View File

@@ -155,7 +155,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
<small class="form-helper-text">When enabled, private torrents will not be checked for being failed imports</small>
</div>
</div>
@@ -168,7 +168,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
</div>
</div>
@@ -264,7 +264,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
<small class="form-helper-text">When enabled, private torrents will not be checked for being stalled</small>
</div>
</div>
@@ -277,7 +277,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
</div>
</div>
</p-accordion-content>
@@ -383,7 +383,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
<small class="form-helper-text">When enabled, private torrents will not be checked for being slow</small>
</div>
</div>
@@ -396,7 +396,7 @@
</label>
<div class="field-input">
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
</div>
</div>
@@ -413,6 +413,7 @@
[min]="0"
placeholder="Enter minimum speed"
helpText="Minimum speed threshold for slow downloads (e.g., 100KB/s)"
type="speed"
>
</app-byte-size-input>
</div>
@@ -454,6 +455,7 @@
[min]="0"
placeholder="Enter size threshold"
helpText="Downloads will be ignored if size exceeds, e.g., 25 GB"
type="size"
>
</app-byte-size-input>
</div>

View File

@@ -176,25 +176,40 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
effect(() => {
const config = this.queueCleanerConfig();
if (config) {
// Save original cron expression
const cronExpression = config.cronExpression;
// Handle the case where ignorePrivate is true but deletePrivate is also true
// This shouldn't happen, but if it does, correct it
const correctedConfig = { ...config };
// Reset form with the config values
// For Queue Cleaner (apply to all sections)
if (correctedConfig.failedImport?.ignorePrivate && correctedConfig.failedImport?.deletePrivate) {
correctedConfig.failedImport.deletePrivate = false;
}
if (correctedConfig.stalled?.ignorePrivate && correctedConfig.stalled?.deletePrivate) {
correctedConfig.stalled.deletePrivate = false;
}
if (correctedConfig.slow?.ignorePrivate && correctedConfig.slow?.deletePrivate) {
correctedConfig.slow.deletePrivate = false;
}
// Save original cron expression
const cronExpression = correctedConfig.cronExpression;
// Reset form with the corrected config values
this.queueCleanerForm.patchValue({
enabled: config.enabled,
useAdvancedScheduling: config.useAdvancedScheduling || false,
cronExpression: config.cronExpression,
jobSchedule: config.jobSchedule || {
enabled: correctedConfig.enabled,
useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false,
cronExpression: correctedConfig.cronExpression,
jobSchedule: correctedConfig.jobSchedule || {
every: 5,
type: ScheduleUnit.Minutes
},
failedImport: config.failedImport,
stalled: config.stalled,
slow: config.slow,
failedImport: correctedConfig.failedImport,
stalled: correctedConfig.stalled,
slow: correctedConfig.slow,
});
// Then update all other dependent form control states
this.updateFormControlDisabledStates(config);
this.updateFormControlDisabledStates(correctedConfig);
// Store original values for dirty checking
this.storeOriginalValues();
@@ -255,6 +270,30 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
this.updateMainControlsState(enabled);
});
}
// Add listeners for ignorePrivate changes in each section
['failedImport', 'stalled', 'slow'].forEach(section => {
const ignorePrivateControl = this.queueCleanerForm.get(`${section}.ignorePrivate`);
if (ignorePrivateControl) {
ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((ignorePrivate: boolean) => {
const deletePrivateControl = this.queueCleanerForm.get(`${section}.deletePrivate`);
if (ignorePrivate && deletePrivateControl) {
// If ignoring private, uncheck and disable delete private
deletePrivateControl.setValue(false);
deletePrivateControl.disable({ onlySelf: true });
} else if (!ignorePrivate && deletePrivateControl) {
// If not ignoring private, enable delete private (if parent section is enabled)
const sectionEnabled = this.isSectionEnabled(section);
if (sectionEnabled) {
deletePrivateControl.enable({ onlySelf: true });
}
}
});
}
});
// Listen for changes to the 'useAdvancedScheduling' control
const advancedControl = this.queueCleanerForm.get('useAdvancedScheduling');
@@ -349,6 +388,17 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
this.originalFormValues = JSON.parse(JSON.stringify(this.queueCleanerForm.getRawValue()));
this.hasActualChanges = false;
}
// Helper method to check if a section is enabled
private isSectionEnabled(section: string): boolean {
const mainEnabled = this.queueCleanerForm.get('enabled')?.value || false;
if (!mainEnabled) return false;
const maxStrikesControl = this.queueCleanerForm.get(`${section}.maxStrikes`);
const maxStrikes = maxStrikesControl?.value || 0;
return maxStrikes >= 3;
}
// Check if the current form values are different from the original values
private formValuesChanged(): boolean {
@@ -463,8 +513,17 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
if (enable) {
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.enable(options);
this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.enable(options);
this.queueCleanerForm.get("failedImport")?.get("ignoredPatterns")?.enable(options);
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.queueCleanerForm.get("failedImport.ignorePrivate")?.value || false;
const deletePrivateControl = this.queueCleanerForm.get("failedImport.deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable(options);
} else if (deletePrivateControl) {
deletePrivateControl.disable(options);
}
} else {
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.disable(options);
this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.disable(options);
@@ -482,7 +541,16 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
if (enable) {
this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.enable(options);
this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.enable(options);
this.queueCleanerForm.get("stalled")?.get("deletePrivate")?.enable(options);
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.queueCleanerForm.get("stalled.ignorePrivate")?.value || false;
const deletePrivateControl = this.queueCleanerForm.get("stalled.deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable(options);
} else if (deletePrivateControl) {
deletePrivateControl.disable(options);
}
} else {
this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.disable(options);
this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.disable(options);
@@ -500,10 +568,19 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
if (enable) {
this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.enable(options);
this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.enable(options);
this.queueCleanerForm.get("slow")?.get("deletePrivate")?.enable(options);
this.queueCleanerForm.get("slow")?.get("minSpeed")?.enable(options);
this.queueCleanerForm.get("slow")?.get("maxTime")?.enable(options);
this.queueCleanerForm.get("slow")?.get("ignoreAboveSize")?.enable(options);
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.queueCleanerForm.get("slow.ignorePrivate")?.value || false;
const deletePrivateControl = this.queueCleanerForm.get("slow.deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable(options);
} else if (deletePrivateControl) {
deletePrivateControl.disable(options);
}
} else {
this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.disable(options);
this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.disable(options);

View File

@@ -4,7 +4,9 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModu
import { InputNumberModule } from 'primeng/inputnumber';
import { SelectButtonModule } from 'primeng/selectbutton';
type ByteSizeUnit = 'KB' | 'MB' | 'GB' | 'TB';
export type ByteSizeInputType = 'speed' | 'size';
type ByteSizeUnit = 'KB' | 'MB' | 'GB';
@Component({
selector: 'app-byte-size-input',
@@ -26,31 +28,57 @@ export class ByteSizeInputComponent implements ControlValueAccessor {
@Input() disabled: boolean = false;
@Input() placeholder: string = 'Enter size';
@Input() helpText: string = '';
@Input() type: ByteSizeInputType = 'size';
// Value in the selected unit
value = signal<number | null>(null);
// The selected unit
unit = signal<ByteSizeUnit>('MB');
// Available units
unitOptions = [
{ label: 'KB', value: 'KB' },
{ label: 'MB', value: 'MB' },
{ label: 'GB', value: 'GB' },
{ label: 'TB', value: 'TB' }
];
// Available units, computed based on type
get unitOptions() {
switch (this.type) {
case 'speed':
return [
{ label: 'KB/s', value: 'KB' },
{ label: 'MB/s', value: 'MB' }
];
case 'size':
default:
return [
{ label: 'MB', value: 'MB' },
{ label: 'GB', value: 'GB' }
];
}
}
// Get default unit based on type
private getDefaultUnit(): ByteSizeUnit {
switch (this.type) {
case 'speed':
return 'KB';
case 'size':
default:
return 'MB';
}
}
// ControlValueAccessor interface methods
private onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {};
ngOnInit(): void {
this.unit.set(this.getDefaultUnit());
}
/**
* Parse the string value in format '100MB', '1.5GB', etc.
*/
writeValue(value: string): void {
if (!value) {
this.value.set(null);
this.unit.set(this.getDefaultUnit());
return;
}
@@ -62,15 +90,24 @@ export class ByteSizeInputComponent implements ControlValueAccessor {
if (match) {
const numValue = parseFloat(match[1]);
const unit = match[2].toUpperCase() as ByteSizeUnit;
this.value.set(numValue);
this.unit.set(unit);
// Validate unit is allowed for this type
const allowedUnits = this.unitOptions.map(opt => opt.value);
if (allowedUnits.includes(unit)) {
this.value.set(numValue);
this.unit.set(unit);
} else {
// If unit not allowed, use default
this.value.set(numValue);
this.unit.set(this.getDefaultUnit());
}
} else {
this.value.set(null);
this.unit.set(this.getDefaultUnit());
}
} catch (e) {
console.error('Error parsing byte size value:', value, e);
this.value.set(null);
this.unit.set(this.getDefaultUnit());
}
}
@@ -91,12 +128,10 @@ export class ByteSizeInputComponent implements ControlValueAccessor {
*/
updateValue(): void {
this.onTouched();
if (this.value() === null) {
this.onChange('');
return;
}
// Format as "100MB", "1.5GB", etc.
const formattedValue = `${this.value()}${this.unit()}`;
this.onChange(formattedValue);