Created
March 2, 2020 15:40
-
-
Save futurepaul/e4f099c8689815f394e8a2299ce78f26 to your computer and use it in GitHub Desktop.
Code on tape (meta)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useRef } from "react"; | |
function decimalToTime(seconds, duration) { | |
let fmtTime = new Date(1000 * seconds).toISOString().substr(11, 8); | |
if (duration >= 3600) { | |
return fmtTime; | |
} else { | |
//remove the hours | |
fmtTime = fmtTime.substring(3); | |
if (duration >= 600) { | |
return fmtTime; | |
} else { | |
//remove the tens minute | |
return fmtTime.substring(1); | |
} | |
} | |
} | |
const AudioPlayer = React.forwardRef( | |
( | |
{ audioSrcUrl, onClickPlay, onMouseDown, onMouseUp, playing, setPlaying }, | |
audioPlayerEl | |
) => { | |
const [muted, setMuted] = useState(false); | |
const [timeString, setTimeString] = useState("0:00 / 0:00"); | |
const [progress, setProgress] = useState(0); | |
// const audioPlayerEl = ref; | |
const play = () => { | |
onClickPlay(); | |
if (!playing) { | |
try { | |
audioPlayerEl.current.play(); | |
setPlaying(true); | |
} catch (e) { | |
console.error(e); | |
} | |
} else { | |
audioPlayerEl.current.pause(); | |
setPlaying(false); | |
} | |
}; | |
const mute = () => { | |
audioPlayerEl.current.muted = !muted; | |
setMuted(!muted); | |
}; | |
const timeUpdate = e => { | |
let audio = audioPlayerEl.current; | |
setProgress((100 * audio.currentTime) / audio.duration); | |
// Report back to our parent | |
// onTimeUpdate(audio.currentTime); | |
// console.log("current time:" + audio.currentTime); | |
let time = `${decimalToTime( | |
audio.currentTime, | |
audio.duration | |
)} / ${decimalToTime(audio.duration, audio.duration)}`; | |
setTimeString(time); | |
if (audio.ended) setPlaying(false); | |
}; | |
const playHeadInput = evt => { | |
let audio = audioPlayerEl.current; | |
const scrubPercent = evt.target.value; | |
let scrubDestination = (scrubPercent / 100) * audio.duration; | |
audio.currentTime = scrubDestination; | |
}; | |
return ( | |
<> | |
<div className="player"> | |
<audio | |
ref={audioPlayerEl} | |
src={audioSrcUrl} | |
onTimeUpdate={timeUpdate} | |
></audio> | |
<button className={`play ${playing && "playing"}`} onClick={play}> | |
{playing ? "Pause" : "Play"} | |
</button> | |
<div className="time">{timeString}</div> | |
<input | |
className="playHead" | |
value={progress} | |
type="range" | |
min="0" | |
max="100" | |
step=".1" | |
onInput={playHeadInput} | |
onChange={() => {}} | |
onMouseDown={onMouseDown} | |
onMouseUp={onMouseUp} | |
/> | |
<button | |
className={`mute ${muted && "muted"}`} | |
onClick={mute} | |
></button> | |
</div> | |
<style jsx>{` | |
.player { | |
width: 100%; | |
display: flex; | |
flex-direction: row; | |
justify-content: flex-start; | |
align-items: center; | |
} | |
input[type="range"] { | |
height: 1rem; | |
-webkit-appearance: none; | |
flex-grow: 1; | |
padding-top: 1rem; | |
padding-bottom: 1rem; | |
padding-right: 3px; | |
border: 1px solid white; | |
} | |
input[type="range"]:focus { | |
outline: none; | |
} | |
input[type="range"]:-moz-focusring { | |
outline: 1px solid white; | |
outline-offset: -1px; | |
} | |
input[type="range"]::-webkit-slider-runnable-track { | |
height: calc(1rem + 2px); | |
box-shadow: 3px 3px grey; | |
background-image: url(/svg/square.svg); | |
background-size: var(--load-percentage) 100px; | |
background-repeat: no-repeat; | |
border-radius: 0px; | |
border: 1px solid #000000; | |
} | |
input[type="range"]::-moz-range-track { | |
height: calc(1rem + 2px); | |
box-shadow: 3px 3px grey; | |
background-image: url(/svg/square.svg); | |
background-size: var(--load-percentage) 100px; | |
background-repeat: no-repeat; | |
border-radius: 0px; | |
border: 1px solid #000000; | |
} | |
input[type="range"]::-webkit-slider-thumb { | |
height: 1rem; | |
width: 1rem; | |
border-radius: 0px; | |
background: black; | |
-webkit-appearance: none; | |
} | |
input[type="range"]::-moz-range-thumb { | |
height: 1rem; | |
width: 1rem; | |
border-radius: 0px; | |
background: black; | |
-webkit-appearance: none; | |
} | |
.player button { | |
width: 1rem; | |
height: 1rem; | |
border: none; | |
outline: none; | |
background-repeat: no-repeat; | |
background-position: center; | |
padding: 1rem; | |
} | |
.time { | |
width: 6rem; | |
text-align: center; | |
flex: none; | |
} | |
button.play { | |
padding-left: 0rem; | |
background-color: black; | |
-webkit-mask: url(/svg/sharp-play_arrow-24px.svg) no-repeat 50% 50%; | |
mask: url(/svg/sharp-play_arrow-24px.svg) no-repeat 50% 50%; | |
} | |
button.play.playing { | |
-webkit-mask: url(/svg/sharp-pause-24px.svg) no-repeat 50% 50%; | |
mask: url(/svg/sharp-pause-24px.svg) no-repeat 50% 50%; | |
} | |
button.mute { | |
-webkit-mask: url(/svg/sharp-volume_up-24px.svg) no-repeat 50% 50%; | |
background-image: url(/svg/sharp-volume_up-24px.svg); | |
} | |
button.mute.muted { | |
-webkit-mask: url(/svg/sharp-volume_off-24px.svg) no-repeat 50% 50%; | |
background-image: url(/svg/sharp-volume_off-24px.svg); | |
} | |
`}</style> | |
</> | |
); | |
} | |
); | |
export default AudioPlayer; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useRef, useEffect } from "react"; | |
import Editor from "../components/Editor/Editor"; | |
import AudioPlayer from "../components/AudioPlayer"; | |
import Tabs from "../components/Tabs"; | |
import useInterval from "../hooks/useInterval"; | |
function findClosestEvent(scrubTime, events) { | |
const length = events.length; | |
const totalTime = events[length - 1].time; | |
let guess = Math.floor((scrubTime / totalTime) * length); | |
// console.log(`length: ${length}, totalTime: ${totalTime}, guess: ${guess}`); | |
while (guess > 1 && guess < length - 1) { | |
if ( | |
scrubTime <= events[guess + 1].time && | |
scrubTime >= events[guess].time | |
) { | |
break; | |
} else if (scrubTime > events[guess + 1].time) { | |
guess += 1; | |
} else if (scrubTime < events[guess].time) { | |
guess -= 1; | |
} else { | |
break; | |
} | |
} | |
return Math.min(guess, length - 1); | |
} | |
function calculateDelay(startTime, stamp) { | |
let currentTime = performance.now(); | |
let delay = Math.floor(stamp - (currentTime - startTime)); | |
if (delay < 1) { | |
delay = 1; | |
} | |
console.log(`delay: ${delay}, ct: ${currentTime}, st: ${startTime}`); | |
return delay; | |
} | |
const Play = ({ gistID, files, eventLog, audio }) => { | |
// Meta playback state | |
const [playbackStartTime, setPlaybackStartTime] = useState(null); | |
const [playing, setPlaying] = useState(false); | |
const [following, setFollowing] = useState(true); | |
const [interval, setNextInterval] = useState(0); | |
const [isScrubbing, setIsScrubbing] = useState(false); | |
const audioRef = useRef(null); | |
// Current playback state | |
const [cursor, setCursor] = useState({ lineNumber: 1, column: 1 }); | |
const [activeTab, setActiveTab] = useState(0); | |
const [index, setIndex] = useState(0); | |
// This is called after we release the slider of the aduio player. | |
const onPostScrub = () => { | |
audioRef.current.pause(); | |
setNextInterval(null); | |
setPlaying(false); | |
let t = audioRef.current.currentTime; | |
let ms = Math.round(t * 1000); | |
let newIndex = findClosestEvent(ms, eventLog); | |
let event = eventLog[newIndex]; | |
setIndex(newIndex); | |
setActiveTab(event.tab); | |
setCursor(event.cursor); | |
}; | |
const continuePlayback = () => { | |
// let t = audioRef.current.currentTime; | |
// let ms = Math.round(t * 1000); | |
// let newIndex = findClosestEvent(ms, eventLog); | |
if (index < eventLog.length - 1) { | |
let newPlaybackStartTime = performance.now() - eventLog[index].time; | |
let delay = calculateDelay(newPlaybackStartTime, eventLog[index].time); | |
setPlaybackStartTime(newPlaybackStartTime); | |
setNextInterval(delay); | |
} else { | |
setNextInterval(null); | |
setPlaying(false); | |
} | |
}; | |
const startPlaying = () => { | |
if (!playing) { | |
audioRef.current | |
.play() | |
.then(() => { | |
setPlaying(true); | |
continuePlayback(); | |
}) | |
.catch(e => { | |
console.error(e); | |
}); | |
} else { | |
setNextInterval(null); | |
audioRef.current.pause(); | |
setPlaying(false); | |
} | |
}; | |
const onKeyPress = e => { | |
if (e.charCode === 32) { | |
startPlaying(); | |
} | |
}; | |
useInterval( | |
() => { | |
if (index + 1 < eventLog.length) { | |
let currentEvent = eventLog[index]; | |
if (following) { | |
setActiveTab(currentEvent.tab); | |
setCursor(currentEvent.cursor); | |
} | |
let delay = calculateDelay(playbackStartTime, eventLog[index + 1].time); | |
// let errorCalc = | |
// playbackStartTime + eventLog[index].time - performance.now(); | |
// console.log(`error amount: ${errorCalc}`); | |
setNextInterval(delay); | |
setIndex(index + 1); | |
} else { | |
let currentEvent = eventLog[index]; | |
if (following) { | |
setActiveTab(currentEvent.tab); | |
setCursor(currentEvent.cursor); | |
} | |
setPlaying(false); | |
} | |
}, | |
playing ? interval : null | |
); | |
const requestActiveTab = id => { | |
setActiveTab(id); | |
}; | |
//TODO: handle missing audio | |
return ( | |
<div onKeyPress={onKeyPress}> | |
<AudioPlayer | |
onClickPlay={startPlaying} | |
audioSrcUrl={audio} | |
onMouseDown={() => setIsScrubbing(true)} | |
onMouseUp={onPostScrub} | |
ref={audioRef} | |
playing={playing} | |
setPlaying={setPlaying} | |
/> | |
<Tabs | |
activeTab={activeTab} | |
requestActiveTab={requestActiveTab} | |
files={files} | |
/> | |
<Editor | |
gist={files} | |
tabID={activeTab} | |
cursor={cursor} | |
onCursorChange={e => console.log("on cursor change:" + e)} | |
/> | |
<style jsx>{` | |
.nav { | |
display: flex; | |
justify-content: flex-start; | |
align-items: flex-end; | |
border-bottom: 2px solid black; | |
padding-left: 1rem; | |
} | |
`}</style> | |
</div> | |
); | |
}; | |
export default Play; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useContext, useEffect } from "react"; | |
import Editor from "../../components/Editor/Editor"; | |
import RecordControls from "../../components/RecordControls"; | |
import Tabs from "../../components/Tabs"; | |
import WarningBanner from "../../components/WarningBanner"; | |
import EditorContext from "../../context/editor/editorContext"; | |
import Router from "next/router"; | |
import fetch from "isomorphic-unfetch"; | |
import useInterval from "../../hooks/useInterval"; | |
import Layout from "../../components/Layout"; | |
const defaultCursor = { lineNumber: 1, column: 1 }; | |
const Record = ({ gistID, files }) => { | |
// App state | |
const [cursor, setCursor] = useState(defaultCursor); | |
const [activeTab, setActiveTab] = useState(0); | |
const [perTabCursor, setPerTabCursor] = useState([]); | |
// Event recording logic | |
const [eventLog, setEventLog] = useState(null); | |
const [recordingStartTime, setRecordingStartTime] = useState(null); | |
const [isRecording, setIsRecording] = useState(null); | |
const [timeSoFar, setTimeSoFar] = useState(null); | |
// Editor context for forwarding state to the playback preview | |
const editorContext = useContext(EditorContext); | |
const { setGists, setGistID, saveEventLog, recordingError } = editorContext; | |
const setTabAndCursor = (tab, cursor) => { | |
if (isRecording) { | |
let event = { | |
time: Math.floor(performance.now() - recordingStartTime), | |
cursor, | |
tab | |
}; | |
setEventLog(eventLog.concat(event)); | |
} | |
// console.log( | |
// `tab: ${tab}, cursor: { line: ${cursor.lineNumber}, column: ${cursor.column}}` | |
// ); | |
let tempPerTabCursor = perTabCursor; | |
tempPerTabCursor[tab] = cursor; | |
setPerTabCursor(tempPerTabCursor); | |
setActiveTab(tab); | |
setCursor(cursor); | |
}; | |
const onClickRecord = (shouldStart, startTime) => { | |
if (shouldStart && startTime) { | |
setIsRecording(true); | |
setEventLog([{ time: 0, cursor: cursor, tab: activeTab }]); | |
console.log("Starting recording"); | |
setRecordingStartTime(startTime); | |
} else { | |
setIsRecording(false); | |
console.log("Stopping recording"); | |
console.log(eventLog); | |
} | |
}; | |
const onCursorChange = e => { | |
let newCursor = { lineNumber: e.lineNumber, column: e.column }; | |
setTabAndCursor(activeTab, newCursor); | |
}; | |
const requestActiveTab = newTabID => { | |
let cursor = perTabCursor[newTabID] | |
? perTabCursor[newTabID] | |
: defaultCursor; | |
console.log(perTabCursor); | |
setTabAndCursor(newTabID, cursor); | |
}; | |
const gotoPlaybackPreview = () => { | |
setGists(files); | |
setGistID(gistID); | |
saveEventLog(eventLog); | |
Router.push("/play"); | |
}; | |
useInterval( | |
() => { | |
setTabAndCursor(activeTab, cursor); | |
setTimeSoFar(Math.floor(performance.now() - recordingStartTime)); | |
console.log( | |
`Set tab: ${activeTab} and cursor: l: ${cursor.lineNumber}, c: ${cursor.column} using interval` | |
); | |
}, | |
isRecording ? 1000 : null | |
); | |
const success = ( | |
<WarningBanner> | |
Recorded!{" "} | |
<button className="continue" onClick={gotoPlaybackPreview}> | |
{" "} | |
Go to playback preview{" "} | |
</button>{" "} | |
<button className="danger" onClick={() => location.reload()}> | |
Clear recording and start over | |
</button> | |
</WarningBanner> | |
); | |
const microphone_fail = ( | |
<WarningBanner> | |
<strong>Error:</strong> Something went wrong. Did you say yes to the | |
microphone? <button onClick={() => location.reload()}>Start over</button> | |
</WarningBanner> | |
); | |
const browser_fail = ( | |
<WarningBanner> | |
<strong>Error:</strong> Sorry, your browser isn't supported! | |
</WarningBanner> | |
); | |
return ( | |
<Layout title="Record"> | |
{recordingError === "microphone" && microphone_fail} | |
{recordingError === "browser" && browser_fail} | |
{!isRecording && | |
eventLog && | |
eventLog.length > 1 && | |
recordingError === null | |
? success | |
: recordingError === null && ( | |
<RecordControls | |
onClickRecord={onClickRecord} | |
cursor={cursor} | |
timeSoFar={timeSoFar} | |
/> | |
)} | |
<Tabs | |
activeTab={activeTab} | |
requestActiveTab={requestActiveTab} | |
files={files} | |
/> | |
<Editor | |
gist={files} | |
tabID={activeTab} | |
cursor={cursor} | |
onCursorChange={onCursorChange} | |
/> | |
</Layout> | |
); | |
}; | |
const client_id = process.env.GITHUB_CLIENT_ID; | |
const client_secret = process.env.GITHUB_CLIENT_SECRET; | |
Record.getInitialProps = async ctx => { | |
let query = ctx.query.id; | |
try { | |
let url = `https://api.github.com/gists/${query}?client_id=${client_id}&client_secret=${client_secret}`; | |
const res = await fetch(url); | |
let json = await res.json(); | |
let gistFiles = json.files; | |
let files = Object.keys(gistFiles).map(key => gistFiles[key]); | |
return { gistID: query, files: files }; | |
} catch (error) { | |
console.error(error); | |
return { | |
gistID: null, | |
gists: null | |
}; | |
} | |
}; | |
export default Record; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment