Control URL now seeds or matches server client-side; Client group switcher

This commit is contained in:
James Barnsley
2021-10-17 21:09:26 +13:00
parent d6b185577b
commit 5c152efea5
14 changed files with 541 additions and 341 deletions

View File

@ -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}
/>
);
})}
</>
);

View File

@ -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;

View File

@ -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 ? ' 🔒' : ''}`,
},
};

View File

@ -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] };

View File

@ -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,
};

View File

@ -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;
}

View File

@ -56,6 +56,7 @@
padding: 0;
font-weight: bold;
font-size: 16px;
line-height: 1em;
}
&:hover {