From 6eeb8e7580957e37b2ddaa98d36dfbb172fee4cf Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 27 Jul 2025 20:36:55 +0800 Subject: [PATCH] layout: fix insets for double borders and add book spine decorator (#1688) --- .../src/app/library/components/BookItem.tsx | 11 +++-- .../src/app/reader/components/BooksGrid.tsx | 2 +- .../app/reader/components/DoubleBorder.tsx | 32 +++++++-------- apps/readest-app/src/components/BookCover.tsx | 41 ++++++++++++++++--- apps/readest-app/src/styles/globals.css | 33 +++++++++++++++ 5 files changed, 94 insertions(+), 25 deletions(-) diff --git a/apps/readest-app/src/app/library/components/BookItem.tsx b/apps/readest-app/src/app/library/components/BookItem.tsx index b5b9b14d..1dd1257a 100644 --- a/apps/readest-app/src/app/library/components/BookItem.tsx +++ b/apps/readest-app/src/app/library/components/BookItem.tsx @@ -61,12 +61,17 @@ const BookItem: React.FC = ({ >
- + {bookSelected && (
)} diff --git a/apps/readest-app/src/app/reader/components/BooksGrid.tsx b/apps/readest-app/src/app/reader/components/BooksGrid.tsx index 96ce2f8a..c371fd9f 100644 --- a/apps/readest-app/src/app/reader/components/BooksGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BooksGrid.tsx @@ -153,7 +153,7 @@ const BooksGrid: React.FC = ({ bookKeys, onCloseBook }) => { showFooter={showFooter} borderColor={viewSettings.borderColor} horizontalGap={horizontalGapPercent} - contentInsets={contentInsets} + insets={viewInsets} /> )} {showHeader && ( diff --git a/apps/readest-app/src/app/reader/components/DoubleBorder.tsx b/apps/readest-app/src/app/reader/components/DoubleBorder.tsx index 7dab3d0d..a1972985 100644 --- a/apps/readest-app/src/app/reader/components/DoubleBorder.tsx +++ b/apps/readest-app/src/app/reader/components/DoubleBorder.tsx @@ -5,7 +5,7 @@ interface DoubleBorderProps { horizontalGap: number; showHeader: boolean; showFooter: boolean; - contentInsets: Insets; + insets: Insets; } const paddingPx = 10; @@ -14,7 +14,7 @@ const DoubleBorder: React.FC = ({ borderColor, showHeader, showFooter, - contentInsets, + insets, }) => { return (
@@ -23,10 +23,10 @@ const DoubleBorder: React.FC = ({ className={'borderframe pointer-events-none absolute'} style={{ border: `4px solid ${borderColor}`, - height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px + ${paddingPx * 2}px)`, - top: `calc(${contentInsets.top}px - ${paddingPx}px)`, - left: `calc(${contentInsets.left}px - ${paddingPx}px)`, - right: `calc(${contentInsets.right}px - ${paddingPx}px)`, + height: `calc(100% - ${insets.top + insets.bottom}px + ${paddingPx * 2}px)`, + top: `calc(${insets.top}px - ${paddingPx}px)`, + left: `calc(${insets.left}px - ${paddingPx}px)`, + right: `calc(${insets.right}px - ${paddingPx}px)`, }} >
{/* inner frame */} @@ -34,10 +34,10 @@ const DoubleBorder: React.FC = ({ className={'borderframe pointer-events-none absolute'} style={{ border: `1px solid ${borderColor}`, - height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`, - top: `${contentInsets.top}px`, - left: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px`, - right: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px`, + height: `calc(100% - ${insets.top + insets.bottom}px)`, + top: `${insets.top}px`, + left: `calc(${insets.left + (showFooter ? 32 : 0)}px`, + right: `calc(${insets.right + (showHeader ? 32 : 0)}px`, }} /> {/* footer */} @@ -49,9 +49,9 @@ const DoubleBorder: React.FC = ({ borderBottom: `1px solid ${borderColor}`, borderLeft: `1px solid ${borderColor}`, width: '32px', - height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`, - top: `${contentInsets.top}px`, - left: `calc(${contentInsets.left}px)`, + height: `calc(100% - ${insets.top + insets.bottom}px)`, + top: `${insets.top}px`, + left: `calc(${insets.left}px)`, }} /> )} @@ -64,9 +64,9 @@ const DoubleBorder: React.FC = ({ borderBottom: `1px solid ${borderColor}`, borderRight: `1px solid ${borderColor}`, width: '32px', - height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`, - top: `${contentInsets.top}px`, - left: `calc(100% - ${contentInsets.right}px - 32px)`, + height: `calc(100% - ${insets.top + insets.bottom}px)`, + top: `${insets.top}px`, + left: `calc(100% - ${insets.right}px - 32px)`, }} /> )} diff --git a/apps/readest-app/src/components/BookCover.tsx b/apps/readest-app/src/components/BookCover.tsx index 1c794c6a..9cfbe239 100644 --- a/apps/readest-app/src/components/BookCover.tsx +++ b/apps/readest-app/src/components/BookCover.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import Image from 'next/image'; -import { memo, useEffect, useRef } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { Book } from '@/types/book'; import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings'; import { formatAuthors, formatTitle } from '@/utils/book'; @@ -11,12 +11,25 @@ interface BookCoverProps { coverFit?: LibraryCoverFitType; className?: string; imageClassName?: string; + showSpine?: boolean; isPreview?: boolean; } const BookCover: React.FC = memo( - ({ book, mode = 'grid', coverFit = 'crop', className, imageClassName, isPreview }) => { + ({ + book, + mode = 'grid', + coverFit = 'crop', + showSpine = false, + className, + imageClassName, + isPreview, + }) => { const coverRef = useRef(null); + const [imageLoaded, setImageLoaded] = useState(false); + const [imageError, setImageError] = useState(false); + + const shouldShowSpine = showSpine && imageLoaded && !imageError; const toggleImageVisibility = (showImage: boolean) => { if (coverRef.current) { @@ -31,7 +44,15 @@ const BookCover: React.FC = memo( } }; + const handleImageLoad = () => { + setImageLoaded(true); + setImageError(false); + toggleImageVisibility(true); + }; + const handleImageError = () => { + setImageLoaded(false); + setImageError(true); toggleImageVisibility(false); }; @@ -42,7 +63,11 @@ const BookCover: React.FC = memo( return (
{coverFit === 'crop' ? ( = memo( alt={book.title} fill={true} className={clsx('cover-image crop-cover-img object-cover', imageClassName)} + onLoad={handleImageLoad} onError={handleImageError} /> ) : (
= memo( 'cover-image fit-cover-img h-auto max-h-full w-auto max-w-full shadow-md', imageClassName, )} + onLoad={handleImageLoad} onError={handleImageError} />
)} +
= memo( prevProps.mode === nextProps.mode && prevProps.coverFit === nextProps.coverFit && prevProps.isPreview === nextProps.isPreview && + prevProps.showSpine === nextProps.showSpine && prevProps.className === nextProps.className && prevProps.imageClassName === nextProps.imageClassName ); diff --git a/apps/readest-app/src/styles/globals.css b/apps/readest-app/src/styles/globals.css index 8cae4d67..e1c22eab 100644 --- a/apps/readest-app/src/styles/globals.css +++ b/apps/readest-app/src/styles/globals.css @@ -333,3 +333,36 @@ foliate-view { user-select: none; touch-action: manipulation; } + +.book-spine { + background-image: + linear-gradient(180deg, + hsla(0, 0%, 90%, 0.2) 0%, /* Top highlight */ + hsla(0, 0%, 85%, 0.15) 0.5%, /* Top highlight */ + hsla(0, 0%, 40%, 0.1) 40%, /* Center fade */ + hsla(0, 0%, 25%, 0.15) 90%, /* Bottom shadow */ + hsla(0, 0%, 15%, 0.2) 100% /* Bottom edge dark */ + ), + linear-gradient(90deg, + hsla(0, 0%, 0%, 0.1) 0%, /* Left binding shadow */ + hsla(0, 0%, 20%, 0.2) 0.5%, /* Dark transition */ + hsla(0, 0%, 95%, 0.4) 0.8%, /* Peak highlight */ + hsla(0, 0%, 90%, 0.4) 1.8%, /* Peak highlight */ + hsla(0, 0%, 70%, 0.3) 2.0%, /* Gradient highlight */ + hsla(0, 0%, 60%, 0.3) 2.8%, /* Gradient highlight */ + hsla(0, 0%, 50%, 0.2) 3.4%, /* Shadow between highlights */ + hsla(0, 0%, 40%, 0.2) 4.2%, /* Shadow between highlights */ + hsla(0, 0%, 70%, 0.15) 4.6%, /* Second peak highlight */ + hsla(0, 0%, 70%, 0.15) 5.4%, /* Second gradient highlight */ + hsla(0, 0%, 40%, 0.1) 6.0%, /* Transition shadow */ + transparent 88%, /* Main book surface */ + hsla(0, 0%, 70%, 0.1) 99%, /* Right edge highlight */ + hsla(0, 0%, 30%, 0.2) 99.5%, /* Right edge shadow */ + hsla(0, 0%, 10%, 0.2) 100% /* Right binding edge */ + ); + mask: linear-gradient(180deg, + black 0%, /* Full opacity (1.0) */ + rgba(0,0,0,0.9) 50%, + rgba(0,0,0,0.7) 100% /* Half opacity (0.6) */ + ); +}