Book Reader Redesign with e-ink focus (#1246)

* Refactored the drawer into offcanvas component. Had to write some hacks to emulate how bootstrap's javascript implementation works as ngBootstrap doesn't have a component yet.

* Cleaned up some of the code

* Rewrote drawer to align it with the new design

* First pass, refactored table of content into it's own component

* Refactored all of the settings logic into a separate component. Everything is broken.

* More settings on on reactive form

* More code cleanup on settings

* Misc fixes around the drawer code. Fixed a bug where range sliders were inheriting background color of normal text inputs

* Fixed dark mode with book reader.

We now clear the theme from the main app so book reader is self-contained. Styles for dark mode are injected into the reading-section. Styles that were previously in scss are now only for the actual menu system.

* Cleaned up drawer styling on header

* Removed an ngIf statement for click to paginate

* Tweaked the accent style to have smaller font size and adjusted style on light mode. Cleaned up some clearTimeout code in a further effort to streamline codebase.

* Refactored Dark mode into a basic theme. Currently styles are hardcoded.

* Patched book theme in from themes branch

* Patched in the backend for Book Theme (not tested yet)

* Fixed a bug in seeding code for book themes. Started integration of themes into the reader settings

* Everything except managing themes is working. Themes are a bit shakey, having second thoughts if we should have them or not.

* Reverted the ability to do custom user book themes. Code is stable with system themes.

* Stablize the Styles (#1128)

* Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful.

* When a series has a reading list, we now show the connection on Series detail.

* Removed all baseurl code from UI and not-connected component since we no longer use it.

* Fixed tag badges not showing a border. Added last read time to the series detail page

* Fixed up error interceptor to remove no-connection code

* Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI.

* Cleaned up the selected tab and tab display logic

* Fixed a bad where statement in reading lists for series

* Fixed up tab logic again

* Fixed a small margin on search backdrop

* Made badge expander button smaller to align with badges

* Fixed a few UIs due to .form-group and .form-row being removed

* Updated Theme component page to help with style testing

* Added more components to theme tester

* Cleaned up some styling

* Fixed opacity on search item hover

* Bump versions by dotnet-bump-version.

* Tweaked the accordion styles for light mode

* Set dark book theme as default. Refactored resetSettings to be much cleaner

* Started the refactor to allow book themes to affect global css variables

* Fixed some issues with my css variable declarations

* Fixed a close model state update

* Lots of work, but dark mode on the book reader is basically done. We have to code the themes much like the site themes

* Some black theme enhancements

* Started working on column layout in book reader.

* Cleaned up the CSS on Reader Settings

* Hooked up reading direction

* Got column and double column layout working

* Implemented some basic virtual paging and hooked in book color theme and layout mode into user preferences.

* Migration wrote, can edit page layout and color theme on book reader. Removed book dark mode since no longer needed.

Fixed a bug on login/register forms where when input is focused, text is white and not black.

* When loading book reader, apply column layout.

* Lots of work around 2 column layout, working on images not splitting. Still not working, committing so i can merge develop in and validate code with new manga reader.

* Fixed images being split into 2 BUT regression on each page boundary, total reading height is smaller and smaller

* Fixed some rendering bugs where toggling column layouts would shrink images on screen constantly.

Fixed a bug where bottom bar wouldn't render on column layout in some conditions (this might need to be reworked)

* Started progress on progress work

* Updated .NET to 6.0.4

* Fixed a bug where DataContextModelSnapshot was being removed on build thus new migrations were broken.

* Tweaked the code around progress saving so that we don't loose track of last scroll element on page load

* Trying to restore progress, but stuck

* Extra merge stuff

* Fixed a bug where volumes that are a range fail to generate series detail

* No gutters on whole app. Book reader backend now applies the image class automatically at the backend.

* Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service.

* Removed bottom padding

* Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken.

* Fixed the height of the action bar to simplify logic and keep the code cleaner. Refactored book service image scoping to be much more streamlined and efficient

* Fixed the height of action bar to 62px and adjusted code to use the hardcoded px. (code commented)

* Removed commented out code from fixed action bar height

* Progress restoration seems to be working

* Code cleanup

* Ensure the bottom action bar is at the bottom of the viewport on small pages

* Fixed book fonts not setting properly and added OpenDyslexic font.

* Fixed up some font issues

* Updated drawer so all sections are open by default

* Switched some LINQ to use MinBy

* When navigating between pages and column layout, adjust the shift for the user.

* Removed some debug code

* Blacklist .qpkg folders and don't scan Recently-Snapshot or recycle folders.

* Renamed the scale width to be scoped to kavita to avoid conflicts.

* Refactored ngx-sliders out to use normal range instead. Changed up the preferences to separate image and book settinngs into own accordion.

* updated user preferences for new migration options (not committed yet)

* Removed some debug code

* Remove console.logs

* Migration committed, let's release this to users.

* A lot of crazy code just to ensure that when you close drawer the toggle reflectst that state.
This commit is contained in:
Joseph Milazzo 2022-05-08 19:52:15 -05:00 committed by GitHub
parent 641e0a71e9
commit 2723a6cd10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3708 additions and 1190 deletions

View file

@ -94,16 +94,12 @@
</ItemGroup>
<ItemGroup>
<Compile Remove="Interfaces\IMetadataService.cs" />
<Compile Remove="obj\**" />
<Compile Remove="cache\**" />
<Compile Remove="backups\**" />
<Compile Remove="logs\**" />
<Compile Remove="temp\**" />
<Compile Remove="covers\**" />
<Compile Remove="DTOs\Email\SmtpConfig.cs" />
<Compile Remove="DTOs\Email\EmailOptionsDto.cs" />
<Compile Remove="Helpers\Converters\SmtpConverter.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Theme;
using API.Extensions;
using API.Services;
using API.Services.Tasks;
using Kavita.Common;
@ -13,13 +14,13 @@ namespace API.Controllers;
public class ThemeController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ISiteThemeService _siteThemeService;
private readonly IThemeService _themeService;
private readonly ITaskScheduler _taskScheduler;
public ThemeController(IUnitOfWork unitOfWork, ISiteThemeService siteThemeService, ITaskScheduler taskScheduler)
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
{
_unitOfWork = unitOfWork;
_siteThemeService = siteThemeService;
_themeService = themeService;
_taskScheduler = taskScheduler;
}
@ -39,9 +40,9 @@ public class ThemeController : BaseApiController
[Authorize("RequireAdminRole")]
[HttpPost("update-default")]
public async Task<ActionResult> UpdateDefault(UpdateDefaultSiteThemeDto dto)
public async Task<ActionResult> UpdateDefault(UpdateDefaultThemeDto dto)
{
await _siteThemeService.UpdateDefault(dto.ThemeId);
await _themeService.UpdateDefault(dto.ThemeId);
return Ok();
}
@ -54,7 +55,7 @@ public class ThemeController : BaseApiController
{
try
{
return Ok(await _siteThemeService.GetContent(themeId));
return Ok(await _themeService.GetContent(themeId));
}
catch (KavitaException ex)
{

View file

@ -82,11 +82,12 @@ namespace API.Controllers
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode;
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
// TODO: Remove this code - this overrides layout mode to be single until the mode is released

View file

@ -4,6 +4,9 @@ using API.Services;
namespace API.DTOs.Theme;
/// <summary>
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
/// </summary>
public class SiteThemeDto
{
public int Id { get; set; }

View file

@ -1,6 +1,6 @@
namespace API.DTOs.Theme;
public class UpdateDefaultSiteThemeDto
public class UpdateDefaultThemeDto
{
public int ThemeId { get; set; }
}

View file

@ -1,4 +1,5 @@
using API.Entities;
using API.DTOs.Theme;
using API.Entities;
using API.Entities.Enums;
namespace API.DTOs
@ -74,5 +75,7 @@ namespace API.DTOs
/// </summary>
/// <remarks>Should default to Dark</remarks>
public SiteTheme Theme { get; set; }
public string BookReaderThemeName { get; set; }
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
}
}

View file

@ -44,33 +44,40 @@ namespace API.Data
public DbSet<SeriesRelation> SeriesRelation { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(modelBuilder);
base.OnModelCreating(builder);
modelBuilder.Entity<AppUser>()
builder.Entity<AppUser>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
modelBuilder.Entity<AppRole>()
builder.Entity<AppRole>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
modelBuilder.Entity<SeriesRelation>()
builder.Entity<SeriesRelation>()
.HasOne(pt => pt.Series)
.WithMany(p => p.Relations)
.HasForeignKey(pt => pt.SeriesId)
.OnDelete(DeleteBehavior.ClientCascade);
modelBuilder.Entity<SeriesRelation>()
builder.Entity<SeriesRelation>()
.HasOne(pt => pt.TargetSeries)
.WithMany(t => t.RelationOf)
.HasForeignKey(pt => pt.TargetSeriesId);
builder.Entity<AppUserPreferences>()
.Property(b => b.BookThemeName)
.HasDefaultValue("Dark");
builder.Entity<AppUserPreferences>()
.Property(b => b.BackgroundColor)
.HasDefaultValue("#000000");
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class BookReaderUpdate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "BookReaderDarkMode",
table: "AppUserPreferences",
newName: "PageLayoutMode");
migrationBuilder.AlterColumn<string>(
name: "BackgroundColor",
table: "AppUserPreferences",
type: "TEXT",
nullable: true,
defaultValue: "#000000",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AddColumn<string>(
name: "BookThemeName",
table: "AppUserPreferences",
type: "TEXT",
nullable: true,
defaultValue: "Dark");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BookThemeName",
table: "AppUserPreferences");
migrationBuilder.RenameColumn(
name: "PageLayoutMode",
table: "AppUserPreferences",
newName: "BookReaderDarkMode");
migrationBuilder.AlterColumn<string>(
name: "BackgroundColor",
table: "AppUserPreferences",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true,
oldDefaultValue: "#000000");
}
}
}

