webamp/js/selectors.ts
2019-03-19 06:55:24 -07:00

662 lines
18 KiB
TypeScript

import {
AppState,
PlaylistTrack,
WebampWindow,
WindowId,
WindowInfo,
LoadedURLTrack,
WindowPositions,
PlaylistStyle,
TransitionType,
} from "./types";
import { createSelector } from "reselect";
import * as Utils from "./utils";
import {
BANDS,
TRACK_HEIGHT,
WINDOW_RESIZE_SEGMENT_WIDTH,
WINDOW_RESIZE_SEGMENT_HEIGHT,
WINDOW_WIDTH,
MEDIA_STATUS,
MEDIA_TAG_REQUEST_STATUS,
WINDOWS,
VISUALIZERS,
} from "./constants";
import { createPlaylistURL } from "./playlistHtml";
import * as fromTracks from "./reducers/tracks";
import * as fromDisplay from "./reducers/display";
import * as fromEqualizer from "./reducers/equalizer";
import * as fromMedia from "./reducers/media";
import * as fromWindows from "./reducers/windows";
import * as TrackUtils from "./trackUtils";
import * as MarqueeUtils from "./marqueeUtils";
import { generateGraph } from "./resizeUtils";
import { SerializedStateV1 } from "./serializedStates/v1Types";
export const getSliders = (state: AppState) => state.equalizer.sliders;
export const getEqfData = createSelector(
getSliders,
sliders => {
const preset: { [key: string]: number | string } = {
name: "Entry1",
preamp: Utils.denormalizeEqBand(sliders.preamp),
};
BANDS.forEach(band => {
preset[`hz${band}`] = Utils.denormalizeEqBand(sliders[band]);
});
const eqfData = {
presets: [preset],
type: "Winamp EQ library file v1.1",
};
return eqfData;
}
);
export const getTracks = (state: AppState) => state.tracks;
export const getTracksMatchingFilter = createSelector(
getTracks,
tracks => {
const tracksArray = Object.values(tracks);
const filter = Utils.makeCachingFilterFunction(
tracksArray,
(track, query) => TrackUtils.trackFilterContents(track).includes(query)
);
return (filterString: string): PlaylistTrack[] => {
return filter(filterString.toLowerCase());
};
}
);
export const getTrackUrl = (state: AppState) => {
return (id: number): string | null => {
const track = state.tracks[id];
return track == null ? null : track.url;
};
};
export const getTrackOrder = (state: AppState) => state.playlist.trackOrder;
export const getTrackCount = createSelector(
getTrackOrder,
trackOrder => trackOrder.length
);
export const getOrderedTracks = createSelector(
getTracks,
getTrackOrder,
(tracks, trackOrder) => trackOrder.filter(id => tracks[id])
);
const getOrderedTrackObjects = createSelector(
getTracks,
getOrderedTracks,
(tracks, trackOrder): PlaylistTrack[] => trackOrder.map(id => tracks[id])
);
export const getSelectedTrackIds = (state: AppState): Set<number> => {
return state.playlist.selectedTracks;
};
export const getSelectedTrackObjects = createSelector(
getOrderedTrackObjects,
getSelectedTrackIds,
(tracks, selectedIds) => tracks.filter(track => selectedIds.has(track.id))
);
// If a duration is `null`, it counts as zero, which seems fine enough.
const runningTimeFromTracks = (tracks: PlaylistTrack[]) =>
tracks.reduce((time, track) => time + Number(track.duration), 0);
const getTotalRunningTime = createSelector(
getOrderedTrackObjects,
runningTimeFromTracks
);
const getSelectedRunningTime = createSelector(
getSelectedTrackObjects,
runningTimeFromTracks
);
// Note: We should append "+" to these values if some of the tracks are of unknown time.
export const getRunningTimeMessage = createSelector(
getTotalRunningTime,
getSelectedRunningTime,
(totalRunningTime, selectedRunningTime) =>
`${Utils.getTimeStr(selectedRunningTime)}/${Utils.getTimeStr(
totalRunningTime
)}`
);
// TODO: use slectors to get memoization
export const getCurrentTrackIndex = (state: AppState): number => {
const { playlist } = state;
if (playlist.currentTrack == null) {
return -1;
}
return playlist.trackOrder.indexOf(playlist.currentTrack);
};
export const getCurrentTrackNumber = createSelector(
getCurrentTrackIndex,
currentTrackIndex => currentTrackIndex + 1
);
export const getCurrentTrackId = (state: AppState) =>
state.playlist.currentTrack;
export const nextTrack = (state: AppState, n = 1) => {
const {
playlist: { trackOrder },
media: { repeat },
} = state;
const trackCount = getTrackCount(state);
if (trackCount === 0) {
return null;
}
const currentIndex = getCurrentTrackIndex(state);
let nextIndex = currentIndex + n;
if (repeat) {
nextIndex = nextIndex % trackCount;
if (nextIndex < 0) {
// Handle wrapping around backwards
nextIndex += trackCount;
}
return trackOrder[nextIndex];
}
if (currentIndex === trackCount - 1 && n > 0) {
return null;
} else if (currentIndex === 0 && n < 0) {
return null;
}
nextIndex = Utils.clamp(nextIndex, 0, trackCount - 1);
return trackOrder[nextIndex];
};
const BASE_WINDOW_HEIGHT = 58;
export const getNumberOfVisibleTracks = (state: AppState) => {
const playlistSize = getWindowSize(state)("playlist");
return Math.floor(
(BASE_WINDOW_HEIGHT + WINDOW_RESIZE_SEGMENT_HEIGHT * playlistSize[1]) /
TRACK_HEIGHT
);
};
export const getOverflowTrackCount = createSelector(
getTrackCount,
getNumberOfVisibleTracks,
(trackCount, numberOfVisibleTracks) =>
Math.max(0, trackCount - numberOfVisibleTracks)
);
const _getPlaylistScrollPosition = (state: AppState) =>
state.display.playlistScrollPosition;
export const getPlaylistScrollPosition = createSelector(
getOverflowTrackCount,
_getPlaylistScrollPosition,
(overflowTrackCount, playlistScrollPosition) => {
if (overflowTrackCount === 0) {
return 0;
}
return Math.round(
(Math.round((overflowTrackCount * playlistScrollPosition) / 100) /
overflowTrackCount) *
100
);
}
);
export const getScrollOffset = createSelector(
_getPlaylistScrollPosition,
getTrackCount,
getNumberOfVisibleTracks,
(playlistScrollPosition, trackCount, numberOfVisibleTracks) => {
const overflow = Math.max(0, trackCount - numberOfVisibleTracks);
return Utils.percentToIndex(playlistScrollPosition / 100, overflow + 1);
}
);
export const getVisibleTrackIds = createSelector(
getScrollOffset,
getTrackOrder,
getNumberOfVisibleTracks,
(offset, trackOrder, numberOfVisibleTracks) =>
trackOrder.slice(offset, offset + numberOfVisibleTracks)
);
export const getTrackIsVisibleFunction = createSelector(
getVisibleTrackIds,
visibleTrackIds => {
return (id: number) => visibleTrackIds.includes(id);
}
);
export const getVisibleTracks = createSelector(
getVisibleTrackIds,
getTracks,
(visibleTrackIds, tracks) => visibleTrackIds.map(id => tracks[id])
);
export const getPlaylist = (state: AppState) => state.playlist;
export const getDuration = (state: AppState): number | null => {
const { playlist, tracks } = state;
if (playlist.currentTrack == null) {
return null;
}
const currentTrack = tracks[playlist.currentTrack];
return currentTrack && currentTrack.duration;
};
export const getTrackDisplayName = createSelector(
getTracks,
tracks => {
return (trackId: number | null) =>
fromTracks.getTrackDisplayName(tracks, trackId);
}
);
export const getCurrentTrackDisplayName = createSelector(
getCurrentTrackId,
getTrackDisplayName,
(id, getName) => {
return getName(id);
}
);
export const getMediaIsPlaying = (state: AppState) =>
state.media.status === MEDIA_STATUS.PLAYING;
export const getCurrentTrack = createSelector(
getCurrentTrackId,
getTracks,
(trackId, tracks): PlaylistTrack | null => {
return trackId == null ? null : tracks[trackId];
}
);
export const getCurrentlyPlayingTrackIdIfLoaded = createSelector(
getMediaIsPlaying,
getCurrentTrack,
(mediaIsPlaying, currentTrack) => {
if (
!mediaIsPlaying ||
!currentTrack ||
currentTrack.mediaTagsRequestStatus ===
MEDIA_TAG_REQUEST_STATUS.INITIALIZED
) {
return null;
}
return currentTrack.id;
}
);
export const getCurrentTrackInfo = createSelector(
getCurrentTrack,
(track: PlaylistTrack | null): LoadedURLTrack | null => {
if (track == null) {
return null;
}
return {
url: track.url,
metaData: {
title: track.title || null,
artist: track.artist || null,
album: track.album || null,
albumArtUrl: track.albumArtUrl || null,
},
};
}
);
export const getMinimalMediaText = createSelector(
getCurrentTrackNumber,
getCurrentTrackDisplayName,
(trackNumber, name) => (name == null ? null : `${trackNumber}. ${name}`)
);
export const getMediaText = createSelector(
getMinimalMediaText,
getDuration,
(minimalMediaText, duration) =>
minimalMediaText == null
? null
: `${minimalMediaText} (${Utils.getTimeStr(duration)})`
);
export const getNumberOfTracks = (state: AppState) =>
getTrackOrder(state).length;
const getPlaylistDuration = createSelector(
getTracks,
tracks =>
Object.values(tracks).reduce(
(total, track) => total + (track.duration || 0),
0
)
);
export const getPlaylistURL = createSelector(
getNumberOfTracks,
getPlaylistDuration,
getTrackOrder,
getTracks,
getTrackDisplayName,
(numberOfTracks, playlistDuration, trackOrder, tracks, getDisplayName) =>
createPlaylistURL({
numberOfTracks,
averageTrackLength: Utils.getTimeStr(playlistDuration / numberOfTracks),
// TODO: Handle hours
playlistLengthMinutes: Math.floor(playlistDuration / 60),
playlistLengthSeconds: Math.floor(playlistDuration % 60),
tracks: trackOrder.map(
(id, i) =>
`${i + 1}. ${getDisplayName(id)} (${Utils.getTimeStr(
tracks[id].duration
)})`
),
})
);
const WINDOW_HEIGHT = 116;
const SHADE_WINDOW_HEIGHT = 14;
function getWPixelSize(w: WebampWindow, doubled: boolean) {
const [width, height] = w.size;
const doubledMultiplier = doubled && w.canDouble ? 2 : 1;
const pix = {
height: WINDOW_HEIGHT + height * WINDOW_RESIZE_SEGMENT_HEIGHT,
width: WINDOW_WIDTH + width * WINDOW_RESIZE_SEGMENT_WIDTH,
};
return {
height: (w.shade ? SHADE_WINDOW_HEIGHT : pix.height) * doubledMultiplier,
width: pix.width * doubledMultiplier,
};
}
export function getWindowSize(state: AppState) {
return (windowId: WindowId) => state.windows.genWindows[windowId].size;
}
export function getWindowOpen(state: AppState) {
return (windowId: WindowId) => state.windows.genWindows[windowId].open;
}
export function getWindowShade(state: AppState) {
return (windowId: WindowId) => state.windows.genWindows[windowId].shade;
}
export function getWindowHidden(state: AppState) {
return (windowId: WindowId) => state.windows.genWindows[windowId].hidden;
}
export function getFocusedWindow(state: AppState): WindowId {
return state.windows.focused;
}
export function getWindowPosition(state: AppState) {
return (windowId: WindowId) => state.windows.genWindows[windowId].position;
}
export function getPositionsAreRelative(state: AppState) {
return state.windows.positionsAreRelative;
}
export const getGenWindows = (state: AppState) => {
return state.windows.genWindows;
};
export const getWindowPositions = createSelector(
getGenWindows,
(windows): WindowPositions => Utils.objectMap(windows, w => w.position)
);
export function getDoubled(state: AppState) {
return state.display.doubled;
}
export function getLlamaMode(state: AppState) {
return state.display.llama;
}
export const getWindowSizes = createSelector(
getGenWindows,
getDoubled,
(windows, doubled) => {
return Utils.objectMap(windows, w => getWPixelSize(w, doubled));
}
);
export const getWindowPixelSize = createSelector(
getWindowSizes,
sizes => {
return (windowId: WindowId) => sizes[windowId];
}
);
// TODO: Now that both size and position are stored on genWindows this seems a bit silly.
export const getWindowsInfo = createSelector(
getWindowSizes,
getWindowPositions,
(sizes, positions): WindowInfo[] =>
Object.keys(sizes).map(key => ({ key, ...sizes[key], ...positions[key] }))
);
export const getWindowGraph = createSelector(
getWindowsInfo,
generateGraph
);
export const getSkinPlaylistStyle = (state: AppState): PlaylistStyle => {
return (
state.display.skinPlaylistStyle || {
normal: "#00FF00",
current: "#FFFFFF",
normalbg: "#000000",
selectedbg: "#0000C6",
font: "Arial",
}
);
};
export const getSkinGenExColors = (state: AppState) => {
return state.display.skinGenExColors;
};
export const getVisualizerStyle = (state: AppState): string => {
const milkdrop = state.windows.genWindows[WINDOWS.MILKDROP];
if (milkdrop != null && milkdrop.open) {
return VISUALIZERS.MILKDROP;
}
return fromDisplay.getVisualizerStyle(state.display);
};
export const getVolume = (state: AppState) => state.media.volume;
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 = 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,
media: fromMedia.getSerializedState(state.media),
equalizer: fromEqualizer.getSerializedState(state.equalizer),
display: fromDisplay.getSerializedState(state.display),
windows: fromWindows.getSerializedState(state.windows),
};
}
export function getEqualizerEnabled(state: AppState): boolean {
return state.equalizer.on;
}
export function getEqualizerAuto(state: AppState): boolean {
return state.equalizer.auto;
}
export function getBrowserWindowSize(
state: AppState
): { height: number; width: number } {
return state.windows.browserWindowSize;
}
export const getOpenWindows = createSelector(
getGenWindows,
genWindows => Utils.objectFilter(genWindows, w => w.open)
);
export const getStackedLayoutPositions = createSelector(
getOpenWindows,
getDoubled,
(openWindows, doubled): WindowPositions => {
let offset = 0;
return Utils.objectMap(openWindows, w => {
const position = { x: 0, y: offset };
offset += getWPixelSize(w, doubled).height;
return position;
});
}
);
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 (getUserInputFocus(state)) {
case "balance":
return MarqueeUtils.getBalanceText(state.media.balance);
case "volume":
return MarqueeUtils.getVolumeText(state.media.volume);
case "position":
const duration = getDuration(state);
if (duration == null) {
// This probably can't ever happen.
return defaultText;
}
return MarqueeUtils.getPositionText(
duration,
getUserInputScrubPosition(state)
);
case "double":
return MarqueeUtils.getDoubleSizeModeText(state.display.doubled);
case "eq":
const band = state.userInput.bandFocused;
if (band == null) {
// This probably can't ever happen.
return defaultText;
}
return MarqueeUtils.getEqText(band, state.equalizer.sliders[band]);
default:
break;
}
if (state.playlist.currentTrack != null) {
const mediaText = getMediaText(state);
if (mediaText == null) {
// This probably can't ever happen.
return defaultText;
}
return mediaText;
}
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,
display: {
...state.display,
skinGenLetterWidths: "[[REDACTED]]",
skinImages: "[[REDACTED]]",
skinCursors: "[[REDACTED]]",
skinRegion: "[[REDACTED]]",
},
};
}
export function getMilkdropDesktopEnabled(state: AppState): boolean {
return state.milkdrop.display === "DESKTOP";
}
export function getMilkdropFullscreenEnabled(state: AppState): boolean {
return state.milkdrop.display === "FULLSCREEN";
}
export function getPresets(state: AppState): any {
return state.milkdrop.presets;
}
export function getButterchurn(state: AppState): any {
return state.milkdrop.butterchurn;
}
export function getPresetTransitionType(state: AppState): TransitionType {
return state.milkdrop.transitionType;
}
export function getCurrentPresetIndex(state: AppState): number | null {
return state.milkdrop.currentPresetIndex;
}
export function getCurrentPreset(state: AppState): any | null {
const index = getCurrentPresetIndex(state);
if (index == null) {
return null;
}
const preset = state.milkdrop.presets[index];
if (preset == null || preset.type === "UNRESOLVED") {
return null;
}
return preset.preset;
}
export function getPresetNames(state: AppState): string[] {
return state.milkdrop.presets.map(preset => preset.name);
}
export function getPresetOverlayOpen(state: AppState): boolean {
return state.milkdrop.overlay;
}
export function getPresetsAreCycling(state: AppState): boolean {
return state.milkdrop.cycling;
}
export function getRandomizePresets(state: AppState): boolean {
return state.milkdrop.randomize;
}