diff --git a/enjoy/src/constants.ts b/enjoy/src/constants.ts
index d8dbd556..2ce6097a 100644
--- a/enjoy/src/constants.ts
+++ b/enjoy/src/constants.ts
@@ -376,7 +376,7 @@ export const IPA_MAPPING: { [key: string]: string } = {
g: "g",
q: "k",
ɢ: "g",
- ʔ: "",
+ ʔ: "t",
ɡ: "g",
m: "m",
ɱ: "m",
@@ -420,7 +420,7 @@ export const IPA_MAPPING: { [key: string]: string } = {
ʈʃ: "tʃ",
dʒ: "dʒ",
ʋ: "v",
- ɹ: "r",
+ ɹ: "ɹ",
ɻ: "r",
j: "j",
ɰ: "w",
@@ -444,29 +444,29 @@ export const IPA_MAPPING: { [key: string]: string } = {
ɘ: "ə",
ɵ: "ə",
ɤ: "ɒ",
- o: "ɔː",
+ o: "o",
ə: "ə",
- oː: "ɔː",
- ɛ: "æ",
+ oː: "oː",
+ ɛ: "ɛ",
œ: "æ",
- ɜ: "əː",
+ ɜ: "ɜ",
ɞ: "əː",
ʌ: "ʌ",
- ɔ: "ɔː",
+ ɔ: "ɔ",
ɜː: "əː",
uː: "uː",
ɔː: "ɔː",
- ɛː: "æ",
+ ɛː: "ɛ:",
æ: "æ",
- a: "ɑː",
- ɶ: "ɑː",
- ɐ: "ɑː",
- ɑ: "ɑː",
+ a: "ɑ",
+ ɶ: "ɑ",
+ ɐ: "ə",
+ ɑ: "ɑ",
ɒ: "ɒ",
ɑː: "ɑː",
"◌˞": "",
- ɚ: "ɪə",
- ɝ: "ɪə",
+ ɚ: "ɚ",
+ ɝ: "ɝ",
ɹ̩: "r",
eɪ: "eɪ",
əʊ: "əʊ",
@@ -474,20 +474,20 @@ export const IPA_MAPPING: { [key: string]: string } = {
aɪ: "aɪ",
ɔɪ: "ɔɪ",
aʊ: "aʊ",
- iə: "ɪə",
- ɜr: "ɪə(r)",
- ɑr: "ɑː(r)",
- ɔr: "ɔː(r)",
- oʊr: "əʊ(r)",
- oːɹ: "ɔː(r)",
- ir: "iː(r)",
- ɪɹ: "ɪ(r)",
- ɔːɹ: "ɔː(r)",
- ɑːɹ: "ɑː(r)",
- ʊɹ: "ʊ(r)",
- ʊr: "ʊ(r)",
- ɛr: "æ(r)",
- ɛɹ: "æ(r)",
+ iə: "iə",
+ ɜr: "ɜr",
+ ɑr: "ɑr",
+ ɔr: "ɔr",
+ oʊr: "əʊr",
+ oːɹ: "ɔːɹ",
+ ir: "ir",
+ ɪɹ: "ɪɹ",
+ ɔːɹ: "ɔːɹ",
+ ɑːɹ: "ɑːɹ",
+ ʊɹ: "ʊɹ",
+ ʊr: "ʊɹ",
+ ɛr: "ɛr",
+ ɛɹ: "ɛɹ",
əl: "ə",
aɪɚ: "aɪ",
aɪə: "aɪ",
diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json
index b16228ba..8639fb0e 100644
--- a/enjoy/src/i18n/zh-CN.json
+++ b/enjoy/src/i18n/zh-CN.json
@@ -273,7 +273,7 @@
"editResource": "编辑资源",
"deleteResource": "删除资源",
"deleteResourceConfirmation": "您确定要删除资源 {{name}} 吗?",
- "transcribeAudioConfirmation": "这将删除原来的语音文本,您确定要重新对 {{name}} 进行语音转文本吗?",
+ "transcribeMediaConfirmation": "这将删除原来的语音文本,您确定要重新对 {{name}} 进行语音转文本吗?",
"localFile": "本地文件",
"recentlyAdded": "最近添加",
"resourcesYouAddedRecently": "最近添加的资源",
diff --git a/enjoy/src/index.css b/enjoy/src/index.css
index fe6a9eba..82e68e07 100644
--- a/enjoy/src/index.css
+++ b/enjoy/src/index.css
@@ -3,6 +3,7 @@
@import "@vidstack/react/player/styles/default/layouts/audio.css";
@import "@vidstack/react/player/styles/default/layouts/video.css";
@import "intl-tel-input/build/css/intlTelInput.css";
+@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap");
@tailwind base;
@tailwind components;
diff --git a/enjoy/src/renderer/components/audios/audio-player.tsx b/enjoy/src/renderer/components/audios/audio-player.tsx
index d7da9e1f..ae75b6a1 100644
--- a/enjoy/src/renderer/components/audios/audio-player.tsx
+++ b/enjoy/src/renderer/components/audios/audio-player.tsx
@@ -6,17 +6,14 @@ import {
MediaPlayerControls,
MediaTabs,
MediaCurrentRecording,
+ MediaPlayer,
} from "@renderer/components";
-import { formatDuration } from "@renderer/lib/utils";
import { useAudio } from "@renderer/hooks";
export const AudioPlayer = (props: { id?: string; md5?: string }) => {
const { id, md5 } = props;
- const { media, currentTime, setMedia, setRef } = useContext(
- MediaPlayerProviderContext
- );
+ const { setMedia } = useContext(MediaPlayerProviderContext);
const { audio } = useAudio({ id, md5 });
- const ref = useRef(null);
useEffect(() => {
if (!audio) return;
@@ -24,14 +21,6 @@ export const AudioPlayer = (props: { id?: string; md5?: string }) => {
setMedia(audio);
}, [audio]);
- useEffect(() => {
- setRef(ref);
-
- return () => {
- setRef(null);
- };
- }, [ref]);
-
return (
@@ -51,18 +40,7 @@ export const AudioPlayer = (props: { id?: string; md5?: string }) => {
-
-
-
-
- {formatDuration(currentTime || 0)}
-
- /
-
- {formatDuration(media?.duration || 0)}
-
-
-
+
diff --git a/enjoy/src/renderer/components/medias/index.ts b/enjoy/src/renderer/components/medias/index.ts
index d138b819..ba93a3a5 100644
--- a/enjoy/src/renderer/components/medias/index.ts
+++ b/enjoy/src/renderer/components/medias/index.ts
@@ -6,6 +6,7 @@ export * from "./media-current-recording";
export * from "./media-recorder";
export * from "./media-transcription";
export * from "./media-player";
+export * from "./media-provider";
export * from "./media-tabs";
export * from "./media-loading-modal";
export * from "./add-media-button";
diff --git a/enjoy/src/renderer/components/medias/media-caption.tsx b/enjoy/src/renderer/components/medias/media-caption.tsx
index 99d02847..e99f5c03 100644
--- a/enjoy/src/renderer/components/medias/media-caption.tsx
+++ b/enjoy/src/renderer/components/medias/media-caption.tsx
@@ -5,7 +5,6 @@ import { Button, toast, ScrollArea, Separator } from "@renderer/components/ui";
import { t } from "i18next";
import { LanguagesIcon, SpeechIcon } from "lucide-react";
import { Timeline } from "echogarden/dist/utilities/Timeline.d.js";
-import { IPA_MAPPING } from "@/constants";
import { useAiCommand } from "@renderer/hooks";
import { LoaderIcon } from "lucide-react";
import { convertIpaToNormal } from "@/utils";
@@ -337,7 +336,7 @@ export const MediaCaption = () => {
{/* use the words splitted by caption text if it is matched with the timeline length, otherwise use the timeline */}
- {caption.text.includes("-")
+ {caption.text.split(" ").length !== caption.timeline.length
? (caption.timeline || []).map((w, index) => (
{
{w.text}
{displayIpa && (
-
+
{w.timeline
.map((t) =>
t.timeline
@@ -379,7 +378,7 @@ export const MediaCaption = () => {
{word}
{displayIpa && (
-
+
{caption.timeline[index].timeline
.map((t) =>
t.timeline
@@ -417,7 +416,7 @@ export const MediaCaption = () => {
{word.text}
-
+
/
{word.timeline
.map((t) =>
diff --git a/enjoy/src/renderer/components/medias/media-current-recording.tsx b/enjoy/src/renderer/components/medias/media-current-recording.tsx
index 58d75af4..b4a2fa3b 100644
--- a/enjoy/src/renderer/components/medias/media-current-recording.tsx
+++ b/enjoy/src/renderer/components/medias/media-current-recording.tsx
@@ -37,6 +37,8 @@ import {
ChevronDownIcon,
MoreVerticalIcon,
TextCursorInputIcon,
+ MicIcon,
+ SquareIcon,
} from "lucide-react";
import { t } from "i18next";
import { formatDuration } from "@renderer/lib/utils";
@@ -46,6 +48,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
const { height = 192 } = props;
const {
isRecording,
+ setIsRecording,
currentRecording,
renderPitchContour: renderMediaPitchContour,
regions: mediaRegions,
@@ -67,6 +70,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
const [frequencies, setFrequencies] = useState([]);
const [peaks, setPeaks] = useState([]);
+ const [width, setWidth] = useState();
const ref = useRef(null);
@@ -191,6 +195,15 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
});
};
+ const calContainerWidth = () => {
+ const w = document
+ .querySelector(".media-recording-container")
+ ?.getBoundingClientRect()?.width;
+ if (!w) return;
+
+ setWidth(w - 48);
+ };
+
useEffect(() => {
if (!ref.current) return;
if (isRecording) return;
@@ -305,9 +318,6 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
const scrollContainer = player.getWrapper()?.closest(".scroll");
if (!scrollContainer) return;
- scrollContainer.style.width = `${
- ref.current.getBoundingClientRect().width
- }px`;
scrollContainer.style.scrollbarWidth = "thin";
}, [ref, player]);
@@ -335,6 +345,25 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
mediaActiveRegion,
]);
+ useEffect(() => {
+ if (!ref?.current) return;
+
+ ref.current.style.width = `${width}px`;
+ }, [width]);
+
+ useEffect(() => {
+ calContainerWidth();
+ window.addEventListener("resize", () => {
+ calContainerWidth();
+ });
+
+ return () => {
+ window.removeEventListener("resize", () => {
+ calContainerWidth();
+ });
+ };
+ }, []);
+
useHotkeys(
["Ctrl+R", "Meta+R"],
(keyboardEvent, hotkeyEvent) => {
@@ -354,18 +383,27 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
if (isRecording) return ;
if (!currentRecording?.src)
return (
-
-
+
);
return (
-
+
@@ -380,7 +418,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
-
+
{
)}
+
+
{
+
setDetailIsOpen(open)}>
{
);
};
+
+export const MediaRecordButton = (props: {
+ isRecording: boolean;
+ setIsRecording: (value: boolean) => void;
+}) => {
+ const { isRecording, setIsRecording } = props;
+
+ return (
+
setIsRecording(!isRecording)}
+ id="media-record-button"
+ data-tooltip-id="media-player-controls-tooltip"
+ data-tooltip-content={
+ isRecording ? t("stopRecording") : t("startRecording")
+ }
+ className="aspect-square p-0 h-8 rounded-full bg-red-500 hover:bg-red-500/90"
+ >
+ {isRecording ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/enjoy/src/renderer/components/medias/media-player-controls.tsx b/enjoy/src/renderer/components/medias/media-player-controls.tsx
index 506a481e..f99c7b99 100644
--- a/enjoy/src/renderer/components/medias/media-player-controls.tsx
+++ b/enjoy/src/renderer/components/medias/media-player-controls.tsx
@@ -1,15 +1,6 @@
import { useEffect, useState, useContext } from "react";
import { type Region as RegionType } from "wavesurfer.js/dist/plugins/regions";
import {
- AlertDialog,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogCancel,
- AlertDialogAction,
DropdownMenu,
DropdownMenuItem,
DropdownMenuTrigger,
@@ -18,7 +9,6 @@ import {
Popover,
PopoverTrigger,
PopoverContent,
- toast,
} from "@renderer/components/ui";
import {
MediaPlayerProviderContext,
@@ -31,17 +21,9 @@ import {
Repeat1Icon,
RepeatIcon,
GaugeIcon,
- ZoomInIcon,
- ZoomOutIcon,
- MicIcon,
- MinimizeIcon,
- GalleryHorizontalIcon,
- SpellCheckIcon,
- Share2Icon,
ListRestartIcon,
SkipForwardIcon,
SkipBackIcon,
- SquareIcon,
SaveIcon,
UndoIcon,
TextCursorInputIcon,
@@ -54,14 +36,8 @@ import debounce from "lodash/debounce";
import { AlignmentResult } from "echogarden/dist/api/API.d.js";
const PLAYBACK_RATE_OPTIONS = [0.75, 0.8, 0.9, 1.0];
-const ZOOM_RATIO_OPTIONS = [
- 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0,
-];
-const MIN_ZOOM_RATIO = 0.25;
-const MAX_ZOOM_RATIO = 4.0;
export const MediaPlayerControls = () => {
const {
- media,
decoded,
wavesurfer,
currentTime,
@@ -71,7 +47,6 @@ export const MediaPlayerControls = () => {
setZoomRatio,
fitZoomRatio,
transcription,
- pitchChart,
regions,
activeRegion,
setActiveRegion,
@@ -79,14 +54,10 @@ export const MediaPlayerControls = () => {
setEditingRegion,
transcriptionDraft,
setTranscriptionDraft,
- isRecording,
- setIsRecording,
} = useContext(MediaPlayerProviderContext);
- const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
+ const { EnjoyApp } = useContext(AppSettingsProviderContext);
const [playMode, setPlayMode] = useState<"loop" | "single" | "all">("single");
const [playbackRate, setPlaybackRate] = useState
(1);
- const [displayInlineCaption, setDisplayInlineCaption] =
- useState(true);
const [isSelectingRegion, setIsSelectingRegion] = useState(false);
const playOrPause = () => {
@@ -116,34 +87,6 @@ export const MediaPlayerControls = () => {
setCurrentSegmentIndex(currentSegmentIndex + 1);
};
- const onShare = async () => {
- if (!media.source && !media.isUploaded) {
- try {
- await EnjoyApp.audios.upload(media.id);
- } catch (err) {
- toast.error(t("shareFailed"), {
- description: err.message,
- });
- return;
- }
- }
- webApi
- .createPost({
- targetType: media.mediaType,
- targetId: media.id,
- })
- .then(() => {
- toast.success(t("sharedSuccessfully"), {
- description: t("sharedAudio"),
- });
- })
- .catch((err) => {
- toast.error(t("shareFailed"), {
- description: err.message,
- });
- });
- };
-
/*
* Update segmentRegion when currentSegmentIndex is updated
* or when editingRegion is toggled.
@@ -457,228 +400,144 @@ export const MediaPlayerControls = () => {
}, [regions, activeRegion]);
return (
-
-
-
- {wavesurfer?.isPlaying() ? (
+
+
+
+
-
+
+ {playbackRate != 1.0 && (
+
+ {playbackRate.toFixed(2)}
+
+ )}
- ) : (
+
+
+ {t("playbackRate")}
+
+ {PLAYBACK_RATE_OPTIONS.map((rate, i) => (
+
{
+ setPlaybackRate(rate);
+ }}
+ >
+ {rate}
+
+ ))}
+
+
+
+
+
+
-
+ {playMode === "single" && }
+ {playMode === "loop" && }
+ {playMode === "all" && }
- )}
-
+
+
+ setPlayMode("single")}
+ >
+
+ {t("playSingleSegment")}
+
+ setPlayMode("loop")}
+ >
+
+ {t("playInLoop")}
+
+ setPlayMode("all")}
+ >
+
+ {t("playAllSegments")}
+
+
+
-
+
+
+
+
+ {wavesurfer?.isPlaying() ? (
-
+
-
+ ) : (
-
+
+ )}
-
-
-
- {playMode === "single" && }
- {playMode === "loop" && }
- {playMode === "all" && }
-
-
-
- setPlayMode("single")}
- >
-
- {t("playSingleSegment")}
-
- setPlayMode("loop")}
- >
-
- {t("playInLoop")}
-
- setPlayMode("all")}
- >
-
- {t("playAllSegments")}
-
-
-
+
+
+
-
-
-
-
- {playbackRate != 1.0 && (
-
- {playbackRate.toFixed(2)}
-
- )}
-
-
-
- {t("playbackRate")}
-
- {PLAYBACK_RATE_OPTIONS.map((rate, i) => (
-
{
- setPlaybackRate(rate);
- }}
- >
- {rate}
-
- ))}
-
-
-
-
-
1.0 ? "secondary" : "ghost"}`}
- data-tooltip-id="media-player-controls-tooltip"
- data-tooltip-content={t("zoomIn")}
- className="relative aspect-square p-0 h-10"
- onClick={() => {
- if (zoomRatio < MAX_ZOOM_RATIO) {
- const nextZoomRatio = ZOOM_RATIO_OPTIONS.find(
- (rate) => rate > zoomRatio
- );
- setZoomRatio(nextZoomRatio || MAX_ZOOM_RATIO);
- }
- }}
- >
-
-
-
-
{
- if (zoomRatio > MIN_ZOOM_RATIO) {
- const nextZoomRatio = ZOOM_RATIO_OPTIONS.reverse().find(
- (rate) => rate < zoomRatio
- );
- setZoomRatio(nextZoomRatio || MIN_ZOOM_RATIO);
- }
- }}
- >
-
-
-
-
{
- if (zoomRatio == fitZoomRatio) {
- setZoomRatio(1.0);
- } else {
- setZoomRatio(fitZoomRatio);
- }
- }}
- >
-
-
-
-
{
- setDisplayInlineCaption(!displayInlineCaption);
- if (pitchChart) {
- pitchChart.options.scales.x.display = !displayInlineCaption;
- pitchChart.update();
- }
- }}
- >
-
-
-
-
{
- wavesurfer.setOptions({
- autoCenter: !wavesurfer?.options?.autoCenter,
- });
- }}
- >
-
-
-
-
setIsSelectingRegion(!isSelectingRegion)}
- >
-
-
+
setIsSelectingRegion(!isSelectingRegion)}
+ >
+
+
+
{
>
-
- {editingRegion && (
-
-
{
- setEditingRegion(false);
- setTranscriptionDraft(null);
- }}
- >
-
-
-
{
- if (!transcriptionDraft) return;
+ {editingRegion && (
+
+ {
+ setEditingRegion(false);
+ setTranscriptionDraft(null);
+ }}
+ >
+
+
+ {
+ if (!transcriptionDraft) return;
- EnjoyApp.transcriptions
- .update(transcription.id, {
- result: transcriptionDraft,
- })
- .then(() => {
- setTranscriptionDraft(null);
- setEditingRegion(false);
- });
- }}
- >
-
-
-
- )}
-
-
-
-
-
-
-
- {media?.mediaType === "Audio"
- ? t("shareAudio")
- : t("shareVideo")}
-
-
- {media?.mediaType === "Audio"
- ? t("areYouSureToShareThisAudioToCommunity")
- : t("areYouSureToShareThisVideoToCommunity")}
-
-
-
- {t("cancel")}
-
-
- {t("share")}
-
-
-
-
-
-
-
-
-
-
-
-
-
setIsRecording(!isRecording)}
- id="media-record-button"
- data-tooltip-id="media-player-controls-tooltip"
- data-tooltip-content={
- isRecording ? t("stopRecording") : t("startRecording")
- }
- className="aspect-square p-0 h-12 rounded-full bg-red-500 hover:bg-red-500/90"
- >
- {isRecording ? (
-
- ) : (
-
+ EnjoyApp.transcriptions
+ .update(transcription.id, {
+ result: transcriptionDraft,
+ })
+ .then(() => {
+ setTranscriptionDraft(null);
+ setEditingRegion(false);
+ });
+ }}
+ >
+
+
+
)}
-
+
diff --git a/enjoy/src/renderer/components/medias/media-player.tsx b/enjoy/src/renderer/components/medias/media-player.tsx
index 9d0979ad..341c9329 100644
--- a/enjoy/src/renderer/components/medias/media-player.tsx
+++ b/enjoy/src/renderer/components/medias/media-player.tsx
@@ -1,43 +1,266 @@
-import { useContext } from "react";
-import { MediaPlayerProviderContext } from "@renderer/context";
+import { useEffect, useContext, useRef, useState } from "react";
import {
- MediaPlayer as VidstackMediaPlayer,
- MediaProvider,
- isAudioProvider,
- isVideoProvider,
- useMediaRemote,
-} from "@vidstack/react";
+ AppSettingsProviderContext,
+ MediaPlayerProviderContext,
+} from "@renderer/context";
+import { formatDuration } from "@renderer/lib/utils";
+import { t } from "i18next";
import {
- DefaultAudioLayout,
- defaultLayoutIcons,
-} from "@vidstack/react/player/layouts/default";
+ AlertDialog,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogCancel,
+ AlertDialogAction,
+ DropdownMenu,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ Button,
+ toast,
+} from "@renderer/components/ui";
+import {
+ GalleryHorizontalIcon,
+ Share2Icon,
+ SpellCheckIcon,
+ MinimizeIcon,
+ ZoomInIcon,
+ ZoomOutIcon,
+ MoreVerticalIcon,
+} from "lucide-react";
+
+const ZOOM_RATIO_OPTIONS = [
+ 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0,
+];
+const MIN_ZOOM_RATIO = 0.25;
+const MAX_ZOOM_RATIO = 4.0;
export const MediaPlayer = () => {
- const { media, setMediaProvider, setDecodeError } = useContext(
- MediaPlayerProviderContext
- );
- const mediaRemote = useMediaRemote();
- if (!media?.src) return null;
+ const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
+ const {
+ media,
+ currentTime,
+ setRef,
+ pitchChart,
+ wavesurfer,
+ zoomRatio,
+ setZoomRatio,
+ fitZoomRatio,
+ } = useContext(MediaPlayerProviderContext);
+ const [displayInlineCaption, setDisplayInlineCaption] =
+ useState
(true);
+ const [isSharing, setIsSharing] = useState(false);
+ const [width, setWidth] = useState();
+
+ const ref = useRef(null);
+
+ const onShare = async () => {
+ if (!media.source && !media.isUploaded) {
+ try {
+ await EnjoyApp.audios.upload(media.id);
+ } catch (err) {
+ toast.error(t("shareFailed"), {
+ description: err.message,
+ });
+ return;
+ }
+ }
+ webApi
+ .createPost({
+ targetType: media.mediaType,
+ targetId: media.id,
+ })
+ .then(() => {
+ toast.success(t("sharedSuccessfully"), {
+ description: t("sharedAudio"),
+ });
+ })
+ .catch((err) => {
+ toast.error(t("shareFailed"), {
+ description: err.message,
+ });
+ });
+ };
+
+ const calContainerWidth = () => {
+ const w = document
+ .querySelector(".media-player-container")
+ ?.getBoundingClientRect()?.width;
+ if (!w) return;
+
+ setWidth(w - 48);
+ };
+
+ useEffect(() => {
+ if (ref?.current) {
+ setRef(ref);
+ }
+ }, [ref]);
+
+ useEffect(() => {
+ if (!ref?.current) return;
+
+ ref.current.style.width = `${width}px`;
+ }, [width]);
+
+ useEffect(() => {
+ calContainerWidth();
+ window.addEventListener("resize", () => {
+ calContainerWidth();
+ });
+
+ return () => {
+ window.removeEventListener("resize", () => {
+ calContainerWidth();
+ });
+ };
+ }, []);
return (
-
-
{
- mediaRemote.setTarget(nativeEvent.target);
- const { provider } = detail;
- if (isAudioProvider(provider)) {
- setMediaProvider(provider.audio);
- } else if (isVideoProvider(provider)) {
- setMediaProvider(provider.video);
- }
- }}
- onError={(err) => setDecodeError(err.message)}
- >
-
-
-
+
+
+
+
+ {formatDuration(currentTime || 0)}
+ /
+
+ {formatDuration(media?.duration || 0)}
+
+
+
+
+
{
+ if (zoomRatio == fitZoomRatio) {
+ setZoomRatio(1.0);
+ } else {
+ setZoomRatio(fitZoomRatio);
+ }
+ }}
+ >
+
+
+
+
1.0 ? "secondary" : "outline"}`}
+ data-tooltip-id="media-player-controls-tooltip"
+ data-tooltip-content={t("zoomIn")}
+ className="relative aspect-square rounded-full p-0 h-8"
+ onClick={() => {
+ if (zoomRatio < MAX_ZOOM_RATIO) {
+ const nextZoomRatio = ZOOM_RATIO_OPTIONS.find(
+ (rate) => rate > zoomRatio
+ );
+ setZoomRatio(nextZoomRatio || MAX_ZOOM_RATIO);
+ }
+ }}
+ >
+
+
+
+
{
+ if (zoomRatio > MIN_ZOOM_RATIO) {
+ const nextZoomRatio = ZOOM_RATIO_OPTIONS.reverse().find(
+ (rate) => rate < zoomRatio
+ );
+ setZoomRatio(nextZoomRatio || MIN_ZOOM_RATIO);
+ }
+ }}
+ >
+
+
+
+
{
+ setDisplayInlineCaption(!displayInlineCaption);
+ if (pitchChart) {
+ pitchChart.options.scales.x.display = !displayInlineCaption;
+ pitchChart.update();
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {
+ wavesurfer.setOptions({
+ autoCenter: !wavesurfer?.options?.autoCenter,
+ });
+ }}
+ >
+
+ {t("autoCenter")}
+
+
+ setIsSharing(true)}
+ >
+
+ {t("share")}
+
+
+
+
+
+
+
+
+ {media?.mediaType === "Audio"
+ ? t("shareAudio")
+ : t("shareVideo")}
+
+
+ {media?.mediaType === "Audio"
+ ? t("areYouSureToShareThisAudioToCommunity")
+ : t("areYouSureToShareThisVideoToCommunity")}
+
+
+
+ {t("cancel")}
+
+
+ {t("share")}
+
+
+
+
+
+
);
};
diff --git a/enjoy/src/renderer/components/medias/media-provider.tsx b/enjoy/src/renderer/components/medias/media-provider.tsx
new file mode 100644
index 00000000..7e2de34e
--- /dev/null
+++ b/enjoy/src/renderer/components/medias/media-provider.tsx
@@ -0,0 +1,43 @@
+import { useContext } from "react";
+import { MediaPlayerProviderContext } from "@renderer/context";
+import {
+ MediaPlayer as VidstackMediaPlayer,
+ MediaProvider as VidstackMediaProvider,
+ isAudioProvider,
+ isVideoProvider,
+ useMediaRemote,
+} from "@vidstack/react";
+import {
+ DefaultAudioLayout,
+ defaultLayoutIcons,
+} from "@vidstack/react/player/layouts/default";
+
+export const MediaProvider = () => {
+ const { media, setMediaProvider, setDecodeError } = useContext(
+ MediaPlayerProviderContext
+ );
+ const mediaRemote = useMediaRemote();
+ if (!media?.src) return null;
+
+ return (
+
+ {
+ mediaRemote.setTarget(nativeEvent.target);
+ const { provider } = detail;
+ if (isAudioProvider(provider)) {
+ setMediaProvider(provider.audio);
+ } else if (isVideoProvider(provider)) {
+ setMediaProvider(provider.video);
+ }
+ }}
+ onError={(err) => setDecodeError(err.message)}
+ >
+
+
+
+
+ );
+};
diff --git a/enjoy/src/renderer/components/medias/media-recorder.tsx b/enjoy/src/renderer/components/medias/media-recorder.tsx
index 1026a74c..068f4566 100644
--- a/enjoy/src/renderer/components/medias/media-recorder.tsx
+++ b/enjoy/src/renderer/components/medias/media-recorder.tsx
@@ -8,10 +8,8 @@ import WaveSurfer from "wavesurfer.js";
import { t } from "i18next";
import { useTranscribe } from "@renderer/hooks";
import { toast } from "@renderer/components/ui";
-import {
- FFMPEG_TRIM_SILENCE_OPTIONS,
- FFMPEG_CONVERT_WAV_OPTIONS,
-} from "@/constants";
+import { MediaRecordButton } from "@renderer/components";
+import { FFMPEG_CONVERT_WAV_OPTIONS } from "@/constants";
export const MediaRecorder = (props: { height?: number }) => {
const { height = 192 } = props;
@@ -74,7 +72,7 @@ export const MediaRecorder = (props: { height?: number }) => {
success: t("recordingSaved"),
error: (e) => t("failedToSaveRecording" + " : " + e.message),
position: "bottom-right",
- },
+ }
);
};
@@ -134,12 +132,21 @@ export const MediaRecorder = (props: { height?: number }) => {
}, []);
return (
-
-
- {duration / 10}
- / 300
-
-
+
+
+
+ {duration / 10}
+ / 300
+
+
+
+
+
+
+
);
};
diff --git a/enjoy/src/renderer/components/medias/media-tabs.tsx b/enjoy/src/renderer/components/medias/media-tabs.tsx
index 1b4e7897..0c5f7440 100644
--- a/enjoy/src/renderer/components/medias/media-tabs.tsx
+++ b/enjoy/src/renderer/components/medias/media-tabs.tsx
@@ -1,7 +1,7 @@
import { useEffect, useContext, useState } from "react";
import { MediaPlayerProviderContext } from "@renderer/context";
import {
- MediaPlayer,
+ MediaProvider,
MediaTranscription,
MediaInfoPanel,
MediaRecordings,
@@ -11,7 +11,7 @@ import { t } from "i18next";
export const MediaTabs = () => {
const { media, decoded } = useContext(MediaPlayerProviderContext);
- const [tab, setTab] = useState("player");
+ const [tab, setTab] = useState("provider");
useEffect(() => {
if (!decoded) return;
@@ -27,9 +27,9 @@ export const MediaTabs = () => {
{media.mediaType === "Video" && (
setTab("player")}
+ onClick={() => setTab("provider")}
>
{t("player")}
@@ -61,8 +61,8 @@ export const MediaTabs = () => {
-
-
+
+
diff --git a/enjoy/src/renderer/components/sidebar.tsx b/enjoy/src/renderer/components/sidebar.tsx
index 38e5d324..7f2a915a 100644
--- a/enjoy/src/renderer/components/sidebar.tsx
+++ b/enjoy/src/renderer/components/sidebar.tsx
@@ -55,6 +55,28 @@ export const Sidebar = () => {
+
+
+
+
+ {t("sidebar.aiAssistant")}
+
+
+
+
{
{t("sidebar.mine")}
-
-
-
-
- {t("sidebar.aiAssistant")}
-
-
-
-
{
const { id, md5 } = props;
- const { media, currentTime, setMedia, setRef } = useContext(
- MediaPlayerProviderContext
- );
+ const { setMedia } = useContext(MediaPlayerProviderContext);
const { video } = useVideo({ id, md5 });
- const ref = useRef(null);
useEffect(() => {
if (!video) return;
@@ -24,10 +21,6 @@ export const VideoPlayer = (props: { id?: string; md5?: string }) => {
setMedia(video);
}, [video]);
- useEffect(() => {
- setRef(ref);
- }, [ref]);
-
return (
@@ -47,18 +40,7 @@ export const VideoPlayer = (props: { id?: string; md5?: string }) => {
-
-
-
-
- {formatDuration(currentTime || 0)}
-
- /
-
- {formatDuration(media?.duration || 0)}
-
-
-
+
diff --git a/enjoy/src/renderer/hooks/use-transcriptions.tsx b/enjoy/src/renderer/hooks/use-transcriptions.tsx
index 9671fead..2d07276d 100644
--- a/enjoy/src/renderer/hooks/use-transcriptions.tsx
+++ b/enjoy/src/renderer/hooks/use-transcriptions.tsx
@@ -90,7 +90,9 @@ export const useTranscriptions = (media: AudioType | VideoType) => {
/*
* Pre-process
- * Some words end with period should not be a single sentence, like Mr./Ms./Dr. etc
+ * 1. Some words end with period should not be a single sentence, like Mr./Ms./Dr. etc
+ * 2. Some words connected by `-`(like scrach-off) are split into multiple words in words timeline, merge them for display;
+ * 3. Some numbers with `%` are split into `number + percent` in words timeline, merge thme for display;
*/
timeline.forEach((sentence, i) => {
const nextSentence = timeline[i + 1];
@@ -107,6 +109,35 @@ export const useTranscriptions = (media: AudioType | VideoType) => {
];
nextSentence.startTime = sentence.startTime;
timeline.splice(i, 1);
+ } else {
+ const words = sentence.text.split(" ");
+
+ sentence.timeline.forEach((token, j) => {
+ const word = words[j]?.trim()?.toLowerCase();
+
+ const match = word?.match(/-|%/);
+ if (!match) return;
+
+ for (let k = j + 1; k <= sentence.timeline.length - 1; k++) {
+ if (word.includes(sentence.timeline[k].text.toLowerCase())) {
+ let connector = "";
+ if (match[0] === "-") {
+ connector = "-";
+ }
+ token.text = [token.text, sentence.timeline[k].text].join(
+ connector
+ );
+ token.timeline = [
+ ...token.timeline,
+ ...sentence.timeline[k].timeline,
+ ];
+ token.endTime = sentence.timeline[k].endTime;
+ sentence.timeline.splice(k, 1);
+ } else {
+ break;
+ }
+ }
+ });
}
});
diff --git a/enjoy/tailwind.config.js b/enjoy/tailwind.config.js
index 2302b9cd..0db1711a 100644
--- a/enjoy/tailwind.config.js
+++ b/enjoy/tailwind.config.js
@@ -11,6 +11,9 @@ module.exports = {
},
},
extend: {
+ fontFamily: {
+ code: ['"Source Code Pro"'],
+ },
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",