mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Polish 4 (#3577)
Co-authored-by: Zeoic <zeorgaming@gmail.com> Co-authored-by: Fesaa <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
parent
b38400c092
commit
0ffe0228e5
65
.github/workflows/openapi-gen.yml
vendored
Normal file
65
.github/workflows/openapi-gen.yml
vendored
Normal file
@ -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 '<PackageReference Include="Swashbuckle.AspNetCore" Version="[^"]*"' API/API.csproj | grep -o 'Version="[^"]*"' | cut -d'"' -f2)
|
||||||
|
echo "VERSION=$VERSION" >> $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 }}
|
@ -2276,6 +2276,72 @@ public class ExternalMetadataServiceTests : AbstractDbTest
|
|||||||
Assert.Equal(seriesName, sequel.Relations.First().TargetSeries.Name);
|
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
|
#endregion
|
||||||
|
|
||||||
|
@ -12,10 +12,11 @@
|
|||||||
<LangVersion>latestmajor</LangVersion>
|
<LangVersion>latestmajor</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
<!-- Moved to GA -->
|
||||||
<Delete Files="../openapi.json" />
|
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||||
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
<!-- <Delete Files="../openapi.json" />-->
|
||||||
</Target>
|
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
||||||
|
<!-- </Target>-->
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
|
@ -652,9 +652,9 @@ public class SeriesController : BaseApiController
|
|||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("update-match")]
|
[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();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
@ -213,9 +213,10 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
{
|
{
|
||||||
return await _context.Series
|
return await _context.Series
|
||||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
.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)
|
.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)
|
.OrderByDescending(s => s.Library.Type)
|
||||||
.ThenBy(s => s.NormalizedName)
|
.ThenBy(s => s.NormalizedName)
|
||||||
.Select(s => s.Id)
|
.Select(s => s.Id)
|
||||||
@ -229,6 +230,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
.Include(s => s.Library)
|
.Include(s => s.Library)
|
||||||
.Include(s => s.ExternalSeriesMetadata)
|
.Include(s => s.ExternalSeriesMetadata)
|
||||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||||
|
.Where(s => s.Library.AllowMetadataMatching)
|
||||||
.FilterMatchState(filter.MatchStateOption)
|
.FilterMatchState(filter.MatchStateOption)
|
||||||
.OrderBy(s => s.NormalizedName)
|
.OrderBy(s => s.NormalizedName)
|
||||||
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
|
||||||
|
@ -18,6 +18,7 @@ using Kavita.Common.Extensions;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Data.Repositories;
|
namespace API.Data.Repositories;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum LibraryIncludes
|
public enum LibraryIncludes
|
||||||
@ -260,7 +261,7 @@ public class LibraryRepository : ILibraryRepository
|
|||||||
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int>? libraryIds)
|
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int>? libraryIds)
|
||||||
{
|
{
|
||||||
var ret = await _context.Series
|
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)
|
.Select(s => s.Metadata.Language)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
@ -19,11 +19,13 @@ public interface IScrobbleRepository
|
|||||||
void Attach(ScrobbleError error);
|
void Attach(ScrobbleError error);
|
||||||
void Remove(ScrobbleEvent evt);
|
void Remove(ScrobbleEvent evt);
|
||||||
void Remove(IEnumerable<ScrobbleEvent> events);
|
void Remove(IEnumerable<ScrobbleEvent> events);
|
||||||
|
void Remove(IEnumerable<ScrobbleError> errors);
|
||||||
void Update(ScrobbleEvent evt);
|
void Update(ScrobbleEvent evt);
|
||||||
Task<IList<ScrobbleEvent>> GetByEvent(ScrobbleEventType type, bool isProcessed = false);
|
Task<IList<ScrobbleEvent>> GetByEvent(ScrobbleEventType type, bool isProcessed = false);
|
||||||
Task<IList<ScrobbleEvent>> GetProcessedEvents(int daysAgo);
|
Task<IList<ScrobbleEvent>> GetProcessedEvents(int daysAgo);
|
||||||
Task<bool> Exists(int userId, int seriesId, ScrobbleEventType eventType);
|
Task<bool> Exists(int userId, int seriesId, ScrobbleEventType eventType);
|
||||||
Task<IEnumerable<ScrobbleErrorDto>> GetScrobbleErrors();
|
Task<IEnumerable<ScrobbleErrorDto>> GetScrobbleErrors();
|
||||||
|
Task<IList<ScrobbleError>> GetAllScrobbleErrorsForSeries(int seriesId);
|
||||||
Task ClearScrobbleErrors();
|
Task ClearScrobbleErrors();
|
||||||
Task<bool> HasErrorForSeries(int seriesId);
|
Task<bool> HasErrorForSeries(int seriesId);
|
||||||
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
||||||
@ -66,6 +68,11 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||||||
_context.ScrobbleEvent.RemoveRange(events);
|
_context.ScrobbleEvent.RemoveRange(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Remove(IEnumerable<ScrobbleError> errors)
|
||||||
|
{
|
||||||
|
_context.ScrobbleError.RemoveRange(errors);
|
||||||
|
}
|
||||||
|
|
||||||
public void Update(ScrobbleEvent evt)
|
public void Update(ScrobbleEvent evt)
|
||||||
{
|
{
|
||||||
_context.Entry(evt).State = EntityState.Modified;
|
_context.Entry(evt).State = EntityState.Modified;
|
||||||
@ -113,6 +120,13 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<ScrobbleError>> GetAllScrobbleErrorsForSeries(int seriesId)
|
||||||
|
{
|
||||||
|
return await _context.ScrobbleError
|
||||||
|
.Where(e => e.SeriesId == seriesId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ClearScrobbleErrors()
|
public async Task ClearScrobbleErrors()
|
||||||
{
|
{
|
||||||
_context.ScrobbleError.RemoveRange(_context.ScrobbleError);
|
_context.ScrobbleError.RemoveRange(_context.ScrobbleError);
|
||||||
@ -161,4 +175,5 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||||||
return await _context.ScrobbleEvent.Where(e => e.SeriesId == seriesId)
|
return await _context.ScrobbleEvent.Where(e => e.SeriesId == seriesId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -276,18 +276,18 @@ public static class Seed
|
|||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
// Port, IpAddresses and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
// 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;
|
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;
|
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;
|
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;
|
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;
|
Configuration.CacheSize + string.Empty;
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task SeedMetadataSettings(DataContext context)
|
public static async Task SeedMetadataSettings(DataContext context)
|
||||||
|
@ -252,8 +252,6 @@ public static class SeriesFilter
|
|||||||
if (!condition) return queryable;
|
if (!condition) return queryable;
|
||||||
|
|
||||||
var subQuery = queryable
|
var subQuery = queryable
|
||||||
.Include(s => s.Progress)
|
|
||||||
.Where(s => s.Progress != null)
|
|
||||||
.Select(s => new
|
.Select(s => new
|
||||||
{
|
{
|
||||||
SeriesId = s.Id,
|
SeriesId = s.Id,
|
||||||
@ -372,7 +370,7 @@ public static class SeriesFilter
|
|||||||
|
|
||||||
var subQuery = queryable
|
var subQuery = queryable
|
||||||
.Include(s => s.Progress)
|
.Include(s => s.Progress)
|
||||||
.Where(s => s.Progress != null)
|
.Where(s => s.Progress.Any())
|
||||||
.Select(s => new
|
.Select(s => new
|
||||||
{
|
{
|
||||||
SeriesId = s.Id,
|
SeriesId = s.Id,
|
||||||
@ -435,7 +433,7 @@ public static class SeriesFilter
|
|||||||
|
|
||||||
var subQuery = queryable
|
var subQuery = queryable
|
||||||
.Include(s => s.Progress)
|
.Include(s => s.Progress)
|
||||||
.Where(s => s.Progress != null)
|
.Where(s => s.Progress.Any())
|
||||||
.Select(s => new
|
.Select(s => new
|
||||||
{
|
{
|
||||||
SeriesId = s.Id,
|
SeriesId = s.Id,
|
||||||
|
@ -311,8 +311,16 @@ public class BookService : IBookService
|
|||||||
|
|
||||||
var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
|
var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
|
||||||
image.Attributes.Remove(key);
|
image.Attributes.Remove(key);
|
||||||
|
|
||||||
|
if (!imageFile.StartsWith("http"))
|
||||||
|
{
|
||||||
// UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx
|
// UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx
|
||||||
image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile));
|
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
|
// Add a custom class that the reader uses to ensure images stay within reader
|
||||||
parent.AddClass("kavita-scale-width-container");
|
parent.AddClass("kavita-scale-width-container");
|
||||||
|
@ -49,7 +49,7 @@ public interface IExternalMetadataService
|
|||||||
|
|
||||||
Task<IList<MalStackDto>> GetStacksForUser(int userId);
|
Task<IList<MalStackDto>> GetStacksForUser(int userId);
|
||||||
Task<IList<ExternalSeriesMatchDto>> MatchSeries(MatchSeriesDto dto);
|
Task<IList<ExternalSeriesMatchDto>> MatchSeries(MatchSeriesDto dto);
|
||||||
Task FixSeriesMatch(int seriesId, int anilistId);
|
Task FixSeriesMatch(int seriesId, int anilistId, long? malId);
|
||||||
Task UpdateSeriesDontMatch(int seriesId, bool dontMatch);
|
Task UpdateSeriesDontMatch(int seriesId, bool dontMatch);
|
||||||
Task<bool> WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId);
|
Task<bool> WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId);
|
||||||
}
|
}
|
||||||
@ -111,7 +111,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
public async Task FetchExternalDataTask()
|
public async Task FetchExternalDataTask()
|
||||||
{
|
{
|
||||||
// Find all Series that are eligible and limit
|
// 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;
|
if (ids.Count == 0) return;
|
||||||
|
|
||||||
_logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+", ids.Count);
|
_logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+", ids.Count);
|
||||||
@ -133,6 +133,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <param name="libraryType"></param>
|
/// <param name="libraryType"></param>
|
||||||
|
/// <returns>If a successful match was made</returns>
|
||||||
public async Task<bool> FetchSeriesMetadata(int seriesId, LibraryType libraryType)
|
public async Task<bool> FetchSeriesMetadata(int seriesId, LibraryType libraryType)
|
||||||
{
|
{
|
||||||
if (!IsPlusEligible(libraryType)) return false;
|
if (!IsPlusEligible(libraryType)) return false;
|
||||||
@ -150,8 +151,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
_logger.LogDebug("Prefetching Kavita+ data for Series {SeriesId}", seriesId);
|
_logger.LogDebug("Prefetching Kavita+ data for Series {SeriesId}", seriesId);
|
||||||
|
|
||||||
// Prefetch SeriesDetail data
|
// Prefetch SeriesDetail data
|
||||||
await GetSeriesDetailPlus(seriesId, libraryType);
|
return await GetSeriesDetailPlus(seriesId, libraryType) != null;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<MalStackDto>> GetStacksForUser(int userId)
|
public async Task<IList<MalStackDto>> GetStacksForUser(int userId)
|
||||||
@ -303,7 +303,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <param name="anilistId"></param>
|
/// <param name="anilistId"></param>
|
||||||
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);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library);
|
||||||
if (series == null) return;
|
if (series == null) return;
|
||||||
@ -317,7 +317,8 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, new PlusSeriesRequestDto()
|
var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, new PlusSeriesRequestDto()
|
||||||
{
|
{
|
||||||
AniListId = anilistId,
|
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)
|
if (metadata.Series == null)
|
||||||
@ -329,6 +330,11 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
// Find all scrobble events and rewrite them to be the correct
|
// Find all scrobble events and rewrite them to be the correct
|
||||||
var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId);
|
var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId);
|
||||||
_unitOfWork.ScrobbleRepository.Remove(events);
|
_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();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
// Regenerate all events for the series for all users
|
// Regenerate all events for the series for all users
|
||||||
@ -566,7 +572,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
return false;
|
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 names = new [] {relation.SeriesName.PreferredTitle, relation.SeriesName.RomajiTitle, relation.SeriesName.EnglishTitle, relation.SeriesName.NativeTitle};
|
||||||
var relatedSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(
|
var relatedSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(
|
||||||
|
@ -930,6 +930,7 @@ public class ScrobblingService : IScrobblingService
|
|||||||
|
|
||||||
if (await _unitOfWork.ExternalSeriesMetadataRepository.IsBlacklistedSeries(evt.SeriesId))
|
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()
|
_unitOfWork.ScrobbleRepository.Attach(new ScrobbleError()
|
||||||
{
|
{
|
||||||
Comment = UnknownSeriesErrorMessage,
|
Comment = UnknownSeriesErrorMessage,
|
||||||
@ -942,6 +943,7 @@ public class ScrobblingService : IScrobblingService
|
|||||||
evt.ProcessDateUtc = DateTime.UtcNow;
|
evt.ProcessDateUtc = DateTime.UtcNow;
|
||||||
_unitOfWork.ScrobbleRepository.Update(evt);
|
_unitOfWork.ScrobbleRepository.Update(evt);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using API.DTOs.Theme;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums.Theme;
|
using API.Entities.Enums.Theme;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Flurl.Http;
|
using Flurl.Http;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
@ -192,7 +193,8 @@ public class ThemeService : IThemeService
|
|||||||
|
|
||||||
private static List<string> GetPreviewUrls(IEnumerable<GitHubContent> themeContents)
|
private static List<string> GetPreviewUrls(IEnumerable<GitHubContent> 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)
|
.Select(p => p.DownloadUrl)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
"Port": 5000,
|
"Port": 5000,
|
||||||
"IpAddresses": "",
|
"IpAddresses": "",
|
||||||
"BaseUrl": "/",
|
"BaseUrl": "/",
|
||||||
"Cache": 50
|
"Cache": 75
|
||||||
}
|
}
|
||||||
|
13
UI/Web/src/app/_directives/enter-blur.directive.ts
Normal file
13
UI/Web/src/app/_directives/enter-blur.directive.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -242,7 +242,7 @@ export class SeriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateMatch(seriesId: number, series: ExternalSeriesDetail) {
|
updateMatch(seriesId: number, series: ExternalSeriesDetail) {
|
||||||
return this.httpClient.post<string>(this.baseUrl + 'series/update-match?seriesId=' + seriesId + '&aniListId=' + series.aniListId, {}, TextResonse);
|
return this.httpClient.post<string>(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId}${series.malId ? '&malId=' + series.malId : ''}`, {}, TextResonse);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDontMatch(seriesId: number, dontMatch: boolean) {
|
updateDontMatch(seriesId: number, dontMatch: boolean) {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<li [ngbNavItem]="TabID.General">
|
<li [ngbNavItem]="TabID.General">
|
||||||
<a ngbNavLink>{{t(TabID.General)}}</a>
|
<a ngbNavLink>{{t(TabID.General)}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-md-9 col-sm-12 mb-3">
|
<div class="col-md-9 col-sm-12 mb-3">
|
||||||
<app-setting-item [title]="t('title-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
<app-setting-item [title]="t('title-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<div class="input-group" [ngClass]="{'lock-active': chapter.titleNameLocked}">
|
<div class="input-group" [ngClass]="{'lock-active': chapter.titleNameLocked}">
|
||||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'titleNameLocked' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'titleNameLocked' }"></ng-container>
|
||||||
<input class="form-control" formControlName="titleName" type="text"
|
<input class="form-control" formControlName="titleName" type="text"
|
||||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||||
@if (formControl.errors; as errors) {
|
@if (formControl.errors; as errors) {
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
@if (errors.required) {
|
@if (errors.required) {
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<div class="input-group" [ngClass]="{'lock-active': chapter.sortOrderLocked}">
|
<div class="input-group" [ngClass]="{'lock-active': chapter.sortOrderLocked}">
|
||||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'sortOrderLocked' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'sortOrderLocked' }"></ng-container>
|
||||||
<input class="form-control" formControlName="sortOrder" type="number" min="0" step="0.1" inputmode="numeric"
|
<input class="form-control" formControlName="sortOrder" type="number" min="0" step="0.1" inputmode="numeric"
|
||||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||||
@if (formControl.errors; as errors) {
|
@if (formControl.errors; as errors) {
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
@if (errors.required) {
|
@if (errors.required) {
|
||||||
@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-md-9 col-sm-12 mb-3">
|
<div class="col-md-9 col-sm-12 mb-3">
|
||||||
<app-setting-item [title]="t('isbn-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
<app-setting-item [title]="t('isbn-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
<div class="input-group" [ngClass]="{'lock-active': chapter.isbnLocked}">
|
<div class="input-group" [ngClass]="{'lock-active': chapter.isbnLocked}">
|
||||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'isbnLocked' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'isbnLocked' }"></ng-container>
|
||||||
<input class="form-control" formControlName="isbn" type="text"
|
<input class="form-control" formControlName="isbn" type="text"
|
||||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||||
@if (formControl.errors; as errors) {
|
@if (formControl.errors; as errors) {
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
@if (errors.required) {
|
@if (errors.required) {
|
||||||
@ -99,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-9 col-md-12">
|
<div class="col-lg-9 col-md-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('language-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('language-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -137,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<app-setting-item [title]="t('summary-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
<app-setting-item [title]="t('summary-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -163,7 +163,7 @@
|
|||||||
<a ngbNavLink>{{t(TabID.Tags)}}</a>
|
<a ngbNavLink>{{t(TabID.Tags)}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<!-- genre & tag -->
|
<!-- genre & tag -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('genres-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('genres-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -204,7 +204,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- imprint & publisher -->
|
<!-- imprint & publisher -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('imprint-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('imprint-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -245,7 +245,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- team & location -->
|
<!-- team & location -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('team-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('team-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -286,7 +286,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- character -->
|
<!-- character -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 pe-2">
|
<div class="col-lg-12 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('character-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('character-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -319,7 +319,7 @@
|
|||||||
<a ngbNavLink>{{t(TabID.People)}}</a>
|
<a ngbNavLink>{{t(TabID.People)}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<!-- writer & cover artist -->
|
<!-- writer & cover artist -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('writer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('writer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -360,7 +360,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- penciller & colorist -->
|
<!-- penciller & colorist -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('penciller-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('penciller-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -401,7 +401,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- inker & letterer -->
|
<!-- inker & letterer -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('inker-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('inker-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -442,7 +442,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- translator -->
|
<!-- translator -->
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 pe-2">
|
<div class="col-lg-12 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('translator-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('translator-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -487,7 +487,7 @@
|
|||||||
<li [ngbNavItem]="TabID.Info">
|
<li [ngbNavItem]="TabID.Info">
|
||||||
<a ngbNavLink>{{t(TabID.Info)}}</a>
|
<a ngbNavLink>{{t(TabID.Info)}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('pages-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('pages-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -508,7 +508,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('read-time-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('read-time-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -530,7 +530,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('date-added-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('date-added-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -556,7 +556,7 @@
|
|||||||
<div class="setting-section-break"></div>
|
<div class="setting-section-break"></div>
|
||||||
|
|
||||||
<div class="container-fluid mb-3">
|
<div class="container-fluid mb-3">
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<h6 class="section-title">{{t('links-label')}}</h6>
|
<h6 class="section-title">{{t('links-label')}}</h6>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@for(link of WebLinks; track link) {
|
@for(link of WebLinks; track link) {
|
||||||
@ -571,7 +571,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (accountService.isAdmin$ | async) {
|
@if (accountService.isAdmin$ | async) {
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@for (file of chapter.files; track file.id) {
|
@for (file of chapter.files; track file.id) {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<li [ngbNavItem]="TabID.Info">
|
<li [ngbNavItem]="TabID.Info">
|
||||||
<a ngbNavLink>{{t(TabID.Info)}}</a>
|
<a ngbNavLink>{{t(TabID.Info)}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('pages-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('pages-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('read-time-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('read-time-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('date-added-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('date-added-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
|
@ -7,17 +7,15 @@ import {ScrobbleEventTypePipe} from "../../_pipes/scrobble-event-type.pipe";
|
|||||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import {ScrobbleEventSortField} from "../../_models/scrobbling/scrobble-event-filter";
|
import {ScrobbleEventSortField} from "../../_models/scrobbling/scrobble-event-filter";
|
||||||
import {debounceTime, take} from "rxjs/operators";
|
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 {SortEvent} from "../table/_directives/sortable-header.directive";
|
||||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
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 {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||||
import {TranslocoLocaleModule} from "@jsverse/transloco-locale";
|
import {TranslocoLocaleModule} from "@jsverse/transloco-locale";
|
||||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||||
import {ToastrService} from "ngx-toastr";
|
|
||||||
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
|
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
|
||||||
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
||||||
import {CardActionablesComponent} from "../card-actionables/card-actionables.component";
|
|
||||||
import {AsyncPipe} from "@angular/common";
|
import {AsyncPipe} from "@angular/common";
|
||||||
import {AccountService} from "../../_services/account.service";
|
import {AccountService} from "../../_services/account.service";
|
||||||
|
|
||||||
@ -32,7 +30,7 @@ export interface DataTablePage {
|
|||||||
selector: 'app-user-scrobble-history',
|
selector: 'app-user-scrobble-history',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule,
|
imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule,
|
||||||
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, CardActionablesComponent, AsyncPipe],
|
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe],
|
||||||
templateUrl: './user-scrobble-history.component.html',
|
templateUrl: './user-scrobble-history.component.html',
|
||||||
styleUrls: ['./user-scrobble-history.component.scss'],
|
styleUrls: ['./user-scrobble-history.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
@ -47,7 +45,6 @@ export class UserScrobbleHistoryComponent implements OnInit {
|
|||||||
private readonly scrobblingService = inject(ScrobblingService);
|
private readonly scrobblingService = inject(ScrobblingService);
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly toastr = inject(ToastrService);
|
|
||||||
protected readonly accountService = inject(AccountService);
|
protected readonly accountService = inject(AccountService);
|
||||||
|
|
||||||
|
|
||||||
@ -67,7 +64,8 @@ export class UserScrobbleHistoryComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
||||||
this.onPageChange({offset: 0});
|
this.pageInfo.pageNumber = 0;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => {
|
this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => {
|
||||||
this.tokenExpired = hasExpired;
|
this.tokenExpired = hasExpired;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="settings-hostname" aria-describedby="hostname-validations" class="form-control" formControlName="hostName" type="text"
|
<input id="settings-hostname" aria-describedby="hostname-validations" class="form-control" formControlName="hostName" type="text"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
|
||||||
</div>
|
</div>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
{{formControl.value | defaultValue}}
|
{{formControl.value | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="text" class="form-control" formControlName="senderAddress" id="settings-sender-address" />
|
<input type="text" class="form-control" formControlName="senderAddress" id="settings-sender-address" appEnterBlur/>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@
|
|||||||
{{formControl.value | defaultValue}}
|
{{formControl.value | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="text" class="form-control" formControlName="senderDisplayName" id="settings-sender-displayname" />
|
<input type="text" class="form-control" formControlName="senderDisplayName" id="settings-sender-displayname" appEnterBlur />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@
|
|||||||
{{formControl.value | defaultValue}}
|
{{formControl.value | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="text" class="form-control" formControlName="host" id="settings-host" />
|
<input type="text" class="form-control" formControlName="host" id="settings-host" appEnterBlur />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@
|
|||||||
{{formControl.value | defaultValue}}
|
{{formControl.value | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="number" inputmode="numeric" min="1" class="form-control" formControlName="port" id="settings-port" />
|
<input type="number" inputmode="numeric" min="1" class="form-control" formControlName="port" id="settings-port" appEnterBlur />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -107,7 +107,7 @@
|
|||||||
{{formControl.value | defaultValue}}
|
{{formControl.value | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="text" class="form-control" formControlName="userName" id="settings-username" />
|
<input type="text" class="form-control" formControlName="userName" id="settings-username" appEnterBlur />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -120,7 +120,7 @@
|
|||||||
{{formControl.value ? '********' : null | defaultValue}}
|
{{formControl.value ? '********' : null | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="text" class="form-control" formControlName="password" id="settings-password" />
|
<input type="text" class="form-control" formControlName="password" id="settings-password" appEnterBlur />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@
|
|||||||
{{formControl.value | bytes}}
|
{{formControl.value | bytes}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input type="number" inputmode="numeric" min="1" class="form-control" formControlName="sizeLimit" id="settings-size-limit" />
|
<input type="number" inputmode="numeric" min="1" class="form-control" formControlName="sizeLimit" id="settings-size-limit" appEnterBlur />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {SettingSwitchComponent} from "../../settings/_components/setting-switch/
|
|||||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||||
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
|
import {EnterBlurDirective} from "../../_directives/enter-blur.directive";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-manage-email-settings',
|
selector: 'app-manage-email-settings',
|
||||||
@ -17,7 +18,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||||||
styleUrls: ['./manage-email-settings.component.scss'],
|
styleUrls: ['./manage-email-settings.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ReactiveFormsModule, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, BytesPipe]
|
imports: [ReactiveFormsModule, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, BytesPipe, EnterBlurDirective]
|
||||||
})
|
})
|
||||||
export class ManageEmailSettingsComponent implements OnInit {
|
export class ManageEmailSettingsComponent implements OnInit {
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import {PersonRole} from "../../_models/metadata/person";
|
|||||||
import {PersonRolePipe} from "../../_pipes/person-role.pipe";
|
import {PersonRolePipe} from "../../_pipes/person-role.pipe";
|
||||||
import {allMetadataSettingField, MetadataSettingField} from "../_models/metadata-setting-field";
|
import {allMetadataSettingField, MetadataSettingField} from "../_models/metadata-setting-field";
|
||||||
import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe";
|
import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe";
|
||||||
|
import {EnterBlurDirective} from "../../_directives/enter-blur.directive";
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -33,6 +34,7 @@ import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe
|
|||||||
AgeRatingPipe,
|
AgeRatingPipe,
|
||||||
PersonRolePipe,
|
PersonRolePipe,
|
||||||
MetadataSettingFiledPipe,
|
MetadataSettingFiledPipe,
|
||||||
|
EnterBlurDirective,
|
||||||
],
|
],
|
||||||
templateUrl: './manage-metadata-settings.component.html',
|
templateUrl: './manage-metadata-settings.component.html',
|
||||||
styleUrl: './manage-metadata-settings.component.scss',
|
styleUrl: './manage-metadata-settings.component.scss',
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="hostname-validations" class="invalid-feedback">
|
<div id="hostname-validations" class="invalid-feedback">
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
|
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="resetBaseUrl()">{{t('reset')}}</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="resetBaseUrl()">{{t('reset')}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -69,7 +69,7 @@
|
|||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="settings-ipaddresses" aria-describedby="settings-ipaddresses-help" class="form-control" formControlName="ipAddresses" type="text"
|
<input id="settings-ipaddresses" aria-describedby="settings-ipaddresses-help" class="form-control" formControlName="ipAddresses" type="text"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="resetIPAddresses()">{{t('reset')}}</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="resetIPAddresses()">{{t('reset')}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -94,7 +94,7 @@
|
|||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input id="settings-port" aria-describedby="settings-port-help" class="form-control"
|
<input id="settings-port" aria-describedby="settings-port-help" class="form-control"
|
||||||
formControlName="port" type="number" step="1" min="1"
|
formControlName="port" type="number" step="1" min="1"
|
||||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
onkeypress="return event.charCode >= 48 && event.charCode <= 57" appEnterBlur>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@
|
|||||||
<input id="settings-backup" aria-describedby="total-backups-validations" class="form-control"
|
<input id="settings-backup" aria-describedby="total-backups-validations" class="form-control"
|
||||||
formControlName="totalBackups" type="number" inputmode="numeric" step="1" min="1" max="30"
|
formControlName="totalBackups" type="number" inputmode="numeric" step="1" min="1" max="30"
|
||||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="total-backups-validations" class="invalid-feedback">
|
<div id="total-backups-validations" class="invalid-feedback">
|
||||||
@ -146,7 +146,7 @@
|
|||||||
<input id="settings-logs" aria-describedby="total-logs-validations" class="form-control"
|
<input id="settings-logs" aria-describedby="total-logs-validations" class="form-control"
|
||||||
formControlName="totalLogs" type="number" inputmode="numeric" step="1" min="1" max="30"
|
formControlName="totalLogs" type="number" inputmode="numeric" step="1" min="1" max="30"
|
||||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="total-logs-validations" class="invalid-feedback">
|
<div id="total-logs-validations" class="invalid-feedback">
|
||||||
@ -202,7 +202,7 @@
|
|||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<input id="setting-cache-size" aria-describedby="cache-size-help" class="form-control" formControlName="cacheSize"
|
<input id="setting-cache-size" aria-describedby="cache-size-help" class="form-control" formControlName="cacheSize"
|
||||||
type="number" inputmode="numeric" step="5" min="50" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
type="number" inputmode="numeric" step="5" min="50" onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="cache-size-validations" class="invalid-feedback">
|
<div id="cache-size-validations" class="invalid-feedback">
|
||||||
@ -271,7 +271,7 @@
|
|||||||
<input id="setting-on-deck-progress-days" aria-describedby="on-deck-progress-days-validations" class="form-control" formControlName="onDeckProgressDays"
|
<input id="setting-on-deck-progress-days" aria-describedby="on-deck-progress-days-validations" class="form-control" formControlName="onDeckProgressDays"
|
||||||
type="number" inputmode="numeric" step="1" min="1"
|
type="number" inputmode="numeric" step="1" min="1"
|
||||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="on-deck-last-progress-validations" class="invalid-feedback">
|
<div id="on-deck-last-progress-validations" class="invalid-feedback">
|
||||||
@ -298,7 +298,7 @@
|
|||||||
<input id="on-deck-last-chapter-add" aria-describedby="on-deck-last-chapter-add-validations" class="form-control" formControlName="onDeckUpdateDays"
|
<input id="on-deck-last-chapter-add" aria-describedby="on-deck-last-chapter-add-validations" class="form-control" formControlName="onDeckUpdateDays"
|
||||||
type="number" inputmode="numeric" step="1" min="1"
|
type="number" inputmode="numeric" step="1" min="1"
|
||||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
|
||||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="on-deck-last-chapter-add-validations" class="invalid-feedback">
|
<div id="on-deck-last-chapter-add-validations" class="invalid-feedback">
|
||||||
|
@ -14,6 +14,7 @@ import {ConfirmService} from "../../shared/confirm.service";
|
|||||||
import {debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs";
|
import {debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
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;
|
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'],
|
styleUrls: ['./manage-settings.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ReactiveFormsModule, TitleCasePipe, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe]
|
imports: [ReactiveFormsModule, TitleCasePipe, TranslocoModule, SettingItemComponent, SettingSwitchComponent, DefaultValuePipe, EnterBlurDirective]
|
||||||
})
|
})
|
||||||
export class ManageSettingsComponent implements OnInit {
|
export class ManageSettingsComponent implements OnInit {
|
||||||
|
|
||||||
|
@ -13,14 +13,14 @@
|
|||||||
<li [ngbNavItem]="tabs[TabID.General]">
|
<li [ngbNavItem]="tabs[TabID.General]">
|
||||||
<a ngbNavLink>{{t(tabs[TabID.General])}}</a>
|
<a ngbNavLink>{{t(tabs[TabID.General])}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<app-setting-item [title]="t('name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@if (editSeriesForm.get('name'); as formControl) {
|
@if (editSeriesForm.get('name'); as formControl) {
|
||||||
<input id="name" class="form-control" formControlName="name" type="text" readonly
|
<input id="name" class="form-control" formControlName="name" type="text" readonly
|
||||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||||
@if (formControl.errors) {
|
@if (formControl.errors) {
|
||||||
@if (formControl.errors.required) {
|
@if (formControl.errors.required) {
|
||||||
<div class="invalid-feedback">{{t('required-field')}}</div>
|
<div class="invalid-feedback">{{t('required-field')}}</div>
|
||||||
@ -33,14 +33,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
@if (editSeriesForm.get('sortName'); as formControl) {
|
@if (editSeriesForm.get('sortName'); as formControl) {
|
||||||
<app-setting-item [title]="t('sort-name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('sort-name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}">
|
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}">
|
||||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
|
||||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text" [class.is-invalid]="formControl.invalid && formControl.touched">
|
<input id="sort-name" class="form-control" formControlName="sortName" type="text" [class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||||
@if (formControl.errors) {
|
@if (formControl.errors) {
|
||||||
@if (formControl.errors.required) {
|
@if (formControl.errors.required) {
|
||||||
<div class="invalid-feedback">{{t('required-field')}}</div>
|
<div class="invalid-feedback">{{t('required-field')}}</div>
|
||||||
@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<app-setting-item [title]="t('localized-name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('localized-name-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (metadata) {
|
@if (metadata) {
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<app-setting-item [title]="t('summary-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('summary-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -91,7 +91,7 @@
|
|||||||
<a ngbNavLink>{{t(tabs[TabID.Metadata])}}</a>
|
<a ngbNavLink>{{t(tabs[TabID.Metadata])}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-8 col-md-12 pe-2">
|
<div class="col-lg-8 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('language-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('language-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -131,7 +131,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('genres-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('genres-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -152,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('tags-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('tags-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -173,7 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('age-rating-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('age-rating-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -215,7 +215,7 @@
|
|||||||
<li [ngbNavItem]="tabs[TabID.People]">
|
<li [ngbNavItem]="tabs[TabID.People]">
|
||||||
<a ngbNavLink>{{t(tabs[TabID.People])}}</a>
|
<a ngbNavLink>{{t(tabs[TabID.People])}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('writer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('writer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -233,7 +233,7 @@
|
|||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('cover-artist-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('cover-artist-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -253,7 +253,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('publisher-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('publisher-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -272,7 +272,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('imprint-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('imprint-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -291,7 +291,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('penciller-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('penciller-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -311,7 +311,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('letterer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('letterer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -329,7 +329,7 @@
|
|||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('inker-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('inker-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -350,7 +350,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('editor-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('editor-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -368,7 +368,7 @@
|
|||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('colorist-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('colorist-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -387,7 +387,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('translator-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('translator-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -406,7 +406,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('character-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('character-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -425,7 +425,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('team-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('team-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -444,7 +444,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('location-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('location-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
@ -498,7 +498,7 @@
|
|||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<h5>{{t('info-title')}}</h5>
|
<h5>{{t('info-title')}}</h5>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('library-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('library-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -519,7 +519,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('folder-path-title')" [subtitle]="t('folder-path-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('folder-path-title')" [subtitle]="t('folder-path-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -541,7 +541,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (metadata) {
|
@if (metadata) {
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('max-items-title')" [subtitle]="t('highest-count-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('max-items-title')" [subtitle]="t('highest-count-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -562,7 +562,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('publication-status-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('publication-status-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -584,7 +584,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('created-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('created-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -605,7 +605,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('last-scanned-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('last-scanned-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -626,7 +626,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 pe-2">
|
<div class="col-lg-6 col-md-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<app-setting-item [title]="t('total-pages-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
<app-setting-item [title]="t('total-pages-title')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||||
@ -660,7 +660,7 @@
|
|||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1">{{formatVolumeName(volume)}}</h5>
|
<h5 class="mt-0 mb-1">{{formatVolumeName(volume)}}</h5>
|
||||||
<div>
|
<div>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{t('added-title')}} {{volume.createdUtc | utcToLocalTime | defaultDate}}
|
{{t('added-title')}} {{volume.createdUtc | utcToLocalTime | defaultDate}}
|
||||||
</div>
|
</div>
|
||||||
@ -668,7 +668,7 @@
|
|||||||
{{t('last-modified-title')}} {{volume.lastModifiedUtc | utcToLocalTime | translocoDate: {dateStyle: 'short' } | defaultDate}}
|
{{t('last-modified-title')}} {{volume.lastModifiedUtc | utcToLocalTime | translocoDate: {dateStyle: 'short' } | defaultDate}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
|
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
|
||||||
[attr.aria-expanded]="!volumeCollapsed[volume.name]" [disabled]="!isAdmin">
|
[attr.aria-expanded]="!volumeCollapsed[volume.name]" [disabled]="!isAdmin">
|
||||||
@ -685,7 +685,7 @@
|
|||||||
@for(file of volume.volumeFiles; track file.id) {
|
@for(file of volume.volumeFiles; track file.id) {
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<span>{{file.filePath}}</span>
|
<span>{{file.filePath}}</span>
|
||||||
<div class="row g-0">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{t('chapter-title')}} {{file.chapter}}
|
{{t('chapter-title')}} {{file.chapter}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
@if (works$ | async; as works) {
|
@if (works$ | async; as works) {
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
|
<!-- TODO: If the person is JUST a character, then switch this title to: 'A character in` -->
|
||||||
<app-carousel-reel [items]="works" [title]="t('known-for-title')" (sectionClick)="loadFilterByPerson()">
|
<app-carousel-reel [items]="works" [title]="t('known-for-title')" (sectionClick)="loadFilterByPerson()">
|
||||||
<ng-template #carouselItem let-item>
|
<ng-template #carouselItem let-item>
|
||||||
<app-card-item [entity]="item"
|
<app-card-item [entity]="item"
|
||||||
|
@ -829,7 +829,34 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
|
|
||||||
if (!this.router.url.includes('#')) {
|
if (!this.router.url.includes('#')) {
|
||||||
this.updateSelectedTab();
|
this.updateSelectedTab();
|
||||||
|
} else if (this.activeTabId != TabID.Storyline) {
|
||||||
|
// Validate that the tab we are selected is still there (in case this comes from a messageHub)
|
||||||
|
switch (this.activeTabId) {
|
||||||
|
case TabID.Related:
|
||||||
|
if (!this.hasRelations) this.updateSelectedTab();
|
||||||
|
break;
|
||||||
|
case TabID.Specials:
|
||||||
|
if (!this.hasSpecials) this.updateSelectedTab();
|
||||||
|
break;
|
||||||
|
case TabID.Volumes:
|
||||||
|
if (this.volumes.length === 0) this.updateSelectedTab();
|
||||||
|
break;
|
||||||
|
case TabID.Chapters:
|
||||||
|
if (this.chapters.length === 0) this.updateSelectedTab();
|
||||||
|
break;
|
||||||
|
case TabID.Recommendations:
|
||||||
|
if (!this.hasRecommendations) this.updateSelectedTab();
|
||||||
|
break;
|
||||||
|
case TabID.Reviews:
|
||||||
|
if (this.reviews.length === 0) this.updateSelectedTab();
|
||||||
|
break;
|
||||||
|
case TabID.Details:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -917,6 +944,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BUG: Related or other tab can be in history but no longer there, need to default
|
||||||
|
|
||||||
this.updateUrl(this.activeTabId);
|
this.updateUrl(this.activeTabId);
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,11 @@ export class EditListComponent implements OnInit {
|
|||||||
.map(key => this.form.get(key)?.value)
|
.map(key => this.form.get(key)?.value)
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
|
// Recreate form to ensure index's match
|
||||||
|
this.form = new FormGroup({});
|
||||||
|
this.Items.forEach((item, index) => {
|
||||||
|
this.form.addControl('link' + index, new FormControl(item, []));
|
||||||
|
})
|
||||||
|
|
||||||
this.emit();
|
this.emit();
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
@ -41,7 +41,7 @@ $image-width: 160px;
|
|||||||
position: relative;
|
position: relative;
|
||||||
color: var(--card-text-color);
|
color: var(--card-text-color);
|
||||||
border: 1px var(--card-border-color);
|
border: 1px var(--card-border-color);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
|
10
openapi.json
10
openapi.json
@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.1",
|
"openapi": "3.0.1",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Kavita",
|
"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.17",
|
"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.18",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
@ -11102,6 +11102,14 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "malId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user