diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 4147ce641..473035c20 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -478,9 +478,6 @@ public class SeriesService : ISeriesService foreach (var chapter in chapters) { - // if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName; - // else chapter.Title = await FormatChapterTitle(userId, chapter, libraryType); - chapter.Title = await FormatChapterTitle(userId, chapter, libraryType); if (!chapter.IsSpecial) continue; @@ -536,7 +533,12 @@ public class SeriesService : ISeriesService { var firstChapter = volume.Chapters.First(); // On Books, skip volumes that are specials, since these will be shown - if (firstChapter.IsSpecial) return false; + // if (firstChapter.IsSpecial) + // { + // // Some books can be SP marker and also position of 0, this will trick Kavita into rendering it as part of a non-special volume + // // We need to rename the entity so that it renders out correctly + // return false; + // } if (string.IsNullOrEmpty(firstChapter.TitleName)) { if (firstChapter.Range.Equals(Parser.LooseLeafVolume)) return false; @@ -550,7 +552,7 @@ public class SeriesService : ISeriesService volume.Name = firstChapter.TitleName; } - return true; + return !firstChapter.IsSpecial; } volume.Name = $"{volumeLabel.Trim()} {volume.Name}".Trim(); diff --git a/API/Services/Tasks/Scanner/Parser/BookParser.cs b/API/Services/Tasks/Scanner/Parser/BookParser.cs index 73e5513ac..499e554ef 100644 --- a/API/Services/Tasks/Scanner/Parser/BookParser.cs +++ b/API/Services/Tasks/Scanner/Parser/BookParser.cs @@ -13,7 +13,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer info.ComicInfo = comicInfo; // We need a special piece of code to override the Series IF there is a special marker in the filename for epub files - if (info.IsSpecial && info.Volumes == "0" && info.ComicInfo.Series != info.Series) + if (info.IsSpecial && info.Volumes is "0" or "0.0" && info.ComicInfo.Series != info.Series) { info.Series = info.ComicInfo.Series; } diff --git a/UI/Web/README.md b/UI/Web/README.md index c8d1a002f..4efc47cbc 100644 --- a/UI/Web/README.md +++ b/UI/Web/README.md @@ -36,5 +36,4 @@ Run `npm run start` - all components must be standalone # Update latest angular -`ng update @angular/core @angular/cli @typescript-es -lint/parser @angular/localize @angular/compiler-cli @angular/cli @angular-devkit/build-angular @angular/cdk` +`ng update @angular/core @angular/cli @typescript-eslint/parser @angular/localize @angular/compiler-cli @angular-devkit/build-angular @angular/cdk` diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html new file mode 100644 index 000000000..600e46637 --- /dev/null +++ b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html @@ -0,0 +1,29 @@ + + + diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.scss b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.ts b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.ts new file mode 100644 index 000000000..a2ba71d94 --- /dev/null +++ b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.ts @@ -0,0 +1,98 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, DestroyRef, + EventEmitter, + inject, + Input, + OnInit, + Output +} from '@angular/core'; +import {NgClass} from "@angular/common"; +import {TranslocoDirective} from "@jsverse/transloco"; +import {Breakpoint, UtilityService} from "../../shared/_services/utility.service"; +import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; +import {Action, ActionItem} from "../../_services/action-factory.service"; +import {AccountService} from "../../_services/account.service"; +import {tap} from "rxjs"; +import {User} from "../../_models/user"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'app-actionable-modal', + standalone: true, + imports: [ + NgClass, + TranslocoDirective + ], + templateUrl: './actionable-modal.component.html', + styleUrl: './actionable-modal.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ActionableModalComponent implements OnInit { + + protected readonly utilityService = inject(UtilityService); + protected readonly modal = inject(NgbActiveModal); + protected readonly accountService = inject(AccountService); + protected readonly cdRef = inject(ChangeDetectorRef); + protected readonly destroyRef = inject(DestroyRef); + protected readonly Breakpoint = Breakpoint; + + @Input() actions: ActionItem[] = []; + @Input() willRenderAction!: (action: ActionItem) => boolean; + @Input() shouldRenderSubMenu!: (action: ActionItem, dynamicList: null | Array) => boolean; + @Output() actionPerformed = new EventEmitter>(); + + currentLevel: string[] = []; + currentItems: ActionItem[] = []; + user!: User | undefined; + + ngOnInit() { + this.currentItems = this.actions; + this.accountService.currentUser$.pipe(tap(user => { + this.user = user; + this.cdRef.markForCheck(); + }), takeUntilDestroyed(this.destroyRef)).subscribe(); + } + + handleItemClick(item: ActionItem) { + if (item.children && item.children.length > 0) { + this.currentLevel.push(item.title); + this.currentItems = item.children; + } else if (item.dynamicList) { + item.dynamicList.subscribe(dynamicItems => { + this.currentLevel.push(item.title); + this.currentItems = dynamicItems.map(di => ({ + ...item, + title: di.title, + _extra: di + })); + }); + } else { + this.actionPerformed.emit(item); + this.modal.close(item); + } + this.cdRef.markForCheck(); + } + + handleBack() { + if (this.currentLevel.length > 0) { + this.currentLevel.pop(); + + let items = this.actions; + for (let level of this.currentLevel) { + items = items.find(item => item.title === level)?.children || []; + } + + this.currentItems = items; + this.cdRef.markForCheck(); + } + } + + // willRenderAction(action: ActionItem) { + // if (this.user === undefined) return false; + // + // return this.accountService.canInvokeAction(this.user, action.action); + // } + +} diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html index ca84104d4..f40b9b1eb 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html @@ -1,40 +1,45 @@ @if (actions.length > 0) { -
- -
- + @if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) { + + } @else { +
+ +
+ +
-
- - @for(action of list; track action.id) { - - @if (action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined) { - @if (action.dynamicList !== undefined && (action.dynamicList | async | dynamicList); as dList) { - @for(dynamicItem of dList; track dynamicItem.title) { - - } - } @else if (willRenderAction(action)) { - - } - } @else { - @if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async)) { - -
- @if (willRenderAction(action)) { - + + @for(action of list; track action.id) { + + @if (action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined) { + @if (action.dynamicList !== undefined && (action.dynamicList | async | dynamicList); as dList) { + @for(dynamicItem of dList; track dynamicItem.title) { + } -
- + } @else if (willRenderAction(action)) { + + } + } @else { + @if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async)) { + +
+ @if (willRenderAction(action)) { + + } +
+ +
-
+ } } } - } -
+ + } } diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts index 8c054c69b..86005c21e 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts @@ -8,13 +8,16 @@ import { OnInit, Output } from '@angular/core'; -import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle} from '@ng-bootstrap/ng-bootstrap'; +import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle, NgbModal} from '@ng-bootstrap/ng-bootstrap'; import { AccountService } from 'src/app/_services/account.service'; import { Action, ActionItem } from 'src/app/_services/action-factory.service'; import {AsyncPipe, NgTemplateOutlet} from "@angular/common"; import {TranslocoDirective} from "@jsverse/transloco"; import {DynamicListPipe} from "./_pipes/dynamic-list.pipe"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {Breakpoint, UtilityService} from "../../shared/_services/utility.service"; +import {NavLinkModalComponent} from "../../nav/_components/nav-link-modal/nav-link-modal.component"; +import {ActionableModalComponent} from "../actionable-modal/actionable-modal.component"; @Component({ selector: 'app-card-actionables', @@ -29,6 +32,10 @@ export class CardActionablesComponent implements OnInit { private readonly cdRef = inject(ChangeDetectorRef); private readonly accountService = inject(AccountService); private readonly destroyRef = inject(DestroyRef); + protected readonly utilityService = inject(UtilityService); + protected readonly modalService = inject(NgbModal); + + protected readonly Breakpoint = Breakpoint; @Input() iconClass = 'fa-ellipsis-v'; @Input() btnClass = ''; @@ -110,4 +117,16 @@ export class CardActionablesComponent implements OnInit { action._extra = dynamicItem; this.performAction(event, action); } + + openMobileActionableMenu(event: any) { + this.preventEvent(event); + + const ref = this.modalService.open(ActionableModalComponent, {fullscreen: 'sm'}); + ref.componentInstance.actions = this.actions; + ref.componentInstance.willRenderAction = this.willRenderAction.bind(this); + ref.componentInstance.shouldRenderSubMenu = this.shouldRenderSubMenu.bind(this); + ref.componentInstance.actionPerformed.subscribe((action: ActionItem) => { + this.performAction(event, action); + }); + } } 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 2152c2095..a8b27c953 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 @@ -17,7 +17,7 @@ import { TrackByFunction, ViewChild } from '@angular/core'; -import {NavigationEnd, NavigationStart, Router} from '@angular/router'; +import {NavigationStart, Router} from '@angular/router'; import {VirtualScrollerComponent, VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller'; import {FilterSettings} from 'src/app/metadata-filter/filter-settings'; import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service'; 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 74a35f4fb..b06e6b009 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 @@ -812,7 +812,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { && (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1) ); - //console.log('Requesting page ', pageNum, ' found page: ', img, ' and app is requesting new image? ', forceNew); + console.log('Requesting page ', pageNum, ' found page: ', img, ' and app is requesting new image? ', forceNew); if (!img || forceNew) { img = new Image(); img.src = this.getPageUrl(pageNum, chapterId); @@ -1416,7 +1416,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { let numOffset = this.pageNum + i; if (numOffset > this.maxPages - 1) { - continue; + break; } const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length; @@ -1532,14 +1532,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (direction === PAGING_DIRECTION.BACKWARDS) { if (this.continuousChapterInfos[ChapterInfoPosition.Previous] === undefined) return; const n = this.continuousChapterInfos[ChapterInfoPosition.Previous]!.pages; - pages = Array.from({length: n + 1}, (v, k) => n - k); + // Ensure we only load up to 5 pages backward + pages = Array.from({ length: Math.min(n + 1, 5) }, (v, k) => n - k); } else { pages = [0, 1, 2, 3, 4]; } - let images = []; + const images = []; pages.forEach((_, i: number) => { - let img = new Image(); + const img = new Image(); img.src = this.getPageUrl(i, chapterId); images.push(img) }); diff --git a/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html b/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html index ee122452b..a34b63a5b 100644 --- a/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html +++ b/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html @@ -4,13 +4,13 @@
-
@switch (currentStepIndex) { @case (Step.Import) {

{{t('import-description')}}

-

{{t('cbl-repo') | safeHtml}}

+

+
diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index efff0d901..0019f436d 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1462,7 +1462,6 @@ "theme": "Theme", "customize": "Customize", "cbl-import": "CBL Reading List", - "cbl-repo": "You can find many reading lists in the community repo.", "mal-stack-import": "MAL Stack" }, @@ -1668,7 +1667,8 @@ "validate-cbl-step": "Validate CBL", "dry-run-step": "Dry Run", "final-import-step": "Final Step", - "comicvine-parsing-label": "Use Comic Vine Series matching" + "comicvine-parsing-label": "Use Comic Vine Series matching", + "cbl-repo": "You can find many reading lists in the community repo." }, "pdf-reader": { @@ -2420,7 +2420,10 @@ "promote": "Promote", "promote-tooltip": "Make the item visible to all users", "new-collection": "New Collection", - "multiple-selections": "Multiple Selections" + "multiple-selections": "Multiple Selections", + "back-to": "Back to {{action}}", + "title": "Actions" + }, "preferences": { diff --git a/openapi.json b/openapi.json index 4e95289cf..c8531966b 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.2.10", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.2.11", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"