More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes
This commit is contained in:
Joe Milazzo 2023-05-19 11:32:24 -05:00 committed by GitHub
parent 5f607b3dab
commit 64666540cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 131 additions and 307 deletions

View file

@ -1,200 +0,0 @@
import { ElementRef, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DomHelperService {
constructor() {}
// from: https://stackoverflow.com/questions/40597658/equivalent-of-angular-equals-in-angular2#44649659
deepEquals(x: any, y: any) {
if (x === y) {
return true; // if both x and y are null or undefined and exactly the same
} else if (!(x instanceof Object) || !(y instanceof Object)) {
return false; // if they are not strictly equal, they both need to be Objects
} else if (x.constructor !== y.constructor) {
// they must have the exact same prototype chain, the closest we can do is
// test their constructor.
return false;
} else {
for (const p in x) {
if (!x.hasOwnProperty(p)) {
continue; // other properties were tested using x.constructor === y.constructor
}
if (!y.hasOwnProperty(p)) {
return false; // allows to compare x[ p ] and y[ p ] when set to undefined
}
if (x[p] === y[p]) {
continue; // if they have the same strict value or identity then they are equal
}
if (typeof (x[p]) !== 'object') {
return false; // Numbers, Strings, Functions, Booleans must be strictly equal
}
if (!this.deepEquals(x[p], y[p])) {
return false;
}
}
for (const p in y) {
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
return false;
}
}
return true;
}
}
isHidden(node: ElementRef){
const el = node.nativeElement?node.nativeElement:node;
const elemStyle = window.getComputedStyle(el);
return el.style.display === 'none' || elemStyle.visibility === 'hidden' || el.hasAttribute('hidden') || elemStyle.display === 'none';
}
isTabable(node: ElementRef): boolean {
const el = node.nativeElement?node.nativeElement:node;
const tagName = el.tagName;
if(this.isHidden(node)){
return false;
}
// el.attribute:NamdedNodeMap
if (el.attributes.hasOwnProperty('tabindex')) {
return (parseInt(el.attributes.getNamedItem('tabindex'),10) >= 0);
}
if (tagName === 'A' || tagName === 'AREA' || tagName === 'BUTTON' || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') {
if (tagName === 'A' || tagName === 'AREA') {
return (el.attributes.getNamedItem('href') !== '');
}
return !el.attributes.hasOwnProperty('disabled'); // check for cases when: disabled="true" and disabled="false"
}
return false;
}
private isValidChild(child: any): boolean { // child:ElementRef.nativeElement
return child.nodeType == 1 && child.nodeName != 'SCRIPT' && child.nodeName != 'STYLE';
}
private hasValidParent(obj: any) { // obj:ElementRef.nativeElement
return (this.isValidChild(obj) && obj.parentElement.nodeName !== 'BODY');
}
private traverse(obj: any, fromTop: boolean): ElementRef | undefined | boolean {
// obj:ElementRef||ElementRef.nativeElement
var obj = obj? (obj.nativeElement?obj.nativeElement:obj) : document.getElementsByTagName('body')[0];
if (this.isValidChild(obj) && this.isTabable(obj)) {
return obj;
}
// If object is hidden, skip it's children
if (this.isValidChild(obj) && this.isHidden(obj)) {
return undefined;
}
// If object is hidden, skip it's children
if (obj.classList && obj.classList.contains('ng-hide')) { // some nodes don't have classList?!
return false;
}
if (obj.hasChildNodes()) {
var child;
if (fromTop) {
child = obj.firstChild;
} else {
child = obj.lastChild;
}
while(child) {
var res = this.traverse(child, fromTop);
if(res){
return res;
}
else{
if (fromTop) {
child = child.nextSibling;
} else {
child = child.previousSibling;
}
}
}
}
else{
return undefined;
}
}
previousElement(el: any, isFocusable: boolean): any { // ElementRef | undefined | boolean
var elem = el.nativeElement ? el.nativeElement : el;
if (el.hasOwnProperty('length')) {
elem = el[0];
}
var parent = elem.parentElement;
var previousElem = undefined;
if(isFocusable) {
if (this.hasValidParent(elem)) {
var siblings = parent.children;
if (siblings.length > 0) {
// Good practice to splice out the elem from siblings if there, saving some time.
// We allow for a quick check for jumping to parent first before removing.
if (siblings[0] === elem) {
// If we are looking at immidiate parent and elem is first child, we need to go higher
var e = this.previousElement(elem.parentNode, isFocusable);
if (this.isTabable(e)) {
return e;
}
} else {
// I need to filter myself and any nodes next to me from the siblings
var indexOfElem = Array.prototype.indexOf.call(siblings, elem);
const that = this;
siblings = Array.prototype.filter.call(siblings, function(item, itemIndex) {
if (!that.deepEquals(elem, item) && itemIndex < indexOfElem) {
return true;
}
});
}
// We need to search backwards
for (var i = 0; i <= siblings.length-1; i++) {//for (var i = siblings.length-1; i >= 0; i--) {
var ret = this.traverse(siblings[i], false);
if (ret !== undefined) {
return ret;
}
}
var e = this.previousElement(elem.parentNode, isFocusable);
if (this.isTabable(e)) {
return e;
}
}
}
} else {
var siblings = parent.children;
if (siblings.length > 1) {
// Since indexOf is on Array.prototype and parent.children is a NodeList, we have to use call()
var index = Array.prototype.indexOf.call(siblings, elem);
previousElem = siblings[index-1];
}
}
return previousElem;
};
lastTabableElement(el: any) {
/* This will return the first tabable element from the parent el */
var elem = el.nativeElement?el.nativeElement:el;
if (el.hasOwnProperty('length')) {
elem = el[0];
}
return this.traverse(elem, false);
};
firstTabableElement(el: any) {
/* This will return the first tabable element from the parent el */
var elem = el.nativeElement ? el.nativeElement : el;
if (el.hasOwnProperty('length')) {
elem = el[0];
}
return this.traverse(elem, true);
};
isInDOM(obj: Node) {
return document.documentElement.contains(obj);
}
}

