diff --git a/js/actionCreators/files.ts b/js/actionCreators/files.ts
index 4980a083..98e632d6 100644
--- a/js/actionCreators/files.ts
+++ b/js/actionCreators/files.ts
@@ -267,7 +267,19 @@ export function loadMediaFile(
if (metaData != null) {
const { artist, title, album } = metaData;
- dispatch({ type: SET_MEDIA_TAGS, artist, title, album, id });
+ dispatch({
+ type: SET_MEDIA_TAGS,
+ artist,
+ title,
+ album,
+ // For now, we lie about these next three things.
+ // TODO: Ideally we would leave these as null and force a media data
+ // fetch when the user starts playing.
+ sampleRate: 44000,
+ bitrate: 192000,
+ numberOfChannels: 2,
+ id
+ });
} else if ("blob" in track) {
// Blobs can be loaded quickly
dispatch(fetchMediaTags(track.blob, id));
diff --git a/js/actionCreators/media.ts b/js/actionCreators/media.ts
index 46b67efd..4a8c1b80 100644
--- a/js/actionCreators/media.ts
+++ b/js/actionCreators/media.ts
@@ -17,6 +17,7 @@ import {
import { MEDIA_STATUS } from "../constants";
import { openMediaFileDialog } from "./";
import { GetState, Dispatch, Dispatchable } from "../types";
+import * as Selectors from "../selectors";
function playRandomTrack(): Dispatchable {
return (dispatch: Dispatch, getState: GetState) => {
@@ -91,14 +92,16 @@ export function previous(): Dispatchable {
export function seekForward(seconds: number): Dispatchable {
return function(dispatch, getState) {
- const { timeElapsed, length } = getState().media;
- if (length == null) {
+ const state = getState();
+ const timeElapsed = Selectors.getTimeElapsed(state);
+ const duration = Selectors.getDuration(state);
+ if (duration == null) {
return;
}
const newTimeElapsed = timeElapsed + seconds;
dispatch({
type: SEEK_TO_PERCENT_COMPLETE,
- percent: (newTimeElapsed / length) * 100
+ percent: (newTimeElapsed / duration) * 100
});
};
}
diff --git a/js/actionTypes.ts b/js/actionTypes.ts
index 891a82f7..9783752f 100644
--- a/js/actionTypes.ts
+++ b/js/actionTypes.ts
@@ -60,7 +60,6 @@ export const MEDIA_TAG_REQUEST_FAILED = "MEDIA_TAG_REQUEST_FAILED";
export const NETWORK_CONNECTED = "NETWORK_CONNECTED";
export const NETWORK_DISCONNECTED = "NETWORK_DISCONNECTED";
export const UPDATE_WINDOW_POSITIONS = "UPDATE_WINDOW_POSITIONS";
-export const CHANNEL_COUNT_CHANGED = "CHANNEL_COUNT_CHANGED";
export const WINDOW_SIZE_CHANGED = "WINDOW_SIZE_CHANGED";
export const TOGGLE_WINDOW_SHADE_MODE = "TOGGLE_WINDOW_SHADE_MODE";
export const LOADED = "LOADED";
diff --git a/js/components/MainWindow/Kbps.js b/js/components/MainWindow/Kbps.js
deleted file mode 100644
index 3d0688a7..00000000
--- a/js/components/MainWindow/Kbps.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from "react";
-import { connect } from "react-redux";
-
-import CharacterString from "../CharacterString";
-
-const Kbps = props => (
-
- {props.kbps}
-
-);
-
-export default connect(state => ({ kbps: state.media.kbps }))(Kbps);
diff --git a/js/components/MainWindow/Kbps.tsx b/js/components/MainWindow/Kbps.tsx
new file mode 100644
index 00000000..d7b3d87f
--- /dev/null
+++ b/js/components/MainWindow/Kbps.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import { connect } from "react-redux";
+
+import CharacterString from "../CharacterString";
+import { AppState } from "../../types";
+import * as Selectors from "../../selectors";
+
+interface StateProps {
+ kbps: string | null;
+}
+
+const Kbps = (props: StateProps) => (
+
+ {props.kbps || ""}
+
+);
+
+function mapStateToProps(state: AppState): StateProps {
+ return { kbps: Selectors.getKbps(state) };
+}
+
+export default connect(mapStateToProps)(Kbps);
diff --git a/js/components/MainWindow/Khz.js b/js/components/MainWindow/Khz.js
deleted file mode 100644
index 7d689b4f..00000000
--- a/js/components/MainWindow/Khz.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from "react";
-import { connect } from "react-redux";
-
-import CharacterString from "../CharacterString";
-
-const Khz = props => (
-
- {props.khz}
-
-);
-
-export default connect(state => state.media)(Khz);
diff --git a/js/components/MainWindow/Khz.tsx b/js/components/MainWindow/Khz.tsx
new file mode 100644
index 00000000..8ddd9ab2
--- /dev/null
+++ b/js/components/MainWindow/Khz.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import { connect } from "react-redux";
+
+import CharacterString from "../CharacterString";
+import { AppState } from "../../types";
+import * as Selectors from "../../selectors";
+
+interface StateProps {
+ khz: string | null;
+}
+
+const Khz = (props: StateProps) => (
+
+ {props.khz || ""}
+
+);
+
+function mapStateToProps(state: AppState): StateProps {
+ return { khz: Selectors.getKhz(state) };
+}
+
+export default connect(mapStateToProps)(Khz);
diff --git a/js/components/MainWindow/Position.js b/js/components/MainWindow/Position.tsx
similarity index 53%
rename from js/components/MainWindow/Position.js
rename to js/components/MainWindow/Position.tsx
index 6daa353c..21434f45 100644
--- a/js/components/MainWindow/Position.js
+++ b/js/components/MainWindow/Position.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { connect } from "react-redux";
+import { AppState, Dispatch } from "../../types";
import {
SEEK_TO_PERCENT_COMPLETE,
@@ -7,13 +8,26 @@ import {
UNSET_FOCUS,
SET_SCRUB_POSITION
} from "../../actionTypes";
+import * as Selectors from "../../selectors";
+
+interface StateProps {
+ displayedPosition: number;
+ position: number;
+}
+
+interface DispatchProps {
+ seekToPercentComplete(e: React.MouseEvent): void;
+ setPosition(e: React.MouseEvent): void;
+}
+
+type Props = StateProps & DispatchProps;
const Position = ({
position,
seekToPercentComplete,
displayedPosition,
setPosition
-}) => {
+}: Props) => {
// In shade mode, the position slider shows up differently depending on if
// it's near the start, middle or end of its progress
let className = "";
@@ -43,16 +57,15 @@ const Position = ({
);
};
-const mapStateToProps = ({ media, userInput }) => {
- let position;
- if (media.length) {
- position = (Math.floor(media.timeElapsed) / media.length) * 100;
- } else {
- position = 0;
- }
+const mapStateToProps = (state: AppState): StateProps => {
+ const duration = Selectors.getDuration(state);
+ const timeElapsed = Selectors.getTimeElapsed(state);
+ const userInputFocus = Selectors.getUserInputFocus(state);
+ const scrubPosition = Selectors.getUserInputScrubPosition(state);
+ const position = duration ? (Math.floor(timeElapsed) / duration) * 100 : 0;
const displayedPosition =
- userInput.focus === "position" ? userInput.scrubPosition : position;
+ userInputFocus === "position" ? scrubPosition : position;
return {
displayedPosition,
@@ -60,14 +73,20 @@ const mapStateToProps = ({ media, userInput }) => {
};
};
-const mapDispatchToProps = dispatch => ({
+const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
seekToPercentComplete: e => {
- dispatch({ type: SEEK_TO_PERCENT_COMPLETE, percent: e.target.value });
+ dispatch({
+ type: SEEK_TO_PERCENT_COMPLETE,
+ percent: Number((e.target as HTMLInputElement).value)
+ });
dispatch({ type: UNSET_FOCUS });
},
setPosition: e => {
dispatch({ type: SET_FOCUS, input: "position" });
- dispatch({ type: SET_SCRUB_POSITION, position: e.target.value });
+ dispatch({
+ type: SET_SCRUB_POSITION,
+ position: Number((e.target as HTMLInputElement).value)
+ });
}
});
diff --git a/js/components/MainWindow/Time.tsx b/js/components/MainWindow/Time.tsx
index 96daca1e..51b315c5 100644
--- a/js/components/MainWindow/Time.tsx
+++ b/js/components/MainWindow/Time.tsx
@@ -4,11 +4,12 @@ import { TimeMode, AppState, Dispatch } from "../../types";
import { getTimeObj } from "../../utils";
import * as Actions from "../../actionCreators";
+import * as Selectors from "../../selectors";
import { TIME_MODE } from "../../constants";
interface StateProps {
timeElapsed: number;
- length: number;
+ duration: number;
timeMode: TimeMode;
}
@@ -18,12 +19,12 @@ interface DispatchProps {
const Time = ({
timeElapsed,
- length,
+ duration,
timeMode,
toggleTimeMode
}: StateProps & DispatchProps) => {
const seconds =
- timeMode === TIME_MODE.ELAPSED ? timeElapsed : length - timeElapsed;
+ timeMode === TIME_MODE.ELAPSED ? timeElapsed : duration - timeElapsed;
const timeObj = getTimeObj(seconds);
return (
@@ -50,8 +51,10 @@ const Time = ({
};
const mapStateToProps = (state: AppState): StateProps => {
- const { timeElapsed, length, timeMode } = state.media;
- return { timeElapsed, length: length || 0, timeMode };
+ const timeElapsed = Selectors.getTimeElapsed(state);
+ const duration = Selectors.getDuration(state);
+ const { timeMode } = state.media;
+ return { timeElapsed, duration: duration || 0, timeMode };
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
toggleTimeMode: () => dispatch(Actions.toggleTimeMode())
diff --git a/js/components/MiniTime.tsx b/js/components/MiniTime.tsx
index 9c746b16..5a9f6aee 100644
--- a/js/components/MiniTime.tsx
+++ b/js/components/MiniTime.tsx
@@ -1,4 +1,4 @@
-import { AppState, Action } from "../types";
+import { AppState, Action, Dispatchable, Dispatch } from "../types";
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
@@ -6,6 +6,7 @@ import { getTimeObj } from "../utils";
import { TOGGLE_TIME_MODE } from "../actionTypes";
import { TIME_MODE, MEDIA_STATUS } from "../constants";
import Character from "./Character";
+import * as Selectors from "../selectors";
import "../../css/mini-time.css";
@@ -26,15 +27,20 @@ const Background = () => (
);
-type StateProps = {
+interface StateProps {
status: string | null;
timeMode: string;
timeElapsed: number;
length: number | null;
- toggle: () => void;
-};
+}
-const MiniTime = (props: StateProps) => {
+interface DispatchProps {
+ toggle: () => void;
+}
+
+type Props = StateProps & DispatchProps;
+
+const MiniTime = (props: Props) => {
let seconds = null;
// TODO: Clean this up: If stopped, just render the background, rather than
// rendering spaces twice.
@@ -66,14 +72,14 @@ const MiniTime = (props: StateProps) => {
);
};
-const mapStateToProps = (state: AppState) => ({
+const mapStateToProps = (state: AppState): StateProps => ({
status: state.media.status,
timeMode: state.media.timeMode,
- timeElapsed: state.media.timeElapsed,
- length: state.media.length
+ timeElapsed: Selectors.getTimeElapsed(state),
+ length: Selectors.getDuration(state)
});
-const mapDispatchToProps = (dispatch: (action: Action) => void) => ({
+const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
// TODO: move to actionCreators
toggle: () => {
dispatch({ type: TOGGLE_TIME_MODE });
diff --git a/js/media/elementSource.ts b/js/media/elementSource.ts
index a88b4c07..b48bd88e 100644
--- a/js/media/elementSource.ts
+++ b/js/media/elementSource.ts
@@ -147,16 +147,6 @@ export default class ElementSource {
return this._audio.currentTime;
}
- getNumberOfChannels() {
- return this._source.channelCount;
- }
-
- getSampleRate() {
- // This is a lie. This is the sample rate of the context, not the
- // underlying source media.
- return this._context.sampleRate;
- }
-
_setStatus(status: MediaStatus) {
this._status = status;
this._emitter.trigger("statusChange");
diff --git a/js/media/index.ts b/js/media/index.ts
index 9d3858f3..4c4f5680 100644
--- a/js/media/index.ts
+++ b/js/media/index.ts
@@ -17,7 +17,6 @@ function createStereoPanner(context: AudioContext): StereoPannerNode {
export default class Media {
_emitter: Emitter;
_context: AudioContext;
- _channels: number | null;
_balance: number;
_staticSource: AnalyserNode;
_preamp: GainNode;
@@ -52,8 +51,6 @@ export default class Media {
document.body.addEventListener("click", resume, false);
document.body.addEventListener("keydown", resume, false);
}
- // We don't currently know how many channels
- this._channels = null;
this._balance = 0;
// The _source node has to be recreated each time it's stopped or
@@ -152,11 +149,6 @@ export default class Media {
this._gainNode.connect(this._context.destination);
}
- _setChannels(num: number | null) {
- this._channels = num;
- this._emitter.trigger("channelupdate");
- }
-
getAnalyser() {
return this._analyser;
}
@@ -178,14 +170,6 @@ export default class Media {
return (this.timeElapsed() / this.duration()) * 100;
}
- channels() {
- return this._channels == null ? 2 : this._channels;
- }
-
- sampleRate() {
- return this._source.getSampleRate();
- }
-
/* Actions */
async play() {
await this._source.play();
@@ -248,7 +232,6 @@ export default class Media {
async loadFromUrl(url: string, autoPlay: boolean) {
this._emitter.trigger("waiting");
await this._source.loadUrl(url);
- this._setChannels(null);
this._emitter.trigger("stopWaiting");
if (autoPlay) {
this.play();
diff --git a/js/mediaMiddleware.ts b/js/mediaMiddleware.ts
index 31c67e56..9e2dc151 100644
--- a/js/mediaMiddleware.ts
+++ b/js/mediaMiddleware.ts
@@ -17,7 +17,6 @@ import {
SET_EQ_ON,
PLAY_TRACK,
BUFFER_TRACK,
- CHANNEL_COUNT_CHANGED,
LOAD_SERIALIZED_STATE
} from "./actionTypes";
import { next as nextTrack } from "./actionCreators";
@@ -70,19 +69,12 @@ export default (media: Media) => (store: MiddlewareStore) => {
id,
type: SET_MEDIA,
kbps: "128",
- khz: Math.round(media.sampleRate() / 1000).toString(),
- channels: media.channels(),
+ khz: "44",
+ channels: 2,
length: media.duration()
});
});
- media.on("channelupdate", () => {
- store.dispatch({
- type: CHANNEL_COUNT_CHANGED,
- channels: media.channels()
- });
- });
-
return (next: Dispatch) => (action: Action) => {
const returnValue = next(action);
const state = store.getState();
diff --git a/js/reducers/media.ts b/js/reducers/media.ts
index 03688a93..8f8d4446 100644
--- a/js/reducers/media.ts
+++ b/js/reducers/media.ts
@@ -14,7 +14,6 @@ import {
TOGGLE_TIME_MODE,
UPDATE_TIME_ELAPSED,
ADD_TRACK_FROM_URL,
- CHANNEL_COUNT_CHANGED,
LOAD_SERIALIZED_STATE
} from "../actionTypes";
import { TIME_MODE, MEDIA_STATUS } from "../constants";
@@ -23,12 +22,8 @@ import { MediaSerializedStateV1 } from "../serializedStates/v1Types";
export interface MediaState {
timeMode: TimeMode; // TODO: Convert this to an enum
timeElapsed: number;
- length: number | null;
- kbps: string | null;
- khz: string | null;
volume: number;
balance: number;
- channels: number | null; // TODO: Convert this to an enum
shuffle: boolean;
repeat: boolean;
status: MediaStatus | null; // TODO: Convert this to an enum
@@ -37,15 +32,12 @@ export interface MediaState {
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
@@ -66,8 +58,6 @@ const media = (
case STOP:
case IS_STOPPED:
return { ...state, status: MEDIA_STATUS.STOPPED };
- case CHANNEL_COUNT_CHANGED:
- return { ...state, channels: action.channels };
case TOGGLE_TIME_MODE:
const newMode =
state.timeMode === TIME_MODE.REMAINING
@@ -79,28 +69,11 @@ const media = (
case ADD_TRACK_FROM_URL:
return {
...state,
- timeElapsed: 0,
- length: null,
- kbps: null,
- khz: null,
- channels: null
+ timeElapsed: 0
};
case SET_MEDIA:
return {
- ...state,
- length: action.length,
- kbps: action.kbps,
- khz: action.khz,
- channels: action.channels
- };
- case SET_MEDIA_TAGS:
- const { sampleRate, bitrate, numberOfChannels } = action;
- const { kbps, khz, channels } = state;
- return {
- ...state,
- kbps: bitrate != null ? String(Math.round(bitrate / 1000)) : kbps,
- khz: sampleRate != null ? String(Math.round(sampleRate / 1000)) : khz,
- channels: numberOfChannels != null ? numberOfChannels : channels
+ ...state
};
case SET_VOLUME:
return { ...state, volume: action.volume };
diff --git a/js/reducers/tracks.ts b/js/reducers/tracks.ts
index 42effa6e..38cfce71 100644
--- a/js/reducers/tracks.ts
+++ b/js/reducers/tracks.ts
@@ -42,19 +42,6 @@ const tracks = (
[action.id]: newTrack
};
}
- case SET_MEDIA_TAGS: {
- return {
- ...state,
- [action.id]: {
- ...state[action.id],
- mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.COMPLETE,
- title: action.title,
- artist: action.artist,
- album: action.album,
- albumArtUrl: action.albumArtUrl
- }
- };
- }
case MEDIA_TAG_REQUEST_INITIALIZED:
return {
...state,
@@ -80,6 +67,32 @@ const tracks = (
}
};
}
+ case SET_MEDIA_TAGS:
+ const track = state[action.id];
+ const {
+ sampleRate,
+ bitrate,
+ numberOfChannels,
+ title,
+ artist,
+ album,
+ albumArtUrl
+ } = action;
+ const { kbps, khz, channels } = track;
+ return {
+ ...state,
+ [action.id]: {
+ ...track,
+ mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.COMPLETE,
+ title,
+ artist,
+ album,
+ albumArtUrl,
+ kbps: bitrate != null ? String(Math.round(bitrate / 1000)) : kbps,
+ khz: sampleRate != null ? String(Math.round(sampleRate / 1000)) : khz,
+ channels: numberOfChannels != null ? numberOfChannels : channels
+ }
+ };
default:
return state;
}
diff --git a/js/screenshotInitialState.ts b/js/screenshotInitialState.ts
index 2e4a3698..6935fe8b 100644
--- a/js/screenshotInitialState.ts
+++ b/js/screenshotInitialState.ts
@@ -7,28 +7,40 @@ const defaultTracksState = {
title: "Llama Whipping Intro",
artist: "DJ Mike Llama",
duration: 5,
- url: ""
+ url: "",
+ kbps: "128",
+ khz: "44",
+ channels: 2
},
"1": {
id: 1,
title: "Rock Is Dead",
artist: "Marilyn Manson",
duration: 191, // 3:11
- url: ""
+ url: "",
+ kbps: "128",
+ khz: "44",
+ channels: 2
},
"2": {
id: 2,
title: "Spybreak! (Short One)",
artist: "Propellerheads",
duration: 240, // 4:00
- url: ""
+ url: "",
+ kbps: "128",
+ khz: "44",
+ channels: 2
},
"3": {
id: 3,
title: "Bad Blood",
artist: "Ministry",
duration: 300, // 5:00
- url: ""
+ url: "",
+ kbps: "128",
+ khz: "44",
+ channels: 2
}
};
@@ -50,11 +62,7 @@ const initialState: DeepPartial = {
},
media: {
status: "PLAYING",
- kbps: "128",
- khz: "44",
- length: 5,
- timeElapsed: 3,
- channels: 2
+ timeElapsed: 3
},
playlist: {
trackOrder: [0, 1, 2, 3],
diff --git a/js/selectors.ts b/js/selectors.ts
index c401df53..d76d25a0 100644
--- a/js/selectors.ts
+++ b/js/selectors.ts
@@ -454,7 +454,17 @@ export const getBalance = (state: AppState) => state.media.balance;
export const getShuffle = (state: AppState) => state.media.shuffle;
export const getRepeat = (state: AppState) => state.media.repeat;
-export const getChannels = (state: AppState) => state.media.channels;
+export const getChannels = createSelector(
+ getCurrentTrack,
+ (track: PlaylistTrack | null): number | null => {
+ return track != null ? track.channels || null : null;
+ }
+);
+
+export const getTimeElapsed = (state: AppState): number => {
+ return state.media.timeElapsed;
+};
+
export function getSerlializedState(state: AppState): SerializedStateV1 {
return {
version: 1,
@@ -496,25 +506,33 @@ export const getStackedLayoutPositions = createSelector(
}
);
+export const getUserInputFocus = (state: AppState): string | null => {
+ return state.userInput.focus;
+};
+
+export const getUserInputScrubPosition = (state: AppState): number => {
+ return state.userInput.scrubPosition;
+};
// TODO: Make this a reselect selector
export const getMarqueeText = (state: AppState): string => {
const defaultText = "Winamp 2.91";
if (state.userInput.userMessage != null) {
return state.userInput.userMessage;
}
- switch (state.userInput.focus) {
+ switch (getUserInputFocus(state)) {
case "balance":
return MarqueeUtils.getBalanceText(state.media.balance);
case "volume":
return MarqueeUtils.getVolumeText(state.media.volume);
case "position":
- if (state.media.length == null) {
+ const duration = getDuration(state);
+ if (duration == null) {
// This probably can't ever happen.
return defaultText;
}
return MarqueeUtils.getPositionText(
- state.media.length,
- state.userInput.scrubPosition
+ duration,
+ getUserInputScrubPosition(state)
);
case "double":
return MarqueeUtils.getDoubleSizeModeText(state.display.doubled);
@@ -539,6 +557,20 @@ export const getMarqueeText = (state: AppState): string => {
return defaultText;
};
+export const getKbps = createSelector(
+ getCurrentTrack,
+ (track: PlaylistTrack | null): string | null => {
+ return track != null ? track.kbps || null : null;
+ }
+);
+
+export const getKhz = createSelector(
+ getCurrentTrack,
+ (track: PlaylistTrack | null): string | null => {
+ return track != null ? track.khz || null : null;
+ }
+);
+
export function getDebugData(state: AppState) {
return {
...state,
diff --git a/js/types.ts b/js/types.ts
index d8928830..51f81ff9 100644
--- a/js/types.ts
+++ b/js/types.ts
@@ -168,10 +168,6 @@ export type Action =
| {
type: "IS_STOPPED";
}
- | {
- type: "CHANNEL_COUNT_CHANGED";
- channels: number;
- }
| {
type: "TOGGLE_TIME_MODE";
}
@@ -526,6 +522,9 @@ export interface PlaylistTrack {
albumArtUrl?: string | null;
mediaTagsRequestStatus: MediaTagRequestStatus;
duration: number | null;
+ kbps?: string;
+ khz: string;
+ channels?: number;
}
export interface AppState {