diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index cbf491c02..169c34bd9 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -89,8 +89,7 @@ namespace API.Controllers private async Task HasDownloadPermission() { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - var roles = await _userManager.GetRolesAsync(user); - return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); + return await _downloadService.HasDownloadPermission(user); } private async Task GetFirstFileDownload(IEnumerable files) diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 5cef859a4..1e6d381a1 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -765,7 +765,7 @@ public class OpdsController : BaseApiController filename); accLink.TotalPages = chapter.Pages; - return new FeedEntry() + var entry = new FeedEntry() { Id = mangaFile.Id.ToString(), Title = title, @@ -776,7 +776,6 @@ public class OpdsController : BaseApiController { CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"), CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"), - accLink, CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey) }, Content = new FeedEntryContent() @@ -785,6 +784,15 @@ public class OpdsController : BaseApiController Type = "text" } }; + + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey)); + if (await _downloadService.HasDownloadPermission(user)) + { + entry.Links.Add(accLink); + } + + + return entry; } [HttpGet("{apiKey}/image")] diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index d42b775b2..88207bf3c 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -105,16 +105,6 @@ namespace API.Controllers { _logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername()); - if (updateSettingsDto.CacheDirectory.Equals(string.Empty)) - { - return BadRequest("Cache Directory cannot be empty"); - } - - if (!Directory.Exists(updateSettingsDto.CacheDirectory)) - { - return BadRequest("Directory does not exist or is not accessible."); - } - // We do not allow CacheDirectory changes, so we will ignore. var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); var updateBookmarks = false; diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 711e742e8..0a7eeb4d1 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.Entities.Enums; using API.Extensions; using AutoMapper; using Microsoft.AspNetCore.Authorization; @@ -87,6 +88,9 @@ namespace API.Controllers existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection; 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 + existingPreferences.LayoutMode = LayoutMode.Single; + _unitOfWork.UserRepository.Update(existingPreferences); if (await _unitOfWork.CommitAsync()) diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 03f853d33..d3abaa313 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -37,5 +37,6 @@ namespace API.DTOs.Settings /// /// If null or empty string, will default back to default install setting aka public string EmailServiceUrl { get; set; } + public string InstallVersion { get; set; } } } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 16845ad59..c772dae1e 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -610,8 +610,6 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true) { - //var allSeriesWithProgress = await _context.AppUserProgresses.Select(p => p.SeriesId).ToListAsync(); - //var allChapters = await GetChapterIdsForSeriesAsync(allSeriesWithProgress); var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30); var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter)) .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => @@ -625,7 +623,7 @@ public class SeriesRepository : ISeriesRepository .Where(p => p.Id == progress.Id && p.AppUserId == userId) .Max(p => p.LastModified), // This is only taking into account chapters that have progress on them, not all chapters in said series - LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created) + LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created), //LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created) }); if (cutoffOnDate) diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 31ea46d4b..3678ef7e5 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -45,6 +45,9 @@ namespace API.Helpers.Converters case ServerSettingKey.EmailServiceUrl: destination.EmailServiceUrl = row.Value; break; + case ServerSettingKey.InstallVersion: + destination.InstallVersion = row.Value; + break; } } diff --git a/API/Program.cs b/API/Program.cs index b81fe61d5..07d596ff0 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -36,7 +36,6 @@ namespace API var directoryService = new DirectoryService(null, new FileSystem()); - //MigrateConfigFiles.Migrate(isDocker, directoryService); // Before anything, check if JWT has been generated properly or if user still has default if (!Configuration.CheckIfJwtTokenSet() && diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 9531aa785..9e9aa0ac2 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -150,7 +150,7 @@ namespace API.Services { // @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; + 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)) { @@ -343,7 +343,7 @@ namespace API.Services { foreach (var styleLinks in styleNodes) { - var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value); + 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.ContainsKey(key)) @@ -358,11 +358,20 @@ namespace API.Services key = correctedKey; } - var styleContent = await ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase, - book.Content.Css[key].FileName, book); - if (styleContent != null) + try { - body.PrependChild(HtmlNode.CreateNode($"")); + var cssFile = book.Content.Css[key]; + + var styleContent = await ScopeStyles(await cssFile.ReadContentAsync(), apiBase, + cssFile.FileName, book); + if (styleContent != null) + { + body.PrependChild(HtmlNode.CreateNode($"")); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an error reading css file for inlining likely due to a key mismatch in metadata"); } } } @@ -460,7 +469,7 @@ namespace API.Services } /// - /// Removes the leading ../ + /// Removes all leading ../ /// /// /// diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 6e5b335d1..fb7675735 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -84,6 +84,8 @@ namespace API.Services ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config"); BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks"); SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes"); + + ExistOrCreate(SiteThemeDirectory); } /// diff --git a/API/Services/DownloadService.cs b/API/Services/DownloadService.cs index 51830f0ab..7fc1418e6 100644 --- a/API/Services/DownloadService.cs +++ b/API/Services/DownloadService.cs @@ -2,7 +2,9 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.Entities; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.StaticFiles; namespace API.Services; @@ -11,15 +13,18 @@ public interface IDownloadService { Task<(byte[], string, string)> GetFirstFileDownload(IEnumerable files); string GetContentTypeFromFile(string filepath); + Task HasDownloadPermission(AppUser user); } public class DownloadService : IDownloadService { private readonly IDirectoryService _directoryService; + private readonly UserManager _userManager; private readonly FileExtensionContentTypeProvider _fileTypeProvider = new FileExtensionContentTypeProvider(); - public DownloadService(IDirectoryService directoryService) + public DownloadService(IDirectoryService directoryService, UserManager userManager) { _directoryService = directoryService; + _userManager = userManager; } /// @@ -53,4 +58,10 @@ public class DownloadService : IDownloadService return contentType; } + + public async Task HasDownloadPermission(AppUser user) + { + var roles = await _userManager.GetRolesAsync(user); + return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); + } } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 298b8f2b7..36a4a5c50 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -99,11 +99,12 @@ public class StatsService : IStatsService public async Task GetServerInfo() { var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId); + var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); var serverInfo = new ServerInfoDto { InstallId = installId.Value, Os = RuntimeInformation.OSDescription, - KavitaVersion = BuildInfo.Version.ToString(), + KavitaVersion = installVersion.Value, DotnetVersion = Environment.Version.ToString(), IsDocker = new OsInfo(Array.Empty()).IsDocker, NumOfCores = Math.Max(Environment.ProcessorCount, 1), diff --git a/API/Startup.cs b/API/Startup.cs index b52640bab..c9cbb32c5 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Middleware; using API.Services; @@ -148,13 +149,19 @@ namespace API // Apply all migrations on startup var logger = serviceProvider.GetRequiredService>(); var userManager = serviceProvider.GetRequiredService>(); - + var context = serviceProvider.GetRequiredService(); await MigrateBookmarks.Migrate(directoryService, unitOfWork, logger, cacheService); // Only run this if we are upgrading await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager); + + // Update the version in the DB after all migrations are run + var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); + installVersion.Value = BuildInfo.Version.ToString(); + unitOfWork.SettingsRepository.Update(installVersion); + await unitOfWork.CommitAsync(); }).GetAwaiter() .GetResult(); } @@ -206,21 +213,6 @@ namespace API app.UseDefaultFiles(); - // This is not implemented completely. Commenting out until implemented - // var service = serviceProvider.GetRequiredService(); - // var settings = service.SettingsRepository.GetSettingsDto(); - // if (!string.IsNullOrEmpty(settings.BaseUrl) && !settings.BaseUrl.Equals("/")) - // { - // var path = !settings.BaseUrl.StartsWith("/") - // ? $"/{settings.BaseUrl}" - // : settings.BaseUrl; - // path = !path.EndsWith("/") - // ? $"{path}/" - // : path; - // app.UsePathBase(path); - // Console.WriteLine("Starting with base url as " + path); - // } - app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = new FileExtensionContentTypeProvider() diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index be6e99a18..cccd8a5ef 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -8995,9 +8995,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minipass": { "version": "3.1.6", @@ -9224,9 +9224,9 @@ } }, "node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", "dev": true }, "node-gyp": { @@ -10498,8 +10498,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "strip-ansi": { @@ -10704,8 +10703,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "ansi-styles": { @@ -12296,9 +12294,9 @@ } }, "url-parse": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.4.tgz", - "integrity": "sha512-ITeAByWWoqutFClc/lRZnFplgXgEZr3WJ6XngMM/N9DMIm4K8zXPCZ1Jdu0rERwO84w1WC5wkle2ubwTA4NTBg==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 4acda1413..85f1ca6d4 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -101,6 +101,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => { this.libraryName = names[this.series.libraryId]; }); + + this.initSeries = Object.assign({}, this.series); this.editSeriesForm = this.fb.group({ @@ -217,10 +219,14 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false }; }); this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => { + console.log('compareFN:') + console.log('options: ', options); + console.log('filter: ', filter); + console.log('results: ', options.filter(m => this.utilityService.filter(m.title, filter))); return options.filter(m => this.utilityService.filter(m.title, filter)); } - this.collectionTagSettings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => { - return a.id == b.id; + this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => { + return a.title === b.title; } if (this.metadata.collectionTags) { @@ -248,7 +254,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { this.tagsSettings.addTransformFn = ((title: string) => { return {id: 0, title: title }; }); - this.tagsSettings.singleCompareFn = (a: Tag, b: Tag) => { + this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => { return a.id == b.id; } @@ -272,7 +278,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { this.genreSettings.compareFn = (options: Genre[], filter: string) => { return options.filter(m => this.utilityService.filter(m.title, filter)); } - this.genreSettings.singleCompareFn = (a: Genre, b: Genre) => { + this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => { return a.title == b.title; } @@ -316,7 +322,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages) .pipe(map(items => this.languageSettings.compareFn(items, filter))); - this.languageSettings.singleCompareFn = (a: Language, b: Language) => { + this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { return a.isoCode == b.isoCode; } @@ -366,7 +372,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { return options.filter(m => this.utilityService.filter(m.name, filter)); } - personSettings.singleCompareFn = (a: Person, b: Person) => { + personSettings.selectionCompareFn = (a: Person, b: Person) => { return a.name == b.name && a.role == b.role; } personSettings.fetchFn = (filter: string) => { diff --git a/UI/Web/src/app/events-widget/events-widget.component.ts b/UI/Web/src/app/events-widget/events-widget.component.ts index c350de5c6..e93418ffd 100644 --- a/UI/Web/src/app/events-widget/events-widget.component.ts +++ b/UI/Web/src/app/events-widget/events-widget.component.ts @@ -110,7 +110,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { break; case 'ended': data = this.progressEventsSource.getValue(); - data = data.filter(m => m.name !== message.name); // This does not work // && m.title !== message.title + data = data.filter(m => m.name !== message.name); this.progressEventsSource.next(data); this.activeEvents = Math.max(this.activeEvents - 1, 0); break; diff --git a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.scss b/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.scss index 8dece9927..4f2a03219 100644 --- a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.scss +++ b/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.scss @@ -42,10 +42,10 @@ img, .full-width { } .bookmark-effect { - animation: bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1); + animation: infinite-scroll-bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1); } -@keyframes bookmark { +@keyframes infinite-scroll-bookmark { 0%, 100% { filter: opacity(1); } diff --git a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts index 4c4acd0e3..b179f4253 100644 --- a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts +++ b/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts @@ -2,6 +2,7 @@ import { DOCUMENT } from '@angular/common'; import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core'; import { BehaviorSubject, fromEvent, merge, ReplaySubject, Subject } from 'rxjs'; import { debounceTime, take, takeUntil } from 'rxjs/operators'; +import { ScrollService } from 'src/app/scroll.service'; import { ReaderService } from '../../_services/reader.service'; import { PAGING_DIRECTION } from '../_models/reader-enums'; import { WebtoonImage } from '../_models/webtoon-image'; @@ -93,7 +94,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { /** * The minimum width of images in webtoon. On image loading, this is checked and updated. All images will get this assigned to them for rendering. */ - webtoonImageWidth: number = window.innerWidth || this.document.documentElement.clientWidth || this.document.body.clientWidth; + webtoonImageWidth: number = window.innerWidth || this.document.body.clientWidth || this.document.documentElement.clientWidth; /** * Used to tell if a scrollTo() operation is in progress */ @@ -145,7 +146,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { get areImagesWiderThanWindow() { let [_, innerWidth] = this.getInnerDimensions(); - return this.webtoonImageWidth > (innerWidth || document.documentElement.clientWidth); + return this.webtoonImageWidth > (innerWidth || document.body.clientWidth); } @@ -153,7 +154,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { private readonly onDestroy = new Subject(); - constructor(private readerService: ReaderService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) { + constructor(private readerService: ReaderService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) { // This will always exist at this point in time since this is used within manga reader const reader = document.querySelector('.reader'); if (reader !== null) { @@ -230,7 +231,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { recalculateImageWidth() { const [_, innerWidth] = this.getInnerDimensions(); - this.webtoonImageWidth = innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + this.webtoonImageWidth = innerWidth || document.body.clientWidth || document.documentElement.clientWidth; } getVerticalOffset() { @@ -244,8 +245,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { } return (offset + || document.body.scrollTop || document.documentElement.scrollTop - || document.body.scrollTop || 0); + || 0); } /** @@ -290,19 +292,20 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { getTotalHeight() { let totalHeight = 0; document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height); - return totalHeight; + return Math.round(totalHeight); } getTotalScroll() { if (this.isFullscreenMode) { return this.readerElemRef.nativeElement.offsetHeight + this.readerElemRef.nativeElement.scrollTop; } - return document.documentElement.offsetHeight + document.documentElement.scrollTop; + return document.body.offsetHeight + document.body.scrollTop; } getScrollTop() { if (this.isFullscreenMode) { return this.readerElemRef.nativeElement.scrollTop; } - return document.documentElement.scrollTop; + + return document.body.scrollTop; } checkIfShouldTriggerContinuousReader() { @@ -316,14 +319,14 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { if (this.atTop && this.pageNum > 0) { this.atTop = false; } - + if (totalScroll === totalHeight && !this.atBottom) { this.atBottom = true; this.setPageNum(this.totalPages); // Scroll user back to original location this.previousScrollHeightMinusTop = this.getScrollTop(); - requestAnimationFrame(() => document.documentElement.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2)); + requestAnimationFrame(() => document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2)); } else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) { // This if statement will fire once we scroll into the spacer at all this.loadNextChapter.emit(); @@ -335,8 +338,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { this.atTop = true; // Scroll user back to original location - this.previousScrollHeightMinusTop = document.documentElement.scrollHeight - document.documentElement.scrollTop; - requestAnimationFrame(() => window.scrollTo(0, SPACER_SCROLL_INTO_PX)); // TODO: does this need to be fullscreen protected? + this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop; + + const reader = this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body; + requestAnimationFrame(() => this.scrollService.scrollTo((SPACER_SCROLL_INTO_PX / 2), reader)); } else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) { // If already at top, then we moving on this.loadPrevChapter.emit(); @@ -377,8 +382,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { return (rect.bottom >= 0 && rect.right >= 0 && - rect.top <= (innerHeight || document.documentElement.clientHeight) && - rect.left <= (innerWidth || document.documentElement.clientWidth) + rect.top <= (innerHeight || document.body.clientHeight) && + rect.left <= (innerWidth || document.body.clientWidth) ); } @@ -398,10 +403,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { if (rect.bottom >= 0 && rect.right >= 0 && - rect.top <= (innerHeight || document.documentElement.clientHeight) && - rect.left <= (innerWidth || document.documentElement.clientWidth) + rect.top <= (innerHeight || document.body.clientHeight) && + rect.left <= (innerWidth || document.body.clientWidth) ) { - const topX = (innerHeight || document.documentElement.clientHeight); + const topX = (innerHeight || document.body.clientHeight); return Math.abs(rect.top / topX) <= 0.25; } return false; diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index 64e355699..ca6f845e6 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -19,6 +19,7 @@ Keyboard Shortcuts Modal + {{readerService.imageUrlToPageNum(canvasImage.src)}} - {{PageNumber + 1}} @@ -154,7 +155,7 @@
-
+