Commit 0e4d2d25 authored by memoclaw's avatar memoclaw

refactor: simplify audio attachment playback component

parent 9676e725
...@@ -87,10 +87,10 @@ export const VoiceRecorderPanel: FC<VoiceRecorderPanelProps> = ({ ...@@ -87,10 +87,10 @@ export const VoiceRecorderPanel: FC<VoiceRecorderPanelProps> = ({
<div className="mt-3"> <div className="mt-3">
<AudioAttachmentItem <AudioAttachmentItem
filename={recording.localFile.file.name} filename={recording.localFile.file.name}
displayName="Voice note"
sourceUrl={recording.localFile.previewUrl} sourceUrl={recording.localFile.previewUrl}
mimeType={recording.mimeType} mimeType={recording.mimeType}
size={recording.localFile.file.size} size={recording.localFile.file.size}
title="Voice note"
/> />
</div> </div>
)} )}
......
...@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils"; ...@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import SectionHeader from "../SectionHeader"; import SectionHeader from "../SectionHeader";
import AudioAttachmentItem from "./AudioAttachmentItem";
interface AttachmentListEditorProps { interface AttachmentListEditorProps {
attachments: Attachment[]; attachments: Attachment[];
...@@ -23,38 +22,11 @@ const AttachmentItemCard: FC<{ ...@@ -23,38 +22,11 @@ const AttachmentItemCard: FC<{
canMoveUp?: boolean; canMoveUp?: boolean;
canMoveDown?: boolean; canMoveDown?: boolean;
}> = ({ item, onRemove, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true }) => { }> = ({ 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 fileTypeLabel = getFileTypeLabel(mimeType);
const fileSizeLabel = size ? formatFileSize(size) : undefined; const fileSizeLabel = size ? formatFileSize(size) : undefined;
const displayName = category === "audio" && /^voice-(recording|note)-/i.test(filename) ? "Voice note" : filename; 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 ( return (
<div className="relative rounded border border-transparent px-1.5 py-1 transition-all hover:border-border hover:bg-accent/20"> <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"> <div className="flex items-center gap-1.5">
......
...@@ -147,7 +147,13 @@ const VisualSection = ({ attachments, onImageClick }: { attachments: Attachment[ ...@@ -147,7 +147,13 @@ const VisualSection = ({ attachments, onImageClick }: { attachments: Attachment[
const AudioList = ({ attachments }: { attachments: Attachment[] }) => ( const AudioList = ({ attachments }: { attachments: Attachment[] }) => (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{attachments.map((attachment) => ( {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> </div>
); );
......
import { FileAudioIcon, PauseIcon, PlayIcon } from "lucide-react"; import { FileAudioIcon, PauseIcon, PlayIcon } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentUrl } from "@/utils/attachment";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; 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; const AUDIO_PLAYBACK_RATES = [1, 1.5, 2] as const;
...@@ -47,30 +45,22 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on ...@@ -47,30 +45,22 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on
); );
interface AudioAttachmentItemProps { interface AudioAttachmentItemProps {
attachment?: Attachment; filename: string;
filename?: string; sourceUrl: string;
displayName?: string; mimeType: string;
sourceUrl?: string;
mimeType?: string;
size?: number; size?: number;
actionSlot?: ReactNode; title?: string;
} }
const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mimeType, size, actionSlot }: AudioAttachmentItemProps) => { const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: AudioAttachmentItemProps) => {
const resolvedFilename = attachment?.filename ?? filename ?? "audio";
const resolvedDisplayName = displayName ?? resolvedFilename;
const resolvedSourceUrl = attachment ? getAttachmentUrl(attachment) : (sourceUrl ?? "");
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);
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [playbackRate, setPlaybackRate] = useState<(typeof AUDIO_PLAYBACK_RATES)[number]>(1); const [playbackRate, setPlaybackRate] = useState<(typeof AUDIO_PLAYBACK_RATES)[number]>(1);
const { fileTypeLabel, fileSizeLabel } = attachment const displayTitle = title ?? filename;
? getAttachmentMetadata(attachment) const fileTypeLabel = getFileTypeLabel(mimeType);
: { const fileSizeLabel = size ? formatFileSize(size) : undefined;
fileTypeLabel: getFileTypeLabel(mimeType ?? ""),
fileSizeLabel: size ? formatFileSize(size) : undefined,
};
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0; const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
useEffect(() => { useEffect(() => {
...@@ -131,8 +121,8 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim ...@@ -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="flex min-w-0 flex-1 items-start justify-between gap-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium leading-5 text-foreground" title={resolvedFilename}> <div className="truncate text-sm font-medium leading-5 text-foreground" title={filename}>
{resolvedDisplayName} {displayTitle}
</div> </div>
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs leading-4 text-muted-foreground"> <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> <span>{fileTypeLabel}</span>
...@@ -146,12 +136,11 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim ...@@ -146,12 +136,11 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
</div> </div>
<div className="mt-0.5 flex shrink-0 items-center gap-1"> <div className="mt-0.5 flex shrink-0 items-center gap-1">
{actionSlot}
<button <button
type="button" type="button"
onClick={handlePlaybackRateChange} 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" 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 {playbackRate}x
</button> </button>
...@@ -159,7 +148,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim ...@@ -159,7 +148,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
type="button" type="button"
onClick={togglePlayback} 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" 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]" />} {isPlaying ? <PauseIcon className="size-3" /> : <PlayIcon className="size-3 translate-x-[0.5px]" />}
</button> </button>
...@@ -168,7 +157,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim ...@@ -168,7 +157,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
</div> </div>
<AudioProgressBar <AudioProgressBar
filename={resolvedFilename} filename={filename}
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
progressPercent={progressPercent} progressPercent={progressPercent}
...@@ -177,7 +166,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim ...@@ -177,7 +166,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
<audio <audio
ref={audioRef} ref={audioRef}
src={resolvedSourceUrl} src={sourceUrl}
preload="metadata" preload="metadata"
className="hidden" className="hidden"
onLoadedMetadata={(e) => handleDuration(e.currentTarget.duration)} onLoadedMetadata={(e) => handleDuration(e.currentTarget.duration)}
......
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