diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index b6bd53a15..728cf29c2 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using API.DTOs.KavitaPlus.Metadata; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; @@ -217,6 +218,12 @@ public sealed class DataContext : IdentityDbContext JsonSerializer.Serialize(v, JsonSerializerOptions.Default), v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ); + builder.Entity() + .Property(x => x.Whitelist) + .HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), + v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) + ); // Configure one-to-many relationship builder.Entity() @@ -224,6 +231,7 @@ public sealed class DataContext : IdentityDbContext x.MetadataSettings) .HasForeignKey(x => x.MetadataSettingsId) .OnDelete(DeleteBehavior.Cascade); + builder.Entity() .Property(b => b.Enabled) .HasDefaultValue(true); diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index 57c2ed3ac..050038e86 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -224,6 +224,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor public async Task> GetAllSeries(ManageMatchFilterDto filter) { return await _context.Series + .Include(s => s.Library) .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .FilterMatchState(filter.MatchStateOption) .OrderBy(s => s.NormalizedName) diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index bd54f39f6..e6fbb8990 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -308,7 +308,7 @@ public static class Seed EnableTags = false, EnableGenres = true, EnableLocalizedName = false, - FirstLastPeopleNaming = false, + FirstLastPeopleNaming = true, PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character] }; await context.MetadataSettings.AddAsync(existing); diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index 3b3a28241..324d67291 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -288,11 +288,15 @@ public static class QueryableExtensions return stateOption switch { MatchStateOption.All => query, - MatchStateOption.Matched => query.Where(s => s.ExternalSeriesMetadata != null && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue && !s.IsBlacklisted), - MatchStateOption.NotMatched => query.Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted), + MatchStateOption.Matched => query + .Include(s => s.ExternalSeriesMetadata) + .Where(s => s.ExternalSeriesMetadata != null && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue && !s.IsBlacklisted), + MatchStateOption.NotMatched => query. + Include(s => s.ExternalSeriesMetadata) + .Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted), MatchStateOption.Error => query.Where(s => s.IsBlacklisted), MatchStateOption.DontMatch => query.Where(s => s.DontMatch), - _ => throw new ArgumentOutOfRangeException(nameof(stateOption), stateOption, null) + _ => query }; } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 9f8ed71a6..9efb1268b 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -345,10 +345,13 @@ public class AutoMapperProfiles : Profile opt.MapFrom(src => src)) .ForMember(dest => dest.IsMatched, opt => - opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0 && src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue)) + opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0 + && src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue)) .ForMember(dest => dest.ValidUntilUtc, - opt => - opt.MapFrom(src => src.ExternalSeriesMetadata.ValidUntilUtc)); + opt => opt.MapFrom(src => + src.ExternalSeriesMetadata != null + ? src.ExternalSeriesMetadata.ValidUntilUtc + : DateTime.MinValue)); CreateMap(); @@ -361,10 +364,14 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId)) .ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type)); + CreateMap(); + CreateMap() .ForMember(dest => dest.Blacklist, opt => opt.MapFrom(src => src.Blacklist ?? new List())) - .ForMember(dest => dest.Whitelist, opt => opt.MapFrom(src => src.Whitelist ?? new List())); - CreateMap(); + .ForMember(dest => dest.Whitelist, opt => opt.MapFrom(src => src.Whitelist ?? new List())) + .ForMember(dest => dest.AgeRatingMappings, opt => opt.MapFrom(src => src.AgeRatingMappings ?? new Dictionary())); + + } } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index fc66e3592..1858480c1 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -195,6 +195,7 @@ public class ExternalMetadataService : IExternalMetadataService var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata); + if (series == null) return []; var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); var potentialMalId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); @@ -512,7 +513,7 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = true; } - if (settings.EnableStartDate && externalMetadata.StartDate.HasValue) + if (settings.EnableStartDate && !series.Metadata.ReleaseYearLocked && externalMetadata.StartDate.HasValue) { series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year; madeModification = true; @@ -526,7 +527,7 @@ public class ExternalMetadataService : IExternalMetadataService // Process Genres if (externalMetadata.Genres != null) { - foreach (var genre in externalMetadata.Genres.Where(g => !settings.Blacklist.Contains(g))) + foreach (var genre in externalMetadata.Genres) { // Apply field mappings var mappedGenre = ApplyFieldMapping(genre, MetadataFieldType.Genre, settings.FieldMappings); @@ -537,9 +538,12 @@ public class ExternalMetadataService : IExternalMetadataService } // Strip blacklisted items from processedGenres - processedGenres = processedGenres.Distinct().Where(g => !settings.Blacklist.Contains(g)).ToList(); + processedGenres = processedGenres + .Distinct() + .Where(g => !settings.Blacklist.Contains(g)) + .ToList(); - if (settings.EnableGenres && processedGenres.Count > 0) + if (settings.EnableGenres && !series.Metadata.GenresLocked && processedGenres.Count > 0) { _logger.LogDebug("Found {GenreCount} genres for {SeriesName}", processedGenres.Count, series.Name); var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(processedGenres.Select(Parser.Normalize))).ToList(); @@ -567,13 +571,14 @@ public class ExternalMetadataService : IExternalMetadataService } // Strip blacklisted items from processedTags - processedTags = processedTags.Distinct() + processedTags = processedTags + .Distinct() .Where(g => !settings.Blacklist.Contains(g)) .Where(g => settings.Whitelist.Count == 0 || settings.Whitelist.Contains(g)) .ToList(); // Set the tags for the series and ensure they are in the DB - if (settings.EnableTags && processedTags.Count > 0) + if (settings.EnableTags && !series.Metadata.TagsLocked && processedTags.Count > 0) { _logger.LogDebug("Found {TagCount} tags for {SeriesName}", processedTags.Count, series.Name); var allTags = (await _unitOfWork.TagRepository.GetAllTagsByNameAsync(processedTags.Select(Parser.Normalize))) @@ -591,22 +596,36 @@ public class ExternalMetadataService : IExternalMetadataService #region Age Rating - // Determine Age Rating - var ageRating = DetermineAgeRating(processedGenres.Concat(processedTags), settings.AgeRatingMappings); - if (!series.Metadata.AgeRatingLocked && series.Metadata.AgeRating <= ageRating) + if (!series.Metadata.AgeRatingLocked) { - series.Metadata.AgeRating = ageRating; - _unitOfWork.SeriesRepository.Update(series); - madeModification = true; - } + try + { + // Determine Age Rating + var totalTags = processedGenres + .Concat(processedTags) + .Concat(series.Metadata.Genres.Select(g => g.Title)) + .Concat(series.Metadata.Tags.Select(g => g.Title)); + var ageRating = DetermineAgeRating(totalTags, settings.AgeRatingMappings); + if (!series.Metadata.AgeRatingLocked && series.Metadata.AgeRating <= ageRating) + { + series.Metadata.AgeRating = ageRating; + _unitOfWork.SeriesRepository.Update(series); + madeModification = true; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue determining Age Rating for Series {SeriesName} ({SeriesId})", series.Name, series.Id); + } + } #endregion #region People if (settings.EnablePeople) { - series.Metadata.People ??= new List(); + series.Metadata.People ??= []; // Ensure all people are named correctly externalMetadata.Staff = externalMetadata.Staff.Select(s => @@ -635,7 +654,10 @@ public class ExternalMetadataService : IExternalMetadataService Name = w.Name, AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = CleanSummary(w.Description), - }).ToList(); + }) + .Concat(series.Metadata.People.Where(p => p.Role == PersonRole.Writer).Select(p => _mapper.Map(p))) + .DistinctBy(p => Parser.Normalize(p.Name)) + .ToList(); // NOTE: PersonRoles can be a hashset @@ -661,7 +683,10 @@ public class ExternalMetadataService : IExternalMetadataService Name = w.Name, AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = CleanSummary(w.Description), - }).ToList(); + }) + .Concat(series.Metadata.People.Where(p => p.Role == PersonRole.CoverArtist).Select(p => _mapper.Map(p))) + .DistinctBy(p => Parser.Normalize(p.Name)) + .ToList(); if (!series.Metadata.CoverArtistLocked && artists.Count > 0 && settings.PersonRoles.Contains(PersonRole.CoverArtist)) { @@ -684,7 +709,10 @@ public class ExternalMetadataService : IExternalMetadataService Name = w.Name, AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), Description = CleanSummary(w.Description), - }).ToList(); + }) + .Concat(series.Metadata.People.Where(p => p.Role == PersonRole.Character).Select(p => _mapper.Map(p))) + .DistinctBy(p => Parser.Normalize(p.Name)) + .ToList(); if (!series.Metadata.CharacterLocked && characters.Count > 0) @@ -713,13 +741,27 @@ public class ExternalMetadataService : IExternalMetadataService #endregion + #region Publication Status + if (!series.Metadata.PublicationStatusLocked && settings.EnablePublicationStatus) { - var chapters = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Chapters))!.Volumes.SelectMany(v => v.Chapters).ToList(); - var wasChanged = DeterminePublicationStatus(series, chapters, externalMetadata); - _unitOfWork.SeriesRepository.Update(series); - madeModification = madeModification || wasChanged; + try + { + var chapters = + (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Chapters))!.Volumes + .SelectMany(v => v.Chapters).ToList(); + var wasChanged = DeterminePublicationStatus(series, chapters, externalMetadata); + _unitOfWork.SeriesRepository.Update(series); + madeModification = madeModification || wasChanged; + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue determining Publication Status for Series {SeriesName} ({SeriesId})", series.Name, series.Id); + } } + #endregion + + #region Relationships if (settings.EnableRelationships && externalMetadata.Relations != null && defaultAdmin != null) { @@ -773,6 +815,7 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = true; } } + #endregion return madeModification; } @@ -889,6 +932,8 @@ public class ExternalMetadataService : IExternalMetadataService private static AgeRating DetermineAgeRating(IEnumerable values, Dictionary mappings) { // Find highest age rating from mappings + mappings ??= new Dictionary(); + return values .Select(v => mappings.TryGetValue(v, out var mapping) ? mapping : AgeRating.Unknown) .DefaultIfEmpty(AgeRating.Unknown) @@ -913,6 +958,7 @@ public class ExternalMetadataService : IExternalMetadataService }; series.ExternalSeriesMetadata = externalSeriesMetadata; _unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata); + return externalSeriesMetadata; } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 3ee151c38..2f1795999 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -349,7 +349,8 @@ public class SeriesService : ISeriesService var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames); // Use a dictionary for quick lookups - var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName).ToDictionary(p => p.NormalizedName, p => p); + var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName) + .ToDictionary(p => p.NormalizedName, p => p); // List to track people that will be added to the metadata var peopleToAdd = new List(); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index c65219897..12521a039 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -450,12 +450,12 @@ public class ScannerService : IScannerService // That way logging and UI informing is all in one place with full context _logger.LogError("[ScannerService] Some of the root folders for the library are empty. " + "Either your mount has been disconnected or you are trying to delete all series in the library. " + - "Scan has be aborted. " + + "Scan has been aborted. " + "Check that your mount is connected or change the library's root folder and rescan"); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.", "Either your mount has been disconnected or you are trying to delete all series in the library. " + - "Scan has be aborted. " + + "Scan has been aborted. " + "Check that your mount is connected or change the library's root folder and rescan")); return false; diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index fb869a9ac..88d1096b4 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -114,6 +114,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService var nightlyDto = new UpdateNotificationDto { + // TODO: I should pass Title to the FE so that Nightly Release can be localized UpdateTitle = $"Nightly Release {nightly.Version} - {prInfo.Title}", UpdateVersion = nightly.Version, CurrentVersion = dto.CurrentVersion, @@ -446,7 +447,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService { var sections = new Dictionary>(StringComparer.OrdinalIgnoreCase); var lines = body.Split('\n'); - string currentSection = null; + string? currentSection = null; foreach (var line in lines) { diff --git a/UI/Web/src/app/_models/series-detail/external-series-detail.ts b/UI/Web/src/app/_models/series-detail/external-series-detail.ts index 85d89c760..aa62d3960 100644 --- a/UI/Web/src/app/_models/series-detail/external-series-detail.ts +++ b/UI/Web/src/app/_models/series-detail/external-series-detail.ts @@ -37,6 +37,11 @@ export interface ExternalSeriesDetail { summary?: string; volumeCount?: number; chapterCount?: number; + /** + * These are duplicated with volumeCount based on where it's being invoked. + */ + volumes?: number; + chapters?: number; staff: Array; tags: Array; provider: ScrobbleProvider; diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index 0aa808939..7ea7ccf15 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -63,7 +63,7 @@
- + diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts index 48697b89c..a03ef140e 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts @@ -22,6 +22,7 @@ import {SeriesFormatComponent} from "../../shared/series-format/series-format.co import {MangaFormatPipe} from "../../_pipes/manga-format.pipe"; import {LanguageNamePipe} from "../../_pipes/language-name.pipe"; import {AsyncPipe} from "@angular/common"; +import {SafeUrlPipe} from "../../_pipes/safe-url.pipe"; @Component({ selector: 'app-details-tab', @@ -39,7 +40,8 @@ import {AsyncPipe} from "@angular/common"; SeriesFormatComponent, MangaFormatPipe, LanguageNamePipe, - AsyncPipe + AsyncPipe, + SafeUrlPipe ], templateUrl: './details-tab.component.html', styleUrl: './details-tab.component.scss', diff --git a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html index 977e4e548..a8ac15b3a 100644 --- a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html +++ b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html @@ -32,9 +32,9 @@ } @else {
{{t('details')}} - @if ((item.series.volumeCount || 0) > 0 || (item.series.chapterCount || 0) > 0) { - {{t('volume-count', {num: item.series.volumeCount})}} - {{t('chapter-count', {num: item.series.chapterCount})}} + @if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) { + {{t('volume-count', {num: item.series.volumes})}} + {{t('chapter-count', {num: item.series.chapters})}} } @else { {{t('releasing')}} } diff --git a/UI/Web/src/app/admin/license/license.component.html b/UI/Web/src/app/admin/license/license.component.html index 34a4ac196..99b594261 100644 --- a/UI/Web/src/app/admin/license/license.component.html +++ b/UI/Web/src/app/admin/license/license.component.html @@ -4,7 +4,7 @@ {{t('faq-title')}}
-
+

