refactor: simplify audio attachment playback component

This commit is contained in:
memoclaw 2026-04-02 21:56:01 +08:00
parent 9676e72533
commit 0e4d2d25ca
4 changed files with 25 additions and 58 deletions

View File

@ -87,10 +87,10 @@ export const VoiceRecorderPanel: FC<VoiceRecorderPanelProps> = ({
<div className="mt-3">
<AudioAttachmentItem
filename={recording.localFile.file.name}
displayName="Voice note"
sourceUrl={recording.localFile.previewUrl}
mimeType={recording.mimeType}
size={recording.localFile.file.size}
title="Voice note"
/>
</div>
)}

View File

@ -6,7 +6,6 @@ import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import SectionHeader from "../SectionHeader";
import AudioAttachmentItem from "./AudioAttachmentItem";
interface AttachmentListEditorProps {
attachments: Attachment[];
@ -23,38 +22,11 @@ const AttachmentItemCard: FC<{
canMoveUp?: boolean;
canMoveDown?: boolean;
}> = ({ item, onRemove, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true }) => {
const { category, filename, thumbnailUrl, mimeType, size, sourceUrl } = item;
const { category, filename, thumbnailUrl, mimeType, size } = item;
const fileTypeLabel = getFileTypeLabel(mimeType);
const fileSizeLabel = size ? formatFileSize(size) : undefined;
const displayName = category === "audio" && /^voice-(recording|note)-/i.test(filename) ? "Voice note" : filename;
if (category === "audio") {
return (
<div className="rounded border border-transparent transition-all hover:border-border hover:bg-accent/20">
<AudioAttachmentItem
filename={filename}
displayName={displayName}
sourceUrl={sourceUrl}
mimeType={mimeType}
size={size}
actionSlot={
onRemove ? (
<button
type="button"
onClick={onRemove}
className="inline-flex size-6.5 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
title="Remove"
aria-label="Remove attachment"
>
<XIcon className="h-3 w-3" />
</button>
) : undefined
}
/>
</div>
);
}
return (
<div className="relative rounded border border-transparent px-1.5 py-1 transition-all hover:border-border hover:bg-accent/20">
<div className="flex items-center gap-1.5">

View File

@ -147,7 +147,13 @@ const VisualSection = ({ attachments, onImageClick }: { attachments: Attachment[
const AudioList = ({ attachments }: { attachments: Attachment[] }) => (
<div className="flex flex-col gap-2">
{attachments.map((attachment) => (
<AudioAttachmentItem key={attachment.name} attachment={attachment} />
<AudioAttachmentItem
key={attachment.name}
filename={attachment.filename}
sourceUrl={getAttachmentUrl(attachment)}
mimeType={attachment.type}
size={Number(attachment.size)}
/>
))}
</div>
);

View File

@ -1,9 +1,7 @@
import { FileAudioIcon, PauseIcon, PlayIcon } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentUrl } from "@/utils/attachment";
import { useEffect, useRef, useState } from "react";
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import { formatAudioTime, getAttachmentMetadata } from "./attachmentViewHelpers";
import { formatAudioTime } from "./attachmentViewHelpers";
const AUDIO_PLAYBACK_RATES = [1, 1.5, 2] as const;
@ -47,30 +45,22 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on
);
interface AudioAttachmentItemProps {
attachment?: Attachment;
filename?: string;
displayName?: string;
sourceUrl?: string;
mimeType?: string;
filename: string;
sourceUrl: string;
mimeType: string;
size?: number;
actionSlot?: ReactNode;
title?: string;
}
const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mimeType, size, actionSlot }: AudioAttachmentItemProps) => {
const resolvedFilename = attachment?.filename ?? filename ?? "audio";
const resolvedDisplayName = displayName ?? resolvedFilename;
const resolvedSourceUrl = attachment ? getAttachmentUrl(attachment) : (sourceUrl ?? "");
const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: AudioAttachmentItemProps) => {
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [playbackRate, setPlaybackRate] = useState<(typeof AUDIO_PLAYBACK_RATES)[number]>(1);
const { fileTypeLabel, fileSizeLabel } = attachment
? getAttachmentMetadata(attachment)
: {
fileTypeLabel: getFileTypeLabel(mimeType ?? ""),
fileSizeLabel: size ? formatFileSize(size) : undefined,
};
const displayTitle = title ?? filename;
const fileTypeLabel = getFileTypeLabel(mimeType);
const fileSizeLabel = size ? formatFileSize(size) : undefined;
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
useEffect(() => {
@ -131,8 +121,8 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
<div className="flex min-w-0 flex-1 items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium leading-5 text-foreground" title={resolvedFilename}>
{resolvedDisplayName}
<div className="truncate text-sm font-medium leading-5 text-foreground" title={filename}>
{displayTitle}
</div>
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs leading-4 text-muted-foreground">
<span>{fileTypeLabel}</span>
@ -146,12 +136,11 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
</div>
<div className="mt-0.5 flex shrink-0 items-center gap-1">
{actionSlot}
<button
type="button"
onClick={handlePlaybackRateChange}
className="inline-flex h-6 items-center justify-center px-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
aria-label={`Playback speed ${playbackRate}x for ${resolvedDisplayName}`}
aria-label={`Playback speed ${playbackRate}x for ${displayTitle}`}
>
{playbackRate}x
</button>
@ -159,7 +148,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
type="button"
onClick={togglePlayback}
className="inline-flex size-6.5 items-center justify-center rounded-md border border-border/45 bg-background/85 text-foreground transition-colors hover:bg-muted/45"
aria-label={isPlaying ? `Pause ${resolvedDisplayName}` : `Play ${resolvedDisplayName}`}
aria-label={isPlaying ? `Pause ${displayTitle}` : `Play ${displayTitle}`}
>
{isPlaying ? <PauseIcon className="size-3" /> : <PlayIcon className="size-3 translate-x-[0.5px]" />}
</button>
@ -168,7 +157,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
</div>
<AudioProgressBar
filename={resolvedFilename}
filename={filename}
currentTime={currentTime}
duration={duration}
progressPercent={progressPercent}
@ -177,7 +166,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
<audio
ref={audioRef}
src={resolvedSourceUrl}
src={sourceUrl}
preload="metadata"
className="hidden"
onLoadedMetadata={(e) => handleDuration(e.currentTarget.duration)}