Book Reader Bugfixes (#1254)

* Fixed image scoping breaking and books not being able to load images

* Cleaned up a lot of variables and added more jsdoc. After shifting the margin, we try to recover the column layout width,height, and scroll posisiton.

* Tap to paginate is working on first load now

* On resize, rescroll to attempt to avoid breakage

* Fixed transparent background for action bar on white theme

* Moved some lists to immutable arrays

* Actually fixed backgournd now

* Fixed some settings not updating in book reader on load

* Put some code in place to test out opening menu with clicking on the document

* Fixed settings not propagating to the reader

* Fixing 2 column when loading annd ios mobile

* Fixed an issue where paging to prev page would sometimes skip the first page.

* Fixing previous page skipping first page of chapter

* removing console logs

* Save progress when we page

* Click on document to show the side nav

* Removed columns auto because it could render more columns than applicable. Don't explicitly call saveProgress on prev page, as we already do in another call.

Adjusted the logic to calculate windowHeight and width to be the same throughout the reader.

* Setting select fix and settings polish

* Fixed awkward tooltip wording

* Added a message for when there is nothing to show on recommended tab

* Removed bug marker, there was no bug after all

* Fixing book title truncation in action bar

* When counting volumes or chapters that have range, count the max part of the range for publication status.

* Fixing TOC rendering issue

* Styling fixes

- Fixed an issue where the image height in the book reader was the column height plus padding so it was breaking pagination calc.
- Centered book reader setting pills
- Made inactive setting pill into a ghost button
- Fixed spacing across the reader settings drawer

* Added a bit of code to allow us to disable buttons before we click for next chapter load

* Removed titles from action bars

* The next page button will now show as the primary color to indicate to the user what the next forward page is.

* Added a view series to bookmark page and removed actions from header since it didn't work

* Fixed a bug where pagination wasn't mutating url state

* Lots of changes, code is kinda working.

Added Immersive Mode, but didn't generate migration.

Added concept of virtual pages with ability to see them. Math is still slightly off.

Cleaned up prefetching code so we do it much earlier.

Added some code that doesn't work to disable buttons with virtual paging included.

* When turning immersive mode on, force tap to paginate

* Refactored out the book reader state as it wasn't very beneficial

* Fixed total virtual page calculation

* Next/prev page seems to be working pretty well

* Applied Robbie's virtual page logic and fixed a bug in prev page code

* Changed the next page to use same virtual page logic

* Getting back and forward working...somehow.

* removing redundant code

* Fixing book title overflow from new action bar changes

* Polishing pagination styles

* Changing chapter to section

* Fixing up other book reader themes

* Fixed the login header being off-center

* Fixing styling to follow approach

* Refactored the pagination buttons to properly call next/prev page based on reading direction

* Drawer pagination buttons now respect when there is no chapters (prev/next)

* Everything except disabling buttons when on last possible page working

* Added Book Reader immersive mode migration

* Disable next/prev buttons for continuous reading before we request next/prev chapter if there is no chapter.

* Show a tooltip for the title

* Fixed unit test

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joseph Milazzo 2022-05-13 19:30:37 -05:00 committed by GitHub
parent dfcc2f0813
commit f701f8e599
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2284 additions and 290 deletions

View file

@ -88,6 +88,7 @@ namespace API.Controllers
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
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

@ -77,5 +77,10 @@ namespace API.DTOs
public SiteTheme Theme { get; set; }
public string BookReaderThemeName { get; set; }
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
/// <summary>
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false;
}
}

View file

@ -35,7 +35,7 @@ namespace API.Data
return new Volume()
{
Name = volumeNumber,
Number = (int) Parser.Parser.MinimumNumberFromRange(volumeNumber),
Number = (int) Parser.Parser.MinNumberFromRange(volumeNumber),
Chapters = new List<Chapter>()
};
}
@ -46,7 +46,7 @@ namespace API.Data
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
return new Chapter()
{
Number = specialTreatment ? "0" : Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty,
Number = specialTreatment ? "0" : Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
Range = specialTreatment ? info.Filename : info.Chapters,
Title = (specialTreatment && info.Format == MangaFormat.Epub)
? info.Title

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class BookReaderImmersiveMode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "BookReaderImmersiveMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BookReaderImmersiveMode",
table: "AppUserPreferences");
}
}
}

