Upgrading to more functional components

This commit is contained in:
James Barnsley
2021-11-21 19:57:37 +13:00
parent f8da0fbf33
commit 2e09488273
15 changed files with 20594 additions and 24365 deletions

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { bindActionCreators } from 'redux';
import { Route, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import { Route, Switch, useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import ReactGA from 'react-ga';
import * as Sentry from '@sentry/browser';
@ -56,53 +55,36 @@ import Reset from './views/modals/Reset';
import Servers from './views/modals/Servers';
import { scrollTo, isTouchDevice } from './util/helpers';
import * as coreActions from './services/core/actions';
import * as uiActions from './services/ui/actions';
import * as pusherActions from './services/pusher/actions';
import * as mopidyActions from './services/mopidy/actions';
import * as spotifyActions from './services/spotify/actions';
import * as lastfmActions from './services/lastfm/actions';
import * as geniusActions from './services/genius/actions';
import * as snapcastActions from './services/snapcast/actions';
import MediaSession from './components/MediaSession';
import ErrorBoundary from './components/ErrorBoundary';
const App = ({
language,
allow_reporting,
initial_setup_complete,
snapcast_enabled,
context_menu,
history,
location,
debug_info,
theme,
dragging,
touch_dragging,
sidebar_open,
slim_mode,
wide_scrollbar_enabled,
hide_scrollbars,
smooth_scrolling_enabled,
hotkeys_enabled,
...props
}) => {
const {
mopidyActions: mopidy,
pusherActions: pusher,
uiActions: ui,
snapcastActions: snapcast,
} = props;
const {
pathname,
state: {
scroll_position,
} = {},
} = location;
window.language = language;
const App = () => {
const [isReady, setIsReady] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);
const { pathname, state: { scroll_position } = {} } = useLocation();
const dispatch = useDispatch();
const language = useSelector((state) => state.ui.language);
window.language = language;
const theme = useSelector((state) => state.ui.theme);
const wide_scrollbar_enabled = useSelector((state) => state.ui.wide_scrollbar_enabled);
const hide_scrollbars = useSelector((state) => state.ui.hide_scrollbars);
const smooth_scrolling_enabled = useSelector((state) => state.ui.smooth_scrolling_enabled);
const hotkeys_enabled = useSelector((state) => state.ui.hotkeys_enabled);
const allow_reporting = useSelector((state) => state.ui.allow_reporting);
const touch_dragging = useSelector((state) => state.ui.touch_dragging);
const dragging = useSelector((state) => state.ui.dragger?.active);
const context_menu = useSelector((state) => state.ui.context_menu);
const initial_setup_complete = useSelector((state) => state.ui.initial_setup_complete);
const slim_mode = useSelector((state) => state.ui.slim_mode);
const sidebar_open = useSelector((state) => state.ui.sidebar_open);
const debug_info = useSelector((state) => state.ui.debug_info);
const snapcast_enabled = useSelector((state) => state.snapcast.enabled);
// Accept incoming preconfiguration via URL parameters and inject into application state.
// For example: iris?snapcast={"enabled":true,"host":"myserver.local"}
@ -114,11 +96,23 @@ const App = ({
const json = JSON.parse(value);
switch (key) {
case 'ui':
dispatch(uiActions.set(json));
console.info(`Applying preconfiguration for ${key}:`, value)
break;
case 'spotify':
case 'pusher':
case 'snapcast':
dispatch(spotifyActions.set(json));
console.info(`Applying preconfiguration for ${key}:`, value)
break;
case 'mopidy':
props[`${key}Actions`].set(json);
dispatch(mopidyActions.set(json));
console.info(`Applying preconfiguration for ${key}:`, value)
break;
case 'pusher':
dispatch(pusherActions.set(json));
console.info(`Applying preconfiguration for ${key}:`, value)
break;
case 'snapcast':
dispatch(snapcastActions.set(json));
console.info(`Applying preconfiguration for ${key}:`, value)
break;
default:
@ -187,11 +181,11 @@ const App = ({
});
}
mopidy.connect();
pusher.connect();
ui.getBroadcasts();
dispatch(mopidyActions.connect());
dispatch(pusherActions.connect());
dispatch(uiActions.getBroadcasts());
if (snapcast_enabled) {
snapcast.connect();
dispatch(snapcastActions.connect());
}
if (!initial_setup_complete) {
history.push('/initial-setup');
@ -213,10 +207,10 @@ const App = ({
scrollTo(parseInt(scroll_position, 10), false);
}
uiActions.toggleSidebar(false);
uiActions.setSelectedTracks([]);
dispatch(uiActions.toggleSidebar(false));
dispatch(uiActions.setSelectedTracks([]));
if (context_menu) {
uiActions.hideContextMenu();
dispatch(uiActions.hideContextMenu());
}
}, [pathname]);
@ -230,14 +224,19 @@ const App = ({
* @param e Event
* */
const handleFocusAndBlur = () => {
uiActions.setWindowFocus(document.hasFocus());
}
dispatch(uiActions.setWindowFocus(document.hasFocus()));
};
const handleInstallPrompt = (e) => {
e.preventDefault();
console.log('Install prompt detected');
uiActions.installPrompt(e);
}
dispatch(uiActions.installPrompt(e));
};
const handleInteraction = () => {
if (hasInteracted) return;
setHasInteracted(true);
};
let className = `${theme}-theme app-inner`;
className += ` ${navigator.onLine ? 'online' : 'offline'}`;
@ -274,8 +273,8 @@ const App = ({
return (
<div
className={className}
onClick={() => hasInteracted ? null : setHasInteracted(true)}
onKeyDown={() => hasInteracted ? null : setHasInteracted(true)}
onClick={handleInteraction}
onKeyDown={handleInteraction}
>
<div className="body">
<Switch>
@ -296,16 +295,8 @@ const App = ({
<Route>
<div>
<Sidebar
location={location}
history={history}
tabIndex="3"
/>
<PlaybackControls
history={history}
slim_mode={slim_mode}
tabIndex="2"
/>
<Sidebar tabIndex="3" />
<PlaybackControls tabIndex="2"/>
<main id="main" className="smooth-scroll" tabIndex="1">
<Switch>
@ -405,11 +396,8 @@ const App = ({
</Switch>
</div>
<ResizeListener
uiActions={uiActions}
slim_mode={slim_mode}
/>
{hotkeys_enabled && <Hotkeys history={history} />}
<ResizeListener />
{hotkeys_enabled && <Hotkeys />}
<ContextMenu />
<Dragger />
<Notifications />
@ -420,62 +408,4 @@ const App = ({
);
};
const mapStateToProps = (state) => {
const {
ui: {
language,
theme,
wide_scrollbar_enabled,
hide_scrollbars,
smooth_scrolling_enabled,
hotkeys_enabled,
allow_reporting,
touch_dragging,
initial_setup_complete,
slim_mode,
sidebar_open,
dragger: {
active: dragging,
} = {},
context_menu,
debug_info,
},
snapcast: {
enabled: snapcast_enabled,
},
} = state;
return {
language,
theme,
wide_scrollbar_enabled,
hide_scrollbars,
smooth_scrolling_enabled,
hotkeys_enabled,
allow_reporting,
touch_dragging,
initial_setup_complete,
slim_mode,
sidebar_open,
dragging,
context_menu,
debug_info,
snapcast_enabled,
};
};
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
pusherActions: bindActionCreators(pusherActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
lastfmActions: bindActionCreators(lastfmActions, dispatch),
geniusActions: bindActionCreators(geniusActions, dispatch),
snapcastActions: bindActionCreators(snapcastActions, dispatch),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(App);
export default App;

View File

@ -1,63 +1,45 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as mopidyActions from '../../services/mopidy/actions';
import { useSelector, useDispatch } from 'react-redux';
import { setTimePosition } from '../../services/mopidy/actions';
import { throttle } from '../../util/helpers';
class ProgressSlider extends React.Component {
constructor(props) {
super(props);
const ProgressSlider = () => {
const dispatch = useDispatch();
const connected = useSelector((state) => state.mopidy.connected);
const time_position = useSelector((state) => state.mopidy.time_position);
const play_state = useSelector((state) => state.mopidy.play_state);
const current_track = useSelector((state) => state.core.current_track);
this.handleChange = throttle(this.handleChange.bind(this), 250);
}
const onChangeThrottled = (e) => {
const value = parseInt(e.target.value, 10);
dispatch(setTimePosition(current_track?.duration * (value / 100)));
};
const onChange = throttle(onChangeThrottled, 250);
handleChange(value) {
this.props.mopidyActions.setTimePosition(this.props.current_track.duration * (value / 100));
}
render() {
let percent = 0;
if (this.props.connected && this.props.time_position && this.props.current_track && this.props.current_track.duration) {
percent = this.props.time_position / this.props.current_track.duration;
percent *= 100;
if (percent > 1000) {
percent = 100;
}
let percent = 0;
if (connected && time_position && current_track && current_track.duration) {
percent = time_position / current_track.duration;
percent *= 100;
if (percent > 1000) {
percent = 100;
}
return (
<div className={`slider slider--playback-progress slider--${this.props.play_state}`}>
<input
type="range"
min="0"
max="100"
value={percent}
className="slider__input"
onChange={(e) => this.handleChange(parseInt(e.target.value))}
/>
<div className="slider__track">
<div className="slider__track__progress" style={{ width: `${percent}%` }} />
</div>
</div>
);
}
return (
<div className={`slider slider--playback-progress slider--${play_state}`}>
<input
type="range"
min="0"
max="100"
value={percent}
className="slider__input"
onChange={onChange}
/>
<div className="slider__track">
<div className="slider__track__progress" style={{ width: `${percent}%` }} />
</div>
</div>
);
}
/**
* Export our component
*
* We also integrate our global store, using connect()
* */
const mapStateToProps = (state, ownProps) => ({
current_track: (state.core.current_track && state.core.items[state.core.current_track.uri] !== undefined ? state.core.items[state.core.current_track.uri] : null),
connected: state.mopidy.connected,
time_position: state.mopidy.time_position,
play_state: state.mopidy.play_state,
});
const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(ProgressSlider);
export default ProgressSlider;

View File

@ -1,5 +1,6 @@
import ReactGA from 'react-ga';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { indexToArray, sortItems } from '../util/arrays';
import { collate } from '../util/format';
@ -9,6 +10,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as snapcastActions from '../services/snapcast/actions';
const Hotkeys = () => {
const history = useHistory();
const dispatch = useDispatch();
const volume = useSelector((state) => state.mopidy.volume);
const mute = useSelector((state) => state.mopidy.mute);

View File

@ -1,7 +1,6 @@
import React from 'react';
import { Link as RouterLink, useHistory, useLocation } from 'react-router-dom';
import { scrollTo } from '../util/helpers';
import { decodeUri } from '../util/format';
const updateScrollPosition = ({
retainScroll = false,

View File

@ -1,6 +1,5 @@
import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as uiActions from '../services/ui/actions';
import * as spotifyActions from '../services/spotify/actions';
@ -15,280 +14,309 @@ import { i18n, I18n } from '../locale';
import ErrorBoundary from './ErrorBoundary';
import Button from './Button';
class Notifications extends React.Component {
importConfiguration(notification_key, configuration) {
const Notification = ({
item: {
key,
type,
level,
title,
content,
description,
links,
configuration,
duration,
closing,
},
importConfiguration,
}) => {
const dispatch = useDispatch();
const { removeNotification } = uiActions;
switch (type) {
case 'shortcut':
return (
<div className="notification__wrapper">
<div className={`notification notification--shortcut${closing ? ' closing' : ''}`} data-duration={duration}>
<Icon name={content} />
{title && (
<h4 className="notification__title">{title}</h4>
)}
</div>
</div>
);
case 'share-configuration-received':
return (
<div className="notification__wrapper">
<div className="notification notification--info" data-duration={duration}>
<Icon
name="close"
className="notification__close-button"
onClick={() => dispatch(removeNotification(key, true))}
/>
<h4 className="notification__title">
<I18n path="modal.share_configuration.import.title" />
</h4>
<div className="notification__content">
<p>
<I18n path="modal.share_configuration.import.subtitle" />
</p>
<ul>
{configuration.ui && (
<li><I18n path="modal.share_configuration.interface" /></li>
)}
{configuration.spotify && (
<li><I18n path="services.spotify.title" /></li>
)}
{configuration.lastfm && (
<li><I18n path="services.lastfm.title" /></li>
)}
{configuration.genius && (
<li><I18n path="services.genius.title" /></li>
)}
{configuration.snapcast && (
<li><I18n path="services.snapcast.title" /></li>
)}
</ul>
<p>
<I18n path="modal.share_configuration.import.do_you_want_to_import" />
</p>
</div>
<div className="notification__actions">
<Button
className="notification__actions__item"
onClick={() => importConfiguration(key, configuration)}
tracking={{ category: 'ShareConfiguration', action: 'Import' }}
>
<I18n path="modal.share_configuration.import.import_now" />
</Button>
</div>
</div>
</div>
);
default:
return (
<div className="notification__wrapper" key={key}>
<div
className={`notification notification--${level}${closing ? ' closing' : ''}`}
data-key={key}
data-duration={duration}
>
<Icon
name="close"
className="notification__close-button"
onClick={() => dispatch(removeNotification(key, true))}
/>
{title && (
<h4 className="notification__title">
{title}
</h4>
)}
{content && (
<div className="notification__content">
{content}
</div>
)}
{description && (
<div className="notification__description">
{description}
</div>
)}
{links && (
<div className="notification__actions">
{
links.map((link, i) => (
<Button
type="secondary"
className="notification__actions__item"
href={link.url}
target={link.new_window ? '_blank' : 'self'}
key={i}
tracking={{ category: 'NotificationLink', action: 'Click', label: link.text }}
>
{link.text}
</Button>
))
}
</div>
)}
</div>
</div>
);
}
};
const NotificationItems = () => {
const index = useSelector((state) => state.ui.notifications);
const items = indexToArray(index);
const dispatch = useDispatch();
const importConfiguration = (notification_key, configuration) => {
if (configuration.ui) {
this.props.uiActions.set(configuration.ui);
dispatch(uiActions.set(configuration.ui));
}
if (configuration.spotify) {
this.props.spotifyActions.importAuthorization(configuration.spotify.authorization, configuration.spotify.me);
dispatch(spotifyActions.importAuthorization(
configuration.spotify.authorization,
configuration.spotify.me,
));
}
if (configuration.snapcast) {
this.props.snapcastActions.set(configuration.snapcast);
setTimeout(() => this.props.snapcastActions.connect(), 100);
dispatch(snapcastActions.set(configuration.snapcast));
setTimeout(() => dispatch(snapcastActions.connect()), 100);
}
if (configuration.lastfm) {
this.props.lastfmActions.importAuthorization(configuration.lastfm.authorization, configuration.lastfm.me);
dispatch(lastfmActions.importAuthorization(
configuration.lastfm.authorization,
configuration.lastfm.me,
));
}
if (configuration.genius) {
this.props.geniusActions.importAuthorization(configuration.genius.authorization, configuration.genius.me);
dispatch(geniusActions.importAuthorization(
configuration.genius.authorization,
configuration.genius.me,
));
}
this.props.uiActions.removeNotification(notification_key, true);
this.props.uiActions.createNotification({
dispatch(uiActions.removeNotification(notification_key, true));
dispatch(uiActions.createNotification({
content: i18n('modal.share_configuration.import.successful'),
});
}));
}
renderNotifications() {
if (!this.props.notifications || this.props.notifications.length <= 0) return null;
if (!items || !items.length) return null;
const notifications = indexToArray(this.props.notifications);
return (
<ErrorBoundary>
{items.map((item) => (
<Notification
key={`${item.key}_${item.status}`}
item={item}
importConfiguration={importConfiguration}
/>
))}
</ErrorBoundary>
);
};
return (
<ErrorBoundary>
{
notifications.map((notification) => {
switch (notification.type) {
case 'shortcut':
return (
<div className="notification__wrapper" key={notification.key}>
<div className={`notification notification--shortcut${notification.closing ? ' closing' : ''}`} data-duration={notification.duration}>
<Icon name={notification.content} />
{notification.title && (
<h4 className="notification__title">{notification.title}</h4>
)}
</div>
</div>
);
const Process = ({
item: {
total,
remaining,
level = 'info',
content,
description,
status,
closing,
},
cancelProcess,
closeProcess,
}) => {
let progress = 0;
if (total && remaining) {
progress = ((total - remaining) / total).toFixed(4);
}
case 'share-configuration-received':
return (
<div className="notification__wrapper" key={notification.key}>
<div className="notification notification--info" key={notification.key} data-duration={notification.duration}>
<Icon
name="close"
className="notification__close-button"
onClick={(e) => this.props.uiActions.removeNotification(notification.key, true)}
/>
<h4 className="notification__title">
<I18n path="modal.share_configuration.import.title" />
</h4>
<div className="notification__content">
<p>
<I18n path="modal.share_configuration.import.subtitle" />
</p>
<ul>
{notification.configuration.ui && (
<li><I18n path="modal.share_configuration.interface" /></li>
)}
{notification.configuration.spotify && (
<li><I18n path="services.spotify.title" /></li>
)}
{notification.configuration.lastfm && (
<li><I18n path="services.lastfm.title" /></li>
)}
{notification.configuration.genius && (
<li><I18n path="services.genius.title" /></li>
)}
{notification.configuration.snapcast && (
<li><I18n path="services.snapcast.title" /></li>
)}
</ul>
<p>
<I18n path="modal.share_configuration.import.do_you_want_to_import" />
</p>
</div>
<div className="notification__actions">
<Button
className="notification__actions__item"
onClick={() => this.importConfiguration(notification.key, notification.configuration)}
tracking={{ category: 'ShareConfiguration', action: 'Import' }}
>
<I18n path="modal.share_configuration.import.import_now" />
</Button>
</div>
</div>
</div>
);
default:
return (
<div className="notification__wrapper" key={notification.key}>
<div
className={`notification notification--${notification.level}${notification.closing ? ' closing' : ''}`}
data-key={notification.key}
data-duration={notification.duration}
>
<Icon
name="close"
className="notification__close-button"
onClick={() => this.props.uiActions.removeNotification(notification.key, true)}
/>
{notification.title && (
<h4 className="notification__title">
{notification.title}
</h4>
)}
{notification.content && (
<div className="notification__content">
{notification.content}
</div>
)}
{notification.description && (
<div className="notification__description">
{notification.description}
</div>
)}
{notification.links && (
<div className="notification__actions">
{
notification.links.map((link, i) => (
<Button
type="secondary"
className="notification__actions__item"
href={link.url}
target={link.new_window ? '_blank' : 'self'}
key={i}
tracking={{ category: 'NotificationLink', action: 'Click', label: link.text }}
>
{link.text}
</Button>
))
}
</div>
)}
</div>
</div>
);
switch (status) {
case 'running':
return (
<div className="notification__wrapper">
<div
className={
`notification notification--${level} notification--process${closing ? ' closing' : ''}`
}
})
}
</ErrorBoundary>
);
}
renderProcess(process) {
const {
total,
remaining,
level = 'info',
content,
description,
status,
closing,
key,
} = process;
const { uiActions } = this.props;
let progress = 0;
if (total && remaining) {
progress = ((total - remaining) / total).toFixed(4);
}
switch (status) {
case 'running':
return (
<div className="notification__wrapper" key={key}>
<div
className={
`notification notification--${level} notification--process${closing ? ' closing' : ''}`
}
>
<Loader
progress={progress}
loading
mini
white
/>
{content && content !== '' && <div className="notification__content">{content}</div>}
{description && description !== '' && <div className="notification__description">{description}</div>}
<Icon name="close" className="notification__close-button" onClick={() => { uiActions.cancelProcess(key); }} />
</div>
>
<Loader
progress={progress}
loading
mini
white
/>
{content && content !== '' && <div className="notification__content">{content}</div>}
{description && description !== '' && <div className="notification__description">{description}</div>}
<Icon name="close" className="notification__close-button" onClick={cancelProcess} />
</div>
);
case 'finished':
return (
<div className="notification__wrapper" key={key}>
<div
className={
`notification notification--${level} notification--process${closing ? ' closing' : ''}`
}
>
<Icon className="notification__icon" name={level === 'error' ? 'close' : 'check'} />
{content && content !== '' && <div className="notification__content">{content}</div>}
{description && description !== '' && <div className="notification__description">{description}</div>}
<Icon name="close" className="notification__close-button" onClick={() => { uiActions.closeProcess(key); }} />
</div>
</div>
);
case 'finished':
return (
<div className="notification__wrapper">
<div
className={
`notification notification--${level} notification--process${closing ? ' closing' : ''}`
}
>
<Icon className="notification__icon" name={level === 'error' ? 'close' : 'check'} />
{content && content !== '' && <div className="notification__content">{content}</div>}
{description && description !== '' && <div className="notification__description">{description}</div>}
<Icon name="close" className="notification__close-button" onClick={closeProcess} />
</div>
);
case 'cancelling':
return (
<div className="notification__wrapper" key={key}>
<div
className={
`notification notification--${level} notification--process cancelling${closing ? ' closing' : ''}`
}
>
<Loader
progress={progress}
loading
mini
white
/>
{content && content !== '' && <div className="notification__content">{content}</div>}
{description && description !== '' && <div className="notification__description">{description}</div>}
<Icon name="close" className="notification__close-button" />
</div>
</div>
);
case 'cancelling':
return (
<div className="notification__wrapper">
<div
className={
`notification notification--${level} notification--process cancelling${closing ? ' closing' : ''}`
}
>
<Loader
progress={progress}
loading
mini
white
/>
{content && content !== '' && <div className="notification__content">{content}</div>}
{description && description !== '' && <div className="notification__description">{description}</div>}
<Icon name="close" className="notification__close-button" />
</div>
);
case 'cancelled':
case 'completed':
default:
return null;
}
</div>
);
default:
return null;
}
};
renderProcesses = () => {
const { processes: processesObj } = this.props;
const processes = indexToArray(processesObj).filter((process) => process.notification);
if (!processes) return null;
const ProcessItems = () => {
const dispatch = useDispatch();
const index = useSelector((state) => state.ui.processes);
const items = indexToArray(index).filter(
(p) => p.notification && p.status !== 'cancelled' && p.status !== 'completed',
);
if (!items || !items.length) return null;
return (
<>
{processes.map((process) => this.renderProcess(process))}
</>
);
}
const {
cancelProcess,
closeProcess,
} = uiActions;
render() {
return (
<div className="notifications">
{this.renderNotifications()}
{this.renderProcesses()}
</div>
);
}
}
return (
<>
{items.map((item) => (
<Process
key={`${item.key}_${item.status}`}
item={item}
closeProcess={() => dispatch(closeProcess(item.key))}
cancelProcess={() => dispatch(cancelProcess(item.key))}
/>
))}
</>
);
};
const mapStateToProps = (state) => ({
broadcasts: (state.ui.broadcasts ? state.ui.broadcasts : []),
notifications: (state.ui.notifications ? state.ui.notifications : []),
processes: (state.ui.processes ? state.ui.processes : {}),
});
const Notifications = () => {
return (
<div className="notifications">
<NotificationItems />
<ProcessItems />
</div>
);
};
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
geniusActions: bindActionCreators(geniusActions, dispatch),
lastfmActions: bindActionCreators(lastfmActions, dispatch),
snapcastActions: bindActionCreators(snapcastActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Notifications);
export default Notifications;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import Link from './Link';
import ProgressSlider from './Fields/ProgressSlider';
import VolumeControl from './Fields/VolumeControl';
@ -12,367 +11,303 @@ import LinksSentence from './LinksSentence';
import Thumbnail from './Thumbnail';
import Icon from './Icon';
import { scrollTo } from '../util/helpers';
import * as uiActions from '../services/ui/actions';
import * as coreActions from '../services/core/actions';
import { showContextMenu, toggleSidebar } from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import { I18n } from '../locale';
import { makeItemSelector } from '../util/selectors';
class PlaybackControls extends React.Component {
constructor(props) {
super(props);
this.stream = null;
this.state = {
expanded: false,
current_track: null,
stream_title: null,
transition_track: null,
transition_direction: null,
};
}
const PlaybackControls = () => {
const [expanded, setExpanded] = useState();
const [transitionTrack, setTransitionTrack] = useState();
const [transitionDirection, setTransitionDirection] = useState();
const [touchMeta, setTouchMeta] = useState({});
static getDerivedStateFromProps({ current_track, stream_title }, state) {
return {
...state,
current_track,
stream_title,
};
}
const history = useHistory();
const dispatch = useDispatch();
const touch_enabled = useSelector((state) => state.ui.touch_enabled);
const sidebar_open = useSelector((state) => state.ui.sidebar_open);
const volume = useSelector((state) => state.mopidy.volume);
const mute = useSelector((state) => state.mopidy.mute);
const time_position = useSelector((state) => state.mopidy.time_position);
const play_state = useSelector((state) => state.mopidy.play_state);
const consume = useSelector((state) => state.mopidy.consume);
const random = useSelector((state) => state.mopidy.random);
const repeat = useSelector((state) => state.mopidy.repeat);
const streamTitle = useSelector((state) => state.core.streamTitle);
const currentTrackUri = useSelector((state) => state.core.current_track?.uri);
const currentTrack = useSelector(makeItemSelector(currentTrackUri));
const nextTrackUri = useSelector((state) => state.core.next_track_uri);
const nextTrack = useSelector(makeItemSelector(nextTrackUri));
setTransition(direction) {
this.setState({
transition_track: this.state.current_track,
transition_direction: direction,
});
const setTransition = (direction) => {
setTransitionTrack(currentTrack);
setTransitionDirection(direction);
// Allow time for the animation to complete, then remove
// the transitioning track from state
setTimeout(() => {
this.setState({
transition_track: null,
});
setTransitionTrack(null);
},
250);
}
handleContextMenu = (e) => {
const {
current_track,
} = this.state;
const {
uiActions: {
showContextMenu,
},
} = this.props;
};
const handleContextMenu = (e) => {
e.preventDefault();
showContextMenu({
e,
context: 'current-track',
items: [current_track],
uris: [current_track.uri],
});
}
dispatch(
showContextMenu({
e,
context: 'current-track',
items: [currentTrack],
uris: [currentTrack.uri],
}),
);
};
handleTouchStart = (e) => {
const { touch_enabled } = this.props;
const handleTouchStart = (e) => {
if (!touch_enabled) return;
const timestamp = Math.floor(Date.now());
// Save touch start details
this.start_time = timestamp;
this.start_position = {
x: e.touches[0].clientX,
};
setTouchMeta({
start_time: timestamp,
start_position: {
x: e.touches[0].clientX,
},
});
return false;
}
};
handleTouchEnd = (e) => {
const { touch_enabled } = this.props;
const handleTouchEnd = (e) => {
if (!touch_enabled) return;
const timestamp = Math.floor(Date.now());
const tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
const tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
const tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
const tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
const end_position = {
x: e.changedTouches[0].clientX,
};
// Too long between touchstart and touchend
if (this.start_time + tap_time_threshold < timestamp) {
if (touchMeta.start_time + tap_time_threshold < timestamp) {
return false;
}
// Make sure there's enough distance between start and end before we handle
// this event as a 'tap'
if (this.start_position.x + tap_distance_threshold > end_position.x
&& this.start_position.x - tap_distance_threshold < end_position.x) {
if (
touchMeta.start_position.x + tap_distance_threshold > end_position.x
&& touchMeta.start_position.x - tap_distance_threshold < end_position.x
) {
// Scroll to top (without smooth_scroll)
scrollTo(null, false);
this.props.history.push('/queue');
history.push('/queue');
} else {
// Swipe to the left = previous track
if (this.start_position.x < end_position.x) {
this.setTransition('previous');
this.props.mopidyActions.previous();
if (touchMeta.start_position.x < end_position.x) {
setTransition('previous');
dispatch(mopidyActions.previous());
// Swipe to the right = skip track
} else if (this.start_position.x > end_position.x) {
this.setTransition('next');
this.props.mopidyActions.next();
} else if (touchMeta.start_position.x > end_position.x) {
setTransition('next');
dispatch(mopidyActions.next());
}
}
this.end_time = timestamp;
setTouchMeta((prev) => ({ ...prev, end_time: timestamp }));
e.preventDefault();
}
};
renderPlayButton() {
let button = <button className="control play" onClick={() => this.props.mopidyActions.play()}><Icon name="play_circle_filled" type="material" /></button>;
if (this.props.play_state == 'playing') {
button = <button className="control play" onClick={() => this.props.mopidyActions.pause()}><Icon name="pause_circle_filled" type="material" /></button>;
}
return button;
}
return (
<div className={`playback-controls${expanded ? ' playback-controls--expanded' : ''}${touch_enabled ? ' playback-controls--touch-enabled' : ''}`}>
renderConsumeButton() {
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}>
<Icon name="restaurant" type="material" />
<span className="tooltip__content">
<I18n path="playback_controls.consume" />
</span>
</button>
);
if (this.props.consume) {
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}>
<div className="playback-controls__background" />
{nextTrack && nextTrack.images ? <Thumbnail className="hide" size="large" images={nextTrack.images} /> : null}
<div
className="current-track__wrapper"
transition={transitionTrack}
direction={transitionDirection}
>
{transitionTrack && transitionDirection && (
<div className="current-track current-track__outgoing">
<div className="text">
<div className="title">
{transitionTrack.name}
</div>
<div className="artist">
<LinksSentence items={transitionTrack.artists} type="artist" nolinks />
</div>
</div>
</div>
)}
{currentTrack && (!transitionTrack || transitionTrack.tlid !== currentTrack.tlid) ? (
<div
className="current-track current-track__incoming"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onContextMenu={handleContextMenu}
tabIndex="-1"
key={currentTrack.tlid}
>
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
<Thumbnail size="small" images={currentTrack.images} type="track" />
</Link>
<div className="text">
<div className="title">
{streamTitle && <span>{streamTitle}</span>}
{!streamTitle && currentTrack && <span>{currentTrack.name}</span>}
{!streamTitle && !currentTrack && <span>-</span>}
</div>
<div className="artist">
{
(currentTrack && currentTrack.artists
&& <LinksSentence items={currentTrack.artists} type="artist" />)
|| (streamTitle && <span className="links-sentence">{streamTitle}</span>)
|| <LinksSentence />
}
</div>
</div>
</div>
) : (
<div
className="current-track"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
tabIndex="-1"
>
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
<Thumbnail size="small" type="track" />
</Link>
<div className="text">
<div className="title">&nbsp;</div>
<div className="artist">&nbsp;</div>
</div>
</div>
)}
</div>
<section className="playback">
<button
type="button"
className="control previous"
onClick={() => dispatch(mopidyActions.previous())}
>
<Icon name="navigate_before" type="material" />
</button>
{
play_state === 'playing' ? (
<button
type="button"
className="control play"
onClick={() => dispatch(mopidyActions.pause())}
>
<Icon name="pause_circle_filled" type="material" />
</button>
) : (
<button
type="button"
className="control play"
onClick={() => dispatch(mopidyActions.play())}
>
<Icon name="play_circle_filled" type="material" />
</button>
)
}
<button
type="button"
className="control next"
onClick={() => dispatch(mopidyActions.next())}
>
<Icon name="navigate_next" type="material" />
</button>
</section>
<section className="progress">
<div className="time time--current">
{time_position ? <Dater type="length" data={time_position} /> : '-'}
</div>
<ProgressSlider />
<div className="time time--total">
{currentTrack ? <Dater type="length" data={currentTrack.duration} /> : '-'}
</div>
</section>
<section className="settings">
<button
type="button"
className={`control${consume ? ' control--active' : ''} tooltip`}
onClick={() => dispatch(mopidyActions.setConsume(!consume))}
>
<Icon name="restaurant" type="material" />
<span className="tooltip__content">
<I18n path="playback_controls.consume" />
</span>
</button>
);
}
return button;
}
renderRandomButton() {
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}>
<Icon name="shuffle" type="material" />
<span className="tooltip__content">
<I18n path="playback_controls.shuffle" />
</span>
</button>
);
if (this.props.random) {
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}>
<button
type="button"
className={`control${random ? ' control--active' : ''} tooltip`}
onClick={() => dispatch(mopidyActions.setRandom(!random))}
>
<Icon name="shuffle" type="material" />
<span className="tooltip__content">
<I18n path="playback_controls.shuffle" />
</span>
</button>
);
}
return button;
}
renderRepeatButton() {
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}>
<Icon name="repeat" />
<span className="tooltip__content">
<I18n path="playback_controls.repeat" />
</span>
</button>
);
if (this.props.repeat) {
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}>
<Icon name="repeat" />
<button
type="button"
className={`control${repeat ? ' control--active' : ''} tooltip`}
onClick={() => dispatch(mopidyActions.setRepeat(!repeat))}
>
<Icon name="repeat" type="material" />
<span className="tooltip__content">
<I18n path="playback_controls.repeat" />
</span>
</button>
);
}
return button;
}
<OutputControl force_expanded={expanded} />
</section>
render() {
const {
next_track,
touch_enabled,
time_position,
} = this.props;
const {
current_track,
expanded,
stream_title,
transition_track,
transition_direction,
} = this.state;
<section className="volume">
<MuteControl
mute={mute}
onMuteChange={(value) => dispatch(mopidyActions.setMute(value))}
/>
<VolumeControl
scrollWheel
volume={volume}
mute={mute}
onVolumeChange={(value) => dispatch(mopidyActions.setVolume(value))}
/>
</section>
return (
<div className={`playback-controls${expanded ? ' playback-controls--expanded' : ''}${touch_enabled ? ' playback-controls--touch-enabled' : ''}`}>
<div className="playback-controls__background" />
{next_track && next_track.images ? <Thumbnail className="hide" size="large" images={next_track.images} /> : null}
<div
className="current-track__wrapper"
transition={transition_track}
direction={transition_direction}
<section className="triggers">
<button
type="button"
className="control expanded-controls"
onClick={() => setExpanded(!expanded)}
>
{transition_track && transition_direction && (
<div className="current-track current-track__outgoing">
<div className="text">
<div className="title">
{transition_track.name}
</div>
<div className="artist">
<LinksSentence items={transition_track.artists} type="artist" nolinks />
</div>
</div>
</div>
)}
{current_track && (!transition_track || transition_track.tlid !== current_track.tlid) ? (
<div
className="current-track current-track__incoming"
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
onContextMenu={this.handleContextMenu}
tabIndex="-1"
key={current_track.tlid}
>
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
<Thumbnail size="small" images={current_track.images} type="track" />
</Link>
<div className="text">
<div className="title">
{stream_title && <span>{stream_title}</span>}
{!stream_title && current_track && <span>{current_track.name}</span>}
{!stream_title && !current_track && <span>-</span>}
</div>
<div className="artist">
{
(current_track && current_track.artists
&& <LinksSentence items={current_track.artists} type="artist" />)
|| (stream_title && <span className="links-sentence">{stream_title}</span>)
|| <LinksSentence />
}
</div>
</div>
</div>
{expanded ? (
<Icon name="expand_more" type="material" />
) : (
<div
className="current-track"
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
tabIndex="-1"
>
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
<Thumbnail size="small" type="track" />
</Link>
<div className="text">
<div className="title">&nbsp;</div>
<div className="artist">&nbsp;</div>
</div>
</div>
<Icon name="expand_less" type="material" />
)}
</div>
</button>
<button
type="button"
className={`control sidebar-toggle${sidebar_open ? ' open' : ''}`}
onClick={() => dispatch(toggleSidebar())}
>
<Icon className="open" name="menu" type="material" />
</button>
</section>
<section className="playback">
<button className="control previous" onClick={() => this.props.mopidyActions.previous()}>
<Icon name="navigate_before" type="material" />
</button>
{ this.renderPlayButton() }
<button className="control next" onClick={() => this.props.mopidyActions.next()}>
<Icon name="navigate_next" type="material" />
</button>
</section>
<section className="progress">
<div className="time time--current">
{time_position ? <Dater type="length" data={time_position} /> : '-'}
</div>
<ProgressSlider />
<div className="time time--total">
{current_track ? <Dater type="length" data={current_track.duration} /> : '-'}
</div>
</section>
<section className="settings">
{this.renderConsumeButton()}
{this.renderRandomButton()}
{this.renderRepeatButton()}
<OutputControl force_expanded={this.state.expanded} />
</section>
<section className="volume">
<MuteControl
mute={this.props.mute}
onMuteChange={(mute) => this.props.mopidyActions.setMute(mute)}
/>
<VolumeControl
scrollWheel
volume={this.props.volume}
mute={this.props.mute}
onVolumeChange={(percent) => this.props.mopidyActions.setVolume(percent)}
/>
</section>
<section className="triggers">
<button className="control expanded-controls" onClick={(e) => this.setState({ expanded: !this.state.expanded })}>
{this.state.expanded ? <Icon name="expand_more" type="material" /> : <Icon name="expand_less" type="material" />}
</button>
<button className={`control sidebar-toggle${this.props.sidebar_open ? ' open' : ''}`} onClick={(e) => this.props.uiActions.toggleSidebar()}>
<Icon className="open" name="menu" type="material" />
</button>
</section>
</div>
);
}
</div>
);
}
const mapStateToProps = (state) => {
const {
items,
next_track_uri,
current_track,
stream_title,
} = state.core;
return {
snapcast_enabled: state.pusher.config.snapcast_enabled,
current_track: current_track ? items[current_track.uri] || current_track : null,
stream_title,
next_track: items[next_track_uri],
radio_enabled: (!!(state.ui.radio && state.ui.radio.enabled)),
play_state: state.mopidy.play_state,
time_position: state.mopidy.time_position,
consume: state.mopidy.consume,
repeat: state.mopidy.repeat,
random: state.mopidy.random,
volume: state.mopidy.volume,
mute: state.mopidy.mute,
sidebar_open: state.ui.sidebar_open,
slim_mode: state.ui.slim_mode,
touch_enabled: state.ui.playback_controls_touch_enabled,
};
};
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls);
export default PlaybackControls;

View File

@ -1,17 +1,23 @@
export default (props) => {
window.addEventListener('resize', handleWindowResize, false);
import { useSelector, useDispatch } from 'react-redux';
import { setSlimMode } from '../services/ui/actions';
const handleWindowResize = (e) => {
export default () => {
const slim_mode = useSelector((state) => state.ui.slim_mode);
const dispatch = useDispatch();
const handleWindowResize = () => {
const width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
if (width <= 800) {
if (!props.slim_mode) {
props.uiActions.setSlimMode(true);
if (!slim_mode) {
dispatch(setSlimMode(true));
}
} else if (props.slim_mode) {
props.uiActions.setSlimMode(false);
} else if (slim_mode) {
dispatch(setSlimMode(false));
}
};
window.addEventListener('resize', handleWindowResize, false);
return null;
};

View File

@ -1,195 +1,157 @@
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter } from 'react-router';
import { useSelector } from 'react-redux';
import Link from './Link';
import Icon from './Icon';
import Dropzones from './Fields/Dropzones';
import PinList from './Fields/PinList';
import DropdownField from './Fields/DropdownField';
import Button from './Button';
import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import { I18n, i18n } from '../locale';
import { toggleSidebar } from '../services/ui/actions';
class Sidebar extends React.Component {
closeSidebar = () => {
const {
uiActions: {
toggleSidebar,
},
} = this.props;
toggleSidebar(false);
}
renderStatusIcon() {
const {
update_available,
mopidy_connected,
pusher_connected,
snapcast_connected,
snapcast_enabled,
} = this.props;
if (update_available) {
return (
<span className="status tooltip tooltip--right">
<Icon name="cloud_download" className="green-text" />
<span className="tooltip__content">
<I18n path="sidebar.update_available" />
</span>
</span>
);
}
if (!navigator.onLine) {
return (
<span className="status tooltip tooltip--right">
<Icon name="wifi_off" className="red-text" />
<span className="tooltip__content">
<I18n path="sidebar.browser_offline" />
</span>
</span>
);
}
if (!mopidy_connected || !pusher_connected || (!snapcast_connected && snapcast_enabled)) {
return (
<span className="status tooltip tooltip--right">
<Icon name="warning" className="red-text" />
<span className="tooltip__content">
{!mopidy_connected && (
<I18n path="sidebar.not_connected" name={i18n('services.mopidy.title')} contentAfter>
<br />
</I18n>
)}
{!pusher_connected && (
<I18n path="sidebar.not_connected" name={i18n('services.pusher.title')} contentAfter>
<br />
</I18n>
)}
{!snapcast_connected && snapcast_enabled && (
<I18n path="sidebar.not_connected" name={i18n('services.snapcast.title')} contentAfter>
<br />
</I18n>
)}
</span>
</span>
);
}
return null;
}
render = () => {
const {
history,
spotify_available,
} = this.props;
const StatusIcon = () => {
const update_available = useSelector((state) => state.pusher?.version?.update_available);
const mopidy_connected = useSelector((state) => state.mopidy.connected);
const pusher_connected = useSelector((state) => state.pusher.connected);
const snapcast_connected = useSelector((state) => state.snapcast.connected);
const snapcast_enabled = useSelector((state) => state.snapcast.enabled);
if (update_available) {
return (
<aside className="sidebar">
<div className="sidebar__liner">
<nav className="sidebar__menu">
<section className="sidebar__menu__section">
<Link to="/queue" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="play_arrow" type="material" />
<I18n path="sidebar.now_playing" />
</Link>
<Link to="/search" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="search" type="material" />
<I18n path="sidebar.search" />
</Link>
</section>
{spotify_available && (
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">
<I18n path="sidebar.discover" />
</title>
<Link to="/discover/recommendations" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="explore" type="material" />
<I18n path="sidebar.discover" />
</Link>
<Link to="/discover/categories" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="mood" type="material" />
<I18n path="sidebar.genre" />
</Link>
<Link to="/discover/featured" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="star" type="material" />
<I18n path="sidebar.featured_playlists" />
</Link>
<Link to="/discover/new-releases" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="new_releases" type="material" />
<I18n path="sidebar.new_releases" />
</Link>
</section>
)}
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">
<I18n path="sidebar.my_music" />
</title>
<Link to="/library/playlists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="queue_music" type="material" />
<I18n path="sidebar.playlists" />
</Link>
<PinList />
<Link to="/library/artists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="recent_actors" type="material" />
<I18n path="sidebar.artists" />
</Link>
<Link to="/library/albums" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="album" type="material" />
<I18n path="sidebar.albums" />
</Link>
<Link to="/library/tracks" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="music_note" type="material" />
<I18n path="sidebar.tracks" />
</Link>
<Link to="/library/browse" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="folder" type="material" />
<I18n path="sidebar.browse" />
</Link>
</section>
<section className="sidebar__menu__section">
<Link to="/settings" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="settings" type="material" />
<I18n path="sidebar.settings" />
{this.renderStatusIcon()}
</Link>
</section>
</nav>
</div>
<Dropzones />
<div className="close" onClick={this.closeSidebar}>
<Icon name="close" />
</div>
</aside>
<span className="status tooltip tooltip--right">
<Icon name="cloud_download" className="green-text" />
<span className="tooltip__content">
<I18n path="sidebar.update_available" />
</span>
</span>
);
}
if (!navigator.onLine) {
return (
<span className="status tooltip tooltip--right">
<Icon name="wifi_off" className="red-text" />
<span className="tooltip__content">
<I18n path="sidebar.browser_offline" />
</span>
</span>
);
}
if (!mopidy_connected || !pusher_connected || (!snapcast_connected && snapcast_enabled)) {
return (
<span className="status tooltip tooltip--right">
<Icon name="warning" className="red-text" />
<span className="tooltip__content">
{!mopidy_connected && (
<I18n path="sidebar.not_connected" name={i18n('services.mopidy.title')} contentAfter>
<br />
</I18n>
)}
{!pusher_connected && (
<I18n path="sidebar.not_connected" name={i18n('services.pusher.title')} contentAfter>
<br />
</I18n>
)}
{!snapcast_connected && snapcast_enabled && (
<I18n path="sidebar.not_connected" name={i18n('services.snapcast.title')} contentAfter>
<br />
</I18n>
)}
</span>
</span>
);
}
return null;
}
const mapStateToProps = (state) => ({
mopidy_connected: state.mopidy.connected,
pusher_connected: state.pusher.connected,
spotify_available: state.spotify.access_token,
spotify_authorized: state.spotify.authorization,
snapcast_connected: state.snapcast.connected,
snapcast_enabled: state.snapcast.enabled,
update_available: (state.pusher.version && state.pusher.version.update_available ? state.pusher.version.update_available : false),
test_mode: (state.ui.test_mode ? state.ui.test_mode : false),
dragger: state.ui.dragger,
});
const Sidebar = () => {
const spotify_available = useSelector((state) => state.spotify.access_token);
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
return (
<aside className="sidebar">
<div className="sidebar__liner">
<nav className="sidebar__menu">
<section className="sidebar__menu__section">
<Link to="/queue" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="play_arrow" type="material" />
<I18n path="sidebar.now_playing" />
</Link>
<Link to="/search" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="search" type="material" />
<I18n path="sidebar.search" />
</Link>
</section>
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Sidebar));
{spotify_available && (
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">
<I18n path="sidebar.discover" />
</title>
<Link to="/discover/recommendations" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="explore" type="material" />
<I18n path="sidebar.discover" />
</Link>
<Link to="/discover/categories" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="mood" type="material" />
<I18n path="sidebar.genre" />
</Link>
<Link to="/discover/featured" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="star" type="material" />
<I18n path="sidebar.featured_playlists" />
</Link>
<Link to="/discover/new-releases" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="new_releases" type="material" />
<I18n path="sidebar.new_releases" />
</Link>
</section>
)}
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">
<I18n path="sidebar.my_music" />
</title>
<Link to="/library/playlists" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="queue_music" type="material" />
<I18n path="sidebar.playlists" />
</Link>
<PinList />
<Link to="/library/artists" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="recent_actors" type="material" />
<I18n path="sidebar.artists" />
</Link>
<Link to="/library/albums" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="album" type="material" />
<I18n path="sidebar.albums" />
</Link>
<Link to="/library/tracks" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="music_note" type="material" />
<I18n path="sidebar.tracks" />
</Link>
<Link to="/library/browse" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="folder" type="material" />
<I18n path="sidebar.browse" />
</Link>
</section>
<section className="sidebar__menu__section">
<Link to="/settings" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="settings" type="material" />
<I18n path="sidebar.settings" />
<StatusIcon />
</Link>
</section>
</nav>
</div>
<Dropzones />
<div className="close" onClick={() => dispatch(toggleSidebar(false))}>
<Icon name="close" />
</div>
</aside>
);
};
export default Sidebar;