CellAnnotationList.vue 11 KB
Newer Older
Anton's avatar
Intial  
Anton committed
1
<template lang="pug">
Anton Koch's avatar
Anton Koch committed
2
  // annotation list
Anton's avatar
Intial  
Anton committed
3
4
5
6
  div.annotation-list(:class="{'display-preview': preview, 'display-full': display}")
    template(v-if="display")
      template(v-if="video && video.body")

Anton Koch's avatar
Anton Koch committed
7
        //q-list-header
Anton's avatar
Intial  
Anton committed
8
9
          q-item {{videoMeta.title}}

Anton's avatar
Anton committed
10
        template(v-if="cell.displayType === 'tabs'")
Mathias Bär's avatar
Mathias Bär committed
11
12
13
          .annotation-tabs-container.column
            .annotation-tab(
              v-for="(annotation, index) in annotations",
Anton's avatar
Anton committed
14
              @click="event => {handleTabClick(event, annotation, index)}", :key="`tab-${index}`",
Anton's avatar
Anton committed
15
              :class="{'active': index === activeTabIdx}")
Mathias Bär's avatar
Mathias Bär committed
16
17
              div {{index + 1}}
          .annotation-content-container
Anton's avatar
Anton committed
18
            q-item.annotation(v-for="(annotation, index) in annotations", v-show="index === activeTabIdx", :key="`annotation-${index}`")
Mathias Bär's avatar
Mathias Bär committed
19
              div
Anton's avatar
Cleanup  
Anton committed
20
                .date {{formatSelectorForList(annotation)}}
21
                markdown-display.content(:content="annotation.body.value")
Mathias Bär's avatar
Mathias Bär committed
22

23
        template(v-else)
Mathias Bär's avatar
Mathias Bär committed
24
25
26
27
28
29
30
31
32
33
34
35
36
37

          template(v-for="(annotation, index) in annotations")
            q-item-separator(v-if="index > 0")

            template(v-if="cell.allow_annotations && $store.state.auth.user && showAnnotationInput(index)")
              q-item.annotation-input-container
                q-input.annotation-input(type="textarea",
                :max-height="200",
                :min-rows="3"
                style="width:100%",
                v-model="newAnnotationText",
                @keyup.native.enter="handleInputChanged",
                stack-label="Leave Comment")
              q-item-separator
Anton's avatar
Intial  
Anton committed
38

39
            q-item.annotation
Anton's avatar
Anton committed
40
              a(:class="{'active': index === currentIndex}",
41
42
43
              @click.prevent="event => {handleAnnotationClick(event, annotation, index)}")
                .row.no-wrap
                  .date {{formatSelectorForList(annotation)}}
44
                  markdown-display.content(:content="annotation.body.value", :options="mdOptions")
Anton's avatar
Intial  
Anton committed
45
46

      template(v-else)
Anton's avatar
Anton committed
47
        strong Loading annotations
Anton's avatar
Intial  
Anton committed
48
49
50
51

      q-item-separator

    template(v-else)
Anton's avatar
Anton committed
52
      cell-info(:cell="cell", type="Annotation List")
Anton's avatar
Intial  
Anton committed
53
54
55
56

</template>

<script>
57
import { DateTime, Interval } from 'luxon'
Anton's avatar
Anton committed
58
import constants from 'mbjs-data-models/src/constants'
Anton's avatar
Anton committed
59
import CellInfo from '../CellInfo'
Anton's avatar
Anton committed
60
61
62
import Username from '../../../shared/elements/Username'
import VideoTitle from '../../../shared/elements/VideoTitle'
import MarkdownDisplay from '../../../shared/elements/MarkdownDisplay'
Anton's avatar
Intial  
Anton committed
63

