mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 14:20:48 -05:00
Enhance EPUB reader with new themes and UI improvements (#1948)
Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
@@ -6,217 +6,259 @@
|
||||
}
|
||||
@if (!isLoading) {
|
||||
<div>
|
||||
<div class="epub-header">
|
||||
<div class="epub-header" [class.hidden]="!showHeader">
|
||||
<div class="header-left">
|
||||
<p-button size="small" class="menu-toggle-button" (click)="toggleDrawer()" icon="pi pi-bars" severity="secondary"></p-button>
|
||||
<div class="progress-info">
|
||||
<span class="progress-percentage"><span class="progress-label">Progress: </span>{{ progressPercentage }}%</span>
|
||||
</div>
|
||||
<p-drawer [(visible)]="isDrawerVisible" [modal]="true" [position]="'left'" header="Table of Contents">
|
||||
<div class="mb-3">
|
||||
<h3 class="text-base font-semibold mb-2">Chapters</h3>
|
||||
<ul class="chapter-list">
|
||||
@for (chapter of chapters; track chapter) {
|
||||
<li (click)="navigateToChapter(chapter); $event.stopPropagation()"
|
||||
class="chapter-item"
|
||||
[class.current-chapter]="chapter.href === currentChapterHref"
|
||||
[style.padding-left.rem]="chapter.level * 1.5 + 0.75">
|
||||
{{ chapter.label }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<p-drawer [(visible)]="isDrawerVisible" [modal]="false" [position]="'left'" [style]="{ width: '320px' }">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">Table of Contents</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3 class="text-base font-semibold mb-2">Bookmarks</h3>
|
||||
@if (bookmarks.length === 0) {
|
||||
<p class="text-sm text-gray-400 italic">No bookmarks yet</p>
|
||||
} @else {
|
||||
<ul class="bookmark-list">
|
||||
@for (bookmark of bookmarks; track bookmark.id) {
|
||||
<li class="bookmark-item">
|
||||
<div class="bookmark-content" (click)="navigateToBookmark(bookmark); $event.stopPropagation()">
|
||||
<i class="pi pi-bookmark-fill mr-2"></i>
|
||||
<span>{{ bookmark.title }}</span>
|
||||
<p-tabs value="0">
|
||||
<p-tablist>
|
||||
<p-tab value="0">
|
||||
<div class="tab-header">
|
||||
<i class="pi pi-book"></i>
|
||||
<span>Chapters</span>
|
||||
</div>
|
||||
</p-tab>
|
||||
<p-tab value="1">
|
||||
<div class="tab-header">
|
||||
<i class="pi pi-bookmark"></i>
|
||||
<span>Bookmarks</span>
|
||||
</div>
|
||||
</p-tab>
|
||||
</p-tablist>
|
||||
<p-tabpanels>
|
||||
<p-tabpanel value="0">
|
||||
<div class="tab-content">
|
||||
<ul class="chapter-list">
|
||||
@for (chapter of chapters; track chapter) {
|
||||
<li (click)="navigateToChapter(chapter); $event.stopPropagation()"
|
||||
class="chapter-item"
|
||||
[class.current-chapter]="chapter.href === currentChapterHref"
|
||||
[style.padding-left.rem]="chapter.level * 1.5 + 0.75">
|
||||
<i class="pi pi-chevron-right chapter-icon"></i>
|
||||
<span class="chapter-label">{{ chapter.label }}</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel value="1">
|
||||
<div class="tab-content">
|
||||
@if (bookmarks.length === 0) {
|
||||
<div class="empty-state">
|
||||
<i class="pi pi-bookmark"></i>
|
||||
<p>No bookmarks yet</p>
|
||||
<span>Tap the bookmark icon to save your place</span>
|
||||
</div>
|
||||
<p-button
|
||||
icon="pi pi-trash"
|
||||
(click)="deleteBookmark(bookmark.id); $event.stopPropagation()"
|
||||
[rounded]="true"
|
||||
[text]="true"
|
||||
severity="danger"
|
||||
size="small"
|
||||
pTooltip="Delete bookmark">
|
||||
</p-button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<ul class="bookmark-list">
|
||||
@for (bookmark of bookmarks; track bookmark.id) {
|
||||
<li class="bookmark-item" (click)="navigateToBookmark(bookmark); $event.stopPropagation()">
|
||||
<i class="pi pi-bookmark-fill bookmark-icon"></i>
|
||||
<span class="bookmark-label">{{ bookmark.title }}</span>
|
||||
<button
|
||||
class="bookmark-delete"
|
||||
(click)="deleteBookmark(bookmark.id); $event.stopPropagation()">
|
||||
<i class="pi pi-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
</p-tabpanels>
|
||||
</p-tabs>
|
||||
</p-drawer>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<p class="text-sm font-medium">{{ currentChapter }}</p>
|
||||
<p class="text font-medium">{{ currentChapter }}</p>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="flex gap-4">
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isBookmarked ? 'pi pi-bookmark-fill' : 'pi pi-bookmark'"
|
||||
(click)="addBookmark()"
|
||||
severity="secondary"
|
||||
[disabled]="!currentCfi || isAddingBookmark"
|
||||
pTooltip="Add bookmark">
|
||||
</p-button>
|
||||
@if (!locationsReady) {
|
||||
<div class="location-indicator" pTooltip="Saving progress not ready yet">
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
}
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isBookmarked ? 'pi pi-bookmark-fill' : 'pi pi-bookmark'"
|
||||
(click)="addBookmark()"
|
||||
severity="secondary"
|
||||
[disabled]="!currentCfi || isAddingBookmark">
|
||||
</p-button>
|
||||
<div>
|
||||
<p-button size="small" class="settings-toggle-button" (click)="toggleSettingsDrawer()" icon="pi pi-cog" severity="secondary"></p-button>
|
||||
<p-drawer [(visible)]="isSettingsDrawerVisible" [modal]="true" [position]="'right'" header="Settings">
|
||||
<div class="flex flex-col gap-4 max-w-md mx-auto">
|
||||
<p-drawer [(visible)]="isSettingsDrawerVisible" [modal]="false" [position]="'right'" [style]="{ width: '340px' }">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">Reader Settings</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="font-semibold text-gray-200">Font Size:</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<p-button size="small" icon="pi pi-minus" (click)="decreaseFontSize()" severity="info"></p-button>
|
||||
<span class="text-gray-100">{{ fontSize }}%</span>
|
||||
<p-button size="small" icon="pi pi-plus" (click)="increaseFontSize()" severity="info"></p-button>
|
||||
<div class="settings-container">
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-header">
|
||||
<i class="pi pi-text-height"></i>
|
||||
<span>Typography</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-row">
|
||||
<label>Font Size</label>
|
||||
<div class="font-size-controls">
|
||||
<button class="control-btn" (click)="decreaseFontSize()">
|
||||
<i class="pi pi-minus"></i>
|
||||
</button>
|
||||
<span class="font-size-value">{{ fontSize }}%</span>
|
||||
<button class="control-btn" (click)="increaseFontSize()">
|
||||
<i class="pi pi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label for="font-type">Font Family</label>
|
||||
<p-select
|
||||
appendTo="body"
|
||||
id="font-type"
|
||||
[options]="fontTypes"
|
||||
[(ngModel)]="selectedFontType"
|
||||
(onChange)="changeFontType()"
|
||||
[style]="{ width: '100%' }"
|
||||
placeholder="Select font">
|
||||
</p-select>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>Line Height</label>
|
||||
<div class="slider-container">
|
||||
<span class="slider-value">{{ lineHeight }}</span>
|
||||
<p-slider
|
||||
[(ngModel)]="lineHeight"
|
||||
[min]="1"
|
||||
[max]="2.5"
|
||||
[step]="0.1"
|
||||
(onSlideEnd)="updateThemeStyle()"
|
||||
[style]="{ width: '100%' }">
|
||||
</p-slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>Letter Spacing</label>
|
||||
<div class="slider-container">
|
||||
<span class="slider-value">{{ letterSpacing ?? 0 }} em</span>
|
||||
<p-slider
|
||||
[(ngModel)]="letterSpacing"
|
||||
[min]="0"
|
||||
[max]="0.2"
|
||||
[step]="0.01"
|
||||
(onSlideEnd)="updateThemeStyle()"
|
||||
[style]="{ width: '100%' }">
|
||||
</p-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="font-type" class="block font-semibold text-gray-200">Font:</label>
|
||||
<p-select
|
||||
id="font-type"
|
||||
[options]="fontTypes"
|
||||
[(ngModel)]="selectedFontType"
|
||||
(onChange)="changeFontType()"
|
||||
class="w-full"
|
||||
placeholder="Select font type">
|
||||
</p-select>
|
||||
<div class="settings-card">
|
||||
<div class="card-header">
|
||||
<i class="pi pi-palette"></i>
|
||||
<span>Appearance</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-row">
|
||||
<label>Theme</label>
|
||||
<div class="theme-selector">
|
||||
@for (theme of themes; track theme) {
|
||||
<button
|
||||
type="button"
|
||||
class="theme-option"
|
||||
[class.selected]="selectedTheme === theme.value"
|
||||
[style.background-color]="getThemeColor(theme.value)"
|
||||
(click)="selectTheme(theme.value)"
|
||||
[attr.aria-label]="theme.label"
|
||||
[pTooltip]="theme.label"
|
||||
tooltipPosition="bottom">
|
||||
@if (selectedTheme === theme.value) {
|
||||
<i class="pi pi-check"></i>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-divider></p-divider>
|
||||
<div class="settings-card">
|
||||
<div class="card-header">
|
||||
<i class="pi pi-file"></i>
|
||||
<span>Layout</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-row">
|
||||
<label>Reading Mode</label>
|
||||
<div class="toggle-group">
|
||||
<button
|
||||
class="toggle-option"
|
||||
[class.active]="selectedFlow === 'paginated'"
|
||||
(click)="selectedFlow = 'paginated'; changeScrollMode()">
|
||||
<i class="pi pi-book"></i>
|
||||
<span>Paginated</span>
|
||||
</button>
|
||||
<button
|
||||
class="toggle-option"
|
||||
[class.active]="selectedFlow === 'scrolled'"
|
||||
(click)="selectedFlow = 'scrolled'; changeScrollMode()">
|
||||
<i class="pi pi-align-justify"></i>
|
||||
<span>Scrolled</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="block font-semibold text-gray-200">Theme:</label>
|
||||
<div class="flex gap-3 mt-1" role="radiogroup" aria-label="Theme selection">
|
||||
@for (theme of themes; track theme) {
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 rounded-full border-2 transition-all"
|
||||
[class.border-blue-600]="selectedTheme === theme.value"
|
||||
[style.background-color]="getThemeColor(theme.value)"
|
||||
(click)="selectTheme(theme.value)"
|
||||
[attr.aria-pressed]="selectedTheme === theme.value"
|
||||
[attr.aria-label]="theme.label">
|
||||
</button>
|
||||
@if (selectedFlow === 'paginated' && !isMobileDevice()) {
|
||||
<div class="setting-row">
|
||||
<label>Page Spread</label>
|
||||
<div class="toggle-group">
|
||||
<button
|
||||
class="toggle-option"
|
||||
[class.active]="selectedSpread === 'single'"
|
||||
(click)="selectedSpread = 'single'; changeSpreadMode()">
|
||||
<i class="pi pi-file"></i>
|
||||
<span>Single</span>
|
||||
</button>
|
||||
<button
|
||||
class="toggle-option"
|
||||
[class.active]="selectedSpread === 'double'"
|
||||
(click)="selectedSpread = 'double'; changeSpreadMode()">
|
||||
<i class="pi pi-copy"></i>
|
||||
<span>Double</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label id="flow-label" class="block font-semibold text-gray-200">Flow:</label>
|
||||
<div class="flex gap-4" role="radiogroup" aria-labelledby="flow-label">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p-radioButton
|
||||
name="flow"
|
||||
[value]="'paginated'"
|
||||
[(ngModel)]="selectedFlow"
|
||||
(onClick)="changeScrollMode()"
|
||||
inputId="flow-paginated">
|
||||
</p-radioButton>
|
||||
<label for="flow-paginated" class="cursor-pointer select-none text-gray-200">Paginated</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p-radioButton
|
||||
name="flow"
|
||||
[value]="'scrolled'"
|
||||
[(ngModel)]="selectedFlow"
|
||||
(onClick)="changeScrollMode()"
|
||||
inputId="flow-scrolled">
|
||||
</p-radioButton>
|
||||
<label for="flow-scrolled" class="cursor-pointer select-none text-gray-200">Scrolled</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (selectedFlow === 'paginated' && !isMobileDevice()) {
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label id="spread-label" class="block font-semibold text-gray-200">Page Spread:</label>
|
||||
<div class="flex gap-4" role="radiogroup" aria-labelledby="spread-label">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p-radioButton
|
||||
name="spread"
|
||||
[value]="'single'"
|
||||
[(ngModel)]="selectedSpread"
|
||||
(onClick)="changeSpreadMode()"
|
||||
inputId="spread-single">
|
||||
</p-radioButton>
|
||||
<label for="spread-single" class="cursor-pointer select-none text-gray-200">Single</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p-radioButton
|
||||
name="spread"
|
||||
[value]="'double'"
|
||||
[(ngModel)]="selectedSpread"
|
||||
(onClick)="changeSpreadMode()"
|
||||
inputId="spread-auto">
|
||||
</p-radioButton>
|
||||
<label for="spread-auto" class="cursor-pointer select-none text-gray-200">Double</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div>
|
||||
<label class="block mb-1 font-semibold text-gray-200 pb-2">Line Height: {{ lineHeight }}</label>
|
||||
<p-slider
|
||||
[(ngModel)]="lineHeight"
|
||||
[min]="1"
|
||||
[max]="2.5"
|
||||
[step]="0.1"
|
||||
(onSlideEnd)="updateThemeStyle()"
|
||||
class="w-full"
|
||||
aria-label="Line Height">
|
||||
</p-slider>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block mb-1 font-semibold text-gray-200 pb-2 pt-2">Letter Spacing: {{ letterSpacing }}em</label>
|
||||
<p-slider
|
||||
[(ngModel)]="letterSpacing"
|
||||
[min]="0"
|
||||
[max]="0.2"
|
||||
[step]="0.01"
|
||||
(onSlideEnd)="updateThemeStyle()"
|
||||
class="w-full"
|
||||
aria-label="Letter Spacing">
|
||||
</p-slider>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</p-drawer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="epubContainer" #epubContainer></div>
|
||||
<div id="epubContainer" #epubContainer (click)="onBookClick($event)" (mousemove)="onBookMouseMove($event)"></div>
|
||||
@if (selectedFlow !== 'scrolled') {
|
||||
<button class="epub-controls-left" [class.visible]="showControls" [class.dark-theme]="selectedTheme === 'black' || selectedTheme === 'grey'" (click)="prevPage(); onBookTouch()">←</button>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, any>([
|
||||
['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;
|
||||
|
||||
|
||||
@@ -3,17 +3,16 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Page Spread</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (spread of cbxSpreads; track spread) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="spread.key"
|
||||
name="spread"
|
||||
[value]="spread.key"
|
||||
[(ngModel)]="selectedCbxSpread">
|
||||
</p-radiobutton>
|
||||
<label [for]="spread.key">{{ spread.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedCbxSpread === spread.key"
|
||||
(click)="selectedCbxSpread = spread.key">
|
||||
<i [class]="spread.icon"></i>
|
||||
<span class="option-text">{{ spread.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,17 +26,16 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Page Layout</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (mode of cbxViewModes; track mode) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="mode.key"
|
||||
name="zoom"
|
||||
[value]="mode.key"
|
||||
[(ngModel)]="selectedCbxViewMode">
|
||||
</p-radiobutton>
|
||||
<label [for]="mode.key">{{ mode.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedCbxViewMode === mode.key"
|
||||
(click)="selectedCbxViewMode = mode.key">
|
||||
<i [class]="mode.icon"></i>
|
||||
<span class="option-text">{{ mode.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,17 +49,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Fit Mode</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (mode of cbxFitModes; track mode) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="mode.key"
|
||||
name="fitMode"
|
||||
[value]="mode.key"
|
||||
[(ngModel)]="selectedCbxFitMode">
|
||||
</p-radiobutton>
|
||||
<label [for]="mode.key">{{ mode.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedCbxFitMode === mode.key"
|
||||
(click)="selectedCbxFitMode = mode.key"
|
||||
[pTooltip]="mode.name"
|
||||
tooltipPosition="bottom">
|
||||
<i [class]="mode.icon"></i>
|
||||
<span class="option-text">{{ mode.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,17 +74,16 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Scroll Mode</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (mode of cbxScrollModes; track mode) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="mode.key"
|
||||
name="scrollMode"
|
||||
[value]="mode.key"
|
||||
[(ngModel)]="selectedCbxScrollMode">
|
||||
</p-radiobutton>
|
||||
<label [for]="mode.key">{{ mode.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedCbxScrollMode === mode.key"
|
||||
(click)="selectedCbxScrollMode = mode.key">
|
||||
<i [class]="mode.icon"></i>
|
||||
<span class="option-text">{{ mode.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,17 +97,21 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Background Color</label>
|
||||
<div class="radio-group">
|
||||
<div class="theme-selector">
|
||||
@for (color of cbxBackgroundColors; track color) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="color.key"
|
||||
name="backgroundColor"
|
||||
[value]="color.key"
|
||||
[(ngModel)]="selectedCbxBackgroundColor">
|
||||
</p-radiobutton>
|
||||
<label [for]="color.key">{{ color.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-option"
|
||||
[class.selected]="selectedCbxBackgroundColor === color.key"
|
||||
[style.background-color]="color.color"
|
||||
(click)="selectedCbxBackgroundColor = color.key"
|
||||
[attr.aria-label]="color.name"
|
||||
[pTooltip]="color.name"
|
||||
tooltipPosition="bottom">
|
||||
@if (selectedCbxBackgroundColor === color.key) {
|
||||
<i class="pi pi-check"></i>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Theme</label>
|
||||
<div class="radio-group">
|
||||
<div class="theme-selector">
|
||||
@for (theme of themes; track theme) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'theme-' + theme.key"
|
||||
name="theme"
|
||||
[value]="theme.key"
|
||||
[(ngModel)]="selectedTheme">
|
||||
</p-radiobutton>
|
||||
<label [for]="'theme-' + theme.key">{{ theme.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-option"
|
||||
[class.selected]="selectedTheme === theme.key"
|
||||
[style.background-color]="theme.color"
|
||||
(click)="selectedTheme = theme.key"
|
||||
[attr.aria-label]="theme.name"
|
||||
[pTooltip]="theme.name"
|
||||
tooltipPosition="bottom">
|
||||
@if (selectedTheme === theme.key) {
|
||||
<i class="pi pi-check"></i>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,17 +31,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Font</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (font of fonts; track font) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'font-' + font.key"
|
||||
name="font"
|
||||
[value]="font.key"
|
||||
[(ngModel)]="selectedFont">
|
||||
</p-radiobutton>
|
||||
<label [for]="'font-' + font.key">{{ font.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button font-button"
|
||||
[class.selected]="selectedFont === font.key"
|
||||
[attr.data-font]="font.key"
|
||||
(click)="selectedFont = font.key"
|
||||
[pTooltip]="font.name"
|
||||
tooltipPosition="bottom">
|
||||
<span class="option-text">{{ font.displayName }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,17 +56,16 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Flow</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (flow of flowOptions; track flow) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'flow-' + flow.key"
|
||||
name="flow"
|
||||
[value]="flow.key"
|
||||
[(ngModel)]="selectedFlow">
|
||||
</p-radiobutton>
|
||||
<label [for]="'flow-' + flow.key">{{ flow.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedFlow === flow.key"
|
||||
(click)="selectedFlow = flow.key">
|
||||
<i [class]="flow.icon"></i>
|
||||
<span class="option-text">{{ flow.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,17 +79,16 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Page Spread</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (spread of spreadOptions; track spread) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'spread-' + spread.key"
|
||||
name="spread"
|
||||
[value]="spread.key"
|
||||
[(ngModel)]="selectedSpread">
|
||||
</p-radiobutton>
|
||||
<label [for]="'spread-' + spread.key">{{ spread.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedSpread === spread.key"
|
||||
(click)="selectedSpread = spread.key">
|
||||
<i [class]="spread.icon"></i>
|
||||
<span class="option-text">{{ spread.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Page Spread</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (spread of spreads; track spread) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="spread.key"
|
||||
name="spread"
|
||||
[value]="spread.key"
|
||||
[(ngModel)]="selectedSpread">
|
||||
</p-radiobutton>
|
||||
<label [for]="spread.key">{{ spread.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedSpread === spread.key"
|
||||
(click)="selectedSpread = spread.key"
|
||||
[pTooltip]="spread.name"
|
||||
tooltipPosition="bottom">
|
||||
<i [class]="spread.icon"></i>
|
||||
<span class="option-text">{{ spread.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,17 +28,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Page Zoom</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (zoom of zooms; track zoom) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="zoom.key"
|
||||
name="zoom"
|
||||
[value]="zoom.key"
|
||||
[(ngModel)]="selectedZoom">
|
||||
</p-radiobutton>
|
||||
<label [for]="zoom.key">{{ zoom.name }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedZoom === zoom.key"
|
||||
(click)="selectedZoom = zoom.key"
|
||||
[pTooltip]="zoom.name"
|
||||
tooltipPosition="bottom">
|
||||
<i [class]="zoom.icon"></i>
|
||||
<span class="option-text">{{ zoom.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' {
|
||||
|
||||
@@ -26,18 +26,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">PDF Reader Settings</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (item of scopeOptions; track item) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'pdf-' + item"
|
||||
name="pdfScope"
|
||||
[value]="item"
|
||||
[(ngModel)]="selectedPdfScope"
|
||||
(ngModelChange)="onPdfScopeChange()">
|
||||
</p-radiobutton>
|
||||
<label [for]="'pdf-' + item">{{ item }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedPdfScope === item.key"
|
||||
(click)="selectedPdfScope = item.key; onPdfScopeChange()"
|
||||
[pTooltip]="item.name"
|
||||
tooltipPosition="bottom">
|
||||
<i [class]="item.icon"></i>
|
||||
<span class="option-text">{{ item.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,18 +51,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">EPUB Reader Settings</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (item of scopeOptions; track item) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'epub-' + item"
|
||||
name="epubScope"
|
||||
[value]="item"
|
||||
[(ngModel)]="selectedEpubScope"
|
||||
(ngModelChange)="onEpubScopeChange()">
|
||||
</p-radiobutton>
|
||||
<label [for]="'epub-' + item">{{ item }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedEpubScope === item.key"
|
||||
(click)="selectedEpubScope = item.key; onEpubScopeChange()"
|
||||
[pTooltip]="item.name"
|
||||
tooltipPosition="bottom">
|
||||
<i [class]="item.icon"></i>
|
||||
<span class="option-text">{{ item.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,18 +76,18 @@
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">CBX Reader Settings</label>
|
||||
<div class="radio-group">
|
||||
<div class="button-group">
|
||||
@for (item of scopeOptions; track item) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'cbx-' + item"
|
||||
name="cbxScope"
|
||||
[value]="item"
|
||||
[(ngModel)]="selectedCbxScope"
|
||||
(ngModelChange)="onCbxScopeChange()">
|
||||
</p-radiobutton>
|
||||
<label [for]="'cbx-' + item">{{ item }}</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="option-button"
|
||||
[class.selected]="selectedCbxScope === item.key"
|
||||
(click)="selectedCbxScope = item.key; onCbxScopeChange()"
|
||||
[pTooltip]="item.name"
|
||||
tooltipPosition="bottom">
|
||||
<i [class]="item.icon"></i>
|
||||
<span class="option-text">{{ item.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user