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();
}

View file

@ -546,6 +546,15 @@
"title": "Announcements"
},
"out-of-date-modal": {
"title": "Don't fall behind!",
"close": "{{common.close}}",
"subtitle": "It seems your install is more than {{count}} versions behind!",
"description-1": "Please consider upgrading so that you're running the latest version of Kavita.",
"description-2": "Take a look at our <a href='https://wiki.kavitareader.com/guides/updating/' target='_blank' rel='noreferrer noopener'>wiki</a> for instructions on how to update.",
"description-3": "If there is a specific reason you haven't updated yet we would love to know what is keeping you on an outdated version! Stop by our discord and let us know what is blocking your upgrade path."
},
"changelog": {
"installed": "Installed",
"download": "Download",
@ -1190,6 +1199,9 @@
"folder-watching-label": "Folder Watching",
"folder-watching-tooltip": "Allows Kavita to monitor Library Folders to detect changes and invoke scanning on those changes. This allows content to be updated without manually invoking scans or waiting for nightly scans. Will always wait 10 minutes before triggering scan.",
"enable-folder-watching": "Enable Folder Watching",
"host-name-label": "{{manage-email-settings.host-name-label}}",
"host-name-tooltip": "{{manage-email-settings.host-name-tooltip}}",
"host-name-validation": "{{manage-email-settings.host-name-validation}}",
"reset-to-default": "{{common.reset-to-default}}",
@ -1643,7 +1655,8 @@
"last-chapter-added": "Item Added",
"time-to-read": "Time to Read",
"release-year": "Release Year",
"read-progress": "Last Read"
"read-progress": "Last Read",
"average-rating": "Average Rating"
},
"edit-series-modal": {
@ -2029,6 +2042,7 @@
"change-email-no-email": "Email has been updated",
"device-updated": "Device updated",
"device-created": "Device created",
"delete-device": "Are you sure you want to delete this device?",
"confirm-regen-covers": "Refresh covers will force all cover images to be recalculated. This is a heavy operation. Are you sure you don't want to perform a Scan instead?",
"alert-long-running": "This is a long running process. Please give it the time to complete before invoking again.",
"confirm-delete-multiple-series": "Are you sure you want to delete {{count}} series? It will not modify files on disk.",