Fixed up code comments for Amelia.

Fixed a bug where not all detail pages had the same size font.

Fixed series detail page not having subtitle as a themeable variable (--detail-subtitle-color).
This commit is contained in:
Joseph Milazzo 2025-05-07 19:01:03 -05:00
parent bbea28fd05
commit 9844503671
41 changed files with 200 additions and 106 deletions

View file

@ -32,11 +32,7 @@ public class PersonServiceTests: AbstractDbTest
Name= "Delores Casey", Name= "Delores Casey",
NormalizedName = "Delores Casey".ToNormalized(), NormalizedName = "Delores Casey".ToNormalized(),
Description = "Hi, I'm Delores Casey!", Description = "Hi, I'm Delores Casey!",
Aliases = [new PersonAlias Aliases = [new PersonAliasBuilder("Casey, Delores").Build()],
{
Alias = "Casey, Delores",
NormalizedAlias = "Casey, Delores".ToNormalized(),
}],
AniListId = 27, AniListId = 27,
}; };

View file

@ -8,6 +8,7 @@ using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;

View file

@ -6,9 +6,9 @@ using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering; using API.DTOs.Filtering;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.DTOs.Recommendation; using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities.Enums; using API.Entities.Enums;

View file

@ -15,6 +15,7 @@ using API.DTOs.CollectionTags;
using API.DTOs.Filtering; using API.DTOs.Filtering;
using API.DTOs.Filtering.v2; using API.DTOs.Filtering.v2;
using API.DTOs.OPDS; using API.DTOs.OPDS;
using API.DTOs.Person;
using API.DTOs.Progress; using API.DTOs.Progress;
using API.DTOs.Search; using API.DTOs.Search;
using API.Entities; using API.Entities;

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.DTOs.Person;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Helpers; using API.Helpers;

View file

@ -4,7 +4,7 @@ using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs.Person;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;

View file

@ -63,6 +63,7 @@ public class SearchController : BaseApiController
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList(); var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList();
if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted")); if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted"));

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.Person;
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Metadata; namespace API.DTOs.Metadata;

View file

@ -1,4 +1,6 @@
namespace API.DTOs; using API.DTOs.Person;
namespace API.DTOs;
/// <summary> /// <summary>
/// Used to browse writers and click in to see their series /// Used to browse writers and click in to see their series

View file

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace API.DTOs; namespace API.DTOs.Person;
#nullable enable #nullable enable
public class PersonDto public class PersonDto
@ -13,6 +13,7 @@ public class PersonDto
public string? SecondaryColor { get; set; } public string? SecondaryColor { get; set; }
public string? CoverImage { get; set; } public string? CoverImage { get; set; }
public List<string> Aliases { get; set; } = [];
public string? Description { get; set; } public string? Description { get; set; }
/// <summary> /// <summary>

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.Person;
namespace API.DTOs.ReadingLists; namespace API.DTOs.ReadingLists;

View file

@ -2,6 +2,7 @@
using API.DTOs.Collection; using API.DTOs.Collection;
using API.DTOs.CollectionTags; using API.DTOs.CollectionTags;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.DTOs.Reader; using API.DTOs.Reader;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs; namespace API.DTOs;

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs; namespace API.DTOs;

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations namespace API.Data.Migrations
{ {
[DbContext(typeof(DataContext))] [DbContext(typeof(DataContext))]
[Migration("20250504212806_PersonAliases")] [Migration("20250507221026_PersonAliases")]
partial class PersonAliases partial class PersonAliases
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1851,7 +1851,7 @@ namespace API.Data.Migrations
b.Property<string>("NormalizedAlias") b.Property<string>("NormalizedAlias")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("PersonId") b.Property<int>("PersonId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
@ -3109,9 +3109,13 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Person.PersonAlias", b => modelBuilder.Entity("API.Entities.Person.PersonAlias", b =>
{ {
b.HasOne("API.Entities.Person.Person", null) b.HasOne("API.Entities.Person.Person", "Person")
.WithMany("Aliases") .WithMany("Aliases")
.HasForeignKey("PersonId"); .HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Person");
}); });
modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b =>

View file

@ -18,7 +18,7 @@ namespace API.Data.Migrations
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Alias = table.Column<string>(type: "TEXT", nullable: true), Alias = table.Column<string>(type: "TEXT", nullable: true),
NormalizedAlias = table.Column<string>(type: "TEXT", nullable: true), NormalizedAlias = table.Column<string>(type: "TEXT", nullable: true),
PersonId = table.Column<int>(type: "INTEGER", nullable: true) PersonId = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -27,7 +27,8 @@ namespace API.Data.Migrations
name: "FK_PersonAlias_Person_PersonId", name: "FK_PersonAlias_Person_PersonId",
column: x => x.PersonId, column: x => x.PersonId,
principalTable: "Person", principalTable: "Person",
principalColumn: "Id"); principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(

View file

@ -1848,7 +1848,7 @@ namespace API.Data.Migrations
b.Property<string>("NormalizedAlias") b.Property<string>("NormalizedAlias")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("PersonId") b.Property<int>("PersonId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
@ -3106,9 +3106,13 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Person.PersonAlias", b => modelBuilder.Entity("API.Entities.Person.PersonAlias", b =>
{ {
b.HasOne("API.Entities.Person.Person", null) b.HasOne("API.Entities.Person.Person", "Person")
.WithMany("Aliases") .WithMany("Aliases")
.HasForeignKey("PersonId"); .HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Person");
}); });
modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b =>

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.DTOs.Person;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Person; using API.Entities.Person;
using API.Extensions; using API.Extensions;

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data.Misc; using API.Data.Misc;
using API.DTOs; using API.DTOs.Person;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;

