Add remote server sink/source tunneling
This commit is contained in:
parent
558f0b37e1
commit
ac4b6d42b7
|
@ -1,20 +1,39 @@
|
|||
|
||||
const { map } = require('ramda');
|
||||
|
||||
const { createActions: createActionCreators } = require('redux-actions');
|
||||
|
||||
const withMetaPulseServerId = payloadCreator => {
|
||||
const metaCreator = (...args) => ({
|
||||
pulseServerId: args[payloadCreator.length],
|
||||
});
|
||||
|
||||
return [
|
||||
payloadCreator,
|
||||
metaCreator,
|
||||
];
|
||||
};
|
||||
|
||||
const noop = () => null;
|
||||
const identity = x => x;
|
||||
|
||||
module.exports = createActionCreators({
|
||||
PULSE: {
|
||||
READY: null,
|
||||
CLOSE: null,
|
||||
PULSE: map(withMetaPulseServerId, {
|
||||
READY: noop,
|
||||
CLOSE: noop,
|
||||
|
||||
ERROR: null,
|
||||
CONNECT: noop,
|
||||
DISCONNECT: noop,
|
||||
|
||||
NEW: null,
|
||||
CHANGE: null,
|
||||
REMOVE: null,
|
||||
ERROR: identity,
|
||||
|
||||
INFO: null,
|
||||
NEW: identity,
|
||||
CHANGE: identity,
|
||||
REMOVE: identity,
|
||||
|
||||
SERVER_INFO: null,
|
||||
INFO: identity,
|
||||
|
||||
SERVER_INFO: identity,
|
||||
|
||||
MOVE_SINK_INPUT: (sinkInputIndex, destSinkIndex) => ({ sinkInputIndex, destSinkIndex }),
|
||||
MOVE_SOURCE_OUTPUT: (sourceOutputIndex, destSourceIndex) => ({ sourceOutputIndex, destSourceIndex }),
|
||||
|
@ -46,8 +65,5 @@ module.exports = createActionCreators({
|
|||
|
||||
SET_DEFAULT_SINK_BY_NAME: name => ({ name }),
|
||||
SET_DEFAULT_SOURCE_BY_NAME: name => ({ name }),
|
||||
|
||||
REMOTE_SERVER_CONNECT: null,
|
||||
REMOTE_SERVER_DISCONNECT: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -15,6 +15,8 @@ const { bindActionCreators } = require('redux');
|
|||
|
||||
const { pulse: pulseActions } = require('../../actions');
|
||||
|
||||
const { primaryPulseServer } = require('../../reducers/pulse');
|
||||
|
||||
const Button = require('../button');
|
||||
const Label = require('../label');
|
||||
const Select = require('../select');
|
||||
|
@ -97,7 +99,7 @@ class Cards extends React.Component {
|
|||
|
||||
module.exports = connect(
|
||||
state => ({
|
||||
cards: state.pulse.infos.cards,
|
||||
cards: state.pulse[primaryPulseServer].infos.cards,
|
||||
preferences: state.preferences,
|
||||
}),
|
||||
dispatch => ({
|
||||
|
|
|
@ -28,10 +28,11 @@ const {
|
|||
} = require('ramda');
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const r = require('r-dom');
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { connect, Provider: ReduxProvider } = require('react-redux');
|
||||
const { bindActionCreators } = require('redux');
|
||||
|
||||
const { HotKeys } = require('react-hotkeys');
|
||||
|
@ -48,6 +49,8 @@ const {
|
|||
|
||||
const {
|
||||
getPaiByTypeAndIndex,
|
||||
getPaiByDgoFromInfos,
|
||||
|
||||
getDerivedMonitorSources,
|
||||
|
||||
getClientSinkInputs,
|
||||
|
@ -70,6 +73,8 @@ const { size } = require('../../constants/view');
|
|||
|
||||
const VolumeSlider = require('../../components/volume-slider');
|
||||
|
||||
const { primaryPulseServer } = require('../../reducers/pulse');
|
||||
|
||||
const { keyMap } = require('../hot-keys');
|
||||
|
||||
const {
|
||||
|
@ -128,8 +133,6 @@ const selectionObjectTypes = {
|
|||
},
|
||||
};
|
||||
|
||||
const dgoToPai = new WeakMap();
|
||||
|
||||
const key = pao => `${pao.type}-${pao.index}`;
|
||||
|
||||
const sourceKey = pai => {
|
||||
|
@ -287,8 +290,7 @@ const renderNode = (nodeRef, data, key, selected, hovered) => r({
|
|||
hovered,
|
||||
});
|
||||
|
||||
const getVolumesForThumbnail = ({ pai, state }) => {
|
||||
const { lockChannelsTogether } = state.preferences;
|
||||
const getVolumesForThumbnail = ({ pai, lockChannelsTogether }) => {
|
||||
let volumes = (pai && pai.channelVolumes) || [];
|
||||
if (lockChannelsTogether) {
|
||||
if (volumes.every(v => v === volumes[0])) {
|
||||
|
@ -300,14 +302,20 @@ const getVolumesForThumbnail = ({ pai, state }) => {
|
|||
return volumes;
|
||||
};
|
||||
|
||||
const VolumeThumbnail = ({ pai, state }) => {
|
||||
if (state.preferences.hideVolumeThumbnails) {
|
||||
const VolumeThumbnail = connect(
|
||||
state => ({
|
||||
hideVolumeThumbnails: state.preferences.hideVolumeThumbnails,
|
||||
lockChannelsTogether: state.preferences.lockChannelsTogether,
|
||||
}),
|
||||
)(({ pai, hideVolumeThumbnails, lockChannelsTogether }) => {
|
||||
if (hideVolumeThumbnails) {
|
||||
return r(React.Fragment);
|
||||
}
|
||||
|
||||
const normVolume = PA_VOLUME_NORM;
|
||||
const baseVolume = defaultTo(normVolume, pai && pai.baseVolume);
|
||||
|
||||
const volumes = getVolumesForThumbnail({ pai, state });
|
||||
const volumes = getVolumesForThumbnail({ pai, lockChannelsTogether });
|
||||
const muted = !pai || pai.muted;
|
||||
|
||||
const step = size / 32;
|
||||
|
@ -378,22 +386,27 @@ const VolumeThumbnail = ({ pai, state }) => {
|
|||
]);
|
||||
}),
|
||||
]);
|
||||
};
|
||||
});
|
||||
|
||||
const getVolumes = ({ pai, state }) => {
|
||||
const { lockChannelsTogether } = state.preferences;
|
||||
const getVolumes = ({ pai, lockChannelsTogether }) => {
|
||||
let volumes = (pai && pai.channelVolumes) || [];
|
||||
if (lockChannelsTogether) {
|
||||
volumes = [
|
||||
maximum(volumes),
|
||||
];
|
||||
}
|
||||
return { volumes, lockChannelsTogether };
|
||||
return volumes;
|
||||
};
|
||||
|
||||
const VolumeControls = ({ pai, state }) => {
|
||||
const { maxVolume, volumeStep } = state.preferences;
|
||||
const { volumes, lockChannelsTogether } = getVolumes({ pai, state });
|
||||
const VolumeControls = connect(
|
||||
state => pick([
|
||||
'maxVolume',
|
||||
'volumeStep',
|
||||
'lockChannelsTogether',
|
||||
], state.preferences),
|
||||
dispatch => bindActionCreators(pulseActions, dispatch),
|
||||
)(({ pai, maxVolume, volumeStep, lockChannelsTogether, ...props }) => {
|
||||
const volumes = getVolumes({ pai, lockChannelsTogether });
|
||||
const baseVolume = pai && pai.baseVolume;
|
||||
const muted = !pai || pai.muted;
|
||||
|
||||
|
@ -410,36 +423,40 @@ const VolumeControls = ({ pai, state }) => {
|
|||
onChange: v => {
|
||||
if (pai.type === 'sink') {
|
||||
if (lockChannelsTogether) {
|
||||
state.setSinkVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
props.setSinkVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
} else {
|
||||
state.setSinkChannelVolume(pai.index, channelIndex, v);
|
||||
props.setSinkChannelVolume(pai.index, channelIndex, v);
|
||||
}
|
||||
} else if (pai.type === 'source') {
|
||||
if (lockChannelsTogether) {
|
||||
state.setSourceVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
props.setSourceVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
} else {
|
||||
state.setSourceChannelVolume(pai.index, channelIndex, v);
|
||||
props.setSourceChannelVolume(pai.index, channelIndex, v);
|
||||
}
|
||||
} else if (pai.type === 'sinkInput') {
|
||||
if (lockChannelsTogether) {
|
||||
state.setSinkInputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
props.setSinkInputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
} else {
|
||||
state.setSinkInputChannelVolume(pai.index, channelIndex, v);
|
||||
props.setSinkInputChannelVolume(pai.index, channelIndex, v);
|
||||
}
|
||||
} else if (pai.type === 'sourceOutput') {
|
||||
if (lockChannelsTogether) {
|
||||
state.setSourceOutputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
props.setSourceOutputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||
} else {
|
||||
state.setSourceOutputChannelVolume(pai.index, channelIndex, v);
|
||||
props.setSourceOutputChannelVolume(pai.index, channelIndex, v);
|
||||
}
|
||||
}
|
||||
},
|
||||
})),
|
||||
]);
|
||||
};
|
||||
});
|
||||
|
||||
const Icon = ({ state, name, ...props }) => {
|
||||
const src = state.icons[name];
|
||||
const Icon = connect(
|
||||
state => ({
|
||||
icons: state.icons,
|
||||
}),
|
||||
)(({ icons, name, title }) => {
|
||||
const src = icons[name];
|
||||
|
||||
if (!src) {
|
||||
return r(React.Fragment);
|
||||
|
@ -448,9 +465,9 @@ const Icon = ({ state, name, ...props }) => {
|
|||
return r.img({
|
||||
className: 'node-name-icon',
|
||||
src,
|
||||
...props,
|
||||
title,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const RemoteTunnelInfo = ({ pai }) => {
|
||||
const fqdn = path([ 'properties', 'tunnel', 'remote', 'fqdn' ], pai);
|
||||
|
@ -466,8 +483,12 @@ const RemoteTunnelInfo = ({ pai }) => {
|
|||
]);
|
||||
};
|
||||
|
||||
const DebugText = ({ dgo, pai, state }) => {
|
||||
if (!state.preferences.showDebugInfo) {
|
||||
const DebugText = connect(
|
||||
state => ({
|
||||
showDebugInfo: state.preferences.showDebugInfo,
|
||||
}),
|
||||
)(({ dgo, pai, showDebugInfo }) => {
|
||||
if (!showDebugInfo) {
|
||||
return r(React.Fragment);
|
||||
}
|
||||
|
||||
|
@ -479,15 +500,18 @@ const DebugText = ({ dgo, pai, state }) => {
|
|||
JSON.stringify(dgo, null, 2),
|
||||
JSON.stringify(pai, null, 2),
|
||||
]);
|
||||
};
|
||||
});
|
||||
|
||||
const SinkText = ({ dgo, pai, state, selected }) => r(React.Fragment, [
|
||||
const SinkText = connect(
|
||||
state => ({
|
||||
defaultSinkName: state.pulse[primaryPulseServer].serverInfo.defaultSinkName,
|
||||
}),
|
||||
)(({ dgo, pai, selected, defaultSinkName }) => r(React.Fragment, [
|
||||
r.div({
|
||||
className: 'node-name',
|
||||
}, [
|
||||
state.serverInfo.defaultSinkName === pai.name && r(React.Fragment, [
|
||||
defaultSinkName === pai.name && r(React.Fragment, [
|
||||
r(Icon, {
|
||||
state,
|
||||
name: 'starred',
|
||||
title: 'Default sink',
|
||||
}),
|
||||
|
@ -501,20 +525,23 @@ const SinkText = ({ dgo, pai, state, selected }) => r(React.Fragment, [
|
|||
r.div({
|
||||
className: 'node-main',
|
||||
}, [
|
||||
r(selected ? VolumeControls : VolumeThumbnail, { pai, state }),
|
||||
r(selected ? VolumeControls : VolumeThumbnail, { pai }),
|
||||
]),
|
||||
|
||||
r(RemoteTunnelInfo, { pai }),
|
||||
r(DebugText, { dgo, pai, state }),
|
||||
]);
|
||||
r(DebugText, { dgo, pai }),
|
||||
]));
|
||||
|
||||
const SourceText = ({ dgo, pai, state, selected }) => r(React.Fragment, [
|
||||
const SourceText = connect(
|
||||
state => ({
|
||||
defaultSourceName: state.pulse[primaryPulseServer].serverInfo.defaultSourceName,
|
||||
}),
|
||||
)(({ dgo, pai, selected, defaultSourceName }) => r(React.Fragment, [
|
||||
r.div({
|
||||
className: 'node-name',
|
||||
}, [
|
||||
state.serverInfo.defaultSourceName === pai.name && r(React.Fragment, [
|
||||
defaultSourceName === pai.name && r(React.Fragment, [
|
||||
r(Icon, {
|
||||
state,
|
||||
name: 'starred',
|
||||
title: 'Default source',
|
||||
}),
|
||||
|
@ -528,17 +555,21 @@ const SourceText = ({ dgo, pai, state, selected }) => r(React.Fragment, [
|
|||
r.div({
|
||||
className: 'node-main',
|
||||
}, [
|
||||
r(selected ? VolumeControls : VolumeThumbnail, { pai, state }),
|
||||
r(selected ? VolumeControls : VolumeThumbnail, { pai }),
|
||||
]),
|
||||
|
||||
r(RemoteTunnelInfo, { pai }),
|
||||
r(DebugText, { dgo, pai, state }),
|
||||
]);
|
||||
r(DebugText, { dgo, pai }),
|
||||
]));
|
||||
|
||||
const ClientText = ({ dgo, pai, state }) => {
|
||||
const ClientText = connect(
|
||||
state => ({
|
||||
modules: state.pulse[primaryPulseServer].infos.modules,
|
||||
}),
|
||||
)(({ dgo, pai, modules }) => {
|
||||
let title = path('properties.application.process.binary'.split('.'), pai);
|
||||
|
||||
const module = state.infos.modules[pai.moduleIndex];
|
||||
const module = modules[pai.moduleIndex];
|
||||
if (module && module.name === 'module-native-protocol-tcp') {
|
||||
title = path([ 'properties', 'native-protocol', 'peer' ], pai) || title;
|
||||
}
|
||||
|
@ -548,21 +579,24 @@ const ClientText = ({ dgo, pai, state }) => {
|
|||
className: 'node-name',
|
||||
title,
|
||||
}, pai.name),
|
||||
r(DebugText, { dgo, pai, state }),
|
||||
r(DebugText, { dgo, pai }),
|
||||
]);
|
||||
};
|
||||
});
|
||||
|
||||
const ModuleText = ({ dgo, pai, state }) => r(React.Fragment, [
|
||||
const ModuleText = ({ dgo, pai }) => r(React.Fragment, [
|
||||
r.div({
|
||||
className: 'node-name',
|
||||
title: pai.properties.module.description,
|
||||
}, pai.name),
|
||||
r(DebugText, { dgo, pai, state }),
|
||||
r(DebugText, { dgo, pai }),
|
||||
]);
|
||||
|
||||
const renderNodeText = state => (dgo, i, selected) => {
|
||||
const pai = dgoToPai.get(dgo);
|
||||
|
||||
const NodeText = connect(
|
||||
(state, { dgo }) => ({
|
||||
icons: state.icons,
|
||||
pai: dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)(state),
|
||||
}),
|
||||
)(({ dgo, pai, selected, icons }) => {
|
||||
if (!pai) {
|
||||
return r(React.Fragment);
|
||||
}
|
||||
|
@ -576,7 +610,7 @@ const renderNodeText = state => (dgo, i, selected) => {
|
|||
width: size,
|
||||
height: size,
|
||||
|
||||
backgroundImage: (icon => icon && `url(${icon})`)(state.icons[getPaiIcon(pai)]),
|
||||
backgroundImage: (icon => icon && `url(${icon})`)(icons[getPaiIcon(pai)]),
|
||||
},
|
||||
}, r({
|
||||
sink: SinkText,
|
||||
|
@ -586,10 +620,16 @@ const renderNodeText = state => (dgo, i, selected) => {
|
|||
}[dgo.type] || ModuleText, {
|
||||
dgo,
|
||||
pai,
|
||||
state,
|
||||
selected,
|
||||
})));
|
||||
};
|
||||
});
|
||||
|
||||
const withStorePassthrough = component => store =>
|
||||
(...args) => r(ReduxProvider, { store }, component(...args));
|
||||
|
||||
const renderNodeText = withStorePassthrough((dgo, i, selected) => {
|
||||
return r(NodeText, { dgo, selected });
|
||||
});
|
||||
|
||||
const renderEdge = props => r(Edge, {
|
||||
classSet: {
|
||||
|
@ -598,23 +638,27 @@ const renderEdge = props => r(Edge, {
|
|||
...props,
|
||||
});
|
||||
|
||||
const renderEdgeText = state => ({ data: dgo, transform, selected }) => {
|
||||
const pai = dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)({ pulse: state });
|
||||
const EdgeText = connect(
|
||||
(state, { dgo }) => ({
|
||||
pai: dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)(state),
|
||||
}),
|
||||
)(({ dgo, pai, transform, selected }) => r('foreignObject', {
|
||||
transform,
|
||||
}, r.div({
|
||||
className: 'edge-text',
|
||||
style: {
|
||||
width: size,
|
||||
height: size,
|
||||
},
|
||||
}, [
|
||||
pai && (!selected) && r(VolumeThumbnail, { pai }),
|
||||
pai && selected && r(VolumeControls, { pai }),
|
||||
r(DebugText, { dgo, pai }),
|
||||
])));
|
||||
|
||||
return r('foreignObject', {
|
||||
transform,
|
||||
}, r.div({
|
||||
className: 'edge-text',
|
||||
style: {
|
||||
width: size,
|
||||
height: size,
|
||||
},
|
||||
}, [
|
||||
pai && (!selected) && r(VolumeThumbnail, { pai, state }),
|
||||
pai && selected && r(VolumeControls, { pai, state }),
|
||||
r(DebugText, { dgo, pai, state }),
|
||||
]));
|
||||
};
|
||||
const renderEdgeText = withStorePassthrough(({ data: dgo, transform, selected }) => {
|
||||
return r(EdgeText, { dgo, transform, selected });
|
||||
});
|
||||
|
||||
const layoutEngine = new LayoutEngine();
|
||||
|
||||
|
@ -744,7 +788,7 @@ class Graph extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const pai = dgoToPai.get(node);
|
||||
const pai = getPaiByDgoFromInfos(node)(props.infos);
|
||||
if (pai) {
|
||||
if (props.preferences.hideMonitors &&
|
||||
pai.properties.device &&
|
||||
|
@ -782,16 +826,6 @@ class Graph extends React.Component {
|
|||
return filteredNodeKeys.has(edge.source) && filteredNodeKeys.has(edge.target);
|
||||
}, edges);
|
||||
|
||||
nodes.forEach(node => {
|
||||
const pai = getPaiByTypeAndIndex(node.type, node.index)({ pulse: props });
|
||||
dgoToPai.set(node, pai);
|
||||
});
|
||||
|
||||
edges.forEach(edge => {
|
||||
const pai = getPaiByTypeAndIndex(edge.type, edge.index)({ pulse: props });
|
||||
dgoToPai.set(edge, pai);
|
||||
});
|
||||
|
||||
let { selected, moved, contexted } = state;
|
||||
|
||||
if (contexted && contexted !== backgroundSymbol && selected !== contexted) {
|
||||
|
@ -841,6 +875,8 @@ class Graph extends React.Component {
|
|||
|
||||
this.graphViewElement = document.querySelector('#graph .view-wrapper');
|
||||
this.graphViewElement.setAttribute('tabindex', '-1');
|
||||
|
||||
this.props.connect();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -889,7 +925,7 @@ class Graph extends React.Component {
|
|||
}
|
||||
|
||||
onNodeMouseDown(event, data) {
|
||||
const pai = dgoToPai.get(data);
|
||||
const pai = getPaiByDgoFromInfos(data)(this.props.infos);
|
||||
if (pai && event.button === 1) {
|
||||
if (pai.type === 'sink' ||
|
||||
pai.type === 'source' ||
|
||||
|
@ -923,8 +959,8 @@ class Graph extends React.Component {
|
|||
}
|
||||
|
||||
onCreateEdge(source, target) {
|
||||
const sourcePai = dgoToPai.get(source);
|
||||
const targetPai = dgoToPai.get(target);
|
||||
const sourcePai = getPaiByDgoFromInfos(source)(this.props.infos);
|
||||
const targetPai = getPaiByDgoFromInfos(target)(this.props.infos);
|
||||
if (sourcePai && targetPai &&
|
||||
source.type === 'source' && target.type === 'sink'
|
||||
) {
|
||||
|
@ -947,7 +983,7 @@ class Graph extends React.Component {
|
|||
}
|
||||
|
||||
onEdgeMouseDown(event, data) {
|
||||
const pai = dgoToPai.get(data);
|
||||
const pai = getPaiByDgoFromInfos(data)(this.props.infos);
|
||||
if (pai && event.button === 1) {
|
||||
if (pai.type === 'sinkInput' ||
|
||||
pai.type === 'sourceOutput'
|
||||
|
@ -979,7 +1015,7 @@ class Graph extends React.Component {
|
|||
this.props.setSourceOutputMuteByIndex(pai.index, muted);
|
||||
} else if (pai.type === 'sink') {
|
||||
if (sourceBiased) {
|
||||
const sinkInputs = getSinkSinkInputs(pai)({ pulse: this.props });
|
||||
const sinkInputs = getSinkSinkInputs(pai)(this.context.store.getState());
|
||||
this.toggleAllMute(sinkInputs);
|
||||
} else {
|
||||
this.props.setSinkMute(pai.index, muted);
|
||||
|
@ -988,25 +1024,25 @@ class Graph extends React.Component {
|
|||
this.props.setSourceMute(pai.index, muted);
|
||||
} else if (pai.type === 'client') {
|
||||
if (sourceBiased) {
|
||||
const sourceOutputs = getClientSourceOutputs(pai)({ pulse: this.props });
|
||||
const sourceOutputs = getClientSourceOutputs(pai)(this.context.store.getState());
|
||||
this.toggleAllMute(sourceOutputs);
|
||||
} else {
|
||||
const sinkInputs = getClientSinkInputs(pai)({ pulse: this.props });
|
||||
const sinkInputs = getClientSinkInputs(pai)(this.context.store.getState());
|
||||
this.toggleAllMute(sinkInputs);
|
||||
}
|
||||
} else if (pai.type === 'module') {
|
||||
if (sourceBiased) {
|
||||
const sourceOutputs = getModuleSourceOutputs(pai)({ pulse: this.props });
|
||||
const sourceOutputs = getModuleSourceOutputs(pai)(this.context.store.getState());
|
||||
this.toggleAllMute(sourceOutputs);
|
||||
} else {
|
||||
const sinkInputs = getModuleSinkInputs(pai)({ pulse: this.props });
|
||||
const sinkInputs = getModuleSinkInputs(pai)(this.context.store.getState());
|
||||
this.toggleAllMute(sinkInputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDelete(selected) {
|
||||
const pai = dgoToPai.get(selected);
|
||||
const pai = getPaiByDgoFromInfos(selected)(this.props.infos);
|
||||
|
||||
if (selected.type === 'client') {
|
||||
this.props.killClientByIndex(selected.index);
|
||||
|
@ -1040,8 +1076,7 @@ class Graph extends React.Component {
|
|||
}
|
||||
|
||||
canContextMenuSetAsDefault() {
|
||||
const { contexted } = this.state;
|
||||
const pai = dgoToPai.get(contexted);
|
||||
const pai = getPaiByDgoFromInfos(this.state.contexted)(this.props.infos);
|
||||
|
||||
if (pai && pai.type === 'sink' && pai.name !== this.props.serverInfo.defaultSinkName) {
|
||||
return true;
|
||||
|
@ -1055,7 +1090,7 @@ class Graph extends React.Component {
|
|||
}
|
||||
|
||||
setAsDefault(data) {
|
||||
const pai = dgoToPai.get(data);
|
||||
const pai = getPaiByDgoFromInfos(data)(this.props.infos);
|
||||
|
||||
if (pai.type === 'sink') {
|
||||
this.props.setDefaultSinkByName(pai.name);
|
||||
|
@ -1100,21 +1135,21 @@ class Graph extends React.Component {
|
|||
if (all) {
|
||||
this.toggleAllMute(this.props.infos.sources);
|
||||
} else {
|
||||
const defaultSource = getDefaultSourcePai({ pulse: this.props });
|
||||
const defaultSource = getDefaultSourcePai(this.context.store.getState());
|
||||
this.toggleMute(defaultSource);
|
||||
}
|
||||
} else {
|
||||
if (all) { // eslint-disable-line no-lonely-if
|
||||
this.toggleAllMute(this.props.infos.sinks);
|
||||
} else {
|
||||
const defaultSink = getDefaultSinkPai({ pulse: this.props });
|
||||
const defaultSink = getDefaultSinkPai(this.context.store.getState());
|
||||
this.toggleMute(defaultSink);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const pai = dgoToPai.get(this.state.selected);
|
||||
const pai = getPaiByDgoFromInfos(this.state.selected)(this.props.infos);
|
||||
|
||||
if (!pai) {
|
||||
return;
|
||||
|
@ -1157,9 +1192,9 @@ class Graph extends React.Component {
|
|||
let pai;
|
||||
|
||||
if (this.state.selected) {
|
||||
pai = dgoToPai.get(this.state.selected);
|
||||
pai = getPaiByDgoFromInfos(this.state.selected)(this.props.infos);
|
||||
} else {
|
||||
pai = getDefaultSinkPai({ pulse: this.props });
|
||||
pai = getDefaultSinkPai(this.context.store.getState());
|
||||
}
|
||||
|
||||
if (!pai) {
|
||||
|
@ -1167,12 +1202,12 @@ class Graph extends React.Component {
|
|||
}
|
||||
|
||||
if (pai.type === 'client') {
|
||||
const sinkInputs = getClientSinkInputs(pai)({ pulse: this.props });
|
||||
const sinkInputs = getClientSinkInputs(pai)(this.context.store.getState());
|
||||
this._volumeAll(sinkInputs, direction);
|
||||
return;
|
||||
}
|
||||
if (pai.type === 'module') {
|
||||
const sinkInputs = getModuleSinkInputs(pai)({ pulse: this.props });
|
||||
const sinkInputs = getModuleSinkInputs(pai)(this.context.store.getState());
|
||||
this._volumeAll(sinkInputs, direction);
|
||||
return;
|
||||
}
|
||||
|
@ -1407,10 +1442,10 @@ class Graph extends React.Component {
|
|||
renderDefs,
|
||||
|
||||
renderNode,
|
||||
renderNodeText: renderNodeText(this.props),
|
||||
renderNodeText: renderNodeText(this.context.store),
|
||||
|
||||
renderEdge,
|
||||
renderEdgeText: renderEdgeText(this.props),
|
||||
renderEdgeText: renderEdgeText(this.context.store),
|
||||
}),
|
||||
|
||||
this.state.contexted && (
|
||||
|
@ -1438,12 +1473,16 @@ class Graph extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
Graph.contextTypes = {
|
||||
store: PropTypes.any,
|
||||
};
|
||||
|
||||
module.exports = connect(
|
||||
state => ({
|
||||
serverInfo: state.pulse.serverInfo,
|
||||
serverInfo: state.pulse[primaryPulseServer].serverInfo,
|
||||
|
||||
objects: state.pulse.objects,
|
||||
infos: state.pulse.infos,
|
||||
objects: state.pulse[primaryPulseServer].objects,
|
||||
infos: state.pulse[primaryPulseServer].infos,
|
||||
|
||||
derivations: {
|
||||
monitorSources: getDerivedMonitorSources(state),
|
||||
|
|
|
@ -19,6 +19,8 @@ const weakmapId = require('../../utils/weakmap-id');
|
|||
|
||||
const { pulse: pulseActions } = require('../../actions');
|
||||
|
||||
const { primaryPulseServer } = require('../../reducers/pulse');
|
||||
|
||||
const actionTypeText = {
|
||||
[pulseActions.ready]: 'Connected to PulseAudio',
|
||||
[pulseActions.close]: 'Disconnected from PulseAudio',
|
||||
|
@ -84,6 +86,6 @@ Log.defaultProps = {
|
|||
|
||||
module.exports = connect(
|
||||
state => ({
|
||||
log: state.pulse.log,
|
||||
log: state.pulse[primaryPulseServer].log,
|
||||
}),
|
||||
)(Log);
|
||||
|
|
|
@ -21,7 +21,7 @@ const WindowMenu = props => r(WindowMenuBase, [
|
|||
label: 'File',
|
||||
}, [
|
||||
r(MenuItem, {
|
||||
label: 'Connect to server...',
|
||||
label: 'Open a server...',
|
||||
accelerator: 'CommandOrControl+N',
|
||||
onClick: props.openConnectToServerModal,
|
||||
}),
|
||||
|
|
|
@ -20,7 +20,7 @@ class ConnectToServerModal extends React.PureComponent {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: 'tcp:remote-computer.lan',
|
||||
address: props.defaults.address,
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
@ -33,7 +33,7 @@ class ConnectToServerModal extends React.PureComponent {
|
|||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: merge(process.env, {
|
||||
PULSE_SERVER: this.state.value,
|
||||
PULSE_SERVER: this.state.address,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -62,8 +62,8 @@ class ConnectToServerModal extends React.PureComponent {
|
|||
r(Input, {
|
||||
style: { width: '100%' },
|
||||
autoFocus: true,
|
||||
value: this.state.value,
|
||||
onChange: e => this.setState({ value: e.target.value }),
|
||||
value: this.state.address,
|
||||
onChange: e => this.setState({ address: e.target.value }),
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
|
@ -84,4 +84,10 @@ class ConnectToServerModal extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
ConnectToServerModal.defaultProps = {
|
||||
defaults: {
|
||||
address: 'tcp:remote-computer.lan',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ConnectToServerModal;
|
||||
|
|
|
@ -9,6 +9,7 @@ const {
|
|||
const r = require('r-dom');
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const Modal = require('react-modal');
|
||||
|
||||
|
@ -26,6 +27,8 @@ const {
|
|||
|
||||
const { modules } = require('../../constants/pulse');
|
||||
|
||||
const { primaryPulseServer } = require('../../reducers/pulse');
|
||||
|
||||
const ConnectToServerModal = require('./connect-to-server');
|
||||
const ConfirmationModal = require('./confirmation');
|
||||
const NewGraphObjectModal = require('./new-graph-object');
|
||||
|
@ -80,7 +83,7 @@ class Modals extends React.PureComponent {
|
|||
return continuation();
|
||||
}
|
||||
|
||||
const target = f(...args);
|
||||
const target = f.apply(this, args);
|
||||
|
||||
if (!target) {
|
||||
return continuation();
|
||||
|
@ -93,7 +96,7 @@ class Modals extends React.PureComponent {
|
|||
});
|
||||
}, {
|
||||
unloadModuleByIndex(index) {
|
||||
const pai = getPaiByTypeAndIndex('module', index)({ pulse: props });
|
||||
const pai = getPaiByTypeAndIndex('module', index)(this.context.store.getState());
|
||||
|
||||
if (pai && path([ pai.name, 'confirmUnload' ], modules)) {
|
||||
return pai;
|
||||
|
@ -105,8 +108,11 @@ class Modals extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
openConnectToServerModal() {
|
||||
this.setState({ connectToServerModalOpen: true });
|
||||
openConnectToServerModal(modalDefaults) {
|
||||
this.setState({
|
||||
connectToServerModalOpen: true,
|
||||
modalDefaults,
|
||||
});
|
||||
}
|
||||
|
||||
openNewGraphObjectModal() {
|
||||
|
@ -145,9 +151,11 @@ class Modals extends React.PureComponent {
|
|||
toggle,
|
||||
}),
|
||||
|
||||
r(ConnectToServerModal, {
|
||||
isOpen: this.state.connectToServerModalOpen,
|
||||
this.state.connectToServerModalOpen && r(ConnectToServerModal, {
|
||||
isOpen: true,
|
||||
onRequestClose: this.handleCancel,
|
||||
|
||||
defaults: this.state.modalDefaults,
|
||||
}),
|
||||
|
||||
r(NewGraphObjectModal, {
|
||||
|
@ -172,9 +180,13 @@ class Modals extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
Modals.contextTypes = {
|
||||
store: PropTypes.any,
|
||||
};
|
||||
|
||||
module.exports = connect(
|
||||
state => ({
|
||||
infos: state.pulse.infos,
|
||||
infos: state.pulse[primaryPulseServer].infos,
|
||||
preferences: state.preferences,
|
||||
}),
|
||||
dispatch => bindActionCreators(merge(pulseActions, preferencesActions), dispatch),
|
||||
|
|
|
@ -24,19 +24,19 @@ const {
|
|||
} = require('../../actions');
|
||||
const { formatModuleArgs } = require('../../utils/module-args');
|
||||
|
||||
const { getRemoteServerByAddress } = require('../../selectors');
|
||||
const { primaryPulseServer } = require('../../reducers/pulse');
|
||||
|
||||
const Button = require('../button');
|
||||
const Label = require('../label');
|
||||
|
||||
const RemoteServer = connect(
|
||||
(state, props) => ({
|
||||
remoteServer: getRemoteServerByAddress(props.address)(state),
|
||||
remoteServer: state.pulse[props.address],
|
||||
}),
|
||||
dispatch => ({
|
||||
actions: bindActionCreators(merge(pulseActions, preferencesActions), dispatch),
|
||||
}),
|
||||
)(({ address, remoteServer = {}, actions }) => {
|
||||
)(({ address, remoteServer = {}, actions, ...props }) => {
|
||||
const { targetState, state } = remoteServer;
|
||||
const hostname = path([ 'serverInfo', 'hostname' ], remoteServer);
|
||||
|
||||
|
@ -44,35 +44,53 @@ const RemoteServer = connect(
|
|||
r.div({
|
||||
style: { display: 'flex', justifyContent: 'space-between' },
|
||||
}, [
|
||||
r(Label, {
|
||||
userSelect: true,
|
||||
}, [
|
||||
hostname || address,
|
||||
r.div([
|
||||
r.div([ hostname ]),
|
||||
r.code(address),
|
||||
]),
|
||||
|
||||
targetState === 'ready' ? r(Button, {
|
||||
onClick: () => {
|
||||
actions.remoteServerDisconnect(address);
|
||||
},
|
||||
}, 'Disconnect') : r(React.Fragment, [
|
||||
r.div([
|
||||
r(Button, {
|
||||
onClick: () => {
|
||||
actions.remoteServerDisconnect(address);
|
||||
actions.setDelete('remoteServerAddresses', address);
|
||||
props.openConnectToServerModal({ address });
|
||||
},
|
||||
}, 'Forget'),
|
||||
}, 'Open'),
|
||||
|
||||
r(Button, {
|
||||
' ',
|
||||
|
||||
targetState === 'ready' ? r(Button, {
|
||||
onClick: () => {
|
||||
actions.remoteServerConnect(address);
|
||||
actions.disconnect(address);
|
||||
},
|
||||
}, 'Connect'),
|
||||
}, 'Disconnect') : r(React.Fragment, [
|
||||
r(Button, {
|
||||
onClick: () => {
|
||||
actions.disconnect(address);
|
||||
actions.setDelete('remoteServerAddresses', address);
|
||||
},
|
||||
}, 'Forget'),
|
||||
|
||||
' ',
|
||||
|
||||
r(Button, {
|
||||
onClick: () => {
|
||||
actions.connect(address);
|
||||
},
|
||||
}, 'Connect'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
state === 'ready' ? r(React.Fragment, [
|
||||
// TODO
|
||||
]) : targetState === 'ready' ? r(Label, [
|
||||
state === 'ready' ? r(Label, {
|
||||
passive: true,
|
||||
}, [
|
||||
keys(remoteServer.objects.sinks).length,
|
||||
' sinks and ',
|
||||
keys(remoteServer.objects.sources).length,
|
||||
' sources.',
|
||||
]) : targetState === 'ready' ? r(Label, {
|
||||
passive: true,
|
||||
}, [
|
||||
'Connecting...',
|
||||
]) : null,
|
||||
]);
|
||||
|
@ -188,7 +206,10 @@ class Cards extends React.Component {
|
|||
'Remote servers:',
|
||||
]),
|
||||
|
||||
...map(address => r(RemoteServer, { address }), remoteServerAddresses),
|
||||
...map(address => r(RemoteServer, {
|
||||
address,
|
||||
openConnectToServerModal: this.props.openConnectToServerModal,
|
||||
}), remoteServerAddresses),
|
||||
]) : r(Label, [
|
||||
'No known servers',
|
||||
]),
|
||||
|
@ -212,7 +233,7 @@ class Cards extends React.Component {
|
|||
|
||||
module.exports = connect(
|
||||
state => ({
|
||||
modules: state.pulse.infos.modules,
|
||||
modules: state.pulse[primaryPulseServer].infos.modules,
|
||||
preferences: state.preferences,
|
||||
}),
|
||||
dispatch => ({
|
||||
|
|
|
@ -7,6 +7,8 @@ const r = require('r-dom');
|
|||
|
||||
const { connect } = require('react-redux');
|
||||
|
||||
const { primaryPulseServer } = require('../../reducers/pulse');
|
||||
|
||||
const localHostname = os.hostname();
|
||||
const { username: localUsername } = os.userInfo();
|
||||
|
||||
|
@ -27,6 +29,6 @@ class ServerInfo extends React.Component {
|
|||
|
||||
module.exports = connect(
|
||||
state => ({
|
||||
serverInfo: state.pulse.serverInfo,
|
||||
serverInfo: state.pulse[primaryPulseServer].serverInfo,
|
||||
}),
|
||||
)(ServerInfo);
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
|
||||
const {
|
||||
map,
|
||||
} = require('ramda');
|
||||
|
||||
const r = require('r-dom');
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@futpib/paclient": "^0.0.8",
|
||||
"@futpib/paclient": "^0.0.9",
|
||||
"@futpib/react-electron-menu": "^0.3.1",
|
||||
"bluebird": "^3.5.3",
|
||||
"camelcase": "^5.0.0",
|
||||
|
@ -35,6 +35,7 @@
|
|||
"freedesktop-icons": "^0.1.0",
|
||||
"ini": "^1.3.5",
|
||||
"mathjs": "^5.2.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"r-dom": "^2.4.0",
|
||||
"ramda": "^0.25.0",
|
||||
"react": "^16.6.0",
|
||||
|
|
|
@ -9,7 +9,7 @@ const {
|
|||
equals,
|
||||
takeLast,
|
||||
over,
|
||||
lensPath,
|
||||
lensProp,
|
||||
} = require('ramda');
|
||||
|
||||
const { combineReducers } = require('redux');
|
||||
|
@ -20,8 +20,11 @@ const { pulse } = require('../actions');
|
|||
|
||||
const { things } = require('../constants/pulse');
|
||||
|
||||
const initialState = {
|
||||
const primaryPulseServer = '__PRIMARY_PULSE_SERVER__';
|
||||
|
||||
const serverInitialState = {
|
||||
state: 'closed',
|
||||
targetState: 'closed',
|
||||
|
||||
serverInfo: {},
|
||||
|
||||
|
@ -29,17 +32,22 @@ const initialState = {
|
|||
infos: fromPairs(map(({ key }) => [ key, {} ], things)),
|
||||
|
||||
log: { items: [] },
|
||||
|
||||
remoteServers: {},
|
||||
};
|
||||
|
||||
const initialState = {};
|
||||
|
||||
const logMaxItems = 3;
|
||||
|
||||
const reducer = combineReducers({
|
||||
const serverReducer = combineReducers({
|
||||
state: handleActions({
|
||||
[pulse.ready]: always('ready'),
|
||||
[pulse.close]: always('closed'),
|
||||
}, initialState.state),
|
||||
}, serverInitialState.state),
|
||||
|
||||
targetState: handleActions({
|
||||
[pulse.connect]: always('ready'),
|
||||
[pulse.disconnect]: always('closed'),
|
||||
}, serverInitialState.targetState),
|
||||
|
||||
serverInfo: handleActions({
|
||||
[pulse.serverInfo]: (state, { payload }) => {
|
||||
|
@ -47,8 +55,8 @@ const reducer = combineReducers({
|
|||
state :
|
||||
payload;
|
||||
},
|
||||
[pulse.close]: always(initialState.serverInfo),
|
||||
}, initialState.serverInfo),
|
||||
[pulse.close]: always(serverInitialState.serverInfo),
|
||||
}, serverInitialState.serverInfo),
|
||||
|
||||
objects: combineReducers(fromPairs(map(({ key, type }) => [ key, handleActions({
|
||||
[pulse.new]: (state, { payload }) => {
|
||||
|
@ -94,8 +102,8 @@ const reducer = combineReducers({
|
|||
}
|
||||
return state;
|
||||
},
|
||||
[pulse.close]: () => initialState.objects[key],
|
||||
}, initialState.objects[key]) ], things))),
|
||||
[pulse.close]: () => serverInitialState.objects[key],
|
||||
}, serverInitialState.objects[key]) ], things))),
|
||||
|
||||
infos: combineReducers(fromPairs(map(({ key, type }) => [ key, handleActions({
|
||||
[pulse.remove]: (state, { payload }) => {
|
||||
|
@ -112,8 +120,8 @@ const reducer = combineReducers({
|
|||
[payload.index]: payload,
|
||||
});
|
||||
},
|
||||
[pulse.close]: () => initialState.objects[key],
|
||||
}, initialState.infos[key]) ], things))),
|
||||
[pulse.close]: () => serverInitialState.objects[key],
|
||||
}, serverInitialState.infos[key]) ], things))),
|
||||
|
||||
log: combineReducers({
|
||||
items: handleActions({
|
||||
|
@ -129,16 +137,18 @@ const reducer = combineReducers({
|
|||
type: 'info',
|
||||
action: type,
|
||||
})),
|
||||
}, initialState.log.items),
|
||||
}, serverInitialState.log.items),
|
||||
}),
|
||||
|
||||
remoteServers: handleActions({
|
||||
[pulse.remoteServerConnect]: (state, { payload }) => over(lensPath([ payload, 'targetState' ]), always('ready'), state),
|
||||
[pulse.remoteServerDisconnect]: (state, { payload }) => over(lensPath([ payload, 'targetState' ]), always('closed'), state),
|
||||
}, initialState.remoteServers),
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
const { pulseServerId = primaryPulseServer } = action.meta || {};
|
||||
return over(lensProp(pulseServerId), s => serverReducer(s, action), state);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initialState,
|
||||
reducer,
|
||||
|
||||
primaryPulseServer,
|
||||
};
|
||||
|
|
|
@ -15,37 +15,43 @@ const { createSelector } = require('reselect');
|
|||
|
||||
const { things } = require('../constants/pulse');
|
||||
|
||||
const { primaryPulseServer } = require('../reducers/pulse');
|
||||
|
||||
const storeKeyByType = map(prop('key'), indexBy(prop('type'), things));
|
||||
|
||||
const getPaiByTypeAndIndex = (type, index) => state => path([ storeKeyByType[type], index ], state.pulse.infos);
|
||||
const getPaiByTypeAndIndex = (type, index, pulseServerId = primaryPulseServer) =>
|
||||
state => path([ pulseServerId, 'infos', storeKeyByType[type], index ], state.pulse);
|
||||
|
||||
const getClientSinkInputs = client => state => pickBy(
|
||||
const getPaiByTypeAndIndexFromInfos = (type, index) => infos => path([ storeKeyByType[type], index ], infos);
|
||||
const getPaiByDgoFromInfos = ({ type, index }) => infos => path([ storeKeyByType[type], index ], infos);
|
||||
|
||||
const getClientSinkInputs = (client, pulseServerId = primaryPulseServer) => state => pickBy(
|
||||
si => si.clientIndex === client.index,
|
||||
state.pulse.infos.sinkInputs,
|
||||
state.pulse[pulseServerId].infos.sinkInputs,
|
||||
);
|
||||
|
||||
const getModuleSinkInputs = module => state => pickBy(
|
||||
const getModuleSinkInputs = (module, pulseServerId = primaryPulseServer) => state => pickBy(
|
||||
si => si.moduleIndex === module.index,
|
||||
state.pulse.infos.sinkInputs,
|
||||
state.pulse[pulseServerId].infos.sinkInputs,
|
||||
);
|
||||
|
||||
const getClientSourceOutputs = client => state => pickBy(
|
||||
const getClientSourceOutputs = (client, pulseServerId = primaryPulseServer) => state => pickBy(
|
||||
so => so.clientIndex === client.index,
|
||||
state.pulse.infos.sourceOutputs,
|
||||
state.pulse[pulseServerId].infos.sourceOutputs,
|
||||
);
|
||||
|
||||
const getModuleSourceOutputs = module => state => pickBy(
|
||||
const getModuleSourceOutputs = (module, pulseServerId = primaryPulseServer) => state => pickBy(
|
||||
so => so.moduleIndex === module.index,
|
||||
state.pulse.infos.sourceOutputs,
|
||||
state.pulse[pulseServerId].infos.sourceOutputs,
|
||||
);
|
||||
|
||||
const getSinkSinkInputs = sink => state => pickBy(
|
||||
const getSinkSinkInputs = (sink, pulseServerId = primaryPulseServer) => state => pickBy(
|
||||
si => si.sinkIndex === sink.index,
|
||||
state.pulse.infos.sinkInputs,
|
||||
state.pulse[pulseServerId].infos.sinkInputs,
|
||||
);
|
||||
|
||||
const getDerivedMonitorSources = createSelector(
|
||||
state => state.pulse.infos.sources,
|
||||
state => state.pulse[primaryPulseServer].infos.sources,
|
||||
sources => map(source => ({
|
||||
index: source.index,
|
||||
type: 'monitorSource',
|
||||
|
@ -55,21 +61,22 @@ const getDerivedMonitorSources = createSelector(
|
|||
);
|
||||
|
||||
const getDefaultSourcePai = createSelector(
|
||||
state => state.pulse.infos.sources,
|
||||
state => state.pulse.serverInfo.defaultSourceName,
|
||||
state => state.pulse[primaryPulseServer].infos.sources,
|
||||
state => state.pulse[primaryPulseServer].serverInfo.defaultSourceName,
|
||||
(sources, defaultSourceName) => find(propEq('name', defaultSourceName), values(sources)),
|
||||
);
|
||||
|
||||
const getDefaultSinkPai = createSelector(
|
||||
state => state.pulse.infos.sinks,
|
||||
state => state.pulse.serverInfo.defaultSinkName,
|
||||
state => state.pulse[primaryPulseServer].infos.sinks,
|
||||
state => state.pulse[primaryPulseServer].serverInfo.defaultSinkName,
|
||||
(sinks, defaultSinkName) => find(propEq('name', defaultSinkName), values(sinks)),
|
||||
);
|
||||
|
||||
const getRemoteServerByAddress = address => state => state.pulse.remoteServers[address];
|
||||
|
||||
module.exports = {
|
||||
getPaiByTypeAndIndex,
|
||||
getPaiByTypeAndIndexFromInfos,
|
||||
getPaiByDgoFromInfos,
|
||||
|
||||
getDerivedMonitorSources,
|
||||
|
||||
getClientSinkInputs,
|
||||
|
@ -82,6 +89,4 @@ module.exports = {
|
|||
|
||||
getDefaultSinkPai,
|
||||
getDefaultSourcePai,
|
||||
|
||||
getRemoteServerByAddress,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
|
||||
const {
|
||||
difference,
|
||||
keys,
|
||||
filter,
|
||||
values,
|
||||
propEq,
|
||||
compose,
|
||||
indexBy,
|
||||
} = require('ramda');
|
||||
|
||||
const Bluebird = require('bluebird');
|
||||
|
||||
const PAClient = require('@futpib/paclient');
|
||||
|
@ -11,6 +21,10 @@ const { things } = require('../constants/pulse');
|
|||
|
||||
const { getPaiByTypeAndIndex } = require('../selectors');
|
||||
|
||||
const { primaryPulseServer } = require('../reducers/pulse');
|
||||
|
||||
const { parseModuleArgs, formatModuleArgs } = require('../utils/module-args');
|
||||
|
||||
function getFnFromType(type) {
|
||||
let fn;
|
||||
switch (type) {
|
||||
|
@ -48,7 +62,11 @@ function setSourceOutputChannelVolume(pa, store, index, channelIndex, volume, cb
|
|||
pa.setSourceOutputVolumesByIndex(index, pai.channelVolumes.map((v, i) => i === channelIndex ? volume : v), cb);
|
||||
}
|
||||
|
||||
module.exports = store => {
|
||||
const createPulseClient = (store, pulseServerId = primaryPulseServer) => {
|
||||
let state = store.getState();
|
||||
|
||||
const getPulseServerState = (s = state) => s.pulse[pulseServerId] || {};
|
||||
|
||||
const pa = new PAClient();
|
||||
|
||||
const getInfo = (type, index) => {
|
||||
|
@ -72,13 +90,13 @@ module.exports = store => {
|
|||
throw err;
|
||||
}
|
||||
info.type = info.type || type;
|
||||
store.dispatch(pulseActions.info(info));
|
||||
store.dispatch(pulseActions.info(info, pulseServerId));
|
||||
});
|
||||
};
|
||||
|
||||
pa
|
||||
.on('ready', () => {
|
||||
store.dispatch(pulseActions.ready());
|
||||
store.dispatch(pulseActions.ready(pulseServerId));
|
||||
pa.subscribe('all');
|
||||
|
||||
getServerInfo();
|
||||
|
@ -89,14 +107,14 @@ module.exports = store => {
|
|||
infos.forEach(info => {
|
||||
const { index } = info;
|
||||
info.type = info.type || type;
|
||||
store.dispatch(pulseActions.new({ type, index }));
|
||||
store.dispatch(pulseActions.info(info));
|
||||
store.dispatch(pulseActions.new({ type, index }, pulseServerId));
|
||||
store.dispatch(pulseActions.info(info, pulseServerId));
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.on('close', () => {
|
||||
store.dispatch(pulseActions.close());
|
||||
store.dispatch(pulseActions.close(pulseServerId));
|
||||
reconnect();
|
||||
})
|
||||
.on('new', (type, index) => {
|
||||
|
@ -104,7 +122,7 @@ module.exports = store => {
|
|||
getServerInfo();
|
||||
return;
|
||||
}
|
||||
store.dispatch(pulseActions.new({ type, index }));
|
||||
store.dispatch(pulseActions.new({ type, index }, pulseServerId));
|
||||
getInfo(type, index);
|
||||
})
|
||||
.on('change', (type, index) => {
|
||||
|
@ -112,20 +130,33 @@ module.exports = store => {
|
|||
getServerInfo();
|
||||
return;
|
||||
}
|
||||
store.dispatch(pulseActions.change({ type, index }));
|
||||
store.dispatch(pulseActions.change({ type, index }, pulseServerId));
|
||||
getInfo(type, index);
|
||||
})
|
||||
.on('remove', (type, index) => {
|
||||
store.dispatch(pulseActions.remove({ type, index }));
|
||||
store.dispatch(pulseActions.remove({ type, index }, pulseServerId));
|
||||
})
|
||||
.on('error', error => {
|
||||
handleError(error);
|
||||
});
|
||||
|
||||
const reconnect = () => new Bluebird((resolve, reject) => {
|
||||
const server = getPulseServerState();
|
||||
if (server.targetState !== 'ready') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
pa.once('ready', resolve);
|
||||
pa.once('error', reject);
|
||||
pa.connect();
|
||||
|
||||
if (pulseServerId === primaryPulseServer) {
|
||||
pa.connect();
|
||||
} else {
|
||||
pa.connect({
|
||||
serverString: pulseServerId,
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error.message === 'Unable to connect to PulseAudio server') {
|
||||
return Bluebird.delay(5000).then(reconnect);
|
||||
|
@ -133,14 +164,12 @@ module.exports = store => {
|
|||
throw error;
|
||||
});
|
||||
|
||||
reconnect();
|
||||
|
||||
const getServerInfo = () => {
|
||||
pa.getServerInfo((err, info) => {
|
||||
if (err) {
|
||||
handleError(err);
|
||||
} else {
|
||||
store.dispatch(pulseActions.serverInfo(info));
|
||||
store.dispatch(pulseActions.serverInfo(info, pulseServerId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -152,7 +181,7 @@ module.exports = store => {
|
|||
|
||||
console.error(error);
|
||||
|
||||
store.dispatch(pulseActions.error(error));
|
||||
store.dispatch(pulseActions.error(error, pulseServerId));
|
||||
};
|
||||
|
||||
const handlePulseActions = handleActions({
|
||||
|
@ -250,10 +279,120 @@ module.exports = store => {
|
|||
},
|
||||
}, null);
|
||||
|
||||
return {
|
||||
handleAction: action => handlePulseActions(null, action),
|
||||
|
||||
storeWillUpdate(prevState, nextState) {
|
||||
state = nextState;
|
||||
const prev = getPulseServerState(prevState);
|
||||
const next = getPulseServerState(nextState);
|
||||
|
||||
if (prev === next) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prev.targetState !== next.targetState) {
|
||||
if (next.targetState === 'ready') {
|
||||
reconnect();
|
||||
} else if (next.targetState === 'closed') {
|
||||
pa.end();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const tunnelAttempts = {};
|
||||
const tunnelAttemptTimeout = 15000;
|
||||
const isNotMonitor = s => s.monitorSourceIndex < 0;
|
||||
const updateTunnels = (dispatch, primaryState, remoteServerId, remoteState) => {
|
||||
const sourceTunnels = compose(
|
||||
indexBy(m => parseModuleArgs(m.args).source),
|
||||
filter(propEq('name', 'module-tunnel-source')),
|
||||
values,
|
||||
)(primaryState.infos.modules);
|
||||
const sinkTunnels = compose(
|
||||
indexBy(m => parseModuleArgs(m.args).sink),
|
||||
filter(propEq('name', 'module-tunnel-sink')),
|
||||
values,
|
||||
)(primaryState.infos.modules);
|
||||
|
||||
const remoteSources = filter(isNotMonitor, values(remoteState.infos.sources));
|
||||
const remoteSinks = values(remoteState.infos.sinks);
|
||||
|
||||
// FIXME: BUG: sounce/sink name collisions are possible, should also check server id
|
||||
|
||||
remoteSinks.forEach(sink => {
|
||||
if ((tunnelAttempts[sink.name] || 0) + tunnelAttemptTimeout > Date.now()) {
|
||||
return;
|
||||
}
|
||||
if (!sinkTunnels[sink.name]) {
|
||||
tunnelAttempts[sink.name] = Date.now();
|
||||
dispatch(pulseActions.loadModule('module-tunnel-sink', formatModuleArgs({
|
||||
server: remoteServerId,
|
||||
sink: sink.name,
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
remoteSources.forEach(source => {
|
||||
if ((tunnelAttempts[source.name] || 0) + tunnelAttemptTimeout > Date.now()) {
|
||||
return;
|
||||
}
|
||||
if (!sourceTunnels[source.name]) {
|
||||
tunnelAttempts[source.name] = Date.now();
|
||||
dispatch(pulseActions.loadModule('module-tunnel-source', formatModuleArgs({
|
||||
server: remoteServerId,
|
||||
source: source.name,
|
||||
})));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = store => {
|
||||
const clients = {
|
||||
[primaryPulseServer]: createPulseClient(store, primaryPulseServer),
|
||||
};
|
||||
|
||||
return next => action => {
|
||||
const { pulseServerId = primaryPulseServer } = action.meta || {};
|
||||
|
||||
const prevState = store.getState();
|
||||
|
||||
const ret = next(action);
|
||||
|
||||
handlePulseActions(null, action);
|
||||
const nextState = store.getState();
|
||||
|
||||
const newPulseServerIds = difference(keys(nextState.pulse), keys(clients));
|
||||
|
||||
newPulseServerIds.forEach(pulseServerId => {
|
||||
clients[pulseServerId] = createPulseClient(store, pulseServerId);
|
||||
});
|
||||
|
||||
const client = clients[pulseServerId];
|
||||
if (client) {
|
||||
client.handleAction(action);
|
||||
if (prevState !== nextState) {
|
||||
client.storeWillUpdate(prevState, nextState);
|
||||
}
|
||||
}
|
||||
|
||||
const primaryState = nextState.pulse[primaryPulseServer];
|
||||
keys(nextState.pulse).forEach(pulseServerId => {
|
||||
if (pulseServerId === primaryPulseServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteState = nextState.pulse[pulseServerId];
|
||||
|
||||
if (primaryState.state === 'ready' &&
|
||||
remoteState.state === 'ready' &&
|
||||
primaryState.targetState === 'ready' &&
|
||||
primaryState.targetState === 'ready'
|
||||
) {
|
||||
updateTunnels(store.dispatch, primaryState, pulseServerId, remoteState);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
const {
|
||||
map,
|
||||
toPairs,
|
||||
fromPairs,
|
||||
} = require('ramda');
|
||||
|
||||
const separators = {
|
||||
|
@ -18,4 +19,10 @@ const formatModuleArgs = object => map(([ k, v ]) => {
|
|||
return `${k}=${v}`;
|
||||
}, toPairs(object)).join(' ');
|
||||
|
||||
module.exports = { formatModuleArgs };
|
||||
const parseModuleArgs = (args = '') => fromPairs(args.split(' ').map(arg => {
|
||||
const [ key, ...value ] = arg.split('=');
|
||||
// TODO: `separators`
|
||||
return [ key, value.join('=') ];
|
||||
}));
|
||||
|
||||
module.exports = { formatModuleArgs, parseModuleArgs };
|
||||
|
|
|
@ -156,10 +156,10 @@
|
|||
dependencies:
|
||||
arrify "^1.0.1"
|
||||
|
||||
"@futpib/paclient@^0.0.8":
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@futpib/paclient/-/paclient-0.0.8.tgz#c7530d2175798aba9ca21e3d0312bbfa3fd18a44"
|
||||
integrity sha512-Uaup+EdAWKtfuos4wBlDuUWeZfj/OtTtllGpniFTElEiD+MDvryzq64t/Ibokt3a5TkVY3M2O69YZaGH2J6Gqw==
|
||||
"@futpib/paclient@^0.0.9":
|
||||
version "0.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@futpib/paclient/-/paclient-0.0.9.tgz#406949cea4543725ab4d25267dad8e4cf8a8a423"
|
||||
integrity sha512-uNMUcd4XXJy1HrT7TP/dxCx6KUquBZyF0UQH7dWDT0VH/tmYmkpOHnBUcVjGVSG3CWWtAlqtw+vsk+N+1aBRMw==
|
||||
|
||||
"@futpib/react-electron-menu@^0.3.1":
|
||||
version "0.3.1"
|
||||
|
|
Loading…
Reference in New Issue
Block a user