mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -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;
|
||||
|
||||
public abstract class BasicTest
|
||||
public abstract class AbstractDbTest
|
||||
{
|
||||
private readonly DbConnection _connection;
|
||||
protected readonly DbConnection _connection;
|
||||
protected readonly DataContext _context;
|
||||
protected readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
@ -30,8 +30,9 @@ public abstract class BasicTest
|
||||
protected const string LogDirectory = "C:/kavita/config/logs/";
|
||||
protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||
protected const string TempDirectory = "C:/kavita/config/temp/";
|
||||
protected const string DataDirectory = "C:/data/";
|
||||
|
||||
protected BasicTest()
|
||||
protected AbstractDbTest()
|
||||
{
|
||||
var contextOptions = new DbContextOptionsBuilder()
|
||||
.UseSqlite(CreateInMemoryDatabase())
|
||||
@ -50,13 +51,12 @@ public abstract class BasicTest
|
||||
private static DbConnection CreateInMemoryDatabase()
|
||||
{
|
||||
var connection = new SqliteConnection("Filename=:memory:");
|
||||
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async Task<bool> SeedDb()
|
||||
protected async Task<bool> SeedDb()
|
||||
{
|
||||
await _context.Database.MigrateAsync();
|
||||
var filesystem = CreateFileSystem();
|
||||
@ -91,14 +91,7 @@ public abstract class BasicTest
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
protected async Task ResetDb()
|
||||
{
|
||||
_context.Series.RemoveRange(_context.Series.ToList());
|
||||
_context.Users.RemoveRange(_context.Users.ToList());
|
||||
_context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
protected abstract Task ResetDb();
|
||||
|
||||
protected static MockFileSystem CreateFileSystem()
|
||||
{
|
||||
@ -111,7 +104,7 @@ public abstract class BasicTest
|
||||
fileSystem.AddDirectory(BookmarkDirectory);
|
||||
fileSystem.AddDirectory(LogDirectory);
|
||||
fileSystem.AddDirectory(TempDirectory);
|
||||
fileSystem.AddDirectory("C:/data/");
|
||||
fileSystem.AddDirectory(DataDirectory);
|
||||
|
||||
return fileSystem;
|
||||
}
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -28,70 +29,14 @@ using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
public class CleanupServiceTests
|
||||
public class CleanupServiceTests : AbstractDbTest
|
||||
{
|
||||
private readonly ILogger<CleanupService> _logger = Substitute.For<ILogger<CleanupService>>();
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||
|
||||
private readonly DbConnection _connection;
|
||||
private readonly DataContext _context;
|
||||
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
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()
|
||||
public CleanupServiceTests() : base()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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.Users.RemoveRange(_context.Users.ToList());
|
||||
@ -115,20 +62,6 @@ public class CleanupServiceTests
|
||||
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
|
||||
|
||||
#region DeleteSeriesCoverImages
|
||||
@ -142,7 +75,7 @@ public class CleanupServiceTests
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
var s = DbFactory.Series("Test 1");
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
@ -175,7 +108,7 @@ public class CleanupServiceTests
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
@ -209,7 +142,7 @@ public class CleanupServiceTests
|
||||
filesystem.AddFile($"{CoverImageDirectory}v01_c1000.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
@ -259,7 +192,7 @@ public class CleanupServiceTests
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
@ -307,7 +240,7 @@ public class CleanupServiceTests
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Users.Add(new AppUser()
|
||||
{
|
||||
@ -569,6 +502,62 @@ public class CleanupServiceTests
|
||||
}
|
||||
|
||||
#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
|
||||
//
|
||||
// [Fact]
|
||||
@ -579,7 +568,7 @@ public class CleanupServiceTests
|
||||
// filesystem.AddFile($"{BookmarkDirectory}1/1/1/0002.jpg", new MockFileData(""));
|
||||
//
|
||||
// // Delete all Series to reset state
|
||||
// await ResetDB();
|
||||
// await ResetDb();
|
||||
//
|
||||
// _context.Series.Add(new Series()
|
||||
// {
|
||||
@ -651,7 +640,7 @@ public class CleanupServiceTests
|
||||
// filesystem.AddFile($"{BookmarkDirectory}1/1/2/0002.jpg", new MockFileData(""));
|
||||
//
|
||||
// // Delete all Series to reset state
|
||||
// await ResetDB();
|
||||
// await ResetDb();
|
||||
//
|
||||
// _context.Series.Add(new Series()
|
||||
// {
|
||||
|
@ -12,20 +12,20 @@ using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
public class DeviceServiceTests : BasicTest
|
||||
public class DeviceServiceDbTests : AbstractDbTest
|
||||
{
|
||||
private readonly ILogger<DeviceService> _logger = Substitute.For<ILogger<DeviceService>>();
|
||||
private readonly IDeviceService _deviceService;
|
||||
|
||||
public DeviceServiceTests() : base()
|
||||
public DeviceServiceDbTests() : base()
|
||||
{
|
||||
_deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For<IEmailService>());
|
||||
}
|
||||
|
||||
protected new Task ResetDb()
|
||||
protected override async Task ResetDb()
|
||||
{
|
||||
_context.Users.RemoveRange(_context.Users.ToList());
|
||||
return Task.CompletedTask;
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1429,6 +1429,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstVolume_NoProgress()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1479,6 +1480,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1_WithProgress()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1524,6 +1526,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1596,6 +1599,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial2()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1674,6 +1678,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstSpecial()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1743,6 +1748,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenNonRead_LooseLeafChaptersAndVolumes()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1785,6 +1791,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesAndAFewLooseChaptersRead()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1851,6 +1858,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllRead()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1914,6 +1922,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllReadAndAllChapters()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1959,6 +1968,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstSpecial_WhenAllReadAndAllChapters()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -2020,6 +2030,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -2079,6 +2090,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkChaptersUntilAsRead_ShouldMarkAllChaptersAsRead()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -2121,6 +2133,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkChaptersUntilAsRead_ShouldMarkUptTillChapterNumberAsRead()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -2165,6 +2178,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkChaptersUntilAsRead_ShouldMarkAsRead_OnlyVolumesWithChapter0()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
|
@ -181,6 +181,96 @@ public class ReadingListServiceTests
|
||||
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
|
||||
|
||||
|
@ -28,80 +28,18 @@ using Xunit;
|
||||
|
||||
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 const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
private const string DataDirectory = "C:/data/";
|
||||
|
||||
public SeriesServiceTests()
|
||||
public SeriesServiceTests() : base()
|
||||
{
|
||||
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>(),
|
||||
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>());
|
||||
}
|
||||
#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;
|
||||
|
||||
_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()
|
||||
protected override async Task ResetDb()
|
||||
{
|
||||
_context.Series.RemoveRange(_context.Series.ToList());
|
||||
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
|
||||
@ -113,19 +51,6 @@ public class SeriesServiceTests
|
||||
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)
|
||||
{
|
||||
return new UpdateRelatedSeriesDto()
|
||||
@ -1465,7 +1390,7 @@ public class SeriesServiceTests
|
||||
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Library.Add(new Library()
|
||||
var lib = new Library()
|
||||
{
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
@ -1481,20 +1406,21 @@ public class SeriesServiceTests
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series Prequels",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series Sequels",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
_context.Library.Add(lib);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@ -1505,7 +1431,7 @@ public class SeriesServiceTests
|
||||
addRelationDto.Sequels.Add(3);
|
||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1);
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib.Id);
|
||||
_unitOfWork.LibraryRepository.Delete(library);
|
||||
|
||||
try
|
||||
@ -1524,7 +1450,7 @@ public class SeriesServiceTests
|
||||
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary_WhenSeriesCrossLibraries()
|
||||
{
|
||||
await ResetDb();
|
||||
_context.Library.Add(new Library()
|
||||
var lib1 = new Library()
|
||||
{
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
@ -1564,17 +1490,17 @@ public class SeriesServiceTests
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series Prequels",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series Sequels",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
};
|
||||
_context.Library.Add(lib1);
|
||||
var lib2 = new Library()
|
||||
{
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
@ -1590,20 +1516,21 @@ public class SeriesServiceTests
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series 2",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series Prequels 2",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Name = "Test Series Sequels 2",
|
||||
Volumes = new List<Volume>(){}
|
||||
Volumes = new List<Volume>() { }
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
_context.Library.Add(lib2);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@ -1613,7 +1540,7 @@ public class SeriesServiceTests
|
||||
addRelationDto.Adaptations.Add(4); // cross library link
|
||||
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);
|
||||
|
||||
try
|
||||
|
@ -32,7 +32,7 @@ public class ImageController : BaseApiController
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId"})]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
@ -48,7 +48,7 @@ public class ImageController : BaseApiController
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("library-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId"})]
|
||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
||||
@ -64,7 +64,7 @@ public class ImageController : BaseApiController
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId"})]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
@ -79,7 +79,7 @@ public class ImageController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Id of Series</param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"seriesId"})]
|
||||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||
{
|
||||
@ -98,7 +98,7 @@ public class ImageController : BaseApiController
|
||||
/// <param name="collectionTagId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("collection-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId"})]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||
@ -114,7 +114,7 @@ public class ImageController : BaseApiController
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("readinglist-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId"})]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int 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>
|
||||
/// <returns></returns>
|
||||
[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)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
@ -155,7 +155,7 @@ public class ImageController : BaseApiController
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpGet("cover-upload")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"filename"})]
|
||||
public ActionResult GetCoverUploadImage(string filename)
|
||||
{
|
||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||
|
@ -13,6 +13,7 @@ public class ServerStatisticsDto
|
||||
public long TotalGenres { get; set; }
|
||||
public long TotalTags { get; set; }
|
||||
public long TotalPeople { get; set; }
|
||||
public long TotalReadingTime { get; set; }
|
||||
public IEnumerable<ICount<SeriesDto>> MostReadSeries { get; set; }
|
||||
/// <summary>
|
||||
/// Total users who have started/reading/read per series
|
||||
|
@ -145,4 +145,39 @@ public class ServerInfoDto
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
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
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.Includes(includes)
|
||||
.OrderBy(s => s.SortName).ToListAsync();
|
||||
.OrderBy(s => s.SortName.ToLower())
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -223,7 +224,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(s => s.SortName);
|
||||
.OrderBy(s => s.SortName.ToLower());
|
||||
|
||||
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 => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.IsRestricted(QueryContext.Search)
|
||||
.OrderBy(l => l.Name)
|
||||
.OrderBy(l => l.Name.ToLower())
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
@ -335,7 +336,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Include(s => s.Library)
|
||||
.OrderBy(s => s.SortName)
|
||||
.OrderBy(s => s.SortName.ToLower())
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
@ -356,7 +357,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|| EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))
|
||||
.Where(c => c.Promoted || isAdmin)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.Title)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
@ -377,7 +378,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
@ -387,7 +388,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
@ -719,7 +720,8 @@ public class SeriesRepository : ISeriesRepository
|
||||
})
|
||||
.Where(s => s.PagesRead > 0
|
||||
&& 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)
|
||||
.Select(s => s.Series)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
@ -777,7 +779,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
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.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||
@ -790,7 +792,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
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.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||
@ -844,7 +846,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
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.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||
@ -856,7 +858,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
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.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||
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))
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.Title)
|
||||
.OrderBy(t => t.Title.ToLower())
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -911,7 +913,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.ThenInclude(m => m.Series)
|
||||
.SelectMany(c => c.SeriesMetadatas.Select(sm => sm.Series).Where(s => userLibraries.Contains(s.LibraryId)))
|
||||
.OrderBy(s => s.LibraryId)
|
||||
.ThenBy(s => s.SortName)
|
||||
.ThenBy(s => s.SortName.ToLower())
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
@ -941,7 +943,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
||||
.OrderBy(s => s.SortName)
|
||||
.OrderBy(s => s.SortName.ToLower())
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
|
@ -299,6 +299,8 @@ public class ArchiveService : IArchiveService
|
||||
try
|
||||
{
|
||||
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
||||
// Remove the folder as we have the zip
|
||||
_directoryService.ClearAndDeleteDirectory(tempLocation);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
|
@ -575,39 +575,20 @@ public class ReaderService : IReaderService
|
||||
{
|
||||
var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 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
|
||||
{
|
||||
MinHours = minHours,
|
||||
MaxHours = maxHours,
|
||||
MinHours = Math.Min(minHours, maxHours),
|
||||
MaxHours = Math.Max(minHours, maxHours),
|
||||
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour))
|
||||
};
|
||||
}
|
||||
|
||||
var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 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
|
||||
{
|
||||
MinHours = minHoursPages,
|
||||
MaxHours = maxHoursPages,
|
||||
MinHours = Math.Min(minHoursPages, maxHoursPages),
|
||||
MaxHours = Math.Max(minHoursPages, maxHoursPages),
|
||||
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
||||
};
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ public class ReadingListService : IReadingListService
|
||||
public async Task<bool> DeleteReadingListItem(UpdateReadingListPosition dto)
|
||||
{
|
||||
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;
|
||||
foreach (var readingListItem in readingList.Items)
|
||||
|
@ -32,6 +32,7 @@ public interface IStatisticService
|
||||
IEnumerable<StatCount<int>> GetPagesReadCountByYear(int userId = 0);
|
||||
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
|
||||
Task UpdateServerStatistics();
|
||||
Task<long> TimeSpentReadingForUsersAsync(IList<int> userIds, IList<int> libraryIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -62,18 +63,20 @@ public class StatisticService : IStatisticService
|
||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
||||
.SumAsync(p => p.PagesRead);
|
||||
|
||||
var ids = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
||||
.Where(p => p.PagesRead > 0)
|
||||
.Select(p => new {p.ChapterId, p.SeriesId})
|
||||
.ToListAsync();
|
||||
// var ids = await _context.AppUserProgresses
|
||||
// .Where(p => p.AppUserId == userId)
|
||||
// .Where(p => libraryIds.Contains(p.LibraryId))
|
||||
// .Where(p => p.PagesRead > 0)
|
||||
// .Select(p => new {p.ChapterId, p.SeriesId})
|
||||
// .ToListAsync();
|
||||
|
||||
var chapterIds = ids.Select(id => id.ChapterId);
|
||||
//var chapterIds = ids.Select(id => id.ChapterId);
|
||||
|
||||
var timeSpentReading = await _context.Chapter
|
||||
.Where(c => chapterIds.Contains(c.Id))
|
||||
.SumAsync(c => c.AvgHoursToRead);
|
||||
// var timeSpentReading = await _context.Chapter
|
||||
// .Where(c => chapterIds.Contains(c.Id))
|
||||
// .SumAsync(c => c.AvgHoursToRead);
|
||||
|
||||
var timeSpentReading = await TimeSpentReadingForUsersAsync(new List<int>() {userId}, libraryIds);
|
||||
|
||||
var totalWordsRead = (long) Math.Round(await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
@ -275,6 +278,8 @@ public class StatisticService : IStatisticService
|
||||
.Distinct()
|
||||
.Count();
|
||||
|
||||
|
||||
|
||||
return new ServerStatisticsDto()
|
||||
{
|
||||
ChapterCount = await _context.Chapter.CountAsync(),
|
||||
@ -289,7 +294,8 @@ public class StatisticService : IStatisticService
|
||||
MostActiveLibraries = mostActiveLibrary,
|
||||
MostPopularSeries = mostPopularSeries,
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
series.MinHoursToRead = seriesEstimate.MinHours;
|
||||
series.MaxHoursToRead = seriesEstimate.MaxHours;
|
||||
|
@ -25,18 +25,23 @@ public interface IStatsService
|
||||
Task<ServerInfoDto> GetServerInfo();
|
||||
Task SendCancellation();
|
||||
}
|
||||
/// <summary>
|
||||
/// This is for reporting to the stat server
|
||||
/// </summary>
|
||||
public class StatsService : IStatsService
|
||||
{
|
||||
private readonly ILogger<StatsService> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly DataContext _context;
|
||||
private readonly IStatisticService _statisticService;
|
||||
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;
|
||||
_unitOfWork = unitOfWork;
|
||||
_context = context;
|
||||
_statisticService = statisticService;
|
||||
|
||||
FlurlHttp.ConfigureClient(ApiUrl, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
@ -116,6 +121,14 @@ public class StatsService : IStatsService
|
||||
DotnetVersion = Environment.Version.ToString(),
|
||||
IsDocker = new OsInfo().IsDocker,
|
||||
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(),
|
||||
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
|
||||
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
|
||||
@ -127,6 +140,7 @@ public class StatsService : IStatsService
|
||||
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
|
||||
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
|
||||
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
|
||||
StoreCoversAsWebP = serverSettings.ConvertCoverToWebP,
|
||||
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||
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()
|
||||
{
|
||||
return _context.SeriesRelation.AnyAsync();
|
||||
@ -242,6 +280,7 @@ public class StatsService : IStatsService
|
||||
return await _context.AppUserPreferences.Select(p => p.PageSplitOption).Distinct().ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
private async Task<IEnumerable<LayoutMode>> AllMangaReaderLayoutModes()
|
||||
{
|
||||
return await _context.AppUserPreferences.Select(p => p.LayoutMode).Distinct().ToListAsync();
|
||||
|
@ -64,15 +64,23 @@ export class ThemeService implements OnDestroy {
|
||||
getColorScheme() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* --theme-color from theme. Updates the meta tag
|
||||
* @returns
|
||||
*/
|
||||
getThemeColor() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* --theme-color from theme. Updates the meta tag
|
||||
* @returns
|
||||
*/
|
||||
getThemeColor() {
|
||||
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) {
|
||||
return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
|
||||
}
|
||||
@ -155,6 +163,12 @@ export class ThemeService implements OnDestroy {
|
||||
const themeColor = this.getThemeColor();
|
||||
if (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();
|
||||
|
@ -973,11 +973,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
triggerSwipePagination(direction: KeyDirection) {
|
||||
|
||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
||||
if (direction === KeyDirection.Right)
|
||||
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
|
||||
}
|
||||
switch(direction) {
|
||||
case KeyDirection.Down:
|
||||
this.nextPage();
|
||||
@ -996,8 +991,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
onSwipeEnd(event: SwipeEvent) {
|
||||
const threshold = .12;
|
||||
|
||||
// Positive number means swiping right/down, negative means left
|
||||
switch (this.readerMode) {
|
||||
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
|
||||
if (direction === KeyDirection.Right) {
|
||||
this.hasHitZeroScroll = false;
|
||||
if (scrollLeft === 0 && this.ReadingAreaWidth === 0) {
|
||||
this.triggerSwipePagination(direction);
|
||||
return;
|
||||
}
|
||||
if (!this.hasHitRightScroll && this.checkIfPaginationAllowed(direction)) {
|
||||
this.hasHitRightScroll = true;
|
||||
return;
|
||||
@ -1036,7 +1033,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Next page triggered');
|
||||
this.triggerSwipePagination(direction);
|
||||
break;
|
||||
}
|
||||
@ -1072,32 +1068,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Next page triggered');
|
||||
this.triggerSwipePagination(direction);
|
||||
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 { ReaderService } from 'src/app/_services/reader.service';
|
||||
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';
|
||||
|
||||
@Injectable({
|
||||
|
@ -17,16 +17,7 @@
|
||||
</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 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>
|
||||
<div class="col-auto mb-2">
|
||||
<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
|
||||
</app-icon-and-title>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@ -91,28 +91,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-2 pb-2 ">
|
||||
<app-top-readers></app-top-readers>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<ng-container *ngIf="breakpoint$ | async as bp">
|
||||
<div class="row g-0 pt-2 pb-2" *ngIf="bp > Breakpoint.Mobile">
|
||||
<app-top-readers></app-top-readers>
|
||||
</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="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-day-breakdown></app-day-breakdown>
|
||||
<div class="row g-0 pt-4 pb-2" style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<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 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>
|
||||
|
@ -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 { 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 { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { ImageService } from 'src/app/_services/image.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]);
|
||||
}
|
||||
|
||||
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,
|
||||
private metadataService: MetadataService, private modalService: NgbModal) {
|
||||
private metadataService: MetadataService, private modalService: NgbModal, private utilityService: UtilityService) {
|
||||
this.seriesImage = (data: PieDataItem) => {
|
||||
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
|
||||
return '';
|
||||
}
|
||||
|
||||
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
|
||||
|
||||
this.stats$ = this.statService.getServerStatistics().pipe(takeUntil(this.onDestroy), shareReplay());
|
||||
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntil(this.onDestroy));
|
||||
this.mostActiveUsers$ = this.stats$.pipe(
|
||||
|
@ -20,7 +20,7 @@
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
|
@ -12,6 +12,7 @@ export interface ServerStatistics {
|
||||
totalGenres: number;
|
||||
totalTags: number;
|
||||
totalPeople: number;
|
||||
totalReadingTime: number;
|
||||
mostActiveUsers: Array<StatCount<User>>;
|
||||
mostActiveLibraries: Array<StatCount<Library>>;
|
||||
mostReadSeries: Array<StatCount<Series>>;
|
||||
|
@ -13,7 +13,7 @@
|
||||
<meta name="msapplication-TileColor" content="#4ac694">
|
||||
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
@ -1,7 +1,6 @@
|
||||
//
|
||||
:root, :root .default {
|
||||
--theme-color: #000000;
|
||||
--color-scheme: dark;
|
||||
/* Base colors */
|
||||
--primary-color: #4ac694;
|
||||
--primary-color-dark-shade: #3B9E76;
|
||||
--primary-color-darker-shade: #338A67;
|
||||
@ -11,6 +10,11 @@
|
||||
--body-text-color: #efefef;
|
||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
--primary-color-scrollbar: rgba(74,198,148,0.75);
|
||||
|
||||
/* Meta and Globals */
|
||||
--theme-color: #000000;
|
||||
--color-scheme: dark;
|
||||
--tile-color: var(--primary-color);
|
||||
|
||||
|
||||
/* Navbar */
|
||||
@ -116,7 +120,7 @@
|
||||
|
||||
|
||||
/* 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-border-color: rgba(239, 239, 239, 0.125);
|
||||
--list-group-hover-text-color: white;
|
||||
@ -176,6 +180,7 @@
|
||||
--ratingstar-star-filled: var(--primary-color);
|
||||
|
||||
/* Global */
|
||||
//--hr-color: transparent;
|
||||
--hr-color: rgba(239, 239, 239, 0.125);
|
||||
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||
--accent-text-color: lightgrey;
|
||||
@ -207,7 +212,6 @@
|
||||
--manga-reader-overlay-filter: blur(10px);
|
||||
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
|
||||
--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-prev-highlight-bg-color: rgba(65, 105, 225, 0.5);
|
||||
|
||||
@ -242,7 +246,4 @@
|
||||
|
||||
/* 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%);
|
||||
|
||||
/* Bootstrap overrides */
|
||||
--hr-color: transparent;
|
||||
}
|
||||
|
40
openapi.json
40
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.6.1.26"
|
||||
"version": "0.6.1.27"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -13201,6 +13201,40 @@
|
||||
"usingRestrictedProfiles": {
|
||||
"type": "boolean",
|
||||
"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,
|
||||
@ -13323,6 +13357,10 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"totalReadingTime": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"mostReadSeries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user