diff --git a/src/components/MainLayout/index.js b/src/components/MainLayout/index.js index 3061d5d..abdfc26 100644 --- a/src/components/MainLayout/index.js +++ b/src/components/MainLayout/index.js @@ -42,6 +42,9 @@ export const MainLayout = ({ onChangeTimestamps, enabledTools = defaultEnabledTools, showValues = false, + onStartPlayback, + onStopPlayback, + isPlayingMedia, }) => { const themeColors = useColors() const [activeDurationGroup, setActiveDurationGroup] = useState(null) @@ -173,6 +176,9 @@ export const MainLayout = ({ selectedDurationIndex={selectedDurationIndex} durationGroups={durationGroups} allowCustomLabels={allowCustomLabels} + onStartPlayback={onStartPlayback} + onStopPlayback={onStopPlayback} + isPlayingMedia={isPlayingMedia} /> { /> ) } + +export const AudioPlayback = () => { + const [durationGroups, setDurationGroups] = useState([]) + + const [timestamps, setTimestamps] = useState([]) + + const [isPlayingMedia, setIsPlayingMedia] = useState(false) + + return ( + a[0] - b[0]), + color: solarized.green, + }, + ], + ]} + durationGroups={durationGroups} + timestamps={timestamps} + onChangeTimestamps={setTimestamps} + onChangeDurationGroups={setDurationGroups} + onStartPlayback={() => setIsPlayingMedia(true)} + onStopPlayback={() => setIsPlayingMedia(false)} + isPlayingMedia={isPlayingMedia} + /> + ) +} diff --git a/src/components/MouseTransformHandler/index.js b/src/components/MouseTransformHandler/index.js index df4ae6a..f050189 100644 --- a/src/components/MouseTransformHandler/index.js +++ b/src/components/MouseTransformHandler/index.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react" +import React, { useState, useRef, useEffect, useCallback } from "react" import { styled } from "@material-ui/core/styles" import useEventCallback from "use-event-callback" import useToolMode from "../../hooks/use-tool-mode" @@ -146,14 +146,20 @@ export const MouseTransformHandler = ({ e.preventDefault() }) - // TODO + const containerMountCallback = useCallback((ref) => { + if (ref === null) { + containerRef.current.removeEventListener("wheel", onWheel) + } + containerRef.current = ref + ref.addEventListener("wheel", onWheel, { passive: false }) + }, []) + return ( {children} diff --git a/src/components/ReactTimeSeries/ReactTimeSeries.stories.js b/src/components/ReactTimeSeries/ReactTimeSeries.stories.js index 50b56d3..5634d66 100644 --- a/src/components/ReactTimeSeries/ReactTimeSeries.stories.js +++ b/src/components/ReactTimeSeries/ReactTimeSeries.stories.js @@ -172,3 +172,19 @@ export const LargeTimeNoneFormat = () => { /> ) } + +export const AudioPlayback = () => { + return ( + null} + /> + ) +} diff --git a/src/components/ReactTimeSeries/index.js b/src/components/ReactTimeSeries/index.js index ba3a601..d7fdac6 100644 --- a/src/components/ReactTimeSeries/index.js +++ b/src/components/ReactTimeSeries/index.js @@ -1,10 +1,12 @@ -import React, { useMemo, useState } from "react" +import React, { useMemo, useState, useEffect, useRef } from "react" import { setIn } from "seamless-immutable" import useEventCallback from "use-event-callback" import { useAsyncMemo } from "use-async-memo" import { RecoilRoot } from "recoil" import useGetRandomColorUsingHash from "../../hooks/use-get-random-color-using-hash" import Measure from "react-measure" +import { useSetTimeCursorTime } from "../../hooks/use-time-cursor-time" +import useRootAudioElm from "../../hooks/use-root-audio-elm" import MainLayout from "../MainLayout" @@ -52,6 +54,7 @@ export const ReactTimeSeriesWithoutContext = ({ const timeDataAvailable = [sampleTimeData, audioUrl, csvUrl].some(Boolean) + const [isPlayingMedia, setIsPlayingMedia] = useState(false) const [error, setError] = useState(null) const timeData = useAsyncMemo( async () => { @@ -195,6 +198,43 @@ export const ReactTimeSeriesWithoutContext = ({ if (!widthProp) setWidth(bounds.width) }) + const audioSource = useRef() + + const [, setRootAudioElm] = useRootAudioElm() + const setTimeCursorTime = useSetTimeCursorTime() + + useEffect(() => { + if (audioUrl) { + audioSource.current = new Audio(audioUrl) + setTimeCursorTime(0) + } + }, []) + + const onAudioTimeChanged = useEventCallback(() => { + setTimeCursorTime(audioSource.current.currentTime * 1000) + }) + const onAudioPaused = useEventCallback(() => { + audioSource.current.removeEventListener("timeupdate", onAudioTimeChanged) + audioSource.current.removeEventListener("pause", onAudioPaused) + }) + + const onStartPlayback = useEventCallback(() => { + setIsPlayingMedia(true) + if (audioSource.current) { + audioSource.current.play() + audioSource.current.addEventListener("timeupdate", onAudioTimeChanged) + audioSource.current.addEventListener("pause", onAudioPaused) + setRootAudioElm(audioSource.current) + } + }) + + const onStopPlayback = useEventCallback(() => { + setIsPlayingMedia(false) + if (audioSource.current) { + audioSource.current.pause() + } + }) + if (timeDataLoading) return "loading" // TODO real loader if (!timeData) { @@ -228,6 +268,9 @@ export const ReactTimeSeriesWithoutContext = ({ allowCustomLabels={allowCustomLabels} enabledTools={enabledTools} showValues={showValues} + onStartPlayback={onStartPlayback} + onStopPlayback={onStopPlayback} + isPlayingMedia={isPlayingMedia} /> )} diff --git a/src/components/Timeline/Timeline.stories.js b/src/components/Timeline/Timeline.stories.js index 943013d..cae4eea 100644 --- a/src/components/Timeline/Timeline.stories.js +++ b/src/components/Timeline/Timeline.stories.js @@ -75,3 +75,22 @@ export const TimeWithTextMarkers = (args) => { /> ) } + +export const TimeWithCurrentTimeCursor = (args) => { + const colors = useColors() + return ( + + ) +} diff --git a/src/components/Timeline/index.js b/src/components/Timeline/index.js index 66543b6..a03fa9c 100644 --- a/src/components/Timeline/index.js +++ b/src/components/Timeline/index.js @@ -1,8 +1,14 @@ -import React from "react" +import React, { useRef } from "react" import range from "lodash/range" import { styled } from "@material-ui/core/styles" import useColors from "../../hooks/use-colors" import TimeStamp from "../TimeStamp" +import { + useTimeCursorTime, + useSetTimeCursorTime, +} from "../../hooks/use-time-cursor-time" +import useRootAudioElm from "../../hooks/use-root-audio-elm" +import useEventCallback from "use-event-callback" import { formatTime } from "../../utils/format-time" @@ -11,6 +17,7 @@ const Container = styled("div")(({ width, themeColors }) => ({ overflow: "hidden", position: "relative", height: 64, + cursor: "pointer", borderBottom: `1px solid ${themeColors.Selection}`, color: themeColors.fg, })) @@ -21,6 +28,7 @@ const TimeText = styled("div")(({ x, faded }) => ({ fontSize: 12, fontVariantNumeric: "tabular-nums", position: "absolute", + top: 16, left: x, borderLeft: "1px solid rgba(255,255,255,0.5)", paddingLeft: 4, @@ -28,6 +36,17 @@ const TimeText = styled("div")(({ x, faded }) => ({ opacity: faded ? 0.25 : 0.75, })) +const TimeCursor = styled("div")(({ left, themeColors }) => ({ + position: "absolute", + width: 0, + height: 0, + top: 0, + left: left - 6, + borderLeft: "8px solid transparent", + borderRight: "8px solid transparent", + borderTop: `12px solid ${themeColors.green}`, +})) + const Svg = styled("svg")({ position: "absolute", left: 0, @@ -43,6 +62,7 @@ export const Timeline = ({ gridLineMetrics, onClickTimestamp, onRemoveTimestamp, + timeCursorTime: timeCursorTimeProp, }) => { const themeColors = useColors() const visibleDuration = visibleTimeEnd - visibleTimeStart @@ -51,6 +71,11 @@ export const Timeline = ({ const timeTextTimes = range(timeTextCount).map( (i) => visibleTimeStart + (visibleDuration / timeTextCount) * i ) + const recoilTimeCursorTime = useTimeCursorTime() + const setTimeCursorTime = useSetTimeCursorTime() + const [rootAudioElm] = useRootAudioElm() + const timeCursorTime = + timeCursorTimeProp === undefined ? recoilTimeCursorTime : timeCursorTimeProp const { numberOfMajorGridLines, @@ -58,8 +83,27 @@ export const Timeline = ({ majorGridLinePixelDistance, } = gridLineMetrics + const containerRef = useRef() + + const onClickTimeline = useEventCallback((e) => { + if (!rootAudioElm) return + const { clientX } = e + const pxDistanceFromStart = + clientX - containerRef.current.getBoundingClientRect().left + const time = + (pxDistanceFromStart / width) * (visibleTimeEnd - visibleTimeStart) + + visibleTimeStart + rootAudioElm.currentTime = time / 1000 + setTimeCursorTime(time) + }) + return ( - + {range(timeTextCount).map((timeTextIndex) => ( ) })} + {timeCursorTime !== undefined && ( + + )} ) } diff --git a/src/components/Toolbar/Toolbar.stories.js b/src/components/Toolbar/Toolbar.stories.js index 29b1f3c..b5ceeb9 100644 --- a/src/components/Toolbar/Toolbar.stories.js +++ b/src/components/Toolbar/Toolbar.stories.js @@ -41,6 +41,9 @@ export const Primary = () => { ) ) }} + onStartPlayback={() => null} + onStopPlayback={() => null} + isPlayingMedia={false} /> ) } diff --git a/src/components/Toolbar/index.js b/src/components/Toolbar/index.js index 47c330d..9a3f10d 100644 --- a/src/components/Toolbar/index.js +++ b/src/components/Toolbar/index.js @@ -18,6 +18,8 @@ import NormalSelect from "react-select" import LocationOnIcon from "@material-ui/icons/LocationOn" import TimelapseIcon from "@material-ui/icons/Timelapse" import ZoomInIcon from "@material-ui/icons/ZoomIn" +import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" +import PauseCircleOutlineIcon from "@material-ui/icons/PauseCircleOutline" import Color from "color" const Container = styled("div")(({ themeColors }) => ({ @@ -96,6 +98,9 @@ export const Toolbar = ({ selectedDurationIndex, onChangeSelectedItemLabel, allowCustomLabels = false, + onStartPlayback, + onStopPlayback, + isPlayingMedia = false, }) => { const themeColors = useColors() const [mode, setToolMode] = useToolMode() @@ -219,6 +224,22 @@ export const Toolbar = ({ )} + {onStartPlayback && !isPlayingMedia && ( + + )} + {onStopPlayback && isPlayingMedia && ( + + )}