From 969986e0970d04d07686181f02730ebaca488d2a Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Wed, 14 Jan 2026 13:32:24 -0700 Subject: [PATCH] The Last Polish Pass (#4353) --- API.Tests/API.Tests.csproj | 2 +- API/API.csproj | 16 ++--- API/Controllers/LicenseController.cs | 5 +- API/Services/Reading/ReadingSessionService.cs | 7 +- Kavita.Common/Kavita.Common.csproj | 6 +- UI/Web/src/app/_services/nav.service.ts | 65 ++++++++++--------- UI/Web/src/app/_services/theme.service.ts | 25 +++++-- .../app/admin/license/license.component.html | 5 +- .../app/admin/license/license.component.ts | 8 ++- UI/Web/src/app/app.component.html | 8 +-- .../nav-header/nav-header.component.html | 4 +- .../nav-header/nav-header.component.ts | 5 +- .../side-nav-item.component.html | 12 ++-- .../side-nav-item/side-nav-item.component.ts | 9 ++- .../side-nav/side-nav.component.html | 16 +++-- .../side-nav/side-nav.component.ts | 21 +++--- .../preference-nav.component.ts | 7 +- .../publication-status-stats.component.html | 2 +- .../server-stats-stats-tab.component.ts | 1 - .../font-manager/font-manager.component.html | 26 ++++---- .../manage-reading-profiles.component.html | 4 +- .../manage-reading-profiles.component.ts | 4 +- .../theme-manager.component.html | 12 ++-- .../theme-manager/theme-manager.component.ts | 47 ++++++-------- UI/Web/src/assets/langs/en.json | 2 +- 25 files changed, 169 insertions(+), 150 deletions(-) diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 86fc1101a..098676370 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/API/API.csproj b/API/API.csproj index 9cbc93238..1658d7be3 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -53,14 +53,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -77,11 +77,11 @@ - - - - - + + + + + @@ -105,7 +105,7 @@ - + diff --git a/API/Controllers/LicenseController.cs b/API/Controllers/LicenseController.cs index f58b79dfb..9e946b222 100644 --- a/API/Controllers/LicenseController.cs +++ b/API/Controllers/LicenseController.cs @@ -30,7 +30,6 @@ public class LicenseController( /// /// [HttpGet("valid-license")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache, VaryByQueryKeys = ["forceCheck"])] public async Task> HasValidLicense(bool forceCheck = false) { @@ -53,7 +52,6 @@ public class LicenseController( /// [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("has-license")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)] public async Task> HasLicense() { return Ok(!string.IsNullOrEmpty( @@ -63,11 +61,10 @@ public class LicenseController( /// /// Asks Kavita+ for the latest license info /// - /// Force checking the API and skip the 8 hour cache + /// Force checking the API and skip the 8-hour cache /// [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("info")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache, VaryByQueryKeys = ["forceCheck"])] public async Task> GetLicenseInfo(bool forceCheck = false) { try diff --git a/API/Services/Reading/ReadingSessionService.cs b/API/Services/Reading/ReadingSessionService.cs index 9fe5cfc08..b921b8658 100644 --- a/API/Services/Reading/ReadingSessionService.cs +++ b/API/Services/Reading/ReadingSessionService.cs @@ -30,7 +30,6 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, private readonly ILogger _logger; private readonly HybridCache _cache; private readonly TimeSpan _sessionTimeout; - private readonly TimeSpan _pollInterval; private readonly Timer _cleanupTimer; private readonly SemaphoreSlim _cleanupLock = new(1, 1); private static readonly ConcurrentDictionary UserLocks = new(); @@ -53,13 +52,13 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, _logger = logger; _cache = cache; _sessionTimeout = sessionTimeout ?? TimeSpan.FromMinutes(10); - _pollInterval = pollInterval ?? TimeSpan.FromMinutes(5); + var pollInterval1 = pollInterval ?? TimeSpan.FromMinutes(5); _cleanupTimer = new Timer( callback: _ => _ = RunCleanupAsync(), state: null, - dueTime: _pollInterval, - period: _pollInterval + dueTime: pollInterval1, + period: pollInterval1 ); } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 604e5eb62..086114d0e 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -12,9 +12,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index 6a2f4bea6..32d813510 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -1,6 +1,6 @@ import {DOCUMENT} from '@angular/common'; -import {DestroyRef, inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2} from '@angular/core'; -import {filter, ReplaySubject, take} from 'rxjs'; +import {DestroyRef, inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2, signal} from '@angular/core'; +import {filter, take} from 'rxjs'; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; @@ -8,7 +8,7 @@ import {TextResonse} from "../_types/text-response"; import {AccountService} from "./account.service"; import {map} from "rxjs/operators"; import {NavigationEnd, Router} from "@angular/router"; -import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop"; +import {takeUntilDestroyed, toObservable} from "@angular/core/rxjs-interop"; import {WikiLink} from "../_models/wiki"; import {AuthGuard} from "../_guards/auth.guard"; @@ -32,14 +32,14 @@ interface NavItem { providedIn: 'root' }) export class NavService { - private document = inject(DOCUMENT); - private httpClient = inject(HttpClient); - - + private readonly document = inject(DOCUMENT); + private readonly httpClient = inject(HttpClient); private readonly accountService = inject(AccountService); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); + private readonly renderer: Renderer2; + private readonly baseUrl = environment.apiUrl; public localStorageSideNavKey = 'kavita--sidenav--expanded'; public navItems: NavItem[] = [ @@ -73,25 +73,36 @@ export class NavService { } ] - private navbarVisibleSource = new ReplaySubject(1); + private navBarVisible = signal(false); /** * If the top Nav bar is rendered or not */ - navbarVisible$ = this.navbarVisibleSource.asObservable(); + navbarVisibleSignal = this.navBarVisible.asReadonly(); + /** + * If the top Nav bar is rendered or not + */ + navbarVisible$ = toObservable(this.navBarVisible); - private sideNavCollapseSource = new ReplaySubject(1); + + private sideNavCollapsed = signal(false); /** * If the Side Nav is in a collapsed state or not. */ - sideNavCollapsed$ = this.sideNavCollapseSource.asObservable(); - sideNavCollapsedSignal = toSignal(this.sideNavCollapsed$, {initialValue: false}); + sideNavCollapsedSignal = this.sideNavCollapsed.asReadonly(); + /** + * If the Side Nav is in a collapsed state or not. + */ + sideNavCollapsed$ = toObservable(this.sideNavCollapsed); - private sideNavVisibilitySource = new ReplaySubject(1); + private sideNavVisibility = signal(false); /** * If the side nav is rendered or not into the DOM. */ - sideNavVisibility$ = this.sideNavVisibilitySource.asObservable(); - sideNavVisibilitySignal = toSignal(this.sideNavVisibility$, {initialValue: false}) + sideNavVisibilitySignal = this.sideNavVisibility.asReadonly(); + /** + * If the side nav is rendered or not into the DOM. + */ + sideNavVisibility$ = toObservable(this.sideNavVisibility); usePreferenceSideNav$ = this.router.events.pipe( filter(event => event instanceof NavigationEnd), @@ -105,8 +116,7 @@ export class NavService { takeUntilDestroyed(this.destroyRef), ); - private renderer: Renderer2; - baseUrl = environment.apiUrl; + constructor() { const rendererFactory = inject(RendererFactory2); @@ -121,7 +131,7 @@ export class NavService { }); const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false; - this.sideNavCollapseSource.next(sideNavState); + this.sideNavCollapsed.set(sideNavState); this.showSideNav(); } @@ -164,7 +174,7 @@ export class NavService { this.renderer.setStyle(bodyElem, 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); this.renderer.setStyle(bodyElem, 'overflow', 'hidden'); this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); - this.navbarVisibleSource.next(true); + this.navBarVisible.set(true); }, 10); } @@ -179,7 +189,7 @@ export class NavService { this.renderer.setStyle(bodyElem, 'scrollbar-gutter', 'initial', RendererStyleFlags2.Important); this.renderer.removeStyle(this.document.querySelector('html'), 'height'); this.renderer.setStyle(bodyElem, 'overflow', 'auto'); - this.navbarVisibleSource.next(false); + this.navBarVisible.set(false); }, 10); } @@ -208,27 +218,24 @@ export class NavService { * Shows the side nav. When being visible, the side nav will automatically return to previous collapsed state. */ showSideNav() { - this.sideNavVisibilitySource.next(true); + this.sideNavVisibility.set(true); } /** * Hides the side nav. This is useful for the readers and login page. */ hideSideNav() { - this.sideNavVisibilitySource.next(false); + this.sideNavVisibility.set(false); } toggleSideNav() { - this.sideNavCollapseSource.pipe(take(1)).subscribe(val => { - if (val === undefined) val = false; - const newVal = !(val || false); - this.sideNavCollapseSource.next(newVal); - localStorage.setItem(this.localStorageSideNavKey, newVal + ''); - }); + const newValue = !this.sideNavCollapsed(); + this.sideNavCollapsed.set(newValue); + localStorage.setItem(this.localStorageSideNavKey, newValue + ''); } collapseSideNav(isCollapsed: boolean) { - this.sideNavCollapseSource.next(isCollapsed); + this.sideNavCollapsed.set(isCollapsed); localStorage.setItem(this.localStorageSideNavKey, isCollapsed + ''); } } diff --git a/UI/Web/src/app/_services/theme.service.ts b/UI/Web/src/app/_services/theme.service.ts index fb1c6e800..8a1d3191c 100644 --- a/UI/Web/src/app/_services/theme.service.ts +++ b/UI/Web/src/app/_services/theme.service.ts @@ -88,9 +88,9 @@ export class ThemeService { const evt = (message.payload as SiteThemeUpdatedEvent); this.currentTheme$.pipe(take(1)).subscribe(currentTheme => { if (currentTheme && currentTheme.name !== EVENTS.SiteThemeProgress) return; + console.log('Active theme has been updated, refreshing theme'); this.setTheme(currentTheme.name); - }); } }); @@ -234,10 +234,7 @@ export class ThemeService { this.setTheme('dark'); return; } - const styleElem = this.document.createElement('style'); - styleElem.id = 'theme-' + theme.name; - styleElem.appendChild(this.document.createTextNode(content)); - this.renderer.appendChild(this.document.head, styleElem); + this.injectStyleNode(theme, content); // Check if the theme has --theme-color and apply it to meta tag const themeColor = this.getThemeColor(); @@ -283,7 +280,23 @@ export class ThemeService { } private unsetThemes() { - this.themeCache.forEach(theme => this.document.body.classList.remove(theme.selector)); + this.themeCache.forEach(theme => { + this.document.body.classList.remove(theme.selector); + + if (theme.provider === ThemeProvider.System) return; + // Remove the injected style (unless it's Dark) + const styleElem = this.document.querySelector('style#theme-' + theme.name); + if (styleElem) { + this.renderer.removeChild(this.document.head, styleElem); + } + }); + } + + private injectStyleNode(theme: SiteTheme, content: string) { + const styleElem = this.document.createElement('style'); + styleElem.id = 'theme-' + theme.name; + styleElem.appendChild(this.document.createTextNode(content)); + this.renderer.appendChild(this.document.head, styleElem); } private unsetBookThemes() { diff --git a/UI/Web/src/app/admin/license/license.component.html b/UI/Web/src/app/admin/license/license.component.html index 1c303b4b4..a1a9afd5f 100644 --- a/UI/Web/src/app/admin/license/license.component.html +++ b/UI/Web/src/app/admin/license/license.component.html @@ -142,8 +142,9 @@
- - {{isValidVersion ? t('valid') : t('invalid')}] + @let validVersion = licInfo.isValidVersion; + + {{validVersion ? t('valid') : t('invalid')}] diff --git a/UI/Web/src/app/admin/license/license.component.ts b/UI/Web/src/app/admin/license/license.component.ts index ce8afc425..185bb06ce 100644 --- a/UI/Web/src/app/admin/license/license.component.ts +++ b/UI/Web/src/app/admin/license/license.component.ts @@ -62,10 +62,14 @@ export class LicenseComponent implements OnInit { ngOnInit(): void { - this.loadLicenseInfo().subscribe(); + this.loadLicenseInfo(); } loadLicenseInfo(forceCheck = false) { + this.getLicenseInfoObservable(forceCheck).subscribe(); + } + + getLicenseInfoObservable(forceCheck = false) { this.isChecking.set(true); return this.licenseService.hasAnyLicense() @@ -110,7 +114,7 @@ export class LicenseComponent implements OnInit { this.isViewMode.set(true); this.isSaving.set(false); this.cdRef.markForCheck(); - this.loadLicenseInfo().subscribe(async (info) => { + this.getLicenseInfoObservable().subscribe(async (info) => { if (info?.isActive && !hadActiveLicenseBefore) { await this.confirmService.info(translate('license.k+-unlocked-description'), translate('license.k+-unlocked')); } else { diff --git a/UI/Web/src/app/app.component.html b/UI/Web/src/app/app.component.html index 79870044a..b05dae600 100644 --- a/UI/Web/src/app/app.component.html +++ b/UI/Web/src/app/app.component.html @@ -10,8 +10,8 @@ } - @let sideNavVisible = navService.sideNavVisibility$ | async; - @let sideNavCollapsed = navService.sideNavCollapsed$ | async; + @let sideNavVisible = navService.sideNavVisibilitySignal(); + @let sideNavCollapsed = navService.sideNavCollapsedSignal(); @let usePreferenceSideNav = navService.usePreferenceSideNav$ | async;
@@ -23,11 +23,11 @@ } } -
+
@if (sideNavVisible) {
-
diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html index f8833aaa4..e9ded3340 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html @@ -1,12 +1,12 @@ - @if (navService.navbarVisible$ | async) { + @if (navService.navbarVisibleSignal()) { @let user = currentUser();