{{t('kavita+-desc-part-1')}} {{t('kavita+-desc-part-2')}} {{t('kavita+-desc-part-3')}}

diff --git a/UI/Web/src/app/admin/manage-logs/manage-logs.component.ts b/UI/Web/src/app/admin/manage-logs/manage-logs.component.ts index e46bb3d76..4e69801c3 100644 --- a/UI/Web/src/app/admin/manage-logs/manage-logs.component.ts +++ b/UI/Web/src/app/admin/manage-logs/manage-logs.component.ts @@ -31,6 +31,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy { constructor(private accountService: AccountService) { } ngOnInit(): void { + // TODO: Come back and implement this one day this.accountService.currentUser$.pipe(take(1)).subscribe(user => { if (user) { this.hubConnection = new HubConnectionBuilder() diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index ac1480009..1f2c8a8fb 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -25,7 +25,7 @@ const ValidIpAddress = /^(\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\: styleUrls: ['./manage-settings.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, NgbTooltip, TitleCasePipe, TranslocoModule, NgTemplateOutlet, PageLayoutModePipe, SettingItemComponent, SettingSwitchComponent, SafeHtmlPipe] + imports: [ReactiveFormsModule, TitleCasePipe, TranslocoModule, SettingItemComponent, SettingSwitchComponent] }) export class ManageSettingsComponent implements OnInit { diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts index c7b6c050a..f8c60660b 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts @@ -18,7 +18,6 @@ import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; -import {ConfirmService} from "../../shared/confirm.service"; import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component"; import {DefaultModalOptions} from "../../_models/default-modal-options"; import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable"; @@ -38,7 +37,8 @@ interface AdhocTask { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ReactiveFormsModule, AsyncPipe, TitleCasePipe, DefaultValuePipe, - TranslocoModule, TranslocoLocaleModule, UtcToLocalTimePipe, SettingItemComponent, SettingButtonComponent, NgxDatatableModule] + TranslocoModule, TranslocoLocaleModule, UtcToLocalTimePipe, SettingItemComponent, + SettingButtonComponent, NgxDatatableModule] }) export class ManageTasksSettingsComponent implements OnInit { diff --git a/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.scss b/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.scss index 56a2d5278..9c2e6d76f 100644 --- a/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.scss +++ b/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.scss @@ -2,7 +2,7 @@ border-radius: 0.5rem; } -.blog-content { +:host ::ng-deep .blog-content { margin-bottom: 1.5rem; line-height: 1.6; word-wrap: break-word; diff --git a/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.ts b/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.ts index 4caeae127..40973449d 100644 --- a/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.ts +++ b/UI/Web/src/app/announcements/_components/changelog-update-item/changelog-update-item.component.ts @@ -27,4 +27,5 @@ export class ChangelogUpdateItemComponent { @Input({required:true}) update: UpdateVersionEvent | null = null; @Input() index: number = 0; @Input() showExtras: boolean = true; + } diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index e7d7723ea..f859b33d7 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -103,9 +103,7 @@ const blackList = [Action.Edit, Action.Info, Action.IncognitoRead, Action.Read, MangaFormatPipe, DefaultDatePipe, TimeAgoPipe, - TagBadgeComponent, PublicationStatusPipe, - NgbTooltip, BytesPipe, ImageComponent, NgbCollapse, @@ -117,7 +115,6 @@ const blackList = [Action.Edit, Action.Info, Action.IncognitoRead, Action.Read, EditListComponent, SettingButtonComponent, SettingItemComponent, - ReadTimePipe, ], templateUrl: './edit-series-modal.component.html', styleUrls: ['./edit-series-modal.component.scss'], @@ -655,6 +652,11 @@ export class EditSeriesModalComponent implements OnInit { case Action.Download: this.downloadService.download('series', this.series); break; + case Action.Match: + this.actionService.matchSeries(this.series, _ => { + this.modal.close({success: true, series: this.series, coverImageUpdate: false, updateExternal: true}); + }); + break; } } } diff --git a/UI/Web/src/app/person-detail/person-detail.component.html b/UI/Web/src/app/person-detail/person-detail.component.html index e5021bfd4..6a992e613 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.html +++ b/UI/Web/src/app/person-detail/person-detail.component.html @@ -7,6 +7,13 @@

{{person.name}} + + @if (person.aniListId) { + + + + }

@@ -45,6 +52,7 @@ }
} +
diff --git a/UI/Web/src/app/person-detail/person-detail.component.ts b/UI/Web/src/app/person-detail/person-detail.component.ts index e86f6713b..83de0f18a 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.ts +++ b/UI/Web/src/app/person-detail/person-detail.component.ts @@ -41,6 +41,7 @@ import {ThemeService} from "../_services/theme.service"; import {DefaultModalOptions} from "../_models/default-modal-options"; import {ToastrService} from "ngx-toastr"; import {LicenseService} from "../_services/license.service"; +import {SafeUrlPipe} from "../_pipes/safe-url.pipe"; @Component({ selector: 'app-person-detail', @@ -49,16 +50,15 @@ import {LicenseService} from "../_services/license.service"; AsyncPipe, ImageComponent, SideNavCompanionBarComponent, - NgStyle, ReadMoreComponent, TagBadgeComponent, PersonRolePipe, CarouselReelComponent, - SeriesCardComponent, CardItemComponent, CardActionablesComponent, TranslocoDirective, - ChapterCardComponent + ChapterCardComponent, + SafeUrlPipe ], templateUrl: './person-detail.component.html', styleUrl: './person-detail.component.scss', @@ -93,8 +93,14 @@ export class PersonDetailComponent { filter: SeriesFilterV2 | null = null; personActions: Array> = this.actionService.getPersonActions(this.handleAction.bind(this)); chaptersByRole: any = {}; + anilistUrl: string = ''; private readonly personSubject = new BehaviorSubject(null); - protected readonly person$ = this.personSubject.asObservable(); + protected readonly person$ = this.personSubject.asObservable().pipe(tap(p => { + if (p?.aniListId) { + this.anilistUrl = translate('person-detail.anilist-url').replace('{AniListId}', p!.aniListId! + ''); + this.cdRef.markForCheck(); + } + })); get HasCoverImage() { return (this.person as Person).coverImage; diff --git a/UI/Web/src/app/settings/_components/settings/settings.component.html b/UI/Web/src/app/settings/_components/settings/settings.component.html index ccb75c7ec..168c98a85 100644 --- a/UI/Web/src/app/settings/_components/settings/settings.component.html +++ b/UI/Web/src/app/settings/_components/settings/settings.component.html @@ -1,11 +1,11 @@
-

+

{{fragment | settingFragment}}

-
+
@if (accountService.currentUser$ | async; as user) { @if (accountService.hasAdminRole(user)) { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index cf2feec71..229a7f214 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -763,6 +763,7 @@ "matched-status-label": "Matched", "unmatched-status-label": "Not Matched", "blacklist-status-label": "Needs Manual Match", + "dont-match-status-label": "{{dont-match-label}}", "all-status-label": "All", "dont-match-label": "Don't Match", "no-data": "{{common.no-data}}" @@ -1071,7 +1072,8 @@ "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" + "all-roles": "Roles", + "anilist-url": "{{edit-person-modal.anilist-tooltip}}" }, "library-settings-modal": { @@ -2677,7 +2679,7 @@ "title": "Actions", "copy-settings": "Copy Settings From", "match": "Match", - "match-description": "Match Series with Kavita+ manually" + "match-tooltip": "Match Series with Kavita+ manually" }, "preferences": { diff --git a/openapi.json b/openapi.json index 3bab561af..fd1cf8071 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.9", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.10", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"