Random Cleanup + OPDS Base Url Support (#1926)

* Updated a ton of dependencies. PDFs reader got a big update from PDF.js 2.6 -> 3.x

* Rolled back fontawesome update

* Updated to latest angular patch. Fixed search being too long instead of just to the end of the browser screen.

* Fixed alignment on download icon for download indicator in cards

* Include progress information on Want To Read API and when marking something as Read, perform cleanup service on want to read.

* Removed mark-read updating want to read. As there are series restrictions and it could be misleading.

* Tweaked login page spacing when form is dirty

* Replaced an object instantiation

* Commented out a few tests that always break when updating NetVips (but always work)

* Updated ngx-toastr

* Added styles for alerts to Kavita. They were somehow missing. Fixed an issue where when OPDS was disabled, user preferences wouldn't tell them.

* Wired up a reset base url button to match Ip Addresses

* Disable ipAddress and port for docker users

* Removed cache dir since it's kinda pointless currently

* Started the update for OPDS BaseUrl support

* Fixed OPDS url not reflecting base url on localhost

* Added extra plumbing to allow sending a real email when testing a custom service.

* Implemented OPDS support under Base Url. Added pagination to all APIs where applicable.

* Added a swallowing of permission denied on Updating baseurl in index.html for inapplicable users.

* Fixed a bad test
This commit is contained in:
Joe Milazzo 2023-04-14 19:44:35 -05:00 committed by GitHub
parent 1bf4fde58f
commit 21a9f28923
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2519 additions and 1219 deletions

View file

@ -23,6 +23,19 @@
</div>
</div>
<div class="mb-3">
<label for="settings-hostname" class="form-label">Host Name</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
<ng-template #hostNameTooltip>Domain Name (of Reverse Proxy). If set, email generation will always use this.</ng-template>
<span class="visually-hidden" id="settings-hostname-help">Domain Name (of Reverse Proxy). If set, email generation will always use this.</span>
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
Host name must start with http(s) and not end in /
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>

View file

@ -21,19 +21,23 @@ export class ManageEmailSettingsComponent implements OnInit {
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, []));
});
}
resetForm() {
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
this.settingsForm.markAsPristine();
}
async saveSettings() {
const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
modelSettings.hostName = this.settingsForm.get('hostName')?.value;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.toastr.success('Server settings updated');
@ -43,7 +47,7 @@ export class ManageEmailSettingsComponent implements OnInit {
}
resetToDefaults() {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.toastr.success('Server settings updated');
@ -53,7 +57,7 @@ export class ManageEmailSettingsComponent implements OnInit {
}
resetEmailServiceUrl() {
this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings.emailServiceUrl = settings.emailServiceUrl;
this.resetForm();
this.toastr.success('Email Service Reset');
@ -65,7 +69,7 @@ export class ManageEmailSettingsComponent implements OnInit {
testEmailServiceUrl() {
this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value || '').pipe(take(1)).subscribe(async (result: EmailTestResult) => {
if (result.successful) {
this.toastr.success('Email Service Url validated');
this.toastr.success('Email Service was reachable');
} else {
this.toastr.error('Email Service Url did not respond. ' + result.errorMessage);
}

View file

@ -1,12 +1,14 @@
<div class="container-fluid">
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
<p class="text-warning pt-2">Changing Port or Base Url requires a manual restart of Kavita to take effect.</p>
<div class="mb-3">
<div class="alert alert-warning" role="alert">
<strong>Notice:</strong> Changing Port or Base Url requires a manual restart of Kavita to take effect.
</div>
<!-- <div class="mb-3">
<label for="settings-cachedir" class="form-label">Cache Directory</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
<ng-template #cacheDirectoryTooltip>Where the server places temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
<span class="visually-hidden" id="settings-cachedir-help">Where the server places temporary files when reading. This will be cleaned up on a regular basis.</span>
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
</div>
</div> -->
<div class="mb-3">
<label for="settings-bookmarksdir" class="form-label">Bookmarks Directory</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
@ -20,25 +22,16 @@
</div>
</div>
<div class="mb-3">
<label for="settings-hostname" class="form-label">Host Name</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
<ng-template #hostNameTooltip>Domain Name (of Reverse Proxy). If set, email generation will always use this.</ng-template>
<span class="visually-hidden" id="settings-hostname-help">Domain Name (of Reverse Proxy). If set, email generation will always use this.</span>
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
Host name must start with http(s) and not end in /
</div>
</div>
</div>
<div class="mb-3">
<label for="settings-baseurl" class="form-label">Base Url</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user.</ng-template>
<span class="visually-hidden" id="settings-cachedir-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user.</span>
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
[class.is-invalid]="settingsForm.get('baseUrl')?.invalid && settingsForm.get('baseUrl')?.touched">
<div class="input-group">
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
[class.is-invalid]="settingsForm.get('baseUrl')?.invalid && settingsForm.get('baseUrl')?.touched">
<button class="btn btn-outline-secondary" (click)="resetBaseUrl()">Reset</button>
</div>
<div id="baseurl-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
<div *ngIf="settingsForm.get('baseUrl')?.errors?.pattern">
Base URL must start and end with /

View file

@ -4,6 +4,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { TagBadgeCursor } from 'src/app/shared/tag-badge/tag-badge.component';
import { ServerService } from 'src/app/_services/server.service';
import { SettingsService } from '../settings.service';
import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component';
import { ServerSettings } from '../_models/server-settings';
@ -27,7 +28,7 @@ export class ManageSettingsComponent implements OnInit {
}
constructor(private settingsService: SettingsService, private toastr: ToastrService,
private modalService: NgbModal) { }
private modalService: NgbModal, private serverService: ServerService) { }
ngOnInit(): void {
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => {
@ -54,6 +55,13 @@ export class ManageSettingsComponent implements OnInit {
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, []));
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
this.serverService.getServerInfo().subscribe(info => {
if (info.isDocker) {
this.settingsForm.get('ipAddresses')?.disable();
this.settingsForm.get('port')?.disable();
}
})
});
}
@ -80,7 +88,7 @@ export class ManageSettingsComponent implements OnInit {
async saveSettings() {
const modelSettings = this.settingsForm.value;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.toastr.success('Server settings updated');
@ -90,7 +98,7 @@ export class ManageSettingsComponent implements OnInit {
}
resetToDefaults() {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.toastr.success('Server settings updated');
@ -100,15 +108,25 @@ export class ManageSettingsComponent implements OnInit {
}
resetIPAddresses() {
this.settingsService.resetIPAddressesSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.settingsService.resetIPAddressesSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings.ipAddresses = settings.ipAddresses;
this.settingsForm.get("ipAddresses")?.setValue(this.serverSettings.ipAddresses);
this.settingsForm.get('ipAddresses')?.setValue(this.serverSettings.ipAddresses);
this.toastr.success('IP Addresses Reset');
}, (err: any) => {
console.error('error: ', err);
});
}
resetBaseUrl() {
this.settingsService.resetBaseUrl().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings.baseUrl = settings.baseUrl;
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
this.toastr.success('Base Url Reset');
}, (err: any) => {
console.error('error: ', err);
});
}
openDirectoryChooser(existingDirectory: string, formControl: string) {
const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg' });
modalRef.componentInstance.startingFolder = existingDirectory || '';

View file

@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs';
import { environment } from 'src/environments/environment';
import { TextResonse } from '../_types/text-response';
import { ServerSettings } from './_models/server-settings';
@ -37,6 +38,10 @@ export class SettingsService {
return this.http.post<ServerSettings>(this.baseUrl + 'settings/reset-ip-addresses', {});
}
resetBaseUrl() {
return this.http.post<ServerSettings>(this.baseUrl + 'settings/reset-base-url', {});
}
resetEmailServerSettings() {
return this.http.post<ServerSettings>(this.baseUrl + 'settings/reset-email-url', {});
}
@ -58,6 +63,6 @@ export class SettingsService {
}
getOpdsEnabled() {
return this.http.get<boolean>(this.baseUrl + 'settings/opds-enabled', TextResonse);
return this.http.get<string>(this.baseUrl + 'settings/opds-enabled', TextResonse).pipe(map(d => d === 'true'));
}
}

View file

@ -59,7 +59,7 @@ form {
border: none;
&:focus-visible {
width: calc(100vw - 175px);
width: calc(100vw - 180px);
}
&:empty {

View file

@ -22,13 +22,11 @@
[showHandToolButton]="true"
[showOpenFileButton]="false"
[showPrintButton]="false"
[showBookmarkButton]="false"
[showRotateButton]="false"
[showDownloadButton]="false"
[showPropertiesButton]="false"
[(zoom)]="zoomSetting"
[showSecondaryToolbarButton]="true"
[showBorders]="true"
[theme]="theme"
[formTheme]="theme"

View file

@ -9,10 +9,11 @@
<input class="form-control custom-input" formControlName="username" id="username" type="text" autofocus placeholder="Username">
</div>
<div class="mb-3">
<div class="mb-2">
<label for="password" class="form-label visually-hidden">Password</label>
<input class="form-control custom-input" formControlName="password" name="password" id="password" type="password" ngModel pattern="^.{6,32}$" placeholder="Password">
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="loginForm.dirty || loginForm.touched">
<input class="form-control custom-input" formControlName="password" name="password"
id="password" type="password" ngModel pattern="^.{6,32}$" placeholder="Password">
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="loginForm.get('password')?.errors?.pattern" >
<div class="" *ngIf="loginForm.get('password')?.errors?.pattern">
Password must be between 6 and 32 characters in length
</div>

View file

@ -1,7 +1,7 @@
.number {
position: absolute;
top: 50%;
left: 50%;
left: 46%;
font-size: 18px;
}

View file

@ -34,7 +34,7 @@ export class CircularLoaderComponent {
/**
* The height in pixels of the loader
*/
@Input() height: string = '100px';
@Input() height: string = '100px';
/**
* Centers the icon in the middle of the loader. Best for card use.
*/

View file

@ -340,8 +340,8 @@
<ng-container *ngIf="tab.fragment === FragmentID.Clients">
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server. This will not affect Tachiyomi users.</div>
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
<p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p>
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
</ng-container>

View file

@ -77,6 +77,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
];
active = this.tabs[1];
opdsEnabled: boolean = false;
baseUrl: string = '';
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
private onDestroy = new Subject<void>();
@ -106,6 +107,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
});
this.settingsService.getServerSettings().subscribe(settings => this.baseUrl = settings.baseUrl);
this.settingsService.getOpdsEnabled().subscribe(res => {
this.opdsEnabled = res;
this.cdRef.markForCheck();
@ -257,7 +260,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
return `${location.origin}${environment.apiUrl}opds/${key}`;
}
return `${location.origin}/api/opds/${key}`;
return `${location.origin}${this.baseUrl}api/opds/${key}`;
}
handleBackgroundColorChange() {

View file

@ -38,6 +38,7 @@
@import './theme/components/carousel';
@import './theme/components/offcanvas';
@import './theme/components/table';
@import './theme/components/alerts';
@import './theme/utilities/utilities';

View file

@ -0,0 +1,5 @@
.alert-warning {
--bs-alert-color: black;
--bs-alert-bg: #fff3cd;
--bs-alert-border-color: #ffecb5
}