mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-10-24 15:28:58 -04: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 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 LastModifiedUtc { get; set; }
|
||||
|
||||
@ -328,8 +328,8 @@ public class UserRepository : IUserRepository
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true)
|
||||
{
|
||||
var query = _context.AppUser
|
||||
.Includes(includeFlags);
|
||||
var query = _context.AppUser.Includes(includeFlags);
|
||||
|
||||
if (track)
|
||||
{
|
||||
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.OwnerUserId, opt => opt.MapFrom(src => src.AppUserId))
|
||||
.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>()
|
||||
.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.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>();
|
||||
}
|
||||
|
||||
@ -11,6 +11,12 @@ namespace API.Middleware;
|
||||
|
||||
public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
|
||||
private static readonly JsonSerializerOptions ExceptionJsonSerializeOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
@ -27,13 +33,7 @@ public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddlewa
|
||||
|
||||
var response = new ApiException(context.Response.StatusCode, errorMessage, ex.StackTrace);
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy =
|
||||
JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(response, options);
|
||||
var json = JsonSerializer.Serialize(response, ExceptionJsonSerializeOptions);
|
||||
|
||||
await context.Response.WriteAsync(json);
|
||||
|
||||
|
||||
@ -156,11 +156,6 @@ public class AnnotationService(
|
||||
{
|
||||
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
|
||||
IList<FullAnnotationDto> annotations;
|
||||
if (annotationIds == null)
|
||||
@ -172,6 +167,15 @@ public class AnnotationService(
|
||||
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
|
||||
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var hostname = !string.IsNullOrWhiteSpace(settings.HostName) ? settings.HostName : "http://localhost:5000";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||
import {Observable, throwError} from 'rxjs';
|
||||
import {Router} from '@angular/router';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {catchError} from 'rxjs/operators';
|
||||
import {AccountService} from '../_services/account.service';
|
||||
import {translate, TranslocoService} from "@jsverse/transloco";
|
||||
import {AuthGuard} from "../_guards/auth.guard";
|
||||
import {APP_BASE_HREF} from "@angular/common";
|
||||
@ -40,6 +40,9 @@ export class ErrorInterceptor implements HttpInterceptor {
|
||||
case 500:
|
||||
this.handleServerException(error);
|
||||
break;
|
||||
case 413:
|
||||
this.handlePayloadTooLargeException(error);
|
||||
break;
|
||||
default:
|
||||
// Don't throw multiple Something unexpected went wrong
|
||||
const genericError = translate('errors.generic');
|
||||
@ -100,6 +103,10 @@ export class ErrorInterceptor implements HttpInterceptor {
|
||||
this.toast('errors.not-found');
|
||||
}
|
||||
|
||||
private handlePayloadTooLargeException(error: any) {
|
||||
this.toast('errors.upload-too-large');
|
||||
}
|
||||
|
||||
private handleServerException(error: any) {
|
||||
const err = error.error;
|
||||
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 {LicenseService} from "./license.service";
|
||||
import {LocalizationService} from "./localization.service";
|
||||
import {Annotation} from "../book-reader/_models/annotations/annotation";
|
||||
|
||||
export enum Role {
|
||||
Admin = 'Admin',
|
||||
@ -194,6 +195,46 @@ export class AccountService {
|
||||
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}) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<ng-container *transloco="let t; prefix: 'annotation-card'">
|
||||
@if (visible()) {
|
||||
@if (visible() && accountService.showAnnotationLikes(annotation())) {
|
||||
<button (click)="handleLikeChange()" class="ms-2 btn-unstyled"
|
||||
[class.clickable]="annotation().ownerUserId !== accountService.userId()"
|
||||
[attr.aria-label]="liked()
|
||||
|
||||
@ -38,8 +38,6 @@ export class EpubHighlightComponent {
|
||||
if (!updateEvent || !annotation || updateEvent.annotation.id !== annotation.id) return;
|
||||
if (updateEvent.type !== 'edit') return;
|
||||
|
||||
console.log('[highlight] annotation updated', annotation);
|
||||
|
||||
this.annotation.set(annotations.filter(a => a.id === annotation.id)[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
(mousedown)="mouseDown($event)" >
|
||||
|
||||
<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()}"
|
||||
[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">
|
||||
@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>
|
||||
</button>
|
||||
}
|
||||
@ -107,7 +107,7 @@
|
||||
<button class="btn btn-secondary btn-icon me-1" (click)="viewAnnotations()">
|
||||
<i class="fa-solid fa-highlighter" aria-hidden="true"></i>
|
||||
</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>
|
||||
</button>
|
||||
<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 virtualizedPageNum!: Signal<number>;
|
||||
protected virtualizedMaxPages!: Signal<number>;
|
||||
protected bookContentPaddingBottom = computed(() => {
|
||||
const layoutMode = this.layoutMode();
|
||||
if (layoutMode !== BookPageLayoutMode.Default) return '0px';
|
||||
return '40px';
|
||||
});
|
||||
|
||||
pageWidthForPagination = computed(() => {
|
||||
const layoutMode = this.layoutMode();
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import {AgeRating} from "../../../_models/metadata/age-rating";
|
||||
|
||||
export interface Annotation {
|
||||
id: number;
|
||||
@ -28,5 +29,5 @@ export interface Annotation {
|
||||
|
||||
seriesName: 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",
|
||||
"font-manual-upload": "There was an issue creating Font from manual upload",
|
||||
"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": {
|
||||
"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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user