Skip to content

Commit

Permalink
feat: masonry item blur hash and html render also
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Sep 30, 2024
1 parent a954eb9 commit 6f94ec5
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 26 deletions.
8 changes: 7 additions & 1 deletion apps/renderer/src/components/ui/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { parseHtml } from "~/lib/parse-html"
import type { RemarkOptions } from "~/lib/parse-markdown"
import { parseMarkdown } from "~/lib/parse-markdown"
import { cn } from "~/lib/utils"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"

import { MediaContainerWidthProvider } from "../media"
import { MarkdownRenderContainerRefContext } from "./context"

export const Markdown: Component<
Expand Down Expand Up @@ -79,10 +81,14 @@ const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(
[children, remarkOptions],
)

const { w: containerWidth } = useWrappedElementSize()

if (!markdownElement) return null
return (
<MarkdownRenderContainerRefContext.Provider value={refElement}>
{createElement(as, { ...rest, ref: setRefElement }, markdownElement)}
<MediaContainerWidthProvider width={containerWidth}>
{createElement(as, { ...rest, ref: setRefElement }, markdownElement)}
</MediaContainerWidthProvider>
{accessory && <Fragment key={shouldForceReMountKey}>{accessory}</Fragment>}
</MarkdownRenderContainerRefContext.Provider>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { cn } from "~/lib/utils"
import { useEntryContentContext } from "~/modules/entry-content/hooks"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
import { useEntry } from "~/store/entry"

import { Media } from "../../media"

Expand All @@ -13,17 +15,23 @@ export const MarkdownBlockImage = (
) => {
const size = useWrappedElementSize()

const { entryId } = useEntryContentContext()
const media = useEntry(entryId, (entry) => entry?.entries.media?.find((m) => m.url === props.src))

return (
<Media
type="photo"
{...props}
height={media?.height || props.height}
width={media?.width || props.width}
blurhash={media?.blurhash}
mediaContainerClassName={cn(
"rounded",
size.w < Number.parseInt(props.width as string) && "w-full",
)}
showFallback
popper
className="flex justify-center"
className="my-8 flex justify-center"
/>
)
}
102 changes: 91 additions & 11 deletions apps/renderer/src/components/ui/media.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useForceUpdate } from "framer-motion"
import type { FC, ImgHTMLAttributes, VideoHTMLAttributes } from "react"
import { memo, useMemo, useState } from "react"
import { createContext, memo, useContext, useMemo, useState } from "react"
import { Blurhash } from "react-blurhash"
import { useEventCallback } from "usehooks-ts"

