cleanup code a bit more
This commit is contained in:
parent
07b51dd2f2
commit
1f72542234
25 changed files with 146 additions and 122 deletions
116
src/App/App.tsx
116
src/App/App.tsx
|
|
@ -5,16 +5,18 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from
|
|||
|
||||
import { detectConnections } from "@/statecharts/detect_connections";
|
||||
import { parseStatechart } from "../statecharts/parser";
|
||||
import { BottomPanel } from "./BottomPanel";
|
||||
import { defaultSideBarState, SideBar, SideBarState } from "./SideBar";
|
||||
import { BottomPanel } from "./BottomPanel/BottomPanel";
|
||||
import { defaultSideBarState, SideBar, SideBarState } from "./SideBar/SideBar";
|
||||
import { InsertMode } from "./TopPanel/InsertModes";
|
||||
import { TopPanel } from "./TopPanel/TopPanel";
|
||||
import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
|
||||
import { makeIndividualSetters } from "./makePartialSetter";
|
||||
import { useEditor } from "./useEditor";
|
||||
import { useSimulator } from "./useSimulator";
|
||||
import { useUrlHashState } from "./useUrlHashState";
|
||||
import { makeAllSetters } from "./makePartialSetter";
|
||||
import { useEditor } from "./hooks/useEditor";
|
||||
import { useSimulator } from "./hooks/useSimulator";
|
||||
import { useUrlHashState } from "../hooks/useUrlHashState";
|
||||
import { plants } from "./plants";
|
||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
||||
import { ModalOverlay } from "./Modals/ModalOverlay";
|
||||
|
||||
export type EditHistory = {
|
||||
current: VisualEditorState,
|
||||
|
|
@ -56,9 +58,12 @@ export function App() {
|
|||
|
||||
const persist = useUrlHashState<VisualEditorState | AppState & {editorState: VisualEditorState}>(
|
||||
recoveredState => {
|
||||
if (recoveredState === null) {
|
||||
setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
||||
}
|
||||
// we support two formats
|
||||
// @ts-ignore
|
||||
if (recoveredState.nextID) {
|
||||
else if (recoveredState.nextID) {
|
||||
// old format
|
||||
setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []}));
|
||||
}
|
||||
|
|
@ -77,6 +82,7 @@ export function App() {
|
|||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (editorState !== null) {
|
||||
console.log('persisting state to url');
|
||||
persist({editorState, ...appState});
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -104,7 +110,15 @@ export function App() {
|
|||
|
||||
const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar);
|
||||
|
||||
const setters = makeIndividualSetters(setAppState, Object.keys(appState) as (keyof AppState)[]);
|
||||
// console.log('render app', {ast, plant, appState});
|
||||
// useDetectChange(ast, 'ast');
|
||||
// useDetectChange(plant, 'plant');
|
||||
// useDetectChange(scrollDownSidebar, 'scrollDownSidebar');
|
||||
// useDetectChange(appState, 'appState');
|
||||
// useDetectChange(simulator.time, 'simulator.time');
|
||||
// useDetectChange(simulator.trace, 'simulator.trace');
|
||||
|
||||
const setters = makeAllSetters(setAppState, Object.keys(appState) as (keyof AppState)[]);
|
||||
|
||||
const syntaxErrors = parsed && parsed[1] || [];
|
||||
const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx];
|
||||
|
|
@ -121,63 +135,51 @@ export function App() {
|
|||
|
||||
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
|
||||
|
||||
return <>
|
||||
return <ModalOverlay modal={modal} setModal={setModal}>
|
||||
{/* top-to-bottom: everything -> bottom panel */}
|
||||
<div className="stackVertical" style={{height:'100%'}}>
|
||||
|
||||
{/* Modal dialog */}
|
||||
{modal && <div
|
||||
className="modalOuter"
|
||||
onMouseDown={() => setModal(null)}>
|
||||
<div className="modalInner">
|
||||
<span onMouseDown={e => e.stopPropagation()}>
|
||||
{modal}
|
||||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
{/* left-to-right: main -> sidebar */}
|
||||
<div className="stackHorizontal" style={{flexGrow:1, overflow: "auto"}}>
|
||||
|
||||
{/* top-to-bottom: everything -> bottom panel */}
|
||||
<div className="stackVertical" style={{height:'100%'}}>
|
||||
|
||||
{/* left-to-right: main -> sidebar */}
|
||||
<div className="stackHorizontal" style={{flexGrow:1, overflow: "auto"}}>
|
||||
|
||||
{/* top-to-bottom: top bar, editor */}
|
||||
<div className="stackVertical" style={{flexGrow:1, overflow: "auto"}}>
|
||||
{/* Top bar */}
|
||||
<div
|
||||
className="shadowBelow"
|
||||
style={{flex: '0 0 content'}}
|
||||
>
|
||||
{editHistory && <TopPanel
|
||||
{...{onUndo, onRedo, onRotate, setModal, editHistory, ...simulator, ...setters, ...appState}}
|
||||
/>}
|
||||
{/* top-to-bottom: top bar, editor */}
|
||||
<div className="stackVertical" style={{flexGrow:1, overflow: "auto"}}>
|
||||
{/* Top bar */}
|
||||
<div
|
||||
className="shadowBelow"
|
||||
style={{flex: '0 0 content'}}
|
||||
>
|
||||
{editHistory && <TopPanel
|
||||
{...{onUndo, onRedo, onRotate, setModal, editHistory, ...simulator, ...setters, ...appState}}
|
||||
/>}
|
||||
</div>
|
||||
{/* Editor */}
|
||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||
{editorState && conns && syntaxErrors &&
|
||||
<VisualEditor {...{state: editorState, setState: setEditorState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, makeCheckPoint, ...appState}}/>}
|
||||
</div>
|
||||
</div>
|
||||
{/* Editor */}
|
||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||
{editorState && conns && syntaxErrors &&
|
||||
<VisualEditor {...{state: editorState, setState: setEditorState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, makeCheckPoint, ...appState}}/>}
|
||||
|
||||
{/* Right: sidebar */}
|
||||
<div style={{
|
||||
flex: '0 0 content',
|
||||
borderLeft: '1px solid lightgrey',
|
||||
overflowY: "auto",
|
||||
overflowX: "auto",
|
||||
maxWidth: 'min(400px, 50vw)',
|
||||
}}>
|
||||
<div className="stackVertical" style={{height:'100%'}}>
|
||||
<SideBar {...{...appState, refRightSideBar, ast, plantState, ...simulator, ...setters}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: sidebar */}
|
||||
<div style={{
|
||||
flex: '0 0 content',
|
||||
borderLeft: '1px solid lightgrey',
|
||||
overflowY: "auto",
|
||||
overflowX: "auto",
|
||||
maxWidth: 'min(400px, 50vw)',
|
||||
}}>
|
||||
<div className="stackVertical" style={{height:'100%'}}>
|
||||
<SideBar {...{...appState, refRightSideBar, ast, plantState, ...simulator, ...setters}} />
|
||||
</div>
|
||||
{/* Bottom panel */}
|
||||
<div style={{flex: '0 0 content'}}>
|
||||
{syntaxErrors && <BottomPanel {...{errors: syntaxErrors}}/>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom panel */}
|
||||
<div style={{flex: '0 0 content'}}>
|
||||
{syntaxErrors && <BottomPanel {...{errors: syntaxErrors}}/>}
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
</ModalOverlay>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { TraceableError } from "../statecharts/parser";
|
||||
import { TraceableError } from "../../statecharts/parser";
|
||||
|
||||
import "./BottomPanel.css";
|
||||
|
||||
import logo from "../../artwork/logo-playful.svg";
|
||||
import { PersistentDetailsLocalStorage } from "./PersistentDetails";
|
||||
import logo from "../../../artwork/logo-playful.svg";
|
||||
import { PersistentDetailsLocalStorage } from "../PersistentDetails";
|
||||
|
||||
export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||
const [greeting, setGreeting] = useState(
|
||||
17
src/App/Modals/ModalOverlay.tsx
Normal file
17
src/App/Modals/ModalOverlay.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Dispatch, PropsWithChildren, ReactElement, SetStateAction } from "react";
|
||||
|
||||
export function ModalOverlay(props: PropsWithChildren<{modal: ReactElement|null, setModal: Dispatch<SetStateAction<ReactElement|null>>}>) {
|
||||
return <>
|
||||
{props.modal && <div
|
||||
className="modalOuter"
|
||||
onMouseDown={() => props.setModal(null)}>
|
||||
<div className="modalInner">
|
||||
<span onMouseDown={e => e.stopPropagation()}>
|
||||
{props.modal}
|
||||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{props.children}
|
||||
</>;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { usePersistentState } from "@/App/persistent_state"
|
||||
import { usePersistentState } from "@/hooks/usePersistentState"
|
||||
import { DetailsHTMLAttributes, Dispatch, PropsWithChildren, SetStateAction } from "react";
|
||||
|
||||
type Props = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useAudioContext } from "@/App/useAudioContext";
|
||||
import { useAudioContext } from "@/hooks/useAudioContext";
|
||||
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
|
||||
import { detectConnections } from "@/statecharts/detect_connections";
|
||||
import { parseStatechart } from "@/statecharts/parser";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { RT_Statechart } from "@/statecharts/runtime_types";
|
|||
import { memo, useEffect } from "react";
|
||||
|
||||
import "./Microwave.css";
|
||||
import { useAudioContext } from "../../useAudioContext";
|
||||
import { useAudioContext } from "../../../hooks/useAudioContext";
|
||||
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
|
||||
import { detectConnections } from "@/statecharts/detect_connections";
|
||||
import { parseStatechart } from "@/statecharts/parser";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Statechart } from "@/statecharts/abstract_syntax";
|
|||
import { EventTrigger } from "@/statecharts/label_ast";
|
||||
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
|
||||
import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
|
||||
import { setsEqual } from "@/util/util";
|
||||
|
||||
export type PlantRenderProps<StateType> = {
|
||||
state: StateType,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
|
|||
import { detectConnections } from "@/statecharts/detect_connections";
|
||||
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
|
||||
import { RT_Statechart } from "@/statecharts/runtime_types";
|
||||
import { useAudioContext } from "@/App/useAudioContext";
|
||||
import { useAudioContext } from "@/hooks/useAudioContext";
|
||||
import { memo, useEffect } from "react";
|
||||
import { objectsEqual } from "@/util/util";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Dispatch, memo, SetStateAction, useCallback } from "react";
|
||||
import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
|
||||
import { Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types";
|
||||
import { formatTime } from "../util/util";
|
||||
import { TimeMode, timeTravel } from "../statecharts/time";
|
||||
import { BigStepCause, TraceItem, TraceState } from "./App";
|
||||
import { Statechart, stateDescription } from "../../statecharts/abstract_syntax";
|
||||
import { Mode, RaisedEvent, RT_Event } from "../../statecharts/runtime_types";
|
||||
import { formatTime } from "../../util/util";
|
||||
import { TimeMode, timeTravel } from "../../statecharts/time";
|
||||
import { Environment } from "@/statecharts/environment";
|
||||
import { BigStepCause, TraceItem, TraceState } from "../hooks/useSimulator";
|
||||
|
||||
type RTHistoryProps = {
|
||||
trace: TraceState|null,
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { ConcreteState, UnstableState, stateDescription, Transition } from "../statecharts/abstract_syntax";
|
||||
import { Action, EventTrigger, Expression } from "../statecharts/label_ast";
|
||||
|
||||
import "./AST.css";
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import { memo, useEffect } from "react";
|
||||
import { usePersistentState } from "../../hooks/usePersistentState";
|
||||
import { ConcreteState, stateDescription, Transition, UnstableState } from "../../statecharts/abstract_syntax";
|
||||
import { Action, EventTrigger, Expression } from "../../statecharts/label_ast";
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo";
|
||||
|
||||
export function ShowTransition(props: {transition: Transition}) {
|
||||
return <>➝ {stateDescription(props.transition.tgt)}</>;
|
||||
|
|
@ -46,10 +48,6 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst
|
|||
</li>;
|
||||
});
|
||||
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "./TopPanel/KeyInfo";
|
||||
import { memo, useEffect } from "react";
|
||||
import { usePersistentState } from "./persistent_state";
|
||||
|
||||
export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) {
|
||||
const raiseHandlers = inputEvents.map(({event}) => {
|
||||
|
|
@ -8,14 +8,15 @@ import { Conns } from '@/statecharts/timed_reactive';
|
|||
import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import { Statechart } from '@/statecharts/abstract_syntax';
|
||||
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST';
|
||||
import { Plant } from './Plant/Plant';
|
||||
import { Plant } from '../Plant/Plant';
|
||||
import { checkProperty, PropertyCheckResult } from './check_property';
|
||||
import { Setters } from './makePartialSetter';
|
||||
import { Setters } from '../makePartialSetter';
|
||||
import { RTHistory } from './RTHistory';
|
||||
import { BigStepCause, TraceState } from './useSimulator';
|
||||
import { plants, UniversalPlantState } from './plants';
|
||||
import { BigStepCause, TraceState } from '../hooks/useSimulator';
|
||||
import { plants, UniversalPlantState } from '../plants';
|
||||
import { TimeMode } from '@/statecharts/time';
|
||||
import { PersistentDetails } from './PersistentDetails';
|
||||
import { PersistentDetails } from '../PersistentDetails';
|
||||
import "./SideBar.css";
|
||||
|
||||
type SavedTraces = [string, BigStepCause[]][];
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { RT_Statechart } from "@/statecharts/runtime_types";
|
||||
import { Plant } from "./Plant/Plant";
|
||||
import { TraceItem } from "./useSimulator";
|
||||
import { Plant } from "../Plant/Plant";
|
||||
import { TraceItem } from "../hooks/useSimulator";
|
||||
|
||||
// const endpoint = "http://localhost:15478/check_property";
|
||||
const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
|
||||
|
|
@ -18,10 +18,10 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
|||
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||
import StopIcon from '@mui/icons-material/Stop';
|
||||
import { InsertModes } from "./InsertModes";
|
||||
import { usePersistentState } from "@/App/persistent_state";
|
||||
import { usePersistentState } from "@/hooks/usePersistentState";
|
||||
import { RotateButtons } from "./RotateButtons";
|
||||
import { SpeedControl } from "./SpeedControl";
|
||||
import { TraceState } from "../useSimulator";
|
||||
import { TraceState } from "../hooks/useSimulator";
|
||||
|
||||
export type TopPanelProps = {
|
||||
trace: TraceState | null,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { InsertMode } from "../TopPanel/InsertModes";
|
||||
import { Mode } from "@/statecharts/runtime_types";
|
||||
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
|
||||
import { Arrow, ArrowPart, Diamond, History, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax";
|
||||
import { Connections } from "../../statecharts/detect_connections";
|
||||
import { TraceableError } from "../../statecharts/parser";
|
||||
import { ArcDirection, arcDirection } from "../../util/geometry";
|
||||
import { InsertMode } from "../TopPanel/InsertModes";
|
||||
import { ArrowSVG } from "./ArrowSVG";
|
||||
import { DiamondSVG } from "./DiamondSVG";
|
||||
import { HistorySVG } from "./HistorySVG";
|
||||
import { RountangleSVG } from "./RountangleSVG";
|
||||
import { TextSVG } from "./TextSVG";
|
||||
import { useCopyPaste } from "./useCopyPaste";
|
||||
|
||||
import "./VisualEditor.css";
|
||||
import { useMouse } from "./useMouse";
|
||||
import { useCopyPaste } from "./hooks/useCopyPaste";
|
||||
import { useMouse } from "./hooks/useMouse";
|
||||
|
||||
export type ConcreteSyntax = {
|
||||
rountangles: Rountangle[];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax";
|
||||
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
|
||||
import { Selection, VisualEditorState } from "./VisualEditor";
|
||||
import { Selection, VisualEditorState } from "../VisualEditor";
|
||||
import { addV2D } from "@/util/geometry";
|
||||
|
||||
// const offset = {x: 40, y: 40};
|
||||
|
|
@ -2,10 +2,10 @@ import { rountangleMinSize } from "@/statecharts/concrete_syntax";
|
|||
import { addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "@/util/geometry";
|
||||
import { getBBoxInSvgCoords } from "@/util/svg_helper";
|
||||
import { Dispatch, useCallback, useEffect, useState } from "react";
|
||||
import { MIN_ROUNTANGLE_SIZE } from "../parameters";
|
||||
import { InsertMode } from "../TopPanel/InsertModes";
|
||||
import { Selecting, SelectingState } from "./Selection";
|
||||
import { Selection, VisualEditorState } from "./VisualEditor";
|
||||
import { MIN_ROUNTANGLE_SIZE } from "../../parameters";
|
||||
import { InsertMode } from "../../TopPanel/InsertModes";
|
||||
import { Selecting, SelectingState } from "../Selection";
|
||||
import { Selection, VisualEditorState } from "../VisualEditor";
|
||||
|
||||
export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry";
|
||||
import { HISTORY_RADIUS } from "./parameters";
|
||||
import { HISTORY_RADIUS } from "../parameters";
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect } from "react";
|
||||
import { EditHistory } from "./App";
|
||||
import { VisualEditorState } from "./VisualEditor/VisualEditor";
|
||||
import { EditHistory } from "../App";
|
||||
|
||||
export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) {
|
||||
useEffect(() => {
|
||||
|
|
@ -3,9 +3,9 @@ import { RuntimeError } from "@/statecharts/interpreter";
|
|||
import { BigStep, RaisedEvent } from "@/statecharts/runtime_types";
|
||||
import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Plant } from "./Plant/Plant";
|
||||
import { Plant } from "../Plant/Plant";
|
||||
import { getSimTime, getWallClkDelay, TimeMode } from "@/statecharts/time";
|
||||
import { UniversalPlantState } from "./plants";
|
||||
import { UniversalPlantState } from "../plants";
|
||||
|
||||
type CoupledState = {
|
||||
sc: BigStep,
|
||||
|
|
@ -107,6 +107,7 @@ export function useSimulator(ast: Statechart|null, plant: Plant<any, UniversalPl
|
|||
|
||||
// timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
|
||||
useEffect(() => {
|
||||
// console.log('time effect:', time, currentTraceItem);
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
if (currentTraceItem !== null && cE !== null) {
|
||||
if (currentTraceItem.kind === "bigstep") {
|
||||
|
|
@ -3,16 +3,16 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
|
|||
export function makePartialSetter<T, K extends keyof T>(fullSetter: Dispatch<SetStateAction<T>>, key: K): Dispatch<SetStateAction<T[typeof key]>> {
|
||||
return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => {
|
||||
fullSetter(oldFullValue => {
|
||||
if (typeof newValueOrCallback === 'function') {
|
||||
const newValue = (typeof newValueOrCallback === 'function') ? (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]) : newValueOrCallback as T[K];
|
||||
if (newValue === oldFullValue[key]) {
|
||||
return oldFullValue;
|
||||
}
|
||||
else {
|
||||
return {
|
||||
...oldFullValue,
|
||||
[key]: (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]),
|
||||
[key]: newValue,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...oldFullValue,
|
||||
[key]: newValueOrCallback as T[K],
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
@ -21,16 +21,16 @@ export type Setters<T extends {[key: string]: any}> = {
|
|||
[K in keyof T as `set${Capitalize<Extract<K, string>>}`]: Dispatch<SetStateAction<T[K]>>;
|
||||
}
|
||||
|
||||
export function makeIndividualSetters<T extends {[key: string]: any}>(
|
||||
export function makeAllSetters<T extends {[key: string]: any}>(
|
||||
fullSetter: Dispatch<SetStateAction<T>>,
|
||||
keys: (keyof T)[],
|
||||
): Setters<T> {
|
||||
// @ts-ignore
|
||||
return useMemo(() =>
|
||||
return useMemo(() => {
|
||||
console.log('creating setters for App');
|
||||
// @ts-ignore
|
||||
Object.fromEntries(keys.map((key: string) => {
|
||||
return Object.fromEntries(keys.map((key: string) => {
|
||||
return [`set${key.charAt(0).toUpperCase()}${key.slice(1)}`, makePartialSetter(fullSetter, key)];
|
||||
})),
|
||||
[fullSetter]
|
||||
);
|
||||
}));
|
||||
}, [fullSetter]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
||||
|
||||
// like useState, but it is persisted in localStorage
|
||||
// important: values must be JSON-(de-)serializable
|
||||
export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<SetStateAction<T>>] {
|
||||
const [state, setState] = useState(() => {
|
||||
const recovered = localStorage.getItem(key);
|
||||
let parsed;
|
||||
if (recovered !== null) {
|
||||
try {
|
||||
parsed = JSON.parse(recovered);
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
// console.warn(`failed to recover state for option '${key}'`, e,
|
||||
// '(this is normal when running the app for the first time)');
|
||||
}
|
||||
}
|
||||
return initial;
|
||||
});
|
||||
|
||||
const setStateWrapped = useCallback((val: SetStateAction<T>) => {
|
||||
setState((oldState: T) => {
|
||||
let newVal;
|
||||
if (typeof val === 'function') {
|
||||
// @ts-ignore: i don't understand why 'val' might not be callable
|
||||
newVal = val(oldState);
|
||||
}
|
||||
else {
|
||||
newVal = val;
|
||||
}
|
||||
const serialized = JSON.stringify(newVal);
|
||||
localStorage.setItem(key, serialized);
|
||||
return newVal;
|
||||
});
|
||||
}, [setState]);
|
||||
|
||||
return [state, setStateWrapped];
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { memoize } from "@/util/util";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
|
||||
// I was trying to get the 'microwave running' sound to play gapless on Chrome, and the Web Audio API turned out to be the only thing that worked properly. It has some nice bonus features as well, such as setting the playback rate, and audio filters.
|
||||
|
||||
// The result is a simple Web Audio API wrapper for React:
|
||||
|
||||
export function useAudioContext(speed: number) {
|
||||
const ref = useRef<{ctx: AudioContext, hipass: BiquadFilterNode}>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
const audioCtx = new AudioContext();
|
||||
const hipass = audioCtx.createBiquadFilter();
|
||||
hipass.type = 'highpass';
|
||||
hipass.frequency.value = 20; // Hz (let's not blow up anyone's speakers)
|
||||
hipass.connect(audioCtx.destination);
|
||||
ref.current = { ctx: audioCtx, hipass };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [sounds, setSounds] = useState<AudioBufferSourceNode[]>([]);
|
||||
|
||||
const url2AudioBuf: (url:string) => Promise<AudioBuffer> = useCallback(memoize((url: string) => {
|
||||
return fetch(url)
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(buf => ref.current!.ctx.decodeAudioData(buf));
|
||||
}), [ref.current]);
|
||||
|
||||
function play(url: string, loop: boolean, gain: number = 1) {
|
||||
const srcPromise = url2AudioBuf(url)
|
||||
.then(audioBuf => {
|
||||
const src = ref.current!.ctx.createBufferSource();
|
||||
const gainNode = ref.current!.ctx.createGain();
|
||||
gainNode.gain.value = gain;
|
||||
gainNode.connect(ref.current!.hipass);
|
||||
src.buffer = audioBuf;
|
||||
src.connect(gainNode);
|
||||
src.playbackRate.value = speed;
|
||||
src.loop = loop;
|
||||
src.start();
|
||||
setSounds(sounds => [...sounds, src]);
|
||||
src.addEventListener("ended", () => {
|
||||
setSounds(sounds => sounds.filter(s => s !== src));
|
||||
})
|
||||
return src;
|
||||
});
|
||||
// return callback to stop playing
|
||||
return () => srcPromise.then(src => src.stop());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (speed !== 0) {
|
||||
sounds.forEach(src => {
|
||||
src.playbackRate.value = speed;
|
||||
});
|
||||
ref.current!.ctx.resume();
|
||||
}
|
||||
else {
|
||||
ref.current!.ctx.suspend();
|
||||
}
|
||||
}, [speed]);
|
||||
|
||||
return [play, url2AudioBuf] as [(url: string, loop: boolean, gain?: number) => ()=>void, (url:string)=>void];
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
// persist state in URL hash
|
||||
export function useUrlHashState<T>(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void {
|
||||
|
||||
// recover editor state from URL - we need an effect here because decompression is asynchronous
|
||||
useEffect(() => {
|
||||
console.log('recovering state...');
|
||||
const compressedState = window.location.hash.slice(1);
|
||||
if (compressedState.length === 0) {
|
||||
// empty URL hash
|
||||
console.log("no state to recover");
|
||||
// setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
||||
return;
|
||||
}
|
||||
let compressedBuffer;
|
||||
try {
|
||||
compressedBuffer = Uint8Array.fromBase64(compressedState); // may throw
|
||||
} catch (e) {
|
||||
// probably invalid base64
|
||||
console.error("failed to recover state:", e);
|
||||
// setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
||||
return;
|
||||
}
|
||||
const ds = new DecompressionStream("deflate");
|
||||
const writer = ds.writable.getWriter();
|
||||
writer.write(compressedBuffer).catch(() => {}); // any promise rejections will be detected when we try to read
|
||||
writer.close().catch(() => {});
|
||||
new Response(ds.readable).arrayBuffer()
|
||||
.then(decompressedBuffer => {
|
||||
const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer));
|
||||
recoverCallback(recoveredState);
|
||||
})
|
||||
.catch(e => {
|
||||
// any other error: invalid JSON, or decompression failed.
|
||||
console.error("failed to recover state:", e);
|
||||
// setEditHistory({current: emptyState, history: [], future: []});
|
||||
});
|
||||
}, []);
|
||||
|
||||
function persist(state: T) {
|
||||
const serializedState = JSON.stringify(state);
|
||||
const stateBuffer = new TextEncoder().encode(serializedState);
|
||||
const cs = new CompressionStream("deflate");
|
||||
const writer = cs.writable.getWriter();
|
||||
writer.write(stateBuffer);
|
||||
writer.close();
|
||||
// todo: cancel this promise handler when concurrently starting another compression job
|
||||
new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => {
|
||||
const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64();
|
||||
window.location.hash = "#"+compressedStateString;
|
||||
});
|
||||
}
|
||||
|
||||
return persist;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue