Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recreates Emby configuration page with a simpler layout. #4776

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<h1 mat-dialog-title>Server Configuration</h1>
<mat-dialog-content>
<form #embyServerForm="ngForm">
<h2>Connection</h2>

<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Server Name</mat-label>
<input matInput placeholder="Server Name" name="name" [(ngModel)]="server.name" value="{{server.name}}"
(change)="processChangeEvent()">
<mat-hint>Auto populated during discovery of the server if left empty.</mat-hint>
</mat-form-field>

<div class="row">
<mat-form-field class="col-md-6 col-12" appearance="outline" floatLabel=auto>
<mat-label>Hostname / IP</mat-label>
<input matInput placeholder="Hostname or IP" name="ip" [(ngModel)]="server.ip" value="{{server.ip}}"
(change)="processChangeEvent()" #serverHostnameIpControl="ngModel" required>
<mat-error *ngIf="serverHostnameIpControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field>

<mat-form-field class="col-md-4 col-7" appearance="outline" floatLabel=auto>
<mat-label>Port</mat-label>
<input matInput placeholder="Port" name="port" [(ngModel)]="server.port" value="{{server.port}}"
(change)="processChangeEvent()" #serverPortControl="ngModel" required pattern="^[0-9]*$">
<mat-error *ngIf="serverPortControl.hasError('required')">Must be specified.</mat-error>
<mat-error *ngIf="serverPortControl.hasError('pattern')">Must be a number.</mat-error>
</mat-form-field>

<mat-slide-toggle class="col-md-2 col-5 mt-3" id="ssl" name="ssl" [(ngModel)]="server.ssl" [checked]="server.ssl"
(change)="processChangeEvent()">
SSL
</mat-slide-toggle>
</div>

<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Base URL</mat-label>
<input matInput placeholder="Base Url" name="subDir" [(ngModel)]="server.subDir" value="{{server.subDir}}"
(change)="processChangeEvent()">
<mat-hint>Optional url path to be used with reverse proxy. Example: 'emby' to get
'https://ip:port/emby'.</mat-hint>
</mat-form-field>

<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>API Key</mat-label>
<input matInput placeholder="Api Key" name="apiKey" [(ngModel)]="server.apiKey" value="{{server.apiKey}}"
(change)="processChangeEvent()" #serverApiKeyControl="ngModel" required>
<mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field>

<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Externally Facing Hostname</mat-label>
<input matInput placeholder="e.g. https://emby.server.com/" name="serverHostname"
[(ngModel)]="server.serverHostname" value="{{server.serverHostname}}" (change)="processChangeEvent()">
<mat-hint>
The external address that users will navigate to when they press the 'View On Emby'
button.
<br />
<span>Current URL: {{server.serverHostname ? server.serverHostname :
'https://app.emby.media'}}/#!/item/item.html?id=1</span>
</mat-hint>
</mat-form-field>

<h2>Libraries</h2>
<label *ngIf="server.embySelectedLibraries && server.embySelectedLibraries.length == 0">
Discover the server to load available libraries. If you still not seeing any libraries, make sure they are
configured in Emby.</label>
<div *ngIf="server.embySelectedLibraries && server.embySelectedLibraries.length > 0">
<label>Please select the libraries for Ombi to monitor. If nothing is selected, Ombi will monitor all
libraries.</label>
<div *ngFor="let lib of server.embySelectedLibraries">
<div class="md-form-field">
<div class="checkbox">
<mat-slide-toggle name="library-{{lib.key}}" [(ngModel)]="lib.enabled" [checked]="lib.enabled"
(change)="processChangeEvent()">
{{lib.title}}
</mat-slide-toggle>
</div>
</div>
</div>
</div>

</form>
</mat-dialog-content>

<mat-dialog-actions align=end>
<button style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="warn" *ngIf="!data.isNewServer"
(click)="delete()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-trash"></i>
<span> Delete</span>
</span>
</button>

<button style="margin: .5em 0 0 0.5em;" mat-stroked-button color="basic" (click)="cancel()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-times"></i>
<span> Cancel</span>
</span>
</button>

<button style="margin: .5em 0 0 .5em;" mat-stroked-button color="{{serverDiscoveryRequired ? 'accent' : 'basic'}}"
(click)="discoverServer()" id="discover">
<span>Discover Server</span>
</button>

<button style="margin: .5em 0 0 .5em;" mat-stroked-button color="accent"
[disabled]="!isChangeDetected || serverDiscoveryRequired || isServerNameMissing" (click)="save()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i style="vertical-align: text-top;" class="fas fa-plus"></i>
<span> Save</span>
</span>
</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@media (max-width: 978px) {
::ng-deep .mat-dialog-container {
overflow: unset;
display: flex;
flex-direction: column;

.mat-dialog-content{
max-height: unset;
}

.mat-dialog-actions{
min-height: unset;
}

emby-server-dialog-component {
display: flex;
flex-direction: column;
min-height: 1px;
}
}
}

::ng-deep mat-form-field .mat-form-field {
&-subscript-wrapper {
position: static;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { Component, Inject, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import {
IEmbyLibrariesSettings,
IEmbyServer,
IEmbySettings,
} from "app/interfaces";
import {
EmbyService,
NotificationService,
SettingsService,
TesterService,
} from "app/services";
import { isEqual } from "lodash";

export interface EmbyServerDialogData {
server: IEmbyServer;
isNewServer: boolean;
savedSettings: IEmbySettings;
}

@Component({
selector: "emby-server-dialog-component",
templateUrl: "emby-server-dialog.component.html",
styleUrls: ["emby-server-dialog.component.scss"],
})
export class EmbyServerDialog {
@ViewChild("embyServerForm") embyServerForm: NgForm;
public server: IEmbyServer;
public isServerNameMissing: boolean;
public isChangeDetected: boolean;
public serverDiscoveryRequired: boolean;
private validatedFields: {
ip: string;
port: number;
ssl: boolean;
apiKey: string;
subDir: string;
};

constructor(
private dialogRef: MatDialogRef<EmbyServerDialog>,
@Inject(MAT_DIALOG_DATA) public data: EmbyServerDialogData,
private notificationService: NotificationService,
private settingsService: SettingsService,
private testerService: TesterService,
private embyService: EmbyService
) {
this.server = structuredClone(data.server);
this.isChangeDetected = false;
this.serverDiscoveryRequired = data.isNewServer;
this.isServerNameMissing = !this.server.name;
this.validatedFields = {
ip: this.server.ip,
port: this.server.port,
ssl: this.server.ssl,
apiKey: this.server.apiKey,
subDir: this.server.subDir,
};
}

public processChangeEvent() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we are recreating some of the built in validation of angular reactive forms here, we should probably use a form with the servers being a form array (it's what we have been moving the other pages to).

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started off with an angular reactive form but found it easier to implement all the validation and rules this way. I found that I had to listen to every change event anyway to determine if server discovery is required again.

if (
this.validatedFields.ip !== this.server.ip ||
this.validatedFields.port?.toString() !== this.server.port?.toString() ||
this.validatedFields.ssl !== this.server.ssl ||
this.validatedFields.apiKey !== this.server.apiKey ||
this.validatedFields.subDir !== this.server.subDir ||
!this.embyServerForm.valid
) {
this.serverDiscoveryRequired = true;
} else {
this.serverDiscoveryRequired = false;
}

this.isServerNameMissing = !this.server.name;
this.isChangeDetected = !isEqual(this.data.server, this.server);
}

public cancel() {
this.dialogRef.close();
}

public delete() {
const settings: IEmbySettings = structuredClone(this.data.savedSettings);
const index = settings.servers.findIndex((i) => i.id === this.server.id);
if (index == -1) return;

settings.servers.splice(index, 1);
const errorMessage = "There was an error removing the server.";
this.settingsService.saveEmby(settings).subscribe({
next: (result) => {
if (result) {
this.data.savedSettings.servers.splice(index, 1);
this.dialogRef.close();
} else {
this.notificationService.error(errorMessage);
}
},
error: () => {
this.notificationService.error(errorMessage);
},
});
}

public save() {
const settings: IEmbySettings = structuredClone(this.data.savedSettings);
if (this.data.isNewServer) {
settings.servers.push(this.server);
} else {
const index = settings.servers.findIndex((i) => i.id === this.server.id);
if (index !== -1) settings.servers[index] = this.server;
}

const errorMessage = "There was an error saving the server.";
this.settingsService.saveEmby(settings).subscribe({
next: (result) => {
if (result) {
if (this.data.isNewServer) {
this.data.savedSettings.servers.push(this.server);
} else {
const index = this.data.savedSettings.servers.findIndex(
(i) => i.id === this.server.id
);
if (index !== -1)
this.data.savedSettings.servers[index] = this.server;
}
this.dialogRef.close();
} else {
this.notificationService.error(errorMessage);
}
},
error: () => {
this.notificationService.error(errorMessage);
},
});
}

public discoverServer() {
this.embyServerForm.form.markAllAsTouched();
if (!this.embyServerForm.valid) return;

const errorMessage = `Failed to connect to the server. Make sure configuration is correct.`;
this.testerService.embyTest(this.server).subscribe({
next: (result) => {
if (!result) {
this.notificationService.error(errorMessage);
return;
}

this.retrieveServerNameAndId(this.server);
},
error: () => {
this.notificationService.error(errorMessage);
},
});
}

private retrieveServerNameAndId(server: IEmbyServer) {
const errorMessage =
"Failed to discover server. Make sure configuration is correct.";
this.embyService.getPublicInfo(server).subscribe({
next: (result) => {
if (!result) {
this.notificationService.error(errorMessage);
return;
}

if (!server.name) {
server.name = result.serverName;
this.isServerNameMissing = false;
}
server.serverId = result.id;
this.loadLibraries(server);
},
error: () => {
this.notificationService.error(errorMessage);
},
});
}

private loadLibraries(server: IEmbyServer) {
this.embyService.getLibraries(server).subscribe({
next: (result) => {
server.embySelectedLibraries = result.items.map((item) => {
const index = server.embySelectedLibraries.findIndex(
(x) => x.key == item.id
);
const enabled =
index === -1 ? false : server.embySelectedLibraries[index].enabled;
const lib: IEmbyLibrariesSettings = {
key: item.id,
title: item.name,
enabled: enabled,
collectionType: item.collectionType,
};
return lib;
});

this.serverDiscoveryRequired = false;
this.validatedFields = {
ip: this.server.ip,
port: this.server.port,
ssl: this.server.ssl,
apiKey: this.server.apiKey,
subDir: this.server.subDir,
};
this.notificationService.success("Successfully discovered the server.");
},
error: () => {
const errorMessage = "There was an error retrieving libraries.";
this.notificationService.error(errorMessage);
},
});
}
}
Loading