diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index 95828bfd7..d32e6eef5 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -7,11 +7,11 @@
-
+
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/API.Tests/Parser/BookParserTests.cs b/API.Tests/Parser/BookParserTests.cs
index 003dbfecc..52fd02ae8 100644
--- a/API.Tests/Parser/BookParserTests.cs
+++ b/API.Tests/Parser/BookParserTests.cs
@@ -39,4 +39,5 @@ public class BookParserTests
// var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
// Assert.Equal(expected, actual);
// }
+
}
diff --git a/API/API.csproj b/API/API.csproj
index 4ef864443..a48c5b6c0 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -60,8 +60,8 @@
-
-
+
+
@@ -102,9 +102,9 @@
-
+
-
+
diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs
index 1e7026cfd..9b81e1e04 100644
--- a/API/Controllers/BookController.cs
+++ b/API/Controllers/BookController.cs
@@ -98,9 +98,10 @@ public class BookController : BaseApiController
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var key = BookService.CoalesceKeyForAnyFile(book, file);
- if (!book.Content.AllFiles.Local.ContainsKey(key)) return BadRequest("File was not found in book");
- var bookFile = book.Content.AllFiles.Local[key];
+ if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest("File was not found in book");
+
+ var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key);
var content = await bookFile.ReadContentAsBytesAsync();
var contentType = BookService.GetContentType(bookFile.ContentType);
diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs
index 754ed7503..419bd3d97 100644
--- a/API/Controllers/DownloadController.cs
+++ b/API/Controllers/DownloadController.cs
@@ -117,7 +117,7 @@ public class DownloadController : BaseApiController
private ActionResult GetFirstFileDownload(IEnumerable files)
{
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
- return PhysicalFile(zipFile, contentType, System.Web.HttpUtility.UrlEncode(fileDownloadName), true);
+ return PhysicalFile(zipFile, contentType, Uri.EscapeDataString(fileDownloadName), true);
}
///
@@ -163,7 +163,7 @@ public class DownloadController : BaseApiController
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
- return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(downloadName), true);
+ return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true);
}
catch (Exception ex)
{
diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs
index 2b950f00f..95c796573 100644
--- a/API/Controllers/SettingsController.cs
+++ b/API/Controllers/SettingsController.cs
@@ -184,7 +184,7 @@ public class SettingsController : BaseApiController
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
{
- if (OsInfo.IsDocker) break;
+ if (OsInfo.IsDocker) continue;
setting.Value = updateSettingsDto.Port + string.Empty;
// Port is managed in appSetting.json
Configuration.Port = updateSettingsDto.Port;
@@ -193,7 +193,7 @@ public class SettingsController : BaseApiController
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
{
- if (OsInfo.IsDocker) break;
+ if (OsInfo.IsDocker) continue;
// Validate IP addresses
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(','))
{
@@ -217,14 +217,7 @@ public class SettingsController : BaseApiController
? $"{path}/"
: path;
setting.Value = path;
- try
- {
- Configuration.BaseUrl = updateSettingsDto.BaseUrl;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not set base url. Give this exception to majora2007");
- }
+ Configuration.BaseUrl = updateSettingsDto.BaseUrl;
_unitOfWork.SettingsRepository.Update(setting);
}
diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs
index 23919e3bf..aa8660418 100644
--- a/API/Services/BookService.cs
+++ b/API/Services/BookService.cs
@@ -127,7 +127,7 @@ public class BookService : IBookService
var hrefParts = CleanContentKeys(anchor.GetAttributeValue("href", string.Empty))
.Split("#");
// Some keys get uri encoded when parsed, so replace any of those characters with original
- var mappingKey = HttpUtility.UrlDecode(hrefParts[0]);
+ var mappingKey = Uri.UnescapeDataString(hrefParts[0]);
if (!mappings.ContainsKey(mappingKey))
{
@@ -136,6 +136,15 @@ public class BookService : IBookService
var part = hrefParts.Length > 1
? hrefParts[1]
: anchor.GetAttributeValue("href", string.Empty);
+
+ // hrefParts[0] might not have path from mappings
+ var pageKey = mappings.Keys.FirstOrDefault(mKey => mKey.EndsWith(hrefParts[0]));
+ if (!string.IsNullOrEmpty(pageKey))
+ {
+ mappings.TryGetValue(pageKey, out currentPage);
+ }
+
+
anchor.Attributes.Add("kavita-page", $"{currentPage}");
anchor.Attributes.Add("kavita-part", part);
anchor.Attributes.Remove("href");
@@ -186,9 +195,9 @@ public class BookService : IBookService
{
key = prepend + key;
}
- if (!book.Content.AllFiles.Local.ContainsKey(key)) continue;
+ if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue;
- var bookFile = book.Content.AllFiles.Local[key];
+ //var bookFile = book.Content.AllFiles.Local[key];
var content = await bookFile.ReadContentAsBytesAsync();
importBuilder.Append(Encoding.UTF8.GetString(content));
}
@@ -227,7 +236,6 @@ public class BookService : IBookService
private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend)
{
- //foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml))
foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml))
{
if (!match.Success) continue;
@@ -238,7 +246,6 @@ public class BookService : IBookService
private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend)
{
- //foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex().Matches(stylesheetHtml))
foreach (Match match in Parser.FontSrcUrlRegex.Matches(stylesheetHtml))
{
if (!match.Success) continue;
@@ -249,7 +256,6 @@ public class BookService : IBookService
private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book)
{
- //var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex().Matches(stylesheetHtml);
var matches = Parser.CssImageUrlRegex.Matches(stylesheetHtml);
foreach (Match match in matches)
{
@@ -257,7 +263,7 @@ public class BookService : IBookService
var importFile = match.Groups["Filename"].Value;
var key = CleanContentKeys(importFile);
- if (!book.Content.AllFiles.Local.ContainsKey(key)) continue;
+ if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) continue;
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + key);
}
@@ -290,7 +296,7 @@ public class BookService : IBookService
var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
image.Attributes.Remove(key);
// UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx
- image.Attributes.Add(key, $"{apiBase}" + HttpUtility.UrlEncode(imageFile));
+ image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile));
// Add a custom class that the reader uses to ensure images stay within reader
parent.AddClass("kavita-scale-width-container");
@@ -307,9 +313,9 @@ public class BookService : IBookService
///
private static string GetKeyForImage(EpubBookRef book, string imageFile)
{
- if (book.Content.Images.Local.ContainsKey(imageFile)) return imageFile;
+ if (book.Content.Images.ContainsLocalFileRefWithKey(imageFile)) return imageFile;
- var correctedKey = book.Content.Images.Local.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
+ var correctedKey = book.Content.Images.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
imageFile = correctedKey;
@@ -318,13 +324,14 @@ public class BookService : IBookService
{
// 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.Local.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
+ book.Content.Images.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
if (correctedKey != null)
{
imageFile = correctedKey;
}
}
+
return imageFile;
}
@@ -342,6 +349,7 @@ public class BookService : IBookService
}
private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary mappings)
+
{
var anchors = doc.DocumentNode.SelectNodes("//a");
if (anchors == null) return;
@@ -372,9 +380,9 @@ public class BookService : IBookService
var key = CleanContentKeys(styleLinks.Attributes["href"].Value);
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
// In this case, we will do a search for the key that ends with
- if (!book.Content.Css.Local.ContainsKey(key))
+ if (!book.Content.Css.ContainsLocalFileRefWithKey(key))
{
- var correctedKey = book.Content.Css.Local.Keys.SingleOrDefault(s => s.EndsWith(key));
+ var correctedKey = book.Content.Css.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(key));
if (correctedKey == null)
{
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
@@ -386,7 +394,7 @@ public class BookService : IBookService
try
{
- var cssFile = book.Content.Css.Local[key];
+ var cssFile = book.Content.Css.GetLocalFileRefByKey(key);
var styleContent = await ScopeStyles(await cssFile.ReadContentAsync(), apiBase,
cssFile.FilePath, book);
@@ -507,6 +515,7 @@ public class BookService : IBookService
item.Property == "display-seq" && item.Refines == metadataItem.Refines);
if (count == null || count.Content == "0")
{
+ // TODO: Rewrite this to use a StringBuilder
// Treat this as a Collection
info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") + readingListElem.Title.Replace(",", "_");
}
@@ -687,7 +696,9 @@ public class BookService : IBookService
foreach (var contentFileRef in await book.GetReadingOrderAsync())
{
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) continue;
+ // Some keys are different than FilePath, so we add both to ease loookup
dict.Add(contentFileRef.FilePath, pageCount); // FileName -> FilePath
+ dict.TryAdd(contentFileRef.Key, pageCount); // FileName -> FilePath
pageCount += 1;
}
@@ -861,7 +872,7 @@ public class BookService : IBookService
if (mappings.ContainsKey(CleanContentKeys(key))) return key;
// Fallback to searching for key (bad epub metadata)
- var correctedKey = book.Content.Html.Local.Keys.FirstOrDefault(s => s.EndsWith(key));
+ var correctedKey = book.Content.Html.Local.Select(s => s.Key).FirstOrDefault(s => s.EndsWith(key));
if (!string.IsNullOrEmpty(correctedKey))
{
key = correctedKey;
@@ -885,17 +896,18 @@ public class BookService : IBookService
public static string CoalesceKeyForAnyFile(EpubBookRef book, string key)
{
- if (book.Content.AllFiles.Local.ContainsKey(key)) return key;
+ if (book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return key;
var cleanedKey = CleanContentKeys(key);
- if (book.Content.AllFiles.Local.ContainsKey(cleanedKey)) return cleanedKey;
+ if (book.Content.AllFiles.ContainsLocalFileRefWithKey(cleanedKey)) return cleanedKey;
+ // TODO: Figure this out
// Fallback to searching for key (bad epub metadata)
- var correctedKey = book.Content.AllFiles.Local.Keys.SingleOrDefault(s => s.EndsWith(key));
- if (!string.IsNullOrEmpty(correctedKey))
- {
- key = correctedKey;
- }
+ // var correctedKey = book.Content.AllFiles.Keys.SingleOrDefault(s => s.EndsWith(key));
+ // if (!string.IsNullOrEmpty(correctedKey))
+ // {
+ // key = correctedKey;
+ // }
return key;
}
@@ -928,7 +940,7 @@ public class BookService : IBookService
foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null))
{
- var key = CoalesceKey(book, mappings, nestedChapter.Link?.ContentFileName);
+ var key = CoalesceKey(book, mappings, nestedChapter.Link?.ContentFilePath);
if (mappings.TryGetValue(key, out var mapping))
{
nestedChapters.Add(new BookChapterItem
@@ -947,12 +959,15 @@ public class BookService : IBookService
if (chaptersList.Count != 0) return chaptersList;
// Generate from TOC from links (any point past this, Kavita is generating as a TOC doesn't exist)
- var tocPage = book.Content.Html.Local.Keys.FirstOrDefault(k => k.ToUpper().Contains("TOC"));
- if (tocPage == null) return chaptersList;
+ var tocPage = book.Content.Html.Local.Select(s => s.Key).FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) ||
+ k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase));
+ if (string.IsNullOrEmpty(tocPage)) return chaptersList;
// Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content
+ if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList;
+ var content = await file.ReadContentAsync();
+
var doc = new HtmlDocument();
- var content = await book.Content.Html.Local[tocPage].ReadContentAsync();
doc.LoadHtml(content);
var anchors = doc.DocumentNode.SelectNodes("//a");
if (anchors == null) return chaptersList;
@@ -1096,7 +1111,7 @@ public class BookService : IBookService
}
else
{
- var groupKey = CoalesceKey(book, mappings, navigationItem.Link.ContentFileName);
+ var groupKey = CoalesceKey(book, mappings, navigationItem.Link.ContentFilePath);
if (mappings.ContainsKey(groupKey))
{
chaptersList.Add(new BookChapterItem
@@ -1133,8 +1148,8 @@ public class BookService : IBookService
{
// Try to get the cover image from OPF file, if not set, try to parse it from all the files, then result to the first one.
var coverImageContent = epubBook.Content.Cover
- ?? epubBook.Content.Images.Local.Values.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) // FileName -> FilePath
- ?? epubBook.Content.Images.Local.Values.FirstOrDefault();
+ ?? epubBook.Content.Images.Local.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) // FileName -> FilePath
+ ?? epubBook.Content.Images.Local.FirstOrDefault();
if (coverImageContent == null) return string.Empty;
using var stream = coverImageContent.GetContentStream();
diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs
index cec383905..d5b9617a1 100644
--- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs
+++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs
@@ -172,7 +172,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
{
using var book = await EpubReader.OpenBookAsync(filePath, BookService.BookReaderOptions);
- var totalPages = book.Content.Html.Local.Values;
+ var totalPages = book.Content.Html.Local;
foreach (var bookPage in totalPages)
{
var progress = Math.Max(0F,
diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs
index 23ed4a7b6..d2a583a22 100644
--- a/API/Services/Tasks/Scanner/Parser/Parser.cs
+++ b/API/Services/Tasks/Scanner/Parser/Parser.cs
@@ -1058,4 +1058,19 @@ public static class Parser
{
return string.IsNullOrEmpty(name) ? string.Empty : name.Replace('_', ' ');
}
+
+ public static string? ExtractFilename(string fileUrl)
+ {
+ var matches = Parser.CssImageUrlRegex.Matches(fileUrl);
+ foreach (Match match in matches)
+ {
+ if (!match.Success) continue;
+
+ // NOTE: This is failing for //localhost:5000/api/book/29919/book-resources?file=OPS/images/tick1.jpg
+ var importFile = match.Groups["Filename"].Value;
+ if (!importFile.Contains("?")) return importFile;
+ }
+
+ return null;
+ }
}
diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs
index d3609de48..13faee4e8 100644
--- a/API/Services/TokenService.cs
+++ b/API/Services/TokenService.cs
@@ -97,9 +97,8 @@ public class TokenService : ITokenService
}
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken);
- if (!validated)
+ if (!validated && tokenContent.ValidTo > DateTime.UtcNow.Add(TimeSpan.FromHours(1)))
{
-
_logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token");
return null;
}
diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts
index 26d683c7c..a897b0550 100644
--- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts
+++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts
@@ -771,7 +771,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const links = this.readingSectionElemRef.nativeElement.querySelectorAll('a');
links.forEach((link: any) => {
link.addEventListener('click', (e: any) => {
- console.log('Link clicked: ', e);
if (!e.target.attributes.hasOwnProperty('kavita-page')) { return; }
const page = parseInt(e.target.attributes['kavita-page'].value, 10);
if (this.adhocPageHistory.peek()?.page !== this.pageNum) {
diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
index 09bdb95fe..76a98032a 100644
--- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
+++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
@@ -9,10 +9,10 @@
@@ -28,7 +28,7 @@
{{item.title}}
-
+
@@ -40,7 +40,7 @@
{{item.title}}
-
+
@@ -54,7 +54,7 @@
{{item.title}}
-
+
@@ -72,7 +72,7 @@
{{item.title}}
-
+
@@ -84,7 +84,7 @@
-
+
@@ -98,11 +98,11 @@
-
+
-
+
0">
Characters
@@ -111,7 +111,7 @@
-
+
@@ -124,7 +124,7 @@
-
+
@@ -137,7 +137,7 @@
-
+
@@ -150,7 +150,7 @@
-
+
@@ -163,7 +163,7 @@
-
+
@@ -175,11 +175,11 @@
-
+
-
+
0">
Pencillers
@@ -188,7 +188,7 @@
-
+
@@ -201,20 +201,20 @@
-
+
-
\ No newline at end of file
+
diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts
index 990707851..2dc8c3818 100644
--- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts
+++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts
@@ -29,7 +29,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
@Input() series!: Series;
isCollapsed: boolean = true;
- hasExtendedProperites: boolean = false;
+ hasExtendedProperties: boolean = false;
imageService = inject(ImageService);
@@ -55,20 +55,20 @@ export class SeriesMetadataDetailComponent implements OnChanges {
return this.seriesMetadata?.webLinks.split(',') || [];
}
- constructor(public utilityService: UtilityService, public metadataService: MetadataService,
+ constructor(public utilityService: UtilityService, public metadataService: MetadataService,
private router: Router, public readerService: ReaderService,
private readonly cdRef: ChangeDetectorRef) {
-
+
}
-
+
ngOnChanges(changes: SimpleChanges): void {
- this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 ||
- this.seriesMetadata.editors.length > 0 ||
- this.seriesMetadata.coverArtists.length > 0 ||
+ this.hasExtendedProperties = this.seriesMetadata.colorists.length > 0 ||
+ this.seriesMetadata.editors.length > 0 ||
+ this.seriesMetadata.coverArtists.length > 0 ||
this.seriesMetadata.inkers.length > 0 ||
this.seriesMetadata.letterers.length > 0 ||
this.seriesMetadata.pencillers.length > 0 ||
- this.seriesMetadata.publishers.length > 0 ||
+ this.seriesMetadata.publishers.length > 0 ||
this.seriesMetadata.translators.length > 0 ||
this.seriesMetadata.tags.length > 0;
diff --git a/UI/Web/src/app/shared/_services/dom-helper.service.ts b/UI/Web/src/app/shared/_services/dom-helper.service.ts
deleted file mode 100644
index 22aa1adf3..000000000
--- a/UI/Web/src/app/shared/_services/dom-helper.service.ts
+++ /dev/null
@@ -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);
- }
-
-}
diff --git a/UI/Web/src/app/shared/_services/download.service.ts b/UI/Web/src/app/shared/_services/download.service.ts
index a6439ba1b..7cfb06e4b 100644
--- a/UI/Web/src/app/shared/_services/download.service.ts
+++ b/UI/Web/src/app/shared/_services/download.service.ts
@@ -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;
+ let sizeCheckCall: Observable;
let downloadCall: Observable;
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 {
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));
}),
diff --git a/openapi.json b/openapi.json
index 8bdce79c3..3f35d590d 100644
--- a/openapi.json
+++ b/openapi.json
@@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
- "version": "0.7.2.11"
+ "version": "0.7.2.12"
},
"servers": [
{