Book Reader Bugfixes (#1254)

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

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

* Tap to paginate is working on first load now

* On resize, rescroll to attempt to avoid breakage

* Fixed transparent background for action bar on white theme

* Moved some lists to immutable arrays

* Actually fixed backgournd now

* Fixed some settings not updating in book reader on load

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

* Fixed settings not propagating to the reader

* Fixing 2 column when loading annd ios mobile

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

* Fixing previous page skipping first page of chapter

* removing console logs

* Save progress when we page

* Click on document to show the side nav

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

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

* Setting select fix and settings polish

* Fixed awkward tooltip wording

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

* Removed bug marker, there was no bug after all

* Fixing book title truncation in action bar

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

* Fixing TOC rendering issue

* Styling fixes

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

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

* Removed titles from action bars

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

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

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

* Lots of changes, code is kinda working.

Added Immersive Mode, but didn't generate migration.

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

Cleaned up prefetching code so we do it much earlier.

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

* When turning immersive mode on, force tap to paginate

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

* Fixed total virtual page calculation

* Next/prev page seems to be working pretty well

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

* Changed the next page to use same virtual page logic

* Getting back and forward working...somehow.

* removing redundant code

* Fixing book title overflow from new action bar changes

* Polishing pagination styles

* Changing chapter to section

* Fixing up other book reader themes

* Fixed the login header being off-center

* Fixing styling to follow approach

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

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

* Everything except disabling buttons when on last possible page working

* Added Book Reader immersive mode migration

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

* Show a tooltip for the title

* Fixed unit test

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

View File

@ -31,7 +31,7 @@ namespace API.Tests.Helpers
return new Volume()
{
Name = volumeNumber,
Number = (int) API.Parser.Parser.MinimumNumberFromRange(volumeNumber),
Number = (int) API.Parser.Parser.MinNumberFromRange(volumeNumber),
Pages = pages,
Chapters = chaps
};
@ -43,7 +43,7 @@ namespace API.Tests.Helpers
{
IsSpecial = isSpecial,
Range = range,
Number = API.Parser.Parser.MinimumNumberFromRange(range) + string.Empty,
Number = API.Parser.Parser.MinNumberFromRange(range) + string.Empty,
Files = files ?? new List<MangaFile>(),
Pages = pageCount,

View File

@ -140,7 +140,7 @@ namespace API.Tests.Parser
[InlineData("40.1_a", 0)]
public void MinimumNumberFromRangeTest(string input, float expected)
{
Assert.Equal(expected, MinimumNumberFromRange(input));
Assert.Equal(expected, MinNumberFromRange(input));
}
[Theory]
@ -153,7 +153,7 @@ namespace API.Tests.Parser
[InlineData("40.1_a", 0)]
public void MaximumNumberFromRangeTest(string input, float expected)
{
Assert.Equal(expected, MaximumNumberFromRange(input));
Assert.Equal(expected, MaxNumberFromRange(input));
}
[Theory]

View File

@ -88,6 +88,7 @@ namespace API.Controllers
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
// TODO: Remove this code - this overrides layout mode to be single until the mode is released

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ export interface Preferences {
bookReaderReadingDirection: ReadingDirection;
bookReaderThemeName: string;
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
// Global
theme: SiteTheme;

View File

@ -27,7 +27,11 @@ export enum Action {
/**
* Essentially a download, but handled differently. Needed so card bubbles it up for handling
*/
DownloadBookmark = 12
DownloadBookmark = 12,
/**
* Open Series detail page for said series
*/
ViewSeries = 13
}
export interface ActionItem<T> {
@ -305,6 +309,12 @@ export class ActionFactoryService {
];
this.bookmarkActions = [
{
action: Action.ViewSeries,
title: 'View Series',
callback: this.dummyCallback,
requiresAdmin: false
},
{
action: Action.DownloadBookmark,
title: 'Download',

View File

@ -80,7 +80,7 @@
<h4>Reoccuring Tasks</h4>
<div class="mb-3">
<label for="settings-tasks-scan" class="form-label">Library Scan</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metatdata around manga files.</ng-template>
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metadata around manga files.</ng-template>
<span class="visually-hidden" id="settings-tasks-scan-help">How often Kavita will scan and refresh metatdata around manga files.</span>
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>

View File

@ -127,7 +127,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
}
onPageChange(pagination: Pagination) {
this.filterUtilityService.updateUrlFromPagination(this.pagination);
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
this.loadPage();
}

View File

@ -62,6 +62,7 @@ export class AppComponent implements OnInit {
setDocHeight() {
// Sets a CSS variable for the actual device viewport height. Needed for mobile dev.
this.document.documentElement.style.setProperty('--vh', `${window.innerHeight/100}px`);
let vh = window.innerHeight * 0.01;
this.document.documentElement.style.setProperty('--vh', `${vh}px`);
}
}

View File

@ -13,6 +13,9 @@ export const BookBlackTheme = `
/* Drawer */
--drawer-bg-color: #292929;
--drawer-text-color: white;
--drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(255 255 255 / 20%);
--drawer-pagination-border: 1px solid rgb(0 0 0 / 13%);
/* Accordion */
--accordion-header-text-color: rgba(74, 198, 148, 0.9);

View File

@ -13,6 +13,8 @@ export const BookDarkTheme = `
/* Drawer */
--drawer-bg-color: #292929;
--drawer-text-color: white;
--drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(255 255 255 / 20%);
--drawer-pagination-border: 1px solid rgb(0 0 0 / 13%);
/* Accordion */
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
@ -44,6 +46,15 @@ export const BookDarkTheme = `
--btn-disabled-text-color: white;
--btn-disabled-border-color: #6c757d;
/* Inputs */
--input-bg-color: #343a40;
--input-bg-readonly-color: #434648;
--input-focused-border-color: #ccc;
--input-text-color: #fff;
--input-placeholder-color: #aeaeae;
--input-border-color: #ccc;
--input-focus-boxshadow-color: rgb(255 255 255 / 50%);
/* Nav (Tabs) */
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
--nav-tab-text-color: var(--body-text-color);
@ -78,6 +89,7 @@ export const BookDarkTheme = `
--br-actionbar-button-text-color: white;
--br-actionbar-button-hover-border-color: #6c757d;
--br-actionbar-bg-color: black;
}
@ -116,4 +128,5 @@ background-color: initial !important;
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {color: rgb(211, 138, 138) !important}
.book-content :link:not(cite), :link .book-content *:not(cite) {color: #8db2e5 !important}
`;

View File

@ -1,7 +1,15 @@
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
export const BookWhiteTheme = `
:root() .brtheme-white {
--brtheme-link-text-color: green;
--brtheme-bg-color: lightgrey;
:root .brtheme-white {
--br-actionbar-bg-color: white;
/* Drawer */
--drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(0 0 0 / 13%);
--drawer-pagination-border: 1px solid rgb(0 0 0 / 13%);
}
.reader-container {
color: black !important;
background-image: none !important;
background-color: white !important;
}
`;

View File

@ -7,30 +7,54 @@
Book Settings
</h5>
<div subheader>
<div class="row g-0">
<button class="btn btn-small btn-icon col-1" style="padding-left: 0px" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<div class="col-1 page-stub ps-1">{{pageNum}}</div>
<div class="col-8 pe-1" style="margin-top: 15px">
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
<div class="pagination-cont">
<ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default">
<div class="virt-pagination-cont">
<div class="g-0 text-center">
Page
</div>
<div class="d-flex align-items-center justify-content-between text-center row g-0" *ngIf="getVirtualPage() as vp" >
<button class="btn btn-small btn-icon col-1" (click)="prevPage()" title="Prev Page">
<i class="fa-solid fa-caret-left" aria-hidden="true"></i>
</button>
<div class="col-1">{{vp[0]}}</div>
<div class="col-8">
<ngb-progressbar title="virtual pages" type="primary" height="5px" (click)="loadPage()" [value]="vp[0]" [max]="vp[1]"></ngb-progressbar>
</div>
<div class="col-1 btn-icon" (click)="loadPage()" title="Go to last page">{{vp[1]}}</div>
<button class="btn btn-small btn-icon col-1" (click)="nextPage()" title="Next Page"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
</div>
</div>
</ng-container>
<div class="g-0 text-center">
Section
</div>
<div class="d-flex align-items-center justify-content-between text-center row g-0">
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<div class="col-1">{{pageNum}}</div>
<div class="col-8">
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
</div>
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
</div>
<div class="col-1 btn-icon page-stub pe-1" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
<button class="btn btn-small btn-icon col-1" style="padding-right: 0px; padding-left: 0px" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
</div>
</div>
<div body class="drawer-body">
<!-- TODO: Center align the tab pills -->
<nav role="navigation">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-pills mb-2" [destroyOnHide]="false">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="reader-pills nav nav-pills mb-2" [destroyOnHide]="false">
<li [ngbNavItem]="TabID.Settings">
<a ngbNavLink>Settings</a>
<ng-template ngbNavContent>
<app-reader-settings
(colorThemeUpdate)="setOverrideStyles($event)"
(colorThemeUpdate)="updateColorTheme($event)"
(styleUpdate)="updateReaderStyles($event)"
(clickToPaginateChanged)="showPaginationOverlay($event)"
(fullscreen)="toggleFullscreen()"
(layoutModeUpdate)="updateLayoutMode($event)"
(readingDirection)="readingDirection = $event"
(immersiveMode)="immersiveMode = $event"
></app-reader-settings>
</ng-template>
</li>
@ -48,15 +72,14 @@
</app-drawer>
</div>
<div #readingSection class="reading-section {{ColumnLayout}}" [ngStyle]="{'padding-top': '62px'}"
[@isLoading]="isLoading ? true : false">
<div #readingSection class="reading-section {{ColumnLayout}}" [@isLoading]="isLoading ? true : false">
<div #readingHtml class="book-content" [ngStyle]="{'max-height': ColumnHeight, 'column-width': ColumnWidth}"
[innerHtml]="page" *ngIf="page !== undefined"></div>
[innerHtml]="page" *ngIf="page !== undefined" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)"></div>
<ng-container *ngIf="clickToPaginate">
<div class="left {{clickOverlayClass('left')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="prevPage()" tabindex="-1"></div>
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="nextPage()" tabindex="-1"></div>
<ng-container *ngIf="clickToPaginate">
<div class="left {{clickOverlayClass('left')}} no-observe" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)" tabindex="-1"></div>
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)" tabindex="-1"></div>
</ng-container>
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default)" (click)="$event.stopPropagation();">
@ -65,15 +88,17 @@
</div>
<ng-template #actionBar>
<div class="action-bar row g-0 justify-content-between">
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="prevPage()"
[disabled]="IsPrevDisabled"
<div class="action-bar row g-0 justify-content-between" *ngIf="!immersiveMode || drawerOpen">
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsPrevDisabled : IsNextDisabled"
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsPrevChapter : IsNextChapter) ? 'fa-angle-double-left' : 'fa-angle-left'}}" aria-hidden="true"></i>
<span class="d-none d-sm-block">&nbsp;{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}}</span>
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsPrevChapter : IsNextChapter) ? 'fa-angle-double-left' : 'fa-angle-left'}} {{readingDirection === ReadingDirection.RightToLeft ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" title="Go Back"><i class="fa fa-reply" aria-hidden="true"></i><span class="d-none d-sm-block">&nbsp;Go Back</span></button>
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()"><i class="fa fa-bars" aria-hidden="true"></i><span class="d-none d-sm-block">Settings</span></button>
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" title="Go Back">
<i class="fa fa-reply" aria-hidden="true"></i>
</button>
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()">
<i class="fa fa-bars" aria-hidden="true"></i></button>
<div class="book-title col-2 d-none d-sm-block">
<ng-container *ngIf="isLoading; else showTitle">
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
@ -82,15 +107,14 @@
</ng-container>
<ng-template #showTitle>
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode</span>)</span>
<span class="book-title-text ms-1" [title]="bookTitle">{{bookTitle}}</span>
<span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>
</ng-template>
</div>
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="d-none d-sm-block">&nbsp;Close</span></button>
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"
[disabled]="IsNextDisabled"
(click)="nextPage()" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsNextChapter : IsPrevChapter) ? 'fa-angle-double-right' : 'fa-angle-right'}}" aria-hidden="true"></i>
<span class="d-none d-sm-block">{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}}&nbsp;</span>
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsNextDisabled : IsPrevDisabled"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsNextChapter : IsPrevChapter) ? 'fa-angle-double-right' : 'fa-angle-right'}} {{readingDirection === ReadingDirection.LeftToRight ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>
</div>
</ng-template>

View File

@ -57,6 +57,22 @@ $primary-color: #0062cc;
padding-right: 2px;
}
.drawer-body {
.reader-pills {
justify-content: center;
margin: 0 0.25rem;
li a {
border: 1px solid var(--primary-color);
margin: 0 0.25rem;
.active {
border: unset;
}
}
}
}
// Drawer End
.fixed-top {
@ -67,16 +83,25 @@ $primary-color: #0062cc;
opacity: 0;
}
::ng-deep .bg-warning {
background-color: yellow;
}
.action-bar {
background-color: var(--br-actionbar-bg-color);
overflow: hidden;
box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%);
max-height: 62px;
max-height: 38px;
height: 38px;
.book-title-text {
text-align: center;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media(max-width: 875px) {
@ -89,6 +114,11 @@ $primary-color: #0062cc;
margin-top: 10px;
text-align: center;
text-transform: capitalize;
max-height: inherit;
}
.next-page-highlight {
color: var(--primary-color);
}
}
@ -98,7 +128,8 @@ $primary-color: #0062cc;
max-height: 100vh;
width: 100%;
//overflow: auto; // This will break progress reporting
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
padding-top: 38px;
}
.reader-container {
@ -108,10 +139,9 @@ $primary-color: #0062cc;
.book-content {
position: relative;
padding-top: 20px;
padding-bottom: 20px;
padding: 20px 0;
margin: 0px 0px;
height: calc(var(--vh)*100); // This will ensure bottom bar extends to the bottom of the screen
height: calc(var(--vh, 1vh) * 100); // This will ensure bottom bar extends to the bottom of the screen
a, :link {
color: var(--brtheme-link-text-color);
@ -120,6 +150,20 @@ $primary-color: #0062cc;
background-color: var(--brtheme-bg-color);
}
.pagination-cont {
background: var(--br-actionbar-bg-color);
border-radius: 5px;
padding: 5px 15px;
margin: 0 0 5px;
border: var(--drawer-pagination-border);
}
.virt-pagination-cont {
padding-bottom: 5px;
margin-bottom: 5px;
box-shadow: var(--drawer-pagination-horizontal-rule);
}
// This is essentially fitting the text to height and when you press next you are scrolling over by page width
@ -130,7 +174,13 @@ $primary-color: #0062cc;
overflow: hidden;
word-break: break-word;
overflow-wrap: break-word;
&.debug {
column-rule: 20px solid rebeccapurple;
}
}
}
.column-layout-2 {
@ -140,8 +190,14 @@ $primary-color: #0062cc;
overflow: hidden;
word-break: break-word;
overflow-wrap: break-word;
&.debug {
column-rule: 20px solid rebeccapurple;
}
}
}
// A bunch of resets so books render correctly
@ -182,6 +238,7 @@ $primary-color: #0062cc;
cursor: pointer;
opacity: 0;
background: transparent;
padding-top: 38px;
}
// This class pushes the click area to the left a bit to let users click the scrollbar
@ -195,6 +252,7 @@ $primary-color: #0062cc;
cursor: pointer;
opacity: 0;
background: transparent;
padding-top: 38px;
}
.left {
@ -207,6 +265,7 @@ $primary-color: #0062cc;
cursor: pointer;
opacity: 0;
background: transparent;
padding-top: 38px;
}
.highlight {

View File

@ -2,8 +2,8 @@ import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy,
import {DOCUMENT, Location} from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, fromEvent, Subject } from 'rxjs';
import { debounceTime, filter, take, takeUntil, tap } from 'rxjs/operators';
import { forkJoin, fromEvent, of, Subject } from 'rxjs';
import { catchError, debounceTime, take, takeUntil, tap } from 'rxjs/operators';
import { Chapter } from 'src/app/_models/chapter';
import { AccountService } from 'src/app/_services/account.service';
import { NavService } from 'src/app/_services/nav.service';
@ -24,7 +24,6 @@ import { BookTheme } from 'src/app/_models/preferences/book-theme';
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
import { PageStyle } from '../reader-settings/reader-settings.component';
import { User } from 'src/app/_models/user';
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
import { ThemeService } from 'src/app/_services/theme.service';
import { ScrollService } from 'src/app/_services/scroll.service';
import { PAGING_DIRECTION } from 'src/app/manga-reader/_models/reader-enums';
@ -40,7 +39,7 @@ interface HistoryPoint {
scrollOffset: number;
}
const TOP_OFFSET = -50 * 1.5; // px the sticky header takes up
const TOP_OFFSET = -50 * 1.5; // px the sticky header takes up // TODO: Do I need this or can I change it with new fixed top height
const CHAPTER_ID_NOT_FETCHED = -2;
const CHAPTER_ID_DOESNT_EXIST = -1;
@ -77,6 +76,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
volumeId!: number;
chapterId!: number;
chapter!: Chapter;
user!: User;
/**
* Reading List id. Defaults to -1.
*/
@ -112,30 +113,38 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls.
* @see Stack
*/
continuousChaptersStack: Stack<number> = new Stack(); // TODO: See if this can be moved into reader service so we can reduce code duplication between readers
continuousChaptersStack: Stack<number> = new Stack(); // TODO: See if continuousChaptersStack can be moved into reader service so we can reduce code duplication between readers (and also use ChapterInfo with it instead)
/**
* Belongs to the drawer component
*/
activeTabId: TabID = TabID.Settings;
/**
* Belongs to drawer component
*/
drawerOpen = false;
/**
* Book reader setting that hides the menuing system
*/
immersiveMode: boolean = false;
/**
* If we are loading from backend
*/
isLoading = true;
/**
* Title of the book. Rendered in action bars
*/
bookTitle: string = '';
clickToPaginate = false;
/**
* The boolean that decides if the clickToPaginate overlay is visible or not.
*/
clickToPaginateVisualOverlay = false;
clickToPaginateVisualOverlayTimeout: any = undefined; // For animation
clickToPaginateVisualOverlayTimeout2: any = undefined; // For kicking off animation, giving enough time to render html
page: SafeHtml | undefined = undefined; // This is the html we get from the server
styles: SafeHtml | undefined = undefined; // This is the css we get from the server
@ViewChild('readingHtml', {static: false}) readingHtml!: ElementRef<HTMLDivElement>;
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('reader', {static: true}) reader!: ElementRef;
/**
* This is the html we get from the server
*/
page: SafeHtml | undefined = undefined;
/**
* Next Chapter Id. This is not garunteed to be a valid ChapterId. Prefetched on page load (non-blocking).
*/
@ -174,22 +183,24 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
pageStyles!: PageStyle;
darkMode = true;
backgroundColor: string = 'white';
/**
* Offset for drawer and rendering canvas. Fixed to 62px.
*/
topOffset: number = 62;
topOffset: number = 38;
/**
* Used for showing/hiding bottom action bar. Calculates if there is enough scroll to show it.
* Will hide if all content in book is absolute positioned
*/
scrollbarNeeded = false;
readingDirection: ReadingDirection = ReadingDirection.LeftToRight;
private readonly onDestroy = new Subject<void>();
clickToPaginate = false;
/**
* Used solely for fullscreen to apply a hack
*/
darkMode = true;
/**
* A anchors that map to the page number. When you click on one of these, we will load a given page up for the user.
*/
pageAnchors: {[n: string]: number } = {};
currentPageAnchor: string = '';
/**
@ -200,7 +211,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Library Type used for rendering chapter or issue
*/
libraryType: LibraryType = LibraryType.Book;
/**
* If the web browser is in fullscreen mode
*/
@ -211,20 +221,34 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
layoutMode: BookPageLayoutMode = BookPageLayoutMode.Default;
/**
* Width of the document (in non-column layout), used for column layout virtual paging
*/
windowWidth: number = 0;
windowHeight: number = 0;
user!: User;
/**
* used to track if a click is a drag or not, for opening menu
*/
mousePosition = {
x: 0,
y: 0
};
/**
* Used to keep track of direction user is paging, to help with virtual paging on column layout
*/
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
private readonly onDestroy = new Subject<void>();
@ViewChild('readingHtml', {static: false}) readingHtml!: ElementRef<HTMLDivElement>;
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('reader', {static: true}) reader!: ElementRef;
get BookPageLayoutMode() {
@ -239,31 +263,75 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return ReadingDirection;
}
get PAGING_DIRECTION() {
return PAGING_DIRECTION;
}
/**
* Disables the Left most button
*/
get IsPrevDisabled(): boolean {
if (this.readingDirection === ReadingDirection.LeftToRight) {
// Acting as Previous button
return this.prevPageDisabled && this.pageNum === 0;
} else {
// Acting as a Next button
return this.nextPageDisabled && this.pageNum + 1 > this.maxPages - 1;
return this.isPrevPageDisabled();
}
// Acting as a Next button
return this.isNextPageDisabled();
}
get IsNextDisabled(): boolean {
if (this.readingDirection === ReadingDirection.LeftToRight) {
// Acting as Next button
return this.nextPageDisabled && this.pageNum + 1 > this.maxPages - 1;
} else {
// Acting as Previous button
return this.prevPageDisabled && this.pageNum === 0;
return this.isNextPageDisabled();
}
// Acting as Previous button
return this.isPrevPageDisabled();
}
get IsNextChapter(): boolean {
return this.pageNum + 1 >= this.maxPages;
isNextPageDisabled() {
const [currentVirtualPage, totalVirtualPages, _] = this.getVirtualPage();
const condition = (this.nextPageDisabled || this.nextChapterId === CHAPTER_ID_DOESNT_EXIST) && this.pageNum + 1 > this.maxPages - 1;
if (this.layoutMode !== BookPageLayoutMode.Default) {
return condition && currentVirtualPage === totalVirtualPages;
}
return condition;
}
isPrevPageDisabled() {
const [currentVirtualPage,,] = this.getVirtualPage();
const condition = (this.prevPageDisabled || this.prevChapterId === CHAPTER_ID_DOESNT_EXIST) && this.pageNum === 0;
if (this.layoutMode !== BookPageLayoutMode.Default) {
return condition && currentVirtualPage === 0;
}
return condition;
}
/**
* Determines if we show >> or >
*/
get IsNextChapter(): boolean {
if (this.layoutMode === BookPageLayoutMode.Default) {
return this.pageNum + 1 >= this.maxPages;
}
const [currentVirtualPage, totalVirtualPages, _] = this.getVirtualPage();
if (this.readingHtml == null) return this.pageNum + 1 >= this.maxPages;
return this.pageNum + 1 >= this.maxPages && (currentVirtualPage === totalVirtualPages);
}
/**
* Determines if we show << or <
*/
get IsPrevChapter(): boolean {
return this.pageNum === 0;
if (this.layoutMode === BookPageLayoutMode.Default) {
return this.pageNum === 0;
}
const [currentVirtualPage,,] = this.getVirtualPage();
if (this.readingHtml == null) return this.pageNum + 1 >= this.maxPages;
return this.pageNum === 0 && (currentVirtualPage === 0);
}
get ColumnWidth() {
@ -340,25 +408,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
// Find the element that is on screen to bookmark against
const intersectingEntries = Array.from(this.readingHtml.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span'))
.filter(element => !element.classList.contains('no-observe'))
.filter(entry => {
return this.utilityService.isInViewport(entry, this.topOffset);
});
intersectingEntries.sort(this.sortElements);
if (intersectingEntries.length > 0) {
let path = this.getXPathTo(intersectingEntries[0]);
if (path === '') { return; }
if (!path.startsWith('id')) {
path = '//html[1]/' + path;
}
this.lastSeenScrollPartPath = path;
}
const xpath: string | null | undefined = this.getFirstVisibleElementXPath();
if (xpath !== null && xpath !== undefined) this.lastSeenScrollPartPath = xpath;
if (this.lastSeenScrollPartPath !== '') {
this.saveProgress();
@ -485,13 +537,19 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.nextChapterId = chapterId;
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
this.nextChapterDisabled = true;
this.nextChapterPrefetched = true;
return;
}
this.setPageNum(this.pageNum);
});
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.prevChapterId = chapterId;
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
this.prevChapterDisabled = true;
this.prevChapterPrefetched = true; // If there is no prev chapter, then mark it as prefetched
return;
}
this.setPageNum(this.pageNum);
});
// Check if user progress has part, if so load it so we scroll to it
@ -507,13 +565,22 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
onResize(event: any){
// Update the window Height
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, window.innerHeight);
this.updateWidthAndHeightCalcs();
const resumeElement = this.getFirstVisibleElementXPath();
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
this.scrollTo(resumeElement); // This works pretty well, but not perfect
}
}
@HostListener('window:orientationchange', ['$event'])
onOrientationChange() {
// Update the window Height
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, window.innerHeight);
this.updateWidthAndHeightCalcs();
const resumeElement = this.getFirstVisibleElementXPath();
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
this.scrollTo(resumeElement); // This works pretty well, but not perfect
}
}
@HostListener('window:keydown', ['$event'])
@ -563,6 +630,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadPrevChapter() {
if (this.prevPageDisabled) { return; }
this.isLoading = true;
this.continuousChaptersStack.pop();
const prevChapter = this.continuousChaptersStack.peek();
@ -574,7 +642,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId) {
if (this.prevChapterPrefetched && this.prevChapterId === CHAPTER_ID_DOESNT_EXIST) {
return;
}
if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId && !this.prevChapterPrefetched) {
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.prevChapterId = chapterId;
this.loadChapter(chapterId, 'Prev');
@ -738,11 +810,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.readingSectionElemRef.nativeElement.clientHeight;
// Virtual Paging stuff
this.windowWidth = window.innerWidth
|| this.document.documentElement.clientWidth
|| this.document.body.clientWidth;
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, this.windowHeight);
this.updateWidthAndHeightCalcs();
this.updateLayoutMode(this.layoutMode || BookPageLayoutMode.Default);
// Find all the part ids and their top offset
@ -761,9 +829,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.reader.nativeElement.children
// We need to check if we are paging back, because we need to adjust the scroll
if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) {
this.scrollService.scrollToX(this.readingHtml.nativeElement.scrollWidth, this.readingHtml.nativeElement);
setTimeout(() => this.scrollService.scrollToX(this.readingHtml.nativeElement.scrollWidth, this.readingHtml.nativeElement));
} else {
this.scrollService.scrollToX(0, this.readingHtml.nativeElement);
setTimeout(() => this.scrollService.scrollToX(0, this.readingHtml.nativeElement));
}
}
}
@ -789,20 +857,35 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.pageNum >= this.maxPages - 10) {
// Tell server to cache the next chapter
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
if (!this.nextChapterPrefetched && this.nextChapterId !== CHAPTER_ID_DOESNT_EXIST) { // && !this.nextChapterDisabled
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1), catchError(err => {
this.nextChapterDisabled = true;
return of(null);
})).subscribe(res => {
this.nextChapterPrefetched = true;
});
}
} else if (this.pageNum <= 10) {
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
if (!this.prevChapterPrefetched && this.prevChapterId !== CHAPTER_ID_DOESNT_EXIST) { // && !this.prevChapterDisabled
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1), catchError(err => {
this.prevChapterDisabled = true;
return of(null);
})).subscribe(res => {
this.prevChapterPrefetched = true;
});
}
}
}
movePage(direction: PAGING_DIRECTION) {
if (direction === PAGING_DIRECTION.BACKWARDS) {
this.prevPage();
return;
}
this.nextPage();
}
prevPage() {
const oldPageNum = this.pageNum;
@ -810,13 +893,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// We need to handle virtual paging before we increment the actual page
if (this.layoutMode !== BookPageLayoutMode.Default) {
const [currentVirtualPage, _, pageWidth] = this.getVirtualPage();
const scrollOffset = this.readingHtml.nativeElement.scrollLeft;
const pageWidth = this.readingSectionElemRef.nativeElement.clientWidth - (this.readingSectionElemRef.nativeElement.clientWidth*(parseInt(this.pageStyles['margin-left'], 10) / 100))*2 + 20;
if (currentVirtualPage > 1) {
if (scrollOffset - pageWidth >= 0) {
this.scrollService.scrollToX(scrollOffset - pageWidth, this.readingHtml.nativeElement);
this.saveProgress();
// -2 apparently goes back 1 virtual page...
this.scrollService.scrollToX((currentVirtualPage - 2) * pageWidth, this.readingHtml.nativeElement);
this.handleScrollEvent();
return;
}
}
@ -834,9 +917,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
if (oldPageNum === this.pageNum) { return; }
// If prev and in default layout, need to handle somehow
this.loadPage();
}
@ -850,16 +930,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// We need to handle virtual paging before we increment the actual page
if (this.layoutMode !== BookPageLayoutMode.Default) {
const [currentVirtualPage, totalVirtualPages, pageWidth] = this.getVirtualPage();
const scrollOffset = this.readingHtml.nativeElement.scrollLeft;
const totalScroll = this.readingHtml.nativeElement.scrollWidth;
const pageWidth = this.readingSectionElemRef.nativeElement.clientWidth - (this.readingSectionElemRef.nativeElement.clientWidth*(parseInt(this.pageStyles['margin-left'], 10) / 100))*2 + 20;
if (scrollOffset + pageWidth < totalScroll) {
this.scrollService.scrollToX(scrollOffset + pageWidth, this.readingHtml.nativeElement);
if (currentVirtualPage < totalVirtualPages) {
//this.scrollService.scrollToX(scrollOffset + pageWidth, this.readingHtml.nativeElement);
// +0 apparently goes forward 1 virtual page...
this.scrollService.scrollToX((currentVirtualPage) * pageWidth, this.readingHtml.nativeElement);
this.handleScrollEvent();
this.saveProgress();
return;
}
}
@ -883,6 +960,86 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.loadPage();
}
/**
*
* @returns Total Page width (excluding margin)
*/
getPageWidth() {
if (this.readingSectionElemRef == null) return 0;
const margin = (this.readingSectionElemRef.nativeElement.clientWidth*(parseInt(this.pageStyles['margin-left'], 10) / 100))*2;
const columnGap = 20;
return this.readingSectionElemRef.nativeElement.clientWidth - margin + columnGap;
}
/**
* currentVirtualPage starts at 1
* @returns
*/
getVirtualPage() {
if (this.readingHtml === undefined || this.readingSectionElemRef === undefined) return [1, 1, 0];
const scrollOffset = this.readingHtml.nativeElement.scrollLeft;
const totalScroll = this.readingHtml.nativeElement.scrollWidth;
const pageWidth = this.getPageWidth();
// console.log('scrollOffset: ', scrollOffset);
// console.log('totalScroll: ', totalScroll);
// console.log('page width: ', pageWidth);
// console.log('delta: ', totalScroll - scrollOffset)
// // If everything fits on a single page
// if (totalScroll - pageWidth === 0) {
// return [1, 1, pageWidth];
// }
// // totalVirtualPages needs to be -1 because we can't scroll to totalOffset only on page 2
// const currentVirtualPage = Math.max(1, (scrollOffset === 0) ? 1 : Math.round(scrollOffset / pageWidth));
const delta = totalScroll - scrollOffset;
//let totalVirtualPages = Math.max(1, Math.round((totalScroll - pageWidth) / pageWidth));
const totalVirtualPages = Math.max(1, Math.round((totalScroll) / pageWidth));
let currentVirtualPage = 1;
// If first virtual page, i.e. totalScroll and delta are the same value
if (totalScroll - delta === 0) {
currentVirtualPage = 1;
// If second virtual page
} else if (totalScroll - delta === pageWidth) {
currentVirtualPage = 2;
// Otherwise do math to get correct page. i.e. scrollOffset + pageWidth (this accounts for first page offset)
} else {
currentVirtualPage = Math.min(Math.max(1, Math.round((scrollOffset + pageWidth) / pageWidth)), totalVirtualPages);
}
console.log('currentPage: ', currentVirtualPage , ' totalPage: ', totalVirtualPages);
return [currentVirtualPage, totalVirtualPages, pageWidth];
}
getFirstVisibleElementXPath() {
let resumeElement: string | null = null;
const intersectingEntries = Array.from(this.readingHtml.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span'))
.filter(element => !element.classList.contains('no-observe'))
.filter(entry => {
return this.utilityService.isInViewport(entry, this.topOffset);
});
intersectingEntries.sort(this.sortElements);
if (intersectingEntries.length > 0) {
let path = this.getXPathTo(intersectingEntries[0]);
if (path === '') { return; }
if (!path.startsWith('id')) {
path = '//html[1]/' + path;
}
resumeElement = path;
}
return resumeElement;
}
/**
* Applies styles onto the html of the book page
*/
@ -890,6 +1047,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pageStyles = pageStyles;
if (this.readingHtml === undefined || !this.readingHtml.nativeElement) return;
// Before we apply styles, let's get an element on the screen so we can scroll to it after any shifts
const resumeElement: string | null | undefined = this.getFirstVisibleElementXPath();
// Line Height must be placed on each element in the page
// Apply page level overrides
@ -916,14 +1077,20 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.renderer.setStyle(elem, item[0], item[1], RendererStyleFlags2.Important);
});
}
// After layout shifts, we need to refocus the scroll bar
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
this.updateWidthAndHeightCalcs();
this.scrollTo(resumeElement); // This works pretty well, but not perfect
}
}
setOverrideStyles(theme: BookTheme) {
// TODO: Put optimization in to avoid any work if the theme is the same as selected (or have reading settings control handle that)
/**
* Applies styles and classes that control theme
* @param theme
*/
updateColorTheme(theme: BookTheme) {
// Remove all themes
Array.from(this.document.querySelectorAll('style[id^="brtheme-"]')).forEach(elem => elem.remove());
@ -939,6 +1106,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.themeService.setBookTheme(theme.selector);
}
updateWidthAndHeightCalcs() {
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, window.innerHeight);
this.windowWidth = Math.max(this.readingSectionElemRef.nativeElement.clientWidth, window.innerWidth);
}
toggleDrawer() {
this.drawerOpen = !this.drawerOpen;
}
@ -1067,10 +1239,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
showPaginationOverlay(clickToPaginate: boolean) {
this.clickToPaginate = clickToPaginate;
// if (this.clickToPaginateVisualOverlayTimeout2 !== undefined) {
// clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
// this.clickToPaginateVisualOverlayTimeout2 = undefined;
// }
this.clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
if (!clickToPaginate) { return; }
@ -1105,7 +1273,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* @returns
*/
clickOverlayClass(side: 'right' | 'left') {
// TODO: See if we can use RXjs or a component to manage this
// TODO: See if we can use RXjs or a component to manage this aka an observable that emits the highlight to show at any given time
if (!this.clickToPaginateVisualOverlay) {
return '';
}
@ -1115,4 +1283,30 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
return side === 'right' ? 'highlight-2' : 'highlight';
}
toggleMenu(event: MouseEvent) {
const targetElement = (event.target as Element);
const mouseOffset = 5;
if (!this.immersiveMode) return;
if (targetElement.getAttribute('onclick') !== null || targetElement.getAttribute('href') !== null || targetElement.getAttribute('role') !== null || targetElement.getAttribute('kavita-part') != null) {
// Don't do anything, it's actionable
return;
}
if (
Math.abs(this.mousePosition.x - event.screenX) <= mouseOffset &&
Math.abs(this.mousePosition.y - event.screenY) <= mouseOffset
) {
this.drawerOpen = true;
}
}
mouseDown($event: MouseEvent) {
this.mousePosition.x = $event.screenX;
this.mousePosition.y = $event.screenY;
}
}

View File

@ -63,23 +63,38 @@
</h2>
</ng-template>
<ng-template ngbPanelContent>
<div class="controls">
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="readingdirection" class="form-label">Reading Direction</label>
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
<i class="fa {{readingDirectionModel === ReadingDirection.LeftToRight ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i>
<span class="phone-hidden">&nbsp;{{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
</button>
</div>
<div class="controls">
<label for="tap-pagination" class="form-label">Tap Pagination</label>
<div class="accent" id="tap-pagination-help">Click the edges of the screen to paginate</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="tap-pagination" class="form-label">Tap Pagination&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tapPagination-help"></i></label>
<ng-template #tapPaginationTooltip>Click the edges of the screen to paginate</ng-template>
<span class="visually-hidden" id="tapPagination-help">
<ng-container [ngTemplateOutlet]="tapPaginationTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tap-pagination-help">
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tapPagination-help">
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? 'On' : 'Off'}} </label>
</div>
</div>
<div class="controls">
<label id="fullscreen" class="form-label">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="immersive-mode" class="form-label">Immersive Mode&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="immersiveModeTooltip" role="button" tabindex="0" aria-describedby="immersiveMode-help"></i></label>
<ng-template #immersiveModeTooltip>This will hide the menu behind a click on the reader document and turn tap to paginate on</ng-template>
<span class="visually-hidden" id="immersiveMode-help">
<ng-container [ngTemplateOutlet]="immersiveModeTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="immersive-mode" formControlName="bookReaderImmersiveMode" class="form-check-input" aria-labelledby="immersiveMode-help">
<label>{{settingsForm.get('bookReaderImmersiveMode')?.value ? 'On' : 'Off'}} </label>
</div>
</div>
<!-- TODO: move this inline style into a class -->
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="fullscreen" class="form-label">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
<span class="visually-hidden" id="fullscreen-help">
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
@ -91,7 +106,11 @@
</div>
<div class="controls">
<label id="layout-mode" class="form-label">Layout Mode</label>
<label id="layout-mode" class="form-label" style="margin-bottom:0.5rem">Layout Mode&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="layoutTooltip" role="button" tabindex="1" aria-describedby="layout-help"></i></label>
<ng-template #layoutTooltip>Default: Mirrors epub file (usually one long scrolling page per chapter).<br/>1 Column: Creates a single virtual page at a time.<br/>2 Column: Creates two virtual pages at a time laid out side-by-side.</ng-template>
<span class="visually-hidden" id="layout-help">
<ng-container [ngTemplateOutlet]="layoutTooltip"></ng-container>
</span>
<br>
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
@ -120,7 +139,7 @@
<ng-template ngbPanelContent>
<div class="controls">
<ng-container *ngFor="let theme of themes">
<button class="btn btn-icon" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<button class="btn btn-icon color" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
{{theme.name}}
</button>

View File

@ -1,9 +1,43 @@
.dot {
height: 25px;
width: 25px;
border-radius: 50%;
.controls {
margin: 0.25rem 0 0.25rem;
.form-label {
margin: 0;
}
.btn.btn-icon {
display: flex;
width: 50%;
justify-content: center;
align-items: center;
&.color {
display: unset;
width: auto;
.dot {
height: 25px;
width: 25px;
border-radius: 50%;
margin: 0 auto;
}
}
}
.form-check.form-switch {
width: 50%;
display: flex;
justify-content: center;
input {
margin-right: 0.25rem;
}
}
}
.active {
border: 1px solid var(--primary-color);
}
}
::ng-deep .accordion-body {
padding: 0.25rem 1rem 1rem !important;
}

View File

@ -1,5 +1,5 @@
import { DOCUMENT } from '@angular/common';
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Subject, take, takeUntil } from 'rxjs';
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
@ -63,7 +63,6 @@ const mobileBreakpointMarginOverride = 700;
styleUrls: ['./reader-settings.component.scss']
})
export class ReaderSettingsComponent implements OnInit, OnDestroy {
/**
* Outputs when clickToPaginate is changed
*/
@ -88,6 +87,10 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
* Outputs when reading direction is changed
*/
@Output() readingDirection: EventEmitter<ReadingDirection> = new EventEmitter();
/**
* Outputs when immersive mode is changed
*/
@Output() immersiveMode: EventEmitter<boolean> = new EventEmitter();
user!: User;
/**
@ -127,7 +130,8 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
constructor(private bookService: BookService, private accountService: AccountService, @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {}
constructor(private bookService: BookService, private accountService: AccountService,
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {}
ngOnInit(): void {
@ -153,10 +157,9 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
if (this.user.preferences.bookReaderReadingDirection === undefined) {
this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight;
}
this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection;
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(fontName => {
const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family;
@ -180,7 +183,6 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
this.clickToPaginateChanged.emit(value);
});
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
this.pageStyles['line-height'] = value + '%';
@ -196,11 +198,25 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((layoutMode: BookPageLayoutMode) => {
console.log(layoutMode);
this.layoutModeUpdate.emit(layoutMode);
});
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user.preferences.bookReaderImmersiveMode, []));
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((immersiveMode: boolean) => {
if (immersiveMode) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
}
this.immersiveMode.emit(immersiveMode);
});
this.setTheme(this.user.preferences.bookReaderThemeName || this.themeService.defaultBookTheme);
// Emit first time so book reader gets the setting
this.readingDirection.emit(this.readingDirectionModel);
this.clickToPaginateChanged.emit(this.user.preferences.bookReaderTapToPaginate);
this.layoutModeUpdate.emit(this.user.preferences.bookReaderLayoutMode);
this.resetSettings();
} else {
this.resetSettings();

View File

@ -1,5 +1,5 @@
<div class="table-of-contents">
<h3>Table of Contents</h3>
<!-- <h3>Table of Contents</h3> -->
<div *ngIf="chapters.length === 0">
<em>This book does not have Table of Contents set in the metadata or a toc file</em>
</div>

View File

@ -7,5 +7,5 @@
}
.chapter-title {
padding-inline-start: 0px
padding-inline-start: 1rem;
}

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs';
import { BookChapterItem } from '../_models/book-chapter-item';

View File

@ -1,9 +1,8 @@
<app-side-nav-companion-bar [hasFilter]="false">
<h2 title>
<app-card-actionables [actions]="actions"></app-card-actionables>
Bookmarks
</h2>
<h6 subtitle style="margin-left:40px;">{{series?.length}} Series</h6>
<h6 subtitle>{{series?.length}} Series</h6>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout

View File

@ -71,6 +71,9 @@ export class BookmarksComponent implements OnInit, OnDestroy {
case(Action.DownloadBookmark):
this.downloadBookmarks(series);
break;
case(Action.ViewSeries):
this.router.navigate(['library', series.libraryId, 'series', series.id]);
break;
default:
break;
}

View File

@ -103,13 +103,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
download$: Observable<Download> | null = null;
downloadInProgress: boolean = false;
/**
* Handles touch events for selection on mobile devices
*/
prevTouchTime: number = 0;
/**
* Handles touch events for selection on mobile devices to ensure you are touch scrolling
* Handles touch events for selection on mobile devices to ensure you aren't touch scrolling
*/
prevOffset: number = 0;

View File

@ -158,7 +158,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
}
onPageChange(pagination: Pagination) {
this.filterUtilityService.updateUrlFromPagination(this.seriesPagination);
this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined);
this.loadPage();
}

View File

@ -183,7 +183,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
}
onPageChange(pagination: Pagination) {
this.filterUtilityService.updateUrlFromPagination(this.pagination);
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
this.loadPage();
}

View File

@ -1,6 +1,8 @@
<!-- TODO: If there is nothing, then show a message -->
<ng-container *ngIf="noData">
<p>Nothing to show here. Add some metadata to your library, read something or rate something.</p>
</ng-container>
<ng-container *ngIf="onDeck$ | async as onDeck">
<app-carousel-reel [items]="onDeck" title="On Deck">

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { map, Observable, shareReplay } from 'rxjs';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { map, merge, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
import { Genre } from 'src/app/_models/genre';
import { Series } from 'src/app/_models/series';
import { MetadataService } from 'src/app/_services/metadata.service';
@ -11,7 +11,7 @@ import { SeriesService } from 'src/app/_services/series.service';
templateUrl: './library-recommended.component.html',
styleUrls: ['./library-recommended.component.scss']
})
export class LibraryRecommendedComponent implements OnInit {
export class LibraryRecommendedComponent implements OnInit, OnDestroy {
@Input() libraryId: number = 0;
@ -24,30 +24,40 @@ export class LibraryRecommendedComponent implements OnInit {
genre: string = '';
genre$!: Observable<Genre>;
all$!: Observable<any>;
noData: boolean = true;
private onDestroy: Subject<void> = new Subject();
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService, private metadataService: MetadataService) { }
ngOnInit(): void {
this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId)
.pipe(map(p => p.result), shareReplay());
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId)
.pipe(map(p => p.result), shareReplay());
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
this.rediscover$ = this.recommendationService.getRediscover(this.libraryId)
.pipe(map(p => p.result), shareReplay());
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
this.onDeck$ = this.seriesService.getOnDeck(this.libraryId)
.pipe(map(p => p.result), shareReplay());
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(map(genres => genres[Math.floor(Math.random() * genres.length)]), shareReplay());
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(takeUntil(this.onDestroy), map(genres => genres[Math.floor(Math.random() * genres.length)]), shareReplay());
this.genre$.subscribe(genre => {
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(map(p => p.result), shareReplay());
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
});
this.all$ = merge(this.quickReads$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntil(this.onDestroy));
this.all$.subscribe(() => this.noData = false);
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
import { ReadingList } from 'src/app/_models/reading-list';
import { AccountService } from 'src/app/_services/account.service';
@ -24,7 +25,8 @@ export class ReadingListsComponent implements OnInit {
isAdmin: boolean = false;
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService) { }
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
private filterUtilityService: FilterUtilitiesService) { }
ngOnInit(): void {
this.loadPage();
@ -84,7 +86,7 @@ export class ReadingListsComponent implements OnInit {
}
onPageChange(pagination: Pagination) {
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);;
this.loadPage();
}

View File

@ -45,7 +45,7 @@
font-weight: bold;
display: inline-block;
vertical-align: middle;
width: 280px;
width: 100%;
}
.card-text {

View File

@ -49,11 +49,11 @@ export class FilterUtilitiesService {
* @param pagination
* @param filter
*/
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter) {
let params = '?page=' + pagination.currentPage;
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) {
const params = '?page=' + pagination.currentPage;
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
}
/**

View File

@ -87,7 +87,7 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
</div>
</div>
@ -95,7 +95,7 @@
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="show-screen-hints" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
</div>
</div>
@ -123,13 +123,24 @@
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
<label id="taptopaginate" class="form-check-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="visually-hidden" 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>
</div>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="immersivemode-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersivemode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
<label id="immersivemode" class="form-check-label">Immersive Mode</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
<ng-template #immersivemodeOptionTooltip>This will hide the menu behind a click on the reader document and turn tap to paginate on</ng-template>
<span class="visually-hidden" id="settings-immersivemode-option-help">This will hide the menu behind a click on the reader document and turn tap to paginate on</span>
</div>
</div>
</div>
</div>
<div class="row g-0">

View File

@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { take, takeUntil } from 'rxjs/operators';
import { Title } from '@angular/platform-browser';
import { BookService } from 'src/app/book-reader/book.service';
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes } from 'src/app/_models/preferences/preferences';
@ -11,7 +11,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { SettingsService } from 'src/app/admin/settings.service';
import { bookColorThemes } from 'src/app/book-reader/reader-settings/reader-settings.component';
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
import { forkJoin } from 'rxjs';
import { forkJoin, Subject } from 'rxjs';
enum AccordionPanelID {
ImageReader = 'image-reader',
@ -55,6 +55,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
opdsEnabled: boolean = false;
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
private onDestroy = new Subject<void>();
get AccordionPanelID() {
return AccordionPanelID;
}
@ -114,6 +116,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
});
@ -125,10 +128,18 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
const values = this.passwordChangeForm.value;
this.passwordsMatch = values.password === values.confirmPassword;
}));
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(mode => {
if (mode) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
}
});
}
ngOnDestroy() {
this.obserableHandles.forEach(o => o.unsubscribe());
this.onDestroy.next();
this.onDestroy.complete();
}
public get password() { return this.passwordChangeForm.get('password'); }
@ -152,6 +163,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsForm.get('bookReaderLayoutMode')?.setValue(this.user.preferences.bookReaderLayoutMode);
this.settingsForm.get('bookReaderThemeName')?.setValue(this.user.preferences.bookReaderThemeName);
this.settingsForm.get('theme')?.setValue(this.user.preferences.theme);
this.settingsForm.get('bookReaderImmersiveMode')?.setValue(this.user.preferences.bookReaderImmersiveMode);
}
resetPasswordForm() {
@ -180,7 +192,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10),
bookReaderLayoutMode: parseInt(modelSettings.bookReaderLayoutMode, 10),
bookReaderThemeName: modelSettings.bookReaderThemeName,
theme: modelSettings.theme
theme: modelSettings.theme,
bookReaderImmersiveMode: modelSettings.bookReaderImmersiveMode
};
this.obserableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {