mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-13 01:07:22 -04:00
General frontend improvements (#252)
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user