View file

@ -15,6 +15,7 @@ using API.DTOs.Filtering;
using API.DTOs.Filtering.v2; using API.DTOs.Filtering.v2;
using API.DTOs.KavitaPlus.Metadata; using API.DTOs.KavitaPlus.Metadata;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
using API.DTOs.Recommendation; using API.DTOs.Recommendation;
using API.DTOs.Scrobbling; using API.DTOs.Scrobbling;
@ -455,11 +456,18 @@ public class SeriesRepository : ISeriesRepository
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider) .ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
result.Persons = await _context.SeriesMetadata // I can't work out how to map people in DB layer
var personIds = await _context.SeriesMetadata
.SearchPeople(searchQuery, seriesIds) .SearchPeople(searchQuery, seriesIds)
.Take(maxRecords) .Select(p => p.Id)
.OrderBy(t => t.NormalizedName)
.Distinct() .Distinct()
.OrderBy(id => id)
.Take(maxRecords)
.ToListAsync();
result.Persons = await _context.Person
.Where(p => personIds.Contains(p.Id))
.OrderBy(p => p.NormalizedName)
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider) .ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
@ -475,8 +483,8 @@ public class SeriesRepository : ISeriesRepository
.ProjectTo<TagDto>(_mapper.ConfigurationProvider) .ProjectTo<TagDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
result.Files = new List<MangaFileDto>(); result.Files = [];
result.Chapters = new List<ChapterDto>(); result.Chapters = (List<ChapterDto>) [];
if (includeChapterAndFiles) if (includeChapterAndFiles)

View file

@ -46,8 +46,8 @@ public class Person : IHasCoverImage
//public long MetronId { get; set; } = 0; //public long MetronId { get; set; } = 0;
// Relationships // Relationships
public ICollection<ChapterPeople> ChapterPeople { get; set; } = new List<ChapterPeople>(); public ICollection<ChapterPeople> ChapterPeople { get; set; } = [];
public ICollection<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = new List<SeriesMetadataPeople>(); public ICollection<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = [];
public void ResetColorScape() public void ResetColorScape()

View file

@ -3,8 +3,9 @@ namespace API.Entities.Person;
public class PersonAlias public class PersonAlias
{ {
public int Id { get; set; } public int Id { get; set; }
public required string Alias { get; set; }
public required string NormalizedAlias { get; set; }
public string Alias { get; set; } public int PersonId { get; set; }
public Person Person { get; set; }
public string NormalizedAlias { get; set; }
} }

View file

