diff --git a/src/components/Article/ArticleCard.css b/src/components/Article/ArticleCard.css index 960fc6b7f..1b9babaae 100644 --- a/src/components/Article/ArticleCard.css +++ b/src/components/Article/ArticleCard.css @@ -13,7 +13,7 @@ .article-card-info { color: var(--color-text-3); - font-size: 13px; + font-size: 0.9rem; } .article-card-mini-content { @@ -120,7 +120,7 @@ .icon-starred { margin-left: 8px; color: var(--color-text-3); - font-size: 13px; + font-size: 0.9rem; } .thumbnail { @@ -134,7 +134,7 @@ margin-top: 0; margin-bottom: 8px; color: var(--color-text-3); - font-size: 13px; + font-size: 0.9rem; line-height: 1.5; } @@ -145,8 +145,19 @@ .article-card-meta { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 4px; + align-items: flex-start; + gap: 8px; + padding-bottom: 8px; +} + +.article-title { + flex: 1; + min-width: 0; +} + +.published-time { + white-space: nowrap; + align-self: start; } .article-card-body { @@ -159,3 +170,23 @@ flex: 1; min-width: 0; } + +.article-card-image-container-wide { + width: 100%; + height: 160px; + margin: 8px 0; + border-radius: var(--border-radius-medium); + overflow: hidden; + box-shadow: var(--shadow); +} + +.article-card-image-container-wide .thumbnail { + width: 100%; + height: 160px; +} + +.article-card-image-container-wide img { + width: 100%; + height: 100%; + object-fit: cover; +} diff --git a/src/components/Article/ArticleCard.jsx b/src/components/Article/ArticleCard.jsx index 748527c9c..793c60c4d 100644 --- a/src/components/Article/ArticleCard.jsx +++ b/src/components/Article/ArticleCard.jsx @@ -14,21 +14,37 @@ import FeedIcon from "../ui/FeedIcon"; import ImageWithLazyLoading from "./ImageWithLazyLoading"; import "./ArticleCard.css"; -const ArticleCardImage = ({ entry, isThumbnail, setHasError }) => { - const imageSize = isThumbnail - ? { width: "80px", height: "80px" } - : { width: "100%", height: "160px" }; +const ASPECT_RATIO_THRESHOLD = 4 / 3; + +const ArticleCardImage = ({ + entry, + setHasError, + isWideImage, + onLoadComplete, +}) => { + const imageSize = isWideImage + ? { width: "100%", height: "100%" } + : { width: "80px", height: "80px" }; + + const handleImageLoad = (e) => { + const img = e.target; + if (img) { + const aspectRatio = img.naturalWidth / img.naturalHeight; + onLoadComplete(aspectRatio); + } + }; return ( -
+
); @@ -40,17 +56,22 @@ const extractTextFromHtml = (html) => { return div.textContent || div.innerText || ""; }; -const ArticleCardContent = ({ entry, showFeedIcon, mini, children }) => { +const ArticleCardContent = ({ entry, showFeedIcon, children }) => { const { showDetailedRelativeTime, showEstimatedReadingTime } = useStore(settingsState); const [hasError, setHasError] = useState(false); + const [isWideImage, setIsWideImage] = useState(false); const contentClass = classNames({ - "article-card-mini-content": mini, - "article-card-mini-content-padding": mini && showFeedIcon, + "article-card-mini-content": true, + "article-card-mini-content-padding": showFeedIcon, }); + const handleImageLoadComplete = (aspectRatio) => { + setIsWideImage(aspectRatio >= ASPECT_RATIO_THRESHOLD); + }; + return (
{ opacity: entry.status === "unread" ? 1 : 0.5, }} > -
+
{showFeedIcon && ( - + )} {entry.feed.title} {generateRelativeTime( @@ -101,6 +119,16 @@ const ArticleCardContent = ({ entry, showFeedIcon, mini, children }) => { {entry.title}
+ {entry.imgSrc && !hasError && isWideImage && ( +
+ +
+ )}
{ {entry.starred && }
- {entry.imgSrc && !hasError && ( + {entry.imgSrc && !hasError && !isWideImage && (
)} @@ -146,7 +175,7 @@ const ArticleCardContent = ({ entry, showFeedIcon, mini, children }) => { ); }; -const ArticleCard = ({ entry, handleEntryClick, mini, children }) => { +const ArticleCard = ({ entry, handleEntryClick, children }) => { const { markReadOnScroll, showFeedIcon } = useStore(settingsState); const { activeContent } = useStore(contentState); @@ -196,11 +225,7 @@ const ArticleCard = ({ entry, handleEntryClick, mini, children }) => { > + {children} } diff --git a/src/components/Article/ArticleList.jsx b/src/components/Article/ArticleList.jsx index 192abbaba..664358c9a 100644 --- a/src/components/Article/ArticleList.jsx +++ b/src/components/Article/ArticleList.jsx @@ -55,8 +55,7 @@ const LoadMoreComponent = ({ getEntries }) => { const ArticleList = forwardRef( ({ getEntries, handleEntryClick, cardsRef }, ref) => { - const { layout, pageSize } = useStore(settingsState); - const isCompactLayout = layout === "small"; + const { pageSize } = useStore(settingsState); const { isArticleListReady, loadMoreVisible } = useStore(contentState); const filteredEntries = useStore(filteredEntriesState); @@ -78,10 +77,7 @@ const ArticleList = forwardRef( const virtualizer = useVirtualizer({ count: filteredEntries.length, getScrollElement: () => cardsRef.current, - estimateSize: useCallback( - () => (isCompactLayout ? 120 : 280), - [isCompactLayout], - ), + estimateSize: useCallback(() => 160, []), overscan: 5, }); const virtualItems = virtualizer.getVirtualItems(); @@ -126,7 +122,6 @@ const ArticleList = forwardRef( diff --git a/src/components/Article/ImageWithLazyLoading.jsx b/src/components/Article/ImageWithLazyLoading.jsx index 660184007..22a8f4600 100644 --- a/src/components/Article/ImageWithLazyLoading.jsx +++ b/src/components/Article/ImageWithLazyLoading.jsx @@ -13,6 +13,7 @@ const ImageWithLazyLoading = ({ status, width, setHasError, + onLoad, }) => { const [isLoaded, setIsLoaded] = useState(false); @@ -21,7 +22,12 @@ const ImageWithLazyLoading = ({ onChange: (inView) => { if (inView && !isLoaded) { const image = new Image(); - image.onload = () => setIsLoaded(true); + image.onload = () => { + setIsLoaded(true); + if (onLoad) { + onLoad(image); + } + }; image.onerror = () => setHasError(true); image.src = src; } @@ -55,6 +61,7 @@ const ImageWithLazyLoading = ({ objectFit: "cover", borderRadius, }} + onLoad={onLoad} /> ) : (
diff --git a/src/components/Article/LoadingCards.jsx b/src/components/Article/LoadingCards.jsx index d90d53a0d..195376180 100644 --- a/src/components/Article/LoadingCards.jsx +++ b/src/components/Article/LoadingCards.jsx @@ -2,14 +2,10 @@ import { Card, Skeleton } from "@arco-design/web-react"; import { useStore } from "@nanostores/react"; import { contentState } from "../../store/contentState"; -import { settingsState } from "../../store/settingsState"; import "./LoadingCards.css"; -const LoadingCard = ({ layout, isArticleListReady }) => ( - : null} - > +const LoadingCard = ({ isArticleListReady }) => ( + ( ); const LoadingCards = () => { - const { layout } = useStore(settingsState); const { isArticleListReady } = useStore(contentState); - const cardCount = layout === "large" ? 2 : 4; return ( !isArticleListReady && - Array.from({ length: cardCount }, (_, i) => ( + Array.from({ length: 4 }, (_, i) => ( key={i} - layout={layout} isArticleListReady={isArticleListReady} /> )) diff --git a/src/components/Settings/Appearance.jsx b/src/components/Settings/Appearance.jsx index 2e6e1bc7d..b724235a5 100644 --- a/src/components/Settings/Appearance.jsx +++ b/src/components/Settings/Appearance.jsx @@ -22,7 +22,6 @@ const Appearance = () => { articleWidth, fontSize, fontFamily, - layout, showDetailedRelativeTime, showEstimatedReadingTime, showFeedIcon, @@ -101,20 +100,6 @@ const Appearance = () => { - - - handleConfigChange({ layout: value ? "small" : "large" }) - } - /> - - - -