diff --git a/js/actionCreators/index.ts b/js/actionCreators/index.ts
index edad6e15..d9880f89 100644
--- a/js/actionCreators/index.ts
+++ b/js/actionCreators/index.ts
@@ -94,7 +94,13 @@ export {
scrollDownFourTracks,
dragSelected
} from "./playlist";
-export { initializePresets } from "./milkdrop";
+export {
+ initializePresets,
+ requestPresetAtIndex,
+ selectRandomPreset,
+ selectNextPreset,
+ togglePresetOverlay
+} from "./milkdrop";
import * as Selectors from "../selectors";
diff --git a/js/actionCreators/milkdrop.ts b/js/actionCreators/milkdrop.ts
index 8a59547a..c23746a8 100644
--- a/js/actionCreators/milkdrop.ts
+++ b/js/actionCreators/milkdrop.ts
@@ -1,6 +1,12 @@
-import { INITIALIZE_PRESETS, GOT_BUTTERCHURN } from "../actionTypes";
-import { Dispatchable } from "../types";
-import Presets from "../components/MilkdropWindow/Presets";
+import {
+ GOT_BUTTERCHURN_PRESETS,
+ GOT_BUTTERCHURN,
+ SELECT_PRESET_AT_INDEX,
+ RESOLVE_PRESET_AT_INDEX,
+ TOGGLE_PRESET_OVERLAY
+} from "../actionTypes";
+import * as Selectors from "../selectors";
+import { Dispatchable, TransitionType } from "../types";
export function initializePresets(presetOptions: any): Dispatchable {
return async dispatch => {
@@ -10,8 +16,9 @@ export function initializePresets(presetOptions: any): Dispatchable {
presetKeys,
minimalPresets
} = await loadInitialDependencies();
+ dispatch({ type: GOT_BUTTERCHURN, butterchurn });
- const presetDefinitions = presetKeys.map((key: string) => {
+ const presets = presetKeys.map((key: string) => {
if (minimalPresets[key] != null) {
return {
type: "BUTTERCHURN_JSON",
@@ -19,32 +26,66 @@ export function initializePresets(presetOptions: any): Dispatchable {
definition: minimalPresets[key]
};
}
- return async () => {
- // TODO: Avoid a race where we try to resolve this promise more than once in parallel.
- const nonMinimalPresets = await loadNonMinimalPresets();
- return {
- type: "BUTTERCHURN_JSON",
- name: key,
- definition: nonMinimalPresets[key]
- };
+ return {
+ type: "LAZY_BUTTERCHURN_JSON",
+ name: key,
+ getDefinition: async () => {
+ // TODO: Avoid a race where we try to resolve this promise more than once in parallel.
+ const nonMinimalPresets = await loadNonMinimalPresets();
+ return nonMinimalPresets[key];
+ }
};
});
- dispatch({
- type: "GOT_BUTTERCHUN_PRESET",
- json: minimalPresets[presetKeys[1]]
- });
+ dispatch({ type: GOT_BUTTERCHURN_PRESETS, presets });
+ dispatch(requestPresetAtIndex(0, TransitionType.IMMEDIATE));
+ };
+}
- setTimeout(() => {
- console.log(minimalPresets[presetKeys[0]]);
- dispatch({
- type: "GOT_BUTTERCHUN_PRESET",
- json: minimalPresets[presetKeys[0]]
- });
- }, 8000);
+export function selectNextPreset(): Dispatchable {
+ return (dispatch, getState) => {
+ const state = getState();
+ const currentPresetIndex = Selectors.getCurrentPresetIndex(state);
+ if (currentPresetIndex == null) {
+ return;
+ }
+ const nextPresetIndex = currentPresetIndex + 1;
+ dispatch(requestPresetAtIndex(nextPresetIndex, TransitionType.DEFAULT));
+ };
+}
- dispatch({ type: GOT_BUTTERCHURN, butterchurn });
- // dispatch({ type: INITIALIZE_PRESETS, presets });
+export function selectRandomPreset(): Dispatchable {
+ return (dispatch, getState) => {
+ const state = getState();
+ const randomIndex = Math.floor(
+ Math.random() * state.milkdrop.presets.length
+ );
+ dispatch(requestPresetAtIndex(randomIndex, TransitionType.DEFAULT));
+ };
+}
+
+export function requestPresetAtIndex(
+ index: number,
+ transitionType: TransitionType
+): Dispatchable {
+ return async (dispatch, getState) => {
+ const state = getState();
+ const preset = state.milkdrop.presets[index];
+ if (preset == null) {
+ // Index might be out of range.
+ return;
+ }
+ switch (preset.type) {
+ case "BUTTERCHURN_JSON":
+ dispatch({ type: SELECT_PRESET_AT_INDEX, index, transitionType });
+ return;
+ case "LAZY_BUTTERCHURN_JSON":
+ const json = await preset.getDefinition();
+ // What if the index has changed?
+ dispatch({ type: RESOLVE_PRESET_AT_INDEX, index, json });
+ dispatch({ type: SELECT_PRESET_AT_INDEX, index, transitionType });
+ return;
+ }
};
}
@@ -88,3 +129,7 @@ async function _fetchPreset(
return { [presetName]: preset };
}
+
+export function togglePresetOverlay(): Dispatchable {
+ return { type: TOGGLE_PRESET_OVERLAY };
+}
diff --git a/js/actionTypes.ts b/js/actionTypes.ts
index bfea95a7..47111ab2 100644
--- a/js/actionTypes.ts
+++ b/js/actionTypes.ts
@@ -77,5 +77,8 @@ export const ENABLE_MEDIA_LIBRARY = "ENABLE_MEDIA_LIBRARY";
export const ENABLE_MILKDROP = "ENABLE_MILKDROP";
export const SET_MILKDROP_DESKTOP = "SET_MILKDROP_DESKTOP";
export const SET_VISUALIZER_STYLE = "SET_VISUALIZER_STYLE";
-export const INITIALIZE_PRESETS = "INITIALIZE_PRESETS";
+export const GOT_BUTTERCHURN_PRESETS = "GOT_BUTTERCHURN_PRESETS";
export const GOT_BUTTERCHURN = "GOT_BUTTERCHURN";
+export const RESOLVE_PRESET_AT_INDEX = "RESOLVE_PRESET_AT_INDEX";
+export const SELECT_PRESET_AT_INDEX = "SELECT_PRESET_AT_INDEX";
+export const TOGGLE_PRESET_OVERLAY = "TOGGLE_PRESET_OVERLAY";
diff --git a/js/components/MilkdropWindow/Background.js b/js/components/MilkdropWindow/Background.tsx
similarity index 69%
rename from js/components/MilkdropWindow/Background.js
rename to js/components/MilkdropWindow/Background.tsx
index 9561d892..2b762e18 100644
--- a/js/components/MilkdropWindow/Background.js
+++ b/js/components/MilkdropWindow/Background.tsx
@@ -1,6 +1,11 @@
import React from "react";
-const Background = props => {
+interface Props {
+ innerRef?: (node: HTMLElement) => void;
+ children: React.ReactNode;
+}
+
+const Background = (props: Props) => {
const { innerRef, ...restProps } = props;
return (
{
height: "100%",
width: "100%"
}}
- tabIndex="0"
- {...restProps}
- />
+ tabIndex={0}
+ >
+ {props.children}
+
);
};
diff --git a/js/components/MilkdropWindow/Desktop.js b/js/components/MilkdropWindow/Desktop.tsx
similarity index 95%
rename from js/components/MilkdropWindow/Desktop.js
rename to js/components/MilkdropWindow/Desktop.tsx
index ee5a1c71..8ff8ffc6 100644
--- a/js/components/MilkdropWindow/Desktop.js
+++ b/js/components/MilkdropWindow/Desktop.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom";
export default class Desktop extends React.Component {
+ _desktopNode?: HTMLElement;
componentWillUnmount() {
document.body.removeChild(this._desktopNode);
this._desktopNode = null;
diff --git a/js/components/MilkdropWindow/MilkdropContextMenu.js b/js/components/MilkdropWindow/MilkdropContextMenu.js
deleted file mode 100644
index e16d2c9c..00000000
--- a/js/components/MilkdropWindow/MilkdropContextMenu.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { Hr, Node } from "../ContextMenu";
-
-const MilkdropContextMenu = props => (
-
-
-
-
-
-
-);
-
-export default MilkdropContextMenu;
diff --git a/js/components/MilkdropWindow/MilkdropContextMenu.tsx b/js/components/MilkdropWindow/MilkdropContextMenu.tsx
new file mode 100644
index 00000000..49fe7d46
--- /dev/null
+++ b/js/components/MilkdropWindow/MilkdropContextMenu.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { Hr, Node } from "../ContextMenu";
+import { connect } from "react-redux";
+import { WINDOWS } from "../../constants";
+import * as Selectors from "../../selectors";
+import * as Actions from "../../actionCreators";
+import { AppState, Dispatch } from "../../types";
+import ContextMenuWraper from "../ContextMenuWrapper";
+
+interface StateProps {
+ desktop: boolean;
+}
+
+interface DispatchProps {
+ toggleFullscreen(): void;
+ closeWindow(): void;
+ toggleDesktop(): void;
+}
+
+type Props = StateProps & DispatchProps;
+
+const MilkdropContextMenu = (props: Props) => (
+ {
+ <>
+
+
+
+
+ >;
+ }}
+ />
+);
+
+const mapStateToProps = (state: AppState): StateProps => ({
+ desktop: Selectors.getMilkdropDesktopEnabled(state)
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
+ closeWindow: () => dispatch(Actions.closeWindow(WINDOWS.MILKDROP)),
+ toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop()),
+ toggleFullscreen: () => {
+ throw new Error("Implement fullscreen");
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MilkdropContextMenu);
diff --git a/js/components/MilkdropWindow/PresetOverlay.js b/js/components/MilkdropWindow/PresetOverlay.js
deleted file mode 100644
index 6b98b31c..00000000
--- a/js/components/MilkdropWindow/PresetOverlay.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import React from "react";
-import { promptForFileReferences } from "../../fileUtils";
-import { clamp } from "../../utils";
-
-const ENTRY_HEIGHT = 14;
-const HEIGHT_PADDING = 15;
-const WIDTH_PADDING = 20;
-
-const LoadingState = () => (
-
- Loading presets
-
-);
-
-const ListWrapper = ({ width, height, children }) => (
-
-);
-
-class PresetOverlay extends React.Component {
- constructor(props) {
- super(props);
- const listIndex = this._listIndexFromPresetIndex(props.currentPreset);
- this.state = {
- selectedListIndex: clamp(listIndex, 0, this._maxListIndex())
- };
- }
-
- componentDidMount() {
- this._unsubscribeFocusedKeyDown = this.props.onFocusedKeyDown(
- this._handleFocusedKeyboardInput
- );
- }
-
- componentWillUnmount() {
- if (this._unsubscribeFocusedKeyDown) {
- this._unsubscribeFocusedKeyDown();
- }
- }
-
- _handleFocusedKeyboardInput = e => {
- switch (e.keyCode) {
- case 38: // up arrow
- this.setState({
- selectedListIndex: Math.max(this.state.selectedListIndex - 1, 0)
- });
- e.stopPropagation();
- break;
- case 40: // down arrow
- this.setState({
- selectedListIndex: Math.min(
- this.state.selectedListIndex + 1,
- this._maxListIndex()
- )
- });
- e.stopPropagation();
- break;
- case 13: // enter
- if (this.state.selectedListIndex === 0) {
- this.loadLocalDir();
- } else {
- this.props.selectPreset(
- this._presetIndexFromListIndex(this.state.selectedListIndex)
- );
- }
- e.stopPropagation();
- break;
- case 27: // escape
- this.props.closeOverlay();
- e.stopPropagation();
- break;
- }
- };
-
- _presetIndexFromListIndex(listIndex) {
- return listIndex - 1;
- }
-
- _listIndexFromPresetIndex(listIndex) {
- return listIndex + 1;
- }
-
- _maxListIndex() {
- // Number of presets, plus one for the "Load Local Directory" option, minus
- // one to convert a length to an index.
- return this.props.presetKeys.length; // - 1 + 1;
- }
-
- async loadLocalDir() {
- const fileReferences = await promptForFileReferences({ directory: true });
- // TODO: Technically there is a race condition here, since the component
- // could get unmounted before the promise resolves.
- this.props.loadPresets(fileReferences);
- }
-
- _renderList() {
- const { presetKeys, currentPreset, height, width } = this.props;
- const { selectedListIndex } = this.state;
-
- const maxVisibleRows = Math.floor((height - HEIGHT_PADDING) / ENTRY_HEIGHT);
- const rowsToShow = Math.floor(maxVisibleRows * 0.75); // Only fill 3/4 of the screen.
- const [startIndex, endIndex] = getRangeCenteredOnIndex(
- this._maxListIndex() + 1, // Add one to convert an index to a length
- rowsToShow,
- selectedListIndex
- );
-
- const presetElms = [];
- for (let i = startIndex; i <= endIndex; i++) {
- const presetIndex = this._presetIndexFromListIndex(i);
- const isSelected = i === selectedListIndex;
- const isCurrent = presetIndex === currentPreset;
- let color;
- if (isSelected) {
- color = isCurrent ? "#FFCC22" : "#FF5050";
- } else {
- color = isCurrent ? "#CCFF03" : "#CCCCCC";
- }
- presetElms.push(
-
- {i === 0 ? "Load Local Directory" : presetKeys[presetIndex]}
-
- );
- }
-
- return (
-
- {presetElms}
-
- );
- }
-
- render() {
- return this.props.presetKeys != null ? (
- this._renderList()
- ) : (
-
- );
- }
-}
-
-// Find a tuple `[startIndex, endIndex]` representing start/end indexes into an
-// array of length `length`, that descripe a range of size up to `rangeSize`
-// where a best effort is made to center `indexToCenter`.
-export function getRangeCenteredOnIndex(length, maxRangeSize, indexToCenter) {
- const rangeSize = Math.min(length, maxRangeSize);
- const halfRangeSize = Math.floor(rangeSize / 2);
- const idealStartIndex = indexToCenter - halfRangeSize;
- const startIndex = clamp(idealStartIndex, 0, length - rangeSize);
- const endIndex = startIndex + rangeSize - 1;
- return [startIndex, endIndex];
-}
-
-export default PresetOverlay;
diff --git a/js/components/MilkdropWindow/PresetOverlay.tsx b/js/components/MilkdropWindow/PresetOverlay.tsx
new file mode 100644
index 00000000..6ad9ad00
--- /dev/null
+++ b/js/components/MilkdropWindow/PresetOverlay.tsx
@@ -0,0 +1,210 @@
+import React from "react";
+import { promptForFileReferences } from "../../fileUtils";
+import * as Selectors from "../../selectors";
+import * as Actions from "../../actionCreators";
+import { clamp } from "../../utils";
+import { AppState, Dispatch, TransitionType } from "../../types";
+import { connect } from "react-redux";
+
+const ENTRY_HEIGHT = 14;
+const HEIGHT_PADDING = 15;
+const WIDTH_PADDING = 20;
+
+const LOADING_STYLE: React.CSSProperties = {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ color: "white",
+ background: "rgba(0.33, 0.33, 0.33, 0.33)"
+};
+
+const OUTER_WRAPPER_STYLE: React.CSSProperties = {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ padding: "15px 10px 0 10px"
+};
+
+const INNER_WRAPPER_STYLE: React.CSSProperties = {
+ display: "inline-block",
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ background: "rgba(0, 0, 0, 0.815)",
+ fontSize: "12px"
+};
+
+interface State {
+ selectedListIndex: number;
+}
+
+interface StateProps {
+ presetKeys: string[];
+ currentPresetIndex: number | null; // Index
+}
+
+interface DispatchProps {
+ requestPresetAtIndex(i: number): void;
+ togglePresetOverlay(): void;
+}
+
+interface OwnProps {
+ height: number;
+ width: number;
+}
+
+type Props = StateProps & DispatchProps & OwnProps;
+
+class PresetOverlay extends React.Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { selectedListIndex: 0 };
+ }
+
+ _presetIndexFromListIndex(listIndex: number) {
+ return listIndex - 1;
+ }
+
+ _listIndexFromPresetIndex(listIndex: number) {
+ return listIndex + 1;
+ }
+
+ _maxListIndex() {
+ // Number of presets, plus one for the "Load Local Directory" option, minus
+ // one to convert a length to an index.
+ return this.props.presetKeys.length; // - 1 + 1;
+ }
+
+ _renderList() {
+ const { presetKeys, currentPresetIndex, height } = this.props;
+ const { selectedListIndex } = this.state;
+
+ const maxVisibleRows = Math.floor((height - HEIGHT_PADDING) / ENTRY_HEIGHT);
+ const rowsToShow = Math.floor(maxVisibleRows * 0.75); // Only fill 3/4 of the screen.
+ const [startIndex, endIndex] = getRangeCenteredOnIndex(
+ this._maxListIndex() + 1, // Add one to convert an index to a length
+ rowsToShow,
+ selectedListIndex
+ );
+
+ const presetElms = [];
+ for (let i = startIndex; i <= endIndex; i++) {
+ const presetIndex = this._presetIndexFromListIndex(i);
+ const isSelected = i === selectedListIndex;
+ const isCurrent = presetIndex === currentPresetIndex;
+ let color: string;
+ if (isSelected) {
+ color = isCurrent ? "#FFCC22" : "#FF5050";
+ } else {
+ color = isCurrent ? "#CCFF03" : "#CCCCCC";
+ }
+ presetElms.push(
+
+ {i === 0 ? "Load Local Directory" : presetKeys[presetIndex]}
+
+ );
+ }
+
+ return presetElms;
+ }
+
+ _handleFocusedKeyboardInput = (e: React.KeyboardEvent) => {
+ switch (e.keyCode) {
+ case 38: // up arrow
+ this.setState({
+ selectedListIndex: Math.max(this.state.selectedListIndex - 1, 0)
+ });
+ e.stopPropagation();
+ break;
+ case 40: // down arrow
+ this.setState({
+ selectedListIndex: Math.min(
+ this.state.selectedListIndex + 1,
+ this._maxListIndex()
+ )
+ });
+ e.stopPropagation();
+ break;
+ case 13: // enter
+ if (this.state.selectedListIndex === 0) {
+ this.loadLocalDir();
+ } else {
+ this.props.requestPresetAtIndex(
+ this._presetIndexFromListIndex(this.state.selectedListIndex)
+ );
+ }
+ e.stopPropagation();
+ break;
+ case 27: // escape
+ this.props.togglePresetOverlay();
+ e.stopPropagation();
+ break;
+ }
+ };
+
+ render() {
+ const { height, width } = this.props;
+ if (this.props.presetKeys == null) {
+ return (
+
+ Loading presets
+
+ );
+ }
+ return (
+
+ );
+ }
+}
+
+// Find a tuple `[startIndex, endIndex]` representing start/end indexes into an
+// array of length `length`, that descripe a range of size up to `rangeSize`
+// where a best effort is made to center `indexToCenter`.
+export function getRangeCenteredOnIndex(
+ length: number,
+ maxRangeSize: number,
+ indexToCenter: number
+): [number, number] {
+ const rangeSize = Math.min(length, maxRangeSize);
+ const halfRangeSize = Math.floor(rangeSize / 2);
+ const idealStartIndex = indexToCenter - halfRangeSize;
+ const startIndex = clamp(idealStartIndex, 0, length - rangeSize);
+ const endIndex = startIndex + rangeSize - 1;
+ return [startIndex, endIndex];
+}
+
+function mapStateToProps(state: AppState): StateProps {
+ return {
+ presetKeys: Selectors.getPresetNames(state),
+ currentPresetIndex: Selectors.getCurrentPresetIndex(state)
+ };
+}
+
+function mapDispatchToProps(dispatch: Dispatch): DispatchProps {
+ return {
+ requestPresetAtIndex: (i: number) => {
+ dispatch(Actions.requestPresetAtIndex(i, TransitionType.DEFAULT));
+ },
+ togglePresetOverlay: () => dispatch(Actions.togglePresetOverlay())
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(PresetOverlay);
diff --git a/js/components/MilkdropWindow/Presets.js b/js/components/MilkdropWindow/Presets.js
deleted file mode 100644
index 12a83758..00000000
--- a/js/components/MilkdropWindow/Presets.js
+++ /dev/null
@@ -1,158 +0,0 @@
-function getRandomIndex(arr) {
- return Math.floor(Math.random() * arr.length);
-}
-function getRandomValue(arr) {
- return arr[getRandomIndex(arr)];
-}
-
-function getLast(arr) {
- return arr[arr.length - 1];
-}
-
-async function readFileAsText(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = function(e) {
- resolve(e.target.result);
- };
- reader.onerror = function(e) {
- reject(e);
- };
-
- reader.readAsText(file);
- });
-}
-
-/**
- * Track a collection of async loaded presets
- *
- * Presets can be changed via `next`, `previous` or `selectIndex`. In each case,
- * a promise is returned. If the promise resolves to `null` it means the
- * selection was canceled by a subsequent request before it could be fulfilled.
- * If the request is successful, the promise resolves to the selected preset.
- *
- * We assume a model where some portion of the preset are supplied at initialization
- * and the remainder can be loaded async via the function `getRest`.
- */
-export default class Presets {
- constructor({
- keys,
- initialPresets,
- getRest,
- presetConverterEndpoint,
- loadConvertPreset,
- randomize = true
- }) {
- this._keys = keys; // Alphabetical list of preset names
- this._presets = initialPresets; // Presets indexed by name
- this._getRest = getRest; // An async function to get the rest of the presets
- this._presetConverterEndpoint = presetConverterEndpoint;
- this._loadConvertPreset = loadConvertPreset;
- this._history = []; // Indexes into _keys
-
- this._randomize = randomize;
-
- // Initialize with a key that we already have.
- const avaliableKeys = Object.keys(initialPresets);
- const currentKey = randomize
- ? getRandomValue(avaliableKeys)
- : avaliableKeys[0];
- this._currentIndex = this._keys.indexOf(currentKey);
- this._history.push(this._currentIndex);
- }
-
- toggleRandomize() {
- this._randomize = !this._randomize;
- }
-
- setRandomize(val) {
- this._randomize = val;
- }
-
- addPresets(presets) {
- const startIdx = this._keys.length;
- this._keys = this._keys.concat(Object.keys(presets));
- const endIndx = this._keys.length;
-
- this._presets = Object.assign(this._presets, presets);
-
- return [startIdx, endIndx];
- }
-
- loadPresets(presets) {
- this._keys = Object.keys(presets);
- this._presets = presets;
- this._history = [];
-
- return this._keys.length;
- }
-
- async next() {
- let idx;
- if (this._randomize || this._history.length === 0) {
- idx = getRandomIndex(this._keys);
- } else {
- idx = (getLast(this._history) + 1) % this._keys.length;
- }
- this._history.push(idx);
- return this._selectIndex(idx);
- }
-
- async previous() {
- if (this._history.length > 1) {
- this._history.pop();
- return this._selectIndex(getLast(this._history));
- }
- // We are at the very beginning. There is no "previous" preset.
- return Promise.resolve();
- }
-
- async selectIndex(idx) {
- // The public version of this method must add to the history
- this._history.push(idx);
- return this._selectIndex(idx);
- }
-
- async _convertPreset(file) {
- const convertPreset = await this._loadConvertPreset();
- return convertPreset(file, this._presetConverterEndpoint);
- }
-
- async _selectIndex(idx) {
- const preset = this._presets[this._keys[idx]];
- if (!preset) {
- const rest = await this._getRest();
- this._presets = Object.assign(this._presets, rest);
- if (getLast(this._history) !== idx) {
- // This selection must be obsolete. Return null so that
- // the caller knows this request got canceled.
- return null;
- }
- }
- if (preset && preset.file) {
- try {
- const fileContents = await readFileAsText(preset.file);
- const convertedPreset = await this._convertPreset(fileContents);
- this._presets[this._keys[idx]] = convertedPreset;
- } catch (e) {
- console.error(e);
- alert(`Unable to convert MilkDrop preset ${this._keys[idx]}`);
- }
- }
- this._currentIndex = idx;
- return this.getCurrent();
- }
-
- getKeys() {
- return this._keys;
- }
-
- getCurrentIndex() {
- return this._currentIndex;
- }
-
- getCurrent() {
- // #matryoshka
- return this._presets[this._keys[this._currentIndex]];
- }
-}
diff --git a/js/components/MilkdropWindow/Visualizer.tsx b/js/components/MilkdropWindow/Visualizer.tsx
index 5d90ed89..6737a7f3 100644
--- a/js/components/MilkdropWindow/Visualizer.tsx
+++ b/js/components/MilkdropWindow/Visualizer.tsx
@@ -9,7 +9,7 @@ interface StateProps {
isEnabledVisualizer: boolean;
playing: boolean;
butterchurn: any;
- trackTitle: string;
+ trackTitle: string | null;
currentPreset: any;
transitionType: TransitionType;
}
@@ -68,6 +68,7 @@ function Visualizer(props: Props) {
// Load presets when they change
useEffect(() => {
+ console.log("Preset change", props);
if (visualizer == null || props.currentPreset == null) {
return;
}
@@ -92,7 +93,7 @@ function Visualizer(props: Props) {
if (!shouldAnimate || visualizer == null) {
return;
}
- let animationFrameRequest = null;
+ let animationFrameRequest: number | null = null;
const loop = () => {
visualizer.render();
animationFrameRequest = window.requestAnimationFrame(loop);
diff --git a/js/components/MilkdropWindow/index.tsx b/js/components/MilkdropWindow/index.tsx
index 042b4761..52c88bdb 100644
--- a/js/components/MilkdropWindow/index.tsx
+++ b/js/components/MilkdropWindow/index.tsx
@@ -14,20 +14,24 @@ import Visualizer from "./Visualizer";
import "../../../css/milkdrop-window.css";
import Background from "./Background";
+import PresetOverlay from "./PresetOverlay";
+
+const MILLISECONDS_BETWEEN_PRESET_TRANSITIONS = 15000;
interface StateProps {
desktop: boolean;
+ overlay: boolean;
}
interface DispatchProps {
closeWindow(): void;
toggleDesktop(): void;
+ togglePresetOverlay(): void;
+ selectRandomPreset(): void;
}
interface OwnProps {
analyser: AnalyserNode;
- height: number;
- width: number;
onFocusedKeyDown(cb: (e: KeyboardEvent) => void): () => void;
}
@@ -51,7 +55,7 @@ function Milkdrop(props: Props) {
// this.props.presets.toggleRandomize();
break;
case 76: // L
- // this.setState({ presetOverlay: !this.state.presetOverlay });
+ props.togglePresetOverlay();
e.stopPropagation();
break;
case 84: // T
@@ -66,10 +70,20 @@ function Milkdrop(props: Props) {
}
});
}, [props.onFocusedKeyDown]);
+
+ // Cycle presets
+ useEffect(() => {
+ const intervalId = setInterval(
+ props.selectRandomPreset,
+ MILLISECONDS_BETWEEN_PRESET_TRANSITIONS
+ );
+ return () => clearImmediate(intervalId);
+ }, [props.selectRandomPreset]);
return (
- {({ height, width }) => (
+ {({ height, width }: { width: number; height: number }) => (
+ {props.overlay && }
)}
@@ -78,12 +92,15 @@ function Milkdrop(props: Props) {
}
const mapStateToProps = (state: AppState): StateProps => ({
- desktop: Selectors.getMilkdropDesktopEnabled(state)
+ desktop: Selectors.getMilkdropDesktopEnabled(state),
+ overlay: Selectors.getPresetOverlayOpen(state)
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
closeWindow: () => dispatch(Actions.closeWindow(WINDOWS.MILKDROP)),
- toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop())
+ toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop()),
+ togglePresetOverlay: () => dispatch(Actions.togglePresetOverlay()),
+ selectRandomPreset: () => dispatch(Actions.selectRandomPreset())
});
export default connect(
diff --git a/js/reducers/milkdrop.ts b/js/reducers/milkdrop.ts
index f24950bf..29c05ae4 100644
--- a/js/reducers/milkdrop.ts
+++ b/js/reducers/milkdrop.ts
@@ -1,45 +1,20 @@
-import { Action } from "../types";
+import { Action, PresetId, Preset } from "../types";
import {
SET_MILKDROP_DESKTOP,
- INITIALIZE_PRESETS,
- GOT_BUTTERCHURN
+ GOT_BUTTERCHURN_PRESETS,
+ GOT_BUTTERCHURN,
+ RESOLVE_PRESET_AT_INDEX,
+ SELECT_PRESET_AT_INDEX,
+ TOGGLE_PRESET_OVERLAY
} from "../actionTypes";
+import * as Utils from "../utils";
import { TransitionType } from "../types";
-// This is what we actually pass to butterchurn
-type ButterchurnPresetJson = {
- type: "BUTTERCHURN_JSON";
- name: string;
- definition: any;
-};
-
-// A URL that points to a Butterchurn preset
-interface ButterchurnPresetUrl {
- type: "BUTTERCHURN_URL";
- url: string;
-}
-
-// A URL that points to a .milk preset
-interface MilkdropPresetUrl {
- type: "MILKDROP_URL";
- url: string;
-}
-
-type PresetDefinition =
- | ButterchurnPresetJson
- | ButterchurnPresetUrl
- | MilkdropPresetUrl;
-
-type LazyPresetDefinition = () => Promise;
-
-type PresetId = string;
-
export interface MilkdropState {
desktop: boolean;
+ overlay: boolean;
presetOrder: PresetId[];
- presetDefinitions: {
- [presetId: string]: PresetDefinition | LazyPresetDefinition;
- };
+ presets: Preset[];
currentPresetIndex: number | null;
butterchurn: any;
transitionType: TransitionType;
@@ -47,8 +22,9 @@ export interface MilkdropState {
const defaultMilkdropState = {
desktop: false,
+ overlay: false,
presetOrder: [],
- presetDefinitions: {},
+ presets: [],
currentPresetIndex: null,
butterchurn: null,
transitionType: TransitionType.DEFAULT
@@ -63,16 +39,39 @@ export const milkdrop = (
return { ...state, desktop: action.enabled };
case GOT_BUTTERCHURN:
return { ...state, butterchurn: action.butterchurn };
- case "GOT_BUTTERCHUN_PRESET":
- const id = "TEMP";
+ case GOT_BUTTERCHURN_PRESETS:
return {
...state,
- presetDefinitions: {
- [id]: action.json
- },
- currentPresetIndex: 0,
- presetOrder: [id]
+ presets: state.presets.concat(
+ action.presets.map(preset => {
+ switch (preset.type) {
+ case "BUTTERCHURN_JSON":
+ case "LAZY_BUTTERCHURN_JSON":
+ return preset;
+ default:
+ throw new Error(`Invalid preset type: ${preset.type}`);
+ }
+ })
+ )
};
+ case RESOLVE_PRESET_AT_INDEX:
+ const preset = state.presets[action.index];
+ return {
+ ...state,
+ presets: Utils.replaceAtIndex(state.presets, action.index, {
+ name: preset.name,
+ type: "BUTTERCHURN_JSON",
+ definition: action.json
+ })
+ };
+ case SELECT_PRESET_AT_INDEX:
+ return {
+ ...state,
+ currentPresetIndex: action.index,
+ transitionType: action.transitionType
+ };
+ case TOGGLE_PRESET_OVERLAY:
+ return { ...state, overlay: !state.overlay };
default:
return state;
}
diff --git a/js/selectors.ts b/js/selectors.ts
index 609e3230..33d5d327 100644
--- a/js/selectors.ts
+++ b/js/selectors.ts
@@ -626,10 +626,25 @@ export function getPresetTransitionType(state: AppState): TransitionType {
return state.milkdrop.transitionType;
}
+export function getCurrentPresetIndex(state: AppState): number | null {
+ return state.milkdrop.currentPresetIndex;
+}
export function getCurrentPreset(state: AppState): any | null {
- const { currentPresetIndex, presetOrder, presetDefinitions } = state.milkdrop;
- if (currentPresetIndex == null) {
+ const index = getCurrentPresetIndex(state);
+ if (index == null) {
return null;
}
- return presetDefinitions[presetOrder[currentPresetIndex]];
+ const preset = state.milkdrop.presets[index];
+ if (preset == null || preset.type !== "BUTTERCHURN_JSON") {
+ return null;
+ }
+ return preset.definition;
+}
+
+export function getPresetNames(state: AppState): string[] {
+ return state.milkdrop.presets.map(preset => preset.name);
+}
+
+export function getPresetOverlayOpen(state: AppState): boolean {
+ return state.milkdrop.overlay;
}
diff --git a/js/types.ts b/js/types.ts
index 7f8ac4f4..376418d3 100644
--- a/js/types.ts
+++ b/js/types.ts
@@ -131,6 +131,41 @@ export interface ButterchurnOptions {
initialButterchurnPresetUrl?: string | null;
}
+// This is what we actually pass to butterchurn
+type ButterchurnPresetJson = {
+ type: "BUTTERCHURN_JSON";
+ name: string;
+ definition: Object;
+};
+
+type LazyButterchurnPresetJson = {
+ type: "LAZY_BUTTERCHURN_JSON";
+ name: string;
+ getDefinition: () => Promise