@ -50,27 +50,26 @@ public static class SearchQueryableExtensions
// Get people from SeriesMetadata // Get people from SeriesMetadata
var peopleFromSeriesMetadata = queryable var peopleFromSeriesMetadata = queryable
.Where(sm => seriesIds.Contains(sm.SeriesId)) .Where(sm => seriesIds.Contains(sm.SeriesId))
.SelectMany(sm => sm.People) .SelectMany(sm => sm.People.Select(sp => sp.Person))
.Include(sp => sp.Person.Aliases) .Where(p =>
.Where(p => (p.Person.Name != null && EF.Functions.Like(p.Person.Name, $"%{searchQuery}%")) EF.Functions.Like(p.Name, $"%{searchQuery}%") ||
|| p.Person.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%"))) p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%"))
.Select(p => p.Person); );
// Get people from ChapterPeople by navigating through Volume -> Series
var peopleFromChapterPeople = queryable var peopleFromChapterPeople = queryable
.Where(sm => seriesIds.Contains(sm.SeriesId)) .Where(sm => seriesIds.Contains(sm.SeriesId))
.SelectMany(sm => sm.Series.Volumes) .SelectMany(sm => sm.Series.Volumes)
.SelectMany(v => v.Chapters) .SelectMany(v => v.Chapters)
.SelectMany(ch => ch.People) .SelectMany(ch => ch.People.Select(cp => cp.Person))
.Include(cp => cp.Person.Aliases) .Where(p =>
.Where(p => (p.Person.Name != null && EF.Functions.Like(p.Person.Name, $"%{searchQuery}%")) EF.Functions.Like(p.Name, $"%{searchQuery}%") ||
|| p.Person.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%"))) p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%"))
.Select(cp => cp.Person); );
// Combine both queries and ensure distinct results // Combine both queries and ensure distinct results
return peopleFromSeriesMetadata return peopleFromSeriesMetadata
.Union(peopleFromChapterPeople) .Union(peopleFromChapterPeople)
.Distinct() .Select(p => p)
.OrderBy(p => p.NormalizedName); .OrderBy(p => p.NormalizedName);
} }

View file

@ -15,6 +15,7 @@ using API.DTOs.KavitaPlus.Manage;
using API.DTOs.KavitaPlus.Metadata; using API.DTOs.KavitaPlus.Metadata;
using API.DTOs.MediaErrors; using API.DTOs.MediaErrors;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Person;
using API.DTOs.Progress; using API.DTOs.Progress;
using API.DTOs.Reader; using API.DTOs.Reader;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
@ -68,7 +69,8 @@ public class AutoMapperProfiles : Profile
CreateMap<AppUserCollection, AppUserCollectionDto>() CreateMap<AppUserCollection, AppUserCollectionDto>()
.ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName)) .ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName))
.ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)); .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count));
CreateMap<Person, PersonDto>(); CreateMap<Person, PersonDto>()
.ForMember(dest => dest.Aliases, opt => opt.MapFrom(src => src.Aliases.Select(s => s.Alias)));
CreateMap<Genre, GenreTagDto>(); CreateMap<Genre, GenreTagDto>();
CreateMap<Tag, TagDto>(); CreateMap<Tag, TagDto>();
CreateMap<AgeRating, AgeRatingDto>(); CreateMap<AgeRating, AgeRatingDto>();

View file

@ -0,0 +1,19 @@
using API.Entities.Person;
using API.Extensions;
namespace API.Helpers.Builders;
public class PersonAliasBuilder : IEntityBuilder<PersonAlias>
{
private readonly PersonAlias _alias;
public PersonAlias Build() => _alias;
public PersonAliasBuilder(string name)
{
_alias = new PersonAlias()
{
Alias = name.Trim(),
NormalizedAlias = name.ToNormalized(),
};
}
}

View file

