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:
Joseph Milazzo 2022-01-07 06:56:28 -08:00 committed by GitHub
parent 32bfe46187
commit 2b57449a63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 426 additions and 315 deletions

View File

@ -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));

View File

@ -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;

View File

@ -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");

View File

@ -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>

View File

@ -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)

View File

@ -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>

View File

@ -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();

View File

@ -209,7 +209,7 @@ namespace API
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
});
app.UseRouting();

View File

@ -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();
// }
}
/**

View File

@ -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>

View File

@ -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

View File

@ -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');
}
});
}
}
}

View File

@ -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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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'">

View File

@ -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)) {