diff --git a/js/actionCreators/files.ts b/js/actionCreators/files.ts index 455c3419..f85eb079 100644 --- a/js/actionCreators/files.ts +++ b/js/actionCreators/files.ts @@ -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) + ); + }; +} diff --git a/js/actionCreators/index.ts b/js/actionCreators/index.ts index c58b345f..67419536 100644 --- a/js/actionCreators/index.ts +++ b/js/actionCreators/index.ts @@ -90,6 +90,8 @@ export { downloadPreset, setEqFromObject, downloadHtmlPlaylist, + addDirAtIndex, + addFilesAtIndex, } from "./files"; export { cropPlaylist, diff --git a/js/components/EqualizerWindow/EqOn.tsx b/js/components/EqualizerWindow/EqOn.tsx index aa56447f..36d5794f 100644 --- a/js/components/EqualizerWindow/EqOn.tsx +++ b/js/components/EqualizerWindow/EqOn.tsx @@ -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 ( -
+
); }; -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; diff --git a/js/components/EqualizerWindow/EqualizerShade.tsx b/js/components/EqualizerWindow/EqualizerShade.tsx index 64b01582..affbab13 100644 --- a/js/components/EqualizerWindow/EqualizerShade.tsx +++ b/js/components/EqualizerWindow/EqualizerShade.tsx @@ -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 (
-
-
+
+
closeWindow("equalizer")} />
); }; - -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; diff --git a/js/components/EqualizerWindow/PresetsContextMenu.tsx b/js/components/EqualizerWindow/PresetsContextMenu.tsx index 39bb8e00..5a2404f9 100644 --- a/js/components/EqualizerWindow/PresetsContextMenu.tsx +++ b/js/components/EqualizerWindow/PresetsContextMenu.tsx @@ -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) => ( - ( - <> - - {builtin.presets.map(preset => ( - props.setEqFromObject(preset)} - label={preset.name} - /> - ))} -
- -
- - - )} - > -
- -); - -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 ( + ( + <> + + {builtin.presets.map(preset => ( + setEqFromObject(preset)} + label={preset.name} + /> + ))} +
+ +
+ + + )} + > +
+ + ); }; -export default connect(null, mapDispatchToProps)(PresetsContextMenu); +export default PresetsContextMenu; diff --git a/js/components/EqualizerWindow/index.tsx b/js/components/EqualizerWindow/index.tsx index 832c45f3..dc23dea2 100644 --- a/js/components/EqualizerWindow/index.tsx +++ b/js/components/EqualizerWindow/index.tsx @@ -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 (
- {props.shade ? ( + {shade ? ( ) : (
+
closeWindow(WINDOWS.EQUALIZER)} /> -
- -
-
-
+ +
+
+
{BANDS.map(hertz => ( setHertzValue(hertz, value)} /> ))}
@@ -93,24 +83,4 @@ const EqualizerWindow = (props: StateProps & DispatchProps) => {
); }; - -// 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; diff --git a/js/components/FocusTarget.tsx b/js/components/FocusTarget.tsx index d8c008a7..0dcb4348 100644 --- a/js/components/FocusTarget.tsx +++ b/js/components/FocusTarget.tsx @@ -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; diff --git a/js/components/GenWindow/__snapshots__/index.test.js.snap b/js/components/GenWindow/__snapshots__/index.test.js.snap deleted file mode 100644 index 2bff9770..00000000 --- a/js/components/GenWindow/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,120 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GenWindow renders to snapshot 1`] = ` -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/js/components/GenWindow/index.test.js b/js/components/GenWindow/index.test.js deleted file mode 100644 index 2226f7f2..00000000 --- a/js/components/GenWindow/index.test.js +++ /dev/null @@ -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( - - {}} - windowId="TEST_WINDOW_ID" - scrollVolume={() => {}} - windowSize={[0, 0]} - > - {() => {}} - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/js/components/GenWindow/index.tsx b/js/components/GenWindow/index.tsx index 585bd738..596d7b3e 100644 --- a/js/components/GenWindow/index.tsx +++ b/js/components/GenWindow/index.tsx @@ -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 (
close(windowId)} + onClick={() => closeWindow(windowId)} />
@@ -112,7 +95,7 @@ export const GenWindow = ({
setGenWindowSize(windowId, size)} + setWindowSize={size => setWindowSize(windowId, size)} id={"gen-resize-target"} />
@@ -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; diff --git a/js/components/MilkdropWindow/MilkdropContextMenu.tsx b/js/components/MilkdropWindow/MilkdropContextMenu.tsx index 410ff5f0..f672460d 100644 --- a/js/components/MilkdropWindow/MilkdropContextMenu.tsx +++ b/js/components/MilkdropWindow/MilkdropContextMenu.tsx @@ -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) => ( - { - return ( - <> - - -
- - - ); - }} - > - {props.children} -
-); + const closeWindow = useActionCreator(Actions.closeWindow); + const toggleDesktop = useActionCreator(Actions.toggleMilkdropDesktop); + const toggleFullscreen = useActionCreator(Actions.toggleMilkdropFullscreen); + return ( + { + return ( + <> + + +
+ closeWindow(WINDOWS.MILKDROP)} label="Quit" /> + + ); + }} + > + {props.children} +
+ ); +}; -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; diff --git a/js/components/MilkdropWindow/PresetOverlay.tsx b/js/components/MilkdropWindow/PresetOverlay.tsx index 715f42a2..5ae175f1 100644 --- a/js/components/MilkdropWindow/PresetOverlay.tsx +++ b/js/components/MilkdropWindow/PresetOverlay.tsx @@ -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; diff --git a/js/components/MilkdropWindow/Visualizer.tsx b/js/components/MilkdropWindow/Visualizer.tsx index 3756147c..10123a9b 100644 --- a/js/components/MilkdropWindow/Visualizer.tsx +++ b/js/components/MilkdropWindow/Visualizer.tsx @@ -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( 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; diff --git a/js/components/MilkdropWindow/index.tsx b/js/components/MilkdropWindow/index.tsx index 32a772b5..eb45fe8d 100644 --- a/js/components/MilkdropWindow/index.tsx +++ b/js/components/MilkdropWindow/index.tsx @@ -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 ( - + ); @@ -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 ( - - {props.overlay && } - -
- + + {overlay && } + +
+
@@ -165,34 +148,4 @@ function Milkdrop(props: Props) { ); } - -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; diff --git a/js/components/MiniTime.tsx b/js/components/MiniTime.tsx index 6c966d25..dc299eaf 100644 --- a/js/components/MiniTime.tsx +++ b/js/components/MiniTime.tsx @@ -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 = () => ( ); -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 (
@@ -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; diff --git a/js/components/OptionsContextMenu.tsx b/js/components/OptionsContextMenu.tsx index 031682c6..bfc1f8bd 100644 --- a/js/components/OptionsContextMenu.tsx +++ b/js/components/OptionsContextMenu.tsx @@ -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) => ( - - {/* */} - -
- - - {/* */} - - {/* */} -
- - -
-); - -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 ( + <> + {/* */} + +
+ + + {/* */} + + {/* */} +
+ + + + ); }; -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; diff --git a/js/components/PlaybackContextMenu.tsx b/js/components/PlaybackContextMenu.tsx index 3a3738b8..3b9d2b24 100644 --- a/js/components/PlaybackContextMenu.tsx +++ b/js/components/PlaybackContextMenu.tsx @@ -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) => ( - - - - - - -
- {/* +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 ( + + + + + + +
+ {/* */} - props.seekBackward(5)} - /> - props.seekForward(5)} - /> - {/* + seekBackward(5)} + /> + seekForward(5)} + /> + {/* */} - props.nextN(-10)} - /> - props.nextN(10)} - /> - {/* + nextN(-10)} /> + nextN(10)} /> + {/*
*/} -
-); - -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)), - }; +
+ ); }; -export default connect(null, mapDispatchToProps)(PlaybackContextMenu); +export default PlaybackContextMenu; diff --git a/js/components/PlaylistWindow/AddMenu.tsx b/js/components/PlaylistWindow/AddMenu.tsx index da590de0..e510c3ea 100644 --- a/js/components/PlaylistWindow/AddMenu.tsx +++ b/js/components/PlaylistWindow/AddMenu.tsx @@ -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 ( + +
window.alert("Not supported in Webamp")} + /> +
addDirAtIndex(nextIndex)} /> +
addFilesAtIndex(nextIndex)} /> + + ); +}; -/* eslint-disable no-alert */ - -const AddMenu = ({ - nextIndex, - addFilesAtIndex, - addDirAtIndex, -}: StateProps & DispatchProps) => ( - -
alert("Not supported in Webamp")} /> -
addDirAtIndex(nextIndex)} /> -
addFilesAtIndex(nextIndex)} /> - -); - -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; diff --git a/js/components/PlaylistWindow/MiscMenu.tsx b/js/components/PlaylistWindow/MiscMenu.tsx index af95fcbf..90960cd8 100644 --- a/js/components/PlaylistWindow/MiscMenu.tsx +++ b/js/components/PlaylistWindow/MiscMenu.tsx @@ -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 = () => ( @@ -15,7 +15,7 @@ const MiscMenu = () => ( />
e.stopPropagation()}> - +
); diff --git a/js/components/PlaylistWindow/MiscOptionsContextMenu.tsx b/js/components/PlaylistWindow/MiscOptionsContextMenu.tsx index 3e65ba65..28021687 100644 --- a/js/components/PlaylistWindow/MiscOptionsContextMenu.tsx +++ b/js/components/PlaylistWindow/MiscOptionsContextMenu.tsx @@ -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) => ( - ( - - )} - > -
- -); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => { - return { - downloadHtmlPlaylist: () => dispatch(downloadHtmlPlaylist()), - }; +const MiscOptionsContextMenu = () => { + const downloadHtmlPlaylist = useActionCreator(Actions.downloadHtmlPlaylist); + return ( + ( + + )} + > +
+ + ); }; -export const ConnectedMiscOptionsContextMenu = connect( - null, - mapDispatchToProps -)(MiscOptionsContextMenu); + +export default MiscOptionsContextMenu; diff --git a/js/components/PlaylistWindow/PlaylistActionArea.tsx b/js/components/PlaylistWindow/PlaylistActionArea.tsx index 81344e41..c8612b3a 100644 --- a/js/components/PlaylistWindow/PlaylistActionArea.tsx +++ b/js/components/PlaylistWindow/PlaylistActionArea.tsx @@ -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) => ( - - -
-
-
-
-
-
-
-
- - -); - -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 ( + + +
+
+
+
+
+
+
+
+ + + ); }; - -export default connect(null, mapDispatchToProps)(PlaylistWindow); +export default PlaylistWindow; diff --git a/js/components/PlaylistWindow/PlaylistResizeTarget.tsx b/js/components/PlaylistWindow/PlaylistResizeTarget.tsx index d255a124..99435927 100644 --- a/js/components/PlaylistWindow/PlaylistResizeTarget.tsx +++ b/js/components/PlaylistWindow/PlaylistResizeTarget.tsx @@ -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 ( + { + setWindowSize("playlist", size); + }} + widthOnly={widthOnly} + /> + ); +} +export default PlaylistResizeTarget; diff --git a/js/components/PlaylistWindow/PlaylistShade.tsx b/js/components/PlaylistWindow/PlaylistShade.tsx index 19aeba0e..361bdee1 100644 --- a/js/components/PlaylistWindow/PlaylistShade.tsx +++ b/js/components/PlaylistWindow/PlaylistShade.tsx @@ -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 (
focusWindow("playlist")} onDoubleClick={toggleShade} >
@@ -81,37 +65,14 @@ function PlaylistShade({
-
+
closeWindow("playlist")} + />
); } -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; diff --git a/js/components/PlaylistWindow/RemoveMenu.tsx b/js/components/PlaylistWindow/RemoveMenu.tsx index e30d1881..c0acd0dd 100644 --- a/js/components/PlaylistWindow/RemoveMenu.tsx +++ b/js/components/PlaylistWindow/RemoveMenu.tsx @@ -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) => ( - -
alert("Not supported in Webamp")} - /> -
-
-
- -); - -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 ( + +
alert("Not supported in Webamp")} + /> +
+
+
+ + ); }; -export default connect(() => ({}), mapDispatchToProps)(RemoveMenu); + +export default RemoveMenu; diff --git a/js/components/PlaylistWindow/RunningTimeDisplay.tsx b/js/components/PlaylistWindow/RunningTimeDisplay.tsx index ace3b1a2..1c69e41d 100644 --- a/js/components/PlaylistWindow/RunningTimeDisplay.tsx +++ b/js/components/PlaylistWindow/RunningTimeDisplay.tsx @@ -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) => ( -
- {/* This div is probably not strictly needed */} -
- - {rightPad(props.runningTimeMessage, 18, " ")} - +const RunningTimeDisplay = () => { + const runningTimeMessage = useTypedSelector(Actions.getRunningTimeMessage); + const text = useMemo(() => rightPad(runningTimeMessage, 18, " "), [ + runningTimeMessage, + ]); + return ( +
+ {/* This div is probably not strictly needed */} +
+ {text} +
-
-); + ); +}; -const mapStateToProps = (state: AppState): Props => ({ - runningTimeMessage: getRunningTimeMessage(state), -}); - -export default connect(mapStateToProps)(RunningTimeDisplay); +export default RunningTimeDisplay; diff --git a/js/components/PlaylistWindow/TrackCell.tsx b/js/components/PlaylistWindow/TrackCell.tsx index fdf001d6..4ea3fc77 100644 --- a/js/components/PlaylistWindow/TrackCell.tsx +++ b/js/components/PlaylistWindow/TrackCell.tsx @@ -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) => 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) => void; - ctrlClick: (e: React.MouseEvent) => 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) => { 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}
); } -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; diff --git a/js/components/PlaylistWindow/TrackList.tsx b/js/components/PlaylistWindow/TrackList.tsx index 98d8c9f5..d17a930d 100644 --- a/js/components/PlaylistWindow/TrackList.tsx +++ b/js/components/PlaylistWindow/TrackList.tsx @@ -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) => 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(null); const [moving, setMoving] = useState(false); const [mouseStartY, setMouseStartY] = useState(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) => ( {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} >
{_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) => - 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; diff --git a/js/components/PlaylistWindow/TrackTitle.tsx b/js/components/PlaylistWindow/TrackTitle.tsx index ed020c75..2bc69cbc 100644 --- a/js/components/PlaylistWindow/TrackTitle.tsx +++ b/js/components/PlaylistWindow/TrackTitle.tsx @@ -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 ( + + {paddedTrackNumber}. {title} + + ); +}; -const TrackTitle = (props: OwnProps & StateProps) => ( - - {props.paddedTrackNumber}. {props.title} - -); - -const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => ({ - title: getTrackDisplayName(state)(ownProps.id), -}); - -export default connect(mapStateToProps)(TrackTitle); +export default TrackTitle; diff --git a/js/components/PlaylistWindow/index.tsx b/js/components/PlaylistWindow/index.tsx index 459c0871..4eb45459 100644 --- a/js/components/PlaylistWindow/index.tsx +++ b/js/components/PlaylistWindow/index.tsx @@ -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, startIndex: number): void; - scrollVolume(e: React.WheelEvent): 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, 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} >
@@ -124,7 +111,10 @@ function PlaylistWindow({
-
+
close(WINDOWS.PLAYLIST)} + />
@@ -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; diff --git a/js/components/SkinsContextMenu.tsx b/js/components/SkinsContextMenu.tsx index 3ae43a2c..9bd9b5dc 100644 --- a/js/components/SkinsContextMenu.tsx +++ b/js/components/SkinsContextMenu.tsx @@ -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) => ( - - -
- "} /> - {props.availableSkins.map(skin => ( - props.setSkin(skin.url)} - label={skin.name} - /> - ))} -
-); - -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 ( + + +
+ "} /> + {availableSkins.map(skin => ( + setSkin(skin.url)} + label={skin.name} + /> + ))} +
+ ); }; - -export default connect(mapStateToProps, mapDispatchToProps)(SkinContextMenu); +export default SkinContextMenu; diff --git a/js/reducers/milkdrop.ts b/js/reducers/milkdrop.ts index 3363d1b1..ca75186d 100644 --- a/js/reducers/milkdrop.ts +++ b/js/reducers/milkdrop.ts @@ -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 = { diff --git a/js/selectors.ts b/js/selectors.ts index 09c044cb..5f56916a 100644 --- a/js/selectors.ts +++ b/js/selectors.ts @@ -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; +} diff --git a/js/types.ts b/js/types.ts index 88d11591..fbc23d1a 100644 --- a/js/types.ts +++ b/js/types.ts @@ -55,6 +55,11 @@ export type Skin = { name: string; }; +export interface MilkdropMessage { + text: string; + time: number; +} + export type Band = | 60 | 170