PDF Reader Settings, New Reading Modes, and lots of fixes (#2828)

Co-authored-by: Elry <144011449+ElryWeeb@users.noreply.github.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: William Brockhus <pickeringw@gmail.com>
Co-authored-by: Shivam Amin <xShivam.Amin@gmail.com>
This commit is contained in:
Joe Milazzo 2024-03-30 15:07:03 -05:00 committed by GitHub
parent f22f30b5a9
commit 2bde0ac82a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 4410 additions and 439 deletions

View file

@ -69,7 +69,7 @@
<PackageReference Include="Hangfire.InMemory" Version="0.8.1" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.60" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
@ -81,8 +81,8 @@
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="2.4.0" />
<PackageReference Include="NetVips.Native" Version="8.15.1" />
<PackageReference Include="NetVips" Version="2.4.1" />
<PackageReference Include="NetVips.Native" Version="8.15.2" />
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
@ -95,14 +95,14 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.0.88079">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.28" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.0" />
<PackageReference Include="System.IO.Abstractions" Version="21.0.2" />
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup>

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.DTOs.Uploads;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.SignalR;
@ -98,6 +99,7 @@ public class UploadController : BaseApiController
try
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
@ -225,17 +227,14 @@ public class UploadController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
}
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename)
{
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
if (thumbnailSize > 0)
{
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, encodeFormat, thumbnailSize);
}
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var encodeFormat = settings.EncodeMediaAs;
var coverImageSize = settings.CoverImageSize;
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, encodeFormat);
filename, encodeFormat, coverImageSize.GetDimensions().Width);
}
/// <summary>
@ -326,8 +325,7 @@ public class UploadController : BaseApiController
try
{
var filePath = await CreateThumbnail(uploadFileDto,
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}",
ImageService.LibraryThumbnailWidth);
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
{

View file

@ -118,6 +118,12 @@ public class UsersController : BaseApiController
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
existingPreferences.PdfTheme = preferencesDto.PdfTheme;
existingPreferences.PdfLayoutMode = preferencesDto.PdfLayoutMode;
existingPreferences.PdfScrollMode = preferencesDto.PdfScrollMode;
existingPreferences.PdfSpreadMode = preferencesDto.PdfSpreadMode;
if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
{
existingPreferences.Locale = preferencesDto.Locale;

View file

@ -152,4 +152,25 @@ public class UserPreferencesDto
/// </summary>
[Required]
public string Locale { get; set; }
/// <summary>
/// PDF Reader: Theme of the Reader
/// </summary>
[Required]
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
/// <summary>
/// PDF Reader: Scroll mode of the reader
/// </summary>
[Required]
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
/// <summary>
/// PDF Reader: Layout Mode of the reader
/// </summary>
[Required]
public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
/// <summary>
/// PDF Reader: Spread Mode of the reader
/// </summary>
[Required]
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PdfSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PdfLayoutMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfScrollMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfSpreadMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfTheme",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PdfLayoutMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfScrollMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfSpreadMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfTheme",
table: "AppUserPreferences");
}
}
}

View file

@ -355,6 +355,18 @@ namespace API.Data.Migrations
b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER");
b.Property<int>("PdfLayoutMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfScrollMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfSpreadMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfTheme")
.HasColumnType("INTEGER");
b.Property<bool>("PromptForDownloadSize")
.HasColumnType("INTEGER");

View file

@ -7,6 +7,9 @@ namespace API.Entities;
public class AppUserPreferences
{
public int Id { get; set; }
#region MangaReader
/// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary>
@ -51,6 +54,11 @@ public class AppUserPreferences
/// Manga Reader Option: Should swiping trigger pagination
/// </summary>
public bool SwipeToPaginate { get; set; }
#endregion
#region EpubReader
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
@ -75,17 +83,11 @@ public class AppUserPreferences
/// Book Reader Option: What direction should the next/prev page buttons go
/// </summary>
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary>
/// Book Reader Option: Defines the writing styles vertical/horizontal
/// </summary>
public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal;
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0];
/// <summary>
/// Book Reader Option: The color theme to decorate the book contents
/// </summary>
/// <remarks>Should default to Dark</remarks>
@ -101,6 +103,37 @@ public class AppUserPreferences
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false;
#endregion
#region PdfReader
/// <summary>
/// PDF Reader: Theme of the Reader
/// </summary>
public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark;
/// <summary>
/// PDF Reader: Scroll mode of the reader
/// </summary>
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
/// <summary>
/// PDF Reader: Layout Mode of the reader
/// </summary>
public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
/// <summary>
/// PDF Reader: Spread Mode of the reader
/// </summary>
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;
#endregion
#region Global
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0];
/// <summary>
/// Global Site Option: If the UI should layout items as Cards or List items
/// </summary>
@ -132,6 +165,8 @@ public class AppUserPreferences
/// </summary>
public string Locale { get; set; }
#endregion
public AppUser AppUser { get; set; } = null!;
public int AppUserId { get; set; }
}

View file

@ -0,0 +1,21 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
public enum PdfLayoutMode
{
/// <summary>
/// Multiple pages render stacked (normal pdf experience)
/// </summary>
[Description("Multiple")]
Multiple = 0,
// [Description("Single")]
// Single = 1,
/// <summary>
/// A book mode where page turns are animated and layout is side-by-side
/// </summary>
[Description("Book")]
Book = 2,
// [Description("Infinite Scroll")]
// InfiniteScroll = 3
}

View file

