diff --git a/js/constants.js b/js/constants.ts similarity index 78% rename from js/constants.js rename to js/constants.ts index ff2ffcbb..fa784b72 100644 --- a/js/constants.js +++ b/js/constants.ts @@ -1,5 +1,17 @@ +import { Band, MediaTagRequestStatus } from "./types"; import * as baseSkin from "./baseSkin.json"; -export const BANDS = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]; +export const BANDS: Band[] = [ + 60, + 170, + 310, + 600, + 1000, + 3000, + 6000, + 12000, + 14000, + 16000 +]; export const WINDOWS = { MAIN: "main", @@ -12,7 +24,11 @@ export const LOAD_STYLE = { PLAY: "PLAY" }; -export const MEDIA_TAG_REQUEST_STATUS = { +// TODO: Make this an enum? +export const MEDIA_TAG_REQUEST_STATUS: Record< + MediaTagRequestStatus, + MediaTagRequestStatus +> = { INITIALIZED: "INITIALIZED", FAILED: "FAILED", COMPLETE: "COMPLETE", diff --git a/js/reducers/equalizer.js b/js/reducers/equalizer.ts similarity index 60% rename from js/reducers/equalizer.js rename to js/reducers/equalizer.ts index 9ab95fbb..172a7482 100644 --- a/js/reducers/equalizer.js +++ b/js/reducers/equalizer.ts @@ -1,3 +1,5 @@ +import { EqualizerState, Action } from "./../types"; + import { BANDS } from "../constants"; import { SET_BAND_VALUE, @@ -6,20 +8,28 @@ import { SET_EQ_OFF } from "../actionTypes"; -const equalizer = (state, action) => { - if (!state) { - state = { - on: true, - auto: false, - sliders: { - preamp: 50 - } - }; - BANDS.forEach(band => { - state.sliders[band] = 50; - }); - return state; +const defaultState = { + on: true, + auto: false, + sliders: { + preamp: 50, + 60: 50, + 170: 50, + 310: 50, + 600: 50, + 1000: 50, + 3000: 50, + 6000: 50, + 12000: 50, + 14000: 50, + 16000: 50 } +}; + +const equalizer = ( + state: EqualizerState = defaultState, + action: Action +): EqualizerState => { switch (action.type) { case SET_BAND_VALUE: const newSliders = { ...state.sliders, [action.band]: action.value }; diff --git a/js/reducers/index.js b/js/reducers/index.ts similarity index 81% rename from js/reducers/index.js rename to js/reducers/index.ts index d871053a..28c6c27f 100644 --- a/js/reducers/index.js +++ b/js/reducers/index.ts @@ -1,3 +1,4 @@ +import { AppState, Action } from "../types"; import { combineReducers } from "redux"; import playlist from "./playlist"; @@ -9,7 +10,7 @@ import equalizer from "./equalizer"; import network from "./network"; import settings from "./settings"; -const reducer = combineReducers({ +const reducer = combineReducers({ userInput, windows, display, diff --git a/js/reducers/media.ts b/js/reducers/media.ts index 5a3cd9db..8ff868f3 100644 --- a/js/reducers/media.ts +++ b/js/reducers/media.ts @@ -17,26 +17,28 @@ import { } from "../actionTypes"; import { TIME_MODE, MEDIA_STATUS } from "../constants"; -const media = (state: MediaState, action: Action): MediaState => { - if (!state) { - return { - timeMode: TIME_MODE.ELAPSED, - timeElapsed: 0, - length: null, // Consider renaming to "duration" - kbps: null, - khz: null, - // The winamp ini file declares the default volume as "200". - // The UI seems to show a default volume near 78, which would - // math with the default value being 200 out of 255. - volume: Math.round((200 / 255) * 100), - balance: 0, - channels: null, - shuffle: false, - repeat: false, - // TODO: Enforce possible values - status: MEDIA_STATUS.STOPPED - }; - } +const defaultState = { + timeMode: TIME_MODE.ELAPSED, + timeElapsed: 0, + length: null, // Consider renaming to "duration" + kbps: null, + khz: null, + // The winamp ini file declares the default volume as "200". + // The UI seems to show a default volume near 78, which would + // math with the default value being 200 out of 255. + volume: Math.round((200 / 255) * 100), + balance: 0, + channels: null, + shuffle: false, + repeat: false, + // TODO: Enforce possible values + status: MEDIA_STATUS.STOPPED +}; + +const media = ( + state: MediaState = defaultState, + action: Action +): MediaState => { switch (action.type) { // TODO: Make these constants case PLAY: @@ -53,7 +55,7 @@ const media = (state: MediaState, action: Action): MediaState => { const newMode = state.timeMode === TIME_MODE.REMAINING ? TIME_MODE.ELAPSED - : TIME_MODE.TIME_REMAINING; + : TIME_MODE.REMAINING; return { ...state, timeMode: newMode }; case UPDATE_TIME_ELAPSED: return { ...state, timeElapsed: action.elapsed }; diff --git a/js/reducers/playlist.js b/js/reducers/playlist.ts similarity index 78% rename from js/reducers/playlist.js rename to js/reducers/playlist.ts index c89e0c5e..2e598469 100644 --- a/js/reducers/playlist.js +++ b/js/reducers/playlist.ts @@ -1,3 +1,4 @@ +import { PlaylistState, PlaylistTrack, Action } from "../types"; import { SET_MEDIA, CLICKED_TRACK, @@ -25,6 +26,8 @@ import { MEDIA_TAG_REQUEST_STATUS } from "../constants"; import { filenameFromUrl } from "../fileUtils"; import { shuffle, moveSelected, objectMap, objectFilter } from "../utils"; +const foo = objectMap({ foo: 1 }, String); + const defaultPlaylistState = { trackOrder: [], currentTrack: null, @@ -32,16 +35,22 @@ const defaultPlaylistState = { lastSelectedIndex: null }; -const playlist = (state = defaultPlaylistState, action) => { +const playlist = ( + state: PlaylistState = defaultPlaylistState, + action: Action +): PlaylistState => { switch (action.type) { case CLICKED_TRACK: const clickedId = String(state.trackOrder[action.index]); return { ...state, - tracks: objectMap(state.tracks, (track, id) => ({ - ...track, - selected: id === clickedId - })), + tracks: objectMap( + state.tracks, + (track, id) => ({ + ...track, + selected: id === clickedId + }) + ), lastSelectedIndex: action.index }; case CTRL_CLICKED_TRACK: @@ -68,31 +77,43 @@ const playlist = (state = defaultPlaylistState, action) => { const selected = new Set(state.trackOrder.slice(start, end + 1)); return { ...state, - tracks: objectMap(state.tracks, (track, trackId) => ({ - ...track, - selected: selected.has(Number(trackId)) - })) + tracks: objectMap( + state.tracks, + (track, trackId) => ({ + ...track, + selected: selected.has(Number(trackId)) + }) + ) }; case SELECT_ALL: return { ...state, - tracks: objectMap(state.tracks, track => ({ ...track, selected: true })) + tracks: objectMap( + state.tracks, + track => ({ ...track, selected: true }) + ) }; case SELECT_ZERO: return { ...state, - tracks: objectMap(state.tracks, track => ({ - ...track, - selected: false - })) + tracks: objectMap( + state.tracks, + track => ({ + ...track, + selected: false + }) + ) }; case INVERT_SELECTION: return { ...state, - tracks: objectMap(state.tracks, track => ({ - ...track, - selected: !track.selected - })) + tracks: objectMap( + state.tracks, + track => ({ + ...track, + selected: !track.selected + }) + ) }; case REMOVE_ALL_TRACKS: // TODO: Consider disposing of ObjectUrls @@ -112,7 +133,9 @@ const playlist = (state = defaultPlaylistState, action) => { trackOrder: state.trackOrder.filter( trackId => !actionIds.includes(trackId) ), - currentTrack: actionIds.includes(currentTrack) ? null : currentTrack, + currentTrack: actionIds.includes(Number(currentTrack)) + ? null + : currentTrack, tracks: objectFilter( state.tracks, (track, trackId) => !action.ids.includes(trackId) @@ -159,18 +182,20 @@ const playlist = (state = defaultPlaylistState, action) => { // TODO: This could probably be made to work, but we clear it just to be safe. lastSelectedIndex: null }; - case SET_MEDIA: + case SET_MEDIA: { + const newTrack = { + ...state.tracks[action.id], + duration: action.length + }; return { ...state, tracks: { ...state.tracks, - [action.id]: { - ...state.tracks[action.id], - duration: action.length - } + [action.id]: newTrack } }; - case SET_MEDIA_TAGS: + } + case SET_MEDIA_TAGS: { return { ...state, tracks: { @@ -184,6 +209,7 @@ const playlist = (state = defaultPlaylistState, action) => { } } }; + } case MEDIA_TAG_REQUEST_INITIALIZED: return { ...state, @@ -206,17 +232,19 @@ const playlist = (state = defaultPlaylistState, action) => { } } }; - case SET_MEDIA_DURATION: + case SET_MEDIA_DURATION: { + const newTrack = { + ...state.tracks[action.id], + duration: action.duration + }; return { ...state, tracks: { ...state.tracks, - [action.id]: { - ...state.tracks[action.id], - duration: action.duration - } + [action.id]: newTrack } }; + } case PLAY_TRACK: case BUFFER_TRACK: return { @@ -241,7 +269,10 @@ const playlist = (state = defaultPlaylistState, action) => { export default playlist; -export const getTrackDisplayName = (state, id) => { +export const getTrackDisplayName = ( + state: PlaylistState, + id: number +): string | null => { const track = state.tracks[id]; if (track == null) { return null; diff --git a/js/reducers/windows.js b/js/reducers/windows.ts similarity index 90% rename from js/reducers/windows.js rename to js/reducers/windows.ts index 6e4cf34d..1cdcf9e6 100644 --- a/js/reducers/windows.js +++ b/js/reducers/windows.ts @@ -1,3 +1,4 @@ +import { WindowState, Action } from "../types"; import { WINDOWS } from "../constants"; import { SET_FOCUSED_WINDOW, @@ -10,7 +11,7 @@ import { TOGGLE_WINDOW_SHADE_MODE } from "../actionTypes"; -const defaultWindowsState = { +const defaultWindowsState: WindowState = { focused: WINDOWS.MAIN, genWindows: { // TODO: Remove static capabilites and derive them from ids/generic @@ -54,7 +55,10 @@ const defaultWindowsState = { positions: {} }; -const windows = (state = defaultWindowsState, action) => { +const windows = ( + state: WindowState = defaultWindowsState, + action: Action +): WindowState => { switch (action.type) { case SET_FOCUSED_WINDOW: return { ...state, focused: action.window }; @@ -62,8 +66,9 @@ const windows = (state = defaultWindowsState, action) => { const { canShade } = state.genWindows[action.windowId]; if (!canShade) { throw new Error( - "Tried to shade/unshade a window that cannot be shaded:", - action.windowId + `Tried to shade/unshade a window that cannot be shaded: ${ + action.windowId + }` ); } return { @@ -133,8 +138,7 @@ const windows = (state = defaultWindowsState, action) => { const { canResize } = state.genWindows[action.windowId]; if (!canResize) { throw new Error( - "Tried to resize a window that cannot be resized:", - action.windowId + `Tried to resize a window that cannot be resized: ${action.windowId}` ); } return { diff --git a/js/types.ts b/js/types.ts index 136b7246..95e27aad 100644 --- a/js/types.ts +++ b/js/types.ts @@ -3,7 +3,18 @@ type Skin = { name: string; }; -type Band = null; // TODO: Use a real type here. +export type Band = + | 60 + | 170 + | 310 + | 600 + | 1000 + | 3000 + | 6000 + | 12000 + | 14000 + | 16000; +type Slider = Band | "preamp"; // TODO: Use a type to ensure these keys mirror the CURSORS constant in // skinParser.js @@ -20,6 +31,8 @@ type SkinImages = { [sprite: string]: string }; // TODO: type these keys type SkinRegion = { [windowName: string]: string[] }; +type WindowId = string; + // TODO: Fill these out once we actually use them. type SkinData = { skinImages: SkinImages; @@ -69,9 +82,15 @@ export type Action = } | { type: "ADD_TRACK_FROM_URL"; + atIndex: number | null; + id: string; + defaultName: string; + duration: number | null; + url: string; } | { type: "SET_MEDIA"; + id: number; length: number; kbps: number; khz: number; @@ -163,6 +182,127 @@ export type Action = | { type: "SET_DUMMY_VIZ_DATA"; data: null; + } + | { + type: "SET_BAND_VALUE"; + band: Slider; + value: number; + } + | { + type: "SET_EQ_ON"; + } + | { + type: "SET_EQ_OFF"; + } + | { + type: "SET_EQ_AUTO"; + value: boolean; + } + | { + type: "SET_FOCUSED_WINDOW"; + window: WindowId; + } + | { + type: "TOGGLE_WINDOW_SHADE_MODE"; + windowId: WindowId; + } + | { + type: "TOGGLE_WINDOW"; + windowId: WindowId; + } + | { + type: "CLOSE_WINDOW"; + windowId: WindowId; + } + | { + type: "SET_WINDOW_VISIBILITY"; + windowId: WindowId; + hidden: boolean; + } + | { + type: "ADD_GEN_WINDOW"; + windowId: WindowId; + title: string; + open: boolean; + } + | { + type: "WINDOW_SIZE_CHANGED"; + windowId: WindowId; + size: [number, number]; + } + | { + type: "UPDATE_WINDOW_POSITIONS"; + positions: {}; + } + | { + type: "CLICKED_TRACK"; + index: number; + } + | { + type: "CTRL_CLICKED_TRACK"; + index: number; + } + | { + type: "SHIFT_CLICKED_TRACK"; + index: number; + } + | { + type: "SELECT_ALL"; + } + | { + type: "SELECT_ZERO"; + } + | { + type: "INVERT_SELECTION"; + } + | { + type: "REMOVE_ALL_TRACKS"; + } + | { + type: "REMOVE_TRACKS"; + ids: string[]; + } + | { + type: "REVERSE_LIST"; + } + | { + type: "RANDOMIZE_LIST"; + } + | { + type: "SET_TRACK_ORDER"; + trackOrder: number[]; + } + | { + type: "SET_MEDIA_TAGS"; + id: number; + title: string; + artist: string; + albumArtUrl: string; + } + | { + type: "MEDIA_TAG_REQUEST_INITIALIZED"; + id: number; + } + | { + type: "MEDIA_TAG_REQUEST_FAILED"; + id: number; + } + | { + type: "SET_MEDIA_DURATION"; + id: number; + duration: number; + } + | { + type: "PLAY_TRACK"; + id: number; + } + | { + type: "BUFFER_TRACK"; + id: number; + } + | { + type: "DRAG_SELECTED"; + offset: number; }; export interface SettingsState { @@ -189,7 +329,7 @@ export interface MediaState { export interface UserInputState { focus: string | null; // TODO: Convert this to an enum? - bandFocused: Band; + bandFocused: Band | null; scrubPosition: number; userMessage: string | null; } @@ -214,3 +354,62 @@ export interface DisplayState { zIndex: number; dummyVizData: null; // TODO: Figure out what kind of data this actually is. } + +export interface EqualizerState { + on: boolean; + auto: boolean; + sliders: Record; +} + +export interface Window { + title: string; + size: [number, number]; + open: boolean; + hidden: boolean; + shade?: boolean; + canResize: boolean; + canShade: boolean; + canDouble: boolean; + generic: boolean; + hotkey?: string; +} + +export interface WindowState { + focused: string; + genWindows: { [name: string]: Window }; + positions: {}; // TODO: Make this more strict +} + +export type MediaTagRequestStatus = + | "INITIALIZED" + | "FAILED" + | "COMPLETE" + | "NOT_REQUESTED"; + +export interface PlaylistTrack { + artist: string; + title: string; + url: string; + defaultName: string; + albumArtUrl: string | null; + selected: boolean; + mediaTagsRequestStatus: MediaTagRequestStatus; +} + +export interface PlaylistState { + trackOrder: number[]; + tracks: { [id: number]: PlaylistTrack }; + lastSelectedIndex: number | null; + currentTrack: number | null; +} + +export interface AppState { + userInput: UserInputState; + windows: WindowState; + display: DisplayState; + settings: SettingsState; + equalizer: EqualizerState; + playlist: PlaylistState; + media: MediaState; + network: NetworkState; +}