mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-31 22:14:09 +00:00
Use Redux hooks in all functional components
This commit is contained in:
parent
9933fde444
commit
df74fe6b4f
33 changed files with 633 additions and 1346 deletions
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ export {
|
|||
downloadPreset,
|
||||
setEqFromObject,
|
||||
downloadHtmlPlaylist,
|
||||
addDirAtIndex,
|
||||
addFilesAtIndex,
|
||||
} from "./files";
|
||||
export {
|
||||
cropPlaylist,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ export type Skin = {
|
|||
name: string;
|
||||
};
|
||||
|
||||
export interface MilkdropMessage {
|
||||
text: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type Band =
|
||||
| 60
|
||||
| 170
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue