diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index adb2732eb..e9552ad9e 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -153,7 +153,7 @@ namespace API.Controllers var files = (await _unitOfWork.UserRepository.GetAllBookmarksByIds(downloadBookmarkDto.Bookmarks .Select(b => b.Id) .ToList())) - .Select(b => _directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName)); + .Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName))); var (fileBytes, _) = await _archiveService.CreateZipForDownload(files, tempFolder); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b6070b9d9..e8ffa9e16 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -48,7 +48,7 @@ public interface ISeriesRepository Task DeleteSeriesAsync(int seriesId); Task GetSeriesByIdAsync(int seriesId); Task> GetSeriesByIdsAsync(IList seriesIds); - Task GetChapterIdsForSeriesAsync(int[] seriesIds); + Task GetChapterIdsForSeriesAsync(IList seriesIds); Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); /// /// Used to add Progress/Rating information to series list. @@ -325,7 +325,7 @@ public class SeriesRepository : ISeriesRepository .ToListAsync(); } - public async Task GetChapterIdsForSeriesAsync(int[] seriesIds) + public async Task GetChapterIdsForSeriesAsync(IList seriesIds) { var volumes = await _context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) @@ -516,6 +516,9 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter) { + //var allSeriesWithProgress = await _context.AppUserProgresses.Select(p => p.SeriesId).ToListAsync(); + //var allChapters = await GetChapterIdsForSeriesAsync(allSeriesWithProgress); + var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter)) .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new @@ -524,17 +527,21 @@ public class SeriesRepository : ISeriesRepository PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) .Sum(s1 => s1.PagesRead), progress.AppUserId, - LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId) - .Max(p => p.LastModified) + LastReadingProgress = _context.AppUserProgresses.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 => allChapters.Contains(c.Id)).Max(c => c.Created) }); + // I think I need another Join statement. The problem is the chapters are still limited to progress var retSeries = query.Where(s => s.AppUserId == userId && s.PagesRead > 0 && s.PagesRead < s.Series.Pages) - .OrderByDescending(s => s.LastModified) // TODO: This needs to be Chapter Created (Max) - .ThenByDescending(s => s.Series.LastModified) + .OrderByDescending(s => s.LastReadingProgress) + .ThenByDescending(s => s.LastChapterCreated) .Select(s => s.Series) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index c44d9b98f..c3508f541 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -226,14 +226,14 @@ public class ScannerService : IScannerService // Check if any of the folder roots are not available (ie disconnected from network, etc) and fail if any of them are if (library.Folders.Any(f => !_directoryService.IsDriveMounted(f.Path))) { - _logger.LogError("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); + _logger.LogCritical("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); return; } // For Docker instances check if any of the folder roots are not available (ie disconnected volumes, etc) and fail if any of them are if (library.Folders.Any(f => _directoryService.IsDirectoryEmpty(f.Path))) { - _logger.LogError("Some of the root folders for the library are empty. " + + _logger.LogCritical("Some of the root folders for the library are empty. " + "Either your mount has been disconnected or you are trying to delete all series in the library. " + "Scan will be aborted. " + "Check that your mount is connected or change the library's root folder and rescan"); diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html index 178831b64..fb98db171 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html @@ -1,4 +1,4 @@ -
+
Skip to main content @@ -116,10 +116,8 @@
-
-
-
-
+
+
diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss index 7466d73d7..65de3db54 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss @@ -159,6 +159,10 @@ $primary-color: #0062cc; //overflow: auto; // This will break progress reporting } +.reader-container { + outline: none; // Only the reading section itself shouldn't receive any outline. We use it to shift focus in fullscreen mode +} + .book-content { position: relative; } diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index 2d729030b..39e0cc637 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -747,6 +747,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } else { this.scrollService.scrollTo(0, this.reader.nativeElement); } + + // On fullscreen we need to click the document before arrow keys will scroll down. + if (this.isFullscreen) { + this.renderer.setAttribute(this.reader.nativeElement, 'tabIndex', '0'); + this.reader.nativeElement.focus(); + } } setPageNum(pageNum: number) { 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 15dbd8b5c..c723bbcb2 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 @@ -25,6 +25,10 @@ width: 100% !important; } +// .img-container { +// overflow: auto; +// } + @keyframes move-up-down { 0%, 100% { 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 7b4ff0712..e2ecb7f40 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 @@ -63,6 +63,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { @Input() goToPage: ReplaySubject = new ReplaySubject(); @Input() bookmarkPage: ReplaySubject = new ReplaySubject(); + @Input() fullscreenToggled: ReplaySubject = new ReplaySubject(); /** * Stores and emits all the src urls @@ -152,7 +153,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { } ngOnInit(): void { - fromEvent(window, 'scroll') + const reader = document.querySelector('.reader') || window; + fromEvent(reader, 'scroll') .pipe(debounceTime(20), takeUntil(this.onDestroy)) .subscribe((event) => this.handleScrollEvent(event)); @@ -183,6 +185,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { } }); } + + if (this.fullscreenToggled) { + this.fullscreenToggled.pipe(takeUntil(this.onDestroy)).subscribe(isFullscreen => { + this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen); + this.setPageNum(this.pageNum, true); + }); + } } /** 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 346ba56ec..ca95853bc 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index c43ce14ee..98883657c 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -131,6 +131,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * An event emiter when a bookmark on a page change occurs. Used soley by the webtoon reader. */ showBookmarkEffectEvent: ReplaySubject = new ReplaySubject(); + /** + * An event emiter when fullscreen mode is toggled. Used soley by the webtoon reader. + */ + fullscreenEvent: ReplaySubject = new ReplaySubject(); /** * If the menu is open/visible. */ @@ -845,63 +849,67 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } renderPage() { - if (this.ctx && this.canvas) { - this.canvasImage.onload = null; + if (!this.ctx || !this.canvas) { return; } - this.setCanvasSize(); + this.canvasImage.onload = null; - const needsSplitting = this.isCoverImage(); - this.updateSplitPage(); + this.setCanvasSize(); - if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) { - this.canvas.nativeElement.width = this.canvasImage.width / 2; - this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height); - } else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) { - this.canvas.nativeElement.width = this.canvasImage.width / 2; - this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height); + const needsSplitting = this.isCoverImage(); + this.updateSplitPage(); + + if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) { + this.canvas.nativeElement.width = this.canvasImage.width / 2; + this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height); + } else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) { + this.canvas.nativeElement.width = this.canvasImage.width / 2; + this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height); + } else { + if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) { + this.updateScalingForFirstPageRender(); + } + + // Fit Split on a page that needs splitting + if (!this.shouldRenderAsFitSplit()) { + this.setCanvasSize(); + this.ctx.drawImage(this.canvasImage, 0, 0); + this.isLoading = false; + return; + } + + const windowWidth = window.innerWidth + || document.documentElement.clientWidth + || document.body.clientWidth; + const windowHeight = window.innerHeight + || document.documentElement.clientHeight + || document.body.clientHeight; + // If the user's screen is wider than the image, just pretend this is no split, as it will render nicer + this.canvas.nativeElement.width = windowWidth; + this.canvas.nativeElement.height = windowHeight; + const ratio = this.canvasImage.width / this.canvasImage.height; + let newWidth = windowWidth; + let newHeight = newWidth / ratio; + if (newHeight > windowHeight) { + newHeight = windowHeight; + newWidth = newHeight * ratio; + } + + // Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit + if (windowWidth > newWidth) { + this.setCanvasSize(); + this.ctx.drawImage(this.canvasImage, 0, 0); } else { - if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) { - this.updateScalingForFirstPageRender(); - } - - // Fit Split on a page that needs splitting - if (!this.shouldRenderAsFitSplit()) { - this.ctx.drawImage(this.canvasImage, 0, 0); - } - - const windowWidth = window.innerWidth - || document.documentElement.clientWidth - || document.body.clientWidth; - const windowHeight = window.innerHeight - || document.documentElement.clientHeight - || document.body.clientHeight; - // If the user's screen is wider than the image, just pretend this is no split, as it will render nicer - this.canvas.nativeElement.width = windowWidth; - this.canvas.nativeElement.height = windowHeight; - const ratio = this.canvasImage.width / this.canvasImage.height; - let newWidth = windowWidth; - let newHeight = newWidth / ratio; - if (newHeight > windowHeight) { - newHeight = windowHeight; - newWidth = newHeight * ratio; - } - - // Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit - if (windowWidth > newWidth) { - this.setCanvasSize(); - this.ctx.drawImage(this.canvasImage, 0, 0); - } else { - this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); - this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight); - } + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight); } - - // Reset scroll on non HEIGHT Fits - if (this.getFit() !== FITTING_OPTION.HEIGHT) { - window.scrollTo(0, 0); - } - } + + // Reset scroll on non HEIGHT Fits + if (this.getFit() !== FITTING_OPTION.HEIGHT) { + window.scrollTo(0, 0); + } + + this.isLoading = false; } @@ -1080,12 +1088,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.readerService.exitFullscreen(() => { this.isFullscreen = false; this.firstPageRendered = false; + this.fullscreenEvent.next(false); this.render(); }); } else { this.readerService.enterFullscreen(this.reader.nativeElement, () => { this.isFullscreen = true; this.firstPageRendered = false; + this.fullscreenEvent.next(true); this.render(); }); }