@ -0,0 +1,21 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
/// <summary>
/// Enum values match PdfViewer's enums
/// </summary>
public enum PdfScrollMode
{
[Description("Vertical")]
Vertical = 0,
[Description("Horizontal")]
Horizontal = 1,
// [Description("Wrapped")]
// Wrapped = 2,
/// <summary>
/// Single page view (tap to pagninate)
/// </summary>
[Description("Page")]
Page = 3
}

View file

@ -0,0 +1,13 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
public enum PdfSpreadMode
{
[Description("None")]
None = 0,
[Description("Odd")]
Odd = 1,
[Description("Even")]
Even = 2
}

View file

@ -0,0 +1,11 @@
using System.ComponentModel;
namespace API.Entities.Enums.UserPreferences;
public enum PdfTheme
{
[Description("Dark")]
Dark = 0,
[Description("Light")]
Light = 1
}

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using API.Entities;
using API.Helpers;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;
namespace API.Extensions;
@ -24,6 +25,7 @@ public static class ChapterListExtensions
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
/// </summary>
/// <remarks>This uses GetNumberTitle() to calculate the Range to compare against the info.Chapters</remarks>
/// <param name="chapters"></param>
/// <param name="info"></param>
/// <returns></returns>
@ -31,9 +33,12 @@ public static class ChapterListExtensions
{
var normalizedPath = Parser.NormalizePath(info.FullFilePath);
var specialTreatment = info.IsSpecialInfo();
// NOTE: This can fail to find the chapter when Range is "1.0" as the chapter will store it as "1" hence why we need to emulate a Chapter
var fakeChapter = new ChapterBuilder(info.Chapters, info.Chapters).Build();
fakeChapter.UpdateFrom(info);
return specialTreatment
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
: chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle());
}
/// <summary>

View file

@ -164,7 +164,9 @@ public static class IncludesExtensions
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
{
query = query.Include(u => u.UserPreferences);
query = query
.Include(u => u.UserPreferences)
.ThenInclude(p => p.Theme);
}
if (includeFlags.HasFlag(AppUserIncludes.WantToRead))

View file

@ -36,7 +36,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
var builder = new ChapterBuilder(Parser.DefaultChapter);
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters))
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters)!)
.WithRange(specialTreatment ? info.Filename : info.Chapters)
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
? info.Title

View file

@ -382,7 +382,7 @@ public class BookService : IBookService
}
}
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link");
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link[@href]");
if (styleNodes != null)
{
foreach (var styleLinks in styleNodes)

View file

@ -148,14 +148,14 @@ public class LibraryWatcher : ILibraryWatcher
private void OnChanged(object sender, FileSystemEventArgs e)
{
_logger.LogDebug("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType);
_logger.LogTrace("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType);
if (e.ChangeType != WatcherChangeTypes.Changed) return;
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name))));
}
private void OnCreated(object sender, FileSystemEventArgs e)
{
_logger.LogDebug("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
_logger.LogTrace("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name)));
}
@ -167,7 +167,7 @@ public class LibraryWatcher : ILibraryWatcher
private void OnDeleted(object sender, FileSystemEventArgs e) {
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
if (!isDirectory) return;
_logger.LogDebug("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
_logger.LogTrace("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true));
}
@ -285,10 +285,10 @@ public class LibraryWatcher : ILibraryWatcher
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
_logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder);
if (!rootFolder.Any()) return string.Empty;
if (rootFolder.Count == 0) return string.Empty;
// Select the first folder and join with library folder, this should give us the folder to scan.
return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1]));
return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1]));
}

View file

@ -115,13 +115,21 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
{
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
}
if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format))
{
info.IsSpecial = true;
info.Chapters = Parser.DefaultChapter;
info.Volumes = Parser.SpecialVolume;
}
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
{
info.Chapters = info.ComicInfo.Number;
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
{
info.IsSpecial = false;
info.Volumes = $"{Parser.SpecialVolumeNumber}";
info.Volumes = Parser.SpecialVolume;
}
}
@ -130,6 +138,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
{
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
}
}
public abstract bool IsApplicable(string filePath, LibraryType type);

View file

@ -121,6 +121,10 @@ public static class Parser
private static readonly Regex[] MangaVolumeRegex = new[]
{
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Dance in the Vampire Bund v16-17
new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
@ -194,6 +198,10 @@ public static class Parser
private static readonly Regex[] MangaSeriesRegex = new[]
{
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
@ -368,6 +376,10 @@ public static class Parser
private static readonly Regex[] ComicSeriesRegex = new[]
{
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
@ -456,6 +468,10 @@ public static class Parser
private static readonly Regex[] ComicVolumeRegex = new[]
{
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex(
@"^(?<Series>.+?)(?: |_)(t|v)(?<Volume>" + NumberRange + @")",
@ -492,6 +508,10 @@ public static class Parser
private static readonly Regex[] ComicChapterRegex = new[]
{
// Thai Volume: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n
new Regex(
@"(บทที่|ตอนที่)(\s)?(\.?)(\s|_)?(?<Chapter>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Batman & Wildcat (1 of 3)
new Regex(
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
@ -557,6 +577,10 @@ public static class Parser
private static readonly Regex[] MangaChapterRegex = new[]
{
// Thai Chapter: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n, เล่ม n -> Volume n, เล่มที่ n -> Volume n
new Regex(
@"(?<Volume>((เล่ม|เล่มที่))?(\s|_)?\.?\d+)(\s|_)(บทที่|ตอนที่)\.?(\s|_)?(?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
new Regex(
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)",

View file

@ -701,7 +701,8 @@ public class ProcessSeries : IProcessSeries
{
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
{
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series);
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}",
existingChapter.Range, volume.Name, parsedInfos[0].Series);
volume.Chapters.Remove(existingChapter);
}
else