mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Security Event Logging & Bugfixes (#1882)
* Fixed bookmarking failing to convert to webp * Brought the ag-swipe/ng-swipe code into Kavita due to being abandoned by developer and angular requirements. * Fixed average reading time per week finally * Cleaned up some extra decimals on time duration pipe * Don't try to update index.html for base url on local. Fixed ag-swipe on prod mode. * Updated a link on theme manager to point to the new github * Range knobs should be primary color on firefox too * Implemented the ability to get thumbnails of pages inside an archive or pdf. * Updated packages and fixed opds-ps 1.2 issue * Fixed lock file * Allow Kavita's Swagger to hit instances with CORS * Added IP/Request logging for Security Audits * Linked up Summary tag from CBL into Kavita. * Redid the migration so SecurityEvent now has UTC date as well. * Split security logging to a separate file * Update to new versions of checkout and setup * Added a PR check on PR body to ensure that it doesn't contain any characters that break our discord hook. * Updating action * optimize regex in action * Fixed an issue where fit to width would cause the actual height of the image to be shown for pagination bars, instead of rendered. * Added some new code in GetPageFromFiles to ensure pages that exceed array map down to last file. * Added comment about robots * Fixed up unit tests for new ReaderService signature * Kavita now cleans up empty reading lists at night * Don't allow nightly cleanup to run if we are running media conversion tasks * Fixed some bugs in typeahead, it should behave much more reliably. * Fix an issue where emulate comic book wasn't extending to the bottom properly * Added support for Series Chapter 001 Volume 001 * Refactor XFrameOptions="SameOrigins" out to allow users to override in appsettings.json. * Added a rate limiter for some endpoints, but it doesn't seem to be triggering --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
21203414f0
commit
c10acb1279
25
.github/workflows/sonar-scan.yml
vendored
25
.github/workflows/sonar-scan.yml
vendored
@ -8,17 +8,25 @@ on:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
check_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Body
|
||||
uses: JJ/github-pr-contains-action@releases/v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyDoesNotContain: "["|`]"
|
||||
build:
|
||||
name: Build .Net
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
@ -74,19 +82,18 @@ jobs:
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
|
||||
|
||||
version:
|
||||
name: Bump version on Develop push
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
@ -139,7 +146,7 @@ jobs:
|
||||
echo "::set-output name=BODY::$body"
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
@ -176,7 +183,7 @@ jobs:
|
||||
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
||||
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
@ -253,7 +260,7 @@ jobs:
|
||||
echo "::set-output name=BODY::$body"
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@ -293,7 +300,7 @@ jobs:
|
||||
id: parse-version
|
||||
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
- name: Install Swashbuckle CLI
|
||||
|
@ -6,11 +6,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.1" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.4" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.4" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -9,6 +9,7 @@ namespace API.Tests.Helpers;
|
||||
|
||||
public class PersonHelperTests
|
||||
{
|
||||
#region UpdatePeople
|
||||
[Fact]
|
||||
public void UpdatePeople_ShouldAddNewPeople()
|
||||
{
|
||||
@ -47,7 +48,15 @@ public class PersonHelperTests
|
||||
|
||||
Assert.Equal(3, allPeople.Count);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UpdatePeopleList
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region RemovePeople
|
||||
[Fact]
|
||||
public void RemovePeople_ShouldRemovePeopleOfSameRole()
|
||||
{
|
||||
@ -111,6 +120,10 @@ public class PersonHelperTests
|
||||
Assert.Equal(2, peopleRemoved.Count);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region KeepOnlySamePeopleBetweenLists
|
||||
[Fact]
|
||||
public void KeepOnlySamePeopleBetweenLists()
|
||||
{
|
||||
@ -135,6 +148,9 @@ public class PersonHelperTests
|
||||
|
||||
Assert.Equal(2, peopleRemoved.Count);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AddPeople
|
||||
|
||||
[Fact]
|
||||
public void AddPeople_ShouldAddOnlyNonExistingPeople()
|
||||
@ -157,4 +173,6 @@ public class PersonHelperTests
|
||||
Assert.Equal(4, existingPeople.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ public class MangaParserTests
|
||||
[InlineData("몰?루 아카이브 7.5권", "7.5")]
|
||||
[InlineData("63권#200", "63")]
|
||||
[InlineData("시즌34삽화2", "34")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
||||
public void ParseVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
||||
@ -195,6 +196,7 @@ public class MangaParserTests
|
||||
[InlineData("Манга Том 1 3-4 Глава", "Манга")]
|
||||
[InlineData("Esquire 6권 2021년 10월호", "Esquire")]
|
||||
[InlineData("Accel World: Vol 1", "Accel World")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "Accel World")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||
@ -278,6 +280,7 @@ public class MangaParserTests
|
||||
[InlineData("Манга Глава 2", "2")]
|
||||
[InlineData("Манга 2 Глава", "2")]
|
||||
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "1")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -16,7 +17,6 @@ using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -437,7 +437,8 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>(),
|
||||
Substitute.For<IImageService>(), new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
|
||||
@ -534,7 +535,8 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>());
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
|
||||
await readerService.MarkSeriesAsRead(user, s.Id);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -29,10 +29,9 @@ namespace API.Tests.Services;
|
||||
public class ReaderServiceTests
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
private readonly DataContext _context;
|
||||
private readonly ReaderService _readerService;
|
||||
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
@ -50,6 +49,9 @@ public class ReaderServiceTests
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||
var mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
}
|
||||
|
||||
#region Setup
|
||||
@ -147,10 +149,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
Assert.Equal(0, await readerService.CapPageToChapter(1, -1));
|
||||
Assert.Equal(1, await readerService.CapPageToChapter(1, 10));
|
||||
Assert.Equal(0, await _readerService.CapPageToChapter(1, -1));
|
||||
Assert.Equal(1, await _readerService.CapPageToChapter(1, 10));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -186,9 +187,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||
|
||||
var successful = await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
ChapterId = 1,
|
||||
PageNum = 1,
|
||||
@ -230,9 +231,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||
|
||||
var successful = await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
ChapterId = 1,
|
||||
PageNum = 1,
|
||||
@ -244,7 +245,7 @@ public class ReaderServiceTests
|
||||
Assert.True(successful);
|
||||
Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1));
|
||||
|
||||
Assert.True(await readerService.SaveReadingProgress(new ProgressDto()
|
||||
Assert.True(await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
ChapterId = 1,
|
||||
PageNum = 1,
|
||||
@ -294,10 +295,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1);
|
||||
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
@ -338,15 +339,15 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
|
||||
await readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
|
||||
@ -427,9 +428,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
@ -475,10 +476,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
}
|
||||
@ -524,10 +525,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
}
|
||||
@ -566,10 +567,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
@ -614,9 +615,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("0", actualChapter.Range);
|
||||
@ -658,10 +659,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
@ -693,10 +694,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
@ -735,10 +736,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
@ -777,10 +778,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("A.cbz", actualChapter.Range);
|
||||
@ -815,10 +816,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("A.cbz", actualChapter.Range);
|
||||
@ -857,10 +858,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 3, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
@ -898,10 +899,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("B.cbz", actualChapter.Range);
|
||||
@ -952,9 +953,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
}
|
||||
@ -998,9 +999,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.Equal("22", actualChapter.Range);
|
||||
}
|
||||
@ -1044,10 +1045,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// prevChapter should be id from ch.21 from volume 2001
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 4, 7, 1);
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 4, 7, 1);
|
||||
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
@ -1091,10 +1092,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
@ -1133,10 +1134,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
Assert.Equal(2, prevChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.Equal("2", actualChapter.Range);
|
||||
@ -1170,10 +1171,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
Assert.Equal(-1, prevChapter);
|
||||
}
|
||||
|
||||
@ -1204,10 +1205,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
Assert.Equal(-1, prevChapter);
|
||||
}
|
||||
|
||||
@ -1244,10 +1245,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
Assert.Equal(-1, prevChapter);
|
||||
}
|
||||
|
||||
@ -1293,14 +1294,14 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,5, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1);
|
||||
var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter);
|
||||
Assert.Equal(1, float.Parse(chapterInfoDto.ChapterNumber));
|
||||
|
||||
// This is first chapter of first volume
|
||||
prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,4, 1);
|
||||
prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,4, 1);
|
||||
Assert.Equal(-1, prevChapter);
|
||||
}
|
||||
|
||||
@ -1332,10 +1333,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
Assert.Equal(-1, prevChapter);
|
||||
}
|
||||
|
||||
@ -1374,10 +1375,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
|
||||
Assert.NotEqual(-1, prevChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.Equal("A.cbz", actualChapter.Range);
|
||||
@ -1418,10 +1419,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
Assert.NotEqual(-1, prevChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.Equal("22", actualChapter.Range);
|
||||
@ -1472,9 +1473,9 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("1", nextChapter.Range);
|
||||
}
|
||||
@ -1511,15 +1512,15 @@ public class ReaderServiceTests
|
||||
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 2,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("1", nextChapter.Range);
|
||||
}
|
||||
@ -1560,24 +1561,24 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 2,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 3,
|
||||
@ -1587,7 +1588,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("22", nextChapter.Range);
|
||||
|
||||
@ -1638,10 +1639,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume and 1st chapter of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 6, // Chapter 0 volume 1 id
|
||||
@ -1650,7 +1651,7 @@ public class ReaderServiceTests
|
||||
}, 1);
|
||||
|
||||
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 7, // Chapter 21 volume 2 id
|
||||
@ -1660,7 +1661,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("22", nextChapter.Range);
|
||||
|
||||
@ -1702,24 +1703,24 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 2,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 3,
|
||||
@ -1729,7 +1730,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("31", nextChapter.Range);
|
||||
}
|
||||
@ -1769,8 +1770,8 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("1", nextChapter.Range);
|
||||
}
|
||||
@ -1811,21 +1812,21 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Mark everything but chapter 101 as read
|
||||
await readerService.MarkSeriesAsRead(user, 1);
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Unmark last chapter as read
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1)).Chapters.ElementAt(1).Id,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1)).Chapters.ElementAt(2).Id,
|
||||
@ -1834,7 +1835,7 @@ public class ReaderServiceTests
|
||||
}, 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("101", nextChapter.Range);
|
||||
}
|
||||
@ -1869,24 +1870,24 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 2,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 3,
|
||||
@ -1896,7 +1897,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("1", nextChapter.Range);
|
||||
}
|
||||
@ -1933,14 +1934,14 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user, 1);
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("11", nextChapter.Range);
|
||||
}
|
||||
@ -1973,24 +1974,24 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 2,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 3,
|
||||
@ -2000,7 +2001,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("Some Special Title", nextChapter.Range);
|
||||
}
|
||||
@ -2041,9 +2042,9 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user, 1);
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Add 2 new unread series to the Series
|
||||
@ -2053,7 +2054,7 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// This tests that if you add a series later to a volume and a loose leaf chapter, we continue from that volume, rather than loose leaf
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
Assert.Equal("14.9", nextChapter.Range);
|
||||
}
|
||||
|
||||
@ -2104,18 +2105,18 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersAsRead(user, 1,
|
||||
await _readerService.MarkChaptersAsRead(user, 1,
|
||||
new List<Chapter>()
|
||||
{
|
||||
readChapter1, readChapter2
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal(4, nextChapter.VolumeId);
|
||||
}
|
||||
@ -2152,10 +2153,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
|
||||
await _readerService.MarkChaptersUntilAsRead(user, 1, 5);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Validate correct chapters have read status
|
||||
@ -2194,10 +2195,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 2.5f);
|
||||
await _readerService.MarkChaptersUntilAsRead(user, 1, 2.5f);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Validate correct chapters have read status
|
||||
@ -2236,10 +2237,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 2);
|
||||
await _readerService.MarkChaptersUntilAsRead(user, 1, 2);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Validate correct chapters have read status
|
||||
@ -2289,12 +2290,12 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
const int markReadUntilNumber = 47;
|
||||
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, markReadUntilNumber);
|
||||
await _readerService.MarkChaptersUntilAsRead(user, 1, markReadUntilNumber);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(1, 1);
|
||||
@ -2347,9 +2348,9 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
await readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||
|
||||
await _readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
Assert.Equal(4, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
@ -2386,15 +2387,15 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
|
||||
await readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||
await _readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
|
||||
@ -2476,10 +2477,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkVolumesUntilAsRead(user, 1, 2002);
|
||||
await _readerService.MarkVolumesUntilAsRead(user, 1, 2002);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Validate loose leaf chapters don't get marked as read
|
||||
@ -2534,10 +2535,10 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkVolumesUntilAsRead(user, 1, 2002);
|
||||
await _readerService.MarkVolumesUntilAsRead(user, 1, 2002);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Validate loose leaf chapters don't get marked as read
|
||||
@ -2577,9 +2578,9 @@ public class ReaderServiceTests
|
||||
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,6", "8,8", "9,9"})]
|
||||
public void GetPairs_ShouldReturnPairsForNoWideImages(string caseName, IList<bool> wides, IList<string> expectedPairs)
|
||||
{
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var files = wides.Select((b, i) => new FileDimensionDto() {PageNumber = i, Height = 1, Width = 1, FileName = string.Empty, IsWide = b}).ToList();
|
||||
var pairs = readerService.GetPairs(files);
|
||||
var pairs = _readerService.GetPairs(files);
|
||||
var expectedDict = new Dictionary<int, int>();
|
||||
foreach (var pair in expectedPairs)
|
||||
{
|
||||
|
@ -500,7 +500,8 @@ public class ReadingListServiceTests
|
||||
Assert.Equal(3, readingList.Items.Count);
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>());
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
// Mark 2 as fully read
|
||||
await readerService.MarkChaptersAsRead(user, 1,
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List<int>() {2})).ToList());
|
||||
|
@ -27,6 +27,8 @@ public class TachiyomiServiceTests
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly DataContext _context;
|
||||
private readonly ReaderService _readerService;
|
||||
private readonly TachiyomiService _tachiyomiService;
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
@ -44,6 +46,10 @@ public class TachiyomiServiceTests
|
||||
_mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, _mapper, null);
|
||||
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), _readerService);
|
||||
|
||||
}
|
||||
|
||||
@ -151,10 +157,7 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Null(latestChapter);
|
||||
}
|
||||
@ -201,16 +204,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user,1);
|
||||
await _readerService.MarkSeriesAsRead(user,1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("96", latestChapter.Number);
|
||||
}
|
||||
@ -257,16 +258,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("21", latestChapter.Number);
|
||||
}
|
||||
@ -312,17 +311,15 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.0001", latestChapter.Number);
|
||||
}
|
||||
|
||||
@ -362,17 +359,15 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await readerService.MarkSeriesAsRead(user, 1);
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.0003", latestChapter.Number);
|
||||
}
|
||||
|
||||
@ -417,17 +412,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,2002/10_000F);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,2002/10_000F);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.2002", latestChapter.Number);
|
||||
}
|
||||
|
||||
@ -478,10 +470,7 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Null(latestChapter);
|
||||
}
|
||||
@ -527,16 +516,13 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user,1);
|
||||
await _readerService.MarkSeriesAsRead(user,1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("96", latestChapter.Number);
|
||||
}
|
||||
@ -583,16 +569,13 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("21", latestChapter.Number);
|
||||
}
|
||||
@ -637,17 +620,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.0001", latestChapter.Number);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
@ -26,7 +28,8 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
public WordCountAnalysisTests() : base()
|
||||
{
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>());
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
}
|
||||
|
||||
protected override async Task ResetDb()
|
||||
|
@ -67,15 +67,15 @@
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.3" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="NetVips" Version="2.2.0" />
|
||||
@ -92,14 +92,14 @@
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.0" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.53.0.62665">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.54.0.64047">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.4" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||
</ItemGroup>
|
||||
|
@ -13,6 +13,7 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Middleware.RateLimit;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
@ -22,6 +23,7 @@ using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -769,6 +771,7 @@ public class AccountController : BaseApiController
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("forgot-password")]
|
||||
[EnableRateLimiting("Authentication")]
|
||||
public async Task<ActionResult<string>> ForgotPassword([FromQuery] string email)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
|
||||
@ -847,6 +850,7 @@ public class AccountController : BaseApiController
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("resend-confirmation-email")]
|
||||
[EnableRateLimiting("Authentication")]
|
||||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
@ -904,8 +904,11 @@ public class OpdsController : BaseApiController
|
||||
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg",
|
||||
$"{Prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
|
||||
link.TotalPages = mangaFile.Pages;
|
||||
link.LastRead = progress.PageNum;
|
||||
link.LastReadDate = progress.LastModifiedUtc;
|
||||
if (progress != null)
|
||||
{
|
||||
link.LastRead = progress.PageNum;
|
||||
link.LastReadDate = progress.LastModifiedUtc;
|
||||
}
|
||||
link.IsPageStream = true;
|
||||
return link;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -34,12 +34,14 @@ public class ReaderController : BaseApiController
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||
IReaderService readerService, IBookmarkService bookmarkService,
|
||||
IAccountService accountService, IEventHub eventHub)
|
||||
IAccountService accountService, IEventHub eventHub, IImageService imageService, IDirectoryService directoryService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
@ -48,6 +50,8 @@ public class ReaderController : BaseApiController
|
||||
_bookmarkService = bookmarkService;
|
||||
_accountService = accountService;
|
||||
_eventHub = eventHub;
|
||||
_imageService = imageService;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -114,6 +118,20 @@ public class ReaderController : BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("thumbnail")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetThumbnail(int chapterId, int pageNum)
|
||||
{
|
||||
var chapter = await _cacheService.Ensure(chapterId, true);
|
||||
if (chapter == null) return BadRequest("There was an issue extracting images from chapter");
|
||||
var images = _cacheService.GetCachedPages(chapterId);
|
||||
|
||||
var path = await _readerService.GetThumbnail(chapter, pageNum, images);
|
||||
var format = Path.GetExtension(path).Replace(".", string.Empty); // TODO: Make this an extension
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an image for a given bookmark series. Side effect: This will cache the bookmark images for reading.
|
||||
/// </summary>
|
||||
@ -172,13 +190,14 @@ public class ReaderController : BaseApiController
|
||||
/// <summary>
|
||||
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||
/// </summary>
|
||||
/// <remarks>This is generally the first call when attempting to read to allow pre-generation of assets needed for reading</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
|
||||
/// <param name="includeDimensions">Include file dimensions. Only useful for image based reading</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-info")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})]
|
||||
public async Task<ActionResult<ChapterInfoDto?>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
|
||||
{
|
||||
if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
|
@ -47,6 +47,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
public DbSet<FolderPath> FolderPath { get; set; } = null!;
|
||||
public DbSet<Device> Device { get; set; } = null!;
|
||||
public DbSet<ServerStatistics> ServerStatistics { get; set; } = null!;
|
||||
public DbSet<SecurityEvent> SecurityEvent { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
1901
API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs
generated
Normal file
1901
API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
API/Data/Migrations/20230316123908_SecurityEvent.cs
Normal file
40
API/Data/Migrations/20230316123908_SecurityEvent.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SecurityEvent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SecurityEvent",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
IpAddress = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RequestMethod = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RequestPath = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserAgent = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SecurityEvent", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SecurityEvent");
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
@ -944,6 +944,35 @@ namespace API.Data.Migrations
|
||||
b.ToTable("ReadingListItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SecurityEvent", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestMethod")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SecurityEvent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -47,6 +47,7 @@ public interface IReadingListRepository
|
||||
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
|
||||
Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId);
|
||||
Task<int> RemoveReadingListsWithoutSeries();
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
@ -132,6 +133,18 @@ public class ReadingListRepository : IReadingListRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> RemoveReadingListsWithoutSeries()
|
||||
{
|
||||
var listsToDelete = await _context.ReadingList
|
||||
.Include(c => c.Items)
|
||||
.Where(c => c.Items.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
_context.RemoveRange(listsToDelete);
|
||||
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
|
27
API/Data/Repositories/SecurityEventRepository.cs
Normal file
27
API/Data/Repositories/SecurityEventRepository.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
public interface ISecurityEventRepository
|
||||
{
|
||||
void Add(SecurityEvent securityEvent);
|
||||
}
|
||||
|
||||
public class SecurityEventRepository : ISecurityEventRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public SecurityEventRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Add(SecurityEvent securityEvent)
|
||||
{
|
||||
_context.SecurityEvent.Add(securityEvent);
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ public interface IUnitOfWork
|
||||
ISiteThemeRepository SiteThemeRepository { get; }
|
||||
IMangaFileRepository MangaFileRepository { get; }
|
||||
IDeviceRepository DeviceRepository { get; }
|
||||
ISecurityEventRepository SecurityEventRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
@ -62,6 +63,7 @@ public class UnitOfWork : IUnitOfWork
|
||||
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
|
||||
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context);
|
||||
public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper);
|
||||
public ISecurityEventRepository SecurityEventRepository => new SecurityEventRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
14
API/Entities/SecurityEvent.cs
Normal file
14
API/Entities/SecurityEvent.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
public class SecurityEvent
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string IpAddress { get; set; }
|
||||
public string RequestMethod { get; set; }
|
||||
public string RequestPath { get; set; }
|
||||
public string UserAgent { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
}
|
@ -7,6 +7,7 @@ using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
|
||||
namespace API.Helpers;
|
||||
|
||||
@ -156,7 +157,7 @@ public static class PersonHelper
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(DbFactory.Person(tag.Name, role));
|
||||
handleAdd(new PersonBuilder(tag.Name, role).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Filters;
|
||||
using Serilog.Formatting.Display;
|
||||
|
||||
namespace API.Logging;
|
||||
@ -12,6 +13,7 @@ namespace API.Logging;
|
||||
public static class LogLevelOptions
|
||||
{
|
||||
public const string LogFile = "config/logs/kavita.log";
|
||||
public const string SecurityLogFile = "config/logs/security.log";
|
||||
public const bool LogRollingEnabled = true;
|
||||
/// <summary>
|
||||
/// Controls the Logging Level of the Application
|
||||
|
36
API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs
Normal file
36
API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.RateLimiting;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
namespace API.Middleware.RateLimit;
|
||||
|
||||
public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy<string>
|
||||
{
|
||||
public RateLimitPartition<string> GetPartition(HttpContext httpContext)
|
||||
{
|
||||
return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(),
|
||||
partition => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
AutoReplenishment = true,
|
||||
PermitLimit = 1,
|
||||
Window = TimeSpan.FromMinutes(10),
|
||||
});
|
||||
}
|
||||
|
||||
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } =
|
||||
(context, _) =>
|
||||
{
|
||||
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
||||
{
|
||||
context.HttpContext.Response.Headers.RetryAfter =
|
||||
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||
return new ValueTask();
|
||||
};
|
||||
}
|
62
API/Middleware/SecurityEventMiddleware.cs
Normal file
62
API/Middleware/SecurityEventMiddleware.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Logging;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace API.Middleware;
|
||||
|
||||
public class SecurityEventMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SecurityEventMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
|
||||
_logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(Path.Join(Directory.GetCurrentDirectory(), "config/logs/", "security.log"), rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
|
||||
var requestMethod = context.Request.Method;
|
||||
var requestPath = context.Request.Path;
|
||||
var userAgent = context.Request.Headers["User-Agent"];
|
||||
|
||||
var securityEvent = new SecurityEvent
|
||||
{
|
||||
IpAddress = ipAddress,
|
||||
RequestMethod = requestMethod,
|
||||
RequestPath = requestPath,
|
||||
UserAgent = userAgent,
|
||||
CreatedAt = DateTime.Now,
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
using (var scope = context.RequestServices.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
dbContext.Add(securityEvent);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_logger.Debug("Request Processed: {@SecurityEvent}", securityEvent);
|
||||
}
|
||||
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
@ -76,7 +76,8 @@ public class BookmarkService : IBookmarkService
|
||||
/// <summary>
|
||||
/// This is a job that runs after a bookmark is saved
|
||||
/// </summary>
|
||||
private async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
/// <remarks>This must be public</remarks>
|
||||
public async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
@ -32,6 +32,7 @@ public interface ICacheService
|
||||
void CleanupChapters(IEnumerable<int> chapterIds);
|
||||
void CleanupBookmarks(IEnumerable<int> seriesIds);
|
||||
string GetCachedPagePath(int chapterId, int page);
|
||||
IEnumerable<string> GetCachedPages(int chapterId);
|
||||
IEnumerable<FileDimensionDto> GetCachedFileDimensions(int chapterId);
|
||||
string GetCachedBookmarkPagePath(int seriesId, int page);
|
||||
string GetCachedFile(Chapter chapter);
|
||||
@ -58,6 +59,13 @@ public class CacheService : ICacheService
|
||||
_bookmarkService = bookmarkService;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetCachedPages(int chapterId)
|
||||
{
|
||||
var path = GetCachePath(chapterId);
|
||||
return _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions)
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension);
|
||||
}
|
||||
|
||||
public IEnumerable<FileDimensionDto> GetCachedFileDimensions(int chapterId)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
@ -276,15 +284,7 @@ public class CacheService : ICacheService
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||
.ToArray();
|
||||
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (page > files.Length) page = files.Length;
|
||||
|
||||
// Since array is 0 based, we need to keep that in account (only affects last image)
|
||||
return page == files.Length ? files.ElementAt(page - 1) : files.ElementAt(page);
|
||||
return GetPageFromFiles(files, page);
|
||||
}
|
||||
|
||||
public async Task<int> CacheBookmarkForSeries(int userId, int seriesId)
|
||||
@ -310,4 +310,33 @@ public class CacheService : ICacheService
|
||||
|
||||
_directoryService.ClearAndDeleteDirectory(destDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns either the file or an empty string
|
||||
/// </summary>
|
||||
/// <param name="files"></param>
|
||||
/// <param name="pageNum"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetPageFromFiles(string[] files, int pageNum)
|
||||
{
|
||||
files = files
|
||||
.AsEnumerable()
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||
.ToArray();
|
||||
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (pageNum < 0)
|
||||
{
|
||||
pageNum = 0;
|
||||
}
|
||||
|
||||
// Since array is 0 based, we need to keep that in account (only affects last image)
|
||||
return pageNum >= files.Length ? files.ElementAt(Math.Min(pageNum - 1, files.Length - 1)) : files.ElementAt(pageNum);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -22,9 +22,25 @@ public interface IImageService
|
||||
/// <param name="thumbnailWidth">Width of thumbnail</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320);
|
||||
|
||||
/// <summary>
|
||||
/// Writes out a thumbnail by stream input
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="outputDirectory"></param>
|
||||
/// <param name="saveAsWebP"></param>
|
||||
/// <returns></returns>
|
||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
/// <summary>
|
||||
/// Writes out a thumbnail by file path input
|
||||
/// </summary>
|
||||
/// <param name="sourceFile"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="outputDirectory"></param>
|
||||
/// <param name="saveAsWebP"></param>
|
||||
/// <returns></returns>
|
||||
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
/// <summary>
|
||||
/// Converts the passed image to webP and outputs it in the same directory
|
||||
/// </summary>
|
||||
/// <param name="filePath">Full path to the image to convert</param>
|
||||
@ -115,6 +131,19 @@ public class ImageService : IImageService
|
||||
return filename;
|
||||
}
|
||||
|
||||
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
{
|
||||
using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
{
|
||||
_directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
|
||||
} catch (Exception) {/* Swallow exception */}
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
|
||||
return filename;
|
||||
}
|
||||
|
||||
public Task<string> ConvertToWebP(string filePath, string outputPath)
|
||||
{
|
||||
var file = _directoryService.FileSystem.FileInfo.New(filePath);
|
||||
@ -218,6 +247,16 @@ public class ImageService : IImageService
|
||||
return $"readinglist{readingListId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name format for a thumbnail (temp thumbnail)
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetThumbnailFormat(int chapterId)
|
||||
{
|
||||
return $"thumbnail{chapterId}";
|
||||
}
|
||||
|
||||
|
||||
public static string CreateMergedImage(List<string> coverImages, string dest)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -33,6 +34,7 @@ public interface IReaderService
|
||||
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
|
||||
HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub);
|
||||
IDictionary<int, int> GetPairs(IEnumerable<FileDimensionDto> dimensions);
|
||||
Task<string> GetThumbnail(Chapter chapter, int pageNum, IEnumerable<string> cachedImages);
|
||||
}
|
||||
|
||||
public class ReaderService : IReaderService
|
||||
@ -40,6 +42,8 @@ public class ReaderService : IReaderService
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReaderService> _logger;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default;
|
||||
|
||||
@ -48,14 +52,17 @@ public class ReaderService : IReaderService
|
||||
public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F;
|
||||
private const float MinPagesPerMinute = 3.33F;
|
||||
private const float MaxPagesPerMinute = 2.75F;
|
||||
public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F;
|
||||
public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F; //3.04
|
||||
|
||||
|
||||
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub)
|
||||
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub, IImageService imageService,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_eventHub = eventHub;
|
||||
_imageService = imageService;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId)
|
||||
@ -644,6 +651,44 @@ public class ReaderService : IReaderService
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="chapter"></param>
|
||||
/// <param name="pageNum"></param>
|
||||
/// <param name="cachedImages"></param>
|
||||
/// <returns>Full path of thumbnail</returns>
|
||||
public async Task<string> GetThumbnail(Chapter chapter, int pageNum, IEnumerable<string> cachedImages)
|
||||
{
|
||||
var outputDirectory =
|
||||
_directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, ImageService.GetThumbnailFormat(chapter.Id));
|
||||
try
|
||||
{
|
||||
var saveAsWebp =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
{
|
||||
var outputtedThumbnails = cachedImages
|
||||
.Select((img, idx) =>
|
||||
_directoryService.FileSystem.Path.Join(outputDirectory,
|
||||
_imageService.WriteCoverThumbnail(img, $"{idx}", outputDirectory, saveAsWebp)))
|
||||
.ToArray();
|
||||
return CacheService.GetPageFromFiles(outputtedThumbnails, pageNum);
|
||||
}
|
||||
|
||||
var files = _directoryService.GetFilesWithExtension(outputDirectory,
|
||||
Tasks.Scanner.Parser.Parser.ImageFileExtensions);
|
||||
return CacheService.GetPageFromFiles(files, pageNum);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an error when trying to get thumbnail for Chapter {ChapterId}, Page {PageNum}", chapter.Id, pageNum);
|
||||
_directoryService.ClearAndDeleteDirectory(outputDirectory);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a Chapter name based on the library it's in
|
||||
/// </summary>
|
||||
@ -668,4 +713,6 @@ public class ReaderService : IReaderService
|
||||
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -509,7 +509,7 @@ public class ReadingListService : IReadingListService
|
||||
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
|
||||
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
|
||||
{
|
||||
readingList = DbFactory.ReadingList(cblReading.Name, string.Empty, false);
|
||||
readingList = DbFactory.ReadingList(cblReading.Name, cblReading.Summary, false);
|
||||
user.ReadingLists.Add(readingList);
|
||||
}
|
||||
else
|
||||
|
@ -103,7 +103,6 @@ public class StatisticService : IStatisticService
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
var averageReadingTimePerWeek = _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
.Join(_context.Chapter, p => p.ChapterId, c => c.Id,
|
||||
@ -112,7 +111,7 @@ public class StatisticService : IStatisticService
|
||||
AverageReadingHours = Math.Min((float) p.PagesRead / (float) c.Pages, 1.0) * ((float) c.AvgHoursToRead)
|
||||
})
|
||||
.Select(x => x.AverageReadingHours)
|
||||
.Average() / 7.0;
|
||||
.Average() * 7.0;
|
||||
|
||||
return new UserReadStatistics()
|
||||
{
|
||||
|
@ -58,6 +58,17 @@ public class CleanupService : ICleanupService
|
||||
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||
public async Task Cleanup()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true) ||
|
||||
TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true))
|
||||
{
|
||||
_logger.LogInformation("Cleanup put on hold as a conversion to WebP in progress");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a conversion to WebP in progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Starting Cleanup");
|
||||
await SendProgress(0F, "Starting cleanup");
|
||||
_logger.LogInformation("Cleaning temp directory");
|
||||
@ -90,6 +101,7 @@ public class CleanupService : ICleanupService
|
||||
await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated();
|
||||
await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated();
|
||||
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
await _unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries();
|
||||
}
|
||||
|
||||
private async Task SendProgress(float progress, string subtitle)
|
||||
|
@ -225,6 +225,11 @@ public static class Parser
|
||||
new Regex(
|
||||
@"(?<Series>.+?):? (\b|_|-)(vol)\.?(\s|-|_)?\d+",
|
||||
MatchOptions, RegexTimeout),
|
||||
// [xPearse] Kyochuu Rettou Chapter 001 Volume 1 [English] [Manga] [Volume Scans]
|
||||
new Regex(
|
||||
@"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)",
|
||||
MatchOptions,
|
||||
RegexTimeout),
|
||||
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
|
||||
new Regex(
|
||||
@"(?<Series>.+?):? (\b|_|-)(vol)(ume)",
|
||||
|
@ -51,6 +51,9 @@ public class ProcessSeries : IProcessSeries
|
||||
private IList<Person> _people;
|
||||
private Dictionary<string, Tag> _tags;
|
||||
private Dictionary<string, CollectionTag> _collectionTags;
|
||||
private readonly object _peopleLock;
|
||||
private readonly object _genreLock;
|
||||
private readonly object _tagLock;
|
||||
|
||||
public ProcessSeries(IUnitOfWork unitOfWork, ILogger<ProcessSeries> logger, IEventHub eventHub,
|
||||
IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService,
|
||||
@ -838,7 +841,7 @@ public class ProcessSeries : IProcessSeries
|
||||
if (person == null)
|
||||
{
|
||||
person = DbFactory.Person(name, role);
|
||||
lock (_people)
|
||||
lock (_peopleLock)
|
||||
{
|
||||
_people.Add(person);
|
||||
}
|
||||
@ -865,7 +868,7 @@ public class ProcessSeries : IProcessSeries
|
||||
if (newTag)
|
||||
{
|
||||
genre = DbFactory.Genre(name);
|
||||
lock (_genres)
|
||||
lock (_genreLock)
|
||||
{
|
||||
_genres.Add(normalizedName, genre);
|
||||
_unitOfWork.GenreRepository.Attach(genre);
|
||||
@ -894,7 +897,7 @@ public class ProcessSeries : IProcessSeries
|
||||
if (tag == null)
|
||||
{
|
||||
tag = DbFactory.Tag(name);
|
||||
lock (_tags)
|
||||
lock (_tagLock)
|
||||
{
|
||||
_tags.Add(normalizedName, tag);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Threading.RateLimiting;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
@ -14,6 +15,7 @@ using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Logging;
|
||||
using API.Middleware;
|
||||
using API.Middleware.RateLimit;
|
||||
using API.Services;
|
||||
using API.Services.HostedServices;
|
||||
using API.Services.Tasks;
|
||||
@ -179,6 +181,19 @@ public class Startup
|
||||
|
||||
services.AddResponseCaching();
|
||||
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
options.AddPolicy("Authentication", httpContext =>
|
||||
new AuthenticationRateLimiterPolicy().GetPartition(httpContext));
|
||||
// RateLimitPartition.GetFixedWindowLimiter(httpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
// partition => new FixedWindowRateLimiterOptions
|
||||
// {
|
||||
// AutoReplenishment = true,
|
||||
// PermitLimit = 1,
|
||||
// Window = TimeSpan.FromMinutes(1),
|
||||
// }));
|
||||
});
|
||||
|
||||
services.AddHangfire(configuration => configuration
|
||||
.UseSimpleAssemblyNameTypeSerializer()
|
||||
.UseRecommendedSerializerSettings()
|
||||
@ -259,6 +274,7 @@ public class Startup
|
||||
|
||||
|
||||
app.UseMiddleware<ExceptionMiddleware>();
|
||||
app.UseMiddleware<SecurityEventMiddleware>();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
@ -278,10 +294,16 @@ public class Startup
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
var basePath = Configuration.BaseUrl;
|
||||
app.UseRateLimiter();
|
||||
|
||||
var basePath = Configuration.BaseUrl;
|
||||
app.UsePathBase(basePath);
|
||||
UpdateBaseUrlInIndex(basePath);
|
||||
if (!env.IsDevelopment())
|
||||
{
|
||||
// We don't update the index.html in local as we don't serve from there
|
||||
UpdateBaseUrlInIndex(basePath);
|
||||
}
|
||||
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
@ -292,7 +314,17 @@ public class Startup
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials() // For SignalR token query param
|
||||
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200", $"http://{GetLocalIpAddress()}:5000")
|
||||
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200", $"http://{GetLocalIpAddress()}:5000", "https://kavita.majora2007.duckdns.org")
|
||||
.WithExposedHeaders("Content-Disposition", "Pagination"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Allow CORS for Kavita's url
|
||||
app.UseCors(policy => policy
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials() // For SignalR token query param
|
||||
.WithOrigins("https://kavita.majora2007.duckdns.org")
|
||||
.WithExposedHeaders("Content-Disposition", "Pagination"));
|
||||
}
|
||||
|
||||
@ -311,6 +343,7 @@ public class Startup
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + TimeSpan.FromHours(24);
|
||||
ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex,nofollow";
|
||||
}
|
||||
});
|
||||
|
||||
@ -326,7 +359,7 @@ public class Startup
|
||||
new[] { "Accept-Encoding" };
|
||||
|
||||
// Don't let the site be iframed outside the same origin (clickjacking)
|
||||
context.Response.Headers.XFrameOptions = "SAMEORIGIN";
|
||||
context.Response.Headers.XFrameOptions = Configuration.XFrameOptions;
|
||||
|
||||
// Setup CSP to ensure we load assets only from these origins
|
||||
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';");
|
||||
@ -359,19 +392,26 @@ public class Startup
|
||||
});
|
||||
|
||||
var _logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
|
||||
_logger.LogInformation("Starting with base url as {baseUrl}", basePath);
|
||||
_logger.LogInformation("Starting with base url as {BaseUrl}", basePath);
|
||||
}
|
||||
|
||||
private static void UpdateBaseUrlInIndex(string baseUrl)
|
||||
{
|
||||
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker) return;
|
||||
var htmlDoc = new HtmlDocument();
|
||||
var indexHtmlPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html");
|
||||
htmlDoc.Load(indexHtmlPath);
|
||||
try
|
||||
{
|
||||
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker) return;
|
||||
var htmlDoc = new HtmlDocument();
|
||||
var indexHtmlPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html");
|
||||
htmlDoc.Load(indexHtmlPath);
|
||||
|
||||
var baseNode = htmlDoc.DocumentNode.SelectSingleNode("/html/head/base");
|
||||
baseNode.SetAttributeValue("href", baseUrl);
|
||||
htmlDoc.Save(indexHtmlPath);
|
||||
var baseNode = htmlDoc.DocumentNode.SelectSingleNode("/html/head/base");
|
||||
baseNode.SetAttributeValue("href", baseUrl);
|
||||
htmlDoc.Save(indexHtmlPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "There was an error setting base url");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnShutdown()
|
||||
|
@ -10,6 +10,7 @@ public static class Configuration
|
||||
{
|
||||
public const string DefaultIpAddresses = "0.0.0.0,::";
|
||||
public const string DefaultBaseUrl = "/";
|
||||
public const string DefaultXFrameOptions = "SAMEORIGIN";
|
||||
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
|
||||
|
||||
public static int Port
|
||||
@ -36,6 +37,8 @@ public static class Configuration
|
||||
set => SetBaseUrl(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
public static string XFrameOptions => GetXFrameOptions(GetAppSettingFilename());
|
||||
|
||||
private static string GetAppSettingFilename()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(AppSettingsFilename))
|
||||
@ -224,7 +227,7 @@ public static class Configuration
|
||||
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||
{
|
||||
var baseUrl = tokenElement.GetString();
|
||||
if (!String.IsNullOrEmpty(baseUrl))
|
||||
if (!string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
baseUrl = !baseUrl.StartsWith("/")
|
||||
? $"/{baseUrl}"
|
||||
@ -277,6 +280,35 @@ public static class Configuration
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region XFrameOrigins
|
||||
private static string GetXFrameOptions(string filePath)
|
||||
{
|
||||
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||
{
|
||||
return DefaultBaseUrl;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
|
||||
const string key = "XFrameOrigins";
|
||||
|
||||
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||
{
|
||||
var origins = tokenElement.GetString();
|
||||
return !string.IsNullOrEmpty(origins) ? origins : DefaultBaseUrl;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Error reading app settings: " + ex.Message);
|
||||
}
|
||||
|
||||
return DefaultXFrameOptions;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private class AppSettings
|
||||
{
|
||||
public string TokenKey { get; set; }
|
||||
|
@ -14,7 +14,7 @@
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.53.0.62665">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.54.0.64047">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
39
UI/Web/package-lock.json
generated
39
UI/Web/package-lock.json
generated
@ -34,7 +34,6 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-swipe": "^2.0.1",
|
||||
"ngx-color-picker": "^13.0.0",
|
||||
"ngx-extended-pdf-viewer": "^15.2.2",
|
||||
"ngx-file-drop": "^14.0.2",
|
||||
@ -5620,14 +5619,6 @@
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ag-swipe-core": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ag-swipe-core/-/ag-swipe-core-1.0.2.tgz",
|
||||
"integrity": "sha512-NNONbrEbsmu6wsl7E07eGYVZw8Wx7hOok2TlhQLU/50EUhmI3Vpg8EDz0rWhV/HrfUAoEd4LxBvLAeT9DswQDw==",
|
||||
"dependencies": {
|
||||
"rxjs": "^7.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -13906,19 +13897,6 @@
|
||||
"rxjs": ">=6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ng-swipe": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ng-swipe/-/ng-swipe-2.0.1.tgz",
|
||||
"integrity": "sha512-y4w2d719VK1u6KUlNqhHVevzT+yR30bnTTLkFNEsVG3Gp5+oZhUnflVNWfzIw+O8GCjZqVLelwla/jOkqUclmQ==",
|
||||
"dependencies": {
|
||||
"ag-swipe-core": "^1.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^14.0.0",
|
||||
"@angular/core": "^14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-color-picker": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-13.0.0.tgz",
|
||||
@ -22337,14 +22315,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ag-swipe-core": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ag-swipe-core/-/ag-swipe-core-1.0.2.tgz",
|
||||
"integrity": "sha512-NNONbrEbsmu6wsl7E07eGYVZw8Wx7hOok2TlhQLU/50EUhmI3Vpg8EDz0rWhV/HrfUAoEd4LxBvLAeT9DswQDw==",
|
||||
"requires": {
|
||||
"rxjs": "^7.5.5"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -28587,15 +28557,6 @@
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"ng-swipe": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ng-swipe/-/ng-swipe-2.0.1.tgz",
|
||||
"integrity": "sha512-y4w2d719VK1u6KUlNqhHVevzT+yR30bnTTLkFNEsVG3Gp5+oZhUnflVNWfzIw+O8GCjZqVLelwla/jOkqUclmQ==",
|
||||
"requires": {
|
||||
"ag-swipe-core": "^1.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"ngx-color-picker": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-13.0.0.tgz",
|
||||
|
@ -40,7 +40,6 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-swipe": "^2.0.1",
|
||||
"ngx-color-picker": "^13.0.0",
|
||||
"ngx-extended-pdf-viewer": "^15.2.2",
|
||||
"ngx-file-drop": "^14.0.2",
|
||||
|
@ -101,6 +101,10 @@ export class ReaderService {
|
||||
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
||||
}
|
||||
|
||||
getThumbnailUrl(chapterId: number, page: number) {
|
||||
return this.baseUrl + 'reader/thumbnail?chapterId=' + chapterId + '&page=' + page;
|
||||
}
|
||||
|
||||
getBookmarkPageUrl(seriesId: number, apiKey: string, page: number) {
|
||||
return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between text-center row g-0">
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<div class="col-1">{{pageNum}}</div>
|
||||
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
|
||||
<div class="col-8">
|
||||
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||
</div>
|
||||
|
@ -379,10 +379,12 @@
|
||||
</div>
|
||||
<div class="row g-0 mb-2" *ngIf="metadata">
|
||||
<div class="col-md-6">
|
||||
Max Items: {{metadata.maxCount}} <i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max of Volume/Issue field in ComicInfo. Used in conjunction with total items to determine publication status." role="button" tabindex="0"></i>
|
||||
Max Items: {{metadata.maxCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max of Volume/Issue field in ComicInfo. Used in conjunction with total items to determine publication status." role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6" title="">
|
||||
Total Items: {{metadata.totalCount}} <i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Total number of issues/volumes in the series" role="button" tabindex="0"></i>
|
||||
<div class="col-md-6">
|
||||
Total Items: {{metadata.totalCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Total number of issues/volumes in the series" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6">Publication Status: {{metadata.publicationStatus | publicationStatus}}</div>
|
||||
<div class="col-md-6">Total Pages: {{series.pages}}</div>
|
||||
|
@ -278,6 +278,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.compareFnForAdd = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
return a.title === b.title;
|
||||
}
|
||||
@ -310,6 +313,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
|
||||
if (this.metadata.tags) {
|
||||
this.tagsSettings.savedData = this.metadata.tags;
|
||||
@ -331,6 +337,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.genreSettings.compareFnForAdd = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
@ -372,6 +381,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.languageSettings.compareFn = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
@ -426,6 +438,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
personSettings.compareFn = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
personSettings.compareFnForAdd = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.name, filter));
|
||||
}
|
||||
|
||||
personSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name && a.role == b.role;
|
||||
|
@ -1,6 +1,8 @@
|
||||
@use '../../../../manga-reader-common';
|
||||
|
||||
.image-container {
|
||||
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
|
||||
|
||||
#image-1 {
|
||||
&.double {
|
||||
margin: 0 0 0 auto;
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
// Overrides for reverse
|
||||
.image-container {
|
||||
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
|
||||
|
||||
&.reverse {
|
||||
overflow: unset;
|
||||
display: flex;
|
||||
|
@ -31,8 +31,8 @@ import { DoubleRendererComponent } from '../double-renderer/double-renderer.comp
|
||||
import { DoubleReverseRendererComponent } from '../double-reverse-renderer/double-reverse-renderer.component';
|
||||
import { SingleRendererComponent } from '../single-renderer/single-renderer.component';
|
||||
import { ChapterInfo } from '../../_models/chapter-info';
|
||||
import { SwipeEvent } from 'ng-swipe';
|
||||
import { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component';
|
||||
import { SwipeEvent } from 'src/app/ng-swipe/ag-swipe.core';
|
||||
|
||||
|
||||
const PREFETCH_PAGES = 10;
|
||||
@ -379,7 +379,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// This is for the pagination area
|
||||
get MaxHeight() {
|
||||
if (this.FittingOption !== FITTING_OPTION.HEIGHT) {
|
||||
return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px';
|
||||
return Math.min(this.readingArea?.nativeElement?.clientHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px';
|
||||
}
|
||||
return 'calc(var(--vh) * 100)';
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ import { DoubleRendererComponent } from './_components/double-renderer/double-re
|
||||
import { DoubleReverseRendererComponent } from './_components/double-reverse-renderer/double-reverse-renderer.component';
|
||||
import { MangaReaderComponent } from './_components/manga-reader/manga-reader.component';
|
||||
import { FittingIconPipe } from './_pipes/fitting-icon.pipe';
|
||||
import { SwipeModule } from 'ng-swipe';
|
||||
import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no-cover/double-no-cover-renderer.component';
|
||||
import { NgSwipeModule } from '../ng-swipe/ng-swipe.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -45,7 +45,7 @@ import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no
|
||||
SharedModule,
|
||||
ReaderSharedModule,
|
||||
|
||||
SwipeModule
|
||||
NgSwipeModule
|
||||
],
|
||||
exports: [
|
||||
MangaReaderComponent
|
||||
|
@ -329,6 +329,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
|
||||
|
||||
this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
|
||||
return a.title == b.title;
|
||||
|
103
UI/Web/src/app/ng-swipe/ag-swipe.core.ts
Normal file
103
UI/Web/src/app/ng-swipe/ag-swipe.core.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { fromEvent, Observable, race, Subscription } from 'rxjs';
|
||||
import { elementAt, map, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
|
||||
export interface SwipeCoordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export enum SwipeDirection {
|
||||
X = 'x',
|
||||
Y = 'y'
|
||||
}
|
||||
|
||||
export interface SwipeStartEvent {
|
||||
x: number;
|
||||
y: number;
|
||||
direction: SwipeDirection;
|
||||
}
|
||||
|
||||
export interface SwipeEvent {
|
||||
direction: SwipeDirection;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export interface SwipeSubscriptionConfig {
|
||||
domElement: HTMLElement;
|
||||
onSwipeMove?: (event: SwipeEvent) => void;
|
||||
onSwipeEnd?: (event: SwipeEvent) => void;
|
||||
}
|
||||
|
||||
|
||||
export function createSwipeSubscription({ domElement, onSwipeMove, onSwipeEnd }: SwipeSubscriptionConfig): Subscription {
|
||||
if (!(domElement instanceof HTMLElement)) {
|
||||
throw new Error('Provided domElement should be an instance of HTMLElement');
|
||||
}
|
||||
|
||||
if ((typeof onSwipeMove !== 'function') && (typeof onSwipeEnd !== 'function')) {
|
||||
throw new Error('At least one of the following swipe event handler functions should be provided: onSwipeMove and/or onSwipeEnd');
|
||||
}
|
||||
|
||||
const touchStarts$ = fromEvent<TouchEvent>(domElement, 'touchstart').pipe(map(getTouchCoordinates));
|
||||
const touchMoves$ = fromEvent<TouchEvent>(domElement, 'touchmove').pipe(map(getTouchCoordinates));
|
||||
const touchEnds$ = fromEvent<TouchEvent>(domElement, 'touchend').pipe(map(getTouchCoordinates));
|
||||
const touchCancels$ = fromEvent<TouchEvent>(domElement, 'touchcancel');
|
||||
|
||||
const touchStartsWithDirection$: Observable<SwipeStartEvent> = touchStarts$.pipe(
|
||||
switchMap((touchStartEvent: SwipeCoordinates) => touchMoves$.pipe(
|
||||
elementAt(3),
|
||||
map((touchMoveEvent: SwipeCoordinates) => ({
|
||||
x: touchStartEvent.x,
|
||||
y: touchStartEvent.y,
|
||||
direction: getTouchDirection(touchStartEvent, touchMoveEvent)
|
||||
})
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
return touchStartsWithDirection$.pipe(
|
||||
switchMap(touchStartEvent => touchMoves$.pipe(
|
||||
map(touchMoveEvent => getTouchDistance(touchStartEvent, touchMoveEvent)),
|
||||
tap((coordinates: SwipeCoordinates) => {
|
||||
if (typeof onSwipeMove !== 'function') { return; }
|
||||
onSwipeMove(getSwipeEvent(touchStartEvent, coordinates));
|
||||
}),
|
||||
takeUntil(race(
|
||||
touchEnds$.pipe(
|
||||
map(touchEndEvent => getTouchDistance(touchStartEvent, touchEndEvent)),
|
||||
tap((coordinates: SwipeCoordinates) => {
|
||||
if (typeof onSwipeEnd !== 'function') { return; }
|
||||
onSwipeEnd(getSwipeEvent(touchStartEvent, coordinates));
|
||||
})
|
||||
),
|
||||
touchCancels$
|
||||
))
|
||||
))
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
function getTouchCoordinates(touchEvent: TouchEvent): SwipeCoordinates {
|
||||
return {
|
||||
x: touchEvent.changedTouches[0].clientX,
|
||||
y: touchEvent.changedTouches[0].clientY
|
||||
};
|
||||
}
|
||||
|
||||
function getTouchDistance(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeCoordinates {
|
||||
return {
|
||||
x: moveCoordinates.x - startCoordinates.x,
|
||||
y: moveCoordinates.y - startCoordinates.y
|
||||
};
|
||||
}
|
||||
|
||||
function getTouchDirection(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeDirection {
|
||||
const { x, y } = getTouchDistance(startCoordinates, moveCoordinates);
|
||||
return Math.abs(x) < Math.abs(y) ? SwipeDirection.Y : SwipeDirection.X;
|
||||
}
|
||||
|
||||
function getSwipeEvent(touchStartEvent: SwipeStartEvent, coordinates: SwipeCoordinates): SwipeEvent {
|
||||
return {
|
||||
direction: touchStartEvent.direction,
|
||||
distance: coordinates[touchStartEvent.direction]
|
||||
};
|
||||
}
|
32
UI/Web/src/app/ng-swipe/ng-swipe.directive.ts
Normal file
32
UI/Web/src/app/ng-swipe/ng-swipe.directive.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { createSwipeSubscription, SwipeEvent } from './ag-swipe.core';
|
||||
|
||||
@Directive({
|
||||
selector: '[ngSwipe]'
|
||||
})
|
||||
export class SwipeDirective implements OnInit, OnDestroy {
|
||||
private swipeSubscription: Subscription | undefined;
|
||||
|
||||
@Output() swipeMove: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
|
||||
@Output() swipeEnd: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
private zone: NgZone
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.swipeSubscription = createSwipeSubscription({
|
||||
domElement: this.elementRef.nativeElement,
|
||||
onSwipeMove: (swipeMoveEvent: SwipeEvent) => this.swipeMove.emit(swipeMoveEvent),
|
||||
onSwipeEnd: (swipeEndEvent: SwipeEvent) => this.swipeEnd.emit(swipeEndEvent)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.swipeSubscription?.unsubscribe?.();
|
||||
}
|
||||
}
|
18
UI/Web/src/app/ng-swipe/ng-swipe.module.ts
Normal file
18
UI/Web/src/app/ng-swipe/ng-swipe.module.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SwipeDirective } from './ng-swipe.directive';
|
||||
|
||||
// All code in this module is based on https://github.com/aGoncharuks/ag-swipe and may contain further enhancements or bugfixes.
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SwipeDirective
|
||||
],
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
exports: [
|
||||
SwipeDirective
|
||||
]
|
||||
})
|
||||
export class NgSwipeModule { }
|
@ -13,7 +13,7 @@ export class TimeDurationPipe implements PipeTransform {
|
||||
if (hours < 1) {
|
||||
return `${(hours * 60).toFixed(1)} minutes`;
|
||||
} else if (hours < 24) {
|
||||
return `${hours} hours`;
|
||||
return `${hours.toFixed(1)} hours`;
|
||||
} else if (hours < 720) {
|
||||
return `${(hours / 24).toFixed(1)} days`;
|
||||
} else if (hours < 8760) {
|
||||
|
@ -92,11 +92,17 @@ export class UtilityService {
|
||||
}
|
||||
|
||||
filter(input: string, filter: string): boolean {
|
||||
if (input === null || filter === null) return false;
|
||||
if (input === null || filter === null || input === undefined || filter === undefined) return false;
|
||||
const reg = /[_\.\-]/gi;
|
||||
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
|
||||
}
|
||||
|
||||
filterMatches(input: string, filter: string): boolean {
|
||||
if (input === null || filter === null || input === undefined || filter === undefined) return false;
|
||||
const reg = /[_\.\-]/gi;
|
||||
return input.toUpperCase().replace(reg, '') === filter.toUpperCase().replace(reg, '');
|
||||
}
|
||||
|
||||
isVolume(d: any) {
|
||||
return d != null && d.hasOwnProperty('chapters');
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Average Reading / Week" [clickable]="false" fontClasses="fas fa-eye">
|
||||
{{avgHoursPerWeekSpentReading | timeDuration}}
|
||||
{{avgHoursPerWeekSpentReading | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
|
@ -236,16 +236,16 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
this.filteredOptions = this.typeaheadForm.get('typeahead')!.valueChanges
|
||||
.pipe(
|
||||
// Adjust input box to grow
|
||||
tap(val => {
|
||||
tap((val: string) => {
|
||||
if (this.inputElem != null && this.inputElem.nativeElement != null) {
|
||||
this.renderer2.setStyle(this.inputElem.nativeElement, 'width', 15 * (val.trim().length + 1) + 'px');
|
||||
this.focusedIndex = 0;
|
||||
}
|
||||
}),
|
||||
map(val => val.trim()),
|
||||
map((val: string) => val.trim()),
|
||||
auditTime(this.settings.debounce),
|
||||
//distinctUntilChanged(), // ?!: BUG Doesn't trigger the search to run when filtered array changes
|
||||
filter(val => {
|
||||
filter((val: string) => {
|
||||
// If minimum filter characters not met, do not filter
|
||||
if (this.settings.minCharacters === 0) return true;
|
||||
|
||||
@ -256,11 +256,11 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}),
|
||||
|
||||
switchMap(val => {
|
||||
switchMap((val: string) => {
|
||||
this.isLoadingOptions = true;
|
||||
return this.settings.fetchFn(val.trim()).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
||||
}),
|
||||
tap((filteredOptions) => {
|
||||
tap((filteredOptions: any[]) => {
|
||||
this.isLoadingOptions = false;
|
||||
this.focusedIndex = 0;
|
||||
this.cdRef.markForCheck();
|
||||
@ -398,6 +398,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.toggleSelection(opt);
|
||||
console.log('Selected ', opt);
|
||||
|
||||
this.resetField();
|
||||
this.onInputFocus();
|
||||
@ -410,6 +411,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
const newItem = this.settings.addTransformFn(title);
|
||||
this.newItemAdded.emit(newItem);
|
||||
this.toggleSelection(newItem);
|
||||
console.log('Selected ', newItem);
|
||||
|
||||
this.resetField();
|
||||
this.onInputFocus();
|
||||
@ -482,14 +484,43 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
updateShowAddItem(options: any[]) {
|
||||
// ?! BUG This will still technicially allow you to add the same thing as a previously added item. (Code will just toggle it though)
|
||||
this.showAddItem = this.settings.addIfNonExisting && this.typeaheadControl.value.trim()
|
||||
&& this.typeaheadControl.value.trim().length >= Math.max(this.settings.minCharacters, 1)
|
||||
&& this.typeaheadControl.dirty
|
||||
&& (typeof this.settings.compareFn == 'function' && this.settings.compareFn(options, this.typeaheadControl.value.trim()).length === 0);
|
||||
this.showAddItem = false;
|
||||
this.cdRef.markForCheck();
|
||||
if (!this.settings.addIfNonExisting) return;
|
||||
|
||||
const inputText = this.typeaheadControl.value.trim();
|
||||
if (inputText.length < Math.max(this.settings.minCharacters, 1)) return;
|
||||
if (!this.typeaheadControl.dirty) return; // Do we need this?
|
||||
|
||||
// Check if this new option will interfere with any existing ones not shown
|
||||
|
||||
if (typeof this.settings.compareFnForAdd == 'function') {
|
||||
console.log('filtered options: ', this.optionSelection.selected());
|
||||
const willDuplicateExist = this.settings.compareFnForAdd(this.optionSelection.selected(), inputText);
|
||||
console.log('duplicate check: ', willDuplicateExist);
|
||||
if (willDuplicateExist.length > 0) {
|
||||
console.log("can't show add, duplicates will exist");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.settings.compareFn == 'function') {
|
||||
// The problem here is that compareFn can report that duplicate will exist as it does contains not match
|
||||
const matches = this.settings.compareFn(options, inputText);
|
||||
console.log('matches for ', inputText, ': ', matches);
|
||||
console.log('matches include input string: ', matches.includes(this.settings.addTransformFn(inputText)));
|
||||
if (matches.length > 0 && matches.includes(this.settings.addTransformFn(inputText))) {
|
||||
console.log("can't show add, there are still ");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.showAddItem = true;
|
||||
|
||||
if (this.showAddItem) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleLock(event: any) {
|
||||
|
@ -29,9 +29,13 @@ export class TypeaheadSettings<T> {
|
||||
savedData!: T[] | T;
|
||||
/**
|
||||
* Function to compare the elements. Should return all elements that fit the matching criteria.
|
||||
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
|
||||
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead.
|
||||
*/
|
||||
compareFn!: ((optionList: T[], filter: string) => T[]);
|
||||
/**
|
||||
* Must be defined when addIfNonExisting is true. Used to ensure no duplicates exist when adding.
|
||||
*/
|
||||
compareFnForAdd!: ((optionList: T[], filter: string) => T[]);
|
||||
/**
|
||||
* Function which is used for comparing objects when keeping track of state.
|
||||
* Useful over shallow equal when you have image urls that have random numbers on them.
|
||||
|
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
|
||||
<p *ngIf="isAdmin">
|
||||
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://wiki.kavitareader.com/en/guides/settings/themes" target="_blank" rel="noopener noreferrer">wiki</a>.
|
||||
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://github.com/Kareadita/Themes" target="_blank" rel="noopener noreferrer">theme github</a>.
|
||||
</p>
|
||||
|
||||
<div class="row g-0">
|
||||
|
@ -14,8 +14,8 @@
|
||||
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
|
||||
<meta name='robots' content='noindex,follow' />
|
||||
|
||||
<!-- Don't allow indexing from Bots -->
|
||||
<meta name='robots' content='none' />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
|
@ -24,10 +24,10 @@ input:not([type="range"]), .form-control {
|
||||
}
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb:active {
|
||||
.form-range::-webkit-slider-thumb:active, .form-range::-moz-range-thumb:active {
|
||||
background-color: var(--input-range-active-color);
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb {
|
||||
.form-range::-webkit-slider-thumb, .form-range::-moz-range-thumb {
|
||||
background-color: var(--input-range-color);
|
||||
}
|
||||
|
33
openapi.json
33
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.1.20"
|
||||
"version": "0.7.1.22"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -3631,6 +3631,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Reader/thumbnail": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reader"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "chapterId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageNum",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Reader/bookmark-image": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -3739,6 +3769,7 @@
|
||||
"Reader"
|
||||
],
|
||||
"summary": "Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.",
|
||||
"description": "This is generally the first call when attempting to read to allow pre-generation of assets needed for reading",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "chapterId",
|
||||
|
Loading…
x
Reference in New Issue
Block a user