diff --git a/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.html b/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.html index 4a20bfcf..283fd365 100644 --- a/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.html +++ b/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.html @@ -6,217 +6,259 @@ } @if (!isLoading) {
-
+
Progress: {{ progressPercentage }}%
- -
-

Chapters

-
    - @for (chapter of chapters; track chapter) { -
  • - {{ chapter.label }} -
  • - } -
-
+ + +
+ Table of Contents +
+
- - -
-

Bookmarks

- @if (bookmarks.length === 0) { -

No bookmarks yet

- } @else { -
    - @for (bookmark of bookmarks; track bookmark.id) { -
  • -
    - - {{ bookmark.title }} + + + +
    + + Chapters +
    +
    + +
    + + Bookmarks +
    +
    +
    + + +
    +
      + @for (chapter of chapters; track chapter) { +
    • + + {{ chapter.label }} +
    • + } +
    +
    +
    + +
    + @if (bookmarks.length === 0) { +
    + +

    No bookmarks yet

    + Tap the bookmark icon to save your place
    - - -
  • - } -
- } -
+ } @else { +
    + @for (bookmark of bookmarks; track bookmark.id) { +
  • + + {{ bookmark.title }} + +
  • + } +
+ } +
+ + +
-

{{ currentChapter }}

+

{{ currentChapter }}

- - @if (!locationsReady) {
} + +
- -
+ + +
+ Reader Settings +
+
-
- Font Size: -
- - {{ fontSize }}% - +
+ +
+
+ + Typography +
+
+
+ +
+ + {{ fontSize }}% + +
+
+ +
+ + + +
+ +
+ +
+ {{ lineHeight }} + + +
+
+ +
+ +
+ {{ letterSpacing ?? 0 }} em + + +
+
- - -
- - - +
+
+ + Appearance +
+
+
+ +
+ @for (theme of themes; track theme) { + + } +
+
+
- +
+
+ + Layout +
+
+
+ +
+ + +
+
-
- -
- @for (theme of themes; track theme) { - + @if (selectedFlow === 'paginated' && !isMobileDevice()) { +
+ +
+ + +
+
}
- - -
- -
-
- - - -
-
- - - -
-
-
- - @if (selectedFlow === 'paginated' && !isMobileDevice()) { - - -
- -
-
- - - -
-
- - - -
-
-
- } - - - -
- - - -
- -
- - - -
-
-
+
@if (selectedFlow !== 'scrolled') { } diff --git a/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.scss b/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.scss index 78621664..9ed15d1d 100644 --- a/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.scss +++ b/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.scss @@ -5,7 +5,22 @@ justify-content: space-between; padding-left: 1rem; padding-right: 1rem; - position: relative; + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1001; + background: linear-gradient(180deg, + rgba(0, 0, 0, 0.85) 0%, + rgba(0, 0, 0, 0.75) 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + transition: transform 0.3s ease; + min-height: 3rem; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + + &.hidden { + transform: translateY(-100%); + } } .header-left { @@ -21,6 +36,25 @@ top: 50%; transform: translate(-50%, -50%); pointer-events: none; + max-width: 40%; + + @media (max-width: 768px) { + max-width: 50%; + } + + @media (max-width: 480px) { + max-width: 40%; + } + + p { + color: rgba(255, 255, 255, 0.95); + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; + } } .header-right { @@ -38,10 +72,16 @@ .progress-percentage { font-size: 0.85rem; - color: var(--text-color); - font-weight: 500; + color: rgba(255, 255, 255, 0.95); + font-weight: 600; display: flex; align-items: center; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + padding: 0.375rem 0.75rem; + background: rgba(255, 255, 255, 0.1); + border-radius: 0.5rem; + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.15); } .progress-label { @@ -55,12 +95,32 @@ flex-direction: column; height: 100dvh; overflow: hidden; + position: relative; +} + +.spinner-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--ground-background); + z-index: 1002; } #epubContainer { height: 100%; width: 100%; overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; } .menu-toggle-button, @@ -68,6 +128,30 @@ top: 10px; z-index: 1000; margin: 0; + + &::ng-deep .p-button { + background: rgba(255, 255, 255, 0.1) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + color: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(8px); + transition: all 0.2s ease !important; + + &:hover { + background: rgba(255, 255, 255, 0.2) !important; + border-color: rgba(255, 255, 255, 0.3) !important; + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + &:active { + transform: scale(0.95); + } + + .p-button-icon { + color: rgba(255, 255, 255, 0.95) !important; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + } + } } .menu-toggle-button { @@ -136,31 +220,59 @@ margin: 0; display: flex; flex-direction: column; - gap: 10px; } .chapter-item { - padding: 0; + padding: 0.4rem 0.25rem; font-size: 14px; color: var(--text-color); cursor: pointer; - transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease; - border-radius: 0.35rem; + transition: all 0.2s ease; + border-radius: 0.5rem; background-color: transparent; - display: block; + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; + + .chapter-icon { + font-size: 0.75rem; + color: var(--primary-color); + opacity: 0; + transition: opacity 0.2s ease; + } + + .chapter-label { + flex: 1; + } &:hover { - color: var(--primary-color); + background-color: color-mix(in srgb, var(--primary-color) 8%, transparent); + transform: translateX(4px); + + .chapter-icon { + opacity: 1; + } } } .current-chapter { color: var(--primary-color); - font-weight: bold; - background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary-color) 35%, transparent); - padding: 0.35rem 0.75rem; - border-radius: 0.45rem; + font-weight: 600; + background: linear-gradient(135deg, + color-mix(in srgb, var(--primary-color) 15%, transparent), + color-mix(in srgb, var(--primary-color) 8%, transparent)); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary-color) 25%, transparent); + + .chapter-icon { + opacity: 1; + } + + &:hover { + background: linear-gradient(135deg, + color-mix(in srgb, var(--primary-color) 20%, transparent), + color-mix(in srgb, var(--primary-color) 12%, transparent)); + } } .bookmark-list { @@ -169,26 +281,69 @@ margin: 0; display: flex; flex-direction: column; - gap: 8px; } .bookmark-item { + padding: 0.4rem 0.75rem; + font-size: 14px; + color: var(--text-color); + cursor: pointer; + transition: all 0.2s ease; + border-radius: 0.5rem; + background-color: transparent; display: flex; align-items: center; - justify-content: space-between; - padding: 0.5rem 0.75rem; - border-radius: 0.35rem; - background-color: color-mix(in srgb, var(--primary-color) 10%, transparent); - transition: background-color 0.3s ease; + gap: 0.5rem; + margin-bottom: 0.25rem; + position: relative; + + .bookmark-icon { + font-size: 0.875rem; + color: var(--primary-color); + flex-shrink: 0; + } + + .bookmark-label { + flex: 1; + font-weight: 500; + } + + .bookmark-delete { + opacity: 0; + background: transparent; + border: none; + cursor: pointer; + padding: 0.375rem; + border-radius: 0.375rem; + color: color-mix(in srgb, var(--text-color) 60%, transparent); + transition: all 0.2s ease; + flex-shrink: 0; + margin-left: auto; + + &:hover { + background: color-mix(in srgb, #ef4444 15%, transparent); + color: #ef4444; + } + + i { + font-size: 0.875rem; + } + } &:hover { - background-color: color-mix(in srgb, var(--primary-color) 20%, transparent); + background-color: color-mix(in srgb, var(--primary-color) 8%, transparent); + transform: translateX(4px); + + .bookmark-delete { + opacity: 1; + } } } .bookmark-content { display: flex; align-items: center; + gap: 0.75rem; cursor: pointer; flex: 1; color: var(--text-color); @@ -196,6 +351,11 @@ i { color: var(--primary-color); + font-size: 1rem; + } + + .bookmark-title { + font-weight: 500; } &:hover { @@ -203,33 +363,429 @@ } } -.location-indicator { - display: flex; - align-items: center; - margin-left: 0.5rem; +.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; - .dot { - height: 10px; - width: 10px; - background-color: red; - border-radius: 50%; - display: inline-block; - box-shadow: 0 0 4px #bbb; + &:hover { + background: color-mix(in srgb, #ef4444 15%, transparent); + color: #ef4444; + } + + i { + font-size: 0.875rem; } } -.spinner-wrapper { +.empty-state { display: flex; + flex-direction: column; align-items: center; justify-content: center; - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - z-index: 999; + padding: 2rem 1rem; + text-align: center; + color: color-mix(in srgb, var(--text-color) 50%, transparent); + + i { + font-size: 2.5rem; + margin-bottom: 1rem; + opacity: 0.3; + } + + p { + font-size: 0.875rem; + font-weight: 500; + margin: 0 0 0.25rem 0; + } + + span { + font-size: 0.75rem; + opacity: 0.7; + } +} + +.settings-container { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.settings-card { + background: color-mix(in srgb, var(--surface-card) 50%, var(--surface-ground)); + border: 1px solid color-mix(in srgb, var(--text-color) 10%, transparent); + border-radius: 0.75rem; + overflow: hidden; + transition: all 0.2s ease; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: color-mix(in srgb, var(--primary-color) 20%, transparent); + } + + .card-header { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 1rem 1.125rem; + background: linear-gradient(135deg, + color-mix(in srgb, var(--primary-color) 8%, transparent), + color-mix(in srgb, var(--primary-color) 4%, transparent)); + border-bottom: 1px solid color-mix(in srgb, var(--text-color) 8%, transparent); + + i { + color: var(--primary-color); + font-size: 1rem; + } + + span { + font-weight: 600; + font-size: 0.9375rem; + color: var(--text-color); + } + } + + .card-content { + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; + } +} + +.setting-row { + display: flex; + flex-direction: column; + gap: 0.625rem; + + > label { + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-color); + text-transform: uppercase; + letter-spacing: 0.025em; + } +} + +.font-size-controls { + display: flex; + align-items: center; + gap: 1rem; + background: color-mix(in srgb, var(--text-color) 5%, transparent); + padding: 0.5rem; + border-radius: 0.5rem; + width: fit-content; + + .control-btn { + background: var(--primary-color); + color: white; + border: none; + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + transform: scale(1.05); + box-shadow: 0 2px 8px color-mix(in srgb, var(--primary-color) 40%, transparent); + } + + &:active { + transform: scale(0.95); + } + } + + .font-size-value { + font-weight: 600; + color: var(--text-color); + min-width: 3rem; + text-align: center; + } +} + +.slider-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0 0.75rem; + + .slider-value { + font-size: 0.85rem; + font-weight: 600; + color: var(--primary-color); + align-self: flex-end; + } +} + +.theme-selector { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + + .theme-option { + width: 3rem; + height: 3rem; + border-radius: 0.5rem; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + i { + color: white; + font-size: 1rem; + font-weight: bold; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + } + + &:hover { + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &.selected { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); + } + } +} + +.toggle-group { + display: flex; + gap: 0.5rem; + background: color-mix(in srgb, var(--text-color) 5%, transparent); + padding: 0.375rem; + border-radius: 0.5rem; + + .toggle-option { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 0.8rem; + background: transparent; + border: none; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s ease; + color: color-mix(in srgb, var(--text-color) 70%, transparent); + font-weight: 500; + font-size: 0.95rem; + + i { + font-size: 0.95rem; + } + + &:hover { + background: color-mix(in srgb, var(--text-color) 8%, transparent); + color: var(--text-color); + } + + &.active { + background: var(--primary-color); + color: white; + box-shadow: 0 2px 6px color-mix(in srgb, var(--primary-color) 30%, transparent); + } + } } ::ng-deep .p-divider.p-divider-horizontal { margin: 0.25rem 0 0.5rem 0 !important; } + +.drawer-header { + display: flex; + align-items: center; + gap: 0.75rem; + + i { + color: var(--primary-color); + } + + .drawer-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); + } +} + +.drawer-section { + margin-bottom: 1.5rem; + + .section-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid color-mix(in srgb, var(--text-color) 10%, transparent); + + i { + color: var(--primary-color); + font-size: 1rem; + } + + h3 { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-color); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0; + } + } + + .section-content { + padding: 0 0.25rem; + } +} + +.drawer-divider { + height: 1px; + background: linear-gradient(to right, transparent, color-mix(in srgb, var(--text-color) 20%, transparent), transparent); + margin: 1.5rem 0; +} + +::ng-deep .p-drawer .p-tabs { + .p-tablist { + background: transparent; + border: none; + border-bottom: 2px solid color-mix(in srgb, var(--text-color) 10%, transparent); + padding: 0 1rem; + margin: 0 -1rem; + display: flex; + gap: 0; + + .p-tab { + background: transparent; + border: none; + border-bottom: 2px solid transparent; + padding: 1rem 1.5rem; + margin-bottom: -2px; + transition: all 0.2s ease; + cursor: pointer; + + &:hover { + background: color-mix(in srgb, var(--primary-color) 5%, transparent); + } + + &[aria-selected="true"] { + border-bottom-color: var(--primary-color); + + .tab-header { + color: var(--primary-color); + } + } + } + + .p-tab:focus-visible { + box-shadow: none; + outline: 2px solid color-mix(in srgb, var(--primary-color) 30%, transparent); + outline-offset: -2px; + } + } + + .p-tabpanels { + background: transparent; + padding: 0; + + .p-tabpanel { + padding: 0; + } + } +} + +::ng-deep .p-drawer-left { + .p-drawer-content { + padding: 0 !important; + } +} + +.tab-header { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9375rem; + font-weight: 500; + + i { + font-size: 1rem; + } +} + +.tab-content { + padding: 1.25rem 0.5rem; +} + +::ng-deep .header-right { + .p-button { + background: rgba(255, 255, 255, 0.1) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + color: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(8px); + transition: all 0.2s ease !important; + + &:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.2) !important; + border-color: rgba(255, 255, 255, 0.3) !important; + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + &:active:not(:disabled) { + transform: scale(0.95); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .p-button-icon { + color: rgba(255, 255, 255, 0.95) !important; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + + &.pi-bookmark-fill { + color: #fbbf24 !important; + filter: drop-shadow(0 2px 6px rgba(251, 191, 36, 0.5)); + } + } + } +} + +.location-indicator { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + + .dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #fbbf24; + box-shadow: 0 0 8px rgba(251, 191, 36, 0.6); + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} diff --git a/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.ts b/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.ts index b8c575c5..c8d1d162 100644 --- a/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.ts +++ b/booklore-ui/src/app/features/readers/epub-reader/component/epub-reader.component.ts @@ -1,32 +1,29 @@ -import {Component, ElementRef, inject, OnDestroy, OnInit, ViewChild, NgZone} from '@angular/core'; +import {Component, ElementRef, inject, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core'; import ePub from 'epubjs'; import {Drawer} from 'primeng/drawer'; -import {Subscription} from 'rxjs'; +import {forkJoin, Subscription} from 'rxjs'; import {Button} from 'primeng/button'; - import {FormsModule} from '@angular/forms'; import {ActivatedRoute} from '@angular/router'; import {Book, BookSetting} from '../../../book/model/book.model'; import {BookService} from '../../../book/service/book.service'; -import {forkJoin} from 'rxjs'; import {Select} from 'primeng/select'; import {UserService} from '../../../settings/user-management/user.service'; import {ProgressSpinner} from 'primeng/progressspinner'; -import {MessageService} from 'primeng/api'; -import {BookMarkService, BookMark} from '../../../../shared/service/book-mark.service'; +import {MessageService, PrimeTemplate} from 'primeng/api'; +import {BookMark, BookMarkService} 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} from '../epub-theme-util'; -import {RadioButton} from 'primeng/radiobutton'; -import { PageTitleService } from "../../../../shared/service/page-title.service"; -import {Divider} from 'primeng/divider'; +import {EpubThemeUtil, EpubTheme} from '../epub-theme-util'; +import {PageTitleService} from "../../../../shared/service/page-title.service"; +import {Tab, TabList, TabPanel, TabPanels, Tabs} from 'primeng/tabs'; @Component({ selector: 'app-epub-reader', templateUrl: './epub-reader.component.html', styleUrls: ['./epub-reader.component.scss'], - imports: [Drawer, Button, FormsModule, Select, ProgressSpinner, Tooltip, Slider, RadioButton, Divider], + imports: [Drawer, Button, FormsModule, Select, ProgressSpinner, Tooltip, Slider, PrimeTemplate, Tabs, TabList, Tab, TabPanels, TabPanel], standalone: true }) export class EpubReaderComponent implements OnInit, OnDestroy { @@ -51,7 +48,11 @@ export class EpubReaderComponent implements OnInit, OnDestroy { public progressPercentage = 0; showControls = !this.isMobileDevice(); + showHeader = true; private hideControlsTimeout?: number; + private hideHeaderTimeout?: number; + private isMouseInTopRegion = false; + private headerShownByMobileTouch = false; private book: any; private rendition: any; @@ -67,7 +68,7 @@ export class EpubReaderComponent implements OnInit, OnDestroy { letterSpacing?: number; fontTypes: any[] = [ - {label: "Book's Internal", value: null}, + {label: "Publisher's Default", value: null}, {label: 'Serif', value: 'serif'}, {label: 'Sans Serif', value: 'sans-serif'}, {label: 'Roboto', value: 'roboto'}, @@ -76,10 +77,21 @@ export class EpubReaderComponent implements OnInit, OnDestroy { ]; themes: any[] = [ - {label: 'White', value: 'white'}, - {label: 'Black', value: 'black'}, - {label: 'Grey', value: 'grey'}, - {label: 'Sepia', value: 'sepia'}, + {label: 'White', value: EpubTheme.WHITE}, + {label: 'Black', value: EpubTheme.BLACK}, + {label: 'Grey', value: EpubTheme.GREY}, + {label: 'Sepia', value: EpubTheme.SEPIA}, + {label: 'Green', value: EpubTheme.GREEN}, + {label: 'Lavender', value: EpubTheme.LAVENDER}, + {label: 'Cream', value: EpubTheme.CREAM}, + {label: 'Light Blue', value: EpubTheme.LIGHT_BLUE}, + {label: 'Peach', value: EpubTheme.PEACH}, + {label: 'Mint', value: EpubTheme.MINT}, + {label: 'Dark Slate', value: EpubTheme.DARK_SLATE}, + {label: 'Dark Olive', value: EpubTheme.DARK_OLIVE}, + {label: 'Dark Purple', value: EpubTheme.DARK_PURPLE}, + {label: 'Dark Teal', value: EpubTheme.DARK_TEAL}, + {label: 'Dark Brown', value: EpubTheme.DARK_BROWN}, ]; private route = inject(ActivatedRoute); @@ -178,7 +190,9 @@ export class EpubReaderComponent implements OnInit, OnDestroy { this.trackProgress(); this.setupTouchListener(); this.isLoading = false; + this.startHeaderAutoHide(); }); + }; fileReader.readAsArrayBuffer(epubData); @@ -364,10 +378,28 @@ export class EpubReaderComponent implements OnInit, OnDestroy { toggleDrawer(): void { this.isDrawerVisible = !this.isDrawerVisible; + if (this.isDrawerVisible) { + this.showHeader = true; + this.headerShownByMobileTouch = false; + this.clearHeaderTimeout(); + } else { + if (!this.isMobileDevice()) { + this.startHeaderAutoHide(); + } + } } toggleSettingsDrawer(): void { this.isSettingsDrawerVisible = !this.isSettingsDrawerVisible; + if (this.isSettingsDrawerVisible) { + this.showHeader = true; + this.headerShownByMobileTouch = false; + this.clearHeaderTimeout(); + } else { + if (!this.isMobileDevice()) { + this.startHeaderAutoHide(); + } + } } private trackProgress(): void { @@ -415,7 +447,7 @@ export class EpubReaderComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.routeSubscription?.unsubscribe(); - + if (this.rendition) { this.rendition.off('keyup', this.keyListener); } @@ -424,21 +456,8 @@ export class EpubReaderComponent implements OnInit, OnDestroy { if (this.hideControlsTimeout) { window.clearTimeout(this.hideControlsTimeout); } - } - getThemeColor(themeKey: string | undefined): string { - switch (themeKey) { - case 'white': - return '#ffffff'; - case 'black': - return '#000000'; - case 'grey': - return '#808080'; - case 'sepia': - return '#704214'; - default: - return '#ffffff'; - } + this.clearHeaderTimeout(); } selectTheme(themeKey: string): void { @@ -446,6 +465,65 @@ export class EpubReaderComponent implements OnInit, OnDestroy { this.changeThemes(); } + getThemeColor(themeKey: string | undefined): string { + return EpubThemeUtil.getThemeColor(themeKey); + } + + onBookClick(event: MouseEvent): void { + if (this.isDrawerVisible || this.isSettingsDrawerVisible) { + this.isDrawerVisible = false; + this.isSettingsDrawerVisible = false; + this.startHeaderAutoHide(); + return; + } + + const clickY = event.clientY; + const screenHeight = window.innerHeight; + + if (this.isMobileDevice()) { + const isTopClick = clickY < screenHeight * 0.2; + const isBottomClick = clickY > screenHeight * 0.2; + + if (isTopClick && !this.showHeader) { + // Touch top 20% - show header and mark as shown by mobile touch + this.showHeader = true; + this.headerShownByMobileTouch = true; + this.clearHeaderTimeout(); + } else if (isBottomClick && this.showHeader && this.headerShownByMobileTouch) { + // Touch lower 80% - hide header + this.showHeader = false; + this.headerShownByMobileTouch = false; + this.clearHeaderTimeout(); + } + } else { + // Desktop behavior + const isTopClick = clickY < screenHeight * 0.1; + if (isTopClick) { + this.showHeader = true; + this.startHeaderAutoHide(); + } + } + } + + onBookMouseMove(event: MouseEvent): void { + if (this.isDrawerVisible || this.isSettingsDrawerVisible || this.isMobileDevice()) { + return; + } + + const mouseY = event.clientY; + const screenHeight = window.innerHeight; + const isInTopRegion = mouseY < screenHeight * 0.1; + + if (isInTopRegion && !this.isMouseInTopRegion) { + this.isMouseInTopRegion = true; + this.showHeader = true; + this.clearHeaderTimeout(); + } else if (!isInTopRegion && this.isMouseInTopRegion) { + this.isMouseInTopRegion = false; + this.startHeaderAutoHide(); + } + } + onBookTouch(): void { if (this.isMobileDevice()) { this.showControls = true; @@ -456,7 +534,35 @@ export class EpubReaderComponent implements OnInit, OnDestroy { this.ngZone.run(() => { this.showControls = false; }); - }, 3000); + }, 2000); + } + } + + private startHeaderAutoHide(): void { + this.clearHeaderTimeout(); + + if (this.isDrawerVisible || this.isSettingsDrawerVisible || this.isMouseInTopRegion) { + return; + } + + // Don't auto-hide on mobile if header was shown by touch + if (this.isMobileDevice() && this.headerShownByMobileTouch) { + return; + } + + this.hideHeaderTimeout = window.setTimeout(() => { + this.ngZone.run(() => { + if (!this.isDrawerVisible && !this.isSettingsDrawerVisible && !this.isMouseInTopRegion) { + this.showHeader = false; + } + }); + }, 1000); + } + + private clearHeaderTimeout(): void { + if (this.hideHeaderTimeout) { + window.clearTimeout(this.hideHeaderTimeout); + this.hideHeaderTimeout = undefined; } } diff --git a/booklore-ui/src/app/features/readers/epub-reader/epub-theme-util.ts b/booklore-ui/src/app/features/readers/epub-reader/epub-theme-util.ts index 9ce619ae..27cbbaee 100644 --- a/booklore-ui/src/app/features/readers/epub-reader/epub-theme-util.ts +++ b/booklore-ui/src/app/features/readers/epub-reader/epub-theme-util.ts @@ -1,22 +1,40 @@ +export enum EpubTheme { + WHITE = 'white', + BLACK = 'black', + GREY = 'grey', + SEPIA = 'sepia', + GREEN = 'green', + LAVENDER = 'lavender', + CREAM = 'cream', + LIGHT_BLUE = 'light-blue', + PEACH = 'peach', + MINT = 'mint', + DARK_SLATE = 'dark-slate', + DARK_OLIVE = 'dark-olive', + DARK_PURPLE = 'dark-purple', + DARK_TEAL = 'dark-teal', + DARK_BROWN = 'dark-brown' +} + export class EpubThemeUtil { static readonly themesMap = new Map([ - ['black', { + [EpubTheme.BLACK, { "body": {"background-color": "#000000", "color": "#f9f9f9"}, "p": {"color": "#f9f9f9"}, "h1, h2, h3, h4, h5, h6": {"color": "#f9f9f9"}, "a": {"color": "#f9f9f9"}, - "img": {"-webkit-filter": "invert(1) hue-rotate(180deg)", "filter": "invert(1) hue-rotate(180deg)"}, + "img": {"-webkit-filter": "none", "filter": "none"}, "code": {"color": "#00ff00", "background-color": "black"} }], - ['sepia', { + [EpubTheme.SEPIA, { "body": {"background-color": "#f4ecd8", "color": "#6e4b3a"}, "p": {"color": "#6e4b3a"}, "h1, h2, h3, h4, h5, h6": {"color": "#6e4b3a"}, "a": {"color": "#8b4513"}, - "img": {"-webkit-filter": "sepia(1) contrast(1.5)", "filter": "sepia(1) contrast(1.5)"}, + "img": {"-webkit-filter": "none", "filter": "none"}, "code": {"color": "#8b0000", "background-color": "#f4ecd8"} }], - ['white', { + [EpubTheme.WHITE, { "body": {"background-color": "#ffffff", "color": "#000000"}, "p": {"color": "#000000"}, "h1, h2, h3, h4, h5, h6": {"color": "#000000"}, @@ -24,16 +42,141 @@ export class EpubThemeUtil { "img": {"-webkit-filter": "none", "filter": "none"}, "code": {"color": "#d14", "background-color": "#f5f5f5"} }], - ['grey', { + [EpubTheme.GREY, { "body": {"background-color": "#404040", "color": "#d3d3d3"}, "p": {"color": "#d3d3d3"}, "h1, h2, h3, h4, h5, h6": {"color": "#d3d3d3"}, "a": {"color": "#1e90ff"}, "img": {"filter": "none"}, "code": {"color": "#d14", "background-color": "#585858"} + }], + [EpubTheme.GREEN, { + "body": {"background-color": "rgb(232, 245, 233)", "color": "#1b5e20"}, + "p": {"color": "#1b5e20"}, + "h1, h2, h3, h4, h5, h6": {"color": "#1b5e20"}, + "a": {"color": "#2e7d32"}, + "img": {"filter": "none"}, + "code": {"color": "#c62828", "background-color": "#e8f5e9"} + }], + [EpubTheme.LAVENDER, { + "body": {"background-color": "rgb(243, 237, 247)", "color": "#4a148c"}, + "p": {"color": "#4a148c"}, + "h1, h2, h3, h4, h5, h6": {"color": "#4a148c"}, + "a": {"color": "#6a1b9a"}, + "img": {"filter": "none"}, + "code": {"color": "#c62828", "background-color": "#f3e5f5"} + }], + [EpubTheme.CREAM, { + "body": {"background-color": "rgb(255, 253, 245)", "color": "#3e2723"}, + "p": {"color": "#3e2723"}, + "h1, h2, h3, h4, h5, h6": {"color": "#3e2723"}, + "a": {"color": "#5d4037"}, + "img": {"filter": "none"}, + "code": {"color": "#d84315", "background-color": "#fff8e1"} + }], + [EpubTheme.LIGHT_BLUE, { + "body": {"background-color": "rgb(232, 244, 253)", "color": "#01579b"}, + "p": {"color": "#01579b"}, + "h1, h2, h3, h4, h5, h6": {"color": "#01579b"}, + "a": {"color": "#0277bd"}, + "img": {"filter": "none"}, + "code": {"color": "#c62828", "background-color": "#e1f5fe"} + }], + [EpubTheme.PEACH, { + "body": {"background-color": "rgb(255, 243, 238)", "color": "#bf360c"}, + "p": {"color": "#bf360c"}, + "h1, h2, h3, h4, h5, h6": {"color": "#bf360c"}, + "a": {"color": "#d84315"}, + "img": {"filter": "none"}, + "code": {"color": "#c62828", "background-color": "#fbe9e7"} + }], + [EpubTheme.MINT, { + "body": {"background-color": "rgb(224, 247, 250)", "color": "#004d40"}, + "p": {"color": "#004d40"}, + "h1, h2, h3, h4, h5, h6": {"color": "#004d40"}, + "a": {"color": "#00695c"}, + "img": {"filter": "none"}, + "code": {"color": "#c62828", "background-color": "#e0f2f1"} + }], + [EpubTheme.DARK_SLATE, { + "body": {"background-color": "rgb(47, 55, 66)", "color": "#e4e7eb"}, + "p": {"color": "#e4e7eb"}, + "h1, h2, h3, h4, h5, h6": {"color": "#f0f3f7"}, + "a": {"color": "#7dd3fc"}, + "img": {"filter": "brightness(0.9)"}, + "code": {"color": "#fbbf24", "background-color": "#374151"} + }], + [EpubTheme.DARK_OLIVE, { + "body": {"background-color": "rgb(56, 61, 47)", "color": "#e8ecd7"}, + "p": {"color": "#e8ecd7"}, + "h1, h2, h3, h4, h5, h6": {"color": "#f4f7e3"}, + "a": {"color": "#bef264"}, + "img": {"filter": "brightness(0.9)"}, + "code": {"color": "#fde047", "background-color": "#3f4536"} + }], + [EpubTheme.DARK_PURPLE, { + "body": {"background-color": "rgb(49, 39, 67)", "color": "#e9d5ff"}, + "p": {"color": "#e9d5ff"}, + "h1, h2, h3, h4, h5, h6": {"color": "#f3e8ff"}, + "a": {"color": "#d8b4fe"}, + "img": {"filter": "brightness(0.9)"}, + "code": {"color": "#fde047", "background-color": "#3b2d4f"} + }], + [EpubTheme.DARK_TEAL, { + "body": {"background-color": "rgb(29, 53, 56)", "color": "#ccfbf1"}, + "p": {"color": "#ccfbf1"}, + "h1, h2, h3, h4, h5, h6": {"color": "#e0fdf8"}, + "a": {"color": "#5eead4"}, + "img": {"filter": "brightness(0.9)"}, + "code": {"color": "#fde047", "background-color": "#204145"} + }], + [EpubTheme.DARK_BROWN, { + "body": {"background-color": "rgb(54, 42, 36)", "color": "#f5e6d3"}, + "p": {"color": "#f5e6d3"}, + "h1, h2, h3, h4, h5, h6": {"color": "#fef3e2"}, + "a": {"color": "#fcd34d"}, + "img": {"filter": "brightness(0.9)"}, + "code": {"color": "#fde047", "background-color": "#42332d"} }] ]); + static getThemeColor(themeKey: string | undefined): string { + switch (themeKey) { + case EpubTheme.WHITE: + return '#ffffff'; + case EpubTheme.BLACK: + return '#000000'; + case EpubTheme.GREY: + return '#808080'; + case EpubTheme.SEPIA: + return '#704214'; + case EpubTheme.GREEN: + return 'rgb(232, 245, 233)'; + case EpubTheme.LAVENDER: + return 'rgb(243, 237, 247)'; + case EpubTheme.CREAM: + return 'rgb(255, 253, 245)'; + case EpubTheme.LIGHT_BLUE: + return 'rgb(232, 244, 253)'; + case EpubTheme.PEACH: + return 'rgb(255, 243, 238)'; + case EpubTheme.MINT: + return 'rgb(224, 247, 250)'; + case EpubTheme.DARK_SLATE: + return 'rgb(47, 55, 66)'; + case EpubTheme.DARK_OLIVE: + return 'rgb(56, 61, 47)'; + case EpubTheme.DARK_PURPLE: + return 'rgb(49, 39, 67)'; + case EpubTheme.DARK_TEAL: + return 'rgb(29, 53, 56)'; + case EpubTheme.DARK_BROWN: + return 'rgb(54, 42, 36)'; + default: + return '#ffffff'; + } + } + static applyTheme(rendition: any, themeKey: string, fontFamily?: string, fontSize?: number, lineHeight?: number, letterSpacing?: number): void { if (!rendition) return; diff --git a/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.html b/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.html index f61f6959..5bfafddb 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.html +++ b/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.html @@ -3,17 +3,16 @@
-
+
@for (spread of cbxSpreads; track spread) { -
- - - -
+ }
@@ -27,17 +26,16 @@
-
+
@for (mode of cbxViewModes; track mode) { -
- - - -
+ }
@@ -51,17 +49,18 @@
-
+
@for (mode of cbxFitModes; track mode) { -
- - - -
+ }
@@ -75,17 +74,16 @@
-
+
@for (mode of cbxScrollModes; track mode) { -
- - - -
+ }
@@ -99,17 +97,21 @@
-
+
@for (color of cbxBackgroundColors; track color) { -
- - - -
+ }
diff --git a/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.scss b/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.scss index de5853fd..acb72bc7 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.scss +++ b/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.scss @@ -1,7 +1,7 @@ .cbx-preferences-container { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; } .setting-item { @@ -45,16 +45,6 @@ flex-shrink: 0; min-width: 100px; } - - .radio-group { - display: flex; - gap: 1.5rem; - - @media (max-width: 768px) { - flex-direction: column; - gap: 0.75rem; - } - } } .setting-description { @@ -65,37 +55,86 @@ } } -.setting-control { - flex-shrink: 0; +.button-group { display: flex; - flex-direction: column; - gap: 0.75rem; - align-items: flex-end; - - @media (max-width: 768px) { - align-items: flex-start; - width: 100%; - } -} - -.radio-group { - display: flex; - gap: 1.5rem; - - @media (max-width: 768px) { - flex-direction: column; - gap: 0.75rem; - } -} - -.radio-option { - display: flex; - align-items: center; gap: 0.5rem; + flex-wrap: wrap; - label { - font-size: 0.875rem; - color: var(--p-text-color); + .option-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + border: 1.5px solid color-mix(in srgb, var(--text-color) 20%, transparent); + background: color-mix(in srgb, var(--text-color) 5%, transparent); + color: var(--text-color); cursor: pointer; + transition: all 0.2s ease; + font-size: 0.8125rem; + font-weight: 500; + min-width: 4.5rem; + + i { + font-size: 0.875rem; + } + + .option-text { + white-space: nowrap; + } + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: color-mix(in srgb, var(--primary-color) 40%, transparent); + } + + &.selected { + border-color: var(--primary-color); + background: color-mix(in srgb, var(--primary-color) 10%, transparent); + color: var(--primary-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 15%, transparent); + + &:hover { + transform: translateY(-2px); + } + } + } +} + +.theme-selector { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + + .theme-option { + width: 2.5rem; + height: 2.5rem; + border-radius: 0.375rem; + border: 1.5px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + i { + color: white; + font-size: 0.875rem; + font-weight: bold; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + } + + &:hover { + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &.selected { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 25%, transparent); + } } } diff --git a/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.ts b/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.ts index ebe3897e..06f4eab7 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.ts +++ b/booklore-ui/src/app/features/settings/reader-preferences/cbx-reader-preferences/cbx-reader-preferences-component.ts @@ -1,14 +1,14 @@ import {Component, inject, Input} from '@angular/core'; -import {RadioButton} from 'primeng/radiobutton'; import {FormsModule} from '@angular/forms'; import {CbxBackgroundColor, CbxFitMode, CbxPageSpread, CbxPageViewMode, CbxScrollMode, UserSettings} from '../../user-management/user.service'; import {ReaderPreferencesService} from '../reader-preferences-service'; +import {TooltipModule} from 'primeng/tooltip'; @Component({ selector: 'app-cbx-reader-preferences-component', imports: [ - RadioButton, - FormsModule + FormsModule, + TooltipModule ], templateUrl: './cbx-reader-preferences-component.html', styleUrl: './cbx-reader-preferences-component.scss' @@ -27,32 +27,32 @@ export class CbxReaderPreferencesComponent { private static readonly PROP_BACKGROUND_COLOR = 'backgroundColor'; readonly cbxSpreads = [ - {name: 'Even', key: CbxPageSpread.EVEN}, - {name: 'Odd', key: CbxPageSpread.ODD} + {name: 'Even', key: CbxPageSpread.EVEN, icon: 'pi pi-align-left'}, + {name: 'Odd', key: CbxPageSpread.ODD, icon: 'pi pi-align-right'} ]; readonly cbxViewModes = [ - {name: 'Single Page', key: CbxPageViewMode.SINGLE_PAGE}, - {name: 'Two Page', key: CbxPageViewMode.TWO_PAGE}, + {name: 'Single Page', key: CbxPageViewMode.SINGLE_PAGE, icon: 'pi pi-book'}, + {name: 'Two Page', key: CbxPageViewMode.TWO_PAGE, icon: 'pi pi-copy'}, ]; readonly cbxFitModes = [ - {name: 'Fit Page', key: CbxFitMode.FIT_PAGE}, - {name: 'Fit Width', key: CbxFitMode.FIT_WIDTH}, - {name: 'Fit Height', key: CbxFitMode.FIT_HEIGHT}, - {name: 'Actual Size', key: CbxFitMode.ACTUAL_SIZE}, - {name: 'Automatic', key: CbxFitMode.AUTO} + {name: 'Fit Page', key: CbxFitMode.FIT_PAGE, icon: 'pi pi-window-maximize'}, + {name: 'Fit Width', key: CbxFitMode.FIT_WIDTH, icon: 'pi pi-arrows-h'}, + {name: 'Fit Height', key: CbxFitMode.FIT_HEIGHT, icon: 'pi pi-arrows-v'}, + {name: 'Actual Size', key: CbxFitMode.ACTUAL_SIZE, icon: 'pi pi-expand'}, + {name: 'Automatic', key: CbxFitMode.AUTO, icon: 'pi pi-sparkles'} ]; readonly cbxScrollModes = [ - {name: 'Paginated', key: CbxScrollMode.PAGINATED}, - {name: 'Infinite', key: CbxScrollMode.INFINITE} + {name: 'Paginated', key: CbxScrollMode.PAGINATED, icon: 'pi pi-book'}, + {name: 'Infinite', key: CbxScrollMode.INFINITE, icon: 'pi pi-sort-alt'} ]; readonly cbxBackgroundColors = [ - {name: 'Gray', key: CbxBackgroundColor.GRAY}, - {name: 'Black', key: CbxBackgroundColor.BLACK}, - {name: 'White', key: CbxBackgroundColor.WHITE} + {name: 'Gray', key: CbxBackgroundColor.GRAY, color: '#808080'}, + {name: 'Black', key: CbxBackgroundColor.BLACK, color: '#000000'}, + {name: 'White', key: CbxBackgroundColor.WHITE, color: '#FFFFFF'} ]; get selectedCbxSpread(): CbxPageSpread { diff --git a/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.html b/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.html index f8820ccd..5778b2cc 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.html +++ b/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.html @@ -3,17 +3,21 @@
-
+
@for (theme of themes; track theme) { -
- - - -
+ }
@@ -27,17 +31,18 @@
-
+
@for (font of fonts; track font) { -
- - - -
+ }
@@ -51,17 +56,16 @@
-
+
@for (flow of flowOptions; track flow) { -
- - - -
+ }
@@ -75,17 +79,16 @@
-
+
@for (spread of spreadOptions; track spread) { -
- - - -
+ }
diff --git a/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.scss b/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.scss index 4c733a14..3f0c50fe 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.scss +++ b/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.scss @@ -45,16 +45,6 @@ flex-shrink: 0; min-width: 100px; } - - .radio-group { - display: flex; - gap: 1.5rem; - - @media (max-width: 768px) { - flex-direction: column; - gap: 0.75rem; - } - } } .setting-description { @@ -99,3 +89,111 @@ color: var(--p-text-color); } } + +.theme-selector { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + + .theme-option { + width: 2.5rem; + height: 2.5rem; + border-radius: 0.375rem; + border: 1.5px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + i { + color: white; + font-size: 0.875rem; + font-weight: bold; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + } + + &:hover { + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &.selected { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 25%, transparent); + } + } +} + +.button-group { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + + .option-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + border: 1.5px solid color-mix(in srgb, var(--text-color) 20%, transparent); + background: color-mix(in srgb, var(--text-color) 5%, transparent); + color: var(--text-color); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.8125rem; + font-weight: 500; + min-width: 4.5rem; + + i { + font-size: 0.875rem; + } + + .option-text { + white-space: nowrap; + } + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: color-mix(in srgb, var(--primary-color) 40%, transparent); + } + + &.selected { + border-color: var(--primary-color); + background: color-mix(in srgb, var(--primary-color) 10%, transparent); + color: var(--primary-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 15%, transparent); + + &:hover { + transform: translateY(-2px); + } + } + + &.font-button { + font-size: 1rem; + + &[data-font="serif"] .option-text { + font-family: Georgia, 'Times New Roman', serif; + } + + &[data-font="sans-serif"] .option-text { + font-family: Arial, Helvetica, sans-serif; + } + + &[data-font="roboto"] .option-text { + font-family: 'Roboto', sans-serif; + } + + &[data-font="cursive"] .option-text { + font-family: cursive; + } + + &[data-font="monospace"] .option-text { + font-family: 'Courier New', monospace; + } + } + } +} diff --git a/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.ts b/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.ts index 10fe0251..879e9183 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.ts +++ b/booklore-ui/src/app/features/settings/reader-preferences/epub-reader-preferences/epub-reader-preferences-component.ts @@ -1,16 +1,16 @@ import {Component, inject, Input} from '@angular/core'; import {Button} from 'primeng/button'; -import {RadioButton} from 'primeng/radiobutton'; import {FormsModule} from '@angular/forms'; import {ReaderPreferencesService} from '../reader-preferences-service'; import {UserSettings} from '../../user-management/user.service'; +import {Tooltip} from 'primeng/tooltip'; @Component({ selector: 'app-epub-reader-preferences-component', imports: [ Button, - RadioButton, - FormsModule + FormsModule, + Tooltip ], templateUrl: './epub-reader-preferences-component.html', styleUrl: './epub-reader-preferences-component.scss' @@ -22,29 +22,40 @@ export class EpubReaderPreferencesComponent { private readonly readerPreferencesService = inject(ReaderPreferencesService); readonly fonts = [ - {name: 'Book Default', key: null}, - {name: 'Serif', key: 'serif'}, - {name: 'Sans Serif', key: 'sans-serif'}, - {name: 'Roboto', key: 'roboto'}, - {name: 'Cursive', key: 'cursive'}, - {name: 'Monospace', key: 'monospace'} + {name: 'Book Default', displayName: 'Default', key: null}, + {name: 'Serif', displayName: 'Aa', key: 'serif'}, + {name: 'Sans Serif', displayName: 'Aa', key: 'sans-serif'}, + {name: 'Roboto', displayName: 'Aa', key: 'roboto'}, + {name: 'Cursive', displayName: 'Aa', key: 'cursive'}, + {name: 'Monospace', displayName: 'Aa', key: 'monospace'} ]; readonly flowOptions = [ - {name: 'Paginated', key: 'paginated'}, - {name: 'Scrolled', key: 'scrolled'} + {name: 'Paginated', key: 'paginated', icon: 'pi pi-book'}, + {name: 'Scrolled', key: 'scrolled', icon: 'pi pi-sort-alt'} ]; readonly spreadOptions = [ - {name: 'Single Page', key: 'single'}, - {name: 'Double Page', key: 'double'} + {name: 'Single', key: 'single', icon: 'pi pi-file'}, + {name: 'Double', key: 'double', icon: 'pi pi-copy'} ]; readonly themes = [ - {name: 'White', key: 'white'}, - {name: 'Black', key: 'black'}, - {name: 'Grey', key: 'grey'}, - {name: 'Sepia', key: 'sepia'} + {name: 'White', key: 'white', color: '#FFFFFF'}, + {name: 'Black', key: 'black', color: '#1A1A1A'}, + {name: 'Grey', key: 'grey', color: '#4B5563'}, + {name: 'Sepia', key: 'sepia', color: '#F4ECD8'}, + {name: 'Green', key: 'green', color: '#D1FAE5'}, + {name: 'Lavender', key: 'lavender', color: '#E9D5FF'}, + {name: 'Cream', key: 'cream', color: '#FEF3C7'}, + {name: 'Light Blue', key: 'light-blue', color: '#DBEAFE'}, + {name: 'Peach', key: 'peach', color: '#FECACA'}, + {name: 'Mint', key: 'mint', color: '#A7F3D0'}, + {name: 'Dark Slate', key: 'dark-slate', color: '#1E293B'}, + {name: 'Dark Olive', key: 'dark-olive', color: '#3F3F2C'}, + {name: 'Dark Purple', key: 'dark-purple', color: '#3B2F4A'}, + {name: 'Dark Teal', key: 'dark-teal', color: '#0F3D3E'}, + {name: 'Dark Brown', key: 'dark-brown', color: '#3E2723'} ]; get selectedTheme(): string | null { diff --git a/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.html b/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.html index 9aeed5c8..ec2d0b80 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.html +++ b/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.html @@ -3,17 +3,18 @@
-
+
@for (spread of spreads; track spread) { -
- - - -
+ }
@@ -27,17 +28,18 @@
-
+
@for (zoom of zooms; track zoom) { -
- - - -
+ }
diff --git a/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.scss b/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.scss index 475fa82b..e7d51a2e 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.scss +++ b/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.scss @@ -1,7 +1,7 @@ .pdf-preferences-container { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; } .setting-item { @@ -45,16 +45,6 @@ flex-shrink: 0; min-width: 100px; } - - .radio-group { - display: flex; - gap: 1.5rem; - - @media (max-width: 768px) { - flex-direction: column; - gap: 0.75rem; - } - } } .setting-description { @@ -65,37 +55,50 @@ } } -.setting-control { - flex-shrink: 0; +.button-group { display: flex; - flex-direction: column; - gap: 0.75rem; - align-items: flex-end; - - @media (max-width: 768px) { - align-items: flex-start; - width: 100%; - } -} - -.radio-group { - display: flex; - gap: 1.5rem; - - @media (max-width: 768px) { - flex-direction: column; - gap: 0.75rem; - } -} - -.radio-option { - display: flex; - align-items: center; gap: 0.5rem; + flex-wrap: wrap; - label { - font-size: 0.875rem; - color: var(--p-text-color); + .option-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + border: 1.5px solid color-mix(in srgb, var(--text-color) 20%, transparent); + background: color-mix(in srgb, var(--text-color) 5%, transparent); + color: var(--text-color); cursor: pointer; + transition: all 0.2s ease; + font-size: 0.8125rem; + font-weight: 500; + min-width: 4.5rem; + + i { + font-size: 0.875rem; + } + + .option-text { + white-space: nowrap; + } + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: color-mix(in srgb, var(--primary-color) 40%, transparent); + } + + &.selected { + border-color: var(--primary-color); + background: color-mix(in srgb, var(--primary-color) 10%, transparent); + color: var(--primary-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 15%, transparent); + + &:hover { + transform: translateY(-2px); + } + } } } diff --git a/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.ts b/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.ts index 686b3529..3e8107e9 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.ts +++ b/booklore-ui/src/app/features/settings/reader-preferences/pdf-reader-preferences/pdf-reader-preferences-component.ts @@ -1,14 +1,14 @@ import {Component, inject, Input} from '@angular/core'; -import {RadioButton} from 'primeng/radiobutton'; import {FormsModule} from '@angular/forms'; import {ReaderPreferencesService} from '../reader-preferences-service'; -import {UserSettings} from '../../user-management/user.service'; +import {PageSpread, UserSettings} from '../../user-management/user.service'; +import {TooltipModule} from 'primeng/tooltip'; @Component({ selector: 'app-pdf-reader-preferences-component', imports: [ - RadioButton, - FormsModule + FormsModule, + TooltipModule ], templateUrl: './pdf-reader-preferences-component.html', styleUrl: './pdf-reader-preferences-component.scss' @@ -18,17 +18,17 @@ export class PdfReaderPreferencesComponent { @Input() userSettings!: UserSettings; - readonly spreads = [ - {name: 'Even', key: 'even'}, - {name: 'Odd', key: 'odd'}, - {name: 'None', key: 'off'} + readonly spreads: Array<{name: string; key: PageSpread; icon: string}> = [ + {name: 'Even', key: 'even', icon: 'pi pi-align-left'}, + {name: 'Odd', key: 'odd', icon: 'pi pi-align-right'}, + {name: 'None', key: 'off', icon: 'pi pi-minus'} ]; - readonly zooms = [ - {name: 'Auto Zoom', key: 'auto'}, - {name: 'Page Fit', key: 'page-fit'}, - {name: 'Page Width', key: 'page-width'}, - {name: 'Actual Size', key: 'page-actual'} + readonly zooms: Array<{name: string; key: string; icon: string}> = [ + {name: 'Auto Zoom', key: 'auto', icon: 'pi pi-sparkles'}, + {name: 'Page Fit', key: 'page-fit', icon: 'pi pi-window-maximize'}, + {name: 'Page Width', key: 'page-width', icon: 'pi pi-arrows-h'}, + {name: 'Actual Size', key: 'page-actual', icon: 'pi pi-expand'} ]; get selectedSpread(): 'even' | 'odd' | 'off' { diff --git a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.html b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.html index 3fca93c4..252213a3 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.html +++ b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.html @@ -26,18 +26,18 @@
-
+
@for (item of scopeOptions; track item) { -
- - - -
+ }
@@ -51,18 +51,18 @@
-
+
@for (item of scopeOptions; track item) { -
- - - -
+ }
@@ -76,18 +76,18 @@
-
+
@for (item of scopeOptions; track item) { -
- - - -
+ }
diff --git a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.scss b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.scss index 7eae473e..882c3f04 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.scss +++ b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.scss @@ -147,16 +147,6 @@ flex-shrink: 0; min-width: 150px; } - - .radio-group { - display: flex; - gap: 1.5rem; - - @media (max-width: 768px) { - flex-direction: column; - gap: 0.75rem; - } - } } .setting-description { @@ -201,3 +191,51 @@ cursor: pointer; } } + +.button-group { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + + .option-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + border: 1.5px solid color-mix(in srgb, var(--text-color) 20%, transparent); + background: color-mix(in srgb, var(--text-color) 5%, transparent); + color: var(--text-color); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.8125rem; + font-weight: 500; + min-width: 4.5rem; + + i { + font-size: 0.875rem; + } + + .option-text { + white-space: nowrap; + } + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: color-mix(in srgb, var(--primary-color) 40%, transparent); + } + + &.selected { + border-color: var(--primary-color); + background: color-mix(in srgb, var(--primary-color) 10%, transparent); + color: var(--primary-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 15%, transparent); + + &:hover { + transform: translateY(-2px); + } + } + } +} diff --git a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.ts b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.ts index 2f374c48..fdc619ec 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.ts +++ b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.component.ts @@ -3,7 +3,7 @@ import {FormsModule} from '@angular/forms'; import {filter, takeUntil} from 'rxjs/operators'; import {Observable, Subject} from 'rxjs'; -import {RadioButton} from 'primeng/radiobutton'; +import {TooltipModule} from 'primeng/tooltip'; import {UserService, UserSettings, UserState} from '../user-management/user.service'; import {ReaderPreferencesService} from './reader-preferences-service'; import {EpubReaderPreferencesComponent} from './epub-reader-preferences/epub-reader-preferences-component'; @@ -15,10 +15,13 @@ import {CbxReaderPreferencesComponent} from './cbx-reader-preferences/cbx-reader templateUrl: './reader-preferences.component.html', standalone: true, styleUrls: ['./reader-preferences.component.scss'], - imports: [FormsModule, RadioButton, EpubReaderPreferencesComponent, PdfReaderPreferencesComponent, CbxReaderPreferencesComponent] + imports: [FormsModule, TooltipModule, EpubReaderPreferencesComponent, PdfReaderPreferencesComponent, CbxReaderPreferencesComponent] }) export class ReaderPreferences implements OnInit, OnDestroy { - readonly scopeOptions = ['Global', 'Individual']; + readonly scopeOptions = [ + {name: 'Global', key: 'Global', icon: 'pi pi-globe'}, + {name: 'Individual', key: 'Individual', icon: 'pi pi-user'} + ]; selectedPdfScope!: string; selectedEpubScope!: string;