Select previous preset

This commit is contained in:
Jordan Eldredge 2019-03-09 15:11:43 -08:00
parent 02aa688ed3
commit 8559dd7257
8 changed files with 115 additions and 194 deletions

View file

@ -99,9 +99,11 @@ export {
requestPresetAtIndex,
selectRandomPreset,
selectNextPreset,
selectPreviousPreset,
togglePresetOverlay,
appendPresetFileList,
handlePresetDrop
handlePresetDrop,
loadPresets
} from "./milkdrop";
import * as Selectors from "../selectors";

View file

@ -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;

View file

@ -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";

View file

@ -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);
});
});

View file

@ -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(

View file

@ -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);
});

View file

@ -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 {

View file

@ -141,8 +141,6 @@ export type StatePreset =
| { type: "RESOLVED"; name: string; preset: Object }
| { type: "UNRESOLVED"; name: string; getPreset: () => Promise<Object> };
export type PresetId = string;
export interface ButterchurnOptions {
getPresets(): Promise<Preset[]>;
importButterchurn(): Promise<any>;
@ -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[];