Add volumes
This commit is contained in:
parent
fa26db923c
commit
f756bc9626
|
@ -21,5 +21,15 @@ module.exports = createActionCreators({
|
||||||
KILL_SOURCE_OUTPUT_BY_INDEX: sourceOutputIndex => ({ sourceOutputIndex }),
|
KILL_SOURCE_OUTPUT_BY_INDEX: sourceOutputIndex => ({ sourceOutputIndex }),
|
||||||
|
|
||||||
UNLOAD_MODULE_BY_INDEX: moduleIndex => ({ moduleIndex }),
|
UNLOAD_MODULE_BY_INDEX: moduleIndex => ({ moduleIndex }),
|
||||||
|
|
||||||
|
SET_SINK_VOLUMES: (index, channelVolumes) => ({ index, channelVolumes }),
|
||||||
|
SET_SOURCE_VOLUMES: (index, channelVolumes) => ({ index, channelVolumes }),
|
||||||
|
SET_SINK_INPUT_VOLUMES: (index, channelVolumes) => ({ index, channelVolumes }),
|
||||||
|
SET_SOURCE_OUTPUT_VOLUMES: (index, channelVolumes) => ({ index, channelVolumes }),
|
||||||
|
|
||||||
|
SET_SINK_CHANNEL_VOLUME: (index, channelIndex, volume) => ({ index, channelIndex, volume }),
|
||||||
|
SET_SOURCE_CHANNEL_VOLUME: (index, channelIndex, volume) => ({ index, channelIndex, volume }),
|
||||||
|
SET_SINK_INPUT_CHANNEL_VOLUME: (index, channelIndex, volume) => ({ index, channelIndex, volume }),
|
||||||
|
SET_SOURCE_OUTPUT_CHANNEL_VOLUME: (index, channelIndex, volume) => ({ index, channelIndex, volume }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ const r = require('r-dom');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
GraphView: GraphViewBase,
|
GraphView: GraphViewBase,
|
||||||
|
Node: NodeBase,
|
||||||
Edge: EdgeBase,
|
Edge: EdgeBase,
|
||||||
GraphUtils,
|
GraphUtils,
|
||||||
} = require('react-digraph');
|
} = require('react-digraph');
|
||||||
|
@ -18,8 +19,11 @@ class GraphView extends GraphViewBase {
|
||||||
_super_handleNodeMove: this.handleNodeMove,
|
_super_handleNodeMove: this.handleNodeMove,
|
||||||
handleNodeMove: this.constructor.prototype.handleNodeMove.bind(this),
|
handleNodeMove: this.constructor.prototype.handleNodeMove.bind(this),
|
||||||
|
|
||||||
_super_getEdgeComponent: this.handleNodeMove,
|
_super_getEdgeComponent: this.getEdgeComponent,
|
||||||
getEdgeComponent: this.constructor.prototype.getEdgeComponent.bind(this),
|
getEdgeComponent: this.constructor.prototype.getEdgeComponent.bind(this),
|
||||||
|
|
||||||
|
_super_getNodeComponent: this.getNodeComponent,
|
||||||
|
getNodeComponent: this.constructor.prototype.getNodeComponent.bind(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +49,29 @@ class GraphView extends GraphViewBase {
|
||||||
super.componentDidUpdate(prevProps, prevState);
|
super.componentDidUpdate(prevProps, prevState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNodeComponent(id, node) {
|
||||||
|
const { nodeTypes, nodeSubtypes, nodeSize, renderNode, renderNodeText, nodeKey } = this.props;
|
||||||
|
return r(Node, {
|
||||||
|
key: id,
|
||||||
|
id,
|
||||||
|
data: node,
|
||||||
|
nodeTypes,
|
||||||
|
nodeSize,
|
||||||
|
nodeKey,
|
||||||
|
nodeSubtypes,
|
||||||
|
onNodeMouseEnter: this.handleNodeMouseEnter,
|
||||||
|
onNodeMouseLeave: this.handleNodeMouseLeave,
|
||||||
|
onNodeMove: this.handleNodeMove,
|
||||||
|
onNodeUpdate: this.handleNodeUpdate,
|
||||||
|
onNodeSelected: this.handleNodeSelected,
|
||||||
|
renderNode,
|
||||||
|
renderNodeText,
|
||||||
|
isSelected: this.state.selectedNodeObj.node === node,
|
||||||
|
layoutEngine: this.layoutEngine,
|
||||||
|
viewWrapperElem: this.viewWrapper.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleNodeMove(position, nodeId, shiftKey) {
|
handleNodeMove(position, nodeId, shiftKey) {
|
||||||
this._super_handleNodeMove(position, nodeId, shiftKey);
|
this._super_handleNodeMove(position, nodeId, shiftKey);
|
||||||
if (this.props.onNodeMove) {
|
if (this.props.onNodeMove) {
|
||||||
|
@ -52,7 +79,7 @@ class GraphView extends GraphViewBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEdgeComponent(edge) {
|
getEdgeComponent(edge, nodeMoving) {
|
||||||
if (!this.props.renderEdge) {
|
if (!this.props.renderEdge) {
|
||||||
return this._super_getEdgeComponent(edge);
|
return this._super_getEdgeComponent(edge);
|
||||||
}
|
}
|
||||||
|
@ -74,13 +101,46 @@ class GraphView extends GraphViewBase {
|
||||||
targetNode: targetNode || targetPosition,
|
targetNode: targetNode || targetPosition,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
isSelected: selected,
|
isSelected: selected,
|
||||||
|
nodeMoving,
|
||||||
renderEdgeText,
|
renderEdgeText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncRenderEdge(edge, nodeMoving = false) {
|
||||||
|
if (!edge.source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idVar = edge.target ? `${edge.source}-${edge.target}` : 'custom';
|
||||||
|
const id = `edge-${idVar}`;
|
||||||
|
const element = this.getEdgeComponent(edge, nodeMoving);
|
||||||
|
this.renderEdge(id, element, edge, nodeMoving);
|
||||||
|
|
||||||
|
if (this.isEdgeSelected(edge)) {
|
||||||
|
const container = document.getElementById(`${id}-container`);
|
||||||
|
container.parentNode.appendChild(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = 120;
|
const size = 120;
|
||||||
|
|
||||||
|
class Node extends NodeBase {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
Object.assign(this, {
|
||||||
|
_super_handleDragEnd: this.handleDragEnd,
|
||||||
|
handleDragEnd: this.constructor.prototype.handleDragEnd.bind(this),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd(...args) {
|
||||||
|
this.oldSibling = null;
|
||||||
|
return this._super_handleDragEnd(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EdgeBase.calculateOffset = function (nodeSize, source, target) {
|
EdgeBase.calculateOffset = function (nodeSize, source, target) {
|
||||||
const arrowVector = math.matrix([ target.x - source.x, target.y - source.y ]);
|
const arrowVector = math.matrix([ target.x - source.x, target.y - source.y ]);
|
||||||
const offsetLength = Math.max(0, Math.min((0.75 * size), (math.norm(arrowVector) / 2) - 40));
|
const offsetLength = Math.max(0, Math.min((0.75 * size), (math.norm(arrowVector) / 2) - 40));
|
||||||
|
@ -112,10 +172,6 @@ class Edge extends EdgeBase {
|
||||||
className: 'edge-path',
|
className: 'edge-path',
|
||||||
d: this.getPathDescription(data) || undefined,
|
d: this.getPathDescription(data) || undefined,
|
||||||
}),
|
}),
|
||||||
this.props.renderEdgeText && r(this.props.renderEdgeText, {
|
|
||||||
data,
|
|
||||||
transform: this.getEdgeHandleTranslation(),
|
|
||||||
}),
|
|
||||||
]),
|
]),
|
||||||
r.g({
|
r.g({
|
||||||
className: 'edge-mouse-handler',
|
className: 'edge-mouse-handler',
|
||||||
|
@ -128,6 +184,11 @@ class Edge extends EdgeBase {
|
||||||
'data-target': data.target,
|
'data-target': data.target,
|
||||||
d: this.getPathDescription(data) || undefined,
|
d: this.getPathDescription(data) || undefined,
|
||||||
}),
|
}),
|
||||||
|
this.props.renderEdgeText && !this.props.nodeMoving && r(this.props.renderEdgeText, {
|
||||||
|
data,
|
||||||
|
transform: this.getEdgeHandleTranslation(),
|
||||||
|
selected: this.props.isSelected,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ const {
|
||||||
map,
|
map,
|
||||||
values,
|
values,
|
||||||
flatten,
|
flatten,
|
||||||
memoizeWith,
|
|
||||||
path,
|
path,
|
||||||
filter,
|
filter,
|
||||||
forEach,
|
forEach,
|
||||||
merge,
|
merge,
|
||||||
|
repeat,
|
||||||
} = require('ramda');
|
} = require('ramda');
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
@ -18,6 +18,7 @@ const { connect } = require('react-redux');
|
||||||
const { bindActionCreators } = require('redux');
|
const { bindActionCreators } = require('redux');
|
||||||
|
|
||||||
const d = require('../../utils/d');
|
const d = require('../../utils/d');
|
||||||
|
const memoize = require('../../utils/memoize');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pulse: pulseActions,
|
pulse: pulseActions,
|
||||||
|
@ -30,6 +31,8 @@ const {
|
||||||
PA_VOLUME_NORM,
|
PA_VOLUME_NORM,
|
||||||
} = require('../../constants/pulse');
|
} = require('../../constants/pulse');
|
||||||
|
|
||||||
|
const VolumeSlider = require('../../components/volume-slider');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
GraphView,
|
GraphView,
|
||||||
} = require('./satellites-graph');
|
} = require('./satellites-graph');
|
||||||
|
@ -38,18 +41,8 @@ const {
|
||||||
Edge,
|
Edge,
|
||||||
} = require('./base');
|
} = require('./base');
|
||||||
|
|
||||||
const weakmapId_ = new WeakMap();
|
|
||||||
const weakmapId = o => {
|
|
||||||
if (!weakmapId_.has(o)) {
|
|
||||||
weakmapId_.set(o, String(Math.random()));
|
|
||||||
}
|
|
||||||
return weakmapId_.get(o);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dgoToPai = new WeakMap();
|
const dgoToPai = new WeakMap();
|
||||||
|
|
||||||
const memoize = memoizeWith(weakmapId);
|
|
||||||
|
|
||||||
const key = pao => `${pao.type}-${pao.index}`;
|
const key = pao => `${pao.type}-${pao.index}`;
|
||||||
|
|
||||||
const sourceKey = pai => {
|
const sourceKey = pai => {
|
||||||
|
@ -72,12 +65,12 @@ const paoToNode = memoize(pao => ({
|
||||||
type: pao.type,
|
type: pao.type,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const paiToEdge = memoize(pai => ({
|
const paoToEdge = memoize(pao => ({
|
||||||
id: key(pai),
|
id: key(pao),
|
||||||
source: sourceKey(pai),
|
source: sourceKey(pao),
|
||||||
target: targetKey(pai),
|
target: targetKey(pao),
|
||||||
index: pai.index,
|
index: pao.index,
|
||||||
type: pai.type,
|
type: pao.type,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const getPaiIcon = memoize(pai => {
|
const getPaiIcon = memoize(pai => {
|
||||||
|
@ -211,12 +204,26 @@ const renderNode = (nodeRef, data, key, selected, hovered) => r({
|
||||||
hovered,
|
hovered,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getVolumesForThumbnail = ({ pai, state }) => {
|
||||||
|
const { lockChannelsTogether } = state.preferences;
|
||||||
|
let volumes = (pai && pai.channelVolumes) || [];
|
||||||
|
if (lockChannelsTogether) {
|
||||||
|
if (volumes.every(v => v === volumes[0])) {
|
||||||
|
volumes = [
|
||||||
|
volumes.reduce((a, b) => Math.max(a, b)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volumes;
|
||||||
|
};
|
||||||
|
|
||||||
const VolumeThumbnail = ({ pai, state }) => {
|
const VolumeThumbnail = ({ pai, state }) => {
|
||||||
if (state.preferences.hideVolumeThumbnails) {
|
if (state.preferences.hideVolumeThumbnails) {
|
||||||
return r(React.Fragment);
|
return r(React.Fragment);
|
||||||
}
|
}
|
||||||
|
const { baseVolume } = pai;
|
||||||
|
|
||||||
const volumes = (pai && pai.channelVolumes) || [];
|
const volumes = getVolumesForThumbnail({ pai, state });
|
||||||
const muted = !pai || pai.muted;
|
const muted = !pai || pai.muted;
|
||||||
|
|
||||||
const step = size / 32;
|
const step = size / 32;
|
||||||
|
@ -229,6 +236,7 @@ const VolumeThumbnail = ({ pai, state }) => {
|
||||||
'volume-thumbnail': true,
|
'volume-thumbnail': true,
|
||||||
'volume-thumbnail-muted': muted,
|
'volume-thumbnail-muted': muted,
|
||||||
},
|
},
|
||||||
|
height: (2 * padding) + height,
|
||||||
}, [
|
}, [
|
||||||
r.line({
|
r.line({
|
||||||
className: 'volume-thumbnail-ruler-line',
|
className: 'volume-thumbnail-ruler-line',
|
||||||
|
@ -238,6 +246,14 @@ const VolumeThumbnail = ({ pai, state }) => {
|
||||||
y2: padding + height,
|
y2: padding + height,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
baseVolume && r.line({
|
||||||
|
className: 'volume-thumbnail-ruler-line',
|
||||||
|
x1: padding + ((baseVolume / PA_VOLUME_NORM) * width),
|
||||||
|
x2: padding + ((baseVolume / PA_VOLUME_NORM) * width),
|
||||||
|
y1: padding,
|
||||||
|
y2: padding + height,
|
||||||
|
}),
|
||||||
|
|
||||||
r.line({
|
r.line({
|
||||||
className: 'volume-thumbnail-ruler-line',
|
className: 'volume-thumbnail-ruler-line',
|
||||||
x1: padding + width,
|
x1: padding + width,
|
||||||
|
@ -256,6 +272,63 @@ const VolumeThumbnail = ({ pai, state }) => {
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVolumes = ({ pai, state }) => {
|
||||||
|
const { lockChannelsTogether } = state.preferences;
|
||||||
|
let volumes = (pai && pai.channelVolumes) || [];
|
||||||
|
if (lockChannelsTogether) {
|
||||||
|
volumes = [
|
||||||
|
volumes.reduce((a, b) => Math.max(a, b)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return { volumes, lockChannelsTogether };
|
||||||
|
};
|
||||||
|
|
||||||
|
const VolumeControls = ({ pai, state }) => {
|
||||||
|
const { maxVolume } = state.preferences;
|
||||||
|
const { volumes, lockChannelsTogether } = getVolumes({ pai, state });
|
||||||
|
const baseVolume = pai && pai.baseVolume;
|
||||||
|
const muted = !pai || pai.muted;
|
||||||
|
|
||||||
|
return r.div({
|
||||||
|
className: 'volume-controls',
|
||||||
|
}, [
|
||||||
|
...volumes.map((v, channelIndex) => r(VolumeSlider, {
|
||||||
|
muted,
|
||||||
|
baseVolume,
|
||||||
|
normVolume: PA_VOLUME_NORM,
|
||||||
|
maxVolume: PA_VOLUME_NORM * maxVolume,
|
||||||
|
value: v,
|
||||||
|
onChange: v => {
|
||||||
|
if (pai.type === 'sink') {
|
||||||
|
if (lockChannelsTogether) {
|
||||||
|
state.setSinkVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||||
|
} else {
|
||||||
|
state.setSinkChannelVolume(pai.index, channelIndex, v);
|
||||||
|
}
|
||||||
|
} else if (pai.type === 'source') {
|
||||||
|
if (lockChannelsTogether) {
|
||||||
|
state.setSourceVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||||
|
} else {
|
||||||
|
state.setSourceChannelVolume(pai.index, channelIndex, v);
|
||||||
|
}
|
||||||
|
} else if (pai.type === 'sinkInput') {
|
||||||
|
if (lockChannelsTogether) {
|
||||||
|
state.setSinkInputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||||
|
} else {
|
||||||
|
state.setSinkInputChannelVolume(pai.index, channelIndex, v);
|
||||||
|
}
|
||||||
|
} else if (pai.type === 'sourceOutput') {
|
||||||
|
if (lockChannelsTogether) {
|
||||||
|
state.setSourceOutputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
||||||
|
} else {
|
||||||
|
state.setSourceOutputChannelVolume(pai.index, channelIndex, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
const DebugText = ({ dgo, pai, state }) => r.div({
|
const DebugText = ({ dgo, pai, state }) => r.div({
|
||||||
style: {
|
style: {
|
||||||
fontSize: '50%',
|
fontSize: '50%',
|
||||||
|
@ -265,21 +338,23 @@ const DebugText = ({ dgo, pai, state }) => r.div({
|
||||||
JSON.stringify(pai, null, 2),
|
JSON.stringify(pai, null, 2),
|
||||||
] : []);
|
] : []);
|
||||||
|
|
||||||
const SinkText = ({ dgo, pai, state }) => r.div([
|
const SinkText = ({ dgo, pai, state, selected }) => r.div([
|
||||||
r.div({
|
r.div({
|
||||||
className: 'node-name',
|
className: 'node-name',
|
||||||
title: pai.name,
|
title: pai.name,
|
||||||
}, pai.description),
|
}, pai.description),
|
||||||
r(VolumeThumbnail, { pai, state }),
|
!selected && r(VolumeThumbnail, { pai, state }),
|
||||||
|
selected && r(VolumeControls, { pai, state }),
|
||||||
r(DebugText, { dgo, pai, state }),
|
r(DebugText, { dgo, pai, state }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SourceText = ({ dgo, pai, state }) => r.div([
|
const SourceText = ({ dgo, pai, state, selected }) => r.div([
|
||||||
r.div({
|
r.div({
|
||||||
className: 'node-name',
|
className: 'node-name',
|
||||||
title: pai.name,
|
title: pai.name,
|
||||||
}, pai.description),
|
}, pai.description),
|
||||||
r(VolumeThumbnail, { pai, state }),
|
!selected && r(VolumeThumbnail, { pai, state }),
|
||||||
|
selected && r(VolumeControls, { pai, state }),
|
||||||
r(DebugText, { dgo, pai, state }),
|
r(DebugText, { dgo, pai, state }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -299,7 +374,7 @@ const ModuleText = ({ dgo, pai, state }) => r.div([
|
||||||
r(DebugText, { dgo, pai, state }),
|
r(DebugText, { dgo, pai, state }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const renderNodeText = state => dgo => r('foreignObject', {
|
const renderNodeText = state => (dgo, i, selected) => r('foreignObject', {
|
||||||
x: -s2,
|
x: -s2,
|
||||||
y: -s2,
|
y: -s2,
|
||||||
}, r.div({
|
}, r.div({
|
||||||
|
@ -319,6 +394,7 @@ const renderNodeText = state => dgo => r('foreignObject', {
|
||||||
dgo,
|
dgo,
|
||||||
pai: dgoToPai.get(dgo),
|
pai: dgoToPai.get(dgo),
|
||||||
state,
|
state,
|
||||||
|
selected,
|
||||||
})));
|
})));
|
||||||
|
|
||||||
const renderEdge = props => r(Edge, {
|
const renderEdge = props => r(Edge, {
|
||||||
|
@ -328,7 +404,7 @@ const renderEdge = props => r(Edge, {
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderEdgeText = state => ({ data: dgo, transform }) => {
|
const renderEdgeText = state => ({ data: dgo, transform, selected }) => {
|
||||||
const pai = dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)({ pulse: state });
|
const pai = dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)({ pulse: state });
|
||||||
|
|
||||||
return r('foreignObject', {
|
return r('foreignObject', {
|
||||||
|
@ -340,7 +416,8 @@ const renderEdgeText = state => ({ data: dgo, transform }) => {
|
||||||
height: size,
|
height: size,
|
||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
pai && r(VolumeThumbnail, { pai, state }),
|
pai && (!selected) && r(VolumeThumbnail, { pai, state }),
|
||||||
|
pai && selected && r(VolumeControls, { pai, state }),
|
||||||
r(DebugText, { dgo, pai, state }),
|
r(DebugText, { dgo, pai, state }),
|
||||||
]));
|
]));
|
||||||
};
|
};
|
||||||
|
@ -444,9 +521,9 @@ class Graph extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let edges = map(paiToEdge, flatten(map(values, [
|
let edges = map(paoToEdge, flatten(map(values, [
|
||||||
this.props.infos.sinkInputs,
|
this.props.objects.sinkInputs,
|
||||||
this.props.infos.sourceOutputs,
|
this.props.objects.sourceOutputs,
|
||||||
])));
|
])));
|
||||||
|
|
||||||
const connectedNodeKeys = new Set();
|
const connectedNodeKeys = new Set();
|
||||||
|
|
|
@ -5,9 +5,6 @@ const {
|
||||||
prop,
|
prop,
|
||||||
groupBy,
|
groupBy,
|
||||||
flatten,
|
flatten,
|
||||||
addIndex,
|
|
||||||
mapObjIndexed,
|
|
||||||
values,
|
|
||||||
} = require('ramda');
|
} = require('ramda');
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
@ -16,11 +13,39 @@ const r = require('r-dom');
|
||||||
|
|
||||||
const plusMinus = require('../../utils/plus-minus');
|
const plusMinus = require('../../utils/plus-minus');
|
||||||
|
|
||||||
|
const memoize = require('../../utils/memoize');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
GraphView: GraphViewBase,
|
GraphView: GraphViewBase,
|
||||||
} = require('./base');
|
} = require('./base');
|
||||||
|
|
||||||
const mapIndexed = addIndex(map);
|
const originalEdgeToSatelliteNode = edge => ({
|
||||||
|
id: `${edge.target}__satellite__${edge.id}`,
|
||||||
|
edge: edge.id,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
type: 'satellite',
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalEdgeAndSatelliteNodeToSatelliteEdge = (edge, satelliteNode) => {
|
||||||
|
const satelliteEdge = {
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.source,
|
||||||
|
target: satelliteNode.id,
|
||||||
|
originalTarget: edge.target,
|
||||||
|
index: edge.index,
|
||||||
|
type: edge.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
satelliteEdgeToOriginalEdge.set(satelliteEdge, edge);
|
||||||
|
return satelliteEdge;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalEdgeToSatellites = memoize(edge => {
|
||||||
|
const satelliteNode = originalEdgeToSatelliteNode(edge);
|
||||||
|
const satelliteEdge = originalEdgeAndSatelliteNodeToSatelliteEdge(edge, satelliteNode);
|
||||||
|
return { satelliteEdge, satelliteNode };
|
||||||
|
});
|
||||||
|
|
||||||
const Satellite = () => r(React.Fragment);
|
const Satellite = () => r(React.Fragment);
|
||||||
|
|
||||||
|
@ -33,8 +58,10 @@ class GraphView extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
edgesByTargetNodeKey: {},
|
originalEdgesByTargetNodeKey: {},
|
||||||
satelliteNodesByTargetNodeKey: {},
|
satelliteNodesByTargetNodeKey: {},
|
||||||
|
satelliteEdges: [],
|
||||||
|
selected: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.graph = React.createRef();
|
this.graph = React.createRef();
|
||||||
|
@ -48,23 +75,41 @@ class GraphView extends React.Component {
|
||||||
renderNode: this.renderNode.bind(this),
|
renderNode: this.renderNode.bind(this),
|
||||||
renderNodeText: this.renderNodeText.bind(this),
|
renderNodeText: this.renderNodeText.bind(this),
|
||||||
|
|
||||||
|
renderEdge: this.renderEdge.bind(this),
|
||||||
|
renderEdgeText: this.renderEdgeText.bind(this),
|
||||||
|
|
||||||
afterRenderEdge: this.afterRenderEdge.bind(this),
|
afterRenderEdge: this.afterRenderEdge.bind(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props) {
|
static getDerivedStateFromProps(props) {
|
||||||
const { nodeKey, edgeKey } = props;
|
const originalEdgesByTargetNodeKey = groupBy(prop('target'), props.edges);
|
||||||
|
|
||||||
const edgesByTargetNodeKey = groupBy(prop('target'), props.edges);
|
let { selected } = props;
|
||||||
const satelliteNodesByTargetNodeKey = map(map(edge => ({
|
|
||||||
[nodeKey]: `${edge.target}__satellite__${edge[edgeKey]}`,
|
|
||||||
edge: edge[edgeKey],
|
|
||||||
source: edge.source,
|
|
||||||
target: edge.target,
|
|
||||||
type: 'satellite',
|
|
||||||
})), edgesByTargetNodeKey);
|
|
||||||
|
|
||||||
return { edgesByTargetNodeKey, satelliteNodesByTargetNodeKey };
|
const satelliteEdges = [];
|
||||||
|
|
||||||
|
const satelliteNodesByTargetNodeKey = map(edges => map(edge => {
|
||||||
|
const {
|
||||||
|
satelliteNode,
|
||||||
|
satelliteEdge,
|
||||||
|
} = originalEdgeToSatellites(edge);
|
||||||
|
|
||||||
|
if (edge === selected) {
|
||||||
|
selected = satelliteEdge;
|
||||||
|
}
|
||||||
|
|
||||||
|
satelliteEdges.push(satelliteEdge);
|
||||||
|
|
||||||
|
return satelliteNode;
|
||||||
|
}, edges), originalEdgesByTargetNodeKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
originalEdgesByTargetNodeKey,
|
||||||
|
satelliteNodesByTargetNodeKey,
|
||||||
|
satelliteEdges,
|
||||||
|
selected,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static repositionSatellites(position, satelliteNodes) {
|
static repositionSatellites(position, satelliteNodes) {
|
||||||
|
@ -119,6 +164,14 @@ class GraphView extends React.Component {
|
||||||
return r(React.Fragment);
|
return r(React.Fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEdge(...args) {
|
||||||
|
return this.props.renderEdge(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEdgeText(...args) {
|
||||||
|
return this.props.renderEdgeText(...args);
|
||||||
|
}
|
||||||
|
|
||||||
afterRenderEdge(id, element, edge, edgeContainer) {
|
afterRenderEdge(id, element, edge, edgeContainer) {
|
||||||
const originalEdge = satelliteEdgeToOriginalEdge.get(edge);
|
const originalEdge = satelliteEdgeToOriginalEdge.get(edge);
|
||||||
this.props.afterRenderEdge(id, element, originalEdge || edge, edgeContainer);
|
this.props.afterRenderEdge(id, element, originalEdge || edge, edgeContainer);
|
||||||
|
@ -126,7 +179,11 @@ class GraphView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { nodeKey } = this.props;
|
const { nodeKey } = this.props;
|
||||||
const { edgesByTargetNodeKey, satelliteNodesByTargetNodeKey } = this.state;
|
const {
|
||||||
|
satelliteNodesByTargetNodeKey,
|
||||||
|
satelliteEdges: edges,
|
||||||
|
selected,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const nodes = flatten(map(node => {
|
const nodes = flatten(map(node => {
|
||||||
const satelliteNodes = satelliteNodesByTargetNodeKey[node[nodeKey]] || [];
|
const satelliteNodes = satelliteNodesByTargetNodeKey[node[nodeKey]] || [];
|
||||||
|
@ -134,26 +191,6 @@ class GraphView extends React.Component {
|
||||||
return satelliteNodes.concat(node);
|
return satelliteNodes.concat(node);
|
||||||
}, this.props.nodes));
|
}, this.props.nodes));
|
||||||
|
|
||||||
let { selected } = this.props;
|
|
||||||
|
|
||||||
const edges = flatten(values(mapObjIndexed((edges, target) => mapIndexed((edge, i) => {
|
|
||||||
const satelliteEdge = {
|
|
||||||
id: edge.id,
|
|
||||||
source: edge.source,
|
|
||||||
target: satelliteNodesByTargetNodeKey[target][i][nodeKey],
|
|
||||||
originalTarget: edge.target,
|
|
||||||
index: edge.index,
|
|
||||||
type: edge.type,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (edge === selected) {
|
|
||||||
selected = satelliteEdge;
|
|
||||||
}
|
|
||||||
|
|
||||||
satelliteEdgeToOriginalEdge.set(satelliteEdge, edge);
|
|
||||||
return satelliteEdge;
|
|
||||||
}, edges), edgesByTargetNodeKey)));
|
|
||||||
|
|
||||||
return r(GraphViewBase, {
|
return r(GraphViewBase, {
|
||||||
...this.props,
|
...this.props,
|
||||||
|
|
||||||
|
@ -172,6 +209,9 @@ class GraphView extends React.Component {
|
||||||
renderNode: this.renderNode,
|
renderNode: this.renderNode,
|
||||||
renderNodeText: this.renderNodeText,
|
renderNodeText: this.renderNodeText,
|
||||||
|
|
||||||
|
renderEdge: this.renderEdge,
|
||||||
|
renderEdgeText: this.renderEdgeText,
|
||||||
|
|
||||||
afterRenderEdge: this.props.afterRenderEdge && this.afterRenderEdge,
|
afterRenderEdge: this.props.afterRenderEdge && this.afterRenderEdge,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
7
components/input/index.js
Normal file
7
components/input/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
const r = require('r-dom');
|
||||||
|
|
||||||
|
module.exports = props => r.input({
|
||||||
|
className: 'input',
|
||||||
|
...props,
|
||||||
|
}, props.children);
|
6
components/label/index.js
Normal file
6
components/label/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
const r = require('r-dom');
|
||||||
|
|
||||||
|
module.exports = props => r.label({
|
||||||
|
className: 'label',
|
||||||
|
}, props.children);
|
10
components/number-input/index.js
Normal file
10
components/number-input/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
const r = require('r-dom');
|
||||||
|
|
||||||
|
const Label = require('../label');
|
||||||
|
const Input = require('../input');
|
||||||
|
|
||||||
|
module.exports = props => r(Label, [
|
||||||
|
...[].concat(props.children),
|
||||||
|
r(Input, props),
|
||||||
|
]);
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pick,
|
pick,
|
||||||
|
defaultTo,
|
||||||
} = require('ramda');
|
} = require('ramda');
|
||||||
|
|
||||||
const r = require('r-dom');
|
const r = require('r-dom');
|
||||||
|
@ -14,6 +15,7 @@ const { preferences: preferencesActions } = require('../../actions');
|
||||||
|
|
||||||
const Button = require('../button');
|
const Button = require('../button');
|
||||||
const Checkbox = require('../checkbox');
|
const Checkbox = require('../checkbox');
|
||||||
|
const NumberInput = require('../number-input');
|
||||||
|
|
||||||
const Preferences = withStateHandlers(
|
const Preferences = withStateHandlers(
|
||||||
{
|
{
|
||||||
|
@ -84,6 +86,24 @@ const Preferences = withStateHandlers(
|
||||||
}, 'Hide volume thumbnails'),
|
}, 'Hide volume thumbnails'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
r.div([
|
||||||
|
r(Checkbox, {
|
||||||
|
checked: props.preferences.lockChannelsTogether,
|
||||||
|
onChange: () => props.actions.toggle('lockChannelsTogether'),
|
||||||
|
}, 'Lock channels together'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
r.div([
|
||||||
|
r(NumberInput, {
|
||||||
|
type: 'number',
|
||||||
|
value: defaultTo(150, Math.round(props.preferences.maxVolume * 100)),
|
||||||
|
onChange: e => {
|
||||||
|
const v = defaultTo(150, Math.max(0, parseInt(e.target.value, 10)));
|
||||||
|
props.actions.set({ maxVolume: v / 100 });
|
||||||
|
},
|
||||||
|
}, 'Maximum volume: '),
|
||||||
|
]),
|
||||||
|
|
||||||
r.div([
|
r.div([
|
||||||
r(Checkbox, {
|
r(Checkbox, {
|
||||||
checked: props.preferences.showDebugInfo,
|
checked: props.preferences.showDebugInfo,
|
||||||
|
|
4
components/slider/index.js
Normal file
4
components/slider/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
module.exports = class Slider {
|
||||||
|
|
||||||
|
};
|
167
components/volume-slider/index.js
Normal file
167
components/volume-slider/index.js
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/* global document */
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const r = require('r-dom');
|
||||||
|
|
||||||
|
const width = 300;
|
||||||
|
const height = 18;
|
||||||
|
|
||||||
|
const clamp = x => Math.min(
|
||||||
|
width - (height / 2),
|
||||||
|
Math.max(
|
||||||
|
(height / 2),
|
||||||
|
x,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const vol2pix = (v, maxVolume) => (v / maxVolume) * (width - height);
|
||||||
|
const pix2vol = (x, maxVolume) => (x * maxVolume) / (width - height);
|
||||||
|
|
||||||
|
module.exports = class VolumeSlider extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.svg = React.createRef();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
draggingX: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(this, {
|
||||||
|
handlePointerDown: this.handlePointerDown.bind(this),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.svg.current.addEventListener('pointerdown', this.handlePointerDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.svg.current.removeEventListener('pointerdown', this.handlePointerDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const originX = e.clientX - e.offsetX;
|
||||||
|
|
||||||
|
const move = e => {
|
||||||
|
if (this.state.draggingX !== null) {
|
||||||
|
this.setState({
|
||||||
|
draggingX: clamp(e.clientX - originX),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const up = e => {
|
||||||
|
this.setState({
|
||||||
|
draggingX: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.removeEventListener('pointermove', move);
|
||||||
|
document.removeEventListener('pointerup', up);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const click = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
document.removeEventListener('click', click, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('pointermove', move);
|
||||||
|
document.addEventListener('pointerup', up);
|
||||||
|
document.addEventListener('click', click, true);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
draggingX: clamp(e.offsetX),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
const { draggingX } = this.state;
|
||||||
|
const { maxVolume } = this.props;
|
||||||
|
|
||||||
|
if (draggingX === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetValue = Math.floor(pix2vol(draggingX - (height / 2), maxVolume));
|
||||||
|
|
||||||
|
this.props.onChange(targetValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
muted,
|
||||||
|
baseVolume,
|
||||||
|
normVolume,
|
||||||
|
maxVolume,
|
||||||
|
value,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
draggingX,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const x = draggingX === null ?
|
||||||
|
((height / 2) + vol2pix(value, maxVolume)) :
|
||||||
|
draggingX;
|
||||||
|
|
||||||
|
const baseX = (height / 2) + vol2pix(baseVolume, maxVolume);
|
||||||
|
const normX = (height / 2) + vol2pix(normVolume, maxVolume);
|
||||||
|
|
||||||
|
return r.svg({
|
||||||
|
ref: this.svg,
|
||||||
|
classSet: {
|
||||||
|
'volume-slider': true,
|
||||||
|
'volume-slider-muted': muted,
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}, [
|
||||||
|
baseVolume && r.line({
|
||||||
|
className: 'volume-slider-base-mark',
|
||||||
|
x1: baseX,
|
||||||
|
x2: baseX,
|
||||||
|
y1: 0,
|
||||||
|
y2: height,
|
||||||
|
}),
|
||||||
|
|
||||||
|
r.line({
|
||||||
|
className: 'volume-slider-norm-mark',
|
||||||
|
x1: normX,
|
||||||
|
x2: normX,
|
||||||
|
y1: 0,
|
||||||
|
y2: height,
|
||||||
|
}),
|
||||||
|
|
||||||
|
r.line({
|
||||||
|
className: 'volume-slider-bg',
|
||||||
|
x1: height / 2,
|
||||||
|
x2: width - (height / 2),
|
||||||
|
y1: height / 2,
|
||||||
|
y2: height / 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
r.line({
|
||||||
|
className: 'volume-slider-fill',
|
||||||
|
x1: height / 2,
|
||||||
|
x2: x,
|
||||||
|
y1: height / 2,
|
||||||
|
y2: height / 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
r.circle({
|
||||||
|
className: 'volume-slider-handle',
|
||||||
|
cx: x,
|
||||||
|
cy: height / 2,
|
||||||
|
r: (height - 2) / 2,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
76
index.css
76
index.css
|
@ -33,12 +33,30 @@ button:active {
|
||||||
top: 1px;
|
top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background: var(--themeUnfocusedBaseColor);
|
||||||
|
color: var(--themeUnfocusedFgColor);
|
||||||
|
border: 1px solid var(--borders);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--themeSelectedBgColor);
|
||||||
|
}
|
||||||
|
.input[type="number"] {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
.view-wrapper .graph {
|
.view-wrapper .graph {
|
||||||
background: var(--themeBaseColor);
|
background: var(--themeBaseColor);
|
||||||
}
|
}
|
||||||
|
@ -63,6 +81,10 @@ button:active {
|
||||||
color: var(--themeSelectedFgColor);
|
color: var(--themeSelectedFgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-wrapper .edge-container:hover .edge {
|
||||||
|
stroke: var(--themeSelectedBgColor);
|
||||||
|
}
|
||||||
|
|
||||||
.view-wrapper .graph .edge.selected {
|
.view-wrapper .graph .edge.selected {
|
||||||
stroke: var(--themeSelectedBgColor);
|
stroke: var(--themeSelectedBgColor);
|
||||||
}
|
}
|
||||||
|
@ -90,6 +112,7 @@ button:active {
|
||||||
|
|
||||||
#edge-custom-container .edge-path {
|
#edge-custom-container .edge-path {
|
||||||
marker-end: none;
|
marker-end: none;
|
||||||
|
stroke: var(--themeSelectedBgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-wrapper .graph .edge {
|
.view-wrapper .graph .edge {
|
||||||
|
@ -100,6 +123,16 @@ button:active {
|
||||||
fill: var(--borders);
|
fill: var(--borders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-wrapper .edge-mouse-handler.edge-mouse-handler {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.view-wrapper .edge-mouse-handler .edge-overlay-path {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.view-wrapper .edge-mouse-handler .edge-text {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.preferences {
|
.preferences {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -172,3 +205,46 @@ button:active {
|
||||||
.edge-text {
|
.edge-text {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.volume-controls {
|
||||||
|
background: var(--themeBgColor);
|
||||||
|
border: 1px solid var(--borders);
|
||||||
|
|
||||||
|
pointer-events: initial;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
width: 308px;
|
||||||
|
|
||||||
|
margin-left: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-controls:hover {
|
||||||
|
border-color: var(--themeSelectedBgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider-norm-mark, .volume-slider-base-mark {
|
||||||
|
stroke: var(--borders);
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider-bg {
|
||||||
|
stroke: var(--borders);
|
||||||
|
stroke-width: 6px;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider-fill {
|
||||||
|
stroke: var(--successColor);
|
||||||
|
stroke-width: 6px;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider-handle {
|
||||||
|
fill: var(--themeBgColor);
|
||||||
|
stroke: var(--borders);
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider-handle:hover {
|
||||||
|
stroke: var(--themeSelectedBgColor);
|
||||||
|
}
|
||||||
|
|
8
index.js
8
index.js
|
@ -10,4 +10,12 @@ app.on('ready', () => {
|
||||||
win.setAutoHideMenuBar(true);
|
win.setAutoHideMenuBar(true);
|
||||||
win.setMenuBarVisibility(false);
|
win.setMenuBarVisibility(false);
|
||||||
win.loadFile('index.html');
|
win.loadFile('index.html');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
|
||||||
|
|
||||||
|
installExtension(REACT_DEVELOPER_TOOLS)
|
||||||
|
.then(name => console.log(`Added Extension: ${name}`))
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^0.25.0",
|
"ava": "^0.25.0",
|
||||||
"electron": "^3.0.8",
|
"electron": "^3.0.8",
|
||||||
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"eslint-config-xo-overrides": "^1.1.2",
|
"eslint-config-xo-overrides": "^1.1.2",
|
||||||
"remotedev-server": "^0.2.6",
|
"remotedev-server": "^0.2.6",
|
||||||
"uws": "^99.0.0",
|
"uws": "^99.0.0",
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@futpib/paclient": "^0.0.3",
|
"@futpib/paclient": "^0.0.5",
|
||||||
"bluebird": "^3.5.3",
|
"bluebird": "^3.5.3",
|
||||||
"camelcase": "^5.0.0",
|
"camelcase": "^5.0.0",
|
||||||
"electron-store": "^2.0.0",
|
"electron-store": "^2.0.0",
|
||||||
|
|
|
@ -17,6 +17,9 @@ const initialState = {
|
||||||
hidePulseaudioApps: true,
|
hidePulseaudioApps: true,
|
||||||
|
|
||||||
hideVolumeThumbnails: false,
|
hideVolumeThumbnails: false,
|
||||||
|
lockChannelsTogether: true,
|
||||||
|
|
||||||
|
maxVolume: 1.5,
|
||||||
|
|
||||||
showDebugInfo: false,
|
showDebugInfo: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,8 @@ const {
|
||||||
omit,
|
omit,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
map,
|
map,
|
||||||
|
pick,
|
||||||
|
equals,
|
||||||
} = require('ramda');
|
} = require('ramda');
|
||||||
|
|
||||||
const { combineReducers } = require('redux');
|
const { combineReducers } = require('redux');
|
||||||
|
@ -33,6 +35,9 @@ const reducer = combineReducers({
|
||||||
if (payload.type !== type) {
|
if (payload.type !== type) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
if (payload.type === 'sinkInput' || payload.type === 'sourceOutput') {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
return merge(state, {
|
return merge(state, {
|
||||||
[payload.index]: payload,
|
[payload.index]: payload,
|
||||||
});
|
});
|
||||||
|
@ -43,6 +48,32 @@ const reducer = combineReducers({
|
||||||
}
|
}
|
||||||
return omit([ payload.index ], state);
|
return omit([ payload.index ], state);
|
||||||
},
|
},
|
||||||
|
[pulse.info]: (state, { payload }) => {
|
||||||
|
if (payload.type !== type) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (payload.type === 'sinkInput' || payload.type === 'sourceOutput') {
|
||||||
|
const newPao = pick([
|
||||||
|
'type',
|
||||||
|
'index',
|
||||||
|
'moduleIndex',
|
||||||
|
'clientIndex',
|
||||||
|
'sinkIndex',
|
||||||
|
'sourceIndex',
|
||||||
|
], payload);
|
||||||
|
|
||||||
|
const oldPao = state[payload.index];
|
||||||
|
|
||||||
|
if (equals(newPao, oldPao)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return merge(state, {
|
||||||
|
[newPao.index]: newPao,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
},
|
||||||
[pulse.close]: () => initialState.objects[key],
|
[pulse.close]: () => initialState.objects[key],
|
||||||
}, initialState.objects[key]) ], things))),
|
}, initialState.objects[key]) ], things))),
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ const { pulse: pulseActions } = require('../actions');
|
||||||
|
|
||||||
const { things } = require('../constants/pulse');
|
const { things } = require('../constants/pulse');
|
||||||
|
|
||||||
|
const { getPaiByTypeAndIndex } = require('../selectors');
|
||||||
|
|
||||||
function getFnFromType(type) {
|
function getFnFromType(type) {
|
||||||
let fn;
|
let fn;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -29,6 +31,23 @@ function getFnFromType(type) {
|
||||||
return 'get' + fn[0].toUpperCase() + fn.slice(1);
|
return 'get' + fn[0].toUpperCase() + fn.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSinkChannelVolume(pa, store, index, channelIndex, volume, cb) {
|
||||||
|
const pai = getPaiByTypeAndIndex('sink', index)(store.getState());
|
||||||
|
pa.setSinkVolumes(index, pai.channelVolumes.map((v, i) => i === channelIndex ? volume : v), cb);
|
||||||
|
}
|
||||||
|
function setSourceChannelVolume(pa, store, index, channelIndex, volume, cb) {
|
||||||
|
const pai = getPaiByTypeAndIndex('source', index)(store.getState());
|
||||||
|
pa.setSourceVolumes(index, pai.channelVolumes.map((v, i) => i === channelIndex ? volume : v), cb);
|
||||||
|
}
|
||||||
|
function setSinkInputChannelVolume(pa, store, index, channelIndex, volume, cb) {
|
||||||
|
const pai = getPaiByTypeAndIndex('sinkInput', index)(store.getState());
|
||||||
|
pa.setSinkInputVolumesByIndex(index, pai.channelVolumes.map((v, i) => i === channelIndex ? volume : v), cb);
|
||||||
|
}
|
||||||
|
function setSourceOutputChannelVolume(pa, store, index, channelIndex, volume, cb) {
|
||||||
|
const pai = getPaiByTypeAndIndex('sourceOutput', index)(store.getState());
|
||||||
|
pa.setSourceOutputVolumesByIndex(index, pai.channelVolumes.map((v, i) => i === channelIndex ? volume : v), cb);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = store => {
|
module.exports = store => {
|
||||||
const pa = new PAClient();
|
const pa = new PAClient();
|
||||||
|
|
||||||
|
@ -128,6 +147,37 @@ module.exports = store => {
|
||||||
pa.unloadModuleByIndex(moduleIndex, rethrow);
|
pa.unloadModuleByIndex(moduleIndex, rethrow);
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[pulseActions.setSinkVolumes]: (state, { payload: { index, channelVolumes } }) => {
|
||||||
|
pa.setSinkVolumes(index, channelVolumes, rethrow);
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
[pulseActions.setSourceVolumes]: (state, { payload: { index, channelVolumes } }) => {
|
||||||
|
pa.setSourceVolumes(index, channelVolumes, rethrow);
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
[pulseActions.setSinkInputVolumes]: (state, { payload: { index, channelVolumes } }) => {
|
||||||
|
pa.setSinkInputVolumesByIndex(index, channelVolumes, rethrow);
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
[pulseActions.setSourceOutputVolumes]: (state, { payload: { index, channelVolumes } }) => {
|
||||||
|
pa.setSourceOutputVolumesByIndex(index, channelVolumes, rethrow);
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[pulseActions.setSinkChannelVolume]: (state, { payload: { index, channelIndex, volume } }) => {
|
||||||
|
return setSinkChannelVolume(pa, store, index, channelIndex, volume, rethrow);
|
||||||
|
},
|
||||||
|
[pulseActions.setSourceChannelVolume]: (state, { payload: { index, channelIndex, volume } }) => {
|
||||||
|
return setSourceChannelVolume(pa, store, index, channelIndex, volume, rethrow);
|
||||||
|
},
|
||||||
|
[pulseActions.setSinkInputChannelVolume]: (state, { payload: { index, channelIndex, volume } }) => {
|
||||||
|
return setSinkInputChannelVolume(pa, store, index, channelIndex, volume, rethrow);
|
||||||
|
},
|
||||||
|
[pulseActions.setSourceOutputChannelVolume]: (state, { payload: { index, channelIndex, volume } }) => {
|
||||||
|
return setSourceOutputChannelVolume(pa, store, index, channelIndex, volume, rethrow);
|
||||||
|
},
|
||||||
|
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
return next => action => {
|
return next => action => {
|
||||||
|
|
16
utils/memoize.js
Normal file
16
utils/memoize.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
const {
|
||||||
|
memoizeWith,
|
||||||
|
} = require('ramda');
|
||||||
|
|
||||||
|
const weakmapId_ = new WeakMap();
|
||||||
|
const weakmapId = o => {
|
||||||
|
if (!weakmapId_.has(o)) {
|
||||||
|
weakmapId_.set(o, String(Math.random()));
|
||||||
|
}
|
||||||
|
return weakmapId_.get(o);
|
||||||
|
};
|
||||||
|
|
||||||
|
const memoize = memoizeWith(weakmapId);
|
||||||
|
|
||||||
|
module.exports = memoize;
|
30
yarn.lock
30
yarn.lock
|
@ -2,6 +2,11 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"7zip@0.0.6":
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/7zip/-/7zip-0.0.6.tgz#9cafb171af82329490353b4816f03347aa150a30"
|
||||||
|
integrity sha1-nK+xca+CMpSQNTtIFvAzR6oVCjA=
|
||||||
|
|
||||||
"@ava/babel-plugin-throws-helper@^2.0.0":
|
"@ava/babel-plugin-throws-helper@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ava/babel-plugin-throws-helper/-/babel-plugin-throws-helper-2.0.0.tgz#2fc1fe3c211a71071a4eca7b8f7af5842cd1ae7c"
|
resolved "https://registry.yarnpkg.com/@ava/babel-plugin-throws-helper/-/babel-plugin-throws-helper-2.0.0.tgz#2fc1fe3c211a71071a4eca7b8f7af5842cd1ae7c"
|
||||||
|
@ -79,10 +84,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
arrify "^1.0.1"
|
arrify "^1.0.1"
|
||||||
|
|
||||||
"@futpib/paclient@^0.0.3":
|
"@futpib/paclient@^0.0.5":
|
||||||
version "0.0.3"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@futpib/paclient/-/paclient-0.0.3.tgz#54c10ac6d811c5104b66a9a4985809955c8ffee6"
|
resolved "https://registry.yarnpkg.com/@futpib/paclient/-/paclient-0.0.5.tgz#0de89ee7175e3de994bc298ddb1e461aa3007543"
|
||||||
integrity sha512-9OuBDQRb9U55y3Xu89tZV+oiwt2ghgTsSAqM+SAySoLCBt0yG5LaB9PCV+bxkBy6aY6bvvPuNrYd88hL57gaxQ==
|
integrity sha512-49jeRSEOXto3MntDj2Dzm4t7U9M0X41seqF+T/xwFRYk/pBUKdiXHuofNFJvn4rwM7P4BVjxU6fRpCOEAxH/VA==
|
||||||
|
|
||||||
"@ladjs/time-require@^0.1.4":
|
"@ladjs/time-require@^0.1.4":
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
|
@ -1409,6 +1414,11 @@ cross-spawn@^6.0.5:
|
||||||
shebang-command "^1.2.0"
|
shebang-command "^1.2.0"
|
||||||
which "^1.2.9"
|
which "^1.2.9"
|
||||||
|
|
||||||
|
cross-unzip@0.0.2:
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/cross-unzip/-/cross-unzip-0.0.2.tgz#5183bc47a09559befcf98cc4657964999359372f"
|
||||||
|
integrity sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=
|
||||||
|
|
||||||
crypto-random-string@^1.0.0:
|
crypto-random-string@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
|
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
|
||||||
|
@ -1899,6 +1909,16 @@ ejs@^2.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
|
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
|
||||||
integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==
|
integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==
|
||||||
|
|
||||||
|
electron-devtools-installer@^2.2.4:
|
||||||
|
version "2.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-2.2.4.tgz#261a50337e37121d338b966f07922eb4939a8763"
|
||||||
|
integrity sha512-b5kcM3hmUqn64+RUcHjjr8ZMpHS2WJ5YO0pnG9+P/RTdx46of/JrEjuciHWux6pE+On6ynWhHJF53j/EDJN0PA==
|
||||||
|
dependencies:
|
||||||
|
"7zip" "0.0.6"
|
||||||
|
cross-unzip "0.0.2"
|
||||||
|
rimraf "^2.5.2"
|
||||||
|
semver "^5.3.0"
|
||||||
|
|
||||||
electron-download@^4.1.0:
|
electron-download@^4.1.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
|
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
|
||||||
|
@ -5485,7 +5505,7 @@ ret@~0.1.10:
|
||||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||||
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
||||||
|
|
||||||
rimraf@^2.2.8, rimraf@^2.6.1:
|
rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.6.1:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
|
||||||
integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==
|
integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user