From 95e5ece6522482c0028ffe895f4460ac513816ad Mon Sep 17 00:00:00 2001 From: 2SC1815J <2SC1815J@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:00:00 +0900 Subject: [PATCH] support for captions and auto-scrolling of annotation list with video playback --- src/components/AnnotationSettings.js | 39 +++++++--- src/components/AnnotationsOverlayVideo.js | 92 ++++++++++++++++++----- src/components/CanvasAnnotations.js | 6 +- src/components/VideoViewer.js | 54 ++++++++++++- src/components/ViewerNavigationVideo.js | 30 ++++++-- src/config/settings.js | 2 +- src/containers/AnnotationSettings.js | 5 ++ src/containers/AnnotationsOverlayVideo.js | 2 + src/containers/CanvasAnnotations.js | 2 + src/containers/VideoViewer.js | 7 +- src/containers/ViewerNavigationVideo.js | 8 ++ src/state/actions/action-types.js | 4 + src/state/actions/annotation.js | 12 +++ src/state/actions/window.js | 34 +++++++++ src/state/reducers/windows.js | 32 ++++++++ src/state/selectors/searches.js | 5 +- src/state/selectors/window.js | 33 ++++++++ 17 files changed, 325 insertions(+), 42 deletions(-) diff --git a/src/components/AnnotationSettings.js b/src/components/AnnotationSettings.js index 29ec4a843..9e764a4ce 100644 --- a/src/components/AnnotationSettings.js +++ b/src/components/AnnotationSettings.js @@ -1,5 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import SyncIcon from '@material-ui/icons/Sync'; +import SyncDisabledIcon from '@material-ui/icons/SyncDisabled'; import VisibilityIcon from '@material-ui/icons/VisibilitySharp'; import VisibilityOffIcon from '@material-ui/icons/VisibilityOffSharp'; import MiradorMenuButton from '../containers/MiradorMenuButton'; @@ -14,26 +16,45 @@ export class AnnotationSettings extends Component { */ render() { const { - displayAll, displayAllDisabled, t, toggleAnnotationDisplay, + autoScroll, autoScrollDisabled, + displayAll, displayAllDisabled, t, toggleAnnotationAutoScroll, toggleAnnotationDisplay, } = this.props; return ( - <MiradorMenuButton - aria-label={t(displayAll ? 'displayNoAnnotations' : 'highlightAllAnnotations')} - onClick={toggleAnnotationDisplay} - disabled={displayAllDisabled} - size="small" - > - { displayAll ? <VisibilityIcon /> : <VisibilityOffIcon /> } - </MiradorMenuButton> + <> + <MiradorMenuButton + aria-label={t(displayAll ? 'displayNoAnnotations' : 'highlightAllAnnotations')} + onClick={toggleAnnotationDisplay} + disabled={displayAllDisabled} + size="small" + > + { displayAll ? <VisibilityIcon /> : <VisibilityOffIcon /> } + </MiradorMenuButton> + <MiradorMenuButton + aria-label={autoScroll ? 'Disable auto scroll' : 'Enable auto scroll'} + onClick={toggleAnnotationAutoScroll} + disabled={autoScrollDisabled} + size="small" + > + { autoScroll ? <SyncIcon /> : <SyncDisabledIcon /> } + </MiradorMenuButton> + </> ); } } +AnnotationSettings.defaultProps = { + autoScroll: true, + autoScrollDisabled: true, + toggleAnnotationAutoScroll: () => {}, +}; AnnotationSettings.propTypes = { + autoScroll: PropTypes.bool, + autoScrollDisabled: PropTypes.bool, displayAll: PropTypes.bool.isRequired, displayAllDisabled: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, + toggleAnnotationAutoScroll: PropTypes.func, toggleAnnotationDisplay: PropTypes.func.isRequired, windowId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types }; diff --git a/src/components/AnnotationsOverlayVideo.js b/src/components/AnnotationsOverlayVideo.js index 4f66399cf..7e1a5f436 100755 --- a/src/components/AnnotationsOverlayVideo.js +++ b/src/components/AnnotationsOverlayVideo.js @@ -87,6 +87,7 @@ export class AnnotationsOverlayVideo extends Component { } else { this.temporalOffset = 0; } + this.currentTimeNearestAnnotationId = null; this.state = { showProgress: false, @@ -111,13 +112,14 @@ export class AnnotationsOverlayVideo extends Component { hoveredAnnotationIds, selectedAnnotationId, highlightAllAnnotations, paused, - setCurrentTime, - setPaused, + seekToTime, } = this.props; this.initializeViewer(); + let prevVideoPausedState; if (this.video) { + prevVideoPausedState = this.video.paused; if (this.video.paused && !paused) { const promise = this.video.play(); if (promise !== undefined) { @@ -126,6 +128,15 @@ export class AnnotationsOverlayVideo extends Component { } else if (!this.video.paused && paused) { this.video.pause(); } + if (seekToTime !== prevProps.seekToTime) { + if (seekToTime !== undefined) { + this.seekTo(seekToTime, true); + return; + } + } + if (this.video.seeking) { + return; + } if (currentTime !== prevProps.currentTime) { if (paused && this.video.paused) { this.video.currentTime = currentTime - this.temporalOffset; @@ -154,27 +165,58 @@ export class AnnotationsOverlayVideo extends Component { const selectedAnnotationsUpdated = selectedAnnotationId !== prevProps.selectedAnnotationId; if (selectedAnnotationsUpdated && selectedAnnotationId) { - setPaused(true); - this.video && this.video.pause(); + if (this.currentTimeNearestAnnotationId + && this.currentTimeNearestAnnotationId === selectedAnnotationId) { + // go through + } else { + annotations.forEach((annotation) => { + annotation.resources.forEach((resource) => { + if (resource.id !== selectedAnnotationId) return; + if (!canvasWorld.canvasIds.includes(resource.targetId)) return; + if (!AnnotationsOverlayVideo.isAnnotaionInTemporalSegment(resource, currentTime)) { + const temporalfragment = resource.temporalfragmentSelector; + if (temporalfragment && temporalfragment.length > 0 && this.video) { + const seekto = temporalfragment[0] || 0; + this.seekTo(seekto, !prevVideoPausedState); + } + } + }); + }); + } + } + + // auto scroll + if (this.video && !this.video.paused) { + let minElapsedTimeAfterStart = Number.MAX_VALUE; + let candidateAnnotation; annotations.forEach((annotation) => { annotation.resources.forEach((resource) => { - if (resource.id !== selectedAnnotationId) return; if (!canvasWorld.canvasIds.includes(resource.targetId)) return; - if (!AnnotationsOverlayVideo.isAnnotaionInTemporalSegment(resource, currentTime)) { + if (AnnotationsOverlayVideo.isAnnotaionInTemporalSegment(resource, currentTime)) { const temporalfragment = resource.temporalfragmentSelector; if (temporalfragment && temporalfragment.length > 0 && this.video) { const seekto = temporalfragment[0] || 0; - setPaused(true); - this.video.pause(); - setCurrentTime(seekto); - const videoTime = seekto - this.temporalOffset; - if (videoTime >= 0) { - this.video.currentTime = videoTime; + const elapsedTimeAfterStart = currentTime - seekto; + if (elapsedTimeAfterStart >= 0 && elapsedTimeAfterStart < minElapsedTimeAfterStart) { + minElapsedTimeAfterStart = elapsedTimeAfterStart; + candidateAnnotation = resource.resource; } } } }); }); + if (candidateAnnotation) { + if (candidateAnnotation.id !== prevProps.selectedAnnotationId) { + const { + selectAnnotation, + windowId, + } = this.props; + if (selectedAnnotationId !== candidateAnnotation.id) { + selectAnnotation(windowId, candidateAnnotation.id); + } + this.currentTimeNearestAnnotationId = candidateAnnotation.id; + } + } } const redrawAnnotations = drawAnnotations !== prevProps.drawAnnotations @@ -230,17 +272,12 @@ export class AnnotationsOverlayVideo extends Component { /** */ onVideoPlaying(event) { if (this.video && this.video.currentTime !== 0) { - const { currentTime } = this.props; + const { currentTime, seekToTime } = this.props; const currentTimeToVideoTime = currentTime - this.temporalOffset; const diff = Math.abs(currentTimeToVideoTime - this.video.currentTime); const acceptableDiff = 1; // sec. - if (diff > acceptableDiff) { - const { setCurrentTime, setPaused } = this.props; - // In the flow of pausing, adjusting currentTime, and resuming playback, - // it is necessary to handle cases where the user explicitly pauses playback. - setPaused(true); - setCurrentTime(this.video.currentTime + this.temporalOffset); // rewind time - setPaused(false); + if (diff > acceptableDiff && seekToTime === undefined) { + this.seekTo(this.video.currentTime + this.temporalOffset, true); } } this.setState({ showProgress: false }); @@ -397,6 +434,19 @@ export class AnnotationsOverlayVideo extends Component { return imageSource; } + /** @private */ + seekTo(seekTo, resume) { + const { setCurrentTime, setPaused } = this.props; + setPaused(true); + setCurrentTime(seekTo); + this.video.addEventListener('seeked', function seeked(event) { + event.currentTarget.removeEventListener(event.type, seeked); + if (resume) { + setPaused(false); + } + }); + } + /** @private */ isCanvasSizeSpecified() { const { canvas, canvasWorld } = this.props; @@ -600,6 +650,7 @@ AnnotationsOverlayVideo.defaultProps = { palette: {}, paused: true, searchAnnotations: [], + seekToTime: undefined, selectAnnotation: () => {}, selectedAnnotationId: undefined, setCurrentTime: () => {}, @@ -621,6 +672,7 @@ AnnotationsOverlayVideo.propTypes = { palette: PropTypes.object, // eslint-disable-line react/forbid-prop-types paused: PropTypes.bool, searchAnnotations: PropTypes.arrayOf(PropTypes.object), + seekToTime: PropTypes.number, selectAnnotation: PropTypes.func, selectedAnnotationId: PropTypes.string, setCurrentTime: PropTypes.func, diff --git a/src/components/CanvasAnnotations.js b/src/components/CanvasAnnotations.js index 137a4ef0b..2a7efadb5 100644 --- a/src/components/CanvasAnnotations.js +++ b/src/components/CanvasAnnotations.js @@ -58,7 +58,7 @@ export class CanvasAnnotations extends Component { */ render() { const { - annotations, classes, index, label, selectedAnnotationId, t, totalSize, + annotations, autoScroll, classes, index, label, selectedAnnotationId, t, totalSize, listContainerComponent, htmlSanitizationRuleSet, hoveredAnnotationIds, containerRef, } = this.props; @@ -76,7 +76,7 @@ export class CanvasAnnotations extends Component { containerRef={containerRef} key={`${annotation.id}-scroll`} offsetTop={96} // offset for the height of the form above - scrollTo={selectedAnnotationId === annotation.id} + scrollTo={autoScroll ? (selectedAnnotationId === annotation.id) : false} > <MenuItem button @@ -126,6 +126,7 @@ CanvasAnnotations.propTypes = { id: PropTypes.string.isRequired, }), ), + autoScroll: PropTypes.bool, classes: PropTypes.objectOf(PropTypes.string), containerRef: PropTypes.oneOfType([ PropTypes.func, @@ -146,6 +147,7 @@ CanvasAnnotations.propTypes = { }; CanvasAnnotations.defaultProps = { annotations: [], + autoScroll: true, classes: {}, containerRef: undefined, hoveredAnnotationIds: [], diff --git a/src/components/VideoViewer.js b/src/components/VideoViewer.js index 4c285507f..d13ebbdaf 100644 --- a/src/components/VideoViewer.js +++ b/src/components/VideoViewer.js @@ -19,11 +19,28 @@ export class VideoViewer extends Component { }; } + /** */ + componentDidMount() { + const { annotations, setHasTextTrack, setPaused } = this.props; + setPaused(true); + const vttContent = flatten( + flattenDeep([ + annotations.map(annotation => annotation.resources.map( + resources_ => resources_.resource, + )), + ]).filter(resource => resource.body && resource.body[0] && resource.body[0].format === 'text/vtt'), + ); + if (vttContent && vttContent.length > 0) { + setHasTextTrack(true); + } + } + /** */ componentDidUpdate(prevProps) { const { canvas, currentTime, muted, paused, setCurrentTime, setPaused, + textTrackDisabled, } = this.props; if (paused !== prevProps.paused) { @@ -51,9 +68,20 @@ export class VideoViewer extends Component { if (video.muted !== muted) { video.muted = muted; } + if (video.textTracks && video.textTracks.length > 0) { + const newMode = textTrackDisabled ? 'hidden' : 'showing'; + if (video.textTracks[0].mode !== newMode) { + video.textTracks[0].mode = newMode; + } + } } } + /** */ + componentWillUnmount() { + this.timerStop(); + } + /** */ timerStart() { const { currentTime } = this.props; @@ -85,7 +113,7 @@ export class VideoViewer extends Component { /** */ render() { const { - canvas, classes, currentTime, videoOptions, windowId, + annotations, canvas, classes, currentTime, videoOptions, windowId, } = this.props; const videoResources = flatten( @@ -107,13 +135,26 @@ export class VideoViewer extends Component { }), ]).filter((resource) => resource.body && resource.body[0].__jsonld && resource.body[0].__jsonld.type === 'Video'), ); + const vttContent = flatten( + flattenDeep([ + annotations.map(annotation => annotation.resources.map( + resources_ => resources_.resource, + )), + ]).filter(resource => resource.body && resource.body[0] && resource.body[0].format === 'text/vtt'), + ); + // Only one video can be displayed at a time in this implementation. const len = videoResources.length; const video = len > 0 ? videoResources[len - 1].body[0] : null; const videoTargetTemporalfragment = len > 0 ? videoResources[len - 1].temporalfragment : []; - + let caption = null; + if (vttContent && vttContent.length > 0) { + caption = { + id: vttContent[0].body[0].id, + }; + } return ( <div className={classes.flexContainer}> <div className={classes.flexFill}> @@ -121,6 +162,9 @@ export class VideoViewer extends Component { <> <video className={classes.video} key={video.id} ref={this.videoRef} {...videoOptions}> <source src={video.id} type={video.getFormat()} /> + { caption && ( + <track src={caption.id} /> + )} </video> <AnnotationsOverlayVideo windowId={windowId} videoRef={this.videoRef} videoTarget={videoTargetTemporalfragment} key={`${windowId} ${video.id}`} /> </> @@ -134,23 +178,29 @@ export class VideoViewer extends Component { } VideoViewer.propTypes = { + annotations: PropTypes.arrayOf(PropTypes.object), canvas: PropTypes.object, // eslint-disable-line react/forbid-prop-types classes: PropTypes.objectOf(PropTypes.string).isRequired, currentTime: PropTypes.number, muted: PropTypes.bool, paused: PropTypes.bool, setCurrentTime: PropTypes.func, + setHasTextTrack: PropTypes.func, setPaused: PropTypes.func, + textTrackDisabled: PropTypes.bool, videoOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types windowId: PropTypes.string.isRequired, }; VideoViewer.defaultProps = { + annotations: [], canvas: {}, currentTime: 0, muted: false, paused: true, setCurrentTime: () => {}, + setHasTextTrack: () => {}, setPaused: () => {}, + textTrackDisabled: true, videoOptions: {}, }; diff --git a/src/components/ViewerNavigationVideo.js b/src/components/ViewerNavigationVideo.js index 47355af9c..0542fe7f6 100755 --- a/src/components/ViewerNavigationVideo.js +++ b/src/components/ViewerNavigationVideo.js @@ -1,3 +1,5 @@ +import ClosedCaption from '@material-ui/icons/ClosedCaption'; +import ClosedCaptionOutlined from '@material-ui/icons/ClosedCaptionOutlined'; import React, { Component } from 'react'; import PauseRoundedIcon from '@material-ui/icons/PauseRounded'; import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded'; @@ -19,13 +21,9 @@ export class ViewerNavigationVideo extends Component { /** */ handleChange = (event, newValue) => { - const { paused, setCurrentTime, setPaused } = this.props; + const { paused, setCurrentTime, setSeekTo } = this.props; if (!paused) { - // In the flow of pausing, adjusting currentTime, and resuming playback, - // it is necessary to handle cases where the user explicitly pauses playback. - setPaused(true); - setCurrentTime(newValue); - setPaused(false); + setSeekTo(newValue); } else { setCurrentTime(newValue); } @@ -39,10 +37,13 @@ export class ViewerNavigationVideo extends Component { classes, currentTime, duration, + hasTextTrack, muted, paused, setMuted, setPaused, + setTextTrackDisabled, + textTrackDisabled, } = this.props; const start = (duration > 3600 || duration === undefined) ? 11 : 14; @@ -79,6 +80,15 @@ export class ViewerNavigationVideo extends Component { > { muted ? <VolumeOffIcon /> : <VolumeUpIcon /> } </MiradorMenuButton> + { hasTextTrack && ( + <MiradorMenuButton + aria-label={textTrackDisabled ? 'CC show' : 'CC hide'} + className={textTrackDisabled ? ns('next-canvas-button') : ns('next-canvas-button')} + onClick={() => { setTextTrackDisabled(!textTrackDisabled); }} + > + { textTrackDisabled ? <ClosedCaptionOutlined /> : <ClosedCaption /> } + </MiradorMenuButton> + )} <span className={classes.divider} /> </div> ); @@ -89,19 +99,27 @@ ViewerNavigationVideo.propTypes = { classes: PropTypes.objectOf(PropTypes.string).isRequired, currentTime: PropTypes.number, duration: PropTypes.number, + hasTextTrack: PropTypes.bool, muted: PropTypes.bool, paused: PropTypes.bool, setCurrentTime: PropTypes.func, setMuted: PropTypes.func, setPaused: PropTypes.func, + setSeekTo: PropTypes.func, + setTextTrackDisabled: PropTypes.func, + textTrackDisabled: PropTypes.bool, }; ViewerNavigationVideo.defaultProps = { currentTime: 0, duration: undefined, + hasTextTrack: false, muted: false, paused: true, setCurrentTime: () => {}, setMuted: () => {}, setPaused: () => {}, + setSeekTo: () => {}, + setTextTrackDisabled: () => {}, + textTrackDisabled: true, }; diff --git a/src/config/settings.js b/src/config/settings.js index 50e2bc752..e2b1025f0 100644 --- a/src/config/settings.js +++ b/src/config/settings.js @@ -360,7 +360,7 @@ export default { crossOrigin: 'anonymous', }, videoOptions: { // Additional props passed to <audio> element - controls: true, + controls: false, crossOrigin: 'anonymous', }, auth: { diff --git a/src/containers/AnnotationSettings.js b/src/containers/AnnotationSettings.js index 1c1dd8d0c..ff5b8ea37 100644 --- a/src/containers/AnnotationSettings.js +++ b/src/containers/AnnotationSettings.js @@ -13,6 +13,8 @@ import { AnnotationSettings } from '../components/AnnotationSettings'; * Mapping redux state to component props using connect */ const mapStateToProps = (state, { windowId }) => ({ + autoScroll: getWindow(state, { windowId }).autoScrollAnnotationList, + autoScrollDisabled: getAnnotationResourcesByMotivation(state, { windowId }).length < 2, displayAll: getWindow(state, { windowId }).highlightAllAnnotations, displayAllDisabled: getAnnotationResourcesByMotivation( state, @@ -24,6 +26,9 @@ const mapStateToProps = (state, { windowId }) => ({ * Mapping redux action dispatches to component props using connect */ const mapDispatchToProps = (dispatch, { windowId }) => ({ + toggleAnnotationAutoScroll: () => { + dispatch(actions.toggleAnnotationAutoScroll(windowId)); + }, toggleAnnotationDisplay: () => { dispatch(actions.toggleAnnotationDisplay(windowId)); }, diff --git a/src/containers/AnnotationsOverlayVideo.js b/src/containers/AnnotationsOverlayVideo.js index 5840561d5..bc32f71b8 100755 --- a/src/containers/AnnotationsOverlayVideo.js +++ b/src/containers/AnnotationsOverlayVideo.js @@ -13,6 +13,7 @@ import { getConfig, getCurrentCanvas, getWindowCurrentTime, + getWindowSeekToTime, getWindowPausedStatus, getPresentAnnotationsOnSelectedCanvases, getSelectedAnnotationId, @@ -39,6 +40,7 @@ const mapStateToProps = (state, { windowId }) => ({ state, { windowId }, ), + seekToTime: getWindowSeekToTime(state, { windowId }), selectedAnnotationId: getSelectedAnnotationId(state, { windowId }), }); diff --git a/src/containers/CanvasAnnotations.js b/src/containers/CanvasAnnotations.js index 7c9caa748..cdd6caaa0 100644 --- a/src/containers/CanvasAnnotations.js +++ b/src/containers/CanvasAnnotations.js @@ -9,6 +9,7 @@ import { getCanvasLabel, getSelectedAnnotationId, getConfig, + getWindow, } from '../state/selectors'; import { CanvasAnnotations } from '../components/CanvasAnnotations'; @@ -32,6 +33,7 @@ const mapStateToProps = (state, { canvasId, windowId }) => ({ state, { canvasId, windowId }, ), ), + autoScroll: getWindow(state, { windowId }).autoScrollAnnotationList, htmlSanitizationRuleSet: getConfig(state).annotations.htmlSanitizationRuleSet, label: getCanvasLabel(state, { canvasId, diff --git a/src/containers/VideoViewer.js b/src/containers/VideoViewer.js index bbd148c85..07919ecc1 100644 --- a/src/containers/VideoViewer.js +++ b/src/containers/VideoViewer.js @@ -12,21 +12,26 @@ import { getWindowMutedStatus, getWindowPausedStatus, getWindowCurrentTime, + getWindowTextTrackDisabledStatus, + getPresentAnnotationsOnSelectedCanvases, } from '../state/selectors'; /** */ const mapStateToProps = (state, { windowId }) => ({ + annotations: getPresentAnnotationsOnSelectedCanvases(state, { windowId }), canvas: (getCurrentCanvas(state, { windowId }) || {}), canvasWorld: getCurrentCanvasWorld(state, { windowId }), currentTime: getWindowCurrentTime(state, { windowId }), muted: getWindowMutedStatus(state, { windowId }), paused: getWindowPausedStatus(state, { windowId }), + textTrackDisabled: getWindowTextTrackDisabledStatus(state, { windowId }), videoOptions: getConfig(state).videoOptions, }); /** */ const mapDispatchToProps = (dispatch, { windowId }) => ({ setCurrentTime: (...args) => dispatch(actions.setWindowCurrentTime(windowId, ...args)), + setHasTextTrack: (...args) => dispatch(actions.setWindowHasTextTrack(windowId, ...args)), setPaused: (...args) => dispatch(actions.setWindowPaused(windowId, ...args)), }); @@ -46,7 +51,7 @@ const styles = () => ({ height: '100%', maxHeight: '100%', maxWidth: '100%', - 'object-fit': 'scale-down', + 'object-fit': 'contain', // 'scale-down', 'object-position': 'left top', width: '100%', }, diff --git a/src/containers/ViewerNavigationVideo.js b/src/containers/ViewerNavigationVideo.js index 8f411ae3e..5d894b0d9 100755 --- a/src/containers/ViewerNavigationVideo.js +++ b/src/containers/ViewerNavigationVideo.js @@ -10,14 +10,18 @@ import { getWindowCurrentTime, getWindowMutedStatus, getWindowPausedStatus, + getWindowTextTrackDisabledStatus, + getWindowHasTextTrack, } from '../state/selectors'; /** */ const mapStateToProps = (state, { windowId }) => ({ currentTime: getWindowCurrentTime(state, { windowId }), duration: getCurrentCanvasDuration(state, { windowId }), + hasTextTrack: getWindowHasTextTrack(state, { windowId }), muted: getWindowMutedStatus(state, { windowId }), paused: getWindowPausedStatus(state, { windowId }), + textTrackDisabled: getWindowTextTrackDisabledStatus(state, { windowId }), }); /** @@ -29,6 +33,10 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ setCurrentTime: (...args) => dispatch(actions.setWindowCurrentTime(windowId, ...args)), setMuted: (...args) => dispatch(actions.setWindowMuted(windowId, ...args)), setPaused: (...args) => dispatch(actions.setWindowPaused(windowId, ...args)), + setSeekTo: (...args) => dispatch(actions.setWindowSeekTo(windowId, ...args)), + setTextTrackDisabled: (...args) => dispatch( + actions.setWindowTextTrackDisabled(windowId, ...args), + ), }); const styles = { diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index 9c610f53b..f234ac6ba 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -13,6 +13,7 @@ const ActionTypes = { DESELECT_ANNOTATION: 'mirador/DESELECT_ANNOTATION', SELECT_ANNOTATION: 'mirador/SELECT_ANNOTATION', TOGGLE_ANNOTATION_DISPLAY: 'mirador/TOGGLE_ANNOTATION_DISPLAY', + TOGGLE_ANNOTATION_AUTOSCROLL: 'mirador/TOGGLE_ANNOTATION_AUTOSCROLL', FOCUS_WINDOW: 'mirador/FOCUS_WINDOW', SET_WORKSPACE_FULLSCREEN: 'mirador/SET_WORKSPACE_FULLSCREEN', @@ -74,8 +75,11 @@ const ActionTypes = { HIDE_COLLECTION_DIALOG: 'mirador/HIDE_COLLECTION_DIALOG', SET_CURRENT_TIME: 'mirador/SET_CURRENT_TIME', + SET_SEEK_TO_TIME: 'mirador/SET_SEEK_TO_TIME', SET_VIDEO_PAUSED: 'mirador/SET_VIDEO_PAUSED', SET_VIDEO_MUTED: 'mirador/SET_VIDEO_MUTED', + SET_VIDEO_TEXTTRACK_DISABLED: 'mirador/SET_VIDEO_TEXTTRACK_DISABLED', + SET_VIDEO_HAS_TEXTTRACK: 'mirador/SET_VIDEO_HAS_TEXTTRACK', }; export default ActionTypes; diff --git a/src/state/actions/annotation.js b/src/state/actions/annotation.js index 50d780d5a..cc860edef 100644 --- a/src/state/actions/annotation.js +++ b/src/state/actions/annotation.js @@ -107,6 +107,18 @@ export function toggleAnnotationDisplay(windowId) { }; } +/** + * toggleAnnotationAutoScroll - action creator + * + * @param {String} windowId + * @memberof ActionCreators + */ +export function toggleAnnotationAutoScroll(windowId) { + return { + type: ActionTypes.TOGGLE_ANNOTATION_AUTOSCROLL, windowId, + }; +} + /** * toggleAnnotationDisplay - action creator * diff --git a/src/state/actions/window.js b/src/state/actions/window.js index c0e63ee91..2609358ab 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -61,6 +61,7 @@ export function addWindow({ companionWindows, manifest, ...options }) { } const defaultOptions = { + autoScrollAnnotationList: true, canvasId: undefined, collectionIndex: 0, companionAreaOpen: true, @@ -221,6 +222,17 @@ export function setWindowCurrentTime(windowId, currentTime) { }); } +/** */ +export function setWindowSeekTo(windowId, seekToTime) { + return ((dispatch) => { + dispatch({ + seekToTime, + type: ActionTypes.SET_SEEK_TO_TIME, + windowId, + }); + }); +} + /** */ export function setWindowPaused(windowId, paused) { return ((dispatch) => { @@ -242,3 +254,25 @@ export function setWindowMuted(windowId, muted) { }); }); } + +/** */ +export function setWindowTextTrackDisabled(windowId, disabled) { + return ((dispatch) => { + dispatch({ + textTrackDisabled: (disabled === undefined) ? true : disabled, + type: ActionTypes.SET_VIDEO_TEXTTRACK_DISABLED, + windowId, + }); + }); +} + +/** */ +export function setWindowHasTextTrack(windowId, hasTextTrack) { + return ((dispatch) => { + dispatch({ + hasTextTrack: (hasTextTrack === undefined) ? false : hasTextTrack, + type: ActionTypes.SET_VIDEO_HAS_TEXTTRACK, + windowId, + }); + }); +} diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js index 3734acdba..10644ee99 100644 --- a/src/state/reducers/windows.js +++ b/src/state/reducers/windows.js @@ -141,6 +141,14 @@ export const windowsReducer = (state = {}, action) => { highlightAllAnnotations: !state[action.windowId].highlightAllAnnotations, }, }; + case ActionTypes.TOGGLE_ANNOTATION_AUTOSCROLL: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + autoScrollAnnotationList: !state[action.windowId].autoScrollAnnotationList, + }, + }; case ActionTypes.IMPORT_MIRADOR_STATE: return action.state.windows || []; case ActionTypes.REQUEST_SEARCH: @@ -177,6 +185,14 @@ export const windowsReducer = (state = {}, action) => { currentTime: action.currentTime, }, }; + case ActionTypes.SET_SEEK_TO_TIME: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + seekToTime: action.seekToTime, + }, + }; case ActionTypes.SET_VIDEO_PAUSED: return { ...state, @@ -193,6 +209,22 @@ export const windowsReducer = (state = {}, action) => { muted: !!action.muted, }, }; + case ActionTypes.SET_VIDEO_TEXTTRACK_DISABLED: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + textTrackDisabled: !!action.textTrackDisabled, + }, + }; + case ActionTypes.SET_VIDEO_HAS_TEXTTRACK: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + hasTextTrack: !!action.hasTextTrack, + }, + }; default: return state; } diff --git a/src/state/selectors/searches.js b/src/state/selectors/searches.js index 3e541084e..a1cac26ae 100644 --- a/src/state/selectors/searches.js +++ b/src/state/selectors/searches.js @@ -69,7 +69,10 @@ export const getSearchNumTotal = createSelector( && result.json && result.json.within )); - return resultWithWithin?.json?.within?.total; + if (resultWithWithin && resultWithWithin.json && resultWithWithin.json.within) { + return resultWithWithin.json.within.total; + } + return undefined; }, ); diff --git a/src/state/selectors/window.js b/src/state/selectors/window.js index ebf7082a6..62a526c9d 100755 --- a/src/state/selectors/window.js +++ b/src/state/selectors/window.js @@ -12,6 +12,17 @@ export const getWindowCurrentTime = createSelector( }, ); +export const getWindowSeekToTime = createSelector( + [ + getWindow, + ], + (window) => { + if (!window) return undefined; + + return window.seekToTime; + }, +); + export const getWindowPausedStatus = createSelector( [ getWindow, @@ -33,3 +44,25 @@ export const getWindowMutedStatus = createSelector( return window.muted; }, ); + +export const getWindowTextTrackDisabledStatus = createSelector( + [ + getWindow, + ], + (window) => { + if (!window) return undefined; + + return window.textTrackDisabled; + }, +); + +export const getWindowHasTextTrack = createSelector( + [ + getWindow, + ], + (window) => { + if (!window) return undefined; + + return window.hasTextTrack; + }, +); -- GitLab