diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs index 15648318c..5c8fad2e7 100644 --- a/API/Controllers/PersonController.cs +++ b/API/Controllers/PersonController.cs @@ -42,10 +42,15 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetPersonDtoByName(name, User.GetUserId())); } + /// + /// Returns all roles for a Person + /// + /// + /// [HttpGet("roles")] - public async Task>> GetRolesForPersonByName(string name) + public async Task>> GetRolesForPersonByName(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(name, User.GetUserId())); + return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, User.GetUserId())); } /// diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index 333d4b45f..6fee371bb 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -490,23 +490,27 @@ public class UploadController : BaseApiController [HttpPost("person")] public async Task UploadPersonCoverImageFromUrl(UploadFileDto uploadFileDto) { - // Check if Url is non-empty, request the image and place in temp, then ask image service to handle it. - // See if we can do this all in memory without touching underlying system - if (string.IsNullOrEmpty(uploadFileDto.Url)) - { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required")); - } - try { var person = await _unitOfWork.PersonRepository.GetPersonById(uploadFileDto.Id); if (person == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); - var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetPersonFormat(uploadFileDto.Id)}"); - if (!string.IsNullOrEmpty(filePath)) + if (!string.IsNullOrEmpty(uploadFileDto.Url)) { - person.CoverImage = filePath; - person.CoverImageLocked = true; + var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetPersonFormat(uploadFileDto.Id)}"); + + if (!string.IsNullOrEmpty(filePath)) + { + person.CoverImage = filePath; + person.CoverImageLocked = true; + _imageService.UpdateColorScape(person); + _unitOfWork.PersonRepository.Update(person); + } + } + else + { + person.CoverImage = string.Empty; + person.CoverImageLocked = false; _imageService.UpdateColorScape(person); _unitOfWork.PersonRepository.Update(person); } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 4c5093377..ee911ccba 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -33,7 +33,7 @@ public interface IPersonRepository Task GetCoverImageAsync(int personId); Task GetCoverImageByNameAsync(string name); - Task> GetRolesForPersonByName(string name, int userId); + Task> GetRolesForPersonByName(int personId, int userId); Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams); Task GetPersonById(int personId); Task GetPersonDtoByName(string name, int userId); @@ -145,18 +145,28 @@ public class PersonRepository : IPersonRepository .SingleOrDefaultAsync(); } - public async Task> GetRolesForPersonByName(string name, int userId) + public async Task> GetRolesForPersonByName(int personId, int userId) { - // TODO: This will need to check both series and chapters (in cases where komf only updates series) - var normalized = name.ToNormalized(); var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Person - .Where(p => p.NormalizedName == normalized) + // Query roles from ChapterPeople + var chapterRoles = await _context.Person + .Where(p => p.Id == personId) .RestrictAgainstAgeRestriction(ageRating) .SelectMany(p => p.ChapterPeople.Select(cp => cp.Role)) .Distinct() .ToListAsync(); + + // Query roles from SeriesMetadataPeople + var seriesRoles = await _context.Person + .Where(p => p.Id == personId) + .RestrictAgainstAgeRestriction(ageRating) + .SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role)) + .Distinct() + .ToListAsync(); + + // Combine and return distinct roles + return chapterRoles.Union(seriesRoles).Distinct(); } public async Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 7cfcd36e6..f826b67f7 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -1046,8 +1046,6 @@ public class SeriesRepository : ISeriesRepository .Select(u => u.CollapseSeriesRelationships) .SingleOrDefaultAsync(); - - query ??= _context.Series .AsNoTracking(); @@ -1064,7 +1062,6 @@ public class SeriesRepository : ISeriesRepository query = ApplyWantToReadFilter(filter, query, userId); - query = await ApplyCollectionFilter(filter, query, userId, userRating); @@ -1136,6 +1133,7 @@ public class SeriesRepository : ISeriesRepository var seriesIds = _context.AppUser.Where(u => u.Id == userId) .SelectMany(u => u.WantToRead) .Select(s => s.SeriesId); + if (bool.Parse(wantToReadStmt.Value)) { query = query.Where(s => seriesIds.Contains(s.Id)); @@ -1152,6 +1150,7 @@ public class SeriesRepository : ISeriesRepository { var filterIncludeLibs = new List(); var filterExcludeLibs = new List(); + if (filter.Statements != null) { foreach (var stmt in filter.Statements.Where(stmt => stmt.Field == FilterField.Libraries)) @@ -1993,17 +1992,25 @@ public class SeriesRepository : ISeriesRepository public async Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter) { var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - var query = _context.AppUser + var seriesIds = await _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) - .Select(w => w.Series) + .Select(w => w.Series.Id) + .Distinct() + .ToListAsync(); + + var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None); + + // Apply the Want to Read filtering + query = query.Where(s => seriesIds.Contains(s.Id)); + + var retSeries = query + .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking(); - var filteredQuery = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None, query); - - return await PagedList.CreateAsync(filteredQuery.ProjectTo(_mapper.ConfigurationProvider), userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); } public async Task> GetWantToReadForUserAsync(int userId) diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index dc3743c78..ff9d3ec03 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -259,8 +259,7 @@ public static class SeriesFilter .Where(p => p != null && p.AppUserId == userId) .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0) * 100 }) - .AsSplitQuery() - .AsEnumerable(); + .AsSplitQuery(); switch (comparison) { diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index 201c8dd28..627111b89 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -79,7 +79,6 @@ public static class QueryableExtensions .Include(l => l.AppUsers) .Where(lib => lib.AppUsers.Any(user => user.Id == userId)) .IsRestricted(queryContext) - .AsNoTracking() .AsSplitQuery() .Select(lib => lib.Id); } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 8d2600f71..c1c3ea71f 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -585,6 +585,7 @@ public class ImageService : IImageService /// public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth) { + // TODO: This code has no concept of cropping nor Thumbnail Size try { using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index c40252188..119baaf94 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -334,7 +334,10 @@ public class SeriesService : ISeriesService private async Task HandlePeopleUpdateAsync(SeriesMetadata metadata, ICollection peopleDtos, PersonRole role) { // Normalize all names from the DTOs - var normalizedNames = peopleDtos.Select(p => Parser.Normalize(p.Name)).ToList(); + var normalizedNames = peopleDtos + .Select(p => Parser.Normalize(p.Name)) + .Distinct() + .ToList(); // Bulk select people who already exist in the database var existingPeople = await _unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames); diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 04d6df2c1..2dbd4ed34 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -83,6 +83,7 @@ public class TaskScheduler : ITaskScheduler public static readonly ImmutableArray ScanTasks = ["ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"]; + private static readonly ImmutableArray NonCronOptions = ["disabled", "daily", "weekly"]; private static readonly Random Rnd = new Random(); @@ -122,10 +123,10 @@ public class TaskScheduler : ITaskScheduler public async Task ScheduleTasks() { _logger.LogInformation("Scheduling reoccurring tasks"); - var nonCronOptions = new List(["disabled", "daily", "weekly"]); + var setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Value; - if (setting == null || (!nonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting))) + if (IsInvalidCronSetting(setting)) { _logger.LogError("Scan Task has invalid cron, defaulting to Daily"); RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), @@ -141,9 +142,9 @@ public class TaskScheduler : ITaskScheduler setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value; - if (setting == null || (!nonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting))) + if (IsInvalidCronSetting(setting)) { - _logger.LogError("Backup Task has invalid cron, defaulting to Daily"); + _logger.LogError("Backup Task has invalid cron, defaulting to Weekly"); RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, RecurringJobOptions); } @@ -161,18 +162,18 @@ public class TaskScheduler : ITaskScheduler } setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup)).Value; - if (setting == null || (!nonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting))) - { - _logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), - CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); - } - else + if (IsInvalidCronSetting(setting)) { _logger.LogError("Cleanup Task has invalid cron, defaulting to Daily"); RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions); } + else + { + _logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting); + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), + CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); + } RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), @@ -186,6 +187,11 @@ public class TaskScheduler : ITaskScheduler await ScheduleKavitaPlusTasks(); } + private static bool IsInvalidCronSetting(string setting) + { + return setting == null || (!NonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting)); + } + public async Task ScheduleKavitaPlusTasks() { // KavitaPlus based (needs license check) diff --git a/UI/Web/src/app/_models/default-modal-options.ts b/UI/Web/src/app/_models/default-modal-options.ts new file mode 100644 index 000000000..4dbb391a9 --- /dev/null +++ b/UI/Web/src/app/_models/default-modal-options.ts @@ -0,0 +1 @@ +export const DefaultModalOptions = {scrollable: true, size: 'xl', fullscreen: 'xl'}; diff --git a/UI/Web/src/app/_pipes/person-role.pipe.ts b/UI/Web/src/app/_pipes/person-role.pipe.ts index c73f0b04d..c1395ae5b 100644 --- a/UI/Web/src/app/_pipes/person-role.pipe.ts +++ b/UI/Web/src/app/_pipes/person-role.pipe.ts @@ -1,6 +1,6 @@ import {inject, Pipe, PipeTransform} from '@angular/core'; import { PersonRole } from '../_models/metadata/person'; -import {TranslocoService} from "@jsverse/transloco"; +import {translate, TranslocoService} from "@jsverse/transloco"; @Pipe({ name: 'personRole', @@ -8,39 +8,38 @@ import {TranslocoService} from "@jsverse/transloco"; }) export class PersonRolePipe implements PipeTransform { - translocoService = inject(TranslocoService); transform(value: PersonRole): string { switch (value) { case PersonRole.Artist: - return this.translocoService.translate('person-role-pipe.artist'); + return translate('person-role-pipe.artist'); case PersonRole.Character: - return this.translocoService.translate('person-role-pipe.character'); + return translate('person-role-pipe.character'); case PersonRole.Colorist: - return this.translocoService.translate('person-role-pipe.colorist'); + return translate('person-role-pipe.colorist'); case PersonRole.CoverArtist: - return this.translocoService.translate('person-role-pipe.artist'); + return translate('person-role-pipe.artist'); case PersonRole.Editor: - return this.translocoService.translate('person-role-pipe.editor'); + return translate('person-role-pipe.editor'); case PersonRole.Inker: - return this.translocoService.translate('person-role-pipe.inker'); + return translate('person-role-pipe.inker'); case PersonRole.Letterer: - return this.translocoService.translate('person-role-pipe.letterer'); + return translate('person-role-pipe.letterer'); case PersonRole.Penciller: - return this.translocoService.translate('person-role-pipe.penciller'); + return translate('person-role-pipe.penciller'); case PersonRole.Publisher: - return this.translocoService.translate('person-role-pipe.publisher'); + return translate('person-role-pipe.publisher'); case PersonRole.Imprint: - return this.translocoService.translate('person-role-pipe.imprint'); + return translate('person-role-pipe.imprint'); case PersonRole.Writer: - return this.translocoService.translate('person-role-pipe.writer'); + return translate('person-role-pipe.writer'); case PersonRole.Team: - return this.translocoService.translate('person-role-pipe.team'); + return translate('person-role-pipe.team'); case PersonRole.Location: - return this.translocoService.translate('person-role-pipe.location'); + return translate('person-role-pipe.location'); case PersonRole.Translator: - return this.translocoService.translate('person-role-pipe.translator'); + return translate('person-role-pipe.translator'); case PersonRole.Other: - return this.translocoService.translate('person-role-pipe.other'); + return translate('person-role-pipe.other'); default: return ''; } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 4933ca092..1d214cd98 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -27,6 +27,7 @@ import {FilterService} from "./filter.service"; import {ReadingListService} from "./reading-list.service"; import {ChapterService} from "./chapter.service"; import {VolumeService} from "./volume.service"; +import {DefaultModalOptions} from "../_models/default-modal-options"; export type LibraryActionCallback = (library: Partial) => void; export type SeriesActionCallback = (series: Series) => void; @@ -121,7 +122,7 @@ export class ActionService { } editLibrary(library: Partial, callback?: LibraryActionCallback) { - const modalRef = this.modalService.open(LibrarySettingsModalComponent, {size: 'xl', fullscreen: 'md'}); + const modalRef = this.modalService.open(LibrarySettingsModalComponent, DefaultModalOptions); modalRef.componentInstance.library = library; modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => { if (callback) callback(library) diff --git a/UI/Web/src/app/_services/jumpbar.service.ts b/UI/Web/src/app/_services/jumpbar.service.ts index e4ac0b01f..d9919ff57 100644 --- a/UI/Web/src/app/_services/jumpbar.service.ts +++ b/UI/Web/src/app/_services/jumpbar.service.ts @@ -16,7 +16,8 @@ export class JumpbarService { getResumeKey(key: string) { - if (this.resumeKeys.hasOwnProperty(key)) return this.resumeKeys[key]; + const k = key.toUpperCase(); + if (this.resumeKeys.hasOwnProperty(k)) return this.resumeKeys[k]; return ''; } @@ -26,7 +27,8 @@ export class JumpbarService { } saveResumeKey(key: string, value: string) { - this.resumeKeys[key] = value; + const k = key.toUpperCase(); + this.resumeKeys[k] = value; } saveResumePosition(url: string, value: number) { @@ -75,9 +77,11 @@ export class JumpbarService { _removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array, jumpBarKeysToRender: Array) { const removedIndexes: Array = []; + for(let removal = 0; removal < numberOfRemovals; removal++) { let min = 100000000; let minIndex = -1; + for(let i = 1; i < midPoint; i++) { if (jumpBarKeys[i].size < min && !removedIndexes.includes(i)) { min = jumpBarKeys[i].size; @@ -101,7 +105,7 @@ export class JumpbarService { getJumpKeys(data :Array, keySelector: (data: any) => string) { const keys: {[key: string]: number} = {}; data.forEach(obj => { - let ch = keySelector(obj).charAt(0); + let ch = keySelector(obj).charAt(0).toUpperCase(); if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { ch = '#'; } @@ -111,10 +115,11 @@ export class JumpbarService { keys[ch] += 1; }); return Object.keys(keys).map(k => { + k = k.toUpperCase(); return { key: k, size: keys[k], - title: k.toUpperCase() + title: k } }).sort((a, b) => { if (a.key < b.key) return -1; diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index 00d4b98af..f37ba2d65 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -29,8 +29,8 @@ export class PersonService { return this.httpClient.get(this.baseUrl + `person?name=${name}`); } - getRolesForPerson(name: string) { - return this.httpClient.get>(this.baseUrl + `person/roles?name=${name}`); + getRolesForPerson(personId: number) { + return this.httpClient.get>(this.baseUrl + `person/roles?personId=${personId}`); } getSeriesMostKnownFor(personId: number) { diff --git a/UI/Web/src/app/_single-modules/publisher-flipper/publisher-flipper.component.html b/UI/Web/src/app/_single-modules/publisher-flipper/publisher-flipper.component.html index a1907f85a..00b625413 100644 --- a/UI/Web/src/app/_single-modules/publisher-flipper/publisher-flipper.component.html +++ b/UI/Web/src/app/_single-modules/publisher-flipper/publisher-flipper.component.html @@ -4,7 +4,7 @@
0) { this.currentPublisher = this.publishers[0]; this.nextPublisher = this.publishers[1] || this.publishers[0]; - if (this.publishers.length > 1) { - this.startFlipping(); - } + } + } + + ngAfterViewInit() { + if (this.publishers.length > 1) { + this.startFlipping(); // Start flipping cycle once the view is initialized + } + } + + ngAfterViewChecked() { + // This lifecycle hook will be called after Angular performs change detection in each cycle + if (this.isFlipped) { + // Only update publishers after the flip is complete + this.currentIndex = (this.currentIndex + 1) % this.publishers.length; + this.currentPublisher = this.publishers[this.currentIndex]; + this.nextPublisher = this.publishers[(this.currentIndex + 1) % this.publishers.length]; } } @@ -55,22 +77,9 @@ export class PublisherFlipperComponent implements OnInit, OnDestroy { private startFlipping() { this.intervalId = setInterval(() => { - // First flip - this.isFlipped = true; - this.cdRef.markForCheck(); - - // Update content after flip animation completes - setTimeout(() => { - // Update indices and content - this.currentIndex = (this.currentIndex + 1) % this.publishers.length; - this.currentPublisher = this.publishers[this.currentIndex]; - this.nextPublisher = this.publishers[(this.currentIndex + 1) % this.publishers.length]; - - // Reset flip - this.isFlipped = false; - - this.cdRef.markForCheck(); - }, ANIMATION_TIME); // Full transition time to ensure flip completes + // Toggle flip state, initiating the flip animation + this.isFlipped = !this.isFlipped; + this.cdRef.detectChanges(); // Explicitly detect changes to trigger re-render }, ANIMATION_TIME); } diff --git a/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.ts b/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.ts index 74d1729f3..efdcc76c7 100644 --- a/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.ts +++ b/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.ts @@ -29,6 +29,7 @@ import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {TranslocoLocaleModule} from "@jsverse/transloco-locale"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; +import {DefaultModalOptions} from "../../_models/default-modal-options"; @Component({ selector: 'app-manage-scrobble-errors', @@ -111,7 +112,7 @@ export class ManageScrobbleErrorsComponent implements OnInit { editSeries(seriesId: number) { this.seriesService.getSeries(seriesId).subscribe(series => { - const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'xl' }); + const modalRef = this.modalService.open(EditSeriesModalComponent, DefaultModalOptions); modalRef.componentInstance.series = series; }); } 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 5d4dc9ad3..acfbe89f4 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 @@ -20,6 +20,7 @@ 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"; interface AdhocTask { name: string; @@ -128,7 +129,7 @@ export class ManageTasksSettingsComponent implements OnInit { this.toastr.info(translate('toasts.no-updates')); return; } - const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); + const modalRef = this.modalService.open(UpdateNotificationModalComponent, DefaultModalOptions); modalRef.componentInstance.updateData = update; } }, diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index 850204e07..ad60e391a 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -22,6 +22,7 @@ import {makeBindingParser} from "@angular/compiler"; import {LoadingComponent} from "../../shared/loading/loading.component"; import {TimeAgoPipe} from "../../_pipes/time-ago.pipe"; import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe"; +import {DefaultModalOptions} from "../../_models/default-modal-options"; @Component({ selector: 'app-manage-users', @@ -87,7 +88,7 @@ export class ManageUsersComponent implements OnInit { } openEditUser(member: Member) { - const modalRef = this.modalService.open(EditUserComponent, { scrollable: true, size: 'xl', fullscreen: 'md' }); + const modalRef = this.modalService.open(EditUserComponent, DefaultModalOptions); modalRef.componentInstance.member = member; modalRef.closed.subscribe(() => { this.loadMembers(); @@ -107,7 +108,7 @@ export class ManageUsersComponent implements OnInit { } inviteUser() { - const modalRef = this.modalService.open(InviteUserComponent, {size: 'xl'}); + const modalRef = this.modalService.open(InviteUserComponent, DefaultModalOptions); modalRef.closed.subscribe((successful: boolean) => { this.loadMembers(); }); @@ -133,7 +134,7 @@ export class ManageUsersComponent implements OnInit { } updatePassword(member: Member) { - const modalRef = this.modalService.open(ResetPasswordModalComponent); + const modalRef = this.modalService.open(ResetPasswordModalComponent, DefaultModalOptions); modalRef.componentInstance.member = member; } diff --git a/UI/Web/src/app/cards/series-card/series-card.component.ts b/UI/Web/src/app/cards/series-card/series-card.component.ts index d613ec8c3..2d653a82b 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.ts +++ b/UI/Web/src/app/cards/series-card/series-card.component.ts @@ -43,6 +43,7 @@ import {User} from "../../_models/user"; import {ScrollService} from "../../_services/scroll.service"; import {ReaderService} from "../../_services/reader.service"; import {SeriesFormatComponent} from "../../shared/series-format/series-format.component"; +import {DefaultModalOptions} from "../../_models/default-modal-options"; function deepClone(obj: any): any { if (obj === null || typeof obj !== 'object') { @@ -278,7 +279,7 @@ export class SeriesCardComponent implements OnInit, OnChanges { } openEditModal(data: Series) { - const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'lg' }); + const modalRef = this.modalService.open(EditSeriesModalComponent, DefaultModalOptions); modalRef.componentInstance.series = data; modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => { if (closeResult.success) { diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts index be99d110f..c1e3482a5 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts @@ -83,6 +83,7 @@ import {PublicationStatusPipe} from "../_pipes/publication-status.pipe"; import {DefaultDatePipe} from "../_pipes/default-date.pipe"; import {MangaFormatPipe} from "../_pipes/manga-format.pipe"; import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component"; +import {DefaultModalOptions} from "../_models/default-modal-options"; enum TabID { Related = 'related-tab', @@ -317,7 +318,7 @@ export class ChapterDetailComponent implements OnInit { } openEditModal() { - const ref = this.modalService.open(EditChapterModalComponent, { size: 'xl' }); + const ref = this.modalService.open(EditChapterModalComponent, DefaultModalOptions); ref.componentInstance.chapter = this.chapter; ref.componentInstance.libraryType = this.libraryType; ref.componentInstance.libraryId = this.libraryId; diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts index 4df8fa91b..6e1e501b9 100644 --- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts +++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts @@ -42,6 +42,7 @@ import {SeriesCardComponent} from "../../../cards/series-card/series-card.compon import {ActionService} from "../../../_services/action.service"; import {KEY_CODES} from "../../../shared/_services/utility.service"; import {WikiLink} from "../../../_models/wiki"; +import {DefaultModalOptions} from "../../../_models/default-modal-options"; @Component({ @@ -153,7 +154,7 @@ export class AllCollectionsComponent implements OnInit { }); break; case(Action.Edit): - const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true }); + const modalRef = this.modalService.open(EditCollectionTagsComponent, DefaultModalOptions); modalRef.componentInstance.tag = collectionTag; modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => { if (results.success) { diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts index 9e8d4fc0e..e0192781d 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts @@ -64,6 +64,7 @@ import {PromotedIconComponent} from "../../../shared/_components/promoted-icon/p import { SmartCollectionDrawerComponent } from "../../../_single-module/smart-collection-drawer/smart-collection-drawer.component"; +import {DefaultModalOptions} from "../../../_models/default-modal-options"; @Component({ selector: 'app-collection-detail', @@ -330,7 +331,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { } openEditCollectionTagModal(collectionTag: UserCollection) { - const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true }); + const modalRef = this.modalService.open(EditCollectionTagsComponent, DefaultModalOptions); modalRef.componentInstance.tag = this.collectionTag; modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => { this.updateTag(this.collectionTag.id); diff --git a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts index 38bf8cd9c..ec9dd5260 100644 --- a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts +++ b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts @@ -26,6 +26,7 @@ import { SentenceCasePipe } from '../../../_pipes/sentence-case.pipe'; import { CircularLoaderComponent } from '../../../shared/circular-loader/circular-loader.component'; import { NgClass, NgStyle, AsyncPipe } from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; +import {DefaultModalOptions} from "../../../_models/default-modal-options"; @Component({ selector: 'app-nav-events-toggle', @@ -159,7 +160,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { handleUpdateAvailableClick(message: NotificationProgressEvent | UpdateVersionEvent) { if (this.updateNotificationModalRef != null) { return; } - this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); + this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, DefaultModalOptions); if (message.hasOwnProperty('body')) { this.updateNotificationModalRef.componentInstance.updateData = (message as NotificationProgressEvent).body as UpdateVersionEvent; } else { 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 47a051411..3ebe918f1 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.ts +++ b/UI/Web/src/app/person-detail/person-detail.component.ts @@ -38,6 +38,7 @@ import {EditPersonModalComponent} from "./_modal/edit-person-modal/edit-person-m import {translate, TranslocoDirective} from "@jsverse/transloco"; import {ChapterCardComponent} from "../cards/chapter-card/chapter-card.component"; import {ThemeService} from "../_services/theme.service"; +import {DefaultModalOptions} from "../_models/default-modal-options"; @Component({ selector: 'app-person-detail', @@ -109,7 +110,7 @@ export class PersonDetailComponent { this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor); // Fetch roles and process them - this.roles$ = this.personService.getRolesForPerson(this.personName).pipe( + this.roles$ = this.personService.getRolesForPerson(this.person.id).pipe( tap(roles => { this.roles = roles; this.filter = this.createFilter(roles); @@ -187,7 +188,7 @@ export class PersonDetailComponent { handleAction(action: ActionItem, person: Person) { switch (action.action) { case(Action.Edit): - const ref = this.modalService.open(EditPersonModalComponent, {scrollable: true, size: 'lg', fullscreen: 'md'}); + const ref = this.modalService.open(EditPersonModalComponent, DefaultModalOptions); ref.componentInstance.person = this.person; ref.closed.subscribe(r => { diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html index f82e8e4b0..51a046214 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.html @@ -5,10 +5,9 @@
{{entity.publishers[0].name}}
+
} - - @@ -17,7 +16,7 @@ - @if (libraryType === LibraryType.Book || libraryType === LibraryType.LightNovel) { + @if ((libraryType === LibraryType.Book || libraryType === LibraryType.LightNovel) && mangaFormat !== MangaFormat.PDF) { {{t('words-count', {num: readingTimeEntity.wordCount | compactNumber})}} } @else { {{t('pages-count', {num: readingTimeEntity.pages | compactNumber})}} diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts index 3855656f6..4d1d91b1e 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts @@ -46,6 +46,8 @@ export class MetadataDetailRowComponent { private readonly filterUtilityService = inject(FilterUtilitiesService); protected readonly LibraryType = LibraryType; + protected readonly FilterField = FilterField; + protected readonly MangaFormat = MangaFormat; @Input({required: true}) entity!: IHasCast; @Input({required: true}) readingTimeEntity!: IHasReadingTime; @@ -59,7 +61,4 @@ export class MetadataDetailRowComponent { if (queryParamName === FilterField.None) return; this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe(); } - - - protected readonly FilterField = FilterField; } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 8893d78aa..f5de0fbe4 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -145,6 +145,7 @@ import {UserCollection} from "../../../_models/collection-tag"; import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component"; import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe"; import {CoverImageComponent} from "../../../_single-module/cover-image/cover-image.component"; +import {DefaultModalOptions} from "../../../_models/default-modal-options"; enum TabID { @@ -1041,7 +1042,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } openEditChapter(chapter: Chapter) { - const ref = this.modalService.open(EditChapterModalComponent, { size: 'xl' }); + const ref = this.modalService.open(EditChapterModalComponent, DefaultModalOptions); ref.componentInstance.chapter = chapter; ref.componentInstance.libraryType = this.libraryType; ref.componentInstance.seriesId = this.series?.id; @@ -1055,7 +1056,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } openEditVolume(volume: Volume) { - const ref = this.modalService.open(EditVolumeModalComponent, { size: 'xl' }); + const ref = this.modalService.open(EditVolumeModalComponent, DefaultModalOptions); ref.componentInstance.volume = volume; ref.componentInstance.libraryType = this.libraryType; ref.componentInstance.seriesId = this.series?.id; @@ -1069,7 +1070,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } openEditSeriesModal() { - const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'xl' }); + const modalRef = this.modalService.open(EditSeriesModalComponent, DefaultModalOptions); modalRef.componentInstance.series = this.series; modalRef.closed.subscribe((closeResult: EditSeriesModalCloseResult) => { if (closeResult.success) { @@ -1088,7 +1089,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { openReviewModal() { const userReview = this.getUserReview(); - const modalRef = this.modalService.open(ReviewSeriesModalComponent, { scrollable: true, size: 'lg' }); + const modalRef = this.modalService.open(ReviewSeriesModalComponent, DefaultModalOptions); modalRef.componentInstance.series = this.series; if (userReview.length > 0) { modalRef.componentInstance.review = userReview[0]; diff --git a/UI/Web/src/app/shared/person-badge/person-badge.component.html b/UI/Web/src/app/shared/person-badge/person-badge.component.html index 544d2de98..c6beb422f 100644 --- a/UI/Web/src/app/shared/person-badge/person-badge.component.html +++ b/UI/Web/src/app/shared/person-badge/person-badge.component.html @@ -2,13 +2,16 @@
@if (HasCoverImage) { - - +
+ + +
} @else { - +
+ +
} diff --git a/UI/Web/src/app/shared/person-badge/person-badge.component.scss b/UI/Web/src/app/shared/person-badge/person-badge.component.scss index 4e7e7d1a8..4679209bf 100644 --- a/UI/Web/src/app/shared/person-badge/person-badge.component.scss +++ b/UI/Web/src/app/shared/person-badge/person-badge.component.scss @@ -7,11 +7,11 @@ font-size: .8rem; display: inline-block; cursor: pointer; - width: 100px; + width: 120px; word-break: break-word; i { - font-size: 2rem; + font-size: 2.96rem; font-weight: bold; cursor: pointer; } diff --git a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts index f64d859f2..582453427 100644 --- a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts +++ b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts @@ -19,6 +19,7 @@ import {ScrobbleEventTypePipe} from "../../_pipes/scrobble-event-type.pipe"; import {SortableHeader} from "../../_single-module/table/_directives/sortable-header.directive"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; import {EditDeviceModalComponent} from "../_modals/edit-device-modal/edit-device-modal.component"; +import {DefaultModalOptions} from "../../_models/default-modal-options"; @Component({ selector: 'app-manage-devices', @@ -71,7 +72,7 @@ export class ManageDevicesComponent implements OnInit { } addDevice() { - const ref = this.modalService.open(EditDeviceModalComponent, { scrollable: true, size: 'xl', fullscreen: 'md' }); + const ref = this.modalService.open(EditDeviceModalComponent, DefaultModalOptions); ref.componentInstance.device = null; ref.closed.subscribe((result: Device | null) => { @@ -82,7 +83,7 @@ export class ManageDevicesComponent implements OnInit { } editDevice(device: Device) { - const ref = this.modalService.open(EditDeviceModalComponent, { scrollable: true, size: 'xl', fullscreen: 'md' }); + const ref = this.modalService.open(EditDeviceModalComponent, DefaultModalOptions); ref.componentInstance.device = device; ref.closed.subscribe((result: Device | null) => { diff --git a/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.ts b/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.ts index b70831c53..561068404 100644 --- a/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.ts +++ b/UI/Web/src/app/user-settings/theme-manager/theme-manager.component.ts @@ -31,6 +31,7 @@ import {Select2Module} from "ng-select2-component"; import {LoadingComponent} from "../../shared/loading/loading.component"; import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {PreviewImageModalComponent} from "../../shared/_components/carousel-modal/preview-image-modal.component"; +import {DefaultModalOptions} from "../../_models/default-modal-options"; interface ThemeContainer { downloadable?: DownloadableSiteTheme; @@ -191,7 +192,7 @@ export class ThemeManagerComponent { previewImage(imgUrl: string) { if (imgUrl === '') return; - const ref = this.modalService.open(PreviewImageModalComponent, {size: 'xl', fullscreen: 'lg'}); + const ref = this.modalService.open(PreviewImageModalComponent, DefaultModalOptions); ref.componentInstance.title = this.selectedTheme!.name; ref.componentInstance.image = imgUrl; } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.ts b/UI/Web/src/app/volume-detail/volume-detail.component.ts index 859247946..fb11c93e1 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -88,6 +88,7 @@ import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations. import {DefaultDatePipe} from "../_pipes/default-date.pipe"; import {MangaFormatPipe} from "../_pipes/manga-format.pipe"; import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component"; +import {DefaultModalOptions} from "../_models/default-modal-options"; enum TabID { @@ -527,7 +528,7 @@ export class VolumeDetailComponent implements OnInit { } openEditModal() { - const ref = this.modalService.open(EditVolumeModalComponent, { size: 'xl' }); + const ref = this.modalService.open(EditVolumeModalComponent, DefaultModalOptions); ref.componentInstance.volume = this.volume; ref.componentInstance.libraryType = this.libraryType; ref.componentInstance.libraryId = this.libraryId; @@ -537,7 +538,7 @@ export class VolumeDetailComponent implements OnInit { } openEditChapterModal(chapter: Chapter) { - const ref = this.modalService.open(EditChapterModalComponent, { size: 'xl' }); + const ref = this.modalService.open(EditChapterModalComponent, DefaultModalOptions); ref.componentInstance.chapter = chapter; ref.componentInstance.libraryType = this.libraryType; ref.componentInstance.libraryId = this.libraryId;