Delete snapcast client; Hide whole group if all clients within it are disconnected and hidden
This commit is contained in:
@ -8,8 +8,7 @@ import Icon from '../Icon';
|
||||
import {
|
||||
loadAlbum,
|
||||
loadArtist,
|
||||
loadTrack,
|
||||
loadTracks,
|
||||
loadPlaylist,
|
||||
removeTracksFromPlaylist,
|
||||
deletePlaylist,
|
||||
} from '../../services/core/actions';
|
||||
@ -129,7 +128,7 @@ const ContextMenuItems = ({
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Refresh uri={item.uri} action={loadTrack} />
|
||||
<Refresh uri={item.uri} action={loadPlaylist} />
|
||||
<Copy uris={[item.uri]} />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,65 +1,70 @@
|
||||
import React from 'react';
|
||||
import TextField from './TextField';
|
||||
import { throttle } from '../../util/helpers';
|
||||
|
||||
export default class LatencyControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = throttle(this.handleChange.bind(this), 100);
|
||||
}
|
||||
const LatencyControl = ({
|
||||
onChange,
|
||||
value: valueProp,
|
||||
max,
|
||||
}) => {
|
||||
let value = valueProp;
|
||||
const throttledChange = (nextValue) => onChange(nextValue, value);
|
||||
const handleChange = throttle(throttledChange, 100);
|
||||
let percentage = Math.round((value / max) * 100 / 2);
|
||||
let left = 50;
|
||||
let width = percentage;
|
||||
let negative = false;
|
||||
|
||||
handleChange(value) {
|
||||
this.props.onChange(value, this.props.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Zero, or positive value
|
||||
if (this.props.value >= 0) {
|
||||
var { value } = this.props;
|
||||
if (value > this.props.max) {
|
||||
value = this.props.max;
|
||||
}
|
||||
var percentage = Math.round((value / this.props.max) * 100 / 2);
|
||||
var left = 50;
|
||||
var width = percentage;
|
||||
var negative = false;
|
||||
|
||||
if (width > (this.props.max / 2)) width = this.props.max / 2;
|
||||
|
||||
// Negative value
|
||||
// We reverse it to a positive for easier maths and style rules
|
||||
} else {
|
||||
var value = -this.props.value;
|
||||
if (value < -this.props.max) {
|
||||
value = -this.props.max;
|
||||
}
|
||||
var percentage = Math.round((value / this.props.max) * 100 / 2);
|
||||
var left = 50 - percentage;
|
||||
var width = percentage;
|
||||
var negative = true;
|
||||
|
||||
if (left < 0) left = 0;
|
||||
if (width > (this.props.max / 2)) width = this.props.max / 2;
|
||||
// Zero, or positive value
|
||||
if (value >= 0) {
|
||||
if (value > max) {
|
||||
value = max;
|
||||
}
|
||||
if (width > (max / 2)) width = max / 2;
|
||||
|
||||
return (
|
||||
<span className="latency-control">
|
||||
<div className="slider__wrapper">
|
||||
<div className="slider slider--latency">
|
||||
<input
|
||||
type="range"
|
||||
min={-(this.props.max)}
|
||||
max={this.props.max}
|
||||
className="slider__input"
|
||||
value={this.props.value}
|
||||
onChange={(e) => this.handleChange(parseInt(e.target.value))}
|
||||
/>
|
||||
<div className="zero" />
|
||||
<div className="slider__track">
|
||||
<div className={`slider__track__progress slider__track__progress--${negative ? 'negative' : 'positive'}`} style={{ width: `${width}%`, left: `${left}%` }} />
|
||||
</div>
|
||||
// Negative value
|
||||
// We reverse it to a positive for easier maths and style rules
|
||||
} else {
|
||||
value = -value;
|
||||
if (value < -max) {
|
||||
value = -max;
|
||||
}
|
||||
percentage = Math.round((value / max) * 100 / 2);
|
||||
left = 50 - percentage;
|
||||
width = percentage;
|
||||
negative = true;
|
||||
|
||||
if (left < 0) left = 0;
|
||||
if (width > (max / 2)) width = max / 2;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="latency-control">
|
||||
<div className="slider__wrapper">
|
||||
<div className="slider slider--latency">
|
||||
<input
|
||||
type="range"
|
||||
min={-(max)}
|
||||
max={max}
|
||||
className="slider__input"
|
||||
value={value}
|
||||
onChange={(e) => handleChange(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<div className="zero" />
|
||||
<div className="slider__track">
|
||||
<div className={`slider__track__progress slider__track__progress--${negative ? 'negative' : 'positive'}`} style={{ width: `${width}%`, left: `${left}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
<TextField
|
||||
className="tiny"
|
||||
type="number"
|
||||
onChange={handleChange}
|
||||
value={String(value)}
|
||||
autosave
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default LatencyControl;
|
||||
|
||||
@ -40,6 +40,7 @@ const Link = ({
|
||||
retainScroll,
|
||||
scrollTo: scrollTarget,
|
||||
onContextMenu,
|
||||
onClick,
|
||||
className = '',
|
||||
activeClassName,
|
||||
to,
|
||||
@ -49,9 +50,12 @@ const Link = ({
|
||||
if (!to) return <span className={className}>{children}</span>;
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const onClick = () => updateScrollPosition({
|
||||
history, location, retainScroll, scrollTarget,
|
||||
});
|
||||
const handleClick = (e) => {
|
||||
updateScrollPosition({
|
||||
history, location, retainScroll, scrollTarget,
|
||||
});
|
||||
if (onClick) onClick(e);
|
||||
}
|
||||
|
||||
// Decode both links. This handles issues where one link is encoded and the other isn't, but
|
||||
// they're otherwise identical
|
||||
@ -63,7 +67,7 @@ const Link = ({
|
||||
const active = history && isLinkActive ? (activeClassName || 'active') : '';
|
||||
return (
|
||||
<RouterLink
|
||||
onClick={onClick}
|
||||
onClick={handleClick}
|
||||
onContextMenu={onContextMenu}
|
||||
className={`${className} ${active}`}
|
||||
to={to}
|
||||
|
||||
@ -5,9 +5,10 @@ import LatencyControl from './Fields/LatencyControl';
|
||||
import TextField from './Fields/TextField';
|
||||
import SelectField from './Fields/SelectField';
|
||||
import { I18n, i18n } from '../locale';
|
||||
import Link from './Link';
|
||||
|
||||
const SnapcastClients = ({
|
||||
actions, group, clients, groups,
|
||||
const SnapcastClients = ({
|
||||
actions, group, clients, groups,
|
||||
}) => {
|
||||
if (!clients || clients.length <= 0) {
|
||||
return (
|
||||
@ -77,15 +78,18 @@ const SnapcastClients = ({
|
||||
value={client.latency}
|
||||
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
|
||||
/>
|
||||
<TextField
|
||||
className="tiny"
|
||||
type="number"
|
||||
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
|
||||
value={String(client.latency)}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
className="button button--destructive button--small"
|
||||
onClick={() => actions.deleteClient(client.id)}
|
||||
to="/settings/services/snapcast/"
|
||||
scrollTo="#services-menu"
|
||||
>
|
||||
<I18n path="actions.delete" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="snapcast__client__volume field field--condensed">
|
||||
<VolumeControl
|
||||
className="snapcast__volume-control snapcast__client__volume-control"
|
||||
|
||||
@ -115,16 +115,17 @@ const SnapcastGroups = (props) => {
|
||||
|
||||
const renderMenuItem = (simpleGroup) => {
|
||||
const group = collate(simpleGroup, { clients });
|
||||
const anyClients = (
|
||||
!show_disconnected_clients && (
|
||||
!group.clients
|
||||
|| !group.clients.length
|
||||
|| !group.clients.filter((client) => client.connected).length
|
||||
)
|
||||
const noClients = (
|
||||
!group.clients
|
||||
|| !group.clients.length
|
||||
|| !group.clients.filter((client) => client.connected).length
|
||||
);
|
||||
|
||||
if (noClients && !show_disconnected_clients) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={`snapcast__groups__menu-item menu-item${anyClients ? ' menu-item--no-clients' : ''}`}
|
||||
className={`snapcast__groups__menu-item menu-item${noClients ? ' menu-item--no-clients' : ''}`}
|
||||
activeClassName="menu-item--active"
|
||||
key={group.id}
|
||||
history={history}
|
||||
|
||||
@ -510,11 +510,9 @@ const SnapcastMiddleware = (function () {
|
||||
case 'SNAPCAST_DELETE_CLIENT':
|
||||
request(store, 'Server.DeleteClient', { id: action.id })
|
||||
.then(
|
||||
() => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_CLIENT_REMOVED',
|
||||
key: action.data.params.id,
|
||||
});
|
||||
(response) => {
|
||||
// Groups contain clients
|
||||
store.dispatch(snapcastActions.groupsLoaded(response.server.groups, true));
|
||||
},
|
||||
(error) => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
|
||||
@ -18,6 +18,12 @@ export default function reducer(snapcast = {}, action) {
|
||||
case 'SNAPCAST_CLIENTS_LOADED':
|
||||
return { ...snapcast, clients: action.clients };
|
||||
|
||||
case 'SNAPCAST_CLIENT_DELETED': {
|
||||
const clients = { ...snapcast.clients };
|
||||
delete clients[action.key];
|
||||
return { ...snapcast, clients };
|
||||
}
|
||||
|
||||
case 'SNAPCAST_GROUPS_LOADED':
|
||||
if (action.flush) {
|
||||
var groups = {};
|
||||
|
||||
@ -129,7 +129,7 @@
|
||||
|
||||
&__client {
|
||||
@include clearfix();
|
||||
padding: 10px 10px 10px 60px;
|
||||
padding: 10px 10px 30px 60px;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 25%;
|
||||
@ -178,16 +178,14 @@
|
||||
|
||||
&__latency {
|
||||
input[type="number"] {
|
||||
width: 20%;
|
||||
max-width: 70px;
|
||||
}
|
||||
|
||||
.latency-control {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding-top: 4px;
|
||||
width: 80%;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.zero {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user