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 API.Entities.Enums;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static API.Parser.Parser;
|
using static API.Parser.Parser;
|
||||||
@ -58,20 +59,28 @@ namespace API.Tests.Parser
|
|||||||
Assert.Equal(expected, CleanTitle(input, isComic));
|
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]
|
[Theory]
|
||||||
// //[InlineData("@font-face{font-family:\"PaytoneOne\";src:url(\"..\\/Fonts\\/PaytoneOne.ttf\")}", "@font-face{font-family:\"PaytoneOne\";src:url(\"PaytoneOne.ttf\")}")]
|
[InlineData("src: url(fonts/AvenirNext-UltraLight.ttf)", new [] {"src: url(", "fonts/AvenirNext-UltraLight.ttf", ")"})]
|
||||||
// [InlineData("@font-face{font-family:\"PaytoneOne\";src:url(\"..\\/Fonts\\/PaytoneOne.ttf\")}", "..\\/Fonts\\/PaytoneOne.ttf")]
|
[InlineData("src: url(ideal-sans-serif.woff)", new [] {"src: url(", "ideal-sans-serif.woff", ")"})]
|
||||||
// //[InlineData("@font-face{font-family:'PaytoneOne';src:url('..\\/Fonts\\/PaytoneOne.ttf')}", "@font-face{font-family:'PaytoneOne';src:url('PaytoneOne.ttf')}")]
|
[InlineData("src: local(\"Helvetica Neue Bold\")", new [] {"src: local(\"", "Helvetica Neue Bold", "\")"})]
|
||||||
// //[InlineData("@font-face{\r\nfont-family:'PaytoneOne';\r\nsrc:url('..\\/Fonts\\/PaytoneOne.ttf')\r\n}", "@font-face{font-family:'PaytoneOne';src:url('PaytoneOne.ttf')}")]
|
[InlineData("src: url(\"/fonts/OpenSans-Regular-webfont.woff2\")", new [] {"src: url(\"", "/fonts/OpenSans-Regular-webfont.woff2", "\")"})]
|
||||||
// public void ReplaceStyleUrlTest(string input, string expected)
|
[InlineData("src: local(\"/fonts/OpenSans-Regular-webfont.woff2\")", new [] {"src: local(\"", "/fonts/OpenSans-Regular-webfont.woff2", "\")"})]
|
||||||
// {
|
public void FontCssCorrectlySeparates(string input, string[] expected)
|
||||||
// var replacementStr = "PaytoneOne.ttf";
|
{
|
||||||
// // Use Match to validate since replace is weird
|
Assert.Equal(expected, FontSrcUrlRegex.Match(input).Groups.Values.Select(g => g.Value).Where((s, i) => i > 0).ToArray());
|
||||||
// //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]
|
[Theory]
|
||||||
@ -161,6 +170,8 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("cover.jpg", true)]
|
[InlineData("cover.jpg", true)]
|
||||||
[InlineData("cover.png", true)]
|
[InlineData("cover.png", true)]
|
||||||
[InlineData("ch1/cover.png", true)]
|
[InlineData("ch1/cover.png", true)]
|
||||||
|
[InlineData("ch1/backcover.png", false)]
|
||||||
|
[InlineData("backcover.png", false)]
|
||||||
public void IsCoverImageTest(string inputPath, bool expected)
|
public void IsCoverImageTest(string inputPath, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, IsCoverImage(inputPath));
|
Assert.Equal(expected, IsCoverImage(inputPath));
|
||||||
|
@ -12,7 +12,6 @@ using API.Extensions;
|
|||||||
using API.Services;
|
using API.Services;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -10,6 +10,7 @@ using API.Extensions;
|
|||||||
using API.Services;
|
using API.Services;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using VersOne.Epub;
|
using VersOne.Epub;
|
||||||
|
|
||||||
@ -21,10 +22,11 @@ namespace API.Controllers
|
|||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ICacheService _cacheService;
|
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;
|
_logger = logger;
|
||||||
_bookService = bookService;
|
_bookService = bookService;
|
||||||
@ -212,146 +214,40 @@ namespace API.Controllers
|
|||||||
|
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
var doc = new HtmlDocument {OptionFixNestedTags = true};
|
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 apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl;
|
||||||
var bookPages = await book.GetReadingOrderAsync();
|
var bookPages = await book.GetReadingOrderAsync();
|
||||||
foreach (var contentFileRef in bookPages)
|
foreach (var contentFileRef in bookPages)
|
||||||
{
|
{
|
||||||
if (page == counter)
|
if (page != counter)
|
||||||
{
|
{
|
||||||
var content = await contentFileRef.ReadContentAsync();
|
counter++;
|
||||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content);
|
continue;
|
||||||
|
|
||||||
// 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++;
|
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");
|
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
|
/// This is the link to where the data was scraped from
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Web { get; set; } = string.Empty;
|
public string Web { get; set; } = string.Empty;
|
||||||
public int Day { get; set; }
|
public int Day { get; set; } = 0;
|
||||||
public int Month { get; set; }
|
public int Month { get; set; } = 0;
|
||||||
public int Year { get; set; }
|
public int Year { get; set; } = 0;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>See here for some examples https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face</remarks>
|
/// <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);
|
MatchOptions, RegexTimeout);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/@import
|
/// https://developer.mozilla.org/en-US/docs/Web/CSS/@import
|
||||||
@ -54,7 +55,7 @@ namespace API.Parser
|
|||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||||
MatchOptions, RegexTimeout);
|
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);
|
MatchOptions, RegexTimeout);
|
||||||
|
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
|
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
|
||||||
@ -1088,11 +1089,12 @@ namespace API.Parser
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests whether the file is a cover image such that: contains "cover", is named "folder", and is an image
|
/// Tests whether the file is a cover image such that: contains "cover", is named "folder", and is an image
|
||||||
/// </summary>
|
/// </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>
|
/// <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)
|
public static bool HasBlacklistedFolderInPath(string path)
|
||||||
|
@ -47,6 +47,8 @@ namespace API.Services
|
|||||||
/// <param name="fileFilePath"></param>
|
/// <param name="fileFilePath"></param>
|
||||||
/// <param name="targetDirectory">Where the files will be extracted to. If doesn't exist, will be created.</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);
|
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
|
public class BookService : IBookService
|
||||||
@ -168,13 +170,10 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
stylesheetHtml = stylesheetHtml.Insert(0, importBuilder.ToString());
|
stylesheetHtml = stylesheetHtml.Insert(0, importBuilder.ToString());
|
||||||
var importMatches = Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml);
|
|
||||||
foreach (Match match in importMatches)
|
EscapeCSSImportReferences(ref stylesheetHtml, apiBase, prepend);
|
||||||
{
|
|
||||||
if (!match.Success) continue;
|
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
|
||||||
var importFile = match.Groups["Filename"].Value;
|
|
||||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are any background images and rewrite those urls
|
// Check if there are any background images and rewrite those urls
|
||||||
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
||||||
@ -201,6 +200,26 @@ namespace API.Services
|
|||||||
return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss());
|
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)
|
private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book)
|
||||||
{
|
{
|
||||||
var matches = Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml);
|
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)
|
public ComicInfo GetComicInfo(string filePath)
|
||||||
{
|
{
|
||||||
if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return null;
|
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>
|
/// <summary>
|
||||||
/// Extracts the cover image to covers directory and returns file path back
|
/// Extracts the cover image to covers directory and returns file path back
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -323,6 +323,12 @@ public class MetadataService : IMetadataService
|
|||||||
series.Metadata.ReleaseYear = series.Volumes
|
series.Metadata.ReleaseYear = series.Volumes
|
||||||
.SelectMany(volume => volume.Chapters).Min(c => c.ReleaseDate.Year);
|
.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 genres = comicInfos.SelectMany(i => i?.Genre.Split(",")).Distinct().ToList();
|
||||||
var tags = comicInfos.SelectMany(i => i?.Tags.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();
|
var people = series.Volumes.SelectMany(volume => volume.Chapters).SelectMany(c => c.People).ToList();
|
||||||
|
@ -209,7 +209,7 @@ namespace API
|
|||||||
|
|
||||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
{
|
{
|
||||||
ForwardedHeaders = ForwardedHeaders.All
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
|
||||||
});
|
});
|
||||||
|
|
||||||
app.UseRouting();
|
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>
|
<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>
|
<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>
|
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||||
@ -94,7 +94,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #nestedChildren>
|
<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, '')">
|
<li class="{{chapterGroup.page == pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
|
||||||
{{chapterGroup.title}}
|
{{chapterGroup.title}}
|
||||||
</li>
|
</li>
|
||||||
@ -110,8 +110,11 @@
|
|||||||
</app-drawer>
|
</app-drawer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}" [@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}"
|
||||||
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px', 'margin': '0px 0px'}" [innerHtml]="page" *ngIf="page !== undefined"></div>
|
[@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 class="left {{clickOverlayClass('left')}} no-observe" (click)="prevPage()" *ngIf="clickToPaginate">
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,19 +154,33 @@ $primary-color: #0062cc;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reading-section {
|
.reading-section {
|
||||||
height: 100vh;
|
max-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
//overflow: auto; // This will break progress reporting
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-content {
|
.book-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bunch of resets so books render correctly
|
||||||
|
::ng-deep .book-content {
|
||||||
|
& a, & :link {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-body {
|
.drawer-body {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chapter-title {
|
||||||
|
padding-inline-start: 0px
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .scale-width {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Click to Paginate styles
|
// Click to Paginate styles
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { AfterViewInit, Component, ElementRef, HostListener, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
||||||
import {Location} from '@angular/common';
|
import {DOCUMENT, Location} from '@angular/common';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
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 seriesService: SeriesService, private readerService: ReaderService, private location: Location,
|
||||||
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
||||||
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
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.navService.hideNavBar();
|
||||||
|
|
||||||
this.darkModeStyleElem = this.renderer.createElement('style');
|
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) {
|
if (bodyNode !== undefined && bodyNode !== null) {
|
||||||
this.originalBodyColor = bodyNode.style.background;
|
this.originalBodyColor = bodyNode.style.background;
|
||||||
}
|
}
|
||||||
@ -296,14 +297,16 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// check scroll offset and if offset is after any of the "id" markers, save progress
|
// 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) => {
|
.pipe(debounceTime(200), takeUntil(this.onDestroy)).subscribe((event) => {
|
||||||
if (this.isLoading) return;
|
if (this.isLoading) return;
|
||||||
|
|
||||||
|
console.log('Scroll');
|
||||||
|
|
||||||
// Highlight the current chapter we are on
|
// Highlight the current chapter we are on
|
||||||
if (Object.keys(this.pageAnchors).length !== 0) {
|
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
|
// 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);
|
const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);
|
||||||
if (alreadyReached.length > 0) {
|
if (alreadyReached.length > 0) {
|
||||||
@ -350,7 +353,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
const bodyNode = document.querySelector('body');
|
const bodyNode = this.document.querySelector('body');
|
||||||
if (bodyNode !== undefined && bodyNode !== null && this.originalBodyColor !== undefined) {
|
if (bodyNode !== undefined && bodyNode !== null && this.originalBodyColor !== undefined) {
|
||||||
bodyNode.style.background = this.originalBodyColor;
|
bodyNode.style.background = this.originalBodyColor;
|
||||||
if (this.user.preferences.siteDarkMode) {
|
if (this.user.preferences.siteDarkMode) {
|
||||||
@ -359,7 +362,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.navService.showNavBar();
|
this.navService.showNavBar();
|
||||||
|
|
||||||
const head = document.querySelector('head');
|
const head = this.document.querySelector('head');
|
||||||
this.renderer.removeChild(head, this.darkModeStyleElem);
|
this.renderer.removeChild(head, this.darkModeStyleElem);
|
||||||
|
|
||||||
if (this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
if (this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
||||||
@ -581,8 +584,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
resetSettings() {
|
resetSettings() {
|
||||||
const windowWidth = window.innerWidth
|
const windowWidth = window.innerWidth
|
||||||
|| document.documentElement.clientWidth
|
|| this.document.documentElement.clientWidth
|
||||||
|| document.body.clientWidth;
|
|| this.document.body.clientWidth;
|
||||||
|
|
||||||
let margin = '15%';
|
let margin = '15%';
|
||||||
if (windowWidth <= 700) {
|
if (windowWidth <= 700) {
|
||||||
@ -631,7 +634,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveFocus() {
|
moveFocus() {
|
||||||
const elems = document.getElementsByClassName('reading-section');
|
const elems = this.document.getElementsByClassName('reading-section');
|
||||||
if (elems.length > 0) {
|
if (elems.length > 0) {
|
||||||
(elems[0] as HTMLDivElement).focus();
|
(elems[0] as HTMLDivElement).focus();
|
||||||
}
|
}
|
||||||
@ -679,10 +682,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
getPageMarkers(ids: Array<string>) {
|
getPageMarkers(ids: Array<string>) {
|
||||||
try {
|
try {
|
||||||
return document.querySelectorAll(ids.map(id => '#' + this.cleanIdSelector(id)).join(', '));
|
return this.document.querySelectorAll(ids.map(id => '#' + this.cleanIdSelector(id)).join(', '));
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
// Fallback to anchors instead. Some books have ids that are not valid for querySelectors, so anchors should be used instead
|
// 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);
|
this.setupPage(part, scrollTop);
|
||||||
return;
|
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)
|
Promise.all(Array.from(imgs)
|
||||||
.filter(img => !img.complete)
|
.filter(img => !img.complete)
|
||||||
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
|
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
|
||||||
@ -868,14 +875,25 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
updateReaderStyles() {
|
updateReaderStyles() {
|
||||||
if (this.readingHtml != undefined && this.readingHtml.nativeElement) {
|
if (this.readingHtml != undefined && this.readingHtml.nativeElement) {
|
||||||
for(let i = 0; i < this.readingHtml.nativeElement.children.length; i++) {
|
// for(let i = 0; i < this.readingHtml.nativeElement.children.length; i++) {
|
||||||
const elem = this.readingHtml.nativeElement.children.item(i);
|
// const elem = this.readingHtml.nativeElement.children.item(i);
|
||||||
if (elem?.tagName != 'STYLE') {
|
// if (elem?.tagName != 'STYLE') {
|
||||||
Object.entries(this.pageStyles).forEach(item => {
|
// Object.entries(this.pageStyles).forEach(item => {
|
||||||
this.renderer.setStyle(elem, item[0], item[1], RendererStyleFlags2.Important);
|
// 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() {
|
setOverrideStyles() {
|
||||||
const bodyNode = document.querySelector('body');
|
const bodyNode = this.document.querySelector('body');
|
||||||
if (bodyNode !== undefined && bodyNode !== null) {
|
if (bodyNode !== undefined && bodyNode !== null) {
|
||||||
if (this.user.preferences.siteDarkMode) {
|
if (this.user.preferences.siteDarkMode) {
|
||||||
bodyNode.classList.remove('bg-dark');
|
bodyNode.classList.remove('bg-dark');
|
||||||
@ -912,7 +930,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
bodyNode.style.background = this.getDarkModeBackgroundColor();
|
bodyNode.style.background = this.getDarkModeBackgroundColor();
|
||||||
}
|
}
|
||||||
this.backgroundColor = this.getDarkModeBackgroundColor();
|
this.backgroundColor = this.getDarkModeBackgroundColor();
|
||||||
const head = document.querySelector('head');
|
const head = this.document.querySelector('head');
|
||||||
if (this.darkMode) {
|
if (this.darkMode) {
|
||||||
this.renderer.appendChild(head, this.darkModeStyleElem)
|
this.renderer.appendChild(head, this.darkModeStyleElem)
|
||||||
} else {
|
} else {
|
||||||
@ -948,7 +966,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// Part selector is a XPATH
|
// Part selector is a XPATH
|
||||||
element = this.getElementFromXPath(partSelector);
|
element = this.getElementFromXPath(partSelector);
|
||||||
} else {
|
} else {
|
||||||
element = document.querySelector('*[id="' + partSelector + '"]');
|
element = this.document.querySelector('*[id="' + partSelector + '"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element === null) return;
|
if (element === null) return;
|
||||||
@ -984,7 +1002,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getElementFromXPath(path: string) {
|
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) {
|
if (node?.nodeType === Node.ELEMENT_NODE) {
|
||||||
return node as Element;
|
return node as Element;
|
||||||
}
|
}
|
||||||
@ -994,7 +1012,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
getXPathTo(element: any): string {
|
getXPathTo(element: any): string {
|
||||||
if (element === null) return '';
|
if (element === null) return '';
|
||||||
if (element.id !== '') { return 'id("' + element.id + '")'; }
|
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;
|
let ix = 0;
|
||||||
@ -1027,12 +1045,16 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.isFullscreen) {
|
if (this.isFullscreen) {
|
||||||
this.readerService.exitFullscreen(() => {
|
this.readerService.exitFullscreen(() => {
|
||||||
this.isFullscreen = false;
|
this.isFullscreen = false;
|
||||||
|
this.renderer.removeStyle(this.reader.nativeElement, 'background');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
||||||
this.isFullscreen = true;
|
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>
|
<ng-template ngbPanelContent>
|
||||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||||
<h3 id="manga-header">Image Reader</h3>
|
<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>
|
<div class="row no-gutters">
|
||||||
<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>
|
<div class="form-group col-md-6 col-sm-12 pr-2">
|
||||||
<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>
|
<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>
|
||||||
<select class="form-control" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
<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>
|
||||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
<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>
|
<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>
|
||||||
|
|
||||||
|
|
||||||
<div class="form-group">
|
<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>
|
<!-- TODO: Turn this into a checkbox -->
|
||||||
<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">
|
|
||||||
<label id="auto-close-label">Auto Close Menu</label>
|
<label id="auto-close-label">Auto Close Menu</label>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
@ -96,67 +104,82 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<h3>Book Reader</h3>
|
<h3>Book Reader</h3>
|
||||||
<div class="form-group">
|
<div class="row no-gutters">
|
||||||
<label id="dark-mode-label">Dark Mode</label>
|
<div class="form-group col-md-6 col-sm-12 pr-2">
|
||||||
<div class="form-group">
|
<label id="dark-mode-label">Dark Mode</label>
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
<div class="form-group">
|
||||||
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
<label class="custom-control-label" for="dark-mode">True</label>
|
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
||||||
</div>
|
<label class="custom-control-label" for="dark-mode">True</label>
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
</div>
|
||||||
<input type="radio" id="not-dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="false" aria-labelledby="dark-mode-label">
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
<label class="custom-control-label" for="not-dark-mode">False</label>
|
<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>
|
||||||
</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">
|
<div class="form-group col-md-6 col-sm-12">
|
||||||
<label>Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
<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 #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
<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-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>
|
<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="custom-slider"><ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider></div>
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<div class="float-right mb-3">
|
||||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||||
@ -165,23 +188,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ngb-panel>
|
</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>
|
</ngb-accordion>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="tab.fragment === 'bookmarks'">
|
<ng-container *ngIf="tab.fragment === 'bookmarks'">
|
||||||
|
@ -52,6 +52,10 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app-root {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Utiliities
|
// Utiliities
|
||||||
@include media-breakpoint-down(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) {
|
@include media-breakpoint-down(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user