Merge branch 'nick/record' into feat/audio-recording

This commit is contained in:
nick 2025-02-23 17:19:02 +08:00
commit 59cb359f00
7 changed files with 121 additions and 6 deletions

0
scripts/build.sh Normal file → Executable file
View file

View file

@ -81,5 +81,6 @@
"protobufjs": "^7.4.0",
"typescript": "^5.7.3",
"vite": "^6.0.6"
}
}
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}

View file

@ -0,0 +1,110 @@
import { Button } from "@usememos/mui";
import { MicIcon, StopCircleIcon } from "lucide-react";
import { useCallback, useContext, useState } from "react";
import toast from "react-hot-toast";
import { useResourceStore } from "@/store/v1";
import { Resource } from "@/types/proto/api/v1/resource_service";
import { useTranslate } from "@/utils/i18n";
import { MemoEditorContext } from "../types";
const RecordAudioButton = () => {
const t = useTranslate();
const context = useContext(MemoEditorContext);
const resourceStore = useResourceStore();
const [isRecording, setIsRecording] = useState(false);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
// 检测浏览器支持的音频格式
const getSupportedMimeType = () => {
const types = ["audio/webm", "audio/mp4", "audio/aac", "audio/wav", "audio/ogg"];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return null;
};
const startRecording = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType = getSupportedMimeType();
if (!mimeType) {
throw new Error("No supported audio format found");
}
const recorder = new MediaRecorder(stream, {
mimeType: mimeType,
});
const chunks: BlobPart[] = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: mimeType });
const buffer = new Uint8Array(await blob.arrayBuffer());
// 根据不同的 mimeType 选择合适的文件扩展名
const getFileExtension = (mimeType: string) => {
switch (mimeType) {
case "audio/webm":
return "webm";
case "audio/mp4":
return "m4a";
case "audio/aac":
return "aac";
case "audio/wav":
return "wav";
case "audio/ogg":
return "ogg";
default:
return "webm";
}
};
try {
const resource = await resourceStore.createResource({
resource: Resource.fromPartial({
filename: `recording-${new Date().getTime()}.${getFileExtension(mimeType)}`,
type: mimeType,
size: buffer.length,
content: buffer,
}),
});
context.setResourceList([...context.resourceList, resource]);
} catch (error: any) {
console.error(error);
toast.error(error.details);
}
stream.getTracks().forEach((track) => track.stop());
};
// 每秒记录一次数据
recorder.start(1000);
setMediaRecorder(recorder);
setIsRecording(true);
} catch (error) {
console.error(error);
toast.error(t("message.microphone-not-available"));
}
}, [context, resourceStore, t]);
const stopRecording = useCallback(() => {
if (mediaRecorder) {
mediaRecorder.stop();
setMediaRecorder(null);
setIsRecording(false);
}
}, [mediaRecorder]);
return (
<Button className="relative" size="sm" variant="plain" onClick={isRecording ? stopRecording : startRecording}>
{isRecording ? <StopCircleIcon className="w-5 h-5 mx-auto text-red-500" /> : <MicIcon className="w-5 h-5 mx-auto" />}
</Button>
);
};
export default RecordAudioButton;

View file

@ -26,6 +26,7 @@ import VisibilityIcon from "../VisibilityIcon";
import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover";
import LocationSelector from "./ActionButton/LocationSelector";
import MarkdownMenu from "./ActionButton/MarkdownMenu";
import RecordAudioButton from "./ActionButton/RecordAudioButton";
import TagSelector from "./ActionButton/TagSelector";
import UploadResourceButton from "./ActionButton/UploadResourceButton";
import Editor, { EditorRefActions } from "./Editor";
@ -466,6 +467,7 @@ const MemoEditor = observer((props: Props) => {
<TagSelector editorRef={editorRef} />
<MarkdownMenu editorRef={editorRef} />
<UploadResourceButton />
<RecordAudioButton />
<AddMemoRelationPopover editorRef={editorRef} />
{workspaceMemoRelatedSetting.enableLocation && (
<LocationSelector

View file

@ -18,7 +18,7 @@ const MemoResource: React.FC<Props> = (props: Props) => {
return (
<div className={`w-auto flex flex-row justify-start items-center text-gray-500 dark:text-gray-400 hover:opacity-80 ${className}`}>
{resource.type.startsWith("audio") ? (
<audio src={resourceUrl} controls></audio>
<audio src={resourceUrl} controls className="max-w-full" controlsList="nodownload" />
) : (
<>
<ResourceIcon className="!w-4 !h-4 mr-1" resource={resource} />

View file

@ -162,7 +162,8 @@
"remove-completed-task-list-items-successfully": "The removal was successful",
"failed-to-embed-memo": "Failed to embed memo",
"description-is-required": "Description is required",
"fill-all-required-fields": "Please fill all required fields"
"fill-all-required-fields": "Please fill all required fields",
"microphone-not-available": "Cannot access microphone"
},
"reference": {
"add-references": "Add references",
@ -320,7 +321,7 @@
},
"disable-password-login": "Disable password login",
"disable-password-login-final-warning": "Please type \"CONFIRM\" if you know what you are doing.",
"disable-password-login-warning": "This will disable password login for all users. It is not possible to log in without reverting this setting in the database if your configured identity providers fail. Youll also have to be extra carefull when removing an identity provider",
"disable-password-login-warning": "This will disable password login for all users. It is not possible to log in without reverting this setting in the database if your configured identity providers fail. You'll also have to be extra carefull when removing an identity provider",
"disable-public-memos": "Disable public memos",
"disable-markdown-shortcuts-in-editor": "Disable Markdown shortcuts in editor",
"display-with-updated-time": "Display with updated time",

View file

@ -155,7 +155,8 @@
"succeed-copy-link": "复制链接到剪贴板成功。",
"update-succeed": "更新成功",
"user-not-found": "未找到该用户",
"remove-completed-task-list-items-successfully": "移除成功!"
"remove-completed-task-list-items-successfully": "移除成功!",
"microphone-not-available": "无法访问麦克风"
},
"reference": {
"add-references": "添加引用",