View file

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -166,10 +166,9 @@ namespace API.Data.Migrations
.HasColumnType("INTEGER");
b.Property<string>("BackgroundColor")
.HasColumnType("TEXT");
b.Property<bool>("BookReaderDarkMode")
.HasColumnType("INTEGER");
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("#000000");
b.Property<string>("BookReaderFontFamily")
.HasColumnType("TEXT");
@ -189,9 +188,17 @@ namespace API.Data.Migrations
b.Property<bool>("BookReaderTapToPaginate")
.HasColumnType("INTEGER");
b.Property<string>("BookThemeName")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("Dark");
b.Property<int>("LayoutMode")
.HasColumnType("INTEGER");
b.Property<int>("PageLayoutMode")
.HasColumnType("INTEGER");
b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER");

View file

@ -19,7 +19,6 @@ public interface ISiteThemeRepository
Task<SiteThemeDto> GetThemeDtoByName(string themeName);
Task<SiteTheme> GetDefaultTheme();
Task<IEnumerable<SiteTheme>> GetThemes();
Task<SiteTheme> GetThemeById(int themeId);
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;

View file

@ -25,6 +25,7 @@ namespace API.Entities
/// </example>
/// </summary>
public ReaderMode ReaderMode { get; set; }
/// <summary>
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
/// </summary>
@ -42,10 +43,6 @@ namespace API.Entities
/// </summary>
public string BackgroundColor { get; set; } = "#000000";
/// <summary>
/// Book Reader Option: Should the background color be dark
/// </summary>
public bool BookReaderDarkMode { get; set; } = true;
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
public int BookReaderMargin { get; set; } = 15;
@ -74,7 +71,17 @@ namespace API.Entities
/// </summary>
/// <remarks>Should default to Dark</remarks>
public SiteTheme Theme { get; set; }
/// <summary>
/// Book Reader Option: The color theme to decorate the book contents
/// </summary>
/// <remarks>Should default to Dark</remarks>
public string BookThemeName { get; set; } = "Dark";
/// <summary>
/// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height,
/// 2 column is fit to height, 2 columns
/// </summary>
/// <remarks>Defaults to Default</remarks>
public BookPageLayoutMode PageLayoutMode { get; set; } = BookPageLayoutMode.Default;
public AppUser AppUser { get; set; }

