Upgrading redux; Refactoring tabs; Adding servers index

This commit is contained in:
James Barnsley
2020-04-26 22:04:39 +12:00
parent b95c0fe3cb
commit 92292b18d7
20 changed files with 8852 additions and 4806 deletions

View File

@ -7,7 +7,7 @@ export default class TextField extends React.Component {
this.state = {
in_focus: false,
value: (this.props.value ? this.props.value : ''),
value: props.value || '',
};
}
@ -20,31 +20,40 @@ export default class TextField extends React.Component {
return null;
}
handleChange(e) {
handleChange = (e) => {
this.setState({ value: e.target.value });
}
handleFocus(e) {
handleFocus = () => {
this.setState({ in_focus: true });
}
handleBlur(e) {
handleBlur = () => {
const { value, onChange } = this.props;
const { value: stateValue } = this.state;
this.setState({ in_focus: false });
if (this.state.value !== this.props.value) {
this.props.onChange(this.state.value);
if (stateValue !== value) {
onChange(stateValue);
}
}
render() {
render = () => {
const {
className,
type = 'text',
placeholder,
} = this.props;
const { value } = this.state;
return (
<input
className={this.props.className ? this.props.className : ''}
type={this.props.type ? this.props.type : 'text'}
onChange={(e) => this.handleChange(e)}
onFocus={(e) => this.handleFocus(e)}
onBlur={(e) => this.handleBlur(e)}
value={this.state.value}
placeholder={this.props.placeholder ? this.props.placeholder : null}
className={className}
type={type}
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
value={value}
placeholder={placeholder}
/>
);
}

View File

@ -66,49 +66,49 @@ class Hotkeys extends React.Component {
prevent = true;
break;
case 'escape':
if (dragging) {
uiActions.dragEnd();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
} else if (modal) {
window.history.back();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
case 's': // Stop
mopidyActions.stop();
uiActions.createNotification({ content: 'stop', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Stop' });
break;
case 'b': // Seek backwards 30 sec
var new_position = play_time_position - 30000;
if (new_position < 0) {
new_position = 0;
}
mopidyActions.setTimePosition(new_position);
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
break;
case 's':
history.push('/search');
case 'f': // Seek forwards 30 sec
mopidyActions.setTimePosition(play_time_position + 30000);
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
break;
case 'q':
history.push('/queue');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
break;
case 'k':
history.push('/kiosk-mode');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
break;
case ',':
window.history.back();
case '<': // Previous track
mopidyActions.previous();
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Back' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
break;
case '.':
window.history.forward();
case '>': // Next track
mopidyActions.next();
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Forward' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
break;
case '=':
case '+': // Volume up
if (volume !== 'false') {
volume += 5;
if (volume > 100) {
@ -124,7 +124,7 @@ class Hotkeys extends React.Component {
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume up' });
break;
case '-':
case '-': // Volume down
if (volume !== 'false') {
volume -= 5;
if (volume < 0) {
@ -140,7 +140,7 @@ class Hotkeys extends React.Component {
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume down' });
break;
case '0':
case '0': // Mute
if (mute) {
mopidyActions.setMute(false);
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
@ -153,36 +153,34 @@ class Hotkeys extends React.Component {
prevent = true;
break;
case ';':
var new_position = play_time_position - 30000;
if (new_position < 0) {
new_position = 0;
case 'escape': // Cancel current action/context
if (dragging) {
uiActions.dragEnd();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
} else if (modal) {
window.history.back();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
}
mopidyActions.setTimePosition(new_position);
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
break;
case "'":
mopidyActions.setTimePosition(play_time_position + 30000);
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
case '1': // Navigation: Queue
history.push('/queue');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
break;
case '[':
mopidyActions.previous();
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
case '2': // Navigation: Search
history.push('/search');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
break;
case ']':
mopidyActions.next();
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
case '3': // Navigation: Kiosk mode
history.push('/kiosk-mode');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
break;
default:

95
src/js/components/Servers.js Executable file
View File

@ -0,0 +1,95 @@
import React, { memo, useCallback } from 'react';
import { useStore, useDispatch } from 'react-redux'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from './Link';
import Icon from './Icon';
import URILink from './URILink';
import TextField from './Fields/TextField';
import { indexToArray } from '../util/arrays';
import * as uiActions from '../services/ui/actions';
import * as coreActions from '../services/core/actions';
import * as mopidyActions from '../services/mopidy/actions';
const Servers = ({
match: { params: { id: serverId } },
history,
}) => {
const store = useStore();
const dispatch = useDispatch();
const { mopidy: { servers } } = store.getState();
const renderMenu = () => (
<div className="sub-tabs__menu" id="servers-menu">
<div className="menu__inner">
{indexToArray(servers).map((server) => (
<Link
history={history}
className="menu-item"
activeClassName="menu-item--active"
to={`/settings/servers/${server.id}`}
scrollTo="#servers-menu"
key={server.id}
>
<div className="menu-item__inner">
<div className="menu-item__title">
{server.name}
</div>
</div>
</Link>
))}
</div>
</div>
);
const renderServer = () => {
if (!serverId) return null;
const server = servers[serverId];
if (!server) return null;
return (
<div className="sub-tabs__content">
<label className="field">
<div className="name">Name</div>
<div className="input">
<TextField
type="text"
value={server.name}
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, name: value }))}
/>
</div>
</label>
<label className="field">
<div className="name">Host</div>
<div className="input">
<TextField
value={server.host}
onChange={value => updateServer({ id: server.id, host: value })}
/>
</div>
</label>
<label className="field">
<div className="name">Port</div>
<div className="input">
<TextField
type="text"
value={server.port}
onChange={value => updateServer({ id: server.id, port: value })}
/>
</div>
</label>
</div>
);
};
return (
<div className="sub-tabs sub-tabs--servers">
{renderMenu()}
{renderServer()}
</div>
);
};
export default Servers;

View File

@ -7,6 +7,7 @@ import Link from './Link';
import Thumbnail from './Thumbnail';
import Icon from './Icon';
import URILink from './URILink';
import TextField from './Fields/TextField';
import SpotifyAuthenticationFrame from './Fields/SpotifyAuthenticationFrame';
import LastfmAuthenticationFrame from './Fields/LastfmAuthenticationFrame';
import GeniusAuthenticationFrame from './Fields/GeniusAuthenticationFrame';
@ -109,11 +110,8 @@ class Services extends React.Component {
<label className="field">
<div className="name">Country</div>
<div className="input">
<input
type="text"
onChange={(e) => this.setState({ country: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'country' })}
onBlur={(e) => this.props.spotifyActions.set({ country: e.target.value })}
<TextField
onChange={value => this.props.spotifyActions.set({ country: value })}
value={this.state.country}
/>
<div className="description">
@ -322,9 +320,9 @@ class Services extends React.Component {
}
return (
<div className="menu" id="services-menu">
<div className="sub-tabs__menu menu" id="services-menu">
<div className="menu__inner">
<Link history={this.props.history} className="menu-item menu-item--spotify" activeClassName="menu-item--active" to="/settings/spotify" scrollTo="#services-menu">
<Link history={this.props.history} className="menu-item menu-item--spotify" activeClassName="menu-item--active" to="/settings/services/spotify" scrollTo="#services-menu">
<div className="menu-item__inner">
{spotify_icon}
<div className="menu-item__title">
@ -333,7 +331,7 @@ class Services extends React.Component {
{this.props.spotify.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
</div>
</Link>
<Link history={this.props.history} className="menu-item menu-item--lastfm" activeClassName="menu-item--active" to="/settings/lastfm" scrollTo="#services-menu">
<Link history={this.props.history} className="menu-item menu-item--lastfm" activeClassName="menu-item--active" to="/settings/services/lastfm" scrollTo="#services-menu">
<div className="menu-item__inner">
{lastfm_icon}
<div className="menu-item__title">
@ -342,7 +340,7 @@ class Services extends React.Component {
{this.props.lastfm.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
</div>
</Link>
<Link history={this.props.history} className="menu-item menu-item--genius" activeClassName="menu-item--active" to="/settings/genius" scrollTo="#services-menu">
<Link history={this.props.history} className="menu-item menu-item--genius" activeClassName="menu-item--active" to="/settings/services/genius" scrollTo="#services-menu">
<div className="menu-item__inner">
{genius_icon}
<div className="menu-item__title">
@ -351,7 +349,7 @@ class Services extends React.Component {
{this.props.genius.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Unauthorized</span>}
</div>
</Link>
<Link history={this.props.history} className="menu-item menu-item--snapcast" activeClassName="menu-item--active" to="/settings/snapcast" scrollTo="#services-menu">
<Link history={this.props.history} className="menu-item menu-item--snapcast" activeClassName="menu-item--active" to="/settings/services/snapcast" scrollTo="#services-menu">
<div className="menu-item__inner">
<Icon className="menu-item__icon" name="devices" />
<div className="menu-item__title">
@ -368,7 +366,7 @@ class Services extends React.Component {
)}
</div>
</Link>
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/icecast" scrollTo="#services-menu">
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/services/icecast" scrollTo="#services-menu">
<div className="menu-item__inner">
<Icon className="menu-item__icon" name="wifi_tethering" />
<div className="menu-item__title">
@ -386,15 +384,15 @@ class Services extends React.Component {
const { match } = this.props;
switch (match.params.service) {
case 'spotify':
return <div className="service">{this.renderSpotify()}</div>;
return <div className="sub-tabs__content">{this.renderSpotify()}</div>;
case 'lastfm':
return <div className="service">{this.renderLastfm()}</div>;
return <div className="sub-tabs__content">{this.renderLastfm()}</div>;
case 'genius':
return <div className="service">{this.renderGenius()}</div>;
return <div className="sub-tabs__content">{this.renderGenius()}</div>;
case 'icecast':
return <div className="service">{this.renderIcecast()}</div>;
return <div className="sub-tabs__content">{this.renderIcecast()}</div>;
case 'snapcast':
return <div className="service">{<Snapcast match={this.props.match} />}</div>;
return <div className="sub-tabs__content">{<Snapcast match={this.props.match} />}</div>;
default:
return null;
}
@ -402,7 +400,7 @@ class Services extends React.Component {
render() {
return (
<div className="services">
<div className="sub-tabs sub-tabs--services">
{this.renderMenu()}
{this.renderService()}
</div>

View File

@ -137,7 +137,7 @@ const SnapcastGroups = (props) => {
activeClassName="menu-item--active"
key={group.id}
history={history}
to={`/settings/snapcast/${group.id}`}
to={`/settings/services/snapcast/${group.id}`}
scrollTo="#services-snapcast-groups"
>
<div className="snapcast__groups__menu-item__inner menu-item__inner">
@ -154,8 +154,8 @@ const SnapcastGroups = (props) => {
}
return (
<div className="snapcast__groups" id="services-snapcast-groups">
<div className="snapcast__groups__menu menu">
<div className="sub-tabs snapcast__groups" id="services-snapcast-groups">
<div className="sub-tabs__menu snapcast__groups__menu menu">
<div className="menu__inner">
{sortItems(groupsArray, 'name').map((group) => renderMenuItem(group))}
</div>

View File

@ -8,6 +8,13 @@ export function set(data) {
};
}
export function updateServer(server) {
return {
type: 'MOPIDY_UPDATE_SERVER',
server,
};
}
export function setConnection(data) {
return {
type: 'MOPIDY_SET_CONNECTION',

View File

@ -6,6 +6,11 @@ export default function reducer(mopidy = {}, action) {
case 'MOPIDY_SET':
return { ...mopidy, ...action.data };
case 'MOPIDY_UPDATE_SERVER':
const updated = {};
updated[action.server.id] = { ...mopidy.servers[action.server.id], ...action.server };
return { ...mopidy, servers: { ...mopidy.servers, ...updated } };
case 'MOPIDY_CONNECT':
case 'MOPIDY_CONNECTING':
return {

View File

@ -61,6 +61,15 @@ let state = {
host: window.location.hostname,
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
ssl: (window.location.protocol === 'https:'),
servers: {
default: {
id: 'default',
name: 'Default',
host: window.location.hostname,
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
ssl: (window.location.protocol === 'https:')
},
},
mute: false,
volume: 0,
progress: 0,

View File

@ -11,6 +11,7 @@ import TextField from '../components/Fields/TextField';
import Header from '../components/Header';
import Icon from '../components/Icon';
import Services from '../components/Services';
import Servers from '../components/Servers';
import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions';
import * as pusherActions from '../services/pusher/actions';
@ -205,34 +206,14 @@ class Settings extends React.Component {
</div>
</label>
<form onSubmit={(e) => this.setConfig(e)}>
<label className="field">
<div className="name">Host</div>
<div className="input">
<TextField
value={mopidy.host}
onChange={value => mopidyActions.setConnection({ host: value })}
/>
</div>
</label>
<label className="field">
<div className="name">Port</div>
<div className="input">
<TextField
type="text"
value={mopidy.port}
onChange={value => mopidyActions.setConnection({ port: value })}
/>
</div>
</label>
</form>
<Route path="/settings/:server?/:id?" component={Servers} />
<h4 className="underline">
Services
<a name="services" />
</h4>
<Route path="/settings/:service?/:id?" component={Services} />
<Route path="/settings/:services?/:service?/:id?" component={Services} />
<h4 className="underline">
Interface

View File

@ -27,9 +27,9 @@
@import 'components/filter-field';
@import 'components/sources-priority-field';
@import 'components/sub-views';
@import 'components/sub-tabs';
@import 'components/debug';
@import 'components/tooltip';
@import 'components/services';
@import 'components/snapcast';
@import 'components/popularity';
@import 'components/colour-field';

View File

@ -1,10 +1,10 @@
.services {
.sub-tabs {
.menu {
&__menu {
@include clearfix();
&__inner {
.menu__inner {
display: flex;
margin: 0 0 0 -5px;
}
@ -89,7 +89,7 @@
}
}
.service {
&__content {
padding: 20px;
border-radius: 0 0 3px 3px;
background: lighten(colour(dark_grey), 4%);
@ -107,7 +107,7 @@
margin-left: -20px;
margin-right: -20px;
.menu {
&__menu {
overflow-x: auto;
&::-webkit-scrollbar{
@ -135,7 +135,7 @@
}
}
.service {
&__content {
margin-top: 0;
}
}