diff --git a/apps/readest-app/src/__tests__/components/BookCover.test.tsx b/apps/readest-app/src/__tests__/components/BookCover.test.tsx
index 572c3a9b..108b727a 100644
--- a/apps/readest-app/src/__tests__/components/BookCover.test.tsx
+++ b/apps/readest-app/src/__tests__/components/BookCover.test.tsx
@@ -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(
+ ,
+ );
+ 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: '',
diff --git a/apps/readest-app/src/app/library/components/BookItem.tsx b/apps/readest-app/src/app/library/components/BookItem.tsx
index a3d6221d..6159b180 100644
--- a/apps/readest-app/src/app/library/components/BookItem.tsx
+++ b/apps/readest-app/src/app/library/components/BookItem.tsx
@@ -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 = ({
const { settings } = useSettingsStore();
const iconSize15 = useResponsiveSize(15);
+ const [coverAspect, setCoverAspect] = useState(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 (
= ({
>
= ({
coverFit={coverFit}
showSpine={false}
imageClassName='rounded shadow-md'
+ onAspectRatioChange={setCoverAspect}
/>
{bookSelected && (
diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx
index 11a0b247..56174c5e 100644
--- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx
+++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx
@@ -175,7 +175,7 @@ const TTSControl: React.FC = ({ bookKey, gridInsets }) => {