From 742cfd3293e55ac2c96e8ba282b48c1b11f75339 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 30 May 2022 16:50:12 -0500 Subject: [PATCH] Jump Bar Testing (#1302) * Implemented a basic jump bar for the library view. This currently just interacts with existing pagination controls and is not inlined with infinite scroll yet. This is a first pass implementation. * Refactored time estimates into the reading service. * Cleaned up when the jump bar is shown to mimic pagination controls * Cleanup up code in reader service. * Scroll to card when selecting a jump key that is shown on the current page. * Ensure estimated times always has the smaller number on left hand side. * Fixed a bug with a missing vertical rule * Fixed an off by 1 pixel for search overlay --- API/Controllers/LibraryController.cs | 11 ++ API/Controllers/ReaderController.cs | 56 +-------- API/DTOs/JumpBar/JumpKeyDto.cs | 20 ++++ API/DTOs/SeriesDetail/SeriesDetailDto.cs | 2 +- API/Data/Repositories/LibraryRepository.cs | 34 ++++++ API/Data/Repositories/UserRepository.cs | 8 ++ API/Services/ReaderService.cs | 55 ++++++++- API/Services/SeriesService.cs | 12 +- UI/Web/package-lock.json | 8 ++ UI/Web/package.json | 1 + UI/Web/src/app/_models/jumpbar/jump-key.ts | 5 + UI/Web/src/app/_services/library.service.ts | 5 + .../card-detail-layout.component.html | 109 ++++++++++-------- .../card-detail-layout.component.ts | 87 ++++++++++++-- UI/Web/src/app/cards/cards.module.ts | 5 + .../library-detail.component.html | 1 + .../library-detail.component.ts | 8 ++ .../infinite-scroller.component.ts | 4 +- .../grouped-typeahead.component.scss | 2 +- .../series-metadata-detail.component.html | 6 +- 20 files changed, 319 insertions(+), 120 deletions(-) create mode 100644 API/DTOs/JumpBar/JumpKeyDto.cs create mode 100644 UI/Web/src/app/_models/jumpbar/jump-key.ts diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 22ac12faf..71a9bf023 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.JumpBar; using API.DTOs.Search; using API.Entities; using API.Entities.Enums; @@ -106,6 +107,16 @@ namespace API.Controllers return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()); } + [HttpGet("jump-bar")] + public async Task>> GetJumpBar(int libraryId) + { + var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library"); + + return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); + } + + [Authorize(Policy = "RequireAdminRole")] [HttpPost("grant-access")] public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 63c920ba0..c64adfacc 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -639,25 +639,7 @@ namespace API.Controllers [HttpGet("manual-read-time")] public ActionResult GetManualReadTime(int wordCount, int pageCount, bool isEpub) { - - if (isEpub) - { - return Ok(new HourEstimateRangeDto() - { - MinHours = (int) Math.Round((wordCount / ReaderService.MinWordsPerHour)), - MaxHours = (int) Math.Round((wordCount / ReaderService.MaxWordsPerHour)), - AvgHours = (int) Math.Round((wordCount / ReaderService.AvgWordsPerHour)), - HasProgress = false - }); - } - - return Ok(new HourEstimateRangeDto() - { - MinHours = (int) Math.Round((pageCount / ReaderService.MinPagesPerMinute / 60F)), - MaxHours = (int) Math.Round((pageCount / ReaderService.MaxPagesPerMinute / 60F)), - AvgHours = (int) Math.Round((pageCount / ReaderService.AvgPagesPerMinute / 60F)), - HasProgress = false - }); + return Ok(_readerService.GetTimeEstimate(wordCount, pageCount, isEpub)); } [HttpGet("read-time")] @@ -667,24 +649,8 @@ namespace API.Controllers var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); var progress = (await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId)).ToList(); - if (series.Format == MangaFormat.Epub) - { - return Ok(new HourEstimateRangeDto() - { - MinHours = (int) Math.Round((series.WordCount / ReaderService.MinWordsPerHour)), - MaxHours = (int) Math.Round((series.WordCount / ReaderService.MaxWordsPerHour)), - AvgHours = (int) Math.Round((series.WordCount / ReaderService.AvgWordsPerHour)), - HasProgress = progress.Any() - }); - } - - return Ok(new HourEstimateRangeDto() - { - MinHours = (int) Math.Round((series.Pages / ReaderService.MinPagesPerMinute / 60F)), - MaxHours = (int) Math.Round((series.Pages / ReaderService.MaxPagesPerMinute / 60F)), - AvgHours = (int) Math.Round((series.Pages / ReaderService.AvgPagesPerMinute / 60F)), - HasProgress = progress.Any() - }); + return Ok(_readerService.GetTimeEstimate(series.WordCount, series.Pages, series.Format == MangaFormat.Epub, + progress.Any())); } @@ -709,24 +675,12 @@ namespace API.Controllers // Word count var progressCount = chapters.Sum(c => c.WordCount); var wordsLeft = series.WordCount - progressCount; - return Ok(new HourEstimateRangeDto() - { - MinHours = (int) Math.Round((wordsLeft / ReaderService.MinWordsPerHour)), - MaxHours = (int) Math.Round((wordsLeft / ReaderService.MaxWordsPerHour)), - AvgHours = (int) Math.Round((wordsLeft / ReaderService.AvgWordsPerHour)), - HasProgress = progressCount > 0 - }); + return _readerService.GetTimeEstimate(wordsLeft, 0, true, progressCount > 0); } var progressPageCount = progress.Sum(p => p.PagesRead); var pagesLeft = series.Pages - progressPageCount; - return Ok(new HourEstimateRangeDto() - { - MinHours = (int) Math.Round((pagesLeft / ReaderService.MinPagesPerMinute / 60F)), - MaxHours = (int) Math.Round((pagesLeft / ReaderService.MaxPagesPerMinute / 60F)), - AvgHours = (int) Math.Round((pagesLeft / ReaderService.AvgPagesPerMinute / 60F)), - HasProgress = progressPageCount > 0 - }); + return _readerService.GetTimeEstimate(0, pagesLeft, false, progressPageCount > 0); } } diff --git a/API/DTOs/JumpBar/JumpKeyDto.cs b/API/DTOs/JumpBar/JumpKeyDto.cs new file mode 100644 index 000000000..44545b65a --- /dev/null +++ b/API/DTOs/JumpBar/JumpKeyDto.cs @@ -0,0 +1,20 @@ +namespace API.DTOs.JumpBar; + +/// +/// Represents an individual button in a Jump Bar +/// +public class JumpKeyDto +{ + /// + /// Number of items in this Key + /// + public int Size { get; set; } + /// + /// Code to use in URL (url encoded) + /// + public string Key { get; set; } + /// + /// What is visible to user + /// + public string Title { get; set; } +} diff --git a/API/DTOs/SeriesDetail/SeriesDetailDto.cs b/API/DTOs/SeriesDetail/SeriesDetailDto.cs index e0a1b0ee8..9bc8a97d8 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace API.DTOs.SeriesDetail; /// /// This is a special DTO for a UI page in Kavita. This performs sorting and grouping and returns exactly what UI requires for layout. diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index d78c5a95d..206b1f1ef 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using API.DTOs; +using API.DTOs.JumpBar; using API.Entities; using API.Entities.Enums; using AutoMapper; @@ -38,6 +40,7 @@ public interface ILibraryRepository Task GetLibraryTypeAsync(int libraryId); Task> GetLibraryForIdsAsync(IList libraryIds); Task GetTotalFiles(); + IEnumerable GetJumpBarAsync(int libraryId); } public class LibraryRepository : ILibraryRepository @@ -123,6 +126,37 @@ public class LibraryRepository : ILibraryRepository return await _context.MangaFile.CountAsync(); } + public IEnumerable GetJumpBarAsync(int libraryId) + { + var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId) + .Select(s => s.SortName.ToUpper()) + .OrderBy(s => s) + .AsEnumerable() + .Select(s => s[0]); + + // Map the title to the number of entities + var firstCharacterMap = new Dictionary(); + foreach (var sortChar in seriesSortCharacters) + { + var c = sortChar; + var isAlpha = char.IsLetter(sortChar); + if (!isAlpha) c = '#'; + if (!firstCharacterMap.ContainsKey(c)) + { + firstCharacterMap[c] = 0; + } + + firstCharacterMap[c] += 1; + } + + return firstCharacterMap.Keys.Select(k => new JumpKeyDto() + { + Key = k + string.Empty, + Size = firstCharacterMap[k], + Title = k + string.Empty + }); + } + public async Task> GetLibraryDtosAsync() { return await _context.Library diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 60c72a77c..dd9c279cd 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -56,6 +56,7 @@ public interface IUserRepository Task> GetAllUsers(); Task> GetAllPreferencesByThemeAsync(int themeId); + Task HasAccessToLibrary(int libraryId, int userId); } public class UserRepository : IUserRepository @@ -238,6 +239,13 @@ public class UserRepository : IUserRepository .ToListAsync(); } + public async Task HasAccessToLibrary(int libraryId, int userId) + { + return await _context.Library + .Include(l => l.AppUsers) + .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId)); + } + public async Task> GetAdminUsersAsync() { return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index d75e952cd..8cf68c25b 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -7,6 +7,7 @@ using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.Reader; using API.Entities; using API.Extensions; using API.SignalR; @@ -28,6 +29,7 @@ public interface IReaderService Task GetContinuePoint(int seriesId, int userId); Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber); + HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub, bool hasProgress = false); } public class ReaderService : IReaderService @@ -168,7 +170,7 @@ public class ReaderService : IReaderService var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList(); if (progresses.Count > 1) { - user.Progresses = new List() + user.Progresses = new List { user.Progresses.First() }; @@ -478,7 +480,7 @@ public class ReaderService : IReaderService /// public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber) { - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List() { seriesId }, true); + var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); foreach (var volume in volumes.OrderBy(v => v.Number)) { var chapters = volume.Chapters @@ -490,10 +492,57 @@ public class ReaderService : IReaderService public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber) { - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List() { seriesId }, true); + var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); foreach (var volume in volumes.OrderBy(v => v.Number).Where(v => v.Number <= volumeNumber && v.Number > 0)) { MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } } + + public HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub, bool hasProgress = false) + { + if (isEpub) + { + var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 1); + var maxHours = Math.Max((int) Math.Round((wordCount / MaxWordsPerHour)), 1); + if (maxHours < minHours) + { + return new HourEstimateRangeDto + { + MinHours = maxHours, + MaxHours = minHours, + AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour)), + HasProgress = hasProgress + }; + } + return new HourEstimateRangeDto + { + MinHours = minHours, + MaxHours = maxHours, + AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour)), + HasProgress = hasProgress + }; + } + + var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 60F)), 1); + var maxHoursPages = Math.Max((int) Math.Round((pageCount / MaxPagesPerMinute / 60F)), 1); + if (maxHoursPages < minHoursPages) + { + return new HourEstimateRangeDto + { + MinHours = maxHoursPages, + MaxHours = minHoursPages, + AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F)), + HasProgress = hasProgress + }; + } + + return new HourEstimateRangeDto + { + MinHours = minHoursPages, + MaxHours = maxHoursPages, + AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F)), + HasProgress = hasProgress + }; + } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 63fb87d66..1b6a18ea2 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -8,9 +8,9 @@ using API.Data; using API.DTOs; using API.DTOs.CollectionTags; using API.DTOs.Metadata; +using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Extensions; using API.Helpers; using API.SignalR; using Microsoft.Extensions.Logging; @@ -98,7 +98,7 @@ public class SeriesService : ISeriesService series.Metadata.SummaryLocked = true; } - if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata.Language) + if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language) { series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language; series.Metadata.LanguageLocked = true; @@ -112,7 +112,7 @@ public class SeriesService : ISeriesService }); series.Metadata.Genres ??= new List(); - UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata.Genres, series, allGenres, (genre) => + UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) => { series.Metadata.Genres.Add(genre); }, () => series.Metadata.GenresLocked = true); @@ -521,11 +521,11 @@ public class SeriesService : ISeriesService /// /// Should we show the given chapter on the UI. We only show non-specials and non-zero chapters. /// - /// + /// /// - private static bool ShouldIncludeChapter(ChapterDto c) + private static bool ShouldIncludeChapter(ChapterDto chapter) { - return !c.IsSpecial && !c.Number.Equals(Parser.Parser.DefaultChapter); + return !chapter.IsSpecial && !chapter.Number.Equals(Parser.Parser.DefaultChapter); } public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType) diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index b3750f3e3..4a429abc2 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -9543,6 +9543,14 @@ "tslib": "^2.0.0" } }, + "ngx-infinite-scroll": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-13.0.2.tgz", + "integrity": "sha512-RSezL0DUxo1B57SyRMOSt3a/5lLXJs6P8lavtxOh10uhX+hn662cMYHUO7LiU2a/vJxef2R020s4jkUqhnXTcg==", + "requires": { + "tslib": "^2.3.0" + } + }, "ngx-toastr": { "version": "14.2.1", "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-14.2.1.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index 44d64765e..b2188e96c 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -39,6 +39,7 @@ "ng-circle-progress": "^1.6.0", "ngx-color-picker": "^12.0.0", "ngx-file-drop": "^13.0.0", + "ngx-infinite-scroll": "^13.0.2", "ngx-toastr": "^14.2.1", "requires": "^1.0.2", "rxjs": "~7.5.4", diff --git a/UI/Web/src/app/_models/jumpbar/jump-key.ts b/UI/Web/src/app/_models/jumpbar/jump-key.ts new file mode 100644 index 000000000..e49c9eddc --- /dev/null +++ b/UI/Web/src/app/_models/jumpbar/jump-key.ts @@ -0,0 +1,5 @@ +export interface JumpKey { + size: number; + key: string; + title: string; +} \ No newline at end of file diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 1936124dc..8d0042ef3 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { of } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; +import { JumpKey } from '../_models/jumpbar/jump-key'; import { Library, LibraryType } from '../_models/library'; import { SearchResultGroup } from '../_models/search/search-result-group'; @@ -58,6 +59,10 @@ export class LibraryService { return this.httpClient.get(this.baseUrl + 'library/list' + query); } + getJumpBar(libraryId: number) { + return this.httpClient.get(this.baseUrl + 'library/jump-bar?libraryId=' + libraryId); + } + getLibraries() { return this.httpClient.get(this.baseUrl + 'library'); } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index 67b9812f0..a3f1f36e7 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -17,62 +17,79 @@ - +
+ +
-
-
- + +
+
+ +
-
-

- -

+

+ +

-
- + +
+ +
+
+
+ - -
  • -
    - - - - of {{pagination.totalPages}} -
    -
  • -
    + +
  • +
    + + + + of {{pagination.totalPages}} +
    +
  • +
    -
    -
    +
    +
    + +
    + +
    +
    diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index caea2f0dc..d681dea81 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -1,11 +1,14 @@ -import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core'; -import { Subject } from 'rxjs'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Component, ContentChild, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { from, Subject } from 'rxjs'; import { FilterSettings } from 'src/app/metadata-filter/filter-settings'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; +import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; import { Library } from 'src/app/_models/library'; import { Pagination } from 'src/app/_models/pagination'; import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/series-filter'; import { ActionItem } from 'src/app/_services/action-factory.service'; +import { ScrollService } from 'src/app/_services/scroll.service'; import { SeriesService } from 'src/app/_services/series.service'; const FILTER_PAG_REGEX = /[^0-9]/g; @@ -15,12 +18,15 @@ const FILTER_PAG_REGEX = /[^0-9]/g; templateUrl: './card-detail-layout.component.html', styleUrls: ['./card-detail-layout.component.scss'] }) -export class CardDetailLayoutComponent implements OnInit, OnDestroy { +export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewInit { @Input() header: string = ''; @Input() isLoading: boolean = false; @Input() items: any[] = []; @Input() pagination!: Pagination; + + // Filter Code + @Input() filterOpen!: EventEmitter; /** * Should filtering be shown on the page */ @@ -31,6 +37,9 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { @Input() actions: ActionItem[] = []; @Input() trackByIdentity!: (index: number, item: any) => string; @Input() filterSettings!: FilterSettings; + + @Input() jumpBarKeys: Array = []; + @Output() itemClicked: EventEmitter = new EventEmitter(); @Output() pageChange: EventEmitter = new EventEmitter(); @Output() applyFilter: EventEmitter = new EventEmitter(); @@ -39,15 +48,13 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { @ContentChild('noData') noDataTemplate!: TemplateRef; - // Filter Code - @Input() filterOpen!: EventEmitter; - - filter!: SeriesFilter; libraries: Array> = []; updateApplied: number = 0; + intersectionObserver: IntersectionObserver = new IntersectionObserver((entries) => this.handleIntersection(entries), { threshold: 0.01 }); + private onDestory: Subject = new Subject(); @@ -55,7 +62,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { return Breakpoint; } - constructor(private seriesService: SeriesService, public utilityService: UtilityService) { + constructor(private seriesService: SeriesService, public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, + private scrollService: ScrollService) { this.filter = this.seriesService.createSeriesFilter(); } @@ -72,11 +80,28 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { } } + ngAfterViewInit() { + + const parent = this.document.querySelector('.card-container'); + if (parent == null) return; + console.log('card divs', this.document.querySelectorAll('div[id^="jumpbar-index--"]')); + console.log('cards: ', this.document.querySelectorAll('.card')); + + Array.from(this.document.querySelectorAll('div')).forEach(elem => this.intersectionObserver.observe(elem)); + } + ngOnDestroy() { + this.intersectionObserver.disconnect(); this.onDestory.next(); this.onDestory.complete(); } + handleIntersection(entries: IntersectionObserverEntry[]) { + console.log('interception: ', entries.filter(e => e.target.hasAttribute('no-observe'))); + + + } + onPageChange(page: number) { this.pageChange.emit(this.pagination); } @@ -101,4 +126,50 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { this.updateApplied++; } + // onScroll() { + + // } + + // onScrollDown() { + // console.log('scrolled down'); + // } + // onScrollUp() { + // console.log('scrolled up'); + // } + + + + scrollTo(jumpKey: JumpKey) { + // TODO: Figure out how to do this + + let targetIndex = 0; + for(var i = 0; i < this.jumpBarKeys.length; i++) { + if (this.jumpBarKeys[i].key === jumpKey.key) break; + targetIndex += this.jumpBarKeys[i].size; + } + //console.log('scrolling to card that starts with ', jumpKey.key, + ' with index of ', targetIndex); + + // Basic implementation based on itemsPerPage being the same. + //var minIndex = this.pagination.currentPage * this.pagination.itemsPerPage; + var targetPage = Math.max(Math.ceil(targetIndex / this.pagination.itemsPerPage), 1); + //console.log('We are on page ', this.pagination.currentPage, ' and our target page is ', targetPage); + if (targetPage === this.pagination.currentPage) { + // Scroll to the element + const elem = this.document.querySelector(`div[id="jumpbar-index--${targetIndex}"`); + if (elem !== null) { + elem.scrollIntoView({ + behavior: 'smooth' + }); + } + return; + } + + this.selectPageStr(targetPage + ''); + + // if (minIndex > targetIndex) { + // // We need to scroll forward (potentially to another page) + // } else if (minIndex < targetIndex) { + // // We need to scroll back (potentially to another page) + // } + } } diff --git a/UI/Web/src/app/cards/cards.module.ts b/UI/Web/src/app/cards/cards.module.ts index 61a353e50..3f29a50e3 100644 --- a/UI/Web/src/app/cards/cards.module.ts +++ b/UI/Web/src/app/cards/cards.module.ts @@ -59,6 +59,9 @@ import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-draw NgbTooltipModule, // Card item NgbCollapseModule, NgbRatingModule, + + //ScrollingModule, + //InfiniteScrollModule, NgbOffcanvasModule, // Series Detail, action of cards @@ -68,6 +71,8 @@ import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-draw NgbProgressbarModule, NgxFileDropModule, // Cover Chooser PipeModule, // filter for BulkAddToCollectionComponent + + SharedModule, // IconAndTitleComponent diff --git a/UI/Web/src/app/library-detail/library-detail.component.html b/UI/Web/src/app/library-detail/library-detail.component.html index 55c3d7d45..b9f03447a 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.html +++ b/UI/Web/src/app/library-detail/library-detail.component.html @@ -24,6 +24,7 @@ [pagination]="pagination" [filterSettings]="filterSettings" [filterOpen]="filterOpen" + [jumpBarKeys]="jumpKeys" (applyFilter)="updateFilter($event)" (pageChange)="onPageChange($event)" > diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index 83bb5bf5f..9511e4445 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -18,6 +18,7 @@ import { SeriesService } from '../_services/series.service'; import { NavService } from '../_services/nav.service'; import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service'; import { FilterSettings } from '../metadata-filter/filter-settings'; +import { JumpKey } from '../_models/jumpbar/jump-key'; @Component({ selector: 'app-library-detail', @@ -39,6 +40,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { filterActive: boolean = false; filterActiveCheck!: SeriesFilter; + jumpKeys: Array = []; + tabs: Array<{title: string, fragment: string, icon: string}> = [ {title: 'Library', fragment: '', icon: 'fa-landmark'}, {title: 'Recommended', fragment: 'recomended', icon: 'fa-award'}, @@ -100,6 +103,11 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { this.libraryName = names[this.libraryId]; this.titleService.setTitle('Kavita - ' + this.libraryName); }); + + this.libraryService.getJumpBar(this.libraryId).subscribe(barDetails => { + console.log('JumpBar: ', barDetails); + this.jumpKeys = barDetails; + }); this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this)); this.pagination = this.filterUtilityService.pagination(this.route.snapshot); 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 69afaa279..450400874 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 @@ -448,13 +448,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { this.attachIntersectionObserverElem(event.target); if (imagePage === this.pageNum) { - Promise.all(Array.from(document.querySelectorAll('img')) + Promise.all(Array.from(this.document.querySelectorAll('img')) .filter((img: any) => !img.complete) .map((img: any) => new Promise(resolve => { img.onload = img.onerror = resolve; }))) .then(() => { this.debugLog('[Initialization] All images have loaded from initial prefetch, initFinished = true'); this.debugLog('[Image Load] ! Loaded current page !', this.pageNum); - this.currentPageElem = document.querySelector('img#page-' + this.pageNum); + this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum); // There needs to be a bit of time before we scroll if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) { this.scrollToCurrentPage(); diff --git a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss index 684f24954..878ae4a38 100644 --- a/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss +++ b/UI/Web/src/app/nav/grouped-typeahead/grouped-typeahead.component.scss @@ -99,7 +99,7 @@ form { .dropdown { width: 100vw; - height: calc(100vh - 57px); //header offset + height: calc(100vh - 56px); //header offset background: var(--dropdown-overlay-color); position: fixed; justify-content: center; diff --git a/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html index 8f0035156..cccd28348 100644 --- a/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html @@ -65,9 +65,10 @@ {{series.wordCount | compactNumber}} Words -
    +
    +
    -
    +
    @@ -75,6 +76,7 @@ {{series.pages | number:''}} Pages
    +