Skip to content

Commit

Permalink
Enable playback shortcut keys on timeseries waveform; fix some darkmo…
Browse files Browse the repository at this point in the history
…de issues; small improvements to undo/redo.
  • Loading branch information
rewbs committed Jul 21, 2023
1 parent 11475ef commit 3e8bb04
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 178 deletions.
53 changes: 28 additions & 25 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,40 @@ import Labs from "./Labs";
import Raw from "./Raw";
import Header from "./components/Header";
import { themeFactory } from "./theme";
import { HotkeysProvider } from 'react-hotkeys-hook';

const App = () => {

const theme = extendTheme(themeFactory());

return <CssVarsProvider theme={theme}>
<CssBaseline />
<GlobalStyles
styles={{
code: {
fontFamily: "source-code-pro, Menlo, Monaco, Consolas, 'Courier New',monospace",
background: theme.vars.palette.codeBackground.main,
wordWrap: "break-word",
boxDecorationBreak: "clone",
padding: ".1rem .1rem .1rem",
borderRadius: ".2rem"
}
}}
/>
<BrowserRouter>
<Header />
<Routes>
<Route path="/" element={<Deforum />} />
<Route path="/deforum" element={<Deforum />} />
<Route path="/browser" element={<Browser />} />
<Route path="/analyser" element={<Analyser />} />
<Route path="/labs" element={<Labs />} />
<Route path="/raw" element={<Raw />} />
<Route path="/functionDocs" element={<FunctionDoc />} />
</Routes>
</BrowserRouter>
<HotkeysProvider initiallyActiveScopes={['main']}>
<CssBaseline />
<GlobalStyles
styles={{
code: {
fontFamily: "source-code-pro, Menlo, Monaco, Consolas, 'Courier New',monospace",
background: theme.vars.palette.codeBackground.main,
wordWrap: "break-word",
boxDecorationBreak: "clone",
padding: ".1rem .1rem .1rem",
borderRadius: ".2rem"
}
}}
/>
<BrowserRouter>
<Header />
<Routes>
<Route path="/" element={<Deforum />} />
<Route path="/deforum" element={<Deforum />} />
<Route path="/browser" element={<Browser />} />
<Route path="/analyser" element={<Analyser />} />
<Route path="/labs" element={<Labs />} />
<Route path="/raw" element={<Raw />} />
<Route path="/functionDocs" element={<FunctionDoc />} />
</Routes>
</BrowserRouter>
</HotkeysProvider>
</CssVarsProvider>;
};