View file

@ -0,0 +1,13 @@
using System.ComponentModel;
namespace API.Entities.Enums;
public enum BookPageLayoutMode
{
[Description("Default")]
Default = 0,
[Description("1 Column")]
Column1 = 1,
[Description("2 Column")]
Column2 = 2
}

View file

@ -0,0 +1,15 @@
using API.Entities.Enums.Theme;
namespace API.Entities.Interfaces;
/// <summary>
/// A theme in some kind
/// </summary>
public interface ITheme
{
public string Name { get; set; }
public string NormalizedName { get; set; }
public string FileName { get; set; }
public bool IsDefault { get; set; }
public ThemeProvider Provider { get; set; }
}

View file

@ -8,7 +8,7 @@ namespace API.Entities;
/// <summary>
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
/// </summary>
public class SiteTheme : IEntityDate
public class SiteTheme : IEntityDate, ITheme
{
public int Id { get; set; }
/// <summary>
@ -23,6 +23,7 @@ public class SiteTheme : IEntityDate
/// File path to the content. Stored under <see cref="DirectoryService.SiteThemeDirectory"/>.
/// Must be a .css file
/// </summary>
/// <remarks>System provided themes use an alternative location as they are packaged with the app</remarks>
public string FileName { get; set; }
/// <summary>
/// Only one theme can have this. Will auto-set this as default for new user accounts

View file

@ -40,7 +40,7 @@ namespace API.Extensions
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IBookmarkService, BookmarkService>();
services.AddScoped<ISiteThemeService, SiteThemeService>();
services.AddScoped<IThemeService, ThemeService>();
services.AddScoped<ISeriesService, SeriesService>();

View file

@ -107,7 +107,13 @@ namespace API.Helpers
CreateMap<AppUserPreferences, UserPreferencesDto>()
.ForMember(dest => dest.Theme,
opt =>
opt.MapFrom(src => src.Theme));
opt.MapFrom(src => src.Theme))
.ForMember(dest => dest.BookReaderThemeName,
opt =>
opt.MapFrom(src => src.BookThemeName))
.ForMember(dest => dest.BookReaderLayoutMode,
opt =>
opt.MapFrom(src => src.PageLayoutMode));
CreateMap<AppUserBookmark, BookmarkDto>();