@ -39,11 +39,8 @@ public class PersonBuilder : IEntityBuilder<Person>
return this; return this;
} }
_person.Aliases.Add(new PersonAlias() _person.Aliases.Add(new PersonAliasBuilder(alias).Build());
{
Alias = alias,
NormalizedAlias = alias.ToNormalized(),
});
return this; return this;
} }

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Entities.Person; using API.Entities.Person;
using API.Extensions; using API.Extensions;
using API.Helpers.Builders;
namespace API.Services; namespace API.Services;
@ -68,11 +69,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
MergeChapterPeople(dst, src); MergeChapterPeople(dst, src);
MergeSeriesMetadataPeople(dst, src); MergeSeriesMetadataPeople(dst, src);
dst.Aliases.Add(new PersonAlias dst.Aliases.Add(new PersonAliasBuilder(src.Name).Build());
{
Alias = src.Name,
NormalizedAlias = src.NormalizedName,
});
foreach (var alias in src.Aliases) foreach (var alias in src.Aliases)
{ {
@ -84,12 +81,14 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
await unitOfWork.CommitAsync(); await unitOfWork.CommitAsync();
} }
private void MergeChapterPeople(Person dst, Person src) private static void MergeChapterPeople(Person dst, Person src)
{ {
foreach (var chapter in src.ChapterPeople) foreach (var chapter in src.ChapterPeople)
{ {
var alreadyPresent = dst.ChapterPeople var alreadyPresent = dst.ChapterPeople
.Any(x => x.ChapterId == chapter.ChapterId && x.Role == chapter.Role); .Any(x => x.ChapterId == chapter.ChapterId && x.Role == chapter.Role);
if (alreadyPresent) continue; if (alreadyPresent) continue;
dst.ChapterPeople.Add(new ChapterPeople dst.ChapterPeople.Add(new ChapterPeople
@ -103,12 +102,13 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
} }
} }
private void MergeSeriesMetadataPeople(Person dst, Person src) private static void MergeSeriesMetadataPeople(Person dst, Person src)
{ {
foreach (var series in src.SeriesMetadataPeople) foreach (var series in src.SeriesMetadataPeople)
{ {
var alreadyPresent = dst.SeriesMetadataPeople var alreadyPresent = dst.SeriesMetadataPeople
.Any(x => x.SeriesMetadataId == series.SeriesMetadataId && x.Role == series.Role); .Any(x => x.SeriesMetadataId == series.SeriesMetadataId && x.Role == series.Role);
if (alreadyPresent) continue; if (alreadyPresent) continue;
dst.SeriesMetadataPeople.Add(new SeriesMetadataPeople dst.SeriesMetadataPeople.Add(new SeriesMetadataPeople
@ -125,9 +125,8 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
public async Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases) public async Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases)
{ {
var normalizedAliases = aliases var normalizedAliases = aliases
.Select(a => a.ToNormalized().Trim()) .Select(a => a.ToNormalized())
.Where(a => !string.IsNullOrWhiteSpace(a)) .Where(a => !string.IsNullOrEmpty(a) && a != person.NormalizedName)
.Where(a => a != person.NormalizedName)
.ToList(); .ToList();
if (normalizedAliases.Count == 0) if (normalizedAliases.Count == 0)
@ -141,11 +140,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
if (others.Count != 0) return false; if (others.Count != 0) return false;
person.Aliases = aliases.Select(a => new PersonAlias person.Aliases = aliases.Select(a => new PersonAliasBuilder(a).Build()).ToList();
{
Alias = a.Trim(),
NormalizedAlias = a.Trim().ToNormalized()
}).ToList();
return true; return true;
} }

View file

@ -10,6 +10,7 @@ using API.DTOs.Collection;
using API.DTOs.KavitaPlus.ExternalMetadata; using API.DTOs.KavitaPlus.ExternalMetadata;
using API.DTOs.KavitaPlus.Metadata; using API.DTOs.KavitaPlus.Metadata;
using API.DTOs.Metadata.Matching; using API.DTOs.Metadata.Matching;
using API.DTOs.Person;
using API.DTOs.Recommendation; using API.DTOs.Recommendation;
using API.DTOs.Scrobbling; using API.DTOs.Scrobbling;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
@ -20,6 +21,7 @@ using API.Entities.MetadataMatching;
using API.Entities.Person; using API.Entities.Person;
using API.Extensions; using API.Extensions;
using API.Helpers; using API.Helpers;
using API.Helpers.Builders;
using API.Services.Tasks.Metadata; using API.Services.Tasks.Metadata;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
using API.SignalR; using API.SignalR;
@ -657,11 +659,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (existingPeopleDictionary.TryGetValue(mapping.AlternativeName.ToNormalized(), out var person)) if (existingPeopleDictionary.TryGetValue(mapping.AlternativeName.ToNormalized(), out var person))
{ {
modified = true; modified = true;
person.Aliases.Add(new PersonAlias person.Aliases.Add(new PersonAliasBuilder(mapping.PreferredName).Build());
{
Alias = mapping.PreferredName,
NormalizedAlias = mapping.PreferredName.ToNormalized(),
});
} }
} }

View file

@ -7,6 +7,7 @@ using API.Comparators;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.DTOs.Person;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;

View file

@ -13,7 +13,7 @@
} }
.subtitle { .subtitle {
color: lightgrey; color: var(--detail-subtitle-color);
font-weight: bold; font-weight: bold;
font-size: 0.8rem; font-size: 0.8rem;
} }

View file

@ -22,6 +22,7 @@ export interface Person extends IHasCover {
id: number; id: number;
name: string; name: string;
description: string; description: string;
aliases: Array<string>;
coverImage?: string; coverImage?: string;
coverImageLocked: boolean; coverImageLocked: boolean;
malId?: number; malId?: number;

View file

@ -119,6 +119,18 @@
</div> </div>
<div class="ms-1"> <div class="ms-1">
<div>{{item.name}}</div> <div>{{item.name}}</div>
@if (item.aliases.length > 0) {
<span class="small-text">
{{t('person-aka-label')}}
@for(alias of item.aliases; track alias; let last = $last) {
<span>{{alias}}</span>
@if (!last) {
<span>, </span>
}
}
</span>
}
</div> </div>
</div> </div>
</ng-template> </ng-template>
@ -206,7 +218,7 @@
</div> </div>
} }
} }
</div> </div>
</nav> </nav>

