mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 05:34:21 -04:00
Swipe Issues (#1745)
* Updated theme support to be able to customize the tile color dynamically from a theme via --tile-color. In addition, --theme-color will update apple-mobile-web-app-status-bar-style as well as the non-apple variants * Removed --manga-reader-bg-color as it wasn't used anywhere. Fixed double pagination on swipe. * Cleaned up some dead threshold code for swipe. * Started refactoring tests to use an abstract test class. Stopping because I should do on the .net 7 branch to avoid large merge conflicts. Tests need to be re-designed so they can run in parallel. * Fixed a bug in reading lists where when deleting an item, order could be miscalculated. * Started adding new information for stat service. Refactored time spent reading to be more accurate by taking average time against how much of the chapter the user has read. * Hooked up total time reading at server stat level. Don't show fancy graphs on mobile. * Added new stats for v0.7 * Added a test for Clearing want to read * Fixed a few tests that weren't resetting state between runs * Fixed some broken unit tests * Ensure all Series queries sort by a case invariant string. * Added more aggressive caching of images. This will result in a min delay on pages after a cover is changed. * Fixed a bug where if during new word count calculation, new word count is zero, restoring the old count wasn't working. * Cleaned up some of the code for getting time estimates * Fixed a bug where triggering swipe right wasn't working when there was no scroll * Delete the temp folder for creating a download after a full zip is created.
This commit is contained in:
parent
3d6de68089
commit
549e52b458
@ -17,9 +17,9 @@ using NSubstitute;
|
|||||||
|
|
||||||
namespace API.Tests;
|
namespace API.Tests;
|
||||||
|
|
||||||
public abstract class BasicTest
|
public abstract class AbstractDbTest
|
||||||
{
|
{
|
||||||
private readonly DbConnection _connection;
|
protected readonly DbConnection _connection;
|
||||||
protected readonly DataContext _context;
|
protected readonly DataContext _context;
|
||||||
protected readonly IUnitOfWork _unitOfWork;
|
protected readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
@ -30,8 +30,9 @@ public abstract class BasicTest
|
|||||||
protected const string LogDirectory = "C:/kavita/config/logs/";
|
protected const string LogDirectory = "C:/kavita/config/logs/";
|
||||||
protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||||
protected const string TempDirectory = "C:/kavita/config/temp/";
|
protected const string TempDirectory = "C:/kavita/config/temp/";
|
||||||
|
protected const string DataDirectory = "C:/data/";
|
||||||
|
|
||||||
protected BasicTest()
|
protected AbstractDbTest()
|
||||||
{
|
{
|
||||||
var contextOptions = new DbContextOptionsBuilder()
|
var contextOptions = new DbContextOptionsBuilder()
|
||||||
.UseSqlite(CreateInMemoryDatabase())
|
.UseSqlite(CreateInMemoryDatabase())
|
||||||
@ -50,13 +51,12 @@ public abstract class BasicTest
|
|||||||
private static DbConnection CreateInMemoryDatabase()
|
private static DbConnection CreateInMemoryDatabase()
|
||||||
{
|
{
|
||||||
var connection = new SqliteConnection("Filename=:memory:");
|
var connection = new SqliteConnection("Filename=:memory:");
|
||||||
|
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> SeedDb()
|
protected async Task<bool> SeedDb()
|
||||||
{
|
{
|
||||||
await _context.Database.MigrateAsync();
|
await _context.Database.MigrateAsync();
|
||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
@ -91,14 +91,7 @@ public abstract class BasicTest
|
|||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ResetDb()
|
protected abstract Task ResetDb();
|
||||||
{
|
|
||||||
_context.Series.RemoveRange(_context.Series.ToList());
|
|
||||||
_context.Users.RemoveRange(_context.Users.ToList());
|
|
||||||
_context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList());
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static MockFileSystem CreateFileSystem()
|
protected static MockFileSystem CreateFileSystem()
|
||||||
{
|
{
|
||||||
@ -111,7 +104,7 @@ public abstract class BasicTest
|
|||||||
fileSystem.AddDirectory(BookmarkDirectory);
|
fileSystem.AddDirectory(BookmarkDirectory);
|
||||||
fileSystem.AddDirectory(LogDirectory);
|
fileSystem.AddDirectory(LogDirectory);
|
||||||
fileSystem.AddDirectory(TempDirectory);
|
fileSystem.AddDirectory(TempDirectory);
|
||||||
fileSystem.AddDirectory("C:/data/");
|
fileSystem.AddDirectory(DataDirectory);
|
||||||
|
|
||||||
return fileSystem;
|
return fileSystem;
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Settings;
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
@ -28,70 +29,14 @@ using Xunit;
|
|||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class CleanupServiceTests
|
public class CleanupServiceTests : AbstractDbTest
|
||||||
{
|
{
|
||||||
private readonly ILogger<CleanupService> _logger = Substitute.For<ILogger<CleanupService>>();
|
private readonly ILogger<CleanupService> _logger = Substitute.For<ILogger<CleanupService>>();
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
|
||||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||||
|
|
||||||
private readonly DbConnection _connection;
|
|
||||||
private readonly DataContext _context;
|
|
||||||
|
|
||||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
public CleanupServiceTests() : base()
|
||||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
|
||||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
|
||||||
private const string LogDirectory = "C:/kavita/config/logs/";
|
|
||||||
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
|
||||||
|
|
||||||
|
|
||||||
public CleanupServiceTests()
|
|
||||||
{
|
{
|
||||||
var contextOptions = new DbContextOptionsBuilder()
|
|
||||||
.UseSqlite(CreateInMemoryDatabase())
|
|
||||||
.Options;
|
|
||||||
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
|
||||||
|
|
||||||
_context = new DataContext(contextOptions);
|
|
||||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
|
||||||
var mapper = config.CreateMapper();
|
|
||||||
|
|
||||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Setup
|
|
||||||
|
|
||||||
private static DbConnection CreateInMemoryDatabase()
|
|
||||||
{
|
|
||||||
var connection = new SqliteConnection("Filename=:memory:");
|
|
||||||
|
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> SeedDb()
|
|
||||||
{
|
|
||||||
await _context.Database.MigrateAsync();
|
|
||||||
var filesystem = CreateFileSystem();
|
|
||||||
|
|
||||||
await Seed.SeedSettings(_context, new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
|
||||||
|
|
||||||
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
|
||||||
setting.Value = CacheDirectory;
|
|
||||||
|
|
||||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
|
||||||
setting.Value = BackupDirectory;
|
|
||||||
|
|
||||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
|
||||||
setting.Value = BookmarkDirectory;
|
|
||||||
|
|
||||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync();
|
|
||||||
setting.Value = "10";
|
|
||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
|
||||||
|
|
||||||
_context.Library.Add(new Library()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Manga",
|
Name = "Manga",
|
||||||
@ -103,10 +48,12 @@ public class CleanupServiceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return await _context.SaveChangesAsync() > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ResetDB()
|
#region Setup
|
||||||
|
|
||||||
|
|
||||||
|
protected override async Task ResetDb()
|
||||||
{
|
{
|
||||||
_context.Series.RemoveRange(_context.Series.ToList());
|
_context.Series.RemoveRange(_context.Series.ToList());
|
||||||
_context.Users.RemoveRange(_context.Users.ToList());
|
_context.Users.RemoveRange(_context.Users.ToList());
|
||||||
@ -115,20 +62,6 @@ public class CleanupServiceTests
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MockFileSystem CreateFileSystem()
|
|
||||||
{
|
|
||||||
var fileSystem = new MockFileSystem();
|
|
||||||
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
|
||||||
fileSystem.AddDirectory("C:/kavita/config/");
|
|
||||||
fileSystem.AddDirectory(CacheDirectory);
|
|
||||||
fileSystem.AddDirectory(CoverImageDirectory);
|
|
||||||
fileSystem.AddDirectory(BackupDirectory);
|
|
||||||
fileSystem.AddDirectory(BookmarkDirectory);
|
|
||||||
fileSystem.AddDirectory("C:/data/");
|
|
||||||
|
|
||||||
return fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region DeleteSeriesCoverImages
|
#region DeleteSeriesCoverImages
|
||||||
@ -142,7 +75,7 @@ public class CleanupServiceTests
|
|||||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
||||||
|
|
||||||
// Delete all Series to reset state
|
// Delete all Series to reset state
|
||||||
await ResetDB();
|
await ResetDb();
|
||||||
|
|
||||||
var s = DbFactory.Series("Test 1");
|
var s = DbFactory.Series("Test 1");
|
||||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||||
@ -175,7 +108,7 @@ public class CleanupServiceTests
|
|||||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
||||||
|
|
||||||
// Delete all Series to reset state
|
// Delete all Series to reset state
|
||||||
await ResetDB();
|
await ResetDb();
|
||||||
|
|
||||||
// Add 2 series with cover images
|
// Add 2 series with cover images
|
||||||
var s = DbFactory.Series("Test 1");
|
var s = DbFactory.Series("Test 1");
|
||||||
@ -209,7 +142,7 @@ public class CleanupServiceTests
|
|||||||
filesystem.AddFile($"{CoverImageDirectory}v01_c1000.jpg", new MockFileData(""));
|
filesystem.AddFile($"{CoverImageDirectory}v01_c1000.jpg", new MockFileData(""));
|
||||||
|
|
||||||
// Delete all Series to reset state
|
// Delete all Series to reset state
|
||||||
await ResetDB();
|
await ResetDb();
|
||||||
|
|
||||||
// Add 2 series with cover images
|
// Add 2 series with cover images
|
||||||
var s = DbFactory.Series("Test 1");
|
var s = DbFactory.Series("Test 1");
|
||||||
@ -259,7 +192,7 @@ public class CleanupServiceTests
|
|||||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData(""));
|
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData(""));
|
||||||
|
|
||||||
// Delete all Series to reset state
|
// Delete all Series to reset state
|
||||||
await ResetDB();
|
await ResetDb();
|
||||||
|
|
||||||
// Add 2 series with cover images
|
// Add 2 series with cover images
|
||||||
var s = DbFactory.Series("Test 1");
|
var s = DbFactory.Series("Test 1");
|
||||||
@ -307,7 +240,7 @@ public class CleanupServiceTests
|
|||||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData(""));
|
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData(""));
|
||||||
|
|
||||||
// Delete all Series to reset state
|
// Delete all Series to reset state
|
||||||
await ResetDB();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Users.Add(new AppUser()
|
_context.Users.Add(new AppUser()
|
||||||
{
|
{
|
||||||
@ -569,6 +502,62 @@ public class CleanupServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region CleanupWantToRead
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CleanupWantToRead_ShouldRemoveFullyReadSeries()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
var s = new Series()
|
||||||
|
{
|
||||||
|
Name = "Test CleanupWantToRead_ShouldRemoveFullyReadSeries",
|
||||||
|
Library = new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>(),
|
||||||
|
Metadata = new SeriesMetadata()
|
||||||
|
{
|
||||||
|
PublicationStatus = PublicationStatus.Completed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_context.Series.Add(s);
|
||||||
|
|
||||||
|
var user = new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "CleanupWantToRead_ShouldRemoveFullyReadSeries",
|
||||||
|
WantToRead = new List<Series>()
|
||||||
|
{
|
||||||
|
s
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_context.AppUser.Add(user);
|
||||||
|
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||||
|
Substitute.For<IEventHub>());
|
||||||
|
|
||||||
|
await readerService.MarkSeriesAsRead(user, s.Id);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var cleanupService = new CleanupService(Substitute.For<ILogger<CleanupService>>(), _unitOfWork,
|
||||||
|
Substitute.For<IEventHub>(),
|
||||||
|
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||||
|
|
||||||
|
|
||||||
|
await cleanupService.CleanupWantToRead();
|
||||||
|
|
||||||
|
var wantToRead =
|
||||||
|
await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(user.Id, new UserParams(), new FilterDto());
|
||||||
|
|
||||||
|
Assert.Equal(0, wantToRead.TotalCount);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
// #region CleanupBookmarks
|
// #region CleanupBookmarks
|
||||||
//
|
//
|
||||||
// [Fact]
|
// [Fact]
|
||||||
@ -579,7 +568,7 @@ public class CleanupServiceTests
|
|||||||
// filesystem.AddFile($"{BookmarkDirectory}1/1/1/0002.jpg", new MockFileData(""));
|
// filesystem.AddFile($"{BookmarkDirectory}1/1/1/0002.jpg", new MockFileData(""));
|
||||||
//
|
//
|
||||||
// // Delete all Series to reset state
|
// // Delete all Series to reset state
|
||||||
// await ResetDB();
|
// await ResetDb();
|
||||||
//
|
//
|
||||||
// _context.Series.Add(new Series()
|
// _context.Series.Add(new Series()
|
||||||
// {
|
// {
|
||||||
@ -651,7 +640,7 @@ public class CleanupServiceTests
|
|||||||
// filesystem.AddFile($"{BookmarkDirectory}1/1/2/0002.jpg", new MockFileData(""));
|
// filesystem.AddFile($"{BookmarkDirectory}1/1/2/0002.jpg", new MockFileData(""));
|
||||||
//
|
//
|
||||||
// // Delete all Series to reset state
|
// // Delete all Series to reset state
|
||||||
// await ResetDB();
|
// await ResetDb();
|
||||||
//
|
//
|
||||||
// _context.Series.Add(new Series()
|
// _context.Series.Add(new Series()
|
||||||
// {
|
// {
|
||||||
|
@ -12,20 +12,20 @@ using Xunit;
|
|||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class DeviceServiceTests : BasicTest
|
public class DeviceServiceDbTests : AbstractDbTest
|
||||||
{
|
{
|
||||||
private readonly ILogger<DeviceService> _logger = Substitute.For<ILogger<DeviceService>>();
|
private readonly ILogger<DeviceService> _logger = Substitute.For<ILogger<DeviceService>>();
|
||||||
private readonly IDeviceService _deviceService;
|
private readonly IDeviceService _deviceService;
|
||||||
|
|
||||||
public DeviceServiceTests() : base()
|
public DeviceServiceDbTests() : base()
|
||||||
{
|
{
|
||||||
_deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For<IEmailService>());
|
_deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For<IEmailService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected new Task ResetDb()
|
protected override async Task ResetDb()
|
||||||
{
|
{
|
||||||
_context.Users.RemoveRange(_context.Users.ToList());
|
_context.Users.RemoveRange(_context.Users.ToList());
|
||||||
return Task.CompletedTask;
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1429,6 +1429,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstVolume_NoProgress()
|
public async Task GetContinuePoint_ShouldReturnFirstVolume_NoProgress()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1479,6 +1480,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1_WithProgress()
|
public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1_WithProgress()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1524,6 +1526,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial()
|
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1596,6 +1599,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial2()
|
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial2()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1674,6 +1678,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstSpecial()
|
public async Task GetContinuePoint_ShouldReturnFirstSpecial()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1743,6 +1748,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenNonRead_LooseLeafChaptersAndVolumes()
|
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenNonRead_LooseLeafChaptersAndVolumes()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1785,6 +1791,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesAndAFewLooseChaptersRead()
|
public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesAndAFewLooseChaptersRead()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1851,6 +1858,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllRead()
|
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllRead()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1914,6 +1922,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllReadAndAllChapters()
|
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllReadAndAllChapters()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -1959,6 +1968,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstSpecial_WhenAllReadAndAllChapters()
|
public async Task GetContinuePoint_ShouldReturnFirstSpecial_WhenAllReadAndAllChapters()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -2020,6 +2030,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress()
|
public async Task GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
var series = new Series()
|
var series = new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -2079,6 +2090,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkChaptersUntilAsRead_ShouldMarkAllChaptersAsRead()
|
public async Task MarkChaptersUntilAsRead_ShouldMarkAllChaptersAsRead()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -2121,6 +2133,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkChaptersUntilAsRead_ShouldMarkUptTillChapterNumberAsRead()
|
public async Task MarkChaptersUntilAsRead_ShouldMarkUptTillChapterNumberAsRead()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
@ -2165,6 +2178,7 @@ public class ReaderServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkChaptersUntilAsRead_ShouldMarkAsRead_OnlyVolumesWithChapter0()
|
public async Task MarkChaptersUntilAsRead_ShouldMarkAsRead_OnlyVolumesWithChapter0()
|
||||||
{
|
{
|
||||||
|
await ResetDb();
|
||||||
_context.Series.Add(new Series()
|
_context.Series.Add(new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
|
@ -181,6 +181,96 @@ public class ReadingListServiceTests
|
|||||||
Assert.Equal(2, readingList.Items.Single(i => i.ChapterId == 2).Order);
|
Assert.Equal(2, readingList.Items.Single(i => i.ChapterId == 2).Order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateReadingListItemPosition_MoveLastToFirst_TwoItemsShouldShift_ThenDeleteSecond_OrderShouldBeCorrect()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>()),
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Name = "0",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
AgeRating = AgeRating.Everyone,
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "2",
|
||||||
|
AgeRating = AgeRating.X18Plus
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "3",
|
||||||
|
AgeRating = AgeRating.X18Plus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists);
|
||||||
|
var readingList = new ReadingList();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
readingList
|
||||||
|
};
|
||||||
|
|
||||||
|
// Existing (order, chapterId): (0, 1), (1, 2), (2, 3)
|
||||||
|
await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 2, 3}, readingList);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
Assert.Equal(3, readingList.Items.Count);
|
||||||
|
|
||||||
|
// From 3 to 1
|
||||||
|
// New (order, chapterId): (0, 3), (1, 2), (2, 1)
|
||||||
|
await _readingListService.UpdateReadingListItemPosition(new UpdateReadingListPosition()
|
||||||
|
{
|
||||||
|
FromPosition = 2, ToPosition = 0, ReadingListId = 1, ReadingListItemId = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal(3, readingList.Items.Count);
|
||||||
|
Assert.Equal(0, readingList.Items.Single(i => i.ChapterId == 3).Order);
|
||||||
|
Assert.Equal(1, readingList.Items.Single(i => i.ChapterId == 1).Order);
|
||||||
|
Assert.Equal(2, readingList.Items.Single(i => i.ChapterId == 2).Order);
|
||||||
|
|
||||||
|
// New (order, chapterId): (0, 3), (2, 1): Delete 2nd item
|
||||||
|
await _readingListService.DeleteReadingListItem(new UpdateReadingListPosition()
|
||||||
|
{
|
||||||
|
ReadingListId = 1, ReadingListItemId = readingList.Items.Single(i => i.ChapterId == 2).Id
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(2, readingList.Items.Count);
|
||||||
|
Assert.Equal(0, readingList.Items.Single(i => i.ChapterId == 3).Order);
|
||||||
|
Assert.Equal(1, readingList.Items.Single(i => i.ChapterId == 1).Order);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -28,80 +28,18 @@ using Xunit;
|
|||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class SeriesServiceTests
|
public class SeriesServiceTests : AbstractDbTest
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
|
||||||
|
|
||||||
private readonly DbConnection _connection;
|
|
||||||
private readonly DataContext _context;
|
|
||||||
|
|
||||||
private readonly ISeriesService _seriesService;
|
private readonly ISeriesService _seriesService;
|
||||||
|
|
||||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
public SeriesServiceTests() : base()
|
||||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
|
||||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
|
||||||
private const string DataDirectory = "C:/data/";
|
|
||||||
|
|
||||||
public SeriesServiceTests()
|
|
||||||
{
|
{
|
||||||
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
|
|
||||||
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
|
||||||
|
|
||||||
_context = new DataContext(contextOptions);
|
|
||||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
|
||||||
var mapper = config.CreateMapper();
|
|
||||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
|
||||||
|
|
||||||
_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
||||||
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>());
|
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>());
|
||||||
}
|
}
|
||||||
#region Setup
|
#region Setup
|
||||||
|
|
||||||
private static DbConnection CreateInMemoryDatabase()
|
protected override async Task ResetDb()
|
||||||
{
|
|
||||||
var connection = new SqliteConnection("Filename=:memory:");
|
|
||||||
|
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> SeedDb()
|
|
||||||
{
|
|
||||||
await _context.Database.MigrateAsync();
|
|
||||||
var filesystem = CreateFileSystem();
|
|
||||||
|
|
||||||
await Seed.SeedSettings(_context,
|
|
||||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
|
||||||
|
|
||||||
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
|
||||||
setting.Value = CacheDirectory;
|
|
||||||
|
|
||||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
|
||||||
setting.Value = BackupDirectory;
|
|
||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
|
||||||
|
|
||||||
// var lib = new Library()
|
|
||||||
// {
|
|
||||||
// Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// _context.AppUser.Add(new AppUser()
|
|
||||||
// {
|
|
||||||
// UserName = "majora2007",
|
|
||||||
// Libraries = new List<Library>()
|
|
||||||
// {
|
|
||||||
// lib
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
return await _context.SaveChangesAsync() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetDb()
|
|
||||||
{
|
{
|
||||||
_context.Series.RemoveRange(_context.Series.ToList());
|
_context.Series.RemoveRange(_context.Series.ToList());
|
||||||
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
|
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
|
||||||
@ -113,19 +51,6 @@ public class SeriesServiceTests
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MockFileSystem CreateFileSystem()
|
|
||||||
{
|
|
||||||
var fileSystem = new MockFileSystem();
|
|
||||||
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
|
||||||
fileSystem.AddDirectory("C:/kavita/config/");
|
|
||||||
fileSystem.AddDirectory(CacheDirectory);
|
|
||||||
fileSystem.AddDirectory(CoverImageDirectory);
|
|
||||||
fileSystem.AddDirectory(BackupDirectory);
|
|
||||||
fileSystem.AddDirectory(DataDirectory);
|
|
||||||
|
|
||||||
return fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UpdateRelatedSeriesDto CreateRelationsDto(Series series)
|
private static UpdateRelatedSeriesDto CreateRelationsDto(Series series)
|
||||||
{
|
{
|
||||||
return new UpdateRelatedSeriesDto()
|
return new UpdateRelatedSeriesDto()
|
||||||
@ -1465,7 +1390,7 @@ public class SeriesServiceTests
|
|||||||
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary()
|
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary()
|
||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
_context.Library.Add(new Library()
|
var lib = new Library()
|
||||||
{
|
{
|
||||||
AppUsers = new List<AppUser>()
|
AppUsers = new List<AppUser>()
|
||||||
{
|
{
|
||||||
@ -1481,20 +1406,21 @@ public class SeriesServiceTests
|
|||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series",
|
Name = "Test Series",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
},
|
},
|
||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series Prequels",
|
Name = "Test Series Prequels",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
},
|
},
|
||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series Sequels",
|
Name = "Test Series Sequels",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
_context.Library.Add(lib);
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
@ -1505,7 +1431,7 @@ public class SeriesServiceTests
|
|||||||
addRelationDto.Sequels.Add(3);
|
addRelationDto.Sequels.Add(3);
|
||||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1);
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib.Id);
|
||||||
_unitOfWork.LibraryRepository.Delete(library);
|
_unitOfWork.LibraryRepository.Delete(library);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -1524,7 +1450,7 @@ public class SeriesServiceTests
|
|||||||
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary_WhenSeriesCrossLibraries()
|
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary_WhenSeriesCrossLibraries()
|
||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
_context.Library.Add(new Library()
|
var lib1 = new Library()
|
||||||
{
|
{
|
||||||
AppUsers = new List<AppUser>()
|
AppUsers = new List<AppUser>()
|
||||||
{
|
{
|
||||||
@ -1564,17 +1490,17 @@ public class SeriesServiceTests
|
|||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series Prequels",
|
Name = "Test Series Prequels",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
},
|
},
|
||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series Sequels",
|
Name = "Test Series Sequels",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
_context.Library.Add(lib1);
|
||||||
_context.Library.Add(new Library()
|
var lib2 = new Library()
|
||||||
{
|
{
|
||||||
AppUsers = new List<AppUser>()
|
AppUsers = new List<AppUser>()
|
||||||
{
|
{
|
||||||
@ -1590,20 +1516,21 @@ public class SeriesServiceTests
|
|||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series 2",
|
Name = "Test Series 2",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
},
|
},
|
||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series Prequels 2",
|
Name = "Test Series Prequels 2",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
},
|
},
|
||||||
new Series()
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test Series Sequels 2",
|
Name = "Test Series Sequels 2",
|
||||||
Volumes = new List<Volume>(){}
|
Volumes = new List<Volume>() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
_context.Library.Add(lib2);
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
@ -1613,7 +1540,7 @@ public class SeriesServiceTests
|
|||||||
addRelationDto.Adaptations.Add(4); // cross library link
|
addRelationDto.Adaptations.Add(4); // cross library link
|
||||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Series);
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib1.Id, LibraryIncludes.Series);
|
||||||
_unitOfWork.LibraryRepository.Delete(library);
|
_unitOfWork.LibraryRepository.Delete(library);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -32,7 +32,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-cover")]
|
[HttpGet("chapter-cover")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId"})]
|
||||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||||
{
|
{
|
||||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||||
@ -48,7 +48,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("library-cover")]
|
[HttpGet("library-cover")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId"})]
|
||||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId)
|
public async Task<ActionResult> GetLibraryCoverImage(int libraryId)
|
||||||
{
|
{
|
||||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
||||||
@ -64,7 +64,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("volume-cover")]
|
[HttpGet("volume-cover")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId"})]
|
||||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||||
{
|
{
|
||||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||||
@ -79,7 +79,7 @@ public class ImageController : BaseApiController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId">Id of Series</param>
|
/// <param name="seriesId">Id of Series</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"seriesId"})]
|
||||||
[HttpGet("series-cover")]
|
[HttpGet("series-cover")]
|
||||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||||
{
|
{
|
||||||
@ -98,7 +98,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <param name="collectionTagId"></param>
|
/// <param name="collectionTagId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("collection-cover")]
|
[HttpGet("collection-cover")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId"})]
|
||||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||||
{
|
{
|
||||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||||
@ -114,7 +114,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <param name="readingListId"></param>
|
/// <param name="readingListId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("readinglist-cover")]
|
[HttpGet("readinglist-cover")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId"})]
|
||||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
||||||
{
|
{
|
||||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||||
@ -133,7 +133,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
|
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("bookmark")]
|
[HttpGet("bookmark")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "pageNum", "apiKey"})]
|
||||||
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||||
@ -155,7 +155,7 @@ public class ImageController : BaseApiController
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize(Policy="RequireAdminRole")]
|
[Authorize(Policy="RequireAdminRole")]
|
||||||
[HttpGet("cover-upload")]
|
[HttpGet("cover-upload")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"filename"})]
|
||||||
public ActionResult GetCoverUploadImage(string filename)
|
public ActionResult GetCoverUploadImage(string filename)
|
||||||
{
|
{
|
||||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||||
|
@ -13,6 +13,7 @@ public class ServerStatisticsDto
|
|||||||
public long TotalGenres { get; set; }
|
public long TotalGenres { get; set; }
|
||||||
public long TotalTags { get; set; }
|
public long TotalTags { get; set; }
|
||||||
public long TotalPeople { get; set; }
|
public long TotalPeople { get; set; }
|
||||||
|
public long TotalReadingTime { get; set; }
|
||||||
public IEnumerable<ICount<SeriesDto>> MostReadSeries { get; set; }
|
public IEnumerable<ICount<SeriesDto>> MostReadSeries { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total users who have started/reading/read per series
|
/// Total users who have started/reading/read per series
|
||||||
|
@ -145,4 +145,39 @@ public class ServerInfoDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Introduced in v0.6.0</remarks>
|
/// <remarks>Introduced in v0.6.0</remarks>
|
||||||
public bool UsingRestrictedProfiles { get; set; }
|
public bool UsingRestrictedProfiles { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of users using the Emulate Comic Book setting
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.7.0</remarks>
|
||||||
|
public int UsersWithEmulateComicBook { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Percent (0.0-1.0) of libraries with folder watching enabled
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.7.0</remarks>
|
||||||
|
public float PercentOfLibrariesWithFolderWatchingEnabled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Percent (0.0-1.0) of libraries included in Search
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.7.0</remarks>
|
||||||
|
public float PercentOfLibrariesIncludedInSearch { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Percent (0.0-1.0) of libraries included in Recommended
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.7.0</remarks>
|
||||||
|
public float PercentOfLibrariesIncludedInRecommended { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Percent (0.0-1.0) of libraries included in Dashboard
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.7.0</remarks>
|
||||||
|
public float PercentOfLibrariesIncludedInDashboard { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total reading hours of all users
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.7.0</remarks>
|
||||||
|
public long TotalReadingHours { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is the Server saving covers as WebP
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Added in v0.7.0</remarks>
|
||||||
|
public bool StoreCoversAsWebP { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,8 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
return await _context.Series
|
return await _context.Series
|
||||||
.Where(s => s.LibraryId == libraryId)
|
.Where(s => s.LibraryId == libraryId)
|
||||||
.Includes(includes)
|
.Includes(includes)
|
||||||
.OrderBy(s => s.SortName).ToListAsync();
|
.OrderBy(s => s.SortName.ToLower())
|
||||||
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -223,7 +224,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ThenInclude(v => v.Chapters)
|
.ThenInclude(v => v.Chapters)
|
||||||
.ThenInclude(c => c.Files)
|
.ThenInclude(c => c.Files)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.OrderBy(s => s.SortName);
|
.OrderBy(s => s.SortName.ToLower());
|
||||||
|
|
||||||
return await PagedList<Series>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
return await PagedList<Series>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
@ -316,7 +317,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(l => libraryIds.Contains(l.Id))
|
.Where(l => libraryIds.Contains(l.Id))
|
||||||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||||
.IsRestricted(QueryContext.Search)
|
.IsRestricted(QueryContext.Search)
|
||||||
.OrderBy(l => l.Name)
|
.OrderBy(l => l.Name.ToLower())
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Take(maxRecords)
|
.Take(maxRecords)
|
||||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||||
@ -335,7 +336,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison))
|
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison))
|
||||||
.RestrictAgainstAgeRestriction(userRating)
|
.RestrictAgainstAgeRestriction(userRating)
|
||||||
.Include(s => s.Library)
|
.Include(s => s.Library)
|
||||||
.OrderBy(s => s.SortName)
|
.OrderBy(s => s.SortName.ToLower())
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Take(maxRecords)
|
.Take(maxRecords)
|
||||||
@ -356,7 +357,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|| EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))
|
|| EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))
|
||||||
.Where(c => c.Promoted || isAdmin)
|
.Where(c => c.Promoted || isAdmin)
|
||||||
.RestrictAgainstAgeRestriction(userRating)
|
.RestrictAgainstAgeRestriction(userRating)
|
||||||
.OrderBy(s => s.Title)
|
.OrderBy(s => s.NormalizedTitle)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Take(maxRecords)
|
.Take(maxRecords)
|
||||||
@ -377,7 +378,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.OrderBy(t => t.Title)
|
.OrderBy(t => t.NormalizedTitle)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Take(maxRecords)
|
.Take(maxRecords)
|
||||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||||
@ -387,7 +388,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.OrderBy(t => t.Title)
|
.OrderBy(t => t.NormalizedTitle)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Take(maxRecords)
|
.Take(maxRecords)
|
||||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||||
@ -719,7 +720,8 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
})
|
})
|
||||||
.Where(s => s.PagesRead > 0
|
.Where(s => s.PagesRead > 0
|
||||||
&& s.PagesRead < s.Series.Pages)
|
&& s.PagesRead < s.Series.Pages)
|
||||||
.Where(d => d.LatestReadDate >= cutoffProgressPoint || d.LastChapterAdded >= cutoffLastAddedPoint).OrderByDescending(s => s.LatestReadDate)
|
.Where(d => d.LatestReadDate >= cutoffProgressPoint || d.LastChapterAdded >= cutoffLastAddedPoint)
|
||||||
|
.OrderByDescending(s => s.LatestReadDate)
|
||||||
.ThenByDescending(s => s.LastChapterAdded)
|
.ThenByDescending(s => s.LastChapterAdded)
|
||||||
.Select(s => s.Series)
|
.Select(s => s.Series)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
@ -777,7 +779,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
query = filter.SortOptions.SortField switch
|
query = filter.SortOptions.SortField switch
|
||||||
{
|
{
|
||||||
SortField.SortName => query.OrderBy(s => s.SortName),
|
SortField.SortName => query.OrderBy(s => s.SortName.ToLower()),
|
||||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||||
@ -790,7 +792,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
query = filter.SortOptions.SortField switch
|
query = filter.SortOptions.SortField switch
|
||||||
{
|
{
|
||||||
SortField.SortName => query.OrderByDescending(s => s.SortName),
|
SortField.SortName => query.OrderByDescending(s => s.SortName.ToLower()),
|
||||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||||
@ -844,7 +846,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
query = filter.SortOptions.SortField switch
|
query = filter.SortOptions.SortField switch
|
||||||
{
|
{
|
||||||
SortField.SortName => query.OrderBy(s => s.SortName),
|
SortField.SortName => query.OrderBy(s => s.SortName.ToLower()),
|
||||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||||
@ -856,7 +858,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
query = filter.SortOptions.SortField switch
|
query = filter.SortOptions.SortField switch
|
||||||
{
|
{
|
||||||
SortField.SortName => query.OrderByDescending(s => s.SortName),
|
SortField.SortName => query.OrderByDescending(s => s.SortName.ToLower()),
|
||||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||||
@ -887,7 +889,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId))
|
.Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId))
|
||||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.OrderBy(t => t.Title)
|
.OrderBy(t => t.Title.ToLower())
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
@ -911,7 +913,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ThenInclude(m => m.Series)
|
.ThenInclude(m => m.Series)
|
||||||
.SelectMany(c => c.SeriesMetadatas.Select(sm => sm.Series).Where(s => userLibraries.Contains(s.LibraryId)))
|
.SelectMany(c => c.SeriesMetadatas.Select(sm => sm.Series).Where(s => userLibraries.Contains(s.LibraryId)))
|
||||||
.OrderBy(s => s.LibraryId)
|
.OrderBy(s => s.LibraryId)
|
||||||
.ThenBy(s => s.SortName)
|
.ThenBy(s => s.SortName.ToLower())
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
@ -941,7 +943,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
return await _context.Series
|
return await _context.Series
|
||||||
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
||||||
.OrderBy(s => s.SortName)
|
.OrderBy(s => s.SortName.ToLower())
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
|
@ -299,6 +299,8 @@ public class ArchiveService : IArchiveService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
||||||
|
// Remove the folder as we have the zip
|
||||||
|
_directoryService.ClearAndDeleteDirectory(tempLocation);
|
||||||
}
|
}
|
||||||
catch (AggregateException ex)
|
catch (AggregateException ex)
|
||||||
{
|
{
|
||||||
|
@ -575,39 +575,20 @@ public class ReaderService : IReaderService
|
|||||||
{
|
{
|
||||||
var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 0);
|
var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 0);
|
||||||
var maxHours = Math.Max((int) Math.Round((wordCount / MaxWordsPerHour)), 0);
|
var maxHours = Math.Max((int) Math.Round((wordCount / MaxWordsPerHour)), 0);
|
||||||
if (maxHours < minHours)
|
|
||||||
{
|
|
||||||
return new HourEstimateRangeDto
|
|
||||||
{
|
|
||||||
MinHours = maxHours,
|
|
||||||
MaxHours = minHours,
|
|
||||||
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return new HourEstimateRangeDto
|
return new HourEstimateRangeDto
|
||||||
{
|
{
|
||||||
MinHours = minHours,
|
MinHours = Math.Min(minHours, maxHours),
|
||||||
MaxHours = maxHours,
|
MaxHours = Math.Max(minHours, maxHours),
|
||||||
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour))
|
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 60F)), 0);
|
var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 60F)), 0);
|
||||||
var maxHoursPages = Math.Max((int) Math.Round((pageCount / MaxPagesPerMinute / 60F)), 0);
|
var maxHoursPages = Math.Max((int) Math.Round((pageCount / MaxPagesPerMinute / 60F)), 0);
|
||||||
if (maxHoursPages < minHoursPages)
|
|
||||||
{
|
|
||||||
return new HourEstimateRangeDto
|
|
||||||
{
|
|
||||||
MinHours = maxHoursPages,
|
|
||||||
MaxHours = minHoursPages,
|
|
||||||
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HourEstimateRangeDto
|
return new HourEstimateRangeDto
|
||||||
{
|
{
|
||||||
MinHours = minHoursPages,
|
MinHours = Math.Min(minHoursPages, maxHoursPages),
|
||||||
MaxHours = maxHoursPages,
|
MaxHours = Math.Max(minHoursPages, maxHoursPages),
|
||||||
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ public class ReadingListService : IReadingListService
|
|||||||
public async Task<bool> DeleteReadingListItem(UpdateReadingListPosition dto)
|
public async Task<bool> DeleteReadingListItem(UpdateReadingListPosition dto)
|
||||||
{
|
{
|
||||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||||
readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList();
|
readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).OrderBy(r => r.Order).ToList();
|
||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var readingListItem in readingList.Items)
|
foreach (var readingListItem in readingList.Items)
|
||||||
|
@ -32,6 +32,7 @@ public interface IStatisticService
|
|||||||
IEnumerable<StatCount<int>> GetPagesReadCountByYear(int userId = 0);
|
IEnumerable<StatCount<int>> GetPagesReadCountByYear(int userId = 0);
|
||||||
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
|
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
|
||||||
Task UpdateServerStatistics();
|
Task UpdateServerStatistics();
|
||||||
|
Task<long> TimeSpentReadingForUsersAsync(IList<int> userIds, IList<int> libraryIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -62,18 +63,20 @@ public class StatisticService : IStatisticService
|
|||||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
.Where(p => libraryIds.Contains(p.LibraryId))
|
||||||
.SumAsync(p => p.PagesRead);
|
.SumAsync(p => p.PagesRead);
|
||||||
|
|
||||||
var ids = await _context.AppUserProgresses
|
// var ids = await _context.AppUserProgresses
|
||||||
.Where(p => p.AppUserId == userId)
|
// .Where(p => p.AppUserId == userId)
|
||||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
// .Where(p => libraryIds.Contains(p.LibraryId))
|
||||||
.Where(p => p.PagesRead > 0)
|
// .Where(p => p.PagesRead > 0)
|
||||||
.Select(p => new {p.ChapterId, p.SeriesId})
|
// .Select(p => new {p.ChapterId, p.SeriesId})
|
||||||
.ToListAsync();
|
// .ToListAsync();
|
||||||
|
|
||||||
var chapterIds = ids.Select(id => id.ChapterId);
|
//var chapterIds = ids.Select(id => id.ChapterId);
|
||||||
|
|
||||||
var timeSpentReading = await _context.Chapter
|
// var timeSpentReading = await _context.Chapter
|
||||||
.Where(c => chapterIds.Contains(c.Id))
|
// .Where(c => chapterIds.Contains(c.Id))
|
||||||
.SumAsync(c => c.AvgHoursToRead);
|
// .SumAsync(c => c.AvgHoursToRead);
|
||||||
|
|
||||||
|
var timeSpentReading = await TimeSpentReadingForUsersAsync(new List<int>() {userId}, libraryIds);
|
||||||
|
|
||||||
var totalWordsRead = (long) Math.Round(await _context.AppUserProgresses
|
var totalWordsRead = (long) Math.Round(await _context.AppUserProgresses
|
||||||
.Where(p => p.AppUserId == userId)
|
.Where(p => p.AppUserId == userId)
|
||||||
@ -275,6 +278,8 @@ public class StatisticService : IStatisticService
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.Count();
|
.Count();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return new ServerStatisticsDto()
|
return new ServerStatisticsDto()
|
||||||
{
|
{
|
||||||
ChapterCount = await _context.Chapter.CountAsync(),
|
ChapterCount = await _context.Chapter.CountAsync(),
|
||||||
@ -289,7 +294,8 @@ public class StatisticService : IStatisticService
|
|||||||
MostActiveLibraries = mostActiveLibrary,
|
MostActiveLibraries = mostActiveLibrary,
|
||||||
MostPopularSeries = mostPopularSeries,
|
MostPopularSeries = mostPopularSeries,
|
||||||
MostReadSeries = mostReadSeries,
|
MostReadSeries = mostReadSeries,
|
||||||
RecentlyRead = recentlyRead
|
RecentlyRead = recentlyRead,
|
||||||
|
TotalReadingTime = await TimeSpentReadingForUsersAsync(ArraySegment<int>.Empty, ArraySegment<int>.Empty)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,6 +489,30 @@ public class StatisticService : IStatisticService
|
|||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<long> TimeSpentReadingForUsersAsync(IList<int> userIds, IList<int> libraryIds)
|
||||||
|
{
|
||||||
|
var query = _context.AppUserProgresses
|
||||||
|
.AsSplitQuery();
|
||||||
|
|
||||||
|
if (userIds.Any())
|
||||||
|
{
|
||||||
|
query = query.Where(p => userIds.Contains(p.AppUserId));
|
||||||
|
}
|
||||||
|
if (libraryIds.Any())
|
||||||
|
{
|
||||||
|
query = query.Where(p => libraryIds.Contains(p.LibraryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (long) Math.Round(await query
|
||||||
|
.Join(_context.Chapter,
|
||||||
|
p => p.ChapterId,
|
||||||
|
c => c.Id,
|
||||||
|
(progress, chapter) => new {chapter, progress})
|
||||||
|
.Where(p => p.chapter.AvgHoursToRead > 0)
|
||||||
|
.SumAsync(p =>
|
||||||
|
p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages))));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TopReadDto>> GetTopUsers(int days)
|
public async Task<IEnumerable<TopReadDto>> GetTopUsers(int days)
|
||||||
{
|
{
|
||||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||||
|
@ -223,7 +223,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (series.WordCount == 0 && series.WordCount != 0) series.WordCount = existingWordCount; // Restore original word count if the file hasn't changed
|
if (series.WordCount == 0 && existingWordCount != 0) series.WordCount = existingWordCount; // Restore original word count if the file hasn't changed
|
||||||
var seriesEstimate = _readerService.GetTimeEstimate(series.WordCount, series.Pages, isEpub);
|
var seriesEstimate = _readerService.GetTimeEstimate(series.WordCount, series.Pages, isEpub);
|
||||||
series.MinHoursToRead = seriesEstimate.MinHours;
|
series.MinHoursToRead = seriesEstimate.MinHours;
|
||||||
series.MaxHoursToRead = seriesEstimate.MaxHours;
|
series.MaxHoursToRead = seriesEstimate.MaxHours;
|
||||||
|
@ -25,18 +25,23 @@ public interface IStatsService
|
|||||||
Task<ServerInfoDto> GetServerInfo();
|
Task<ServerInfoDto> GetServerInfo();
|
||||||
Task SendCancellation();
|
Task SendCancellation();
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// This is for reporting to the stat server
|
||||||
|
/// </summary>
|
||||||
public class StatsService : IStatsService
|
public class StatsService : IStatsService
|
||||||
{
|
{
|
||||||
private readonly ILogger<StatsService> _logger;
|
private readonly ILogger<StatsService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly DataContext _context;
|
private readonly DataContext _context;
|
||||||
|
private readonly IStatisticService _statisticService;
|
||||||
private const string ApiUrl = "https://stats.kavitareader.com";
|
private const string ApiUrl = "https://stats.kavitareader.com";
|
||||||
|
|
||||||
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context)
|
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_statisticService = statisticService;
|
||||||
|
|
||||||
FlurlHttp.ConfigureClient(ApiUrl, cli =>
|
FlurlHttp.ConfigureClient(ApiUrl, cli =>
|
||||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||||
@ -116,6 +121,14 @@ public class StatsService : IStatsService
|
|||||||
DotnetVersion = Environment.Version.ToString(),
|
DotnetVersion = Environment.Version.ToString(),
|
||||||
IsDocker = new OsInfo().IsDocker,
|
IsDocker = new OsInfo().IsDocker,
|
||||||
NumOfCores = Math.Max(Environment.ProcessorCount, 1),
|
NumOfCores = Math.Max(Environment.ProcessorCount, 1),
|
||||||
|
UsersWithEmulateComicBook = await _context.AppUserPreferences.CountAsync(p => p.EmulateBook),
|
||||||
|
TotalReadingHours = await _statisticService.TimeSpentReadingForUsersAsync(ArraySegment<int>.Empty, ArraySegment<int>.Empty),
|
||||||
|
|
||||||
|
PercentOfLibrariesWithFolderWatchingEnabled = await GetPercentageOfLibrariesWithFolderWatchingEnabled(),
|
||||||
|
PercentOfLibrariesIncludedInRecommended = await GetPercentageOfLibrariesIncludedInRecommended(),
|
||||||
|
PercentOfLibrariesIncludedInDashboard = await GetPercentageOfLibrariesIncludedInDashboard(),
|
||||||
|
PercentOfLibrariesIncludedInSearch = await GetPercentageOfLibrariesIncludedInSearch(),
|
||||||
|
|
||||||
HasBookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()).Any(),
|
HasBookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()).Any(),
|
||||||
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
|
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
|
||||||
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
|
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
|
||||||
@ -127,6 +140,7 @@ public class StatsService : IStatsService
|
|||||||
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
|
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
|
||||||
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
|
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
|
||||||
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
|
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
|
||||||
|
StoreCoversAsWebP = serverSettings.ConvertCoverToWebP,
|
||||||
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||||
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||||
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
||||||
@ -190,6 +204,30 @@ public class StatsService : IStatsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<float> GetPercentageOfLibrariesWithFolderWatchingEnabled()
|
||||||
|
{
|
||||||
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||||
|
return libraries.Count(l => l.FolderWatching) / (1.0f * libraries.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<float> GetPercentageOfLibrariesIncludedInRecommended()
|
||||||
|
{
|
||||||
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||||
|
return libraries.Count(l => l.IncludeInRecommended) / (1.0f * libraries.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<float> GetPercentageOfLibrariesIncludedInDashboard()
|
||||||
|
{
|
||||||
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||||
|
return libraries.Count(l => l.IncludeInDashboard) / (1.0f * libraries.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<float> GetPercentageOfLibrariesIncludedInSearch()
|
||||||
|
{
|
||||||
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||||
|
return libraries.Count(l => l.IncludeInSearch) / (1.0f * libraries.Count);
|
||||||
|
}
|
||||||
|
|
||||||
private Task<bool> GetIfUsingSeriesRelationship()
|
private Task<bool> GetIfUsingSeriesRelationship()
|
||||||
{
|
{
|
||||||
return _context.SeriesRelation.AnyAsync();
|
return _context.SeriesRelation.AnyAsync();
|
||||||
@ -242,6 +280,7 @@ public class StatsService : IStatsService
|
|||||||
return await _context.AppUserPreferences.Select(p => p.PageSplitOption).Distinct().ToListAsync();
|
return await _context.AppUserPreferences.Select(p => p.PageSplitOption).Distinct().ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<IEnumerable<LayoutMode>> AllMangaReaderLayoutModes()
|
private async Task<IEnumerable<LayoutMode>> AllMangaReaderLayoutModes()
|
||||||
{
|
{
|
||||||
return await _context.AppUserPreferences.Select(p => p.LayoutMode).Distinct().ToListAsync();
|
return await _context.AppUserPreferences.Select(p => p.LayoutMode).Distinct().ToListAsync();
|
||||||
|
@ -64,15 +64,23 @@ export class ThemeService implements OnDestroy {
|
|||||||
getColorScheme() {
|
getColorScheme() {
|
||||||
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --theme-color from theme. Updates the meta tag
|
* --theme-color from theme. Updates the meta tag
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getThemeColor() {
|
getThemeColor() {
|
||||||
return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
|
return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --msapplication-TileColor from theme. Updates the meta tag
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getTileColor() {
|
||||||
|
return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim();
|
||||||
|
}
|
||||||
|
|
||||||
getCssVariable(variable: string) {
|
getCssVariable(variable: string) {
|
||||||
return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
|
return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
|
||||||
}
|
}
|
||||||
@ -155,6 +163,12 @@ export class ThemeService implements OnDestroy {
|
|||||||
const themeColor = this.getThemeColor();
|
const themeColor = this.getThemeColor();
|
||||||
if (themeColor) {
|
if (themeColor) {
|
||||||
this.document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor);
|
this.document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor);
|
||||||
|
this.document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]')?.setAttribute('content', themeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileColor = this.getTileColor();
|
||||||
|
if (themeColor) {
|
||||||
|
this.document.querySelector('meta[name="msapplication-TileColor"]')?.setAttribute('content', themeColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorScheme = this.getColorScheme();
|
const colorScheme = this.getColorScheme();
|
||||||
|
@ -973,11 +973,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
triggerSwipePagination(direction: KeyDirection) {
|
triggerSwipePagination(direction: KeyDirection) {
|
||||||
|
|
||||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
|
||||||
if (direction === KeyDirection.Right)
|
|
||||||
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
|
|
||||||
}
|
|
||||||
switch(direction) {
|
switch(direction) {
|
||||||
case KeyDirection.Down:
|
case KeyDirection.Down:
|
||||||
this.nextPage();
|
this.nextPage();
|
||||||
@ -996,8 +991,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSwipeEnd(event: SwipeEvent) {
|
onSwipeEnd(event: SwipeEvent) {
|
||||||
const threshold = .12;
|
|
||||||
|
|
||||||
// Positive number means swiping right/down, negative means left
|
// Positive number means swiping right/down, negative means left
|
||||||
switch (this.readerMode) {
|
switch (this.readerMode) {
|
||||||
case ReaderMode.Webtoon: break;
|
case ReaderMode.Webtoon: break;
|
||||||
@ -1014,6 +1007,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// We just came from a swipe where pagination was required and we are now at the end of the swipe, so make the user do it once more
|
// We just came from a swipe where pagination was required and we are now at the end of the swipe, so make the user do it once more
|
||||||
if (direction === KeyDirection.Right) {
|
if (direction === KeyDirection.Right) {
|
||||||
this.hasHitZeroScroll = false;
|
this.hasHitZeroScroll = false;
|
||||||
|
if (scrollLeft === 0 && this.ReadingAreaWidth === 0) {
|
||||||
|
this.triggerSwipePagination(direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.hasHitRightScroll && this.checkIfPaginationAllowed(direction)) {
|
if (!this.hasHitRightScroll && this.checkIfPaginationAllowed(direction)) {
|
||||||
this.hasHitRightScroll = true;
|
this.hasHitRightScroll = true;
|
||||||
return;
|
return;
|
||||||
@ -1036,7 +1033,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Next page triggered');
|
|
||||||
this.triggerSwipePagination(direction);
|
this.triggerSwipePagination(direction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1072,32 +1068,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Next page triggered');
|
|
||||||
this.triggerSwipePagination(direction);
|
this.triggerSwipePagination(direction);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const height = (this.readingArea?.nativeElement.scrollHeight === this.readingArea?.nativeElement.clientHeight)
|
|
||||||
? this.readingArea?.nativeElement.clientHeight : this.ReadingAreaHeight;
|
|
||||||
|
|
||||||
if (direction === KeyDirection.Down && this.readingArea?.nativeElement?.scrollTop === height && this.prevScrollTop != 0) {
|
|
||||||
this.prevScrollTop = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === KeyDirection.Up && this.readingArea?.nativeElement?.scrollTop === 0 && this.prevScrollTop != 0) {
|
|
||||||
this.prevScrollTop = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thresholdMet = Math.abs(event.distance) >= height * threshold;
|
|
||||||
if (!thresholdMet) return;
|
|
||||||
|
|
||||||
this.triggerSwipePagination(direction);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
|
|||||||
import { ScalingOption } from 'src/app/_models/preferences/scaling-option';
|
import { ScalingOption } from 'src/app/_models/preferences/scaling-option';
|
||||||
import { ReaderService } from 'src/app/_services/reader.service';
|
import { ReaderService } from 'src/app/_services/reader.service';
|
||||||
import { ChapterInfo } from '../_models/chapter-info';
|
import { ChapterInfo } from '../_models/chapter-info';
|
||||||
import { DimensionMap, FileDimension } from '../_models/file-dimension';
|
import { DimensionMap } from '../_models/file-dimension';
|
||||||
import { FITTING_OPTION } from '../_models/reader-enums';
|
import { FITTING_OPTION } from '../_models/reader-enums';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -17,16 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container>
|
|
||||||
<div class="col-auto mb-2">
|
|
||||||
<app-icon-and-title label="Total Chapters" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Total Chapters">
|
|
||||||
{{stats.chapterCount | compactNumber}} Chapters
|
|
||||||
</app-icon-and-title>
|
|
||||||
</div>
|
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<div class="col-auto mb-2">
|
<div class="col-auto mb-2">
|
||||||
<app-icon-and-title label="Total Files" [clickable]="false" fontClasses="fa-regular fa-file" title="Total Files">
|
<app-icon-and-title label="Total Files" [clickable]="false" fontClasses="fa-regular fa-file" title="Total Files">
|
||||||
@ -69,6 +60,15 @@
|
|||||||
{{stats.totalPeople | compactNumber}} People
|
{{stats.totalPeople | compactNumber}} People
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container>
|
||||||
|
<div class="col-auto mb-2">
|
||||||
|
<app-icon-and-title label="Total Read Time" [clickable]="false" fontClasses="fas fa-eye" title="Total Read Time">
|
||||||
|
{{stats.totalReadingTime | compactNumber}} Hours
|
||||||
|
</app-icon-and-title>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -91,28 +91,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 pt-2 pb-2 ">
|
<ng-container *ngIf="breakpoint$ | async as bp">
|
||||||
<app-top-readers></app-top-readers>
|
<div class="row g-0 pt-2 pb-2" *ngIf="bp > Breakpoint.Mobile">
|
||||||
</div>
|
<app-top-readers></app-top-readers>
|
||||||
|
|
||||||
<div class="row g-0 pt-4 pb-2" style="height: 242px">
|
|
||||||
<div class="col-md-6 col-sm-12">
|
|
||||||
<app-file-breakdown-stats></app-file-breakdown-stats>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12">
|
|
||||||
<app-publication-status-stats></app-publication-status-stats>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
|
||||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
|
||||||
<app-read-by-day-and [isAdmin]="true"></app-read-by-day-and>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
<div class="row g-0 pt-4 pb-2" style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
||||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
<div class="col-md-6 col-sm-12">
|
||||||
<app-day-breakdown></app-day-breakdown>
|
<app-file-breakdown-stats></app-file-breakdown-stats>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
<app-publication-status-stats></app-publication-status-stats>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="row g-0 pt-4 pb-2 " style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
||||||
|
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||||
|
<app-read-by-day-and [isAdmin]="true"></app-read-by-day-and>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 pt-4 pb-2 " style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
||||||
|
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||||
|
<app-day-breakdown></app-day-breakdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { map, Observable, shareReplay, Subject, takeUntil, tap } from 'rxjs';
|
import { BehaviorSubject, map, Observable, of, shareReplay, Subject, takeUntil, tap } from 'rxjs';
|
||||||
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
||||||
|
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||||
@ -32,13 +33,27 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
breakpointSubject = new BehaviorSubject<Breakpoint>(1);
|
||||||
|
breakpoint$: Observable<Breakpoint> = this.breakpointSubject.asObservable();
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
@HostListener('window:orientationchange', ['$event'])
|
||||||
|
onResize() {
|
||||||
|
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get Breakpoint() { return Breakpoint; }
|
||||||
|
|
||||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
||||||
private metadataService: MetadataService, private modalService: NgbModal) {
|
private metadataService: MetadataService, private modalService: NgbModal, private utilityService: UtilityService) {
|
||||||
this.seriesImage = (data: PieDataItem) => {
|
this.seriesImage = (data: PieDataItem) => {
|
||||||
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
|
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
|
||||||
|
|
||||||
this.stats$ = this.statService.getServerStatistics().pipe(takeUntil(this.onDestroy), shareReplay());
|
this.stats$ = this.statService.getServerStatistics().pipe(takeUntil(this.onDestroy), shareReplay());
|
||||||
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntil(this.onDestroy));
|
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntil(this.onDestroy));
|
||||||
this.mostActiveUsers$ = this.stats$.pipe(
|
this.mostActiveUsers$ = this.stats$.pipe(
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<ng-container >
|
<ng-container >
|
||||||
<div class="col-auto mb-2">
|
<div class="col-auto mb-2">
|
||||||
<app-icon-and-title label="Time Spent Reading" [clickable]="false" fontClasses="fas fa-eye" title="Time Spent Reading">
|
<app-icon-and-title label="Time Spent Reading" [clickable]="false" fontClasses="fas fa-eye" title="Time Spent Reading">
|
||||||
{{timeSpentReading}} hours
|
{{timeSpentReading | compactNumber}} hours
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
</div>
|
</div>
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
@ -12,6 +12,7 @@ export interface ServerStatistics {
|
|||||||
totalGenres: number;
|
totalGenres: number;
|
||||||
totalTags: number;
|
totalTags: number;
|
||||||
totalPeople: number;
|
totalPeople: number;
|
||||||
|
totalReadingTime: number;
|
||||||
mostActiveUsers: Array<StatCount<User>>;
|
mostActiveUsers: Array<StatCount<User>>;
|
||||||
mostActiveLibraries: Array<StatCount<Library>>;
|
mostActiveLibraries: Array<StatCount<Library>>;
|
||||||
mostReadSeries: Array<StatCount<Series>>;
|
mostReadSeries: Array<StatCount<Series>>;
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<meta name="msapplication-TileColor" content="#4ac694">
|
<meta name="msapplication-TileColor" content="#4ac694">
|
||||||
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
|
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
|
||||||
<meta name="theme-color" content="#000000">
|
<meta name="theme-color" content="#000000">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
|
||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
//
|
//
|
||||||
:root, :root .default {
|
:root, :root .default {
|
||||||
--theme-color: #000000;
|
/* Base colors */
|
||||||
--color-scheme: dark;
|
|
||||||
--primary-color: #4ac694;
|
--primary-color: #4ac694;
|
||||||
--primary-color-dark-shade: #3B9E76;
|
--primary-color-dark-shade: #3B9E76;
|
||||||
--primary-color-darker-shade: #338A67;
|
--primary-color-darker-shade: #338A67;
|
||||||
@ -11,6 +10,11 @@
|
|||||||
--body-text-color: #efefef;
|
--body-text-color: #efefef;
|
||||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
--primary-color-scrollbar: rgba(74,198,148,0.75);
|
--primary-color-scrollbar: rgba(74,198,148,0.75);
|
||||||
|
|
||||||
|
/* Meta and Globals */
|
||||||
|
--theme-color: #000000;
|
||||||
|
--color-scheme: dark;
|
||||||
|
--tile-color: var(--primary-color);
|
||||||
|
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
@ -116,7 +120,7 @@
|
|||||||
|
|
||||||
|
|
||||||
/* List items */
|
/* List items */
|
||||||
--list-group-item-text-color: var(--body-text-color); /*rgba(74, 198, 148, 0.9)*/
|
--list-group-item-text-color: var(--body-text-color);
|
||||||
--list-group-item-bg-color: #343a40;
|
--list-group-item-bg-color: #343a40;
|
||||||
--list-group-item-border-color: rgba(239, 239, 239, 0.125);
|
--list-group-item-border-color: rgba(239, 239, 239, 0.125);
|
||||||
--list-group-hover-text-color: white;
|
--list-group-hover-text-color: white;
|
||||||
@ -176,6 +180,7 @@
|
|||||||
--ratingstar-star-filled: var(--primary-color);
|
--ratingstar-star-filled: var(--primary-color);
|
||||||
|
|
||||||
/* Global */
|
/* Global */
|
||||||
|
//--hr-color: transparent;
|
||||||
--hr-color: rgba(239, 239, 239, 0.125);
|
--hr-color: rgba(239, 239, 239, 0.125);
|
||||||
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||||
--accent-text-color: lightgrey;
|
--accent-text-color: lightgrey;
|
||||||
@ -207,7 +212,6 @@
|
|||||||
--manga-reader-overlay-filter: blur(10px);
|
--manga-reader-overlay-filter: blur(10px);
|
||||||
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
|
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
|
||||||
--manga-reader-overlay-text-color: white;
|
--manga-reader-overlay-text-color: white;
|
||||||
--manga-reader-bg-color: black; // TODO: Remove this
|
|
||||||
--manga-reader-next-highlight-bg-color: rgba(65, 225, 100, 0.5);
|
--manga-reader-next-highlight-bg-color: rgba(65, 225, 100, 0.5);
|
||||||
--manga-reader-prev-highlight-bg-color: rgba(65, 105, 225, 0.5);
|
--manga-reader-prev-highlight-bg-color: rgba(65, 105, 225, 0.5);
|
||||||
|
|
||||||
@ -242,7 +246,4 @@
|
|||||||
|
|
||||||
/* List Card Item */
|
/* List Card Item */
|
||||||
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
||||||
|
|
||||||
/* Bootstrap overrides */
|
|
||||||
--hr-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
40
openapi.json
40
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.6.1.26"
|
"version": "0.6.1.27"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -13201,6 +13201,40 @@
|
|||||||
"usingRestrictedProfiles": {
|
"usingRestrictedProfiles": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "If there is at least one user that is using an age restricted profile on the instance"
|
"description": "If there is at least one user that is using an age restricted profile on the instance"
|
||||||
|
},
|
||||||
|
"usersWithEmulateComicBook": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of users using the Emulate Comic Book setting",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"percentOfLibrariesWithFolderWatchingEnabled": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Percent (0.0-1.0) of libraries with folder watching enabled",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"percentOfLibrariesIncludedInSearch": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Percent (0.0-1.0) of libraries included in Search",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"percentOfLibrariesIncludedInRecommended": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Percent (0.0-1.0) of libraries included in Recommended",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"percentOfLibrariesIncludedInDashboard": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Percent (0.0-1.0) of libraries included in Dashboard",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"totalReadingHours": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Total reading hours of all users",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"storeCoversAsWebP": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is the Server saving covers as WebP"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@ -13323,6 +13357,10 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
|
"totalReadingTime": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"mostReadSeries": {
|
"mostReadSeries": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user