View file

@ -43,7 +43,7 @@ namespace API.Parser
MatchOptions, RegexTimeout);
private static readonly string XmlRegexExtensions = @"\.xml";
private const string XmlRegexExtensions = @"\.xml";
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
MatchOptions, RegexTimeout);
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
@ -999,7 +999,7 @@ namespace API.Parser
public static bool HasBlacklistedFolderInPath(string path)
{
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._");
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._") || path.Contains(".qpkg");
}

View file

@ -183,6 +183,7 @@ namespace API.Services
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
// Check if there are any background images and rewrite those urls
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
@ -246,67 +247,62 @@ namespace API.Services
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
{
var images = doc.DocumentNode.SelectNodes("//img");
if (images != null)
if (images == null) return;
foreach (var image in images)
{
foreach (var image in images)
if (image.Name != "img") continue;
string key = null;
if (image.Attributes["src"] != null)
{
if (image.Name != "img") continue;
key = "src";
}
else if (image.Attributes["xlink:href"] != null)
{
key = "xlink:href";
}
// Need to do for xlink:href
if (image.Attributes["src"] != null)
if (string.IsNullOrEmpty(key)) continue;
var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
image.Attributes.Remove(key);
image.Attributes.Add(key, $"{apiBase}" + imageFile);
// Add a custom class that the reader uses to ensure images stay within reader
image.AddClass("kavita-scale-width");
}
}
/// <summary>
/// Returns the image key associated with the file. Contains some basic fallback logic.
/// </summary>
/// <param name="book"></param>
/// <param name="imageFile"></param>
/// <returns></returns>
private static string GetKeyForImage(EpubBookRef book, string imageFile)
{
if (!book.Content.Images.ContainsKey(imageFile))
{
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
imageFile = correctedKey;
}
else if (imageFile.StartsWith(".."))
{
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
correctedKey =
book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
if (correctedKey != null)
{
var imageFile = image.Attributes["src"].Value;
if (!book.Content.Images.ContainsKey(imageFile))
{
// TODO: Refactor the Key code to a method to allow the hacks to be tested
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
imageFile = correctedKey;
} else if (imageFile.StartsWith(".."))
{
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
if (correctedKey != null)
{
imageFile = correctedKey;
}
}
}
image.Attributes.Remove("src");
image.Attributes.Add("src", $"{apiBase}" + imageFile);
imageFile = correctedKey;
}
}
}
images = doc.DocumentNode.SelectNodes("//image");
if (images != null)
{
foreach (var image in images)
{
if (image.Name != "image") continue;
if (image.Attributes["xlink:href"] != null)
{
var imageFile = image.Attributes["xlink:href"].Value;
if (!book.Content.Images.ContainsKey(imageFile))
{
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
imageFile = correctedKey;
}
}
image.Attributes.Remove("xlink:href");
image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile);
}
}
}
return imageFile;
}
private static string PrepareFinalHtml(HtmlDocument doc, HtmlNode body)

View file

@ -69,7 +69,7 @@ namespace API.Services
private readonly ILogger<DirectoryService> _logger;
private static readonly Regex ExcludeDirectories = new Regex(
@"@eaDir|\.DS_Store|\.qpkg",
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);

View file

@ -62,7 +62,7 @@ public class MetadataService : IMetadataService
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
private async Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate)
{
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
return false;
@ -97,12 +97,13 @@ public class MetadataService : IMetadataService
null, volume.Created, forceUpdate)) return false;
volume.Chapters ??= new List<Chapter>();
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).FirstOrDefault();
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
if (firstChapter == null) return false;
volume.CoverImage = firstChapter.CoverImage;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false);
return true;
}
@ -133,8 +134,7 @@ public class MetadataService : IMetadataService
if (!_cacheHelper.CoverImageExists(coverImage))
{
coverImage = series.Volumes[0].Chapters.OrderBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)
.FirstOrDefault()?.CoverImage;
coverImage = series.Volumes[0].Chapters.MinBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)?.CoverImage;
}
}
series.CoverImage = firstCover?.CoverImage ?? coverImage;

View file

@ -36,7 +36,7 @@ public class TaskScheduler : ITaskScheduler
private readonly IStatsService _statsService;
private readonly IVersionUpdaterService _versionUpdaterService;
private readonly ISiteThemeService _siteThemeService;
private readonly IThemeService _themeService;
public static BackgroundJobServer Client => new BackgroundJobServer();
private static readonly Random Rnd = new Random();
@ -45,7 +45,7 @@ public class TaskScheduler : ITaskScheduler
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
ISiteThemeService siteThemeService)
IThemeService themeService)
{
_cacheService = cacheService;
_logger = logger;
@ -56,7 +56,7 @@ public class TaskScheduler : ITaskScheduler
_cleanupService = cleanupService;
_statsService = statsService;
_versionUpdaterService = versionUpdaterService;
_siteThemeService = siteThemeService;
_themeService = themeService;
}
public async Task ScheduleTasks()
@ -131,7 +131,7 @@ public class TaskScheduler : ITaskScheduler
public void ScanSiteThemes()
{
_logger.LogInformation("Starting Site Theme scan");
BackgroundJob.Enqueue(() => _siteThemeService.Scan());
BackgroundJob.Enqueue(() => _themeService.Scan());
}
#endregion
@ -149,6 +149,7 @@ public class TaskScheduler : ITaskScheduler
public void ScanLibrary(int libraryId)
{
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
// TODO: If a library scan is already queued up for libraryId, don't do anything
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId));
// When we do a scan, force cache to re-unpack in case page numbers change
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());