View file

@ -138,3 +138,7 @@
} }
} }
} }
.small-text {
font-size: 0.8rem;
}

View file

@ -16,13 +16,6 @@
} }
</h2> </h2>
</ng-container> </ng-container>
<ng-container subtitle>
@if (aliases$ | async; as aliases) {
@if (aliases.length > 0) {
<span>{{t('aka')}} {{aliases.join(", ")}}</span>
}
}
</ng-container>
</app-side-nav-companion-bar> </app-side-nav-companion-bar>
</div> </div>
@ -50,15 +43,43 @@
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12 mt-2"> <div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12 mt-2">
<div class="row g-0 mt-2"> <div class="row g-0 mt-2">
<app-read-more [text]="person.description || t('no-info')"></app-read-more> <app-read-more [maxLength]="500" [text]="person.description || t('no-info')"></app-read-more>
@if (person.aliases.length > 0) {
<span class="fw-bold mt-2">{{t('aka-title')}}</span>
<div>
<app-badge-expander [items]="person.aliases"
[itemsTillExpander]="6">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a href="javascript:void(0)" class="dark-exempt btn-icon">{{item}}</a>
</ng-template>
</app-badge-expander>
</div>
}
@if (roles$ | async; as roles) { @if (roles$ | async; as roles) {
<div class="mt-1"> @if (roles.length > 0) {
<h5>{{t('all-roles')}}</h5> <span class="fw-bold mt-2">{{t('all-roles')}}</span>
@for(role of roles; track role) { <div>
<app-tag-badge [selectionMode]="TagBadgeCursor.Clickable" (click)="loadFilterByRole(role)">{{role | personRole}}</app-tag-badge> <app-badge-expander [items]="roles"
} [itemsTillExpander]="6">
</div> <ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="loadFilterByRole(item)">{{item | personRole}}</a>
</ng-template>
</app-badge-expander>
</div>
}
<!-- -->
<!-- <div class="mt-1">-->
<!-- <h5>{{t('all-roles')}}</h5>-->
<!-- @for(role of roles; track role) {-->
<!-- <app-tag-badge [selectionMode]="TagBadgeCursor.Clickable" (click)="loadFilterByRole(role)">{{role | personRole}}</app-tag-badge>-->
<!-- }-->
<!-- </div>-->
} }
</div> </div>

View file

