Enhance EPUB reader with new themes and UI improvements (#1948)

Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
ACX
2025-12-20 14:37:52 -07:00
committed by GitHub
parent bfa08054b5
commit ab36fa4ab6
16 changed files with 1586 additions and 540 deletions

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

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

View File

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

View File

@@ -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;
}
}
}
}

View File

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

View File

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

View File

@@ -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);
}
}
}
}

View File

@@ -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' {

View File

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

View File

@@ -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);
}
}
}
}

View File

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