View file

@ -301,6 +301,9 @@ public class ScannerService : IScannerService
await CleanupDbEntities();
// await _eventHub.SendMessageAsync(SignalREvents.NotificationProgress,
// MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
}
@ -712,7 +715,7 @@ public class ScannerService : IScannerService
}
}
// BUG: The issue here is that people is just from chapter, but series metadata might already have some people on it
// NOTE: The issue here is that people is just from chapter, but series metadata might already have some people on it
// I might be able to filter out people that are in locked fields?
var people = chapters.SelectMany(c => c.People).ToList();
PersonHelper.KeepOnlySamePeopleBetweenLists(series.Metadata.People,

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
@ -7,24 +6,23 @@ using API.Entities;
using API.Entities.Enums.Theme;
using API.SignalR;
using Kavita.Common;
using Microsoft.AspNetCore.SignalR;
namespace API.Services.Tasks;
public interface ISiteThemeService
public interface IThemeService
{
Task<string> GetContent(int themeId);
Task Scan();
Task UpdateDefault(int themeId);
}
public class SiteThemeService : ISiteThemeService
public class ThemeService : IThemeService
{
private readonly IDirectoryService _directoryService;
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
public SiteThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IEventHub eventHub)
public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IEventHub eventHub)
{
_directoryService = directoryService;
_unitOfWork = unitOfWork;
@ -36,7 +34,6 @@ public class SiteThemeService : ISiteThemeService
/// </summary>
/// <param name="themeId"></param>
/// <returns></returns>
/// <exception cref="KavitaException"></exception>
public async Task<string> GetContent(int themeId)
{
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
@ -55,7 +52,8 @@ public class SiteThemeService : ISiteThemeService
{
_directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory);
var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList();
var themeFiles = _directoryService.GetFilesWithExtension(Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css")
var themeFiles = _directoryService
.GetFilesWithExtension(Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css")
.Where(name => !reservedNames.Contains(Parser.Parser.Normalize(name))).ToList();
var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList();
@ -91,7 +89,8 @@ public class SiteThemeService : ISiteThemeService
});
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(themeFile), themeName, ProgressEventType.Updated));
MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(themeFile), themeName,
ProgressEventType.Updated));
}
@ -116,10 +115,10 @@ public class SiteThemeService : ISiteThemeService
}
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.SiteThemeProgressEvent("", "", ProgressEventType.Ended));
MessageFactory.SiteThemeProgressEvent("", "", ProgressEventType.Ended));
}
/// <summary>
/// Removes the theme and any references to it from Pref and sets them to the default at the time.
/// This commits to DB.

View file

@ -66,6 +66,10 @@ namespace API.SignalR
/// </summary>
private const string SiteThemeProgress = "SiteThemeProgress";
/// <summary>
/// A custom book theme was removed or added
/// </summary>
private const string BookThemeProgress = "BookThemeProgress";
/// <summary>
/// A type of event that has progress (determinate or indeterminate).
/// The underlying event will have a name to give details on how to handle.
/// </summary>
@ -367,5 +371,21 @@ namespace API.SignalR
}
};
}
public static SignalRMessage BookThemeProgressEvent(string subtitle, string themeName, string eventType)
{
return new SignalRMessage()
{
Name = BookThemeProgress,
Title = "Scanning Book Theme",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
ThemeName = themeName,
}
};
}
}
}