Sort by Average Rating and Big Want to Read fix (#2672)

This commit is contained in:
Joe Milazzo 2024-02-01 06:23:45 -06:00 committed by GitHub
parent 03e7d38482
commit 1fd72ada36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 3552 additions and 105 deletions

View file

@ -21,6 +21,10 @@ export enum SortField {
TimeToRead = 5,
ReleaseYear = 6,
ReadProgress = 7,
/**
* Kavita+ only
*/
AverageRating = 8
}
export const allSortFields = Object.keys(SortField)

View file

@ -27,6 +27,8 @@ export class SortFieldPipe implements PipeTransform {
return this.translocoService.translate('sort-field-pipe.release-year');
case SortField.ReadProgress:
return this.translocoService.translate('sort-field-pipe.read-progress');
case SortField.AverageRating:
return this.translocoService.translate('sort-field-pipe.average-rating');
}
}

View file

@ -39,11 +39,16 @@ export class ServerService {
}
checkForUpdate() {
return this.http.get<UpdateVersionEvent>(this.baseUrl + 'server/check-update', {});
return this.http.get<UpdateVersionEvent | null>(this.baseUrl + 'server/check-update');
}
checkHowOutOfDate() {
return this.http.get<string>(this.baseUrl + 'server/checkHowOutOfDate', TextResonse)
.pipe(map(r => parseInt(r, 10)));
}
checkForUpdates() {
return this.http.get(this.baseUrl + 'server/check-for-updates', {});
return this.http.get<UpdateVersionEvent>(this.baseUrl + 'server/check-for-updates', {});
}
getChangelog() {

View file

@ -40,7 +40,7 @@ export class ManageEmailSettingsComponent implements OnInit {
ngOnInit(): void {
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, []));
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
this.settingsForm.addControl('host', new FormControl(this.serverSettings.smtpConfig.host, []));
this.settingsForm.addControl('port', new FormControl(this.serverSettings.smtpConfig.port, []));

View file

@ -5,6 +5,21 @@
<strong>{{t('notice')}}</strong> {{t('restart-required')}}
</div>
<div class="mb-3">
<label for="settings-hostname" class="form-label">{{t('host-name-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
<ng-template #hostNameTooltip>{{t('host-name-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-hostname-help">
<ng-container [ngTemplateOutlet]="hostNameTooltip"></ng-container>
</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">
{{t('host-name-validation')}}
</div>
</div>
</div>
<div class="mb-3">
<label for="settings-baseurl" class="form-label">{{t('base-url-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
<ng-template #baseUrlTooltip>{{t('base-url-tooltip')}}</ng-template>

View file

@ -104,7 +104,6 @@ export class ManageSettingsComponent implements OnInit {
modelSettings.smtpConfig = this.serverSettings.smtpConfig;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();

View file

@ -21,7 +21,6 @@ export class ChangelogComponent implements OnInit {
constructor(private serverService: ServerService) { }
ngOnInit(): void {
this.serverService.getChangelog().subscribe(updates => {
this.updates = updates;
this.isLoading = false;

View file

@ -0,0 +1,18 @@
<ng-container *transloco="let t; read:'out-of-date-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div>
<div class="modal-body">
<p><strong>{{t('subtitle', {count: versionsOutOfDate})}}</strong></p>
<p>{{t('description-1')}}</p>
<p [innerHTML]="t('description-2') | safeHtml"></p>
<p>{{t('description-3')}}</p>
</div>
<div class="modal-footer">
<a class="btn btn-link me-1" href="https://discord.gg/b52wT37kt7" target="_blank" rel="noreferrer noopener">Discord</a>
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
</div>
</ng-container>

View file

@ -0,0 +1,38 @@
import {Component, DestroyRef, inject, Input} from '@angular/core';
import {FormsModule} from "@angular/forms";
import {AsyncPipe, NgForOf, NgIf} from "@angular/common";
import {NgbActiveModal, NgbHighlight, NgbModal, NgbTypeahead} from "@ng-bootstrap/ng-bootstrap";
import {TranslocoDirective} from "@ngneat/transloco";
import {ServerService} from "../../../_services/server.service";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {map} from "rxjs/operators";
import {ChangelogComponent} from "../changelog/changelog.component";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
@Component({
selector: 'app-out-of-date-modal',
standalone: true,
imports: [
FormsModule,
NgForOf,
NgIf,
NgbHighlight,
NgbTypeahead,
TranslocoDirective,
AsyncPipe,
ChangelogComponent,
SafeHtmlPipe
],
templateUrl: './out-of-date-modal.component.html',
styleUrl: './out-of-date-modal.component.scss'
})
export class OutOfDateModalComponent {
private readonly ngbModal = inject(NgbActiveModal);
@Input({required: true}) versionsOutOfDate: number = 0;
close() {
this.ngbModal.close();
}
}

View file

@ -13,6 +13,8 @@ import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.compon
import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {ServerService} from "./_services/server.service";
import {ImportCblModalComponent} from "./reading-list/_modals/import-cbl-modal/import-cbl-modal.component";
import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component";
@Component({
selector: 'app-root',
@ -67,15 +69,6 @@ export class AppComponent implements OnInit {
});
// Every hour, have the UI check for an update. People seriously stay out of date
// interval(60 * 60 * 1000) // 60 minutes in milliseconds
// .pipe(
// switchMap(() => this.accountService.currentUser$),
// filter(u => u !== undefined && this.accountService.hasAdminRole(u)),
// switchMap(_ => this.serverService.checkForUpdates())
// )
// .subscribe();
this.transitionState$ = this.accountService.currentUser$.pipe(
tap(user => {
@ -111,11 +104,21 @@ export class AppComponent implements OnInit {
// On load, make an initial call for valid license
this.accountService.hasValidLicense().subscribe();
interval(4 * 60 * 60 * 1000) // 4 hours in milliseconds
// Every hour, have the UI check for an update. People seriously stay out of date
interval(2* 60 * 60 * 1000) // 2 hours in milliseconds
.pipe(
switchMap(() => this.accountService.currentUser$),
filter(u => this.accountService.hasAdminRole(u!)),
switchMap(_ => this.serverService.checkForUpdates())
filter(u => u !== undefined && this.accountService.hasAdminRole(u)),
switchMap(_ => this.serverService.checkHowOutOfDate()),
filter(versionOutOfDate => {
return !isNaN(versionOutOfDate) && versionOutOfDate > 2;
}),
tap(versionOutOfDate => {
if (!this.ngbModal.hasOpenModals()) {
const ref = this.ngbModal.open(OutOfDateModalComponent, {size: 'xl', fullscreen: 'md'});
ref.componentInstance.versionsOutOfDate = 3;
}
})
)
.subscribe();
}

View file

@ -288,7 +288,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
if (this.activeTabId === TabID.Chapters) chapterArray = this.chapters;
// We must augment chapter indices as Bulk Selection assumes all on one page, but Storyline has mixed
const chapterIndexModifier = this.activeTabId === TabID.Storyline ? this.volumes.length + 1 : 0;
const chapterIndexModifier = this.activeTabId === TabID.Storyline ? this.volumes.length : 0;
const selectedChapterIds = chapterArray.filter((_chapter, index: number) => {
const mappedIndex = index + chapterIndexModifier;
return selectedChapterIndexes.includes(mappedIndex + '');

View file

@ -12,8 +12,9 @@ import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
import { NgIf, NgFor } from '@angular/common';
import { EditDeviceComponent } from '../edit-device/edit-device.component';
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import {TranslocoDirective} from "@ngneat/transloco";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {SettingsService} from "../../admin/settings.service";
import {ConfirmService} from "../../shared/confirm.service";
@Component({
selector: 'app-manage-devices',
@ -28,6 +29,7 @@ export class ManageDevicesComponent implements OnInit {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly deviceService = inject(DeviceService);
private readonly settingsService = inject(SettingsService);
private readonly confirmService = inject(ConfirmService);
devices: Array<Device> = [];
addDeviceIsCollapsed: boolean = true;
@ -53,7 +55,8 @@ export class ManageDevicesComponent implements OnInit {
});
}
deleteDevice(device: Device) {
async deleteDevice(device: Device) {
if (!await this.confirmService.confirm(translate('toasts.delete-device'))) return;
this.deviceService.deleteDevice(device.id).subscribe(() => {
const index = this.devices.indexOf(device);
this.devices.splice(index, 1);

View file

@ -156,7 +156,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.accountService.hasValidLicense$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
if (res) {
this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling});
if (this.tabs.filter(t => t.fragment == FragmentID.Scrobbling).length === 0) {
this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling});
}
this.hasActiveLicense = true;
this.cdRef.markForCheck();
}