diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 4a1d3895..8e7f05ca 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -342,6 +342,12 @@ "sharePrompt": "Share prompt", "sharedPrompt": "Shared a prompt", "areYouSureToShareThisPromptToCommunity": "Are you sure to share this prompt to community?", + "shareRecording": "Share recording", + "sharedRecording": "Shared a recording", + "areYouSureToShareThisRecordingToCommunity": "Are you sure to share this recording to community?", + "shareStory": "Share story", + "sharedStory": "Shared a story", + "areYouSureToShareThisStoryToCommunity": "Are you sure to share this story to community?", "addToLibary": "Add to library", "areYouSureToAddThisVideoToYourLibrary": "Are you sure to add this video to library?", "areYouSureToAddThisAudioToYourLibrary": "Are you sure to add this audio to library?", diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index e4889ae1..fe0a2605 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -342,6 +342,12 @@ "sharePrompt": "分享提示语", "sharedPrompt": "分享了一条提示语", "areYouSureToShareThisPromptToCommunity": "您确定要分享此提示语到社区吗?", + "shareRecording": "分享录音", + "sharedRecording": "分享了一条录音", + "areYouSureToShareThisRecordingToCommunity": "您确定要分享此录音到社区吗?", + "shareStory": "分享文章", + "sharedStory": "分享了一篇文章", + "areYouSureToShareThisStoryToCommunity": "您确定要分享此文章到社区吗?", "addToLibary": "添加到资源库", "areYouSureToAddThisVideoToYourLibrary": "您确定要添加此视频到资料库吗?", "areYouSureToAddThisAudioToYourLibrary": "您确定要添加此音频到资料库吗?", diff --git a/enjoy/src/renderer/components/posts/index.ts b/enjoy/src/renderer/components/posts/index.ts index 801e16c4..961d8bc9 100644 --- a/enjoy/src/renderer/components/posts/index.ts +++ b/enjoy/src/renderer/components/posts/index.ts @@ -1,4 +1,6 @@ export * from "./posts"; -export * from "./post-audio-player"; +export * from "./post-audio"; export * from "./post-card"; export * from "./post-actions"; +export * from "./post-medium"; +export * from "./post-recording"; diff --git a/enjoy/src/renderer/components/posts/post-audio-player.tsx b/enjoy/src/renderer/components/posts/post-audio.tsx similarity index 88% rename from enjoy/src/renderer/components/posts/post-audio-player.tsx rename to enjoy/src/renderer/components/posts/post-audio.tsx index 1e05ed89..c1326827 100644 --- a/enjoy/src/renderer/components/posts/post-audio-player.tsx +++ b/enjoy/src/renderer/components/posts/post-audio.tsx @@ -6,8 +6,11 @@ import { PlayIcon, PauseIcon } from "lucide-react"; import { useIntersectionObserver } from "@uidotdev/usehooks"; import { secondsToTimestamp } from "@renderer/lib/utils"; -export const PostAudioPlayer = (props: { src: string; height?: number }) => { - const { src, height = 80 } = props; +export const PostAudio = (props: { + audio: Partial; + height?: number; +}) => { + const { audio, height = 80 } = props; const [initialized, setInitialized] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [wavesurfer, setWavesurfer] = useState(null); @@ -25,12 +28,12 @@ export const PostAudioPlayer = (props: { src: string; height?: number }) => { // use the intersection observer to only create the wavesurfer instance // when the player is visible if (!entry?.isIntersecting) return; - if (!src) return; + if (!audio.sourceUrl) return; if (wavesurfer) return; const ws = WaveSurfer.create({ container: containerRef.current, - url: src, + url: audio.sourceUrl, height, barWidth: 1, cursorWidth: 0, @@ -41,11 +44,10 @@ export const PostAudioPlayer = (props: { src: string; height?: number }) => { minPxPerSec: 100, waveColor: "#ddd", progressColor: "rgba(0, 0, 0, 0.25)", - normalize: true, }); setWavesurfer(ws); - }, [src, entry]); + }, [audio.sourceUrl, entry]); useEffect(() => { if (!wavesurfer) return; @@ -81,7 +83,7 @@ export const PostAudioPlayer = (props: { src: string; height?: number }) => { return (
- + {secondsToTimestamp(duration)}
@@ -116,6 +118,12 @@ export const PostAudioPlayer = (props: { src: string; height?: number }) => { ref={containerRef} >
+ + {audio.coverUrl && ( +
+ +
+ )} ); }; diff --git a/enjoy/src/renderer/components/posts/post-card.tsx b/enjoy/src/renderer/components/posts/post-card.tsx index d39e551c..cf4aaecf 100644 --- a/enjoy/src/renderer/components/posts/post-card.tsx +++ b/enjoy/src/renderer/components/posts/post-card.tsx @@ -1,6 +1,4 @@ -import { useContext, useEffect, useState } from "react"; -import { AppSettingsProviderContext } from "@renderer/context"; -import { PostAudioPlayer, PostActions } from "@renderer/components"; +import { PostRecording, PostActions, PostMedium } from "@renderer/components"; import { Avatar, AvatarImage, @@ -9,11 +7,6 @@ import { } from "@renderer/components/ui"; import { formatDateTime } from "@renderer/lib/utils"; import { t } from "i18next"; -import { MediaPlayer, MediaProvider } from "@vidstack/react"; -import { - DefaultVideoLayout, - defaultLayoutIcons, -} from "@vidstack/react/player/layouts/default"; import Markdown from "react-markdown"; export const PostCard = (props: { post: PostType }) => { @@ -53,53 +46,18 @@ export const PostCard = (props: { post: PostType }) => { )} + {post.targetType == "Recording" && ( + <> +
+ {t("sharedRecording")} +
+ + + )} + ); }; -const PostMedium = (props: { medium: MediumType }) => { - const { medium } = props; - if (!medium.sourceUrl) return null; - - return ( -
- {medium.mediumType == "Video" && ( - <> -
- {t("sharedAudio")} -
- - - - - - )} - - {medium.mediumType == "Audio" && ( - <> -
- {t("sharedAudio")} -
- - - )} - - {medium.coverUrl && medium.mediumType == "Audio" && ( -
- -
- )} -
- ); -}; - const PostOptions = (props: { post: PostType }) => {}; diff --git a/enjoy/src/renderer/components/posts/post-medium.tsx b/enjoy/src/renderer/components/posts/post-medium.tsx new file mode 100644 index 00000000..bcb6280e --- /dev/null +++ b/enjoy/src/renderer/components/posts/post-medium.tsx @@ -0,0 +1,45 @@ +import { PostAudio } from "@renderer/components"; +import { t } from "i18next"; +import { MediaPlayer, MediaProvider } from "@vidstack/react"; +import { + DefaultVideoLayout, + defaultLayoutIcons, +} from "@vidstack/react/player/layouts/default"; + +export const PostMedium = (props: { medium: MediumType }) => { + const { medium } = props; + if (!medium.sourceUrl) return null; + + return ( +
+ {medium.mediumType == "Video" && ( + <> +
+ {t("sharedAudio")} +
+ + + + + + )} + + {medium.mediumType == "Audio" && ( + <> +
+ {t("sharedAudio")} +
+ } /> + + )} +
+ ); +}; diff --git a/enjoy/src/renderer/components/posts/post-recording.tsx b/enjoy/src/renderer/components/posts/post-recording.tsx new file mode 100644 index 00000000..e52e8cc8 --- /dev/null +++ b/enjoy/src/renderer/components/posts/post-recording.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState, useRef, useCallback } from "react"; +import { PitchContour } from "@renderer/components"; +import WaveSurfer from "wavesurfer.js"; +import { Button, Skeleton } from "@renderer/components/ui"; +import { PlayIcon, PauseIcon } from "lucide-react"; +import { useIntersectionObserver } from "@uidotdev/usehooks"; +import { secondsToTimestamp } from "@renderer/lib/utils"; + +export const PostRecording = (props: { + recording: RecordingType; + height?: number; +}) => { + const { recording, height = 80 } = props; + const [initialized, setInitialized] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [wavesurfer, setWavesurfer] = useState(null); + const containerRef = useRef(); + const [ref, entry] = useIntersectionObserver({ + threshold: 1, + }); + const [duration, setDuration] = useState(0); + + const onPlayClick = useCallback(() => { + wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play(); + }, [wavesurfer]); + + useEffect(() => { + // use the intersection observer to only create the wavesurfer instance + // when the player is visible + if (!entry?.isIntersecting) return; + if (!recording.src) return; + if (wavesurfer) return; + + const ws = WaveSurfer.create({ + container: containerRef.current, + url: recording.src, + height, + barWidth: 1, + cursorWidth: 0, + autoCenter: true, + autoScroll: true, + dragToSeek: true, + hideScrollbar: true, + minPxPerSec: 100, + waveColor: "rgba(0, 0, 0, 0.25)", + progressColor: "rgba(0, 0, 0, 0.5)", + }); + + setWavesurfer(ws); + }, [recording.src, entry]); + + useEffect(() => { + if (!wavesurfer) return; + + const subscriptions = [ + wavesurfer.on("play", () => { + setIsPlaying(true); + }), + wavesurfer.on("pause", () => { + setIsPlaying(false); + }), + wavesurfer.on("decode", () => { + setDuration(wavesurfer.getDuration()); + const peaks = wavesurfer.getDecodedData().getChannelData(0); + const sampleRate = wavesurfer.options.sampleRate; + wavesurfer.renderer.getWrapper().appendChild( + PitchContour({ + peaks, + sampleRate, + height, + }) + ); + setInitialized(true); + }), + ]; + + return () => { + subscriptions.forEach((unsub) => unsub()); + wavesurfer?.destroy(); + }; + }, [wavesurfer]); + + return ( +
+
+ + {secondsToTimestamp(duration)} + +
+ +
+ {!initialized && ( +
+ + + +
+ )} + +
+ +
+ +
+
+ + { + recording.referenceText && ( +
+
+ {recording.referenceText} +
+
+ ) + } +
+ ); +}; diff --git a/enjoy/src/renderer/components/recordings/recording-card.tsx b/enjoy/src/renderer/components/recordings/recording-card.tsx index a829366a..c9a488ac 100644 --- a/enjoy/src/renderer/components/recordings/recording-card.tsx +++ b/enjoy/src/renderer/components/recordings/recording-card.tsx @@ -4,18 +4,26 @@ import { RecordingPlayer } from "@renderer/components"; import { AlertDialog, AlertDialogHeader, + AlertDialogTrigger, AlertDialogDescription, AlertDialogTitle, AlertDialogContent, AlertDialogFooter, AlertDialogCancel, + AlertDialogAction, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + useToast, } from "@renderer/components/ui"; -import { ChevronDownIcon, Trash2Icon, InfoIcon, Share2Icon } from "lucide-react"; +import { + ChevronDownIcon, + Trash2Icon, + InfoIcon, + Share2Icon, +} from "lucide-react"; import { formatDateTime, secondsToTimestamp } from "@renderer/lib/utils"; import { t } from "i18next"; @@ -26,39 +34,69 @@ export const RecordingCard = (props: { }) => { const { recording, id, onSelect } = props; const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const { EnjoyApp } = useContext(AppSettingsProviderContext); + const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext); const [isPlaying, setIsPlaying] = useState(false); + const { toast } = useToast(); const handleDelete = () => { EnjoyApp.recordings.destroy(recording.id); }; + const handleShare = async () => { + if (!recording.updatedAt) { + try { + await EnjoyApp.recordings.upload(recording.id); + } catch (error) { + toast({ + description: error.message, + variant: "destructive", + }); + return; + } + } + + webApi + .createPost({ + targetId: recording.id, + targetType: "Recording", + }) + .then(() => { + toast({ + description: t("recordingShared"), + }); + }) + .catch((error) => { + toast({ + description: error.message, + variant: "destructive", + }); + }); + }; return (
- -
-
-
- - {secondsToTimestamp(recording.duration / 1000)} - -
+
+
+
+ + {secondsToTimestamp(recording.duration / 1000)} + +
- + -
- + /> + + + + + + + + + {t("shareRecording")} + + {t("areYouSureToShareThisRecordingToCommunity")} + + + + {t("cancel")} + + + + + + + + -
-
-
- - {formatDateTime(recording.createdAt)} - + + + setIsDeleteDialogOpen(true)}> + + {t("delete")} + + + + +
- - - setIsDeleteDialogOpen(true)}> - - {t("delete")} - - - - - +
+ + {formatDateTime(recording.createdAt)} + +
+