Anton's avatar
Anton committed
64
65
export default {
  components: {
Anton's avatar
Anton committed
66
    CellInfo,
Anton's avatar
Anton committed
67
    Username,
Anton's avatar
Anton committed
68
69
    VideoTitle,
    MarkdownDisplay
Anton's avatar
Anton committed
70
  },
71
  props: ['cell', 'display', 'preview', 'visible'],
Anton's avatar
Anton committed
72
73
74
75
76
  data () {
    return {
      video: {},
      videoTime: {},
      videoMeta: {},
77
      gridTime: undefined,
Anton's avatar
Anton committed
78
79
80
81
82
83
      contextTime: {},
      annotations: [],
      map: {},
      annotationTimes: [],
      inputIndex: 0,
      newAnnotationText: '',
Mathias Bär's avatar
Mathias Bär committed
84
      staging: process.env.IS_STAGING,
Anton's avatar
Anton committed
85
      activeTabIdx: 0,
86
      playerTime: 0.0,
87
88
89
90
      lastSignal: Date.now(),
      mdOptions: {
        target: '_blank'
      }
Anton's avatar
Anton committed
91
92
93
    }
  },
  async mounted () {
Anton's avatar
Anton committed
94
95
96
97
    const videoAnnotation = await this.$store.dispatch(
      'annotations/get',
      this.cell.sourceUuid
    )
98
    this.videoTime = DateTime.fromMillis(videoAnnotation.target.selector._valueMillis)
Anton's avatar
Anton committed
99
100
    try {
      const meta = await this.$store.dispatch('metadata/get', videoAnnotation)
101
      if (meta && meta.title) this.videoMeta = meta
Anton's avatar
Anton committed
102
103
    }
    catch (e) {
104
      console.debug('Unable to fetch metadata for annotation')
Anton's avatar
Anton committed
105
    }
106
    await this.fetchAnnotations()
Anton's avatar
Anton committed
107

108
109
    this.$root.$on('video-time-changed', this.onVideoTimeChanged)
    this.$root.$on('grid-datetime', this.onGridDateTime)
Anton's avatar
Anton committed
110
  },
Anton's avatar
Anton committed
111
  computed: {
Anton's avatar
Anton committed
112
113
114
    videoUuid () {
      return this.cell ? this.cell.id : undefined
    },
Anton's avatar
Anton committed
115
116
117
118
119
    currentIndex () {
      if (!this.annotations || !this.annotations.length) return

      let idx = -1, annotation = this.annotations[0]
      while (annotation && idx < this.annotations.length &&
120
        this.baseSelector > DateTime.fromMillis(annotation.target.selector._valueMillis)) {
Anton's avatar
Anton committed
121
122
123
124
125
126
127
128
129
130
        idx++
        annotation = this.annotations[idx + 1]
      }
      return idx
    },
    baseSelector () {
      if (!this.videoTime) return DateTime.local().toISO()
      return this.videoTime.plus(this.playerTime * 1000)
    }
  },
Anton's avatar
Anton committed
131
  methods: {
132
    onVideoTimeChanged (time, globalTime, origin = undefined) {
133
134
135
      if (!this.visible) return
      // if (Date.now() - this.lastSignal < 500) return
      // this.lastSignal = Date.now()
136
      if (!origin || (origin.type === 'Video' && this.video.target.id === origin.origin.target.id)) {
Anton's avatar
Anton committed
137
        // console.debug('CellAnnotationList: received video-time-changed event', time, globalTime, origin)
Anton's avatar
Anton committed
138
        this.playerTime = time
139
140
141
        this.gridTime = globalTime
        this.contextTime = globalTime

Anton's avatar
Cleanup  
Anton committed
142
        // TODO: find out what contextTime and inputTime mean
143
144
145
146
147
148
        let inputTime = this.annotationTimes.find(t => t >= this.contextTime)
        if (inputTime) this.inputIndex = this.annotationTimes.indexOf(inputTime)
        else this.inputIndex = 0
      }
    },
    onGridDateTime (datetime, origin = undefined) {
149
      if (!this.visible) return
150
151
152
153
154
      if (!origin || (origin.type === 'Video' && this.video.target.id === origin.origin.target.id)) {
        console.debug('CellAnnotationList: received grid-datetime event', datetime, origin)
        this.gridTime = datetime
      }
    },
Anton's avatar
Anton committed
155
156
157
158
159
160
161
162
163
164
165
166
167
168
    async handleInputChanged () {
      const text = this.newAnnotationText && this.newAnnotationText.trim()
      if (text && text.length > 0) {
        let newAnnotation = {
          body: {
            type: 'TextualBody',
            purpose: 'commenting',
            value: text
          },
          target: {
            id: this.video.target.id,
            type: this.video.target.type,
            selector: {
              type: 'Fragment',
169
              value: DateTime.fromMillis(this.contextTime).toISO()
Anton's avatar
Intial  
Anton committed
170
171
            }
          }
Anton's avatar
Anton committed
172
173
174
175
176
        }
        const annotation = await this.$store.dispatch(
          'annotations/post',
          newAnnotation
        )
177
178
        console.debug('CellAnnotationList: post annotation', annotation)
        await this.fetchAnnotations()
Anton's avatar
Intial  
Anton committed
179
      }
Anton's avatar
Anton committed
180
      this.newAnnotationText = ''
Anton's avatar
Intial  
Anton committed
181
    },
Anton's avatar
Anton committed
182
183
    handleAnnotationClick (event, annotation, index) {
      this.$root.$emit('annotation-trigger', annotation, this.annotationTimes[index])
Anton's avatar
Anton committed
184
    },
Mathias Bär's avatar
Mathias Bär committed
185
186
187
188
    handleTabClick (event, annotation, index) {
      this.activeTabIdx = index
      this.$root.$emit('annotation-trigger', annotation, this.annotationTimes[index])
    },
189
190
191
    isAnnotationActive (annotation, index) {
      let i = 0
      while (i < this.annotations.length) {
192
        const dt = DateTime.fromMillis(this.annotations[i].target.selector._valueMillis)
193
194
195
196
197
198
        if (this.gridTime && this.gridTime >= dt && index === i) return true
        i++
      }
      return false
    },
    formatSelectorForList (annotation) {
199
      const annotationDate = DateTime.fromMillis(annotation.target.selector._valueMillis)
200
201
      return Interval.fromDateTimes(this.videoTime, annotationDate)
        .toDuration()
Anton's avatar
Anton committed
202
        .toFormat(constants.config.TIMECODE_FORMAT)
203
    },
Anton's avatar
Anton committed
204
205
206
    showAnnotationInput (index) {
      return index === this.inputIndex
    },
207
    async fetchAnnotations () {
208
      const videoAnnotation = await this.$store.dispatch('annotations/get', this.videoUuid)
209
210
211
212
      if (videoAnnotation) {
        this.video = videoAnnotation
        this.contextTime = this.videoTime
        const query = {
213
          type: constants.mapTypes.MAP_TYPE_TIMELINE,
214
          id: videoAnnotation.target.id
215
216
217
218
219
220
        }
        const mapResult = await this.$store.dispatch('maps/find', query)
        const map = mapResult.items.shift()
        if (map) {
          this.map = map
        }
221
222
223
        const
          startMillis = videoAnnotation.target.selector._valueMillis + (this.cell.start ? this.cell.start * 1000 : 0),
          endMillis = startMillis + (this.cell.duration || videoAnnotation.target.selector._valueDuration || 0)
224
225
        const annotationsQuery = {
          'target.id': videoAnnotation.target.id,
226
          'target.type': constants.mapTypes.MAP_TYPE_TIMELINE,
227
          'body.type': 'TextualBody',
228
229
230
          'target.selector._valueMillis': {
            $gte: startMillis,
            $lte: endMillis
231
232
          }
        }
233

Anton's avatar
Anton committed
234
        // let regCheck
235
236
237
238
        if (this.cell.textfilter) {
          annotationsQuery['body.value'] = RegExp(`.*${this.cell.textfilter}.*`, 'ig')
        }
        else if (this.cell.regexp) {
Anton's avatar
Anton committed
239
240
241
242
243
244
245
246
          let regexp = this.cell.regexp
          if (process.env.IS_VIEWER) {
            if (RegExp('^/.+/.*$').test(regexp)) {
              const parts = regexp.match(RegExp('^/(.+)/(.*)$'))
              regexp = RegExp(parts[1], parts[2])
            }
          }
          annotationsQuery['body.value'] = regexp
247
248
        }

249
250
        const annotationsResult = await this.$store.dispatch('annotations/find', annotationsQuery)
        let annotations = annotationsResult.items.filter(a => {
251
          return DateTime.fromMillis(a.target.selector._valueMillis) >= this.videoTime
252
        })
253
        annotations = annotations.sort(this.$sort.onRef)
254
255
256
257
        this.annotations = annotations
        this.annotationTimes = []
        annotations.forEach(a => {
          this.annotationTimes.push(
258
            DateTime.fromMillis(a.target.selector._value)
259
          )
Anton's avatar
Anton committed
260
        })
261
      } // TODO: else, show not found?
Anton's avatar
Intial  
Anton committed
262
263
    }
  }
Anton's avatar
Anton committed
264
}
Anton's avatar
Intial  
Anton committed
265
266
267
</script>

<style scoped lang="stylus">
268
  @import '~variables'
Anton Koch's avatar
Anton Koch committed
269
  .q-item
Mathias Bär's avatar
Mathias Bär committed
270
    font-size 1.8vh
Anton Koch's avatar
Anton Koch committed
271
272
273
274
275
    line-height 1.45em
    padding 13px 23px
    -webkit-hyphens: auto
    -ms-hyphens: auto
    hyphens: auto
Anton's avatar
Intial  
Anton committed
276

Anton Koch's avatar
Anton Koch committed
277
278
  .q-item-separator-component
    margin 0
Anton's avatar
Intial  
Anton committed
279

Anton Koch's avatar
Anton Koch committed
280
281
282
283
  .annotation-list
    width 100%
    height 100%
    overflow auto
Anton's avatar
Intial  
Anton committed
284

Anton Koch's avatar
Anton Koch committed
285
  .annotation-list.display-full
Mathias Bär's avatar
colors  
Mathias Bär committed
286
    background-color #303030
Anton's avatar
Intial  
Anton committed
287

Anton Koch's avatar
Anton Koch committed
288
289
  .annotation-list.display-preview
    color #ddd
Anton's avatar
Intial  
Anton committed
290

291
292
293
  .annotation
    .active, a.active
      color $primary
