forked from akai/readest
fix(library): make bookitem-main shrink to match cover in fit mode (#4331)
* fix(library): make bookitem-main shrink to match cover in fit mode Closes #4234. In fit mode the bookitem-main kept its 28/41 aspect regardless of the cover image's natural aspect, leaving extra padding beside (portrait covers) or above (landscape covers) the cover. The select-mode overlay and icons drifted away from the cover edge. BookCover now reports the loaded image's natural aspect ratio. BookItem overrides the bookitem-main's aspect-ratio with the cover's aspect so the box hugs the cover exactly, and proportionally shrinks book-item width for portrait covers so the info row icons align with the cover's right edge. Also wrap the TTS "Back to TTS Location" pill with whitespace-nowrap so long translations (e.g. German "Zurück zur TTS-Position") expand the button width instead of overflowing the fixed height. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(library): scope cover shrink to bookitem-main, leave info row at cell width Per review, the width shrink should only apply to the cover row so the title and info icons keep their original cell-wide layout. Move the width style from .book-item to .bookitem-main alongside its aspectRatio override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, cleanup } from '@testing-library/react';
|
||||
import { render, cleanup, fireEvent } from '@testing-library/react';
|
||||
|
||||
import BookCover from '@/components/BookCover';
|
||||
import { Book } from '@/types/book';
|
||||
@@ -39,6 +39,21 @@ describe('BookCover', () => {
|
||||
expect(img?.getAttribute('loading')).toBe('lazy');
|
||||
});
|
||||
|
||||
it('reports natural aspect ratio via onAspectRatioChange when fit-mode image loads', () => {
|
||||
const onAspectRatioChange = vi.fn();
|
||||
const { container } = render(
|
||||
<BookCover book={makeBook()} coverFit='fit' onAspectRatioChange={onAspectRatioChange} />,
|
||||
);
|
||||
const img = container.querySelector('img.cover-image') as HTMLImageElement;
|
||||
expect(img).toBeTruthy();
|
||||
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 600, configurable: true });
|
||||
Object.defineProperty(img, 'naturalHeight', { value: 900, configurable: true });
|
||||
fireEvent.load(img);
|
||||
|
||||
expect(onAspectRatioChange).toHaveBeenCalledWith(600 / 900);
|
||||
});
|
||||
|
||||
it('falls back to metadata.author on the fallback cover when book.author is empty', () => {
|
||||
const book = makeBook({
|
||||
author: '',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import {
|
||||
LiaCloudUploadAltSolid,
|
||||
@@ -49,6 +50,21 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
const { settings } = useSettingsStore();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
const [coverAspect, setCoverAspect] = useState<number | null>(null);
|
||||
useEffect(() => {
|
||||
setCoverAspect(null);
|
||||
}, [book.hash, book.metadata?.coverImageUrl, book.coverImageUrl]);
|
||||
|
||||
const CELL_ASPECT_RATIO = 28 / 41;
|
||||
const fitCoverInGrid = mode === 'grid' && coverFit === 'fit' && coverAspect !== null;
|
||||
const shouldShrinkWidth = fitCoverInGrid && coverAspect! < CELL_ASPECT_RATIO;
|
||||
const bookitemMainStyle = fitCoverInGrid
|
||||
? {
|
||||
aspectRatio: coverAspect!,
|
||||
...(shouldShrinkWidth ? { width: `${(coverAspect! / CELL_ASPECT_RATIO) * 100}%` } : {}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
role='none'
|
||||
@@ -63,11 +79,13 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'bookitem-main relative flex aspect-[28/41] justify-center overflow-hidden rounded',
|
||||
'bookitem-main relative flex justify-center overflow-hidden rounded',
|
||||
!fitCoverInGrid && 'aspect-[28/41]',
|
||||
coverFit === 'crop' && 'shadow-md',
|
||||
mode === 'grid' && 'items-end',
|
||||
mode === 'list' && 'min-w-20 items-center',
|
||||
)}
|
||||
style={bookitemMainStyle}
|
||||
>
|
||||
<BookCover
|
||||
mode={mode}
|
||||
@@ -75,6 +93,7 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
coverFit={coverFit}
|
||||
showSpine={false}
|
||||
imageClassName='rounded shadow-md'
|
||||
onAspectRatioChange={setCoverAspect}
|
||||
/>
|
||||
{bookSelected && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
|
||||
@@ -175,7 +175,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
<button
|
||||
onClick={tts.handleBackToCurrentTTSLocation}
|
||||
className={clsx(
|
||||
'not-eink:bg-base-300 eink-bordered rounded-full px-4 py-2 font-sans text-sm shadow-lg',
|
||||
'not-eink:bg-base-300 eink-bordered whitespace-nowrap rounded-full px-4 py-2 font-sans text-sm shadow-lg',
|
||||
safeAreaInsets?.top ? 'h-11' : 'h-9',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@ interface BookCoverProps {
|
||||
showSpine?: boolean;
|
||||
isPreview?: boolean;
|
||||
onImageError?: () => void;
|
||||
onAspectRatioChange?: (ratio: number) => void;
|
||||
}
|
||||
|
||||
const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
|
||||
@@ -26,6 +27,7 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
|
||||
imageClassName,
|
||||
isPreview,
|
||||
onImageError,
|
||||
onAspectRatioChange,
|
||||
}) => {
|
||||
const coverRef = useRef<HTMLDivElement>(null);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
@@ -46,10 +48,14 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageLoad = () => {
|
||||
const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
setImageLoaded(true);
|
||||
setImageError(false);
|
||||
toggleImageVisibility(true);
|
||||
const img = e.currentTarget;
|
||||
if (onAspectRatioChange && img.naturalWidth > 0 && img.naturalHeight > 0) {
|
||||
onAspectRatioChange(img.naturalWidth / img.naturalHeight);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageError = () => {
|
||||
|
||||
Reference in New Issue
Block a user