mirror of
https://github.com/captbaritone/webamp.git
synced 2026-08-01 14:33:38 +00:00
Rewrite visualizer to use hooks
This commit is contained in:
parent
59666b8699
commit
5a4fcf47a9
8 changed files with 317 additions and 456 deletions
|
|
@ -4,30 +4,47 @@ import Presets from "../components/MilkdropWindow/Presets";
|
|||
|
||||
export function initializePresets(presetOptions: any): Dispatchable {
|
||||
return async dispatch => {
|
||||
const [
|
||||
{ butterchurn, presetKeys, minimalPresets },
|
||||
initialPreset
|
||||
] = await Promise.all([
|
||||
presetOptions.loadInitialDependencies(),
|
||||
_loadInitialPreset(presetOptions)
|
||||
]);
|
||||
const { loadInitialDependencies, loadNonMinimalPresets } = presetOptions;
|
||||
const {
|
||||
butterchurn,
|
||||
presetKeys,
|
||||
minimalPresets
|
||||
} = await loadInitialDependencies();
|
||||
|
||||
const presets = new Presets({
|
||||
keys: presetKeys,
|
||||
initialPresets: minimalPresets,
|
||||
getRest: presetOptions.loadNonMinimalPresets,
|
||||
presetConverterEndpoint: presetOptions.presetConverterEndpoint,
|
||||
loadConvertPreset: presetOptions.loadConvertPreset
|
||||
const presetDefinitions = presetKeys.map((key: string) => {
|
||||
if (minimalPresets[key] != null) {
|
||||
return {
|
||||
type: "BUTTERCHURN_JSON",
|
||||
name: key,
|
||||
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]
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
if (initialPreset) {
|
||||
const presetIndices = presets.addPresets(initialPreset);
|
||||
const presetIndex = presetIndices[0];
|
||||
await presets.selectIndex(presetIndex);
|
||||
}
|
||||
dispatch({
|
||||
type: "GOT_BUTTERCHUN_PRESET",
|
||||
json: minimalPresets[presetKeys[1]]
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(minimalPresets[presetKeys[0]]);
|
||||
dispatch({
|
||||
type: "GOT_BUTTERCHUN_PRESET",
|
||||
json: minimalPresets[presetKeys[0]]
|
||||
});
|
||||
}, 8000);
|
||||
|
||||
dispatch({ type: GOT_BUTTERCHURN, butterchurn });
|
||||
dispatch({ type: INITIALIZE_PRESETS, presets });
|
||||
// dispatch({ type: INITIALIZE_PRESETS, presets });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -44,25 +61,6 @@ function _presetNameFromURL(url: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
async function _loadInitialPreset({
|
||||
initialButterchurnPresetUrl,
|
||||
initialMilkdropPresetUrl
|
||||
}: {
|
||||
initialButterchurnPresetUrl: string;
|
||||
initialMilkdropPresetUrl: string;
|
||||
}) {
|
||||
if (initialButterchurnPresetUrl && initialMilkdropPresetUrl) {
|
||||
alert(
|
||||
"Unable to handle both milkdropPresetUrl and butterchurnPresetUrl. Please specify one or the other."
|
||||
);
|
||||
} else if (initialButterchurnPresetUrl) {
|
||||
return _fetchPreset(initialButterchurnPresetUrl, { isButterchurn: true });
|
||||
} else if (initialMilkdropPresetUrl) {
|
||||
return _fetchPreset(initialMilkdropPresetUrl, { isButterchurn: false });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function _fetchPreset(
|
||||
presetUrl: string,
|
||||
{ isButterchurn }: { isButterchurn: boolean }
|
||||
|
|
|
|||
|
|
@ -1,283 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as Selectors from "../../selectors";
|
||||
import DropTarget from "../DropTarget";
|
||||
import PresetOverlay from "./PresetOverlay";
|
||||
|
||||
const USER_PRESET_TRANSITION_SECONDS = 5.7;
|
||||
const PRESET_TRANSITION_SECONDS = 2.7;
|
||||
const MILLISECONDS_BETWEEN_PRESET_TRANSITIONS = 15000;
|
||||
|
||||
function transitionTimeFromTransitionType(transitionType) {
|
||||
switch (transitionType) {
|
||||
case "DEFAULT":
|
||||
return PRESET_TRANSITION_SECONDS;
|
||||
case "IMMEDIATE":
|
||||
return 0;
|
||||
case "USER_PRESET":
|
||||
return USER_PRESET_TRANSITION_SECONDS;
|
||||
}
|
||||
}
|
||||
|
||||
class Milkdrop extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.__debugState = "CONSTRUCTED";
|
||||
this.__updates = 0;
|
||||
this.state = {
|
||||
isFullscreen: false,
|
||||
presetOverlay: false
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this._initializeIfNeeded();
|
||||
}
|
||||
|
||||
async _initializeIfNeeded() {
|
||||
if (this.visualizer || !this.props.butterchurn || !this.props.presets) {
|
||||
return;
|
||||
}
|
||||
this.visualizer = this.props.butterchurn.createVisualizer(
|
||||
this.props.analyser.context,
|
||||
this._canvasNode,
|
||||
{
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
meshWidth: 32,
|
||||
meshHeight: 24,
|
||||
pixelRatio: window.devicePixelRatio || 1
|
||||
}
|
||||
);
|
||||
if (this.visualizer == null) {
|
||||
// https://github.com/captbaritone/webamp/issues/731
|
||||
throw new Error("Visualizer not initialized. WAT.");
|
||||
}
|
||||
this.__debugState = "VISUALIZER_CREATED";
|
||||
this.visualizer.connectAudio(this.props.analyser);
|
||||
this.presetCycle = true;
|
||||
this.selectPreset(this.props.presets.getCurrent(), 0);
|
||||
|
||||
// Kick off the animation loop
|
||||
const loop = () => {
|
||||
if (this.props.playing && this.props.isEnabledVisualizer) {
|
||||
this.visualizer.render();
|
||||
}
|
||||
this._animationFrameRequest = window.requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
|
||||
this._unsubscribeFocusedKeyDown = this.props.onFocusedKeyDown(
|
||||
this._handleFocusedKeyboardInput
|
||||
);
|
||||
if (this.visualizer == null) {
|
||||
// https://github.com/captbaritone/webamp/issues/731
|
||||
throw new Error("Visualizer not initialized after await. WAT.");
|
||||
}
|
||||
this.__debugState = "MOUNT_ENDED";
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.__debugState = "WILL_UNMOUNT";
|
||||
this._pauseViz();
|
||||
this._stopCycling();
|
||||
if (this._unsubscribeFocusedKeyDown) {
|
||||
this._unsubscribeFocusedKeyDown();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this._initializeIfNeeded();
|
||||
if (
|
||||
this.props.width !== prevProps.width ||
|
||||
this.props.height !== prevProps.height
|
||||
) {
|
||||
this.visualizer.setRendererSize(this.props.width, this.props.height);
|
||||
}
|
||||
|
||||
if (this.props.trackTitle !== prevProps.trackTitle) {
|
||||
this.visualizer.launchSongTitleAnim(this.props.trackTitle);
|
||||
}
|
||||
}
|
||||
|
||||
_pauseViz() {
|
||||
if (this._animationFrameRequest) {
|
||||
window.cancelAnimationFrame(this._animationFrameRequest);
|
||||
this._animationFrameRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
_stopCycling() {
|
||||
if (this.cycleInterval) {
|
||||
clearInterval(this.cycleInterval);
|
||||
this.cycleInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
_restartCycling() {
|
||||
this._stopCycling();
|
||||
|
||||
if (this.presetCycle) {
|
||||
this.cycleInterval = setInterval(() => {
|
||||
this._nextPreset(PRESET_TRANSITION_SECONDS);
|
||||
}, MILLISECONDS_BETWEEN_PRESET_TRANSITIONS);
|
||||
}
|
||||
}
|
||||
|
||||
_handleFocusedKeyboardInput = e => {
|
||||
switch (e.keyCode) {
|
||||
case 32: // spacebar
|
||||
this._nextPreset(USER_PRESET_TRANSITION_SECONDS);
|
||||
break;
|
||||
case 8: // backspace
|
||||
this._prevPreset(0);
|
||||
break;
|
||||
case 72: // H
|
||||
this._nextPreset(0);
|
||||
break;
|
||||
case 82: // R
|
||||
this.props.presets.toggleRandomize();
|
||||
break;
|
||||
case 76: // L
|
||||
this.setState({ presetOverlay: !this.state.presetOverlay });
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 84: // T
|
||||
this.visualizer.launchSongTitleAnim(this.props.trackTitle);
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 145: // scroll lock
|
||||
case 125: // F14 (scroll lock for OS X)
|
||||
this.presetCycle = !this.presetCycle;
|
||||
this._restartCycling();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
_addNewPresets(files) {
|
||||
const presetKeys = this.props.presets.getKeys();
|
||||
const presets = {};
|
||||
const presetIndices = [];
|
||||
files.forEach(file => {
|
||||
const fileName = file.name;
|
||||
const presetName = fileName.substring(0, fileName.length - 5); // remove .milk
|
||||
const presetIdx = presetKeys.indexOf(presetName);
|
||||
if (presetIdx >= 0) {
|
||||
presetIndices.push(presetIdx);
|
||||
} else {
|
||||
presets[presetName] = { file };
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(presets).length > 0) {
|
||||
const filePresetIndices = this.props.presets.addPresets(presets);
|
||||
for (let j = filePresetIndices[0]; j < filePresetIndices[1]; j++) {
|
||||
presetIndices.push(j);
|
||||
}
|
||||
}
|
||||
|
||||
return presetIndices;
|
||||
}
|
||||
|
||||
async _handleDrop(e) {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const milkFiles = Array.from(files).filter(file =>
|
||||
file.name.endsWith(".milk")
|
||||
);
|
||||
if (milkFiles.length === 0) {
|
||||
alert("Visualizer only supports .milk files");
|
||||
return;
|
||||
}
|
||||
|
||||
const presetIndices = this._addNewPresets(milkFiles);
|
||||
this.selectPreset(
|
||||
await this.props.presets.selectIndex(presetIndices[0]),
|
||||
PRESET_TRANSITION_SECONDS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async _nextPreset(blendTime) {
|
||||
this.selectPreset(await this.props.presets.next(), blendTime);
|
||||
}
|
||||
|
||||
async _prevPreset(blendTime) {
|
||||
this.selectPreset(await this.props.presets.previous(), blendTime);
|
||||
}
|
||||
|
||||
selectPreset(preset, blendTime) {
|
||||
if (preset != null) {
|
||||
this.visualizer.loadPreset(preset, blendTime);
|
||||
this._restartCycling();
|
||||
// TODO: Kinda weird that we use the passed preset for the visualizer,
|
||||
// but set state to the current. Maybe we should just always use the curent..
|
||||
this.setState({ currentPreset: this.props.presets.getCurrentIndex() });
|
||||
}
|
||||
}
|
||||
|
||||
async loadPresets(presetFiles) {
|
||||
const presets = {};
|
||||
const milkFiles = Array.from(presetFiles).filter(file =>
|
||||
file.name.endsWith(".milk")
|
||||
);
|
||||
for (let i = 0; i < milkFiles.length; i++) {
|
||||
const file = milkFiles[i];
|
||||
presets[file.name] = { file };
|
||||
}
|
||||
const numPresets = this.props.presets.loadPresets(presets);
|
||||
this.selectPreset(
|
||||
await this.props.presets.selectIndex(
|
||||
Math.floor(Math.random() * numPresets)
|
||||
),
|
||||
PRESET_TRANSITION_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
closePresetOverlay() {
|
||||
this.setState({ presetOverlay: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.presets == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DropTarget id="milkdrop-window" handleDrop={e => this._handleDrop(e)}>
|
||||
{this.state.presetOverlay && (
|
||||
<PresetOverlay
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
presetKeys={this.props.presets.getKeys()}
|
||||
currentPreset={this.state.currentPreset}
|
||||
onFocusedKeyDown={listener => this.props.onFocusedKeyDown(listener)}
|
||||
selectPreset={async idx => {
|
||||
this.selectPreset(await this.props.presets.selectIndex(idx), 0);
|
||||
}}
|
||||
loadPresets={async presetFiles => this.loadPresets(presetFiles)}
|
||||
closeOverlay={() => this.closePresetOverlay()}
|
||||
/>
|
||||
)}
|
||||
<canvas
|
||||
height={this.props.height}
|
||||
width={this.props.width}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: this.props.isEnabledVisualizer ? "block" : "none"
|
||||
}}
|
||||
ref={node => (this._canvasNode = node)}
|
||||
/>
|
||||
</DropTarget>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
trackTitle: Selectors.getCurrentTrackDisplayName(state),
|
||||
presets: Selectors.getPresets(state),
|
||||
butterchurn: Selectors.getButterchurn(state),
|
||||
transitionType: Selectors.getPresetTransitionType(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Milkdrop);
|
||||
132
js/components/MilkdropWindow/Visualizer.tsx
Normal file
132
js/components/MilkdropWindow/Visualizer.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// @ts-ignore #hook-types
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { VISUALIZERS } from "../../constants";
|
||||
import * as Selectors from "../../selectors";
|
||||
import { AppState, TransitionType } from "../../types";
|
||||
|
||||
interface StateProps {
|
||||
isEnabledVisualizer: boolean;
|
||||
playing: boolean;
|
||||
butterchurn: any;
|
||||
trackTitle: string;
|
||||
currentPreset: any;
|
||||
transitionType: TransitionType;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
analyser: AnalyserNode;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
type Props = StateProps & OwnProps;
|
||||
|
||||
const TRANSITION_TYPE_DURATIONS = {
|
||||
[TransitionType.DEFAULT]: 2.7,
|
||||
[TransitionType.IMMEDIATE]: 0,
|
||||
[TransitionType.USER_PRESET]: 5.7
|
||||
};
|
||||
|
||||
function Visualizer(props: Props) {
|
||||
const canvasRef = useRef(null);
|
||||
const [visualizer, setVisualizer] = useState(null);
|
||||
|
||||
// Initialize the visualizer
|
||||
useEffect(() => {
|
||||
if (canvasRef.current == null || props.butterchurn == null) {
|
||||
return;
|
||||
}
|
||||
if (visualizer != null) {
|
||||
// Note: The visualizer does not offer anyway to clean itself up. So, we
|
||||
// don't offer any way to recreate it. So, if you swap out the analyser
|
||||
// node, or the canvas, that change won't be respected.
|
||||
return;
|
||||
}
|
||||
const _visualizer = props.butterchurn.createVisualizer(
|
||||
props.analyser.context,
|
||||
canvasRef.current,
|
||||
{
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
meshWidth: 32,
|
||||
meshHeight: 24,
|
||||
pixelRatio: window.devicePixelRatio || 1
|
||||
}
|
||||
);
|
||||
_visualizer.connectAudio(props.analyser);
|
||||
setVisualizer(_visualizer);
|
||||
}, [canvasRef.current, props.butterchurn, props.analyser]);
|
||||
|
||||
// Ensure render size stays up to date
|
||||
useEffect(() => {
|
||||
if (visualizer == null) {
|
||||
return;
|
||||
}
|
||||
visualizer.setRendererSize(props.width, props.height);
|
||||
}, [visualizer, props.width, props.height]);
|
||||
|
||||
// Load presets when they change
|
||||
useEffect(() => {
|
||||
if (visualizer == null || props.currentPreset == null) {
|
||||
return;
|
||||
}
|
||||
visualizer.loadPreset(
|
||||
props.currentPreset,
|
||||
TRANSITION_TYPE_DURATIONS[props.transitionType]
|
||||
);
|
||||
}, [visualizer, props.currentPreset]);
|
||||
|
||||
// Handle title animations
|
||||
useEffect(() => {
|
||||
if (visualizer == null || !props.trackTitle) {
|
||||
return;
|
||||
}
|
||||
visualizer.launchSongTitleAnim(props.trackTitle);
|
||||
}, [visualizer, props.trackTitle]);
|
||||
|
||||
const shouldAnimate = props.playing && props.isEnabledVisualizer;
|
||||
|
||||
// Kick off the animation loop
|
||||
useEffect(() => {
|
||||
if (!shouldAnimate || visualizer == null) {
|
||||
return;
|
||||
}
|
||||
let animationFrameRequest = null;
|
||||
const loop = () => {
|
||||
visualizer.render();
|
||||
animationFrameRequest = window.requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
return () => {
|
||||
if (animationFrameRequest != null) {
|
||||
window.cancelAnimationFrame(animationFrameRequest);
|
||||
}
|
||||
};
|
||||
}, [visualizer, shouldAnimate]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
height={props.height}
|
||||
width={props.width}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: props.isEnabledVisualizer ? "block" : "none"
|
||||
}}
|
||||
ref={canvasRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): StateProps => ({
|
||||
isEnabledVisualizer:
|
||||
Selectors.getVisualizerStyle(state) === VISUALIZERS.MILKDROP,
|
||||
playing: Selectors.getMediaIsPlaying(state),
|
||||
butterchurn: Selectors.getButterchurn(state),
|
||||
trackTitle: Selectors.getCurrentTrackDisplayName(state),
|
||||
currentPreset: Selectors.getCurrentPreset(state),
|
||||
transitionType: Selectors.getPresetTransitionType(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Visualizer);
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import screenfull from "screenfull";
|
||||
import ContextMenuWrapper from "../ContextMenuWrapper";
|
||||
import GenWindow from "../GenWindow";
|
||||
import { WINDOWS, VISUALIZERS } from "../../constants";
|
||||
import * as Selectors from "../../selectors";
|
||||
import * as Actions from "../../actionCreators";
|
||||
import MilkdropContextMenu from "./MilkdropContextMenu";
|
||||
import Desktop from "./Desktop";
|
||||
|
||||
import Milkdrop from "./Milkdrop";
|
||||
import Background from "./Background";
|
||||
|
||||
import "../../../css/milkdrop-window.css";
|
||||
|
||||
// This component is just responsible for loading dependencies.
|
||||
// This simplifies the inner <Milkdrop /> component, by allowing
|
||||
// it to always assume that it has its dependencies.
|
||||
class PresetsLoader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isFullscreen: false };
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
return this.props.desktop;
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.props.initializePresets(this.props.options);
|
||||
|
||||
screenfull.onchange(this._handleFullscreenChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
screenfull.off("change", this._handleFullscreenChange);
|
||||
}
|
||||
|
||||
_handleFullscreenChange = () => {
|
||||
this.setState({ isFullscreen: screenfull.isFullscreen });
|
||||
};
|
||||
|
||||
_handleRequestFullsceen = () => {
|
||||
if (screenfull.enabled) {
|
||||
if (!screenfull.isFullscreen) {
|
||||
screenfull.request(this._wrapperNode);
|
||||
} else {
|
||||
screenfull.exit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_renderMilkdrop(size) {
|
||||
const { width, height } = this.state.isFullscreen
|
||||
? { width: screen.width, height: screen.height }
|
||||
: size;
|
||||
// Note: This _wrapperNode must not be removed from the DOM while
|
||||
// in/entering full screen mode. Ensure `this.setState({isFullscreen})`
|
||||
// does not cause this node to change identity.
|
||||
return (
|
||||
<Background innerRef={node => (this._wrapperNode = node)}>
|
||||
<Milkdrop
|
||||
{...this.props}
|
||||
width={width}
|
||||
height={height}
|
||||
isFullscreen={this.state.isFullscreen}
|
||||
/>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.desktop) {
|
||||
const size = { width: window.innerWidth, height: window.innerHeight };
|
||||
return (
|
||||
<ContextMenuWrapper
|
||||
onDoubleClick={this._handleRequestFullsceen}
|
||||
renderContents={() => (
|
||||
<MilkdropContextMenu
|
||||
close={this.props.closeWindow}
|
||||
toggleFullscreen={this._handleRequestFullsceen}
|
||||
desktopMode={this.props.desktop}
|
||||
toggleDesktop={this.props.toggleDesktop}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Desktop>{this._renderMilkdrop(size)}</Desktop>
|
||||
</ContextMenuWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GenWindow title={"Milkdrop"} windowId={WINDOWS.MILKDROP}>
|
||||
{({ height, width }) => (
|
||||
<ContextMenuWrapper
|
||||
onDoubleClick={this._handleRequestFullsceen}
|
||||
renderContents={() => (
|
||||
<MilkdropContextMenu
|
||||
close={this.props.closeWindow}
|
||||
toggleFullscreen={this._handleRequestFullsceen}
|
||||
desktopMode={this.props.desktop}
|
||||
toggleDesktop={this.props.toggleDesktop}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{this._renderMilkdrop({ width, height })}
|
||||
</ContextMenuWrapper>
|
||||
)}
|
||||
</GenWindow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isEnabledVisualizer:
|
||||
Selectors.getVisualizerStyle(state) === VISUALIZERS.MILKDROP,
|
||||
playing: Selectors.getMediaIsPlaying(state),
|
||||
desktop: Selectors.getMilkdropDesktopEnabled(state)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
closeWindow: () => dispatch(Actions.closeWindow(WINDOWS.MILKDROP)),
|
||||
toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop()),
|
||||
initializePresets: presets => dispatch(Actions.initializePresets(presets))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(PresetsLoader);
|
||||
92
js/components/MilkdropWindow/index.tsx
Normal file
92
js/components/MilkdropWindow/index.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// @ts-ignore #hook-types
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import screenfull from "screenfull";
|
||||
import ContextMenuWrapper from "../ContextMenuWrapper";
|
||||
import GenWindow from "../GenWindow";
|
||||
import { WINDOWS, VISUALIZERS } from "../../constants";
|
||||
import * as Selectors from "../../selectors";
|
||||
import * as Actions from "../../actionCreators";
|
||||
import MilkdropContextMenu from "./MilkdropContextMenu";
|
||||
import Desktop from "./Desktop";
|
||||
import { AppState, Dispatch } from "../../types";
|
||||
import Visualizer from "./Visualizer";
|
||||
|
||||
import "../../../css/milkdrop-window.css";
|
||||
import Background from "./Background";
|
||||
|
||||
interface StateProps {
|
||||
desktop: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
closeWindow(): void;
|
||||
toggleDesktop(): void;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
analyser: AnalyserNode;
|
||||
height: number;
|
||||
width: number;
|
||||
onFocusedKeyDown(cb: (e: KeyboardEvent) => void): () => void;
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps & OwnProps;
|
||||
|
||||
function Milkdrop(props: Props) {
|
||||
// Handle keyboard events
|
||||
useEffect(() => {
|
||||
return props.onFocusedKeyDown(e => {
|
||||
switch (e.keyCode) {
|
||||
case 32: // spacebar
|
||||
// this._nextPreset(USER_PRESET_TRANSITION_SECONDS);
|
||||
break;
|
||||
case 8: // backspace
|
||||
// this._prevPreset(0);
|
||||
break;
|
||||
case 72: // H
|
||||
// this._nextPreset(0);
|
||||
break;
|
||||
case 82: // R
|
||||
// this.props.presets.toggleRandomize();
|
||||
break;
|
||||
case 76: // L
|
||||
// this.setState({ presetOverlay: !this.state.presetOverlay });
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 84: // T
|
||||
// this.visualizer.launchSongTitleAnim(this.props.trackTitle);
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 145: // scroll lock
|
||||
case 125: // F14 (scroll lock for OS X)
|
||||
// this.presetCycle = !this.presetCycle;
|
||||
// this._restartCycling();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, [props.onFocusedKeyDown]);
|
||||
return (
|
||||
<GenWindow title={"Milkdrop"} windowId={WINDOWS.MILKDROP}>
|
||||
{({ height, width }) => (
|
||||
<Background>
|
||||
<Visualizer width={width} height={height} analyser={props.analyser} />
|
||||
</Background>
|
||||
)}
|
||||
</GenWindow>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): StateProps => ({
|
||||
desktop: Selectors.getMilkdropDesktopEnabled(state)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
closeWindow: () => dispatch(Actions.closeWindow(WINDOWS.MILKDROP)),
|
||||
toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Milkdrop);
|
||||
|
|
@ -6,16 +6,50 @@ import {
|
|||
} from "../actionTypes";
|
||||
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;
|
||||
presets: any;
|
||||
presetOrder: PresetId[];
|
||||
presetDefinitions: {
|
||||
[presetId: string]: PresetDefinition | LazyPresetDefinition;
|
||||
};
|
||||
currentPresetIndex: number | null;
|
||||
butterchurn: any;
|
||||
transitionType: TransitionType;
|
||||
}
|
||||
|
||||
const defaultMilkdropState = {
|
||||
desktop: false,
|
||||
presets: null,
|
||||
presetOrder: [],
|
||||
presetDefinitions: {},
|
||||
currentPresetIndex: null,
|
||||
butterchurn: null,
|
||||
transitionType: TransitionType.DEFAULT
|
||||
};
|
||||
|
|
@ -27,10 +61,18 @@ export const milkdrop = (
|
|||
switch (action.type) {
|
||||
case SET_MILKDROP_DESKTOP:
|
||||
return { ...state, desktop: action.enabled };
|
||||
case INITIALIZE_PRESETS:
|
||||
return { ...state, presets: action.presets };
|
||||
case GOT_BUTTERCHURN:
|
||||
return { ...state, butterchurn: action.butterchurn };
|
||||
case "GOT_BUTTERCHUN_PRESET":
|
||||
const id = "TEMP";
|
||||
return {
|
||||
...state,
|
||||
presetDefinitions: {
|
||||
[id]: action.json
|
||||
},
|
||||
currentPresetIndex: 0,
|
||||
presetOrder: [id]
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -625,3 +625,11 @@ export function getButterchurn(state: AppState): any {
|
|||
export function getPresetTransitionType(state: AppState): TransitionType {
|
||||
return state.milkdrop.transitionType;
|
||||
}
|
||||
|
||||
export function getCurrentPreset(state: AppState): any | null {
|
||||
const { currentPresetIndex, presetOrder, presetDefinitions } = state.milkdrop;
|
||||
if (currentPresetIndex == null) {
|
||||
return null;
|
||||
}
|
||||
return presetDefinitions[presetOrder[currentPresetIndex]];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@ class Winamp {
|
|||
type: ENABLE_MILKDROP,
|
||||
open: options.__butterchurnOptions.butterchurnOpen
|
||||
});
|
||||
this.store.dispatch(
|
||||
Actions.initializePresets(options.__butterchurnOptions)
|
||||
);
|
||||
}
|
||||
|
||||
if (options.__enableMediaLibrary) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue