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