Use Redux hooks in all functional components

This commit is contained in:
Jordan Eldredge 2020-04-06 15:20:12 -07:00
parent 9933fde444
commit df74fe6b4f
33 changed files with 633 additions and 1346 deletions

View file

@ -365,3 +365,38 @@ export function downloadHtmlPlaylist(): Thunk {
Utils.downloadURI(uri, "Winamp Playlist.html");
};
}
let el: HTMLInputElement | null = document.createElement("input");
el.type = "file";
// @ts-ingore
const DIR_SUPPORT =
// @ts-ignore
typeof el.webkitdirectory !== "undefined" ||
// @ts-ignore
typeof el.mozdirectory !== "undefined" ||
// @ts-ignore
typeof el.directory !== "undefined";
// Release our reference
el = null;
export function addFilesAtIndex(nextIndex: number): Thunk {
return async dispatch => {
const fileReferences = await promptForFileReferences();
dispatch(
addTracksFromReferences(fileReferences, LOAD_STYLE.NONE, nextIndex)
);
};
}
export function addDirAtIndex(nextIndex: number): Thunk {
return async dispatch => {
if (!DIR_SUPPORT) {
alert("Not supported in your browser");
return;
}
const fileReferences = await promptForFileReferences({ directory: true });
dispatch(
addTracksFromReferences(fileReferences, LOAD_STYLE.NONE, nextIndex)
);
};
}

View file

@ -90,6 +90,8 @@ export {
downloadPreset,
setEqFromObject,
downloadHtmlPlaylist,
addDirAtIndex,
addFilesAtIndex,
} from "./files";
export {
cropPlaylist,

View file

@ -1,36 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { toggleEq } from "../../actionCreators";
import { AppState, Dispatch } from "../../types";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import { useActionCreator, useTypedSelector } from "../../hooks";
interface StateProps {
on: boolean;
}
interface DispatchProps {
toggleEq(): void;
}
const EqOn = (props: StateProps & DispatchProps) => {
const EqOn = () => {
const toggleEq = useActionCreator(Actions.toggleEq);
const on = useTypedSelector(Selectors.getEqualizerEnabled);
return (
<div
id="on"
className={classnames({
selected: props.on,
})}
onClick={props.toggleEq}
/>
<div id="on" className={classnames({ selected: on })} onClick={toggleEq} />
);
};
const mapStateToProps = (state: AppState): StateProps => ({
on: state.equalizer.on,
});
const mapDispatchProps = (dispatch: Dispatch): DispatchProps => {
return { toggleEq: () => dispatch(toggleEq()) };
};
export default connect(mapStateToProps, mapDispatchProps)(EqOn);
export default EqOn;

View file

@ -1,24 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import Volume from "../Volume";
import Balance from "../Balance";
import { segment } from "../../utils";
import { closeWindow, toggleEqualizerShadeMode } from "../../actionCreators";
import { AppState, Dispatch } from "../../types";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import { useTypedSelector, useActionCreator } from "../../hooks";
interface StateProps {
volume: number;
balance: number;
}
interface DispatchProps {
closeWindow(): void;
toggleEqualizerShadeMode(): void;
}
const EqualizerShade = (props: StateProps & DispatchProps) => {
const { volume, balance } = props;
const EqualizerShade = () => {
const volume = useTypedSelector(Selectors.getVolume);
const balance = useTypedSelector(Selectors.getBalance);
const closeWindow = useActionCreator(Actions.closeWindow);
const toggleEqualizerShadeMode = useActionCreator(
Actions.toggleEqualizerShadeMode
);
const classes = ["left", "center", "right"];
const eqVolumeClassName = segment(0, 100, volume, classes);
@ -26,27 +21,14 @@ const EqualizerShade = (props: StateProps & DispatchProps) => {
return (
<div
className="draggable"
onDoubleClick={props.toggleEqualizerShadeMode}
onDoubleClick={toggleEqualizerShadeMode}
style={{ width: "100%", height: "100%" }}
>
<div id="equalizer-shade" onClick={props.toggleEqualizerShadeMode} />
<div id="equalizer-close" onClick={props.closeWindow} />
<div id="equalizer-shade" onClick={toggleEqualizerShadeMode} />
<div id="equalizer-close" onClick={() => closeWindow("equalizer")} />
<Volume id="equalizer-volume" className={eqVolumeClassName} />
<Balance id="equalizer-balance" className={eqBalanceClassName} />
</div>
);
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
closeWindow: () => dispatch(closeWindow("equalizer")),
toggleEqualizerShadeMode: () => dispatch(toggleEqualizerShadeMode()),
};
};
const mapStateToProps = (state: AppState): StateProps => ({
volume: state.media.volume,
balance: state.media.balance,
});
export default connect(mapStateToProps, mapDispatchToProps)(EqualizerShade);
export default EqualizerShade;

View file

@ -1,52 +1,38 @@
import React from "react";
import { connect } from "react-redux";
import builtin from "../../../presets/builtin.json";
import {
openEqfFileDialog,
downloadPreset,
setEqFromObject,
} from "../../actionCreators";
import * as Actions from "../../actionCreators";
import { Node, Parent, Hr } from "../ContextMenu";
import ContextMenuTarget from "../ContextMenuTarget";
import { Dispatch, EqfPreset } from "../../types";
import { useActionCreator } from "../../hooks";
interface DispatchProps {
openEqfFileDialog(): void;
downloadPreset(): void;
setEqFromObject(preset: EqfPreset): void;
}
const PresetsContextMenu = (props: DispatchProps) => (
<ContextMenuTarget
top
id="presets-context"
renderMenu={() => (
<>
<Parent label="Load">
{builtin.presets.map(preset => (
<Node
key={preset.name}
onClick={() => props.setEqFromObject(preset)}
label={preset.name}
/>
))}
<Hr />
<Node onClick={props.openEqfFileDialog} label="From Eqf..." />
</Parent>
<Node onClick={props.downloadPreset} label="Save" />
</>
)}
>
<div id="presets" />
</ContextMenuTarget>
);
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
openEqfFileDialog: () => dispatch(openEqfFileDialog()),
downloadPreset: () => dispatch(downloadPreset()),
setEqFromObject: preset => dispatch(setEqFromObject(preset)),
};
const PresetsContextMenu = () => {
const openEqfFileDialog = useActionCreator(Actions.openEqfFileDialog);
const downloadPreset = useActionCreator(Actions.downloadPreset);
const setEqFromObject = useActionCreator(Actions.setEqFromObject);
return (
<ContextMenuTarget
top
id="presets-context"
renderMenu={() => (
<>
<Parent label="Load">
{builtin.presets.map(preset => (
<Node
key={preset.name}
onClick={() => setEqFromObject(preset)}
label={preset.name}
/>
))}
<Hr />
<Node onClick={openEqfFileDialog} label="From Eqf..." />
</Parent>
<Node onClick={downloadPreset} label="Save" />
</>
)}
>
<div id="presets" />
</ContextMenuTarget>
);
};
export default connect(null, mapDispatchToProps)(PresetsContextMenu);
export default PresetsContextMenu;

View file

@ -1,19 +1,9 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { BANDS, WINDOWS } from "../../constants";
import {
setEqBand,
setPreamp,
setEqToMax,
setEqToMid,
setEqToMin,
closeWindow,
toggleEqualizerShadeMode,
} from "../../actionCreators";
import { getWindowShade } from "../../selectors";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import Band from "./Band";
import EqOn from "./EqOn";
@ -23,29 +13,29 @@ import PresetsContextMenu from "./PresetsContextMenu";
import EqualizerShade from "./EqualizerShade";
import "../../../css/equalizer-window.css";
import { Dispatch, Band as BandType, AppState } from "../../types";
import { Band as BandType } from "../../types";
import FocusTarget from "../FocusTarget";
interface StateProps {
doubled: boolean;
selected: boolean;
shade: boolean | undefined;
}
interface DispatchProps {
setPreampValue(value: number): void;
setEqToMin(): void;
setEqToMid(): void;
setEqToMax(): void;
setHertzValue(hertz: BandType): (value: number) => void;
closeEqualizerWindow(): void;
toggleEqualizerShadeMode(): void;
}
import { useTypedSelector, useActionCreator } from "../../hooks";
const bandClassName = (band: BandType) => `band-${band}`;
const EqualizerWindow = (props: StateProps & DispatchProps) => {
const { doubled, selected, shade } = props;
const EqualizerWindow = () => {
const doubled = useTypedSelector(Selectors.getDoubled);
const focusedWindow = useTypedSelector(Selectors.getFocusedWindow);
const getWindowShade = useTypedSelector(Selectors.getWindowShade);
const selected = focusedWindow === WINDOWS.EQUALIZER;
const shade = getWindowShade(WINDOWS.EQUALIZER);
const setPreampValue = useActionCreator(Actions.setPreamp);
const setEqToMin = useActionCreator(Actions.setEqToMin);
const setEqToMid = useActionCreator(Actions.setEqToMid);
const setEqToMax = useActionCreator(Actions.setEqToMax);
const setHertzValue = useActionCreator(Actions.setEqBand);
const closeWindow = useActionCreator(Actions.closeWindow);
const toggleEqualizerShadeMode = useActionCreator(
Actions.toggleEqualizerShadeMode
);
const className = classnames({
selected,
@ -57,34 +47,34 @@ const EqualizerWindow = (props: StateProps & DispatchProps) => {
return (
<div id="equalizer-window" className={className}>
<FocusTarget windowId={WINDOWS.EQUALIZER}>
{props.shade ? (
{shade ? (
<EqualizerShade />
) : (
<div>
<div
className="equalizer-top title-bar draggable"
onDoubleClick={props.toggleEqualizerShadeMode}
onDoubleClick={toggleEqualizerShadeMode}
>
<div id="equalizer-shade" onClick={toggleEqualizerShadeMode} />
<div
id="equalizer-shade"
onClick={props.toggleEqualizerShadeMode}
id="equalizer-close"
onClick={() => closeWindow(WINDOWS.EQUALIZER)}
/>
<div id="equalizer-close" onClick={props.closeEqualizerWindow} />
</div>
<EqOn />
<EqAuto />
<EqGraph />
<PresetsContextMenu />
<Band id="preamp" band="preamp" onChange={props.setPreampValue} />
<div id="plus12db" onClick={props.setEqToMax} />
<div id="zerodb" onClick={props.setEqToMid} />
<div id="minus12db" onClick={props.setEqToMin} />
<Band id="preamp" band="preamp" onChange={setPreampValue} />
<div id="plus12db" onClick={setEqToMax} />
<div id="zerodb" onClick={setEqToMid} />
<div id="minus12db" onClick={setEqToMin} />
{BANDS.map(hertz => (
<Band
key={hertz}
id={bandClassName(hertz)}
band={hertz}
onChange={props.setHertzValue(hertz)}
onChange={value => setHertzValue(hertz, value)}
/>
))}
</div>
@ -93,24 +83,4 @@ const EqualizerWindow = (props: StateProps & DispatchProps) => {
</div>
);
};
// This does not use the shorthand object syntax becuase `setHertzValue` needs
// to return a function.
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
setPreampValue: (value: number) => dispatch(setPreamp(value)),
setEqToMin: () => dispatch(setEqToMin()),
setEqToMid: () => dispatch(setEqToMid()),
setEqToMax: () => dispatch(setEqToMax()),
setHertzValue: (hertz: BandType) => (value: number) =>
dispatch(setEqBand(hertz, value)),
closeEqualizerWindow: () => dispatch(closeWindow("equalizer")),
toggleEqualizerShadeMode: () => dispatch(toggleEqualizerShadeMode()),
});
const mapStateToProps = (state: AppState): StateProps => ({
doubled: state.display.doubled,
selected: state.windows.focused === WINDOWS.EQUALIZER,
shade: getWindowShade(state)("equalizer"),
});
export default connect(mapStateToProps, mapDispatchToProps)(EqualizerWindow);
export default EqualizerWindow;

View file

@ -1,26 +1,18 @@
import React, { useCallback, useState, useEffect } from "react";
import { WindowId, AppState, Dispatch } from "../types";
import { WindowId } from "../types";
import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { connect } from "react-redux";
import { useActionCreator, useTypedSelector } from "../hooks";
interface DispatchProps {
setFocus(windowId: WindowId | null): void;
}
interface StateProps {
focusedWindowId: WindowId | null;
}
interface OwnProps {
interface Props {
onKeyDown?(e: KeyboardEvent): void;
windowId: WindowId;
children: React.ReactNode;
}
type Props = StateProps & DispatchProps & OwnProps;
function FocusTarget(props: Props) {
const { onKeyDown, focusedWindowId, windowId, setFocus, children } = props;
function FocusTarget({ onKeyDown, windowId, children }: Props) {
const focusedWindowId = useTypedSelector(Selectors.getFocusedWindow);
const setFocus = useActionCreator(Actions.setFocusedWindow);
const focusHandler = useCallback(() => {
if (windowId !== focusedWindowId) {
@ -86,16 +78,4 @@ function FocusTarget(props: Props) {
);
}
function mapStateToProps(state: AppState): StateProps {
return {
focusedWindowId: Selectors.getFocusedWindow(state),
};
}
function mapDispatchToProps(dispatch: Dispatch): DispatchProps {
return {
setFocus: windowId => dispatch(Actions.setFocusedWindow(windowId)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FocusTarget);
export default FocusTarget;

View file

@ -1,120 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GenWindow renders to snapshot 1`] = `
<div
onFocus={[Function]}
onMouseDown={[Function]}
style={
Object {
"height": "100%",
"width": "100%",
}
}
tabIndex={-1}
>
<div
className="gen-window window selected"
style={
Object {
"height": undefined,
"width": undefined,
}
}
>
<div
className="gen-top draggable"
>
<div
className="gen-top-left draggable"
/>
<div
className="gen-top-left-fill draggable"
/>
<div
className="gen-top-left-end draggable"
/>
<div
className="gen-top-title draggable"
>
<div
className="draggable gen-text-letter gen-text-m"
/>
<div
className="draggable gen-text-letter gen-text-y"
/>
<div
className="draggable gen-text-letter gen-text-space"
/>
<div
className="draggable gen-text-letter gen-text-w"
/>
<div
className="draggable gen-text-letter gen-text-i"
/>
<div
className="draggable gen-text-letter gen-text-n"
/>
<div
className="draggable gen-text-letter gen-text-d"
/>
<div
className="draggable gen-text-letter gen-text-o"
/>
<div
className="draggable gen-text-letter gen-text-w"
/>
</div>
<div
className="gen-top-right-end draggable"
/>
<div
className="gen-top-right-fill draggable"
/>
<div
className="gen-top-right draggable"
>
<div
className="gen-close selected"
onClick={[Function]}
/>
</div>
</div>
<div
className="gen-middle"
>
<div
className="gen-middle-left draggable"
>
<div
className="gen-middle-left-bottom draggable"
/>
</div>
<div
className="gen-middle-center"
/>
<div
className="gen-middle-right draggable"
>
<div
className="gen-middle-right-bottom draggable"
/>
</div>
</div>
<div
className="gen-bottom draggable"
>
<div
className="gen-bottom-left draggable"
/>
<div
className="gen-bottom-right draggable"
>
<div
id="gen-resize-target"
onMouseDown={[Function]}
/>
</div>
</div>
</div>
</div>
`;

View file

@ -1,32 +0,0 @@
import React from "react";
import { Provider } from "react-redux";
import renderer from "react-test-renderer";
import mockGetStore from "../../__mocks__/storeMock";
import { GenWindow } from "./index";
describe("GenWindow", () => {
let store;
beforeEach(() => {
store = mockGetStore();
});
it("renders to snapshot", () => {
const tree = renderer
.create(
<Provider store={store}>
<GenWindow
title="My Window"
selected
close={() => {}}
windowId="TEST_WINDOW_ID"
scrollVolume={() => {}}
windowSize={[0, 0]}
>
{() => {}}
</GenWindow>
</Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,13 +1,13 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import "../../../css/gen-window.css";
import { setWindowSize, closeWindow } from "../../actionCreators";
import { getWindowPixelSize } from "../../selectors";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import ResizeTarget from "../ResizeTarget";
import { AppState, WindowId, Dispatch } from "../../types";
import { WindowId } from "../../types";
import FocusTarget from "../FocusTarget";
import { useActionCreator, useTypedSelector } from "../../hooks";
interface TextProps {
children: string;
@ -37,40 +37,23 @@ interface WindowSize {
height: number;
}
interface OwnProps {
interface Props {
windowId: WindowId;
children: (windowSize: WindowSize) => React.ReactNode;
title: string;
onKeyDown?(e: KeyboardEvent): void;
}
interface DispatchProps {
close: (windowId: WindowId) => void;
setGenWindowSize: (windowId: WindowId, size: [number, number]) => void;
}
interface StateProps {
windowSize: [number, number];
selected: boolean;
height: number;
width: number;
}
type Props = OwnProps & DispatchProps & StateProps;
// Named export for testing
export const GenWindow = ({
selected,
children,
close,
title,
windowId,
windowSize,
setGenWindowSize,
width,
height,
onKeyDown,
}: Props) => {
export const GenWindow = ({ children, title, windowId, onKeyDown }: Props) => {
const setWindowSize = useActionCreator(Actions.setWindowSize);
const closeWindow = useActionCreator(Actions.closeWindow);
const getWindowPixelSize = useTypedSelector(Selectors.getWindowPixelSize);
const focusedWindow = useTypedSelector(Selectors.getFocusedWindow);
const getWindowSize = useTypedSelector(Selectors.getWindowSize);
const windowSize = getWindowSize(windowId);
const selected = focusedWindow === windowId;
const { width, height } = getWindowPixelSize(windowId);
return (
<FocusTarget windowId={windowId} onKeyDown={onKeyDown}>
<div
@ -89,7 +72,7 @@ export const GenWindow = ({
<div className="gen-top-right draggable">
<div
className="gen-close selected"
onClick={() => close(windowId)}
onClick={() => closeWindow(windowId)}
/>
</div>
</div>
@ -112,7 +95,7 @@ export const GenWindow = ({
<div className="gen-bottom-right draggable">
<ResizeTarget
currentSize={windowSize}
setWindowSize={size => setGenWindowSize(windowId, size)}
setWindowSize={size => setWindowSize(windowId, size)}
id={"gen-resize-target"}
/>
</div>
@ -122,22 +105,4 @@ export const GenWindow = ({
);
};
const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => {
const { width, height } = getWindowPixelSize(state)(ownProps.windowId);
return {
width,
height,
selected: state.windows.focused === ownProps.windowId,
windowSize: state.windows.genWindows[ownProps.windowId].size,
};
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
close: (windowId: WindowId) => dispatch(closeWindow(windowId)),
setGenWindowSize: (windowId: WindowId, size: [number, number]) =>
dispatch(setWindowSize(windowId, size)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(GenWindow);
export default GenWindow;

View file

@ -1,67 +1,46 @@
import React, { ReactNode } 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";
import { useTypedSelector, useActionCreator } from "../../hooks";
interface StateProps {
desktop: boolean;
fullscreen: boolean;
}
interface DispatchProps {
closeWindow(): void;
toggleDesktop(): void;
toggleFullscreen(): void;
}
interface OwnProps {
interface Props {
children: ReactNode;
}
type Props = StateProps & DispatchProps & OwnProps;
const MilkdropContextMenu = (props: Props) => {
const desktop = useTypedSelector(Selectors.getMilkdropDesktopEnabled);
const MilkdropContextMenu = (props: Props) => (
<ContextMenuWraper
renderContents={() => {
return (
<>
<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" />
</>
);
}}
>
{props.children}
</ContextMenuWraper>
);
const closeWindow = useActionCreator(Actions.closeWindow);
const toggleDesktop = useActionCreator(Actions.toggleMilkdropDesktop);
const toggleFullscreen = useActionCreator(Actions.toggleMilkdropFullscreen);
return (
<ContextMenuWraper
renderContents={() => {
return (
<>
<Node
onClick={toggleFullscreen}
label="Fullscreen"
hotkey="Alt+Enter"
/>
<Node
onClick={toggleDesktop}
checked={desktop}
label="Desktop Mode"
hotkey="Alt+D"
/>
<Hr />
<Node onClick={() => closeWindow(WINDOWS.MILKDROP)} label="Quit" />
</>
);
}}
>
{props.children}
</ContextMenuWraper>
);
};
const mapStateToProps = (state: AppState): StateProps => ({
desktop: Selectors.getMilkdropDesktopEnabled(state),
fullscreen: Selectors.getMilkdropFullscreenEnabled(state),
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
closeWindow: () => dispatch(Actions.closeWindow(WINDOWS.MILKDROP)),
toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop()),
toggleFullscreen: () => dispatch(Actions.toggleMilkdropFullscreen()),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(MilkdropContextMenu);
export default MilkdropContextMenu;

View file

@ -3,9 +3,12 @@ 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";
import { useUnmountedRef } from "../../hooks";
import { TransitionType } from "../../types";
import {
useUnmountedRef,
useActionCreator,
useTypedSelector,
} from "../../hooks";
const ENTRY_HEIGHT = 14;
const HEIGHT_PADDING = 15;
@ -34,24 +37,11 @@ const INNER_WRAPPER_STYLE: React.CSSProperties = {
fontSize: "12px",
};
interface StateProps {
presetKeys: string[];
currentPresetIndex: number | null; // Index
}
interface DispatchProps {
requestPresetAtIndex(i: number): void;
togglePresetOverlay(): void;
appendPresetFileList(fileList: FileList): void;
}
interface OwnProps {
interface Props {
height: number;
width: number;
}
type Props = StateProps & DispatchProps & OwnProps;
function presetIndexFromListIndex(listIndex: number) {
return listIndex - 1;
}
@ -60,15 +50,13 @@ function listIndexFromPresetIndex(listIndex: number) {
return listIndex + 1;
}
function PresetOverlay({
currentPresetIndex,
presetKeys,
height,
width,
requestPresetAtIndex,
togglePresetOverlay,
appendPresetFileList,
}: Props) {
function PresetOverlay({ height, width }: Props) {
const presetKeys = useTypedSelector(Selectors.getPresetNames);
const currentPresetIndex = useTypedSelector(Selectors.getCurrentPresetIndex);
const requestPresetAtIndex = useActionCreator(Actions.requestPresetAtIndex);
const togglePresetOverlay = useActionCreator(Actions.togglePresetOverlay);
const appendPresetFileList = useActionCreator(Actions.appendPresetFileList);
const unmountedRef = useUnmountedRef();
const [selectedListIndex, setSelectedListIndex] = useState(() => {
if (currentPresetIndex != null) {
@ -134,7 +122,11 @@ function PresetOverlay({
if (selectedListIndex === 0) {
loadLocalDir();
} else {
requestPresetAtIndex(presetIndexFromListIndex(selectedListIndex));
requestPresetAtIndex(
presetIndexFromListIndex(selectedListIndex),
TransitionType.DEFAULT,
true
);
}
e.stopPropagation();
break;
@ -203,23 +195,4 @@ export function getRangeCenteredOnIndex(
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, true));
},
togglePresetOverlay: () => dispatch(Actions.togglePresetOverlay()),
appendPresetFileList: (fileList: FileList) =>
dispatch(Actions.appendPresetFileList(fileList)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PresetOverlay);
export default PresetOverlay;

View file

@ -1,8 +1,8 @@
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";
import { TransitionType } from "../../types";
import { useTypedSelector } from "../../hooks";
type ButterchurnVisualizer = {
setRendererSize(width: number, height: number): void;
@ -11,46 +11,29 @@ type ButterchurnVisualizer = {
render(): void;
};
interface StateProps {
isEnabledVisualizer: boolean;
playing: boolean;
butterchurn: any;
trackTitle: string | null;
currentPreset: any;
transitionType: TransitionType;
message: {
text: string;
time: number;
} | null;
}
interface OwnProps {
interface Props {
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 {
butterchurn,
analyser,
width,
height,
currentPreset,
transitionType,
trackTitle,
isEnabledVisualizer,
message,
playing,
} = _props;
function Visualizer({ analyser, width, height }: Props) {
const visualizerStyle = useTypedSelector(Selectors.getVisualizerStyle);
const playing = useTypedSelector(Selectors.getMediaIsPlaying);
const butterchurn = useTypedSelector(Selectors.getButterchurn);
const trackTitle = useTypedSelector(Selectors.getCurrentTrackDisplayName);
const currentPreset = useTypedSelector(Selectors.getCurrentPreset);
const transitionType = useTypedSelector(Selectors.getPresetTransitionType);
const message = useTypedSelector(Selectors.getMilkdropMessage);
const isEnabledVisualizer = visualizerStyle === VISUALIZERS.MILKDROP;
const canvasRef = useRef(null);
const [visualizer, setVisualizer] = useState<ButterchurnVisualizer | null>(
null
@ -169,15 +152,4 @@ function Visualizer(_props: Props) {
);
}
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),
message: state.milkdrop.message,
});
export default connect(mapStateToProps)(Visualizer);
export default Visualizer;

View file

@ -1,12 +1,16 @@
import React, { useEffect, useCallback } from "react";
import Fullscreen from "../Fullscreen";
import { connect } from "react-redux";
import { useWindowSize, useScreenSize } from "../../hooks";
import {
useWindowSize,
useScreenSize,
useTypedSelector,
useActionCreator,
} from "../../hooks";
import GenWindow from "../GenWindow";
import { WINDOWS } from "../../constants";
import * as Selectors from "../../selectors";
import * as Actions from "../../actionCreators";
import { AppState, Dispatch, TransitionType } from "../../types";
import { TransitionType } from "../../types";
import Visualizer from "./Visualizer";
import "../../../css/milkdrop-window.css";
@ -18,47 +22,22 @@ import Desktop from "./Desktop";
const MILLISECONDS_BETWEEN_PRESET_TRANSITIONS = 15000;
interface StateProps {
desktop: boolean;
fullscreen: boolean;
overlay: boolean;
presetsAreCycling: boolean;
currentPresetIndex: number | null; // Index
trackTitle: string | null;
mediaIsPlaying: boolean;
}
interface DispatchProps {
closeWindow(): void;
toggleDesktop(): void;
toggleFullscreen(): void;
setFullscreen(fullscreen: boolean): void;
togglePresetOverlay(): void;
selectRandomPreset(): void;
toggleRandomize(): void;
toggleCycling(): void;
handlePresetDrop(e: React.DragEvent): void;
selectNextPreset(transitionType?: TransitionType): void;
selectPreviousPreset(transitionType?: TransitionType): void;
scheduleMilkdropMessage(message: string): void;
}
interface OwnProps {
interface Props {
analyser: AnalyserNode;
}
type Props = StateProps & DispatchProps & OwnProps;
function useKeyHandler() {
const trackTitle = useTypedSelector(Selectors.getCurrentTrackDisplayName);
const selectNextPreset = useActionCreator(Actions.selectNextPreset);
const selectPreviousPreset = useActionCreator(Actions.selectPreviousPreset);
const toggleRandomize = useActionCreator(Actions.toggleRandomizePresets);
const togglePresetOverlay = useActionCreator(Actions.togglePresetOverlay);
const scheduleMilkdropMessage = useActionCreator(
Actions.scheduleMilkdropMessage
);
const toggleCycling = useActionCreator(Actions.togglePresetCycling);
function useKeyHandler(props: Props) {
const {
selectNextPreset,
selectPreviousPreset,
toggleRandomize,
togglePresetOverlay,
scheduleMilkdropMessage,
trackTitle,
toggleCycling,
} = props;
// Handle keyboard events
return useCallback(
(e: KeyboardEvent) => {
@ -92,45 +71,52 @@ function useKeyHandler(props: Props) {
}
},
[
scheduleMilkdropMessage,
selectNextPreset,
selectPreviousPreset,
toggleRandomize,
togglePresetOverlay,
scheduleMilkdropMessage,
trackTitle,
toggleCycling,
togglePresetOverlay,
toggleRandomize,
trackTitle,
]
);
}
function Milkdrop(props: Props) {
const handleKeyDown = useKeyHandler(props);
function Milkdrop({ analyser }: Props) {
const desktop = useTypedSelector(Selectors.getMilkdropDesktopEnabled);
const fullscreen = useTypedSelector(Selectors.getMilkdropFullscreenEnabled);
const overlay = useTypedSelector(Selectors.getPresetOverlayOpen);
const presetsAreCycling = useTypedSelector(Selectors.getPresetsAreCycling);
const currentPresetIndex = useTypedSelector(Selectors.getCurrentPresetIndex);
const mediaIsPlaying = useTypedSelector(Selectors.getMediaIsPlaying);
const toggleFullscreen = useActionCreator(Actions.toggleMilkdropFullscreen);
const selectNextPreset = useActionCreator(Actions.selectNextPreset);
const handlePresetDrop = useActionCreator(Actions.handlePresetDrop);
const setFullscreen = useActionCreator(Actions.setMilkdropFullscreen);
const handleKeyDown = useKeyHandler();
// Cycle presets
useEffect(() => {
if (!props.presetsAreCycling || !props.mediaIsPlaying) {
if (!presetsAreCycling || !mediaIsPlaying) {
return;
}
const intervalId = setInterval(
props.selectNextPreset,
selectNextPreset,
MILLISECONDS_BETWEEN_PRESET_TRANSITIONS
);
return () => clearInterval(intervalId);
}, [
props.selectNextPreset,
props.presetsAreCycling,
props.currentPresetIndex,
props.mediaIsPlaying,
]);
}, [presetsAreCycling, currentPresetIndex, mediaIsPlaying, selectNextPreset]);
const screenSize = useScreenSize();
const windowSize = useWindowSize();
if (props.desktop) {
if (desktop) {
return (
<Desktop>
<MilkdropContextMenu>
<Visualizer {...windowSize} analyser={props.analyser} />
<Visualizer {...windowSize} analyser={analyser} />
</MilkdropContextMenu>
</Desktop>
);
@ -143,18 +129,15 @@ function Milkdrop(props: Props) {
onKeyDown={handleKeyDown}
>
{(genWindowSize: { width: number; height: number }) => {
const size = props.fullscreen ? screenSize : genWindowSize;
const size = fullscreen ? screenSize : genWindowSize;
return (
<MilkdropContextMenu>
<Background>
<DropTarget handleDrop={props.handlePresetDrop}>
{props.overlay && <PresetOverlay {...size} />}
<Fullscreen
enabled={props.fullscreen}
onChange={props.setFullscreen}
>
<div onDoubleClick={props.toggleFullscreen}>
<Visualizer {...size} analyser={props.analyser} />
<DropTarget handleDrop={handlePresetDrop}>
{overlay && <PresetOverlay {...size} />}
<Fullscreen enabled={fullscreen} onChange={setFullscreen}>
<div onDoubleClick={toggleFullscreen}>
<Visualizer {...size} analyser={analyser} />
</div>
</Fullscreen>
</DropTarget>
@ -165,34 +148,4 @@ function Milkdrop(props: Props) {
</GenWindow>
);
}
const mapStateToProps = (state: AppState): StateProps => ({
desktop: Selectors.getMilkdropDesktopEnabled(state),
fullscreen: Selectors.getMilkdropFullscreenEnabled(state),
overlay: Selectors.getPresetOverlayOpen(state),
presetsAreCycling: Selectors.getPresetsAreCycling(state),
currentPresetIndex: Selectors.getCurrentPresetIndex(state),
trackTitle: Selectors.getCurrentTrackDisplayName(state),
mediaIsPlaying: Selectors.getMediaIsPlaying(state),
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
closeWindow: () => dispatch(Actions.closeWindow(WINDOWS.MILKDROP)),
toggleDesktop: () => dispatch(Actions.toggleMilkdropDesktop()),
toggleFullscreen: () => dispatch(Actions.toggleMilkdropFullscreen()),
setFullscreen: (fullscreen: boolean) =>
dispatch(Actions.setMilkdropFullscreen(fullscreen)),
togglePresetOverlay: () => dispatch(Actions.togglePresetOverlay()),
selectRandomPreset: () => dispatch(Actions.selectRandomPreset()),
toggleRandomize: () => dispatch(Actions.toggleRandomizePresets()),
toggleCycling: () => dispatch(Actions.togglePresetCycling()),
handlePresetDrop: e => dispatch(Actions.handlePresetDrop(e)),
selectNextPreset: (transitionType?: TransitionType) =>
dispatch(Actions.selectNextPreset(transitionType)),
selectPreviousPreset: (transitionType?: TransitionType) =>
dispatch(Actions.selectPreviousPreset(transitionType)),
scheduleMilkdropMessage: (message: string) =>
dispatch(Actions.scheduleMilkdropMessage(message)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Milkdrop);
export default Milkdrop;

View file

@ -1,14 +1,13 @@
import { AppState, Dispatch } from "../types";
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { getTimeObj } from "../utils";
import { TOGGLE_TIME_MODE } from "../actionTypes";
import { TIME_MODE, MEDIA_STATUS } from "../constants";
import * as Actions from "../actionCreators";
import Character from "./Character";
import * as Selectors from "../selectors";
import "../../css/mini-time.css";
import { useTypedSelector, useActionCreator } from "../hooks";
// Sigh. When the display is blinking (say when it's paused) we need to
// alternate between the actual character and the space character. Not
@ -27,39 +26,29 @@ const Background = () => (
</React.Fragment>
);
interface StateProps {
status: string | null;
timeMode: string;
timeElapsed: number;
length: number | null;
}
const MiniTime = () => {
const status = useTypedSelector(Selectors.getMediaStatus);
const duration = useTypedSelector(Selectors.getDuration);
const timeElapsed = useTypedSelector(Selectors.getTimeElapsed);
const timeMode = useTypedSelector(Selectors.getTimeMode);
interface DispatchProps {
toggle: () => void;
}
type Props = StateProps & DispatchProps;
const MiniTime = (props: Props) => {
const toggle = useActionCreator(Actions.toggleTimeMode);
let seconds = null;
// TODO: Clean this up: If stopped, just render the background, rather than
// rendering spaces twice.
if (props.status !== MEDIA_STATUS.STOPPED && props.length != null) {
if (status !== MEDIA_STATUS.STOPPED && duration != null) {
seconds =
props.timeMode === TIME_MODE.ELAPSED
? props.timeElapsed
: props.length - props.timeElapsed;
timeMode === TIME_MODE.ELAPSED ? timeElapsed : duration - timeElapsed;
}
const timeObj = getTimeObj(seconds);
const showMinus =
props.timeMode === TIME_MODE.REMAINING &&
props.status !== MEDIA_STATUS.STOPPED;
timeMode === TIME_MODE.REMAINING && status !== MEDIA_STATUS.STOPPED;
return (
<div
onClick={props.toggle}
onClick={toggle}
className={classnames("mini-time", "countdown", {
blinking: props.status === MEDIA_STATUS.PAUSED,
blinking: status === MEDIA_STATUS.PAUSED,
})}
>
<Background />
@ -72,18 +61,4 @@ const MiniTime = (props: Props) => {
);
};
const mapStateToProps = (state: AppState): StateProps => ({
status: state.media.status,
timeMode: state.media.timeMode,
timeElapsed: Selectors.getTimeElapsed(state),
length: Selectors.getDuration(state),
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
// TODO: move to actionCreators
toggle: () => {
dispatch({ type: TOGGLE_TIME_MODE });
},
});
export default connect(mapStateToProps, mapDispatchToProps)(MiniTime);
export default MiniTime;

View file

@ -1,82 +1,56 @@
import React from "react";
import { connect } from "react-redux";
import { Hr, Node } from "./ContextMenu";
import SkinsContextMenu from "./SkinsContextMenu";
import { Dispatch, TimeMode, AppState } from "../types";
import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { TIME_MODE } from "../constants";
import { useActionCreator, useTypedSelector } from "../hooks";
interface StateProps {
timeMode: TimeMode;
doubled: boolean;
repeat: boolean;
shuffle: boolean;
}
const OptionsContextMenu = () => {
const toggleTimeMode = useActionCreator(Actions.toggleTimeMode);
const toggleDoubleSizeMode = useActionCreator(Actions.toggleDoubleSizeMode);
const toggleRepeat = useActionCreator(Actions.toggleRepeat);
const toggleShuffle = useActionCreator(Actions.toggleShuffle);
interface DispatchProps {
toggleTimeMode(): void;
toggleDoubleSizeMode(): void;
toggleRepeat(): void;
toggleShuffle(): void;
}
const OptionsContextMenu = (props: DispatchProps & StateProps) => (
<React.Fragment>
{/* <Node label="Preferences..." /> */}
<SkinsContextMenu />
<Hr />
<Node
label="Time elapsed"
hotkey="(Ctrl+T toggles)"
onClick={props.toggleTimeMode}
checked={props.timeMode === TIME_MODE.ELAPSED}
/>
<Node
label="Time remaining"
hotkey="(Ctrl+T toggles)"
onClick={props.toggleTimeMode}
checked={props.timeMode === TIME_MODE.REMAINING}
/>
{/* <Node label="Always On Top" hotkey="Ctrl+A" /> */}
<Node
label="Double Size"
hotkey="Ctrl+D"
onClick={props.toggleDoubleSizeMode}
checked={props.doubled}
/>
{/* <Node label="EasyMove" hotkey="Ctrl+E" /> */}
<Hr />
<Node
label="Repeat"
hotkey="R"
onClick={props.toggleRepeat}
checked={props.repeat}
/>
<Node
label="Shuffle"
hotkey="S"
onClick={props.toggleShuffle}
checked={props.shuffle}
/>
</React.Fragment>
);
const mapStateToProps = (state: AppState): StateProps => {
return {
doubled: state.display.doubled,
timeMode: state.media.timeMode,
repeat: state.media.repeat,
shuffle: state.media.shuffle,
};
const doubled = useTypedSelector(Selectors.getDoubled);
const timeMode = useTypedSelector(Selectors.getTimeMode);
const repeat = useTypedSelector(Selectors.getRepeat);
const shuffle = useTypedSelector(Selectors.getShuffle);
return (
<>
{/* <Node label="Preferences..." /> */}
<SkinsContextMenu />
<Hr />
<Node
label="Time elapsed"
hotkey="(Ctrl+T toggles)"
onClick={toggleTimeMode}
checked={timeMode === TIME_MODE.ELAPSED}
/>
<Node
label="Time remaining"
hotkey="(Ctrl+T toggles)"
onClick={toggleTimeMode}
checked={timeMode === TIME_MODE.REMAINING}
/>
{/* <Node label="Always On Top" hotkey="Ctrl+A" /> */}
<Node
label="Double Size"
hotkey="Ctrl+D"
onClick={toggleDoubleSizeMode}
checked={doubled}
/>
{/* <Node label="EasyMove" hotkey="Ctrl+E" /> */}
<Hr />
<Node label="Repeat" hotkey="R" onClick={toggleRepeat} checked={repeat} />
<Node
label="Shuffle"
hotkey="S"
onClick={toggleShuffle}
checked={shuffle}
/>
</>
);
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
toggleTimeMode: () => dispatch(Actions.toggleTimeMode()),
toggleDoubleSizeMode: () => dispatch(Actions.toggleDoubleSizeMode()),
toggleRepeat: () => dispatch(Actions.toggleRepeat()),
toggleShuffle: () => dispatch(Actions.toggleShuffle()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(OptionsContextMenu);
export default OptionsContextMenu;

View file

@ -1,74 +1,51 @@
import React from "react";
import { connect } from "react-redux";
import * as Actions from "../actionCreators";
import { Hr, Node } from "./ContextMenu";
import { Dispatch } from "../types";
import { useActionCreator } from "../hooks";
interface Props {
previous(): void;
play(): void;
pause(): void;
stop(): void;
next(): void;
seekBackward(steps: number): void;
seekForward(steps: number): void;
nextN(steps: number): void;
}
const PlaybackContextMenu = (props: Props) => (
<React.Fragment>
<Node label="Previous" hotkey="Z" onClick={props.previous} />
<Node label="Play" hotkey="X" onClick={props.play} />
<Node label="Pause" hotkey="C" onClick={props.pause} />
<Node label="Stop" hotkey="V" onClick={props.stop} />
<Node label="Next" hotkey="B" onClick={props.next} />
<Hr />
{/*
const PlaybackContextMenu = () => {
const previous = useActionCreator(Actions.previous);
const play = useActionCreator(Actions.play);
const pause = useActionCreator(Actions.pause);
const stop = useActionCreator(Actions.stop);
const next = useActionCreator(Actions.next);
const seekForward = useActionCreator(Actions.seekForward);
const seekBackward = useActionCreator(Actions.seekBackward);
const nextN = useActionCreator(Actions.nextN);
return (
<React.Fragment>
<Node label="Previous" hotkey="Z" onClick={previous} />
<Node label="Play" hotkey="X" onClick={play} />
<Node label="Pause" hotkey="C" onClick={pause} />
<Node label="Stop" hotkey="V" onClick={stop} />
<Node label="Next" hotkey="B" onClick={next} />
<Hr />
{/*
<Node label="Stop w/ fadeout" hotkey="Shift+V" />
<Node label="Stop after current" hotkey="Ctrl+V" />
*/}
<Node
label="Back 5 seconds"
hotkey="Left"
onClick={() => props.seekBackward(5)}
/>
<Node
label="Fwd 5 seconds"
hotkey="Right"
onClick={() => props.seekForward(5)}
/>
{/*
<Node
label="Back 5 seconds"
hotkey="Left"
onClick={() => seekBackward(5)}
/>
<Node
label="Fwd 5 seconds"
hotkey="Right"
onClick={() => seekForward(5)}
/>
{/*
<Node label="Start of list" hotkey="Ctrl+Z" />
*/}
<Node
label="10 tracks back"
hotkey="Num. 1"
onClick={() => props.nextN(-10)}
/>
<Node
label="10 tracks fwd"
hotkey="Num. 3"
onClick={() => props.nextN(10)}
/>
{/*
<Node label="10 tracks back" hotkey="Num. 1" onClick={() => nextN(-10)} />
<Node label="10 tracks fwd" hotkey="Num. 3" onClick={() => nextN(10)} />
{/*
<Hr />
<Node label="Jump to time" hotkey="Ctrl+J" />
<Node label="Jump to file" hotkey="J" />
*/}
</React.Fragment>
);
const mapDispatchToProps = (dispatch: Dispatch): Props => {
return {
previous: () => dispatch(Actions.previous()),
play: () => dispatch(Actions.play()),
pause: () => dispatch(Actions.pause()),
stop: () => dispatch(Actions.stop()),
next: () => dispatch(Actions.next()),
seekForward: steps => dispatch(Actions.seekForward(steps)),
seekBackward: steps => dispatch(Actions.seekBackward(steps)),
nextN: steps => dispatch(Actions.nextN(steps)),
};
</React.Fragment>
);
};
export default connect(null, mapDispatchToProps)(PlaybackContextMenu);
export default PlaybackContextMenu;

View file

@ -1,66 +1,24 @@
import React from "react";
import { connect } from "react-redux";
import { LOAD_STYLE } from "../../constants";
import { getTrackCount } from "../../selectors";
import { addTracksFromReferences } from "../../actionCreators";
import { promptForFileReferences } from "../../fileUtils";
import * as Selectors from "../../selectors";
import * as Actions from "../../actionCreators";
import PlaylistMenu from "./PlaylistMenu";
import { AppState, Dispatch } from "../../types";
interface StateProps {
nextIndex: number;
}
import { useTypedSelector, useActionCreator } from "../../hooks";
interface DispatchProps {
addFilesAtIndex(i: number): void;
addDirAtIndex(i: number): void;
}
const el = document.createElement("input");
el.type = "file";
// @ts-ingore
const DIR_SUPPORT =
// @ts-ignore
typeof el.webkitdirectory !== "undefined" ||
// @ts-ignore
typeof el.mozdirectory !== "undefined" ||
// @ts-ignore
typeof el.directory !== "undefined";
const AddMenu = () => {
const nextIndex = useTypedSelector(Selectors.getTrackCount);
const addDirAtIndex = useActionCreator(Actions.addDirAtIndex);
const addFilesAtIndex = useActionCreator(Actions.addFilesAtIndex);
return (
<PlaylistMenu id="playlist-add-menu">
<div
className="add-url"
onClick={() => window.alert("Not supported in Webamp")}
/>
<div className="add-dir" onClick={() => addDirAtIndex(nextIndex)} />
<div className="add-file" onClick={() => addFilesAtIndex(nextIndex)} />
</PlaylistMenu>
);
};
/* eslint-disable no-alert */
const AddMenu = ({
nextIndex,
addFilesAtIndex,
addDirAtIndex,
}: StateProps & DispatchProps) => (
<PlaylistMenu id="playlist-add-menu">
<div className="add-url" onClick={() => alert("Not supported in Webamp")} />
<div className="add-dir" onClick={() => addDirAtIndex(nextIndex)} />
<div className="add-file" onClick={() => addFilesAtIndex(nextIndex)} />
</PlaylistMenu>
);
const mapStateToProps = (state: AppState): StateProps => ({
nextIndex: getTrackCount(state),
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
addFilesAtIndex: async nextIndex => {
const fileReferences = await promptForFileReferences();
dispatch(
addTracksFromReferences(fileReferences, LOAD_STYLE.NONE, nextIndex)
);
},
addDirAtIndex: async nextIndex => {
if (!DIR_SUPPORT) {
alert("Not supported in your browser");
return;
}
const fileReferences = await promptForFileReferences({ directory: true });
dispatch(
addTracksFromReferences(fileReferences, LOAD_STYLE.NONE, nextIndex)
);
},
});
export default connect(mapStateToProps, mapDispatchToProps)(AddMenu);
export default AddMenu;

View file

@ -2,7 +2,7 @@ import React from "react";
import PlaylistMenu from "./PlaylistMenu";
import SortContextMenu from "./SortContextMenu";
import { ConnectedMiscOptionsContextMenu } from "./MiscOptionsContextMenu";
import MiscOptionsContextMenu from "./MiscOptionsContextMenu";
const MiscMenu = () => (
<PlaylistMenu id="playlist-misc-menu">
@ -15,7 +15,7 @@ const MiscMenu = () => (
/>
<div className="misc-options" onClick={e => e.stopPropagation()}>
<ConnectedMiscOptionsContextMenu />
<MiscOptionsContextMenu />
</div>
</PlaylistMenu>
);

View file

@ -1,34 +1,21 @@
import React from "react";
import { connect } from "react-redux";
import { Node } from "../ContextMenu";
import ContextMenuTarget from "../ContextMenuTarget";
import { Dispatch } from "../../types";
import { downloadHtmlPlaylist } from "../../actionCreators";
import * as Actions from "../../actionCreators";
import { useActionCreator } from "../../hooks";
interface DispatchProps {
downloadHtmlPlaylist: () => void;
}
const MiscOptionsContextMenu = (props: DispatchProps) => (
<ContextMenuTarget
top
renderMenu={() => (
<Node
onClick={props.downloadHtmlPlaylist}
label="Generate HTML playlist"
/>
)}
>
<div />
</ContextMenuTarget>
);
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
downloadHtmlPlaylist: () => dispatch(downloadHtmlPlaylist()),
};
const MiscOptionsContextMenu = () => {
const downloadHtmlPlaylist = useActionCreator(Actions.downloadHtmlPlaylist);
return (
<ContextMenuTarget
top
renderMenu={() => (
<Node onClick={downloadHtmlPlaylist} label="Generate HTML playlist" />
)}
>
<div />
</ContextMenuTarget>
);
};
export const ConnectedMiscOptionsContextMenu = connect(
null,
mapDispatchToProps
)(MiscOptionsContextMenu);
export default MiscOptionsContextMenu;

View file

@ -1,55 +1,31 @@
import React from "react";
import { connect } from "react-redux";
import {
play,
pause,
stop,
next,
previous,
openMediaFileDialog,
} from "../../actionCreators";
import * as Actions from "../../actionCreators";
import MiniTime from "../MiniTime";
import RunningTimeDisplay from "./RunningTimeDisplay";
import { Dispatch } from "../../types";
import { useActionCreator } from "../../hooks";
interface Props {
previous: () => void;
play: () => void;
pause: () => void;
stop: () => void;
next: () => void;
openMediaFileDialog: () => void;
}
const PlaylistWindow = (props: Props) => (
<React.Fragment>
<RunningTimeDisplay />
<div className="playlist-action-buttons">
<div className="playlist-previous-button" onClick={props.previous} />
<div className="playlist-play-button" onClick={props.play} />
<div className="playlist-pause-button" onClick={props.pause} />
<div className="playlist-stop-button" onClick={props.stop} />
<div className="playlist-next-button" onClick={props.next} />
<div
className="playlist-eject-button"
onClick={props.openMediaFileDialog}
/>
</div>
<MiniTime />
</React.Fragment>
);
const mapDispatchToProps = (dispatch: Dispatch): Props => {
return {
play: () => dispatch(play()),
pause: () => dispatch(pause()),
stop: () => dispatch(stop()),
openMediaFileDialog: () => dispatch(openMediaFileDialog()),
next: () => dispatch(next()),
previous: () => dispatch(previous()),
};
const PlaylistWindow = () => {
const play = useActionCreator(Actions.play);
const pause = useActionCreator(Actions.pause);
const stop = useActionCreator(Actions.stop);
const openMediaFileDialog = useActionCreator(Actions.openMediaFileDialog);
const next = useActionCreator(Actions.next);
const previous = useActionCreator(Actions.previous);
return (
<React.Fragment>
<RunningTimeDisplay />
<div className="playlist-action-buttons">
<div className="playlist-previous-button" onClick={previous} />
<div className="playlist-play-button" onClick={play} />
<div className="playlist-pause-button" onClick={pause} />
<div className="playlist-stop-button" onClick={stop} />
<div className="playlist-next-button" onClick={next} />
<div className="playlist-eject-button" onClick={openMediaFileDialog} />
</div>
<MiniTime />
</React.Fragment>
);
};
export default connect(null, mapDispatchToProps)(PlaylistWindow);
export default PlaylistWindow;

View file

@ -1,29 +1,27 @@
import { connect } from "react-redux";
import React from "react";
import ResizeTarget from "../ResizeTarget";
import { setWindowSize } from "../../actionCreators";
import { getWindowSize } from "../../selectors";
import { AppState, Dispatch } from "../../types";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import { useTypedSelector, useActionCreator } from "../../hooks";
interface StateProps {
currentSize: [number, number];
id: string;
}
interface DispatchProps {
currentSize: [number, number];
id: string;
}
const mapStateToProps = (state: AppState): StateProps => ({
currentSize: getWindowSize(state)("playlist"),
id: "playlist-resize-target",
});
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
setWindowSize: (size: [number, number]) =>
dispatch(setWindowSize("playlist", size)),
};
type Props = {
widthOnly?: boolean;
};
export default connect(mapStateToProps, mapDispatchToProps)(ResizeTarget);
function PlaylistResizeTarget({ widthOnly }: Props) {
const windowSize = useTypedSelector(Selectors.getWindowSize);
const setWindowSize = useActionCreator(Actions.setWindowSize);
const currentSize = windowSize("playlist");
return (
<ResizeTarget
currentSize={currentSize}
id="playlist-resize-target"
setWindowSize={size => {
setWindowSize("playlist", size);
}}
widthOnly={widthOnly}
/>
);
}
export default PlaylistResizeTarget;

View file

@ -1,8 +1,6 @@
import React, { useMemo } from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { getTimeStr } from "../../utils";
import { SET_FOCUSED_WINDOW } from "../../actionTypes";
import * as Selectors from "../../selectors";
import {
@ -12,36 +10,22 @@ import {
CHARACTER_WIDTH,
UTF8_ELLIPSIS,
} from "../../constants";
import { togglePlaylistShadeMode, closeWindow } from "../../actionCreators";
import * as Actions from "../../actionCreators";
import CharacterString from "../CharacterString";
import PlaylistResizeTarget from "./PlaylistResizeTarget";
import { AppState, WindowId, Dispatch } from "../../types";
import { useTypedSelector, useActionCreator } from "../../hooks";
interface StateProps {
name: string | null;
duration: number | null;
playlistSize: [number, number];
focused: WindowId | null;
trackOrder: number[];
}
function PlaylistShade() {
const focused = useTypedSelector(Selectors.getFocusedWindow);
const getWindowSize = useTypedSelector(Selectors.getWindowSize);
const playlistSize = getWindowSize("playlist");
const duration = useTypedSelector(Selectors.getDuration);
const name = useTypedSelector(Selectors.getMinimalMediaText);
interface DispatchProps {
focusPlaylist: () => void;
close: () => void;
toggleShade: () => void;
}
const closeWindow = useActionCreator(Actions.closeWindow);
const toggleShade = useActionCreator(Actions.togglePlaylistShadeMode);
const focusWindow = useActionCreator(Actions.setFocusedWindow);
type Props = StateProps & DispatchProps;
function PlaylistShade({
name,
playlistSize,
duration,
toggleShade,
close,
focusPlaylist,
focused,
}: Props) {
const addedWidth = playlistSize[0] * WINDOW_RESIZE_SEGMENT_WIDTH;
const trimmedName = useMemo(() => {
@ -55,11 +39,11 @@ function PlaylistShade({
return name.length > nameLength
? name.slice(0, nameLength - 1) + UTF8_ELLIPSIS
: name;
}, [name, addedWidth]);
}, [addedWidth, name]);
const time = useMemo(() => {
return name == null ? "" : getTimeStr(duration);
}, [name, duration]);
}, [duration, name]);
return (
<div
@ -68,7 +52,7 @@ function PlaylistShade({
selected: focused === WINDOWS.PLAYLIST,
})}
style={{ width: `${WINDOW_WIDTH + addedWidth}px` }}
onMouseDown={focusPlaylist}
onMouseDown={() => focusWindow("playlist")}
onDoubleClick={toggleShade}
>
<div className="left">
@ -81,37 +65,14 @@ function PlaylistShade({
</div>
<PlaylistResizeTarget widthOnly />
<div id="playlist-shade-button" onClick={toggleShade} />
<div id="playlist-close-button" onClick={close} />
<div
id="playlist-close-button"
onClick={() => closeWindow("playlist")}
/>
</div>
</div>
</div>
);
}
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
focusPlaylist: () =>
dispatch({
type: SET_FOCUSED_WINDOW,
window: WINDOWS.PLAYLIST,
}),
close: () => dispatch(closeWindow("playlist")),
toggleShade: () => dispatch(togglePlaylistShadeMode()),
};
};
const mapStateToProps = (state: AppState): StateProps => {
const duration = Selectors.getDuration(state);
const {
windows: { focused },
} = state;
return {
focused,
playlistSize: Selectors.getWindowSize(state)("playlist"),
trackOrder: Selectors.getOrderedTracks(state),
duration,
name: Selectors.getMinimalMediaText(state),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PlaylistShade);
export default PlaylistShade;

View file

@ -1,38 +1,25 @@
import React from "react";
import { connect } from "react-redux";
import {
cropPlaylist,
removeSelectedTracks,
removeAllTracks,
} from "../../actionCreators";
import * as Actions from "../../actionCreators";
import PlaylistMenu from "./PlaylistMenu";
import { Dispatch } from "../../types";
import { useActionCreator } from "../../hooks";
/* eslint-disable no-alert */
interface DispatchProps {
removeSelected: () => void;
removeAll: () => void;
crop: () => void;
}
const RemoveMenu = (props: DispatchProps) => (
<PlaylistMenu id="playlist-remove-menu">
<div
className="remove-misc"
onClick={() => alert("Not supported in Webamp")}
/>
<div className="remove-all" onClick={props.removeAll} />
<div className="crop" onClick={props.crop} />
<div className="remove-selected" onClick={props.removeSelected} />
</PlaylistMenu>
);
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
removeSelected: () => dispatch(removeSelectedTracks()),
removeAll: () => dispatch(removeAllTracks()),
crop: () => dispatch(cropPlaylist()),
};
const RemoveMenu = () => {
const removeSelected = useActionCreator(Actions.removeSelectedTracks);
const removeAll = useActionCreator(Actions.removeAllTracks);
const crop = useActionCreator(Actions.cropPlaylist);
return (
<PlaylistMenu id="playlist-remove-menu">
<div
className="remove-misc"
onClick={() => alert("Not supported in Webamp")}
/>
<div className="remove-all" onClick={removeAll} />
<div className="crop" onClick={crop} />
<div className="remove-selected" onClick={removeSelected} />
</PlaylistMenu>
);
};
export default connect(() => ({}), mapDispatchToProps)(RemoveMenu);
export default RemoveMenu;

View file

@ -1,9 +1,8 @@
import React from "react";
import { connect } from "react-redux";
import React, { useMemo } from "react";
import CharacterString from "../CharacterString";
import { getRunningTimeMessage } from "../../selectors";
import { AppState } from "../../types";
import * as Actions from "../../selectors";
import { useTypedSelector } from "../../hooks";
// While all the browsers I care about support String.prototype.padEnd,
// Not all node versions do, and I want tests to pass in Jest...
@ -15,23 +14,19 @@ function rightPad(str: string, len: number, fillChar: string): string {
return str;
}
interface Props {
runningTimeMessage: string;
}
const RunningTimeDisplay = (props: Props) => (
<div className="playlist-running-time-display draggable">
{/* This div is probably not strictly needed */}
<div>
<CharacterString>
{rightPad(props.runningTimeMessage, 18, " ")}
</CharacterString>
const RunningTimeDisplay = () => {
const runningTimeMessage = useTypedSelector(Actions.getRunningTimeMessage);
const text = useMemo(() => rightPad(runningTimeMessage, 18, " "), [
runningTimeMessage,
]);
return (
<div className="playlist-running-time-display draggable">
{/* This div is probably not strictly needed */}
<div>
<CharacterString>{text}</CharacterString>
</div>
</div>
</div>
);
);
};
const mapStateToProps = (state: AppState): Props => ({
runningTimeMessage: getRunningTimeMessage(state),
});
export default connect(mapStateToProps)(RunningTimeDisplay);
export default RunningTimeDisplay;

View file

@ -1,64 +1,54 @@
import React, { useCallback, ReactNode } from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import {
CLICKED_TRACK,
CTRL_CLICKED_TRACK,
SHIFT_CLICKED_TRACK,
PLAY_TRACK,
} from "../../actionTypes";
import * as Selectors from "../../selectors";
import { AppState, Dispatch, PlaylistStyle } from "../../types";
import * as Actions from "../../actionCreators";
import {
useTypedSelector,
useActionCreator,
useTypedDispatch,
} from "../../hooks";
interface OwnProps {
interface Props {
id: number;
index: number;
handleMoveClick: (e: React.MouseEvent<HTMLDivElement>) => void;
children: ReactNode;
}
interface StateProps {
selected: boolean;
current: boolean;
skinPlaylistStyle: PlaylistStyle;
}
function TrackCell({ children, handleMoveClick, index, id }: Props) {
const skinPlaylistStyle = useTypedSelector(Selectors.getSkinPlaylistStyle);
const selectedTrackIds = useTypedSelector(Selectors.getSelectedTrackIds);
const currentTrackId = useTypedSelector(Selectors.getCurrentTrackId);
const selected = selectedTrackIds.has(id);
const current = currentTrackId === id;
interface DispatchProps {
shiftClick: (e: React.MouseEvent<HTMLDivElement>) => void;
ctrlClick: (e: React.MouseEvent<HTMLDivElement>) => void;
click: () => void;
onDoubleClick: () => void;
}
const dispatch = useTypedDispatch();
const playTrack = useActionCreator(Actions.playTrack);
type Props = OwnProps & StateProps & DispatchProps;
function TrackCell({
skinPlaylistStyle,
selected,
current,
children,
shiftClick,
ctrlClick,
onDoubleClick,
handleMoveClick,
click,
}: Props) {
const onMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (e.shiftKey) {
shiftClick(e);
e.preventDefault();
dispatch({ type: SHIFT_CLICKED_TRACK, index });
return;
} else if (e.metaKey || e.ctrlKey) {
ctrlClick(e);
e.preventDefault();
dispatch({ type: CTRL_CLICKED_TRACK, index });
return;
}
if (!selected) {
click();
dispatch({ type: CLICKED_TRACK, index });
}
handleMoveClick(e);
},
[click, ctrlClick, shiftClick, handleMoveClick, selected]
[dispatch, handleMoveClick, index, selected]
);
const style: React.CSSProperties = {
@ -72,35 +62,11 @@ function TrackCell({
onClick={e => e.stopPropagation()}
onMouseDown={onMouseDown}
onContextMenu={e => e.preventDefault()}
onDoubleClick={onDoubleClick}
onDoubleClick={() => playTrack(id)}
>
{children}
</div>
);
}
const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => {
return {
skinPlaylistStyle: Selectors.getSkinPlaylistStyle(state),
selected: Selectors.getSelectedTrackIds(state).has(ownProps.id),
current: Selectors.getCurrentTrackId(state) === ownProps.id,
};
};
const mapDispatchToProps = (
dispatch: Dispatch,
ownProps: OwnProps
): DispatchProps => ({
shiftClick: e => {
e.preventDefault();
return dispatch({ type: SHIFT_CLICKED_TRACK, index: ownProps.index });
},
ctrlClick: e => {
e.preventDefault();
return dispatch({ type: CTRL_CLICKED_TRACK, index: ownProps.index });
},
click: () => dispatch({ type: CLICKED_TRACK, index: ownProps.index }),
onDoubleClick: () => dispatch({ type: PLAY_TRACK, id: ownProps.id }),
});
export default connect(mapStateToProps, mapDispatchToProps)(TrackCell);
export default TrackCell;

View file

@ -1,39 +1,27 @@
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { getTimeStr } from "../../utils";
import {
getVisibleTrackIds,
getScrollOffset,
getNumberOfTracks,
getTracks,
} from "../../selectors";
import * as Selectors from "../../selectors";
import { TRACK_HEIGHT } from "../../constants";
import { SELECT_ZERO } from "../../actionTypes";
import { dragSelected, scrollPlaylistByDelta } from "../../actionCreators";
import * as Actions from "../../actionCreators";
import TrackCell from "./TrackCell";
import TrackTitle from "./TrackTitle";
import { Dispatch, AppState } from "../../types";
import { TracksState } from "../../reducers/tracks";
interface DispatchProps {
selectZero: () => void;
scrollPlaylistByDelta: (e: React.WheelEvent<HTMLDivElement>) => void;
dragSelected: (offset: number) => void;
}
interface StateProps {
trackIds: number[];
offset: number;
numberOfTracks: number;
tracks: TracksState;
}
import { useTypedSelector, useActionCreator } from "../../hooks";
function getNumberLength(number: number): number {
return number.toString().length;
}
function TrackList(props: DispatchProps & StateProps) {
function TrackList() {
const offset = useTypedSelector(Selectors.getScrollOffset);
const trackIds = useTypedSelector(Selectors.getVisibleTrackIds);
const tracks = useTypedSelector(Selectors.getTracks);
const numberOfTracks = useTypedSelector(Selectors.getNumberOfTracks);
const selectZero = useActionCreator(Actions.selectZero);
const dragSelected = useActionCreator(Actions.dragSelected);
const scrollPlaylistByDelta = useActionCreator(Actions.scrollPlaylistByDelta);
const [node, setNode] = useState<Element | null>(null);
const [moving, setMoving] = useState(false);
const [mouseStartY, setMouseStartY] = useState<number | null>(null);
@ -58,7 +46,7 @@ function TrackList(props: DispatchProps & StateProps) {
const proposedDiff = Math.floor((y - mouseStartY) / TRACK_HEIGHT);
if (proposedDiff !== lastDiff) {
const diffDiff = proposedDiff - lastDiff;
props.dragSelected(diffDiff);
dragSelected(diffDiff);
lastDiff = proposedDiff;
}
};
@ -78,11 +66,11 @@ function TrackList(props: DispatchProps & StateProps) {
function _renderTracks(
format: (id: number, i: number) => JSX.Element | string
) {
return props.trackIds.map((id, i) => (
return trackIds.map((id, i) => (
<TrackCell
key={id}
id={id}
index={props.offset + i}
index={offset + i}
handleMoveClick={_handleMoveClick}
>
{format(id, i)}
@ -90,8 +78,7 @@ function TrackList(props: DispatchProps & StateProps) {
));
}
const { tracks, offset } = props;
const maxTrackNumberLength = getNumberLength(props.numberOfTracks);
const maxTrackNumberLength = getNumberLength(numberOfTracks);
const paddedTrackNumForIndex = (i: number) =>
(i + 1 + offset).toString().padStart(maxTrackNumberLength, "\u00A0");
return (
@ -99,8 +86,8 @@ function TrackList(props: DispatchProps & StateProps) {
ref={setNode}
className="playlist-tracks"
style={{ height: "100%" }}
onClick={props.selectZero}
onWheel={props.scrollPlaylistByDelta}
onClick={selectZero}
onWheel={scrollPlaylistByDelta}
>
<div className="playlist-track-titles">
{_renderTracks((id, i) => (
@ -114,18 +101,4 @@ function TrackList(props: DispatchProps & StateProps) {
);
}
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
selectZero: () => dispatch({ type: SELECT_ZERO }),
dragSelected: (offset: number) => dispatch(dragSelected(offset)),
scrollPlaylistByDelta: (e: React.WheelEvent<HTMLDivElement>) =>
dispatch(scrollPlaylistByDelta(e)),
});
const mapStateToProps = (state: AppState): StateProps => ({
offset: getScrollOffset(state),
trackIds: getVisibleTrackIds(state),
tracks: getTracks(state),
numberOfTracks: getNumberOfTracks(state),
});
export default connect(mapStateToProps, mapDispatchToProps)(TrackList);
export default TrackList;

View file

@ -1,25 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import { getTrackDisplayName } from "../../selectors";
import { AppState } from "../../types";
import * as Selectors from "../../selectors";
import { useTypedSelector } from "../../hooks";
interface OwnProps {
interface Props {
id: number;
paddedTrackNumber: string;
}
interface StateProps {
title: string | null;
}
const TrackTitle = ({ id, paddedTrackNumber }: Props) => {
const title = useTypedSelector(Selectors.getTrackDisplayName)(id);
return (
<span>
{paddedTrackNumber}. {title}
</span>
);
};
const TrackTitle = (props: OwnProps & StateProps) => (
<span>
{props.paddedTrackNumber}. {props.title}
</span>
);
const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => ({
title: getTrackDisplayName(state)(ownProps.id),
});
export default connect(mapStateToProps)(TrackTitle);
export default TrackTitle;

View file

@ -1,5 +1,4 @@
import React, { useCallback } from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { WINDOWS, TRACK_HEIGHT, LOAD_STYLE } from "../../constants";
@ -21,56 +20,44 @@ import TrackList from "./TrackList";
import ScrollBar from "./ScrollBar";
import "../../../css/playlist-window.css";
import { AppState, PlaylistStyle, Dispatch } from "../../types";
import { AppState } from "../../types";
import FocusTarget from "../FocusTarget";
import { useActionCreator, useTypedSelector } from "../../hooks";
interface StateProps {
offset: number;
maxTrackIndex: number;
playlistWindowPixelSize: { width: number; height: number };
showVisualizer: boolean;
activateVisualizer: boolean;
selected: boolean;
skinPlaylistStyle: PlaylistStyle;
playlistSize: [number, number];
playlistShade: boolean;
duration: number | null;
}
interface DispatchProps {
close(): void;
toggleShade(): void;
scrollUpFourTracks(): void;
scrollDownFourTracks(): void;
loadMedia(e: React.DragEvent<HTMLDivElement>, startIndex: number): void;
scrollVolume(e: React.WheelEvent<HTMLDivElement>): void;
}
interface OwnProps {
interface Props {
analyser: AnalyserNode;
}
type Props = StateProps & DispatchProps & OwnProps;
function _maxTrackIndex(state: AppState) {
return state.playlist.trackOrder.length - 1;
}
function PlaylistWindow({
skinPlaylistStyle,
selected,
playlistSize,
playlistWindowPixelSize,
playlistShade,
close,
toggleShade,
analyser,
showVisualizer,
activateVisualizer,
maxTrackIndex,
offset,
loadMedia,
scrollUpFourTracks,
scrollDownFourTracks,
scrollVolume,
}: Props) {
const _handleDrop = useCallback(
function PlaylistWindow({ analyser }: Props) {
const offset = useTypedSelector(Selectors.getScrollOffset);
const getWindowSize = useTypedSelector(Selectors.getWindowSize);
const selectedWindow = useTypedSelector(Selectors.getFocusedWindow);
const getWindowShade = useTypedSelector(Selectors.getWindowShade);
const getWindowOpen = useTypedSelector(Selectors.getWindowOpen);
const maxTrackIndex = useTypedSelector(_maxTrackIndex);
const skinPlaylistStyle = useTypedSelector(Selectors.getSkinPlaylistStyle);
const getWindowPixelSize = useTypedSelector(Selectors.getWindowPixelSize);
const selected = selectedWindow === WINDOWS.PLAYLIST;
const playlistShade = Boolean(getWindowShade(WINDOWS.PLAYLIST));
const playlistSize = getWindowSize(WINDOWS.PLAYLIST);
const playlistWindowPixelSize = getWindowPixelSize(WINDOWS.PLAYLIST);
const close = useActionCreator(Actions.closeWindow);
const toggleShade = useActionCreator(Actions.togglePlaylistShadeMode);
const scrollUpFourTracks = useActionCreator(Actions.scrollUpFourTracks);
const scrollDownFourTracks = useActionCreator(Actions.scrollDownFourTracks);
const scrollVolume = useActionCreator(Actions.scrollVolume);
const loadMedia = useActionCreator(Actions.loadMedia);
const showVisualizer = playlistSize[0] > 2;
const activateVisualizer = !getWindowOpen(WINDOWS.MAIN);
const handleDrop = useCallback(
(
e: React.DragEvent<HTMLDivElement>,
targetCoords: { x: number; y: number }
@ -81,9 +68,9 @@ function PlaylistWindow({
0,
maxTrackIndex + 1
);
loadMedia(e, atIndex);
loadMedia(e, LOAD_STYLE.NONE, atIndex);
},
[loadMedia, offset, maxTrackIndex]
[loadMedia, maxTrackIndex, offset]
);
if (playlistShade) {
@ -108,7 +95,7 @@ function PlaylistWindow({
id="playlist-window"
className={classes}
style={style}
handleDrop={_handleDrop}
handleDrop={handleDrop}
onWheel={scrollVolume}
>
<div className="playlist-top draggable" onDoubleClick={toggleShade}>
@ -124,7 +111,10 @@ function PlaylistWindow({
<div className="playlist-top-right-fill draggable" />
<div className="playlist-top-right draggable">
<div id="playlist-shade-button" onClick={toggleShade} />
<div id="playlist-close-button" onClick={close} />
<div
id="playlist-close-button"
onClick={() => close(WINDOWS.PLAYLIST)}
/>
</div>
</div>
<div className="playlist-middle draggable">
@ -172,38 +162,4 @@ function PlaylistWindow({
);
}
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
close: () => dispatch(Actions.closeWindow(WINDOWS.PLAYLIST)),
toggleShade: () => dispatch(Actions.togglePlaylistShadeMode()),
scrollUpFourTracks: () => dispatch(Actions.scrollUpFourTracks()),
scrollDownFourTracks: () => dispatch(Actions.scrollDownFourTracks()),
loadMedia: (e, startIndex) =>
dispatch(Actions.loadMedia(e, LOAD_STYLE.NONE, startIndex)),
scrollVolume: e => dispatch(Actions.scrollVolume(e)),
};
};
const mapStateToProps = (state: AppState): StateProps => {
const {
playlist: { trackOrder },
} = state;
const playlistSize = Selectors.getWindowSize(state)(WINDOWS.PLAYLIST);
return {
offset: Selectors.getScrollOffset(state),
maxTrackIndex: trackOrder.length - 1,
playlistWindowPixelSize: Selectors.getWindowPixelSize(state)(
WINDOWS.PLAYLIST
),
showVisualizer: playlistSize[0] > 2,
activateVisualizer: !Selectors.getWindowOpen(state)(WINDOWS.MAIN),
playlistSize,
selected: Selectors.getFocusedWindow(state) === WINDOWS.PLAYLIST,
skinPlaylistStyle: Selectors.getSkinPlaylistStyle(state),
playlistShade: Boolean(Selectors.getWindowShade(state)(WINDOWS.PLAYLIST)),
duration: Selectors.getDuration(state),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PlaylistWindow);
export default PlaylistWindow;

View file

@ -1,52 +1,28 @@
import React from "react";
import { connect } from "react-redux";
import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { Hr, Node, Parent } from "./ContextMenu";
import { AppState, Skin, Dispatch } from "../types";
import { useActionCreator, useTypedSelector } from "../hooks";
interface StateProps {
availableSkins: Skin[];
}
const SkinContextMenu = () => {
const loadDefaultSkin = useActionCreator(Actions.loadDefaultSkin);
const openSkinFileDialog = useActionCreator(Actions.openSkinFileDialog);
const setSkin = useActionCreator(Actions.setSkinFromUrl);
interface DispatchProps {
loadDefaultSkin(): void;
setSkin(url: string): void;
openSkinFileDialog(): void;
}
type Props = StateProps & DispatchProps;
const SkinContextMenu = (props: Props) => (
<Parent label="Skins">
<Node onClick={props.openSkinFileDialog} label="Load Skin..." />
<Hr />
<Node onClick={props.loadDefaultSkin} label={"<Base Skin>"} />
{props.availableSkins.map(skin => (
<Node
key={skin.url}
onClick={() => props.setSkin(skin.url)}
label={skin.name}
/>
))}
</Parent>
);
const mapStateToProps = (state: AppState): StateProps => ({
availableSkins: state.settings.availableSkins,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
loadDefaultSkin() {
dispatch(Actions.loadDefaultSkin());
},
openSkinFileDialog() {
dispatch(Actions.openSkinFileDialog());
},
setSkin(url: string) {
dispatch(Actions.setSkinFromUrl(url));
},
};
const availableSkins = useTypedSelector(Selectors.getAvaliableSkins);
return (
<Parent label="Skins">
<Node onClick={openSkinFileDialog} label="Load Skin..." />
<Hr />
<Node onClick={loadDefaultSkin} label={"<Base Skin>"} />
{availableSkins.map(skin => (
<Node
key={skin.url}
onClick={() => setSkin(skin.url)}
label={skin.name}
/>
))}
</Parent>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(SkinContextMenu);
export default SkinContextMenu;

View file

@ -1,4 +1,4 @@
import { Action, StatePreset, TransitionType } from "../types";
import { Action, StatePreset, TransitionType, MilkdropMessage } from "../types";
import {
SET_MILKDROP_DESKTOP,
SET_MILKDROP_FULLSCREEN,
@ -14,11 +14,6 @@ import {
} from "../actionTypes";
import * as Utils from "../utils";
interface Message {
text: string;
time: number;
}
export interface MilkdropState {
display: "WINDOW" | "DESKTOP" | "FULLSCREEN";
overlay: boolean;
@ -31,7 +26,7 @@ export interface MilkdropState {
cycling: boolean;
// TODO: This could probably be simplified to just a date and we could assume
// the song title is the message.
message: Message | null;
message: MilkdropMessage | null;
}
const defaultMilkdropState: MilkdropState = {

View file

@ -14,6 +14,7 @@ import {
Cursors,
SkinRegion,
GenLetterWidths,
MilkdropMessage,
} from "./types";
import { createSelector, defaultMemoize } from "reselect";
import * as Utils from "./utils";
@ -634,6 +635,10 @@ export function getDebugData(state: AppState) {
};
}
export function getMilkdropMessage(state: AppState): MilkdropMessage | null {
return state.milkdrop.message;
}
export function getMilkdropDesktopEnabled(state: AppState): boolean {
return state.milkdrop.display === "DESKTOP";
}
@ -752,3 +757,7 @@ export function getLoading(state: AppState): boolean {
export function getWorking(state: AppState): boolean {
return state.display.working;
}
export function getAvaliableSkins(state: AppState) {
return state.settings.availableSkins;
}

View file

@ -55,6 +55,11 @@ export type Skin = {
name: string;
};
export interface MilkdropMessage {
text: string;
time: number;
}
export type Band =
| 60
| 170