From 6340867ba0e133254c03047c38a47142eb029ae2 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 8 Jul 2025 17:50:17 -0500 Subject: [PATCH] PToC (now known as Text Bookmarks) have been refactored and done (except some small css). --- API/Controllers/ReaderController.cs | 6 + API/DTOs/Reader/PersonalToCDto.cs | 18 ++ ...0250708204811_BookAnnotations.Designer.cs} | 8 +- ...s.cs => 20250708204811_BookAnnotations.cs} | 10 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Entities/AppUserTableOfContent.cs | 22 +- API/Services/BookService.cs | 189 ++++++++++++++---- .../src/app/_models/readers/personal-toc.ts | 3 + .../book-line-overlay.component.html | 2 +- .../book-reader/book-reader.component.scss | 4 + .../personal-table-of-contents.component.html | 54 ++--- .../personal-table-of-contents.component.ts | 69 +++---- .../text-bookmark-item.component.html | 29 +++ .../text-bookmark-item.component.scss | 7 + .../text-bookmark-item.component.ts | 36 ++++ UI/Web/src/assets/langs/en.json | 7 +- 16 files changed, 361 insertions(+), 106 deletions(-) rename API/Data/Migrations/{20250704153900_BookAnnotations.Designer.cs => 20250708204811_BookAnnotations.Designer.cs} (99%) rename API/Data/Migrations/{20250704153900_BookAnnotations.cs => 20250708204811_BookAnnotations.cs} (91%) create mode 100644 UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.html create mode 100644 UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.scss create mode 100644 UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.ts diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 178597611..41b8420aa 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -934,6 +934,11 @@ public class ReaderController : BaseApiController return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark")); } + // Look up the chapter this PTOC is associated with to get the chapter title (if there is one) + var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId); + if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist")); + var toc = await _bookService.GenerateTableOfContents(chapter); + var chapterTitle = BookService.GetChapterTitleFromToC(toc, dto.PageNumber); _unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent() { @@ -944,6 +949,7 @@ public class ReaderController : BaseApiController LibraryId = dto.LibraryId, BookScrollId = dto.BookScrollId, SelectedText = dto.SelectedText, + ChapterTitle = chapterTitle, AppUserId = userId }); await _unitOfWork.CommitAsync(); diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/API/DTOs/Reader/PersonalToCDto.cs index a5144120b..66994a7ff 100644 --- a/API/DTOs/Reader/PersonalToCDto.cs +++ b/API/DTOs/Reader/PersonalToCDto.cs @@ -6,7 +6,25 @@ public sealed record PersonalToCDto { public required int Id { get; init; } public required int ChapterId { get; set; } + /// + /// The page to bookmark + /// public required int PageNumber { get; set; } + /// + /// The title of the bookmark. Defaults to Page {PageNumber} if not set + /// public required string Title { get; set; } + /// + /// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page + /// public string? BookScrollId { get; set; } + /// + /// Text of the bookmark + /// + public string? SelectedText { get; set; } + /// + /// Title of the Chapter this PToC was created in + /// + /// Taken from the ToC + public string? ChapterTitle { get; set; } } diff --git a/API/Data/Migrations/20250704153900_BookAnnotations.Designer.cs b/API/Data/Migrations/20250708204811_BookAnnotations.Designer.cs similarity index 99% rename from API/Data/Migrations/20250704153900_BookAnnotations.Designer.cs rename to API/Data/Migrations/20250708204811_BookAnnotations.Designer.cs index e2059e3ef..f3c10a534 100644 --- a/API/Data/Migrations/20250704153900_BookAnnotations.Designer.cs +++ b/API/Data/Migrations/20250708204811_BookAnnotations.Designer.cs @@ -13,7 +13,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace API.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20250704153900_BookAnnotations")] + [Migration("20250708204811_BookAnnotations")] partial class BookAnnotations { /// @@ -892,6 +892,9 @@ namespace API.Data.Migrations b.Property("ChapterId") .HasColumnType("INTEGER"); + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + b.Property("Created") .HasColumnType("TEXT"); @@ -1410,6 +1413,9 @@ namespace API.Data.Migrations b.Property("PrimaryColor") .HasColumnType("TEXT"); + b.Property("RemovePrefixForSortName") + .HasColumnType("INTEGER"); + b.Property("SecondaryColor") .HasColumnType("TEXT"); diff --git a/API/Data/Migrations/20250704153900_BookAnnotations.cs b/API/Data/Migrations/20250708204811_BookAnnotations.cs similarity index 91% rename from API/Data/Migrations/20250704153900_BookAnnotations.cs rename to API/Data/Migrations/20250708204811_BookAnnotations.cs index 16dc39991..81fe51954 100644 --- a/API/Data/Migrations/20250704153900_BookAnnotations.cs +++ b/API/Data/Migrations/20250708204811_BookAnnotations.cs @@ -11,6 +11,12 @@ namespace API.Data.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "ChapterTitle", + table: "AppUserTableOfContent", + type: "TEXT", + nullable: true); + migrationBuilder.AddColumn( name: "SelectedText", table: "AppUserTableOfContent", @@ -74,6 +80,10 @@ namespace API.Data.Migrations migrationBuilder.DropTable( name: "AppUserAnnotation"); + migrationBuilder.DropColumn( + name: "ChapterTitle", + table: "AppUserTableOfContent"); + migrationBuilder.DropColumn( name: "SelectedText", table: "AppUserTableOfContent"); diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index cfbb14516..d5de68777 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -889,6 +889,9 @@ namespace API.Data.Migrations b.Property("ChapterId") .HasColumnType("INTEGER"); + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + b.Property("Created") .HasColumnType("TEXT"); diff --git a/API/Entities/AppUserTableOfContent.cs b/API/Entities/AppUserTableOfContent.cs index 954fdd617..5d110b8b6 100644 --- a/API/Entities/AppUserTableOfContent.cs +++ b/API/Entities/AppUserTableOfContent.cs @@ -18,6 +18,19 @@ public class AppUserTableOfContent : IEntityDate /// The title of the bookmark. Defaults to Page {PageNumber} if not set /// public required string Title { get; set; } + /// + /// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page + /// + public string? BookScrollId { get; set; } + /// + /// Text of the bookmark + /// + public string? SelectedText { get; set; } + /// + /// Title of the Chapter this PToC was created in + /// + /// Taken from the ToC + public string? ChapterTitle { get; set; } public required int SeriesId { get; set; } public virtual Series Series { get; set; } @@ -27,14 +40,7 @@ public class AppUserTableOfContent : IEntityDate public int VolumeId { get; set; } public int LibraryId { get; set; } - /// - /// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page - /// - public string? BookScrollId { get; set; } - /// - /// Text of the bookmark - /// - public string? SelectedText { get; set; } + public DateTime Created { get; set; } public DateTime CreatedUtc { get; set; } diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index f51532671..cfb026e0a 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -63,7 +63,7 @@ public interface IBookService Task?> GetWordCountsPerPage(string bookFilePath); } -public class BookService : IBookService +public partial class BookService : IBookService { private readonly ILogger _logger; private readonly IDirectoryService _directoryService; @@ -1226,6 +1226,88 @@ public class BookService : IBookService /// public async Task> GenerateTableOfContents(Chapter chapter) { + // using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, LenientBookReaderOptions); + // var mappings = await CreateKeyToPageMappingAsync(book); + // + // var navItems = await book.GetNavigationAsync(); + // var chaptersList = new List(); + // + // if (navItems != null) + // { + // foreach (var navigationItem in navItems) + // { + // if (navigationItem.NestedItems.Count == 0) + // { + // CreateToCChapter(book, navigationItem, Array.Empty(), chaptersList, mappings); + // continue; + // } + // + // var nestedChapters = new List(); + // + // foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null)) + // { + // var key = CoalesceKey(book, mappings, nestedChapter.Link?.ContentFilePath); + // if (mappings.TryGetValue(key, out var mapping)) + // { + // nestedChapters.Add(new BookChapterItem + // { + // Title = nestedChapter.Title, + // Page = mapping, + // Part = nestedChapter.Link?.Anchor ?? string.Empty, + // Children = [] + // }); + // } + // } + // + // CreateToCChapter(book, navigationItem, nestedChapters, chaptersList, mappings); + // } + // } + // + // if (chaptersList.Count != 0) return chaptersList; + // // Generate from TOC from links (any point past this, Kavita is generating as a TOC doesn't exist) + // var tocPage = book.Content.Html.Local.Select(s => s.Key) + // .FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) || + // k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase)); + // if (string.IsNullOrEmpty(tocPage)) return chaptersList; + // + // + // // Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content + // if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList; + // var content = await file.ReadContentAsync(); + // + // var doc = new HtmlDocument(); + // doc.LoadHtml(content); + // + // // TODO: We may want to check if there is a toc.ncs file to better handle nested toc + // // We could do a fallback first with ol/lis + // + // + // + // var anchors = doc.DocumentNode.SelectNodes("//a"); + // if (anchors == null) return chaptersList; + // + // foreach (var anchor in anchors) + // { + // if (!anchor.Attributes.Contains("href")) continue; + // + // var key = CoalesceKey(book, mappings, anchor.Attributes["href"].Value.Split("#")[0]); + // + // if (string.IsNullOrEmpty(key) || !mappings.ContainsKey(key)) continue; + // var part = string.Empty; + // if (anchor.Attributes["href"].Value.Contains('#')) + // { + // part = anchor.Attributes["href"].Value.Split("#")[1]; + // } + // chaptersList.Add(new BookChapterItem + // { + // Title = anchor.InnerText, + // Page = mappings[key], + // Part = part, + // Children = [] + // }); + // } + // + // return chaptersList; using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, LenientBookReaderOptions); var mappings = await CreateKeyToPageMappingAsync(book); @@ -1236,53 +1318,29 @@ public class BookService : IBookService { foreach (var navigationItem in navItems) { - if (navigationItem.NestedItems.Count == 0) + var tocItem = CreateToCChapterRecursively(book, navigationItem, mappings); + if (tocItem != null) { - CreateToCChapter(book, navigationItem, Array.Empty(), chaptersList, mappings); - continue; + chaptersList.Add(tocItem); } - - var nestedChapters = new List(); - - foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null)) - { - var key = CoalesceKey(book, mappings, nestedChapter.Link?.ContentFilePath); - if (mappings.TryGetValue(key, out var mapping)) - { - nestedChapters.Add(new BookChapterItem - { - Title = nestedChapter.Title, - Page = mapping, - Part = nestedChapter.Link?.Anchor ?? string.Empty, - Children = new List() - }); - } - } - - CreateToCChapter(book, navigationItem, nestedChapters, chaptersList, mappings); } } if (chaptersList.Count != 0) return chaptersList; + + // Rest of your fallback logic remains the same... // Generate from TOC from links (any point past this, Kavita is generating as a TOC doesn't exist) var tocPage = book.Content.Html.Local.Select(s => s.Key) .FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) || k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase)); if (string.IsNullOrEmpty(tocPage)) return chaptersList; - - // Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList; var content = await file.ReadContentAsync(); var doc = new HtmlDocument(); doc.LoadHtml(content); - // TODO: We may want to check if there is a toc.ncs file to better handle nested toc - // We could do a fallback first with ol/lis - - - var anchors = doc.DocumentNode.SelectNodes("//a"); if (anchors == null) return chaptersList; @@ -1303,19 +1361,55 @@ public class BookService : IBookService Title = anchor.InnerText, Page = mappings[key], Part = part, - Children = new List() + Children = [] }); } return chaptersList; } + private BookChapterItem? CreateToCChapterRecursively(EpubBookRef book, EpubNavigationItemRef navigationItem, Dictionary mappings) + { + // Get the page mapping for the current navigation item + var key = CoalesceKey(book, mappings, navigationItem.Link?.ContentFilePath); + int? page = null; + if (!string.IsNullOrEmpty(key) && mappings.TryGetValue(key, out var mapping)) + { + page = mapping; + } + + // Recursively process nested items + var children = new List(); + if (navigationItem.NestedItems?.Count > 0) + { + foreach (var nestedItem in navigationItem.NestedItems) + { + var childItem = CreateToCChapterRecursively(book, nestedItem, mappings); + if (childItem != null) + { + children.Add(childItem); + } + } + } + + // Only create a BookChapterItem if we have a valid page or children + if (page.HasValue || children.Count > 0) + { + return new BookChapterItem + { + Title = navigationItem.Title ?? string.Empty, + Page = page ?? 0, // You might want to handle this differently + Part = navigationItem.Link?.Anchor ?? string.Empty, + Children = children + }; + } + + return null; + } + private static int CountParentDirectory(string path) { - const string pattern = @"\.\./"; - var matches = Regex.Matches(path, pattern); - - return matches.Count; + return ParentDirectoryRegex().Matches(path).Count; } /// @@ -1481,6 +1575,28 @@ public class BookService : IBookService return string.Empty; } + public static string? GetChapterTitleFromToC(ICollection? tableOfContents, int pageNumber) + { + if (tableOfContents == null) return null; + + foreach (var item in tableOfContents) + { + // Check if current item matches the page number + if (item.Page == pageNumber) + return item.Title; + + // Recursively search children if they exist + if (item.Children?.Count > 0) + { + var childResult = GetChapterTitleFromToC(item.Children, pageNumber); + if (childResult != null) + return childResult; + } + } + + return null; + } + private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size) { @@ -1570,4 +1686,7 @@ public class BookService : IBookService _logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason); } } + + [GeneratedRegex(@"\.\./")] + private static partial Regex ParentDirectoryRegex(); } diff --git a/UI/Web/src/app/_models/readers/personal-toc.ts b/UI/Web/src/app/_models/readers/personal-toc.ts index 3d4c3c9af..b0ce7aa20 100644 --- a/UI/Web/src/app/_models/readers/personal-toc.ts +++ b/UI/Web/src/app/_models/readers/personal-toc.ts @@ -3,6 +3,9 @@ export interface PersonalToC { pageNumber: number; title: string; bookScrollId: string | undefined; + selectedText: string | null; + chapterTitle: string | null; /* Ui Only */ position: 0; + } diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html index 6e3d0d4ad..1912a8978 100644 --- a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html @@ -41,7 +41,7 @@ @case (BookLineOverlayMode.Bookmark) {
- @if (bookmarkForm.dirty || bookmarkForm.touched) { diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss index ee4f5b8f0..526353125 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss @@ -355,6 +355,10 @@ $pagination-opacity: 0; //$pagination-color: red; //$pagination-opacity: 0.7; +.kavita-scale-width::after { + content: ' '; +} + .right { position: absolute; diff --git a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html index 591972499..b48479a5c 100644 --- a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html +++ b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html @@ -1,32 +1,36 @@ - +
- @if (Pages.length === 0) { + @let bookmarks = ptocBookmarks(); + + @if(bookmarks.length >= ShowFilterAfterItems) { + +
+
+ +
+ +
+
+
+ + } + + @for(bookmark of bookmarks | filter: filterList; track bookmark.pageNumber + bookmark.title) { +
+ +
+ } @empty {
- {{t('no-data')}} + @if (formGroup.get('filter')?.value) { + {{t('no-match')}} + } @else { + {{t('no-data')}} + } +
} -
    - @for (page of Pages; track page) { -
  • - {{t('page', {value: page})}} -
      - @for(bookmark of bookmarks[page]; track bookmark) { -
    • - {{bookmark.title}} - -
    • - } -
    -
  • - } - -
diff --git a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts index d45dfb60e..fa118df01 100644 --- a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts +++ b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.ts @@ -1,19 +1,22 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, - Component, DestroyRef, EventEmitter, - Inject, + Component, + DestroyRef, + EventEmitter, inject, Input, + model, OnInit, Output } from '@angular/core'; -import {DOCUMENT} from '@angular/common'; import {ReaderService} from "../../../_services/reader.service"; import {PersonalToC} from "../../../_models/readers/personal-toc"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {TranslocoDirective} from "@jsverse/transloco"; +import {translate, TranslocoDirective} from "@jsverse/transloco"; +import {TextBookmarkItemComponent} from "../text-bookmark-item/text-bookmark-item.component"; +import {ConfirmService} from "../../../shared/confirm.service"; +import {FilterPipe} from "../../../_pipes/filter.pipe"; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; export interface PersonalToCEvent { pageNum: number; @@ -21,31 +24,32 @@ export interface PersonalToCEvent { } @Component({ - selector: 'app-personal-table-of-contents', - imports: [NgbTooltip, TranslocoDirective], - templateUrl: './personal-table-of-contents.component.html', - styleUrls: ['./personal-table-of-contents.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-personal-table-of-contents', + imports: [TranslocoDirective, TextBookmarkItemComponent, FilterPipe, FormsModule, ReactiveFormsModule], + templateUrl: './personal-table-of-contents.component.html', + styleUrls: ['./personal-table-of-contents.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonalTableOfContentsComponent implements OnInit { + private readonly readerService = inject(ReaderService); + private readonly destroyRef = inject(DestroyRef); + private readonly confirmService = inject(ConfirmService); + + protected readonly ShowFilterAfterItems = 10; + @Input({required: true}) chapterId!: number; @Input({required: true}) pageNum: number = 0; @Input({required: true}) tocRefresh!: EventEmitter; @Output() loadChapter: EventEmitter = new EventEmitter(); - private readonly readerService = inject(ReaderService); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly destroyRef = inject(DestroyRef); - bookmarks: {[key: number]: Array} = []; - get Pages() { - return Object.keys(this.bookmarks).map(p => parseInt(p, 10)); - } - - constructor(@Inject(DOCUMENT) private document: Document) {} + ptocBookmarks = model([]); + formGroup = new FormGroup({ + filter: new FormControl('', []) + }); ngOnInit() { this.tocRefresh.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { @@ -57,13 +61,7 @@ export class PersonalTableOfContentsComponent implements OnInit { load() { this.readerService.getPersonalToC(this.chapterId).subscribe(res => { - res.forEach(t => { - if (!this.bookmarks.hasOwnProperty(t.pageNumber)) { - this.bookmarks[t.pageNumber] = []; - } - this.bookmarks[t.pageNumber].push(t); - }) - this.cdRef.markForCheck(); + this.ptocBookmarks.set(res); }); } @@ -71,15 +69,18 @@ export class PersonalTableOfContentsComponent implements OnInit { this.loadChapter.emit({pageNum, scrollPart}); } - removeBookmark(bookmark: PersonalToC) { - this.readerService.removePersonalToc(bookmark.chapterId, bookmark.pageNumber, bookmark.title).subscribe(() => { - this.bookmarks[bookmark.pageNumber] = this.bookmarks[bookmark.pageNumber].filter(t => t.title != bookmark.title); + async removeBookmark(bookmark: PersonalToC) { - if (this.bookmarks[bookmark.pageNumber].length === 0) { - delete this.bookmarks[bookmark.pageNumber]; - } - this.cdRef.markForCheck(); + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-bookmark'))) return; + + this.readerService.removePersonalToc(bookmark.chapterId, bookmark.pageNumber, bookmark.title).subscribe(() => { + this.ptocBookmarks.set(this.ptocBookmarks().filter(t => t.title !== bookmark.title)); }); } + filterList = (listItem: PersonalToC) => { + const query = (this.formGroup.get('filter')?.value || '').toLowerCase(); + return listItem.title.toLowerCase().indexOf(query) >= 0 || listItem.pageNumber.toString().indexOf(query) >= 0 || (listItem.chapterTitle ?? '').toLowerCase().indexOf(query) >= 0; + } + } diff --git a/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.html b/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.html new file mode 100644 index 000000000..3cff44c0f --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.html @@ -0,0 +1,29 @@ + + @let ptoc = bookmark(); + @if (ptoc) { + +
+ +
+
+ {{ptoc.title}} + + +
+
+ @if (ptoc.chapterTitle) { + Chapter "{{ptoc.chapterTitle}}" - + } + {{t('page', {value: ptoc.pageNumber})}} +
+
+ + + + +
+ } +
diff --git a/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.scss b/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.scss new file mode 100644 index 000000000..c947fddfa --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.scss @@ -0,0 +1,7 @@ +.card:hover { + background-color: var(--elevation-layer7) +} +.ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.ts b/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.ts new file mode 100644 index 000000000..258262169 --- /dev/null +++ b/UI/Web/src/app/book-reader/_components/text-bookmark-item/text-bookmark-item.component.ts @@ -0,0 +1,36 @@ +import {Component, EventEmitter, input, Output} from '@angular/core'; +import {PersonalToC} from "../../../_models/readers/personal-toc"; +import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; +import {TranslocoDirective} from "@jsverse/transloco"; + +@Component({ + selector: 'app-text-bookmark-item', + imports: [ + NgbTooltip, + TranslocoDirective + ], + templateUrl: './text-bookmark-item.component.html', + styleUrl: './text-bookmark-item.component.scss' +}) +export class TextBookmarkItemComponent { + bookmark = input.required(); + + @Output() loadBookmark = new EventEmitter(); + @Output() removeBookmark = new EventEmitter(); + + + remove(evt: Event) { + evt.stopPropagation(); + evt.preventDefault(); + + this.removeBookmark.emit(this.bookmark()); + } + + goTo(evt: Event) { + evt.stopPropagation(); + evt.preventDefault(); + + this.loadBookmark.emit(this.bookmark()); + } + +} diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index ebe03285e..c8252f249 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -874,7 +874,9 @@ "personal-table-of-contents": { "no-data": "Nothing Bookmarked yet", "page": "Page {{value}}", - "delete": "Delete {{bookmarkName}}" + "delete": "Delete {{bookmarkName}}", + "no-match": "No Bookmarks match filter", + "filter-label": "{{common.filter}}" }, "confirm-email": { @@ -2779,7 +2781,8 @@ "scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services.", "series-bound-to-reading-profile": "Series bound to Reading Profile {{name}}", "library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}", - "external-match-rate-error": "Kavita ran out of rate looking up {{seriesName}}. Try again in 5 minutes." + "external-match-rate-error": "Kavita ran out of rate looking up {{seriesName}}. Try again in 5 minutes.", + "confirm-delete-bookmark": "Are you sure you want to delete this Bookmark?" }, "read-time-pipe": {