Skip to content
Snippets Groups Projects
Verified Commit 39cfd69a authored by Loïs Poujade's avatar Loïs Poujade
Browse files

UI/fixes for annotation editions form

* default annotation start/end: current time / current +10s
* default annotation text content: (start) -> (end)
* added a button to fill start/end field with current player time
* allow to seekto/goto start/end during annotation edition
* hide time fields/controls when annotating another media than video
parent de1696d2
No related branches found
No related tags found
No related merge requests found
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import {
IconButton, Button, Paper, Grid, Popover, Divider,
MenuList, MenuItem, ClickAwayListener,
} from '@material-ui/core';
import { Alarm, LastPage } from '@material-ui/icons';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import ToggleButton from '@material-ui/lab/ToggleButton';
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import RectangleIcon from '@material-ui/icons/CheckBoxOutlineBlank';
......@@ -17,19 +19,18 @@ import StrokeColorIcon from '@material-ui/icons/BorderColor';
import LineWeightIcon from '@material-ui/icons/LineWeight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import FormatShapesIcon from '@material-ui/icons/FormatShapes';
import Popover from '@material-ui/core/Popover';
import Divider from '@material-ui/core/Divider';
import MenuItem from '@material-ui/core/MenuItem';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import MenuList from '@material-ui/core/MenuList';
import { SketchPicker } from 'react-color';
import { v4 as uuid } from 'uuid';
import { withStyles } from '@material-ui/core/styles';
import CompanionWindow from 'mirador/dist/es/src/containers/CompanionWindow';
import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
import AnnotationDrawing from './AnnotationDrawing';
import TextEditor from './TextEditor';
import WebAnnotation from './WebAnnotation';
import CursorIcon from './icons/Cursor';
import HMSInput from './HMSInput';
import { secondsToHMS } from './utils';
/** Extract time information from annotation target */
function timeFromAnnoTarget(annotarget) {
......@@ -57,6 +58,7 @@ class AnnotationCreation extends Component {
/** */
constructor(props) {
super(props);
const annoState = {};
if (props.annotation) {
//
......@@ -114,9 +116,9 @@ class AnnotationCreation extends Component {
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
svg: null,
tend: '',
tstart: '',
tend: Math.floor(props.currentTime) + 10,
textEditorStateBustingKey: 0,
tstart: Math.floor(props.currentTime),
xywh: null,
...annoState,
};
......@@ -125,6 +127,10 @@ class AnnotationCreation extends Component {
this.updateBody = this.updateBody.bind(this);
this.updateTstart = this.updateTstart.bind(this);
this.updateTend = this.updateTend.bind(this);
this.setTstartNow = this.setTstartNow.bind(this);
this.setTendNow = this.setTendNow.bind(this);
this.seekToTstart = this.seekToTstart.bind(this);
this.seekToTend = this.seekToTend.bind(this);
this.updateGeometry = this.updateGeometry.bind(this);
this.changeTool = this.changeTool.bind(this);
this.changeClosedMode = this.changeClosedMode.bind(this);
......@@ -153,6 +159,40 @@ class AnnotationCreation extends Component {
});
}
/** set annotation start time to current time */
setTstartNow() { this.setState({ tstart: Math.floor(this.props.currentTime) }); }
/** set annotation end time to current time */
setTendNow() { this.setState({ tend: Math.floor(this.props.currentTime) }); }
/** update annotation start time */
updateTstart(value) { this.setState({ tstart: value }); }
/** update annotation end time */
updateTend(value) { this.setState({ tend: value }); }
/** seekTo/goto annotation start time */
seekToTstart() {
const { paused, setCurrentTime, setSeekTo } = this.props;
const { tstart } = this.state;
if (!paused) {
this.setState(setSeekTo(tstart));
} else {
this.setState(setCurrentTime(tstart));
}
}
/** seekTo/goto annotation end time */
seekToTend() {
const { paused, setCurrentTime, setSeekTo } = this.props;
const { tend } = this.state;
if (!paused) {
this.setState(setSeekTo(tend));
} else {
this.setState(setCurrentTime(tend));
}
}
/** */
openChooseColor(e) {
this.setState({
......@@ -200,7 +240,7 @@ class AnnotationCreation extends Component {
canvases.forEach((canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({
body: annoBody,
body: !annoBody.length ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody,
canvasId: canvas.id,
id: (annotation && annotation.id) || `${uuid()}`,
manifestId: canvas.options.resource.id,
......@@ -223,7 +263,9 @@ class AnnotationCreation extends Component {
this.setState({
annoBody: '',
svg: null,
tend: '',
textEditorStateBustingKey: textEditorStateBustingKey + 1,
tstart: '',
xywh: null,
});
}
......@@ -247,12 +289,6 @@ class AnnotationCreation extends Component {
this.setState({ annoBody });
}
/** update annotation start time */
updateTstart(ev) { this.setState({ tstart: ev.target.value }); }
/** update annotation end time */
updateTend(ev) { this.setState({ tend: ev.target.value }); }
/** */
updateGeometry({ svg, xywh }) {
this.setState({
......@@ -272,6 +308,8 @@ class AnnotationCreation extends Component {
tstart, tend, textEditorStateBustingKey,
} = this.state;
const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined';
return (
<CompanionWindow
title={annotation ? 'Edit annotation' : 'New annotation'}
......@@ -287,6 +325,7 @@ class AnnotationCreation extends Component {
svg={svg}
updateGeometry={this.updateGeometry}
windowId={windowId}
player={mediaIsVideo ? VideosReferences.get(windowId) : OSDReferences.get(windowId)}
/>
<form onSubmit={this.submitForm} className={classes.section}>
<Grid container>
......@@ -397,15 +436,33 @@ class AnnotationCreation extends Component {
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Duration
</Typography>
</Grid>
<Grid item xs={12}>
<input name="tstart" type="number" step="1" value={tstart} onChange={this.updateTstart} />
<input name="tend" type="number" step="1" value={tend} onChange={this.updateTend} />
</Grid>
{ mediaIsVideo && (
<>
<Grid item xs={12} onClick={this.seekToTstart}>
<IconButton size="small"><LastPage /></IconButton>
<Typography variant="overline">
Start
</Typography>
</Grid>
<Grid item xs={12} className={classes.paper}>
<IconButton onClick={this.setTstartNow}><Alarm /></IconButton>
<HMSInput seconds={tstart} onChange={this.updateTstart} />
</Grid>
<Grid item xs={12} onClick={this.seekToTend}>
<Typography variant="overline">
<IconButton size="small"><LastPage /></IconButton>
End
</Typography>
</Grid>
<Grid item xs={12} className={classes.paper}>
<IconButton onClick={this.setTendNow}><Alarm /></IconButton>
<HMSInput seconds={tend} onChange={this.updateTend} />
</Grid>
</>
)}
<Grid item xs={12}>
<Typography variant="overline">
Content
......@@ -505,13 +562,17 @@ AnnotationCreation.propTypes = {
adapter: PropTypes.func,
defaults: PropTypes.objectOf(
PropTypes.oneOfType(
[PropTypes.bool, PropTypes.func, PropTypes.number, PropTypes.string]
)
[PropTypes.bool, PropTypes.func, PropTypes.number, PropTypes.string],
),
),
}),
}).isRequired,
currentTime: PropTypes.number,
id: PropTypes.string.isRequired,
paused: PropTypes.bool,
receiveAnnotation: PropTypes.func.isRequired,
setCurrentTime: PropTypes.func,
setSeekTo: PropTypes.func,
windowId: PropTypes.string.isRequired,
};
......@@ -519,6 +580,10 @@ AnnotationCreation.defaultProps = {
annotation: null,
canvases: [],
closeCompanionWindow: () => {},
currentTime: 0,
paused: true,
setCurrentTime: () => {},
setSeekTo: () => {},
};
export default withStyles(styles)(AnnotationCreation);
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { IconButton, Input } from '@material-ui/core';
import { ArrowDownward, ArrowUpward } from '@material-ui/icons';
import { secondsToHMSarray } from './utils';
/** hh:mm:ss input which behave like a single input for parent */
class HMSInput extends Component {
/** Initialize state structure & bindings */
constructor(props) {
super(props);
// eslint-disable-next-line react/destructuring-assignment
const [h, m, s] = secondsToHMSarray(this.props.seconds);
this.state = {
hours: h,
minutes: m,
seconds: s,
};
this.someChange = this.someChange.bind(this);
this.addOneSec = this.addOneSec.bind(this);
this.subOneSec = this.subOneSec.bind(this);
}
/** update */
componentDidUpdate(prevProps) {
const { seconds } = this.props;
if (prevProps.seconds === seconds) return;
const [h, m, s] = secondsToHMSarray(seconds);
this.setState({
hours: h,
minutes: m,
seconds: s,
});
}
/** If one value is updated, tell the parent component the total seconds counts */
someChange(ev) {
const { onChange } = this.props;
const { state } = this;
state[ev.target.name] = Number(ev.target.value);
onChange(state.hours * 3600 + state.minutes * 60 + state.seconds);
}
/** Add one second by simulating an input change */
addOneSec() {
const { seconds } = this.state;
this.someChange({ target: { name: 'seconds', value: seconds + 1 } });
}
/** Substract one second by simulating an input change */
subOneSec() {
const { seconds } = this.state;
this.someChange({ target: { name: 'seconds', value: seconds - 1 } });
}
/** Render */
render() {
const { hours, minutes, seconds } = this.state;
const { classes } = this.props;
return (
<div className={classes.root}>
<div className={classes.root}>
<Input className={classes.input} name="hours" value={hours} onChange={this.someChange} />
<Input className={classes.input} name="minutes" value={minutes} onChange={this.someChange} />
<Input className={classes.input} name="seconds" value={seconds} onChange={this.someChange} />
</div>
<div className={classes.flexcol}>
<IconButton size="small" onClick={this.addOneSec}>
<ArrowUpward />
</IconButton>
<IconButton size="small" onClick={this.subOneSec}>
<ArrowDownward />
</IconButton>
</div>
</div>
);
}
}
/** */
const styles = (theme) => ({
root: {
alignItems: 'center',
display: 'flex',
justifyContent: 'end',
},
// eslint-disable-next-line sort-keys
flexcol: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
},
// eslint-disable-next-line sort-keys
input: {
height: 'fit-content',
margin: '2px',
textAlign: 'center',
width: '4ch',
},
});
HMSInput.propTypes = {
classes: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
seconds: PropTypes.number.isRequired,
};
HMSInput.defaultProps = {
};
export default withStyles(styles)(HMSInput);
import * as actions from 'mirador/dist/es/src/state/actions';
import { getCompanionWindow } from 'mirador/dist/es/src/state/selectors/companionWindows';
import { getWindowCurrentTime, getWindowPausedStatus } from 'mirador/dist/es/src/state/selectors/window';
import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases';
import AnnotationCreation from '../AnnotationCreation';
......@@ -11,11 +12,15 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({
receiveAnnotation: (targetId, annoId, annotation) => dispatch(
actions.receiveAnnotation(targetId, annoId, annotation),
),
setCurrentTime: (...args) => dispatch(actions.setWindowCurrentTime(windowId, ...args)),
setSeekTo: (...args) => dispatch(actions.setWindowSeekTo(windowId, ...args)),
});
/** */
function mapStateToProps(state, { id: companionWindowId, windowId }) {
const { annotationid } = getCompanionWindow(state, { companionWindowId, windowId });
const currentTime = getWindowCurrentTime(state, { windowId });
const cw = getCompanionWindow(state, { companionWindowId, windowId });
const { annotationid } = cw;
const canvases = getVisibleCanvases(state, { windowId });
let annotation = null;
......@@ -33,6 +38,8 @@ function mapStateToProps(state, { id: companionWindowId, windowId }) {
annotation,
canvases,
config: state.config,
currentTime,
paused: getWindowPausedStatus(state, { windowId }),
};
}
......
......@@ -7,3 +7,17 @@ export function mapChildren(layerThing) {
}
return layerThing;
}
/** Pretty print a seconds count into HH:mm:ss */
export function secondsToHMS(secs) {
const [h, m, s] = secondsToHMSarray(secs);
// eslint-disable-next-line require-jsdoc
const pad = (n) => (n < 10 ? `0${n}` : n);
return `${pad(h)}:${pad(m)}:${pad(s)}`;
}
/** Split a second to [hours, minutes, seconds] */
export function secondsToHMSarray(secs) {
const h = Math.floor(secs / 3600);
return [h, Math.floor(secs / 60) - h * 60, secs % 60];
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment