Commit 124708f1 authored by boojack's avatar boojack

chore: refactor attachment media layout and insert menu organization

parent 7e21b728
...@@ -2,6 +2,7 @@ import { LatLng } from "leaflet"; ...@@ -2,6 +2,7 @@ import { LatLng } from "leaflet";
import { uniqBy } from "lodash-es"; import { uniqBy } from "lodash-es";
import { import {
FileIcon, FileIcon,
ImageIcon,
LinkIcon, LinkIcon,
LoaderIcon, LoaderIcon,
type LucideIcon, type LucideIcon,
...@@ -131,14 +132,22 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -131,14 +132,22 @@ const InsertMenu = (props: InsertMenuProps) => {
setMoreSubmenuOpen(false); setMoreSubmenuOpen(false);
}, [onToggleFocusMode]); }, [onToggleFocusMode]);
const handleMediaUploadClick = useCallback(() => {
handleUploadClick("image/*,video/*");
}, [handleUploadClick]);
const handleFileUploadClick = useCallback(() => {
handleUploadClick();
}, [handleUploadClick]);
const menuItems = useMemo( const menuItems = useMemo(
() => () =>
[ [
{ {
key: "upload", key: "upload-media",
label: t("editor.insert-menu.upload-file"), label: t("attachment-library.tabs.media"),
icon: FileIcon, icon: ImageIcon,
onClick: handleUploadClick, onClick: handleMediaUploadClick,
}, },
{ {
key: "record-audio", key: "record-audio",
...@@ -146,6 +155,12 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -146,6 +155,12 @@ const InsertMenu = (props: InsertMenuProps) => {
icon: MicIcon, icon: MicIcon,
onClick: () => props.onAudioRecorderClick?.(), onClick: () => props.onAudioRecorderClick?.(),
}, },
{
key: "upload-file",
label: t("common.file"),
icon: FileIcon,
onClick: handleFileUploadClick,
},
{ {
key: "link", key: "link",
label: t("editor.insert-menu.link-memo"), label: t("editor.insert-menu.link-memo"),
...@@ -159,7 +174,7 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -159,7 +174,7 @@ const InsertMenu = (props: InsertMenuProps) => {
onClick: handleLocationClick, onClick: handleLocationClick,
}, },
] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>, ] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>,
[handleLocationClick, handleOpenLinkDialog, handleUploadClick, props, t], [handleFileUploadClick, handleLocationClick, handleMediaUploadClick, handleOpenLinkDialog, props, t],
); );
return ( return (
...@@ -171,14 +186,14 @@ const InsertMenu = (props: InsertMenuProps) => { ...@@ -171,14 +186,14 @@ const InsertMenu = (props: InsertMenuProps) => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start"> <DropdownMenuContent align="start">
{menuItems.slice(0, 2).map((item) => ( {menuItems.slice(0, 3).map((item) => (
<DropdownMenuItem key={item.key} onClick={item.onClick}> <DropdownMenuItem key={item.key} onClick={item.onClick}>
<item.icon className="w-4 h-4" /> <item.icon className="w-4 h-4" />
{item.label} {item.label}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{menuItems.slice(2).map((item) => ( {menuItems.slice(3).map((item) => (
<DropdownMenuItem key={item.key} onClick={item.onClick}> <DropdownMenuItem key={item.key} onClick={item.onClick}>
<item.icon className="w-4 h-4" /> <item.icon className="w-4 h-4" />
{item.label} {item.label}
......
...@@ -26,8 +26,13 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void ...@@ -26,8 +26,13 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void
if (fileInputRef.current) fileInputRef.current.value = ""; if (fileInputRef.current) fileInputRef.current.value = "";
}; };
const handleUploadClick = () => { const handleUploadClick = (accept = "*") => {
fileInputRef.current?.click(); if (!fileInputRef.current) {
return;
}
fileInputRef.current.accept = accept;
fileInputRef.current.click();
}; };
return { return {
......
import { PauseIcon, PlayIcon } from "lucide-react"; import { PauseIcon, PlayIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import { formatAudioTime } from "./attachmentHelpers"; import { formatAudioTime } from "./attachmentHelpers";
...@@ -56,9 +57,11 @@ interface AudioAttachmentItemProps { ...@@ -56,9 +57,11 @@ interface AudioAttachmentItemProps {
mimeType: string; mimeType: string;
size?: number; size?: number;
title?: string; title?: string;
compact?: boolean;
className?: string;
} }
const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: AudioAttachmentItemProps) => { const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title, compact = false, className }: AudioAttachmentItemProps) => {
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
...@@ -119,9 +122,9 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud ...@@ -119,9 +122,9 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
}; };
return ( return (
<div className="rounded-xl border border-border/40 bg-background/75 px-2 py-1.5"> <div className={cn("rounded-xl border border-border/40 bg-background/75", compact ? "px-3 py-2.5" : "px-2 py-1.5", className)}>
<div className="flex items-center justify-between gap-1.5"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex flex-1 items-baseline gap-1"> <div className={cn("min-w-0 flex flex-1", compact ? "flex-col gap-0.5" : "items-baseline gap-1")}>
<div className="truncate text-sm font-medium leading-5 text-foreground" title={filename}> <div className="truncate text-sm font-medium leading-5 text-foreground" title={filename}>
{displayTitle} {displayTitle}
</div> </div>
...@@ -141,7 +144,7 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud ...@@ -141,7 +144,7 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
</button> </button>
</div> </div>
<div className="mt-1 flex items-center gap-1"> <div className={cn("mt-1", compact ? "space-y-1.5" : "flex items-center gap-1")}>
<AudioProgressBar <AudioProgressBar
filename={filename} filename={filename}
currentTime={currentTime} currentTime={currentTime}
...@@ -151,16 +154,18 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud ...@@ -151,16 +154,18 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
className="min-w-0 flex-1" className="min-w-0 flex-1"
/> />
<div className="shrink-0 text-[10px] tabular-nums text-muted-foreground">{timeLabel}</div> <div className={cn("flex items-center", compact ? "justify-between gap-2" : "shrink-0 gap-1")}>
<div className="shrink-0 text-[10px] tabular-nums text-muted-foreground">{timeLabel}</div>
<button
type="button" <button
onClick={handlePlaybackRateChange} type="button"
className="inline-flex h-5 shrink-0 items-center justify-center rounded-md border border-transparent px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:border-border/40 hover:text-foreground" onClick={handlePlaybackRateChange}
aria-label={`Playback speed ${playbackRate}x for ${displayTitle}`} className="inline-flex h-5 shrink-0 items-center justify-center rounded-md border border-transparent px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:border-border/40 hover:text-foreground"
> aria-label={`Playback speed ${playbackRate}x for ${displayTitle}`}
{playbackRate}x >
</button> {playbackRate}x
</button>
</div>
</div> </div>
<audio <audio
......
...@@ -8,6 +8,8 @@ interface MotionPhotoPlayerProps { ...@@ -8,6 +8,8 @@ interface MotionPhotoPlayerProps {
presentationTimestampUs?: bigint; presentationTimestampUs?: bigint;
containerClassName?: string; containerClassName?: string;
mediaClassName?: string; mediaClassName?: string;
posterClassName?: string;
videoClassName?: string;
active?: boolean; active?: boolean;
loop?: boolean; loop?: boolean;
} }
...@@ -19,6 +21,8 @@ const MotionPhotoPlayer = ({ ...@@ -19,6 +21,8 @@ const MotionPhotoPlayer = ({
presentationTimestampUs, presentationTimestampUs,
containerClassName, containerClassName,
mediaClassName, mediaClassName,
posterClassName,
videoClassName,
active, active,
loop = false, loop = false,
}: MotionPhotoPlayerProps) => { }: MotionPhotoPlayerProps) => {
...@@ -87,7 +91,7 @@ const MotionPhotoPlayer = ({ ...@@ -87,7 +91,7 @@ const MotionPhotoPlayer = ({
<img <img
src={posterUrl} src={posterUrl}
alt={alt} alt={alt}
className={cn("block max-h-full max-w-full select-none object-cover", mediaClassName)} className={cn("block max-h-full max-w-full select-none object-cover", mediaClassName, posterClassName)}
draggable={false} draggable={false}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
...@@ -100,6 +104,7 @@ const MotionPhotoPlayer = ({ ...@@ -100,6 +104,7 @@ const MotionPhotoPlayer = ({
"pointer-events-none absolute inset-0 h-full w-full object-cover transition-opacity duration-200", "pointer-events-none absolute inset-0 h-full w-full object-cover transition-opacity duration-200",
isPlaying ? "opacity-100" : "opacity-0", isPlaying ? "opacity-100" : "opacity-0",
mediaClassName, mediaClassName,
videoClassName,
)} )}
muted muted
playsInline playsInline
......
...@@ -9,6 +9,8 @@ interface MotionPhotoPreviewProps { ...@@ -9,6 +9,8 @@ interface MotionPhotoPreviewProps {
presentationTimestampUs?: bigint; presentationTimestampUs?: bigint;
containerClassName?: string; containerClassName?: string;
mediaClassName?: string; mediaClassName?: string;
posterClassName?: string;
videoClassName?: string;
badgeClassName?: string; badgeClassName?: string;
loop?: boolean; loop?: boolean;
} }
...@@ -20,6 +22,8 @@ const MotionPhotoPreview = ({ ...@@ -20,6 +22,8 @@ const MotionPhotoPreview = ({
presentationTimestampUs, presentationTimestampUs,
containerClassName, containerClassName,
mediaClassName, mediaClassName,
posterClassName,
videoClassName,
badgeClassName, badgeClassName,
loop = false, loop = false,
}: MotionPhotoPreviewProps) => { }: MotionPhotoPreviewProps) => {
...@@ -40,6 +44,8 @@ const MotionPhotoPreview = ({ ...@@ -40,6 +44,8 @@ const MotionPhotoPreview = ({
loop={loop} loop={loop}
containerClassName={cn("max-w-full max-h-full", containerClassName)} containerClassName={cn("max-w-full max-h-full", containerClassName)}
mediaClassName={mediaClassName} mediaClassName={mediaClassName}
posterClassName={posterClassName}
videoClassName={videoClassName}
/> />
<div <div
role="button" role="button"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment