Skip to content

Commit

Permalink
Implement side-by-side match comparison view (#155) (#169)
Browse files Browse the repository at this point in the history
* Refactor file cluster page

* Refacto VideoInformationPane

* Make video details elements collapsible

* Move distance element to common package

* Add comparable file component

* Implement reusable file summary

* Implement match file list

* Implement mother file view

* Fetch matched files scenes

* Setup comparison page routing

* Reset video player on file change

* Fix matched file header

* Improve distance style

* Hook up compare button

* Fix match duplication

* Fix linting issues

* Make compare button primary-colored
  • Loading branch information
stepan-anokhin authored Oct 30, 2020
1 parent f995086 commit c3b80f4
Show file tree
Hide file tree
Showing 47 changed files with 1,481 additions and 145 deletions.
4 changes: 4 additions & 0 deletions web/src/collection/components/CollectionRootPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import FileBrowserPage from "./FileBrowserPage/FileBrowserPage";
import VideoDetailsPage from "./VideoDetailsPage/VideoDetailsPage";
import FileMatchesPage from "./FileMatchesPage/FileMatchesPage";
import FileClusterPage from "./FileClusterPage";
import FileComparisonPage from "./FileComparisonPage";

const useStyles = makeStyles(() => ({
body: {
Expand Down Expand Up @@ -46,6 +47,9 @@ function CollectionRootPage(props) {
<Route exact path={routes.collection.fileCluster}>
<FileClusterPage />
</Route>
<Route exact path={routes.collection.fileComparison}>
<FileComparisonPage />
</Route>
</Switch>
</div>
</AppPage>
Expand Down
56 changes: 16 additions & 40 deletions web/src/collection/components/FileClusterPage/FileClusterPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from "react";
import React, { useCallback } from "react";
import clsx from "clsx";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
Expand All @@ -7,12 +7,10 @@ import FileSummaryHeader from "../FileSummaryHeader";
import { useParams } from "react-router-dom";
import useFile from "../../hooks/useFile";
import FileLoadingHeader from "../FileLoadingHeader";
import { useDispatch, useSelector } from "react-redux";
import { selectFileMatches } from "../../state/selectors";
import { fetchFileMatches, updateFileMatchFilters } from "../../state/actions";
import MatchGraph from "../MatchGraph";
import { useIntl } from "react-intl";
import Loading from "./Loading";
import Loading from "../../../common/components/Loading";
import useMatches from "../../hooks/useMatches";

const useStyles = makeStyles((theme) => ({
root: {
Expand Down Expand Up @@ -50,39 +48,20 @@ function FileClusterPage(props) {
const { id } = useParams();
const messages = useMessages();
const { file, error, loadFile } = useFile(id);
const matchesState = useSelector(selectFileMatches);
const matches = matchesState.matches;
const files = matchesState.files;
const dispatch = useDispatch();
const hasMore = !(matches.length >= matchesState.total);

useEffect(() => {
dispatch(updateFileMatchFilters(id, { hops: 2 }));
}, [id]);

useEffect(() => {
if (
matchesState.loading ||
matchesState.error ||
matches.length >= matchesState.total
) {
return;
}
dispatch(fetchFileMatches());
}, [matchesState]);

const handleRetry = useCallback(() => {
if (matchesState.total == null) {
dispatch(updateFileMatchFilters(id, { hops: 2 }));
} else {
dispatch(fetchFileMatches());
}
}, [matchesState]);
const {
matches,
files,
error: matchError,
loadMatches,
hasMore,
total,
} = useMatches(id, { hops: 2 });

const handleLoadFile = useCallback(() => {
loadFile();
handleRetry();
}, [handleRetry, loadFile]);
loadMatches();
}, [loadMatches, loadFile]);

if (file == null) {
return (
Expand All @@ -99,15 +78,12 @@ function FileClusterPage(props) {

let content;
if (hasMore) {
const progress =
matchesState.total == null
? undefined
: matches.length / matchesState.total;
const progress = total == null ? undefined : matches.length / total;

content = (
<Loading
error={matchesState.error}
onRetry={handleRetry}
error={matchError}
onRetry={loadMatches}
progress={progress}
errorMessage={messages.loadError}
className={classes.loading}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";
import clsx from "clsx";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
import Grid from "@material-ui/core/Grid";
import MotherFile from "./MotherFile/MotherFile";
import MatchFiles from "./MatchFiles/MatchFiles";
import { useParams } from "react-router-dom";

const useStyles = makeStyles((theme) => ({
root: {
paddingTop: theme.dimensions.content.padding * 2,
},
}));

function FileComparisonPage(props) {
const { className } = props;
const classes = useStyles();
const { id: rawId } = useParams();
const id = Number(rawId);

return (
<div className={clsx(classes.root, className)}>
<Grid container spacing={0}>
<Grid item xs={12} lg={6}>
<MotherFile motherFileId={id} />
</Grid>
<Grid item xs={12} lg={6}>
<MatchFiles motherFileId={id} />
</Grid>
</Grid>
</div>
);
}

FileComparisonPage.propTypes = {
className: PropTypes.string,
};

export default FileComparisonPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useCallback, useState } from "react";
import clsx from "clsx";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
import { FileType } from "../../FileBrowserPage/FileType";
import Paper from "@material-ui/core/Paper";
import CollapseButton from "../../../../common/components/CollapseButton";
import { useIntl } from "react-intl";
import Collapse from "@material-ui/core/Collapse";
import VideoInformation from "../../VideoDetailsPage/VideoInformation";

const useStyles = makeStyles((theme) => ({
root: {
boxShadow: "0 12px 18px 0 rgba(0,0,0,0.08)",
display: "flex",
flexDirection: "column",
alignItems: "stretch",
},
header: {
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
},
title: {
...theme.mixins.title3,
fontWeight: "bold",
flexGrow: 1,
},
collapseButton: {
flexGrow: 0,
},
}));

/**
* Get i18n text.
*/
function useMessages() {
const intl = useIntl();
return {
title: intl.formatMessage({ id: "file.details" }),
};
}

function FileDescriptionPane(props) {
const { file, onJump, collapsible, className, ...other } = props;
const classes = useStyles();
const messages = useMessages();
const [collapsed, setCollapsed] = useState(false);

const handleCollapse = useCallback(() => setCollapsed(!collapsed), [
collapsed,
]);

return (
<Paper className={clsx(classes.root, className)} {...other}>
<div className={classes.header}>
<div className={classes.title}>{messages.title}</div>
{collapsible && (
<CollapseButton
collapsed={collapsed}
onClick={handleCollapse}
size="small"
/>
)}
</div>
<Collapse in={!collapsed}>
<VideoInformation file={file} onJump={onJump} />
</Collapse>
</Paper>
);
}

FileDescriptionPane.propTypes = {
/**
* Video file
*/
file: FileType.isRequired,
/**
* Jump to a particular object
*/
onJump: PropTypes.func,
/**
* Enable or disable pane collapse feature.
*/
collapsible: PropTypes.bool,
className: PropTypes.string,
};

export default FileDescriptionPane;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useCallback, useState } from "react";
import clsx from "clsx";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
import { FileType } from "../../FileBrowserPage/FileType";
import VideoPlayerPane from "../../VideoDetailsPage/VideoPlayerPane";
import { seekTo } from "../../VideoDetailsPage/seekTo";
import FileDescriptionPane from "./FileDescriptionPane";

const useStyles = makeStyles((theme) => ({
root: {
// display: "block",
},
pane: {
margin: theme.spacing(2),
},
}));

function FileDetails(props) {
const { file, className } = props;
const classes = useStyles();
const [player, setPlayer] = useState(null);

const handleJump = useCallback(seekTo(player, file), [player, file]);

return (
<div className={clsx(classes.root, className)}>
<VideoPlayerPane
collapsible
file={file}
onPlayerReady={setPlayer}
className={classes.pane}
/>
<FileDescriptionPane
collapsible
file={file}
onJump={handleJump}
className={classes.pane}
/>
</div>
);
}

FileDetails.propTypes = {
/**
* Video file to be displayed
*/
file: FileType.isRequired,
className: PropTypes.string,
};

export default FileDetails;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./FileDetails";
54 changes: 54 additions & 0 deletions web/src/collection/components/FileComparisonPage/LoadingHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import clsx from "clsx";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
import Paper from "@material-ui/core/Paper";
import Loading from "../../../common/components/Loading";

const useStyles = makeStyles((theme) => ({
root: {
boxShadow: "0 12px 18px 0 rgba(0,0,0,0.08)",
minHeight: theme.spacing(12),
display: "flex",
alignItems: "center",
justifyContent: "center",
},
}));

function LoadingHeader(props) {
const { error, errorMessage, onRetry, progress, className, ...other } = props;
const classes = useStyles();
return (
<Paper className={clsx(classes.root, className)} {...other}>
<Loading
error={error}
errorMessage={errorMessage}
onRetry={onRetry}
progress={progress}
/>
</Paper>
);
}

LoadingHeader.propTypes = {
/**
* Indicate loading error
*/
error: PropTypes.bool,
/**
* The value of the progress indicator for the determinate and static variants.
* Value between 0 and 1.
*/
progress: PropTypes.number,
/**
* Trigger loading of the next portion of files
*/
onRetry: PropTypes.func.isRequired,
/**
* Message displayed when error=true
*/
errorMessage: PropTypes.string.isRequired,
className: PropTypes.string,
};

export default LoadingHeader;
Loading

0 comments on commit c3b80f4

Please sign in to comment.