feat(bookmark): improve bookmark feature by adding rename, note, color, priority functionalities (#1946)

* feat(bookmark): add bookmark editing, priority, color, notes, and improved sorting

- Add UpdateBookMarkRequest DTO and bookmark editing dialog/component in frontend
- Extend BookMark model/entity with color, notes, priority, updatedAt fields
- Implement bookmark update API and service logic with validation
- Sort bookmarks by priority and creation date
- Add Flyway migrations for new columns and index
- Update tests for new bookmark features

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* fix(bookmark): prevent notes length display error in edit dialog

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* fix(bookmark): reset editing state and improve dialog cancel handling

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* fix(bookmark): improve edit dialog template with Angular @if and conditional error display

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* feat(bookmark): add view dialog, search, and improved display for bookmarks in reader

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* feat(bookmark): redesign bookmarks section UI with improved layout, styling, and interactions

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* feat(bookmark): enhance view dialog UI with improved layout, styling, and priority display

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* chore(migration): rename migration files to maintain sequential versioning

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* feat(bookmark): add view and edit actions to bookmark list with improved UI and tooltips

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* feat(bookmark): add search and filter functionality to bookmark list in EPUB reader

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

* feat(bookmark): update search input to use PrimeNG IconField and InputIcon components

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs
2025-12-21 06:22:10 +01:00
committed by GitHub
parent 1b4bdc2ddb
commit 645234e66f
18 changed files with 977 additions and 124 deletions

View File

@@ -0,0 +1,191 @@
import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { InputText } from 'primeng/inputtext';
import { ColorPicker } from 'primeng/colorpicker';
import { Textarea } from 'primeng/textarea';
import { InputNumber } from 'primeng/inputnumber';
import { Button } from 'primeng/button';
import { BookMark, UpdateBookMarkRequest } from '../../../../shared/service/book-mark.service';
import { PrimeTemplate } from 'primeng/api';
export interface BookmarkFormData {
title: string;
color: string;
notes: string;
priority: number | null;
}
@Component({
selector: 'app-bookmark-edit-dialog',
standalone: true,
imports: [
CommonModule,
FormsModule,
Dialog,
InputText,
ColorPicker,
Textarea,
InputNumber,
Button,
PrimeTemplate
],
template: `
<p-dialog
[(visible)]="visible"
[modal]="true"
[closable]="true"
[style]="{width: '500px'}"
[draggable]="false"
[resizable]="false"
[closeOnEscape]="true"
[appendTo]="'body'"
header="Edit Bookmark"
(onHide)="onDialogHide()">
@if (formData) {
<div class="p-4">
<div class="field mb-4">
<label for="title" class="block text-sm font-medium mb-2">Title <span class="text-red-500">*</span></label>
<input
pInputText
id="title"
type="text"
[(ngModel)]="formData.title"
class="w-full"
[class.ng-invalid]="titleError"
[class.ng-dirty]="titleError"
placeholder="Enter bookmark title"
[maxlength]="255"
(ngModelChange)="titleError = false">
@if (titleError) {
<small class="text-red-500">Title is required</small>
}
</div>
<div class="field mb-4">
<label for="color" class="block text-sm font-medium mb-2">Color</label>
<div class="flex align-items-center gap-2">
<p-colorPicker
[(ngModel)]="formData.color"
[appendTo]="'body'"
format="hex">
</p-colorPicker>
<input
pInputText
[(ngModel)]="formData.color"
class="w-8rem"
placeholder="#000000"
pattern="^#[0-9A-Fa-f]{6}$">
</div>
</div>
<div class="field mb-4">
<label for="notes" class="block text-sm font-medium mb-2">Notes</label>
<textarea
pInputTextarea
id="notes"
[(ngModel)]="formData.notes"
class="w-full"
rows="3"
placeholder="Add notes about this bookmark"
[maxlength]="2000">
</textarea>
<small class="text-muted">{{ formData.notes.length || 0 }}/2000</small>
</div>
<div class="field mb-4">
<label for="priority" class="block text-sm font-medium mb-2">Priority (1 = High, 5 = Low)</label>
<p-inputNumber
id="priority"
[(ngModel)]="formData.priority"
[min]="1"
[max]="5"
[showButtons]="true"
buttonLayout="horizontal"
spinnerMode="horizontal"
decrementButtonClass="p-button-secondary"
incrementButtonClass="p-button-secondary"
decrementButtonIcon="pi pi-minus"
incrementButtonIcon="pi pi-plus">
</p-inputNumber>
</div>
</div>
}
<ng-template pTemplate="footer">
<div class="flex justify-content-between">
<p-button
label="Cancel"
icon="pi pi-times"
(click)="onCancel()"
[text]="true"
severity="secondary">
</p-button>
<p-button
label="Save"
icon="pi pi-check"
(click)="onSave()"
[loading]="isSaving"
[disabled]="!formData || isSaving">
</p-button>
</div>
</ng-template>
</p-dialog>
`
})
export class BookmarkEditDialogComponent implements OnChanges {
@Input() visible = false;
@Input() bookmark: BookMark | null = null;
@Input() isSaving = false;
@Output() visibleChange = new EventEmitter<boolean>();
@Output() save = new EventEmitter<UpdateBookMarkRequest>();
@Output() cancelEdit = new EventEmitter<void>();
formData: BookmarkFormData | null = null;
titleError = false;
ngOnChanges(changes: SimpleChanges): void {
if (changes['bookmark'] && this.bookmark) {
this.titleError = false;
this.formData = {
title: this.bookmark.title || '',
color: this.bookmark.color || '#3B82F6',
notes: this.bookmark.notes || '',
priority: this.bookmark.priority ?? 3
};
}
}
onSave(): void {
if (!this.formData) return;
if (!this.formData.title || !this.formData.title.trim()) {
this.titleError = true;
return;
}
const request: UpdateBookMarkRequest = {
title: this.formData.title.trim(),
color: this.formData.color || undefined,
notes: this.formData.notes || undefined,
priority: this.formData.priority ?? undefined
};
this.save.emit(request);
}
onDialogHide(): void {
// When dialog is closed via X button, treat it as cancel
this.onCancel();
}
onCancel(): void {
this.formData = null; // Clear form data
this.visible = false;
this.visibleChange.emit(false);
this.cancelEdit.emit();
}
}

View File

@@ -0,0 +1,198 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Dialog } from 'primeng/dialog';
import { Button } from 'primeng/button';
import { BookMark } from '../../../../shared/service/book-mark.service';
import { PrimeTemplate } from 'primeng/api';
@Component({
selector: 'app-bookmark-view-dialog',
standalone: true,
imports: [
CommonModule,
Dialog,
Button,
PrimeTemplate
],
template: `
<p-dialog
[(visible)]="visible"
[modal]="true"
[closable]="true"
[style]="{width: '420px', maxWidth: '95vw'}"
[draggable]="false"
[resizable]="false"
[closeOnEscape]="true"
[appendTo]="'body'"
header="View Bookmark"
(onHide)="onClose()">
@if (bookmark) {
<div class="bookmark-view-content">
<div class="bookmark-view-header">
<span class="bookmark-view-color" [style.background-color]="bookmark.color || 'var(--primary-color)'"></span>
<h3 class="bookmark-view-title">{{ bookmark.title }}</h3>
</div>
<div class="bookmark-view-details">
<div class="bookmark-view-row">
<span class="bookmark-view-label">Created</span>
<span class="bookmark-view-value">{{ bookmark.createdAt | date:'MMM d, y, h:mm a' }}</span>
</div>
<div class="bookmark-view-row">
<span class="bookmark-view-label">Priority</span>
<span class="bookmark-view-priority" [attr.data-priority]="bookmark.priority">
{{ getPriorityLabel(bookmark.priority) }}
</span>
</div>
</div>
<div class="bookmark-view-notes">
<span class="bookmark-view-label">Notes</span>
@if (bookmark.notes) {
<p class="bookmark-view-notes-content">{{ bookmark.notes }}</p>
} @else {
<p class="bookmark-view-notes-empty">No notes added</p>
}
</div>
</div>
}
<ng-template pTemplate="footer">
<p-button
label="Close"
icon="pi pi-times"
(click)="onClose()"
[text]="true"
severity="secondary">
</p-button>
</ng-template>
</p-dialog>
`,
styles: [`
.bookmark-view-content {
padding: 0.5rem 0;
}
.bookmark-view-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--surface-border);
}
.bookmark-view-color {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.bookmark-view-title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--text-color);
word-break: break-word;
}
.bookmark-view-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.bookmark-view-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.bookmark-view-label {
font-size: 0.875rem;
color: var(--text-color-secondary);
}
.bookmark-view-value {
font-size: 0.875rem;
color: var(--text-color);
}
.bookmark-view-priority {
font-size: 0.8rem;
font-weight: 500;
padding: 0.25rem 0.625rem;
border-radius: 1rem;
background: color-mix(in srgb, var(--primary-color) 15%, transparent);
color: var(--primary-color);
}
.bookmark-view-priority[data-priority="1"] {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #ef4444;
}
.bookmark-view-priority[data-priority="2"] {
background: color-mix(in srgb, #f97316 15%, transparent);
color: #f97316;
}
.bookmark-view-priority[data-priority="3"] {
background: color-mix(in srgb, var(--primary-color) 15%, transparent);
color: var(--primary-color);
}
.bookmark-view-priority[data-priority="4"],
.bookmark-view-priority[data-priority="5"] {
background: color-mix(in srgb, #6b7280 15%, transparent);
color: #6b7280;
}
.bookmark-view-notes {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.bookmark-view-notes-content {
margin: 0;
padding: 0.875rem;
background: var(--surface-ground);
border-radius: 0.5rem;
font-size: 0.875rem;
color: var(--text-color);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.bookmark-view-notes-empty {
margin: 0;
font-size: 0.875rem;
color: var(--text-color-secondary);
font-style: italic;
}
`]
})
export class BookmarkViewDialogComponent {
@Input() visible = false;
@Input() bookmark: BookMark | null = null;
@Output() visibleChange = new EventEmitter<boolean>();
onClose(): void {
this.visible = false;
this.visibleChange.emit(false);
}
getPriorityLabel(priority: number | undefined): string {
if (priority === undefined) return 'Normal';
if (priority <= 1) return 'Highest';
if (priority === 2) return 'High';
if (priority === 3) return 'Normal';
if (priority === 4) return 'Low';
return 'Lowest';
}
}

