diff --git a/src/renderer/src/components/ui/marquee/index.tsx b/src/renderer/src/components/ui/marquee/index.tsx index 3c021a1cc..f4387d048 100644 --- a/src/renderer/src/components/ui/marquee/index.tsx +++ b/src/renderer/src/components/ui/marquee/index.tsx @@ -16,6 +16,7 @@ export const TitleMarquee = ({ return (
{ const $container = ref.current @@ -48,7 +49,7 @@ export const TitleMarquee = ({ }) }, [])} > - + {children} diff --git a/src/renderer/src/hooks/biz/useFeedActions.tsx b/src/renderer/src/hooks/biz/useFeedActions.tsx index 0b84a31f2..91d51c2f0 100644 --- a/src/renderer/src/hooks/biz/useFeedActions.tsx +++ b/src/renderer/src/hooks/biz/useFeedActions.tsx @@ -129,7 +129,7 @@ export const useFeedActions = ({ }, { type: "text" as const, - label: "Copy Feed Id", + label: "Copy Feed ID", disabled: isEntryList, click: () => { navigator.clipboard.writeText(feedId) diff --git a/src/renderer/src/modules/entry-column/article-item.tsx b/src/renderer/src/modules/entry-column/article-item.tsx index 3ad84173b..3ea83220f 100644 --- a/src/renderer/src/modules/entry-column/article-item.tsx +++ b/src/renderer/src/modules/entry-column/article-item.tsx @@ -2,8 +2,43 @@ import { ListItem } from "@renderer/modules/entry-column/list-item-template" import type { UniversalItemProps } from "./types" -export function ArticleItem({ entryId, entryPreview, translation }: UniversalItemProps) { +export function ArticleItem({ + entryId, + entryPreview, + translation, +}: UniversalItemProps) { return ( - + ) } + +export const ArticleItemSkeleton = ( +
+
+
+
+
+
+
+ · +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+) diff --git a/src/renderer/src/modules/entry-column/audio-item.tsx b/src/renderer/src/modules/entry-column/audio-item.tsx index 7614845c2..38e3b281e 100644 --- a/src/renderer/src/modules/entry-column/audio-item.tsx +++ b/src/renderer/src/modules/entry-column/audio-item.tsx @@ -2,8 +2,40 @@ import { ListItem } from "@renderer/modules/entry-column/list-item-template" import type { UniversalItemProps } from "./types" -export function AudioItem({ entryId, entryPreview, translation }: UniversalItemProps) { +export function AudioItem({ + entryId, + entryPreview, + translation, +}: UniversalItemProps) { return ( - + ) } + +export const AudioItemSkeleton = ( +
+
+
+
+
+
+ · +
+
+
+
+
+
+
+
+
+
+
+
+
+) diff --git a/src/renderer/src/modules/entry-column/index.tsx b/src/renderer/src/modules/entry-column/index.tsx index e61a40387..576a8a515 100644 --- a/src/renderer/src/modules/entry-column/index.tsx +++ b/src/renderer/src/modules/entry-column/index.tsx @@ -55,7 +55,7 @@ import type { VirtuosoHandle, VirtuosoProps } from "react-virtuoso" import { Virtuoso, VirtuosoGrid } from "react-virtuoso" import { useEntriesByView, useEntryMarkReadHandler } from "./hooks" -import { EntryItem } from "./item" +import { EntryItem, EntryItemSkeleton } from "./item" export function EntryColumn() { const virtuosoRef = useRef(null) @@ -98,12 +98,8 @@ export function EntryColumn() { Footer: useCallback(() => { if (!isFetchingNextPage) return null - return ( -
- -
- ) - }, [isFetchingNextPage]), + return + }, [isFetchingNextPage, view]), }, rangeChanged: (...args: any[]) => { handleMarkReadInRange && @@ -126,7 +122,7 @@ export function EntryColumn() { }, [view], ), - } + } satisfies VirtuosoProps const navigate = useNavigateEntry() return ( diff --git a/src/renderer/src/modules/entry-column/item.tsx b/src/renderer/src/modules/entry-column/item.tsx index 3242fd3be..ccd711b53 100644 --- a/src/renderer/src/modules/entry-column/item.tsx +++ b/src/renderer/src/modules/entry-column/item.tsx @@ -1,5 +1,6 @@ import { useGeneralSettingKey } from "@renderer/atoms/settings/general" import { ListItemHoverOverlay } from "@renderer/components/ui/list-item-hover-overlay" +import { LoadingCircle } from "@renderer/components/ui/loading" import { views } from "@renderer/constants" import { useAsRead } from "@renderer/hooks/biz/useAsRead" import { useEntryActions } from "@renderer/hooks/biz/useEntryActions" @@ -14,18 +15,21 @@ import { Queries } from "@renderer/queries" import type { FlatEntryModel } from "@renderer/store/entry" import { entryActions } from "@renderer/store/entry" import { useEntry } from "@renderer/store/entry/hooks" -import type { FC } from "react" +import type { FC, ReactNode } from "react" import { memo, useCallback } from "react" import { useDebounceCallback } from "usehooks-ts" import { ReactVirtuosoItemPlaceholder } from "../../components/ui/placeholder" -import { ArticleItem } from "./article-item" -import { AudioItem } from "./audio-item" -import { NotificationItem } from "./notification-item" -import { PictureItem } from "./picture-item" -import { SocialMediaItem } from "./social-media-item" +import { ArticleItem, ArticleItemSkeleton } from "./article-item" +import { AudioItem, AudioItemSkeleton } from "./audio-item" +import { + NotificationItem, + NotificationItemSkeleton, +} from "./notification-item" +import { PictureItem, PictureItemSkeleton } from "./picture-item" +import { SocialMediaItem, SocialMediaItemSkeleton } from "./social-media-item" import type { EntryListItemFC } from "./types" -import { VideoItem } from "./video-item" +import { VideoItem, VideoItemSkeleton } from "./video-item" interface EntryItemProps { entryId: string @@ -139,25 +143,25 @@ function EntryItemImpl({ e.preventDefault() showNativeMenu( [ - ...(items + ...items .filter((item) => !item.disabled) .map((item) => ({ type: "text" as const, label: item.name, click: item.onClick, shortcut: item.shortcut, - }))), + })), { type: "separator" as const, }, - ...(feedItems.filter((item) => !item.disabled)), + ...feedItems.filter((item) => !item.disabled), { type: "separator" as const, }, { type: "text" as const, - label: "Copy Entry Id", + label: "Copy Entry ID", click: () => { navigator.clipboard.writeText(entry.entries.id) }, @@ -170,9 +174,7 @@ function EntryItemImpl({ ) return ( -
+
- + +
@@ -199,3 +205,38 @@ export const EntryItem: FC = memo(({ entryId, view }) => { if (!entry) return return }) + +const SkeletonItemMap = { + [FeedViewType.Articles]: ArticleItemSkeleton, + [FeedViewType.SocialMedia]: SocialMediaItemSkeleton, + [FeedViewType.Pictures]: PictureItemSkeleton, + [FeedViewType.Videos]: VideoItemSkeleton, + [FeedViewType.Audios]: AudioItemSkeleton, + [FeedViewType.Notifications]: NotificationItemSkeleton, +} + +const LoadingCircleFallback = ( +
+ +
+) +export const EntryItemSkeleton: FC<{ view: FeedViewType }> = memo( + ({ view }) => { + const SkeletonItem = SkeletonItemMap[view] + return SkeletonItem ? + ( +
{createSkeletonItems(SkeletonItem)}
+ ) : + ( + LoadingCircleFallback + ) + }, +) + +const createSkeletonItems = (element: ReactNode) => { + const children = [] as ReactNode[] + for (let i = 0; i < 10; i++) { + children.push(element) + } + return children +} diff --git a/src/renderer/src/modules/entry-column/notification-item.tsx b/src/renderer/src/modules/entry-column/notification-item.tsx index b695fea03..024a45fee 100644 --- a/src/renderer/src/modules/entry-column/notification-item.tsx +++ b/src/renderer/src/modules/entry-column/notification-item.tsx @@ -2,8 +2,35 @@ import { ListItem } from "@renderer/modules/entry-column/list-item-template" import type { UniversalItemProps } from "./types" -export function NotificationItem({ entryId, entryPreview, translation }: UniversalItemProps) { +export function NotificationItem({ + entryId, + entryPreview, + translation, +}: UniversalItemProps) { return ( - + ) } + +export const NotificationItemSkeleton = ( +
+
+
+
+
+
+ · +
+
+
+
+
+
+
+
+
+) diff --git a/src/renderer/src/modules/entry-column/picture-item.tsx b/src/renderer/src/modules/entry-column/picture-item.tsx index b0c7caaad..1cfa6deef 100644 --- a/src/renderer/src/modules/entry-column/picture-item.tsx +++ b/src/renderer/src/modules/entry-column/picture-item.tsx @@ -44,3 +44,28 @@ export function PictureItem({ ) } + +export const PictureItemSkeleton = ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ · +
+
+
+
+
+
+) diff --git a/src/renderer/src/modules/entry-column/social-media-item.tsx b/src/renderer/src/modules/entry-column/social-media-item.tsx index 5d1537886..4065663f8 100644 --- a/src/renderer/src/modules/entry-column/social-media-item.tsx +++ b/src/renderer/src/modules/entry-column/social-media-item.tsx @@ -5,9 +5,7 @@ import { Media } from "@renderer/components/ui/media" import { usePreviewMedia } from "@renderer/components/ui/media/hooks" import { useAsRead } from "@renderer/hooks/biz/useAsRead" import { useEntryActions } from "@renderer/hooks/biz/useEntryActions" -import { - useRouteParamsSelector, -} from "@renderer/hooks/biz/useRouteParams" +import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams" import { cn } from "@renderer/lib/utils" import { useEntry } from "@renderer/store/entry/hooks" import { useFeedById } from "@renderer/store/feed" @@ -136,3 +134,39 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
) } + +export const SocialMediaItemSkeleton = ( +
+
+
+
+
+
+
+
+ · +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+) diff --git a/src/renderer/src/modules/entry-column/video-item.tsx b/src/renderer/src/modules/entry-column/video-item.tsx index 133747940..cd4b5af90 100644 --- a/src/renderer/src/modules/entry-column/video-item.tsx +++ b/src/renderer/src/modules/entry-column/video-item.tsx @@ -137,3 +137,28 @@ const PreviewVideoModalContent: ModalContentComponent<{ ) } + +export const VideoItemSkeleton = ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ · +
+
+
+
+
+
+) diff --git a/src/renderer/src/styles/tailwind-extend.css b/src/renderer/src/styles/tailwind-extend.css index 88edaa0fb..b6facc15e 100644 --- a/src/renderer/src/styles/tailwind-extend.css +++ b/src/renderer/src/styles/tailwind-extend.css @@ -261,3 +261,13 @@ } } } + +@layer utilities { + .scrollbar-none { + scrollbar-width: none; + -ms-overflow-style: none; + } + .scrollbar-none::-webkit-scrollbar { + display: none; + } +}