More Polish (#4098)

Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2025-10-14 09:25:15 -05:00 committed by GitHub
parent 91a7e189ce
commit dbd2250bb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 93 additions and 29 deletions

View File

@ -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; }

View File

@ -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();

View File

@ -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>();
}

View File

@ -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);

View File

@ -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";

View File

@ -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() !== '') {

View File

@ -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}) {

View File

@ -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()

View File

@ -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]);
});
}

View File

@ -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()">

View File

@ -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();

View File

@ -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;
}

View File

@ -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"