Fix layout (#454)
* player layout autosize * fix video layout * improve style
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useContext, useRef } from "react";
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
import { MediaPlayerProviderContext } from "@renderer/context";
|
||||
import {
|
||||
MediaLoadingModal,
|
||||
@@ -7,43 +7,45 @@ import {
|
||||
MediaTabs,
|
||||
MediaCurrentRecording,
|
||||
MediaPlayer,
|
||||
LoaderSpin,
|
||||
} from "@renderer/components";
|
||||
import { useAudio } from "@renderer/hooks";
|
||||
|
||||
export const AudioPlayer = (props: { id?: string; md5?: string }) => {
|
||||
const { id, md5 } = props;
|
||||
const { setMedia } = useContext(MediaPlayerProviderContext);
|
||||
const { setMedia, layout } = useContext(MediaPlayerProviderContext);
|
||||
const { audio } = useAudio({ id, md5 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!audio) return;
|
||||
|
||||
setMedia(audio);
|
||||
}, [audio]);
|
||||
|
||||
if (!layout) return <LoaderSpin />;
|
||||
|
||||
return (
|
||||
<div data-testid="audio-player">
|
||||
<div className="h-[calc(100vh-37.5rem)] mb-4">
|
||||
<div className="grid grid-cols-3 gap-6 px-6 h-full">
|
||||
<div className="col-span-1 rounded-lg border shadow-lg h-[calc(100vh-37.5rem)]">
|
||||
<div className={`${layout.upperWrapper} mb-4`}>
|
||||
<div className="grid grid-cols-5 xl:grid-cols-3 gap-6 px-6 h-full">
|
||||
<div className={`col-span-2 xl:col-span-1 rounded-lg border shadow-lg ${layout.upperWrapper}`}>
|
||||
<MediaTabs />
|
||||
</div>
|
||||
<div className="col-span-2 h-[calc(100vh-37.5rem)]">
|
||||
<div className={`col-span-3 xl:col-span-2 ${layout.upperWrapper}`}>
|
||||
<MediaCaption />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[33rem] flex flex-col">
|
||||
<div className="h-[13rem] py-2 px-6 mb-4">
|
||||
<MediaCurrentRecording />
|
||||
<div className={`${layout.lowerWrapper} flex flex-col`}>
|
||||
<div className={`${layout.playerWrapper} py-2 px-6`}>
|
||||
<MediaCurrentRecording height={layout.playerHeight} />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-[13rem] px-6 py-2 mb-4">
|
||||
<div className={`${layout.playerWrapper} py-2 px-6`}>
|
||||
<MediaPlayer />
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-background z-10 shadow-xl">
|
||||
<div className={`${layout.panelWrapper} w-full bg-background z-10 shadow-xl`}>
|
||||
<MediaPlayerControls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,8 +46,9 @@ import { formatDuration } from "@renderer/lib/utils";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
const { height = 192 } = props;
|
||||
const { height } = props;
|
||||
const {
|
||||
layout,
|
||||
isRecording,
|
||||
setIsRecording,
|
||||
currentRecording,
|
||||
@@ -281,9 +282,9 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
ws.destroy();
|
||||
ws?.destroy();
|
||||
};
|
||||
}, [ref, currentRecording, isRecording]);
|
||||
}, [ref, currentRecording, isRecording, height]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsComparing(false);
|
||||
@@ -315,7 +316,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
}
|
||||
|
||||
const subscriptions = [
|
||||
regions.on("region-created", () => {}),
|
||||
regions.on("region-created", () => { }),
|
||||
|
||||
regions.on("region-clicked", (region, e) => {
|
||||
e.stopPropagation();
|
||||
@@ -383,16 +384,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
|
||||
useEffect(() => {
|
||||
calContainerWidth();
|
||||
window.addEventListener("resize", () => {
|
||||
calContainerWidth();
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", () => {
|
||||
calContainerWidth();
|
||||
});
|
||||
};
|
||||
}, [currentRecording, isRecording]);
|
||||
}, [currentRecording, isRecording, layout?.width]);
|
||||
|
||||
useHotkeys(
|
||||
["Ctrl+R", "Meta+R"],
|
||||
@@ -410,7 +402,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
[player]
|
||||
);
|
||||
|
||||
if (isRecording) return <MediaRecorder />;
|
||||
if (isRecording) return <MediaRecorder height={height} />;
|
||||
if (!currentRecording?.src)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center space-x-4">
|
||||
@@ -480,27 +472,31 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
setIsRecording={setIsRecording}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant={isComparing ? "secondary" : "outline"}
|
||||
size="icon"
|
||||
data-tooltip-id="media-player-tooltip"
|
||||
data-tooltip-content={t("compare")}
|
||||
className="rounded-full w-8 h-8 p-0"
|
||||
onClick={toggleCompare}
|
||||
>
|
||||
<GitCompareIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
{
|
||||
height >= 192 && <>
|
||||
<Button
|
||||
variant={isComparing ? "secondary" : "outline"}
|
||||
size="icon"
|
||||
data-tooltip-id="media-player-tooltip"
|
||||
data-tooltip-content={t("compare")}
|
||||
className="rounded-full w-8 h-8 p-0"
|
||||
onClick={toggleCompare}
|
||||
>
|
||||
<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="rounded-full w-8 h-8 p-0"
|
||||
onClick={() => setIsSelectingRegion(!isSelectingRegion)}
|
||||
>
|
||||
<TextCursorInputIcon 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="rounded-full w-8 h-8 p-0"
|
||||
onClick={() => setIsSelectingRegion(!isSelectingRegion)}
|
||||
>
|
||||
<TextCursorInputIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -522,17 +518,16 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
>
|
||||
<GaugeCircleIcon
|
||||
className={`w-4 h-4 mr-4
|
||||
${
|
||||
currentRecording.pronunciationAssessment
|
||||
? currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 80
|
||||
? "text-green-500"
|
||||
: currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 60
|
||||
? "text-yellow-600"
|
||||
: "text-red-500"
|
||||
: ""
|
||||
}
|
||||
${currentRecording.pronunciationAssessment
|
||||
? currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 80
|
||||
? "text-green-500"
|
||||
: currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 60
|
||||
? "text-yellow-600"
|
||||
: "text-red-500"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<span>{t("pronunciationAssessment")}</span>
|
||||
|
||||
@@ -456,7 +456,7 @@ export const MediaPlayerControls = () => {
|
||||
}, [grouping]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-20 flex items-center justify-center px-6">
|
||||
<div className="w-full h-full flex items-center justify-center px-6">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -480,11 +480,10 @@ export const MediaPlayerControls = () => {
|
||||
{PLAYBACK_RATE_OPTIONS.map((rate, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`cursor-pointer h-10 w-10 leading-10 rounded-full flex items-center justify-center ${
|
||||
rate === playbackRate
|
||||
className={`cursor-pointer h-10 w-10 leading-10 rounded-full flex items-center justify-center ${rate === playbackRate
|
||||
? "bg-primary text-white text-md"
|
||||
: "text-black/70 text-xs"
|
||||
}`}
|
||||
}`}
|
||||
onClick={() => {
|
||||
setPlaybackRate(rate);
|
||||
}}
|
||||
|
||||
@@ -41,6 +41,7 @@ const MAX_ZOOM_RATIO = 4.0;
|
||||
export const MediaPlayer = () => {
|
||||
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
|
||||
const {
|
||||
layout,
|
||||
media,
|
||||
currentTime,
|
||||
setRef,
|
||||
@@ -132,16 +133,7 @@ export const MediaPlayer = () => {
|
||||
|
||||
useEffect(() => {
|
||||
calContainerWidth();
|
||||
window.addEventListener("resize", () => {
|
||||
calContainerWidth();
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", () => {
|
||||
calContainerWidth();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
}, [layout.width]);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4 media-player-wrapper">
|
||||
@@ -192,38 +184,44 @@ export const MediaPlayer = () => {
|
||||
<ZoomInIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={`${zoomRatio < 1.0 ? "secondary" : "outline"}`}
|
||||
data-tooltip-id="media-player-tooltip"
|
||||
data-tooltip-content={t("zoomOut")}
|
||||
className="relative aspect-square rounded-full p-0 h-8"
|
||||
onClick={() => {
|
||||
if (zoomRatio > MIN_ZOOM_RATIO) {
|
||||
const nextZoomRatio = ZOOM_RATIO_OPTIONS.reverse().find(
|
||||
(rate) => rate < zoomRatio
|
||||
);
|
||||
setZoomRatio(nextZoomRatio || MIN_ZOOM_RATIO);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ZoomOutIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
{
|
||||
layout.name === "lg" && (
|
||||
<>
|
||||
<Button
|
||||
variant={`${zoomRatio < 1.0 ? "secondary" : "outline"}`}
|
||||
data-tooltip-id="media-player-tooltip"
|
||||
data-tooltip-content={t("zoomOut")}
|
||||
className="relative aspect-square rounded-full p-0 h-8"
|
||||
onClick={() => {
|
||||
if (zoomRatio > MIN_ZOOM_RATIO) {
|
||||
const nextZoomRatio = ZOOM_RATIO_OPTIONS.reverse().find(
|
||||
(rate) => rate < zoomRatio
|
||||
);
|
||||
setZoomRatio(nextZoomRatio || MIN_ZOOM_RATIO);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ZoomOutIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={`${displayInlineCaption ? "secondary" : "outline"}`}
|
||||
data-tooltip-id="media-player-tooltip"
|
||||
data-tooltip-content={t("inlineCaption")}
|
||||
className="relative aspect-square rounded-full p-0 h-8"
|
||||
onClick={() => {
|
||||
setDisplayInlineCaption(!displayInlineCaption);
|
||||
if (pitchChart) {
|
||||
pitchChart.options.scales.x.display = !displayInlineCaption;
|
||||
pitchChart.update();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SpellCheckIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={`${displayInlineCaption ? "secondary" : "outline"}`}
|
||||
data-tooltip-id="media-player-tooltip"
|
||||
data-tooltip-content={t("inlineCaption")}
|
||||
className="relative aspect-square rounded-full p-0 h-8"
|
||||
onClick={() => {
|
||||
setDisplayInlineCaption(!displayInlineCaption);
|
||||
if (pitchChart) {
|
||||
pitchChart.options.scales.x.display = !displayInlineCaption;
|
||||
pitchChart.update();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SpellCheckIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -239,6 +237,40 @@ export const MediaPlayer = () => {
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
{
|
||||
layout.name === "sm" && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (zoomRatio > MIN_ZOOM_RATIO) {
|
||||
const nextZoomRatio = ZOOM_RATIO_OPTIONS.reverse().find(
|
||||
(rate) => rate < zoomRatio
|
||||
);
|
||||
setZoomRatio(nextZoomRatio || MIN_ZOOM_RATIO);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ZoomOutIcon className="w-4 h-4 mr-4" />
|
||||
<span>{t("zoomOut")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setDisplayInlineCaption(!displayInlineCaption);
|
||||
if (pitchChart) {
|
||||
pitchChart.options.scales.x.display = !displayInlineCaption;
|
||||
pitchChart.update();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SpellCheckIcon className="w-4 h-4 mr-4" />
|
||||
<span>{t("inlineCaption")}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
@@ -20,8 +20,9 @@ export const MediaProvider = () => {
|
||||
if (!media?.src) return null;
|
||||
|
||||
return (
|
||||
<div className="px-4" data-testid="media-player">
|
||||
<div className="px-2 py-4" data-testid="media-player">
|
||||
<VidstackMediaPlayer
|
||||
className="my-auto"
|
||||
controls
|
||||
src={media.src}
|
||||
onCanPlayThrough={(detail, nativeEvent) => {
|
||||
|
||||
@@ -24,15 +24,13 @@ 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-10 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-10 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 ${
|
||||
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")}
|
||||
@@ -40,25 +38,22 @@ export const MediaTabs = () => {
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`rounded cursor-pointer px-2 py-1 text-sm text-center capitalize ${
|
||||
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 ${
|
||||
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 ${
|
||||
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")}
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
MediaTabs,
|
||||
MediaCurrentRecording,
|
||||
MediaPlayer,
|
||||
LoaderSpin,
|
||||
} from "@renderer/components";
|
||||
import { useVideo } from "@renderer/hooks";
|
||||
|
||||
export const VideoPlayer = (props: { id?: string; md5?: string }) => {
|
||||
const { id, md5 } = props;
|
||||
const { setMedia } = useContext(MediaPlayerProviderContext);
|
||||
const { setMedia, layout } = useContext(MediaPlayerProviderContext);
|
||||
const { video } = useVideo({ id, md5 });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -21,29 +22,31 @@ export const VideoPlayer = (props: { id?: string; md5?: string }) => {
|
||||
setMedia(video);
|
||||
}, [video]);
|
||||
|
||||
if (!layout) return <LoaderSpin />;
|
||||
|
||||
return (
|
||||
<div data-testid="video-player">
|
||||
<div className="h-[calc(100vh-37.5rem)] mb-4">
|
||||
<div className="grid grid-cols-3 gap-4 px-6 h-full">
|
||||
<div className="col-span-1 rounded-lg border shadow-lg h-[calc(100vh-37.5rem)]">
|
||||
<div className={`${layout.upperWrapper} mb-4`}>
|
||||
<div className="grid grid-cols-5 xl:grid-cols-3 gap-6 px-6 h-full">
|
||||
<div className={`col-span-2 xl:col-span-1 rounded-lg border shadow-lg ${layout.upperWrapper}`}>
|
||||
<MediaTabs />
|
||||
</div>
|
||||
<div className="col-span-2 h-[calc(100vh-37.5rem)]">
|
||||
<div className={`col-span-3 xl:col-span-2 ${layout.upperWrapper}`}>
|
||||
<MediaCaption />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[33rem] flex flex-col">
|
||||
<div className="h-[13rem] py-2 px-6 mb-4">
|
||||
<MediaCurrentRecording />
|
||||
<div className={`${layout.lowerWrapper} flex flex-col`}>
|
||||
<div className={`${layout.playerWrapper} py-2 px-6`}>
|
||||
<MediaCurrentRecording height={layout.playerHeight} />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-[13rem] px-6 py-2 mb-4">
|
||||
<div className={`${layout.playerWrapper} py-2 px-6`}>
|
||||
<MediaPlayer />
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-background z-10 shadow-xl">
|
||||
<div className={`${layout.panelWrapper} w-full bg-background z-10 shadow-xl`}>
|
||||
<MediaPlayerControls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user