- @if (navService.sideNavVisibility$ | async) {
+ @if (sideNavVisible) {
-
diff --git a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts
index 9c9ab5732..3ca9c7007 100644
--- a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts
+++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts
@@ -18,7 +18,7 @@ import {
SimpleChanges,
ViewChild
} from '@angular/core';
-import {BehaviorSubject, fromEvent, map, Observable, of, ReplaySubject} from 'rxjs';
+import {BehaviorSubject, fromEvent, map, Observable, of, ReplaySubject, tap} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
import {ScrollService} from 'src/app/_services/scroll.service';
import {ReaderService} from '../../../_services/reader.service';
@@ -37,6 +37,14 @@ import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
*/
const SPACER_SCROLL_INTO_PX = 200;
+/**
+ * Default debounce time from scroll and scrollend event listeners
+ */
+const DEFAULT_SCROLL_DEBOUNCE = 20;
+/**
+ * Safari does not support the scrollEnd event, we can use scroll event with higher debounce time to emulate it
+ */
+const EMULATE_SCROLL_END_DEBOUNCE = 100;
/**
* Bitwise enums for configuring how much debug information we want
@@ -220,14 +228,27 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
* gets promoted to fullscreen.
*/
initScrollHandler() {
- //console.log('Setting up Scroll handler on ', this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body);
- fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll')
- .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
- .subscribe((event) => this.handleScrollEvent(event));
+ const element = this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body;
- fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
- .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
- .subscribe((event) => this.handleScrollEndEvent(event));
+ fromEvent(element, 'scroll')
+ .pipe(
+ debounceTime(DEFAULT_SCROLL_DEBOUNCE),
+ takeUntilDestroyed(this.destroyRef),
+ tap((event) => this.handleScrollEvent(event))
+ )
+ .subscribe();
+
+ const isScrollEndSupported = 'onscrollend' in document;
+ const scrollEndEvent = isScrollEndSupported ? 'scrollend' : 'scroll';
+ const scrollEndDebounce = isScrollEndSupported ? DEFAULT_SCROLL_DEBOUNCE : EMULATE_SCROLL_END_DEBOUNCE;
+
+ fromEvent(element, scrollEndEvent)
+ .pipe(
+ debounceTime(scrollEndDebounce),
+ takeUntilDestroyed(this.destroyRef),
+ tap((event) => this.handleScrollEndEvent(event))
+ )
+ .subscribe();
}
ngOnInit(): void {
@@ -629,6 +650,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
* Move to the next chapter and set the page
*/
moveToNextChapter() {
+ if (!this.allImagesLoaded) return;
+
this.setPageNum(this.totalPages);
this.loadNextChapter.emit();
}
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts
index 9483afdb3..96fb15804 100644
--- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts
+++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts
@@ -1571,6 +1571,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
setPageNum(pageNum: number) {
+ if (pageNum === this.pageNum) return;
+
this.pageNum = Math.max(Math.min(pageNum, this.maxPages - 1), 0);
this.pageNumSubject.next({pageNum: this.pageNum, maxPages: this.maxPages});
this.cdRef.markForCheck();
diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html
index 6421205ab..2a62a7cd6 100644
--- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html
+++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html
@@ -39,7 +39,7 @@
diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts
index 7ce6f6790..f71d70674 100644
--- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts
+++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts
@@ -4,7 +4,7 @@ import {MangaFormat} from 'src/app/_models/manga-format';
import {ReadingListItem} from 'src/app/_models/reading-list';
import {ImageService} from 'src/app/_services/image.service';
import {NgbProgressbar} from '@ng-bootstrap/ng-bootstrap';
-import {DatePipe} from '@angular/common';
+import {APP_BASE_HREF, DatePipe} from '@angular/common';
import {ImageComponent} from '../../../shared/image/image.component';
import {TranslocoDirective} from "@jsverse/transloco";
import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component";
@@ -22,6 +22,7 @@ export class ReadingListItemComponent {
protected readonly imageService = inject(ImageService);
protected readonly MangaFormat = MangaFormat;
+ protected readonly baseUrl = inject(APP_BASE_HREF);
@Input({required: true}) item!: ReadingListItem;
@Input() position: number = 0;
diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts
index 34df77358..9b70e79f3 100644
--- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts
+++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts
@@ -141,7 +141,7 @@ export class LibrarySettingsModalComponent implements OnInit {
get IsKavitaPlusEligible() {
const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType;
- return allKavitaPlusScrobbleEligibleTypes.includes(libType);
+ return allKavitaPlusMetadataApplicableTypes.includes(libType);
}
get IsMetadataDownloadEligible() {
diff --git a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts
index 769b384f5..3296cdebe 100644
--- a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts
+++ b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts
@@ -1,4 +1,4 @@
-import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnInit} from '@angular/core';
+import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit, signal} from '@angular/core';
import {ReadingProfileService} from "../../_services/reading-profile.service";
import {
bookLayoutModes,
@@ -19,7 +19,7 @@ import {NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
import {User} from "../../_models/user";
import {AccountService} from "../../_services/account.service";
-import {debounceTime, distinctUntilChanged, take, tap} from "rxjs/operators";
+import {debounceTime, distinctUntilChanged, map, take, tap} from "rxjs/operators";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import {BookService} from "../../book-reader/_services/book.service";
@@ -42,7 +42,7 @@ import {SettingSwitchComponent} from "../../settings/_components/setting-switch/
import {WritingStylePipe} from "../../_pipes/writing-style.pipe";
import {ColorPickerDirective} from "ngx-color-picker";
import {NgbNav, NgbNavContent, NgbNavItem, NgbNavLinkBase, NgbNavOutlet, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
-import {filter} from "rxjs";
+import {catchError, filter, finalize, of, switchMap} from "rxjs";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {ToastrService} from "ngx-toastr";
@@ -95,8 +95,19 @@ enum TabId {
})
export class ManageReadingProfilesComponent implements OnInit {
+ private readonly readingProfileService = inject(ReadingProfileService);
+ private readonly cdRef = inject(ChangeDetectorRef);
+ private readonly accountService = inject(AccountService);
+ private readonly bookService = inject(BookService);
+ private readonly destroyRef = inject(DestroyRef);
+ private readonly toastr = inject(ToastrService);
+ private readonly confirmService = inject(ConfirmService);
+ private readonly transLoco = inject(TranslocoService);
+
virtualScrollerBreakPoint = 20;
+ savingProfile = signal(false);
+
fontFamilies: Array
= [];
readingProfiles: ReadingProfile[] = [];
user!: User;
@@ -111,16 +122,7 @@ export class ManageReadingProfilesComponent implements OnInit {
return d;
});
- constructor(
- private readingProfileService: ReadingProfileService,
- private cdRef: ChangeDetectorRef,
- private accountService: AccountService,
- private bookService: BookService,
- private destroyRef: DestroyRef,
- private toastr: ToastrService,
- private confirmService: ConfirmService,
- private transLoco: TranslocoService,
- ) {
+ constructor() {
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
this.cdRef.markForCheck();
}
@@ -219,41 +221,49 @@ export class ManageReadingProfilesComponent implements OnInit {
this.readingProfileForm.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(),
+ filter(_ => !this.savingProfile()),
filter(_ => this.readingProfileForm!.valid),
takeUntilDestroyed(this.destroyRef),
- tap(_ => this.autoSave()),
+ tap(_ => this.savingProfile.set(true)),
+ switchMap(_ => this.autoSave()),
+ finalize(() => this.savingProfile.set(false))
).subscribe();
}
private autoSave() {
if (this.selectedProfile!.id == 0) {
- this.readingProfileService.createProfile(this.packData()).subscribe({
- next: createdProfile => {
+ return this.readingProfileService.createProfile(this.packData()).pipe(
+ tap(createdProfile => {
this.selectedProfile = createdProfile;
this.readingProfiles.push(createdProfile);
this.cdRef.markForCheck();
- },
- error: err => {
+ }),
+ catchError(err => {
console.log(err);
this.toastr.error(err.message);
- }
- })
- } else {
- const profile = this.packData();
- this.readingProfileService.updateProfile(profile).subscribe({
- next: newProfile => {
- this.readingProfiles = this.readingProfiles.map(p => {
- if (p.id !== profile.id) return p;
- return newProfile;
- });
- this.cdRef.markForCheck();
- },
- error: err => {
- console.log(err);
- this.toastr.error(err.message);
- }
- })
+
+ return of(null);
+ })
+ );
}
+
+ const profile = this.packData();
+ return this.readingProfileService.updateProfile(profile).pipe(
+ tap(newProfile => {
+ this.readingProfiles = this.readingProfiles.map(p => {
+ if (p.id !== profile.id) return p;
+
+ return newProfile;
+ });
+ this.cdRef.markForCheck();
+ }),
+ catchError(err => {
+ console.log(err);
+ this.toastr.error(err.message);
+
+ return of(null);
+ })
+ );
}
private packData(): ReadingProfile {
diff --git a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html
index 37c833e99..09899980a 100644
--- a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html
+++ b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html
@@ -17,7 +17,7 @@
- {{item.seriesName}}
+ {{item.seriesName}}
diff --git a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.ts b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.ts
index 6d992175d..7b701f784 100644
--- a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.ts
+++ b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.ts
@@ -6,6 +6,7 @@ import {ImageComponent} from "../../shared/image/image.component";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
import {ScrobbleHold} from "../../_models/scrobbling/scrobble-hold";
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
+import {APP_BASE_HREF} from "@angular/common";
@Component({
selector: 'app-user-holds',
@@ -20,6 +21,7 @@ export class ScrobblingHoldsComponent {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly scrobblingService = inject(ScrobblingService);
protected readonly imageService = inject(ImageService);
+ protected readonly baseUrl = inject(APP_BASE_HREF);
isLoading = true;
data: Array = [];
diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json
index 0fd552d40..22ec016c5 100644
--- a/UI/Web/src/assets/langs/en.json
+++ b/UI/Web/src/assets/langs/en.json
@@ -29,7 +29,7 @@
"manual-save-label": "Changing provider settings requires a manual save",
"authority-label": "Authority",
- "authority-tooltip": "The URL to your OIDC provider",
+ "authority-tooltip": "The URL to your OIDC provider, do not include the .well-known/openid-configuration path",
"client-id-label": "Client ID",
"client-id-tooltip": "The ClientID set in your OIDC provider, can be anything",
"secret-label": "Client secret",
@@ -1186,7 +1186,7 @@
"type-label": "Type",
"type-tooltip": "Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Check the wiki for more details on the differences between the library types.",
"kavitaplus-eligible-label": "Kavita+ Eligible",
- "kavitaplus-eligible-tooltip": "Supports Kavita+ metadata features or Scrobbling",
+ "kavitaplus-eligible-tooltip": "Supports Kavita+ metadata features and/or Scrobbling",
"folder-description": "Add folders to your library",
"browse": "Browse for Media Folders",
"help-us-part-1": "Help us out by following ",
@@ -1724,7 +1724,9 @@
"no-data": "There are no other users.",
"loading": "{{common.loading}}",
"actions-header": "Actions",
- "pending-tooltip": "This user has not validated their email"
+ "pending-tooltip": "This user has not validated their email",
+ "identity-provider-oidc-tooltip": "OIDC",
+ "identity-provider-native-tooltip": "Native"
},
"role-localized-pipe": {