View File

@@ -12,72 +12,101 @@
<div class="progress-info">
<span class="progress-percentage"><span class="progress-label">Progress: </span>{{ progressPercentage }}%</span>
</div>
<p-drawer [(visible)]="isDrawerVisible" [modal]="false" [position]="'left'" [style]="{ width: '320px' }">
<ng-template pTemplate="header">
<div class="drawer-header">
<span class="drawer-title">Table of Contents</span>
</div>
</ng-template>
<p-drawer [(visible)]="isDrawerVisible" [modal]="false" [position]="'left'" [style]="{ width: '320px' }">
<ng-template pTemplate="header">
<div class="drawer-header">
<span class="drawer-title">Table of Contents</span>
</div>
</ng-template>
<p-tabs value="0">
<p-tablist>
<p-tab value="0">
<div class="tab-header">
<i class="pi pi-book"></i>
<span>Chapters</span>
<p-tabs value="0">
<p-tablist>
<p-tab value="0">
<div class="tab-header">
<i class="pi pi-book"></i>
<span>Chapters</span>
</div>
</p-tab>
<p-tab value="1">
<div class="tab-header">
<i class="pi pi-bookmark"></i>
<span>Bookmarks</span>
</div>
</p-tab>
</p-tablist>
<p-tabpanels>
<p-tabpanel value="0">
<div class="tab-content">
<ul class="chapter-list">
@for (chapter of chapters; track chapter) {
<li (click)="navigateToChapter(chapter); $event.stopPropagation()"
class="chapter-item"
[class.current-chapter]="chapter.href === currentChapterHref"
[style.padding-left.rem]="chapter.level * 1.5 + 0.75">
<i class="pi pi-chevron-right chapter-icon"></i>
<span class="chapter-label">{{ chapter.label }}</span>
</li>
}
</ul>
</div>
</p-tabpanel>
<p-tabpanel value="1">
<div class="tab-content">
<div class="p-2 mb-2">
<p-iconfield class="w-full">
<p-inputicon class="pi pi-search"/>
<input
pInputText
type="text"
[(ngModel)]="filterText"
placeholder="Search bookmarks..."
class="w-full p-inputtext-sm">
</p-iconfield>
</div>
@if (filteredBookmarks.length === 0) {
<div class="empty-state">
<i class="pi pi-bookmark"></i>
<p>No bookmarks found</p>
<span>{{ filterText ? 'Try a different search term' : 'Tap the bookmark icon to save your place' }}</span>
</div>
} @else {
<ul class="bookmark-list">
@for (bookmark of filteredBookmarks; track bookmark.id) {
<li class="bookmark-item" (click)="navigateToBookmark(bookmark); $event.stopPropagation()">
<i class="pi pi-bookmark-fill bookmark-icon" [style.color]="bookmark.color || 'var(--primary-color)'"></i>
<span class="bookmark-label">{{ bookmark.title }}</span>
<div class="bookmark-actions">
<button
class="bookmark-action-btn"
(click)="openViewDialog(bookmark); $event.stopPropagation()"
pTooltip="View Details"
tooltipPosition="bottom">
<i class="pi pi-info-circle"></i>
</button>
<button
class="bookmark-action-btn"
(click)="openEditBookmarkDialog(bookmark); $event.stopPropagation()"
pTooltip="Edit"
tooltipPosition="bottom">
<i class="pi pi-pencil"></i>
</button>
<button
class="bookmark-action-btn delete"
(click)="deleteBookmark(bookmark.id); $event.stopPropagation()"
pTooltip="Delete"
tooltipPosition="bottom">
<i class="pi pi-trash"></i>
</button>
</div>
</p-tab>
<p-tab value="1">
<div class="tab-header">
<i class="pi pi-bookmark"></i>
<span>Bookmarks</span>
</div>
</p-tab>
</p-tablist>
<p-tabpanels>
<p-tabpanel value="0">
<div class="tab-content">
<ul class="chapter-list">
@for (chapter of chapters; track chapter) {
<li (click)="navigateToChapter(chapter); $event.stopPropagation()"
class="chapter-item"
[class.current-chapter]="chapter.href === currentChapterHref"
[style.padding-left.rem]="chapter.level * 1.5 + 0.75">
<i class="pi pi-chevron-right chapter-icon"></i>
<span class="chapter-label">{{ chapter.label }}</span>
</li>
}
</ul>
</div>
</p-tabpanel>
<p-tabpanel value="1">
<div class="tab-content">
@if (bookmarks.length === 0) {
<div class="empty-state">
<i class="pi pi-bookmark"></i>
<p>No bookmarks yet</p>
<span>Tap the bookmark icon to save your place</span>
</div>
} @else {
<ul class="bookmark-list">
@for (bookmark of bookmarks; track bookmark.id) {
<li class="bookmark-item" (click)="navigateToBookmark(bookmark); $event.stopPropagation()">
<i class="pi pi-bookmark-fill bookmark-icon"></i>
<span class="bookmark-label">{{ bookmark.title }}</span>
<button
class="bookmark-delete"
(click)="deleteBookmark(bookmark.id); $event.stopPropagation()">
<i class="pi pi-trash"></i>
</button>
</li>
}
</ul>
}
</div>
</p-tabpanel>
</p-tabpanels>
</p-tabs>
</p-drawer>
</li>
}
</ul>
}
</div>
</p-tabpanel>
</p-tabpanels>
</p-tabs>
</p-drawer>
</div>
<div class="header-center">
@@ -268,3 +297,18 @@
</div>
}
</div>
<!-- Bookmark Edit Dialog Component -->
<app-bookmark-edit-dialog
[(visible)]="showEditBookmarkDialog"
[bookmark]="editingBookmark"
[isSaving]="isEditingBookmark"
(save)="onBookmarkSave($event)"
(cancelEdit)="onBookmarkCancel()">
</app-bookmark-edit-dialog>
<!-- Bookmark View Dialog Component -->
<app-bookmark-view-dialog
[(visible)]="viewDialogVisible"
[bookmark]="selectedBookmark">
</app-bookmark-view-dialog>

