From 39cfd69a0a8198ee02d769254b7dd6d77577fffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Poujade?= <lois.poujade@tetras-libre.fr> Date: Tue, 20 Dec 2022 15:08:00 +0100 Subject: [PATCH] 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 --- src/AnnotationCreation.js | 121 ++++++++++++++---- src/HMSInput.js | 114 +++++++++++++++++ .../annotationCreationCompanionWindow.js | 9 +- src/utils.js | 14 ++ 4 files changed, 229 insertions(+), 29 deletions(-) create mode 100644 src/HMSInput.js diff --git a/src/AnnotationCreation.js b/src/AnnotationCreation.js index 6bb0b42..e7ecb7f 100644 --- a/src/AnnotationCreation.js +++ b/src/AnnotationCreation.js @@ -1,9 +1,11 @@ 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); diff --git a/src/HMSInput.js b/src/HMSInput.js new file mode 100644 index 0000000..57f7f12 --- /dev/null +++ b/src/HMSInput.js @@ -0,0 +1,114 @@ +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); diff --git a/src/plugins/annotationCreationCompanionWindow.js b/src/plugins/annotationCreationCompanionWindow.js index 8977598..5a1d537 100644 --- a/src/plugins/annotationCreationCompanionWindow.js +++ b/src/plugins/annotationCreationCompanionWindow.js @@ -1,5 +1,6 @@ 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 }), }; } diff --git a/src/utils.js b/src/utils.js index 43e6940..4984a53 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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]; +} -- GitLab