Upgrading to more functional components
This commit is contained in:
190
src/js/App.js
190
src/js/App.js
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"> </div>
|
||||
<div className="artist"> </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"> </div>
|
||||
<div className="artist"> </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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user