Skip to content

Commit

Permalink
feat: picture entry preview modal
Browse files Browse the repository at this point in the history
  • Loading branch information
DIYgod committed Sep 26, 2024
1 parent 8e60217 commit 29f2c0c
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 80 deletions.
28 changes: 8 additions & 20 deletions apps/renderer/src/components/ui/media/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ import { cn } from "~/lib/utils"

import { MotionButtonBase } from "../button"
import { softSpringPreset } from "../constants/spring"
import { KbdCombined } from "../kbd/Kbd"
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"
import { VolumeSlider } from "./VolumeSlider"

type VideoPlayerProps = {
Expand Down Expand Up @@ -225,7 +223,7 @@ const ControlBar = memo(() => {
dragMomentum={false}
dragConstraints={{ current: document.documentElement }}
className={cn(
"absolute inset-x-2 -bottom-10 h-8 rounded-2xl border bg-zinc-100/90 backdrop-blur-xl dark:border-transparent dark:bg-neutral-700/90",
"absolute inset-x-2 bottom-2 h-8 rounded-2xl border bg-zinc-100/90 backdrop-blur-xl dark:border-transparent dark:bg-neutral-700/90",
"flex items-center gap-3 px-3",
"mx-auto max-w-[80vw]",
)}
Expand Down Expand Up @@ -423,8 +421,6 @@ const PlayProgressBar = () => {
const ActionIcon = ({
className,
onClick,
label,
labelDelayDuration = 700,
children,
shortcut,
}: {
Expand All @@ -446,20 +442,12 @@ const ActionIcon = ({
},
)
return (
<Tooltip delayDuration={labelDelayDuration}>
<TooltipTrigger asChild>
<button
type="button"
className="center relative z-[1] size-6 rounded-md hover:bg-theme-button-hover"
onClick={onClick}
>
{children || <i className={className} />}
</button>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-1 text-xs">
{label}
{shortcut && <KbdCombined>{shortcut}</KbdCombined>}
</TooltipContent>
</Tooltip>
<button
type="button"
className="center relative z-[1] size-6 rounded-md hover:bg-theme-button-hover"
onClick={onClick}
>
{children || <i className={className} />}
</button>
)
}
4 changes: 2 additions & 2 deletions apps/renderer/src/components/ui/media/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { useModalStack } from "../modal/stacked/hooks"
import type { PreviewMediaProps } from "./preview-media"
import { PreviewMediaContent } from "./preview-media"

export const usePreviewMedia = () => {
export const usePreviewMedia = (entryId?: string) => {
const { present } = useModalStack()
return useCallback(
(media: PreviewMediaProps[], initialIndex = 0) => {
present({
content: () => (
<div className="relative size-full">
<PreviewMediaContent initialIndex={initialIndex} media={media} />
<PreviewMediaContent initialIndex={initialIndex} media={media} entryId={entryId} />
</div>
),
title: "Media Preview",
Expand Down
112 changes: 62 additions & 50 deletions apps/renderer/src/components/ui/media/preview-media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { tipcClient } from "~/lib/client"
import { stopPropagation } from "~/lib/dom"
import { replaceImgUrlIfNeed } from "~/lib/img-proxy"
import { cn } from "~/lib/utils"
import { EntryContent } from "~/modules/entry-content"

import { ActionButton, MotionButtonBase } from "../button"
import { microReboundPreset } from "../constants/spring"
Expand All @@ -20,55 +21,61 @@ import { VideoPlayer } from "./VideoPlayer"
const Wrapper: Component<{
src: string
showActions?: boolean
}> = ({ children, src, showActions }) => {
entryId?: string
}> = ({ children, src, showActions, entryId }) => {
const { dismiss } = useCurrentModal()

return (
<div className="center relative size-full p-12" onClick={dismiss}>
<div className="center relative size-full px-20 pb-8 pt-10" onClick={dismiss}>
<m.div
className="center size-full"
className="center flex size-full"
initial={{ scale: 0.94, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.94, opacity: 0 }}
transition={microReboundPreset}
>
{children}
</m.div>
<m.div
initial={{ opacity: 0.8 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute bottom-4 right-4 flex gap-3 text-white/70 [&_button]:hover:text-white"
onClick={stopPropagation}
>
<button
onClick={dismiss}
className="center fixed right-6 top-6 size-8 rounded-full border border-white/20 bg-neutral-900 text-white"
type="button"
<div
className={cn(
"relative flex h-full w-auto overflow-hidden",
entryId ? "rounded-l-xl bg-native" : "rounded-xl",
)}
>
<i className="i-mgc-close-cute-re" />
</button>
{showActions && (
<Fragment>
{!!window.electron && (
<ActionButton
tooltip="Download"
onClick={() => {
tipcClient?.download(src)
}}
>
<i className="i-mgc-download-2-cute-re" />
</ActionButton>
{children}
<div
className="absolute bottom-4 right-4 z-[99] flex gap-3 text-white/70 [&_button]:hover:text-white"
onClick={stopPropagation}
>
{showActions && (
<Fragment>
{!!window.electron && (
<ActionButton
tooltip="Download"
onClick={() => {
tipcClient?.download(src)
}}
>
<i className="i-mgc-download-2-cute-re" />
</ActionButton>
)}
<ActionButton
tooltip={COPY_MAP.OpenInBrowser()}
onClick={() => {
window.open(src)
}}
>
<i className="i-mgc-external-link-cute-re" />
</ActionButton>
</Fragment>
)}
<ActionButton
tooltip={COPY_MAP.OpenInBrowser()}
onClick={() => {
window.open(src)
}}
>
<i className="i-mgc-external-link-cute-re" />
</ActionButton>
</Fragment>
</div>
</div>
{entryId && (
<div
className="box-border flex h-full w-[400px] min-w-0 shrink-0 flex-col rounded-r-xl bg-white px-2 pt-1"
onClick={stopPropagation}
>
<EntryContent entryId={entryId} noMedia compact />
</div>
)}
</m.div>
</div>
Expand All @@ -81,7 +88,8 @@ export interface PreviewMediaProps extends MediaModel {
export const PreviewMediaContent: FC<{
media: PreviewMediaProps[]
initialIndex?: number
}> = ({ media, initialIndex = 0 }) => {
entryId?: string
}> = ({ media, initialIndex = 0, entryId }) => {
const [currentMedia, setCurrentMedia] = useState(media[initialIndex])
const [currentSlideIndex, setCurrentSlideIndex] = useState(initialIndex)
const swiperRef = useRef<SwiperRef>(null)
Expand All @@ -100,20 +108,21 @@ export const PreviewMediaContent: FC<{
const src = media[0].url
const { type } = media[0]
return (
<Wrapper src={src} showActions={type !== "video"}>
<Wrapper src={src} showActions={type !== "video"} entryId={entryId}>
{type === "video" ? (
<VideoPlayer
src={src}
controls
autoPlay
muted
className="size-full object-contain"
className={cn("h-full w-auto object-contain", entryId && "rounded-l-xl")}
onClick={stopPropagation}
/>
) : (
<FallbackableImage
fallbackUrl={media[0].fallbackUrl}
className="size-full object-contain"
containerClassName="w-auto"
className="h-full w-auto object-contain"
alt="cover"
src={src}
/>
Expand All @@ -122,7 +131,7 @@ export const PreviewMediaContent: FC<{
)
}
return (
<Wrapper src={currentMedia.url} showActions={currentMedia.type !== "video"}>
<Wrapper src={currentMedia.url} showActions={currentMedia.type !== "video"} entryId={entryId}>
<Swiper
ref={swiperRef}
loop
Expand All @@ -138,7 +147,7 @@ export const PreviewMediaContent: FC<{
setCurrentSlideIndex(realIndex)
}}
modules={[Mousewheel, Keyboard]}
className="size-full"
className="h-full w-auto"
>
{showActions && (
<div tabIndex={-1} onClick={stopPropagation}>
Expand All @@ -148,7 +157,7 @@ export const PreviewMediaContent: FC<{
transition={{ ease: "easeInOut", duration: 0.2 }}
onClick={() => swiperRef.current?.swiper.slidePrev()}
type="button"
className="center fixed left-2 top-1/2 z-[99] size-8 -translate-y-1/2 rounded-full border border-white/20 bg-neutral-900/80 text-white backdrop-blur duration-200 hover:bg-neutral-900"
className="center absolute left-2 top-1/2 z-[99] size-8 -translate-y-1/2 rounded-full border border-white/20 bg-neutral-900/80 text-white backdrop-blur duration-200 hover:bg-neutral-900"
>
<i className="i-mingcute-arrow-left-line" />
</m.button>
Expand All @@ -159,7 +168,7 @@ export const PreviewMediaContent: FC<{
transition={{ ease: "easeInOut", duration: 0.2 }}
onClick={() => swiperRef.current?.swiper.slideNext()}
type="button"
className="center fixed right-2 top-1/2 z-[99] size-8 -translate-y-1/2 rounded-full border border-white/20 bg-neutral-900/80 text-white backdrop-blur duration-200 hover:bg-neutral-900"
className="center absolute right-2 top-1/2 z-[99] size-8 -translate-y-1/2 rounded-full border border-white/20 bg-neutral-900/80 text-white backdrop-blur duration-200 hover:bg-neutral-900"
>
<i className="i-mingcute-arrow-right-line" />
</m.button>
Expand All @@ -168,13 +177,13 @@ export const PreviewMediaContent: FC<{

{showActions && (
<div>
<div className="fixed bottom-4 left-4 text-sm tabular-nums text-white/60 animate-in fade-in-0 slide-in-from-bottom-6">
<div className="absolute bottom-4 left-4 text-sm tabular-nums text-white/60 animate-in fade-in-0 slide-in-from-bottom-6">
{currentSlideIndex + 1} / {media.length}
</div>
<div
tabIndex={-1}
onClick={stopPropagation}
className="center fixed bottom-4 left-1/2 h-6 -translate-x-1/2 gap-2 rounded-full bg-neutral-700/90 px-4 duration-200 animate-in fade-in-0 slide-in-from-bottom-6"
className="center absolute bottom-4 left-1/2 z-[99] h-6 -translate-x-1/2 gap-2 rounded-full bg-neutral-700/90 px-4 duration-200 animate-in fade-in-0 slide-in-from-bottom-6"
>
{Array.from({ length: media.length })
.fill(0)
Expand All @@ -200,6 +209,8 @@ export const PreviewMediaContent: FC<{
{med.type === "video" ? (
<VideoPlayer
src={med.url}
autoPlay
muted
controls
className="size-full object-contain"
onClick={(e) => e.stopPropagation()}
Expand All @@ -223,9 +234,10 @@ export const PreviewMediaContent: FC<{
const FallbackableImage: FC<
Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> & {
src: string
containerClassName?: string
fallbackUrl?: string
}
> = ({ src, onError, fallbackUrl, ...props }) => {
> = ({ src, onError, fallbackUrl, containerClassName, ...props }) => {
const [currentSrc, setCurrentSrc] = useState(() => replaceImgUrlIfNeed(src))
const [isAllError, setIsAllError] = useState(false)

Expand Down Expand Up @@ -266,7 +278,7 @@ const FallbackableImage: FC<
}, [currentSrc, currentState, fallbackUrl, src])

return (
<div className="flex size-full flex-col">
<div className={cn("flex size-full flex-col", containerClassName)}>
{isLoading && !isAllError && (
<div className="center absolute inset-0 size-full">
<i className="i-mgc-loading-3-cute-re size-8 animate-spin text-white/80" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({

const isActive = useRouteParamsSelector(({ entryId }) => entryId === entry?.entries.id)

const previewMedia = usePreviewMedia()
const previewMedia = usePreviewMedia(entryId)
const itemWidth = useMasonryItemWidth()

const [ref, setRef] = useState<HTMLDivElement | null>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from "react"

import { useWhoami } from "~/atoms/user"
import { useAuthQuery } from "~/hooks/common"
import { cn } from "~/lib/utils"
import type { FeedModel } from "~/models"
import { Queries } from "~/queries"
import { useEntry, useEntryReadHistory } from "~/store/entry"
Expand All @@ -11,6 +12,7 @@ import { EntryTranslation } from "../../entry-column/translation"

interface EntryLinkProps {
entryId: string
compact?: boolean
}

const safeUrl = (url: string, baseUrl: string) => {
Expand All @@ -21,7 +23,7 @@ const safeUrl = (url: string, baseUrl: string) => {
}
}

export const EntryTitle = ({ entryId }: EntryLinkProps) => {
export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
const user = useWhoami()
const entry = useEntry(entryId)
const feed = useFeedById(entry?.feedId) as FeedModel
Expand Down Expand Up @@ -62,7 +64,7 @@ export const EntryTitle = ({ entryId }: EntryLinkProps) => {
className="-mx-6 block cursor-button rounded-lg p-6 transition-colors hover:bg-theme-item-hover focus-visible:bg-theme-item-hover focus-visible:!outline-none @sm:-mx-3 @sm:p-3"
rel="noreferrer"
>
<div className="select-text break-words text-3xl font-bold">
<div className={cn("select-text break-words font-bold", compact ? "text-2xl" : "text-3xl")}>
<EntryTranslation source={entry.entries.title} target={translation.data?.title} />
</div>
<div className="mt-2 text-[13px] font-medium text-zinc-500">{getPreferredTitle(feed)}</div>
Expand Down
4 changes: 3 additions & 1 deletion apps/renderer/src/modules/entry-content/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ function EntryHeaderImpl({
view,
entryId,
className,
compact,
}: {
view: number
entryId: string
className?: string
compact?: boolean
}) {
const entry = useEntry(entryId)

Expand Down Expand Up @@ -90,7 +92,7 @@ function EntryHeaderImpl({
</div>

<div className="relative flex shrink-0 items-center justify-end gap-3">
<ElectronAdditionActions view={view} entry={entry} key={entry.entries.id} />
{!compact && <ElectronAdditionActions view={view} entry={entry} key={entry.entries.id} />}

{items
.filter((item) => !item.hide)
Expand Down
Loading

0 comments on commit 29f2c0c

Please sign in to comment.