View file

@ -29,7 +29,7 @@ export interface DownloadEvent {
/**
* Progress of the download itself
*/
progress: number;
progress: number;
}
/**
@ -37,7 +37,7 @@ export interface DownloadEvent {
*/
export type DownloadEntityType = 'volume' | 'chapter' | 'series' | 'bookmark' | 'logs';
/**
* Valid entities for downloading. Undefined exclusively for logs.
* Valid entities for downloading. Undefined exclusively for logs.
*/
export type DownloadEntity = Series | Volume | Chapter | PageBookmark[] | undefined;
@ -56,14 +56,14 @@ export class DownloadService {
public activeDownloads$ = this.downloadsSource.asObservable();
constructor(private httpClient: HttpClient, private confirmService: ConfirmService,
constructor(private httpClient: HttpClient, private confirmService: ConfirmService,
@Inject(SAVER) private save: Saver, private accountService: AccountService) { }
/**
* Returns the entity subtitle (for the event widget) for a given entity
* @param downloadEntityType
* @param downloadEntity
* @returns
* @param downloadEntityType
* @param downloadEntity
* @returns
*/
downloadSubtitle(downloadEntityType: DownloadEntityType, downloadEntity: DownloadEntity | undefined) {
switch (downloadEntityType) {
@ -82,13 +82,13 @@ export class DownloadService {
/**
* Downloads the entity to the user's system. This handles everything around downloads. This will prompt the user based on size checks and UserPreferences.PromptForDownload.
* This will perform the download at a global level, if you need a handle to the download in question, use downloadService.activeDownloads$ and perform a filter on it.
* @param entityType
* @param entity
* This will perform the download at a global level, if you need a handle to the download in question, use downloadService.activeDownloads$ and perform a filter on it.
* @param entityType
* @param entity
* @param callback Optional callback. Returns the download or undefined (if the download is complete).
*/
download(entityType: DownloadEntityType, entity: DownloadEntity, callback?: (d: Download | undefined) => void) {
let sizeCheckCall: Observable<number>;
let sizeCheckCall: Observable<number>;
let downloadCall: Observable<Download>;
switch (entityType) {
case 'series':
@ -155,10 +155,10 @@ export class DownloadService {
private downloadLogs() {
const downloadType = 'logs';
const subtitle = this.downloadSubtitle(downloadType, undefined);
return this.httpClient.get(this.baseUrl + 'server/logs',
return this.httpClient.get(this.baseUrl + 'server/logs',
{observe: 'events', responseType: 'blob', reportProgress: true}
).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => {
this.save(blob, decodeURIComponent(filename));
}),
@ -170,10 +170,10 @@ export class DownloadService {
private downloadSeries(series: Series) {
const downloadType = 'series';
const subtitle = this.downloadSubtitle(downloadType, series);
return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id,
return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id,
{observe: 'events', responseType: 'blob', reportProgress: true}
).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => {
this.save(blob, decodeURIComponent(filename));
}),
@ -209,11 +209,12 @@ export class DownloadService {
private downloadChapter(chapter: Chapter) {
const downloadType = 'chapter';
const subtitle = this.downloadSubtitle(downloadType, chapter);
return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
{observe: 'events', responseType: 'blob', reportProgress: true}
).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => {
console.log('saving: ', filename)
this.save(blob, decodeURIComponent(filename));
}),
tap((d) => this.updateDownloadState(d, downloadType, subtitle)),
@ -224,10 +225,10 @@ export class DownloadService {
private downloadVolume(volume: Volume): Observable<Download> {
const downloadType = 'volume';
const subtitle = this.downloadSubtitle(downloadType, volume);
return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
{observe: 'events', responseType: 'blob', reportProgress: true}
).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => {
this.save(blob, decodeURIComponent(filename));
}),
@ -244,10 +245,10 @@ export class DownloadService {
const downloadType = 'bookmark';
const subtitle = this.downloadSubtitle(downloadType, bookmarks);
return this.httpClient.post(this.baseUrl + 'download/bookmarks', {bookmarks},
return this.httpClient.post(this.baseUrl + 'download/bookmarks', {bookmarks},
{observe: 'events', responseType: 'blob', reportProgress: true}
).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => {
this.save(blob, decodeURIComponent(filename));
}),