Skip to content

Commit

Permalink
Merge pull request #442 from charlielee/issue-412
Browse files Browse the repository at this point in the history
Refactor capture middleware to use React context
  • Loading branch information
charlielee committed Dec 19, 2022
2 parents d573894 + d077bcb commit aee985e
Show file tree
Hide file tree
Showing 27 changed files with 429 additions and 396 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.2.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^5.5.0",
"jest": "^28.1.3",
Expand Down
13 changes: 8 additions & 5 deletions src/renderer/components/animator/Animator/Animator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useSelector } from "react-redux";
import { Take } from "../../../../common/project/Take";
import CaptureContextProvider from "../../../context/CaptureContext/CaptureContextProvider";
import PlaybackContextProvider from "../../../context/PlaybackContext/PlaybackContextProvider";
import { RootState } from "../../../redux/store";
import Content from "../../common/Content/Content";
Expand All @@ -12,7 +13,7 @@ import Tab from "../../common/Tab/Tab";
import TabGroup from "../../common/TabGroup/TabGroup";
import AnimationToolbarWithContext from "../AnimationToolbar/AnimationToolbar";
import CaptureButtonToolbarWithContext from "../CaptureButtonToolbar/CaptureButtonToolbar";
import CaptureTab from "../CaptureTab/CaptureTab";
import CaptureTabWithContext from "../CaptureTab/CaptureTab";
import MediaTab from "../MediaTab/MediaTab";
import Preview from "../Preview/Preview";
import StatusToolbarWithContext from "../StatusToolbar/StatusToolbar";
Expand All @@ -38,7 +39,7 @@ const Animator = ({ take }: AnimatorWithProviderProps): JSX.Element => {
<TabGroup
titles={["Capture", "Guides", "X-Sheet", "Media"]}
tabs={[
<CaptureTab key="capture" />,
<CaptureTabWithContext key="capture" />,

<Tab key="guides">
<SidebarBlock title="Guides" titleIcon={IconName.GUIDES}>
Expand Down Expand Up @@ -70,9 +71,11 @@ const AnimatorWithProvider = ({
}));

return (
<PlaybackContextProvider take={take} {...selectors}>
<Animator take={take} />
</PlaybackContextProvider>
<CaptureContextProvider>
<PlaybackContextProvider take={take} {...selectors}>
<Animator take={take} />
</PlaybackContextProvider>
</CaptureContextProvider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useDispatch } from "react-redux";
import CaptureContext from "../../../context/CaptureContext/CaptureContext";
import PlaybackContext from "../../../context/PlaybackContext/PlaybackContext";
import { takePhoto } from "../../../redux/capture/actions";
import IconName from "../../common/Icon/IconName";
import IconButton from "../../common/IconButton/IconButton";
import Toolbar from "../../common/Toolbar/Toolbar";
Expand All @@ -10,39 +9,47 @@ import ToolbarItem, {
import "./CaptureButtonToolbar.css";

interface CaptureToolbarProps {
takePhoto: () => void;
stopPlayback: () => void;
liveViewVisible: boolean;
}

const CaptureButtonToolbar = ({
takePhoto,
stopPlayback,
liveViewVisible,
}: CaptureToolbarProps): JSX.Element => {
const dispatch = useDispatch();

return (
<Toolbar className="capture-button-toolbar">
<ToolbarItem align={ToolbarItemAlign.CENTER}>
<IconButton
title="Capture Frame"
icon={IconName.CAPTURE}
className="animation-toolbar__capture-button"
onClick={() => {
if (!liveViewVisible) {
stopPlayback();
}
dispatch(takePhoto());
}}
/>
</ToolbarItem>
</Toolbar>
);
};
}: CaptureToolbarProps): JSX.Element => (
<Toolbar className="capture-button-toolbar">
<ToolbarItem align={ToolbarItemAlign.CENTER}>
<IconButton
title="Capture Frame"
icon={IconName.CAPTURE}
className="animation-toolbar__capture-button"
onClick={() => {
if (!liveViewVisible) {
stopPlayback();
}
takePhoto();
}}
/>
</ToolbarItem>
</Toolbar>
);

const CaptureButtonToolbarWithContext = (): JSX.Element => (
<PlaybackContext.Consumer>
{(value) => <CaptureButtonToolbar {...value} />}
</PlaybackContext.Consumer>
<CaptureContext.Consumer>
{({ takePhoto }) => (
<PlaybackContext.Consumer>
{({ stopPlayback, liveViewVisible }) => (
<CaptureButtonToolbar
takePhoto={takePhoto}
stopPlayback={stopPlayback}
liveViewVisible={liveViewVisible}
/>
)}
</PlaybackContext.Consumer>
)}
</CaptureContext.Consumer>
);

export default CaptureButtonToolbarWithContext;
24 changes: 12 additions & 12 deletions src/renderer/components/animator/CaptureTab/CaptureTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ThunkDispatch, Action } from "@reduxjs/toolkit";
import { useDispatch, useSelector } from "react-redux";
import { changeDevice } from "../../../redux/capture/actions";
import { RootState } from "../../../redux/store";
import { setCurrentDeviceFromId } from "../../../redux/thunks";
import IconName from "../../common/Icon/IconName";
import InputGroup from "../../common/Input/InputGroup/InputGroup";
import InputLabel from "../../common/Input/InputLabel/InputLabel";
Expand All @@ -9,15 +10,16 @@ import SidebarBlock from "../../common/SidebarBlock/SidebarBlock";
import Tab from "../../common/Tab/Tab";

const CaptureTab = (): JSX.Element => {
const dispatch = useDispatch();
const { currentDevice, deviceList } = useSelector(
(state: RootState) => state.app
);
const dispatch: ThunkDispatch<RootState, void, Action> = useDispatch();
const { deviceStatus, deviceList } = useSelector((state: RootState) => ({
deviceStatus: state.capture.deviceStatus,
deviceList: state.capture.deviceList,
}));

const buildCameraSourceOptions = () => ({
"No camera selected": "#",
...Object.fromEntries(
deviceList.map((device) => [device.name, device.deviceId])
deviceList.map((identifier) => [identifier.name, identifier.deviceId])
),
});

Expand All @@ -29,13 +31,11 @@ const CaptureTab = (): JSX.Element => {
<InputSelect
id="camera-source-select"
options={buildCameraSourceOptions()}
value={
currentDevice?.deviceId === undefined
? "#"
: currentDevice.deviceId
}
value={deviceStatus?.identifier?.deviceId ?? "#"}
onChange={(deviceId) =>
dispatch(changeDevice(deviceId === "#" ? undefined : deviceId))
dispatch(
setCurrentDeviceFromId(deviceId === "#" ? undefined : deviceId)
)
}
/>
</InputGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import Button from "../../common/Button/Button";
import InputGroup from "../../common/Input/InputGroup/InputGroup";
import InputLabel from "../../common/Input/InputLabel/InputLabel";
import "./ExportDirectory.css";
import { ThunkDispatch, Action } from "@reduxjs/toolkit";

const ExportDirectory = (): JSX.Element => {
const { workingDirectory } = useSelector(
(state: RootState) => state.app.userPreferences
);
const dispatch = useDispatch();
const dispatch: ThunkDispatch<RootState, void, Action> = useDispatch();

return (
<InputGroup>
Expand All @@ -25,9 +26,7 @@ const ExportDirectory = (): JSX.Element => {
<Button
id="exportDirectoryButton"
title="Browse..."
onClick={() =>
dispatch(changeWorkingDirectory(workingDirectory) as any)
}
onClick={() => dispatch(changeWorkingDirectory(workingDirectory))}
/>
</InputGroup>
);
Expand Down
46 changes: 28 additions & 18 deletions src/renderer/components/animator/Preview/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { getFileRefById } from "../../../../common/FileRef";
import { TimelineIndex } from "../../../../common/Flavors";
import { Take } from "../../../../common/project/Take";
import CaptureContext from "../../../context/CaptureContext/CaptureContext";
import PlaybackContext from "../../../context/PlaybackContext/PlaybackContext";
import { attachStreamToVideo } from "../../../redux/capture/actions";
import { RootState } from "../../../redux/store";
import { ImagingDevice } from "../../../services/imagingDevice/ImagingDevice";
import { getHighlightedTrackItem } from "../../../services/project/projectCalculator";
import "./Preview.css";
import PreviewFrame from "./PreviewFrame/PreviewFrame";
Expand All @@ -16,23 +17,24 @@ interface PreviewWithContextProps {
}

interface PreviewProps extends PreviewWithContextProps {
device: ImagingDevice | undefined;
liveViewVisible: boolean;
timelineIndex: TimelineIndex | undefined;
}

const Preview = ({
take,
device,
liveViewVisible,
timelineIndex,
}: PreviewProps): JSX.Element => {
const dispatch = useDispatch();
const { currentDevice, isDeviceOpen, hasCameraAccess, fileRefs } =
useSelector((state: RootState) => ({
currentDevice: state.app.currentDevice,
isDeviceOpen: state.app.isDeviceOpen,
const { deviceStatus, hasCameraAccess, fileRefs } = useSelector(
(state: RootState) => ({
deviceStatus: state.capture.deviceStatus,
hasCameraAccess: state.app.hasCameraAccess,
fileRefs: state.project.fileRefs,
}));
})
);
const [previewSrc, setPreviewSrc] = useState<string | undefined>();

useEffect(() => {
Expand All @@ -45,19 +47,16 @@ const Preview = ({
const { location } = getFileRefById(fileRefs, highlightedTrackItem.id);
setPreviewSrc(location);
}
}, [timelineIndex]);
}, [fileRefs, take.frameTrack, timelineIndex]);

return (
<div className="preview">
{currentDevice && hasCameraAccess && (
<PreviewLiveView
streaming={isDeviceOpen}
updateSrcObject={(element) => dispatch(attachStreamToVideo(element))}
/>
{deviceStatus && hasCameraAccess && (
<PreviewLiveView stream={device?.stream} />
)}

{liveViewVisible &&
!currentDevice &&
!deviceStatus &&
(hasCameraAccess ? (
<h2>Select a Camera Source to begin!</h2>
) : (
Expand All @@ -75,9 +74,20 @@ const Preview = ({
};

const PreviewWithContext = (props: PreviewWithContextProps): JSX.Element => (
<PlaybackContext.Consumer>
{(value) => <Preview {...props} {...value} />}
</PlaybackContext.Consumer>
<CaptureContext.Consumer>
{({ device }) => (
<PlaybackContext.Consumer>
{({ liveViewVisible, timelineIndex }) => (
<Preview
{...props}
device={device}
liveViewVisible={liveViewVisible}
timelineIndex={timelineIndex}
/>
)}
</PlaybackContext.Consumer>
)}
</CaptureContext.Consumer>
);

export default PreviewWithContext;
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ const PreviewFrame = ({ src, hidden }: PreviewFrameProps): JSX.Element => {
if (src === undefined) {
return;
}
const image = imageRef.current;
image.src = src;
image.addEventListener("load", drawImage);
setImageWidth(image.naturalWidth);
setImageHeight(image.naturalHeight);

imageRef.current.src = src;
imageRef.current.addEventListener("load", drawImage);
setImageWidth(imageRef.current.naturalWidth);
setImageHeight(imageRef.current.naturalHeight);

return () => imageRef.current.removeEventListener("load", drawImage);
return () => image.removeEventListener("load", drawImage);
}, [src]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ import { useEffect, useRef } from "react";
import "./PreviewLiveView.css";

interface PreviewLiveViewProps {
streaming: boolean;
updateSrcObject(element: HTMLVideoElement): void;
stream: MediaStream | undefined;
}

const PreviewLiveView = ({
streaming,
updateSrcObject,
}: PreviewLiveViewProps): JSX.Element => {
const PreviewLiveView = ({ stream }: PreviewLiveViewProps): JSX.Element => {
const videoRef = useRef<HTMLVideoElement>(null);

useEffect(() => {
if (videoRef.current && streaming) {
updateSrcObject(videoRef.current);
const video = videoRef.current;
if (video) {
video.srcObject = stream ?? null;
}
}, [streaming]);
}, [stream]);

return <video className="preview-live-view" autoPlay ref={videoRef}></video>;
};
Expand Down

0 comments on commit aee985e

Please sign in to comment.