mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-26 06:39:45 -05:00
try fix sonarr
This commit is contained in:
@@ -133,10 +133,10 @@ export class ConfigurationService {
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Update Sonarr configuration
|
||||
* Update Sonarr configuration (global settings only)
|
||||
*/
|
||||
updateSonarrConfig(config: SonarrConfig): Observable<SonarrConfig> {
|
||||
return this.http.put<SonarrConfig>(`${this.apiUrl}/api/configuration/sonarr`, config).pipe(
|
||||
updateSonarrConfig(config: {enabled: boolean, failedImportMaxStrikes: number}): Observable<any> {
|
||||
return this.http.put<any>(`${this.apiUrl}/api/configuration/sonarr`, config).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error updating Sonarr config:", error);
|
||||
return throwError(() => new Error(error.error?.error || "Failed to update Sonarr configuration"));
|
||||
|
||||
@@ -49,18 +49,22 @@ export class SonarrConfigStore extends signalStore(
|
||||
),
|
||||
|
||||
/**
|
||||
* Save the Sonarr configuration
|
||||
* Save the Sonarr global configuration
|
||||
*/
|
||||
saveConfig: rxMethod<SonarrConfig>(
|
||||
(config$: Observable<SonarrConfig>) => config$.pipe(
|
||||
saveConfig: rxMethod<{enabled: boolean, failedImportMaxStrikes: number}>(
|
||||
(globalConfig$: Observable<{enabled: boolean, failedImportMaxStrikes: number}>) => globalConfig$.pipe(
|
||||
tap(() => patchState(store, { saving: true, error: null })),
|
||||
switchMap(config => configService.updateSonarrConfig(config).pipe(
|
||||
switchMap(globalConfig => configService.updateSonarrConfig(globalConfig).pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
patchState(store, {
|
||||
config,
|
||||
saving: false
|
||||
});
|
||||
const currentConfig = store.config();
|
||||
if (currentConfig) {
|
||||
// Update the local config with the new global settings
|
||||
patchState(store, {
|
||||
config: { ...currentConfig, ...globalConfig },
|
||||
saving: false
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
|
||||
@@ -1,129 +1,229 @@
|
||||
<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">Sonarr Configuration</h2>
|
||||
<span class="card-subtitle">Configure Sonarr integration settings</span>
|
||||
<div class="sonarr-settings-container">
|
||||
<!-- Loading/Error State Component -->
|
||||
<app-loading-error-state
|
||||
*ngIf="sonarrLoading() || sonarrError()"
|
||||
[loading]="sonarrLoading()"
|
||||
[error]="sonarrError()"
|
||||
loadingMessage="Loading settings..."
|
||||
errorMessage="Could not connect to server"
|
||||
></app-loading-error-state>
|
||||
|
||||
<!-- Content - only shown when not loading and no error -->
|
||||
<div *ngIf="!sonarrLoading() && !sonarrError()" class="cards-container">
|
||||
|
||||
<!-- Global Configuration Card -->
|
||||
<p-card styleClass="settings-card">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="card-header">
|
||||
<div class="header-title-container">
|
||||
<h3 class="card-title">Global Settings</h3>
|
||||
<span class="card-subtitle">Configure general Sonarr integration settings</span>
|
||||
</div>
|
||||
<i class="pi pi-cog text-xl"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<form [formGroup]="globalForm" class="p-fluid">
|
||||
<div class="field-row">
|
||||
<label class="field-label">Enable Sonarr Integration</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="enabled" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, Sonarr API integration will be used</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Failed Import Max Strikes</label>
|
||||
<div>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="failedImportMaxStrikes"
|
||||
[min]="-1"
|
||||
[showButtons]="true"
|
||||
buttonLayout="horizontal"
|
||||
incrementButtonIcon="pi pi-plus"
|
||||
decrementButtonIcon="pi pi-minus"
|
||||
[disabled]="!globalForm.get('enabled')?.value"
|
||||
></p-inputNumber>
|
||||
</div>
|
||||
<small class="form-helper-text">Maximum number of strikes before removing a failed import (-1 to use global setting; 0 to disable)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="card-footer mt-3">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save Global Settings"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary"
|
||||
[disabled]="!globalForm.dirty || !hasGlobalChanges || globalForm.invalid || sonarrSaving()"
|
||||
[loading]="sonarrSaving()"
|
||||
(click)="saveGlobalConfig()"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
</p-card>
|
||||
|
||||
<!-- Instance Management Card -->
|
||||
<p-card styleClass="settings-card">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="card-header">
|
||||
<div class="header-title-container">
|
||||
<h3 class="card-title">Sonarr Instances</h3>
|
||||
<span class="card-subtitle">Manage multiple Sonarr server instances</span>
|
||||
</div>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-plus"
|
||||
label="Add Instance"
|
||||
class="p-button-outlined"
|
||||
[disabled]="instanceManagementDisabled"
|
||||
(click)="openAddInstanceModal()"
|
||||
></button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- Disabled state overlay -->
|
||||
<div *ngIf="instanceManagementDisabled" class="disabled-overlay">
|
||||
<div class="disabled-message">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<p>Enable Sonarr integration to manage instances</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-cog text-xl"></i>
|
||||
|
||||
<!-- Empty state when no instances -->
|
||||
<div *ngIf="instances.length === 0 && !instanceManagementDisabled" class="empty-instances-message p-3 text-center">
|
||||
<i class="pi pi-inbox empty-icon"></i>
|
||||
<p>No Sonarr instances configured</p>
|
||||
<small>Add an instance to start using Sonarr integration</small>
|
||||
</div>
|
||||
|
||||
<!-- Instances List -->
|
||||
<div *ngIf="instances.length > 0" class="instances-list">
|
||||
<div *ngFor="let instance of instances" class="instance-item">
|
||||
<div class="instance-header">
|
||||
<div class="instance-title">
|
||||
<i class="pi pi-server instance-icon"></i>
|
||||
<span class="instance-name">{{ instance.name }}</span>
|
||||
</div>
|
||||
<div class="instance-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-text p-button-sm"
|
||||
[disabled]="instanceManagementDisabled"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
pTooltip="Edit instance"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-text p-button-sm p-button-danger"
|
||||
[disabled]="instanceManagementDisabled"
|
||||
(click)="deleteInstance(instance)"
|
||||
pTooltip="Delete instance"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instance-content">
|
||||
<div class="instance-field">
|
||||
<label>URL</label>
|
||||
<span class="field-value">{{ instance.url }}</span>
|
||||
</div>
|
||||
|
||||
<div class="instance-field">
|
||||
<label>API Key</label>
|
||||
<span class="field-value api-key">{{ instance.apiKey | slice:0:8 }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instance Modal -->
|
||||
<p-dialog
|
||||
[(visible)]="showInstanceModal"
|
||||
[modal]="true"
|
||||
[closable]="true"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
styleClass="instance-modal"
|
||||
[header]="modalTitle"
|
||||
(onHide)="closeInstanceModal()"
|
||||
>
|
||||
<form [formGroup]="instanceForm" class="p-fluid instance-form">
|
||||
<div class="field">
|
||||
<label for="instance-name">Name *</label>
|
||||
<input
|
||||
id="instance-name"
|
||||
type="text"
|
||||
pInputText
|
||||
formControlName="name"
|
||||
placeholder="My Sonarr Instance"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'name', 'required')" class="p-error">Name is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-url">URL *</label>
|
||||
<input
|
||||
id="instance-url"
|
||||
type="text"
|
||||
pInputText
|
||||
formControlName="url"
|
||||
placeholder="http://localhost:8989"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'url', 'required')" class="p-error">URL is required</small>
|
||||
<small *ngIf="hasError(instanceForm, 'url', 'invalidUri')" class="p-error">URL must be a valid URL</small>
|
||||
<small *ngIf="hasError(instanceForm, 'url', 'invalidProtocol')" class="p-error">URL must use http or https protocol</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-apikey">API Key *</label>
|
||||
<input
|
||||
id="instance-apikey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Sonarr API key"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'apiKey', 'required')" class="p-error">API key is required</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary"
|
||||
[disabled]="instanceForm.invalid || sonarrSaving()"
|
||||
[loading]="sonarrSaving()"
|
||||
(click)="saveInstance()"
|
||||
></button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- Loading/Error State Component -->
|
||||
<app-loading-error-state
|
||||
*ngIf="sonarrLoading() || sonarrError()"
|
||||
[loading]="sonarrLoading()"
|
||||
[error]="sonarrError()"
|
||||
loadingMessage="Loading settings..."
|
||||
errorMessage="Could not connect to server"
|
||||
></app-loading-error-state>
|
||||
|
||||
<!-- Form Content - only shown when not loading and no error -->
|
||||
<form *ngIf="!sonarrLoading() && !sonarrError()" [formGroup]="sonarrForm" class="p-fluid">
|
||||
<!-- Main Settings -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Enable Sonarr Integration</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="enabled" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, Sonarr API integration will be used</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Failed Import Max Strikes</label>
|
||||
<div>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="failedImportMaxStrikes"
|
||||
[min]="-1"
|
||||
[showButtons]="true"
|
||||
buttonLayout="horizontal"
|
||||
incrementButtonIcon="pi pi-plus"
|
||||
decrementButtonIcon="pi pi-minus"
|
||||
></p-inputNumber>
|
||||
</div>
|
||||
<small class="form-helper-text">Maximum number of strikes before removing a failed import (-1 to use global setting; 0 to disable)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instances Section -->
|
||||
<div class="section-header mt-4">
|
||||
<h3>Sonarr Instances</h3>
|
||||
<small>Configure multiple Sonarr server instances</small>
|
||||
</div>
|
||||
|
||||
<!-- Instances Container -->
|
||||
<div class="instances-container">
|
||||
<!-- Empty state message when no instances -->
|
||||
<div *ngIf="instances.controls.length === 0" class="empty-instances-message p-3 text-center">
|
||||
<p>No Sonarr instances defined. Add an instance to start using Sonarr integration.</p>
|
||||
</div>
|
||||
|
||||
<!-- Instance cards -->
|
||||
<div class="instance-list">
|
||||
<div *ngFor="let instance of instances.controls; let i = index" class="instance-item" [formGroup]="getInstanceAsFormGroup(i)">
|
||||
<div class="instance-header">
|
||||
<div class="instance-title">
|
||||
<i class="pi pi-server instance-icon"></i>
|
||||
<input type="text" pInputText formControlName="name" placeholder="Instance name" class="instance-name-input" />
|
||||
</div>
|
||||
<button pButton type="button" icon="pi pi-trash" class="p-button-danger p-button-sm"
|
||||
(click)="removeInstance(i)" [disabled]="sonarrForm.disabled"></button>
|
||||
</div>
|
||||
<small *ngIf="hasInstanceFieldError(i, 'name', 'required')" class="p-error block">Name is required</small>
|
||||
|
||||
<div class="instance-content">
|
||||
<div class="instance-field">
|
||||
<label>URL</label>
|
||||
<div class="field-input">
|
||||
<input type="text" pInputText formControlName="url" placeholder="http://localhost:8989" required />
|
||||
<small *ngIf="hasInstanceFieldError(i, 'url', 'required')" class="p-error block">URL is required</small>
|
||||
<small *ngIf="hasInstanceFieldError(i, 'url', 'invalidUri')" class="p-error block">URL must be a valid URL</small>
|
||||
<small *ngIf="hasInstanceFieldError(i, 'url', 'invalidProtocol')" class="p-error block">URL must use http or https protocol</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instance-field">
|
||||
<label>API Key</label>
|
||||
<div class="field-input">
|
||||
<input type="password" pInputText formControlName="apiKey" placeholder="Your Sonarr API key" required />
|
||||
<small *ngIf="hasInstanceFieldError(i, 'apiKey', 'required')" class="p-error block">API key is required</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-content-end mt-3">
|
||||
<button pButton type="button" icon="pi pi-plus" label="Add Instance"
|
||||
(click)="addInstance()" [disabled]="!sonarrForm.get('enabled')?.value" class="p-button-outlined"></button>
|
||||
</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]="(!sonarrForm.dirty || !hasActualChanges) || sonarrForm.invalid || sonarrSaving()"
|
||||
[loading]="sonarrSaving()"
|
||||
(click)="saveSonarrConfig()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Reset"
|
||||
icon="pi pi-refresh"
|
||||
class="p-button-secondary p-button-outlined ml-2"
|
||||
(click)="resetSonarrConfig()"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</p-card>
|
||||
<!-- Confirmation Dialog -->
|
||||
<p-confirmDialog></p-confirmDialog>
|
||||
|
||||
@@ -1,4 +1,283 @@
|
||||
/* Sonarr Settings Styles */
|
||||
|
||||
@import '../styles/settings-shared.scss';
|
||||
@import '../styles/arr-shared.scss';
|
||||
@import '../styles/arr-shared.scss';
|
||||
|
||||
.sonarr-settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Card styling
|
||||
.settings-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
|
||||
.header-title-container {
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form styling
|
||||
.field-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.field-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.form-helper-text {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
// Instance management
|
||||
.disabled-overlay {
|
||||
position: relative;
|
||||
background: var(--surface-100);
|
||||
border-radius: 6px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.disabled-message {
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
|
||||
i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-instances-message {
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.instances-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instance-item {
|
||||
background: var(--surface-50);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--surface-100);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.instance-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.instance-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.instance-icon {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.instance-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
.instance-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family);
|
||||
font-size: 0.9rem;
|
||||
word-break: break-all;
|
||||
|
||||
&.api-key {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
background: var(--surface-100);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal styling
|
||||
:host ::ng-deep .instance-modal {
|
||||
width: 90vw;
|
||||
max-width: 500px;
|
||||
|
||||
.p-dialog-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.instance-form {
|
||||
padding: 1.5rem;
|
||||
|
||||
.field {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-error {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
background: var(--surface-50);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.cards-container {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instance-item .instance-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
|
||||
.header-title-container .card-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, OnDestroy, Output, effect, inject } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { SonarrConfigStore } from "./sonarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
@@ -13,10 +13,11 @@ import { InputTextModule } from "primeng/inputtext";
|
||||
import { CheckboxModule } from "primeng/checkbox";
|
||||
import { ButtonModule } from "primeng/button";
|
||||
import { InputNumberModule } from "primeng/inputnumber";
|
||||
import { SelectButtonModule } from "primeng/selectbutton";
|
||||
import { ToastModule } from "primeng/toast";
|
||||
import { DialogModule } from "primeng/dialog";
|
||||
import { ConfirmDialogModule } from "primeng/confirmdialog";
|
||||
import { ConfirmationService } from "primeng/api";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
|
||||
@Component({
|
||||
@@ -30,12 +31,12 @@ import { LoadingErrorStateComponent } from "../../shared/components/loading-erro
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
InputNumberModule,
|
||||
SelectButtonModule,
|
||||
ToastModule,
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
LoadingErrorStateComponent,
|
||||
SelectModule
|
||||
],
|
||||
providers: [SonarrConfigStore],
|
||||
providers: [SonarrConfigStore, ConfirmationService],
|
||||
templateUrl: "./sonarr-settings.component.html",
|
||||
styleUrls: ["./sonarr-settings.component.scss"],
|
||||
})
|
||||
@@ -43,22 +44,26 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
@Output() saved = new EventEmitter<void>();
|
||||
@Output() error = new EventEmitter<string>();
|
||||
|
||||
// Sonarr Configuration Form
|
||||
sonarrForm: FormGroup;
|
||||
// Forms
|
||||
globalForm: FormGroup;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
// Modal state
|
||||
showInstanceModal = false;
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
editingInstance: ArrInstance | null = null;
|
||||
|
||||
// Original form values for tracking changes
|
||||
private originalFormValues: any;
|
||||
|
||||
// Track whether the form has actual changes compared to original values
|
||||
hasActualChanges = false;
|
||||
private originalGlobalValues: any;
|
||||
hasGlobalChanges = false;
|
||||
|
||||
// Clean up subscriptions
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// Inject the necessary services
|
||||
// Services
|
||||
private formBuilder = inject(FormBuilder);
|
||||
// Using the notification service for all toast messages
|
||||
private notificationService = inject(NotificationService);
|
||||
private confirmationService = inject(ConfirmationService);
|
||||
private sonarrStore = inject(SonarrConfigStore);
|
||||
|
||||
// Signals from store
|
||||
@@ -66,24 +71,26 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
sonarrLoading = this.sonarrStore.loading;
|
||||
sonarrError = this.sonarrStore.error;
|
||||
sonarrSaving = this.sonarrStore.saving;
|
||||
instanceOperations = this.sonarrStore.instanceOperations;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
*/
|
||||
canDeactivate(): boolean {
|
||||
return !this.sonarrForm?.dirty || !this.hasActualChanges;
|
||||
return !this.globalForm?.dirty || !this.hasGlobalChanges;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Initialize the main form
|
||||
this.sonarrForm = this.formBuilder.group({
|
||||
// Initialize forms
|
||||
this.globalForm = this.formBuilder.group({
|
||||
enabled: [false],
|
||||
failedImportMaxStrikes: [-1],
|
||||
});
|
||||
|
||||
// Add instances FormArray to main form
|
||||
this.sonarrForm.addControl('instances', this.formBuilder.array([]));
|
||||
this.instanceForm = this.formBuilder.group({
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required, this.uriValidator.bind(this)]],
|
||||
apiKey: ['', Validators.required],
|
||||
});
|
||||
|
||||
// Load Sonarr config data
|
||||
this.sonarrStore.loadConfig();
|
||||
@@ -92,15 +99,15 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
effect(() => {
|
||||
const config = this.sonarrConfig();
|
||||
if (config) {
|
||||
this.updateFormFromConfig(config);
|
||||
this.updateGlobalFormFromConfig(config);
|
||||
}
|
||||
});
|
||||
|
||||
// Track form changes for dirty state
|
||||
this.sonarrForm.valueChanges
|
||||
// Track global form changes
|
||||
this.globalForm.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.hasActualChanges = this.formValuesChanged();
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,51 +120,32 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
}
|
||||
|
||||
/**
|
||||
* Update form with values from the configuration
|
||||
* Update global form with values from the configuration
|
||||
*/
|
||||
private updateFormFromConfig(config: SonarrConfig): void {
|
||||
// Update main form controls
|
||||
this.sonarrForm.patchValue({
|
||||
private updateGlobalFormFromConfig(config: SonarrConfig): void {
|
||||
this.globalForm.patchValue({
|
||||
enabled: config.enabled,
|
||||
failedImportMaxStrikes: config.failedImportMaxStrikes,
|
||||
});
|
||||
|
||||
// Clear and rebuild the instances form array
|
||||
const instancesArray = this.sonarrForm.get('instances') as FormArray;
|
||||
instancesArray.clear();
|
||||
|
||||
// Add all instances to the form array
|
||||
if (config.instances && config.instances.length > 0) {
|
||||
config.instances.forEach(instance => {
|
||||
instancesArray.push(
|
||||
this.formBuilder.group({
|
||||
id: [instance.id || ''],
|
||||
name: [instance.name, Validators.required],
|
||||
url: [instance.url, [Validators.required, this.uriValidator.bind(this)]],
|
||||
apiKey: [instance.apiKey, Validators.required],
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Store original form values for dirty checking
|
||||
this.storeOriginalValues();
|
||||
// Store original values for dirty checking
|
||||
this.storeOriginalGlobalValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store original form values for dirty checking
|
||||
* Store original global form values for dirty checking
|
||||
*/
|
||||
private storeOriginalValues(): void {
|
||||
this.originalFormValues = JSON.parse(JSON.stringify(this.sonarrForm.value));
|
||||
this.sonarrForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
private storeOriginalGlobalValues(): void {
|
||||
this.originalGlobalValues = JSON.parse(JSON.stringify(this.globalForm.value));
|
||||
this.globalForm.markAsPristine();
|
||||
this.hasGlobalChanges = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current form values are different from the original values
|
||||
* Check if the current global form values are different from the original values
|
||||
*/
|
||||
private formValuesChanged(): boolean {
|
||||
return !this.isEqual(this.sonarrForm.value, this.originalFormValues);
|
||||
private globalFormValuesChanged(): boolean {
|
||||
return !this.isEqual(this.globalForm.value, this.originalGlobalValues);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,90 +176,6 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update form control disabled states based on the configuration
|
||||
*/
|
||||
private updateFormControlDisabledStates(config: SonarrConfig): void {
|
||||
const enabled = config.enabled;
|
||||
this.updateMainControlsState(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of main controls based on the 'enabled' control value
|
||||
*/
|
||||
private updateMainControlsState(enabled: boolean): void {
|
||||
const failedImportMaxStrikesControl = this.sonarrForm.get('failedImportMaxStrikes');
|
||||
|
||||
if (enabled) {
|
||||
failedImportMaxStrikesControl?.enable();
|
||||
} else {
|
||||
failedImportMaxStrikesControl?.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new instance to the instances form array
|
||||
* @param instance Optional instance configuration to initialize the form with
|
||||
*/
|
||||
addInstance(instance: ArrInstance | null = null): void {
|
||||
const instanceForm = this.formBuilder.group({
|
||||
id: [instance?.id || ''],
|
||||
name: [instance?.name || '', Validators.required],
|
||||
url: [instance?.url?.toString() || '', [Validators.required, this.uriValidator.bind(this)]],
|
||||
apiKey: [instance?.apiKey || '', Validators.required]
|
||||
});
|
||||
|
||||
this.instances.push(instanceForm);
|
||||
|
||||
// Mark form as dirty to enable save button
|
||||
this.sonarrForm.markAsDirty();
|
||||
this.hasActualChanges = this.formValuesChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an instance at the specified index
|
||||
*/
|
||||
removeInstance(index: number): void {
|
||||
const instanceForm = this.getInstanceAsFormGroup(index);
|
||||
const instanceId = instanceForm.get('id')?.value;
|
||||
|
||||
// Just remove from the form array - deletion will be handled on save
|
||||
this.instances.removeAt(index);
|
||||
|
||||
// Mark form as dirty to enable save button
|
||||
this.sonarrForm.markAsDirty();
|
||||
this.hasActualChanges = this.formValuesChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instances form array
|
||||
*/
|
||||
get instances(): FormArray {
|
||||
return this.sonarrForm.get('instances') as FormArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance at the specified index as a FormGroup
|
||||
*/
|
||||
getInstanceAsFormGroup(index: number): FormGroup {
|
||||
return this.instances.at(index) as FormGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an instance field has an error
|
||||
* @param instanceIndex The index of the instance in the array
|
||||
* @param fieldName The name of the field to check
|
||||
* @param errorName The name of the error to check for
|
||||
* @returns True if the field has the specified error
|
||||
*/
|
||||
hasInstanceFieldError(instanceIndex: number, fieldName: string, errorName: string): boolean {
|
||||
const instancesArray = this.sonarrForm.get('instances') as FormArray;
|
||||
if (!instancesArray || !instancesArray.controls[instanceIndex]) return false;
|
||||
|
||||
const control = (instancesArray.controls[instanceIndex] as FormGroup).get(fieldName);
|
||||
return control !== null && control.hasError(errorName) && control.touched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom validator to check if the input is a valid URI
|
||||
*/
|
||||
@@ -308,148 +212,209 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the form control has an error
|
||||
* @param controlName The name of the control to check
|
||||
* @param errorName The name of the error to check for
|
||||
* @returns True if the control has the specified error
|
||||
* Check if a form control has an error
|
||||
*/
|
||||
hasError(controlName: string, errorName: string): boolean {
|
||||
const control = this.sonarrForm.get(controlName);
|
||||
hasError(form: FormGroup, controlName: string, errorName: string): boolean {
|
||||
const control = form.get(controlName);
|
||||
return control !== null && control.hasError(errorName) && control.touched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the Sonarr configuration
|
||||
* Save the global Sonarr configuration
|
||||
*/
|
||||
saveSonarrConfig(): void {
|
||||
// Mark all form controls as touched to trigger validation
|
||||
this.markFormGroupTouched(this.sonarrForm);
|
||||
saveGlobalConfig(): void {
|
||||
this.markFormGroupTouched(this.globalForm);
|
||||
|
||||
if (this.sonarrForm.invalid) {
|
||||
if (this.globalForm.invalid) {
|
||||
this.notificationService.showError('Please fix the validation errors before saving');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasActualChanges) {
|
||||
if (!this.hasGlobalChanges) {
|
||||
this.notificationService.showSuccess('No changes detected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current config to preserve existing instances
|
||||
const currentConfig = this.sonarrConfig();
|
||||
if (!currentConfig) return;
|
||||
|
||||
// Create the updated main config
|
||||
const updatedConfig: SonarrConfig = {
|
||||
...currentConfig,
|
||||
enabled: this.sonarrForm.get('enabled')?.value,
|
||||
failedImportMaxStrikes: this.sonarrForm.get('failedImportMaxStrikes')?.value
|
||||
const updatedConfig = {
|
||||
enabled: this.globalForm.get('enabled')?.value,
|
||||
failedImportMaxStrikes: this.globalForm.get('failedImportMaxStrikes')?.value
|
||||
};
|
||||
|
||||
// Get the instances from the form
|
||||
const formInstances = this.instances.getRawValue();
|
||||
this.sonarrStore.saveConfig(updatedConfig);
|
||||
|
||||
// Separate creates and updates
|
||||
const creates: CreateArrInstanceDto[] = [];
|
||||
const updates: Array<{ id: string, instance: ArrInstance }> = [];
|
||||
|
||||
formInstances.forEach((instance: any) => {
|
||||
if (instance.id) {
|
||||
// This is an existing instance, prepare for update
|
||||
const updateInstance: ArrInstance = {
|
||||
id: instance.id,
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey
|
||||
};
|
||||
updates.push({ id: instance.id, instance: updateInstance });
|
||||
} else {
|
||||
// This is a new instance, prepare for creation (don't send ID)
|
||||
const createInstance: CreateArrInstanceDto = {
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey
|
||||
};
|
||||
creates.push(createInstance);
|
||||
}
|
||||
});
|
||||
|
||||
// Use the new method that saves config and processes instances sequentially
|
||||
this.sonarrStore.saveConfigAndInstances({
|
||||
config: updatedConfig,
|
||||
instanceOperations: { creates, updates, deletes: [] }
|
||||
});
|
||||
|
||||
// Monitor the saving state to show completion feedback
|
||||
this.monitorSavingCompletion();
|
||||
// Monitor saving completion
|
||||
this.monitorGlobalSaving();
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor saving completion and show appropriate feedback
|
||||
* Monitor global saving completion
|
||||
*/
|
||||
private monitorSavingCompletion(): void {
|
||||
// Use a timeout to check the saving state periodically
|
||||
private monitorGlobalSaving(): void {
|
||||
const checkSavingStatus = () => {
|
||||
const saving = this.sonarrSaving();
|
||||
const error = this.sonarrError();
|
||||
const pendingOps = this.instanceOperations();
|
||||
|
||||
if (!saving && Object.keys(pendingOps).length === 0) {
|
||||
// Operations are complete
|
||||
if (!saving) {
|
||||
if (error) {
|
||||
this.notificationService.showError(`Save completed with issues: ${error}`);
|
||||
this.notificationService.showError(`Save failed: ${error}`);
|
||||
this.error.emit(error);
|
||||
// Don't mark as pristine if there were errors
|
||||
} else {
|
||||
// Complete success
|
||||
this.notificationService.showSuccess('Sonarr configuration saved successfully');
|
||||
this.notificationService.showSuccess('Global configuration saved successfully');
|
||||
this.saved.emit();
|
||||
|
||||
// Reload config from backend to ensure UI is in sync
|
||||
this.sonarrStore.loadConfig();
|
||||
|
||||
// Reset form state after successful save
|
||||
setTimeout(() => {
|
||||
this.sonarrForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
this.storeOriginalValues();
|
||||
}, 100);
|
||||
// Reset form state without reloading from backend
|
||||
this.globalForm.markAsPristine();
|
||||
this.hasGlobalChanges = false;
|
||||
this.storeOriginalGlobalValues();
|
||||
}
|
||||
} else {
|
||||
// Still saving, check again in a short while
|
||||
setTimeout(checkSavingStatus, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// Start monitoring
|
||||
setTimeout(checkSavingStatus, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the Sonarr configuration form to default values
|
||||
* Get instances from current config
|
||||
*/
|
||||
resetSonarrConfig(): void {
|
||||
// Clear all instances
|
||||
const instancesArray = this.sonarrForm.get('instances') as FormArray;
|
||||
instancesArray.clear();
|
||||
|
||||
// Reset main config to defaults
|
||||
this.sonarrForm.patchValue({
|
||||
enabled: false,
|
||||
failedImportMaxStrikes: -1
|
||||
get instances(): ArrInstance[] {
|
||||
return this.sonarrConfig()?.instances || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if instance management should be disabled
|
||||
*/
|
||||
get instanceManagementDisabled(): boolean {
|
||||
return !this.globalForm.get('enabled')?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal to add new instance
|
||||
*/
|
||||
openAddInstanceModal(): void {
|
||||
this.modalMode = 'add';
|
||||
this.editingInstance = null;
|
||||
this.instanceForm.reset();
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal to edit existing instance
|
||||
*/
|
||||
openEditInstanceModal(instance: ArrInstance): void {
|
||||
this.modalMode = 'edit';
|
||||
this.editingInstance = instance;
|
||||
this.instanceForm.patchValue({
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
});
|
||||
|
||||
// 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.sonarrForm.markAsDirty();
|
||||
this.hasActualChanges = true;
|
||||
} else {
|
||||
// If reset brings us back to original state, mark as pristine
|
||||
this.sonarrForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close instance modal
|
||||
*/
|
||||
closeInstanceModal(): void {
|
||||
this.showInstanceModal = false;
|
||||
this.editingInstance = null;
|
||||
this.instanceForm.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save instance (add or edit)
|
||||
*/
|
||||
saveInstance(): void {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
|
||||
if (this.instanceForm.invalid) {
|
||||
this.notificationService.showError('Please fix the validation errors before saving');
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceData: CreateArrInstanceDto = {
|
||||
name: this.instanceForm.get('name')?.value,
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
if (this.modalMode === 'add') {
|
||||
this.sonarrStore.createInstance(instanceData);
|
||||
} else if (this.editingInstance) {
|
||||
this.sonarrStore.updateInstance({
|
||||
id: this.editingInstance.id!,
|
||||
instance: instanceData
|
||||
});
|
||||
}
|
||||
|
||||
this.monitorInstanceSaving();
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor instance saving completion
|
||||
*/
|
||||
private monitorInstanceSaving(): void {
|
||||
const checkSavingStatus = () => {
|
||||
const saving = this.sonarrSaving();
|
||||
const error = this.sonarrError();
|
||||
|
||||
if (!saving) {
|
||||
if (error) {
|
||||
this.notificationService.showError(`Operation failed: ${error}`);
|
||||
} else {
|
||||
const action = this.modalMode === 'add' ? 'created' : 'updated';
|
||||
this.notificationService.showSuccess(`Instance ${action} successfully`);
|
||||
this.closeInstanceModal();
|
||||
}
|
||||
} else {
|
||||
setTimeout(checkSavingStatus, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(checkSavingStatus, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete instance with confirmation
|
||||
*/
|
||||
deleteInstance(instance: ArrInstance): void {
|
||||
this.confirmationService.confirm({
|
||||
message: `Are you sure you want to delete the instance "${instance.name}"?`,
|
||||
header: 'Confirm Deletion',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptButtonStyleClass: 'p-button-danger',
|
||||
accept: () => {
|
||||
this.sonarrStore.deleteInstance(instance.id!);
|
||||
|
||||
// Monitor deletion
|
||||
const checkDeletionStatus = () => {
|
||||
const saving = this.sonarrSaving();
|
||||
const error = this.sonarrError();
|
||||
|
||||
if (!saving) {
|
||||
if (error) {
|
||||
this.notificationService.showError(`Deletion failed: ${error}`);
|
||||
} else {
|
||||
this.notificationService.showSuccess('Instance deleted successfully');
|
||||
}
|
||||
} else {
|
||||
setTimeout(checkDeletionStatus, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(checkDeletionStatus, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modal title based on mode
|
||||
*/
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Sonarr Instance' : 'Edit Sonarr Instance';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user