Control URL now seeds or matches server client-side; Client group switcher
This commit is contained in:
@ -7,6 +7,7 @@ import MuteControl from './MuteControl';
|
||||
import Icon from '../Icon';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import LinksSentence from '../LinksSentence';
|
||||
import DropdownField from './DropdownField';
|
||||
import * as coreActions from '../../services/core/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as pusherActions from '../../services/pusher/actions';
|
||||
@ -23,18 +24,26 @@ const Header = ({ stream, server }) => {
|
||||
meta,
|
||||
status,
|
||||
} = stream || {};
|
||||
let {
|
||||
control_url,
|
||||
images,
|
||||
name,
|
||||
artists,
|
||||
} = meta || {};
|
||||
const controlURL = meta?.control_url ? new URL(meta.control_url) : null;
|
||||
const controlServer = controlURL ? {
|
||||
id: meta?.control_url,
|
||||
url: controlURL,
|
||||
ssl: controlURL.protocol === 'https:',
|
||||
host: controlURL.hostname,
|
||||
port: controlURL.port || (controlURL.protocol === 'https:' ? '443' : '80'),
|
||||
} : null;
|
||||
console.debug({ controlURL, controlServer });
|
||||
const images = meta?.images ? formatImages(digestMopidyImages(controlServer, meta.images)) : null;
|
||||
const current_server_id = useSelector((state) => state.mopidy.current_server);
|
||||
const current_server = useSelector((state) => state.mopidy.servers[current_server_id]);
|
||||
const isCurrentServer = control_url === current_server.url;
|
||||
if (images) images = formatImages(digestMopidyImages(controlServer, images));
|
||||
|
||||
const onClick = () => {
|
||||
if (!controlServer) return;
|
||||
if (!controlServer || isCurrentServer) return;
|
||||
dispatch(mopidyActions.setCurrentServer(controlServer));
|
||||
};
|
||||
|
||||
@ -49,16 +58,17 @@ const Header = ({ stream, server }) => {
|
||||
<h5
|
||||
className="output-control__output__header__title tooltip"
|
||||
onClick={onClick}
|
||||
style={{ cursor: controlServer ? 'pointer' : 'default' }}
|
||||
style={{ cursor: isCurrentServer ? 'default' : 'pointer' }}
|
||||
>
|
||||
{server?.name || id}
|
||||
{isCurrentServer && <Icon name="check" />}
|
||||
{status === 'playing' && <Icon name="play_arrow" />}
|
||||
{controlURL && <div className="tooltip__content">{meta.control_url}</div>}
|
||||
{control_url && <div className="tooltip__content">{control_url}</div>}
|
||||
</h5>
|
||||
{meta && (
|
||||
<ul className="details">
|
||||
<li>{meta?.name}</li>
|
||||
<li><LinksSentence items={meta?.artists} type="artist" nolinks /></li>
|
||||
<li>{name}</li>
|
||||
<li><LinksSentence items={artists} type="artist" nolinks /></li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
@ -66,7 +76,11 @@ const Header = ({ stream, server }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const Clients = ({ clients }) => {
|
||||
const Clients = ({
|
||||
clients,
|
||||
groups,
|
||||
group_id,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
@ -81,7 +95,17 @@ const Clients = ({ clients }) => {
|
||||
return (
|
||||
<div className="output-control__output__clients__item" key={client.id}>
|
||||
<h5 className="output-control__output__clients__item__title">
|
||||
{titleCase(name)}
|
||||
<div>{titleCase(name)}</div>
|
||||
<DropdownField
|
||||
name="Group"
|
||||
value={group_id}
|
||||
icon="speaker_group"
|
||||
options={groups.map((g) => ({ value: g.id, label: g.name }))}
|
||||
noLabel
|
||||
handleChange={
|
||||
(value) => dispatch(snapcastActions.setClientGroup(client.id, value))
|
||||
}
|
||||
/>
|
||||
</h5>
|
||||
<div className="output-control__output__clients__item__volume">
|
||||
<MuteControl
|
||||
@ -102,10 +126,12 @@ const Clients = ({ clients }) => {
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Group = ({
|
||||
groups,
|
||||
item: {
|
||||
id,
|
||||
stream,
|
||||
server,
|
||||
clients,
|
||||
@ -113,8 +139,15 @@ const Group = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="output-control__output">
|
||||
<Header stream={stream} server={server} />
|
||||
<Clients clients={clients} />
|
||||
<Header
|
||||
stream={stream}
|
||||
server={server}
|
||||
/>
|
||||
<Clients
|
||||
clients={clients}
|
||||
group_id={id}
|
||||
groups={groups}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -128,14 +161,22 @@ const Outputs = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{map(groupsByStream, (groups, stream_id) => {
|
||||
const clients_ids = groups.reduce((acc, curr) => [...acc, ...curr.clients_ids], []);
|
||||
{map(groupsByStream, (streamGroups, stream_id) => {
|
||||
const clients_ids = streamGroups.reduce((acc, curr) => [...acc, ...curr.clients_ids], []);
|
||||
const group = {
|
||||
id: streamGroups[0].id, // Technically multiple groups could be setup for the one stream
|
||||
stream: find(streams, (s) => s.id === stream_id),
|
||||
server: find(servers, (s) => s.snapcast_stream === stream_id),
|
||||
clients: clients_ids.map((id) => clients[id]).filter((c) => c.connected),
|
||||
};
|
||||
return <Group item={group} key={stream_id} />;
|
||||
return (
|
||||
<Group
|
||||
item={group}
|
||||
key={stream_id}
|
||||
groups={groups}
|
||||
stream_id={stream_id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useStore, useDispatch } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { Route, Switch, useParams, useHistory } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Link from './Link';
|
||||
import Icon from './Icon';
|
||||
import TextField from './Fields/TextField';
|
||||
@ -10,15 +11,18 @@ import { iconFromKeyword } from '../util/helpers';
|
||||
import { I18n } from '../locale';
|
||||
import { decodeUri, encodeUri } from '../util/format';
|
||||
|
||||
const Server = ({
|
||||
server,
|
||||
current_server,
|
||||
}) => {
|
||||
if (!server) return null;
|
||||
const Server = () => {
|
||||
const { id } = useParams();
|
||||
if (!id) return null;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const remove = () => dispatch(mopidyActions.removeServer(server.id));
|
||||
const current_server = useSelector((state) => state.mopidy.current_server);
|
||||
const server = useSelector((state) => state.mopidy.servers[id]);
|
||||
if (!server) return null;
|
||||
|
||||
const remove = () => dispatch(mopidyActions.removeServer(id));
|
||||
const setAsCurrent = () => dispatch(mopidyActions.setCurrentServer(server));
|
||||
const isCurrent = server.id === current_server;
|
||||
const isCurrent = id === current_server;
|
||||
|
||||
return (
|
||||
<div className="sub-tabs__content">
|
||||
@ -30,7 +34,7 @@ const Server = ({
|
||||
<TextField
|
||||
type="text"
|
||||
value={server.name}
|
||||
onChange={(value) => dispatch(mopidyActions.updateServer({ id: server.id, name: value }))}
|
||||
onChange={(value) => dispatch(mopidyActions.updateServer({ id, name: value }))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
@ -42,7 +46,7 @@ const Server = ({
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={server.host}
|
||||
onChange={(value) => dispatch(mopidyActions.updateServer({ id: server.id, host: value }))}
|
||||
onChange={(value) => dispatch(mopidyActions.updateServer({ id, host: value }))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
@ -55,7 +59,7 @@ const Server = ({
|
||||
<TextField
|
||||
type="text"
|
||||
value={server.port}
|
||||
onChange={(value) => dispatch(mopidyActions.updateServer({ id: server.id, port: value }))}
|
||||
onChange={(value) => dispatch(mopidyActions.updateServer({ id, port: value }))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
@ -72,7 +76,7 @@ const Server = ({
|
||||
name="ssl"
|
||||
value={server.ssl}
|
||||
checked={server.ssl}
|
||||
onChange={() => dispatch(mopidyActions.updateServer({ id: server.id, ssl: !server.ssl }))}
|
||||
onChange={() => dispatch(mopidyActions.updateServer({ id, ssl: !server.ssl }))}
|
||||
/>
|
||||
<span className="label tooltip">
|
||||
<I18n path="settings.servers.encryption.sublabel" />
|
||||
@ -107,36 +111,31 @@ const Server = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Servers = ({
|
||||
match: { params: { id } },
|
||||
history,
|
||||
}) => {
|
||||
const serverId = decodeUri(id);
|
||||
const store = useStore();
|
||||
const Menu = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const servers = indexToArray(useSelector((state) => state.mopidy.servers));
|
||||
|
||||
const {
|
||||
mopidy: {
|
||||
servers,
|
||||
current_server,
|
||||
connected: mopidyConnected,
|
||||
connecting: mopidyConnecting,
|
||||
},
|
||||
pusher: {
|
||||
connected: pusherConnected,
|
||||
connecting: pusherConnecting,
|
||||
},
|
||||
} = store.getState();
|
||||
current_server,
|
||||
connected: mopidyConnected,
|
||||
connecting: mopidyConnecting,
|
||||
} = useSelector((state) => state.mopidy);
|
||||
const {
|
||||
connected: pusherConnected,
|
||||
connecting: pusherConnecting,
|
||||
} = useSelector((state) => state.pusher);
|
||||
|
||||
const addServer = () => {
|
||||
const action = mopidyActions.addServer();
|
||||
dispatch(action);
|
||||
history.push(`/settings/servers/${encodeUri(action.server.id)}`);
|
||||
history.push(`/settings/servers/${action.server.id}`);
|
||||
};
|
||||
|
||||
const Menu = () => (
|
||||
return (
|
||||
<div className="sub-tabs__menu" id="servers-menu">
|
||||
<div className="menu__inner">
|
||||
{indexToArray(servers).map((server) => {
|
||||
{servers.map((server) => {
|
||||
let status = (
|
||||
<span className="status mid_grey-text">
|
||||
<I18n path="settings.servers.inactive" />
|
||||
@ -168,7 +167,7 @@ const Servers = ({
|
||||
history={history}
|
||||
className="menu-item"
|
||||
activeClassName="menu-item--active"
|
||||
to={`/settings/servers/${encodeUri(server.id)}`}
|
||||
to={`/settings/servers/${server.id}`}
|
||||
scrollTo="#servers-menu"
|
||||
key={server.id}
|
||||
>
|
||||
@ -199,16 +198,19 @@ const Servers = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="sub-tabs sub-tabs--servers">
|
||||
<Menu />
|
||||
<Server
|
||||
server={servers[serverId]}
|
||||
current_server={current_server}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Servers = () => (
|
||||
<div className="sub-tabs sub-tabs--servers">
|
||||
<Menu />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/servers/:id"
|
||||
component={Server}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Servers;
|
||||
|
||||
@ -23,19 +23,22 @@ export function updateServers(servers) {
|
||||
}
|
||||
|
||||
export function addServer(data) {
|
||||
const {
|
||||
hostname: host,
|
||||
port,
|
||||
protocol,
|
||||
} = window.location;
|
||||
const server = {
|
||||
host: window.location.hostname,
|
||||
port: window.location.port
|
||||
? window.location.port
|
||||
: (window.location.protocol === 'https:' ? '443' : '80'),
|
||||
ssl: window.location.protocol === 'https:',
|
||||
host,
|
||||
ssl: protocol === 'https:',
|
||||
port: port || (protocol === 'https:' ? '443' : '80'),
|
||||
...data,
|
||||
};
|
||||
return {
|
||||
type: 'MOPIDY_UPDATE_SERVER',
|
||||
server: {
|
||||
...server,
|
||||
id: `http${server.ssl ? 's' : ''}://${server.host}:${server.port}`,
|
||||
id: generateGuid(),
|
||||
name: `${server.host}${server.ssl ? ' 🔒' : ''}`,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import ReactGA from 'react-ga';
|
||||
import Mopidy from 'mopidy';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { sampleSize, compact, chunk } from 'lodash';
|
||||
import { sampleSize, compact, chunk, find } from 'lodash';
|
||||
import { i18n } from '../../locale';
|
||||
import {
|
||||
generateGuid,
|
||||
@ -447,28 +447,37 @@ const MopidyMiddleware = (function () {
|
||||
break;
|
||||
|
||||
case 'MOPIDY_UPDATE_SERVER':
|
||||
const existingServers = store.getState().mopidy.servers;
|
||||
const servers = {
|
||||
...existingServers,
|
||||
[action.server.id]: {
|
||||
...existingServers[action.server.id] || {},
|
||||
...action.server,
|
||||
},
|
||||
const { servers } = store.getState().mopidy;
|
||||
const server = {
|
||||
...servers[action.server.id] || {},
|
||||
...action.server,
|
||||
};
|
||||
store.dispatch(mopidyActions.updateServers(servers));
|
||||
server.url = `http${server.ssl ? 's' : ''}://${server.host}:${server.port}`;
|
||||
store.dispatch(mopidyActions.updateServers({ ...servers, [server.id]: server }));
|
||||
break;
|
||||
|
||||
case 'MOPIDY_SET_CURRENT_SERVER':
|
||||
const existingServer = action.server.id
|
||||
? store.getState().mopidy.servers[action.server.id]
|
||||
: null;
|
||||
if (!existingServer) store.dispatch(mopidyActions.addServer(action.server));
|
||||
case 'MOPIDY_SET_CURRENT_SERVER': {
|
||||
const { server } = action;
|
||||
const { servers } = store.getState().mopidy;
|
||||
let existingServer = null;
|
||||
if (action.server.id) {
|
||||
existingServer = servers[action.server.id] || undefined;
|
||||
} else {
|
||||
// No ID provided, see if we have a server setup with the same details already
|
||||
existingServer = find(servers, (s) => s.url === server.url);
|
||||
}
|
||||
|
||||
if (!existingServer) {
|
||||
const create = mopidyActions.addServer(server);
|
||||
store.dispatch(create);
|
||||
existingServer = create.server;
|
||||
}
|
||||
|
||||
store.dispatch(mopidyActions.set({
|
||||
current_server: action.server.id,
|
||||
host: action.server.host,
|
||||
port: action.server.port,
|
||||
ssl: action.server.ssl,
|
||||
current_server: server.id || existingServer?.id,
|
||||
host: server.host,
|
||||
port: server.port,
|
||||
ssl: server.ssl,
|
||||
connected: false,
|
||||
connecting: false,
|
||||
}));
|
||||
@ -483,6 +492,7 @@ const MopidyMiddleware = (function () {
|
||||
);
|
||||
next(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MOPIDY_GET_SERVER_STATE': {
|
||||
let server = { ...store.getState().mopidy.servers[action.id] };
|
||||
|
||||
@ -214,9 +214,11 @@ function refreshToken(dispatch, getState) {
|
||||
// Server-side authorized (with limited scope) so we need to refresh
|
||||
// using the Mopidy-Spotify credentials
|
||||
} else {
|
||||
const { host, port, ssl } = getState().mopidy;
|
||||
let url = `http${ssl ? 's' : ''}://${host}:${port}/iris/http/refresh_spotify_token`;
|
||||
var config = {
|
||||
method: 'GET',
|
||||
url: `//${getState().mopidy.host}:${getState().mopidy.port}/iris/http/refresh_spotify_token`,
|
||||
url,
|
||||
dataType: 'json',
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
@ -103,7 +103,6 @@
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding-right: 1rem;
|
||||
border-top: 1px solid colour('white');
|
||||
@include theme('dark') {
|
||||
border-color: colour('soft_grey');
|
||||
@ -114,10 +113,37 @@
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding: 0.8rem 0.8rem 0 0.8rem;
|
||||
padding: 0.8rem 0 0 0.8rem;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
|
||||
.dropdown-field {
|
||||
margin-left: auto;
|
||||
|
||||
&__label {
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
|
||||
.icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__options {
|
||||
top: auto;
|
||||
bottom: 20px;
|
||||
right: 0;
|
||||
|
||||
&:before {
|
||||
top: auto;
|
||||
bottom: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__volume {
|
||||
padding-right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
Reference in New Issue
Block a user