Delete snapcast client; Hide whole group if all clients within it are disconnected and hidden

This commit is contained in:
James Barnsley
2022-03-04 20:37:42 +13:00
parent ec0c8e7086
commit 2d88e39671
8 changed files with 107 additions and 92 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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