import * as types from 'app/types' import { store } from 'app/store' import actions from 'app/actions' import { MEDIA_ANNOTATION_TYPES, MEDIA_LABEL_TYPES, TEXT_ANNOTATION_TYPES, INLINE_UTILITY_ANNOTATION_TYPES, FULLSCREEN_UTILITY_ANNOTATION_TYPES, CUE_UTILITY_ANNOTATION_TYPES, CURTAIN_COLOR_LOOKUP, GROWL, } from 'app/constants' import { floatInRange, timestampToSeconds } from 'app/utils' import { buildParagraphs } from 'app/utils/transcript.utils' import { annotationFadeTimings, displayThumbnailURL } from 'app/utils/annotation.utils' import { getNextSection } from 'app/utils/viewer.utils' import { preloadImages } from 'app/utils/image.utils' /* building the list of sections from the raw annotation list */ export const loadSections = () => dispatch => { // list of all sections let sections = [] // current section being processed (i.e. last section) let currentSection, sectionColor // keep tally of all media, so that we can display them with correct IDs in the checklist let mediaIndex = 0 let eventIndex = 0 // dedupe the labels that we see in each section let currentMediaLabels = {} let seenMedia = {} // keep track of all annotations that constitute the "text" of the essay // these include sentences, headings, and inline media. used to build paragraphs, then reset. let sectionTextAnnotationOrder = [] let footnoteList = [] // keep track of all annotations that constitute fullscreen events. // these include curtains, title cards, and fullscreen media. // let fullscreenTimeline = [] // fetch all annotations and media const state = store.getState() const { timeline } = state.align const { order: annotationOrder, lookup: annotationLookup } = state.annotation.index const { lookup: mediaLookup } = state.media.index // console.log(state) // loop over the annotations in time order. annotationOrder.forEach((annotation_id, i) => { // fetch the current annotation const annotation = annotationLookup[annotation_id] // we have reached a new section. if (annotation.type === 'section_heading') { // finish off the previous section. if (currentSection) { currentSection.mediaLabels = Object.keys(currentMediaLabels).sort().join(', ') let { paragraphs, footnotes } = buildParagraphs(sectionTextAnnotationOrder, currentSection.index, footnoteList.length) currentSection.paragraphs = paragraphs footnoteList = footnoteList.concat(footnotes) // get the last end_ts in the episode currentSection.end_ts = ( currentSection.fullscreenTimeline.reduce( (a,b) => Math.max(a, b.end_ts), currentSection.paragraphs[currentSection.paragraphs.length - 1].end_ts, ) ) } // create a new section and reset state variables currentSection = newSection(annotation, sections.length, mediaIndex) currentMediaLabels = {} sectionTextAnnotationOrder = [] // add this new section to the list! sections.push(currentSection) } // sanity check. ignore everything before the first section. if (!currentSection) { return } // add media to the current section. if (MEDIA_ANNOTATION_TYPES.has(annotation.type)) { const media = mediaLookup[annotation.settings.media_id] // fetch the media and add it to the list of media (TODO: handle carousels) if (!media.settings.hide_in_bibliography && !(media.id in seenMedia)) { currentSection.media.push({ start_ts: annotation.start_ts, media, }) seenMedia[media.id] = true } // get the display string for this media type // console.log(annotation.type, media.type) if (annotation.type in MEDIA_LABEL_TYPES) { currentMediaLabels[MEDIA_LABEL_TYPES[annotation.type]] = true } // increment the media tally mediaIndex += 1 // non-fullscreen (or fullscreen-inline) media should be displayed in the transcript. if (!annotation.settings.fullscreen || annotation.settings.inline) { sectionTextAnnotationOrder.push(annotation.id) } } // build timeline of "cue" instructions, which tell elements to change // TODO: modify this to append these instructions to a list based on media_id, so we can grab it for the gallery if (CUE_UTILITY_ANNOTATION_TYPES.has(annotation.type)) { if (annotation.type === 'gallery_advance') { annotation.settings.frame_index = parseInt(annotation.settings.frame_index) annotation.settings.seek_index = annotation.settings.half_frame ? annotation.settings.frame_index + 0.5 : annotation.settings.frame_index currentSection.mediaCues[annotation.settings.media_id] = currentSection.mediaCues[annotation.settings.media_id] || { cues: [], lookup: {} } currentSection.mediaCues[annotation.settings.media_id].cues.push(annotation) currentSection.mediaCues[annotation.settings.media_id].lookup[annotation.settings.frame_index] = annotation } else { currentSection.cues.push(annotation) } } // build timeline of special inline items if (INLINE_UTILITY_ANNOTATION_TYPES.has(annotation.type)) { sectionTextAnnotationOrder.push(annotation.id) if (annotation.type === 'intro') { if (annotation.settings.intro_start_ts) { currentSection.intro_start_ts = timestampToSeconds(annotation.settings.intro_start_ts) } else { currentSection.intro_start_ts = 0 } } } // build timeline of fullscreen events if ((FULLSCREEN_UTILITY_ANNOTATION_TYPES.has(annotation.type) || annotation.settings.fullscreen) && !annotation.settings.inline) { const event = makeFullscreenEvent(eventIndex++, annotation, currentSection) currentSection.fullscreenTimeline.push(event) // for videos, we probably want to show a poster image if ((annotation.type === 'image' || annotation.type === 'video') && !annotation.settings.hide_poster_inline) { sectionTextAnnotationOrder.push(annotation.id) } } // add text annotations to section annotation order if (TEXT_ANNOTATION_TYPES.has(annotation.type)) { sectionTextAnnotationOrder.push(annotation.id) } }) // finished processing all annotations. finish off the last section. if (currentSection) { currentSection.mediaLabels = Object.keys(currentMediaLabels).sort().join(', ') let { paragraphs, footnotes } = buildParagraphs(sectionTextAnnotationOrder, currentSection.index) currentSection.paragraphs = paragraphs footnoteList = footnoteList.concat(footnotes) currentSection.end_ts = timeline.duration } let time_to_first_fullscreen_element, initial_curtain_event // last fixes on the sections sections.forEach((currentSection, i) => { // set the end_ts for each section (i.e. just before the next section starts) if (i < sections.length - 1) { // if the section doesnt have an end_ts (how??) then set it to end right before the next one. // also if the end_ts is beyond the beginning of the next section (more possible...) if (currentSection.end_ts === 0 || currentSection.end_ts > sections[i+1].start_ts - 1) { currentSection.end_ts = sections[i+1].start_ts - 1 } } // if the first fullscreen event is close to the beginning of the section, move it there time_to_first_fullscreen_element = 0 if (currentSection.fullscreenTimeline.length) { time_to_first_fullscreen_element = Math.abs(currentSection.fullscreenTimeline[0].start_ts - currentSection.start_ts) if (time_to_first_fullscreen_element < 2.0) { currentSection.fullscreenTimeline[0].start_ts = currentSection.start_ts time_to_first_fullscreen_element = 0.0 } } if ((!currentSection.fullscreenTimeline.length || time_to_first_fullscreen_element > 0.0) && currentSection.index !== 0) { // this section has no initial fullscreen event, so we should create a blank dummy curtain event sectionColor = currentSection.paragraphs[0].annotations[0].settings.color || 'white' initial_curtain_event = makeFullscreenEvent(0, { start_ts: currentSection.start_ts, end_ts: currentSection.start_ts + 1.1, type: 'curtain', settings: { color: CURTAIN_COLOR_LOOKUP[sectionColor], fade_in_duration: '0:00', fade_out_duration: '0:01', duration: '0:01.1', } }, currentSection) // currentSection.fullscreenTimeline.unshift(initial_curtain_event) } if (currentSection.fullscreenTimeline.length && currentSection.fullscreenTimeline[0].type === 'curtain') { if (currentSection.cover_id) { currentSection.fullscreenTimeline[0].mediaItem = mediaLookup[currentSection.cover_id] } else if (currentSection.media.length) { currentSection.fullscreenTimeline[0].mediaItem = currentSection.media[0].media } currentSection.fullscreenTimeline[0].cover_style = currentSection.cover_style } currentSection.duration = currentSection.end_ts - currentSection.start_ts currentSection.inlineParagraphCount = currentSection.paragraphs.filter(p => !p.hidden).length // console.log(i, currentSection.inlineParagraphCount) }) // Preload chapter cover URLs const chapterCurtainImages = sections .map(section => section.fullscreenTimeline.length && section.fullscreenTimeline[0].mediaItem) .map(section => displayThumbnailURL(section)) .filter(s => !!s) preloadImages(chapterCurtainImages) // console.log(sections) // console.log(footnoteList) dispatch({ type: types.viewer.load_sections, sections, footnoteList }) } const newSection = (annotation, index, mediaIndex) => ({ start_ts: annotation.start_ts, end_ts: 0, title: annotation.text, cover_id: annotation.settings.media_id || 0, cover_style: annotation.settings.cover_style || "cover", media: [], fullscreenTimeline: [], cues: [], mediaCues: {}, index, mediaIndex, no_audio: !!annotation.settings.no_audio, color: annotation.settings.color || 'white', section_nav_color: annotation.settings.section_nav_color || 'white', }) export const makeFullscreenEvent = (index, annotation, currentSection) => { const timing = annotationFadeTimings(annotation) const event = { ...timing, annotation, index, settings: annotation.settings, type: annotation.type, timeline: [], // timelineLookup: {}, } if (annotation.settings.color) { if (annotation.settings.color.name) { event.color = annotation.settings.color } else { event.color = CURTAIN_COLOR_LOOKUP[annotation.settings.color] || CURTAIN_COLOR_LOOKUP.white } } else { event.color = CURTAIN_COLOR_LOOKUP.white } if (annotation.type === 'curtain' && annotation.settings.curtain_style === 'section_heading') { event.section = { title: currentSection.title, index: currentSection.index, } // console.log(event.section) } return event } /* nav UI */ export const setNavStyle = color => dispatch => { dispatch({ type: types.viewer.set_nav_style, color }) } export const setMediaTitle = title => dispatch => { dispatch({ type: types.viewer.set_media_title, title }) } export const showComponent = key => dispatch => { dispatch({ type: types.viewer.toggle_component, key, value: true }) } export const hideComponent = key => dispatch => { dispatch({ type: types.viewer.toggle_component, key, value: false }) } export const showNavComponent = key => dispatch => { dispatch({ type: types.viewer.toggle_nav_component, key, value: true }) } export const hideNavComponent = key => dispatch => { dispatch({ type: types.viewer.toggle_nav_component, key, value: false }) } export const toggleNavComponent = key => dispatch => { dispatch({ type: types.viewer.toggle_nav_component, key, value: !store.getState().viewer[key] }) } export const toggleNavGradient = value => dispatch => { dispatch({ type: types.viewer.toggle_component, key: 'navGradient', value: value }) } export const updateFullscreenStatus = (value, persist, isSingleton) => dispatch => { console.log('fullscreen', value ? 'off' : 'on', persist && 'persist', isSingleton && 'singleton') dispatch({ type: types.viewer.toggle_component, key: 'isFullscreen', value: value }) dispatch({ type: types.viewer.toggle_component, key: 'isFullscreenPersist', value: persist }) dispatch({ type: types.viewer.toggle_component, key: 'isFullscreenSingleton', value: isSingleton }) } export const toggleFullscreenVisible = value => dispatch => { dispatch({ type: types.viewer.toggle_component, key: 'fullscreenVisible', value: value }) } export const toggleComponent = key => dispatch => { hideNavElementsNotMatchedBy(key)(dispatch) const state = store.getState().viewer dispatch({ type: types.viewer.toggle_component, key, value: !state[key] }) } export const hideNavElementsNotMatchedBy = key => dispatch => { const state = store.getState().viewer if (key !== "share" && state.share) { dispatch({ type: types.viewer.toggle_component, key: "share", value: false }) } if (key !== "footnotes" && state.footnotes) { dispatch({ type: types.viewer.toggle_component, key: "footnotes", value: false }) } if (key !== "subscribe" && state.subscribe) { dispatch({ type: types.viewer.toggle_component, key: "subscribe", value: false }) } } export const openTranscript = () => dispatch => { // actions.viewer.hideNavComponent('nav') actions.viewer.hideComponent('checklist') actions.viewer.hideComponent('credits') actions.viewer.toggleComponent('transcript') } export const showCredits = () => dispatch => { actions.viewer.closeGrowl() actions.viewer.showComponent("credits") } /* section / seeking logic */ export const reachedEndOfSection = currentSection => dispatch => { actions.audio.pause() dispatch({ type: types.viewer.reached_end_of_section }) if (currentSection) { // reached end of first section if (currentSection.index === 0) { actions.viewer.openGrowl(GROWL.REACHED_END_OF_FIRST_SECTION) } // reached end of last section } } export const playFromClick = () => dispatch => { const state = store.getState() if (state.audio.play_ts === 0 && state.viewer.currentSection.intro_start_ts) { actions.viewer.seekToTimestamp(state.viewer.currentSection.intro_start_ts) } else { actions.audio.play() } } export const setCurrentSection = (currentSection, nextSection) => dispatch => { dispatch({ type: types.viewer.set_current_section, currentSection, nextSection }) } export const seekToSection = section => dispatch => { actions.audio.seek(section.start_ts) actions.audio.play() actions.viewer.setCurrentSection(section, getNextSection(section)) actions.viewer.hideComponent('nav') actions.viewer.hideComponent('share') } export const seekToMediaItem = (section, mediaItem) => dispatch => { actions.audio.seek(mediaItem.start_ts) actions.audio.play() actions.viewer.setCurrentSection(section, getNextSection(section)) actions.viewer.hideComponent('nav') actions.viewer.hideComponent('checklist') actions.viewer.hideComponent('share') } export const seekToTimestamp = play_ts => dispatch => { actions.audio.seek(play_ts) actions.audio.play() actions.viewer.setSectionFromTimestamp(play_ts) } export const seekToBeginning = () => dispatch => { actions.viewer.hideComponent("credits") actions.viewer.seekToTimestamp(0.0) } export const setSectionFromTimestamp = play_ts => dispatch => { const { sections, currentSection } = store.getState().viewer const insideSection = sections.some((section, i) => { if (floatInRange(section.start_ts, play_ts, section.end_ts)) { if (currentSection !== section) { const nextSection = sections[i+1] actions.viewer.setCurrentSection(section, nextSection) } return true } return false }) if (!insideSection) { actions.viewer.setCurrentSection(sections[sections.length-1], null) } } /* footnotes */ export const openFootnote = (annotation) => dispatch => { // console.log(annotation) dispatch({ type: types.viewer.open_footnote, footnote_id: annotation.footnote_id }) hideNavElementsNotMatchedBy('footnotes')(dispatch) showComponent('footnotes')(dispatch) } /* vitrine modal */ export const openVitrineModal = (media, color, id) => dispatch => { // console.log(media) const index = media.settings.image_order.indexOf(id) dispatch({ type: types.viewer.open_vitrine_modal, media, color, index }) } export const closeVitrineModal = () => dispatch => { dispatch({ type: types.viewer.close_vitrine_modal }) } export const setVitrineIndex = (index) => dispatch => { dispatch({ type: types.viewer.set_vitrine_index, index }) } export const vitrineGo = direction => dispatch => { const { vitrineModal } = store.getState().viewer const { media, index } = vitrineModal const targetIndex = index + direction const shouldClose = (targetIndex < 0) || (targetIndex === media.settings.image_order.length) if (shouldClose) { actions.viewer.closeVitrineModal() } else { actions.viewer.setVitrineIndex(targetIndex) } } /* growl */ export const openGrowl = message => dispatch => { dispatch({ type: types.viewer.open_growl, message }) } export const closeGrowl = () => dispatch => { dispatch({ type: types.viewer.close_growl }) }