diff --git a/.github/workflows/openapi-gen.yml b/.github/workflows/openapi-gen.yml new file mode 100644 index 000000000..517334ead --- /dev/null +++ b/.github/workflows/openapi-gen.yml @@ -0,0 +1,65 @@ +name: Generate OpenAPI Documentation + +on: + push: + branches: [ 'develop', '!release/**' ] + paths: + - '**/*.cs' + - '**/*.csproj' + pull_request: + branches: [ 'develop', '!release/**' ] + workflow_dispatch: + +jobs: + generate-openapi: + runs-on: ubuntu-latest + # Only run on direct pushes to develop, not PRs + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Install dependencies + run: dotnet restore + + - name: Build project + run: dotnet build API/API.csproj --configuration Debug + + - name: Get Swashbuckle version + id: swashbuckle-version + run: | + VERSION=$(grep -o '> $GITHUB_OUTPUT + echo "Found Swashbuckle.AspNetCore version: $VERSION" + + - name: Install matching Swashbuckle CLI tool + run: | + dotnet new tool-manifest --force + dotnet tool install Swashbuckle.AspNetCore.Cli --version ${{ steps.swashbuckle-version.outputs.VERSION }} + + - name: Generate OpenAPI file + run: dotnet swagger tofile --output openapi.json API/bin/Debug/net9.0/API.dll v1 + + - name: Check for changes + id: git-check + run: | + git add openapi.json + git diff --staged --quiet openapi.json || echo "has_changes=true" >> $GITHUB_OUTPUT + + - name: Commit and push if changed + if: steps.git-check.outputs.has_changes == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git commit -m "Update OpenAPI documentation" openapi.json + git push + env: + GITHUB_TOKEN: ${{ secrets.REPO_GHA_PAT }} diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index a47282489..436cd47fd 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -2276,6 +2276,72 @@ public class ExternalMetadataServiceTests : AbstractDbTest Assert.Equal(seriesName, sequel.Relations.First().TargetSeries.Name); } + [Fact] + public async Task Relationships_Prequel_CreatesSequel() + { + await ResetDb(); + + // ID 1: Blue Lock - Episode Nagi + var series = new SeriesBuilder("Blue Lock - Episode Nagi") + .WithLibraryId(1) + .WithFormat(MangaFormat.Archive) + .WithMetadata(new SeriesMetadataBuilder() + .Build()) + .Build(); + _context.Series.Attach(series); + + // ID 2: Blue Lock + var series2 = new SeriesBuilder("Blue Lock") + .WithLibraryId(1) + .WithFormat(MangaFormat.Archive) + .WithMetadata(new SeriesMetadataBuilder() + .Build()) + .Build(); + _context.Series.Attach(series2); + await _context.SaveChangesAsync(); + + var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + metadataSettings.Enabled = true; + metadataSettings.EnableRelationships = true; + _context.MetadataSettings.Update(metadataSettings); + await _context.SaveChangesAsync(); + + // Apply to Blue Lock - Episode Nagi (ID 1), setting Blue Lock (ID 2) as its prequel + await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() + { + Name = "Blue Lock - Episode Nagi", // The series we're updating metadata for + Relations = [new SeriesRelationship() + { + Relation = RelationKind.Prequel, // Blue Lock is the prequel to Nagi + SeriesName = new ALMediaTitle() + { + PreferredTitle = "Blue Lock", + EnglishTitle = "Blue Lock", + NativeTitle = "ブルーロック", + RomajiTitle = "Blue Lock", + }, + PlusMediaFormat = PlusMediaFormat.Manga, + AniListId = 106130, + MalId = 114745, + Provider = ScrobbleProvider.AniList + }] + }, 1); // Apply to series ID 1 (Nagi) + + // Verify Blue Lock - Episode Nagi has Blue Lock as prequel + var nagiSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); + Assert.NotNull(nagiSeries); + Assert.Single(nagiSeries.Relations); + Assert.Equal("Blue Lock", nagiSeries.Relations.First().TargetSeries.Name); + Assert.Equal(RelationKind.Prequel, nagiSeries.Relations.First().RelationKind); + + // Verify Blue Lock has Blue Lock - Episode Nagi as sequel + var blueLockSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); + Assert.NotNull(blueLockSeries); + Assert.Single(blueLockSeries.Relations); + Assert.Equal("Blue Lock - Episode Nagi", blueLockSeries.Relations.First().TargetSeries.Name); + Assert.Equal(RelationKind.Sequel, blueLockSeries.Relations.First().RelationKind); + } + #endregion diff --git a/API/API.csproj b/API/API.csproj index f8e1833ca..8bc02df40 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -12,10 +12,11 @@ latestmajor - - - - + + + + + false diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 7def5c33f..81e2d7924 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -652,9 +652,9 @@ public class SeriesController : BaseApiController /// /// [HttpPost("update-match")] - public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int aniListId) + public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int aniListId, [FromQuery] long? malId) { - BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId)); + BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId)); return Ok(); } diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index 6f5730d93..84b8b3a7c 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -213,9 +213,10 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor { return await _context.Series .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) - .WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) - .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) .Where(s => s.Library.AllowMetadataMatching) + .WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) + .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0) + .Where(s => !s.IsBlacklisted && !s.DontMatch) .OrderByDescending(s => s.Library.Type) .ThenBy(s => s.NormalizedName) .Select(s => s.Id) @@ -229,6 +230,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .Include(s => s.Library) .Include(s => s.ExternalSeriesMetadata) .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) + .Where(s => s.Library.AllowMetadataMatching) .FilterMatchState(filter.MatchStateOption) .OrderBy(s => s.NormalizedName) .ProjectTo(_mapper.ConfigurationProvider) diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 605656d91..78022fa9a 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -18,6 +18,7 @@ using Kavita.Common.Extensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum LibraryIncludes @@ -260,7 +261,7 @@ public class LibraryRepository : ILibraryRepository public async Task> GetAllLanguagesForLibrariesAsync(List? libraryIds) { var ret = await _context.Series - .WhereIf(libraryIds is {Count: > 0} , s => libraryIds.Contains(s.LibraryId)) + .WhereIf(libraryIds is {Count: > 0} , s => libraryIds!.Contains(s.LibraryId)) .Select(s => s.Metadata.Language) .AsSplitQuery() .AsNoTracking() diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index fdbe84e67..5ba2de9f6 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -19,11 +19,13 @@ public interface IScrobbleRepository void Attach(ScrobbleError error); void Remove(ScrobbleEvent evt); void Remove(IEnumerable events); + void Remove(IEnumerable errors); void Update(ScrobbleEvent evt); Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false); Task> GetProcessedEvents(int daysAgo); Task Exists(int userId, int seriesId, ScrobbleEventType eventType); Task> GetScrobbleErrors(); + Task> GetAllScrobbleErrorsForSeries(int seriesId); Task ClearScrobbleErrors(); Task HasErrorForSeries(int seriesId); Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType); @@ -66,6 +68,11 @@ public class ScrobbleRepository : IScrobbleRepository _context.ScrobbleEvent.RemoveRange(events); } + public void Remove(IEnumerable errors) + { + _context.ScrobbleError.RemoveRange(errors); + } + public void Update(ScrobbleEvent evt) { _context.Entry(evt).State = EntityState.Modified; @@ -113,6 +120,13 @@ public class ScrobbleRepository : IScrobbleRepository .ToListAsync(); } + public async Task> GetAllScrobbleErrorsForSeries(int seriesId) + { + return await _context.ScrobbleError + .Where(e => e.SeriesId == seriesId) + .ToListAsync(); + } + public async Task ClearScrobbleErrors() { _context.ScrobbleError.RemoveRange(_context.ScrobbleError); @@ -161,4 +175,5 @@ public class ScrobbleRepository : IScrobbleRepository return await _context.ScrobbleEvent.Where(e => e.SeriesId == seriesId) .ToListAsync(); } + } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index d4f8bbb34..78a42b12f 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -276,18 +276,18 @@ public static class Seed await context.SaveChangesAsync(); // Port, IpAddresses and LoggingLevel are managed in appSettings.json. Update the DB values to match - context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.Port)).Value = Configuration.Port + string.Empty; - context.ServerSetting.First(s => s.Key == ServerSettingKey.IpAddresses).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.IpAddresses)).Value = Configuration.IpAddresses; - context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheDirectory)).Value = directoryService.CacheDirectory + string.Empty; - context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.BackupDirectory)).Value = DirectoryService.BackupDirectory + string.Empty; - context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheSize).Value = + (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheSize)).Value = Configuration.CacheSize + string.Empty; - await context.SaveChangesAsync(); + await context.SaveChangesAsync(); } public static async Task SeedMetadataSettings(DataContext context) diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index 822a859c5..98878ca9f 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -252,8 +252,6 @@ public static class SeriesFilter if (!condition) return queryable; var subQuery = queryable - .Include(s => s.Progress) - .Where(s => s.Progress != null) .Select(s => new { SeriesId = s.Id, @@ -372,7 +370,7 @@ public static class SeriesFilter var subQuery = queryable .Include(s => s.Progress) - .Where(s => s.Progress != null) + .Where(s => s.Progress.Any()) .Select(s => new { SeriesId = s.Id, @@ -435,7 +433,7 @@ public static class SeriesFilter var subQuery = queryable .Include(s => s.Progress) - .Where(s => s.Progress != null) + .Where(s => s.Progress.Any()) .Select(s => new { SeriesId = s.Id, diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 55b652e1f..97527929c 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -311,8 +311,16 @@ public class BookService : IBookService var imageFile = GetKeyForImage(book, image.Attributes[key].Value); image.Attributes.Remove(key); - // UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx - image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile)); + + if (!imageFile.StartsWith("http")) + { + // UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx + image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile)); + } + else + { + image.Attributes.Add(key, imageFile); + } // Add a custom class that the reader uses to ensure images stay within reader parent.AddClass("kavita-scale-width-container"); diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 47cc6cd39..14a05d82f 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -49,7 +49,7 @@ public interface IExternalMetadataService Task> GetStacksForUser(int userId); Task> MatchSeries(MatchSeriesDto dto); - Task FixSeriesMatch(int seriesId, int anilistId); + Task FixSeriesMatch(int seriesId, int anilistId, long? malId); Task UpdateSeriesDontMatch(int seriesId, bool dontMatch); Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId); } @@ -111,7 +111,7 @@ public class ExternalMetadataService : IExternalMetadataService public async Task FetchExternalDataTask() { // Find all Series that are eligible and limit - var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25); + var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, false); if (ids.Count == 0) return; _logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+", ids.Count); @@ -133,6 +133,7 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// + /// If a successful match was made public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) { if (!IsPlusEligible(libraryType)) return false; @@ -150,8 +151,7 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Prefetching Kavita+ data for Series {SeriesId}", seriesId); // Prefetch SeriesDetail data - await GetSeriesDetailPlus(seriesId, libraryType); - return true; + return await GetSeriesDetailPlus(seriesId, libraryType) != null; } public async Task> GetStacksForUser(int userId) @@ -303,7 +303,7 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// - public async Task FixSeriesMatch(int seriesId, int anilistId) + public async Task FixSeriesMatch(int seriesId, int anilistId, long? malId) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); if (series == null) return; @@ -317,7 +317,8 @@ public class ExternalMetadataService : IExternalMetadataService var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, new PlusSeriesRequestDto() { AniListId = anilistId, - SeriesName = string.Empty // Required field + MalId = malId, + SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed }); if (metadata.Series == null) @@ -329,6 +330,11 @@ public class ExternalMetadataService : IExternalMetadataService // Find all scrobble events and rewrite them to be the correct var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); _unitOfWork.ScrobbleRepository.Remove(events); + + // Find all scrobble errors and remove them + var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId); + _unitOfWork.ScrobbleRepository.Remove(errors); + await _unitOfWork.CommitAsync(); // Regenerate all events for the series for all users @@ -566,7 +572,7 @@ public class ExternalMetadataService : IExternalMetadataService return false; } - foreach (var relation in externalMetadataRelations) + foreach (var relation in externalMetadataRelations.Where(r => r.Relation != RelationKind.Parent)) { var names = new [] {relation.SeriesName.PreferredTitle, relation.SeriesName.RomajiTitle, relation.SeriesName.EnglishTitle, relation.SeriesName.NativeTitle}; var relatedSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName( diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index dae9ecab0..f94980c66 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -930,6 +930,7 @@ public class ScrobblingService : IScrobblingService if (await _unitOfWork.ExternalSeriesMetadataRepository.IsBlacklistedSeries(evt.SeriesId)) { + _logger.LogInformation("Series {SeriesName} ({SeriesId}) can't be matched and thus cannot scrobble this event", evt.Series.Name, evt.SeriesId); _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() { Comment = UnknownSeriesErrorMessage, @@ -942,6 +943,7 @@ public class ScrobblingService : IScrobblingService evt.ProcessDateUtc = DateTime.UtcNow; _unitOfWork.ScrobbleRepository.Update(evt); await _unitOfWork.CommitAsync(); + return 0; } diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index a9736854d..ab5f4ae2b 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -9,6 +9,7 @@ using API.DTOs.Theme; using API.Entities; using API.Entities.Enums.Theme; using API.Extensions; +using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Flurl.Http; using HtmlAgilityPack; @@ -192,7 +193,8 @@ public class ThemeService : IThemeService private static List GetPreviewUrls(IEnumerable themeContents) { - return themeContents.Where(c => c.Name.ToLower().EndsWith(".jpg") || c.Name.ToLower().EndsWith(".png") ) + return themeContents + .Where(c => Parser.IsImage(c.Name) ) .Select(p => p.DownloadUrl) .ToList(); } diff --git a/API/config/appsettings.json b/API/config/appsettings.json index 3eeee1c18..c77ff6a30 100644 --- a/API/config/appsettings.json +++ b/API/config/appsettings.json @@ -3,5 +3,5 @@ "Port": 5000, "IpAddresses": "", "BaseUrl": "/", - "Cache": 50 + "Cache": 75 } diff --git a/UI/Web/src/app/_directives/enter-blur.directive.ts b/UI/Web/src/app/_directives/enter-blur.directive.ts new file mode 100644 index 000000000..30329f724 --- /dev/null +++ b/UI/Web/src/app/_directives/enter-blur.directive.ts @@ -0,0 +1,13 @@ +import { Directive, HostListener } from '@angular/core'; + +@Directive({ + selector: '[appEnterBlur]', + standalone: true, +}) +export class EnterBlurDirective { + @HostListener('keydown.enter', ['$event']) + onEnter(event: KeyboardEvent): void { + event.preventDefault(); + document.body.click(); + } +} diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 1af422652..f8644748b 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -242,7 +242,7 @@ export class SeriesService { } updateMatch(seriesId: number, series: ExternalSeriesDetail) { - return this.httpClient.post(this.baseUrl + 'series/update-match?seriesId=' + seriesId + '&aniListId=' + series.aniListId, {}, TextResonse); + return this.httpClient.post(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId}${series.malId ? '&malId=' + series.malId : ''}`, {}, TextResonse); } updateDontMatch(seriesId: number, dontMatch: boolean) { diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html index b9fa21953..b0a419fe1 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html @@ -15,7 +15,7 @@
  • {{t(TabID.General)}} -
    +
    @@ -23,7 +23,7 @@
    + [class.is-invalid]="formControl.invalid && !formControl.untouched"> @if (formControl.errors; as errors) {
    @if (errors.required) { @@ -44,7 +44,7 @@
    + [class.is-invalid]="formControl.invalid && !formControl.untouched"> @if (formControl.errors; as errors) {
    @if (errors.required) { @@ -59,7 +59,7 @@
    -
    +
    @@ -67,7 +67,7 @@
    + [class.is-invalid]="formControl.invalid && !formControl.untouched"> @if (formControl.errors; as errors) {
    @if (errors.required) { @@ -99,7 +99,7 @@
    -
    +
    @@ -137,7 +137,7 @@
    -
    +
    @@ -163,7 +163,7 @@ {{t(TabID.Tags)}} -
    +
    @@ -204,7 +204,7 @@
    -
    +
    @@ -245,7 +245,7 @@
    -
    +
    @@ -286,7 +286,7 @@
    -
    +
    @@ -319,7 +319,7 @@ {{t(TabID.People)}} -
    +
    @@ -360,7 +360,7 @@
    -
    +
    @@ -401,7 +401,7 @@
    -
    +
    @@ -442,7 +442,7 @@
    -
    +
    @@ -487,7 +487,7 @@
  • {{t(TabID.Info)}} -
    +
    @@ -508,7 +508,7 @@
    -
    +
    @@ -530,7 +530,7 @@
    -
    +
    @@ -556,7 +556,7 @@
    -
    +
    {{t('links-label')}}
    @for(link of WebLinks; track link) { @@ -571,7 +571,7 @@ } @if (accountService.isAdmin$ | async) { -
    +
    @for (file of chapter.files; track file.id) { diff --git a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html index 81b8ade5e..b42f4cf45 100644 --- a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html +++ b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html @@ -12,7 +12,7 @@
  • {{t(TabID.Info)}} -
    +
    @@ -33,7 +33,7 @@
    -
    +
    @@ -55,7 +55,7 @@
    -
    +
    diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts index f107c744e..e28153e2a 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts @@ -7,17 +7,15 @@ import {ScrobbleEventTypePipe} from "../../_pipes/scrobble-event-type.pipe"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {ScrobbleEventSortField} from "../../_models/scrobbling/scrobble-event-filter"; import {debounceTime, take} from "rxjs/operators"; -import {PaginatedResult, Pagination} from "../../_models/pagination"; +import {PaginatedResult} from "../../_models/pagination"; import {SortEvent} from "../table/_directives/sortable-header.directive"; import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {translate, TranslocoModule} from "@jsverse/transloco"; +import {TranslocoModule} from "@jsverse/transloco"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {TranslocoLocaleModule} from "@jsverse/transloco-locale"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; -import {ToastrService} from "ngx-toastr"; import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter"; import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable"; -import {CardActionablesComponent} from "../card-actionables/card-actionables.component"; import {AsyncPipe} from "@angular/common"; import {AccountService} from "../../_services/account.service"; @@ -32,7 +30,7 @@ export interface DataTablePage { selector: 'app-user-scrobble-history', standalone: true, imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule, - DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, CardActionablesComponent, AsyncPipe], + DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe], templateUrl: './user-scrobble-history.component.html', styleUrls: ['./user-scrobble-history.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -47,7 +45,6 @@ export class UserScrobbleHistoryComponent implements OnInit { private readonly scrobblingService = inject(ScrobblingService); private readonly cdRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); - private readonly toastr = inject(ToastrService); protected readonly accountService = inject(AccountService); @@ -67,7 +64,8 @@ export class UserScrobbleHistoryComponent implements OnInit { ngOnInit() { - this.onPageChange({offset: 0}); + this.pageInfo.pageNumber = 0; + this.cdRef.markForCheck(); this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => { this.tokenExpired = hasExpired; diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 1f3bf2559..705f320bb 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -19,7 +19,7 @@
    + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
    @@ -43,7 +43,7 @@ {{formControl.value | defaultValue}}
    - +
    } @@ -56,7 +56,7 @@ {{formControl.value | defaultValue}} - + } @@ -69,7 +69,7 @@ {{formControl.value | defaultValue}} - + } @@ -82,7 +82,7 @@ {{formControl.value | defaultValue}} - + } @@ -107,7 +107,7 @@ {{formControl.value | defaultValue}} - + } @@ -120,7 +120,7 @@ {{formControl.value ? '********' : null | defaultValue}} - + } @@ -133,7 +133,7 @@ {{formControl.value | bytes}} - + } diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index 9b7d7e64c..a6304672c 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -10,6 +10,7 @@ import {SettingSwitchComponent} from "../../settings/_components/setting-switch/ import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {BytesPipe} from "../../_pipes/bytes.pipe"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {EnterBlurDirective} from "../../_directives/enter-blur.directive"; @Component({ selector: 'app-manage-email-settings', @@ -17,7 +18,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; styleUrls: ['./manage-email-settings.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, BytesPipe] + imports: [ReactiveFormsModule, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, BytesPipe, EnterBlurDirective] }) export class ManageEmailSettingsComponent implements OnInit { diff --git a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts index 589107998..51018f2fd 100644 --- a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts +++ b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts @@ -18,6 +18,7 @@ import {PersonRole} from "../../_models/metadata/person"; import {PersonRolePipe} from "../../_pipes/person-role.pipe"; import {allMetadataSettingField, MetadataSettingField} from "../_models/metadata-setting-field"; import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe"; +import {EnterBlurDirective} from "../../_directives/enter-blur.directive"; @Component({ @@ -33,6 +34,7 @@ import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe AgeRatingPipe, PersonRolePipe, MetadataSettingFiledPipe, + EnterBlurDirective, ], templateUrl: './manage-metadata-settings.component.html', styleUrl: './manage-metadata-settings.component.scss', diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index a0e2c60b7..1d2a8cf4b 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -21,7 +21,7 @@ + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur> @if(settingsForm.dirty || !settingsForm.untouched) {
    @@ -44,7 +44,7 @@
    + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
    @@ -69,7 +69,7 @@
    + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
    @@ -94,7 +94,7 @@ + onkeypress="return event.charCode >= 48 && event.charCode <= 57" appEnterBlur> } @@ -116,7 +116,7 @@ + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur> @if(settingsForm.dirty || !settingsForm.untouched) {
    @@ -146,7 +146,7 @@ + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur> @if(settingsForm.dirty || !settingsForm.untouched) {
    @@ -202,7 +202,7 @@ + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur> @if(settingsForm.dirty || !settingsForm.untouched) {
    @@ -271,7 +271,7 @@ + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur> @if(settingsForm.dirty || !settingsForm.untouched) {
    @@ -298,7 +298,7 @@ + [class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur> @if(settingsForm.dirty || !settingsForm.untouched) {
    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 f79b29042..6b8243fa2 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 @@ -14,6 +14,7 @@ import {ConfirmService} from "../../shared/confirm.service"; import {debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; +import {EnterBlurDirective} from "../../_directives/enter-blur.directive"; const ValidIpAddress = /^(\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*\,)*\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*$/i; @@ -23,7 +24,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, TitleCasePipe, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe] + imports: [ReactiveFormsModule, TitleCasePipe, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, EnterBlurDirective] }) export class ManageSettingsComponent implements OnInit { diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index a8a549f01..83012fd00 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -13,14 +13,14 @@
  • {{t(tabs[TabID.General])}} -
    +
    @if (editSeriesForm.get('name'); as formControl) { + [class.is-invalid]="formControl.invalid && !formControl.untouched"> @if (formControl.errors) { @if (formControl.errors.required) {
    {{t('required-field')}}
    @@ -33,14 +33,14 @@
    -
    +
    @if (editSeriesForm.get('sortName'); as formControl) {
    - + @if (formControl.errors) { @if (formControl.errors.required) {
    {{t('required-field')}}
    @@ -53,7 +53,7 @@
    -
    +
    @@ -69,7 +69,7 @@
    @if (metadata) { -
    +
    @@ -91,7 +91,7 @@ {{t(tabs[TabID.Metadata])}} -
    +
    @@ -131,7 +131,7 @@
    -
    +
    @@ -152,7 +152,7 @@
    -
    +
    @@ -173,7 +173,7 @@
    -
    +
    @@ -215,7 +215,7 @@
  • {{t(tabs[TabID.People])}} -
    +
    @@ -233,7 +233,7 @@
    -
    +
    @@ -253,7 +253,7 @@
    -
    +
    @@ -272,7 +272,7 @@
    -
    +
    @@ -291,7 +291,7 @@
    -
    +
    @@ -311,7 +311,7 @@
    -
    +
    @@ -329,7 +329,7 @@
    -
    +
    @@ -350,7 +350,7 @@ -
    +
    @@ -368,7 +368,7 @@
    -
    +
    @@ -387,7 +387,7 @@
    -
    +
    @@ -406,7 +406,7 @@
    -
    +
    @@ -425,7 +425,7 @@
    -
    +
    @@ -444,7 +444,7 @@
    -
    +
    @@ -498,7 +498,7 @@
    {{t('info-title')}}
    -
    +
    @@ -519,7 +519,7 @@
    -
    +
    @@ -541,7 +541,7 @@
    @if (metadata) { -
    +
    @@ -562,7 +562,7 @@
    -
    +
    @@ -584,7 +584,7 @@
    } -
    +
    @@ -605,7 +605,7 @@
    -
    +
    @@ -626,7 +626,7 @@
    -
    +
    @@ -660,7 +660,7 @@
    {{formatVolumeName(volume)}}
    -
    +
    {{t('added-title')}} {{volume.createdUtc | utcToLocalTime | defaultDate}}
    @@ -668,7 +668,7 @@ {{t('last-modified-title')}} {{volume.lastModifiedUtc | utcToLocalTime | translocoDate: {dateStyle: 'short' } | defaultDate}}
    -
    +