mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-21 09:09:01 +00:00
[WIP] Milkdrop rewrite
This commit is contained in:
parent
5a4fcf47a9
commit
d1cfc707ff
17 changed files with 508 additions and 452 deletions
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
|
|
@ -17,9 +22,10 @@ const Background = props => {
|
|||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
tabIndex="0"
|
||||
{...restProps}
|
||||
/>
|
||||
tabIndex={0}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import React from "react";
|
||||
import { Hr, Node } from "../ContextMenu";
|
||||
|
||||
const MilkdropContextMenu = props => (
|
||||
<React.Fragment>
|
||||
<Node
|
||||
onClick={props.toggleFullscreen}
|
||||
label="Fullscreen"
|
||||
hotkey="Alt+Enter"
|
||||
/>
|
||||
<Node
|
||||
onClick={props.toggleDesktop}
|
||||
checked={props.desktopMode}
|
||||
label="Desktop Mode"
|
||||
hotkey="Alt+D"
|
||||
/>
|
||||
<Hr />
|
||||
<Node onClick={props.close} label="Quit" />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
export default MilkdropContextMenu;
|
||||
60
js/components/MilkdropWindow/MilkdropContextMenu.tsx
Normal file
60
js/components/MilkdropWindow/MilkdropContextMenu.tsx
Normal file
|
|
@ -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) => (
|
||||
<ContextMenuWraper
|
||||
onDoubleClick={props.toggleFullscreen}
|
||||
renderContents={() => {
|
||||
<>
|
||||
<Node
|
||||
onClick={props.toggleFullscreen}
|
||||
label="Fullscreen"
|
||||
hotkey="Alt+Enter"
|
||||
/>
|
||||
<Node
|
||||
onClick={props.toggleDesktop}
|
||||
checked={props.desktop}
|
||||
label="Desktop Mode"
|
||||
hotkey="Alt+D"
|
||||
/>
|
||||
<Hr />
|
||||
<Node onClick={props.closeWindow} label="Quit" />
|
||||
</>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
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);
|
||||
|
|
@ -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 = () => (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
color: "white",
|
||||
background: "rgba(0.33, 0.33, 0.33, 0.33)"
|
||||
}}
|
||||
>
|
||||
<span>Loading presets</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ListWrapper = ({ width, height, children }) => (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
padding: "15px 10px 0 10px"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: `${width - WIDTH_PADDING}px`,
|
||||
maxHeight: `${height - HEIGHT_PADDING}px`,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
background: "rgba(0, 0, 0, 0.815)",
|
||||
fontSize: "12px"
|
||||
}}
|
||||
>
|
||||
<ul style={{ listStyleType: "none", padding: 0, margin: 0 }}>
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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(
|
||||
<li key={i} style={{ color, lineHeight: `${ENTRY_HEIGHT}px` }}>
|
||||
{i === 0 ? "Load Local Directory" : presetKeys[presetIndex]}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListWrapper width={width - 20} height={height}>
|
||||
{presetElms}
|
||||
</ListWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.presetKeys != null ? (
|
||||
this._renderList()
|
||||
) : (
|
||||
<LoadingState />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
210
js/components/MilkdropWindow/PresetOverlay.tsx
Normal file
210
js/components/MilkdropWindow/PresetOverlay.tsx
Normal file
|
|
@ -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<Props, State> {
|
||||
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(
|
||||
<li key={i} style={{ color, lineHeight: `${ENTRY_HEIGHT}px` }}>
|
||||
{i === 0 ? "Load Local Directory" : presetKeys[presetIndex]}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return presetElms;
|
||||
}
|
||||
|
||||
_handleFocusedKeyboardInput = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
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 (
|
||||
<div style={LOADING_STYLE}>
|
||||
<span>Loading presets</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={OUTER_WRAPPER_STYLE}
|
||||
onKeyDown={this._handleFocusedKeyboardInput}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
...INNER_WRAPPER_STYLE,
|
||||
width: width - 20 - WIDTH_PADDING,
|
||||
maxHeight: height - HEIGHT_PADDING
|
||||
}}
|
||||
>
|
||||
<ul style={{ listStyleType: "none", padding: 0, margin: 0 }}>
|
||||
{this._renderList()}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -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]];
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<GenWindow title={"Milkdrop"} windowId={WINDOWS.MILKDROP}>
|
||||
{({ height, width }) => (
|
||||
{({ height, width }: { width: number; height: number }) => (
|
||||
<Background>
|
||||
{props.overlay && <PresetOverlay width={width} height={height} />}
|
||||
<Visualizer width={width} height={height} analyser={props.analyser} />
|
||||
</Background>
|
||||
)}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<PresetDefinition>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
52
js/types.ts
52
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<Object>;
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export type PresetDefinition =
|
||||
| ButterchurnPresetJson
|
||||
| LazyButterchurnPresetJson
|
||||
| ButterchurnPresetUrl
|
||||
| MilkdropPresetUrl;
|
||||
|
||||
export type Preset = ButterchurnPresetJson | LazyButterchurnPresetJson;
|
||||
|
||||
export type PresetId = string;
|
||||
|
||||
export interface EqfPreset {
|
||||
name: string;
|
||||
hz60: number;
|
||||
|
|
@ -447,13 +482,24 @@ export type Action =
|
|||
enabled: boolean;
|
||||
}
|
||||
| {
|
||||
type: "INITIALIZE_PRESETS";
|
||||
presets: any;
|
||||
type: "GOT_BUTTERCHURN_PRESETS";
|
||||
presets: PresetDefinition[];
|
||||
}
|
||||
| {
|
||||
type: "GOT_BUTTERCHURN";
|
||||
butterchurn: any;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "RESOLVE_PRESET_AT_INDEX";
|
||||
index: number;
|
||||
json: Object;
|
||||
}
|
||||
| {
|
||||
type: "SELECT_PRESET_AT_INDEX";
|
||||
index: number;
|
||||
transitionType: TransitionType;
|
||||
}
|
||||
| { type: "TOGGLE_PRESET_OVERLAY" };
|
||||
|
||||
export type MediaTagRequestStatus =
|
||||
| "INITIALIZED"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {
|
|||
moveSelected,
|
||||
spliceIn,
|
||||
getFileExtension,
|
||||
makeCachingFilterFunction
|
||||
makeCachingFilterFunction,
|
||||
replaceAtIndex
|
||||
} from "./utils";
|
||||
|
||||
const fixture = (filename: string) =>
|
||||
|
|
@ -463,3 +464,9 @@ describe("makeCachingFilterFunction", () => {
|
|||
expect(newComparisons()).toBe(19);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceAtIndex", () => {
|
||||
test("can replace", () => {
|
||||
expect(replaceAtIndex([1, 2, 3, 4], 2, 0)).toEqual([1, 2, 0, 4]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@ export function spliceIn<T>(original: T[], start: number, newValues: T[]): T[] {
|
|||
return newArr;
|
||||
}
|
||||
|
||||
export function replaceAtIndex<T>(arr: T[], index: number, newValue: T): T[] {
|
||||
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
|
||||
}
|
||||
|
||||
export function debounce(func: Function, delay: number): Function {
|
||||
let timeout: number;
|
||||
let callbackArgs: any[] = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue