@if (!isLoading) {
@if (pages.length > 0) {
- @for (url of imageUrls; track url) {
-
![Page Image]()
- }
+
+ @for (url of imageUrls; track url) {
+
![Page Image]()
+ }
+
} @else {
-
No pages available.
+
}
} @else {
-
-
-
Processing pages... This may take a few seconds on first load. Future loads will be significantly faster.
+
+
+
Processing pages... This may take a few seconds on first load. Future loads will be significantly faster.
}
diff --git a/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.scss b/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.scss
index 325300c5f..acbcef4db 100644
--- a/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.scss
+++ b/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.scss
@@ -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;
- }
}
diff --git a/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.ts b/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.ts
index af0618cd1..77dea36a7 100644
--- a/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.ts
+++ b/booklore-ui/src/app/book/components/cbx-reader/cbx-reader.component.ts
@@ -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;