Expand Down
79 changes: 46 additions & 33 deletions src/ParseqUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,31 +205,26 @@ const ParseqUI = (props) => {
// in quick succession.
useDebouncedEffect(() => {
if (autoSaveEnabled && prompts && options && displayedFields && keyframes && managedFields && timeSeries && keyframeLock) {
const savedStatus = saveVersion(activeDocId, getPersistableState());
savedStatus.then((versionIdIfSaved) => {
if (versionIdIfSaved) {
const versionToSave = getPersistableState();
if (recoveredFrom
&& Object.keys(versionToSave)
.filter((k) => k !== 'meta') // exclude this field because it has a timestamp that is expected to change.
.every((k) => equal(versionToSave[k], recoveredFrom[k]))) {
// If the document is identical to the doc we just reverted from, no need to save yet:
console.log("Not saving, would be identical to recovered version.");
} else {
saveVersion(activeDocId, getPersistableState()).then((versionIdIfSaved) => {
if (!versionIdIfSaved) {
// No save occurred.
return;
}
console.log("Non-reversion detected. No longer in a just-recovered state, redo is no longer possible.");
setRecoveredFrom(undefined);
setActiveVersionId(versionIdIfSaved);
setUndoStack((undoStack) => [versionIdIfSaved, ...undoStack]);
// HACK
// We want to reset the undo pointer IFF the user made a non-reversion change.
// We do NOT want to reset the undo pointer if we're only saving after an undo/redo.
// A quick & dirty way to determine this is to store the full state that we last
// recovered from and compare it against the current state...
// This is not memory or CPU efficient, and will have a latency impact when saving
if (recoveredFrom && (!equal(recoveredFrom.reverseRender, reverseRender)
|| !equal(recoveredFrom.keyframeLock, keyframeLock)
|| !equal(recoveredFrom.options, options)
|| !equal(recoveredFrom.managedFields, managedFields)
|| !equal(recoveredFrom.displayedFields, displayedFields)
|| !equal(recoveredFrom.prompts, prompts)
|| !equal(recoveredFrom.keyframes, keyframes)
|| !equal(recoveredFrom.timeSeries, timeSeries))) {
console.log("User made a non-reversion change. Reset revision pointer.", [versionIdIfSaved, ...undoStack], 0);
setRecoveredFrom(undefined);
}
}
});
setLastSaved(Date.now());
setUndoStack((undoStack) => [versionIdIfSaved, ...undoStack]);
setLastSaved(Date.now());
});
};
}
}, 200, [prompts, options, displayedFields, keyframes, autoSaveEnabled, managedFields, timeSeries, keyframeLock, reverseRender, undoStack, recoveredFrom]);

Expand Down Expand Up @@ -1295,6 +1290,29 @@ const ParseqUI = (props) => {

// Footer ------------------------

const debugStatus = useMemo(() => {
if (process.env.NODE_ENV === 'development') {
const undoStackSummary = undoStack.map((v, idx) => {
return v.split("-")[1];
});
console.log("undostack", undoStackSummary)
const recoveredIdx = (recoveredFrom) ? undoStack.findIndex((v) => v === recoveredFrom.versionId) : -1;

return <Alert severity="info">
<ul>
<li>Current version: <code>{activeVersionId}</code>; recovered from: <code>{recoveredFrom?.versionId || 'None'}</code></li>
<li>Undo stack size: {undoStack.length}; "recovered from" index: {recoveredIdx}</li>
<li>Undos available: { (recoveredIdx>=0) ? undoStack.length-recoveredIdx : 0}; Redos available: { (recoveredIdx>=0) ? recoveredIdx : 0}</li>
<li>Next undo: <code>{ (recoveredIdx>-1 && recoveredIdx<undoStack.length) ? undoStackSummary[recoveredIdx+1] : 'None' }</code>; Next redo: <code>{ recoveredIdx>0 ? undoStackSummary[recoveredIdx-1] : 'None' }</code></li>
</ul>

</Alert>
} else {
return <></>;
}
}, [undoStack, recoveredFrom, activeVersionId]);


const renderStatus = useMemo(() => {
let animated_fields = getAnimatedFields(renderedData);
let uses2d = defaultFields.filter(f => f.labels.some(l => l === '2D') && animated_fields.includes(f));
Expand Down Expand Up @@ -1378,6 +1396,7 @@ const ParseqUI = (props) => {
<Grid xs={6}>
<InitialisationStatus status={initStatus} alignItems='center' />
{renderStatus}
{(process.env.NODE_ENV === 'development') && debugStatus}
</Grid>
<Grid xs={2}>
<Stack direction={'column'}>
Expand Down Expand Up @@ -1421,7 +1440,7 @@ const ParseqUI = (props) => {
</Stack>
</Grid>
</Grid>
, [renderStatus, initStatus, renderButton, renderedDataJsonString, activeDocId, autoUpload, needsRender, uploadStatus, autoRender, pinFooter]);
, [renderStatus, initStatus, renderButton, renderedDataJsonString, activeDocId, autoUpload, needsRender, uploadStatus, autoRender, pinFooter, debugStatus]);


const stickyFooter = useMemo(() => <Paper
Expand Down Expand Up @@ -1451,35 +1470,29 @@ const ParseqUI = (props) => {
const idx = (recoveredFrom) ? undoStack.findIndex((v) => v === recoveredFrom.versionId) : 0;
if (idx === -1 || idx >= undoStack.length-1) {
console.log("Cannot undo any further - try using the revert dialog.")
console.log("undostack", undoStack, idx)
return;
}
const versionToLoad = undoStack[idx+1]
loadVersion(activeDocId, versionToLoad).then((loaded) => {
setPersistableState(loaded);
console.log("undostack", undoStack)

//HACK - track the full state that we recovered from, to see whether we deviate from it.
setRecoveredFrom(_.cloneDeep(loaded));
});
}, {preventDefault:true}, [loadVersion, setPersistableState, undoStack, activeDocId])
}, {preventDefault:true, scopes:['main']}, [loadVersion, setPersistableState, undoStack, activeDocId, recoveredFrom])

useHotkeys('shift+mod+z', () => {
const idx = (recoveredFrom) ? undoStack.findIndex((v) => v === recoveredFrom.versionId) : 0;
if (idx <=0) {
console.log("Cannot redo any further - try using the revert dialog.")
console.log("undostack", undoStack, idx)
return;
}
const versionToLoad = undoStack[idx-1]
loadVersion(activeDocId, versionToLoad).then((loaded) => {
setPersistableState(loaded);
console.log("undostack", undoStack)

//HACK - track the full state that we recovered from, to see whether we deviate from it.
setRecoveredFrom(_.cloneDeep(loaded));
});
}, {preventDefault:true}, [loadVersion, setPersistableState, undoStack, activeDocId])
}, {preventDefault:true, scopes:['main']}, [loadVersion, setPersistableState, undoStack, activeDocId, recoveredFrom])

