CBX reader styling improvements (#977)

This commit is contained in:
Aditya Chandel
2025-08-24 12:01:11 -06:00
committed by GitHub
parent e5d54d1caa
commit 2be27f34ad
3 changed files with 319 additions and 115 deletions

View File

@@ -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">&lt;&lt;</button>
<button (click)="previousPage()" [disabled]="currentPage === 0">&lt;</button>
<span>
Page {{ currentPage + 1 }}
{{ isTwoPageView && imageUrls.length > 1 ? '-' + (currentPage + 2) : '' }} of {{ pages.length }}
</span>
<button (click)="nextPage()" [disabled]="currentPage >= pages.length - 1">&gt;</button>
<button class="ml-2" (click)="goToPage(pages.length)" [disabled]="currentPage >= pages.length - 1">&gt;&gt;</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>

View File

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

View File

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