mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Book Reader Issues (#906)
* Refactored the Font Escaping Regex with new unit tests. * Fonts are now properly escaped, somehow a regression was introduced. * Refactored most of the book page loading for the reader into the service. * Fixed a bug where going into fullscreen in non dark mode will cause the background of the reader to go black. Fixed a rendering issue with margin left/right screwing html up. Fixed an issue where line-height: 100% would break book's css, now we remove the styles if they are non-valuable. * Changed how I fixed the black mode in fullscreen * Fixed an issue where anchors wouldn't be colored blue in white mode * Fixed a bug in the code that checks if a filename is a cover where it would choose "backcover" as a cover, despite it not being a valid case. * Validate if ReleaseYear is a valid year and if not, set it to 0 to disable it. * Fixed an issue where some large images could blow out the screen when reading on mobile. Now images will force to be max of width of browser * Put my hack back in for fullscreen putting background color to black * Change forwarded headers from All to explicit names * Fixed an issue where Scheme was not https when it should have been. Now the browser will handle which scheme to request. * Cleaned up the user preferences to stack multiple controls onto one row * Fixed fullscreen scroll issue with progress, but now sticky top is missing. * Corrected the element on which we fullscreen
This commit is contained in:
parent
32bfe46187
commit
2b57449a63
@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using Xunit;
|
||||
using static API.Parser.Parser;
|
||||
@ -58,20 +59,28 @@ namespace API.Tests.Parser
|
||||
Assert.Equal(expected, CleanTitle(input, isComic));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("src: url(fonts/AvenirNext-UltraLight.ttf)", true)]
|
||||
[InlineData("src: url(ideal-sans-serif.woff)", true)]
|
||||
[InlineData("src: local(\"Helvetica Neue Bold\")", true)]
|
||||
[InlineData("src: url(\"/fonts/OpenSans-Regular-webfont.woff2\")", true)]
|
||||
[InlineData("src: local(\"/fonts/OpenSans-Regular-webfont.woff2\")", true)]
|
||||
[InlineData("src: url(data:application/x-font-woff", false)]
|
||||
public void FontCssRewriteMatches(string input, bool expectedMatch)
|
||||
{
|
||||
Assert.Equal(expectedMatch, FontSrcUrlRegex.Matches(input).Count > 0);
|
||||
}
|
||||
|
||||
// [Theory]
|
||||
// //[InlineData("@font-face{font-family:\"PaytoneOne\";src:url(\"..\\/Fonts\\/PaytoneOne.ttf\")}", "@font-face{font-family:\"PaytoneOne\";src:url(\"PaytoneOne.ttf\")}")]
|
||||
// [InlineData("@font-face{font-family:\"PaytoneOne\";src:url(\"..\\/Fonts\\/PaytoneOne.ttf\")}", "..\\/Fonts\\/PaytoneOne.ttf")]
|
||||
// //[InlineData("@font-face{font-family:'PaytoneOne';src:url('..\\/Fonts\\/PaytoneOne.ttf')}", "@font-face{font-family:'PaytoneOne';src:url('PaytoneOne.ttf')}")]
|
||||
// //[InlineData("@font-face{\r\nfont-family:'PaytoneOne';\r\nsrc:url('..\\/Fonts\\/PaytoneOne.ttf')\r\n}", "@font-face{font-family:'PaytoneOne';src:url('PaytoneOne.ttf')}")]
|
||||
// public void ReplaceStyleUrlTest(string input, string expected)
|
||||
// {
|
||||
// var replacementStr = "PaytoneOne.ttf";
|
||||
// // Use Match to validate since replace is weird
|
||||
// //Assert.Equal(expected, FontSrcUrlRegex.Replace(input, "$1" + replacementStr + "$2" + "$3"));
|
||||
// var match = FontSrcUrlRegex.Match(input);
|
||||
// Assert.Equal(!string.IsNullOrEmpty(expected), FontSrcUrlRegex.Match(input).Success);
|
||||
// }
|
||||
[Theory]
|
||||
[InlineData("src: url(fonts/AvenirNext-UltraLight.ttf)", new [] {"src: url(", "fonts/AvenirNext-UltraLight.ttf", ")"})]
|
||||
[InlineData("src: url(ideal-sans-serif.woff)", new [] {"src: url(", "ideal-sans-serif.woff", ")"})]
|
||||
[InlineData("src: local(\"Helvetica Neue Bold\")", new [] {"src: local(\"", "Helvetica Neue Bold", "\")"})]
|
||||
[InlineData("src: url(\"/fonts/OpenSans-Regular-webfont.woff2\")", new [] {"src: url(\"", "/fonts/OpenSans-Regular-webfont.woff2", "\")"})]
|
||||
[InlineData("src: local(\"/fonts/OpenSans-Regular-webfont.woff2\")", new [] {"src: local(\"", "/fonts/OpenSans-Regular-webfont.woff2", "\")"})]
|
||||
public void FontCssCorrectlySeparates(string input, string[] expected)
|
||||
{
|
||||
Assert.Equal(expected, FontSrcUrlRegex.Match(input).Groups.Values.Select(g => g.Value).Where((s, i) => i > 0).ToArray());
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
@ -161,6 +170,8 @@ namespace API.Tests.Parser
|
||||
[InlineData("cover.jpg", true)]
|
||||
[InlineData("cover.png", true)]
|
||||
[InlineData("ch1/cover.png", true)]
|
||||
[InlineData("ch1/backcover.png", false)]
|
||||
[InlineData("backcover.png", false)]
|
||||
public void IsCoverImageTest(string inputPath, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsCoverImage(inputPath));
|
||||
|
@ -12,7 +12,6 @@ using API.Extensions;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -10,6 +10,7 @@ using API.Extensions;
|
||||
using API.Services;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using VersOne.Epub;
|
||||
|
||||
@ -21,10 +22,11 @@ namespace API.Controllers
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
private static readonly string BookApiUrl = "book-resources?file=";
|
||||
private const string BookApiUrl = "book-resources?file=";
|
||||
|
||||
|
||||
public BookController(ILogger<BookController> logger, IBookService bookService, IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
public BookController(ILogger<BookController> logger, IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
{
|
||||
_logger = logger;
|
||||
_bookService = bookService;
|
||||
@ -212,146 +214,40 @@ namespace API.Controllers
|
||||
|
||||
var counter = 0;
|
||||
var doc = new HtmlDocument {OptionFixNestedTags = true};
|
||||
var baseUrl = Request.Scheme + "://" + Request.Host + Request.PathBase + "/api/";
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl;
|
||||
var bookPages = await book.GetReadingOrderAsync();
|
||||
foreach (var contentFileRef in bookPages)
|
||||
{
|
||||
if (page == counter)
|
||||
if (page != counter)
|
||||
{
|
||||
var content = await contentFileRef.ReadContentAsync();
|
||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content);
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
return BadRequest("The file is malformed! Cannot read.");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
body = doc.DocumentNode.SelectSingleNode("/html/body");
|
||||
}
|
||||
|
||||
var inlineStyles = doc.DocumentNode.SelectNodes("//style");
|
||||
if (inlineStyles != null)
|
||||
{
|
||||
foreach (var inlineStyle in inlineStyles)
|
||||
{
|
||||
var styleContent = await _bookService.ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book);
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
|
||||
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link");
|
||||
if (styleNodes != null)
|
||||
{
|
||||
foreach (var styleLinks in styleNodes)
|
||||
{
|
||||
var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value);
|
||||
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
|
||||
// In this case, we will do a search for the key that ends with
|
||||
if (!book.Content.Css.ContainsKey(key))
|
||||
{
|
||||
var correctedKey = book.Content.Css.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (correctedKey == null)
|
||||
{
|
||||
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
key = correctedKey;
|
||||
}
|
||||
|
||||
var styleContent = await _bookService.ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase, book.Content.Css[key].FileName, book);
|
||||
if (styleContent != null)
|
||||
{
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors != null)
|
||||
{
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
BookService.UpdateLinks(anchor, mappings, page);
|
||||
}
|
||||
}
|
||||
|
||||
var images = doc.DocumentNode.SelectNodes("//img");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "img") continue;
|
||||
|
||||
// Need to do for xlink:href
|
||||
if (image.Attributes["src"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["src"].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("src");
|
||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any classes on the html node (some r2l books do this) and move them to body tag for scoping
|
||||
var htmlNode = doc.DocumentNode.SelectSingleNode("//html");
|
||||
if (htmlNode != null && htmlNode.Attributes.Contains("class"))
|
||||
{
|
||||
var bodyClasses = body.Attributes.Contains("class") ? body.Attributes["class"].Value : string.Empty;
|
||||
var classes = htmlNode.Attributes["class"].Value + " " + bodyClasses;
|
||||
body.Attributes.Add("class", $"{classes}");
|
||||
// I actually need the body tag itself for the classes, so i will create a div and put the body stuff there.
|
||||
return Ok($"<div class=\"{body.Attributes["class"].Value}\">{body.InnerHtml}</div>");
|
||||
}
|
||||
|
||||
|
||||
return Ok(body.InnerHtml);
|
||||
counter++;
|
||||
continue;
|
||||
}
|
||||
|
||||
counter++;
|
||||
var content = await contentFileRef.ReadContentAsync();
|
||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content);
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
return BadRequest("The file is malformed! Cannot read.");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
body = doc.DocumentNode.SelectSingleNode("/html/body");
|
||||
}
|
||||
|
||||
return Ok(await _bookService.ScopePage(doc, book, apiBase, body, mappings, page));
|
||||
}
|
||||
|
||||
return BadRequest("Could not find the appropriate html for that page");
|
||||
|
@ -28,9 +28,9 @@ namespace API.Data.Metadata
|
||||
/// This is the link to where the data was scraped from
|
||||
/// </summary>
|
||||
public string Web { get; set; } = string.Empty;
|
||||
public int Day { get; set; }
|
||||
public int Month { get; set; }
|
||||
public int Year { get; set; }
|
||||
public int Day { get; set; } = 0;
|
||||
public int Month { get; set; } = 0;
|
||||
public int Year { get; set; } = 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -28,7 +28,8 @@ namespace API.Parser
|
||||
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
|
||||
/// </summary>
|
||||
/// <remarks>See here for some examples https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face</remarks>
|
||||
public static readonly Regex FontSrcUrlRegex = new Regex(@"(?<Start>(src:\s?)?url\((?!data:).(?!data:))" + "(?<Filename>(?!data:)[^\"']*)" + @"(?<End>.{1}\))",
|
||||
public static readonly Regex FontSrcUrlRegex = new Regex(@"(?<Start>(?:src:\s?)?(?:url|local)\((?!data:)" + "(?:[\"']?)" + @"(?!data:))"
|
||||
+ "(?<Filename>(?!data:)[^\"']+?)" + "(?<End>[\"']?" + @"\);?)",
|
||||
MatchOptions, RegexTimeout);
|
||||
/// <summary>
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/@import
|
||||
@ -54,7 +55,7 @@ namespace API.Parser
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(cover|folder)(?![\w\d])",
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)((?<!back)cover|folder)(?![\w\d])",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
|
||||
@ -1088,11 +1089,12 @@ namespace API.Parser
|
||||
/// <summary>
|
||||
/// Tests whether the file is a cover image such that: contains "cover", is named "folder", and is an image
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <remarks>If the path has "backcover" in it, it will be ignored</remarks>
|
||||
/// <param name="filename">Filename with extension</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsCoverImage(string name)
|
||||
public static bool IsCoverImage(string filename)
|
||||
{
|
||||
return IsImage(name, true) && (CoverImageRegex.IsMatch(name));
|
||||
return IsImage(filename, true) && CoverImageRegex.IsMatch(filename);
|
||||
}
|
||||
|
||||
public static bool HasBlacklistedFolderInPath(string path)
|
||||
|
@ -47,6 +47,8 @@ namespace API.Services
|
||||
/// <param name="fileFilePath"></param>
|
||||
/// <param name="targetDirectory">Where the files will be extracted to. If doesn't exist, will be created.</param>
|
||||
void ExtractPdfImages(string fileFilePath, string targetDirectory);
|
||||
|
||||
Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page);
|
||||
}
|
||||
|
||||
public class BookService : IBookService
|
||||
@ -168,13 +170,10 @@ namespace API.Services
|
||||
}
|
||||
|
||||
stylesheetHtml = stylesheetHtml.Insert(0, importBuilder.ToString());
|
||||
var importMatches = Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml);
|
||||
foreach (Match match in importMatches)
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
||||
}
|
||||
|
||||
EscapeCSSImportReferences(ref stylesheetHtml, apiBase, prepend);
|
||||
|
||||
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
|
||||
|
||||
// Check if there are any background images and rewrite those urls
|
||||
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
||||
@ -201,6 +200,26 @@ namespace API.Services
|
||||
return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss());
|
||||
}
|
||||
|
||||
private static void EscapeCSSImportReferences(ref string stylesheetHtml, string apiBase, string prepend)
|
||||
{
|
||||
foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend)
|
||||
{
|
||||
foreach (Match match in Parser.Parser.FontSrcUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book)
|
||||
{
|
||||
var matches = Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml);
|
||||
@ -216,6 +235,128 @@ namespace API.Services
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
||||
{
|
||||
var images = doc.DocumentNode.SelectNodes("//img");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "img") continue;
|
||||
|
||||
// Need to do for xlink:href
|
||||
if (image.Attributes["src"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["src"].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("src");
|
||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string PrepareFinalHtml(HtmlDocument doc, HtmlNode body)
|
||||
{
|
||||
// Check if any classes on the html node (some r2l books do this) and move them to body tag for scoping
|
||||
var htmlNode = doc.DocumentNode.SelectSingleNode("//html");
|
||||
if (htmlNode == null || !htmlNode.Attributes.Contains("class")) return body.InnerHtml;
|
||||
|
||||
var bodyClasses = body.Attributes.Contains("class") ? body.Attributes["class"].Value : string.Empty;
|
||||
var classes = htmlNode.Attributes["class"].Value + " " + bodyClasses;
|
||||
body.Attributes.Add("class", $"{classes}");
|
||||
// I actually need the body tag itself for the classes, so i will create a div and put the body stuff there.
|
||||
//return Ok($"<div class=\"{body.Attributes["class"].Value}\">{body.InnerHtml}</div>");
|
||||
return $"<div class=\"{body.Attributes["class"].Value}\">{body.InnerHtml}</div>";
|
||||
}
|
||||
|
||||
private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary<string, int> mappings)
|
||||
{
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors != null)
|
||||
{
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
BookService.UpdateLinks(anchor, mappings, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InlineStyles(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body)
|
||||
{
|
||||
var inlineStyles = doc.DocumentNode.SelectNodes("//style");
|
||||
if (inlineStyles != null)
|
||||
{
|
||||
foreach (var inlineStyle in inlineStyles)
|
||||
{
|
||||
var styleContent = await ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book);
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
|
||||
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link");
|
||||
if (styleNodes != null)
|
||||
{
|
||||
foreach (var styleLinks in styleNodes)
|
||||
{
|
||||
var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value);
|
||||
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
|
||||
// In this case, we will do a search for the key that ends with
|
||||
if (!book.Content.Css.ContainsKey(key))
|
||||
{
|
||||
var correctedKey = book.Content.Css.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (correctedKey == null)
|
||||
{
|
||||
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
key = correctedKey;
|
||||
}
|
||||
|
||||
var styleContent = await ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase,
|
||||
book.Content.Css[key].FileName, book);
|
||||
if (styleContent != null)
|
||||
{
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ComicInfo GetComicInfo(string filePath)
|
||||
{
|
||||
if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return null;
|
||||
@ -466,6 +607,27 @@ namespace API.Services
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responsible to scope all the css, links, tags, etc to prepare a self contained html file for the page
|
||||
/// </summary>
|
||||
/// <param name="doc">Html Doc that will be appended to</param>
|
||||
/// <param name="book">Underlying epub</param>
|
||||
/// <param name="apiBase">API Url for file loading to pass through</param>
|
||||
/// <param name="body">Body element from the epub</param>
|
||||
/// <param name="mappings">Epub mappings</param>
|
||||
/// <param name="page">Page number we are loading</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page)
|
||||
{
|
||||
await InlineStyles(doc, book, apiBase, body);
|
||||
|
||||
RewriteAnchors(page, doc, mappings);
|
||||
|
||||
ScopeImages(doc, book, apiBase);
|
||||
|
||||
return PrepareFinalHtml(doc, body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the cover image to covers directory and returns file path back
|
||||
/// </summary>
|
||||
|
@ -323,6 +323,12 @@ public class MetadataService : IMetadataService
|
||||
series.Metadata.ReleaseYear = series.Volumes
|
||||
.SelectMany(volume => volume.Chapters).Min(c => c.ReleaseDate.Year);
|
||||
|
||||
if (series.Metadata.ReleaseYear < 1000)
|
||||
{
|
||||
// Not a valid year, default to 0
|
||||
series.Metadata.ReleaseYear = 0;
|
||||
}
|
||||
|
||||
var genres = comicInfos.SelectMany(i => i?.Genre.Split(",")).Distinct().ToList();
|
||||
var tags = comicInfos.SelectMany(i => i?.Tags.Split(",")).Distinct().ToList();
|
||||
var people = series.Volumes.SelectMany(volume => volume.Chapters).SelectMany(c => c.People).ToList();
|
||||
|
@ -209,7 +209,7 @@ namespace API
|
||||
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.All
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
@ -203,13 +203,6 @@ export class ReaderService {
|
||||
}
|
||||
});
|
||||
}
|
||||
// else if (el.mozRequestFullScreen) {
|
||||
// el.mozRequestFullScreen();
|
||||
// } else if (el.webkitRequestFullscreen) {
|
||||
// el.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
|
||||
// } else if (el.msRequestFullscreen) {
|
||||
// el.msRequestFullscreen();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,13 +214,6 @@ export class ReaderService {
|
||||
}
|
||||
});
|
||||
}
|
||||
// else if (document.msExitFullscreen) {
|
||||
// document.msExitFullscreen();
|
||||
// } else if (document.mozCancelFullScreen) {
|
||||
// document.mozCancelFullScreen();
|
||||
// } else if (document.webkitExitFullscreen) {
|
||||
// document.webkitExitFullscreen();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="container-flex {{darkMode ? 'dark-mode' : ''}}" #reader>
|
||||
<div class="container-flex {{darkMode ? 'dark-mode' : ''}}" style="overflow: auto;" #reader>
|
||||
<div class="fixed-top" #stickyTop>
|
||||
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||
@ -94,7 +94,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<ng-template #nestedChildren>
|
||||
<ul *ngFor="let chapterGroup of chapters" style="padding-inline-start: 0px">
|
||||
<ul *ngFor="let chapterGroup of chapters" class="chapter-title">
|
||||
<li class="{{chapterGroup.page == pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
|
||||
{{chapterGroup.title}}
|
||||
</li>
|
||||
@ -110,8 +110,11 @@
|
||||
</app-drawer>
|
||||
</div>
|
||||
|
||||
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}" [@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
||||
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px', 'margin': '0px 0px'}" [innerHtml]="page" *ngIf="page !== undefined"></div>
|
||||
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}"
|
||||
[@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
||||
|
||||
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px', 'margin': '0px 0px'}"
|
||||
[innerHtml]="page" *ngIf="page !== undefined"></div>
|
||||
|
||||
<div class="left {{clickOverlayClass('left')}} no-observe" (click)="prevPage()" *ngIf="clickToPaginate">
|
||||
</div>
|
||||
|
@ -154,19 +154,33 @@ $primary-color: #0062cc;
|
||||
}
|
||||
|
||||
.reading-section {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
//overflow: auto; // This will break progress reporting
|
||||
}
|
||||
|
||||
.book-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// A bunch of resets so books render correctly
|
||||
::ng-deep .book-content {
|
||||
& a, & :link {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
padding-inline-start: 0px
|
||||
}
|
||||
|
||||
::ng-deep .scale-width {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
// Click to Paginate styles
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, Component, ElementRef, HostListener, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
||||
import {DOCUMENT, Location} from '@angular/common';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -243,7 +243,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
|
||||
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
||||
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
||||
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService) {
|
||||
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService,
|
||||
@Inject(DOCUMENT) private document: Document) {
|
||||
this.navService.hideNavBar();
|
||||
|
||||
this.darkModeStyleElem = this.renderer.createElement('style');
|
||||
@ -281,7 +282,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
const bodyNode = document.querySelector('body');
|
||||
const bodyNode = this.document.querySelector('body');
|
||||
if (bodyNode !== undefined && bodyNode !== null) {
|
||||
this.originalBodyColor = bodyNode.style.background;
|
||||
}
|
||||
@ -296,14 +297,16 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
// check scroll offset and if offset is after any of the "id" markers, save progress
|
||||
fromEvent(window, 'scroll')
|
||||
fromEvent(this.reader.nativeElement, 'scroll')
|
||||
.pipe(debounceTime(200), takeUntil(this.onDestroy)).subscribe((event) => {
|
||||
if (this.isLoading) return;
|
||||
|
||||
console.log('Scroll');
|
||||
|
||||
// Highlight the current chapter we are on
|
||||
if (Object.keys(this.pageAnchors).length !== 0) {
|
||||
// get the height of the document so we can capture markers that are halfway on the document viewport
|
||||
const verticalOffset = this.scrollService.scrollPosition + (document.body.offsetHeight / 2);
|
||||
const verticalOffset = this.scrollService.scrollPosition + (this.document.body.offsetHeight / 2);
|
||||
|
||||
const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);
|
||||
if (alreadyReached.length > 0) {
|
||||
@ -350,7 +353,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const bodyNode = document.querySelector('body');
|
||||
const bodyNode = this.document.querySelector('body');
|
||||
if (bodyNode !== undefined && bodyNode !== null && this.originalBodyColor !== undefined) {
|
||||
bodyNode.style.background = this.originalBodyColor;
|
||||
if (this.user.preferences.siteDarkMode) {
|
||||
@ -359,7 +362,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
this.navService.showNavBar();
|
||||
|
||||
const head = document.querySelector('head');
|
||||
const head = this.document.querySelector('head');
|
||||
this.renderer.removeChild(head, this.darkModeStyleElem);
|
||||
|
||||
if (this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
||||
@ -581,8 +584,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
resetSettings() {
|
||||
const windowWidth = window.innerWidth
|
||||
|| document.documentElement.clientWidth
|
||||
|| document.body.clientWidth;
|
||||
|| this.document.documentElement.clientWidth
|
||||
|| this.document.body.clientWidth;
|
||||
|
||||
let margin = '15%';
|
||||
if (windowWidth <= 700) {
|
||||
@ -631,7 +634,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
moveFocus() {
|
||||
const elems = document.getElementsByClassName('reading-section');
|
||||
const elems = this.document.getElementsByClassName('reading-section');
|
||||
if (elems.length > 0) {
|
||||
(elems[0] as HTMLDivElement).focus();
|
||||
}
|
||||
@ -679,10 +682,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
getPageMarkers(ids: Array<string>) {
|
||||
try {
|
||||
return document.querySelectorAll(ids.map(id => '#' + this.cleanIdSelector(id)).join(', '));
|
||||
return this.document.querySelectorAll(ids.map(id => '#' + this.cleanIdSelector(id)).join(', '));
|
||||
} catch (Exception) {
|
||||
// Fallback to anchors instead. Some books have ids that are not valid for querySelectors, so anchors should be used instead
|
||||
return document.querySelectorAll(ids.map(id => '[href="#' + id + '"]').join(', '));
|
||||
return this.document.querySelectorAll(ids.map(id => '[href="#' + id + '"]').join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,6 +720,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.setupPage(part, scrollTop);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply scaling class to all images to ensure they scale down to max width to not blow out the reader
|
||||
Array.from(imgs).forEach(img => this.renderer.addClass(img, 'scale-width'));
|
||||
|
||||
Promise.all(Array.from(imgs)
|
||||
.filter(img => !img.complete)
|
||||
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
|
||||
@ -868,14 +875,25 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
updateReaderStyles() {
|
||||
if (this.readingHtml != undefined && this.readingHtml.nativeElement) {
|
||||
for(let i = 0; i < this.readingHtml.nativeElement.children.length; i++) {
|
||||
const elem = this.readingHtml.nativeElement.children.item(i);
|
||||
if (elem?.tagName != 'STYLE') {
|
||||
Object.entries(this.pageStyles).forEach(item => {
|
||||
this.renderer.setStyle(elem, item[0], item[1], RendererStyleFlags2.Important);
|
||||
});
|
||||
// for(let i = 0; i < this.readingHtml.nativeElement.children.length; i++) {
|
||||
// const elem = this.readingHtml.nativeElement.children.item(i);
|
||||
// if (elem?.tagName != 'STYLE') {
|
||||
// Object.entries(this.pageStyles).forEach(item => {
|
||||
// if (item[1] == '100%' || item[1] == '0px' || item[1] == 'inherit') return;
|
||||
// this.renderer.setStyle(elem, item[0], item[1], RendererStyleFlags2.Important);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
console.log('pageStyles: ', this.pageStyles);
|
||||
console.log('readingHtml: ', this.readingHtml.nativeElement);
|
||||
Object.entries(this.pageStyles).forEach(item => {
|
||||
if (item[1] == '100%' || item[1] == '0px' || item[1] == 'inherit') {
|
||||
// Remove the style or skip
|
||||
this.renderer.removeStyle(this.readingHtml.nativeElement, item[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.renderer.setStyle(this.readingHtml.nativeElement, item[0], item[1], RendererStyleFlags2.Important);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -903,7 +921,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
setOverrideStyles() {
|
||||
const bodyNode = document.querySelector('body');
|
||||
const bodyNode = this.document.querySelector('body');
|
||||
if (bodyNode !== undefined && bodyNode !== null) {
|
||||
if (this.user.preferences.siteDarkMode) {
|
||||
bodyNode.classList.remove('bg-dark');
|
||||
@ -912,7 +930,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
bodyNode.style.background = this.getDarkModeBackgroundColor();
|
||||
}
|
||||
this.backgroundColor = this.getDarkModeBackgroundColor();
|
||||
const head = document.querySelector('head');
|
||||
const head = this.document.querySelector('head');
|
||||
if (this.darkMode) {
|
||||
this.renderer.appendChild(head, this.darkModeStyleElem)
|
||||
} else {
|
||||
@ -948,7 +966,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Part selector is a XPATH
|
||||
element = this.getElementFromXPath(partSelector);
|
||||
} else {
|
||||
element = document.querySelector('*[id="' + partSelector + '"]');
|
||||
element = this.document.querySelector('*[id="' + partSelector + '"]');
|
||||
}
|
||||
|
||||
if (element === null) return;
|
||||
@ -984,7 +1002,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
getElementFromXPath(path: string) {
|
||||
const node = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
const node = this.document.evaluate(path, this.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (node?.nodeType === Node.ELEMENT_NODE) {
|
||||
return node as Element;
|
||||
}
|
||||
@ -994,7 +1012,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
getXPathTo(element: any): string {
|
||||
if (element === null) return '';
|
||||
if (element.id !== '') { return 'id("' + element.id + '")'; }
|
||||
if (element === document.body) { return element.tagName; }
|
||||
if (element === this.document.body) { return element.tagName; }
|
||||
|
||||
|
||||
let ix = 0;
|
||||
@ -1027,12 +1045,16 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.isFullscreen) {
|
||||
this.readerService.exitFullscreen(() => {
|
||||
this.isFullscreen = false;
|
||||
this.renderer.removeStyle(this.reader.nativeElement, 'background');
|
||||
});
|
||||
} else {
|
||||
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
||||
this.isFullscreen = true;
|
||||
// HACK: This is a bug with how browsers change the background color for fullscreen mode
|
||||
if (!this.darkMode) {
|
||||
this.renderer.setStyle(this.reader.nativeElement, 'background', 'white');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -49,39 +49,47 @@
|
||||
<ng-template ngbPanelContent>
|
||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||
<h3 id="manga-header">Image Reader</h3>
|
||||
<div class="form-group">
|
||||
<label for="settings-reading-direction">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="sr-only" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="form-group col-md-6 col-sm-12 pr-2">
|
||||
<label for="settings-reading-direction">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="sr-only" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-6 col-sm-12">
|
||||
<label for="settings-scaling-option">Scaling Options</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
|
||||
<span class="sr-only" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="form-group col-md-6 col-sm-12 pr-2">
|
||||
<label for="settings-pagesplit-option">Page Splitting</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
|
||||
<span class="sr-only" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-6 col-sm-12">
|
||||
<label for="settings-readingmode-option">Reading Mode</label>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-scaling-option">Scaling Options</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
|
||||
<span class="sr-only" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-pagesplit-option">Page Splitting</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
|
||||
<span class="sr-only" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings-readingmode-option">Reading Mode</label>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<!-- TODO: Turn this into a checkbox -->
|
||||
<label id="auto-close-label">Auto Close Menu</label>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
@ -96,67 +104,82 @@
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Book Reader</h3>
|
||||
<div class="form-group">
|
||||
<label id="dark-mode-label">Dark Mode</label>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
||||
<label class="custom-control-label" for="dark-mode">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="false" aria-labelledby="dark-mode-label">
|
||||
<label class="custom-control-label" for="not-dark-mode">False</label>
|
||||
<div class="row no-gutters">
|
||||
<div class="form-group col-md-6 col-sm-12 pr-2">
|
||||
<label id="dark-mode-label">Dark Mode</label>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
||||
<label class="custom-control-label" for="dark-mode">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="false" aria-labelledby="dark-mode-label">
|
||||
<label class="custom-control-label" for="not-dark-mode">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings-book-reading-direction">Book Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="sr-only" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-control" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="taptopaginate-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||
<span class="sr-only" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label class="custom-control-label" for="taptopaginate">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="false" aria-labelledby="taptopaginate-label">
|
||||
<label class="custom-control-label" for="not-taptopaginate">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings-fontfamily-option">Font Family</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>Font familty to load up. Default will load the book's default font</ng-template>
|
||||
<span class="sr-only" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
||||
<select class="form-control" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="font-size">Font Size</label>
|
||||
<div class="custom-slider"><ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||
<span class="sr-only" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||
<div class="custom-slider"><ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||
<span class="sr-only" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||
<div class="custom-slider"><ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider></div>
|
||||
<div class="form-group col-md-6 col-sm-12">
|
||||
<label id="taptopaginate-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||
<span class="sr-only" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label class="custom-control-label" for="taptopaginate">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="false" aria-labelledby="taptopaginate-label">
|
||||
<label class="custom-control-label" for="not-taptopaginate">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="form-group col-md-6 col-sm-12 pr-2">
|
||||
<label for="settings-book-reading-direction">Book Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="sr-only" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-control" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group col-md-6 col-sm-12">
|
||||
<label for="settings-fontfamily-option">Font Family</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>Font familty to load up. Default will load the book's default font</ng-template>
|
||||
<span class="sr-only" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
||||
<select class="form-control" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="form-group col-md-4 col-sm-12 pr-2">
|
||||
<label id="font-size">Font Size</label>
|
||||
<div class="custom-slider"><ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider></div>
|
||||
</div>
|
||||
<div class="form-group col-md-4 col-sm-12 pr-2">
|
||||
<label>Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||
<span class="sr-only" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||
<div class="custom-slider"><ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4 col-sm-12">
|
||||
<label>Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||
<span class="sr-only" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||
<div class="custom-slider"><ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="float-right mb-3">
|
||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
@ -165,23 +188,6 @@
|
||||
</form>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
|
||||
<!--
|
||||
|
||||
<ngb-panel id="api-panel" title="3rd Party Clients">
|
||||
<ng-template ngbPanelHeader>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">3rd Party Clients</button>
|
||||
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('api-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
|
||||
<p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p>
|
||||
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
|
||||
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
|
||||
</ng-template>
|
||||
</ngb-panel> -->
|
||||
</ngb-accordion>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === 'bookmarks'">
|
||||
|
@ -52,6 +52,10 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
app-root {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
|
||||
// Utiliities
|
||||
@include media-breakpoint-down(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user