mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
CBX reader styling improvements (#977)
This commit is contained in:
@@ -1,56 +1,80 @@
|
||||
<div class="comic-reader-container">
|
||||
<div class="comic-reader-container" tabindex="0">
|
||||
<div class="navigation">
|
||||
<div class="goto-page-controls" style="display: flex; align-items: center; margin-right: auto;">
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="goToPageInput"
|
||||
[min]="1"
|
||||
[max]="pages.length"
|
||||
placeholder="Page"
|
||||
/>
|
||||
<button
|
||||
(click)="goToPageInput !== null && goToPage(goToPageInput)"
|
||||
[disabled]="goToPageInput === null || goToPageInput < 1 || goToPageInput > pages.length">
|
||||
Go
|
||||
<div class="goto-page-controls">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="goToPageInput"
|
||||
[min]="1"
|
||||
[max]="pages.length"
|
||||
placeholder="Page"
|
||||
class="page-input"
|
||||
/>
|
||||
<button
|
||||
class="go-button"
|
||||
(click)="goToPageInput !== null && goToPage(goToPageInput)"
|
||||
[disabled]="goToPageInput === null || goToPageInput < 1 || goToPageInput > pages.length">
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-controls">
|
||||
<button class="nav-button first-page" (click)="goToPage(1)" [disabled]="currentPage === 0" title="First Page">
|
||||
<span>⟨⟨</span>
|
||||
</button>
|
||||
<button class="nav-button prev-page" (click)="previousPage()" [disabled]="currentPage === 0" title="Previous Page">
|
||||
<span>⟨</span>
|
||||
</button>
|
||||
<div class="page-info">
|
||||
<span class="current-page">{{ currentPage + 1 }}</span>
|
||||
<span class="page-separator">{{ isTwoPageView && imageUrls.length > 1 ? '-' + (currentPage + 2) : '' }}</span>
|
||||
<span class="total-pages"> of {{ pages.length }}</span>
|
||||
</div>
|
||||
<button class="nav-button next-page" (click)="nextPage()" [disabled]="currentPage >= pages.length - 1" title="Next Page">
|
||||
<span>⟩</span>
|
||||
</button>
|
||||
<button class="nav-button last-page" (click)="goToPage(pages.length)" [disabled]="currentPage >= pages.length - 1" title="Last Page">
|
||||
<span>⟩⟩</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="page-controls" style="display: flex; align-items: center; justify-content: center; flex-grow: 1;">
|
||||
<button class="mr-2" (click)="goToPage(1)" [disabled]="currentPage === 0"><<</button>
|
||||
<button (click)="previousPage()" [disabled]="currentPage === 0"><</button>
|
||||
<span>
|
||||
Page {{ currentPage + 1 }}
|
||||
{{ isTwoPageView && imageUrls.length > 1 ? '-' + (currentPage + 2) : '' }} of {{ pages.length }}
|
||||
</span>
|
||||
<button (click)="nextPage()" [disabled]="currentPage >= pages.length - 1">></button>
|
||||
<button class="ml-2" (click)="goToPage(pages.length)" [disabled]="currentPage >= pages.length - 1">>></button>
|
||||
</div>
|
||||
|
||||
<div class="spread-controls" style="display: flex; align-items: center; margin-left: auto;">
|
||||
<div class="spread-controls">
|
||||
@if (isTwoPageView) {
|
||||
<button (click)="toggleSpreadDirection()" title="Toggle Spread Direction">
|
||||
{{ pageSpread === 'ODD' ? '⬅️' : '➡️' }}
|
||||
<button class="view-button spread-toggle" (click)="toggleSpreadDirection()" title="Toggle Spread Direction">
|
||||
<span>{{ pageSpread === 'ODD' ? '⬅️' : '➡️' }}</span>
|
||||
</button>
|
||||
}
|
||||
<button (click)="toggleView()" title="Toggle View">
|
||||
{{ isTwoPageView ? '1️⃣' : '2️⃣' }}
|
||||
<button class="view-button layout-toggle" (click)="toggleView()" title="Toggle View">
|
||||
<span>{{ isTwoPageView ? '📄' : '📖' }}</span>
|
||||
</button>
|
||||
<button class="view-button background-toggle" (click)="toggleBackground()" title="Toggle Background Color">
|
||||
<span>{{ backgroundColorIcon }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-container" [class.two-page-view]="isTwoPageView">
|
||||
<div class="image-container"
|
||||
[class.two-page-view]="isTwoPageView"
|
||||
[class.bg-black]="backgroundColor === 'black'"
|
||||
[class.bg-gray]="backgroundColor === 'gray'"
|
||||
[class.bg-white]="backgroundColor === 'white'">
|
||||
@if (!isLoading) {
|
||||
@if (pages.length > 0) {
|
||||
@for (url of imageUrls; track url) {
|
||||
<img [src]="url" alt="Page Image"/>
|
||||
}
|
||||
<div class="pages-wrapper">
|
||||
@for (url of imageUrls; track url) {
|
||||
<img [src]="url" alt="Page Image" class="page-image"/>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p>No pages available.</p>
|
||||
<div class="no-pages">
|
||||
<p>No pages available.</p>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%;">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
<p style="margin-top: 1rem; color: #ccc;">Processing pages... This may take a few seconds on first load. Future loads will be significantly faster.</p>
|
||||
<div class="loading-container">
|
||||
<p-progressSpinner styleClass="custom-spinner"></p-progressSpinner>
|
||||
<p class="loading-text">Processing pages... This may take a few seconds on first load. Future loads will be significantly faster.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,115 +1,267 @@
|
||||
.comic-reader-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background: #1a1a1a;
|
||||
color: #ffffff;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
margin: 1rem;
|
||||
background: linear-gradient(135deg, #2d2d2d 0%, #1f1f1f 100%);
|
||||
border-bottom: 1px solid #404040;
|
||||
padding: 0.25rem 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1000;
|
||||
min-width: 0;
|
||||
|
||||
.goto-page-controls {
|
||||
flex: 0 0 auto;
|
||||
order: 1;
|
||||
min-width: fit-content;
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
background: rgba(51, 51, 51, 0.9);
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
border: 1px solid #555;
|
||||
|
||||
.page-input {
|
||||
width: 55px;
|
||||
padding: 4px 6px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: #404040;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: #4a4a4a;
|
||||
box-shadow: 0 0 0 1px rgba(74, 144, 226, 0.5);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.go-button {
|
||||
padding: 4px 8px;
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #357abd;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-controls {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
order: 2;
|
||||
min-width: 0;
|
||||
|
||||
span {
|
||||
min-width: 8rem;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0 0.25rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
.nav-button {
|
||||
width: 24px;
|
||||
height: 28px;
|
||||
border: 1px solid #555;
|
||||
background: rgba(51, 51, 51, 0.9);
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #4a4a4a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
color: #999;
|
||||
background: #2a2a2a;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
border-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.page-info {
|
||||
margin: 0 0.5rem;
|
||||
padding: 2px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
|
||||
.current-page {
|
||||
color: #4a90e2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-separator {
|
||||
color: #4a90e2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-pages {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spread-controls {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
order: 3;
|
||||
min-width: fit-content;
|
||||
|
||||
button {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
.view-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #555;
|
||||
background: rgba(51, 51, 51, 0.9);
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #4a4a4a;
|
||||
border-color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
padding: 0 1rem;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
background: #0f0f0f;
|
||||
min-height: 0;
|
||||
|
||||
&.bg-black {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
&.bg-gray {
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
&.bg-white {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pages-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.two-page-view {
|
||||
img {
|
||||
.page-image {
|
||||
max-width: calc(50% - 0.5rem);
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.two-page-view) {
|
||||
img {
|
||||
.page-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
.page-image {
|
||||
height: auto;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
flex-shrink: 1;
|
||||
margin: 0;
|
||||
display: block;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.no-pages {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
text-align: center;
|
||||
|
||||
.loading-text {
|
||||
color: #ccc;
|
||||
font-size: 16px;
|
||||
max-width: 400px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goto-page-controls {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.goto-page-controls input[type="number"] {
|
||||
width: 40px;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.goto-page-controls button:disabled {
|
||||
cursor: default;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {Component, HostListener, inject, OnInit} from '@angular/core';
|
||||
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {CbxReaderService} from '../../service/cbx-reader.service';
|
||||
import {BookService} from '../../service/book.service';
|
||||
@@ -31,6 +30,8 @@ export class CbxReaderComponent implements OnInit {
|
||||
pageSpread: CbxPageSpread | PdfPageSpread = CbxPageSpread.ODD;
|
||||
pageViewMode: CbxPageViewMode | PdfPageViewMode = CbxPageViewMode.SINGLE_PAGE;
|
||||
|
||||
backgroundColor: 'black' | 'gray' | 'white' = 'gray';
|
||||
|
||||
private touchStartX = 0;
|
||||
private touchEndX = 0;
|
||||
|
||||
@@ -111,6 +112,29 @@ export class CbxReaderComponent implements OnInit {
|
||||
return this.pageViewMode === CbxPageViewMode.TWO_PAGE || this.pageViewMode === PdfPageViewMode.TWO_PAGE;
|
||||
}
|
||||
|
||||
get backgroundColorIcon(): string {
|
||||
switch (this.backgroundColor) {
|
||||
case 'black': return '⚫';
|
||||
case 'gray': return '🔘';
|
||||
case 'white': return '⚪';
|
||||
default: return '🔘';
|
||||
}
|
||||
}
|
||||
|
||||
toggleBackground(): void {
|
||||
switch (this.backgroundColor) {
|
||||
case 'black':
|
||||
this.backgroundColor = 'gray';
|
||||
break;
|
||||
case 'gray':
|
||||
this.backgroundColor = 'white';
|
||||
break;
|
||||
case 'white':
|
||||
this.backgroundColor = 'black';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toggleView() {
|
||||
if (!this.isTwoPageView && this.isPhonePortrait()) return;
|
||||
this.pageViewMode = this.isTwoPageView ? (this.bookType === "CBX" ? CbxPageViewMode.SINGLE_PAGE : PdfPageViewMode.SINGLE_PAGE) : (this.bookType === "CBX" ? CbxPageViewMode.TWO_PAGE : PdfPageViewMode.TWO_PAGE);
|
||||
@@ -222,7 +246,10 @@ export class CbxReaderComponent implements OnInit {
|
||||
get imageUrls(): string[] {
|
||||
if (!this.pages.length) return [];
|
||||
|
||||
const urls = [this.getPageImageUrl(this.currentPage)];
|
||||
const urls: string[] = [];
|
||||
|
||||
urls.push(this.getPageImageUrl(this.currentPage));
|
||||
|
||||
if (this.isTwoPageView && this.currentPage + 1 < this.pages.length) {
|
||||
urls.push(this.getPageImageUrl(this.currentPage + 1));
|
||||
}
|
||||
@@ -233,17 +260,17 @@ export class CbxReaderComponent implements OnInit {
|
||||
private updateViewerSetting(): void {
|
||||
const bookSetting: BookSetting = this.bookType === "CBX"
|
||||
? {
|
||||
cbxSettings: {
|
||||
pageSpread: this.pageSpread as CbxPageSpread,
|
||||
pageViewMode: this.pageViewMode as CbxPageViewMode,
|
||||
}
|
||||
cbxSettings: {
|
||||
pageSpread: this.pageSpread as CbxPageSpread,
|
||||
pageViewMode: this.pageViewMode as CbxPageViewMode,
|
||||
}
|
||||
}
|
||||
: {
|
||||
newPdfSettings: {
|
||||
pageSpread: this.pageSpread as PdfPageSpread,
|
||||
pageViewMode: this.pageViewMode as PdfPageViewMode,
|
||||
}
|
||||
};
|
||||
newPdfSettings: {
|
||||
pageSpread: this.pageSpread as PdfPageSpread,
|
||||
pageViewMode: this.pageViewMode as PdfPageViewMode,
|
||||
}
|
||||
};
|
||||
this.bookService.updateViewerSetting(bookSetting, this.bookId).subscribe();
|
||||
}
|
||||
|
||||
@@ -252,13 +279,14 @@ export class CbxReaderComponent implements OnInit {
|
||||
? Math.round(((this.currentPage + 1) / this.pages.length) * 1000) / 10
|
||||
: 0;
|
||||
|
||||
if(this.bookType === 'CBX') {
|
||||
if (this.bookType === 'CBX') {
|
||||
this.bookService.saveCbxProgress(this.bookId, this.currentPage + 1, percentage).subscribe();
|
||||
}
|
||||
if(this.bookType === 'PDF') {
|
||||
if (this.bookType === 'PDF') {
|
||||
this.bookService.savePdfProgress(this.bookId, this.currentPage + 1, percentage).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
goToPage(page: number): void {
|
||||
if (page < 1 || page > this.pages.length) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user