From 82c2ac11f4ef2112a0332f2f9c7cdd52444f0d2a Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 4 Jul 2020 17:24:21 +0200 Subject: displaying annotation list, click to select, doubleclick to show form, updating annotations --- animism-align/cli/app/sql/models/annotation.py | 2 +- animism-align/cli/app/sql/models/paragraph.py | 4 ++ .../sql/versions/202007041633_create_database.py | 56 --------------- .../sql/versions/202007041642_create_database.py | 58 +++++++++++++++ animism-align/frontend/app.js | 1 + .../frontend/views/align/align.actions.js | 8 +++ animism-align/frontend/views/align/align.css | 20 +++++- .../components/annotations/annotation.form.js | 6 +- .../components/annotations/annotation.index.js | 83 ++++++++++++++++++++++ .../components/annotations/annotation.types.js | 40 +++++++++++ .../align/containers/annotations.container.js | 8 +-- .../views/align/containers/timeline.container.js | 3 + 12 files changed, 224 insertions(+), 65 deletions(-) delete mode 100644 animism-align/cli/app/sql/versions/202007041633_create_database.py create mode 100644 animism-align/cli/app/sql/versions/202007041642_create_database.py create mode 100644 animism-align/frontend/views/align/components/annotations/annotation.index.js create mode 100644 animism-align/frontend/views/align/components/annotations/annotation.types.js (limited to 'animism-align') diff --git a/animism-align/cli/app/sql/models/annotation.py b/animism-align/cli/app/sql/models/annotation.py index 6cc476c..cc53bd6 100644 --- a/animism-align/cli/app/sql/models/annotation.py +++ b/animism-align/cli/app/sql/models/annotation.py @@ -27,7 +27,7 @@ class Annotation(Base): 'paragraph_id': self.paragraph_id, 'start_ts': self.start_ts, 'end_ts': self.end_ts, - 'sentence': self.description, + 'text': self.text, 'settings': self.settings, } diff --git a/animism-align/cli/app/sql/models/paragraph.py b/animism-align/cli/app/sql/models/paragraph.py index 7c7bcd7..790c9f0 100644 --- a/animism-align/cli/app/sql/models/paragraph.py +++ b/animism-align/cli/app/sql/models/paragraph.py @@ -14,12 +14,16 @@ class Paragraph(Base): __tablename__ = 'paragraph' id = Column(Integer, primary_key=True) type = Column(String(16, convert_unicode=True), nullable=False) + start_ts = Column(Float, nullable=False) + end_ts = Column(Float, nullable=True) settings = Column(JSON, default={}, nullable=True) def toJSON(self): return { 'id': self.id, 'type': self.type, + 'start_ts': self.start_ts, + 'end_ts': self.end_ts, 'settings': self.settings, } diff --git a/animism-align/cli/app/sql/versions/202007041633_create_database.py b/animism-align/cli/app/sql/versions/202007041633_create_database.py deleted file mode 100644 index f8336e5..0000000 --- a/animism-align/cli/app/sql/versions/202007041633_create_database.py +++ /dev/null @@ -1,56 +0,0 @@ -"""create database - -Revision ID: f8936a84e584 -Revises: -Create Date: 2020-07-04 16:33:01.643193 - -""" -from alembic import op -import sqlalchemy as sa -import sqlalchemy_utc - - -# revision identifiers, used by Alembic. -revision = 'f8936a84e584' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('paragraph', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), - sa.Column('settings', sa.JSON(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('upload', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('sha256', sa.String(length=256), nullable=False), - sa.Column('fn', sa.String(length=256), nullable=False), - sa.Column('ext', sa.String(length=4, _expect_unicode=True), nullable=False), - sa.Column('username', sa.String(length=16, _expect_unicode=True), nullable=False), - sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('annotation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), - sa.Column('paragraph_id', sa.Integer(), nullable=True), - sa.Column('start_ts', sa.Float(), nullable=False), - sa.Column('end_ts', sa.Float(), nullable=True), - sa.Column('text', sa.Text(_expect_unicode=True), nullable=True), - sa.Column('settings', sa.JSON(), nullable=True), - sa.ForeignKeyConstraint(['paragraph_id'], ['paragraph.id'], ), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('annotation') - op.drop_table('upload') - op.drop_table('paragraph') - # ### end Alembic commands ### diff --git a/animism-align/cli/app/sql/versions/202007041642_create_database.py b/animism-align/cli/app/sql/versions/202007041642_create_database.py new file mode 100644 index 0000000..bec0cc3 --- /dev/null +++ b/animism-align/cli/app/sql/versions/202007041642_create_database.py @@ -0,0 +1,58 @@ +"""create database + +Revision ID: 4ede1524f909 +Revises: +Create Date: 2020-07-04 16:42:00.443043 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc + + +# revision identifiers, used by Alembic. +revision = '4ede1524f909' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('paragraph', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), + sa.Column('start_ts', sa.Float(), nullable=False), + sa.Column('end_ts', sa.Float(), nullable=True), + sa.Column('settings', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('upload', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('sha256', sa.String(length=256), nullable=False), + sa.Column('fn', sa.String(length=256), nullable=False), + sa.Column('ext', sa.String(length=4, _expect_unicode=True), nullable=False), + sa.Column('username', sa.String(length=16, _expect_unicode=True), nullable=False), + sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('annotation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), + sa.Column('paragraph_id', sa.Integer(), nullable=True), + sa.Column('start_ts', sa.Float(), nullable=False), + sa.Column('end_ts', sa.Float(), nullable=True), + sa.Column('text', sa.Text(_expect_unicode=True), nullable=True), + sa.Column('settings', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['paragraph_id'], ['paragraph.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('annotation') + op.drop_table('upload') + op.drop_table('paragraph') + # ### end Alembic commands ### diff --git a/animism-align/frontend/app.js b/animism-align/frontend/app.js index 975b5b2..1c40c09 100644 --- a/animism-align/frontend/app.js +++ b/animism-align/frontend/app.js @@ -19,6 +19,7 @@ export default class App extends Component { componentDidMount() { actions.site.loadText() actions.site.loadPeaks() + actions.annotation.index() } render() { return ( diff --git a/animism-align/frontend/views/align/align.actions.js b/animism-align/frontend/views/align/align.actions.js index b3883ae..b10f257 100644 --- a/animism-align/frontend/views/align/align.actions.js +++ b/animism-align/frontend/views/align/align.actions.js @@ -38,11 +38,19 @@ export const showNewAnnotationForm = (start_ts, text) => dispatch => { 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 }) diff --git a/animism-align/frontend/views/align/align.css b/animism-align/frontend/views/align/align.css index 323dc9a..e4629b7 100644 --- a/animism-align/frontend/views/align/align.css +++ b/animism-align/frontend/views/align/align.css @@ -112,4 +112,22 @@ canvas { } .annotationForm .row { justify-content: space-between; -} \ No newline at end of file +} +.annotationIndex { + width: 305px; +} +.annotation { + position: absolute; + left: 5px; + max-width: 300px; + padding: 0.25rem 0.375rem; + box-shadow: 2px 2px 4px rgba(0,0,0,0.5); + border-radius: 2px; + cursor: pointer; +} +.annotation.sentence { + background-color: #83b; +} +.annotation.header { + background-color: #838; +} diff --git a/animism-align/frontend/views/align/components/annotations/annotation.form.js b/animism-align/frontend/views/align/components/annotations/annotation.form.js index 9432948..b62c36e 100644 --- a/animism-align/frontend/views/align/components/annotations/annotation.form.js +++ b/animism-align/frontend/views/align/components/annotations/annotation.form.js @@ -24,9 +24,13 @@ class AnnotationForm extends Component { this.handleSubmit = this.handleSubmit.bind(this) } 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 - console.log(e.keyCode) switch (e.keyCode) { case 38: // up e.preventDefault() diff --git a/animism-align/frontend/views/align/components/annotations/annotation.index.js b/animism-align/frontend/views/align/components/annotations/annotation.index.js new file mode 100644 index 0000000..7b562c2 --- /dev/null +++ b/animism-align/frontend/views/align/components/annotations/annotation.index.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import actions from '../../../../actions' + +import { ZOOM_STEPS } from '../../constants' +import { clamp } from '../../../../util' +import { positionToTime, timeToPosition } from '../../align.util' + +import { AnnotationElementLookup } from './annotation.types' + +class AnnotationIndex extends Component { + 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 = window.innerHeight * secondsPerPixel // secs per pixel + + let timeMin = start_ts - 30.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(annotation) { + actions.audio.seek(annotation.start_ts) + } + handleDoubleClick(annotation) { + actions.align.showEditAnnotationForm(annotation) + } + render() { + const { timeline } = this.props + const { start_ts } = timeline + const { items } = this.state + return ( +
+ {items.map(annotation => { + const { id, type, start_ts } = annotation + const AnnotationElement = AnnotationElementLookup[type] + const y = timeToPosition(start_ts, timeline) + return ( + + ) + })} +
+ ) + } +} + +const mapStateToProps = state => ({ + timeline: state.align.timeline, + index: state.annotation.index, +}) + +const mapDispatchToProps = dispatch => ({ +}) + +export default connect(mapStateToProps, mapDispatchToProps)(AnnotationIndex) diff --git a/animism-align/frontend/views/align/components/annotations/annotation.types.js b/animism-align/frontend/views/align/components/annotations/annotation.types.js new file mode 100644 index 0000000..465844f --- /dev/null +++ b/animism-align/frontend/views/align/components/annotations/annotation.types.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react' + +import actions from '../../../../actions' + +import { ZOOM_STEPS } from '../../constants' +import { clamp } from '../../../../util' +import { positionToTime, timeToPosition } from '../../align.util' + +export const AnnotationSentence = ({ y, annotation, onClick, onDoubleClick }) => { + const { start_ts, text } = annotation + return ( +
onClick(annotation)} + onDoubleClick={() => onDoubleClick(annotation)} + > + {text} +
+ ) +} + +export const AnnotationHeader = ({ y, annotation, onClick, onDoubleClick }) => { + const { start_ts, text } = annotation + return ( +
onClick(annotation)} + onDoubleClick={() => onDoubleClick(annotation)} + > + {text} +
+ ) +} + +export const AnnotationElementLookup = { + sentence: AnnotationSentence, + header: AnnotationHeader, +} \ No newline at end of file diff --git a/animism-align/frontend/views/align/containers/annotations.container.js b/animism-align/frontend/views/align/containers/annotations.container.js index e32757b..b6cdace 100644 --- a/animism-align/frontend/views/align/containers/annotations.container.js +++ b/animism-align/frontend/views/align/containers/annotations.container.js @@ -11,15 +11,16 @@ import { clamp } from '../../../util' import { positionToTime } from '../align.util' import AnnotationForm from '../components/annotations/annotation.form' +import AnnotationIndex from '../components/annotations/annotation.index' class Annotations extends Component { constructor(props){ super(props) - // this.handleKeydown = this.handleKeydown.bind(this) } render() { return (
+ {this.props.annotation.start_ts && } @@ -28,11 +29,6 @@ class Annotations extends Component { } } -/* -- get the first sentence from the text -- display the form at that point -*/ - const mapStateToProps = state => ({ timeline: state.align.timeline, annotation: state.align.annotation, diff --git a/animism-align/frontend/views/align/containers/timeline.container.js b/animism-align/frontend/views/align/containers/timeline.container.js index 4167d2d..23b9435 100644 --- a/animism-align/frontend/views/align/containers/timeline.container.js +++ b/animism-align/frontend/views/align/containers/timeline.container.js @@ -53,6 +53,9 @@ class Timeline extends Component { } else { // console.log(e.keyCode) switch (e.keyCode) { + case 27: // escape + actions.align.hideAnnotationForm() + break case 32: // spacebar actions.audio.toggle() break -- cgit v1.2.3-70-g09d2