Refactor current recording component (#691)

* upgrade deps

* update buttons order

* auto assess when modal open

* display assess score in recording list

* display assess mark on transcription list

* auto scroll to current segment

* toast may close

* update ui components
This commit is contained in:
an-lee
2024-06-20 13:25:56 +08:00
committed by GitHub
parent d3e93ec39b
commit 814be8369d
16 changed files with 1112 additions and 738 deletions

View File

@@ -28,6 +28,7 @@ import {
SheetContent,
SheetHeader,
SheetClose,
SheetTitle,
} from "@renderer/components/ui";
import {
GitCompareIcon,
@@ -500,6 +501,34 @@ export const MediaCurrentRecording = () => {
setIsRecording={setIsRecording}
/>
<Button
variant={detailIsOpen ? "secondary" : "outline"}
size="icon"
id="media-pronunciation-assessment-button"
data-tooltip-id="media-player-tooltip"
data-tooltip-content={t("pronunciationAssessment")}
className={
layout?.name === "sm" ? "hidden" : "rounded-full w-8 h-8 p-0"
}
onClick={() => setDetailIsOpen(true)}
>
<GaugeCircleIcon
className={`w-4 h-4
${
currentRecording.pronunciationAssessment
? currentRecording.pronunciationAssessment
.pronunciationScore >= 80
? "text-green-500"
: currentRecording.pronunciationAssessment
.pronunciationScore >= 60
? "text-yellow-600"
: "text-red-500"
: ""
}
`}
/>
</Button>
<Button
variant={isComparing ? "secondary" : "outline"}
size="icon"
@@ -514,19 +543,6 @@ export const MediaCurrentRecording = () => {
<GitCompareIcon className="w-4 h-4" />
</Button>
<Button
variant={isSelectingRegion ? "secondary" : "outline"}
size="icon"
data-tooltip-id="media-player-tooltip"
data-tooltip-content={t("selectRegion")}
className={
layout?.name === "sm" ? "hidden" : "rounded-full w-8 h-8 p-0"
}
onClick={() => setIsSelectingRegion(!isSelectingRegion)}
>
<TextCursorInputIcon className="w-4 h-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -545,27 +561,10 @@ export const MediaCurrentRecording = () => {
<>
<DropdownMenuItem
className="cursor-pointer"
onClick={toggleCompare}
onClick={() => setDetailIsOpen(true)}
>
<GitCompareIcon className="w-4 h-4 mr-4" />
<span>{t("compare")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setIsSelectingRegion(!isSelectingRegion)}
>
<TextCursorInputIcon className="w-4 h-4 mr-4" />
<span>{t("selectRegion")}</span>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setDetailIsOpen(true)}
>
<GaugeCircleIcon
className={`w-4 h-4 mr-4
<GaugeCircleIcon
className={`w-4 h-4 mr-4
${
currentRecording.pronunciationAssessment
? currentRecording.pronunciationAssessment
@@ -578,10 +577,27 @@ export const MediaCurrentRecording = () => {
: ""
}
`}
/>
<span>{t("pronunciationAssessment")}</span>
</DropdownMenuItem>
/>
<span>{t("pronunciationAssessment")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={toggleCompare}
>
<GitCompareIcon className="w-4 h-4 mr-4" />
<span>{t("compare")}</span>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
className="cursor-pointer"
data-tooltip-content={t("selectRegion")}
onClick={() => setIsSelectingRegion(!isSelectingRegion)}
>
<TextCursorInputIcon className="w-4 h-4 mr-4" />
<span>{t("selectRegion")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setIsSharing(true)}
@@ -625,6 +641,9 @@ export const MediaCurrentRecording = () => {
displayClose={false}
>
<SheetHeader className="flex items-center justify-center -mt-4 mb-2">
<SheetTitle className="hidden">
{t("pronunciationAssessment")}
</SheetTitle>
<SheetClose>
<ChevronDownIcon />
</SheetClose>

View File

@@ -22,6 +22,7 @@ import {
MediaPlayerProviderContext,
} from "@renderer/context";
import {
GaugeCircleIcon,
LoaderIcon,
MicIcon,
MoreHorizontalIcon,
@@ -90,6 +91,28 @@ export const MediaRecordings = () => {
<span>{formatDuration(recording.duration, "ms")}</span>
</div>
<div className="flex items-center space-x-2">
{recording.pronunciationAssessment?.result && (
<div
className={`flex items-center space-x-1
${
recording.pronunciationAssessment
? recording.pronunciationAssessment
.pronunciationScore >= 80
? "text-green-500"
: recording.pronunciationAssessment
.pronunciationScore >= 60
? "text-yellow-600"
: "text-red-500"
: ""
}
`}
>
<GaugeCircleIcon className="w-4 h-4" />
<span className="text-xs font-mono">
{recording.pronunciationAssessment.pronunciationScore}
</span>
</div>
)}
<span className="text-sm text-muted-foreground">
{formatDateTime(recording.createdAt)}
</span>

View File

@@ -24,13 +24,15 @@ export const MediaTabs = () => {
return (
<ScrollArea className="h-full">
<div
className={`p-1 bg-muted rounded-t-lg mb-2 text-sm sticky top-0 z-[1] grid gap-4 ${media?.mediaType === "Video" ? "grid-cols-4" : "grid-cols-3"
}`}
className={`p-1 bg-muted rounded-t-lg mb-2 text-sm sticky top-0 z-[1] grid gap-4 ${
media?.mediaType === "Video" ? "grid-cols-4" : "grid-cols-3"
}`}
>
{media.mediaType === "Video" && (
<div
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${tab === "provider" ? "bg-background" : ""
}`}
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${
tab === "provider" ? "bg-background" : ""
}`}
onClick={() => setTab("provider")}
>
{t("player")}
@@ -38,22 +40,25 @@ export const MediaTabs = () => {
)}
<div
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${tab === "transcription" ? "bg-background" : ""
}`}
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${
tab === "transcription" ? "bg-background" : ""
}`}
onClick={() => setTab("transcription")}
>
{t("transcription")}
</div>
<div
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${tab === "recordings" ? "bg-background" : ""
}`}
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${
tab === "recordings" ? "bg-background" : ""
}`}
onClick={() => setTab("recordings")}
>
{t("myRecordings")}
</div>
<div
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${tab === "info" ? "bg-background" : ""
}`}
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize truncate ${
tab === "info" ? "bg-background" : ""
}`}
onClick={() => setTab("info")}
>
{t("mediaInfo")}
@@ -67,7 +72,7 @@ export const MediaTabs = () => {
<MediaRecordings />
</div>
<div className={tab === "transcription" ? "" : "hidden"}>
<MediaTranscription />
<MediaTranscription display={tab === "transcription"} />
</div>
<div className={tab === "info" ? "" : "hidden"}>
<MediaInfoPanel />

View File

@@ -15,6 +15,7 @@ import {
Button,
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
DropdownMenu,
DropdownMenuContent,
@@ -121,6 +122,7 @@ export const MediaTranscriptionReadButton = (props: {
onPointerDownOutside={(event) => event.preventDefault()}
className="max-w-screen-md xl:max-w-screen-lg h-5/6 flex flex-col p-0"
>
<DialogTitle className="hidden">{t("readThrough")}</DialogTitle>
<ScrollArea className="flex-1 px-6 pt-4">
<div className="select-text mx-auto w-full max-w-prose">
<h3 className="font-bold text-xl my-4">{media.name}</h3>

View File

@@ -19,6 +19,7 @@ import {
MicIcon,
PencilLineIcon,
SquareMenuIcon,
GaugeCircleIcon,
} from "lucide-react";
import { AlignmentResult } from "echogarden/dist/api/API.d.js";
import { formatDuration } from "@renderer/lib/utils";
@@ -28,7 +29,8 @@ import {
MediaTranscriptionGenerateButton,
} from "@renderer/components";
export const MediaTranscription = () => {
export const MediaTranscription = (props: { display?: boolean }) => {
const { display } = props;
const containerRef = useRef<HTMLDivElement>();
const {
decoded,
@@ -69,6 +71,21 @@ export const MediaTranscription = () => {
});
};
const scrollToCurrentSegment = () => {
if (!containerRef?.current) return;
if (!decoded) return;
if (!display) return;
setTimeout(() => {
containerRef.current
?.querySelector(`#segment-${currentSegmentIndex}`)
?.scrollIntoView({
block: "center",
inline: "center",
} as ScrollIntoViewOptions);
}, 300);
};
useEffect(() => {
if (!transcription?.result) return;
@@ -81,18 +98,8 @@ export const MediaTranscription = () => {
}, [transcription?.result]);
useEffect(() => {
if (!containerRef?.current) return;
if (!decoded) return;
setTimeout(() => {
containerRef.current
?.querySelector(`#segment-${currentSegmentIndex}`)
?.scrollIntoView({
block: "center",
inline: "center",
} as ScrollIntoViewOptions);
}, 300);
}, [decoded, currentSegmentIndex, transcription, containerRef]);
scrollToCurrentSegment();
}, [display, decoded, currentSegmentIndex, transcription, containerRef]);
if (!transcription?.result?.timeline) {
return null;
@@ -182,9 +189,10 @@ export const MediaTranscription = () => {
<div className="flex items-center justify-between">
<span className="text-xs opacity-50">#{index + 1}</span>
<div className="flex items-center space-x-2">
{(recordingStats || []).findIndex(
(s) => s.referenceId === index
) !== -1 && <MicIcon className="w-3 h-3 text-sky-500" />}
<RecordingStatsRemark
stats={recordingStats}
referenceId={index}
/>
{(notesStats || []).findIndex(
(s) => s.segment?.segmentIndex === index
) !== -1 && <PencilLineIcon className="w-3 h-3 text-sky-500" />}
@@ -200,3 +208,34 @@ export const MediaTranscription = () => {
</div>
);
};
const RecordingStatsRemark = (props: {
stats: SegementRecordingStatsType;
referenceId: number;
}) => {
const { stats = [], referenceId } = props;
const stat = stats.find((s) => s.referenceId === referenceId);
if (!stat) return null;
return (
<>
{stat.pronunciationAssessment?.pronunciationScore && (
<GaugeCircleIcon
className={`w-3 h-3
${
stat.pronunciationAssessment
? stat.pronunciationAssessment.pronunciationScore >= 80
? "text-green-500"
: stat.pronunciationAssessment.pronunciationScore >=
60
? "text-yellow-600"
: "text-red-500"
: ""
}
`}
/>
)}
<MicIcon className="w-3 h-3 text-sky-500" />
</>
);
};

View File

@@ -14,6 +14,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuItem,
Separator,
DialogTitle,
} from "@renderer/components/ui";
import {
SettingsIcon,
@@ -172,6 +173,9 @@ export const Sidebar = () => {
</DialogTrigger>
<DialogContent className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0">
<DialogTitle className="hidden">
{t("sidebar.preferences")}
</DialogTitle>
<Preferences />
</DialogContent>
</Dialog>

View File

@@ -4,7 +4,7 @@ import {
PronunciationAssessmentScoreResult,
} from "@renderer/components";
import { Separator, ScrollArea } from "@renderer/components/ui";
import { useState, useContext } from "react";
import { useState, useContext, useEffect } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { Tooltip } from "react-tooltip";
@@ -29,12 +29,19 @@ export const RecordingDetail = (props: {
const [assessing, setAssessing] = useState(false);
const assess = () => {
if (assessing) return;
if (result) return;
setAssessing(true);
EnjoyApp.recordings.assess(recording.id, learningLanguage).finally(() => {
setAssessing(false);
});
};
useEffect(() => {
assess();
}, [recording]);
return (
<div className="">
<div className="mb-6 px-4">

View File

@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}

View File

@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@@ -38,13 +38,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>