View file

@ -176,6 +176,9 @@ namespace API.Data.Migrations
b.Property<int>("BookReaderFontSize")
.HasColumnType("INTEGER");
b.Property<bool>("BookReaderImmersiveMode")
.HasColumnType("INTEGER");
b.Property<int>("BookReaderLineSpacing")
.HasColumnType("INTEGER");

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
@ -22,35 +21,36 @@ namespace API.Data
/// <summary>
/// Generated on Startup. Seed.SeedSettings must run before
/// </summary>
public static IList<ServerSetting> DefaultSettings;
public static ImmutableArray<ServerSetting> DefaultSettings;
public static readonly IList<SiteTheme> DefaultThemes = new List<SiteTheme>
{
new()
public static readonly ImmutableArray<SiteTheme> DefaultThemes = ImmutableArray.Create(
new List<SiteTheme>
{
Name = "Dark",
NormalizedName = Parser.Parser.Normalize("Dark"),
Provider = ThemeProvider.System,
FileName = "dark.scss",
IsDefault = true,
},
new()
{
Name = "Light",
NormalizedName = Parser.Parser.Normalize("Light"),
Provider = ThemeProvider.System,
FileName = "light.scss",
IsDefault = false,
},
new()
{
Name = "E-Ink",
NormalizedName = Parser.Parser.Normalize("E-Ink"),
Provider = ThemeProvider.System,
FileName = "e-ink.scss",
IsDefault = false,
},
};
new()
{
Name = "Dark",
NormalizedName = Parser.Parser.Normalize("Dark"),
Provider = ThemeProvider.System,
FileName = "dark.scss",
IsDefault = true,
},
new()
{
Name = "Light",
NormalizedName = Parser.Parser.Normalize("Light"),
Provider = ThemeProvider.System,
FileName = "light.scss",
IsDefault = false,
},
new()
{
Name = "E-Ink",
NormalizedName = Parser.Parser.Normalize("E-Ink"),
Provider = ThemeProvider.System,
FileName = "e-ink.scss",
IsDefault = false,
},
}.ToArray());
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
{
@ -91,24 +91,32 @@ namespace API.Data
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
{
await context.Database.EnsureCreatedAsync();
DefaultSettings = new List<ServerSetting>()
DefaultSettings = ImmutableArray.Create(new List<ServerSetting>()
{
new () {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
new () {Key = ServerSettingKey.TaskScan, Value = "daily"},
new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, // Not used from DB, but DB is sync with appSettings.json
new () {Key = ServerSettingKey.TaskBackup, Value = "daily"},
new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)},
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
new () {Key = ServerSettingKey.BaseUrl, Value = "/"},
new () {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
new () {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
new () {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
new () {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
};
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
new()
{
Key = ServerSettingKey.LoggingLevel, Value = "Information"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
new()
{
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
},
new()
{
Key = ServerSettingKey.Port, Value = "5000"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
}.ToArray());
foreach (var defaultSetting in DefaultSettings)
{

View file

@ -82,6 +82,11 @@ namespace API.Entities
/// </summary>
/// <remarks>Defaults to Default</remarks>
public BookPageLayoutMode PageLayoutMode { get; set; } = BookPageLayoutMode.Default;
/// <summary>
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false;
public AppUser AppUser { get; set; }

View file

@ -926,25 +926,7 @@ namespace API.Parser
}
public static float MaximumNumberFromRange(string range)
{
try
{
if (!Regex.IsMatch(range, @"^[\d-.]+$"))
{
return (float) 0.0;
}
var tokens = range.Replace("_", string.Empty).Split("-");
return tokens.Max(float.Parse);
}
catch
{
return (float) 0.0;
}
}
public static float MinimumNumberFromRange(string range)
public static float MinNumberFromRange(string range)
{
try
{
@ -962,6 +944,24 @@ namespace API.Parser
}
}
public static float MaxNumberFromRange(string range)
{
try
{
if (!Regex.IsMatch(range, @"^[\d-.]+$"))
{
return (float) 0.0;
}
var tokens = range.Replace("_", string.Empty).Split("-");
return tokens.Max(float.Parse);
}
catch
{
return (float) 0.0;
}
}
public static string Normalize(string name)
{
return NormalizeRegex.Replace(name, string.Empty).ToLower();

View file

@ -156,8 +156,7 @@ namespace API.Services
public async Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book)
{
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be
// Scoped
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped
var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty;
var importBuilder = new StringBuilder();
foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
@ -246,13 +245,13 @@ namespace API.Services
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
{
var images = doc.DocumentNode.SelectNodes("//img");
var images = doc.DocumentNode.SelectNodes("//img")
?? doc.DocumentNode.SelectNodes("//image");
if (images == null) return;
foreach (var image in images)
{
if (image.Name != "img") continue;
string key = null;
if (image.Attributes["src"] != null)
{
@ -283,23 +282,22 @@ namespace API.Services
/// <returns></returns>
private static string GetKeyForImage(EpubBookRef book, string imageFile)
{
if (!book.Content.Images.ContainsKey(imageFile))
if (book.Content.Images.ContainsKey(imageFile)) return imageFile;
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
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;
}
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;
}
}
}
return imageFile;
@ -321,12 +319,11 @@ namespace API.Services
private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary<string, int> mappings)
{
var anchors = doc.DocumentNode.SelectNodes("//a");
if (anchors != null)
if (anchors == null) return;
foreach (var anchor in anchors)
{
foreach (var anchor in anchors)
{
BookService.UpdateLinks(anchor, mappings, page);
}
UpdateLinks(anchor, mappings, page);
}
}

View file

@ -475,7 +475,7 @@ public class ReaderService : IReaderService
{
var chapters = volume.Chapters
.OrderBy(c => float.Parse(c.Number))
.Where(c => !c.IsSpecial && Parser.Parser.MaximumNumberFromRange(c.Range) <= chapterNumber);
.Where(c => !c.IsSpecial && Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber);
MarkChaptersAsRead(user, volume.SeriesId, chapters);
}
}

View file

@ -456,7 +456,7 @@ public class SeriesService : ISeriesService
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
.OrderBy(v => Parser.Parser.MinimumNumberFromRange(v.Name))
.OrderBy(v => Parser.Parser.MinNumberFromRange(v.Name))
.ToList();
var chapters = volumes.SelectMany(v => v.Chapters).ToList();

View file

@ -124,7 +124,9 @@ public class ScannerService : IScannerService
var path = Directory.GetParent(existingFolder)?.FullName;
if (!folderPaths.Contains(path) || !folderPaths.Any(p => p.Contains(path ?? string.Empty)))
{
_logger.LogInformation("[ScanService] Aborted: {SeriesName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library", series.OriginalName);
_logger.LogCritical("[ScanService] Aborted: {SeriesName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library", series.OriginalName);
await _eventHub.SendMessageAsync(MessageFactory.Error,
MessageFactory.ErrorEvent($"Scan of {series.Name} aborted", $"{series.OriginalName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library"));
return;
}
if (!string.IsNullOrEmpty(path))
@ -597,8 +599,8 @@ public class ScannerService : IScannerService
// To not have to rely completely on ComicInfo, try to parse out if the series is complete by checking parsed filenames as well.
if (series.Metadata.MaxCount != series.Metadata.TotalCount)
{
var maxVolume = series.Volumes.Max(v => v.Number);
var maxChapter = chapters.Max(c => (int) float.Parse(c.Number));
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range));
if (maxVolume == series.Metadata.TotalCount) series.Metadata.MaxCount = maxVolume;
else if (maxChapter == series.Metadata.TotalCount) series.Metadata.MaxCount = maxChapter;
}
@ -863,7 +865,7 @@ public class ScannerService : IScannerService
// Add files
var specialTreatment = info.IsSpecialInfo();
AddOrUpdateFileForChapter(chapter, info);
chapter.Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty;
chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty;
chapter.Range = specialTreatment ? info.Filename : info.Chapters;
}
@ -910,7 +912,7 @@ public class ScannerService : IScannerService
private void UpdateChapterFromComicInfo(Chapter chapter, ICollection<Person> allPeople, ICollection<Tag> allTags, ICollection<Genre> allGenres, ComicInfo? info)
{
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (firstFile == null ||
_cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, firstFile)) return;