mirror of https://github.com/usememos/memos
chore: refactor memo editor audio recording flow
parent
c3e7e2c316
commit
067d7ff0ce
@ -0,0 +1,52 @@
|
||||
import { LoaderCircleIcon, XIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { formatAudioTime } from "@/components/MemoMetadata/Attachment/attachmentHelpers";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import type { AudioRecorderPanelProps } from "../types/components";
|
||||
|
||||
export const AudioRecorderPanel: FC<AudioRecorderPanelProps> = ({ audioRecorder, onStop, onCancel }) => {
|
||||
const t = useTranslate();
|
||||
const { status, elapsedSeconds } = audioRecorder;
|
||||
|
||||
const isRequestingPermission = status === "requesting_permission";
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-border/60 bg-muted/20 px-2.5 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{isRequestingPermission ? t("editor.audio-recorder.requesting-permission") : t("editor.audio-recorder.recording")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
isRequestingPermission
|
||||
? "border border-border/60 bg-background text-muted-foreground"
|
||||
: "border border-destructive/20 bg-destructive/[0.08] text-destructive",
|
||||
)}
|
||||
>
|
||||
{isRequestingPermission ? (
|
||||
<LoaderCircleIcon className="size-3 animate-spin" />
|
||||
) : (
|
||||
<span className="size-2 rounded-full bg-destructive" />
|
||||
)}
|
||||
{formatAudioTime(elapsedSeconds)}
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
<Button variant="ghost" size="icon" onClick={onCancel} aria-label={t("common.cancel")}>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
<Button size="sm" className="gap-1.5" onClick={onStop} disabled={isRequestingPermission}>
|
||||
<span className="size-2.5 rounded-[2px] bg-current" aria-hidden="true" />
|
||||
{t("editor.audio-recorder.stop")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,135 +0,0 @@
|
||||
import { AudioLinesIcon, LoaderCircleIcon, MicIcon, RotateCcwIcon, SquareIcon, Trash2Icon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { AudioAttachmentItem } from "@/components/MemoMetadata/Attachment";
|
||||
import { formatAudioTime } from "@/components/MemoMetadata/Attachment/attachmentHelpers";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import type { VoiceRecorderPanelProps } from "../types/components";
|
||||
|
||||
export const VoiceRecorderPanel: FC<VoiceRecorderPanelProps> = ({
|
||||
voiceRecorder,
|
||||
onStart,
|
||||
onStop,
|
||||
onKeep,
|
||||
onDiscard,
|
||||
onRecordAgain,
|
||||
onClose,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const { status, elapsedSeconds, error, recording } = voiceRecorder;
|
||||
|
||||
const isRecording = status === "recording";
|
||||
const isRequestingPermission = status === "requesting_permission";
|
||||
const isUnsupported = status === "unsupported";
|
||||
const hasRecording = status === "recorded" && recording;
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-xl border border-border/60 bg-muted/25 px-3 py-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-10 shrink-0 items-center justify-center rounded-xl border border-border/60 bg-background/80 text-muted-foreground",
|
||||
isRecording && "border-destructive/30 bg-destructive/10 text-destructive",
|
||||
hasRecording && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{isRequestingPermission ? (
|
||||
<LoaderCircleIcon className="size-4 animate-spin" />
|
||||
) : hasRecording ? (
|
||||
<AudioLinesIcon className="size-4" />
|
||||
) : (
|
||||
<MicIcon className="size-4" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{isRecording
|
||||
? t("editor.voice-recorder.recording")
|
||||
: isRequestingPermission
|
||||
? t("editor.voice-recorder.requesting-permission")
|
||||
: hasRecording
|
||||
? t("editor.voice-recorder.ready")
|
||||
: isUnsupported
|
||||
? t("editor.voice-recorder.unsupported")
|
||||
: error
|
||||
? t("editor.voice-recorder.error")
|
||||
: t("editor.voice-recorder.title")}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
{isRecording
|
||||
? t("editor.voice-recorder.recording-description", { duration: formatAudioTime(elapsedSeconds) })
|
||||
: isRequestingPermission
|
||||
? t("editor.voice-recorder.requesting-permission-description")
|
||||
: hasRecording
|
||||
? t("editor.voice-recorder.ready-description")
|
||||
: isUnsupported
|
||||
? t("editor.voice-recorder.unsupported-description")
|
||||
: error
|
||||
? error
|
||||
: t("editor.voice-recorder.idle-description")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isRecording && (
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-destructive/20 bg-destructive/[0.08] px-2.5 py-1 text-xs font-medium text-destructive">
|
||||
<span className="size-2 rounded-full bg-destructive" />
|
||||
{formatAudioTime(elapsedSeconds)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasRecording && (
|
||||
<div className="mt-3">
|
||||
<AudioAttachmentItem
|
||||
filename={recording.localFile.file.name}
|
||||
sourceUrl={recording.localFile.previewUrl}
|
||||
mimeType={recording.mimeType}
|
||||
size={recording.localFile.file.size}
|
||||
title="Voice note"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center justify-end gap-2">
|
||||
{hasRecording ? (
|
||||
<>
|
||||
<Button variant="ghost" size="sm" onClick={onDiscard}>
|
||||
<Trash2Icon />
|
||||
{t("editor.voice-recorder.discard")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onRecordAgain}>
|
||||
<RotateCcwIcon />
|
||||
{t("editor.voice-recorder.record-again")}
|
||||
</Button>
|
||||
<Button size="sm" onClick={onKeep}>
|
||||
<AudioLinesIcon />
|
||||
{t("editor.voice-recorder.keep")}
|
||||
</Button>
|
||||
</>
|
||||
) : isRecording ? (
|
||||
<Button size="sm" onClick={onStop}>
|
||||
<SquareIcon />
|
||||
{t("editor.voice-recorder.stop")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
{!isUnsupported && (
|
||||
<Button size="sm" onClick={onStart} disabled={isRequestingPermission}>
|
||||
{isRequestingPermission ? <LoaderCircleIcon className="animate-spin" /> : <MicIcon />}
|
||||
{isRequestingPermission ? t("editor.voice-recorder.requesting") : t("editor.voice-recorder.start")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue