Fix layout (#454)

* player layout autosize

* fix video layout

* improve style
This commit is contained in:
an-lee
2024-03-28 10:35:32 +08:00
committed by GitHub
parent 97f970ed5b
commit e63d77cd82
12 changed files with 239 additions and 139 deletions

View File

@@ -296,7 +296,7 @@
"releaseToStop": "Release to stop",
"deleteRecording": "delete recording",
"deleteRecordingConfirmation": "Are you sure to delete this recording?",
"myRecordings": "my recordings",
"myRecordings": "recordings",
"noRecordingForThisSegmentYet": "No recordings for this segment yet. Press <kbd>R</kbd> to start recording.",
"lastYear": "last year",
"less": "less",

View File

@@ -440,16 +440,20 @@ ${log}
// Create the browser window.
const mainWindow = new BrowserWindow({
icon: "./assets/icon.png",
width: 1920,
height: 1080,
minWidth: 1440,
minHeight: 900,
width: 1440,
height: 900,
minWidth: 1024,
minHeight: 768,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
spellcheck: false,
},
});
mainWindow.on("resize", () => {
mainWindow.webContents.send("window-on-resize", mainWindow.getBounds());
});
mainWindow.webContents.setWindowOpenHandler(() => {
return { action: "allow" };
});

View File

@@ -34,6 +34,17 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
},
version,
},
window: {
onResize: (
callback: (
event: IpcRendererEvent,
bounds: { x: number; y: number; width: number; height: number }
) => void
) => ipcRenderer.on("window-on-resize", callback),
removeListeners: () => {
ipcRenderer.removeAllListeners("window-on-resize");
},
},
system: {
preferences: {
mediaAccess: (mediaType: "microphone" | "camera") => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}}

View File

@@ -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={() => {

View File

@@ -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) => {

View File

@@ -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")}

View File

@@ -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>

View File

@@ -11,8 +11,10 @@ import { TimelineEntry } from "echogarden/dist/utilities/Timeline.d.js";
import { IPA_MAPPING } from "@/constants";
import { toast } from "@renderer/components/ui";
import { Tooltip } from "react-tooltip";
import { debounce } from "lodash";
type MediaPlayerContextType = {
layout: { name: string, width: number, height: number, upperWrapper: string, lowerWrapper: string, playerWrapper: string, panelWrapper: string, playerHeight: number };
media: AudioType | VideoType;
setMedia: (media: AudioType | VideoType) => void;
setMediaProvider: (mediaProvider: HTMLAudioElement | null) => void;
@@ -68,15 +70,35 @@ type MediaPlayerContextType = {
export const MediaPlayerProviderContext =
createContext<MediaPlayerContextType>(null);
const LAYOUT = {
sm: {
name: 'sm',
upperWrapper: "h-[calc(100vh-27.5rem)]",
lowerWrapper: "h-[23rem]",
playerWrapper: "h-[9rem] mb-2",
panelWrapper: "h-16",
playerHeight: 128,
},
lg: {
name: 'lg',
upperWrapper: "h-[calc(100vh-37.5rem)]",
lowerWrapper: "h-[33rem]",
panelWrapper: "h-20",
playerWrapper: "h-[13rem] mb-4",
playerHeight: 192,
},
}
export const MediaPlayerProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const height = 192;
const minPxPerSec = 150;
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const [layout, setLayout] = useState<{ name: string, width: number, height: number, upperWrapper: string, lowerWrapper: string, playerWrapper: string, panelWrapper: string, playerHeight: number }>();
const [media, setMedia] = useState<AudioType | VideoType>(null);
const [mediaProvider, setMediaProvider] = useState<HTMLAudioElement | null>(
null
@@ -126,7 +148,7 @@ export const MediaPlayerProvider = ({
const ws = WaveSurfer.create({
container: ref.current,
height,
height: layout.playerHeight,
waveColor: "#eaeaea",
progressColor: "#c0d6df",
cursorColor: "#ff0054",
@@ -196,7 +218,7 @@ export const MediaPlayerProvider = ({
const canvasId = options?.canvasId || `pitch-contour-${region.id}-canvas`;
canvas.id = canvasId;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
canvas.style.height = `${layout.playerHeight}px`;
pitchContourWidthContainer.appendChild(canvas);
pitchContourWidthContainer.style.position = "absolute";
@@ -204,7 +226,7 @@ export const MediaPlayerProvider = ({
pitchContourWidthContainer.style.left = "0";
pitchContourWidthContainer.style.width = `${width}px`;
pitchContourWidthContainer.style.height = `${height}px`;
pitchContourWidthContainer.style.height = `${layout.playerHeight}px`;
pitchContourWidthContainer.style.marginLeft = `${offsetLeft}px`;
pitchContourWidthContainer.classList.add(
"pitch-contour",
@@ -317,6 +339,16 @@ export const MediaPlayerProvider = ({
);
};
const calculateHeight = () => {
if (window.innerHeight <= 1080) {
setLayout({ ...LAYOUT.sm, width: window.innerWidth, height: window.innerHeight });
} else {
setLayout({ ...LAYOUT.lg, width: window.innerWidth, height: window.innerHeight });
}
}
const deboundeCalculateHeight = debounce(calculateHeight, 100);
/*
* When wavesurfer is decoded,
* set up event listeners for wavesurfer
@@ -432,15 +464,37 @@ export const MediaPlayerProvider = ({
* and mediaProvider is available
*/
useEffect(() => {
if (!layout?.playerHeight) return;
if (!media) return;
if (!ref) return;
if (!mediaProvider) return;
initializeWavesurfer();
setDecoded(false);
setDecodeError(null);
}, [media, ref, mediaProvider]);
return () => {
if (wavesurfer) wavesurfer.destroy();
}
}, [media, ref, mediaProvider, layout?.playerHeight]);
useEffect(() => {
calculateHeight();
EnjoyApp.window.onResize((event, bounds) => {
deboundeCalculateHeight();
})
return () => {
EnjoyApp.window.removeListeners();
}
}, [])
return (
<>
<MediaPlayerProviderContext.Provider
value={{
layout,
media,
setMedia,
setMediaProvider,

View File

@@ -11,6 +11,10 @@ type EnjoyAppType = {
createIssue: (title: string, body: string) => Promise<void>;
version: string;
};
window: {
onResize: (callback: (event, bounds: any) => void) => void;
removeListeners: () => void;
};
system: {
preferences: {
mediaAccess: (mediaType: "microphone") => Promise<boolean>;