From cd024f864ac78b78aa49d2356ee90f5e4eb4c668 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 7 Jun 2026 18:51:54 +0200 Subject: [PATCH] bug-fixes --- src/components/Image/index.tsx | 4 ++ .../Note/PublicationCoverFallback.tsx | 7 ++- src/components/Note/PublicationCoverImage.tsx | 50 +++++++++++++++---- .../Note/PublicationIndexMetadata.tsx | 4 +- src/lib/gutenberg-cover.test.ts | 16 ++++++ src/lib/gutenberg-cover.ts | 17 +++++++ 6 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index 7de660f1..07d07623 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -70,6 +70,8 @@ export default function Image({ className = '', classNames = {}, hideIfError = false, + /** Called after internal URL fallbacks are exhausted and the image still failed to load. */ + onFinalError, errorPlaceholder = , style: wrapperStyleProp, holdUntilClick = false, @@ -95,6 +97,7 @@ export default function Image({ /** Caption below the image; defaults to resolved alt when {@link showAltCaption} is true. */ caption?: string hideIfError?: boolean + onFinalError?: () => void errorPlaceholder?: React.ReactNode /** Passed to the inner `` (e.g. profile banner vs avatar load order). */ fetchPriority?: 'high' | 'low' | 'auto' @@ -291,6 +294,7 @@ export default function Image({ setIsLoading(false) setDisplaySkeleton(false) setHasError(true) + onFinalError?.() } const handleLoad = () => { diff --git a/src/components/Note/PublicationCoverFallback.tsx b/src/components/Note/PublicationCoverFallback.tsx index a2abc8b7..913679f6 100644 --- a/src/components/Note/PublicationCoverFallback.tsx +++ b/src/components/Note/PublicationCoverFallback.tsx @@ -14,14 +14,17 @@ export default function PublicationCoverFallback({ size?: 'library' | 'default' className?: string }) { - const maxClass = size === 'library' ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS + const isLibrary = size === 'library' + const maxClass = isLibrary ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS + + const stackedLayoutClass = isLibrary ? 'aspect-[3/4] w-full' : 'aspect-[3/4] w-48 max-w-full' return (
(null) const isNearViewport = useNearViewport(wrapperRef, { enabled: isLibrary }) const maxClass = isLibrary ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS - const resolvedUrl = isLibrary ? gutenbergLibraryCoverImageUrl(imageUrl) : imageUrl + const candidateUrls = useMemo( + () => gutenbergCoverCandidateUrls(imageUrl, isLibrary), + [imageUrl, isLibrary] + ) + const [urlIndex, setUrlIndex] = useState(0) + const [exhausted, setExhausted] = useState(false) + const activeUrl = candidateUrls[urlIndex] ?? candidateUrls[0] ?? imageUrl.trim() + + useEffect(() => { + setUrlIndex(0) + setExhausted(false) + }, [imageUrl, isLibrary]) + + const handleImageError = useCallback(() => { + setUrlIndex((index) => { + const next = index + 1 + if (next < candidateUrls.length) return next + setExhausted(true) + return index + }) + }, [candidateUrls.length]) + + if (exhausted || !activeUrl) { + return + } + + const stackedLayoutClass = isLibrary ? 'aspect-[3/4] w-full' : 'w-fit' return (
{isNearViewport ? ( ) : isFull ? ( - + ) : null} {showTitle ? ( diff --git a/src/lib/gutenberg-cover.test.ts b/src/lib/gutenberg-cover.test.ts index 6a17048d..51854166 100644 --- a/src/lib/gutenberg-cover.test.ts +++ b/src/lib/gutenberg-cover.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { + gutenbergCoverCandidateUrls, gutenbergCoverImageUrl, gutenbergEbookPageUrl, gutenbergLibraryCoverImageUrl, @@ -58,6 +59,21 @@ describe('gutenberg-cover', () => { expect(gutenbergEbookPageUrl('28217')).toBe('https://www.gutenberg.org/ebooks/28217') }) + it('gutenbergCoverCandidateUrls tries small then medium in library mode', () => { + expect( + gutenbergCoverCandidateUrls( + 'https://www.gutenberg.org/cache/epub/11/pg11.cover.medium.jpg', + true + ) + ).toEqual([ + 'https://www.gutenberg.org/cache/epub/11/pg11.cover.small.jpg', + 'https://www.gutenberg.org/cache/epub/11/pg11.cover.medium.jpg' + ]) + expect(gutenbergCoverCandidateUrls('https://example.com/cover.jpg', true)).toEqual([ + 'https://example.com/cover.jpg' + ]) + }) + it('normalizeGutenbergCoverImageUrl converts ebook pages to cover JPG', () => { expect(normalizeGutenbergCoverImageUrl('https://www.gutenberg.org/ebooks/16702')).toBe( 'https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg' diff --git a/src/lib/gutenberg-cover.ts b/src/lib/gutenberg-cover.ts index d746f919..7ad36333 100644 --- a/src/lib/gutenberg-cover.ts +++ b/src/lib/gutenberg-cover.ts @@ -67,3 +67,20 @@ export function normalizeGutenbergCoverImageUrl(url: string): string { if (DIRECT_IMAGE_EXT.test(trimmed)) return trimmed return gutenbergCoverImageUrl(id) } + +/** Ordered cover URLs to try (library prefers small, then medium; always deduped). */ +export function gutenbergCoverCandidateUrls(url: string, preferSmall: boolean): string[] { + const trimmed = url.trim() + if (!trimmed) return [] + const id = parseGutenbergEbookId(trimmed) + if (!id || !trimmed.toLowerCase().includes('gutenberg')) return [trimmed] + + const ordered: string[] = [] + if (preferSmall) ordered.push(gutenbergCoverImageUrl(id, 'small')) + ordered.push(gutenbergCoverImageUrl(id, 'medium')) + const normalized = normalizeGutenbergCoverImageUrl(trimmed) + for (const candidate of [normalized, trimmed]) { + if (!ordered.includes(candidate)) ordered.push(candidate) + } + return ordered +}