diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2021-03-08 22:11:55 +0100 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2021-03-08 22:11:55 +0100 |
| commit | d2cb17038b8537a609be06be2ed7013dbe27117e (patch) | |
| tree | 028ceac9edddafc03ce80c49d5a05981bec3fcbe /animism-align/frontend/app/views/editor/align/components | |
| parent | b5ceb782f40fc1e402d1e58bc1ced2e4038fd787 (diff) | |
beginning the BIG refactor. moving editor stuff into per-episode hierarchy
Diffstat (limited to 'animism-align/frontend/app/views/editor/align/components')
24 files changed, 2113 insertions, 0 deletions
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 ( + <div + className='annotationForm' + style={{ + top: timeToPosition(annotation.start_ts, timeline), + }} + > + {this.renderButtons()} + {annotationTextTypes.has(annotation.type) && this.renderTextarea()} + {(annotation.type in annotationFormLookup) && this.renderElementForm()} + </div> + ) + } + renderButtons() { + const { annotation } = this.props + return ( + <div className='row buttons'> + <div> + <Select + name='type' + selected={annotation.type} + options={ANNOTATION_SELECT_OPTIONS} + defaultOption='text' + onChange={this.handleSelect} + /> + <div className='ts'>{timestamp(annotation.start_ts, 1, true)}</div> + </div> + <div> + {annotation.id !== 'new' && <button onClick={this.handleDestroy}>Delete</button>} + <button onClick={this.handleSubmit}>Save</button> + </div> + </div> + ) + } + renderTextarea() { + const { annotation } = this.props + return ( + <div className='textarea'> + <textarea + name='text' + value={annotation.text} + onKeyDown={this.handleKeyDown} + onChange={this.handleChange} + ref={this.textareaRef} + /> + </div> + ) + } + renderElementForm() { + const { annotation, media } = this.props + const AnnotationFormElement = annotationFormLookup[annotation.type] + return ( + <AnnotationFormElement + annotation={annotation} + media={media} + handleSettingsChange={this.handleSettingsChange} + handleSettingsSelect={this.handleSettingsSelect} + /> + ) + } +} + +const mapStateToProps = state => ({ + annotation: state.align.annotation, + timeline: state.align.timeline, + media: state.media.index, +}) + +const mapDispatchToProps = dispatch => ({ +}) + +export default connect(mapStateToProps, mapDispatchToProps)(AnnotationForm) diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.css b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.css new file mode 100644 index 0000000..afedbde --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.css @@ -0,0 +1,125 @@ +/* Annotation index */ + +.annotationIndex { + width: 800px; +} +.annotationIndex .annotation { + position: absolute; + left: 5px; + max-width: 300px; + padding: 0.25rem 0.375rem; + box-shadow: 0px 0px 3px rgba(0,0,0,1.0); + border: 1px solid transparent; + border-radius: 2px; + font-size: 12px; + cursor: pointer; + user-select: none; + background-color: #768; +} +.annotation.selected { + border-color: #bbf; + box-shadow: 0px 0px 4px rgba(0,0,0,1.0), 0px 0px 2px rgba(0,0,0,1.0); + z-index: 2; + background-image: linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.4)); +} +.annotationIndex .annotation.media { + width: 300px; + left: calc(405px + 0.5rem); +} +.annotationIndex .annotation.sentence.even { + background-color: #83b; +} +.annotationIndex .annotation.sentence.odd { + background-color: #537; +} +.annotationIndex .annotation.section_heading { + background-color: #983; + z-index: 1; +} +.annotationIndex .annotation.heading_text { + background-color: #838; +} +.annotationIndex .annotation.paragraph_end { + background-color: #003; + border-top: 1px solid #888; + width: 300px; + padding: 1px; +} +.annotationIndex .annotation.utility { + left: calc(505px + 0.5rem); + width: 200px; +} +.annotationIndex .annotation.footnote { + left: calc(605px + 0.5rem); + width: 200px; +} +.annotationIndex .annotation.text_plate { + left: calc(605px + 0.5rem); + width: 250px; +} +.annotationIndex .annotation.utility.curtain { + background-image: linear-gradient(rgba(255,255,255,1.0), rgba(255,255,255,1.0)); + width: 15rem; + padding: 0; + overflow: hidden; +} +.annotationIndex .annotation.utility.curtain span { + position: absolute; + top: 0; left: 0; + z-index: 1; + color: black; + text-shadow: 0 0 3px #fff, 0 0 2px #fff, 0 0 1px #fff; + padding: 0.25rem; +} +.annotationIndex .annotation.utility.curtain .fadeIn { + z-index: 0; + position: absolute; + top: 0; + width: 100%; + background-image: linear-gradient(rgba(0,0,0,1.0), rgba(255,255,255,1.0)); + background-size: cover; +} +.annotationIndex .annotation.utility.curtain .fadeOut { + z-index: 0; + position: absolute; + bottom: 0; + width: 100%; + background-image: linear-gradient(rgba(255,255,255,1.0), rgba(0,0,0,1.0)); + background-size: cover; +} + +/* Condensed layout (first lines) */ + +.annotationIndex.condensed .annotation.sentence { + z-index: 0; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; +} +.annotationIndex.condensed .annotation.section_heading { + z-index: 2; +} +.annotationIndex.condensed .annotation.heading_text { + z-index: 1; +} +.annotationIndex.condensed .annotation.paragraph_end { + border-top-color: #888; +} + +/* Collapsed layout (borders) */ + +.annotationIndex.collapsed .annotation.sentence { + height: 2px; overflow: hidden; padding: 0; z-index: 0; +} +.annotationIndex.collapsed .annotation.sentence.selected { + z-index: 4; +} +.annotationIndex.collapsed .annotation.section_heading { + z-index: 3; +} +.annotationIndex.collapsed .annotation.heading_text { + z-index: 2; +} +.annotationIndex.collapsed .annotation.paragraph_end { + border-top-color: #333; +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.js new file mode 100644 index 0000000..8e1fa43 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotation.index.js @@ -0,0 +1,125 @@ +import React, { PureComponent } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import actions from 'app/actions' + +import { ZOOM_STEPS, INNER_HEIGHT } from 'app/constants' +import { clamp } from 'app/utils' +import { positionToTime, timeToPosition } from 'app/utils/align.utils' + +import { AnnotationElementLookup } from './annotationTypes' + +class AnnotationIndex extends PureComponent { + state = { + items: [], + } + constructor(props){ + super(props) + this.handleClick = this.handleClick.bind(this) + } + componentDidUpdate(prevProps) { + if (this.props.index.loading) return + if (prevProps.timeline !== this.props.timeline || prevProps.index !== this.props.index) { + this.update() + } + } + update() { + let { timeline, index } = this.props + let { start_ts, zoom, duration } = this.props.timeline + const { order, lookup } = index + + let secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 // 0.1 sec / step + let widthTimeDuration = INNER_HEIGHT * secondsPerPixel // secs per pixel + + let timeMin = start_ts - 60.0 + let timeMax = Math.min(start_ts + widthTimeDuration, duration) + + const items = order.filter(id => { + const { start_ts: ts } = lookup[id] + return (timeMin < ts && ts < timeMax) + }).map(id => lookup[id]) + this.setState({ items }) + } + handleClick(e, annotation) { + e.stopPropagation() + if (!annotation) return + if (e.shiftKey) { + e.preventDefault() + this.handleParagraphSelection(annotation, e.metaKey) + } + actions.audio.seek(annotation.start_ts) + actions.align.setSelectedAnnotation(annotation) + } + handleParagraphSelection(annotation, shouldClear) { + const { selected_paragraph_id } = this.props.timeline + if (!selected_paragraph_id || selected_paragraph_id === -1 || shouldClear) { + if (annotation.paragraph_id && !shouldClear) { + actions.align.setSelectedParagraph(annotation.paragraph_id) + } else { + actions.paragraph.create({ + type: 'paragraph', + start_ts: annotation.start_ts, + }).then(data => { + actions.align.setSelectedParagraph(data.res.id) + annotation.paragraph_id = data.res.id + actions.annotation.update(annotation) + }) + } + } else if (selected_paragraph_id !== annotation.paragraph_id) { + annotation.paragraph_id = selected_paragraph_id + actions.annotation.update(annotation) + } + } + handleDoubleClick(e, annotation) { + e.stopPropagation() + actions.align.showEditAnnotationForm(annotation) + } + render() { + const { timeline, media, annotationInForm, selectedAnnotation } = this.props + const { start_ts, zoom, selected_annotation_id } = timeline + const { items } = this.state + const className = (zoom < 2) + ? 'annotationIndex' + : (zoom < 3) + ? 'annotationIndex condensed' + : 'annotationIndex collapsed' + return ( + <div className={className}> + {items.map(annotation => { + if (annotationInForm && annotation.id === annotationInForm.id) { + return null + } + if (annotation.id === selected_annotation_id) { + annotation = selectedAnnotation + } + const { id, type, start_ts } = annotation + const AnnotationElement = AnnotationElementLookup[type] + const y = timeToPosition(start_ts, timeline) + return ( + <AnnotationElement + key={id} + y={y} + selected={annotation.id === selected_annotation_id} + annotation={annotation} + media={media} + timeline={timeline} + onClick={this.handleClick} + onDoubleClick={this.handleDoubleClick} + /> + ) + })} + </div> + ) + } +} + +const mapStateToProps = state => ({ + timeline: state.align.timeline, + annotationInForm: state.align.annotation, + selectedAnnotation: state.align.selectedAnnotation, + index: state.annotation.index, + media: state.media.index.lookup, +}) + +export default connect(mapStateToProps)(AnnotationIndex) diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.gallery.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.gallery.js new file mode 100644 index 0000000..0a8b3fb --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.gallery.js @@ -0,0 +1,132 @@ +import React, { Component } from 'react' + +import { CURTAIN_COLOR_SELECT_OPTIONS } from 'app/constants' +import { TextInput, Select, Checkbox, LabelDescription } from 'app/common' +import { AnnotationFormFullscreen } from './annotationForm.utility' +import { makeMediaItems, makeGalleryItems } from 'app/utils/annotation.utils' + +export const AnnotationFormGallery = ({ annotation, media, handleSettingsSelect, handleSettingsChange }) => { + if (!media.lookup) return <div /> + const image_list_items = makeMediaItems(media, ['gallery']) + return ( + <div className='options'> + <Select + name='media_id' + className="media_id" + selected={annotation.settings.media_id} + options={image_list_items} + defaultOption='Choose a gallery' + onChange={handleSettingsSelect} + /> + + <TextInput + title="Title" + name="title" + placeholder="Enter title or leave blank" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + + <Checkbox + label="Fullscreen" + name="fullscreen" + checked={annotation.settings.fullscreen} + onChange={handleSettingsSelect} + /> + + <Checkbox + label="Inline" + name="inline" + checked={annotation.settings.inline} + onChange={handleSettingsSelect} + /> + + <Checkbox + label="Hide in transcript" + name="hide_in_transcript" + checked={annotation.settings.hide_in_transcript} + onChange={handleSettingsSelect} + /> + + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + + <Select + title='Arrow Color' + name='arrow_color' + selected={annotation.settings.arrow_color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Color of UI arrows' + onChange={handleSettingsSelect} + /> + + {(annotation.settings.fullscreen && !annotation.settings.inline) && ( + <AnnotationFormFullscreen + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + )} + </div> + ) +} + +export const AnnotationFormGalleryAdvance = ({ annotation, media, handleSettingsSelect, handleSettingsChange }) => { + if (!media.lookup) return <div /> + const image_list_items = makeMediaItems(media, ['gallery']) + const { gallery_items, thumbnail } = makeGalleryItems(annotation, media) + return ( + <div className='options'> + <Select + name='media_id' + className="media_id" + selected={annotation.settings.media_id} + options={image_list_items} + defaultOption='Choose a gallery' + onChange={handleSettingsSelect} + /> + + {gallery_items && ( + <Select + name='frame_index' + selected={annotation.settings.frame_index} + options={gallery_items} + defaultOption='Choose an image' + onChange={handleSettingsSelect} + /> + )} + + {thumbnail && ( + <img src={thumbnail.url} /> + )} + + <Checkbox + label="Advance half a frame forward to fit two images in view" + name="half_frame" + checked={annotation.settings.half_frame} + onChange={handleSettingsSelect} + /> + + <Checkbox + label="Hide in transcript" + name="hide_in_transcript" + checked={annotation.settings.hide_in_transcript} + onChange={handleSettingsSelect} + /> + + <Checkbox + label="Show in checklist" + name="show_in_checklist" + checked={annotation.settings.show_in_checklist} + onChange={handleSettingsSelect} + /> + </div> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.image.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.image.js new file mode 100644 index 0000000..3de2ac5 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.image.js @@ -0,0 +1,120 @@ +import React, { Component } from 'react' + +import { CURTAIN_COLOR_SELECT_OPTIONS, IMAGE_INLINE_SIZE_OPTIONS } from 'app/constants' +import { Select, Checkbox, TextInput } from 'app/common' +import { AnnotationFormFullscreen } from './annotationForm.utility' +import { makeMediaItems, makeGalleryItems } from 'app/utils/annotation.utils' + +export const AnnotationFormImage = ({ annotation, media, handleSettingsSelect, handleSettingsChange }) => { + if (!media.lookup) return <div /> + const image_list_items = makeMediaItems(media, ['image', 'gallery']) + const { gallery_items, thumbnail } = makeGalleryItems(annotation, media) + console.log(annotation) + return ( + <div className='options'> + <Select + name='media_id' + className="media_id" + selected={annotation.settings.media_id} + options={image_list_items} + defaultOption='Choose an image' + onChange={handleSettingsSelect} + /> + + {gallery_items && ( + <Select + name='frame_index' + selected={annotation.settings.frame_index} + options={gallery_items} + defaultOption='Choose an image' + onChange={handleSettingsSelect} + /> + )} + + {thumbnail && ( + <img src={thumbnail.url} /> + )} + + <Checkbox + label="Fullscreen" + name="fullscreen" + checked={annotation.settings.fullscreen} + onChange={handleSettingsSelect} + /> + + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + + {(annotation.settings.fullscreen) && ( + <div> + <Checkbox + label="Hide inline image" + name="hide_poster_inline" + checked={annotation.settings.hide_poster_inline} + onChange={handleSettingsSelect} + /> + <Select + title='Inline size' + name='inline_size' + selected={annotation.settings.inline_size} + options={IMAGE_INLINE_SIZE_OPTIONS} + defaultOption='Pick a size' + onChange={handleSettingsSelect} + /> + <Checkbox + label="Hide speaker icon" + name="hide_speaker_icon" + checked={annotation.settings.hide_speaker_icon} + onChange={handleSettingsSelect} + /> + <Select + title='Inline background color' + name='inline_color' + selected={annotation.settings.inline_color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + </div> + )} + + <Checkbox + label="Hide caption" + name="hide_caption" + checked={annotation.settings.hide_caption} + onChange={handleSettingsSelect} + /> + + <Checkbox + label="Hide in transcript" + name="hide_in_transcript" + checked={annotation.settings.hide_in_transcript} + onChange={handleSettingsSelect} + /> + + <TextInput + title="Override start time" + name="override_start_ts" + className="number" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + + {(annotation.settings.fullscreen && !annotation.settings.inline) && ( + <AnnotationFormFullscreen + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + )} + </div> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.text.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.text.js new file mode 100644 index 0000000..8bc7675 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.text.js @@ -0,0 +1,153 @@ +import React, { Component } from 'react' + +import { + CURTAIN_COLOR_SELECT_OPTIONS, + CURTAIN_STYLE_SELECT_OPTIONS, + BLACK_WHITE_SELECT_OPTIONS, + IMAGE_BACKGROUND_SIZE_OPTIONS, +} from 'app/constants' +import { Select, Checkbox, TextInput, LabelDescription } from 'app/common' +import { AnnotationFormFullscreen } from './annotationForm.utility' +import { makeMediaItems } from 'app/utils/annotation.utils' + +export const AnnotationFormSectionHeading = ({ annotation, media, handleSettingsSelect, handleSettingsChange }) => { + const image_list_items = makeMediaItems(media, ['image', 'video', 'gallery']) + return ( + <div className='options'> + <Checkbox + label="Hidden" + name="hidden" + checked={annotation.settings.hidden} + onChange={handleSettingsSelect} + /> + <Select + title='Background Color' + name='color' + selected={annotation.settings.color} + options={BLACK_WHITE_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + <Select + title='Cover image' + name='media_id' + className="media_id" + selected={annotation.settings.media_id} + options={image_list_items} + defaultOption='Choose an image' + onChange={handleSettingsSelect} + /> + <Select + title='Cover style' + name='cover_style' + className="cover_style" + selected={annotation.settings.cover_style} + options={IMAGE_BACKGROUND_SIZE_OPTIONS} + defaultOption='Cover image caption style' + onChange={handleSettingsSelect} + /> + <LabelDescription> + {'Background color for section'} + </LabelDescription> + <Select + title='Transition Color' + name='transition_color' + selected={annotation.settings.transition_color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + <LabelDescription> + {'Player will fade from this color when section begins'} + </LabelDescription> + {!annotation.settings.no_audio && + <div> + <Select + title='Section Nav Color' + name='section_nav_color' + selected={annotation.settings.section_nav_color} + options={BLACK_WHITE_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + <LabelDescription> + {'Section nav thumbnail icon color'} + </LabelDescription> + </div> + } + <Checkbox + label="Section does not have audio" + name="no_audio" + checked={annotation.settings.no_audio} + onChange={handleSettingsSelect} + /> + <LabelDescription> + {'Check if this is a text-only section'} + </LabelDescription> + </div> + ) +} + +export const AnnotationFormTextPlate = ({ annotation, handleSettingsSelect, handleSettingsChange }) => { + return ( + <div className='options'> + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + <Select + title='Curtain style' + name='transition_color' + selected={annotation.settings.transition_color} + options={CURTAIN_STYLE_SELECT_OPTIONS} + defaultOption='Pick a style' + onChange={handleSettingsSelect} + /> + <AnnotationFormFullscreen + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + </div> + ) +} + +export const AnnotationFormSubtitle = ({ annotation, handleSettingsSelect, handleSettingsChange }) => { + return ( + <div className='options'> + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + + <AnnotationFormFullscreen + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + </div> + ) +} + +export const AnnotationFormFootnote = ({ annotation, handleSettingsSelect, handleSettingsChange }) => { + return ( + <div className='options'> + <TextInput + title="Actual Timestamp" + name="actual_ts" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + </div> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.utility.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.utility.js new file mode 100644 index 0000000..7e823cc --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.utility.js @@ -0,0 +1,172 @@ +import React, { Component } from 'react' + +import { timestamp } from 'app/utils' +import { TextInput, LabelDescription, Select, Checkbox } from 'app/common' +import { CURTAIN_COLOR_SELECT_OPTIONS, CURTAIN_STYLE_SELECT_OPTIONS } from 'app/constants' +import { annotationFadeTimings } from 'app/utils/annotation.utils' +import { makeMediaItems } from 'app/utils/annotation.utils' + +export const AnnotationFormIntro = ({ annotation, media, handleSettingsChange, handleSettingsSelect }) => { + if (!media.lookup) return <div /> + const image_list_items = makeMediaItems(media, 'file') + return ( + <div className='options'> + <Select + name='media_id' + className="media_id" + selected={annotation.settings.media_id} + options={image_list_items} + defaultOption='Choose a file' + onChange={handleSettingsSelect} + /> + + <TextInput + title="Title" + name="title" + placeholder="Enter title or leave blank" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + + <TextInput + title="Subtitle" + name="subtitle" + placeholder="Enter subtitle or leave blank" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + + <TextInput + title="Actual start time" + name="intro_start_ts" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + + <LabelDescription> + {'Timestamp where voiceover starts, after any intro sound effect.'} + </LabelDescription> + + <AnnotationFormFullscreen + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + </div> + ) +} + +export const AnnotationFormCurtain = ({ annotation, handleSettingsChange, handleSettingsSelect }) => { + return ( + <div className='options'> + <TextInput + title="Curtain text" + name="curtain_text" + placeholder="Enter text or leave blank" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + + <Select + title='Style' + name='curtain_style' + selected={annotation.settings.curtain_style} + options={CURTAIN_STYLE_SELECT_OPTIONS} + defaultOption='Pick a style' + onChange={handleSettingsSelect} + /> + + <Checkbox + label="Contains flashing light" + name="flashing_light_warning" + checked={annotation.settings.flashing_light_warning} + onChange={handleSettingsSelect} + /> + + <AnnotationFormFullscreen + alwaysAccessible + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + </div> + ) +} + +export const AnnotationFormFullscreen = ({ annotation, handleSettingsChange, handleSettingsSelect }) => { + const { + fadeInDuration, fadeOutDuration, duration, + start_ts, end_ts, fade_in_end_ts, fade_out_start_ts, + } = annotationFadeTimings(annotation) + return ( + <div> + <TextInput + title="Total duration" + name="duration" + className="number" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + <LabelDescription> + {duration} + {' seconds, ends at '} + {timestamp(end_ts)} + </LabelDescription> + + <TextInput + title="Fade in duration" + name="fade_in_duration" + className="number" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + <LabelDescription> + {fadeInDuration} + {' seconds, ends at '} + {timestamp(fade_in_end_ts)} + </LabelDescription> + + <TextInput + title="Fade out duration" + name="fade_out_duration" + className="number" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + <LabelDescription> + {fadeOutDuration} + {' seconds, starts at '} + {timestamp(fade_out_start_ts)} + </LabelDescription> + </div> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.video.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.video.js new file mode 100644 index 0000000..dd5f640 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/annotationForm.video.js @@ -0,0 +1,160 @@ +import React, { Component } from 'react' + +import { CURTAIN_COLOR_SELECT_OPTIONS, IMAGE_BACKGROUND_SIZE_OPTIONS } from 'app/constants' +import { Select, Checkbox, TextInput, LabelDescription } from 'app/common' +import { AnnotationFormFullscreen } from './annotationForm.utility' +import { makeMediaItems } from 'app/utils/annotation.utils' + +export const AnnotationFormVideo = ({ annotation, media, handleSettingsSelect, handleSettingsChange }) => { + if (!media.lookup) return <div /> + const video_list_items = makeMediaItems(media, ['video']) + return ( + <div className='options'> + <Select + name='media_id' + className="media_id" + selected={annotation.settings.media_id} + options={video_list_items} + defaultOption='Choose a video' + onChange={handleSettingsSelect} + /> + <Checkbox + label="Autoplay" + name="autoplay" + checked={annotation.settings.autoplay} + onChange={handleSettingsSelect} + /> + <Checkbox + label="Fullscreen" + name="fullscreen" + checked={annotation.settings.fullscreen} + onChange={handleSettingsSelect} + /> + <Checkbox + label="Inline" + name="inline" + checked={annotation.settings.inline} + onChange={handleSettingsSelect} + /> + <Checkbox + label="Loop" + name="loop" + checked={annotation.settings.loop} + onChange={handleSettingsSelect} + /> + <Checkbox + label="Unmute" + name="unmuted" + checked={annotation.settings.unmuted} + onChange={handleSettingsSelect} + /> + {annotation.settings.inline && ( + <Checkbox + label="Hide Controls" + name="hide_controls" + checked={annotation.settings.hide_controls} + onChange={handleSettingsSelect} + /> + )} + {annotation.settings.inline && ( + <Checkbox + label="Show poster image" + name="poster" + checked={annotation.settings.poster} + onChange={handleSettingsSelect} + /> + )} + {(annotation.settings.fullscreen && !annotation.settings.inline) && ( + <Checkbox + label="Hide inline video poster" + name="hide_poster_inline" + checked={annotation.settings.hide_poster_inline} + onChange={handleSettingsSelect} + /> + )} + {(annotation.settings.fullscreen && !annotation.settings.inline) && ( + <Checkbox + label="Can play full video at end of section" + name="can_play_full_video" + checked={annotation.settings.can_play_full_video} + onChange={handleSettingsSelect} + /> + )} + <Checkbox + label="Hide in transcript" + name="hide_in_transcript" + checked={annotation.settings.hide_in_transcript} + onChange={handleSettingsSelect} + /> + <Select + title='Color' + name='color' + selected={annotation.settings.color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + <Select + title='Poster Size' + name='poster_size' + selected={annotation.settings.poster_size} + options={IMAGE_BACKGROUND_SIZE_OPTIONS} + defaultOption='Select size' + onChange={handleSettingsSelect} + /> + <Select + title='Poster background color' + name='poster_background_color' + selected={annotation.settings.poster_background_color} + options={CURTAIN_COLOR_SELECT_OPTIONS} + defaultOption='Pick a color' + onChange={handleSettingsSelect} + /> + <TextInput + title="Video start time" + name="video_start_ts" + className="number" + placeholder="0:00" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + <LabelDescription> + {'Auto-advances the video to this point when starting'} + </LabelDescription> + + {(annotation.settings.fullscreen && !annotation.settings.inline) && ( + <AnnotationFormFullscreen + annotation={annotation} + handleSettingsChange={handleSettingsChange} + handleSettingsSelect={handleSettingsSelect} + /> + )} + </div> + ) +} + +export const AnnotationFormVideoSetVolume = ({ annotation, handleSettingsChange }) => { + return ( + <div className='options'> + <TextInput + title="Volume" + name="volume" + className="number" + placeholder="1.0" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + <TextInput + title="Fade Time" + name="duration" + className="number" + placeholder="0:01" + data={annotation.settings} + onChange={handleSettingsChange} + autoComplete="off" + /> + </div> + ) +}
\ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/index.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/index.js new file mode 100644 index 0000000..8a1be48 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationForms/index.js @@ -0,0 +1,46 @@ +import { + AnnotationFormSectionHeading, + AnnotationFormTextPlate, + AnnotationFormFootnote, + AnnotationFormSubtitle, +} from './annotationForm.text' + +import { + AnnotationFormVideo, + AnnotationFormVideoSetVolume, +} from './annotationForm.video' + +import { + AnnotationFormImage, +} from './annotationForm.image' + +import { + AnnotationFormGallery, + AnnotationFormGalleryAdvance, +} from './annotationForm.gallery' + +import { + AnnotationFormCurtain, + AnnotationFormIntro, +} from './annotationForm.utility' + +export const annotationFormLookup = { + section_heading: AnnotationFormSectionHeading, + text_plate: AnnotationFormTextPlate, + footnote: AnnotationFormFootnote, + subtitle: AnnotationFormSubtitle, + + image: AnnotationFormImage, + video: AnnotationFormVideo, + video_set_volume: AnnotationFormVideoSetVolume, + + gallery: AnnotationFormGallery, + carousel: AnnotationFormGallery, + grid: AnnotationFormGallery, + vitrine: AnnotationFormGallery, + + gallery_advance: AnnotationFormGalleryAdvance, + + intro: AnnotationFormIntro, + curtain: AnnotationFormCurtain, +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.gallery.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.gallery.js new file mode 100644 index 0000000..c7df7f8 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.gallery.js @@ -0,0 +1,75 @@ +import React, { Component } from 'react' + +import { durationToHeight } from 'app/utils/align.utils' +import { capitalize } from 'app/utils' +import { annotationFadeTimings } from 'app/utils/annotation.utils' + +import { checkAnnotationMediaNotReady, AnnotationMediaLoading } from './annotationTypes.utility' + +export const AnnotationGallery = ({ y, annotation, media, timeline, selected, onClick, onDoubleClick }) => { + const { text } = annotation + const className = selected ? 'annotation media gallery selected' : 'annotation media gallery' + if (checkAnnotationMediaNotReady(annotation, media)) { + return <AnnotationMediaLoading y={y} annotation={annotation} className={className} onClick={onClick} onDoubleClick={onDoubleClick} /> + } + const mediaItem = media[annotation.settings.media_id] + + const { + fadeInDuration, fadeOutDuration, duration, + start_ts, end_ts, fade_in_end_ts, fade_out_start_ts, + } = annotationFadeTimings(annotation) + const durationHeight = annotation.settings.fullscreen ? durationToHeight(duration, timeline) : 'auto' + const fadeInHeight = durationToHeight(fadeInDuration, timeline) + const fadeOutHeight = durationToHeight(fadeOutDuration, timeline) + + const style = { + top: y, + } + if (annotation.settings.fullscreen && !annotation.settings.inline) { + style.height = durationHeight + } + + return ( + <div + className={className} + style={style} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + <div className='meta center'> + <div> + {capitalize(annotation.type)}<br/> + <i>{mediaItem.title}</i><br /> + {mediaItem.author}<br /> + {mediaItem.date} + </div> + </div> + </div> + ) +} + + +export const AnnotationGalleryAdvance = ({ y, annotation, media, timeline, selected, onClick, onDoubleClick }) => { + const className = selected ? 'annotation media gallery_advance selected' : 'annotation media gallery_advance' + const style = { + top: y, + } + let index = parseInt(annotation.settings.frame_index) + 1 + if (annotation.settings.half_frame) { + index += 0.5 + } + return ( + <div + className={className} + style={style} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + <div className='meta center'> + <div> + Advance gallery to frame {index} + </div> + </div> + </div> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.image.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.image.js new file mode 100644 index 0000000..d0a573e --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.image.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react' + +import { durationToHeight } from 'app/utils/align.utils' +import { annotationFadeTimings, thumbnailURL } from 'app/utils/annotation.utils' + +import { checkAnnotationMediaNotReady, AnnotationMediaLoading } from './annotationTypes.utility' + +export const AnnotationImage = ({ y, annotation, media, timeline, selected, onClick, onDoubleClick }) => { + const { text } = annotation + const className = selected ? 'annotation media image selected' : 'annotation media image' + if (checkAnnotationMediaNotReady(annotation, media)) { + return <AnnotationMediaLoading y={y} annotation={annotation} className={className} onClick={onClick} onDoubleClick={onDoubleClick} /> + } + const mediaItem = media[annotation.settings.media_id] + + const { + fadeInDuration, fadeOutDuration, duration, + start_ts, end_ts, fade_in_end_ts, fade_out_start_ts, + } = annotationFadeTimings(annotation) + const durationHeight = annotation.settings.fullscreen ? durationToHeight(duration, timeline) : 'auto' + const fadeInHeight = durationToHeight(fadeInDuration, timeline) + const fadeOutHeight = durationToHeight(fadeOutDuration, timeline) + + const style = { + top: y, + } + if (annotation.settings.fullscreen && !annotation.settings.inline) { + style.height = durationHeight + } + + return ( + <div + className={className} + style={style} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + <div className='meta center'> + <div> + <i>{mediaItem.title}</i>{' - '}{mediaItem.author} + {mediaItem.type === 'gallery' && ( + ' [Frame ' + (1 + parseInt(annotation.settings.frame_index)) + ']' + )} + </div> + </div> + </div> + ) +} + +/* + <div className='img'> + <img src={thumbnailURL(mediaItem)} alt={mediaItem.title} /> + </div> +*/
\ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.text.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.text.js new file mode 100644 index 0000000..ea65610 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.text.js @@ -0,0 +1,121 @@ +import React, { Component } from 'react' + +export const AnnotationSentence = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text, paragraph_id } = annotation + let className = !paragraph_id + ? 'annotation sentence' + : (paragraph_id % 2) + ? 'annotation sentence odd' + : 'annotation sentence even' + if (selected) className += ' selected' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + dangerouslySetInnerHTML={{ __html: text }} + /> + ) +} + +export const AnnotationHeadingText = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text } = annotation + const className = selected ? 'annotation heading_text selected' : 'annotation heading_text' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + {text} + </div> + ) +} + +export const AnnotationSectionHeading = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text } = annotation + const className = selected ? 'annotation section_heading selected' : 'annotation section_heading' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + {text} + </div> + ) +} + +export const AnnotationParagraphEnd = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text } = annotation + const className = selected ? 'annotation paragraph_end selected' : 'annotation paragraph_end' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + </div> + ) +} + +export const AnnotationFootnote = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text, paragraph_id } = annotation + let className = !paragraph_id + ? 'annotation sentence footnote' + : (paragraph_id % 2) + ? 'annotation sentence footnote odd' + : 'annotation sentence footnote even' + if (selected) className += ' selected' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + dangerouslySetInnerHTML={{ __html: text }} + /> + ) +} + +export const AnnotationTextPlate = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text, paragraph_id } = annotation + let className = !paragraph_id + ? 'annotation text_plate' + : (paragraph_id % 2) + ? 'annotation text_plate odd' + : 'annotation text_plate even' + if (selected) className += ' selected' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + dangerouslySetInnerHTML={{ __html: text }} + /> + ) +} + +export const AnnotationSubtitle = ({ y, annotation, selected, onClick, onDoubleClick }) => { + const { start_ts, text, paragraph_id } = annotation + let className = !paragraph_id + ? 'annotation subtitle' + : (paragraph_id % 2) + ? 'annotation subtitle odd' + : 'annotation subtitle even' + if (selected) className += ' selected' + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + dangerouslySetInnerHTML={{ __html: text }} + /> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.utility.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.utility.js new file mode 100644 index 0000000..1238163 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.utility.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react' + +import { annotationFadeTimings } from 'app/utils/annotation.utils' +import { durationToHeight } from 'app/utils/align.utils' + +export const AnnotationIntro = ({ y, annotation, timeline, selected, onClick, onDoubleClick }) => { + const className = selected ? 'annotation utility intro selected' : 'annotation utility intro' + const { + fadeInDuration, fadeOutDuration, duration, + start_ts, end_ts, fade_in_end_ts, fade_out_start_ts, + } = annotationFadeTimings(annotation) + const durationHeight = durationToHeight(duration, timeline) + const fadeInHeight = durationToHeight(fadeInDuration, timeline) + const fadeOutHeight = durationToHeight(fadeOutDuration, timeline) + let style = { + top: y, + } + if (annotation.settings.fullscreen) { + style.height = durationHeight + } + return ( + <div + className={className} + style={style} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + <div style={{ height: fadeInHeight }} className='fadeIn' /> + <div style={{ height: fadeOutHeight }} className='fadeOut' /> + <span style={{ top: fadeInHeight }}> + Intro:<br/> + {annotation.settings.title}<br /> + {annotation.settings.subtitle}<br /> + </span> + </div> + ) +} + +export const AnnotationSchedule = ({ y, annotation, timeline, selected, onClick, onDoubleClick }) => { + const className = selected ? 'annotation utility schedule selected' : 'annotation utility schedule' + let style = { + top: y, + } + return ( + <div + className={className} + style={style} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + Schedule + </div> + ) +} + +export const AnnotationCurtain = ({ y, annotation, timeline, selected, onClick, onDoubleClick }) => { + const className = selected ? 'annotation utility curtain selected' : 'annotation utility curtain' + const { + fadeInDuration, fadeOutDuration, duration, + start_ts, end_ts, fade_in_end_ts, fade_out_start_ts, + } = annotationFadeTimings(annotation) + const durationHeight = durationToHeight(duration, timeline) + const fadeInHeight = durationToHeight(fadeInDuration, timeline) + const fadeOutHeight = durationToHeight(fadeOutDuration, timeline) + return ( + <div + className={className} + style={{ top: y, height: durationHeight }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + <div style={{ height: fadeInHeight }} className='fadeIn' /> + <div style={{ height: fadeOutHeight }} className='fadeOut' /> + <span style={{ top: fadeInHeight }}> + Curtain: {annotation.settings.color}. + {annotation.settings.curtain_text} + </span> + </div> + ) +} + +export const checkAnnotationMediaNotReady = (annotation, media) => { + return (!media) || (!(annotation.settings.media_id in media)) +} + +export const AnnotationMediaLoading = ({ y, annotation, media, className, onClick, onDoubleClick }) => { + if (!media) { + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + >MEDIA NOT SET</div> + ) + } + if (!(annotation.settings.media_id in media)) { + return ( + <div + className={className} + style={{ top: y }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + >MEDIA NOT FOUND</div> + ) + } +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.video.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.video.js new file mode 100644 index 0000000..4ea9595 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/annotationTypes.video.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react' + +import { annotationFadeTimings } from 'app/utils/annotation.utils' +import { durationToHeight } from 'app/utils/align.utils' +import { checkAnnotationMediaNotReady, AnnotationMediaLoading } from './annotationTypes.utility' + +export const AnnotationVideo = ({ y, annotation, media, timeline, selected, onClick, onDoubleClick }) => { + const { start_ts, text } = annotation + const className = selected ? 'annotation media video selected' : 'annotation media video' + if (checkAnnotationMediaNotReady(annotation, media)) { + return <AnnotationMediaLoading y={y} annotation={annotation} className={className} onClick={onClick} onDoubleClick={onDoubleClick} /> + } + const data = media[annotation.settings.media_id] + const { + fadeInDuration, fadeOutDuration, duration, + } = annotationFadeTimings(annotation) + const durationHeight = annotation.settings.fullscreen ? durationToHeight(duration, timeline) : 'auto' + return ( + <div + className={className} + style={{ top: y, height: durationHeight }} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + <div className='meta center'> + <div> + <i>{data.title}</i><br /> + {data.author}<br /> + {data.date} + </div> + </div> + </div> + ) +} + +export const AnnotationVideoSetVolume = ({ y, annotation, timeline, selected, onClick, onDoubleClick }) => { + const className = selected ? 'annotation utility video_set_volume selected' : 'annotation utility video_set_volume' + let style = { + top: y, + } + return ( + <div + className={className} + style={style} + onClick={e => onClick(e, annotation)} + onDoubleClick={e => onDoubleClick(e, annotation)} + > + Set volume to {annotation.settings.volume} + </div> + ) +} diff --git a/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/index.js b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/index.js new file mode 100644 index 0000000..1a89dbd --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/annotations/annotationTypes/index.js @@ -0,0 +1,57 @@ +import React from 'react' + +import { + AnnotationSentence, + AnnotationHeadingText, + AnnotationSectionHeading, + AnnotationParagraphEnd, + AnnotationFootnote, + AnnotationTextPlate, + AnnotationSubtitle, +} from './annotationTypes.text' + +import { + AnnotationVideo, + AnnotationVideoSetVolume, +} from './annotationTypes.video' + +import { + AnnotationImage, +} from './annotationTypes.image' + +import { + AnnotationGallery, + AnnotationGalleryAdvance, +} from './annotationTypes.gallery' + +import { + AnnotationCurtain, + AnnotationIntro, + AnnotationSchedule, +} from './annotationTypes.utility' + +export const AnnotationElementLookup = { + sentence: React.memo(AnnotationSentence), + pullquote_credit: React.memo(AnnotationSentence), + heading_text: React.memo(AnnotationHeadingText), + section_heading: React.memo(AnnotationSectionHeading), + paragraph_end: React.memo(AnnotationParagraphEnd), + footnote: React.memo(AnnotationFootnote), + text_plate: React.memo(AnnotationTextPlate), + subtitle: React.memo(AnnotationSubtitle), + + video: React.memo(AnnotationVideo), + video_set_volume: React.memo(AnnotationVideoSetVolume), + + image: React.memo(AnnotationImage), + gallery: React.memo(AnnotationGallery), + carousel: React.memo(AnnotationGallery), + grid: React.memo(AnnotationGallery), + vitrine: React.memo(AnnotationGallery), + + gallery_advance: React.memo(AnnotationGalleryAdvance), + + intro: React.memo(AnnotationIntro), + schedule: React.memo(AnnotationSchedule), + curtain: React.memo(AnnotationCurtain), +} diff --git a/animism-align/frontend/app/views/editor/align/components/player/playButton.component.js b/animism-align/frontend/app/views/editor/align/components/player/playButton.component.js new file mode 100644 index 0000000..f411941 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/player/playButton.component.js @@ -0,0 +1,22 @@ +import React from 'react' + +import actions from 'app/actions' + +const PlayButton = ({ playing, onClick }) => { + return ( + <div + className={playing ? 'playButton playing' : 'playButton paused'} + onClick={() => { + if (onClick) { + onClick(playing) + } else if (playing) { + actions.audio.pause() + } else { + actions.audio.play() + } + }} + /> + ) +} + +export default PlayButton diff --git a/animism-align/frontend/app/views/editor/align/components/sidebar/script.component.js b/animism-align/frontend/app/views/editor/align/components/sidebar/script.component.js new file mode 100644 index 0000000..6c20dfa --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/sidebar/script.component.js @@ -0,0 +1,23 @@ +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' + +const Script = ({ text }) => { + if (text.loading) return null + return ( + <textarea + className='script' + onChange={e => actions.site.updateText(e.target.value)} + value={text} + /> + ) +} + +const mapStateToProps = state => ({ + text: state.site.text, +}) + +export default connect(mapStateToProps)(Script) diff --git a/animism-align/frontend/app/views/editor/align/components/sidebar/tableOfContents.component.js b/animism-align/frontend/app/views/editor/align/components/sidebar/tableOfContents.component.js new file mode 100644 index 0000000..34aedb5 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/sidebar/tableOfContents.component.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { ROMAN_NUMERALS } from 'app/constants' +import actions from 'app/actions' + +class TableOfContents extends Component { + render() { + const { loading, order, lookup } = this.props.annotation + if (loading || !order) return null + const sectionIds = order.filter(id => lookup[id].type === "section_heading") + return ( + <div className="toc"> + {sectionIds.map((id, i) => ( + <div key={id} onClick={() => actions.align.setScrollPosition(lookup[id].start_ts)}> + {ROMAN_NUMERALS[i]}{'. '}{lookup[id].text} + </div> + ))} + </div> + ) + } +} + +const mapStateToProps = state => ({ + annotation: state.annotation.index, +}) + +export default connect(mapStateToProps)(TableOfContents) diff --git a/animism-align/frontend/app/views/editor/align/components/timeline/cursor.component.js b/animism-align/frontend/app/views/editor/align/components/timeline/cursor.component.js new file mode 100644 index 0000000..4a94100 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/timeline/cursor.component.js @@ -0,0 +1,26 @@ +import React, { Component } from 'react' + +import { ZOOM_STEPS } from 'app/constants' +import { timestamp } from 'app/utils' + +const Cursor = ({ timeline, annotation }) => { + const { start_ts, zoom, cursor_ts, duration } = timeline + const ts = annotation.start_ts || cursor_ts + const secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 + const y = (ts - start_ts) / secondsPerPixel + return ( + <div + className='cursor' + style={{ + top: y, + }} + > + <div className='line' /> + <div className='tickLabel'> + {timestamp(ts, 1)} + </div> + </div> + ) +} + +export default Cursor
\ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/components/timeline/cursorRegion.component.js b/animism-align/frontend/app/views/editor/align/components/timeline/cursorRegion.component.js new file mode 100644 index 0000000..a0c9bd7 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/timeline/cursorRegion.component.js @@ -0,0 +1,28 @@ +import React from 'react' + +import { ZOOM_STEPS } from 'app/constants' +import { timestamp } from 'app/utils' + +const CursorRegion = ({ timeline }) => { + const { start_ts, zoom, cursor_region } = timeline + if (!cursor_region) return null + const secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 + const duration = cursor_region.b_ts - cursor_region.a_ts + const y = (cursor_region.a_ts - start_ts) / secondsPerPixel + const height = (duration) / secondsPerPixel + return ( + <div + className='cursor_region' + style={{ + top: y, + height, + }} + > + <div className='tickLabel'> + {timestamp(duration, 1, true)} + </div> + </div> + ) +} + +export default CursorRegion
\ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/align/components/timeline/playCursor.component.js b/animism-align/frontend/app/views/editor/align/components/timeline/playCursor.component.js new file mode 100644 index 0000000..854c43b --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/timeline/playCursor.component.js @@ -0,0 +1,36 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { ZOOM_STEPS } from 'app/constants' +import { timestamp } from 'app/utils' + +const PlayCursor = ({ timeline, audio }) => { + const { start_ts, zoom, duration } = timeline + const { play_ts } = audio + const secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 + const y = (play_ts - start_ts) / secondsPerPixel + // console.log(play_ts, y) + return ( + <div + className='cursor playCursor' + style={{ + top: y, + }} + > + <div className='line' /> + </div> + ) +} + +const mapStateToProps = state => ({ + timeline: state.align.timeline, + audio: state.audio, +}) + +const mapDispatchToProps = dispatch => ({ + // alignActions: bindActionCreators({ ...alignActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(PlayCursor) diff --git a/animism-align/frontend/app/views/editor/align/components/timeline/ticks.component.js b/animism-align/frontend/app/views/editor/align/components/timeline/ticks.component.js new file mode 100644 index 0000000..4530863 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/timeline/ticks.component.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react' + +import { ZOOM_STEPS, ZOOM_LABEL_STEPS, ZOOM_TICK_STEPS, INNER_HEIGHT } from 'app/constants' +import { timestamp } from 'app/utils' + +export default class Ticks extends Component { + render() { + let { start_ts, zoom, duration } = this.props.timeline + + let secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 // 0.1 sec / step + + let widthTimeDuration = INNER_HEIGHT * secondsPerPixel // secs per pixel + + let timeMin = start_ts + let timeMax = Math.min(start_ts + widthTimeDuration, duration) + let timeWidth = timeMax - timeMin + + let pixelMin = timeMin / secondsPerPixel + + let secondsPerLabel = ZOOM_LABEL_STEPS[zoom] // secs + let pixelsPerLabel = secondsPerLabel / secondsPerPixel + let secondsPerTick = ZOOM_TICK_STEPS[zoom] + let pixelsPerTick = secondsPerTick / secondsPerPixel + + let startOffset = pixelsPerLabel - (pixelMin % pixelsPerLabel) + let startTiming = (pixelMin + startOffset) * secondsPerPixel + + let labelCount = Math.ceil(INNER_HEIGHT / pixelsPerLabel) + 1 + let offset, timing, tickLabels = [], ticks = [] + for (var i = -1; i < labelCount; i++) { + offset = i * pixelsPerLabel + startOffset + if (offset > INNER_HEIGHT) continue + timing = i * secondsPerLabel + startTiming + if (timing > duration) { + break + } + tickLabels.push( + <div className='tickLabel' key={"tickLabel_" + i} + style={{ + top: Math.floor(offset) + }}> + {timestamp(timing)} + </div> + ) + } + + let durationOffset = duration / secondsPerPixel - pixelMin + if (timing > duration) { + tickLabels.push( + <div className='tickLabel tickLabelTotal' key={"tickLabel_total"} + style={{ + top: durationOffset + }}> + {timestamp(duration, 1)} + </div> + ) + ticks.push( + <div className='tick' key={"tick_total"} + style={{ + top: Math.floor(durationOffset), + }} + /> + ) + } + let tickCount = Math.ceil(INNER_HEIGHT / pixelsPerTick) + 6 + for (var i = 0; i < tickCount; i += 1) { + offset = i * pixelsPerTick + startOffset - pixelsPerLabel + if (offset > durationOffset) { + break + } + ticks.push( + <div className='tick' key={"tick_" + i} + style={{ + top: Math.floor(offset), + }} + /> + ) + } + // console.log(ticks.length) + + return ( + <div className='ticks'> + {ticks} + {tickLabels} + </div> + ) + } +} diff --git a/animism-align/frontend/app/views/editor/align/components/timeline/waveform.component.js b/animism-align/frontend/app/views/editor/align/components/timeline/waveform.component.js new file mode 100644 index 0000000..59a2c13 --- /dev/null +++ b/animism-align/frontend/app/views/editor/align/components/timeline/waveform.component.js @@ -0,0 +1,105 @@ +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 { + WAVEFORM_SIZE, INNER_HEIGHT, + ZOOM_STEPS, ZOOM_LABEL_STEPS, ZOOM_TICK_STEPS, +} from 'app/constants' + +class Waveform extends Component { + constructor(props){ + super(props) + this.canvasRef = React.createRef() + } + componentDidMount() { + this.resize() + this.draw() + } + componentDidUpdate() { + this.draw() + } + resize() { + const canvas = this.canvasRef.current + canvas.width = WAVEFORM_SIZE + canvas.height = INNER_HEIGHT + } + draw() { + const canvas = this.canvasRef.current + const ctx = canvas.getContext('2d') + const h = INNER_HEIGHT + this.clearCanvas(ctx, h) + this.drawCurve(ctx, h) + } + clearCanvas(ctx, h) { + const w = WAVEFORM_SIZE + ctx.clearRect(0, 0, w, h) + ctx.fillStyle = 'rgba(0,0,0,0.5)' + ctx.fillRect(0, 0, w, h) + ctx.fillStyle = 'rgba(64,128,192,0.5)' + } + drawCurve(ctx, h) { + let { peaks, timeline } = this.props + let { start_ts, zoom, duration } = timeline + + let secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 // 0.1 sec / step + let stepsPerPixel = ZOOM_STEPS[zoom] // 0.1 sec / step + + let widthTimeDuration = h * secondsPerPixel // secs per pixel + + let timeMin = Math.round(start_ts / secondsPerPixel) * secondsPerPixel + let timeMax = Math.min(timeMin + widthTimeDuration, duration) + let timeWidth = timeMax - timeMin + + let stepMin = Math.floor(timeMin * 10) + let pixelWidth = Math.ceil(timeWidth / secondsPerPixel) + + let i = 0 + let step = stepMin + let waveformPeak = WAVEFORM_SIZE / 2 + let origin = (1 - peaks[step]) * waveformPeak + let y + let peak + // console.log(stepMin, pixelWidth * stepsPerPixel + stepMin) + ctx.beginPath() + ctx.moveTo(origin, 0) + for (i = 0; i < pixelWidth; i++) { + step = i * stepsPerPixel + stepMin + peak = peaks[step] + y = (1 - peak) * waveformPeak + ctx.lineTo(y, i) + } + for (i = pixelWidth - 1; i > 0; i--) { + step = i * stepsPerPixel + stepMin + peak = peaks[step] + y = (1 + peak) * waveformPeak + ctx.lineTo(y, i) + } + ctx.lineTo(origin, 0) + ctx.fillStyle = 'rgba(255,255,255,0.8)' + ctx.fill() + } + render() { + return ( + <canvas + ref={this.canvasRef} + onMouseDown={this.props.onMouseDown} + onMouseUp={this.props.onMouseUp} + /> + ) + } +} + +const mapStateToProps = state => ({ + timeline: state.align.timeline, + peaks: state.site.peaks, +}) + +const mapDispatchToProps = dispatch => ({ + // uploadActions: bindActionCreators({ ...uploadActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(Waveform) |
