mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Dashboard Customization Polish (#2295)
This commit is contained in:
parent
25e759d301
commit
25ffb2ffe1
@ -10,9 +10,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.7" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.7" />
|
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.8" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.51" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.69" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.51" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.69" />
|
||||||
<PackageReference Include="xunit" Version="2.5.0" />
|
<PackageReference Include="xunit" Version="2.5.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -82,6 +82,7 @@ public class MangaParserTests
|
|||||||
[InlineData("63권#200", "63")]
|
[InlineData("63권#200", "63")]
|
||||||
[InlineData("시즌34삽화2", "34")]
|
[InlineData("시즌34삽화2", "34")]
|
||||||
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
||||||
|
[InlineData("Accel World Volume 2", "2")]
|
||||||
public void ParseVolumeTest(string filename, string expected)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
||||||
@ -198,6 +199,7 @@ public class MangaParserTests
|
|||||||
[InlineData("Accel World: Vol 1", "Accel World")]
|
[InlineData("Accel World: Vol 1", "Accel World")]
|
||||||
[InlineData("Accel World Chapter 001 Volume 002", "Accel World")]
|
[InlineData("Accel World Chapter 001 Volume 002", "Accel World")]
|
||||||
[InlineData("Bleach 001-003", "Bleach")]
|
[InlineData("Bleach 001-003", "Bleach")]
|
||||||
|
[InlineData("Accel World Volume 2", "Accel World")]
|
||||||
public void ParseSeriesTest(string filename, string expected)
|
public void ParseSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||||
@ -283,6 +285,7 @@ public class MangaParserTests
|
|||||||
[InlineData("Манга Том 1 2 Глава", "2")]
|
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||||
[InlineData("Accel World Chapter 001 Volume 002", "1")]
|
[InlineData("Accel World Chapter 001 Volume 002", "1")]
|
||||||
[InlineData("Bleach 001-003", "1-3")]
|
[InlineData("Bleach 001-003", "1-3")]
|
||||||
|
[InlineData("Accel World Volume 2", "0")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||||
|
@ -122,6 +122,10 @@ public class SeriesRepositoryTests
|
|||||||
.WithLocalizedName("Heion Sedai no Idaten-tachi")
|
.WithLocalizedName("Heion Sedai no Idaten-tachi")
|
||||||
.WithFormat(MangaFormat.Archive)
|
.WithFormat(MangaFormat.Archive)
|
||||||
.Build())
|
.Build())
|
||||||
|
.WithSeries(new SeriesBuilder("Hitomi-chan is Shy With Strangers")
|
||||||
|
.WithLocalizedName("Hitomi-chan wa Hitomishiri")
|
||||||
|
.WithFormat(MangaFormat.Archive)
|
||||||
|
.Build())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
_unitOfWork.LibraryRepository.Add(library);
|
_unitOfWork.LibraryRepository.Add(library);
|
||||||
@ -133,6 +137,7 @@ public class SeriesRepositoryTests
|
|||||||
[InlineData("The Idaten Deities Know Only Peace", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on series name in DB
|
[InlineData("The Idaten Deities Know Only Peace", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on series name in DB
|
||||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "The Idaten Deities Know Only Peace", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "The Idaten Deities Know Only Peace", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)]
|
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)]
|
||||||
|
[InlineData("Hitomi-chan wa Hitomishiri", MangaFormat.Archive, "", "Hitomi-chan is Shy With Strangers")]
|
||||||
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
|
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
|
||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
@ -1134,10 +1134,10 @@ public class SeriesServiceTests : AbstractDbTest
|
|||||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
|
|
||||||
Assert.Empty(_seriesService.GetRelatedSeries(1, 2).Result.Parent);
|
Assert.Empty((await _seriesService.GetRelatedSeries(1, 2)).Parent);
|
||||||
Assert.Empty(_seriesService.GetRelatedSeries(1, 3).Result.Parent);
|
Assert.Empty((await _seriesService.GetRelatedSeries(1, 3)).Parent);
|
||||||
Assert.Empty(_seriesService.GetRelatedSeries(1, 4).Result.Parent);
|
Assert.Empty((await _seriesService.GetRelatedSeries(1, 4)).Parent);
|
||||||
Assert.NotEmpty(_seriesService.GetRelatedSeries(1, 5).Result.Parent);
|
Assert.NotEmpty((await _seriesService.GetRelatedSeries(1, 5)).Parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -55,54 +55,54 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.4" />
|
<PackageReference Include="Docnet.Core" Version="2.6.0" />
|
||||||
<PackageReference Include="EasyCaching.InMemory" Version="1.9.0" />
|
<PackageReference Include="EasyCaching.InMemory" Version="1.9.1" />
|
||||||
<PackageReference Include="ExCSS" Version="4.2.1" />
|
<PackageReference Include="ExCSS" Version="4.2.2" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
<PackageReference Include="Flurl" Version="3.0.7" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Hangfire" Version="1.8.4" />
|
<PackageReference Include="Hangfire" Version="1.8.5" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.4" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.5" />
|
||||||
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />
|
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
|
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.51" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.53" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||||
<PackageReference Include="NetVips" Version="2.3.1" />
|
<PackageReference Include="NetVips" Version="2.3.1" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.14.3" />
|
<PackageReference Include="NetVips.Native" Version="8.14.5" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.6" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.6" />
|
||||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
<PackageReference Include="Serilog" Version="3.0.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
<PackageReference Include="SharpCompress" Version="0.34.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.2" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.7.0.75501">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.10.0.77988">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.11" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.1" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.51" />
|
<PackageReference Include="System.IO.Abstractions" Version="19.2.69" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -193,7 +193,11 @@ public class AccountController : BaseApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (user == null) return Unauthorized(await _localizationService.Get("en", "bad-credentials"));
|
if (user == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username);
|
||||||
|
return Unauthorized(await _localizationService.Get("en", "bad-credentials"));
|
||||||
|
}
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
||||||
|
|
||||||
@ -205,12 +209,19 @@ public class AccountController : BaseApiController
|
|||||||
if (result.IsLockedOut)
|
if (result.IsLockedOut)
|
||||||
{
|
{
|
||||||
await _userManager.UpdateSecurityStampAsync(user);
|
await _userManager.UpdateSecurityStampAsync(user);
|
||||||
return Unauthorized(await _localizationService.Translate(user.Id, "locked-out"));
|
var errorStr = await _localizationService.Translate(user.Id, "locked-out");
|
||||||
|
_logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive,
|
||||||
|
errorStr);
|
||||||
|
return Unauthorized(errorStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return Unauthorized(await _localizationService.Translate(user.Id, result.IsNotAllowed ? "confirm-email" : "bad-credentials"));
|
var errorStr = await _localizationService.Translate(user.Id,
|
||||||
|
result.IsNotAllowed ? "confirm-email" : "bad-credentials");
|
||||||
|
_logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive,
|
||||||
|
errorStr);
|
||||||
|
return Unauthorized(errorStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,7 +410,6 @@ public class AccountController : BaseApiController
|
|||||||
_logger.LogError(ex, "There was an error during invite user flow, unable to send an email");
|
_logger.LogError(ex, "There was an error during invite user flow, unable to send an email");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
@ -51,7 +51,6 @@ public interface ILibraryRepository
|
|||||||
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
|
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
|
||||||
Task<string?> GetLibraryCoverImageAsync(int libraryId);
|
Task<string?> GetLibraryCoverImageAsync(int libraryId);
|
||||||
Task<IList<string>> GetAllCoverImagesAsync();
|
Task<IList<string>> GetAllCoverImagesAsync();
|
||||||
Task<IDictionary<int, LibraryType>> GetLibraryTypesForIdsAsync(IEnumerable<int> libraryIds);
|
|
||||||
Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||||
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
|
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
|
||||||
}
|
}
|
||||||
@ -346,28 +345,6 @@ public class LibraryRepository : ILibraryRepository
|
|||||||
.ToListAsync())!;
|
.ToListAsync())!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDictionary<int, LibraryType>> GetLibraryTypesForIdsAsync(IEnumerable<int> libraryIds)
|
|
||||||
{
|
|
||||||
var types = await _context.Library
|
|
||||||
.Where(l => libraryIds.Contains(l.Id))
|
|
||||||
.AsNoTracking()
|
|
||||||
.Select(l => new
|
|
||||||
{
|
|
||||||
LibraryId = l.Id,
|
|
||||||
LibraryType = l.Type
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var dict = new Dictionary<int, LibraryType>();
|
|
||||||
|
|
||||||
foreach (var type in types)
|
|
||||||
{
|
|
||||||
dict.TryAdd(type.LibraryId, type.LibraryType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
public async Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||||
{
|
{
|
||||||
var extension = encodeFormat.GetExtension();
|
var extension = encodeFormat.GetExtension();
|
||||||
|
@ -9,7 +9,6 @@ namespace API.Data.Repositories;
|
|||||||
public interface IMangaFileRepository
|
public interface IMangaFileRepository
|
||||||
{
|
{
|
||||||
void Update(MangaFile file);
|
void Update(MangaFile file);
|
||||||
Task<bool> AnyMissingExtension();
|
|
||||||
Task<IList<MangaFile>> GetAllWithMissingExtension();
|
Task<IList<MangaFile>> GetAllWithMissingExtension();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,11 +26,6 @@ public class MangaFileRepository : IMangaFileRepository
|
|||||||
_context.Entry(file).State = EntityState.Modified;
|
_context.Entry(file).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AnyMissingExtension()
|
|
||||||
{
|
|
||||||
return (await _context.MangaFile.CountAsync(f => string.IsNullOrEmpty(f.Extension))) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<MangaFile>> GetAllWithMissingExtension()
|
public async Task<IList<MangaFile>> GetAllWithMissingExtension()
|
||||||
{
|
{
|
||||||
return await _context.MangaFile
|
return await _context.MangaFile
|
||||||
|
@ -15,7 +15,6 @@ public interface IMediaErrorRepository
|
|||||||
void Attach(MediaError error);
|
void Attach(MediaError error);
|
||||||
void Remove(MediaError error);
|
void Remove(MediaError error);
|
||||||
Task<MediaError> Find(string filename);
|
Task<MediaError> Find(string filename);
|
||||||
Task<PagedList<MediaErrorDto>> GetAllErrorDtosAsync(UserParams userParams);
|
|
||||||
IEnumerable<MediaErrorDto> GetAllErrorDtosAsync();
|
IEnumerable<MediaErrorDto> GetAllErrorDtosAsync();
|
||||||
Task<bool> ExistsAsync(MediaError error);
|
Task<bool> ExistsAsync(MediaError error);
|
||||||
Task DeleteAll();
|
Task DeleteAll();
|
||||||
@ -49,15 +48,6 @@ public class MediaErrorRepository : IMediaErrorRepository
|
|||||||
return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync();
|
return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<PagedList<MediaErrorDto>> GetAllErrorDtosAsync(UserParams userParams)
|
|
||||||
{
|
|
||||||
var query = _context.MediaError
|
|
||||||
.OrderByDescending(m => m.Created)
|
|
||||||
.ProjectTo<MediaErrorDto>(_mapper.ConfigurationProvider)
|
|
||||||
.AsNoTracking();
|
|
||||||
return PagedList<MediaErrorDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<MediaErrorDto> GetAllErrorDtosAsync()
|
public IEnumerable<MediaErrorDto> GetAllErrorDtosAsync()
|
||||||
{
|
{
|
||||||
var query = _context.MediaError
|
var query = _context.MediaError
|
||||||
|
@ -19,7 +19,7 @@ public interface IPersonRepository
|
|||||||
Task<IList<Person>> GetAllPeople();
|
Task<IList<Person>> GetAllPeople();
|
||||||
Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId);
|
Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId);
|
||||||
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
|
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
|
||||||
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
|
Task RemoveAllPeopleNoLongerAssociated();
|
||||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId);
|
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId);
|
||||||
Task<int> GetCountAsync();
|
Task<int> GetCountAsync();
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ public class PersonRepository : IPersonRepository
|
|||||||
_context.Person.Remove(person);
|
_context.Person.Remove(person);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false)
|
public async Task RemoveAllPeopleNoLongerAssociated()
|
||||||
{
|
{
|
||||||
var peopleWithNoConnections = await _context.Person
|
var peopleWithNoConnections = await _context.Person
|
||||||
.Include(p => p.SeriesMetadatas)
|
.Include(p => p.SeriesMetadatas)
|
||||||
|
@ -18,7 +18,7 @@ public interface IScrobbleRepository
|
|||||||
void Attach(ScrobbleEvent evt);
|
void Attach(ScrobbleEvent evt);
|
||||||
void Attach(ScrobbleError error);
|
void Attach(ScrobbleError error);
|
||||||
void Remove(ScrobbleEvent evt);
|
void Remove(ScrobbleEvent evt);
|
||||||
void Remove(IList<ScrobbleEvent> evts);
|
void Remove(IEnumerable<ScrobbleEvent> events);
|
||||||
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);
|
||||||
@ -60,9 +60,9 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||||||
_context.ScrobbleEvent.Remove(evt);
|
_context.ScrobbleEvent.Remove(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(IList<ScrobbleEvent> evts)
|
public void Remove(IEnumerable<ScrobbleEvent> events)
|
||||||
{
|
{
|
||||||
_context.ScrobbleEvent.RemoveRange(evts);
|
_context.ScrobbleEvent.RemoveRange(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(ScrobbleEvent evt)
|
public void Update(ScrobbleEvent evt)
|
||||||
|
@ -128,8 +128,6 @@ public interface ISeriesRepository
|
|||||||
Task<Series?> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
Task<Series?> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
||||||
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
|
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
|
||||||
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
||||||
Task<IEnumerable<SeriesDto>> GetAllSeriesDtosByNameAsync(IEnumerable<string> normalizedNames,
|
|
||||||
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
|
||||||
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
||||||
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||||
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
|
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
|
||||||
@ -1054,7 +1052,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
private static IQueryable<Series> BuildFilterGroup(int userId, FilterStatementDto statement, IQueryable<Series> query)
|
private static IQueryable<Series> BuildFilterGroup(int userId, FilterStatementDto statement, IQueryable<Series> query)
|
||||||
{
|
{
|
||||||
var (value, _) = FilterFieldValueConverter.ConvertValue(statement.Field, statement.Value);
|
var value = FilterFieldValueConverter.ConvertValue(statement.Field, statement.Value);
|
||||||
return statement.Field switch
|
return statement.Field switch
|
||||||
{
|
{
|
||||||
FilterField.Summary => query.HasSummary(true, statement.Comparison, (string) value),
|
FilterField.Summary => query.HasSummary(true, statement.Comparison, (string) value),
|
||||||
@ -1085,7 +1083,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
FilterField.WantToRead =>
|
FilterField.WantToRead =>
|
||||||
// This is handled in the higher level of code as it's more general
|
// This is handled in the higher level of code as it's more general
|
||||||
query,
|
query,
|
||||||
FilterField.ReadProgress => query.HasReadingProgress(true, statement.Comparison, (int) value, userId),
|
FilterField.ReadProgress => query.HasReadingProgress(true, statement.Comparison, (float) value, userId),
|
||||||
FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList<MangaFormat>) value),
|
FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList<MangaFormat>) value),
|
||||||
FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value),
|
FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value),
|
||||||
FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value),
|
FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value),
|
||||||
@ -1471,20 +1469,6 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SeriesDto>> GetAllSeriesDtosByNameAsync(IEnumerable<string> normalizedNames, int userId,
|
|
||||||
SeriesIncludes includes = SeriesIncludes.None)
|
|
||||||
{
|
|
||||||
var libraryIds = _context.Library.GetUserLibraries(userId);
|
|
||||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
|
||||||
|
|
||||||
return await _context.Series
|
|
||||||
.Where(s => normalizedNames.Contains(s.NormalizedName))
|
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
|
||||||
.RestrictAgainstAgeRestriction(userRating)
|
|
||||||
.Includes(includes)
|
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds a series by series name or localized name for a given library.
|
/// Finds a series by series name or localized name for a given library.
|
||||||
|
@ -19,7 +19,6 @@ public interface ISiteThemeRepository
|
|||||||
Task<SiteThemeDto?> GetThemeDtoByName(string themeName);
|
Task<SiteThemeDto?> GetThemeDtoByName(string themeName);
|
||||||
Task<SiteTheme> GetDefaultTheme();
|
Task<SiteTheme> GetDefaultTheme();
|
||||||
Task<IEnumerable<SiteTheme>> GetThemes();
|
Task<IEnumerable<SiteTheme>> GetThemes();
|
||||||
Task<SiteTheme?> GetThemeById(int themeId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SiteThemeRepository : ISiteThemeRepository
|
public class SiteThemeRepository : ISiteThemeRepository
|
||||||
@ -89,13 +88,6 @@ public class SiteThemeRepository : ISiteThemeRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SiteTheme?> GetThemeById(int themeId)
|
|
||||||
{
|
|
||||||
return await _context.SiteTheme
|
|
||||||
.Where(t => t.Id == themeId)
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SiteThemeDto?> GetThemeDto(int themeId)
|
public async Task<SiteThemeDto?> GetThemeDto(int themeId)
|
||||||
{
|
{
|
||||||
return await _context.SiteTheme
|
return await _context.SiteTheme
|
||||||
|
@ -17,7 +17,7 @@ public interface ITagRepository
|
|||||||
void Remove(Tag tag);
|
void Remove(Tag tag);
|
||||||
Task<IList<Tag>> GetAllTagsAsync();
|
Task<IList<Tag>> GetAllTagsAsync();
|
||||||
Task<IList<TagDto>> GetAllTagDtosAsync(int userId);
|
Task<IList<TagDto>> GetAllTagDtosAsync(int userId);
|
||||||
Task RemoveAllTagNoLongerAssociated(bool removeExternal = false);
|
Task RemoveAllTagNoLongerAssociated();
|
||||||
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId);
|
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ public class TagRepository : ITagRepository
|
|||||||
_context.Tag.Remove(tag);
|
_context.Tag.Remove(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveAllTagNoLongerAssociated(bool removeExternal = false)
|
public async Task RemoveAllTagNoLongerAssociated()
|
||||||
{
|
{
|
||||||
var tagsWithNoConnections = await _context.Tag
|
var tagsWithNoConnections = await _context.Tag
|
||||||
.Include(p => p.SeriesMetadatas)
|
.Include(p => p.SeriesMetadatas)
|
||||||
|
@ -233,7 +233,7 @@ public static class SeriesFilter
|
|||||||
/// <exception cref="KavitaException"></exception>
|
/// <exception cref="KavitaException"></exception>
|
||||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||||
public static IQueryable<Series> HasReadingProgress(this IQueryable<Series> queryable, bool condition,
|
public static IQueryable<Series> HasReadingProgress(this IQueryable<Series> queryable, bool condition,
|
||||||
FilterComparison comparison, int readProgress, int userId)
|
FilterComparison comparison, float readProgress, int userId)
|
||||||
{
|
{
|
||||||
if (!condition) return queryable;
|
if (!condition) return queryable;
|
||||||
|
|
||||||
|
@ -8,72 +8,72 @@ namespace API.Helpers.Converters;
|
|||||||
|
|
||||||
public static class FilterFieldValueConverter
|
public static class FilterFieldValueConverter
|
||||||
{
|
{
|
||||||
public static (object Value, Type Type) ConvertValue(FilterField field, string value)
|
public static object ConvertValue(FilterField field, string value)
|
||||||
{
|
{
|
||||||
return field switch
|
return field switch
|
||||||
{
|
{
|
||||||
FilterField.SeriesName => (value, typeof(string)),
|
FilterField.SeriesName => value,
|
||||||
FilterField.Path => (value, typeof(string)),
|
FilterField.Path => value,
|
||||||
FilterField.FilePath => (value, typeof(string)),
|
FilterField.FilePath => value,
|
||||||
FilterField.ReleaseYear => (int.Parse(value), typeof(int)),
|
FilterField.ReleaseYear => int.Parse(value),
|
||||||
FilterField.Languages => (value.Split(',').ToList(), typeof(IList<string>)),
|
FilterField.Languages => value.Split(',').ToList(),
|
||||||
FilterField.PublicationStatus => (value.Split(',')
|
FilterField.PublicationStatus => value.Split(',')
|
||||||
.Select(x => (PublicationStatus) Enum.Parse(typeof(PublicationStatus), x))
|
.Select(x => (PublicationStatus) Enum.Parse(typeof(PublicationStatus), x))
|
||||||
.ToList(), typeof(IList<PublicationStatus>)),
|
.ToList(),
|
||||||
FilterField.Summary => (value, typeof(string)),
|
FilterField.Summary => value,
|
||||||
FilterField.AgeRating => (value.Split(',')
|
FilterField.AgeRating => value.Split(',')
|
||||||
.Select(x => (AgeRating) Enum.Parse(typeof(AgeRating), x))
|
.Select(x => (AgeRating) Enum.Parse(typeof(AgeRating), x))
|
||||||
.ToList(), typeof(IList<AgeRating>)),
|
.ToList(),
|
||||||
FilterField.UserRating => (int.Parse(value), typeof(int)),
|
FilterField.UserRating => int.Parse(value),
|
||||||
FilterField.Tags => (value.Split(',')
|
FilterField.Tags => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.CollectionTags => (value.Split(',')
|
FilterField.CollectionTags => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Translators => (value.Split(',')
|
FilterField.Translators => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Characters => (value.Split(',')
|
FilterField.Characters => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Publisher => (value.Split(',')
|
FilterField.Publisher => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Editor => (value.Split(',')
|
FilterField.Editor => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.CoverArtist => (value.Split(',')
|
FilterField.CoverArtist => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Letterer => (value.Split(',')
|
FilterField.Letterer => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Colorist => (value.Split(',')
|
FilterField.Colorist => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Inker => (value.Split(',')
|
FilterField.Inker => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Penciller => (value.Split(',')
|
FilterField.Penciller => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Writers => (value.Split(',')
|
FilterField.Writers => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Genres => (value.Split(',')
|
FilterField.Genres => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.Libraries => (value.Split(',')
|
FilterField.Libraries => value.Split(',')
|
||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(), typeof(IList<int>)),
|
.ToList(),
|
||||||
FilterField.WantToRead => (bool.Parse(value), typeof(bool)),
|
FilterField.WantToRead => bool.Parse(value),
|
||||||
FilterField.ReadProgress => (int.Parse(value), typeof(int)),
|
FilterField.ReadProgress => float.Parse(value),
|
||||||
FilterField.ReadingDate => (DateTime.Parse(value), typeof(DateTime?)),
|
FilterField.ReadingDate => DateTime.Parse(value),
|
||||||
FilterField.Formats => (value.Split(',')
|
FilterField.Formats => value.Split(',')
|
||||||
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
||||||
.ToList(), typeof(IList<MangaFormat>)),
|
.ToList(),
|
||||||
FilterField.ReadTime => (int.Parse(value), typeof(int)),
|
FilterField.ReadTime => int.Parse(value),
|
||||||
_ => throw new ArgumentException("Invalid field type")
|
_ => throw new ArgumentException("Invalid field type")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ public class DirectoryService : IDirectoryService
|
|||||||
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
|
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
|
||||||
|
|
||||||
private static readonly Regex ExcludeDirectories = new Regex(
|
private static readonly Regex ExcludeDirectories = new Regex(
|
||||||
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash",
|
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle",
|
||||||
MatchOptions,
|
MatchOptions,
|
||||||
Tasks.Scanner.Parser.Parser.RegexTimeout);
|
Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||||
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
|
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
|
||||||
|
@ -550,9 +550,10 @@ public static class Parser
|
|||||||
new Regex(
|
new Regex(
|
||||||
@"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
|
@"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
|
|
||||||
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
|
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^(?!Vol)(?<Series>.+?)(?<!Vol)(?<!Vol.)\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
|
@"^(?<Series>.+?)(?<!Vol)(?<!Vol.)(?<!Volume)\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
// Tower Of God S01 014 (CBT) (digital).cbz
|
// Tower Of God S01 014 (CBT) (digital).cbz
|
||||||
new Regex(
|
new Regex(
|
||||||
@ -997,6 +998,7 @@ public static class Parser
|
|||||||
{
|
{
|
||||||
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle")
|
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle")
|
||||||
|| path.StartsWith("._") || Path.GetFileName(path).StartsWith("._") || path.Contains(".qpkg")
|
|| path.StartsWith("._") || Path.GetFileName(path).StartsWith("._") || path.Contains(".qpkg")
|
||||||
|
|| path.StartsWith("#recycle")
|
||||||
|| path.Contains(".caltrash");
|
|| path.Contains(".caltrash");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.SignalR.Presence;
|
using API.SignalR.Presence;
|
||||||
@ -55,8 +56,7 @@ public class EventHub : IEventHub
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task SendMessageToAsync(string method, SignalRMessage message, int userId)
|
public async Task SendMessageToAsync(string method, SignalRMessage message, int userId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId) ?? throw new InvalidOperationException();
|
await _messageHub.Clients.Users(new List<string>() {userId + string.Empty}).SendAsync(method, message);
|
||||||
await _messageHub.Clients.User(user.UserName!).SendAsync(method, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,8 @@ public class MessageHub : Hub
|
|||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
await _tracker.UserConnected(Context.User!.GetUserId(), Context.ConnectionId);
|
var userId = Context.User!.GetUserId();
|
||||||
|
await _tracker.UserConnected(userId, Context.ConnectionId);
|
||||||
|
|
||||||
var currentUsers = await PresenceTracker.GetOnlineUsers();
|
var currentUsers = await PresenceTracker.GetOnlineUsers();
|
||||||
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
|
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
|
||||||
|
@ -80,7 +80,10 @@ public class PresenceTracker : IPresenceTracker
|
|||||||
string[] onlineUsers;
|
string[] onlineUsers;
|
||||||
lock (OnlineUsers)
|
lock (OnlineUsers)
|
||||||
{
|
{
|
||||||
onlineUsers = OnlineUsers.OrderBy(k => k.Value.UserName).Select(k => k.Value.UserName).ToArray();
|
onlineUsers = OnlineUsers
|
||||||
|
.Select(k => k.Value.UserName)
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(onlineUsers);
|
return Task.FromResult(onlineUsers);
|
||||||
@ -91,7 +94,10 @@ public class PresenceTracker : IPresenceTracker
|
|||||||
int[] onlineUsers;
|
int[] onlineUsers;
|
||||||
lock (OnlineUsers)
|
lock (OnlineUsers)
|
||||||
{
|
{
|
||||||
onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin).OrderBy(k => k.Key).Select(k => k.Key).ToArray();
|
onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin)
|
||||||
|
.Select(k => k.Key)
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.7.0.75501">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.10.0.77988">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
25
UI/Web/package-lock.json
generated
25
UI/Web/package-lock.json
generated
@ -20,7 +20,7 @@
|
|||||||
"@angular/router": "^16.1.8",
|
"@angular/router": "^16.1.8",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||||
"@iplab/ngx-file-upload": "^16.0.1",
|
"@iplab/ngx-file-upload": "^16.0.2",
|
||||||
"@microsoft/signalr": "^7.0.11",
|
"@microsoft/signalr": "^7.0.11",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||||
"@ngneat/transloco": "^5.0.7",
|
"@ngneat/transloco": "^5.0.7",
|
||||||
@ -37,8 +37,8 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lazysizes": "^5.3.2",
|
"lazysizes": "^5.3.2",
|
||||||
"ng-circle-progress": "^1.7.1",
|
"ng-circle-progress": "^1.7.1",
|
||||||
"ng-select2-component": "^13.0.6",
|
"ng-select2-component": "^13.0.9",
|
||||||
"ngx-color-picker": "^14.0.0",
|
"ngx-color-picker": "^15.0.0",
|
||||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
"ngx-slider-v2": "^16.0.2",
|
"ngx-slider-v2": "^16.0.2",
|
||||||
@ -2950,9 +2950,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@iplab/ngx-file-upload": {
|
"node_modules/@iplab/ngx-file-upload": {
|
||||||
"version": "16.0.1",
|
"version": "16.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-16.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-16.0.2.tgz",
|
||||||
"integrity": "sha512-gHnEofzmAv9x1YQzjwBTcUPriaiX+S+m/v24lWrC+x4FmEpeqxwnsSE0lSlE04owBQpp7cFOFGCvwQ2JjxenhQ==",
|
"integrity": "sha512-6UppO6lROAbkGs+rFZ6ngutsCPrjs/BMnXsIE9UL4AL1NLRRAXrb28pkU2U7KjtDg/0naJ6JFmpRyUBCw5SXhg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@ -2960,6 +2960,7 @@
|
|||||||
"@angular/animations": "^16.0.0",
|
"@angular/animations": "^16.0.0",
|
||||||
"@angular/common": "^16.0.0",
|
"@angular/common": "^16.0.0",
|
||||||
"@angular/core": "^16.0.0",
|
"@angular/core": "^16.0.0",
|
||||||
|
"@angular/forms": "^16.0.0",
|
||||||
"rxjs": "^7.0.0"
|
"rxjs": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -10558,9 +10559,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ng-select2-component": {
|
"node_modules/ng-select2-component": {
|
||||||
"version": "13.0.6",
|
"version": "13.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.9.tgz",
|
||||||
"integrity": "sha512-CiAelglSz2aeYy0BiXRi32zc49Mq27+J1eDzTrXmf2o50MvNo3asS3NRVQcnSldo/zLcJafWCMueVfjVaV1etw==",
|
"integrity": "sha512-Bj7lHCiHnwNFECyzpn0LyD3IOPnBbIHHYXxpFU313QZgVkEz7oiF9nBnkorAAABIfLk4EiU0nBQkY3CmbVOgfg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ngx-infinite-scroll": ">=16.0.0",
|
"ngx-infinite-scroll": ">=16.0.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@ -10572,9 +10573,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ngx-color-picker": {
|
"node_modules/ngx-color-picker": {
|
||||||
"version": "14.0.0",
|
"version": "15.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-15.0.0.tgz",
|
||||||
"integrity": "sha512-w28zx2DyVpIJeNsTB3T2LUI4Ed/Ujf5Uhxuh0dllputfpxXwZG9ocSJM/0L67+fxA3UnfvvXVZNUX1Ny5nZIIw==",
|
"integrity": "sha512-+7wK8Pz9pm7ywJQOWELRcLYO9J0q4giF4b5QFxq8J3kEcHsUBn0hKOpBbGud+UmNnOwbJVgU2rhyRpGIDUCDJw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
"@angular/router": "^16.1.8",
|
"@angular/router": "^16.1.8",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||||
"@iplab/ngx-file-upload": "^16.0.1",
|
"@iplab/ngx-file-upload": "^16.0.2",
|
||||||
"@microsoft/signalr": "^7.0.11",
|
"@microsoft/signalr": "^7.0.11",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||||
"@ngneat/transloco": "^5.0.7",
|
"@ngneat/transloco": "^5.0.7",
|
||||||
@ -42,8 +42,8 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lazysizes": "^5.3.2",
|
"lazysizes": "^5.3.2",
|
||||||
"ng-circle-progress": "^1.7.1",
|
"ng-circle-progress": "^1.7.1",
|
||||||
"ng-select2-component": "^13.0.6",
|
"ng-select2-component": "^13.0.9",
|
||||||
"ngx-color-picker": "^14.0.0",
|
"ngx-color-picker": "^15.0.0",
|
||||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
"ngx-slider-v2": "^16.0.2",
|
"ngx-slider-v2": "^16.0.2",
|
||||||
|
@ -120,7 +120,6 @@ export class AccountService {
|
|||||||
const user = response;
|
const user = response;
|
||||||
if (user) {
|
if (user) {
|
||||||
this.setCurrentUser(user);
|
this.setCurrentUser(user);
|
||||||
this.messageHub.createHubConnection(user, this.hasAdminRole(user));
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
takeUntilDestroyed(this.destroyRef)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
@ -150,7 +149,8 @@ export class AccountService {
|
|||||||
this.stopRefreshTokenTimer();
|
this.stopRefreshTokenTimer();
|
||||||
|
|
||||||
if (this.currentUser) {
|
if (this.currentUser) {
|
||||||
this.messageHub.createHubConnection(this.currentUser, this.hasAdminRole(this.currentUser));
|
this.messageHub.stopHubConnection();
|
||||||
|
this.messageHub.createHubConnection(this.currentUser);
|
||||||
this.hasValidLicense().subscribe();
|
this.hasValidLicense().subscribe();
|
||||||
this.startRefreshTokenTimer();
|
this.startRefreshTokenTimer();
|
||||||
}
|
}
|
||||||
|
@ -114,8 +114,6 @@ export class MessageHubService {
|
|||||||
*/
|
*/
|
||||||
public onlineUsers$ = this.onlineUsersSource.asObservable();
|
public onlineUsers$ = this.onlineUsersSource.asObservable();
|
||||||
|
|
||||||
isAdmin: boolean = false;
|
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,9 +130,7 @@ export class MessageHubService {
|
|||||||
return event.event === eventType;
|
return event.event === eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
createHubConnection(user: User, isAdmin: boolean) {
|
createHubConnection(user: User) {
|
||||||
this.isAdmin = isAdmin;
|
|
||||||
|
|
||||||
this.hubConnection = new HubConnectionBuilder()
|
this.hubConnection = new HubConnectionBuilder()
|
||||||
.withUrl(this.hubUrl + 'messages', {
|
.withUrl(this.hubUrl + 'messages', {
|
||||||
accessTokenFactory: () => user.token
|
accessTokenFactory: () => user.token
|
||||||
@ -186,7 +182,6 @@ export class MessageHubService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.hubConnection.on(EVENTS.DashboardUpdate, resp => {
|
this.hubConnection.on(EVENTS.DashboardUpdate, resp => {
|
||||||
console.log('dashboard update event came in')
|
|
||||||
this.messagesSource.next({
|
this.messagesSource.next({
|
||||||
event: EVENTS.DashboardUpdate,
|
event: EVENTS.DashboardUpdate,
|
||||||
payload: resp.body as DashboardUpdateEvent
|
payload: resp.body as DashboardUpdateEvent
|
||||||
|
@ -92,7 +92,7 @@ export class LicenseComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteLicense() {
|
async deleteLicense() {
|
||||||
if (!await this.confirmService.confirm(translate('k+-delete-key'))) {
|
if (!await this.confirmService.confirm(translate('toasts.k+-delete-key'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, injec
|
|||||||
import {Title} from '@angular/platform-browser';
|
import {Title} from '@angular/platform-browser';
|
||||||
import {Router, RouterLink} from '@angular/router';
|
import {Router, RouterLink} from '@angular/router';
|
||||||
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
|
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
|
||||||
import {map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
|
import {debounceTime, map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
|
||||||
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||||
import {Library} from 'src/app/_models/library';
|
import {Library} from 'src/app/_models/library';
|
||||||
import {RecentlyAddedItem} from 'src/app/_models/recently-added-item';
|
import {RecentlyAddedItem} from 'src/app/_models/recently-added-item';
|
||||||
@ -57,6 +57,7 @@ export class DashboardComponent implements OnInit {
|
|||||||
streams: Array<DashboardStream> = [];
|
streams: Array<DashboardStream> = [];
|
||||||
genre: Genre | undefined;
|
genre: Genre | undefined;
|
||||||
refreshStreams$ = new Subject<void>();
|
refreshStreams$ = new Subject<void>();
|
||||||
|
refreshStreamsFromDashboardUpdate$ = new Subject<void>();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,6 +81,13 @@ export class DashboardComponent implements OnInit {
|
|||||||
|
|
||||||
this.loadDashboard();
|
this.loadDashboard();
|
||||||
|
|
||||||
|
this.refreshStreamsFromDashboardUpdate$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(1000),
|
||||||
|
tap(() => {
|
||||||
|
console.log('Loading Dashboard')
|
||||||
|
this.loadDashboard()
|
||||||
|
}))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
this.refreshStreams$.pipe(takeUntilDestroyed(this.destroyRef), throttleTime(10_000),
|
this.refreshStreams$.pipe(takeUntilDestroyed(this.destroyRef), throttleTime(10_000),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.loadDashboard()
|
this.loadDashboard()
|
||||||
@ -87,29 +95,12 @@ export class DashboardComponent implements OnInit {
|
|||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
|
||||||
// TODO: Solve how Websockets will work with these dyanamic streams
|
|
||||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
||||||
|
|
||||||
if (res.event === EVENTS.DashboardUpdate) {
|
if (res.event === EVENTS.DashboardUpdate) {
|
||||||
console.log('dashboard update triggered')
|
this.refreshStreamsFromDashboardUpdate$.next();
|
||||||
this.refreshStreams$.next();
|
|
||||||
} else if (res.event === EVENTS.SeriesAdded) {
|
} else if (res.event === EVENTS.SeriesAdded) {
|
||||||
// const seriesAddedEvent = res.payload as SeriesAddedEvent;
|
|
||||||
|
|
||||||
// this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
|
||||||
// if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return;
|
|
||||||
// this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries];
|
|
||||||
// this.cdRef.markForCheck();
|
|
||||||
// });
|
|
||||||
this.refreshStreams$.next();
|
this.refreshStreams$.next();
|
||||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||||
//const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
|
||||||
|
|
||||||
//
|
|
||||||
// this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
|
||||||
// this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
|
||||||
// this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
|
||||||
// this.cdRef.markForCheck();
|
|
||||||
this.refreshStreams$.next();
|
this.refreshStreams$.next();
|
||||||
} else if (res.event === EVENTS.ScanSeries) {
|
} else if (res.event === EVENTS.ScanSeries) {
|
||||||
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
||||||
|
@ -20,9 +20,9 @@
|
|||||||
<span class="visually-hidden">{{t('shortcuts-menu-alt')}}</span>
|
<span class="visually-hidden">{{t('shortcuts-menu-alt')}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!bookmarkMode && hasBookmarkRights" class="btn btn-icon" role="checkbox" [attr.aria-checked]="CurrentPageBookmarked"
|
<button *ngIf="!bookmarkMode && hasBookmarkRights" class="btn btn-icon" role="checkbox" [attr.aria-checked]="CurrentPageBookmarked"
|
||||||
title="{{CurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()">
|
title="{{t(CurrentPageBookmarked ? 'unbookmark-page-tooltip' : 'bookmark-page-tooltip')}}" (click)="bookmarkPage()">
|
||||||
<i class="{{CurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i>
|
<i class="{{CurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">{{CurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span>
|
<span class="visually-hidden">{{t(CurrentPageBookmarked ? 'unbookmark-page-tooltip' : 'bookmark-page-tooltip')}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -737,7 +737,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
|
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
|
||||||
|
|
||||||
let img;
|
let img;
|
||||||
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
|
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
|
||||||
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
|
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
|
||||||
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
|
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
|
||||||
);
|
);
|
||||||
@ -1208,9 +1208,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
setCanvasImage() {
|
setCanvasImage() {
|
||||||
if (this.cachedImages === undefined) return;
|
if (this.cachedImages === undefined) return;
|
||||||
this.canvasImage = this.getPage(this.pageNum, this.chapterId, this.layoutMode !== LayoutMode.Single);
|
this.canvasImage = this.getPage(this.pageNum, this.chapterId, this.layoutMode !== LayoutMode.Single);
|
||||||
this.canvasImage.addEventListener('load', () => {
|
if (!this.canvasImage.complete) {
|
||||||
|
this.canvasImage.addEventListener('load', () => {
|
||||||
|
this.currentImage.next(this.canvasImage);
|
||||||
|
}, false);
|
||||||
|
} else {
|
||||||
this.currentImage.next(this.canvasImage);
|
this.currentImage.next(this.canvasImage);
|
||||||
}, false);
|
}
|
||||||
|
|
||||||
|
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
@ -1329,7 +1334,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]);
|
//const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]);
|
||||||
// console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
|
// console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
|
||||||
// if (this.pageNum === p[1]) return '[' + p + ']';
|
// if (this.pageNum === p[1]) return '[' + p + ']';
|
||||||
// return '' + p
|
// return '' + p
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<ng-template #mobileView>
|
<ng-template #mobileView>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2 col-10">
|
<div class="col-md-4 col-10">
|
||||||
<select class="form-select" formControlName="comparison">
|
<select class="form-select" formControlName="comparison">
|
||||||
<option *ngFor="let opt of groupOptions" [value]="opt.value">{{opt.title}}</option>
|
<option *ngFor="let opt of groupOptions" [value]="opt.value">{{opt.title}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header, ::ng-deep .dropdown-menu{
|
::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header{
|
||||||
background: var(--bs-body-bg);
|
background: var(--bs-body-bg);
|
||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
<ng-container *transloco="let t; read: 'metadata-filter'">
|
<ng-container *transloco="let t; read: 'metadata-filter'">
|
||||||
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
|
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
|
||||||
<div *ngIf="utilityService.getActiveBreakpoint() >= Breakpoint.Tablet">
|
<ng-container *ngIf="utilityService.getActiveBreakpoint() as activeBreakpoint">
|
||||||
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
|
<div *ngIf="activeBreakpoint >= Breakpoint.Tablet; else mobileView">
|
||||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="utilityService.getActiveBreakpoint() < Breakpoint.Desktop">
|
|
||||||
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)">
|
|
||||||
<h5 header>
|
|
||||||
{{t('filter-title')}}
|
|
||||||
</h5>
|
|
||||||
<div body class="drawer-body">
|
|
||||||
<!-- TODO: BUG: Filter section is instantiated twice if this isn't ngIf'd -->
|
|
||||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</app-drawer>
|
</div>
|
||||||
</div>
|
|
||||||
|
<ng-template #mobileView>
|
||||||
|
<div>
|
||||||
|
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)" [width]="600">
|
||||||
|
<h5 header>
|
||||||
|
{{t('filter-title')}}
|
||||||
|
</h5>
|
||||||
|
<div body class="drawer-body">
|
||||||
|
<!-- TODO: BUG: Filter section is instantiated twice if this isn't ngIf'd -->
|
||||||
|
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||||
|
</div>
|
||||||
|
</app-drawer>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #filterSection>
|
<ng-template #filterSection>
|
||||||
@ -30,13 +34,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<form [formGroup]="sortGroup" class="container-fluid">
|
<form [formGroup]="sortGroup" class="container-fluid">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2 col-sm-2">
|
<div class="col-md-2 col-sm-3">
|
||||||
<div class="form-group pe-1">
|
<div class="form-group pe-1">
|
||||||
<label for="limit-to" class="form-label">{{t('limit-label')}}</label>
|
<label for="limit-to" class="form-label">{{t('limit-label')}}</label>
|
||||||
<input id="limit-to" type="number" inputmode="numeric" class="form-control" formControlName="limitTo">
|
<input id="limit-to" type="number" inputmode="numeric" class="form-control" formControlName="limitTo">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-10">
|
<div class="col-md-3 col-sm-9">
|
||||||
<label for="sort-options" class="form-label">{{t('sort-by-label')}}</label>
|
<label for="sort-options" class="form-label">{{t('sort-by-label')}}</label>
|
||||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0;" [disabled]="filterSettings.sortDisabled">
|
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0;" [disabled]="filterSettings.sortDisabled">
|
||||||
<i class="fa fa-arrow-up" [title]="t('ascending-alt')" *ngIf="isAscendingSort; else descSort"></i>
|
<i class="fa fa-arrow-up" [title]="t('ascending-alt')" *ngIf="isAscendingSort; else descSort"></i>
|
||||||
@ -48,17 +52,16 @@
|
|||||||
<option *ngFor="let field of allSortFields" [value]="field">{{field | sortField}}</option>
|
<option *ngFor="let field of allSortFields" [value]="field">{{field | sortField}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-10">
|
<div class="col-md-4 col-sm-12" [ngClass]="{'mt-3': utilityService.getActiveBreakpoint() <= Breakpoint.Mobile}">
|
||||||
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
||||||
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
||||||
<!-- <select2 [data]="smartFilters"-->
|
<!-- <select2 [data]="smartFilters"-->
|
||||||
<!-- id="filter-name"-->
|
<!-- id="filter-name"-->
|
||||||
<!-- formControlName="name"-->
|
<!-- formControlName="name"-->
|
||||||
<!-- (update)="updateFilterValue($event)"-->
|
<!-- (update)="loadSavedFilter($event)"-->
|
||||||
|
<!-- (autoCreateItem)="createFilterValue($event)"-->
|
||||||
<!-- [autoCreate]="true"-->
|
<!-- [autoCreate]="true"-->
|
||||||
<!-- [multiple]="false"-->
|
<!-- displaySearchStatus="always"-->
|
||||||
<!-- [infiniteScroll]="false"-->
|
|
||||||
<!-- [hideSelectedItems]="true"-->
|
|
||||||
<!-- [resettable]="true">-->
|
<!-- [resettable]="true">-->
|
||||||
<!-- </select2>-->
|
<!-- </select2>-->
|
||||||
</div>
|
</div>
|
||||||
@ -73,11 +76,11 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #buttons>
|
<ng-template #buttons>
|
||||||
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
||||||
<div class="col-md-2 col-sm-6 mt-4 pt-2 d-flex justify-content-between">
|
<div class="col-md-6 col-sm-6 mt-4 pt-2 d-flex justify-content-between">
|
||||||
<button class="btn btn-secondary col-6 me-1" (click)="clear()"><i class="fa-solid fa-arrow-rotate-left me-1" aria-hidden="true"></i>{{t('reset')}}</button>
|
<button class="btn btn-secondary col-6 me-1" (click)="clear()"><i class="fa-solid fa-arrow-rotate-left me-1" aria-hidden="true"></i>{{t('reset')}}</button>
|
||||||
<button class="btn btn-primary col-6" (click)="apply()"><i class="fa-solid fa-play me-1" aria-hidden="true"></i>{{t('apply')}}</button>
|
<button class="btn btn-primary col-6" (click)="apply()"><i class="fa-solid fa-play me-1" aria-hidden="true"></i>{{t('apply')}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1 col-sm-6 mt-4 pt-2">
|
<div class="col-md-2 col-sm-6 mt-4 pt-2">
|
||||||
<button class="btn btn-primary col-12" (click)="save()" [disabled]="filterSettings.saveDisabled || !this.sortGroup.get('name')?.value">
|
<button class="btn btn-primary col-12" (click)="save()" [disabled]="filterSettings.saveDisabled || !this.sortGroup.get('name')?.value">
|
||||||
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{t('save')}}
|
{{t('save')}}
|
||||||
|
@ -21,7 +21,7 @@ import {SeriesFilterV2} from '../_models/metadata/v2/series-filter-v2';
|
|||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {TypeaheadComponent} from '../typeahead/_components/typeahead.component';
|
import {TypeaheadComponent} from '../typeahead/_components/typeahead.component';
|
||||||
import {DrawerComponent} from '../shared/drawer/drawer.component';
|
import {DrawerComponent} from '../shared/drawer/drawer.component';
|
||||||
import {AsyncPipe, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common';
|
import {AsyncPipe, NgClass, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common';
|
||||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||||
import {SortFieldPipe} from "../pipe/sort-field.pipe";
|
import {SortFieldPipe} from "../pipe/sort-field.pipe";
|
||||||
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
|
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
|
||||||
@ -30,7 +30,13 @@ import {MetadataService} from "../_services/metadata.service";
|
|||||||
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||||
import {FilterService} from "../_services/filter.service";
|
import {FilterService} from "../_services/filter.service";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import {Select2Module, Select2Option, Select2UpdateEvent} from "ng-select2-component";
|
import {
|
||||||
|
Select2AutoCreateEvent,
|
||||||
|
Select2Module,
|
||||||
|
Select2Option,
|
||||||
|
Select2UpdateEvent,
|
||||||
|
Select2UpdateValue
|
||||||
|
} from "ng-select2-component";
|
||||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -40,7 +46,7 @@ import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
|
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
|
||||||
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module]
|
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module, NgClass]
|
||||||
})
|
})
|
||||||
export class MetadataFilterComponent implements OnInit {
|
export class MetadataFilterComponent implements OnInit {
|
||||||
|
|
||||||
@ -61,6 +67,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
|
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
public readonly utilityService = inject(UtilityService);
|
public readonly utilityService = inject(UtilityService);
|
||||||
|
public readonly filterUtilitiesService = inject(FilterUtilitiesService);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,8 +121,36 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
this.loadFromPresetsAndSetup();
|
this.loadFromPresetsAndSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFilterValue(event: Select2UpdateEvent<any>) {
|
loadSavedFilter(event: Select2UpdateEvent<any>) {
|
||||||
console.log('event: ', event);
|
// Load the filter from the backend and update the screen
|
||||||
|
if (event.value === undefined || typeof(event.value) === 'string') return;
|
||||||
|
const smartFilter = event.value as SmartFilter;
|
||||||
|
this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
console.log('update event: ', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
createFilterValue(event: Select2AutoCreateEvent<any>) {
|
||||||
|
// Create a new name and filter
|
||||||
|
if (!this.filterV2) return;
|
||||||
|
this.filterV2.name = event.value;
|
||||||
|
this.filterService.saveFilter(this.filterV2).subscribe(() => {
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
value: {
|
||||||
|
filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
|
||||||
|
name: event.value,
|
||||||
|
} as SmartFilter,
|
||||||
|
label: event.value
|
||||||
|
};
|
||||||
|
this.smartFilters.push(item);
|
||||||
|
this.sortGroup.get('name')?.setValue(item);
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
this.toastr.success(translate('toasts.smart-filter-updated'));
|
||||||
|
this.apply();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('create event: ', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -204,7 +239,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
|
|
||||||
apply() {
|
apply() {
|
||||||
this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!});
|
this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!});
|
||||||
|
|
||||||
if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) {
|
if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) {
|
||||||
this.toggleSelected();
|
this.toggleSelected();
|
||||||
}
|
}
|
||||||
@ -219,7 +254,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
this.filterService.saveFilter(this.filterV2).subscribe(() => {
|
this.filterService.saveFilter(this.filterV2).subscribe(() => {
|
||||||
this.toastr.success(translate('toasts.smart-filter-updated'));
|
this.toastr.success(translate('toasts.smart-filter-updated'));
|
||||||
this.apply();
|
this.apply();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelected() {
|
toggleSelected() {
|
||||||
@ -231,5 +266,5 @@ export class MetadataFilterComponent implements OnInit {
|
|||||||
this.toggleService.set(!this.filteringCollapsed);
|
this.toggleService.set(!this.filteringCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly Breakpoint = Breakpoint;
|
protected readonly Breakpoint = Breakpoint;
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,9 @@ export class ProviderNamePipe implements PipeTransform {
|
|||||||
return 'MAL';
|
return 'MAL';
|
||||||
case ScrobbleProvider.Kavita:
|
case ScrobbleProvider.Kavita:
|
||||||
return 'Kavita';
|
return 'Kavita';
|
||||||
|
case ScrobbleProvider.GoogleBooks:
|
||||||
|
return 'Google Books';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export class DrawerOptions {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-drawer',
|
selector: 'app-drawer',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TranslocoDirective],
|
imports: [CommonModule, TranslocoDirective],
|
||||||
templateUrl: './drawer.component.html',
|
templateUrl: './drawer.component.html',
|
||||||
styleUrls: ['./drawer.component.scss'],
|
styleUrls: ['./drawer.component.scss'],
|
||||||
exportAs: "drawer",
|
exportAs: "drawer",
|
||||||
|
@ -17,11 +17,11 @@
|
|||||||
{{filter.name}}
|
{{filter.name}}
|
||||||
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
|
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
Add
|
{{t('add')}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item" *ngIf="smartFilters.length === 0">
|
<li class="list-group-item" *ngIf="smartFilters.length === 0">
|
||||||
All Smart filters added to Dashboard or none created yet.
|
{{t('no-data')}}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,6 @@ export class CustomizeDashboardModalComponent {
|
|||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
constructor(public modal: NgbActiveModal) {
|
constructor(public modal: NgbActiveModal) {
|
||||||
|
|
||||||
forkJoin([this.dashboardService.getDashboardStreams(false), this.filterService.getAllFilters()]).subscribe(results => {
|
forkJoin([this.dashboardService.getDashboardStreams(false), this.filterService.getAllFilters()]).subscribe(results => {
|
||||||
this.items = results[0];
|
this.items = results[0];
|
||||||
const smartFilterStreams = new Set(results[0].filter(d => !d.isProvided).map(d => d.name));
|
const smartFilterStreams = new Set(results[0].filter(d => !d.isProvided).map(d => d.name));
|
||||||
|
@ -792,7 +792,7 @@
|
|||||||
"allow-scrobbling-label": "Allow Scrobbling",
|
"allow-scrobbling-label": "Allow Scrobbling",
|
||||||
"allow-scrobbling-tooltip": "Should Kavita scrobble reading events, want to read status, ratings, and reviews to configured providers. This will only occur if the server has an active Kavita+ Subscription.",
|
"allow-scrobbling-tooltip": "Should Kavita scrobble reading events, want to read status, ratings, and reviews to configured providers. This will only occur if the server has an active Kavita+ Subscription.",
|
||||||
"folder-watching-label": "Folder Watching",
|
"folder-watching-label": "Folder Watching",
|
||||||
"folder-watching-tooltip": "Override Server folder watching for this library. If off, folder watching won't run on the folders this library contains. If libraries share folders, then folders may still be ran against.",
|
"folder-watching-tooltip": "Override Server folder watching for this library. If off, folder watching won't run on the folders this library contains. If libraries share folders, then folders may still be ran against. Will always wait 10 minutes before triggering scan.",
|
||||||
"include-in-dashboard-label": "Include in Dashboard",
|
"include-in-dashboard-label": "Include in Dashboard",
|
||||||
"include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.",
|
"include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.",
|
||||||
"include-in-recommendation-label": "Include in Recommended",
|
"include-in-recommendation-label": "Include in Recommended",
|
||||||
@ -1493,6 +1493,8 @@
|
|||||||
"swipe-enabled-label": "Swipe Enabled",
|
"swipe-enabled-label": "Swipe Enabled",
|
||||||
"enable-comic-book-label": "Emulate comic book",
|
"enable-comic-book-label": "Emulate comic book",
|
||||||
"brightness-label": "Brightness",
|
"brightness-label": "Brightness",
|
||||||
|
"bookmark-page-tooltip": "Bookmark Page",
|
||||||
|
"unbookmark-page-tooltip": "Unbookmark Page",
|
||||||
|
|
||||||
"first-time-reading-manga": "Tap the image at any time to open the menu. You can configure different settings or go to page by clicking progress bar. Tap sides of image move to next/prev page.",
|
"first-time-reading-manga": "Tap the image at any time to open the menu. You can configure different settings or go to page by clicking progress bar. Tap sides of image move to next/prev page.",
|
||||||
"layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout",
|
"layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout",
|
||||||
@ -1555,7 +1557,7 @@
|
|||||||
"last-chapter-added": "Item Added",
|
"last-chapter-added": "Item Added",
|
||||||
"time-to-read": "Time to Read",
|
"time-to-read": "Time to Read",
|
||||||
"release-year": "Release Year",
|
"release-year": "Release Year",
|
||||||
"read-progress": "Read Progress"
|
"read-progress": "Last Read"
|
||||||
},
|
},
|
||||||
|
|
||||||
"edit-series-modal": {
|
"edit-series-modal": {
|
||||||
@ -1728,8 +1730,10 @@
|
|||||||
|
|
||||||
"customize-dashboard-modal": {
|
"customize-dashboard-modal": {
|
||||||
"title": "Customize Dashboard",
|
"title": "Customize Dashboard",
|
||||||
|
"no-data": "All Smart filters added to Dashboard or none created yet.",
|
||||||
"close": "{{common.close}}",
|
"close": "{{common.close}}",
|
||||||
"save": "{{common.save}}"
|
"save": "{{common.save}}",
|
||||||
|
"add": "{{common.add}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"filter-field-pipe": {
|
"filter-field-pipe": {
|
||||||
|
@ -5,6 +5,6 @@ export const environment = {
|
|||||||
production: true,
|
production: true,
|
||||||
apiUrl: `${BASE_URL}api/`,
|
apiUrl: `${BASE_URL}api/`,
|
||||||
hubUrl:`${BASE_URL}hubs/`,
|
hubUrl:`${BASE_URL}hubs/`,
|
||||||
buyLink: 'https://buy.stripe.com/3cs7uw67p2Re7JK4gj?prefilled_promo_code=FREETRIAL',
|
buyLink: 'https://buy.stripe.com/00gcOQanFajG0hi5ko?prefilled_promo_code=FREETRIAL',
|
||||||
manageLink: 'https://billing.stripe.com/p/login/28oaFRa3HdHWb5ecMM'
|
manageLink: 'https://billing.stripe.com/p/login/28oaFRa3HdHWb5ecMM'
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"version": "0.7.8.4"
|
"version": "0.7.8.5"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user