@ -25,7 +25,7 @@ import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carou
import {FilterComparison} from "../_models/metadata/v2/filter-comparison"; import {FilterComparison} from "../_models/metadata/v2/filter-comparison";
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
import {allPeople, personRoleForFilterField} from "../_models/metadata/v2/filter-field"; import {allPeople, FilterField, personRoleForFilterField} from "../_models/metadata/v2/filter-field";
import {Series} from "../_models/series"; import {Series} from "../_models/series";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {FilterCombination} from "../_models/metadata/v2/filter-combination"; import {FilterCombination} from "../_models/metadata/v2/filter-combination";
@ -44,6 +44,7 @@ import {LicenseService} from "../_services/license.service";
import {SafeUrlPipe} from "../_pipes/safe-url.pipe"; import {SafeUrlPipe} from "../_pipes/safe-url.pipe";
import {MergePersonModalComponent} from "./_modal/merge-person-modal/merge-person-modal.component"; import {MergePersonModalComponent} from "./_modal/merge-person-modal/merge-person-modal.component";
import {EVENTS, MessageHubService} from "../_services/message-hub.service"; import {EVENTS, MessageHubService} from "../_services/message-hub.service";
import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.component";
interface PersonMergeEvent { interface PersonMergeEvent {
srcId: number, srcId: number,
@ -53,24 +54,25 @@ interface PersonMergeEvent {
@Component({ @Component({
selector: 'app-person-detail', selector: 'app-person-detail',
imports: [ imports: [
AsyncPipe, AsyncPipe,
ImageComponent, ImageComponent,
SideNavCompanionBarComponent, SideNavCompanionBarComponent,
ReadMoreComponent, ReadMoreComponent,
TagBadgeComponent, TagBadgeComponent,
PersonRolePipe, PersonRolePipe,
CarouselReelComponent, CarouselReelComponent,
CardItemComponent, CardItemComponent,
CardActionablesComponent, CardActionablesComponent,
TranslocoDirective, TranslocoDirective,
ChapterCardComponent, ChapterCardComponent,
SafeUrlPipe SafeUrlPipe,
], BadgeExpanderComponent
templateUrl: './person-detail.component.html', ],
styleUrl: './person-detail.component.scss', templateUrl: './person-detail.component.html',
changeDetection: ChangeDetectionStrategy.OnPush styleUrl: './person-detail.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PersonDetailComponent implements OnInit { export class PersonDetailComponent implements OnInit {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
@ -104,6 +106,7 @@ export class PersonDetailComponent implements OnInit {
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this)); personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
chaptersByRole: any = {}; chaptersByRole: any = {};
anilistUrl: string = ''; anilistUrl: string = '';
private readonly personSubject = new BehaviorSubject<Person | null>(null); private readonly personSubject = new BehaviorSubject<Person | null>(null);
protected readonly person$ = this.personSubject.asObservable().pipe(tap(p => { protected readonly person$ = this.personSubject.asObservable().pipe(tap(p => {
if (p?.aniListId) { if (p?.aniListId) {
@ -291,4 +294,6 @@ export class PersonDetailComponent implements OnInit {
action.callback(action, this.person); action.callback(action, this.person);
} }
} }
protected readonly FilterField = FilterField;
} }

View file

@ -130,7 +130,7 @@
<span class="fw-bold">{{t('publication-status-title')}}</span> <span class="fw-bold">{{t('publication-status-title')}}</span>
<div> <div>
@if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) { @if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) {
<a class="dark-exempt btn-icon" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata.publicationStatus)" <a class="dark-exempt btn-icon font-size" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata!.publicationStatus)"
href="javascript:void(0);" href="javascript:void(0);"
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')"> [ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
{{pubStatus}} {{pubStatus}}

View file

@ -30,3 +30,7 @@
:host ::ng-deep .card-actions.btn-actions .btn { :host ::ng-deep .card-actions.btn-actions .btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
} }
.font-size {
font-size: 0.8rem;
}

View file

@ -5,4 +5,8 @@
.collapsed { .collapsed {
height: 35px; height: 35px;
overflow: hidden; overflow: hidden;
} }
::ng-deep .badge-expander .content a {
font-size: 0.8rem;
}

View file

@ -1103,7 +1103,7 @@
}, },
"person-detail": { "person-detail": {
"aka": "Also known as ", "aka-title": "Also known as ",
"known-for-title": "Known For", "known-for-title": "Known For",
"individual-role-title": "As a {{role}}", "individual-role-title": "As a {{role}}",
"browse-person-title": "All Works of {{name}}", "browse-person-title": "All Works of {{name}}",
@ -1859,7 +1859,8 @@
"logout": "Logout", "logout": "Logout",
"all-filters": "Smart Filters", "all-filters": "Smart Filters",
"nav-link-header": "Navigation Options", "nav-link-header": "Navigation Options",
"close": "{{common.close}}" "close": "{{common.close}}",
"person-aka-label": "{{person-detail.aka-title}}:"
}, },
"promoted-icon": { "promoted-icon": {

View file

@ -436,4 +436,7 @@
--login-input-font-family: 'League Spartan', sans-serif; --login-input-font-family: 'League Spartan', sans-serif;
--login-input-placeholder-opacity: 0.5; --login-input-placeholder-opacity: 0.5;
--login-input-placeholder-color: #fff; --login-input-placeholder-color: #fff;
/** Series Detail **/
--detail-subtitle-color: lightgrey;
} }