From d2cb17038b8537a609be06be2ed7013dbe27117e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 8 Mar 2021 22:11:55 +0100 Subject: beginning the BIG refactor. moving editor stuff into per-episode hierarchy --- .../app/views/editor/align/align.actions.js | 136 ++++++++++++++ .../app/views/editor/align/align.container.js | 27 +++ .../frontend/app/views/editor/align/align.css | 160 +++++++++++++++++ .../app/views/editor/align/align.reducer.js | 97 ++++++++++ .../components/annotations/annotation.form.css | 67 +++++++ .../components/annotations/annotation.form.js | 190 ++++++++++++++++++++ .../components/annotations/annotation.index.css | 125 +++++++++++++ .../components/annotations/annotation.index.js | 125 +++++++++++++ .../annotationForms/annotationForm.gallery.js | 132 ++++++++++++++ .../annotationForms/annotationForm.image.js | 120 +++++++++++++ .../annotationForms/annotationForm.text.js | 153 ++++++++++++++++ .../annotationForms/annotationForm.utility.js | 172 ++++++++++++++++++ .../annotationForms/annotationForm.video.js | 160 +++++++++++++++++ .../annotations/annotationForms/index.js | 46 +++++ .../annotationTypes/annotationTypes.gallery.js | 75 ++++++++ .../annotationTypes/annotationTypes.image.js | 54 ++++++ .../annotationTypes/annotationTypes.text.js | 121 +++++++++++++ .../annotationTypes/annotationTypes.utility.js | 107 +++++++++++ .../annotationTypes/annotationTypes.video.js | 51 ++++++ .../annotations/annotationTypes/index.js | 57 ++++++ .../components/player/playButton.component.js | 22 +++ .../align/components/sidebar/script.component.js | 23 +++ .../sidebar/tableOfContents.component.js | 30 ++++ .../align/components/timeline/cursor.component.js | 26 +++ .../components/timeline/cursorRegion.component.js | 28 +++ .../components/timeline/playCursor.component.js | 36 ++++ .../align/components/timeline/ticks.component.js | 88 +++++++++ .../components/timeline/waveform.component.js | 105 +++++++++++ .../align/containers/annotations.container.js | 28 +++ .../editor/align/containers/sidebar.container.js | 35 ++++ .../editor/align/containers/timeline.container.js | 196 +++++++++++++++++++++ 31 files changed, 2792 insertions(+) create mode 100644 animism-align/frontend/app/views/editor/align/align.actions.js create mode 100644 animism-align/frontend/app/views/editor/align/align.container.js create mode 100644 animism-align/frontend/app/views/editor/align/align.css create mode 100644 animism-align/frontend/app/views/editor/align/align.reducer.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.css create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.css create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.gallery.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.image.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.text.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.utility.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.video.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/index.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.gallery.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.image.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.text.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.utility.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.video.js create mode 100644 animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/index.js create mode 100644 animism-align/frontend/app/views/editor/align/components/player/playButton.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/sidebar/script.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/sidebar/tableOfContents.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/timeline/cursor.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/timeline/cursorRegion.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/timeline/playCursor.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/timeline/ticks.component.js create mode 100644 animism-align/frontend/app/views/editor/align/components/timeline/waveform.component.js create mode 100644 animism-align/frontend/app/views/editor/align/containers/annotations.container.js create mode 100644 animism-align/frontend/app/views/editor/align/containers/sidebar.container.js create mode 100644 animism-align/frontend/app/views/editor/align/containers/timeline.container.js (limited to 'animism-align/frontend/app/views/editor/align') diff --git a/animism-align/frontend/app/views/editor/align/align.actions.js b/animism-align/frontend/app/views/editor/align/align.actions.js new file mode 100644 index 0000000..1583e4e --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/align.actions.js @@ -0,0 +1,136 @@ +import * as types from 'app/types' +import { store } from 'app/store' +import actions from 'app/actions' +// import { session } from 'app/session' +import throttle from 'lodash.throttle' +import debounce from 'lodash.debounce' + +import { ZOOM_STEPS } from 'app/constants' +import { timestampToSeconds, post } from 'app/utils' +import { cutFirstSentence } from 'app/utils/align.utils' +import { annotationFadeTimings } from 'app/utils/annotation.utils' + +export const setScrollPosition = start_ts => dispatch => ( + dispatch({ type: types.align.set_display_setting, key: 'start_ts', value: start_ts }) +) + +export const setZoom = zoom => dispatch => { + if (0 <= zoom && zoom < ZOOM_STEPS.length) { + dispatch({ type: types.align.set_display_setting, key: 'zoom', value: zoom }) + } +} +export const throttledSetZoom = throttle(zoom => dispatch => { + setZoom(zoom)(dispatch) +}, 250, { leading: true }) + +export const setCursor = cursor_ts => dispatch => ( + dispatch({ type: types.align.set_display_setting, key: 'cursor_ts', value: cursor_ts }) +) +export const setCursorRegion = (a_ts, b_ts) => dispatch => ( + dispatch({ type: types.align.set_display_setting, key: 'cursor_region', value: { a_ts, b_ts } }) +) +export const clearCursorRegion = () => dispatch => ( + dispatch({ type: types.align.set_display_setting, key: 'cursor_region', value: null }) +) + +export const setSelectedAnnotation = annotation => dispatch => { + debouncedUpdateAnnotation.flush() + dispatch({ type: types.align.set_selected_annotation, data: annotation }) +} +export const clearSelectedAnnotation = () => dispatch => { + debouncedUpdateAnnotation.flush() + dispatch({ type: types.align.clear_selected_annotation }) +} +export const updateSelectedAnnotation = annotation => dispatch => { + debouncedUpdateAnnotation(annotation) + dispatch({ type: types.align.set_selected_annotation, data: { ...annotation } }) +} +export const debouncedUpdateAnnotation = debounce(annotation => { + console.log('updating annotation', annotation) + actions.annotation.update(annotation) +}, 2000, { leading: false, trailing: true }) + +export const cloneSelectedAnnotation = annotation => dispatch => { + const newAnnotation = { ...annotation } + delete newAnnotation.id + if (annotation.settings.fullscreen) { + const { + fadeInDuration, fadeOutDuration, duration, + start_ts, end_ts, fade_in_end_ts, fade_out_start_ts, + } = annotationFadeTimings(annotation) + newAnnotation.start_ts += duration - fadeOutDuration - fadeInDuration + } else { + newAnnotation.start_ts += 1 + } + actions.annotation.create(newAnnotation) + .then(res => { + console.log('cloned annotation', res.res) + setSelectedParagraph(res.res) + }) +} + +export const setSelectedParagraph = paragraph_id => dispatch => { + dispatch({ type: types.align.set_display_setting, key: 'selected_paragraph_id', value: paragraph_id }) +} +export const clearSelectedParagraph = () => dispatch => { + dispatch({ type: types.align.set_display_setting, key: 'selected_paragraph_id', value: -1 }) +} + +export const showNewAnnotationForm = (start_ts, text) => dispatch => { + let croppedText; + if (store.getState().align.annotation.start_ts) { + croppedText = store.getState().align.annotation.text + } else { + croppedText = cutFirstSentence(text) + } + // console.log(croppedText) + dispatch({ + type: types.align.set_temporary_annotation, + data: { + id: 'new', + start_ts, + end_ts: 0.0, + text: croppedText, + type: 'sentence', + settings: {}, + } + }) +} +export const showEditAnnotationForm = (annotation) => dispatch => { + dispatch({ + type: types.align.set_temporary_annotation, + data: annotation, + }) +} + +export const updateAnnotationForm = (key, value) => dispatch => { + dispatch({ type: types.align.update_temporary_annotation, key, value }) +} +export const updateAnnotationSettings = (key, value) => dispatch => { + dispatch({ type: types.align.update_temporary_annotation_settings, key, value }) +} + +export const hideAnnotationForm = () => dispatch => { + dispatch({ + type: types.align.set_temporary_annotation, + data: {} + }) +} + + +export const spliceTime = start_ts => dispatch => { + let duration = timestampToSeconds(prompt("How many seconds to add or remove? Enter a positive / negative number")) + if (!duration) { + return + } + console.log(start_ts, duration) + const data = { + start_ts, duration, + } + post(dispatch, types.api, 'splice', '/api/v1/annotation/splice', data) + .then(res => { + console.log(res) + alert(res.count + ' records updated!') + actions.annotation.index() + }) +} \ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/align.container.js b/animism-align/frontend/app/views/editor/align/align.container.js new file mode 100644 index 0000000..281fd35 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/align.container.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' + +import './align.css' +import './components/annotations/annotation.form.css' +import './components/annotations/annotation.index.css' + +import Timeline from 'app/views/editor/align/containers/timeline.container.js' +import Sidebar from 'app/views/editor/align/containers/sidebar.container.js' + +class Container extends Component { + componentDidMount() { + document.body.scrollTo(0, 0) + document.body.parentNode.scrollTo(0, 0) + } + render() { + return ( +
+
+ +
+ +
+ ) + } +} + +export default Container diff --git a/animism-align/frontend/app/views/editor/align/align.css b/animism-align/frontend/app/views/editor/align/align.css new file mode 100644 index 0000000..d10b601 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/align.css @@ -0,0 +1,160 @@ +* { + +} +.body.loading > div { + padding: 1rem; +} +.body { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + background: linear-gradient( + 0deg, + rgba(0, 0, 64, 0.5), + rgba(64, 64, 128, 0.5) + ); + padding: 0; +} + +.body.alignmentEditor { + height: calc(100% - 3rem); +} + +/* Timeline */ + +canvas { + display: block; +} +.timeline { + display: flex; + flex-direction: row; + position: relative; + width: 300px; + cursor: crosshair; +} +.timelineColumn { + position: relative; +} +.ticks .tick { + position: absolute; + right: 0; + width: 4px; + height: 1px; + background: #ddd; + pointer-events: none; +} +.ticks .tickLabel { + pointer-events: none; + position: absolute; + right: 6px; + font-size: 12px; + width: 40px; + margin-top: -7px; + text-align: right; + text-shadow: 0 0 2px #00f; +} +.timeline .cursor { + width: 100%; + position: absolute; + left: 0; + pointer-events: none; +} +.timeline .cursor .line { + width: 100%; + height: 1px; + background: #00f; +} +.timeline .cursor.playCursor .line { + background: #ddd; +} +.timeline .cursor .tickLabel { + position: absolute; + pointer-events: none; + right: 6px; + font-size: 12px; + width: 40px; + margin-top: -7px; + text-align: right; + text-shadow: 0 0 2px #000, 0 0 2px #000, 0 0 2px #000; + user-select: none; +} +.timeline .cursor_region { + position: absolute; + left: 0; + width: 100%; + border-top: 1px solid #33f; + border-bottom: 1px solid #33f; + background: rgba(32,64,255,0.2); +} +.timeline .cursor_region .tickLabel { + position: absolute; + top: 2px; + left: 6px; + user-select: none; +} + +/* Audio player */ + +.playButton { + /*position: absolute;*/ + /*top: 0; left: 0;*/ + width: 3rem; height: 3rem; + padding: 1rem; + background: transparent; + cursor: pointer; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} +.playButton.playing { + background-image: url('/static/img/icons_pause_white.svg'); +} +.playButton.paused { + background-image: url('/static/img/icons_play_white.svg'); +} + +/* Script */ + +.sidebar { + position: absolute; + top: 0; + right: 0; + min-width: 4rem; + z-index: 8; +} +.sidebar textarea { + height: calc(100vh - 3.15rem); +} +.sidebar button { + position: absolute; + top: 0.25rem; + right: 0.25rem; + z-index: 9; + border: 0; + background: transparent; +} + +/* Annotations */ + +.annotations { + position: relative; + width: 450px; +} + +/* table of contents */ + +.toc { + background: #222; + width: 15rem; + padding: 0.5rem 0; +} +.toc div { + width: 15rem; + padding: 0.25rem 0.75rem; + cursor: pointer; +} +.toc div:hover { + background: #213; +} \ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/align.reducer.js b/animism-align/frontend/app/views/editor/align/align.reducer.js new file mode 100644 index 0000000..d37cd6d --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/align.reducer.js @@ -0,0 +1,97 @@ +import * as types from 'app/types' +// import { session, getDefault, getDefaultInt } from 'app/session' + +const initialState = { + timeline: { + cursor_ts: -1, + cursor_region: null, + start_ts: 0, + zoom: 1, + duration: 0, + selected_annotation_id: -1, + selected_paragraph_id: -1, + }, + peaks: { loading: true }, + text: { loading: true }, + annotation: {}, + selectedAnnotation: {}, + options: { + }, +} + +export default function alignReducer(state = initialState, action) { + // console.log(action.type, action) + switch (action.type) { + case types.peaks.loaded: + // console.log('peaks duration:', action.data.length / 10) + return { + ...state, + peaks: action.data, + } + + case types.text.loaded: + return { + ...state, + text: action.data, + } + + case types.align.set_display_setting: + return { + ...state, + timeline: { + ...state.timeline, + [action.key]: action.value, + } + } + + case types.align.set_selected_annotation: + return { + ...state, + timeline: { + ...state.timeline, + selected_annotation_id: action.data.id, + }, + selectedAnnotation: action.data, + } + + case types.align.clear_selected_annotation: + return { + ...state, + timeline: { + ...state.timeline, + selected_annotation_id: -1, + }, + selectedAnnotation: {}, + } + + case types.align.set_temporary_annotation: + return { + ...state, + annotation: action.data, + } + + case types.align.update_temporary_annotation: + return { + ...state, + annotation: { + ...state.annotation, + [action.key]: action.value, + } + } + + case types.align.update_temporary_annotation_settings: + return { + ...state, + annotation: { + ...state.annotation, + settings: { + ...state.annotation.settings, + [action.key]: action.value, + } + } + } + + default: + return state + } +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.css b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.css new file mode 100644 index 0000000..fa663c7 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.css @@ -0,0 +1,67 @@ +/* Annotation form */ + +.annotationForm { + width: 401px; + padding: 0.5rem; + position: absolute; + left: 0.25rem; + background: #448; + box-shadow: 0 0 2px #000, 0 0 4px #000; + z-index: 10; +} +.annotationForm textarea { + width: 100%; +} +.annotationForm .row { + justify-content: space-between; + align-items: center; +} +.annotationForm .row > div { + display: flex; + align-items: center; +} +.annotationForm .buttons { + margin-bottom: 0.5rem; +} +.annotationForm .ts { + color: #fff; +} +.annotationForm .select.media_id { + width: 100%; + margin-right: 0; +} +.annotationForm div.textarea { + margin-bottom: 0.5rem; +} +.annotationForm img { + max-width: 100%; + max-height: 6rem; +} + +.annotationForm .options label span:first-child { + display: inline-block; + width: 6rem; +} +.annotationForm .options .description { + font-size: 0.75rem; + margin-top: 0.25rem; + margin-bottom: 0.5rem; +} +.annotationForm .color input[type="text"].number { + width: 8rem; +} +.annotationForm .color input[type="text"] { + width: 8rem; +} +.annotationForm .color input[type="color"] { + background: transparent; + border: 0; + height: 1.7rem; + padding: 0; + margin: 0 0.5rem 0 0; + width: 1.4rem; +} +.annotationForm .checkbox { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} \ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.js new file mode 100644 index 0000000..7b1918a --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.form.js @@ -0,0 +1,190 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import actions from 'app/actions' + +import { ANNOTATION_SELECT_OPTIONS, MEDIA_ANNOTATION_TYPES } from 'app/constants' +import { timestamp } from 'app/utils' +import { timeToPosition } from 'app/utils/align.utils' +import { Select } from 'app/common' + +import { annotationFormLookup } from './annotationForms' + +const annotationTextTypes = new Set([ + 'sentence', 'section_heading', 'heading_text', 'pullquote_credit', 'footnote', 'text_plate', +]) + +class AnnotationForm extends Component { + constructor(props){ + super(props) + this.handleChange = this.handleChange.bind(this) + this.handleSelect = this.handleSelect.bind(this) + this.handleSettingsChange = this.handleSettingsChange.bind(this) + this.handleSettingsSelect = this.handleSettingsSelect.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + this.handleDestroy = this.handleDestroy.bind(this) + this.textareaRef = React.createRef() + } + componentDidMount() { + if (this.textareaRef && this.textareaRef.current) { + this.textareaRef.current.focus() + } + } + handleKeyDown(e) { + if (e.keyCode === 27) { // escape + actions.align.hideAnnotationForm() + return + } + // console.log(e.keyCode) + if (!e.metaKey && !e.ctrlKey) return + let { start_ts } = this.props.annotation + switch (e.keyCode) { + case 38: // up + e.preventDefault() + start_ts -= 0.1 + actions.align.updateAnnotationForm('start_ts', Math.max(0, start_ts)) + actions.audio.seek(start_ts) + actions.align.setCursor(start_ts) + break + case 40: // down + e.preventDefault() + start_ts += 0.1 + actions.align.updateAnnotationForm('start_ts', Math.max(0, start_ts)) + actions.audio.seek(start_ts) + actions.align.setCursor(start_ts) + break + case 83: // ctrl-S + e.preventDefault() + this.handleSubmit() + default: + break + } + } + handleChange(e) { + const { name, value } = e.target + this.handleSelect(name, value) + } + handleSelect(name, value) { + actions.align.updateAnnotationForm(name, value) + } + handleSettingsChange(e) { + const { name, value } = e.target + this.handleSettingsSelect(name, value) + } + handleSettingsSelect(name, value) { + if (name.indexOf('_id') !== -1) value = parseInt(value) || 0 + actions.align.updateAnnotationSettings(name, value) + } + handleSubmit() { + const { annotation } = this.props + if (annotation.type === 'paragraph_end') { + annotation.text = '' + } + if (annotation.type in MEDIA_ANNOTATION_TYPES) { + if (!annotation.settings.media_id) return + annotation.text = '' + } + if (annotation.id === 'new') { + delete annotation.id + actions.annotation.create(annotation) + .then(response => { + console.log(response) + actions.align.hideAnnotationForm() + }) + } else { + actions.annotation.update(annotation) + .then(response => { + console.log(response) + actions.align.hideAnnotationForm() + }) + } + } + handleDestroy() { + const { annotation } = this.props + if (annotation.id === 'new') { + actions.align.hideAnnotationForm() + } else { + actions.annotation.destroy(annotation) + .then(response => { + console.log(response) + actions.align.hideAnnotationForm() + }) + } + } + render() { + const { timeline, annotation } = this.props + return ( +
+ {this.renderButtons()} + {annotationTextTypes.has(annotation.type) && this.renderTextarea()} + {(annotation.type in annotationFormLookup) && this.renderElementForm()} +
+ ) + } + renderButtons() { + const { annotation } = this.props + return ( +
+
+