The Last Polish Pass (#4353)

This commit is contained in:
Joe Milazzo
2026-01-14 13:32:24 -07:00
committed by GitHub
parent 070f990af7
commit 969986e097
25 changed files with 169 additions and 150 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.1.0" />
+8 -8
View File
@@ -53,14 +53,14 @@
<!-- Override vulnerable transitive dependencies -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="NeoSmart.Caching.Sqlite.AspNetCore" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="MailKit" Version="4.14.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -77,11 +77,11 @@
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.22" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.2.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
@@ -105,7 +105,7 @@
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="10.0.1" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Drawing.Common" Version="10.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="TimeZoneConverter" Version="7.2.0" />
+1 -4
View File
@@ -30,7 +30,6 @@ public class LicenseController(
/// </summary>
/// <returns></returns>
[HttpGet("valid-license")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache, VaryByQueryKeys = ["forceCheck"])]
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
{
@@ -53,7 +52,6 @@ public class LicenseController(
/// <returns></returns>
[Authorize(PolicyGroups.AdminPolicy)]
[HttpGet("has-license")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
public async Task<ActionResult<bool>> HasLicense()
{
return Ok(!string.IsNullOrEmpty(
@@ -63,11 +61,10 @@ public class LicenseController(
/// <summary>
/// Asks Kavita+ for the latest license info
/// </summary>
/// <param name="forceCheck">Force checking the API and skip the 8 hour cache</param>
/// <param name="forceCheck">Force checking the API and skip the 8-hour cache</param>
/// <returns></returns>
[Authorize(PolicyGroups.AdminPolicy)]
[HttpGet("info")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache, VaryByQueryKeys = ["forceCheck"])]
public async Task<ActionResult<LicenseInfoDto?>> GetLicenseInfo(bool forceCheck = false)
{
try
@@ -30,7 +30,6 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable,
private readonly ILogger<ReadingSessionService> _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<int, SemaphoreSlim> 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
);
}
+3 -3
View File
@@ -12,9 +12,9 @@
<PackageReference Include="Cronos" Version="0.11.1" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.17.0.131074">
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.18.0.131500">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+36 -29
View File
@@ -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>(DOCUMENT);
private httpClient = inject(HttpClient);
private readonly document = inject<Document>(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<boolean>(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<boolean>(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<boolean>(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 + '');
}
}
+19 -6
View File
@@ -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() {
@@ -142,8 +142,9 @@
<div class="mb-2 col-md-6 col-sm-12">
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('supported-version-label')">
<ng-template #view>
<i class="fas {{licInfo.isValidVersion ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
<span class="visually-hidden">{{isValidVersion ? t('valid') : t('invalid')}]</span>
@let validVersion = licInfo.isValidVersion;
<i class="fas {{validVersion ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
<span class="visually-hidden">{{validVersion ? t('valid') : t('invalid')}]</span>
</i>
</ng-template>
</app-setting-item>
@@ -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 {
+4 -4
View File
@@ -10,8 +10,8 @@
}
<app-nav-header />
@let sideNavVisible = navService.sideNavVisibility$ | async;
@let sideNavCollapsed = navService.sideNavCollapsed$ | async;
@let sideNavVisible = navService.sideNavVisibilitySignal();
@let sideNavCollapsed = navService.sideNavCollapsedSignal();
@let usePreferenceSideNav = navService.usePreferenceSideNav$ | async;
<div [ngClass]="{'closed' : sideNavCollapsed, 'content-wrapper': sideNavVisible}">
@@ -23,11 +23,11 @@
}
}
<div [ngClass]="{'g-0': sideNavVisible === false}">
<div [ngClass]="{'g-0': !sideNavVisible}">
<a id="content"></a>
@if (sideNavVisible) {
<div>
<div class="companion-bar" [ngClass]="{'companion-bar-content': sideNavCollapsed === false || usePreferenceSideNav,
<div class="companion-bar" [ngClass]="{'companion-bar-content': !sideNavCollapsed || usePreferenceSideNav,
'companion-bar-collapsed': (sideNavCollapsed && usePreferenceSideNav)}">
<router-outlet />
</div>
@@ -1,12 +1,12 @@
<ng-container *transloco="let t; prefix: 'nav-header'">
@if (navService.navbarVisible$ | async) {
@if (navService.navbarVisibleSignal()) {
@let user = currentUser();
<nav class="navbar navbar-expand-md navbar-dark fixed-top">
<div class="container-fluid">
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-alt')}}</a>
@if (navService.sideNavVisibility$ | async) {
@if (navService.sideNavVisibilitySignal()) {
<a class="side-nav-toggle" (click)="toggleSideNav($event)"><i class="fas fa-bars" aria-hidden="true"></i></a>
}
@@ -1,4 +1,4 @@
import {AsyncPipe, DOCUMENT} from '@angular/common';
import {DOCUMENT} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@@ -55,7 +55,7 @@ import {BreakpointService} from "../../../_services/breakpoint.service";
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink, RouterLinkActive, GroupedTypeaheadComponent, ImageComponent,
SeriesFormatComponent, EventsWidgetComponent, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem,
AsyncPipe, SentenceCasePipe, TranslocoDirective, CollectionOwnerComponent, PromotedIconComponent, QuillViewComponent, ProfileIconComponent]
SentenceCasePipe, TranslocoDirective, CollectionOwnerComponent, PromotedIconComponent, QuillViewComponent, ProfileIconComponent]
})
export class NavHeaderComponent {
@@ -190,6 +190,7 @@ export class NavHeaderComponent {
}
toggleSideNav(event: any) {
console.log('nav-header: toggling side nav');
event.stopPropagation();
this.navService.toggleSideNav();
}
@@ -1,24 +1,26 @@
@let isSideNavCollapsed = navService.sideNavCollapsedSignal();
@if (link === undefined || link.length === 0) {
<div class="side-nav-item" [ngClass]="{'closed': ((navService.sideNavCollapsed$ | async)), 'active': highlighted}">
<div class="side-nav-item" [ngClass]="{'closed': isSideNavCollapsed, 'active': highlighted}">
<ng-container [ngTemplateOutlet]="inner" />
</div>
} @else {
@if (external) {
<a [id]="id" class="side-nav-item" [href]="link" [ngClass]="{'closed': ((navService.sideNavCollapsed$ | async)), 'active': highlighted}" rel="noopener noreferrer" target="_blank">
<a [id]="id" class="side-nav-item" [href]="link" [ngClass]="{'closed': isSideNavCollapsed, 'active': highlighted}" rel="noopener noreferrer" target="_blank">
<ng-container [ngTemplateOutlet]="inner" />
</a>
} @else {
@if (queryParams && queryParams !== {}) {
<a [id]="id" class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': ((navService.sideNavCollapsed$ | async)), 'active': highlighted}" (click)="openLink()">
<a [id]="id" class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': isSideNavCollapsed, 'active': highlighted}" (click)="openLink()">
<ng-container [ngTemplateOutlet]="inner" />
</a>
} @else if (fragment) {
<a [id]="id" class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': ((navService.sideNavCollapsed$ | async)), 'active': highlighted}" [routerLink]="link" [fragment]="fragment">
<a [id]="id" class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': isSideNavCollapsed, 'active': highlighted}" [routerLink]="link" [fragment]="fragment">
<ng-container [ngTemplateOutlet]="inner" />
</a>
} @else {
<a [id]="id" class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': ((navService.sideNavCollapsed$ | async)), 'active': highlighted}" [routerLink]="link" [queryParams]="queryParams">
<a [id]="id" class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': isSideNavCollapsed, 'active': highlighted}" [routerLink]="link" [queryParams]="queryParams">
<ng-container [ngTemplateOutlet]="inner" />
</a>
}
@@ -3,7 +3,7 @@ import {NavigationEnd, Router, RouterLink} from '@angular/router';
import {filter, map, tap} from 'rxjs';
import {NavService} from 'src/app/_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {AsyncPipe, NgClass, NgTemplateOutlet} from "@angular/common";
import {NgClass, NgTemplateOutlet} from "@angular/common";
import {ImageComponent} from "../../../shared/image/image.component";
import {UtilityService} from "../../../shared/_services/utility.service";
import {BreakpointService} from "../../../_services/breakpoint.service";
@@ -11,7 +11,7 @@ import {BreakpointService} from "../../../_services/breakpoint.service";
@Component({
selector: 'app-side-nav-item',
imports: [RouterLink, ImageComponent, NgTemplateOutlet, NgClass, AsyncPipe],
imports: [RouterLink, ImageComponent, NgTemplateOutlet, NgClass],
templateUrl: './side-nav-item.component.html',
styleUrls: ['./side-nav-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -161,9 +161,8 @@ export class SideNavItemComponent implements OnInit {
// If on mobile, automatically collapse the side nav after making a selection
collapseNavIfApplicable() {
if (this.breakpointService.isMobile()) {
this.navService.collapseSideNav(true);
}
if (!this.breakpointService.isMobile()) return;
this.navService.collapseSideNav(true);
}
}
@@ -1,7 +1,11 @@
<ng-container *transloco="let t; prefix: 'side-nav'">
@let sideNavVisible = navService.sideNavVisibilitySignal();
@let sideNavCollapsed = navService.sideNavCollapsedSignal();
@if (accountService.currentUser$ | async; as user) {
<div class="side-nav-container" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async),
'hidden': (navService.sideNavVisibility$ | async) === false,
<div class="side-nav-container" [ngClass]="{'closed' : sideNavCollapsed,
'hidden': sideNavVisible === false,
'no-donate': (licenseService.hasValidLicense$ | async) === true}">
<div class="side-nav" cdkDropList (cdkDropListDropped)="reorderDrop($event)">
@@ -21,7 +25,7 @@
[title]="t('customize')" link="/settings"
[fragment]="SettingsTabId.Customize" />
}
@if (streams.length > ItemLimit && (navService.sideNavCollapsed$ | async) === false && !editMode) {
@if (streams.length > ItemLimit && sideNavCollapsed === false && !editMode) {
<div cdkDrag cdkDragDisabled class="mb-2 mt-3 ms-2 me-2">
<label for="filter" class="form-label visually-hidden">{{t('filter-label')}}</label>
<div class="form-group position-relative">
@@ -103,11 +107,11 @@
}
@if (breakpointService.isTabletOrBelow()) {
<div class="side-nav-overlay" (click)="toggleNavBar()" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async)}"></div>
<div class="side-nav-overlay" (click)="toggleNavBar()" [ngClass]="{'closed' : sideNavCollapsed}"></div>
}
<div class="sidenav-bottom" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async),
'hidden': (navService.sideNavVisibility$ | async) === false || (licenseService.hasValidLicense$ | async) === true}">
<div class="sidenav-bottom" [ngClass]="{'closed' : sideNavCollapsed,
'hidden': sideNavVisible === false || (licenseService.hasValidLicense$ | async) === true}">
@if ((licenseService.hasValidLicense$ | async) === false) {
<app-side-nav-item [ngClass]="'donate'" icon="fa-heart"
[ngbTooltip]="t('donate-tooltip')"
@@ -124,19 +124,17 @@ export class SideNavComponent implements OnInit {
takeUntilDestroyed(this.destroyRef),
)
).pipe(
startWith(null),
filter(data => data !== null),
takeUntilDestroyed(this.destroyRef),
startWith(null),
filter(data => data !== null),
takeUntilDestroyed(this.destroyRef),
);
collapseSideNavOnMobileNav$ = this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
filter(() => this.breakpointService.isMobile()),
switchMap(() => this.navService.sideNavCollapsed$),
take(1),
filter(collapsed => !collapsed)
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
filter(() => this.breakpointService.isMobile() && this.navService.sideNavCollapsedSignal()),
filter(collapsed => !collapsed)
);
@@ -147,8 +145,7 @@ export class SideNavComponent implements OnInit {
}
this.collapseSideNavOnMobileNav$.subscribe(() => {
this.navService.collapseSideNav(false);
this.cdRef.markForCheck();
this.navService.collapseSideNav(false);
});
this.keyBindService.registerListener(
@@ -5,7 +5,8 @@ import {
Component,
DestroyRef,
effect,
inject
inject,
untracked
} from '@angular/core';
import {TranslocoDirective} from "@jsverse/transloco";
import {AsyncPipe, DOCUMENT, NgClass} from "@angular/common";
@@ -214,8 +215,10 @@ export class PreferenceNavComponent implements AfterViewInit {
if (!navEvent) return;
if (this.breakpointService.isAboveMobile()) return;
if (this.navService.sideNavCollapsedSignal()) return;
const isCollapsed = untracked(() => this.navService.sideNavCollapsedSignal());
if (isCollapsed) return;
this.navService.collapseSideNav(true);
});
@@ -15,7 +15,7 @@
>
<ngx-datatable-column prop="name" [sortable]="true" [draggable]="false" [resizeable]="false">
<ng-template ngx-datatable-header-template>
{{t('year-header')}}
{{t('status-header')}}
</ng-template>
<ng-template let-value="value" ngx-datatable-cell-template>
{{value}}
@@ -175,7 +175,6 @@ export class ServerStatsStatsTabComponent {
return;
}
console.log('ran release year encode code')
// Build a map of unique decade ranges to encode
const decadeMap = new Map<string, StatBucket>();
for (const item of items) {
@@ -54,21 +54,21 @@
@switch (uploadMode()) {
@case ('all') {
<div class="row g-0 mt-3 pb-3">
<div class="mx-auto">
<div class="row g-0 mb-3">
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
<div class="mx-auto text-center">
<div class="mb-3">
<i class="fa fa-file-upload" style="font-size: 24px;" aria-hidden="true"></i>
</div>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
<a class="pe-0" href="javascript:void(0)" (click)="uploadMode.set('url')">
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="ps-1 pe-1"></span>
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
</div>
<div class="d-flex justify-content-center align-items-center gap-2">
<a href="javascript:void(0)" (click)="uploadMode.set('url')">
<span class="d-none d-md-inline">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="text-muted"></span>
<span class="d-none d-md-inline">{{t('drag-n-drop')}}</span>
<span class="d-none d-md-inline text-muted"></span>
<a href="javascript:void(0)" (click)="openFileSelector()">
{{t('upload')}}<span class="d-none d-md-inline"> {{t('upload-continued')}}</span>
</a>
</div>
</div>
</div>
@@ -15,7 +15,7 @@
<div class="row g-0 ">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller mb-2 mb-sm-0">
<div class="col-lg-3 col-md-5 col-sm-4 col-xs-4 scroller mb-2 mb-sm-0">
<div class="pe-sm-2">
@if (readingProfiles.length < virtualScrollerBreakPoint) {
@@ -32,7 +32,7 @@
</div>
</div>
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-0 ps-sm-3">
<div class="col-lg-9 col-md-7 col-sm-7 col-xs-7 ps-0 ps-sm-3">
<div class="card p-3">
@if (selectedProfile === null) {
<p class="ps-2">{{t('no-selected')}}</p>
@@ -266,7 +266,7 @@ export class ManageReadingProfilesComponent implements OnInit {
this.cdRef.markForCheck();
}),
catchError(err => {
console.log(err);
console.error(err);
this.toastr.error(err.message);
return of(null);
@@ -285,7 +285,7 @@ export class ManageReadingProfilesComponent implements OnInit {
this.cdRef.markForCheck();
}),
catchError(err => {
console.log(err);
console.error(err);
this.toastr.error(err.message);
return of(null);
@@ -1,7 +1,7 @@
<ng-container *transloco="let t; prefix:'theme-manager'">
<div class="position-relative">
@if ((hasAdmin$ | async)) {
@if (accountService.isAdmin()) {
<button class="btn btn-outline-primary position-absolute custom-position" (click)="selectTheme(undefined)" [title]="t('add')">
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('add')}}</span>
</button>
@@ -43,7 +43,7 @@
<div class="mx-auto">
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
@if (hasAdmin$ | async) {
@if (accountService.isAdmin()) {
{{t('preview-default-admin')}}
} @else {
{{t('preview-default')}}
@@ -56,7 +56,7 @@
@if (files && files.length > 0) {
<app-loading [loading]="isUploadingTheme" />
} @else if (hasAdmin$ | async) {
} @else if (accountService.isAdmin()) {
<ngx-file-drop (onFileDrop)="dropped($event)" [accept]="acceptableExtensions" [directory]="false"
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
@@ -87,14 +87,14 @@
{{selectedTheme.name | sentenceCase}}
<div class="float-end">
@if (selectedTheme.isSiteTheme) {
@if (selectedTheme.name !== 'Dark' && (canUseThemes$ | async)) {
@if (selectedTheme.name !== 'Dark' && canUseThemes()) {
<button class="btn btn-danger me-1" (click)="deleteTheme(selectedTheme.site!)">{{t('delete')}}</button>
}
@if (hasAdmin$ | async) {
@if (accountService.isAdmin()) {
<button class="btn btn-secondary me-1" [disabled]="selectedTheme.site?.isDefault" (click)="updateDefault(selectedTheme.site!)">{{t('set-default')}}</button>
}
<button class="btn btn-primary me-1" [disabled]="currentTheme && selectedTheme.name === currentTheme.name" (click)="applyTheme(selectedTheme.site!)">{{t('apply')}}</button>
} @else if (canUseThemes$ | async) {
} @else if (canUseThemes()) {
<button class="btn btn-primary" [disabled]="selectedTheme.downloadable?.alreadyDownloaded" (click)="downloadTheme(selectedTheme.downloadable!)">{{t('download')}}</button>
}
</div>
@@ -1,15 +1,14 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject,} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, DestroyRef, inject,} from '@angular/core';
import {ToastrService} from 'ngx-toastr';
import {distinctUntilChanged, map, take, tap} from 'rxjs';
import {distinctUntilChanged, tap} from 'rxjs';
import {ThemeService} from 'src/app/_services/theme.service';
import {SiteTheme, ThemeProvider} from 'src/app/_models/preferences/site-theme';
import {User} from 'src/app/_models/user/user';
import {AccountService} from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe';
import {AsyncPipe, NgTemplateOutlet} from '@angular/common';
import {NgTemplateOutlet} from '@angular/common';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {shareReplay} from "rxjs/operators";
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
import {ImageComponent} from "../../shared/image/image.component";
import {DownloadableSiteTheme} from "../../_models/theme/downloadable-site-theme";
@@ -35,14 +34,14 @@ interface ThemeContainer {
templateUrl: './theme-manager.component.html',
styleUrls: ['./theme-manager.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe, SentenceCasePipe, TranslocoDirective, CarouselReelComponent,
ImageComponent, DefaultValuePipe, NgTemplateOutlet, NgxFileDropModule,
ReactiveFormsModule, LoadingComponent]
imports: [SentenceCasePipe, TranslocoDirective, CarouselReelComponent,
ImageComponent, DefaultValuePipe, NgTemplateOutlet, NgxFileDropModule,
ReactiveFormsModule, LoadingComponent]
})
export class ThemeManagerComponent {
private readonly destroyRef = inject(DestroyRef);
protected readonly themeService = inject(ThemeService);
private readonly accountService = inject(AccountService);
protected readonly accountService = inject(AccountService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly confirmService = inject(ConfirmService);
@@ -56,24 +55,17 @@ export class ThemeManagerComponent {
selectedTheme: ThemeContainer | undefined;
downloadableThemes: Array<DownloadableSiteTheme> = [];
downloadedThemes: Array<SiteTheme> = [];
hasAdmin$ = this.accountService.currentUser$.pipe(
takeUntilDestroyed(this.destroyRef),
map(c => c && this.accountService.hasAdminRole(c)),
shareReplay({refCount: true, bufferSize: 1}),
);
canUseThemes$ = this.accountService.currentUser$.pipe(
takeUntilDestroyed(this.destroyRef),
map(c => c && !this.accountService.hasReadOnlyRole(c)),
shareReplay({refCount: true, bufferSize: 1}),
);
canUseThemes = computed(() => {
const user = this.accountService.currentUserSignal();
if (!user) return false;
return !this.accountService.hasReadOnlyRole(user);
});
files: NgxFileDropEntry[] = [];
acceptableExtensions = ['.css'].join(',');
isUploadingTheme: boolean = false;
constructor() {
this.themeService.themes$.pipe(tap(themes => {
@@ -115,17 +107,18 @@ export class ThemeManagerComponent {
}
applyTheme(theme: SiteTheme) {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (!user) return;
const pref = Object.assign({}, user.preferences);
pref.theme = theme;
this.accountService.updatePreferences(pref).subscribe();
// Updating theme emits the new theme to load on the themes$
});
const user = this.accountService.currentUserSignal();
if (!user) return;
// Updating theme emits the new theme to load on the themes$
const pref = Object.assign({}, user.preferences);
pref.theme = theme;
this.accountService.updatePreferences(pref).subscribe();
}
updateDefault(theme: SiteTheme) {
this.themeService.setDefault(theme.id).subscribe(() => {
// TODO: Refactor this key to be in toasts
this.toastr.success(translate('theme-manager.updated-toastr', {name: theme.name}));
});
}
+1 -1
View File
@@ -2717,7 +2717,7 @@
"title": "Publication Status",
"visualisation-label": "Visualisation",
"data-table-label": "Data Table",
"year-header": "Year",
"status-header": "{{manage-matched-metadata.status-header}}",
"count-header": "Count",
"no-data": "{{common.no-data}}"
},