//////////////////////////////////////////
// Main layout
Expand Down
74 changes: 46 additions & 28 deletions src/components/AudioWaveform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Box, Alert, Typography, Button, Stack, TextField, MenuItem, Tab, Tabs,
import Fade from '@mui/material/Fade';
import Grid from '@mui/material/Unstable_Grid2';
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { WaveForm, WaveSurfer } from "wavesurfer-react";
import { WaveForm, WaveSurfer } from "wavesurfer-react"; // TODO: react wrapper isn't that useful, consider removing so we can upgrade to ws7
//@ts-ignore
import TimelinePlugin from "wavesurfer.js/dist/plugin/wavesurfer.timeline.min";
//@ts-ignore
Expand Down Expand Up @@ -150,6 +150,19 @@ export function AudioWaveform(props: AudioWaveformProps) {

const debouncedOnCursorMove = useMemo(() => debounce(props.onCursorMove, 100), [props]);

// Update the colours manually on palette change.
// This is necessary because we are not recreating wavesurfer that often
useEffect(() => {
if (wavesurferRef.current) {
// @ts-ignore - type definition is wrong?
wavesurferRef.current.setWaveColor([palette.waveformStart.main, palette.waveformEnd.main]);
// @ts-ignore - type definition is wrong?
wavesurferRef.current.setProgressColor([palette.waveformProgressMaskStart.main, palette.waveformProgressMaskEnd.main]);
wavesurferRef.current.setCursorColor(palette.success.light);
}
}, [palette]);


const handleDoubleClick = useCallback((event: any) => {
const time = wavesurferRef.current?.getCurrentTime();
//@ts-ignore
Expand Down Expand Up @@ -356,11 +369,11 @@ export function AudioWaveform(props: AudioWaveformProps) {
function playPause(from : number = -1, pauseIfPlaying = true) {
if (isPlaying && pauseIfPlaying) {
wavesurferRef.current?.pause();
setCapturedPos(wavesurferRef.current?.getCurrentTime() || 0);
} else {
if (from>=0) {
wavesurferRef.current?.setCurrentTime(from);
} if (!isPlaying) {
setCapturedPos(wavesurferRef.current?.getCurrentTime() || 0);
wavesurferRef.current?.play();
}
}
Expand Down Expand Up @@ -506,9 +519,7 @@ export function AudioWaveform(props: AudioWaveformProps) {
primaryColor: palette.graphBorder.dark,
secondaryColor: palette.graphBorder.light,
primaryFontColor: palette.graphFont.main,
secondaryFontColor: palette.graphFont.light,
fontFamily: 'Arial',
fontSize: 10,
secondaryFontColor: palette.graphFont.light,
}));
wavesurferRef.current.initPlugin('timeline');
// HACK to force the timeline position to update.
Expand Down Expand Up @@ -646,28 +657,31 @@ export function AudioWaveform(props: AudioWaveformProps) {
props.onAddKeyframes(frames, infoLabel);
}

useHotkeys('space', () => {
playPause();
}, {preventDefault:true}, [playPause]);

useHotkeys('shift+space', () => {
playPause(0, false);
}, {preventDefault:true}, [playPause]);

useHotkeys('ctrl+space', () => {
playPause(capturedPos, false);
}, {preventDefault:true}, [playPause, capturedPos]);

useHotkeys('shift+a', () => {
const time = wavesurferRef.current?.getCurrentTime();
//@ts-ignore
const newMarkers = [...manualEvents, time].sort((a, b) => a - b)
//@ts-ignore
setManualEvents(newMarkers);
}, {preventDefault:true}, [manualEvents])



useHotkeys('space',
() => playPause(),
{preventDefault:true, scopes: ['main']},
[playPause]);

useHotkeys('shift+space',
() => playPause(0, false),
{preventDefault:true, scopes: ['main']},
[playPause]);

useHotkeys('ctrl+space',
() => playPause(capturedPos, false),
{preventDefault:true, scopes: ['main']},
[playPause, capturedPos]);

useHotkeys('shift+a',
() => {
const time = wavesurferRef.current?.getCurrentTime();
//@ts-ignore
const newMarkers = [...manualEvents, time].sort((a, b) => a - b)
//@ts-ignore
setManualEvents(newMarkers);
},
{preventDefault:true, scopes: ['main']},
[manualEvents])

return <>
<Grid container>
Expand Down Expand Up @@ -714,7 +728,11 @@ export function AudioWaveform(props: AudioWaveformProps) {
autoCenter={false}
interact={true}
cursorColor={palette.success.light}
cursorWidth={3}
// @ts-ignore - type definition is wrong?
waveColor={[palette.waveformStart.main, palette.waveformEnd.main]}
// @ts-ignore - type definition is wrong?
progressColor={[palette.waveformProgressMaskStart.main, palette.waveformProgressMaskEnd.main]}
cursorWidth={1}
/>
<div id="timeline" />
<div style={{display:showSpectrogram? 'block' : 'none'}} id="spectrogram" />
Expand Down
20 changes: 17 additions & 3 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ export default function Header() {
const displayBranch = (!GIT_BRANCH || GIT_BRANCH === 'master') ? '' : `Branch: ${GIT_BRANCH};`;
const commitLink = <Link href={"https://github.com/rewbs/sd-parseq/commit/" + GIT_COMMIT_HASH}>{GIT_COMMIT_SHORTHASH}</Link>
const changeLogLink = <Link href={"https://github.com/rewbs/sd-parseq/commits/" + (GIT_BRANCH ?? '')}>all changes</Link>
const environment = (process.env.NODE_ENV === 'development' ? 'dev' : getEnvFromHostname()) ;

function getEnvFromHostname() {
const hostname = window.location.hostname;
if (hostname.includes('--dev')) {
return 'dev (hosted)';
}
if (hostname.includes('--staging')) {
return 'staging (hosted)';
}
if (hostname === 'sd-parseq.web.app') {
return 'production';
}
return 'unknown';
}

const { colorScheme, setColorScheme } = useColorScheme();
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
Expand Down Expand Up @@ -67,7 +82,7 @@ export default function Header() {
<h2>
Parseq <small>v{getVersionNumber()}</small>
<Typography fontSize='0.4em'>
[{process.env.NODE_ENV}] {displayBranch} Built {displayDate} ({commitLink} - {changeLogLink})
[{environment}] {displayBranch} Built {displayDate} ({commitLink} - {changeLogLink})
</Typography>
</h2>
<Box display='none'>
Expand All @@ -80,9 +95,8 @@ export default function Header() {
</Grid>
<Grid xs={6} display='flex' justifyContent="right">
<Stack justifyContent="right" gap={1} alignItems={{ sm: 'stretch', md: 'center' }} direction={{ xs: 'column-reverse', sm: 'column-reverse', md: 'row' }}>
{/* <Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" clickable onClick={() => updateDarkMode(!darkMode)} icon={<FontAwesomeIcon icon={darkMode?faLightbulb:faMoon} />} label={(darkMode?"Light":"(wip) Dark")+" Mode"}/> */}
{/* @ts-ignore */}
<Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" clickable icon={<FontAwesomeIcon icon={colorScheme === 'dark'?faLightbulb:faMoon} />} label={(colorScheme === 'dark'?"Light":"(wip) Dark")+" Mode"}
<Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" clickable icon={<FontAwesomeIcon icon={colorScheme === 'dark'?faLightbulb:faMoon} />} label={(colorScheme === 'dark'?"Light":"Dark")+" Mode"}
onClick={() => {
console.log("Setting color scheme");
const newColorScheme = colorScheme === 'dark' ? 'light' : 'dark';
Expand Down
Loading

0 comments on commit 3e8bb04

Please sign in to comment.