Play/pause snapcast streams using meta_mpd.py plugin
This commit is contained in:
@ -13,70 +13,78 @@ import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as pusherActions from '../../services/pusher/actions';
|
||||
import * as snapcastActions from '../../services/snapcast/actions';
|
||||
import { sortItems, indexToArray } from '../../util/arrays';
|
||||
import { formatImages, digestMopidyImages } from '../../util/format';
|
||||
import { titleCase } from '../../util/helpers';
|
||||
import { I18n } from '../../locale';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
import Link from '../Link';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
|
||||
const Header = ({ stream, server }) => {
|
||||
const Header = ({
|
||||
stream,
|
||||
server,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
id,
|
||||
meta: {
|
||||
name,
|
||||
artists,
|
||||
images: rawImages,
|
||||
} = {},
|
||||
status,
|
||||
uri: {
|
||||
scheme,
|
||||
query: {
|
||||
control_url,
|
||||
properties: {
|
||||
playbackStatus,
|
||||
canPlay,
|
||||
canPause,
|
||||
canControl,
|
||||
metadata: {
|
||||
title,
|
||||
artist: artists = [],
|
||||
artUrl,
|
||||
} = {},
|
||||
} = {},
|
||||
} = stream || {};
|
||||
const controlURL = control_url ? new URL(control_url) : null;
|
||||
const controlServer = controlURL ? {
|
||||
url: control_url,
|
||||
ssl: controlURL.protocol === 'https:',
|
||||
host: controlURL.hostname,
|
||||
port: controlURL.port || (controlURL.protocol === 'https:' ? '443' : '80'),
|
||||
} : null;
|
||||
const images = rawImages ? formatImages(digestMopidyImages(controlServer, rawImages)) : 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;
|
||||
const isControlSwitchable = controlServer && !isCurrentServer;
|
||||
|
||||
const onClick = () => {
|
||||
if (isControlSwitchable) {
|
||||
dispatch(mopidyActions.setCurrentServer(controlServer));
|
||||
}
|
||||
};
|
||||
let onClick = null;
|
||||
switch (playbackStatus) {
|
||||
case 'playing':
|
||||
if (canPause) onClick = () => dispatch(snapcastActions.controlStream(id, 'pause'));
|
||||
break;
|
||||
default:
|
||||
if (canPlay) onClick = () => dispatch(snapcastActions.controlStream(id, 'play'));
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="output-control__stream__header">
|
||||
<Thumbnail
|
||||
images={images}
|
||||
size="small"
|
||||
className="output-control__stream__header__thumbnail"
|
||||
/>
|
||||
<div
|
||||
className={[
|
||||
'output-control__stream__header__art',
|
||||
`output-control__stream__header__art--${onClick !== null ? playbackStatus : 'disabled'}`
|
||||
].join(' ')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Thumbnail
|
||||
image={artUrl}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div className="output-control__stream__header__content">
|
||||
<h5 className="output-control__stream__header__title tooltip">
|
||||
{isControlSwitchable ? (
|
||||
<a onClick={onClick} style={{ cursor: 'pointer' }}>
|
||||
{server?.name || id}
|
||||
</a>
|
||||
) : (server?.name || id)}
|
||||
{isCurrentServer && <Icon name="check" />}
|
||||
{status === 'playing' && <Icon name="play_arrow" />}
|
||||
{control_url && <div className="tooltip__content">{control_url}</div>}
|
||||
{server?.name || id}
|
||||
{!canControl && (
|
||||
<span className="flag">
|
||||
{i18n('snapcast.not_controllable').toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</h5>
|
||||
<ul className="details">
|
||||
<li>{name || scheme || 'Unknown'}</li>
|
||||
{artists && <li><LinksSentence items={artists} type="artist" nolinks /></li>}
|
||||
</ul>
|
||||
{!title && !artists?.length ? (
|
||||
<div className="details">
|
||||
<I18n path={`common.play_state.${playbackStatus || status}`} />
|
||||
</div>
|
||||
) : (
|
||||
<ul className="details">
|
||||
<li>{title}</li>
|
||||
{artists && (
|
||||
<li>
|
||||
<LinksSentence items={artists.map((name) => ({ name }))} type="artist" nolinks />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -334,6 +334,35 @@ class Services extends React.Component {
|
||||
return (
|
||||
<div className="sub-tabs__menu menu" id="services-menu">
|
||||
<div className="menu__inner">
|
||||
<Link
|
||||
history={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">
|
||||
<I18n path="services.snapcast.title" />
|
||||
</div>
|
||||
{!snapcastEnabled && (
|
||||
<span className="status mid_grey-text">
|
||||
<I18n path="settings.services.disabled" />
|
||||
</span>
|
||||
)}
|
||||
{snapcastEnabled && !snapcastConnected && (
|
||||
<span className="status red-text">
|
||||
<I18n path="settings.services.disconnected" />
|
||||
</span>
|
||||
)}
|
||||
{snapcastEnabled && snapcastConnected && (
|
||||
<span className="status green-text">
|
||||
<I18n path="settings.services.connected" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
history={history}
|
||||
className="menu-item menu-item--spotify"
|
||||
@ -403,35 +432,6 @@ class Services extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
history={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">
|
||||
<I18n path="services.snapcast.title" />
|
||||
</div>
|
||||
{!snapcastEnabled && (
|
||||
<span className="status mid_grey-text">
|
||||
<I18n path="settings.services.disabled" />
|
||||
</span>
|
||||
)}
|
||||
{snapcastEnabled && !snapcastConnected && (
|
||||
<span className="status red-text">
|
||||
<I18n path="settings.services.disconnected" />
|
||||
</span>
|
||||
)}
|
||||
{snapcastEnabled && snapcastConnected && (
|
||||
<span className="status green-text">
|
||||
<I18n path="settings.services.connected" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -58,6 +58,11 @@ common:
|
||||
filter: Filter
|
||||
by: ' by '
|
||||
loading_library: 'Loading %{provider} %{type}'
|
||||
play_state:
|
||||
playing: Playing
|
||||
paused: Paused
|
||||
stopped: Stopped
|
||||
idle: Idle
|
||||
fields:
|
||||
start_typing: 'Start typing...'
|
||||
search: 'Search...'
|
||||
@ -216,6 +221,7 @@ snapcast:
|
||||
latency: Latency
|
||||
stream: Stream
|
||||
volume: Volume
|
||||
not_controllable: Not controllable
|
||||
encryption:
|
||||
label: Encryption
|
||||
sublabel: Use SSL
|
||||
|
||||
@ -150,6 +150,14 @@ export function calculateGroupVolume(id, clients) {
|
||||
};
|
||||
}
|
||||
|
||||
export function controlStream(id, command) {
|
||||
return {
|
||||
type: 'SNAPCAST_CONTROL_STREAM',
|
||||
id,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
export function serverLoaded(server) {
|
||||
return {
|
||||
type: 'SNAPCAST_SERVER_LOADED',
|
||||
|
||||
@ -100,10 +100,7 @@ const SnapcastMiddleware = (function () {
|
||||
break;
|
||||
|
||||
case 'Stream.OnUpdate':
|
||||
store.dispatch(snapcastActions.streamLoaded(message.params));
|
||||
break;
|
||||
|
||||
case 'Stream.OnMetadata':
|
||||
case 'Stream.OnProperties':
|
||||
store.dispatch(snapcastActions.streamLoaded(message.params));
|
||||
break;
|
||||
|
||||
@ -651,6 +648,24 @@ const SnapcastMiddleware = (function () {
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'SNAPCAST_CONTROL_STREAM': {
|
||||
const { id, command } = action;
|
||||
request(store, 'Stream.Control', { id, command })
|
||||
.then(
|
||||
() => {
|
||||
// No response body
|
||||
},
|
||||
(error) => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
'Could not control stream',
|
||||
error,
|
||||
error.message,
|
||||
));
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@ -69,8 +69,65 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&__thumbnail {
|
||||
&__art {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:before {
|
||||
@include animate();
|
||||
content: "\f04b";
|
||||
display: block;
|
||||
font-family: "FontAwesome";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-top: -9px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: colour('white');
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@include animate();
|
||||
content: "";
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:before,
|
||||
&:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: default;
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--playing {
|
||||
&:before {
|
||||
content: "\f04c";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
|
||||
Reference in New Issue
Block a user