From 672b7163f169045da17ac0e9a3d136a2a39f65f2 Mon Sep 17 00:00:00 2001 From: ACX <8075870+acx10@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:32:38 -0700 Subject: [PATCH] feat(reader): add fullscreen, keyboard shortcuts help, search cancel, and go-to-percentage to ebook reader (#2698) --- .../ebook-reader/core/event.service.ts | 36 ++- .../dialogs/shortcuts-help.component.html | 42 +++ .../dialogs/shortcuts-help.component.scss | 245 ++++++++++++++++++ .../dialogs/shortcuts-help.component.ts | 74 ++++++ .../ebook-reader/ebook-reader.component.html | 4 + .../ebook-reader/ebook-reader.component.ts | 76 +++++- .../layout/footer/footer.component.html | 51 ++-- .../layout/footer/footer.component.scss | 150 ++++++++--- .../layout/footer/footer.component.ts | 21 +- .../layout/header/header.component.html | 6 + .../layout/header/header.component.ts | 13 + .../layout/header/header.service.ts | 19 ++ .../layout/panel/panel.component.html | 7 +- .../layout/panel/panel.component.scss | 28 +- .../layout/panel/panel.component.ts | 5 + .../layout/panel/panel.service.ts | 8 + .../layout/sidebar/sidebar.service.ts | 8 + 17 files changed, 727 insertions(+), 66 deletions(-) create mode 100644 booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.html create mode 100644 booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.scss create mode 100644 booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.ts diff --git a/booklore-ui/src/app/features/readers/ebook-reader/core/event.service.ts b/booklore-ui/src/app/features/readers/ebook-reader/core/event.service.ts index 8f74d2e38..5294d8fdd 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/core/event.service.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/core/event.service.ts @@ -3,7 +3,7 @@ import {Subject} from 'rxjs'; import {ReaderAnnotationService} from '../features/annotations/annotation-renderer.service'; export interface ViewEvent { - type: 'load' | 'relocate' | 'error' | 'middle-single-tap' | 'draw-annotation' | 'show-annotation' | 'text-selected'; + type: 'load' | 'relocate' | 'error' | 'middle-single-tap' | 'draw-annotation' | 'show-annotation' | 'text-selected' | 'toggle-fullscreen' | 'toggle-shortcuts-help' | 'escape-pressed' | 'go-first-section' | 'go-last-section' | 'toggle-toc' | 'toggle-search' | 'toggle-notes'; detail?: any; popupPosition?: { x: number; y: number; showBelow?: boolean }; } @@ -127,12 +127,42 @@ export class ReaderEventService { return; } const k = event.key; - if (k === 'ArrowLeft' || k === 'h' || k === 'PageUp') { + if (k === 'ArrowLeft' || k === 'PageUp') { this.viewCallbacks?.prev(); event.preventDefault(); - } else if (k === 'ArrowRight' || k === 'l' || k === 'PageDown') { + } else if (k === 'ArrowRight' || k === 'PageDown') { this.viewCallbacks?.next(); event.preventDefault(); + } else if (k === ' ' && event.shiftKey) { + this.viewCallbacks?.prev(); + event.preventDefault(); + } else if (k === ' ') { + this.viewCallbacks?.next(); + event.preventDefault(); + } else if (k === 'Home') { + this.eventSubject.next({type: 'go-first-section'}); + event.preventDefault(); + } else if (k === 'End') { + this.eventSubject.next({type: 'go-last-section'}); + event.preventDefault(); + } else if (k === 'f' || k === 'F') { + this.eventSubject.next({type: 'toggle-fullscreen'}); + event.preventDefault(); + } else if (k === 't' || k === 'T') { + this.eventSubject.next({type: 'toggle-toc'}); + event.preventDefault(); + } else if (k === 's' || k === 'S') { + this.eventSubject.next({type: 'toggle-search'}); + event.preventDefault(); + } else if (k === 'n' || k === 'N') { + this.eventSubject.next({type: 'toggle-notes'}); + event.preventDefault(); + } else if (k === '?') { + this.eventSubject.next({type: 'toggle-shortcuts-help'}); + event.preventDefault(); + } else if (k === 'Escape') { + this.eventSubject.next({type: 'escape-pressed'}); + event.preventDefault(); } }; document.addEventListener('keydown', this.keydownHandler); diff --git a/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.html b/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.html new file mode 100644 index 000000000..6b39b6407 --- /dev/null +++ b/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.html @@ -0,0 +1,42 @@ +
+
+
+

Keyboard Shortcuts

+ +
+ +
+ @for (group of shortcutGroups; track group.title) { +
+

{{ group.title }}

+
+ @for (shortcut of group.shortcuts; track shortcut.description) { +
+
+ @for (key of shortcut.keys; track key; let last = $last) { + {{ key }} + @if (!last) { + + + } + } +
+
+ {{ shortcut.description }} + @if (isMobile && shortcut.mobileGesture) { + {{ shortcut.mobileGesture }} + } +
+
+ } +
+
+ } +
+ + +
+
diff --git a/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.scss b/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.scss new file mode 100644 index 000000000..63da3a7de --- /dev/null +++ b/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.scss @@ -0,0 +1,245 @@ +$dialog-bg: #1a1a1a; +$text-primary: rgba(255, 255, 255, 0.95); +$text-secondary: rgba(255, 255, 255, 0.6); +$text-muted: rgba(255, 255, 255, 0.4); +$border-color: rgba(255, 255, 255, 0.08); +$hover-bg: rgba(255, 255, 255, 0.08); +$active-color: #818cf8; +$transition-fast: 150ms ease; +$transition-normal: 200ms ease; + +.dialog-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + z-index: 13000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn $transition-normal; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.dialog { + background: $dialog-bg; + border: 1px solid $border-color; + border-radius: 12px; + width: 90%; + max-width: 480px; + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + animation: slideUp 250ms cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.dialog-header { + padding: 16px 20px; + border-bottom: 1px solid $border-color; + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%); + + h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: $text-primary; + letter-spacing: -0.3px; + } +} + +.close-btn { + background: none; + border: none; + color: $text-muted; + cursor: pointer; + padding: 6px; + border-radius: 6px; + transition: background $transition-fast, color $transition-fast; + line-height: 1; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: $hover-bg; + color: $text-primary; + } +} + +.dialog-body { + padding: 20px; + overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + gap: 24px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + } +} + +.shortcut-group { + .group-title { + margin: 0 0 12px 0; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + color: $text-muted; + } +} + +.shortcuts-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.shortcut-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + transition: background $transition-fast; + + &:hover { + background: rgba(255, 255, 255, 0.05); + } +} + +.shortcut-keys { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.key { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 26px; + padding: 0 8px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 5px; + font-family: inherit; + font-size: 12px; + font-weight: 500; + color: $text-primary; + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2); +} + +.key-separator { + color: $text-muted; + font-size: 12px; + margin: 0 2px; +} + +.shortcut-info { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + text-align: right; +} + +.shortcut-description { + font-size: 13px; + color: $text-secondary; +} + +.mobile-gesture { + font-size: 11px; + color: $active-color; + font-style: italic; +} + +.dialog-footer { + padding: 16px 20px; + border-top: 1px solid $border-color; + display: flex; + justify-content: flex-end; + background: linear-gradient(0deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%); +} + +.btn { + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background $transition-fast, transform $transition-fast; + border: none; + + &:active { + transform: scale(0.98); + } +} + +.btn-primary { + background: $active-color; + color: white; + + &:hover { + background: #9ba3fb; + } +} + +@media (max-width: 480px) { + .dialog { + width: 95%; + max-height: 90vh; + } + + .shortcut-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .shortcut-info { + align-items: flex-start; + text-align: left; + } +} diff --git a/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.ts new file mode 100644 index 000000000..d090c52c1 --- /dev/null +++ b/booklore-ui/src/app/features/readers/ebook-reader/dialogs/shortcuts-help.component.ts @@ -0,0 +1,74 @@ +import {Component, EventEmitter, Output} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ReaderIconComponent} from '../shared/icon.component'; + +interface ShortcutItem { + keys: string[]; + description: string; + mobileGesture?: string; +} + +interface ShortcutGroup { + title: string; + shortcuts: ShortcutItem[]; +} + +@Component({ + selector: 'app-ebook-shortcuts-help', + standalone: true, + imports: [CommonModule, ReaderIconComponent], + templateUrl: './shortcuts-help.component.html', + styleUrls: ['./shortcuts-help.component.scss'] +}) +export class EbookShortcutsHelpComponent { + @Output() close = new EventEmitter(); + + shortcutGroups: ShortcutGroup[] = [ + { + title: 'Navigation', + shortcuts: [ + {keys: ['←'], description: 'Previous page', mobileGesture: 'Swipe right'}, + {keys: ['→'], description: 'Next page', mobileGesture: 'Swipe left'}, + {keys: ['Space'], description: 'Next page'}, + {keys: ['Shift', 'Space'], description: 'Previous page'}, + {keys: ['Home'], description: 'First section'}, + {keys: ['End'], description: 'Last section'}, + {keys: ['Page Up'], description: 'Previous page'}, + {keys: ['Page Down'], description: 'Next page'} + ] + }, + { + title: 'Panels', + shortcuts: [ + {keys: ['T'], description: 'Table of contents'}, + {keys: ['S'], description: 'Search'}, + {keys: ['N'], description: 'Notes'} + ] + }, + { + title: 'Display', + shortcuts: [ + {keys: ['F'], description: 'Toggle fullscreen'}, + {keys: ['Escape'], description: 'Exit fullscreen / Close dialogs'} + ] + }, + { + title: 'Other', + shortcuts: [ + {keys: ['?'], description: 'Show this help dialog'} + ] + } + ]; + + isMobile = window.innerWidth < 768; + + onClose(): void { + this.close.emit(); + } + + onOverlayClick(event: MouseEvent): void { + if ((event.target as HTMLElement).classList.contains('dialog-overlay')) { + this.onClose(); + } + } +} diff --git a/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.html b/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.html index 42a01f8b7..3126f66f8 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.html +++ b/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.html @@ -82,4 +82,8 @@ (cancel)="onNoteCancel()" > } + + @if (showShortcutsHelp) { + + } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts index 154f2dc1c..8b459241b 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts @@ -29,6 +29,7 @@ import {ReaderHeaderFooterVisibilityManager} from './shared/visibility.util'; import {EpubCustomFontService} from './features/fonts/custom-font.service'; import {TextSelectionAction, TextSelectionPopupComponent} from './shared/selection-popup.component'; import {NoteDialogData, NoteDialogResult, ReaderNoteDialogComponent} from './dialogs/note-dialog.component'; +import {EbookShortcutsHelpComponent} from './dialogs/shortcuts-help.component'; @Component({ selector: 'app-ebook-reader', @@ -43,7 +44,8 @@ import {NoteDialogData, NoteDialogResult, ReaderNoteDialogComponent} from './dia ReaderLeftSidebarComponent, ReaderNavbarComponent, TextSelectionPopupComponent, - ReaderNoteDialogComponent + ReaderNoteDialogComponent, + EbookShortcutsHelpComponent ], schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [ @@ -109,6 +111,8 @@ export class EbookReaderComponent implements OnInit, OnDestroy { showNoteDialog = false; noteDialogData: NoteDialogData | null = null; + isFullscreen = false; + showShortcutsHelp = false; get currentProgressData(): any { return this.progressService.currentProgressData; @@ -151,6 +155,14 @@ export class EbookReaderComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe(() => this.showMetadata = true); + this.headerService.toggleFullscreen$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.toggleFullscreen()); + + this.headerService.showShortcutsHelp$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.showShortcutsHelp = true); + this.isLoading = true; this.initializeFoliate().pipe( switchMap(() => this.epubCustomFontService.loadAndCacheFonts()), @@ -312,6 +324,46 @@ export class EbookReaderComponent implements OnInit, OnDestroy { case 'text-selected': this.selectionService.handleTextSelected(event.detail, event.popupPosition); break; + case 'toggle-fullscreen': + this.toggleFullscreen(); + break; + case 'toggle-shortcuts-help': + this.showShortcutsHelp = !this.showShortcutsHelp; + break; + case 'go-first-section': + this.viewManager.goToSection(0).subscribe(); + break; + case 'go-last-section': { + const s = this.progressService.currentProgressData?.section; + if (s && s.total > 0) { + this.viewManager.goToSection(s.total - 1).subscribe(); + } + break; + } + case 'toggle-toc': + this.sidebarService.toggle('chapters'); + break; + case 'toggle-search': + this.leftSidebarService.toggle('search'); + break; + case 'toggle-notes': + this.leftSidebarService.toggle('notes'); + break; + case 'escape-pressed': + if (this.showShortcutsHelp) { + this.showShortcutsHelp = false; + } else if (this.showNoteDialog) { + this.noteService.closeDialog(); + } else if (this.showControls) { + this.showControls = false; + } else if (this.showQuickSettings) { + this.showQuickSettings = false; + } else if (this.showMetadata) { + this.showMetadata = false; + } else if (this.isFullscreen) { + this.exitFullscreen(); + } + break; } }); } @@ -342,6 +394,28 @@ export class EbookReaderComponent implements OnInit, OnDestroy { } } + @HostListener('document:fullscreenchange') + onFullscreenChange(): void { + this.isFullscreen = !!document.fullscreenElement; + this.headerService.setFullscreen(this.isFullscreen); + } + + toggleFullscreen(): void { + if (document.fullscreenElement) { + this.exitFullscreen(); + } else { + this.enterFullscreen(); + } + } + + private enterFullscreen(): void { + document.documentElement.requestFullscreen?.(); + } + + private exitFullscreen(): void { + document.exitFullscreen?.(); + } + onProgressChange(fraction: number): void { this.viewManager.goToFraction(fraction) .pipe(takeUntil(this.destroy$)) diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.html b/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.html index 5083701f1..89e6c0577 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.html +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.html @@ -31,7 +31,7 @@ @if (showLocationPopover) { -
+
@@ -47,41 +47,54 @@
-
-
- - {{ currentPage }} +
+
+ Chapter + {{ currentChapter }}
-
- - - / {{ locationTotal }} -
-
- - {{ sectionCurrent }} / {{ sectionTotal }} +
+
+ Section + {{ sectionCurrent }} / {{ sectionTotal }} +
+ @if (progressData?.pageItem) { +
+
+ Page + {{ currentPage }} +
+ }
-
+
} diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.scss b/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.scss index c37962ae7..1836d24bc 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.scss +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.scss @@ -189,7 +189,7 @@ $transition-normal: 200ms ease; color: $text-primary; .popover-content { - padding: 20px; + padding: 16px 20px; } .time-info { @@ -197,12 +197,12 @@ $transition-normal: 200ms ease; gap: 24px; align-items: center; justify-content: space-around; - padding: 8px 0; + padding: 4px 0; .time-section { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; text-align: center; flex: 1; @@ -232,49 +232,135 @@ $transition-normal: 200ms ease; .divider { height: 1px; background: $border-color; - margin: 16px 0; + margin: 12px 0; } - .location-controls { + .book-info { display: flex; flex-direction: column; gap: 12px; - padding: 4px 0; - .control-row { + .chapter-info { display: flex; + flex-direction: column; align-items: center; - gap: 16px; + gap: 4px; + text-align: center; - label { - min-width: 70px; - font-size: 13px; - font-weight: 500; - color: $text-secondary; + .label { + font-size: 11px; + color: $text-muted; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; } - input { - width: 65px; - padding: 6px 10px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid $border-color; - border-radius: 6px; + .chapter-name { + font-size: 14px; + font-weight: 500; color: $text-primary; - font-size: 13px; - text-align: right; - transition: background $transition-fast, border-color $transition-fast; - font-variant-numeric: tabular-nums; + line-height: 1.3; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + } - &:focus { - outline: none; - background: $hover-bg; - border-color: $active-color; + .info-row { + display: flex; + align-items: center; + justify-content: center; + gap: 24px; + + .info-column { + display: flex; + flex-direction: column; + gap: 4px; + text-align: center; + + .label { + font-size: 11px; + color: $text-muted; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; + } + + .value { + font-size: 18px; + font-weight: 600; + letter-spacing: -0.3px; + color: $text-primary; } } - span { - font-size: 13px; - color: $text-secondary; + .info-separator { + width: 1px; + height: 36px; + background: $border-color; + } + } + } + + .nav-section { + display: flex; + flex-direction: column; + gap: 10px; + } + + .goto-row { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + + label { + font-size: 13px; + font-weight: 500; + color: $text-secondary; + } + + input { + width: 65px; + padding: 6px 10px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid $border-color; + border-radius: 6px; + color: $text-primary; + font-size: 13px; + text-align: right; + transition: background $transition-fast, border-color $transition-fast; + font-variant-numeric: tabular-nums; + + &:focus { + outline: none; + background: $hover-bg; + border-color: $active-color; + } + } + + span { + font-size: 13px; + color: $text-secondary; + } + + .go-btn { + padding: 7px 18px; + background: $active-color; + border: none; + border-radius: 6px; + color: white; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background $transition-fast; + flex-shrink: 0; + + &:hover { + background: #9ba3fb; } } } @@ -294,8 +380,8 @@ $transition-normal: 200ms ease; border-radius: 8px; cursor: pointer; transition: background $transition-fast, color $transition-fast, transform $transition-fast; - width: 40px; - height: 40px; + width: 44px; + height: 44px; display: flex; align-items: center; justify-content: center; diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.ts index f92693ed7..ee47a2291 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/footer/footer.component.ts @@ -88,14 +88,6 @@ export class ReaderNavbarComponent { return this.formatDuration((this.progressData?.time.section ?? 0) * 60); } - get locationCurrent(): number { - return this.progressData?.location.current ?? 0; - } - - get locationTotal(): number { - return this.progressData?.location.total ?? 0; - } - get sectionCurrent(): number { return this.progressData?.section.current ?? 0; } @@ -104,8 +96,12 @@ export class ReaderNavbarComponent { return this.progressData?.section.total ?? 0; } + get currentChapter(): string { + return this.progressData?.tocItem?.label ?? ''; + } + get currentPage(): string { - return this.progressData?.pageItem?.label ?? 'N/A'; + return this.progressData?.pageItem?.label ?? ''; } get navbarVisible(): boolean { @@ -128,6 +124,13 @@ export class ReaderNavbarComponent { this.progressChange.emit(fraction); } + onGoToPercentage(value: string): void { + const percentage = parseFloat(value); + if (isNaN(percentage) || percentage < 0 || percentage > 100) return; + const fraction = percentage / 100; + this.progressChange.emit(fraction); + } + onFirstSection() { this.managerService.goToSection(0).subscribe(); } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.html b/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.html index c6bc17f0a..e850103b2 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.html +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.html @@ -12,9 +12,15 @@ + + diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.ts index 6246a0895..8222a1496 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.ts @@ -19,6 +19,7 @@ export class ReaderHeaderComponent implements OnInit, OnDestroy { isVisible = false; isCurrentCfiBookmarked = false; + isFullscreen = false; get bookTitle(): string { return this.headerService.title; @@ -36,6 +37,10 @@ export class ReaderHeaderComponent implements OnInit, OnDestroy { this.headerService.isCurrentCfiBookmarked$ .pipe(takeUntil(this.destroy$)) .subscribe(bookmarked => this.isCurrentCfiBookmarked = bookmarked); + + this.headerService.fullscreenState$ + .pipe(takeUntil(this.destroy$)) + .subscribe(fs => this.isFullscreen = fs); } ngOnDestroy(): void { @@ -63,6 +68,14 @@ export class ReaderHeaderComponent implements OnInit, OnDestroy { this.headerService.openControls(); } + onToggleFullscreen(): void { + this.headerService.toggleFullscreen(); + } + + onShowHelp(): void { + this.headerService.showShortcutsHelp(); + } + onClose(): void { if (window.history.length <= 2) { this.router.navigate(['/dashboard']); diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.service.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.service.ts index 17adc2e0b..3b7f4ce66 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.service.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.service.ts @@ -28,8 +28,14 @@ export class ReaderHeaderService { private _showControls = new Subject(); private _showMetadata = new Subject(); + private _toggleFullscreen = new Subject(); + private _showShortcutsHelp = new Subject(); + private _isFullscreen = new BehaviorSubject(false); showControls$ = this._showControls.asObservable(); showMetadata$ = this._showMetadata.asObservable(); + toggleFullscreen$ = this._toggleFullscreen.asObservable(); + showShortcutsHelp$ = this._showShortcutsHelp.asObservable(); + fullscreenState$ = this._isFullscreen.asObservable(); get currentState() { return this.stateService.currentState; @@ -77,6 +83,18 @@ export class ReaderHeaderService { this._showMetadata.next(); } + toggleFullscreen(): void { + this._toggleFullscreen.next(); + } + + setFullscreen(isFullscreen: boolean): void { + this._isFullscreen.next(isFullscreen); + } + + showShortcutsHelp(): void { + this._showShortcutsHelp.next(); + } + close(): void { this.location.back(); } @@ -117,6 +135,7 @@ export class ReaderHeaderService { reset(): void { this._forceVisible.next(false); this._isCurrentCfiBookmarked.next(false); + this._isFullscreen.next(false); this.bookTitle = ''; } } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.html b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.html index 31fbc4480..2cc44354c 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.html +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.html @@ -63,7 +63,12 @@
- Searching... {{ (searchState.progress * 100) | number:'1.0-0' }}% +
+ Searching... {{ (searchState.progress * 100) | number:'1.0-0' }}% + +
} diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.scss b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.scss index 119949232..3af0f8eeb 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.scss +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.scss @@ -452,10 +452,36 @@ $transition-normal: 200ms ease; } } + .progress-info { + display: flex; + align-items: center; + justify-content: space-between; + } + .progress-text { font-size: 11px; color: $text-muted; - text-align: center; + } + + .cancel-search-btn { + width: 22px; + height: 22px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.08); + border: none; + border-radius: 50%; + color: $text-muted; + cursor: pointer; + transition: background $transition-fast, color $transition-fast; + flex-shrink: 0; + + &:hover { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; + } } } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts index 299b2eaee..56dc7a052 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts @@ -128,4 +128,9 @@ export class ReaderLeftSidebarComponent implements OnInit, OnDestroy { this.searchQuery = ''; this.leftSidebarService.clearSearch(); } + + onCancelSearch(): void { + this.searchQuery = ''; + this.leftSidebarService.clearSearch(); + } } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.service.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.service.ts index 2eaab9611..c72bfc7dd 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.service.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.service.ts @@ -64,6 +64,14 @@ export class ReaderLeftSidebarService { this._isOpen.next(false); } + toggle(tab?: LeftSidebarTab): void { + if (this._isOpen.value && (!tab || this._activeTab.value === tab)) { + this.close(); + } else { + this.open(tab); + } + } + setActiveTab(tab: LeftSidebarTab): void { this._activeTab.next(tab); } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.service.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.service.ts index cff396828..81eb02964 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.service.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.service.ts @@ -151,6 +151,14 @@ export class ReaderSidebarService { this._isOpen.next(false); } + toggle(tab?: SidebarTab): void { + if (this._isOpen.value && (!tab || this._activeTab.value === tab)) { + this.close(); + } else { + this.open(tab); + } + } + setActiveTab(tab: SidebarTab): void { this._activeTab.next(tab); }