Display aliases in the UI, allow adding and removing aliases from the Person Edit modal
Adds forgotten translation string
This commit is contained in:
parent
c36123a58f
commit
d1c5f4a377
21 changed files with 228 additions and 18 deletions
|
|
@ -144,6 +144,45 @@ public class PersonServiceTests: AbstractDbTest
|
|||
Assert.Equal(chapter.People.First().PersonId, chapter2.People.First().PersonId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PersonAddAlias_NoOverlap()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jillian Cowan").Build());
|
||||
UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jilly Cowan").WithAlias("Jolly Cowan").Build());
|
||||
await UnitOfWork.CommitAsync();
|
||||
|
||||
var ps = new PersonService(UnitOfWork);
|
||||
|
||||
var person1 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jillian Cowan");
|
||||
var person2 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jilly Cowan");
|
||||
Assert.NotNull(person1);
|
||||
Assert.NotNull(person2);
|
||||
|
||||
// Overlap on Name
|
||||
var success = await ps.UpdatePersonAliasesAsync(person1, ["Jilly Cowan"]);
|
||||
Assert.False(success);
|
||||
|
||||
// Overlap on alias
|
||||
success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan"]);
|
||||
Assert.False(success);
|
||||
|
||||
// No overlap
|
||||
success = await ps.UpdatePersonAliasesAsync(person2, ["Jilly Joy Cowan"]);
|
||||
Assert.True(success);
|
||||
|
||||
// Some overlap
|
||||
success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]);
|
||||
Assert.False(success);
|
||||
|
||||
// Some overlap
|
||||
success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]);
|
||||
Assert.True(success);
|
||||
|
||||
Assert.Single(person2.Aliases);
|
||||
}
|
||||
|
||||
protected override async Task ResetDb()
|
||||
{
|
||||
Context.Person.RemoveRange(Context.Person.ToList());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
|
|
@ -24,9 +25,10 @@ public class PersonController : BaseApiController
|
|||
private readonly ICoverDbService _coverDbService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IPersonService _personService;
|
||||
|
||||
public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper,
|
||||
ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub)
|
||||
ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
|
|
@ -34,6 +36,7 @@ public class PersonController : BaseApiController
|
|||
_coverDbService = coverDbService;
|
||||
_imageService = imageService;
|
||||
_eventHub = eventHub;
|
||||
_personService = personService;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -90,6 +93,10 @@ public class PersonController : BaseApiController
|
|||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique"));
|
||||
}
|
||||
|
||||
var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases);
|
||||
if (!success) return BadRequest(_localizationService.Translate(User.GetUserId(), "aliases-have-overlap"));
|
||||
|
||||
|
||||
person.Name = dto.Name?.Trim();
|
||||
person.Description = dto.Description ?? string.Empty;
|
||||
person.CoverImageLocked = dto.CoverImageLocked;
|
||||
|
|
@ -173,5 +180,36 @@ public class PersonController : BaseApiController
|
|||
return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges Persons into one, this action is irreversible
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("merge")]
|
||||
public async Task<ActionResult> MergePersons(PersonMergeDto dto)
|
||||
{
|
||||
var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId);
|
||||
if (dst == null) return BadRequest();
|
||||
|
||||
var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId);
|
||||
if (src == null) return BadRequest();
|
||||
|
||||
await _personService.MergePeopleAsync(dst, src);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("alias/{personId}/{alias}")]
|
||||
public async Task<ActionResult<bool>> IsValidAlias(int personId, string alias)
|
||||
{
|
||||
// Remove and just check against the passed value?
|
||||
var person = await _unitOfWork.PersonRepository.GetPersonById(personId);
|
||||
if (person == null) return NotFound();
|
||||
|
||||
var other = await _unitOfWork.PersonRepository.GetPersonByNameOrAliasAsync(alias);
|
||||
if (other == null) return Ok(true);
|
||||
|
||||
return Ok(other.Id == person.Id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Runtime.Serialization;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs;
|
||||
#nullable enable
|
||||
|
|
@ -7,6 +7,7 @@ public class PersonDto
|
|||
{
|
||||
public int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public IList<string> Aliases { get; set; } = [];
|
||||
|
||||
public bool CoverImageLocked { get; set; }
|
||||
public string? PrimaryColor { get; set; }
|
||||
|
|
|
|||
17
API/DTOs/Person/PersonMergeDto.cs
Normal file
17
API/DTOs/Person/PersonMergeDto.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
||||
public sealed record PersonMergeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the person being merged into
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int DestId { get; init; }
|
||||
/// <summary>
|
||||
/// The id of the person being merged. This person will be removed, and become an alias of <see cref="DestId"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int SrcId { get; init; }
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
#nullable enable
|
||||
|
|
@ -11,6 +12,7 @@ public sealed record UpdatePersonDto
|
|||
public bool CoverImageLocked { get; set; }
|
||||
[Required]
|
||||
public string Name {get; set;}
|
||||
public IList<string> Aliases { get; set; } = [];
|
||||
public string? Description { get; set; }
|
||||
|
||||
public int? AniListId { get; set; }
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public interface IPersonRepository
|
|||
/// <param name="name"></param>
|
||||
/// <param name="includes"></param>
|
||||
/// <returns></returns>
|
||||
Task<Person?> GetPersonByNameAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
||||
Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
||||
Task<bool> IsNameUnique(string name);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
||||
|
|
@ -235,7 +235,7 @@ public class PersonRepository : IPersonRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public Task<Person?> GetPersonByNameAsync(string name, PersonIncludes includes = PersonIncludes.Aliases)
|
||||
public Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
return _context.Person
|
||||
|
|
@ -246,7 +246,10 @@ public class PersonRepository : IPersonRepository
|
|||
|
||||
public async Task<bool> IsNameUnique(string name)
|
||||
{
|
||||
return !(await _context.Person.AnyAsync(p => p.Name == name));
|
||||
// Should this use Normalized to check?
|
||||
return !(await _context.Person
|
||||
.Includes(PersonIncludes.Aliases)
|
||||
.AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name)));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public static class ApplicationServiceExtensions
|
|||
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
||||
services.AddScoped<IStreamService, StreamService>();
|
||||
services.AddScoped<IRatingService, RatingService>();
|
||||
services.AddScoped<IPersonService, PersonService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IProcessSeries, ProcessSeries>();
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ public class AutoMapperProfiles : Profile
|
|||
CreateMap<AppUserCollection, AppUserCollectionDto>()
|
||||
.ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName))
|
||||
.ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count));
|
||||
CreateMap<Person, PersonDto>();
|
||||
CreateMap<Person, PersonDto>()
|
||||
.ForMember(dst => dst.Aliases, opt =>
|
||||
opt.MapFrom(src => src.Aliases.Select(pa => pa.Alias).ToList()));
|
||||
CreateMap<Genre, GenreTagDto>();
|
||||
CreateMap<Tag, TagDto>();
|
||||
CreateMap<AgeRating, AgeRatingDto>();
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@
|
|||
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
|
||||
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
|
||||
"kavitaplus-restricted": "This is restricted to Kavita+ only",
|
||||
"aliases-have-overlap": "One or more of the aliases have overlap with other people, cannot update",
|
||||
|
||||
"volume-num": "Volume {0}",
|
||||
"book-num": "Book {0}",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities.Person;
|
||||
using API.Extensions;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
|
@ -13,6 +16,15 @@ public interface IPersonService
|
|||
/// <param name="src">Merged person</param>
|
||||
/// <returns></returns>
|
||||
Task MergePeopleAsync(Person dst, Person src);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the alias to the person, requires that the alias is not shared with anyone else
|
||||
/// </summary>
|
||||
/// <remarks>This method does NOT commit changes</remarks>
|
||||
/// <param name="person"></param>
|
||||
/// <param name="aliases"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases);
|
||||
}
|
||||
|
||||
public class PersonService(IUnitOfWork unitOfWork): IPersonService
|
||||
|
|
@ -84,4 +96,32 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
|
|||
unitOfWork.PersonRepository.Update(dst);
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases)
|
||||
{
|
||||
var normalizedAliases = aliases
|
||||
.Select(a => a.ToNormalized().Trim())
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
.Where(a => a != person.NormalizedName)
|
||||
.ToList();
|
||||
|
||||
if (normalizedAliases.Count == 0)
|
||||
{
|
||||
person.Aliases = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases);
|
||||
others = others.Where(p => p.Id != person.Id).ToList();
|
||||
|
||||
if (others.Count != 0) return false;
|
||||
|
||||
person.Aliases = aliases.Select(a => new PersonAlias
|
||||
{
|
||||
Alias = a.Trim(),
|
||||
NormalizedAlias = a.Trim().ToNormalized()
|
||||
}).ToList();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export enum PersonRole {
|
|||
export interface Person extends IHasCover {
|
||||
id: number;
|
||||
name: string;
|
||||
aliases: string[];
|
||||
description: string;
|
||||
coverImage?: string;
|
||||
coverImageLocked: boolean;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {BrowsePerson} from "../_models/person/browse-person";
|
|||
import {Chapter} from "../_models/chapter";
|
||||
import {StandaloneChapter} from "../_models/standalone-chapter";
|
||||
import {TextResonse} from "../_types/text-response";
|
||||
import {al} from "@angular/router/router_module.d-6zbCxc1T";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -55,4 +56,9 @@ export class PersonService {
|
|||
downloadCover(personId: number) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse);
|
||||
}
|
||||
|
||||
isValidAlias(personId: number, alias: string) {
|
||||
return this.httpClient.get<boolean>(this.baseUrl + `person/alias/${personId}/${alias}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ export class EditChapterModalComponent implements OnInit {
|
|||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
return {id: 0, name: title, aliases: [], role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
});
|
||||
|
||||
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
return {id: 0, name: title, aliases: [], description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
});
|
||||
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,17 @@
|
|||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.Aliases">
|
||||
<a ngbNavLink>{{t(TabID.Aliases)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-edit-list [items]="person.aliases"
|
||||
[asyncValidators]="[aliasValidator()]"
|
||||
(updateItems)="person.aliases = $event"
|
||||
[errorMessage]="t('alias-overlap')"
|
||||
[label]="t('aliases-label')"/>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {
|
||||
AbstractControl,
|
||||
AsyncValidatorFn,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule, ValidationErrors,
|
||||
Validators
|
||||
} from "@angular/forms";
|
||||
import {Person} from "../../../_models/metadata/person";
|
||||
import {
|
||||
NgbActiveModal,
|
||||
|
|
@ -14,14 +21,17 @@ import {
|
|||
import {PersonService} from "../../../_services/person.service";
|
||||
import {translate, TranslocoDirective} from '@jsverse/transloco';
|
||||
import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {forkJoin, map, of} from "rxjs";
|
||||
import {UploadService} from "../../../_services/upload.service";
|
||||
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {EditListComponent} from "../../../shared/edit-list/edit-list.component";
|
||||
import {al} from "@angular/router/router_module.d-6zbCxc1T";
|
||||
|
||||
enum TabID {
|
||||
General = 'general-tab',
|
||||
Aliases = 'aliases-tab',
|
||||
CoverImage = 'cover-image-tab',
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +47,8 @@ enum TabID {
|
|||
NgbNavOutlet,
|
||||
CoverImageChooserComponent,
|
||||
SettingItemComponent,
|
||||
NgbNavLink
|
||||
NgbNavLink,
|
||||
EditListComponent
|
||||
],
|
||||
templateUrl: './edit-person-modal.component.html',
|
||||
styleUrl: './edit-person-modal.component.scss',
|
||||
|
|
@ -110,6 +121,7 @@ export class EditPersonModalComponent implements OnInit {
|
|||
id: this.person.id,
|
||||
coverImageLocked: this.person.coverImageLocked,
|
||||
name: this.editForm.get('name')!.value || '',
|
||||
aliases: this.person.aliases,
|
||||
description: this.editForm.get('description')!.value || '',
|
||||
asin: this.editForm.get('asin')!.value || '',
|
||||
// @ts-ignore
|
||||
|
|
@ -165,4 +177,21 @@ export class EditPersonModalComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
aliasValidator(): AsyncValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
const name = control.value;
|
||||
if (!name || name.trim().length === 0) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.personService.isValidAlias(this.person.id, name).pipe(map(valid => {
|
||||
if (valid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { 'inValidAlias': {'alias': name} } as ValidationErrors;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,11 @@
|
|||
|
||||
<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">
|
||||
<app-read-more [text]="person.description || defaultSummaryText"></app-read-more>
|
||||
@if (person.aliases.length > 0) {
|
||||
<!-- I can't UI, help! -->
|
||||
<span>{{t('aka')}} {{person.aliases.join(", ")}}</span>
|
||||
}
|
||||
<app-read-more [text]="person.description || t('no-info')"></app-read-more>
|
||||
|
||||
@if (roles$ | async; as roles) {
|
||||
<div class="mt-1">
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ export class PersonDetailComponent {
|
|||
roles$: Observable<PersonRole[]> | null = null;
|
||||
roles: PersonRole[] | null = null;
|
||||
works$: Observable<Series[]> | null = null;
|
||||
defaultSummaryText = 'No information about this Person';
|
||||
filter: SeriesFilterV2 | null = null;
|
||||
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
|
||||
chaptersByRole: any = {};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@
|
|||
[formControlName]="i"
|
||||
id="item--{{i}}"
|
||||
>
|
||||
@if (item.dirty && item.touched && errorMessage) {
|
||||
@if (item.status === "INVALID") {
|
||||
<div id="item--{{i}}-error" class="invalid-feedback" style="display: inline-block">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {FormArray, FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {AsyncValidatorFn, FormArray, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn} from "@angular/forms";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
|
||||
|
|
@ -28,6 +28,10 @@ export class EditListComponent implements OnInit {
|
|||
|
||||
@Input({required: true}) items: Array<string> = [];
|
||||
@Input({required: true}) label = '';
|
||||
@Input() validators: ValidatorFn[] = []
|
||||
@Input() asyncValidators: AsyncValidatorFn[] = [];
|
||||
// TODO: Make this more dynamic based on which validator failed
|
||||
@Input() errorMessage: string | null = null;
|
||||
@Output() updateItems = new EventEmitter<Array<string>>();
|
||||
|
||||
form: FormGroup = new FormGroup({items: new FormArray([])});
|
||||
|
|
@ -51,7 +55,7 @@ export class EditListComponent implements OnInit {
|
|||
}
|
||||
|
||||
createItemControl(value: string = ''): FormControl {
|
||||
return new FormControl(value, []);
|
||||
return new FormControl(value, this.validators, this.asyncValidators);
|
||||
}
|
||||
|
||||
add() {
|
||||
|
|
|
|||
|
|
@ -1103,12 +1103,14 @@
|
|||
},
|
||||
|
||||
"person-detail": {
|
||||
"aka": "Also known as ",
|
||||
"known-for-title": "Known For",
|
||||
"individual-role-title": "As a {{role}}",
|
||||
"browse-person-title": "All Works of {{name}}",
|
||||
"browse-person-by-role-title": "All Works of {{name}} as a {{role}}",
|
||||
"all-roles": "Roles",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||
"no-info": "No information about this Person"
|
||||
},
|
||||
|
||||
"library-settings-modal": {
|
||||
|
|
@ -2246,6 +2248,7 @@
|
|||
"title": "{{personName}} Details",
|
||||
"general-tab": "{{edit-series-modal.general-tab}}",
|
||||
"cover-image-tab": "{{edit-series-modal.cover-image-tab}}",
|
||||
"aliases-tab": "Aliases",
|
||||
"loading": "{{common.loading}}",
|
||||
"close": "{{common.close}}",
|
||||
"name-label": "{{edit-series-modal.name-label}}",
|
||||
|
|
@ -2263,7 +2266,9 @@
|
|||
"cover-image-description": "{{edit-series-modal.cover-image-description}}",
|
||||
"cover-image-description-extra": "Alternatively you can download a cover from CoversDB if available.",
|
||||
"save": "{{common.save}}",
|
||||
"download-coversdb": "Download from CoversDB"
|
||||
"download-coversdb": "Download from CoversDB",
|
||||
"aliases-label": "Edit aliases",
|
||||
"alias-overlap": "This alias already points towards another person, consider merging them."
|
||||
},
|
||||
|
||||
"day-breakdown": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue