mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-03-06 07:16:10 -05:00
added notifications endpoint
This commit is contained in:
@@ -10,6 +10,7 @@ using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Infrastructure.Services.Interfaces;
|
||||
using Mapster;
|
||||
@@ -285,15 +286,90 @@ public class ConfigurationController : ControllerBase
|
||||
[HttpGet("notifications")]
|
||||
public async Task<IActionResult> GetNotificationsConfig()
|
||||
{
|
||||
// TODO get all notification configs
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// var config = await _dataContext.NotificationsConfigs
|
||||
// .AsNoTracking()
|
||||
// .FirstAsync();
|
||||
// return Ok(config);
|
||||
return null; // Placeholder for future implementation
|
||||
var notifiarrConfig = await _dataContext.NotifiarrConfigs
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var appriseConfig = await _dataContext.AppriseConfigs
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// Return in the expected format with wrapper object
|
||||
var config = new
|
||||
{
|
||||
notifiarr = notifiarrConfig,
|
||||
apprise = appriseConfig
|
||||
};
|
||||
return Ok(config);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateNotificationConfigDto
|
||||
{
|
||||
public NotifiarrConfig? Notifiarr { get; set; }
|
||||
public AppriseConfig? Apprise { get; set; }
|
||||
}
|
||||
|
||||
[HttpPut("notifications")]
|
||||
public async Task<IActionResult> UpdateNotificationsConfig([FromBody] UpdateNotificationConfigDto newConfig)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Update Notifiarr config if provided
|
||||
if (newConfig.Notifiarr != null)
|
||||
{
|
||||
var existingNotifiarr = await _dataContext.NotifiarrConfigs.FirstOrDefaultAsync();
|
||||
if (existingNotifiarr != null)
|
||||
{
|
||||
// Apply updates from DTO, excluding the ID property to avoid EF key modification error
|
||||
var config = new TypeAdapterConfig();
|
||||
config.NewConfig<NotifiarrConfig, NotifiarrConfig>()
|
||||
.Ignore(dest => dest.Id);
|
||||
|
||||
newConfig.Notifiarr.Adapt(existingNotifiarr, config);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dataContext.NotifiarrConfigs.Add(newConfig.Notifiarr);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Apprise config if provided
|
||||
if (newConfig.Apprise != null)
|
||||
{
|
||||
var existingApprise = await _dataContext.AppriseConfigs.FirstOrDefaultAsync();
|
||||
if (existingApprise != null)
|
||||
{
|
||||
// Apply updates from DTO, excluding the ID property to avoid EF key modification error
|
||||
var config = new TypeAdapterConfig();
|
||||
config.NewConfig<AppriseConfig, AppriseConfig>()
|
||||
.Ignore(dest => dest.Id);
|
||||
|
||||
newConfig.Apprise.Adapt(existingApprise, config);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dataContext.AppriseConfigs.Add(newConfig.Apprise);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the configuration
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
return Ok(new { Message = "Notifications configuration updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to save Notifications configuration");
|
||||
return StatusCode(500, "Failed to save Notifications configuration");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ export class DownloadCleanerConfigStore {
|
||||
readonly error = this._error.asReadonly();
|
||||
|
||||
// API endpoints
|
||||
private apiUrl = `${environment.apiUrl}/api/Configuration`;
|
||||
private apiUrl = `${environment.apiUrl}/api/configuration`;
|
||||
|
||||
constructor() {
|
||||
// Load config on initialization
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { NotificationsConfig } from '../../shared/models/notifications-config.model';
|
||||
import { catchError, finalize, of, tap } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationConfigStore {
|
||||
// API endpoints
|
||||
private readonly baseUrl = environment.apiUrl;
|
||||
|
||||
// State signals
|
||||
private _config = signal<NotificationsConfig | null>(null);
|
||||
private _loading = signal<boolean>(false);
|
||||
private _saving = signal<boolean>(false);
|
||||
private _error = signal<string | null>(null);
|
||||
|
||||
// Public selectors
|
||||
readonly config = this._config.asReadonly();
|
||||
readonly loading = this._loading.asReadonly();
|
||||
readonly saving = this._saving.asReadonly();
|
||||
readonly error = this._error.asReadonly();
|
||||
|
||||
// Inject HttpClient
|
||||
private http = inject(HttpClient);
|
||||
|
||||
constructor() {
|
||||
// Load the configuration when the store is created
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notification configuration from the API
|
||||
*/
|
||||
loadConfig(): void {
|
||||
if (this._loading()) return;
|
||||
|
||||
this._loading.set(true);
|
||||
this._error.set(null);
|
||||
|
||||
this.http.get<NotificationsConfig>(`${this.baseUrl}/api/configuration/notifications`)
|
||||
.pipe(
|
||||
tap((config) => {
|
||||
this._config.set(config);
|
||||
this._error.set(null);
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
console.error('Error loading notification configuration:', error);
|
||||
this._error.set(error.message || 'Failed to load notification configuration');
|
||||
return of(null);
|
||||
}),
|
||||
finalize(() => {
|
||||
this._loading.set(false);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notification configuration to the API
|
||||
*/
|
||||
saveConfig(config: NotificationsConfig): void {
|
||||
if (this._saving()) return;
|
||||
|
||||
this._saving.set(true);
|
||||
this._error.set(null);
|
||||
|
||||
this.http.put<any>(`${this.baseUrl}/api/configuration/notifications`, config)
|
||||
.pipe(
|
||||
tap(() => {
|
||||
// Don't set config - let the form stay as-is with string enum values
|
||||
this._error.set(null);
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
console.error('Error saving notification configuration:', error);
|
||||
this._error.set(error.message || 'Failed to save notification configuration');
|
||||
return of(null);
|
||||
}),
|
||||
finalize(() => {
|
||||
this._saving.set(false);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- Toast notifications handled by central toast container -->
|
||||
|
||||
<p-card styleClass="settings-card h-full">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
|
||||
<div class="header-title-container">
|
||||
<h2 class="card-title m-0">Notification Configuration</h2>
|
||||
<span class="card-subtitle">Configure notification settings for Notifiarr and Apprise</span>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-bell text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- Loading/Error Component -->
|
||||
<app-loading-error-state
|
||||
*ngIf="notificationLoading() || notificationError()"
|
||||
[loading]="notificationLoading()"
|
||||
[error]="notificationError()"
|
||||
loadingMessage="Loading notification settings..."
|
||||
errorMessage="Could not connect to server"
|
||||
></app-loading-error-state>
|
||||
|
||||
<!-- Settings Form -->
|
||||
<form *ngIf="!notificationLoading() && !notificationError()" [formGroup]="notificationForm" class="p-fluid">
|
||||
|
||||
<!-- Notifiarr Configuration Section -->
|
||||
<div class="mb-4">
|
||||
<h3 class="section-title">Notifiarr Configuration</h3>
|
||||
<div formGroupName="notifiarr">
|
||||
<!-- API Key -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">API Key</label>
|
||||
<div class="field-input">
|
||||
<p-inputText formControlName="apiKey" inputId="notifiarrApiKey" placeholder="Enter Notifiarr API key"></p-inputText>
|
||||
<small class="form-helper-text">Your Notifiarr API key for authentication</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel ID -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Channel ID</label>
|
||||
<div class="field-input">
|
||||
<p-inputText formControlName="channelId" inputId="notifiarrChannelId" placeholder="Enter channel ID"></p-inputText>
|
||||
<small class="form-helper-text">The channel ID where notifications will be sent</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Triggers -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Event Triggers</label>
|
||||
<div class="field-input">
|
||||
<div class="flex flex-column gap-2">
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onFailedImportStrike" [binary]="true" inputId="notifiarrFailedImport"></p-checkbox>
|
||||
<label for="notifiarrFailedImport" class="ml-2">Failed Import Strike</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onStalledStrike" [binary]="true" inputId="notifiarrStalled"></p-checkbox>
|
||||
<label for="notifiarrStalled" class="ml-2">Stalled Strike</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onSlowStrike" [binary]="true" inputId="notifiarrSlow"></p-checkbox>
|
||||
<label for="notifiarrSlow" class="ml-2">Slow Strike</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onQueueItemDeleted" [binary]="true" inputId="notifiarrDeleted"></p-checkbox>
|
||||
<label for="notifiarrDeleted" class="ml-2">Queue Item Deleted</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onDownloadCleaned" [binary]="true" inputId="notifiarrCleaned"></p-checkbox>
|
||||
<label for="notifiarrCleaned" class="ml-2">Download Cleaned</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onCategoryChanged" [binary]="true" inputId="notifiarrCategory"></p-checkbox>
|
||||
<label for="notifiarrCategory" class="ml-2">Category Changed</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-helper-text">Select which events should trigger Notifiarr notifications</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apprise Configuration Section -->
|
||||
<div class="mb-4">
|
||||
<h3 class="section-title">Apprise Configuration</h3>
|
||||
<div formGroupName="apprise">
|
||||
<!-- URL -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">URL</label>
|
||||
<div class="field-input">
|
||||
<p-inputText formControlName="url" inputId="appriseUrl" placeholder="Enter Apprise URL"></p-inputText>
|
||||
<small class="form-helper-text">The Apprise server URL</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Key</label>
|
||||
<div class="field-input">
|
||||
<p-inputText formControlName="key" inputId="appriseKey" placeholder="Enter key"></p-inputText>
|
||||
<small class="form-helper-text">The key for authentication</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Triggers -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Event Triggers</label>
|
||||
<div class="field-input">
|
||||
<div class="flex flex-column gap-2">
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onFailedImportStrike" [binary]="true" inputId="appriseFailedImport"></p-checkbox>
|
||||
<label for="appriseFailedImport" class="ml-2">Failed Import Strike</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onStalledStrike" [binary]="true" inputId="appriseStalled"></p-checkbox>
|
||||
<label for="appriseStalled" class="ml-2">Stalled Strike</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onSlowStrike" [binary]="true" inputId="appriseSlow"></p-checkbox>
|
||||
<label for="appriseSlow" class="ml-2">Slow Strike</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onQueueItemDeleted" [binary]="true" inputId="appriseDeleted"></p-checkbox>
|
||||
<label for="appriseDeleted" class="ml-2">Queue Item Deleted</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onDownloadCleaned" [binary]="true" inputId="appriseCleaned"></p-checkbox>
|
||||
<label for="appriseCleaned" class="ml-2">Download Cleaned</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-checkbox formControlName="onCategoryChanged" [binary]="true" inputId="appriseCategory"></p-checkbox>
|
||||
<label for="appriseCategory" class="ml-2">Category Changed</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-helper-text">Select which events should trigger Apprise notifications</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="card-footer mt-3">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary"
|
||||
[disabled]="(!notificationForm.dirty || !hasActualChanges) || notificationForm.invalid || notificationSaving()"
|
||||
[loading]="notificationSaving()"
|
||||
(click)="saveNotificationConfig()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Reset"
|
||||
icon="pi pi-refresh"
|
||||
class="p-button-secondary p-button-outlined ml-2"
|
||||
(click)="resetNotificationConfig()"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</p-card>
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Notification Settings Styles */
|
||||
|
||||
@import '../styles/settings-shared.scss';
|
||||
|
||||
.section-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import { Component, EventEmitter, OnDestroy, Output, effect, inject } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { NotificationConfigStore } from "./notification-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { NotificationsConfig } from "../../shared/models/notifications-config.model";
|
||||
import { NotifiarrConfig } from "../../shared/models/notifiarr-config.model";
|
||||
import { AppriseConfig } from "../../shared/models/apprise-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
import { InputTextModule } from "primeng/inputtext";
|
||||
import { CheckboxModule } from "primeng/checkbox";
|
||||
import { ButtonModule } from "primeng/button";
|
||||
import { ToastModule } from "primeng/toast";
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-notification-settings",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
CardModule,
|
||||
InputTextModule,
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
ToastModule,
|
||||
LoadingErrorStateComponent,
|
||||
],
|
||||
providers: [NotificationConfigStore],
|
||||
templateUrl: "./notification-settings.component.html",
|
||||
styleUrls: ["./notification-settings.component.scss"],
|
||||
})
|
||||
export class NotificationSettingsComponent implements OnDestroy, CanComponentDeactivate {
|
||||
@Output() saved = new EventEmitter<void>();
|
||||
@Output() error = new EventEmitter<string>();
|
||||
|
||||
// Notification Configuration Form
|
||||
notificationForm: FormGroup;
|
||||
|
||||
// Original form values for tracking changes
|
||||
private originalFormValues: any;
|
||||
|
||||
// Track whether the form has actual changes compared to original values
|
||||
hasActualChanges = false;
|
||||
|
||||
// Inject the necessary services
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private notificationService = inject(NotificationService);
|
||||
private notificationConfigStore = inject(NotificationConfigStore);
|
||||
|
||||
// Signals from the store
|
||||
readonly notificationConfig = this.notificationConfigStore.config;
|
||||
readonly notificationLoading = this.notificationConfigStore.loading;
|
||||
readonly notificationSaving = this.notificationConfigStore.saving;
|
||||
readonly notificationError = this.notificationConfigStore.error;
|
||||
|
||||
// Subject for unsubscribing from observables when component is destroyed
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
*/
|
||||
canDeactivate(): boolean {
|
||||
return !this.notificationForm.dirty;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Initialize the notification settings form
|
||||
this.notificationForm = this.formBuilder.group({
|
||||
// Notifiarr configuration
|
||||
notifiarr: this.formBuilder.group({
|
||||
apiKey: [''],
|
||||
channelId: [''],
|
||||
onFailedImportStrike: [false],
|
||||
onStalledStrike: [false],
|
||||
onSlowStrike: [false],
|
||||
onQueueItemDeleted: [false],
|
||||
onDownloadCleaned: [false],
|
||||
onCategoryChanged: [false],
|
||||
}),
|
||||
// Apprise configuration
|
||||
apprise: this.formBuilder.group({
|
||||
url: [''],
|
||||
key: [''],
|
||||
onFailedImportStrike: [false],
|
||||
onStalledStrike: [false],
|
||||
onSlowStrike: [false],
|
||||
onQueueItemDeleted: [false],
|
||||
onDownloadCleaned: [false],
|
||||
onCategoryChanged: [false],
|
||||
}),
|
||||
});
|
||||
|
||||
// Setup effect to react to config changes
|
||||
effect(() => {
|
||||
const config = this.notificationConfig();
|
||||
if (config) {
|
||||
// Map the server response to form values
|
||||
const formValue = {
|
||||
notifiarr: config.notifiarr || {
|
||||
apiKey: '',
|
||||
channelId: '',
|
||||
onFailedImportStrike: false,
|
||||
onStalledStrike: false,
|
||||
onSlowStrike: false,
|
||||
onQueueItemDeleted: false,
|
||||
onDownloadCleaned: false,
|
||||
onCategoryChanged: false,
|
||||
},
|
||||
apprise: config.apprise || {
|
||||
url: '',
|
||||
key: '',
|
||||
onFailedImportStrike: false,
|
||||
onStalledStrike: false,
|
||||
onSlowStrike: false,
|
||||
onQueueItemDeleted: false,
|
||||
onDownloadCleaned: false,
|
||||
onCategoryChanged: false,
|
||||
},
|
||||
};
|
||||
|
||||
this.notificationForm.patchValue(formValue);
|
||||
this.storeOriginalValues();
|
||||
this.notificationForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Track form changes for dirty state
|
||||
this.notificationForm.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.hasActualChanges = this.formValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to react to error changes
|
||||
effect(() => {
|
||||
const errorMessage = this.notificationError();
|
||||
if (errorMessage) {
|
||||
// Only emit the error for parent components
|
||||
this.error.emit(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up subscriptions when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current form values are different from the original values
|
||||
*/
|
||||
private formValuesChanged(): boolean {
|
||||
return !this.isEqual(this.notificationForm.value, this.originalFormValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare two objects for equality
|
||||
*/
|
||||
private isEqual(obj1: any, obj2: any): boolean {
|
||||
if (obj1 === obj2) return true;
|
||||
if (obj1 === null || obj2 === null) return false;
|
||||
if (obj1 === undefined || obj2 === undefined) return false;
|
||||
|
||||
if (typeof obj1 !== 'object' && typeof obj2 !== 'object') {
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
if (obj1.length !== obj2.length) return false;
|
||||
for (let i = 0; i < obj1.length; i++) {
|
||||
if (!this.isEqual(obj1[i], obj2[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!this.isEqual(obj1[key], obj2[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store original form values for dirty checking
|
||||
*/
|
||||
private storeOriginalValues(): void {
|
||||
this.originalFormValues = JSON.parse(JSON.stringify(this.notificationForm.value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the notification configuration
|
||||
*/
|
||||
saveNotificationConfig(): void {
|
||||
if (this.notificationForm.invalid) {
|
||||
this.markFormGroupTouched(this.notificationForm);
|
||||
this.notificationService.showValidationError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasActualChanges) {
|
||||
this.notificationService.showSuccess('No changes detected');
|
||||
return;
|
||||
}
|
||||
|
||||
const formValues = this.notificationForm.value;
|
||||
|
||||
const config: NotificationsConfig = {
|
||||
notifiarr: formValues.notifiarr,
|
||||
apprise: formValues.apprise,
|
||||
};
|
||||
|
||||
// Save the configuration
|
||||
this.notificationConfigStore.saveConfig(config);
|
||||
|
||||
// Setup a one-time check to mark form as pristine after successful save
|
||||
const checkSaveCompletion = () => {
|
||||
const loading = this.notificationSaving();
|
||||
const error = this.notificationError();
|
||||
|
||||
if (!loading && !error) {
|
||||
// Mark form as pristine after successful save
|
||||
this.notificationForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
|
||||
// Emit saved event
|
||||
this.saved.emit();
|
||||
// Show success message
|
||||
this.notificationService.showSuccess('Notification configuration saved successfully!');
|
||||
} else if (!loading && error) {
|
||||
// If there's an error, we can stop checking
|
||||
} else {
|
||||
// If still loading, check again in a moment
|
||||
setTimeout(checkSaveCompletion, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// Start checking for save completion
|
||||
checkSaveCompletion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the notification configuration form to default values
|
||||
*/
|
||||
resetNotificationConfig(): void {
|
||||
this.notificationForm.reset({
|
||||
notifiarr: {
|
||||
apiKey: '',
|
||||
channelId: '',
|
||||
onFailedImportStrike: false,
|
||||
onStalledStrike: false,
|
||||
onSlowStrike: false,
|
||||
onQueueItemDeleted: false,
|
||||
onDownloadCleaned: false,
|
||||
onCategoryChanged: false,
|
||||
},
|
||||
apprise: {
|
||||
url: '',
|
||||
key: '',
|
||||
onFailedImportStrike: false,
|
||||
onStalledStrike: false,
|
||||
onSlowStrike: false,
|
||||
onQueueItemDeleted: false,
|
||||
onDownloadCleaned: false,
|
||||
onCategoryChanged: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if this reset actually changes anything compared to the original state
|
||||
const hasChangesAfterReset = this.formValuesChanged();
|
||||
|
||||
if (hasChangesAfterReset) {
|
||||
// Only mark as dirty if the reset actually changes something
|
||||
this.notificationForm.markAsDirty();
|
||||
this.hasActualChanges = true;
|
||||
} else {
|
||||
// If reset brings us back to original state, mark as pristine
|
||||
this.notificationForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all controls in a form group as touched
|
||||
*/
|
||||
private markFormGroupTouched(formGroup: FormGroup): void {
|
||||
Object.values(formGroup.controls).forEach((control) => {
|
||||
control.markAsTouched();
|
||||
|
||||
if ((control as any).controls) {
|
||||
this.markFormGroupTouched(control as FormGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form control has an error after it's been touched
|
||||
*/
|
||||
hasError(controlName: string, errorName: string): boolean {
|
||||
const control = this.notificationForm.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a nested form control has an error after it's been touched
|
||||
*/
|
||||
hasNestedError(groupName: string, controlName: string, errorName: string): boolean {
|
||||
const control = this.notificationForm.get(`${groupName}.${controlName}`);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,9 @@
|
||||
<div class="mb-4">
|
||||
<app-download-cleaner-settings></app-download-cleaner-settings>
|
||||
</div>
|
||||
|
||||
<!-- Notification Settings Component -->
|
||||
<div class="mb-4">
|
||||
<app-notification-settings></app-notification-settings>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SonarrSettingsComponent } from '../sonarr/sonarr-settings.component';
|
||||
import { RadarrSettingsComponent } from "../radarr/radarr-settings.component";
|
||||
import { LidarrSettingsComponent } from "../lidarr/lidarr-settings.component";
|
||||
import { DownloadClientSettingsComponent } from "../download-client/download-client-settings.component";
|
||||
import { NotificationSettingsComponent } from "../notification-settings/notification-settings.component";
|
||||
|
||||
// Define interfaces for settings page
|
||||
interface LogLevel {
|
||||
@@ -50,7 +51,8 @@ interface Category {
|
||||
SonarrSettingsComponent,
|
||||
RadarrSettingsComponent,
|
||||
LidarrSettingsComponent,
|
||||
DownloadClientSettingsComponent
|
||||
DownloadClientSettingsComponent,
|
||||
NotificationSettingsComponent
|
||||
],
|
||||
providers: [MessageService, ConfirmationService],
|
||||
templateUrl: './settings-page.component.html',
|
||||
@@ -89,6 +91,7 @@ export class SettingsPageComponent implements OnInit, CanComponentDeactivate {
|
||||
@ViewChild(GeneralSettingsComponent) generalSettings!: GeneralSettingsComponent;
|
||||
@ViewChild(DownloadCleanerSettingsComponent) downloadCleanerSettings!: DownloadCleanerSettingsComponent;
|
||||
@ViewChild(SonarrSettingsComponent) sonarrSettings!: SonarrSettingsComponent;
|
||||
@ViewChild(NotificationSettingsComponent) notificationSettings!: NotificationSettingsComponent;
|
||||
|
||||
ngOnInit(): void {
|
||||
// Future implementation for other settings sections
|
||||
@@ -119,6 +122,11 @@ export class SettingsPageComponent implements OnInit, CanComponentDeactivate {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if notification settings has unsaved changes
|
||||
if (this.notificationSettings?.canDeactivate() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
6
code/UI/src/app/shared/models/apprise-config.model.ts
Normal file
6
code/UI/src/app/shared/models/apprise-config.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { NotificationConfig } from './notification-config.model';
|
||||
|
||||
export interface AppriseConfig extends NotificationConfig {
|
||||
url?: string;
|
||||
key?: string;
|
||||
}
|
||||
6
code/UI/src/app/shared/models/notifiarr-config.model.ts
Normal file
6
code/UI/src/app/shared/models/notifiarr-config.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { NotificationConfig } from './notification-config.model';
|
||||
|
||||
export interface NotifiarrConfig extends NotificationConfig {
|
||||
apiKey?: string;
|
||||
channelId?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface NotificationConfig {
|
||||
id?: string;
|
||||
onFailedImportStrike: boolean;
|
||||
onStalledStrike: boolean;
|
||||
onSlowStrike: boolean;
|
||||
onQueueItemDeleted: boolean;
|
||||
onDownloadCleaned: boolean;
|
||||
onCategoryChanged: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NotifiarrConfig } from './notifiarr-config.model';
|
||||
import { AppriseConfig } from './apprise-config.model';
|
||||
|
||||
export interface NotificationsConfig {
|
||||
notifiarr?: NotifiarrConfig;
|
||||
apprise?: AppriseConfig;
|
||||
}
|
||||
Reference in New Issue
Block a user