View File

@@ -308,8 +308,15 @@
font-weight: 500;
}
.bookmark-delete {
.bookmark-actions {
opacity: 0;
display: flex;
gap: 0.25rem;
margin-left: auto;
transition: opacity 0.2s ease;
}
.bookmark-action-btn {
background: transparent;
border: none;
cursor: pointer;
@@ -318,9 +325,16 @@
color: color-mix(in srgb, var(--text-color) 60%, transparent);
transition: all 0.2s ease;
flex-shrink: 0;
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: color-mix(in srgb, var(--primary-color) 15%, transparent);
color: var(--primary-color);
}
&.delete:hover {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #ef4444;
}
@@ -334,7 +348,7 @@
background-color: color-mix(in srgb, var(--primary-color) 8%, transparent);
transform: translateX(4px);
.bookmark-delete {
.bookmark-actions {
opacity: 1;
}
}
@@ -363,24 +377,7 @@
}
}
.bookmark-delete {
background: transparent;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.375rem;
color: color-mix(in srgb, var(--text-color) 60%, transparent);
transition: all 0.2s ease;
&:hover {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #ef4444;
}
i {
font-size: 0.875rem;
}
}
.empty-state {
display: flex;

View File

@@ -3,6 +3,8 @@ import ePub from 'epubjs';
import {Drawer} from 'primeng/drawer';
import {forkJoin, Subscription} from 'rxjs';
import {Button} from 'primeng/button';
import {InputText} from 'primeng/inputtext';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {ActivatedRoute} from '@angular/router';
import {Book, BookSetting} from '../../../book/model/book.model';
@@ -11,19 +13,43 @@ import {Select} from 'primeng/select';
import {UserService} from '../../../settings/user-management/user.service';
import {ProgressSpinner} from 'primeng/progressspinner';
import {MessageService, PrimeTemplate} from 'primeng/api';
import {BookMark, BookMarkService} from '../../../../shared/service/book-mark.service';
import {BookMark, BookMarkService, UpdateBookMarkRequest} from '../../../../shared/service/book-mark.service';
import {Tooltip} from 'primeng/tooltip';
import {Slider} from 'primeng/slider';
import {FALLBACK_EPUB_SETTINGS, getChapter} from '../epub-reader-helper';
import {EpubThemeUtil, EpubTheme} from '../epub-theme-util';
import {PageTitleService} from "../../../../shared/service/page-title.service";
import {Tab, TabList, TabPanel, TabPanels, Tabs} from 'primeng/tabs';
import {IconField} from 'primeng/iconfield';
import {InputIcon} from 'primeng/inputicon';
import {BookmarkEditDialogComponent} from './bookmark-edit-dialog.component';
import {BookmarkViewDialogComponent} from './bookmark-view-dialog.component';
@Component({
selector: 'app-epub-reader',
templateUrl: './epub-reader.component.html',
styleUrls: ['./epub-reader.component.scss'],
imports: [Drawer, Button, FormsModule, Select, ProgressSpinner, Tooltip, Slider, PrimeTemplate, Tabs, TabList, Tab, TabPanels, TabPanel],
imports: [
CommonModule,
FormsModule,
Drawer,
Button,
Select,
ProgressSpinner,
Tooltip,
Slider,
PrimeTemplate,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
IconField,
InputIcon,
BookmarkEditDialogComponent,
BookmarkViewDialogComponent,
InputText
],
standalone: true
})
export class EpubReaderComponent implements OnInit, OnDestroy {
@@ -40,8 +66,15 @@ export class EpubReaderComponent implements OnInit, OnDestroy {
isBookmarked = false;
isAddingBookmark = false;
isDeletingBookmark = false;
isEditingBookmark = false;
isUpdatingPosition = false;
private routeSubscription?: Subscription;
// Bookmark Filter & View
filterText = '';
viewDialogVisible = false;
selectedBookmark: BookMark | null = null;
public locationsReady = false;
public approxProgress = 0;
public exactProgress = 0;
@@ -54,6 +87,10 @@ export class EpubReaderComponent implements OnInit, OnDestroy {
private isMouseInTopRegion = false;
private headerShownByMobileTouch = false;
// Properties for bookmark editing
editingBookmark: BookMark | null = null;
showEditBookmarkDialog = false;
private book: any;
private rendition: any;
private keyListener: (e: KeyboardEvent) => void = () => {
@@ -209,6 +246,39 @@ export class EpubReaderComponent implements OnInit, OnDestroy {
});
}
get filteredBookmarks(): BookMark[] {
let filtered = this.bookmarks;
// Filter
if (this.filterText && this.filterText.trim()) {
const lowerFilter = this.filterText.toLowerCase().trim();
filtered = filtered.filter(b =>
(b.title && b.title.toLowerCase().includes(lowerFilter)) ||
(b.notes && b.notes.toLowerCase().includes(lowerFilter))
);
}
// Sort: Priority ASC (1 is high), then CreatedAt DESC
return [...filtered].sort((a, b) => {
const priorityA = a.priority ?? 3; // Default to 3 (Normal) if undefined
const priorityB = b.priority ?? 3;
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return dateB - dateA;
});
}
openViewDialog(bookmark: BookMark): void {
this.selectedBookmark = bookmark;
this.viewDialogVisible = true;
}
updateThemeStyle(): void {
this.applyCombinedTheme();
this.updateViewerSetting();
@@ -620,6 +690,8 @@ export class EpubReaderComponent implements OnInit, OnDestroy {
this.bookMarkService.createBookmark(request).subscribe({
next: (bookmark) => {
this.bookmarks.push(bookmark);
// Force array update for change detection if needed, but simple push works with getter usually if ref is stable
this.bookmarks = [...this.bookmarks];
this.updateBookmarkStatus();
this.messageService.add({
severity: 'success',
@@ -643,6 +715,11 @@ export class EpubReaderComponent implements OnInit, OnDestroy {
if (this.isDeletingBookmark) {
return;
}
// Simple confirmation using window.confirm for now, as consistent with UserManagementComponent behavior seen in linting
if (!confirm('Are you sure you want to delete this bookmark?')) {
return;
}
this.isDeletingBookmark = true;
this.bookMarkService.deleteBookmark(bookmarkId).subscribe({
next: () => {
@@ -682,4 +759,85 @@ export class EpubReaderComponent implements OnInit, OnDestroy {
? this.bookmarks.some(b => b.cfi === this.currentCfi)
: false;
}
openEditBookmarkDialog(bookmark: BookMark): void {
this.editingBookmark = { ...bookmark };
this.showEditBookmarkDialog = true;
}
onBookmarkSave(updateRequest: UpdateBookMarkRequest): void {
if (!this.editingBookmark || this.isEditingBookmark) {
return;
}
this.isEditingBookmark = true;
this.bookMarkService.updateBookmark(this.editingBookmark.id, updateRequest).subscribe({
next: (updatedBookmark) => {
const index = this.bookmarks.findIndex(b => b.id === this.editingBookmark!.id);
if (index !== -1) {
this.bookmarks[index] = updatedBookmark;
this.bookmarks = [...this.bookmarks]; // Trigger change detection for getter
}
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: 'Bookmark updated successfully',
});
this.showEditBookmarkDialog = false;
this.editingBookmark = null; // Reset the editing bookmark after successful save
this.isEditingBookmark = false;
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update bookmark',
});
this.showEditBookmarkDialog = false;
this.editingBookmark = null; // Reset the editing bookmark even on error
this.isEditingBookmark = false;
}
});
}
onBookmarkCancel(): void {
this.showEditBookmarkDialog = false;
this.editingBookmark = null; // Reset the editing bookmark when dialog is cancelled
}
updateBookmarkPosition(bookmarkId: number): void {
if (!this.currentCfi || this.isUpdatingPosition) {
return;
}
this.isUpdatingPosition = true;
const updateRequest = {
cfi: this.currentCfi
};
this.bookMarkService.updateBookmark(bookmarkId, updateRequest).subscribe({
next: (updatedBookmark) => {
const index = this.bookmarks.findIndex(b => b.id === bookmarkId);
if (index !== -1) {
this.bookmarks[index] = updatedBookmark;
this.bookmarks = [...this.bookmarks];
}
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: 'Bookmark position updated successfully',
});
this.isUpdatingPosition = false;
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update bookmark position',
});
this.isUpdatingPosition = false;
}
});
}
}

View File

@@ -5,10 +5,15 @@ import {API_CONFIG} from '../../core/config/api-config';
export interface BookMark {
id: number;
userId?: number;
bookId: number;
cfi: string;
title: string;
color?: string;
notes?: string;
priority?: number;
createdAt: string;
updatedAt?: string;
}
export interface CreateBookMarkRequest {
@@ -17,6 +22,14 @@ export interface CreateBookMarkRequest {
title?: string;
}
export interface UpdateBookMarkRequest {
title?: string;
cfi?: string;
color?: string;
notes?: string;
priority?: number;
}
@Injectable({
providedIn: 'root'
})
@@ -36,4 +49,7 @@ export class BookMarkService {
deleteBookmark(bookmarkId: number): Observable<void> {
return this.http.delete<void>(`${this.url}/${bookmarkId}`);
}
updateBookmark(bookmarkId: number, request: UpdateBookMarkRequest): Observable<BookMark> {
return this.http.put<BookMark>(`${this.url}/${bookmarkId}`, request);
}
}