mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-12-13 00:25:08 -05:00
More Polish (#4098)
Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
parent
91a7e189ce
commit
dbd2250bb4
@ -67,6 +67,11 @@ public sealed record AnnotationDto
|
|||||||
|
|
||||||
public required int OwnerUserId { get; set; }
|
public required int OwnerUserId { get; set; }
|
||||||
public string OwnerUsername { get; set; }
|
public string OwnerUsername { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The age rating of the series this annotation is linked to
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Not required when creating/updating an annotation, this is added in flight</remarks>
|
||||||
|
public AgeRating AgeRating { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedUtc { get; set; }
|
public DateTime CreatedUtc { get; set; }
|
||||||
public DateTime LastModifiedUtc { get; set; }
|
public DateTime LastModifiedUtc { get; set; }
|
||||||
|
|||||||
@ -328,8 +328,8 @@ public class UserRepository : IUserRepository
|
|||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true)
|
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true)
|
||||||
{
|
{
|
||||||
var query = _context.AppUser
|
var query = _context.AppUser.Includes(includeFlags);
|
||||||
.Includes(includeFlags);
|
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
return await query.ToListAsync();
|
return await query.ToListAsync();
|
||||||
|
|||||||
@ -394,12 +394,14 @@ public class AutoMapperProfiles : Profile
|
|||||||
.ForMember(dest => dest.OwnerUsername, opt => opt.MapFrom(src => src.AppUser.UserName))
|
.ForMember(dest => dest.OwnerUsername, opt => opt.MapFrom(src => src.AppUser.UserName))
|
||||||
.ForMember(dest => dest.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId))
|
.ForMember(dest => dest.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId))
|
||||||
.ForMember(dest => dest.SeriesName, opt => opt.MapFrom(src => src.Series.Name))
|
.ForMember(dest => dest.SeriesName, opt => opt.MapFrom(src => src.Series.Name))
|
||||||
.ForMember(dest => dest.LibraryName, opt => opt.MapFrom(src => src.Library.Name));
|
.ForMember(dest => dest.LibraryName, opt => opt.MapFrom(src => src.Library.Name))
|
||||||
|
.ForMember(dest => dest.AgeRating, opt => opt.MapFrom(src => src.Series.Metadata.AgeRating));
|
||||||
|
|
||||||
CreateMap<AppUserAnnotation, FullAnnotationDto>()
|
CreateMap<AppUserAnnotation, FullAnnotationDto>()
|
||||||
.ForMember(dest => dest.SeriesName, opt => opt.MapFrom(src => src.Series.Name))
|
.ForMember(dest => dest.SeriesName, opt => opt.MapFrom(src => src.Series.Name))
|
||||||
.ForMember(dest => dest.VolumeName, opt => opt.MapFrom(src => src.Chapter.Volume.Name))
|
.ForMember(dest => dest.VolumeName, opt => opt.MapFrom(src => src.Chapter.Volume.Name))
|
||||||
.ForMember(dest => dest.LibraryName, opt => opt.MapFrom(src => src.Library.Name));
|
.ForMember(dest => dest.LibraryName, opt => opt.MapFrom(src => src.Library.Name))
|
||||||
|
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.AppUserId));
|
||||||
|
|
||||||
CreateMap<OidcConfigDto, OidcPublicConfigDto>();
|
CreateMap<OidcConfigDto, OidcPublicConfigDto>();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,12 @@ namespace API.Middleware;
|
|||||||
|
|
||||||
public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions ExceptionJsonSerializeOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
};
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -27,13 +33,7 @@ public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddlewa
|
|||||||
|
|
||||||
var response = new ApiException(context.Response.StatusCode, errorMessage, ex.StackTrace);
|
var response = new ApiException(context.Response.StatusCode, errorMessage, ex.StackTrace);
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
var json = JsonSerializer.Serialize(response, ExceptionJsonSerializeOptions);
|
||||||
{
|
|
||||||
PropertyNamingPolicy =
|
|
||||||
JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(response, options);
|
|
||||||
|
|
||||||
await context.Response.WriteAsync(json);
|
await context.Response.WriteAsync(json);
|
||||||
|
|
||||||
|
|||||||
@ -156,11 +156,6 @@ public class AnnotationService(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get users with preferences for highlight colors
|
|
||||||
var users = (await unitOfWork.UserRepository
|
|
||||||
.GetAllUsersAsync(AppUserIncludes.UserPreferences))
|
|
||||||
.ToDictionary(u => u.Id, u => u);
|
|
||||||
|
|
||||||
// Get all annotations for the user with related data
|
// Get all annotations for the user with related data
|
||||||
IList<FullAnnotationDto> annotations;
|
IList<FullAnnotationDto> annotations;
|
||||||
if (annotationIds == null)
|
if (annotationIds == null)
|
||||||
@ -172,6 +167,15 @@ public class AnnotationService(
|
|||||||
annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds);
|
annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userIds = annotations.Select(a => a.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Get users with preferences for highlight colors
|
||||||
|
var users = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences, false))
|
||||||
|
.Where(u => userIds.Contains(u.Id))
|
||||||
|
.ToDictionary(u => u.Id, u => u);
|
||||||
|
|
||||||
// Get settings for hostname
|
// Get settings for hostname
|
||||||
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
var hostname = !string.IsNullOrWhiteSpace(settings.HostName) ? settings.HostName : "http://localhost:5000";
|
var hostname = !string.IsNullOrWhiteSpace(settings.HostName) ? settings.HostName : "http://localhost:5000";
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
|
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import {Observable, throwError} from 'rxjs';
|
||||||
import { Router } from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import { catchError } from 'rxjs/operators';
|
import {catchError} from 'rxjs/operators';
|
||||||
import { AccountService } from '../_services/account.service';
|
import {AccountService} from '../_services/account.service';
|
||||||
import {translate, TranslocoService} from "@jsverse/transloco";
|
import {translate, TranslocoService} from "@jsverse/transloco";
|
||||||
import {AuthGuard} from "../_guards/auth.guard";
|
import {AuthGuard} from "../_guards/auth.guard";
|
||||||
import {APP_BASE_HREF} from "@angular/common";
|
import {APP_BASE_HREF} from "@angular/common";
|
||||||
@ -40,6 +40,9 @@ export class ErrorInterceptor implements HttpInterceptor {
|
|||||||
case 500:
|
case 500:
|
||||||
this.handleServerException(error);
|
this.handleServerException(error);
|
||||||
break;
|
break;
|
||||||
|
case 413:
|
||||||
|
this.handlePayloadTooLargeException(error);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Don't throw multiple Something unexpected went wrong
|
// Don't throw multiple Something unexpected went wrong
|
||||||
const genericError = translate('errors.generic');
|
const genericError = translate('errors.generic');
|
||||||
@ -100,6 +103,10 @@ export class ErrorInterceptor implements HttpInterceptor {
|
|||||||
this.toast('errors.not-found');
|
this.toast('errors.not-found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handlePayloadTooLargeException(error: any) {
|
||||||
|
this.toast('errors.upload-too-large');
|
||||||
|
}
|
||||||
|
|
||||||
private handleServerException(error: any) {
|
private handleServerException(error: any) {
|
||||||
const err = error.error;
|
const err = error.error;
|
||||||
if (err.hasOwnProperty('message') && err.message.trim() !== '') {
|
if (err.hasOwnProperty('message') && err.message.trim() !== '') {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
|
|||||||
import {Action} from "./action-factory.service";
|
import {Action} from "./action-factory.service";
|
||||||
import {LicenseService} from "./license.service";
|
import {LicenseService} from "./license.service";
|
||||||
import {LocalizationService} from "./localization.service";
|
import {LocalizationService} from "./localization.service";
|
||||||
|
import {Annotation} from "../book-reader/_models/annotations/annotation";
|
||||||
|
|
||||||
export enum Role {
|
export enum Role {
|
||||||
Admin = 'Admin',
|
Admin = 'Admin',
|
||||||
@ -194,6 +195,46 @@ export class AccountService {
|
|||||||
return this.httpClient.get<Role[]>(this.baseUrl + 'account/roles');
|
return this.httpClient.get<Role[]>(this.baseUrl + 'account/roles');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should likes be displayed for the given annotation
|
||||||
|
* @param annotation
|
||||||
|
*/
|
||||||
|
showAnnotationLikes(annotation: Annotation) {
|
||||||
|
const user = this.currentUserSignal();
|
||||||
|
if (!user) return false;
|
||||||
|
|
||||||
|
const shareAnnotations = user.preferences.socialPreferences.shareAnnotations;
|
||||||
|
return this.isSocialFeatureEnabled(shareAnnotations, annotation.libraryId, annotation.ageRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given social feature is enabled in a library with associated age rating on the entity
|
||||||
|
* @param feature
|
||||||
|
* @param activeLibrary
|
||||||
|
* @param ageRating
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private isSocialFeatureEnabled(feature: boolean, activeLibrary: number, ageRating: AgeRating) {
|
||||||
|
const user = this.currentUserSignal();
|
||||||
|
if (!user || !feature) return false;
|
||||||
|
|
||||||
|
const socialPreferences = user.preferences.socialPreferences;
|
||||||
|
|
||||||
|
const libraryAllowed = socialPreferences.socialLibraries.length === 0 ||
|
||||||
|
socialPreferences.socialLibraries.includes(activeLibrary);
|
||||||
|
|
||||||
|
if (!libraryAllowed || socialPreferences.socialMaxAgeRating === AgeRating.NotApplicable) {
|
||||||
|
return libraryAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socialPreferences.socialIncludeUnknowns) {
|
||||||
|
return socialPreferences.socialMaxAgeRating >= ageRating;
|
||||||
|
}
|
||||||
|
|
||||||
|
return socialPreferences.socialMaxAgeRating >= ageRating && ageRating !== AgeRating.Unknown;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
login(model: {username: string, password: string, apiKey?: string}) {
|
login(model: {username: string, password: string, apiKey?: string}) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<ng-container *transloco="let t; prefix: 'annotation-card'">
|
<ng-container *transloco="let t; prefix: 'annotation-card'">
|
||||||
@if (visible()) {
|
@if (visible() && accountService.showAnnotationLikes(annotation())) {
|
||||||
<button (click)="handleLikeChange()" class="ms-2 btn-unstyled"
|
<button (click)="handleLikeChange()" class="ms-2 btn-unstyled"
|
||||||
[class.clickable]="annotation().ownerUserId !== accountService.userId()"
|
[class.clickable]="annotation().ownerUserId !== accountService.userId()"
|
||||||
[attr.aria-label]="liked()
|
[attr.aria-label]="liked()
|
||||||
|
|||||||
@ -38,8 +38,6 @@ export class EpubHighlightComponent {
|
|||||||
if (!updateEvent || !annotation || updateEvent.annotation.id !== annotation.id) return;
|
if (!updateEvent || !annotation || updateEvent.annotation.id !== annotation.id) return;
|
||||||
if (updateEvent.type !== 'edit') return;
|
if (updateEvent.type !== 'edit') return;
|
||||||
|
|
||||||
console.log('[highlight] annotation updated', annotation);
|
|
||||||
|
|
||||||
this.annotation.set(annotations.filter(a => a.id === annotation.id)[0]);
|
this.annotation.set(annotations.filter(a => a.id === annotation.id)[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
(mousedown)="mouseDown($event)" >
|
(mousedown)="mouseDown($event)" >
|
||||||
|
|
||||||
<div #readingHtml class="book-content {{layoutMode() | columnLayoutClass}} {{writingStyle() | writingStyleClass}}"
|
<div #readingHtml class="book-content {{layoutMode() | columnLayoutClass}} {{writingStyle() | writingStyleClass}}"
|
||||||
[ngStyle]="{'max-height': columnHeight(), 'max-width': verticalBookContentWidth(), 'width': verticalBookContentWidth(), 'column-width': columnWidth()}"
|
[ngStyle]="{'max-height': columnHeight(), 'max-width': verticalBookContentWidth(), 'width': verticalBookContentWidth(), 'column-width': columnWidth(), 'padding-bottom': bookContentPaddingBottom()}"
|
||||||
[ngClass]="{'immersive': immersiveMode() && actionBarVisible, 'debug': debugMode()}"
|
[ngClass]="{'immersive': immersiveMode() && actionBarVisible, 'debug': debugMode()}"
|
||||||
[innerHtml]="page()" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)" (wheel)="onWheel($event)"></div>
|
[innerHtml]="page()" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)" (wheel)="onWheel($event)"></div>
|
||||||
|
|
||||||
@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
<div class="d-flex align-items-center right-group">
|
<div class="d-flex align-items-center right-group">
|
||||||
@if (!this.adhocPageHistory.isEmpty()) {
|
@if (!this.adhocPageHistory.isEmpty()) {
|
||||||
<button class="btn btn-outline-secondary btn-icon me-1" (click)="goBack()" [title]="t('go-back')">
|
<button class="btn btn-secondary btn-icon me-1" (click)="goBack()" [title]="t('go-back')">
|
||||||
<i class="fa fa-reply" aria-hidden="true"></i>
|
<i class="fa fa-reply" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@ -107,7 +107,7 @@
|
|||||||
<button class="btn btn-secondary btn-icon me-1" (click)="viewAnnotations()">
|
<button class="btn btn-secondary btn-icon me-1" (click)="viewAnnotations()">
|
||||||
<i class="fa-solid fa-highlighter" aria-hidden="true"></i>
|
<i class="fa-solid fa-highlighter" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-icon" (click)="viewToCDrawer()">
|
<button class="btn btn-secondary btn-icon me-1" (click)="viewToCDrawer()">
|
||||||
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-icon me-1" (click)="closeReader()">
|
<button class="btn btn-secondary btn-icon me-1" (click)="closeReader()">
|
||||||
|
|||||||
@ -392,6 +392,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
protected verticalBookContentWidth!: Signal<string>;
|
protected verticalBookContentWidth!: Signal<string>;
|
||||||
protected virtualizedPageNum!: Signal<number>;
|
protected virtualizedPageNum!: Signal<number>;
|
||||||
protected virtualizedMaxPages!: Signal<number>;
|
protected virtualizedMaxPages!: Signal<number>;
|
||||||
|
protected bookContentPaddingBottom = computed(() => {
|
||||||
|
const layoutMode = this.layoutMode();
|
||||||
|
if (layoutMode !== BookPageLayoutMode.Default) return '0px';
|
||||||
|
return '40px';
|
||||||
|
});
|
||||||
|
|
||||||
pageWidthForPagination = computed(() => {
|
pageWidthForPagination = computed(() => {
|
||||||
const layoutMode = this.layoutMode();
|
const layoutMode = this.layoutMode();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import {AgeRating} from "../../../_models/metadata/age-rating";
|
||||||
|
|
||||||
export interface Annotation {
|
export interface Annotation {
|
||||||
id: number;
|
id: number;
|
||||||
@ -28,5 +29,5 @@ export interface Annotation {
|
|||||||
|
|
||||||
seriesName: string;
|
seriesName: string;
|
||||||
libraryName: string;
|
libraryName: string;
|
||||||
|
ageRating: AgeRating;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2715,6 +2715,7 @@
|
|||||||
"delete-font-in-use": "Font is currently in use by at least one user, cannot delete",
|
"delete-font-in-use": "Font is currently in use by at least one user, cannot delete",
|
||||||
"font-manual-upload": "There was an issue creating Font from manual upload",
|
"font-manual-upload": "There was an issue creating Font from manual upload",
|
||||||
"font-already-in-use": "Font already exists by that name",
|
"font-already-in-use": "Font already exists by that name",
|
||||||
|
"upload-too-large": "The file is too large for upload, select a smaller image and try again.",
|
||||||
"import-fields": {
|
"import-fields": {
|
||||||
"non-unique-age-ratings": "Age rating mapping keys aren't unique, please correct your import file",
|
"non-unique-age-ratings": "Age rating mapping keys aren't unique, please correct your import file",
|
||||||
"non-unique-fields": "Field mappings do not have a unique id, please correct your import file"
|
"non-unique-fields": "Field mappings do not have a unique id, please correct your import file"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user