diff --git a/packages/core/src/types/icons.ts b/packages/core/src/types/icons.ts index 63ee874..5e395a3 100644 --- a/packages/core/src/types/icons.ts +++ b/packages/core/src/types/icons.ts @@ -249,6 +249,7 @@ export module CoreIcons { "M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z"; export const pause = "M6 3c-0.552 0-1 0.448-1 1v16c0 0.552 0.448 1 1 1h4c0.552 0 1-0.448 1-1v-16c0-0.552-0.448-1-1-1zM7 5h2v14h-2zM14 3c-0.552 0-1 0.448-1 1v16c0 0.552 0.448 1 1 1h4c0.552 0 1-0.448 1-1v-16c0-0.552-0.448-1-1-1zM15 5h2v14h-2z"; + export const stop = "M18,18H6V6H18V18Z"; export const volumeOn = "M10 7.081v9.839l-3.375-2.7c-0.17-0.137-0.388-0.22-0.625-0.22h-3v-4h3c0.218 0.001 0.439-0.071 0.625-0.219zM10.375 4.219l-4.726 3.781h-3.649c-0.552 0-1 0.448-1 1v6c0 0.552 0.448 1 1 1h3.649l4.726 3.781c0.431 0.345 1.061 0.275 1.406-0.156 0.148-0.185 0.22-0.407 0.219-0.625v-14c0-0.552-0.448-1-1-1-0.237 0-0.455 0.083-0.625 0.219zM18.363 5.637c1.757 1.758 2.635 4.059 2.635 6.364 0 2.304-0.878 4.605-2.635 6.362-0.39 0.391-0.39 1.024 0 1.414s1.024 0.39 1.414 0c2.147-2.147 3.22-4.963 3.221-7.776s-1.074-5.63-3.221-7.778c-0.39-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM14.833 9.167c0.781 0.781 1.171 1.803 1.171 2.828s-0.39 2.047-1.171 2.828c-0.39 0.391-0.39 1.024 0 1.414s1.024 0.39 1.414 0c1.171-1.171 1.757-2.708 1.757-4.242s-0.586-3.071-1.757-4.242c-0.39-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"; export const volumeOff = diff --git a/packages/media/src/dictation/Dictation.tsx b/packages/media/src/dictation/Dictation.tsx new file mode 100644 index 0000000..5e69550 --- /dev/null +++ b/packages/media/src/dictation/Dictation.tsx @@ -0,0 +1,159 @@ +/* + * React Fabric + * @version: 1.0.0 + * + * + * The MIT License (MIT) + * Copyright (c) 2024 Adarsh Pastakia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { Button, CoreIcons, HotKey } from "@react-fabric/core"; +import { Format } from "@react-fabric/utilities"; +import classNames from "classnames"; +import { Fragment, useEffect, useRef, useState } from "react"; +import WaveSurfer from "wavesurfer.js"; +import RecordPlugin from "wavesurfer.js/dist/plugins/record"; + +export interface DictationProps { + size?: "sm"; + variant?: "solid" | "outline"; + hotkey?: string; + onRecord?: (blob: Blob) => void; +} + +export const Dictation = ({ + hotkey = "alt+t", + size, + variant, + onRecord, +}: DictationProps) => { + const [error, setError] = useState(""); + const [progress, setProgress] = useState(0); + const [recording, setRecording] = useState(false); + const wavesurfer = useRef(); + const record = useRef(); + const container = useRef(null); + + useEffect(() => { + if (container.current) { + wavesurfer.current = new WaveSurfer({ + container: container.current, + waveColor: "#0190ff", + cursorWidth: 0, + width: 96, + height: size === "sm" ? 28 : 32, + barWidth: 1, + barRadius: 3, + barGap: 1, + barHeight: 1.5, + minPxPerSec: 16, + }); + + record.current = wavesurfer.current.registerPlugin( + RecordPlugin.create({ + scrollingWaveform: true, + renderRecordedAudio: false, + }), + ); + + record.current.on("record-progress", (time) => { + setProgress(time); + }); + + return () => { + wavesurfer.current?.destroy(); + }; + } + }, [size]); + + useEffect(() => { + record.current?.on("record-end", (blob) => { + setRecording(false); + onRecord?.(blob); + }); + }, [onRecord]); + + const startDictation = useRef(() => { + record.current + ?.startRecording() + .then(() => { + setRecording(true); + }) + .catch(() => { + setError("Unable to access microphone"); + }); + }); + + const stopDictation = useRef(() => { + record.current?.stopRecording(); + }); + + return ( +
+
+ {error && ( + + {error} + + )} + {!error && !recording && ( + + +
+ ); +}; diff --git a/packages/media/src/index.ts b/packages/media/src/index.ts index 86da661..a990865 100644 --- a/packages/media/src/index.ts +++ b/packages/media/src/index.ts @@ -23,8 +23,8 @@ export { AudioPlayer, type AudioPlayerRef } from "./audio/AudioPlayer"; export { type CanvasRef as ImageViewerRef } from "./canvas/Context"; +export { Dictation } from "./dictation/Dictation"; export { ImageViewer } from "./image/ImageViewer"; +export { Thumbnail } from "./thumbnail/Thumbnail"; export { type VideoPlayerRef } from "./video/types"; export { VideoPlayer } from "./video/VideoPlayer"; - -export { Thumbnail } from "./thumbnail/Thumbnail"; diff --git a/packages/media/stories/Dictation.stories.tsx b/packages/media/stories/Dictation.stories.tsx new file mode 100644 index 0000000..56d4549 --- /dev/null +++ b/packages/media/stories/Dictation.stories.tsx @@ -0,0 +1,52 @@ +/* + * React Fabric + * @version: 1.0.0 + * + * + * The MIT License (MIT) + * Copyright (c) 2024 Adarsh Pastakia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { SearchBar } from "@react-fabric/searchbar"; +import { action } from "@storybook/addon-actions"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Dictation } from "../src"; + +const meta: Meta = { + component: Dictation, + title: "@media/Dictation", + parameters: { + layout: "centered", + jest: ["media/tests/Dictation.test.tsx"], + }, +}; + +export default meta; +type DictationStory = StoryObj; + +export const _Dictation: DictationStory = { + render: (args) => { + return ( +
+ +
+ ); + }, + args: { + onRecord: action("onRecord"), + }, +};