mirror of
https://github.com/Kareadita/Kavita.git
synced 2026-06-06 14:55:19 -04:00
The Last Polish Pass (#4353)
This commit is contained in:
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 + '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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}}
|
||||
|
||||
-1
@@ -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) {
|
||||
|
||||
+13
-13
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+2
-2
@@ -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}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user