refactor(ui): extract shared ui components

- Added `HeaderActionButton` and `HeaderActionGroup` components for improved action handling in the UI.
- Introduced `Media`, `MediaContainerWidthProvider`, and related context components to manage media dimensions and rendering.
- Enhanced markdown rendering components (`BlockImage`, `InlineImage`, `MarkdownLink`) to utilize the new media structure, improving image handling and layout.
- Updated various components to import from the new media structure, ensuring consistency and better organization.

These changes aim to enhance the media handling capabilities and improve the overall user experience in the application.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-17 00:55:53 +08:00
parent 423f1298c3
commit ed752c51e7
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
39 changed files with 92 additions and 82 deletions

View File

@ -1,6 +1,6 @@
---
description:
globs:
globs: apps/desktop/layer/renderer/src/**/*
alwaysApply: false
---
You need to find the available UI components in the project.

View File

@ -7,8 +7,9 @@ import { ENTRY_CONTENT_RENDER_CONTAINER_ID } from "~/constants/dom"
import { parseHtml } from "~/lib/parse-html"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
import type { MediaInfoRecord } from "../media"
import { MediaContainerWidthProvider, MediaInfoRecordProvider } from "../media"
import { MediaContainerWidthProvider } from "../media/MediaContainerWidthProvider"
import type { MediaInfoRecord } from "../media/MediaInfoRecord"
import { MediaInfoRecordProvider } from "../media/MediaInfoRecordProvider"
import { MarkdownRenderContainerRefContext } from "./context"
export type HTMLProps<A extends keyof JSX.IntrinsicElements = "div"> = {

View File

@ -4,7 +4,7 @@ import { useContextSelector } from "use-context-selector"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
import { Media } from "../../media"
import { Media } from "../../media/Media"
import { MarkdownImageRecordContext, MarkdownRenderActionContext } from "../context"
export const MarkdownBlockImage = (

View File

@ -2,7 +2,7 @@ import { cn } from "@follow/utils/utils"
import { use } from "react"
import { useContextSelector } from "use-context-selector"
import { Media } from "../../media"
import { Media } from "../../media/Media"
import { MarkdownImageRecordContext, MarkdownRenderActionContext } from "../context"
export const MarkdownInlineImage = (

View File

@ -1,4 +1,5 @@
import { MagneticHoverEffect } from "@follow/components/ui/effect/MagneticHoverEffect.js"
import type { LinkProps } from "@follow/components/ui/link/LinkWithTooltip.js"
import {
Tooltip,
TooltipContent,
@ -8,7 +9,6 @@ import {
import { useCorrectZIndex } from "@follow/components/ui/z-index/ctx.js"
import { use } from "react"
import type { LinkProps } from "../../link"
import { MarkdownRenderActionContext } from "../context"
export const MarkdownLink = (props: LinkProps) => {

View File

@ -3,15 +3,16 @@ import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { cn } from "@follow/utils/utils"
import { useForceUpdate } from "motion/react"
import type { FC, ImgHTMLAttributes, VideoHTMLAttributes } from "react"
import { createContext, memo, use, useMemo, useState } from "react"
import { memo, use, useMemo, useState } from "react"
import { Blurhash, BlurhashCanvas } from "react-blurhash"
import { useEventCallback } from "usehooks-ts"
import { saveImageDimensionsToDb } from "~/store/image/db"
import { usePreviewMedia } from "./media/hooks"
import type { VideoPlayerRef } from "./media/VideoPlayer"
import { VideoPlayer } from "./media/VideoPlayer"
import { useMediaContainerWidth, usePreviewMedia } from "./hooks"
import { MediaInfoRecordContext } from "./MediaInfoRecordContext"
import type { VideoPlayerRef } from "./VideoPlayer"
import { VideoPlayer } from "./VideoPlayer"
type BaseProps = {
mediaContainerClassName?: string
@ -451,32 +452,3 @@ const VideoPreview: FC<{
</div>
)
}
const MediaContainerWidthContext = createContext<number>(0)
export const MediaContainerWidthProvider = ({
children,
width,
}: {
children: React.ReactNode
width: number
}) => {
return <MediaContainerWidthContext value={width}>{children}</MediaContainerWidthContext>
}
const useMediaContainerWidth = () => {
return use(MediaContainerWidthContext)
}
export type MediaInfoRecord = Record<string, { width?: number; height?: number }>
const MediaInfoRecordContext = createContext<MediaInfoRecord>({})
const noop = {} as const
export const MediaInfoRecordProvider = ({
children,
mediaInfo,
}: {
children: React.ReactNode
mediaInfo?: Nullable<MediaInfoRecord>
}) => {
return <MediaInfoRecordContext value={mediaInfo || noop}>{children}</MediaInfoRecordContext>
}

View File

@ -0,0 +1,3 @@
import { createContext } from "react"
export const MediaContainerWidthContext = createContext<number>(0)

View File

@ -0,0 +1,11 @@
import { MediaContainerWidthContext } from "./MediaContainerWidthContext"
export const MediaContainerWidthProvider = ({
children,
width,
}: {
children: React.ReactNode
width: number
}) => {
return <MediaContainerWidthContext value={width}>{children}</MediaContainerWidthContext>
}

View File

@ -0,0 +1 @@
export type MediaInfoRecord = Record<string, { width?: number; height?: number }>

View File

@ -0,0 +1,5 @@
import { createContext } from "react"
import type { MediaInfoRecord } from "./MediaInfoRecord"
export const MediaInfoRecordContext = createContext<MediaInfoRecord>({})

View File

@ -0,0 +1,13 @@
import type { MediaInfoRecord } from "./MediaInfoRecord"
import { MediaInfoRecordContext } from "./MediaInfoRecordContext"
const noop = {} as const
export const MediaInfoRecordProvider = ({
children,
mediaInfo,
}: {
children: React.ReactNode
mediaInfo?: Nullable<MediaInfoRecord>
}) => {
return <MediaInfoRecordContext value={mediaInfo || noop}>{children}</MediaInfoRecordContext>
}

View File

@ -6,7 +6,7 @@ import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
import { uniqBy } from "es-toolkit/compat"
import { useCallback, useRef } from "react"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
const defaultProxySize = {
width: 600,

View File

@ -1,12 +1,13 @@
import { isMobile } from "@follow/components/hooks/useMobile.js"
import { useCallback } from "react"
import { use, useCallback } from "react"
import { replaceImgUrlIfNeed } from "~/lib/img-proxy"
import { PlainModal } from "../modal/stacked/custom-modal"
import { useModalStack } from "../modal/stacked/hooks"
import type { PreviewMediaProps } from "./preview-media"
import { PreviewMediaContent } from "./preview-media"
import { MediaContainerWidthContext } from "./MediaContainerWidthContext"
import type { PreviewMediaProps } from "./PreviewMediaContent"
import { PreviewMediaContent } from "./PreviewMediaContent"
export const usePreviewMedia = (children?: React.ReactNode) => {
const { present } = useModalStack()
@ -39,3 +40,7 @@ export const usePreviewMedia = (children?: React.ReactNode) => {
[children, present],
)
}
export const useMediaContainerWidth = () => {
return use(MediaContainerWidthContext)
}

View File

@ -13,7 +13,7 @@ import { MarkdownBlockImage, MarkdownLink, MarkdownP } from "~/components/ui/mar
import { useIsInParagraphContext } from "~/components/ui/markdown/renderers/ctx"
import { createHeadingRenderer } from "~/components/ui/markdown/renderers/Heading"
import { MarkdownInlineImage } from "~/components/ui/markdown/renderers/InlineImage"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
function markInlineImage(node?: Element) {
for (const item of node?.children ?? []) {

View File

@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"
import { unstable_usePrompt } from "react-router"
import { toast } from "sonner"
import { HeaderActionButton, HeaderActionGroup } from "~/components/ui/button/header-action-button"
import { HeaderActionButton, HeaderActionGroup } from "~/components/ui/button/HeaderActionButton"
import {
DropdownMenu,
DropdownMenuContent,

View File

@ -1,5 +1,7 @@
import { Spring } from "@follow/components/constants/spring.js"
import { EmptyIcon } from "@follow/components/icons/empty.jsx"
import { CollapseControlled } from "@follow/components/ui/collapse/Collapse.js"
import type { LinkProps } from "@follow/components/ui/link/LinkWithTooltip.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { RootPortal } from "@follow/components/ui/portal/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
@ -24,18 +26,16 @@ import { Trans, useTranslation } from "react-i18next"
import { MenuItemText } from "~/atoms/context-menu"
import { useGeneralSettingSelector } from "~/atoms/settings/general"
import { CollapseControlled } from "~/components/ui/collapse"
import { RelativeTime } from "~/components/ui/datetime"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu/dropdown-menu"
import type { LinkProps } from "~/components/ui/link"
import { Markdown } from "~/components/ui/markdown/Markdown"
import { MarkdownLink } from "~/components/ui/markdown/renderers"
import { Media } from "~/components/ui/media"
import { usePreviewMedia } from "~/components/ui/media/hooks"
import { Media } from "~/components/ui/media/Media"
import { PeekModal } from "~/components/ui/modal/inspire/PeekModal"
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"

View File

@ -1,10 +1,10 @@
import { CollapseGroup } from "@follow/components/ui/collapse/Collapse.js"
import { FeedViewType } from "@follow/constants"
import { cn } from "@follow/utils/utils"
import { atom, useAtomValue, useSetAtom } from "jotai"
import { AnimatePresence, LayoutGroup, m } from "motion/react"
import * as React from "react"
import { CollapseGroup } from "~/components/ui/collapse"
import { PeekModalBaseButton } from "~/components/ui/modal/components/base"
import { ROUTE_FEED_PENDING } from "~/constants"
import { useRouteParams } from "~/hooks/biz/useRouteParams"

View File

@ -9,7 +9,7 @@ import { memo } from "react"
import { useTranslation } from "react-i18next"
import { useLocation } from "react-router"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { useFollow } from "~/hooks/biz/useFollow"
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useFeedSafeUrl } from "~/hooks/common/useFeedSafeUrl"

View File

@ -5,6 +5,7 @@ import {
AccordionTrigger,
} from "@follow/components/ui/accordion/index.js"
import { Button } from "@follow/components/ui/button/index.js"
import { DropZone } from "@follow/components/ui/drop-zone/index.js"
import {
Form,
FormControl,
@ -21,8 +22,7 @@ import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import { z } from "zod"
import { DropZone } from "~/components/ui/drop-zone"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { apiFetch } from "~/lib/api-fetch"
import { toastFetchError } from "~/lib/error-parser"

View File

@ -1,7 +1,7 @@
import { Skeleton } from "@follow/components/ui/skeleton/index.jsx"
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { ListItem } from "~/modules/entry-column/templates/list-item-template"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { FeedTitle } from "~/modules/feed/feed-title"

View File

@ -1,7 +1,7 @@
import { Skeleton } from "@follow/components/ui/skeleton/index.jsx"
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { ListItem } from "~/modules/entry-column/templates/list-item-template"
import { FeedTitle } from "~/modules/feed/feed-title"

View File

@ -8,7 +8,8 @@ import { Masonry } from "@follow/components/ui/masonry/index.js"
import { useMeasure } from "@follow/hooks"
import { useState } from "react"
import { Media, MediaContainerWidthProvider } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContainerWidthProvider"
import type { EntryItemStatelessProps } from "../types"

View File

@ -31,7 +31,7 @@ import {
import { useEventCallback } from "usehooks-ts"
import { useActionLanguage, useGeneralSettingKey } from "~/atoms/settings/general"
import { MediaContainerWidthProvider } from "~/components/ui/media"
import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContainerWidthProvider"
import { imageActions } from "~/store/image"
import { getMasonryColumnValue, setMasonryColumnValue, useMasonryColumnValue } from "../atoms"

View File

@ -16,8 +16,8 @@ import { useTranslation } from "react-i18next"
import { useGeneralSettingKey } from "~/atoms/settings/general"
import { RelativeTime } from "~/components/ui/datetime"
import { HTML } from "~/components/ui/markdown/HTML"
import { Media } from "~/components/ui/media"
import { usePreviewMedia } from "~/components/ui/media/hooks"
import { Media } from "~/components/ui/media/Media"
import { useEntryIsRead } from "~/hooks/biz/useAsRead"
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
import { useRenderStyle } from "~/hooks/biz/useRenderStyle"

View File

@ -15,8 +15,8 @@ import { useActionLanguage, useGeneralSettingKey } from "~/atoms/settings/genera
import { m } from "~/components/common/Motion"
import { RelativeTime } from "~/components/ui/datetime"
import { HTML } from "~/components/ui/markdown/HTML"
import { Media } from "~/components/ui/media"
import { usePreviewMedia } from "~/components/ui/media/hooks"
import { Media } from "~/components/ui/media/Media"
import type { ModalContentComponent } from "~/components/ui/modal"
import { FixedModalCloseButton } from "~/components/ui/modal/components/close"
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"

View File

@ -13,7 +13,7 @@ import { AudioPlayer, useAudioPlayerAtomSelector } from "~/atoms/player"
import { useGeneralSettingKey } from "~/atoms/settings/general"
import { useRealInWideMode, useUISettingKey } from "~/atoms/settings/ui"
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { FEED_COLLECTION_LIST } from "~/constants"
import { useEntryIsRead } from "~/hooks/biz/useAsRead"
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"

View File

@ -10,7 +10,8 @@ import type { RenderComponentProps } from "masonic"
import { Masonry } from "masonic"
import { useState } from "react"
import { Media, MediaContainerWidthProvider } from "~/components/ui/media"
import { Media } from "~/components/ui/media/Media"
import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContainerWidthProvider"
const gutter = 24

View File

@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"
import RSSHubIconUrl from "~/assets/rsshub-icon.png?url"
import { whoami } from "~/atoms/user"
import { ErrorTooltip } from "~/components/common/ErrorTooltip"
import { HeaderActionButton, HeaderActionGroup } from "~/components/ui/button/header-action-button"
import { HeaderActionButton, HeaderActionGroup } from "~/components/ui/button/HeaderActionButton"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useAuthQuery } from "~/hooks/common"
import { useSetSubViewRightView, useSubViewTitle } from "~/modules/app-layout/subview/hooks"

View File

@ -1,5 +1,17 @@
import { useGlobalFocusableHasScope } from "@follow/components/common/Focusable/hooks.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuPortal,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@follow/components/ui/context-menu/context-menu.js"
import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
import { nextFrame, preventDefault } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
@ -13,18 +25,6 @@ import {
MenuItemType,
useContextMenuState,
} from "~/atoms/context-menu"
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuPortal,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "~/components/ui/context-menu"
import { HotkeyScope } from "~/constants"
export const ContextMenuProvider: Component = ({ children }) => (

View File

@ -1,9 +1,10 @@
import { Divider } from "@follow/components/ui/divider/Divider.js"
import { RootPortal } from "@follow/components/ui/portal/index.jsx"
import { cn } from "@follow/utils/utils"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import * as React from "react"
import { Divider } from "../divider/Divider.js"
import { RootPortal } from "../portal/index.jsx"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group

View File

@ -60,7 +60,7 @@ export const DropZone = ({
return (
<label
className={`center cursor-button flex h-[100px] w-full rounded-md border border-dashed ${
isDragging ? "border-blue-500 bg-blue-100" : ""
isDragging ? "border-accent bg-accent/10" : ""
}`}
htmlFor="upload-file"
{...dragHandlers}

View File

@ -1,9 +1,4 @@
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.jsx"
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../tooltip/index.jsx"
export interface LinkProps {
href: string

View File

@ -0,0 +1 @@
export * from "./LinkWithTooltip"