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 @@ - + - +
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 @@ - + - +
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": [ {