From b3f6a574cd8be31a626355182215f883c7694c5a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 11 Feb 2024 14:30:13 -0600 Subject: [PATCH] Lots of changes to get magazines working. Some notes is that magazines in reading list mode do not properly load up. Parsing code still needs some work. Need to restrict to really just a small set of conventions until community can give me real data to code against. --- API/Controllers/BookController.cs | 1 + API/Controllers/LibraryController.cs | 11 +++ API/Controllers/ReaderController.cs | 2 + API/DTOs/LibraryTypeDto.cs | 12 +++ API/DTOs/Reader/BookInfoDto.cs | 1 + API/DTOs/Reader/IChapterInfoDto.cs | 1 + API/Data/Repositories/ChapterRepository.cs | 4 +- API/Data/Repositories/LibraryRepository.cs | 14 ++++ .../Tasks/Scanner/Parser/DefaultParser.cs | 18 ++++- API/Services/Tasks/Scanner/Parser/Parser.cs | 57 ++++++++++++-- UI/Web/src/app/_services/library.service.ts | 14 ++++ UI/Web/src/app/_services/reader.service.ts | 40 +++++----- UI/Web/src/app/app.component.ts | 3 +- .../book-reader/book-reader.component.ts | 22 +++--- .../card-detail-drawer.component.ts | 2 +- .../manga-reader/manga-reader.component.ts | 68 ++++++++++++++--- .../reading-list-detail.component.ts | 9 ++- .../series-detail/series-detail.component.ts | 9 ++- .../reading-activity.component.html | 2 +- .../server-stats/server-stats.component.html | 8 +- .../user-preferences.component.ts | 1 - UI/Web/src/assets/langs/en.json | 4 + openapi.json | 74 +++++++++++++++++++ 23 files changed, 315 insertions(+), 62 deletions(-) create mode 100644 API/DTOs/LibraryTypeDto.cs diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 962500ec7..623f145ed 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -82,6 +82,7 @@ public class BookController : BaseApiController SeriesFormat = dto.SeriesFormat, SeriesId = dto.SeriesId, LibraryId = dto.LibraryId, + LibraryType = dto.LibraryType, IsSpecial = dto.IsSpecial, Pages = dto.Pages, }); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index b4b86dccf..57e30ad02 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -504,4 +505,14 @@ public class LibraryController : BaseApiController { return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); } + + /// + /// Return pairs of all types + /// + /// + [HttpGet("types")] + public async Task>> GetLibraryTypes() + { + return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypesAsync(User.GetUserId())); + } } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 89078487d..3d7ac75bf 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -244,6 +244,7 @@ public class ReaderController : BaseApiController SeriesFormat = dto.SeriesFormat, SeriesId = dto.SeriesId, LibraryId = dto.LibraryId, + LibraryType = dto.LibraryType, IsSpecial = dto.IsSpecial, Pages = dto.Pages, SeriesTotalPages = series.Pages, @@ -284,6 +285,7 @@ public class ReaderController : BaseApiController return Ok(info); } + /// /// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading. /// diff --git a/API/DTOs/LibraryTypeDto.cs b/API/DTOs/LibraryTypeDto.cs new file mode 100644 index 000000000..1f23c604b --- /dev/null +++ b/API/DTOs/LibraryTypeDto.cs @@ -0,0 +1,12 @@ +using API.Entities.Enums; + +namespace API.DTOs; + +/// +/// Simple pairing of LibraryId and LibraryType +/// +public class LibraryTypeDto +{ + public int LibraryId { get; set; } + public LibraryType LibraryType { get; set; } +} diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index c379f71f8..3e5cc30dd 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -15,4 +15,5 @@ public class BookInfoDto : IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } = default! ; + public LibraryType LibraryType { get; set; } } diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/API/DTOs/Reader/IChapterInfoDto.cs index 6a9a74a2c..568adf345 100644 --- a/API/DTOs/Reader/IChapterInfoDto.cs +++ b/API/DTOs/Reader/IChapterInfoDto.cs @@ -14,5 +14,6 @@ public interface IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } + public LibraryType LibraryType { get; set; } } diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index a9fbf3ce3..a8bb699ff 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -112,11 +112,11 @@ public class ChapterRepository : IChapterRepository LibraryId = data.LibraryId, Pages = data.Pages, ChapterTitle = data.TitleName, - LibraryType = data.LibraryType + LibraryType = data.LibraryType, }) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(); + .FirstOrDefaultAsync(); return chapterInfo; } diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index d6d562b82..bc19c5312 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -57,6 +57,7 @@ public interface ILibraryRepository Task GetAllowsScrobblingBySeriesId(int seriesId); Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); + Task> GetLibraryTypesAsync(int userId); } public class LibraryRepository : ILibraryRepository @@ -365,4 +366,17 @@ public class LibraryRepository : ILibraryRepository }) .ToDictionaryAsync(entity => entity.Id, entity => entity.Type); } + + public async Task> GetLibraryTypesAsync(int userId) + { + return await _context.Library + .Where(l => l.AppUsers.Any(u => u.Id == userId)) + .Select(l => new LibraryTypeDto() + { + LibraryType = l.Type, + LibraryId = l.Id + }) + .AsSplitQuery() + .ToListAsync(); + } } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 1a08be2dd..3c25cc73e 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -34,6 +34,8 @@ public class DefaultParser : IDefaultParser public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) { var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); + + // We can now remove this as there is the ability to turn off images for non-image libraries // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. if (type != LibraryType.Image && Parser.IsCoverImage(_directoryService.FileSystem.Path.GetFileName(filePath))) return null; @@ -49,7 +51,7 @@ public class DefaultParser : IDefaultParser // If library type is Image or this is not a cover image in a non-image library, then use dedicated parsing mechanism if (type == LibraryType.Image || Parser.IsImage(filePath)) { - // TODO: We can move this up one level + // TODO: We can move this up one level (out of DefaultParser - If we do different Parsers) return ParseImage(filePath, rootPath, ret); } @@ -136,21 +138,33 @@ public class DefaultParser : IDefaultParser var folders = _directoryService.GetFoldersTillRoot(libraryPath, filePath).ToList(); // Usually the LAST folder is the Series and everything up to can have Volume + if (string.IsNullOrEmpty(ret.Series)) { ret.Series = Parser.CleanTitle(folders[^1]); } + var hasGeoCode = !string.IsNullOrEmpty(Parser.ParseGeoCode(ret.Series)); foreach (var folder in folders[..^1]) { if (ret.Volumes == Parser.DefaultVolume) { var vol = Parser.ParseYear(folder); - if (vol != folder) + if (!string.IsNullOrEmpty(vol) && vol != folder) { ret.Volumes = vol; } } + // If folder has a language code in it, then we add that to the Series (Wired (UK)) + if (!hasGeoCode) + { + var geoCode = Parser.ParseGeoCode(folder); + if (!string.IsNullOrEmpty(geoCode)) + { + ret.Series = $"{ret.Series} ({geoCode})"; + hasGeoCode = true; + } + } } } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 7909b5edc..fa1a042e2 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -638,9 +638,10 @@ public static partial class Parser #region Magazine - private static readonly Dictionary _monthMappings = CreateMonthMappings(); - private static readonly Regex[] MagazineSeriesRegex = new[] - { + private static readonly HashSet GeoCodes = new(CreateCountryCodes()); + private static readonly Dictionary MonthMappings = CreateMonthMappings(); + private static readonly Regex[] MagazineSeriesRegex = + [ // 3D World - 2018 UK, 3D World - 022014 new Regex( @"^(?.+?)(_|\s)*-(_|\s)*\d{4,6}.*", @@ -649,10 +650,14 @@ public static partial class Parser new Regex( @"^(?.+?)(_|\s)*-(_|\s)*.*", MatchOptions, RegexTimeout), + // AIR International #1 // This breaks the way the code works + // new Regex( + // @"^(?.+?)(_|\s)+?#", + // MatchOptions, RegexTimeout) // The New Yorker - April 2, 2018 USA // AIR International Magazine 2006 // AIR International Vol. 14 No. 3 (ISSN 1011-3250) - }; + ]; private static readonly Regex[] MagazineVolumeRegex = new[] { @@ -846,6 +851,17 @@ public static partial class Parser return DefaultVolume; } + private static string[] CreateCountryCodes() + { + var codes = CultureInfo.GetCultures(CultureTypes.SpecificCultures) + .Select(culture => new RegionInfo(culture.Name).TwoLetterISORegionName) + .Distinct() + .OrderBy(code => code) + .ToList(); + codes.Add("UK"); + return codes.ToArray(); + } + private static Dictionary CreateMonthMappings() { @@ -879,7 +895,7 @@ public static partial class Parser var value = groups["Chapter"].Value; // If value has non-digits, we need to convert to a digit if (IsNumberRegex().IsMatch(value)) return FormatValue(value, false); - if (_monthMappings.TryGetValue(value, out var parsedMonth)) + if (MonthMappings.TryGetValue(value, out var parsedMonth)) { return FormatValue($"{parsedMonth}", false); } @@ -889,7 +905,36 @@ public static partial class Parser return DefaultChapter; } - public static string ParseYear(string value) + /// + /// Tries to parse a GeoCode (UK, US) out of a string + /// + /// + /// + public static string? ParseGeoCode(string? value) + { + if (string.IsNullOrEmpty(value)) return value; + const string pattern = @"\b(?:\(|\[|\{)([A-Z]{2})(?:\)|\]|\})\b|^([A-Z]{2})$"; + + // Match the pattern in the input string + var match = Regex.Match(value, pattern, RegexOptions.IgnoreCase); + + if (match.Success) + { + // Extract the GeoCode from the first capturing group if it exists, + // otherwise, extract the GeoCode from the second capturing group + var extractedCode = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + + // Validate the extracted GeoCode against the list of valid GeoCodes + if (GeoCodes.Contains(extractedCode)) + { + return extractedCode; + } + } + + return null; + } + + public static string? ParseYear(string? value) { if (string.IsNullOrEmpty(value)) return value; return YearRegex.Match(value).Value; diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 75abf3a03..18cb55027 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -107,6 +107,20 @@ export class LibraryService { return this.httpClient.post(this.baseUrl + 'library/update', model); } + getLibraryTypes() { + if (this.libraryTypes) return of(this.libraryTypes); + return this.httpClient.get>(this.baseUrl + 'library/types').pipe(map(types => { + if (this.libraryTypes === undefined) { + this.libraryTypes = {}; + } + types.forEach(t => { + this.libraryTypes![t.libraryId] = t.libraryType; + }); + + return this.libraryTypes; + })); + } + getLibraryType(libraryId: number) { if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) { return of(this.libraryTypes[libraryId]); diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index bd91c78cb..d3754d5f3 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -1,23 +1,24 @@ -import { HttpClient, HttpParams } from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import {DestroyRef, Inject, inject, Injectable} from '@angular/core'; import {DOCUMENT, Location} from '@angular/common'; -import { Router } from '@angular/router'; -import { environment } from 'src/environments/environment'; -import { ChapterInfo } from '../manga-reader/_models/chapter-info'; -import { Chapter } from '../_models/chapter'; -import { HourEstimateRange } from '../_models/series-detail/hour-estimate-range'; -import { MangaFormat } from '../_models/manga-format'; -import { BookmarkInfo } from '../_models/manga-reader/bookmark-info'; -import { PageBookmark } from '../_models/readers/page-bookmark'; -import { ProgressBookmark } from '../_models/readers/progress-bookmark'; -import { FileDimension } from '../manga-reader/_models/file-dimension'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; +import {ChapterInfo} from '../manga-reader/_models/chapter-info'; +import {Chapter} from '../_models/chapter'; +import {HourEstimateRange} from '../_models/series-detail/hour-estimate-range'; +import {MangaFormat} from '../_models/manga-format'; +import {BookmarkInfo} from '../_models/manga-reader/bookmark-info'; +import {PageBookmark} from '../_models/readers/page-bookmark'; +import {ProgressBookmark} from '../_models/readers/progress-bookmark'; +import {FileDimension} from '../manga-reader/_models/file-dimension'; import screenfull from 'screenfull'; -import { TextResonse } from '../_types/text-response'; -import { AccountService } from './account.service'; +import {TextResonse} from '../_types/text-response'; +import {AccountService} from './account.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {PersonalToC} from "../_models/readers/personal-toc"; import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import NoSleep from 'nosleep.js'; +import {LibraryType} from "../_models/library/library"; export const CHAPTER_ID_DOESNT_EXIST = -1; @@ -72,12 +73,15 @@ export class ReaderService { } - getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) { + getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat, libraryType: LibraryType) { if (format === undefined) format = MangaFormat.ARCHIVE; if (format === MangaFormat.EPUB) { return ['library', libraryId, 'series', seriesId, 'book', chapterId]; } else if (format === MangaFormat.PDF) { + if (libraryType === LibraryType.Magazine) { + return ['library', libraryId, 'series', seriesId, 'manga', chapterId]; + } return ['library', libraryId, 'series', seriesId, 'pdf', chapterId]; } else { return ['library', libraryId, 'series', seriesId, 'manga', chapterId]; @@ -131,8 +135,8 @@ export class ReaderService { return this.httpClient.get(this.baseUrl + 'reader/get-progress?chapterId=' + chapterId); } - getPageUrl(chapterId: number, page: number) { - return `${this.baseUrl}reader/image?chapterId=${chapterId}&apiKey=${this.encodedKey}&page=${page}`; + getPageUrl(chapterId: number, page: number, extractPdf = false) { + return `${this.baseUrl}reader/image?chapterId=${chapterId}&apiKey=${this.encodedKey}&page=${page}&extractPdf=${extractPdf}`; } getThumbnailUrl(chapterId: number, page: number) { @@ -143,8 +147,8 @@ export class ReaderService { return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey); } - getChapterInfo(chapterId: number, includeDimensions = false) { - return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&includeDimensions=' + includeDimensions); + getChapterInfo(chapterId: number, includeDimensions = false, extractPdf = false) { + return this.httpClient.get(this.baseUrl + `reader/chapter-info?chapterId=${chapterId}&includeDimensions=${includeDimensions}&extractPdf=${extractPdf}`); } getFileDimensions(chapterId: number) { diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 941153732..c94605129 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -13,7 +13,6 @@ import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.compon import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ServerService} from "./_services/server.service"; -import {ImportCblModalComponent} from "./reading-list/_modals/import-cbl-modal/import-cbl-modal.component"; import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component"; @Component({ @@ -101,9 +100,11 @@ export class AppComponent implements OnInit { // Bootstrap anything that's needed this.themeService.getThemes().subscribe(); this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe(); + this.libraryService.getLibraryTypes().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe(); // On load, make an initial call for valid license this.accountService.hasValidLicense().subscribe(); + // Every hour, have the UI check for an update. People seriously stay out of date interval(2* 60 * 60 * 1000) // 2 hours in milliseconds .pipe( 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 694f244cf..072ed5c20 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 @@ -634,12 +634,18 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.bookService.getBookInfo(this.chapterId).subscribe(info => { - if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) { - // Redirect to the manga reader. - const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); - this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params}); - return; - } + this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => { + this.libraryType = type; + this.cdRef.markForCheck(); + + if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) { + // Redirect to the manga reader. + const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); + this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat, this.libraryType), {queryParams: params}); + return; + } + }); + this.bookTitle = info.bookTitle; this.cdRef.markForCheck(); @@ -659,10 +665,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.continuousChaptersStack.push(this.chapterId); - this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => { - this.libraryType = type; - }); - this.updateImageSizes(); if (this.pageNum >= this.maxPages) { diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index d4a4a85e6..466812132 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -263,7 +263,7 @@ export class CardDetailDrawerComponent implements OnInit { } const params = this.readerService.getQueryParamsObject(incognito, false); - this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: params}); + this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format, this.libraryType), {queryParams: params}); this.close(); } diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index 9fe413cb2..ec909846c 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -69,6 +69,7 @@ import {SwipeDirective} from '../../../ng-swipe/ng-swipe.directive'; import {LoadingComponent} from '../../../shared/loading/loading.component'; import {translate, TranslocoDirective} from "@ngneat/transloco"; import {shareReplay} from "rxjs/operators"; +import {LibraryService} from "../../../_services/library.service"; const PREFETCH_PAGES = 10; @@ -151,6 +152,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { private readonly cdRef = inject(ChangeDetectorRef); private readonly toastr = inject(ToastrService); public readonly readerService = inject(ReaderService); + public readonly libraryService = inject(LibraryService); public readonly utilityService = inject(UtilityService); public readonly mangaReaderService = inject(ManagaReaderService); @@ -411,7 +413,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { getPageUrl = (pageNum: number, chapterId: number = this.chapterId) => { if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); - return this.readerService.getPageUrl(chapterId, pageNum); + return this.readerService.getPageUrl(chapterId, pageNum, this.libraryType == LibraryType.Magazine); } get CurrentPageBookmarked() { @@ -494,6 +496,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true'; this.bookmarkMode = this.route.snapshot.queryParamMap.get('bookmarkMode') === 'true'; + console.log('ngOnInit called: ', this.libraryId) + const readingListId = this.route.snapshot.queryParamMap.get('readingListId'); if (readingListId != null) { this.readingListMode = true; @@ -610,7 +614,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { }); }); - this.init(); + this.libraryService.getLibraryType(this.libraryId).subscribe(type => { + this.libraryType = type; + this.init(); + this.cdRef.markForCheck(); + }); + } ngAfterViewInit() { @@ -888,15 +897,29 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } + // When readingListMode and we loaded from continuous reader, library/seriesId doesn't get refreshed. Need to reobtain from the chapterId + if (this.readingListMode) { + console.log('chapterId: ', this.chapterId) + this.cdRef.markForCheck(); + } + + // First load gets library type from constructor. Any other loads will get from continuous reader + console.log('library type', this.libraryType, 'mag: ', LibraryType.Magazine, 'equal: ', this.libraryType == LibraryType.Magazine); forkJoin({ progress: this.readerService.getProgress(this.chapterId), - chapterInfo: this.readerService.getChapterInfo(this.chapterId, true), + chapterInfo: this.readerService.getChapterInfo(this.chapterId, true, this.libraryType == LibraryType.Magazine), bookmarks: this.readerService.getBookmarks(this.chapterId), }).pipe(take(1)).subscribe(results => { if (this.readingListMode && (results.chapterInfo.seriesFormat === MangaFormat.EPUB || results.chapterInfo.seriesFormat === MangaFormat.PDF)) { // Redirect to the book reader. const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); - this.router.navigate(this.readerService.getNavigationArray(results.chapterInfo.libraryId, results.chapterInfo.seriesId, this.chapterId, results.chapterInfo.seriesFormat), {queryParams: params}); + if (results.chapterInfo.libraryType === LibraryType.Magazine) { + this.router.navigate(this.readerService.getNavigationArray(results.chapterInfo.libraryId, results.chapterInfo.seriesId, + this.chapterId, results.chapterInfo.seriesFormat, results.chapterInfo.libraryType), {queryParams: params}); + } else { + this.router.navigate(this.readerService.getNavigationArray(results.chapterInfo.libraryId, results.chapterInfo.seriesId, + this.chapterId, results.chapterInfo.seriesFormat, results.chapterInfo.libraryType), {queryParams: params}); + } return; } @@ -967,6 +990,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.closeReader(); }, 200); }); + + // LibraryId is wrong on readinglist mode + // this.libraryService.getLibraryType(this.libraryId).subscribe((type) => { + // this.libraryType = type; + // console.log('library type', this.libraryType, 'mag: ', LibraryType.Magazine, 'equal: ', this.libraryType == LibraryType.Magazine); + // + // }); } closeReader() { @@ -1300,6 +1330,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } loadChapter(chapterId: number, direction: 'Next' | 'Prev') { + if (chapterId > 0) { this.isLoading = true; this.cdRef.markForCheck(); @@ -1307,11 +1338,18 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.chapterId = chapterId; this.continuousChaptersStack.push(chapterId); // Load chapter Id onto route but don't reload - const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); - window.history.replaceState({}, '', newRoute); - this.init(); - const msg = translate(direction === 'Next' ? 'toasts.load-next-chapter' : 'toasts.load-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()}); - this.toastr.info(msg, '', {timeOut: 3000}); + + + if (this.readingListMode) { + this.readerService.getChapterInfo(this.chapterId).subscribe(chapterInfo => { + this.libraryId = chapterInfo.libraryId; + this.seriesId = chapterInfo.seriesId; + this.libraryType = chapterInfo.libraryType; + this.#updatePageStateForNextChapter(direction); + }); + } else { + this.#updatePageStateForNextChapter(direction); + } } else { // This will only happen if no actual chapter can be found const msg = translate(direction === 'Next' ? 'toasts.no-next-chapter' : 'toasts.no-prev-chapter', @@ -1327,6 +1365,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + #updatePageStateForNextChapter(direction: 'Next' | 'Prev') { + const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); + window.history.replaceState({}, '', newRoute); + this.init(); + const msg = translate(direction === 'Next' ? 'toasts.load-next-chapter' : 'toasts.load-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()}); + this.toastr.info(msg, '', {timeOut: 3000}); + } + renderPage() { @@ -1434,7 +1480,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.pageNum >= this.maxPages - 10) { // Tell server to cache the next chapter if (this.nextChapterId > 0 && !this.nextChapterPrefetched) { - this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => { + this.readerService.getChapterInfo(this.nextChapterId, false, this.libraryType == LibraryType.Magazine).pipe(take(1)).subscribe(res => { this.continuousChapterInfos[ChapterInfoPosition.Next] = res; this.nextChapterPrefetched = true; this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.FORWARD); @@ -1442,7 +1488,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } else if (this.pageNum <= 10) { if (this.prevChapterId > 0 && !this.prevChapterPrefetched) { - this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => { + this.readerService.getChapterInfo(this.prevChapterId, false, this.libraryType == LibraryType.Magazine).pipe(take(1)).subscribe(res => { this.continuousChapterInfos[ChapterInfoPosition.Previous] = res; this.prevChapterPrefetched = true; this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.BACKWARDS); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index fa9073380..5281be81d 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -145,7 +145,8 @@ export class ReadingListDetailComponent implements OnInit { readChapter(item: ReadingListItem) { if (!this.readingList) return; const params = this.readerService.getQueryParamsObject(false, true, this.readingList.id); - this.router.navigate(this.readerService.getNavigationArray(item.libraryId, item.seriesId, item.chapterId, item.seriesFormat), {queryParams: params}); + this.router.navigate(this.readerService.getNavigationArray(item.libraryId, item.seriesId, item.chapterId, + item.seriesFormat, item.libraryType), {queryParams: params}); } async handleReadingListActionCallback(action: ActionItem, readingList: ReadingList) { @@ -209,7 +210,8 @@ export class ReadingListDetailComponent implements OnInit { if (!this.readingList) return; const firstItem = this.items[0]; this.router.navigate( - this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, firstItem.seriesFormat), + this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, + firstItem.seriesFormat, firstItem.libraryType), {queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}}); } @@ -226,7 +228,8 @@ export class ReadingListDetailComponent implements OnInit { } this.router.navigate( - this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat), + this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, + currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat, currentlyReadingChapter.libraryType), {queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}}); } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 5071d7b0e..bb14a3f1c 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -719,7 +719,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { // Book libraries only have Volumes or Specials enabled if (this.libraryType === LibraryType.Magazine) { if (this.volumes.length === 0) { - this.activeTabId = TabID.Chapters; + if (this.chapters.length > 0) { + this.activeTabId = TabID.Chapters; + } else { + this.activeTabId = TabID.Specials; + } } else { this.activeTabId = TabID.Volumes; } @@ -840,7 +844,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.toastr.error(this.translocoService.translate('series-detail.no-pages')); return; } - this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: {incognitoMode}}); + this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, + chapter.files[0].format, this.libraryType), {queryParams: {incognitoMode}}); } openVolume(volume: Volume) { diff --git a/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html b/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html index a2ef443a8..53c16644b 100644 --- a/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html +++ b/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html @@ -11,7 +11,7 @@ diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html index cd90281d5..d92afae6c 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html @@ -4,7 +4,7 @@
- {{stats.seriesCount | compactNumber}} Series + {{t('count-series', {num: stats.seriesCount | number})}}
@@ -13,7 +13,7 @@
- {{stats.volumeCount | compactNumber}} Volumes + {{t('count-volume', {num: stats.volumeCount | number})}}
@@ -22,7 +22,7 @@
- {{stats.totalFiles | compactNumber}} Files + {{t('file-volume', {num: stats.totalFiles | number})}}
@@ -88,7 +88,7 @@
- +
diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index a57fef24c..8f8a568db 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -166,7 +166,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.route.fragment.subscribe(frag => { const tab = this.tabs.filter(item => item.fragment === frag); - console.log('tab: ', tab); if (tab.length > 0) { this.active = tab[0]; } else { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index a8f6ae582..d2404c395 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1757,6 +1757,7 @@ "y-axis-label": "Hours Read", "no-data": "No Reading Progress", "time-frame-label": "Time Frame", + "all-users": "All Users", "this-week": "{{time-periods.this-week}}", "last-7-days": "{{time-periods.last-7-days}}", "last-30-days": "{{time-periods.last-30-days}}", @@ -1819,6 +1820,9 @@ "popular-libraries-title": "Popular Libraries", "popular-series-title": "Popular Series", "recently-read-title": "Recently Read", + "series-count": "{{num}} Series", + "volume-count": "{{num}} Volumes", + "file-count": "{{num}} Files", "genre-count": "{{num}} Genres", "tag-count": "{{num}} Tags", "people-count": "{{num}} People", diff --git a/openapi.json b/openapi.json index 0dbb8432a..0510eb1b8 100644 --- a/openapi.json +++ b/openapi.json @@ -2949,6 +2949,45 @@ } } }, + "/api/Library/types": { + "get": { + "tags": [ + "Library" + ], + "summary": "Return pairs of all types", + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeDto" + } + } + } + } + } + } + } + }, "/api/License/valid-license": { "get": { "tags": [ @@ -13458,6 +13497,18 @@ "chapterTitle": { "type": "string", "nullable": true + }, + "libraryType": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "format": "int32" } }, "additionalProperties": false @@ -16145,6 +16196,29 @@ }, "additionalProperties": false }, + "LibraryTypeDto": { + "type": "object", + "properties": { + "libraryId": { + "type": "integer", + "format": "int32" + }, + "libraryType": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Simple pairing of LibraryId and LibraryType" + }, "LoginDto": { "type": "object", "properties": {