import { nextFrame } from "~/lib/dom"
Expand All @@ -18,6 +19,8 @@ type BaseProps = {
mediaContainerClassName?: string
showFallback?: boolean
thumbnail?: boolean

blurhash?: string
}
export type MediaProps = BaseProps &
(
Expand All @@ -30,7 +33,6 @@ export type MediaProps = BaseProps &
type: "photo"
previewImageUrl?: string
cacheDimensions?: boolean
blurhash?: string
})
| (VideoHTMLAttributes<HTMLVideoElement> & {
proxy?: {
Expand All @@ -50,7 +52,7 @@ const MediaImpl: FC<MediaProps> = ({
thumbnail,
...props
}) => {
const { src, style, type, previewImageUrl, showFallback, ...rest } = props
const { src, style, type, previewImageUrl, showFallback, blurhash, ...rest } = props

const [imgSrc, setImgSrc] = useState(() =>
proxy && src && !failedList.has(src)
Expand Down Expand Up @@ -90,15 +92,17 @@ const MediaImpl: FC<MediaProps> = ({
const previewMedia = usePreviewMedia()
const handleClick = useEventCallback((e: React.MouseEvent) => {
if (popper && src) {
const width = Number.parseInt(props.width as string)
const height = Number.parseInt(props.height as string)
previewMedia(
[
{
url: src,
type,
fallbackUrl: imgSrc,
blurhash: "blurhash" in props ? props.blurhash : undefined,
width: Number.parseInt(props.width as string),
height: Number.parseInt(props.height as string),
blurhash: props.blurhash,
width: width || undefined,
height: height || undefined,
},
],
0,
Expand All @@ -119,6 +123,8 @@ const MediaImpl: FC<MediaProps> = ({
}
})

const containerWidth = useMediaContainerWidth()

const InnerContent = useMemo(() => {
switch (type) {
case "photo": {
Expand All @@ -127,11 +133,11 @@ const MediaImpl: FC<MediaProps> = ({
{...(rest as ImgHTMLAttributes<HTMLImageElement>)}
onError={errorHandle}
className={cn(
!(props.width || props.height) && "size-full",
"cursor-card bg-gray-200 object-cover duration-200 dark:bg-neutral-800",
"size-full object-contain",
"bg-gray-200 duration-200 dark:bg-neutral-800",
popper && "cursor-zoom-in",
mediaLoadState === "loaded" ? "opacity-100" : "opacity-0",

"!my-0",
mediaContainerClassName,
)}
src={imgSrc}
Expand Down Expand Up @@ -190,6 +196,7 @@ const MediaImpl: FC<MediaProps> = ({
)
} else {
return (
// TODO blurhash
<div
className={cn("rounded bg-zinc-100 dark:bg-neutral-900", className)}
style={props.style}
Expand All @@ -201,10 +208,29 @@ const MediaImpl: FC<MediaProps> = ({
return (
<span
data-state={type !== "video" ? mediaLoadState : undefined}
className={cn("block overflow-hidden rounded", className)}
className={cn("relative block overflow-hidden rounded", className)}
style={style}
>
{InnerContent}
{!!props.width && !!props.height && !!containerWidth ? (
<AspectRatio
width={Number.parseInt(props.width as string)}
height={Number.parseInt(props.height as string)}
containerWidth={containerWidth}
>
<div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded">
{blurhash ? (
<Blurhash hash={blurhash} width="100%" height="100%" />
) : (
<div className="size-full bg-border" />
)}
</div>
<div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded">
{InnerContent}
</div>
</AspectRatio>
) : (
InnerContent
)}
</span>
)
}
Expand Down Expand Up @@ -241,6 +267,41 @@ const FallbackMedia: FC<MediaProps> = ({ type, mediaContainerClassName, classNam
</div>
)

const AspectRatio = ({
width,
height,
containerWidth,
children,
style,
...props
}: {
width: number
height: number
containerWidth?: number
children: React.ReactNode
style?: React.CSSProperties
[key: string]: any
}) => {
const scaleFactor = containerWidth && width ? Math.min(1, containerWidth / width) : 1

const scaledWidth = width ? width * scaleFactor : undefined
const scaledHeight = height ? height * scaleFactor : undefined

return (
<div
style={{
position: "relative",
width: scaledWidth ? `${scaledWidth}px` : "100%",
height: scaledHeight ? `${scaledHeight}px` : "auto",
...style,
}}
{...props}
>
{children}
</div>
)
}

const VideoPreview: FC<{
src: string
previewImageUrl?: string
Expand Down Expand Up @@ -293,3 +354,22 @@ const VideoPreview: FC<{
</div>
)
}

const MediaContainerWidthContext = createContext<number>(0)
export const MediaContainerWidthProvider = ({
children,
width,
}: {
children: React.ReactNode
width: number
}) => {
return (
<MediaContainerWidthContext.Provider value={width}>
{children}
</MediaContainerWidthContext.Provider>
)
}

const useMediaContainerWidth = () => {
return useContext(MediaContainerWidthContext)
}
4 changes: 4 additions & 0 deletions apps/renderer/src/components/ui/media/SwipeMedia.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function SwipeMedia({
previewImageUrl={med.preview_image_url}
loading="lazy"
proxy={proxySize}
blurhash={med.blurhash}
onClick={(e) => {
e.stopPropagation()
onPreview?.(uniqMedia, i)
Expand Down Expand Up @@ -131,6 +132,9 @@ export function SwipeMedia({
loading="lazy"
proxy={proxySize}
showFallback={true}
height={uniqMedia[0].height}
width={uniqMedia[0].width}
blurhash={uniqMedia[0].blurhash}
/>
) : (
<div className="relative flex aspect-video w-full items-center overflow-hidden rounded-t-2xl border-b">
Expand Down
2 changes: 2 additions & 0 deletions apps/renderer/src/components/ui/media/preview-media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ const FallbackableImage: FC<
return (
<div className={cn("center flex size-full flex-col", containerClassName)}>
{isLoading && !isAllError && (
// FIXME: optimize this if image load, the placeholder background will flash
<div className="center absolute inset-0 size-full">
{blurhash ? (
<div style={{ aspectRatio: `${props.width} / ${props.height}` }} className="w-full">
Expand All @@ -305,6 +306,7 @@ const FallbackableImage: FC<
)}
{!isAllError && (
<img
data-blurhash={blurhash}
src={currentSrc}
onLoad={() => setIsLoading(false)}
onError={handleError}
Expand Down
23 changes: 13 additions & 10 deletions apps/renderer/src/modules/entry-column/Items/picture-masonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useEventCallback } from "usehooks-ts"

import { useGeneralSettingKey } from "~/atoms/settings/general"
import { Masonry } from "~/components/ui/Masonry"
import { MediaContainerWidthProvider } from "~/components/ui/media"
import { useScrollViewElement } from "~/components/ui/scroll-area/hooks"
import { Skeleton } from "~/components/ui/skeleton"
import { useRefValue } from "~/hooks/common"
Expand Down Expand Up @@ -235,16 +236,18 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
<MasonryItemsAspectRatioContext.Provider value={masonryItemsRadio}>
<MasonryItemsAspectRatioSetterContext.Provider value={setMasonryItemsRadio}>
<MasonryIntersectionContext.Provider value={intersectionObserver}>
<Masonry
items={items}
columnGutter={gutter}
columnWidth={currentItemWidth}
columnCount={currentColumn}
overscanBy={2}
render={render}
onRender={handleRender}
itemKey={itemKey}
/>
<MediaContainerWidthProvider width={currentItemWidth}>
<Masonry
items={items}
columnGutter={gutter}
columnWidth={currentItemWidth}
columnCount={currentColumn}
overscanBy={2}
render={render}
onRender={handleRender}
itemKey={itemKey}
/>
</MediaContainerWidthProvider>
</MasonryIntersectionContext.Provider>
</MasonryItemsAspectRatioSetterContext.Provider>
</MasonryItemsAspectRatioContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) =>
<button
type="button"
style={{
right: `${(LIMIT - 1) * 8}px`,
right: `${LIMIT * 8}px`,
zIndex: 11,
}}
className="no-drag-region relative flex size-7 items-center justify-center rounded-full border border-border bg-muted ring-2 ring-background"
Expand Down
17 changes: 15 additions & 2 deletions apps/renderer/src/store/entry/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ import { getEntryIsInView, getFilteredFeedIds } from "./helper"
import { useEntryStore } from "./store"
import type { EntryFilter, FlatEntryModel } from "./types"

export const useEntry = (entryId: Nullable<string>): FlatEntryModel | null =>
useEntryStore(useShallow((state) => (entryId ? state.flatMapEntries[entryId] : null)))
export const useEntry = <T = FlatEntryModel>(
entryId: Nullable<string>,
selector?: (state: FlatEntryModel) => T,
): T | null =>
useEntryStore(
useShallow((state) => {
if (!entryId) return null
const data = state.flatMapEntries[entryId]

if (!data) return null

return selector ? selector(data) : (data as T)
}),
)

// feedId: single feedId, multiple feedId joint by `,`, and `collections`
export const useEntryIdsByFeedId = (feedId: string, filter?: EntryFilter) =>
useEntryStore(
Expand Down

0 comments on commit 6f94ec5

Please sign in to comment.