diff --git a/js/actionCreators/index.ts b/js/actionCreators/index.ts index c429f905..545d94a0 100644 --- a/js/actionCreators/index.ts +++ b/js/actionCreators/index.ts @@ -99,9 +99,11 @@ export { requestPresetAtIndex, selectRandomPreset, selectNextPreset, + selectPreviousPreset, togglePresetOverlay, appendPresetFileList, - handlePresetDrop + handlePresetDrop, + loadPresets } from "./milkdrop"; import * as Selectors from "../selectors"; diff --git a/js/actionCreators/milkdrop.ts b/js/actionCreators/milkdrop.ts index 85a11e70..0a89b4b4 100644 --- a/js/actionCreators/milkdrop.ts +++ b/js/actionCreators/milkdrop.ts @@ -3,7 +3,8 @@ import { GOT_BUTTERCHURN, SELECT_PRESET_AT_INDEX, RESOLVE_PRESET_AT_INDEX, - TOGGLE_PRESET_OVERLAY + TOGGLE_PRESET_OVERLAY, + PRESET_REQUESTED } from "../actionTypes"; import * as Selectors from "../selectors"; import { @@ -61,7 +62,9 @@ export function loadPresets(presets: StatePreset[]): Dispatchable { return (dispatch, getState) => { const presetLength = getState().milkdrop.presets.length; dispatch({ type: GOT_BUTTERCHURN_PRESETS, presets }); - dispatch(requestPresetAtIndex(presetLength, TransitionType.IMMEDIATE)); + dispatch( + requestPresetAtIndex(presetLength, TransitionType.IMMEDIATE, true) + ); }; } @@ -79,7 +82,7 @@ export function appendPresetFileList(fileList: FileList): Dispatchable { return { type: "UNRESOLVED", name: file.name.slice(0, file.name.length - MILK_EXT.length), - getPreset: async () => convertPreset(file) + getPreset: () => convertPreset(file) } as StatePreset; } else if (filename.endsWith(JSON_EXT)) { return { @@ -106,29 +109,50 @@ export function selectNextPreset( transitionType: TransitionType = TransitionType.DEFAULT ): Dispatchable { return (dispatch, getState) => { - const state = getState(); - const currentPresetIndex = Selectors.getCurrentPresetIndex(state); + const currentPresetIndex = Selectors.getCurrentPresetIndex(getState()); if (currentPresetIndex == null) { return; } const nextPresetIndex = currentPresetIndex + 1; - dispatch(requestPresetAtIndex(nextPresetIndex, transitionType)); + dispatch(requestPresetAtIndex(nextPresetIndex, transitionType, true)); }; } -export function selectRandomPreset(): Dispatchable { +export function selectPreviousPreset( + transitionType: TransitionType = TransitionType.DEFAULT +): Dispatchable { return (dispatch, getState) => { const state = getState(); + const { presetHistory } = state.milkdrop; + if (presetHistory.length < 1) { + return; + } + // Awkward. We do -2 becuase the the last track is the current track. + const lastPresetIndex = presetHistory[presetHistory.length - 2]; + + dispatch(requestPresetAtIndex(lastPresetIndex, transitionType, false)); + }; +} + +export function selectRandomPreset( + transitionType: TransitionType = TransitionType.DEFAULT +): Dispatchable { + return (dispatch, getState) => { + const state = getState(); + // TODO: Make this a selector. const randomIndex = Math.floor( Math.random() * state.milkdrop.presets.length ); - dispatch(requestPresetAtIndex(randomIndex, TransitionType.DEFAULT)); + dispatch(requestPresetAtIndex(randomIndex, transitionType, true)); }; } +// TODO: Technically there's a race here. If you request two presets in a row, the +// first one may resolve before the second. export function requestPresetAtIndex( index: number, - transitionType: TransitionType + transitionType: TransitionType, + addToHistory: boolean ): Dispatchable { return async (dispatch, getState) => { const state = getState(); @@ -137,15 +161,14 @@ export function requestPresetAtIndex( // Index might be out of range. return; } + dispatch({ type: PRESET_REQUESTED, index, addToHistory }); switch (preset.type) { case "RESOLVED": dispatch({ type: SELECT_PRESET_AT_INDEX, index, transitionType }); return; case "UNRESOLVED": const json = await preset.getPreset(); - // What if the index has changed? - // Perhaps we could hold a reference to the preset at the index before - // we await and confirm that it hasn't changed after the await? + // TODO: Ensure that this works correctly even if requests resolve out of order dispatch({ type: RESOLVE_PRESET_AT_INDEX, index, json }); dispatch({ type: SELECT_PRESET_AT_INDEX, index, transitionType }); return; diff --git a/js/actionTypes.ts b/js/actionTypes.ts index 47111ab2..8053d7ae 100644 --- a/js/actionTypes.ts +++ b/js/actionTypes.ts @@ -82,3 +82,4 @@ export const GOT_BUTTERCHURN = "GOT_BUTTERCHURN"; export const RESOLVE_PRESET_AT_INDEX = "RESOLVE_PRESET_AT_INDEX"; export const SELECT_PRESET_AT_INDEX = "SELECT_PRESET_AT_INDEX"; export const TOGGLE_PRESET_OVERLAY = "TOGGLE_PRESET_OVERLAY"; +export const PRESET_REQUESTED = "PRESET_REQUESTED"; diff --git a/js/components/MilkdropWindow/__tests__/Presets.test.js b/js/components/MilkdropWindow/__tests__/Presets.test.js deleted file mode 100644 index b3080e23..00000000 --- a/js/components/MilkdropWindow/__tests__/Presets.test.js +++ /dev/null @@ -1,173 +0,0 @@ -import { mockRandom } from "jest-mock-random"; -import Presets from "../Presets"; - -let presets; -beforeEach(() => { - mockRandom([0.0]); - presets = new Presets({ - keys: ["a", "b"], - initialPresets: { a: "Preset A", b: "Preset B" }, - randomize: true - }); -}); - -describe("initialize", () => { - beforeEach(() => {}); - test("picks a random value", () => { - expect(presets.getCurrent()).toBe("Preset A"); - }); - - test("picks another random value", () => { - mockRandom([0.9]); - presets = new Presets({ - keys: ["a", "b"], - initialPresets: { a: "Preset A", b: "Preset B" }, - randomize: true - }); - expect(presets.getCurrent()).toBe("Preset B"); - }); - - test("picks its random value from the initial presets", () => { - presets = new Presets({ - keys: ["a", "b", "c", "d", "e", "f", "g", "h"], - initialPresets: { a: "Preset A" }, - randomize: true - }); - expect(presets.getCurrent()).toBe("Preset A"); - }); -}); - -describe("next", () => { - test("picks a 'random' preset", async () => { - mockRandom([0.9]); - presets.next(); - expect(presets.getCurrent()).toBe("Preset B"); - - mockRandom([0.0]); - presets.next(); - expect(presets.getCurrent()).toBe("Preset A"); - }); - - test("picks the next key", async () => { - presets.setRandomize(false); - presets.next(); - expect(presets.getCurrent()).toBe("Preset B"); - }); - - test("wraps around", async () => { - presets.setRandomize(false); - presets.next(); - presets.next(); - expect(presets.getCurrent()).toBe("Preset A"); - }); -}); - -describe("previous", () => { - test("picks the previous key", async () => { - presets.setRandomize(false); - presets.next(); - presets.previous(); - expect(presets.getCurrent()).toBe("Preset A"); - }); - - test("does nothing when you are on the first item", async () => { - presets.previous(); - expect(presets.getCurrent()).toBe("Preset A"); - }); - - test("can do sequential previouses", async () => { - mockRandom([0.0]); - presets = new Presets({ - keys: ["a", "b", "c", "d"], - initialPresets: { - a: "Preset A", - b: "Preset B", - c: "Preset C", - d: "Preset D" - }, - randomize: false - }); - presets.next(); // b - presets.next(); // c - presets.next(); // d - presets.previous(); // c - presets.previous(); // b - presets.previous(); // a - expect(presets.getCurrent()).toBe("Preset A"); - }); - - test("will successfully resolve an unloaded preset", async () => { - mockRandom([0.0]); - presets = new Presets({ - keys: ["a", "b", "c", "d"], - initialPresets: { - a: "Preset A" - }, - randomize: false, - getRest: () => - Promise.resolve({ - b: "Preset B", - c: "Preset C" - }) - }); - presets.next(); // b - presets.next(); // c - const final = await presets.previous(); // b - expect(final).toBe("Preset B"); - }); -}); - -describe("getRest", () => { - beforeEach(() => { - mockRandom([0.0]); - presets = new Presets({ - keys: ["a", "b"], - initialPresets: { a: "Preset A" }, - getRest: () => - Promise.resolve({ - b: "Preset B" - }) - }); - }); - test("will get the rest of the presets if needed", async () => { - mockRandom([0.9]); - const resolved = presets.next(); - expect(presets.getCurrent()).toBe("Preset A"); - await resolved; - expect(presets.getCurrent()).toBe("Preset B"); - }); - - test("next (loading), previous brings us back to where we started", async () => { - presets.setRandomize(false); - presets.next(); - expect(presets.getCurrent()).toBe("Preset A"); - await presets.previous(); - expect(presets.getCurrent()).toBe("Preset A"); - }); -}); - -describe("selectIndex", () => { - test("adds an entry to the history", async () => { - presets.selectIndex(1); - presets.previous(); - expect(presets.getCurrent()).toBe("Preset A"); - }); -}); - -describe("getCurrentIndex", () => { - test("gets the active index while loading", async () => { - presets = new Presets({ - keys: ["a", "b"], - initialPresets: { a: "Preset A" }, - randomize: false, - getRest: () => - Promise.resolve({ - b: "Preset B" - }) - }); - const resolved = presets.next(); - expect(presets.getCurrentIndex()).toBe(0); - await resolved; - expect(presets.getCurrentIndex()).toBe(1); - }); -}); diff --git a/js/components/MilkdropWindow/index.tsx b/js/components/MilkdropWindow/index.tsx index 11ba4039..d7029ebf 100644 --- a/js/components/MilkdropWindow/index.tsx +++ b/js/components/MilkdropWindow/index.tsx @@ -30,6 +30,7 @@ interface DispatchProps { selectRandomPreset(): void; handlePresetDrop(e: React.DragEvent): void; selectNextPreset(transitionType?: TransitionType): void; + selectPreviousPreset(transitionType?: TransitionType): void; } interface OwnProps { @@ -46,12 +47,13 @@ function Milkdrop(props: Props) { // Handle keyboard events useEffect(() => { return props.onFocusedKeyDown(e => { + console.log(e.keyCode); switch (e.keyCode) { case 32: // spacebar props.selectNextPreset(); break; case 8: // backspace - // this._prevPreset(0); + props.selectPreviousPreset(TransitionType.IMMEDIATE); break; case 72: // H props.selectNextPreset(TransitionType.IMMEDIATE); @@ -141,7 +143,9 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ selectRandomPreset: () => dispatch(Actions.selectRandomPreset()), handlePresetDrop: e => dispatch(Actions.handlePresetDrop(e)), selectNextPreset: (transitionType?: TransitionType) => - dispatch(Actions.selectNextPreset(transitionType)) + dispatch(Actions.selectNextPreset(transitionType)), + selectPreviousPreset: (transitionType?: TransitionType) => + dispatch(Actions.selectPreviousPreset(transitionType)) }); export default connect( diff --git a/js/reducers/milkdrop.test.js b/js/reducers/milkdrop.test.js new file mode 100644 index 00000000..f84b3b26 --- /dev/null +++ b/js/reducers/milkdrop.test.js @@ -0,0 +1,52 @@ +import Emitter from "../emitter"; +import createStore from "../store"; +import * as Actions from "../actionCreators"; +import * as Selectors from "../selectors"; + +// Actually tests action creators too +const media = { + addEventListener: jest.fn(), + setVolume: jest.fn(), + setBalance: jest.fn(), + setPreamp: jest.fn(), + getAnalyser: () => null, + on: jest.fn() +}; + +const store = createStore(media, new Emitter()); + +test("handles history correctly", () => { + const mockPreset = [ + { type: "RESOLVED", name: "First", preset: {} }, + { type: "RESOLVED", name: "Second", preset: {} } + ]; + // Check initial state + expect(store.getState().milkdrop.presetHistory).toEqual([]); + expect(Selectors.getCurrentPreset(store.getState())).toBe(null); + expect(Selectors.getCurrentPresetIndex(store.getState())).toBe(null); + + // Load some presets + store.dispatch(Actions.loadPresets(mockPreset)); + expect(store.getState().milkdrop.presetHistory).toEqual([0]); + expect(Selectors.getCurrentPresetIndex(store.getState())).toBe(0); + + // Select next + store.dispatch(Actions.selectNextPreset()); + expect(store.getState().milkdrop.presetHistory).toEqual([0, 1]); + expect(Selectors.getCurrentPresetIndex(store.getState())).toBe(1); + + // Select previous + store.dispatch(Actions.selectPreviousPreset()); + expect(store.getState().milkdrop.presetHistory).toEqual([0]); + expect(Selectors.getCurrentPresetIndex(store.getState())).toBe(0); + + // Next again + store.dispatch(Actions.selectNextPreset()); + expect(store.getState().milkdrop.presetHistory).toEqual([0, 1]); + expect(Selectors.getCurrentPresetIndex(store.getState())).toBe(1); + + // Previous again + store.dispatch(Actions.selectPreviousPreset()); + expect(store.getState().milkdrop.presetHistory).toEqual([0]); + expect(Selectors.getCurrentPresetIndex(store.getState())).toBe(0); +}); diff --git a/js/reducers/milkdrop.ts b/js/reducers/milkdrop.ts index fbef88c2..0a809274 100644 --- a/js/reducers/milkdrop.ts +++ b/js/reducers/milkdrop.ts @@ -1,11 +1,12 @@ -import { Action, PresetId, StatePreset } from "../types"; +import { Action, StatePreset } from "../types"; import { SET_MILKDROP_DESKTOP, GOT_BUTTERCHURN_PRESETS, GOT_BUTTERCHURN, RESOLVE_PRESET_AT_INDEX, SELECT_PRESET_AT_INDEX, - TOGGLE_PRESET_OVERLAY + TOGGLE_PRESET_OVERLAY, + PRESET_REQUESTED } from "../actionTypes"; import * as Utils from "../utils"; import { TransitionType } from "../types"; @@ -13,7 +14,7 @@ import { TransitionType } from "../types"; export interface MilkdropState { desktop: boolean; overlay: boolean; - presetOrder: PresetId[]; + presetHistory: number[]; presets: StatePreset[]; currentPresetIndex: number | null; butterchurn: any; @@ -23,7 +24,7 @@ export interface MilkdropState { const defaultMilkdropState = { desktop: false, overlay: false, - presetOrder: [], + presetHistory: [], presets: [], currentPresetIndex: null, butterchurn: null, @@ -44,6 +45,18 @@ export const milkdrop = ( ...state, presets: state.presets.concat(action.presets) }; + case PRESET_REQUESTED: + if (action.addToHistory) { + return { + ...state, + presetHistory: [...state.presetHistory, action.index] + }; + } + console.log(state.presetHistory); + return { + ...state, + presetHistory: state.presetHistory.slice(0, -1) + }; case RESOLVE_PRESET_AT_INDEX: const preset = state.presets[action.index]; return { diff --git a/js/types.ts b/js/types.ts index 348c35cd..a6719ed0 100644 --- a/js/types.ts +++ b/js/types.ts @@ -141,8 +141,6 @@ export type StatePreset = | { type: "RESOLVED"; name: string; preset: Object } | { type: "UNRESOLVED"; name: string; getPreset: () => Promise }; -export type PresetId = string; - export interface ButterchurnOptions { getPresets(): Promise; importButterchurn(): Promise; @@ -468,6 +466,7 @@ export type Action = type: "SET_MILKDROP_DESKTOP"; enabled: boolean; } + | { type: "PRESET_REQUESTED"; index: number; addToHistory: boolean } | { type: "GOT_BUTTERCHURN_PRESETS"; presets: StatePreset[];