diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json
index 2e9cc03c..3dd3eb92 100644
--- a/enjoy/src/i18n/en.json
+++ b/enjoy/src/i18n/en.json
@@ -178,7 +178,8 @@
"preferences": "Preferences",
"profile": "My Profile",
"notes": "Note",
- "help": "Help"
+ "help": "Help",
+ "collapse": "Collapse"
},
"form": {
"lengthMustBeAtLeast": "{{field}} must be at least {{length}} characters",
diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json
index aa80da36..4ce392c8 100644
--- a/enjoy/src/i18n/zh-CN.json
+++ b/enjoy/src/i18n/zh-CN.json
@@ -178,7 +178,8 @@
"preferences": "软件设置",
"profile": "个人主页",
"notes": "笔记",
- "help": "帮助"
+ "help": "帮助",
+ "collapse": "收起"
},
"form": {
"lengthMustBeAtLeast": "{{field}} 长度不可超过 {{length}} 个字符",
diff --git a/enjoy/src/renderer/components/audios/audio-player.tsx b/enjoy/src/renderer/components/audios/audio-player.tsx
index 84128a77..081cf1aa 100644
--- a/enjoy/src/renderer/components/audios/audio-player.tsx
+++ b/enjoy/src/renderer/components/audios/audio-player.tsx
@@ -1,15 +1,7 @@
import { useEffect, useContext } from "react";
-import { MediaPlayerProviderContext } from "@renderer/context";
-import {
- MediaLoadingModal,
- MediaCaption,
- MediaPlayerControls,
- MediaTabs,
- MediaCurrentRecording,
- MediaPlayer,
- LoaderSpin,
-} from "@renderer/components";
+import { MediaShadowProviderContext } from "@renderer/context";
import { useAudio } from "@renderer/hooks";
+import { MediaShadowPlayer } from "@renderer/components";
export const AudioPlayer = (props: {
id?: string;
@@ -17,13 +9,8 @@ export const AudioPlayer = (props: {
segmentIndex?: number;
}) => {
const { id, md5, segmentIndex } = props;
- const {
- media,
- setMedia,
- layout,
- setCurrentSegmentIndex,
- getCachedSegmentIndex,
- } = useContext(MediaPlayerProviderContext);
+ const { media, setMedia, setCurrentSegmentIndex, getCachedSegmentIndex } =
+ useContext(MediaShadowProviderContext);
const { audio } = useAudio({ id, md5 });
@@ -46,38 +33,10 @@ export const AudioPlayer = (props: {
}, [media?.id]);
if (!audio) return null;
- if (!layout) return ;
return (
-
-
-
-
-
-
+
+
);
};
diff --git a/enjoy/src/renderer/components/audios/audios-component.tsx b/enjoy/src/renderer/components/audios/audios-component.tsx
index d611c7df..beb628bc 100644
--- a/enjoy/src/renderer/components/audios/audios-component.tsx
+++ b/enjoy/src/renderer/components/audios/audios-component.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState, useReducer, useContext } from "react";
import {
AudioCard,
- AddMediaButton,
+ MediaAddButton,
AudiosTable,
AudioEditForm,
} from "@renderer/components";
@@ -228,7 +228,7 @@ export const AudiosComponent = () => {
onChange={(e) => setQuery(e.target.value)}
/>
-
+
diff --git a/enjoy/src/renderer/components/audios/audios-segment.tsx b/enjoy/src/renderer/components/audios/audios-segment.tsx
index 897e7c0b..88f6b88d 100644
--- a/enjoy/src/renderer/components/audios/audios-segment.tsx
+++ b/enjoy/src/renderer/components/audios/audios-segment.tsx
@@ -4,7 +4,7 @@ import {
AppSettingsProviderContext,
} from "@renderer/context";
import { Button, ScrollArea, ScrollBar } from "@renderer/components/ui";
-import { AudioCard, AddMediaButton } from "@renderer/components";
+import { AudioCard, MediaAddButton } from "@renderer/components";
import { t } from "i18next";
import { Link } from "react-router-dom";
@@ -62,7 +62,7 @@ export const AudiosSegment = (props: { limit?: number }) => {
{audios.length === 0 ? (
) : (
diff --git a/enjoy/src/renderer/components/chats/chat-input.tsx b/enjoy/src/renderer/components/chats/chat-input.tsx
index ba651b98..f3badeb2 100644
--- a/enjoy/src/renderer/components/chats/chat-input.tsx
+++ b/enjoy/src/renderer/components/chats/chat-input.tsx
@@ -51,6 +51,7 @@ export const ChatInput = () => {
isPaused,
askAgent,
onCreateMessage,
+ shadowing,
} = useContext(ChatSessionProviderContext);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const inputRef = useRef(null);
@@ -96,6 +97,7 @@ export const ChatInput = () => {
useHotkeys(
currentHotkeys.StartOrStopRecording,
() => {
+ if (shadowing) return;
if (isRecording) {
stopRecording();
} else {
@@ -107,9 +109,16 @@ export const ChatInput = () => {
}
);
- useHotkeys(currentHotkeys.PlayNextSegment, () => askAgent(), {
- preventDefault: true,
- });
+ useHotkeys(
+ currentHotkeys.PlayNextSegment,
+ () => {
+ if (shadowing) return;
+ askAgent();
+ },
+ {
+ preventDefault: true,
+ }
+ );
if (isRecording) {
return (
diff --git a/enjoy/src/renderer/components/courses/chapter-content.tsx b/enjoy/src/renderer/components/courses/chapter-content.tsx
index 54bf76c2..75f9c344 100644
--- a/enjoy/src/renderer/components/courses/chapter-content.tsx
+++ b/enjoy/src/renderer/components/courses/chapter-content.tsx
@@ -83,15 +83,19 @@ export const ChapterContent = (props: {
)}
-
+
{chapter.sequence}. {chapter?.title}
-
{chapter?.content}
+
+ {chapter?.content}
+
{translation && (
{t("translation")}
- {translation.content}
+
+ {translation.content}
+
)}
diff --git a/enjoy/src/renderer/components/courses/example-content.tsx b/enjoy/src/renderer/components/courses/example-content.tsx
index fc029cc1..ad907204 100644
--- a/enjoy/src/renderer/components/courses/example-content.tsx
+++ b/enjoy/src/renderer/components/courses/example-content.tsx
@@ -145,11 +145,15 @@ export const ExampleContent = (props: {
return (
- {example.content}
+
+ {example.content}
+
{translation && (
{t("translation")}
- {translation.content}
+
+ {translation.content}
+
)}
diff --git a/enjoy/src/renderer/components/llm-chats/llm-message.tsx b/enjoy/src/renderer/components/llm-chats/llm-message.tsx
index 4c26eb40..068295e5 100644
--- a/enjoy/src/renderer/components/llm-chats/llm-message.tsx
+++ b/enjoy/src/renderer/components/llm-chats/llm-message.tsx
@@ -213,11 +213,11 @@ export const LlmMessage = (props: { llmMessage: LlmMessageType }) => {
-
+
{llmMessage.response}
{translation && (
-
+
{translation}
)}
diff --git a/enjoy/src/renderer/components/medias/index.ts b/enjoy/src/renderer/components/medias/index.ts
index 249f2929..b881b8bd 100644
--- a/enjoy/src/renderer/components/medias/index.ts
+++ b/enjoy/src/renderer/components/medias/index.ts
@@ -1,14 +1,7 @@
-export * from "./media-player-controls";
-export * from "./media-caption";
-export * from "./media-info-panel";
-export * from "./media-recordings";
-export * from "./media-current-recording";
-export * from "./media-transcription";
-export * from "./media-transcription-read-button";
-export * from "./media-transcription-generate-button";
-export * from "./media-player";
-export * from "./media-provider";
-export * from "./media-tabs";
+export * from "./media-left-panel";
+export * from "./media-right-panel";
+export * from "./media-bottom-panel";
+
export * from "./media-loading-modal";
-export * from "./add-media-button";
-export * from "./media-transcription-print";
+export * from "./media-add-button";
+export * from "./media-shadow-player";
diff --git a/enjoy/src/renderer/components/medias/add-media-button.tsx b/enjoy/src/renderer/components/medias/media-add-button.tsx
similarity index 98%
rename from enjoy/src/renderer/components/medias/add-media-button.tsx
rename to enjoy/src/renderer/components/medias/media-add-button.tsx
index 407d071f..879434c2 100644
--- a/enjoy/src/renderer/components/medias/add-media-button.tsx
+++ b/enjoy/src/renderer/components/medias/media-add-button.tsx
@@ -20,7 +20,7 @@ import {
DbProviderContext,
} from "@renderer/context";
-export const AddMediaButton = (props: { type?: "Audio" | "Video" }) => {
+export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
const { type = "Audio" } = props;
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
diff --git a/enjoy/src/renderer/components/medias/media-bottom-panel/index.ts b/enjoy/src/renderer/components/medias/media-bottom-panel/index.ts
new file mode 100644
index 00000000..68e2c42e
--- /dev/null
+++ b/enjoy/src/renderer/components/medias/media-bottom-panel/index.ts
@@ -0,0 +1,4 @@
+export * from "./media-bottom-panel";
+export * from "./media-current-recording";
+export * from "./media-waveform";
+export * from "./media-player-controls";
diff --git a/enjoy/src/renderer/components/medias/media-bottom-panel/media-bottom-panel.tsx b/enjoy/src/renderer/components/medias/media-bottom-panel/media-bottom-panel.tsx
new file mode 100644
index 00000000..c4dd54d7
--- /dev/null
+++ b/enjoy/src/renderer/components/medias/media-bottom-panel/media-bottom-panel.tsx
@@ -0,0 +1,22 @@
+import {
+ MediaCurrentRecording,
+ MediaWaveform,
+ MediaPlayerControls,
+} from "@renderer/components";
+
+export const MediaBottomPanel = () => {
+ return (
+
+ );
+};
diff --git a/enjoy/src/renderer/components/medias/media-current-recording.tsx b/enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx
similarity index 63%
rename from enjoy/src/renderer/components/medias/media-current-recording.tsx
rename to enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx
index 7ba1b775..98fdd7c7 100644
--- a/enjoy/src/renderer/components/medias/media-current-recording.tsx
+++ b/enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx
@@ -2,10 +2,10 @@ import { useEffect, useContext, useRef, useState } from "react";
import {
AppSettingsProviderContext,
HotKeysSettingsProviderContext,
- MediaPlayerProviderContext,
+ MediaShadowProviderContext,
} from "@renderer/context";
import { RecordingDetail } from "@renderer/components";
-import { renderPitchContour } from "@renderer/lib/utils";
+import { cn, renderPitchContour } from "@renderer/lib/utils";
import { extractFrequencies } from "@/utils";
import WaveSurfer from "wavesurfer.js";
import Regions from "wavesurfer.js/dist/plugins/regions";
@@ -37,22 +37,28 @@ import {
Share2Icon,
GaugeCircleIcon,
ChevronDownIcon,
- MoreVerticalIcon,
+ MoreHorizontalIcon,
TextCursorInputIcon,
MicIcon,
SquareIcon,
DownloadIcon,
+ XIcon,
+ CheckIcon,
} from "lucide-react";
import { t } from "i18next";
import { formatDuration } from "@renderer/lib/utils";
import { useHotkeys } from "react-hotkeys-hook";
import { LiveAudioVisualizer } from "react-audio-visualize";
+import debounce from "lodash/debounce";
+const ACTION_BUTTON_HEIGHT = 35;
export const MediaCurrentRecording = () => {
const {
- layout,
isRecording,
isPaused,
+ cancelRecording,
+ togglePauseResume,
+ stopRecording,
recordingTime,
mediaRecorder,
currentRecording,
@@ -65,7 +71,7 @@ export const MediaCurrentRecording = () => {
currentSegment,
createSegment,
currentTime: mediaCurrentTime,
- } = useContext(MediaPlayerProviderContext);
+ } = useContext(MediaShadowProviderContext);
const { webApi, EnjoyApp } = useContext(AppSettingsProviderContext);
const { currentHotkeys } = useContext(HotKeysSettingsProviderContext);
const [player, setPlayer] = useState(null);
@@ -79,7 +85,8 @@ export const MediaCurrentRecording = () => {
const [frequencies, setFrequencies] = useState([]);
const [peaks, setPeaks] = useState([]);
- const [width, setWidth] = useState();
+ const [size, setSize] = useState<{ width: number; height: number }>();
+ const [actionButtonsCount, setActionButtonsCount] = useState(0);
const ref = useRef(null);
@@ -256,25 +263,35 @@ export const MediaCurrentRecording = () => {
});
};
- const calContainerWidth = () => {
- const w = document
- .querySelector(".media-recording-wrapper")
- ?.getBoundingClientRect()?.width;
- if (!w) return;
+ const calContainerSize = () => {
+ const size = ref?.current
+ ?.closest(".media-recording-wrapper")
+ ?.getBoundingClientRect();
- setWidth(w - 48);
+ if (!size) return;
+
+ setSize(size);
+ if (player) {
+ player.setOptions({
+ height: size.height - 10, // -10 to leave space for scrollbar
+ });
+ }
+
+ setActionButtonsCount(Math.floor(size.height / ACTION_BUTTON_HEIGHT));
};
+ const debouncedCalContainerSize = debounce(calContainerSize, 100);
+
useEffect(() => {
if (!ref.current) return;
if (isRecording) return;
if (!currentRecording?.src) return;
- if (!layout?.playerHeight) return;
+ const height = ref.current.getBoundingClientRect().height - 10; // -10 to leave space for scrollbar
const ws = WaveSurfer.create({
- container: ref.current,
+ container: ref.current.querySelector(".waveform-container"),
url: currentRecording.src,
- height: layout.playerHeight,
+ height,
barWidth: 2,
cursorWidth: 1,
autoCenter: true,
@@ -320,7 +337,7 @@ export const MediaCurrentRecording = () => {
return () => {
ws?.destroy();
};
- }, [ref, currentRecording, isRecording, layout?.playerHeight]);
+ }, [ref, currentRecording, isRecording]);
useEffect(() => {
setCurrentTime(0);
@@ -409,19 +426,20 @@ export const MediaCurrentRecording = () => {
]);
useEffect(() => {
- if (!width) return;
+ if (!ref?.current) return;
+ if (!player) return;
- const container: HTMLDivElement = document.querySelector(
- ".media-recording-container"
- );
- if (!container) return;
+ const observer = new ResizeObserver(() => {
+ debouncedCalContainerSize();
+ });
+ observer.observe(ref.current);
+ EnjoyApp.window.onResize(debouncedCalContainerSize);
- container.style.width = `${width}px`;
- }, [width, currentRecording, isRecording]);
-
- useEffect(() => {
- calContainerWidth();
- }, [currentRecording, isRecording, layout?.width]);
+ return () => {
+ EnjoyApp.window.removeListeners();
+ observer.disconnect();
+ };
+ }, [ref, player]);
useHotkeys(currentHotkeys.PlayOrPauseRecording, () => {
const button = document.getElementById("recording-play-or-pause-button");
@@ -442,30 +460,152 @@ export const MediaCurrentRecording = () => {
setDetailIsOpen(!detailIsOpen);
});
+ useHotkeys(
+ currentHotkeys.Compare,
+ () => {
+ toggleCompare();
+ },
+ {
+ preventDefault: true,
+ }
+ );
+
+ const Actions = [
+ {
+ id: "recording-record-button-wrapper",
+ name: "record",
+ label: t("record"),
+ icon: MediaRecordButton,
+ active: isRecording,
+ onClick: () => {},
+ },
+ {
+ id: "recording-play-or-pause-button",
+ name: "playOrPause",
+ label: t("playRecording"),
+ icon: player?.isPlaying() ? PauseIcon : PlayIcon,
+ active: player?.isPlaying(),
+ onClick: () => {
+ const region = regions
+ ?.getRegions()
+ ?.find((r) => r.id.startsWith("recording-voice-region"));
+
+ if (region) {
+ region.play();
+ } else {
+ player?.playPause();
+ }
+ },
+ },
+ {
+ id: "media-pronunciation-assessment-button",
+ name: "pronunciationAssessment",
+ label: t("pronunciationAssessment"),
+ icon: GaugeCircleIcon,
+ active: detailIsOpen,
+ iconClassName: currentRecording?.pronunciationAssessment
+ ? currentRecording?.pronunciationAssessment.pronunciationScore >= 80
+ ? "text-green-500"
+ : currentRecording?.pronunciationAssessment.pronunciationScore >= 60
+ ? "text-yellow-600"
+ : "text-red-500"
+ : "",
+ onClick: () => setDetailIsOpen(!detailIsOpen),
+ },
+ {
+ id: "media-compare-button",
+ name: "compare",
+ label: t("compare"),
+ icon: GitCompareIcon,
+ active: isComparing,
+ onClick: toggleCompare,
+ },
+ {
+ id: "media-select-region-button",
+ name: "selectRegion",
+ label: t("selectRegion"),
+ icon: TextCursorInputIcon,
+ active: isSelectingRegion,
+ onClick: () => setIsSelectingRegion(!isSelectingRegion),
+ },
+ {
+ id: "media-share-button",
+ name: "share",
+ label: t("share"),
+ icon: Share2Icon,
+ active: isSharing,
+ onClick: () => setIsSharing(true),
+ },
+ {
+ id: "media-download-button",
+ name: "download",
+ label: t("download"),
+ icon: DownloadIcon,
+ active: false,
+ onClick: handleDownload,
+ },
+ ];
+
if (isRecording || isPaused) {
return (
-
-
-
-
-
- {Math.floor(recordingTime / 60)}:
- {String(recordingTime % 60).padStart(2, "0")}
-
-
-
-
-
+
+
+
+ {Math.floor(recordingTime / 60)}:
+ {String(recordingTime % 60).padStart(2, "0")}
+
+
+
+
+
);
@@ -473,10 +613,15 @@ export const MediaCurrentRecording = () => {
if (!currentRecording?.src)
return (
-
-
+
);
return (
-
-
-
+
+
+
{formatDuration(currentTime || 0)}
@@ -507,149 +657,60 @@ export const MediaCurrentRecording = () => {
-
-