Anton's avatar
Intial  
Anton committed
294

Anton Koch's avatar
Anton Koch committed
295
296
297
  .q-list-header
    padding-top 0.5em
    padding-left 0
Anton's avatar
Intial  
Anton committed
298

Anton Koch's avatar
Anton Koch committed
299
  .annotation
Anton's avatar
Intial  
Anton committed
300

Anton Koch's avatar
Anton Koch committed
301
302
303
    a
      width 100%
      color #ddd
Anton's avatar
Intial  
Anton committed
304

Anton Koch's avatar
Anton Koch committed
305
306
    .date
    .author
Mathias Bär's avatar
Mathias Bär committed
307
      font-size 0.8em
Anton Koch's avatar
Anton Koch committed
308
309
310
      color #a0a0a0
      padding-right 23px
      display: inline-block
Anton's avatar
Intial  
Anton committed
311

Anton Koch's avatar
Anton Koch committed
312
313
314
315
316
317
318
319
320
321
322
323
324
    .date
      line-height 2.1em

    .content
      width 100%

  .q-input.annotation-input
    margin-top 0.5em

  .annotation-input-container
    margin-top -0.5em
    margin-bottom -0.5em
    border-left 10px solid #95b4ff
Anton's avatar
Intial  
Anton committed
325

Anton Koch's avatar
Anton Koch committed
326
327
  .annotation-input-container:hover
    background-color #e7e9ff
Anton's avatar
Intial  
Anton committed
328

Anton Koch's avatar
Anton Koch committed
329
  .q-item-separator-component
Mathias Bär's avatar
colors  
Mathias Bär committed
330
    background-color #252525
Anton's avatar
Intial  
Anton committed
331

Mathias Bär's avatar
Mathias Bär committed
332
333
334
335
  .annotation-content-container
    padding-right 66px

  .annotation-tabs-container
Mathias Bär's avatar
colors  
Mathias Bär committed
336
    background-color #252525
Mathias Bär's avatar
Mathias Bär committed
337
338
339
340
341
342
343
344
345
    position: absolute
    top: 0
    right: 0
    height 100%
    width 66px
    padding-left 1px
    .annotation-tab
      cursor: pointer
      line-height: 100%
Mathias Bär's avatar
Mathias Bär committed
346
      font-size 1.8vh
Mathias Bär's avatar
Mathias Bär committed
347
348
349
350
      width 66px
      flex-grow 1
      text-align: center
      flex-direction: column
Mathias Bär's avatar
colors  
Mathias Bär committed
351
      background-color #303030
Mathias Bär's avatar
Mathias Bär committed
352
353
354
355
      &.active
        background-color $primary
        color: white
      &:not(:last-child)
Mathias Bär's avatar
colors  
Mathias Bär committed
356
        border-bottom: 1px solid #252525
Mathias Bär's avatar
Mathias Bär committed
357
358
359
360
361
      div
        display: flex
        flex-direction: column
        justify-content: center
        height: 100%
Anton's avatar
Intial  
Anton committed
362
</style>