Adding and updating Servers, fixes #524

This commit is contained in:
James Barnsley
2020-04-28 21:24:17 +12:00
parent 92292b18d7
commit 0caa780e38
17 changed files with 454 additions and 258 deletions

View File

@ -12,6 +12,7 @@ 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';
import { iconFromKeyword } from '../util/helpers';
const Servers = ({
match: { params: { id: serverId } },
@ -19,27 +20,70 @@ const Servers = ({
}) => {
const store = useStore();
const dispatch = useDispatch();
const { mopidy: { servers } } = store.getState();
const {
mopidy: {
servers,
current_server,
connected: mopidyConnected,
connecting: mopidyConnecting,
},
pusher: {
connected: pusherConnected,
connecting: pusherConnecting,
},
} = store.getState();
const addServer = () => {
const action = mopidyActions.addServer();
dispatch(action);
history.push(`/settings/servers/${action.server.id}`);
};
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}
{indexToArray(servers).map((server) => {
let status = <span className="status mid_grey-text">Inactive</span>;
if (server.id === current_server) {
if (mopidyConnecting || pusherConnecting) {
status = <span className="status mid_grey-text">Connecting</span>
} else if (!mopidyConnected || !pusherConnected) {
status = <span className="status red-text">Disconnected</span>
} else if (mopidyConnected && pusherConnected) {
status = <span className="status green-text">Connected</span>
}
}
return (
<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">
<Icon className="menu-item__icon" name={iconFromKeyword(server.name) || 'dns'} />
<div className="menu-item__title">
{server.name}
</div>
{status}
</div>
</Link>
);
})}
<span
className="menu-item menu-item--add"
onClick={addServer}
>
<div className="menu-item__inner">
<Icon className="menu-item__icon" name="add" />
<div className="menu-item__title">
Add
</div>
</Link>
))}
<span className="status mid_grey-text">New server</span>
</div>
</span>
</div>
</div>
);
@ -49,6 +93,14 @@ const Servers = ({
const server = servers[serverId];
if (!server) return null;
const remove = () => {
dispatch(mopidyActions.removeServer(server.id));
};
const setAsCurrent = () => {
dispatch(mopidyActions.setCurrentServer(server));
};
return (
<div className="sub-tabs__content">
<label className="field">
@ -66,7 +118,7 @@ const Servers = ({
<div className="input">
<TextField
value={server.host}
onChange={value => updateServer({ id: server.id, host: value })}
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, host: value }))}
/>
</div>
</label>
@ -76,10 +128,18 @@ const Servers = ({
<TextField
type="text"
value={server.port}
onChange={value => updateServer({ id: server.id, port: value })}
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, port: value }))}
/>
</div>
</label>
<label className="field">
<div className="name" />
<div className="input">
<button className="button button--primary" disabled={server.id === current_server} onClick={setAsCurrent}>Switch to this server</button>
<button className="button button--destructive" onClick={remove}>Remove</button>
</div>
</label>
</div>
);
};

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { collate } from '../util/format';
import { sortItems } from '../util/arrays';
import { iconFromKeyword } from '../util/helpers';
import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import TextField from './Fields/TextField';
@ -106,31 +107,6 @@ const SnapcastGroups = (props) => {
!group.clients.filter((client) => client.connected).length
)
);
const icon = () => {
const iconWords = [
{ icon: 'business', words: ['office', 'work'] },
{ icon: 'king_bed', words: ['bed'] },
{ icon: 'weekend', words: ['lounge', 'tv', 'sitting room'] },
{ icon: 'directions_car', words: ['garage', 'laundry'] },
{ icon: 'fitness_center', words: ['gym'] },
{ icon: 'kitchen', words: ['kitchen'] },
{ icon: 'deck', words: ['deck', 'outside'] },
{ icon: 'restaurant_menu', words: ['dining', 'dinner'] },
{ icon: 'laptop', words: ['laptop'] },
{ icon: 'bug_report', words: ['test', 'debug'] },
{ icon: 'child_care', words: ['kids', 'baby'] },
{ icon: 'smartphone', words: ['phone', 'mobile'] },
];
const name = group.name.toLowerCase();
for (let item of iconWords) {
for (let word of item.words) {
if (name.match(new RegExp(`(${word})`, 'gi'))) {
return item.icon;
}
}
};
return 'speaker_group';
}
return (
<Link
className={`snapcast__groups__menu-item menu-item${anyClients ? ' menu-item--no-clients' : ''}`}
@ -141,7 +117,7 @@ const SnapcastGroups = (props) => {
scrollTo="#services-snapcast-groups"
>
<div className="snapcast__groups__menu-item__inner menu-item__inner">
<Icon className="menu-item__icon" name={icon()} />
<Icon className="menu-item__icon" name={iconFromKeyword(group.name.toLowerCase()) || 'speaker_group'} />
<div className="menu-item__title">
{group.name}
{group.mute && (

View File

@ -63,6 +63,15 @@ const localstorageMiddleware = (function () {
);
break;
case 'MOPIDY_UPDATE_SERVERS':
storage.set(
'mopidy',
{
servers: action.servers,
},
);
break;
case 'SPOTIFY_AUTHORIZATION_GRANTED':
if (action.authorization !== undefined) {
var { authorization } = action;

View File

@ -1,5 +1,6 @@
import { createRange } from '../../util/arrays';
import { generateGuid } from '../../util/helpers';
export function set(data) {
return {
@ -15,6 +16,39 @@ export function updateServer(server) {
};
}
export function updateServers(servers) {
return {
type: 'MOPIDY_UPDATE_SERVERS',
servers,
};
}
export function addServer() {
return {
type: 'MOPIDY_UPDATE_SERVER',
server: {
id: generateGuid(),
name: 'New server',
host: window.location.hostname,
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
}
};
}
export function setCurrentServer(server) {
return {
type: 'MOPIDY_SET_CURRENT_SERVER',
server,
};
}
export function removeServer(id) {
return {
type: 'MOPIDY_REMOVE_SERVER',
id,
};
}
export function setConnection(data) {
return {
type: 'MOPIDY_SET_CONNECTION',

View File

@ -284,10 +284,20 @@ const MopidyMiddleware = (function () {
});
break;
case 'MOPIDY_SET_CONNECTION':
store.dispatch(mopidyActions.set(action.data));
case 'MOPIDY_UPDATE_SERVER':
let servers = store.getState().mopidy.servers;
servers[action.server.id] = { ...servers[action.server.id], ...action.server };
store.dispatch(mopidyActions.updateServers(servers));
break;
// Wait 250 ms and then retry connection
case 'MOPIDY_SET_CURRENT_SERVER':
store.dispatch(mopidyActions.set({
current_server: action.server.id,
host: action.server.host,
port: action.server.port
}));
// Wait a moment for store to update, then attempt connection
setTimeout(
() => {
store.dispatch(mopidyActions.connect());
@ -297,6 +307,12 @@ const MopidyMiddleware = (function () {
);
break;
case 'MOPIDY_REMOVE_SERVER':
let remaining_servers = store.getState().mopidy.servers;
delete remaining_servers[action.id];
store.dispatch(mopidyActions.updateServers(remaining_servers));
break;
case 'SET_WINDOW_FOCUS':
// Focus has just been regained

View File

@ -6,17 +6,15 @@ 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_UPDATE_SERVERS':
return { ...mopidy, servers: action.servers };
case 'MOPIDY_CONNECT':
case 'MOPIDY_CONNECTING':
return {
...mopidy,
connected: false,
connecting: true,
connecting: true,
};
case 'MOPIDY_CONNECTED':

View File

@ -1,6 +1,5 @@
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { generateGuid } from '../util/helpers';
@ -61,6 +60,7 @@ let state = {
host: window.location.hostname,
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
ssl: (window.location.protocol === 'https:'),
current_server: 'default',
servers: {
default: {
id: 'default',

View File

@ -410,6 +410,33 @@ const titleCase = function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
/**
* Use a keyword to return an icon name
*/
const iconFromKeyword = (name) => {
const iconWords = [
{ icon: 'business', words: ['office', 'work'] },
{ icon: 'king_bed', words: ['bed'] },
{ icon: 'weekend', words: ['lounge', 'tv', 'sitting room'] },
{ icon: 'directions_car', words: ['garage', 'basement'] },
{ icon: 'local_laundry_service', words: ['laundry'] },
{ icon: 'fitness_center', words: ['gym'] },
{ icon: 'kitchen', words: ['kitchen'] },
{ icon: 'deck', words: ['deck', 'outside'] },
{ icon: 'restaurant_menu', words: ['dining', 'dinner'] },
{ icon: 'laptop', words: ['laptop'] },
{ icon: 'bug_report', words: ['test', 'debug'] },
{ icon: 'child_care', words: ['kids', 'baby'] },
{ icon: 'smartphone', words: ['phone', 'mobile'] },
];
for (let item of iconWords) {
for (let word of item.words) {
if (name.match(new RegExp(`(${word})`, 'gi'))) {
return item.icon;
}
}
};
}
/**
* Scroll to the top of the page
@ -512,6 +539,7 @@ export {
scrollTo,
upgradeSpotifyPlaylistUris,
upgradeSpotifyPlaylistUri,
iconFromKeyword,
};
export default {
@ -536,4 +564,5 @@ export default {
scrollTo,
upgradeSpotifyPlaylistUris,
upgradeSpotifyPlaylistUri,
iconFromKeyword,
};

View File

@ -100,39 +100,6 @@ class Settings extends React.Component {
}
}
renderServerStatus() {
const {
mopidy,
pusher,
} = this.props;
let colour = 'grey';
let icon = 'help';
let status = 'Unknown';
let className = null;
if (mopidy.connecting || pusher.connecting) {
icon = 'autorenew';
status = 'Connecting...';
className = 'icon--spin';
} else if (!mopidy.connected || !pusher.connected) {
colour = 'red';
icon = 'close';
status = 'Disconnected';
} else if (mopidy.connected && pusher.connected) {
colour = 'green';
icon = 'check';
status = 'Connected';
}
return (
<span className={`${colour}-text`}>
<Icon className={className} name={icon} />
{' '}
{status}
</span>
);
}
renderLocalScanButton = () => {
const { processes } = this.props.ui;
const loading = processes.local_scan && processes.local_scan.status === 'running';
@ -184,15 +151,6 @@ class Settings extends React.Component {
<a name="server" />
</h4>
<div className="field">
<div className="name">Status</div>
<div className="input">
<div className="text">
{this.renderServerStatus()}
</div>
</div>
</div>
<label className="field">
<div className="name">Username</div>
<div className="input">

View File

@ -18,6 +18,7 @@
display: block;
box-sizing: border-box;
border: none !important;
cursor: pointer;
&__inner {
position: relative;