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.
This commit is contained in:
Joseph Milazzo 2024-02-11 14:30:13 -06:00
parent 95e7ad0f5b
commit b3f6a574cd
23 changed files with 315 additions and 62 deletions

View file

@ -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,
});

View file

@ -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));
}
/// <summary>
/// Return pairs of all types
/// </summary>
/// <returns></returns>
[HttpGet("types")]
public async Task<ActionResult<IEnumerable<LibraryTypeDto>>> GetLibraryTypes()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypesAsync(User.GetUserId()));
}
}

View file

@ -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);
}
/// <summary>
/// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
/// </summary>

View file

@ -0,0 +1,12 @@
using API.Entities.Enums;
namespace API.DTOs;
/// <summary>
/// Simple pairing of LibraryId and LibraryType
/// </summary>
public class LibraryTypeDto
{
public int LibraryId { get; set; }
public LibraryType LibraryType { get; set; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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;
}

View file

@ -57,6 +57,7 @@ public interface ILibraryRepository
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
Task<IDictionary<int, LibraryType>> GetLibraryTypesBySeriesIdsAsync(IList<int> seriesIds);
Task<IEnumerable<LibraryTypeDto>> 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<IEnumerable<LibraryTypeDto>> 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();
}
}

View file

@ -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;
}
}
}
}

View file

@ -638,9 +638,10 @@ public static partial class Parser
#region Magazine
private static readonly Dictionary<string, int> _monthMappings = CreateMonthMappings();
private static readonly Regex[] MagazineSeriesRegex = new[]
{
private static readonly HashSet<string> GeoCodes = new(CreateCountryCodes());
private static readonly Dictionary<string, int> MonthMappings = CreateMonthMappings();
private static readonly Regex[] MagazineSeriesRegex =
[
// 3D World - 2018 UK, 3D World - 022014
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*\d{4,6}.*",
@ -649,10 +650,14 @@ public static partial class Parser
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*.*",
MatchOptions, RegexTimeout),
// AIR International #1 // This breaks the way the code works
// new Regex(
// @"^(?<Series>.+?)(_|\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<string, int> 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)
/// <summary>
/// Tries to parse a GeoCode (UK, US) out of a string
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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;

View file

@ -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<Array<{libraryId: number, libraryType: LibraryType}>>(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]);

View file

@ -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<ProgressBookmark>(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<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&includeDimensions=' + includeDimensions);
getChapterInfo(chapterId: number, includeDimensions = false, extractPdf = false) {
return this.httpClient.get<ChapterInfo>(this.baseUrl + `reader/chapter-info?chapterId=${chapterId}&includeDimensions=${includeDimensions}&extractPdf=${extractPdf}`);
}
getFileDimensions(chapterId: number) {

View file

@ -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(

View file

@ -634,12 +634,18 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
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), {queryParams: params});
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) {

View file

@ -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();
}

View file

@ -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.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);

View file

@ -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: 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}});
}

View file

@ -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) {
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) {

View file

@ -11,7 +11,7 @@
<label for="time-select-read-by-day" class="form-check-label"></label>
<select id="time-select-read-by-day" class="form-select" formControlName="users"
[class.is-invalid]="formGroup.get('users')?.invalid && formGroup.get('users')?.touched">
<option [value]="0">All Users</option>
<option [value]="0">{{t('all-users')}}</option>
<option *ngFor="let item of users$ | async" [value]="item.id">{{item.username}}</option>
</select>
</div>

View file

@ -4,7 +4,7 @@
<ng-container>
<div class="col-auto mb-2">
<app-icon-and-title [label]="t('total-series-label')" [clickable]="false" fontClasses="fa-solid fa-book-open" [title]="t('total-series-tooltip', {count: stats.seriesCount | number})">
{{stats.seriesCount | compactNumber}} Series
{{t('count-series', {num: stats.seriesCount | number})}}
</app-icon-and-title>
</div>
<div class="vr d-none d-lg-block m-2"></div>
@ -13,7 +13,7 @@
<ng-container >
<div class="col-auto mb-2">
<app-icon-and-title [label]="t('total-volumes-label')" [clickable]="false" fontClasses="fas fa-book" [title]="t('total-volumes-tooltip', {count: stats.volumeCount | number})">
{{stats.volumeCount | compactNumber}} Volumes
{{t('count-volume', {num: stats.volumeCount | number})}}
</app-icon-and-title>
</div>
<div class="vr d-none d-lg-block m-2"></div>
@ -22,7 +22,7 @@
<ng-container>
<div class="col-auto mb-2">
<app-icon-and-title [label]="t('total-files-label')" [clickable]="false" fontClasses="fa-regular fa-file" [title]="t('total-files-tooltip', {count: stats.totalFiles | number})">
{{stats.totalFiles | compactNumber}} Files
{{t('file-volume', {num: stats.totalFiles | number})}}
</app-icon-and-title>
</div>
<div class="vr d-none d-lg-block m-2"></div>
@ -88,7 +88,7 @@
</app-stat-list>
</div>
<div class="col-auto">
<app-stat-list [data$]="recentlyRead$" title="Recently Read" [image]="seriesImage" [handleClick]="openSeries"></app-stat-list>
<app-stat-list [data$]="recentlyRead$" [title]="t('recently-read-title')" [image]="seriesImage" [handleClick]="openSeries"></app-stat-list>
</div>
</div>

View file

@ -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 {

View file

@ -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",

View file

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