layout: fix insets for double borders and add book spine decorator (#1688)

This commit is contained in:
Huang Xin
2025-07-27 20:36:55 +08:00
committed by GitHub
parent 9cebadba57
commit 6eeb8e7580
5 changed files with 94 additions and 25 deletions
@@ -61,12 +61,17 @@ const BookItem: React.FC<BookItemProps> = ({
>
<div
className={clsx(
'relative flex aspect-[28/41] items-center justify-center',
coverFit === 'crop' && 'overflow-hidden shadow-md',
'relative flex items-end justify-center',
coverFit === 'crop' && 'aspect-[28/41] overflow-hidden shadow-md',
mode === 'list' && 'min-w-20',
)}
>
<BookCover mode={mode} book={book} coverFit={coverFit} />
<BookCover
mode={mode}
book={book}
coverFit={coverFit}
showSpine={book.format !== 'PDF' || !!book.metadata?.publisher}
/>
{bookSelected && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
@@ -153,7 +153,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
showFooter={showFooter}
borderColor={viewSettings.borderColor}
horizontalGap={horizontalGapPercent}
contentInsets={contentInsets}
insets={viewInsets}
/>
)}
{showHeader && (
@@ -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<DoubleBorderProps> = ({
borderColor,
showHeader,
showFooter,
contentInsets,
insets,
}) => {
return (
<div>
@@ -23,10 +23,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
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)`,
}}
></div>
{/* inner frame */}
@@ -34,10 +34,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
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<DoubleBorderProps> = ({
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<DoubleBorderProps> = ({
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)`,
}}
/>
)}
+36 -5
View File
@@ -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<BookCoverProps> = memo<BookCoverProps>(
({ book, mode = 'grid', coverFit = 'crop', className, imageClassName, isPreview }) => {
({
book,
mode = 'grid',
coverFit = 'crop',
showSpine = false,
className,
imageClassName,
isPreview,
}) => {
const coverRef = useRef<HTMLDivElement>(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<BookCoverProps> = memo<BookCoverProps>(
}
};
const handleImageLoad = () => {
setImageLoaded(true);
setImageError(false);
toggleImageVisibility(true);
};
const handleImageError = () => {
setImageLoaded(false);
setImageError(true);
toggleImageVisibility(false);
};
@@ -42,7 +63,11 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
return (
<div
ref={coverRef}
className={clsx('book-cover-container relative flex h-full w-full', className)}
className={clsx(
'book-cover-container relative flex w-full',
coverFit === 'crop' && 'h-full',
className,
)}
>
{coverFit === 'crop' ? (
<Image
@@ -50,13 +75,14 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
alt={book.title}
fill={true}
className={clsx('cover-image crop-cover-img object-cover', imageClassName)}
onLoad={handleImageLoad}
onError={handleImageError}
/>
) : (
<div
className={clsx(
'flex h-full w-full justify-center',
mode === 'grid' ? 'items-end' : 'items-center',
'flex w-full justify-center',
mode === 'grid' ? 'h-full items-end' : 'items-center',
)}
>
<Image
@@ -69,10 +95,14 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
'cover-image fit-cover-img h-auto max-h-full w-auto max-w-full shadow-md',
imageClassName,
)}
onLoad={handleImageLoad}
onError={handleImageError}
/>
</div>
)}
<div
className={`book-spine absolute inset-0 ${shouldShowSpine ? 'visible' : 'invisible'}`}
/>
<div
className={clsx(
'fallback-cover invisible absolute inset-0 rounded-none p-2',
@@ -112,6 +142,7 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
prevProps.mode === nextProps.mode &&
prevProps.coverFit === nextProps.coverFit &&
prevProps.isPreview === nextProps.isPreview &&
prevProps.showSpine === nextProps.showSpine &&
prevProps.className === nextProps.className &&
prevProps.imageClassName === nextProps.imageClassName
);
+33
View File
@@ -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) */
);
}