From 5183b0ddfc7d01756f8187b471127e1212139b20 Mon Sep 17 00:00:00 2001 From: ACX <8075870+acx10@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:28:02 -0700 Subject: [PATCH] feat(cbx-reader): add fullscreen, slideshow, RTL, long strip mode and keyboard shortcuts (#2632) --- .../cbx-reader/cbx-reader.component.html | 28 +- .../cbx-reader/cbx-reader.component.scss | 108 ++++++ .../cbx-reader/cbx-reader.component.ts | 363 +++++++++++++++--- .../dialogs/cbx-shortcuts-help.component.html | 42 ++ .../dialogs/cbx-shortcuts-help.component.scss | 245 ++++++++++++ .../dialogs/cbx-shortcuts-help.component.ts | 73 ++++ .../layout/header/cbx-header.component.html | 9 + .../layout/header/cbx-header.component.ts | 22 +- .../layout/header/cbx-header.service.ts | 41 ++ .../cbx-quick-settings.component.html | 44 ++- .../cbx-quick-settings.component.scss | 18 +- .../cbx-quick-settings.component.ts | 52 ++- .../cbx-quick-settings.service.ts | 34 +- .../ebook-reader/shared/icon.component.ts | 47 ++- .../settings/user-management/user.service.ts | 18 +- 15 files changed, 1075 insertions(+), 69 deletions(-) create mode 100644 booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.html create mode 100644 booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.scss create mode 100644 booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.ts diff --git a/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.html b/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.html index a970aed00..bbd2e5d1e 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.html +++ b/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.html @@ -1,4 +1,7 @@ -
+
@@ -23,9 +26,22 @@ > } + @if (showShortcutsHelp) { + + } + + @if (isSlideshowActive) { +
+ Slideshow +
+ } +
0) { @if (scrollMode === CbxScrollMode.PAGINATED) { -
+
@if (isPageTransitioning && previousImageUrls.length > 0) {
@for (url of previousImageUrls; track url) { @@ -47,8 +63,8 @@
}
- @for (url of currentImageUrls; track url) { - Page Image + @for (url of currentImageUrls; track url; let i = $index) { + Page Image }
@@ -62,9 +78,9 @@
} } @else { -
+
@for (url of infiniteScrollImageUrls; track url; let i = $index) { - Page {{ infiniteScrollPages[i] + 1 }} + Page {{ infiniteScrollPages[i] + 1 }} } @if (isLoadingMore) {
diff --git a/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.scss b/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.scss index 8905fae3d..dbbec9a53 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.scss +++ b/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.scss @@ -17,6 +17,46 @@ outline: none; } + // RTL reading mode - reverse page order in two-page view + &.rtl-reading { + .two-page-view { + .pages-wrapper, + .previous-page-layer, + .current-page-layer { + flex-direction: row-reverse; + } + } + } + + // Slideshow indicator + .slideshow-indicator { + position: absolute; + top: 60px; + left: 50%; + transform: translateX(-50%); + z-index: 11; + pointer-events: none; + + .slideshow-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: rgba(74, 144, 226, 0.9); + border-radius: 16px; + font-size: 12px; + font-weight: 500; + color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + animation: pulse 2s ease-in-out infinite; + } + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + .bookmark-indicator { position: absolute; top: 0; @@ -358,6 +398,74 @@ } } + // Long Strip / Webtoon mode - no gaps between pages + &.long-strip { + overflow-y: auto; + overflow-x: hidden; + align-items: flex-start; + touch-action: pan-y; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + + .long-strip-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + width: 100%; + padding: 0; + } + + .page-image { + box-shadow: none; + border-radius: 0; + transition: none; + display: block; + + &:hover { + transform: none; + } + } + + &.fit-width .page-image { + width: 100%; + height: auto; + } + + &.fit-height .page-image { + width: auto; + height: auto; + max-width: 100%; + } + + &.fit-actual-size { + overflow-x: auto; + + .long-strip-wrapper { + width: max-content; + min-width: 100%; + } + + .page-image { + width: auto; + height: auto; + } + } + + &.fit-page .page-image, + &.fit-auto .page-image { + max-width: 100%; + width: auto; + height: auto; + } + + .loading-more { + padding: 2rem; + display: flex; + justify-content: center; + } + } + .no-pages { text-align: center; color: #999; diff --git a/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.ts b/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.ts index 70af178e6..069e701d0 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.ts +++ b/booklore-ui/src/app/features/readers/cbx-reader/cbx-reader.component.ts @@ -6,7 +6,7 @@ import {filter, first, map, switchMap, takeUntil, timeout} from 'rxjs/operators' import {PageTitleService} from "../../../shared/service/page-title.service"; import {CbxReaderService} from '../../book/service/cbx-reader.service'; import {BookService} from '../../book/service/book.service'; -import {CbxBackgroundColor, CbxFitMode, CbxPageSpread, CbxPageViewMode, CbxScrollMode, PdfBackgroundColor, PdfFitMode, PdfPageSpread, PdfPageViewMode, PdfScrollMode, UserService} from '../../settings/user-management/user.service'; +import {CbxBackgroundColor, CbxFitMode, CbxPageSpread, CbxPageViewMode, CbxScrollMode, CbxReadingDirection, CbxSlideshowInterval, PdfBackgroundColor, PdfFitMode, PdfPageSpread, PdfPageViewMode, PdfScrollMode, UserService} from '../../settings/user-management/user.service'; import {MessageService} from 'primeng/api'; import {Book, BookSetting, BookType} from '../../book/model/book.model'; import {BookState} from '../../book/model/state/book-state.model'; @@ -25,6 +25,7 @@ import {CbxFooterService} from './layout/footer/cbx-footer.service'; import {CbxQuickSettingsComponent} from './layout/quick-settings/cbx-quick-settings.component'; import {CbxQuickSettingsService} from './layout/quick-settings/cbx-quick-settings.service'; import {CbxNoteDialogComponent, CbxNoteDialogData, CbxNoteDialogResult} from './dialogs/cbx-note-dialog.component'; +import {CbxShortcutsHelpComponent} from './dialogs/cbx-shortcuts-help.component'; import {BookNoteV2} from '../../../shared/service/book-note-v2.service'; @@ -39,7 +40,8 @@ import {BookNoteV2} from '../../../shared/service/book-note-v2.service'; CbxSidebarComponent, CbxFooterComponent, CbxQuickSettingsComponent, - CbxNoteDialogComponent + CbxNoteDialogComponent, + CbxShortcutsHelpComponent ], providers: [ CbxHeaderService, @@ -92,6 +94,30 @@ export class CbxReaderComponent implements OnInit, OnDestroy { noteDialogData: CbxNoteDialogData | null = null; private editingNote: BookNoteV2 | null = null; + // Fullscreen state + isFullscreen = false; + + // Reading direction + readingDirection: CbxReadingDirection = CbxReadingDirection.LTR; + + // Slideshow state + isSlideshowActive = false; + slideshowInterval: CbxSlideshowInterval = CbxSlideshowInterval.FIVE_SECONDS; + private slideshowTimer: ReturnType | null = null; + + // Double-tap zoom + private lastTapTime = 0; + private originalFitMode: CbxFitMode | PdfFitMode | null = null; + + // Shortcuts help dialog + showShortcutsHelp = false; + + // Double page detection + private pageDimensionsCache = new Map(); + + protected readonly CbxReadingDirection = CbxReadingDirection; + protected readonly CbxSlideshowInterval = CbxSlideshowInterval; + private route = inject(ActivatedRoute); private router = inject(Router); private cbxReaderService = inject(CbxReaderService); @@ -276,6 +302,24 @@ export class CbxReaderComponent implements OnInit, OnDestroy { .subscribe(() => { this.openNoteDialog(); }); + + this.headerService.toggleFullscreen$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.toggleFullscreen(); + }); + + this.headerService.toggleSlideshow$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.toggleSlideshow(); + }); + + this.headerService.showShortcutsHelp$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.showShortcutsHelp = true; + }); } private subscribeToSidebarEvents(): void { @@ -364,6 +408,14 @@ export class CbxReaderComponent implements OnInit, OnDestroy { this.quickSettingsService.backgroundColorChange$ .pipe(takeUntil(this.destroy$)) .subscribe(color => this.onBackgroundColorChange(color)); + + this.quickSettingsService.readingDirectionChange$ + .pipe(takeUntil(this.destroy$)) + .subscribe(direction => this.onReadingDirectionChange(direction)); + + this.quickSettingsService.slideshowIntervalChange$ + .pipe(takeUntil(this.destroy$)) + .subscribe(interval => this.onSlideshowIntervalChange(interval)); } private updateServiceStates(): void { @@ -381,7 +433,14 @@ export class CbxReaderComponent implements OnInit, OnDestroy { scrollMode: this.scrollMode, pageViewMode: this.pageViewMode, pageSpread: this.pageSpread, - backgroundColor: this.backgroundColor + backgroundColor: this.backgroundColor, + readingDirection: this.readingDirection, + slideshowInterval: this.slideshowInterval + }); + + this.headerService.updateState({ + isFullscreen: this.isFullscreen, + isSlideshowActive: this.isSlideshowActive }); this.sidebarService.setCurrentPage(this.currentPage + 1); @@ -411,7 +470,7 @@ export class CbxReaderComponent implements OnInit, OnDestroy { } private preloadAdjacentPages(): void { - if (!this.pages.length || this.scrollMode === CbxScrollMode.INFINITE) return; + if (!this.pages.length || this.scrollMode === CbxScrollMode.INFINITE || this.scrollMode === CbxScrollMode.LONG_STRIP) return; const pagesToPreload: number[] = []; @@ -460,7 +519,7 @@ export class CbxReaderComponent implements OnInit, OnDestroy { } private transitionToNewPage(): void { - if (this.scrollMode === CbxScrollMode.INFINITE) { + if (this.scrollMode === CbxScrollMode.INFINITE || this.scrollMode === CbxScrollMode.LONG_STRIP) { this.updateCurrentImageUrls(); return; } @@ -556,43 +615,23 @@ export class CbxReaderComponent implements OnInit, OnDestroy { } nextPage() { - const previousPage = this.currentPage; - - if (this.scrollMode === CbxScrollMode.INFINITE) { - if (this.currentPage < this.pages.length - 1) { - this.currentPage++; - this.scrollToPage(this.currentPage); - this.updateProgress(); - this.updateSessionProgress(); - this.updateFooterPage(); - } - return; - } - - if (this.isTwoPageView) { - if (this.currentPage + 2 < this.pages.length) { - this.currentPage += 2; - } else if (this.currentPage + 1 < this.pages.length) { - this.currentPage += 1; - } - } else if (this.currentPage < this.pages.length - 1) { - this.currentPage++; - } - - if (this.currentPage !== previousPage) { - this.transitionToNewPage(); - this.updateProgress(); - this.updateSessionProgress(); - this.updateFooterPage(); - } + this.pauseSlideshowOnInteraction(); + this.advancePage(1); } previousPage() { - const previousPage = this.currentPage; + this.pauseSlideshowOnInteraction(); + this.advancePage(-1); + } - if (this.scrollMode === CbxScrollMode.INFINITE) { - if (this.currentPage > 0) { - this.currentPage--; + private advancePage(direction: 1 | -1): void { + const previousPage = this.currentPage; + const step = this.getPageStep(); + + if (this.scrollMode === CbxScrollMode.INFINITE || this.scrollMode === CbxScrollMode.LONG_STRIP) { + const newPage = this.currentPage + direction; + if (newPage >= 0 && newPage < this.pages.length) { + this.currentPage = newPage; this.scrollToPage(this.currentPage); this.updateProgress(); this.updateSessionProgress(); @@ -601,10 +640,25 @@ export class CbxReaderComponent implements OnInit, OnDestroy { return; } - if (this.isTwoPageView) { - this.currentPage = Math.max(0, this.currentPage - 2); + if (direction > 0) { + // Forward navigation + if (this.isTwoPageView) { + const effectiveStep = this.shouldShowSinglePage(this.currentPage) ? 1 : step; + if (this.currentPage + effectiveStep < this.pages.length) { + this.currentPage += effectiveStep; + } else if (this.currentPage + 1 < this.pages.length) { + this.currentPage += 1; + } + } else if (this.currentPage < this.pages.length - 1) { + this.currentPage++; + } } else { - this.currentPage = Math.max(0, this.currentPage - 1); + // Backward navigation + if (this.isTwoPageView) { + this.currentPage = Math.max(0, this.currentPage - step); + } else { + this.currentPage = Math.max(0, this.currentPage - 1); + } } if (this.currentPage !== previousPage) { @@ -613,6 +667,15 @@ export class CbxReaderComponent implements OnInit, OnDestroy { this.updateSessionProgress(); this.updateFooterPage(); } + + // Stop slideshow at last page + if (this.isSlideshowActive && this.currentPage >= this.pages.length - 1) { + this.stopSlideshow(); + } + } + + private getPageStep(): number { + return this.isTwoPageView ? 2 : 1; } private alignCurrentPageToParity() { @@ -646,7 +709,7 @@ export class CbxReaderComponent implements OnInit, OnDestroy { this.quickSettingsService.setScrollMode(mode); this.updateViewerSetting(); - if (this.scrollMode === CbxScrollMode.INFINITE) { + if (this.scrollMode === CbxScrollMode.INFINITE || this.scrollMode === CbxScrollMode.LONG_STRIP) { this.initializeInfiniteScroll(); setTimeout(() => this.scrollToPage(this.currentPage), 100); } else { @@ -690,7 +753,7 @@ export class CbxReaderComponent implements OnInit, OnDestroy { } onScroll(event: Event): void { - if (this.scrollMode !== CbxScrollMode.INFINITE || this.isLoadingMore) return; + if ((this.scrollMode !== CbxScrollMode.INFINITE && this.scrollMode !== CbxScrollMode.LONG_STRIP) || this.isLoadingMore) return; const container = event.target as HTMLElement; const scrollPosition = container.scrollTop + container.clientHeight; @@ -864,8 +927,80 @@ export class CbxReaderComponent implements OnInit, OnDestroy { @HostListener('window:keydown', ['$event']) handleKeyDown(event: KeyboardEvent) { - if (event.key === 'ArrowRight') this.nextPage(); - else if (event.key === 'ArrowLeft') this.previousPage(); + // Ignore if typing in input/textarea + const target = event.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { + return; + } + + const isRtl = this.readingDirection === CbxReadingDirection.RTL; + + switch (event.key) { + case 'ArrowRight': + isRtl ? this.previousPage() : this.nextPage(); + event.preventDefault(); + break; + case 'ArrowLeft': + isRtl ? this.nextPage() : this.previousPage(); + event.preventDefault(); + break; + case ' ': + event.preventDefault(); + event.shiftKey ? this.previousPage() : this.nextPage(); + break; + case 'Home': + event.preventDefault(); + this.firstPage(); + break; + case 'End': + event.preventDefault(); + this.lastPage(); + break; + case 'PageUp': + event.preventDefault(); + this.previousPage(); + break; + case 'PageDown': + event.preventDefault(); + this.nextPage(); + break; + case 'f': + case 'F': + event.preventDefault(); + this.toggleFullscreen(); + break; + case 'd': + case 'D': + event.preventDefault(); + this.toggleReadingDirection(); + break; + case 'p': + case 'P': + event.preventDefault(); + this.toggleSlideshow(); + break; + case '?': + event.preventDefault(); + this.showShortcutsHelp = true; + break; + case 'Escape': + if (this.showShortcutsHelp) { + this.showShortcutsHelp = false; + } else if (this.showNoteDialog) { + this.showNoteDialog = false; + } else if (this.showQuickSettings) { + this.quickSettingsService.close(); + } else if (this.isFullscreen) { + this.exitFullscreen(); + } + break; + } + } + + @HostListener('document:fullscreenchange') + onFullscreenChange(): void { + this.isFullscreen = !!document.fullscreenElement; + this.headerService.updateState({isFullscreen: this.isFullscreen, isSlideshowActive: this.isSlideshowActive}); } @HostListener('touchstart', ['$event']) @@ -897,7 +1032,12 @@ export class CbxReaderComponent implements OnInit, OnDestroy { private handleSwipeGesture() { const delta = this.touchEndX - this.touchStartX; - if (Math.abs(delta) >= 50) delta < 0 ? this.nextPage() : this.previousPage(); + if (Math.abs(delta) >= 50) { + // In RTL mode, swipe directions are reversed + const isRtl = this.readingDirection === CbxReadingDirection.RTL; + const shouldGoNext = isRtl ? delta > 0 : delta < 0; + shouldGoNext ? this.nextPage() : this.previousPage(); + } } private enforcePortraitSinglePageView() { @@ -996,7 +1136,138 @@ export class CbxReaderComponent implements OnInit, OnDestroy { return parts.join(' - '); } + // Fullscreen methods + toggleFullscreen(): void { + if (this.isFullscreen) { + this.exitFullscreen(); + } else { + this.enterFullscreen(); + } + } + + private enterFullscreen(): void { + const elem = document.documentElement; + if (elem.requestFullscreen) { + elem.requestFullscreen().catch(() => {}); + } + } + + private exitFullscreen(): void { + if (document.exitFullscreen) { + document.exitFullscreen().catch(() => {}); + } + } + + // Reading direction methods + toggleReadingDirection(): void { + const newDirection = this.readingDirection === CbxReadingDirection.LTR + ? CbxReadingDirection.RTL + : CbxReadingDirection.LTR; + this.onReadingDirectionChange(newDirection); + } + + onReadingDirectionChange(direction: CbxReadingDirection): void { + this.readingDirection = direction; + this.quickSettingsService.setReadingDirection(direction); + } + + // Slideshow methods + toggleSlideshow(): void { + if (this.isSlideshowActive) { + this.stopSlideshow(); + } else { + this.startSlideshow(); + } + } + + startSlideshow(): void { + if (this.currentPage >= this.pages.length - 1) return; + + this.isSlideshowActive = true; + this.headerService.updateState({isFullscreen: this.isFullscreen, isSlideshowActive: true}); + + this.slideshowTimer = setInterval(() => { + if (this.currentPage < this.pages.length - 1) { + this.advancePage(1); + } else { + this.stopSlideshow(); + } + }, this.slideshowInterval); + } + + stopSlideshow(): void { + if (this.slideshowTimer) { + clearInterval(this.slideshowTimer); + this.slideshowTimer = null; + } + this.isSlideshowActive = false; + this.headerService.updateState({isFullscreen: this.isFullscreen, isSlideshowActive: false}); + } + + private pauseSlideshowOnInteraction(): void { + if (this.isSlideshowActive) { + this.stopSlideshow(); + } + } + + onSlideshowIntervalChange(interval: CbxSlideshowInterval): void { + this.slideshowInterval = interval; + this.quickSettingsService.setSlideshowInterval(interval); + + // Restart slideshow with new interval if active + if (this.isSlideshowActive) { + this.stopSlideshow(); + this.startSlideshow(); + } + } + + // Double-tap zoom + onImageDoubleClick(): void { + if (this.originalFitMode === null) { + // Store current fit mode and switch to actual size + this.originalFitMode = this.fitMode; + this.onFitModeChange(CbxFitMode.ACTUAL_SIZE); + } else { + // Restore original fit mode + this.onFitModeChange(this.originalFitMode as CbxFitMode); + this.originalFitMode = null; + } + } + + // Double page detection + onPageImageLoad(event: Event, pageIndex: number): void { + const img = event.target as HTMLImageElement; + if (img.naturalWidth && img.naturalHeight) { + this.pageDimensionsCache.set(pageIndex, { + width: img.naturalWidth, + height: img.naturalHeight + }); + } + this.imagesLoaded = true; + } + + isSpreadPage(pageIndex: number): boolean { + const dims = this.pageDimensionsCache.get(pageIndex); + if (!dims) return false; + return dims.width > dims.height * 1.5; + } + + shouldShowSinglePage(pageIndex: number): boolean { + return this.isTwoPageView && this.isSpreadPage(pageIndex); + } + + // Shortcuts help dialog + onShortcutsHelpClose(): void { + this.showShortcutsHelp = false; + } + + // Long strip mode check + get isLongStripMode(): boolean { + return this.scrollMode === CbxScrollMode.LONG_STRIP; + } + ngOnDestroy(): void { + this.stopSlideshow(); this.endReadingSession(); this.destroy$.next(); this.destroy$.complete(); diff --git a/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.html b/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.html new file mode 100644 index 000000000..6b39b6407 --- /dev/null +++ b/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-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/cbx-reader/dialogs/cbx-shortcuts-help.component.scss b/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.scss new file mode 100644 index 000000000..a932f5308 --- /dev/null +++ b/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-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: #4a90e2; +$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: #5a9ee6; + } +} + +@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/cbx-reader/dialogs/cbx-shortcuts-help.component.ts b/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.ts new file mode 100644 index 000000000..3bb742dd0 --- /dev/null +++ b/booklore-ui/src/app/features/readers/cbx-reader/dialogs/cbx-shortcuts-help.component.ts @@ -0,0 +1,73 @@ +import {Component, EventEmitter, Output} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ReaderIconComponent} from '../../ebook-reader/shared/icon.component'; + +interface ShortcutItem { + keys: string[]; + description: string; + mobileGesture?: string; +} + +interface ShortcutGroup { + title: string; + shortcuts: ShortcutItem[]; +} + +@Component({ + selector: 'app-cbx-shortcuts-help', + standalone: true, + imports: [CommonModule, ReaderIconComponent], + templateUrl: './cbx-shortcuts-help.component.html', + styleUrls: ['./cbx-shortcuts-help.component.scss'] +}) +export class CbxShortcutsHelpComponent { + @Output() close = new EventEmitter(); + + shortcutGroups: ShortcutGroup[] = [ + { + title: 'Navigation', + shortcuts: [ + {keys: ['←', '→'], description: 'Previous / Next page', mobileGesture: 'Swipe left/right'}, + {keys: ['Space'], description: 'Next page'}, + {keys: ['Shift', 'Space'], description: 'Previous page'}, + {keys: ['Home'], description: 'First page'}, + {keys: ['End'], description: 'Last page'}, + {keys: ['Page Up'], description: 'Previous page'}, + {keys: ['Page Down'], description: 'Next page'} + ] + }, + { + title: 'Display', + shortcuts: [ + {keys: ['F'], description: 'Toggle fullscreen'}, + {keys: ['D'], description: 'Toggle reading direction (LTR/RTL)'}, + {keys: ['Escape'], description: 'Exit fullscreen / Close dialogs'}, + {keys: ['Double-click'], description: 'Toggle zoom (fit page / actual size)', mobileGesture: 'Double-tap'} + ] + }, + { + title: 'Playback', + shortcuts: [ + {keys: ['P'], description: 'Toggle slideshow / auto-play'} + ] + }, + { + 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/cbx-reader/layout/header/cbx-header.component.html b/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.html index 27eb281cb..5d633efb4 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.html +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.html @@ -15,6 +15,15 @@
{{ bookTitle }}
+ + + diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.ts b/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.ts index b901081e9..ad8d33287 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.ts +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.component.ts @@ -1,7 +1,7 @@ import {Component, inject, Input, OnDestroy, OnInit} from '@angular/core'; import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; -import {CbxHeaderService} from './cbx-header.service'; +import {CbxHeaderService, CbxHeaderState} from './cbx-header.service'; import {ReaderIconComponent} from '../../../ebook-reader'; import {CommonModule} from '@angular/common'; @@ -20,6 +20,10 @@ export class CbxHeaderComponent implements OnInit, OnDestroy { @Input() currentPageHasNotes = false; isVisible = true; + state: CbxHeaderState = { + isFullscreen: false, + isSlideshowActive: false + }; get bookTitle(): string { return this.headerService.title; @@ -29,6 +33,10 @@ export class CbxHeaderComponent implements OnInit, OnDestroy { this.headerService.forceVisible$ .pipe(takeUntil(this.destroy$)) .subscribe(visible => this.isVisible = visible); + + this.headerService.state$ + .pipe(takeUntil(this.destroy$)) + .subscribe(state => this.state = state); } ngOnDestroy(): void { @@ -52,6 +60,18 @@ export class CbxHeaderComponent implements OnInit, OnDestroy { this.headerService.openNoteDialog(); } + onToggleFullscreen(): void { + this.headerService.toggleFullscreen(); + } + + onToggleSlideshow(): void { + this.headerService.toggleSlideshow(); + } + + onShowShortcutsHelp(): void { + this.headerService.showShortcutsHelp(); + } + onClose(): void { this.headerService.close(); } diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.service.ts b/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.service.ts index 83cb677db..2e91195e3 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.service.ts +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/header/cbx-header.service.ts @@ -3,6 +3,11 @@ import {Location} from '@angular/common'; import {BehaviorSubject, Subject} from 'rxjs'; import {CbxSidebarService} from '../sidebar/cbx-sidebar.service'; +export interface CbxHeaderState { + isFullscreen: boolean; + isSlideshowActive: boolean; +} + @Injectable() export class CbxHeaderService { private sidebarService = inject(CbxSidebarService); @@ -15,6 +20,12 @@ export class CbxHeaderService { private _forceVisible = new BehaviorSubject(true); forceVisible$ = this._forceVisible.asObservable(); + private _state = new BehaviorSubject({ + isFullscreen: false, + isSlideshowActive: false + }); + state$ = this._state.asObservable(); + private _showQuickSettings = new Subject(); showQuickSettings$ = this._showQuickSettings.asObservable(); @@ -24,6 +35,15 @@ export class CbxHeaderService { private _openNoteDialog = new Subject(); openNoteDialog$ = this._openNoteDialog.asObservable(); + private _toggleFullscreen = new Subject(); + toggleFullscreen$ = this._toggleFullscreen.asObservable(); + + private _toggleSlideshow = new Subject(); + toggleSlideshow$ = this._toggleSlideshow.asObservable(); + + private _showShortcutsHelp = new Subject(); + showShortcutsHelp$ = this._showShortcutsHelp.asObservable(); + get title(): string { return this.bookTitle; } @@ -32,6 +52,10 @@ export class CbxHeaderService { return this._forceVisible.value; } + get state(): CbxHeaderState { + return this._state.value; + } + initialize(bookId: number, title: string | undefined, destroy$: Subject): void { this.bookId = bookId; this.bookTitle = title || ''; @@ -42,6 +66,10 @@ export class CbxHeaderService { this._forceVisible.next(visible); } + updateState(partial: Partial): void { + this._state.next({...this._state.value, ...partial}); + } + openSidebar(): void { this.sidebarService.open(); } @@ -58,12 +86,25 @@ export class CbxHeaderService { this._openNoteDialog.next(); } + toggleFullscreen(): void { + this._toggleFullscreen.next(); + } + + toggleSlideshow(): void { + this._toggleSlideshow.next(); + } + + showShortcutsHelp(): void { + this._showShortcutsHelp.next(); + } + close(): void { this.location.back(); } reset(): void { this._forceVisible.next(true); + this._state.next({isFullscreen: false, isSlideshowActive: false}); this.bookTitle = ''; } } diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.html b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.html index 528508426..b0efbac05 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.html +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.html @@ -24,10 +24,17 @@
- {{ state.scrollMode === CbxScrollMode.PAGINATED ? 'Paginated' : 'Infinite' }} - +
+ @for (option of scrollModeOptions; track option.value) { + + } +
@@ -57,6 +64,35 @@
} + +
+ +
+ {{ state.readingDirection === CbxReadingDirection.LTR ? 'Left to Right' : 'Right to Left' }} + +
+
+ + +
+ +
+
+ @for (option of slideshowIntervalOptions; track option.value) { + + } +
+
+
+
diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss index 480b8bc2a..8dc5d6b42 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss @@ -30,7 +30,7 @@ $transition-normal: 200ms ease; position: absolute; top: 52px; right: 60px; - width: 320px; + width: 340px; background: $panel-bg; border: 1px solid $border-color; border-radius: 12px; @@ -175,6 +175,18 @@ $transition-normal: 200ms ease; color: white; } + &.text-btn { + width: auto; + padding: 0 10px; + font-size: 11px; + font-weight: 500; + + &.small { + padding: 0 6px; + font-size: 10px; + } + } + &.color-btn { padding: 4px; @@ -192,6 +204,10 @@ $transition-normal: 200ms ease; } } +.text-btns { + flex-wrap: wrap; +} + @media (max-width: 600px) { .quick-settings-panel { right: 16px; diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.ts b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.ts index 9853e6572..db02fba5b 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.ts +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.ts @@ -8,6 +8,8 @@ import { CbxPageViewMode, CbxPageSpread, CbxBackgroundColor, + CbxReadingDirection, + CbxSlideshowInterval, PdfPageViewMode, PdfPageSpread } from '../../../../settings/user-management/user.service'; @@ -30,7 +32,9 @@ export class CbxQuickSettingsComponent implements OnInit, OnDestroy { scrollMode: CbxScrollMode.PAGINATED, pageViewMode: CbxPageViewMode.SINGLE_PAGE, pageSpread: CbxPageSpread.ODD, - backgroundColor: CbxBackgroundColor.GRAY + backgroundColor: CbxBackgroundColor.GRAY, + readingDirection: CbxReadingDirection.LTR, + slideshowInterval: CbxSlideshowInterval.FIVE_SECONDS }; protected readonly CbxFitMode = CbxFitMode; @@ -38,6 +42,8 @@ export class CbxQuickSettingsComponent implements OnInit, OnDestroy { protected readonly CbxPageViewMode = CbxPageViewMode; protected readonly CbxPageSpread = CbxPageSpread; protected readonly CbxBackgroundColor = CbxBackgroundColor; + protected readonly CbxReadingDirection = CbxReadingDirection; + protected readonly CbxSlideshowInterval = CbxSlideshowInterval; fitModeOptions: {value: CbxFitMode, label: string, icon: ReaderIconName}[] = [ {value: CbxFitMode.FIT_PAGE, label: 'Fit Page', icon: 'fit-page'}, @@ -47,6 +53,20 @@ export class CbxQuickSettingsComponent implements OnInit, OnDestroy { {value: CbxFitMode.AUTO, label: 'Automatic', icon: 'auto-fit'} ]; + scrollModeOptions: {value: CbxScrollMode, label: string}[] = [ + {value: CbxScrollMode.PAGINATED, label: 'Paginated'}, + {value: CbxScrollMode.INFINITE, label: 'Infinite'}, + {value: CbxScrollMode.LONG_STRIP, label: 'Long Strip'} + ]; + + slideshowIntervalOptions: {value: CbxSlideshowInterval, label: string}[] = [ + {value: CbxSlideshowInterval.THREE_SECONDS, label: '3s'}, + {value: CbxSlideshowInterval.FIVE_SECONDS, label: '5s'}, + {value: CbxSlideshowInterval.TEN_SECONDS, label: '10s'}, + {value: CbxSlideshowInterval.FIFTEEN_SECONDS, label: '15s'}, + {value: CbxSlideshowInterval.THIRTY_SECONDS, label: '30s'} + ]; + backgroundOptions = [ {value: CbxBackgroundColor.BLACK, label: 'Black', color: '#000000'}, {value: CbxBackgroundColor.GRAY, label: 'Gray', color: '#808080'}, @@ -72,19 +92,28 @@ export class CbxQuickSettingsComponent implements OnInit, OnDestroy { return this.state.scrollMode === CbxScrollMode.PAGINATED; } + get isLongStrip(): boolean { + return this.state.scrollMode === CbxScrollMode.LONG_STRIP; + } + get isPhonePortrait(): boolean { return window.innerWidth < 768 && window.innerHeight > window.innerWidth; } + get currentScrollModeLabel(): string { + return this.scrollModeOptions.find(o => o.value === this.state.scrollMode)?.label || 'Paginated'; + } + + get currentSlideshowIntervalLabel(): string { + return this.slideshowIntervalOptions.find(o => o.value === this.state.slideshowInterval)?.label || '5s'; + } + onFitModeSelect(mode: CbxFitMode): void { this.quickSettingsService.emitFitModeChange(mode); } - onScrollModeToggle(): void { - const newMode = this.state.scrollMode === CbxScrollMode.PAGINATED - ? CbxScrollMode.INFINITE - : CbxScrollMode.PAGINATED; - this.quickSettingsService.emitScrollModeChange(newMode); + onScrollModeSelect(mode: CbxScrollMode): void { + this.quickSettingsService.emitScrollModeChange(mode); } onPageViewToggle(): void { @@ -105,6 +134,17 @@ export class CbxQuickSettingsComponent implements OnInit, OnDestroy { this.quickSettingsService.emitBackgroundColorChange(color); } + onReadingDirectionToggle(): void { + const newDirection = this.state.readingDirection === CbxReadingDirection.LTR + ? CbxReadingDirection.RTL + : CbxReadingDirection.LTR; + this.quickSettingsService.emitReadingDirectionChange(newDirection); + } + + onSlideshowIntervalSelect(interval: CbxSlideshowInterval): void { + this.quickSettingsService.emitSlideshowIntervalChange(interval); + } + onOverlayClick(): void { this.quickSettingsService.close(); } diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.service.ts b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.service.ts index d2392deb6..62cfde0a3 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.service.ts +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.service.ts @@ -1,6 +1,6 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject, Subject} from 'rxjs'; -import {CbxBackgroundColor, CbxFitMode, CbxPageSpread, CbxPageViewMode, CbxScrollMode, PdfBackgroundColor, PdfFitMode, PdfPageSpread, PdfPageViewMode, PdfScrollMode} from '../../../../settings/user-management/user.service'; +import {CbxBackgroundColor, CbxFitMode, CbxPageSpread, CbxPageViewMode, CbxScrollMode, CbxReadingDirection, CbxSlideshowInterval, PdfBackgroundColor, PdfFitMode, PdfPageSpread, PdfPageViewMode, PdfScrollMode} from '../../../../settings/user-management/user.service'; export interface CbxQuickSettingsState { fitMode: CbxFitMode | PdfFitMode; @@ -8,6 +8,8 @@ export interface CbxQuickSettingsState { pageViewMode: CbxPageViewMode | PdfPageViewMode; pageSpread: CbxPageSpread | PdfPageSpread; backgroundColor: CbxBackgroundColor | PdfBackgroundColor; + readingDirection: CbxReadingDirection; + slideshowInterval: CbxSlideshowInterval; } @Injectable() @@ -17,7 +19,9 @@ export class CbxQuickSettingsService { scrollMode: CbxScrollMode.PAGINATED, pageViewMode: CbxPageViewMode.SINGLE_PAGE, pageSpread: CbxPageSpread.ODD, - backgroundColor: CbxBackgroundColor.GRAY + backgroundColor: CbxBackgroundColor.GRAY, + readingDirection: CbxReadingDirection.LTR, + slideshowInterval: CbxSlideshowInterval.FIVE_SECONDS }); state$ = this._state.asObservable(); @@ -39,6 +43,12 @@ export class CbxQuickSettingsService { private _backgroundColorChange = new Subject(); backgroundColorChange$ = this._backgroundColorChange.asObservable(); + private _readingDirectionChange = new Subject(); + readingDirectionChange$ = this._readingDirectionChange.asObservable(); + + private _slideshowIntervalChange = new Subject(); + slideshowIntervalChange$ = this._slideshowIntervalChange.asObservable(); + get state(): CbxQuickSettingsState { return this._state.value; } @@ -79,6 +89,14 @@ export class CbxQuickSettingsService { this.updateState({backgroundColor: color}); } + setReadingDirection(direction: CbxReadingDirection): void { + this.updateState({readingDirection: direction}); + } + + setSlideshowInterval(interval: CbxSlideshowInterval): void { + this.updateState({slideshowInterval: interval}); + } + // Actions emitted from component emitFitModeChange(mode: CbxFitMode): void { this._fitModeChange.next(mode); @@ -100,13 +118,23 @@ export class CbxQuickSettingsService { this._backgroundColorChange.next(color); } + emitReadingDirectionChange(direction: CbxReadingDirection): void { + this._readingDirectionChange.next(direction); + } + + emitSlideshowIntervalChange(interval: CbxSlideshowInterval): void { + this._slideshowIntervalChange.next(interval); + } + reset(): void { this._state.next({ fitMode: CbxFitMode.FIT_PAGE, scrollMode: CbxScrollMode.PAGINATED, pageViewMode: CbxPageViewMode.SINGLE_PAGE, pageSpread: CbxPageSpread.ODD, - backgroundColor: CbxBackgroundColor.GRAY + backgroundColor: CbxBackgroundColor.GRAY, + readingDirection: CbxReadingDirection.LTR, + slideshowInterval: CbxSlideshowInterval.FIVE_SECONDS }); this._visible.next(false); } diff --git a/booklore-ui/src/app/features/readers/ebook-reader/shared/icon.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/shared/icon.component.ts index d4e0c1a5e..660366668 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/shared/icon.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/shared/icon.component.ts @@ -26,7 +26,15 @@ export type ReaderIconName = | 'fit-width' | 'fit-height' | 'actual-size' - | 'auto-fit'; + | 'auto-fit' + | 'fullscreen' + | 'fullscreen-exit' + | 'play' + | 'pause' + | 'help' + | 'long-strip' + | 'direction-ltr' + | 'direction-rtl'; interface IconPath { d: string; @@ -147,6 +155,43 @@ const ICONS: Record = { 'auto-fit': [ {d: 'M12 3v3m0 12v3M3 12h3m12 0h3'}, {d: 'M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0-8 0', type: 'path'} + ], + 'fullscreen': [ + {d: 'M8 3H5a2 2 0 0 0-2 2v3'}, + {d: 'M21 8V5a2 2 0 0 0-2-2h-3'}, + {d: 'M3 16v3a2 2 0 0 0 2 2h3'}, + {d: 'M16 21h3a2 2 0 0 0 2-2v-3'} + ], + 'fullscreen-exit': [ + {d: 'M8 3v3a2 2 0 0 1-2 2H3'}, + {d: 'M21 8h-3a2 2 0 0 1-2-2V3'}, + {d: 'M3 16h3a2 2 0 0 1 2 2v3'}, + {d: 'M16 21v-3a2 2 0 0 1 2-2h3'} + ], + 'play': [ + {d: 'M5 3l14 9-14 9V3z'} + ], + 'pause': [ + {d: 'M6,4 L6,20', type: 'line'}, + {d: 'M18,4 L18,20', type: 'line'} + ], + 'help': [ + {d: 'M12 12m-10 0a10 10 0 1 0 20 0a10 10 0 1 0-20 0', type: 'path'}, + {d: 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'}, + {d: 'M12 17h.01'} + ], + 'long-strip': [ + {d: 'M4 3h16v6H4z'}, + {d: 'M4 9h16v6H4z'}, + {d: 'M4 15h16v6H4z'} + ], + 'direction-ltr': [ + {d: 'M5,12 L19,12', type: 'line'}, + {d: 'M15,8 L19,12 L15,16', type: 'polyline'} + ], + 'direction-rtl': [ + {d: 'M19,12 L5,12', type: 'line'}, + {d: 'M9,8 L5,12 L9,16', type: 'polyline'} ] }; diff --git a/booklore-ui/src/app/features/settings/user-management/user.service.ts b/booklore-ui/src/app/features/settings/user-management/user.service.ts index 544fe10b3..c95d078b1 100644 --- a/booklore-ui/src/app/features/settings/user-management/user.service.ts +++ b/booklore-ui/src/app/features/settings/user-management/user.service.ts @@ -85,7 +85,21 @@ export enum CbxFitMode { export enum CbxScrollMode { PAGINATED = 'PAGINATED', - INFINITE = 'INFINITE' + INFINITE = 'INFINITE', + LONG_STRIP = 'LONG_STRIP' +} + +export enum CbxReadingDirection { + LTR = 'LTR', + RTL = 'RTL' +} + +export enum CbxSlideshowInterval { + THREE_SECONDS = 3000, + FIVE_SECONDS = 5000, + TEN_SECONDS = 10000, + FIFTEEN_SECONDS = 15000, + THIRTY_SECONDS = 30000 } export interface PdfReaderSetting { @@ -164,6 +178,8 @@ export interface CbxReaderSetting { fitMode: CbxFitMode; scrollMode?: CbxScrollMode; backgroundColor?: CbxBackgroundColor; + readingDirection?: CbxReadingDirection; + slideshowInterval?: CbxSlideshowInterval; } export interface TableColumnPreference {