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" })
- }
- />
-
-
-
-