import { createSelector } from "reselect"; import { denormalize, getTimeStr, clamp, percentToIndex, objectMap } from "./utils"; import { BANDS, TRACK_HEIGHT, WINDOW_RESIZE_SEGMENT_WIDTH, WINDOW_RESIZE_SEGMENT_HEIGHT, WINDOW_WIDTH } from "./constants"; import { createPlaylistURL } from "./playlistHtml"; import * as fromPlaylist from "./reducers/playlist"; import { generateGraph } from "./resizeUtils"; export const getEqfData = state => { const { sliders } = state.equalizer; const preset = { name: "Entry1", preamp: denormalize(sliders.preamp) }; BANDS.forEach(band => { preset[`hz${band}`] = denormalize(sliders[band]); }); const eqfData = { presets: [preset], type: "Winamp EQ library file v1.1" }; return eqfData; }; export const getTracks = state => state.playlist.tracks; const getTrackOrder = state => 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) => trackOrder.map(id => tracks[id]) ); export const getSelectedTrackObjects = createSelector( getOrderedTrackObjects, tracks => tracks.filter(track => track.selected) ); // If a duration is `null`, it counts as zero, which seems fine enough. const runningTimeFromTracks = tracks => 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) => `${getTimeStr(selectedRunningTime)}/${getTimeStr(totalRunningTime)}` ); // TODO: use slectors to get memoization export const getCurrentTrackIndex = state => state.playlist.trackOrder.indexOf(state.playlist.currentTrack); export const getCurrentTrackNumber = createSelector( getCurrentTrackIndex, currentTrackIndex => currentTrackIndex + 1 ); export const getCurrentTrackId = state => state.playlist.currentTrack; export const nextTrack = (state, 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 = clamp(nextIndex, 0, trackCount - 1); return trackOrder[nextIndex]; }; const BASE_WINDOW_HEIGHT = 58; export const getNumberOfVisibleTracks = state => { 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 => 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 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 => visibleTrackIds.includes(id); } ); export const getVisibleTracks = createSelector( getVisibleTrackIds, getTracks, (visibleTrackIds, tracks) => visibleTrackIds.map(id => tracks[id]) ); export const getPlaylist = state => state.playlist; export const getDuration = state => { const currentTrack = state.playlist.tracks[state.playlist.currentTrack]; return currentTrack && currentTrack.duration; }; export const getTrackDisplayName = (state, trackId) => fromPlaylist.getTrackDisplayName(getPlaylist(state), trackId); export const getCurrentTrackDisplayName = state => { const id = getCurrentTrackId(state); return getTrackDisplayName(state, id); }; export const getMinimalMediaText = createSelector( getCurrentTrackNumber, getCurrentTrackDisplayName, (trackNumber, name) => (name == null ? null : `${trackNumber}. ${name}`) ); export const getMediaText = createSelector( getMinimalMediaText, getDuration, (minimalMediaText, duration) => minimalMediaText == null ? null : // TODO: Maybe the ` *** ` should actually be added by the marquee `${minimalMediaText} (${getTimeStr(duration)}) *** ` ); export const getNumberOfTracks = state => getTrackOrder(state).length; const getPlaylistDuration = createSelector(getTracks, tracks => Object.values(tracks).reduce((total, track) => total + track.duration, 0) ); export const getPlaylistURL = createSelector( state => state, getNumberOfTracks, getPlaylistDuration, getTrackOrder, getTracks, (state, numberOfTracks, playlistDuration, trackOrder, tracks) => createPlaylistURL({ numberOfTracks, averageTrackLength: getTimeStr(playlistDuration / numberOfTracks), // TODO: Handle hours playlistLengthMinutes: Math.floor(playlistDuration / 60), playlistLengthSeconds: Math.floor(playlistDuration % 60), tracks: trackOrder.map( (id, i) => `${i + 1}. ${getTrackDisplayName(state, id)} (${getTimeStr( tracks[id].duration )})` ) }) ); export function getWindowPositions(state) { return state.windows.positions; } const WINDOW_HEIGHT = 116; const SHADE_WINDOW_HEIGHT = 14; function getWPixelSize(w, doubled) { 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, windowId) { return state.windows.genWindows[windowId].size; } export function getWindowOpen(state, windowId) { return state.windows.genWindows[windowId].open; } export function getWindowShade(state, windowId) { return state.windows.genWindows[windowId].shade; } export const getGenWindows = state => { return state.windows.genWindows; }; export function getDoubled(state) { return state.display.doubled; } export const getWindowSizes = createSelector( getGenWindows, getDoubled, (windows, doubled) => { return objectMap(windows, w => getWPixelSize(w, doubled)); } ); export function getWindowPixelSize(state, windowId) { // Rather than compute it directly, we go via `getWindowSizes` // to take advantage of caching. return getWindowSizes(state)[windowId]; } export const getWindowsInfo = createSelector( getWindowSizes, getWindowPositions, (sizes, positions) => Object.keys(sizes).map(key => ({ key, ...sizes[key], ...positions[key] })) ); export const getWindowGraph = createSelector(getWindowsInfo, generateGraph); export const getSkinPlaylistStyle = state => { return ( state.display.skinPlaylistStyle || { normal: "#00FF00", current: "#FFFFFF", normalbg: "#000000", selectedbg: "#0000C6", font: "Arial" } ); };