This commit is contained in:
futpib 2018-11-17 00:25:55 +03:00
parent bfc3fe078c
commit 2af5f85dc6
9 changed files with 585 additions and 286 deletions

View File

@ -6,79 +6,95 @@ const {
sortBy,
} = require('ramda');
const React = require('react');
const r = require('r-dom');
const { connect } = require('react-redux');
const { bindActionCreators } = require('redux');
const { withStateHandlers } = require('recompose');
const { pulse: pulseActions } = require('../../actions');
const Button = require('../button');
const Label = require('../label');
const Select = require('../select');
const Preferences = withStateHandlers(
{
open: false,
},
{
toggle: ({ open }) => () => ({ open: !open }),
},
)(({ open, toggle, ...props }) => r.div({
classSet: {
panel: true,
cards: true,
open,
},
}, open ? [
r.div([
r(Button, {
style: { width: '100%' },
autoFocus: true,
onClick: toggle,
}, 'Close'),
]),
class Cards extends React.Component {
constructor(props) {
super(props);
r.hr(),
this.state = {
open: false,
};
}
...map(card => r(Label, {
title: card.name,
}, [
r(Label, [
path([ 'properties', 'device', 'description' ], card),
]),
toggle() {
this.setState({ open: !this.state.open });
}
r(Select, {
options: sortBy(p => -p.priority, card.profiles),
optionValue: p => p.name,
optionText: p => [
p.description,
!p.available && '(unavailable)',
]
.filter(Boolean)
.join(' '),
value: card.activeProfileName,
onChange: e => {
props.actions.setCardProfile(card.index, e.target.value);
close() {
this.setState({ open: false });
}
render() {
const { open } = this.state;
const toggle = this.toggle.bind(this);
return r.div({
classSet: {
panel: true,
cards: true,
open,
},
}),
]), values(props.cards)),
}, open ? [
r.div([
r(Button, {
style: { width: '100%' },
autoFocus: true,
onClick: toggle,
}, 'Close'),
]),
props.preferences.showDebugInfo && r.pre({
style: {
fontSize: '0.75em',
},
}, [
JSON.stringify(props, null, 2),
]),
] : [
r(Button, {
autoFocus: true,
onClick: toggle,
}, 'Cards'),
]));
r.hr(),
...map(card => r(Label, {
title: card.name,
}, [
r(Label, [
path([ 'properties', 'device', 'description' ], card),
]),
r(Select, {
options: sortBy(p => -p.priority, card.profiles),
optionValue: p => p.name,
optionText: p => [
p.description,
!p.available && '(unavailable)',
]
.filter(Boolean)
.join(' '),
value: card.activeProfileName,
onChange: e => {
this.props.actions.setCardProfile(card.index, e.target.value);
},
}),
]), values(this.props.cards)),
this.props.preferences.showDebugInfo && r.pre({
style: {
fontSize: '0.75em',
},
}, [
JSON.stringify(this.props, null, 2),
]),
] : [
r(Button, {
autoFocus: true,
onClick: toggle,
}, 'Cards'),
]);
}
}
module.exports = connect(
state => ({
@ -88,4 +104,6 @@ module.exports = connect(
dispatch => ({
actions: bindActionCreators(pulseActions, dispatch),
}),
)(Preferences);
null,
{ withRef: true },
)(Cards);

View File

@ -1,16 +1,23 @@
/* global document */
const {
map,
values,
flatten,
path,
filter,
forEach,
merge,
repeat,
defaultTo,
prop,
all,
bind,
compose,
defaultTo,
filter,
find,
flatten,
forEach,
keys,
map,
merge,
path,
pick,
prop,
repeat,
sortBy,
values,
} = require('ramda');
const React = require('react');
@ -20,6 +27,8 @@ const r = require('r-dom');
const { connect } = require('react-redux');
const { bindActionCreators } = require('redux');
const { HotKeys } = require('react-hotkeys');
const d = require('../../utils/d');
const memoize = require('../../utils/memoize');
@ -43,6 +52,8 @@ const { size } = require('../../constants/view');
const VolumeSlider = require('../../components/volume-slider');
const { keyMap } = require('../hot-keys');
const {
GraphView,
} = require('./satellites-graph');
@ -53,6 +64,49 @@ const {
const LayoutEngine = require('./layout-engine');
const leftOf = (x, xs) => {
const i = ((xs.indexOf(x) + xs.length - 1) % xs.length);
return xs[i];
};
const rightOf = (x, xs) => {
const i = ((xs.indexOf(x) + 1) % xs.length);
return xs[i];
};
const selectionObjectTypes = {
order: [
'source',
'sourceOutput',
'client|module',
'sinkInput',
'sink',
],
left(type) {
return leftOf(type, this.order);
},
right(type) {
return rightOf(type, this.order);
},
fromPulseType(type) {
if (type === 'client' || type === 'module') {
return 'client|module';
}
return type;
},
toPulsePredicate(type) {
type = this.fromPulseType(type);
if (type === 'client|module') {
return o => (o.type === 'client' || o.type === 'module');
}
return o => o.type === type;
},
};
const dgoToPai = new WeakMap();
const key = pao => `${pao.type}-${pao.index}`;
@ -97,14 +151,6 @@ const getPaiIcon = memoize(pai => {
path([ 'properties', 'device', 'icon_name' ], pai);
});
const graphConfig = {
nodeTypes: {},
nodeSubtypes: {},
edgeTypes: {},
};
const s2 = size / 2;
const Sink = () => r.path({
@ -485,6 +531,82 @@ class Graph extends React.Component {
});
}
static getDerivedStateFromProps(props) {
let edges = map(paoToEdge, flatten(map(values, [
props.objects.sinkInputs,
props.objects.sourceOutputs,
props.derivations.monitorSources,
])));
const connectedNodeKeys = new Set();
edges.forEach(edge => {
connectedNodeKeys.add(edge.source);
connectedNodeKeys.add(edge.target);
});
const filteredNodeKeys = new Set();
const nodes = filter(node => {
if ((props.preferences.hideDisconnectedClients && node.type === 'client') ||
(props.preferences.hideDisconnectedModules && node.type === 'module') ||
(props.preferences.hideDisconnectedSources && node.type === 'source') ||
(props.preferences.hideDisconnectedSinks && node.type === 'sink')
) {
if (!connectedNodeKeys.has(node.id)) {
return false;
}
}
const pai = dgoToPai.get(node);
if (pai) {
if (props.preferences.hideMonitors &&
pai.properties.device &&
pai.properties.device.class === 'monitor'
) {
return false;
}
if (props.preferences.hidePulseaudioApps) {
const binary = path([ 'properties', 'application', 'process', 'binary' ], pai) || '';
const name = path([ 'properties', 'application', 'name' ], pai) || '';
if (binary.startsWith('pavucontrol') ||
binary.startsWith('kmix') ||
name === 'paclient.js'
) {
return false;
}
}
}
filteredNodeKeys.add(node.id);
return true;
}, map(paoToNode, flatten(map(values, [
props.objects.sinks,
props.objects.sources,
props.objects.clients,
props.objects.modules,
]))));
edges = filter(edge => {
if (props.preferences.hideMonitorSourceEdges && edge.type === 'monitorSource') {
return false;
}
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);
});
return { nodes, edges };
}
shouldComponentUpdate(nextProps, nextState) {
return !(
(nextProps.objects === this.props.objects) &&
@ -497,6 +619,9 @@ class Graph extends React.Component {
componentDidMount() {
this.getIconPath('audio-volume-muted');
this.graphViewElement = document.querySelector('#graph .view-wrapper');
this.graphViewElement.setAttribute('tabindex', '-1');
}
componentDidUpdate() {
@ -550,15 +675,11 @@ class Graph extends React.Component {
const pai = dgoToPai.get(data);
if (pai && event.button === 1) {
if (pai.type === 'sink' ||
pai.type === 'source'
pai.type === 'source' ||
pai.type === 'client' ||
pai.type === 'module'
) {
this.toggleMute(pai);
} else if (pai.type === 'client') {
const sinkInputs = getClientSinkInputs(pai)({ pulse: this.props });
this.toggleAllMute(sinkInputs);
} else if (pai.type === 'module') {
const sinkInputs = getModuleSinkInputs(pai)({ pulse: this.props });
this.toggleAllMute(sinkInputs);
}
}
}
@ -616,85 +737,118 @@ class Graph extends React.Component {
this.props.setSinkMute(pai.index, muted);
} else if (pai.type === 'source') {
this.props.setSourceMute(pai.index, muted);
} else if (pai.type === 'client') {
const sinkInputs = getClientSinkInputs(pai)({ pulse: this.props });
this.toggleAllMute(sinkInputs);
} else if (pai.type === 'module') {
const sinkInputs = getModuleSinkInputs(pai)({ pulse: this.props });
this.toggleAllMute(sinkInputs);
}
}
focus() {
this.graphViewElement.focus();
}
deselect() {
this.setState({ selected: null });
}
hotKeyMute() {
if (!this.state.selected) {
return;
}
const pai = dgoToPai.get(this.state.selected);
if (!pai) {
return;
}
this.toggleMute(pai);
}
_findNextObjectForSelection(object, direction) {
const { type } = object || { type: 'client' };
const predicate = selectionObjectTypes.toPulsePredicate(type);
const candidates = compose(
sortBy(prop('index')),
filter(predicate),
)(this.state.nodes.concat(this.state.edges));
return (direction === 'up' ? leftOf : rightOf)(object, candidates);
}
hotKeyFocusDown() {
const selected = this._findNextObjectForSelection(this.state.selected, 'down');
this.setState({ selected });
}
hotKeyFocusUp() {
const selected = this._findNextObjectForSelection(this.state.selected, 'up');
this.setState({ selected });
}
_findAnyObjectForSelection(types) {
let node = null;
for (const type of types) {
const predicate = selectionObjectTypes.toPulsePredicate(type);
node = find(predicate, this.state.nodes) || find(predicate, this.state.edges);
if (node) {
break;
}
}
return node;
}
_focusHorizontal(direction) {
if (!this.state.selected) {
this.setState({
selected: this._findAnyObjectForSelection(direction === 'left' ? [
'sourceOutput',
'source',
] : [
'sinkInput',
'sink',
]),
});
return;
}
const type0 = this.state.selected.type;
const type1 = selectionObjectTypes[direction](
selectionObjectTypes.fromPulseType(type0),
);
const type2 = selectionObjectTypes[direction](type1);
this.setState({
selected: this._findAnyObjectForSelection([
type1,
type2,
]),
});
}
hotKeyFocusLeft() {
this._focusHorizontal('left');
}
hotKeyFocusRight() {
this._focusHorizontal('right');
}
hotKeyVolumeDown() {
}
hotKeyVolumeUp() {
}
render() {
let edges = map(paoToEdge, flatten(map(values, [
this.props.objects.sinkInputs,
this.props.objects.sourceOutputs,
this.props.derivations.monitorSources,
])));
const { nodes, edges } = this.state;
const connectedNodeKeys = new Set();
edges.forEach(edge => {
connectedNodeKeys.add(edge.source);
connectedNodeKeys.add(edge.target);
});
const filteredNodeKeys = new Set();
const nodes = filter(node => {
if ((this.props.preferences.hideDisconnectedClients && node.type === 'client') ||
(this.props.preferences.hideDisconnectedModules && node.type === 'module') ||
(this.props.preferences.hideDisconnectedSources && node.type === 'source') ||
(this.props.preferences.hideDisconnectedSinks && node.type === 'sink')
) {
if (!connectedNodeKeys.has(node.id)) {
return false;
}
}
const pai = dgoToPai.get(node);
if (pai) {
if (this.props.preferences.hideMonitors &&
pai.properties.device &&
pai.properties.device.class === 'monitor'
) {
return false;
}
if (this.props.preferences.hidePulseaudioApps) {
const binary = path([ 'properties', 'application', 'process', 'binary' ], pai) || '';
const name = path([ 'properties', 'application', 'name' ], pai) || '';
if (binary.startsWith('pavucontrol') ||
binary.startsWith('kmix') ||
name === 'paclient.js'
) {
return false;
}
}
}
filteredNodeKeys.add(node.id);
return true;
}, map(paoToNode, flatten(map(values, [
this.props.objects.sinks,
this.props.objects.sources,
this.props.objects.clients,
this.props.objects.modules,
]))));
edges = filter(edge => {
if (this.props.preferences.hideMonitorSourceEdges && edge.type === 'monitorSource') {
return false;
}
return filteredNodeKeys.has(edge.source) && filteredNodeKeys.has(edge.target);
}, edges);
nodes.forEach(node => {
const pai = getPaiByTypeAndIndex(node.type, node.index)({ pulse: this.props });
dgoToPai.set(node, pai);
});
edges.forEach(edge => {
const pai = getPaiByTypeAndIndex(edge.type, edge.index)({ pulse: this.props });
dgoToPai.set(edge, pai);
});
return r.div({
return r(HotKeys, {
handlers: map(f => bind(f, this), pick(keys(keyMap), this)),
}, r.div({
id: 'graph',
style: {},
}, r(GraphView, {
nodeKey: 'id',
edgeKey: 'id',
@ -704,7 +858,9 @@ class Graph extends React.Component {
selected: this.state.selected,
...graphConfig,
nodeTypes: {},
nodeSubtypes: {},
edgeTypes: {},
onSelectNode: this.onSelectNode,
onCreateNode: this.onCreateNode,
@ -733,7 +889,7 @@ class Graph extends React.Component {
renderEdge,
renderEdgeText: renderEdgeText(this.props),
}));
})));
}
}
@ -751,4 +907,6 @@ module.exports = connect(
preferences: state.preferences,
}),
dispatch => bindActionCreators(merge(pulseActions, iconsActions), dispatch),
null,
{ withRef: true },
)(Graph);

View File

@ -70,7 +70,7 @@ class GraphView extends React.Component {
selected: null,
};
this.graph = React.createRef();
this.graphViewRef = this.props.graphViewRef || React.createRef();
Object.assign(this, {
onSwapEdge: this.onSwapEdge.bind(this),
@ -144,7 +144,7 @@ class GraphView extends React.Component {
const createdEdgeId = `edge-${sourceNode[nodeKey]}-${targetNode[nodeKey]}-container`;
const createdEdge = document.getElementById(createdEdgeId);
createdEdge.remove();
this.graph.current.forceUpdate();
this.graphViewRef.current.forceUpdate();
}
onNodeMove(position, nodeId, shiftKey) {
@ -153,7 +153,7 @@ class GraphView extends React.Component {
if (satelliteNodes) {
this.constructor.repositionSatellites(position, satelliteNodes);
satelliteNodes.forEach(satelliteNode => {
this.graph.current.handleNodeMove(satelliteNode, satelliteNode[nodeKey], shiftKey);
this.graphViewRef.current.handleNodeMove(satelliteNode, satelliteNode[nodeKey], shiftKey);
});
}
}
@ -220,7 +220,7 @@ class GraphView extends React.Component {
selected,
ref: this.graph,
ref: this.graphViewRef,
nodes,
edges,

View File

@ -0,0 +1,75 @@
const {
keys,
pick,
map,
bind,
} = require('ramda');
const React = require('react');
const r = require('r-dom');
const { HotKeys } = require('react-hotkeys');
const keyMap = {
hotKeyEscape: 'escape',
hotKeyFocusCards: 'c',
hotKeyFocusGraph: 'g',
hotKeyFocusPreferences: 'p',
hotKeyFocusDown: [ 'j', 'down' ],
hotKeyFocusUp: [ 'k', 'up' ],
hotKeyFocusLeft: [ 'h', 'left' ],
hotKeyFocusRight: [ 'l', 'right' ],
hotKeyMute: 'm',
};
class MyHotKeys extends React.Component {
constructor(props) {
super(props);
this.graphRef = React.createRef();
this.cardsRef = React.createRef();
this.preferencesRef = React.createRef();
}
hotKeyFocusCards() {
this.cardsRef.current.getWrappedInstance().toggle();
this.preferencesRef.current.getWrappedInstance().close();
}
hotKeyFocusGraph() {
this.cardsRef.current.getWrappedInstance().close();
this.preferencesRef.current.getWrappedInstance().close();
this.graphRef.current.getWrappedInstance().focus();
}
hotKeyFocusPreferences() {
this.preferencesRef.current.getWrappedInstance().toggle();
this.cardsRef.current.getWrappedInstance().close();
}
hotKeyEscape() {
this.hotKeyFocusGraph();
this.graphRef.current.getWrappedInstance().deselect();
}
render() {
return r(HotKeys, {
keyMap,
handlers: map(f => bind(f, this), pick(keys(keyMap), this)),
}, this.props.children({
graphRef: this.graphRef,
cardsRef: this.cardsRef,
preferencesRef: this.preferencesRef,
}));
}
}
module.exports = {
HotKeys: MyHotKeys,
keyMap,
};

View File

@ -4,148 +4,166 @@ const {
defaultTo,
} = require('ramda');
const React = require('react');
const r = require('r-dom');
const { connect } = require('react-redux');
const { bindActionCreators } = require('redux');
const { withStateHandlers } = require('recompose');
const { preferences: preferencesActions } = require('../../actions');
const Button = require('../button');
const Checkbox = require('../checkbox');
const NumberInput = require('../number-input');
const Preferences = withStateHandlers(
{
open: false,
},
{
toggle: ({ open }) => () => ({ open: !open }),
},
)(({ open, toggle, ...props }) => r.div({
classSet: {
panel: true,
preferences: true,
open,
},
}, open ? [
r.div([
r(Button, {
style: { width: '100%' },
autoFocus: true,
onClick: toggle,
}, 'Close'),
]),
class Preferences extends React.Component {
constructor(props) {
super(props);
r.hr(),
this.state = {
open: false,
};
}
r.div([
r(Checkbox, {
checked: props.preferences.hideDisconnectedClients,
onChange: () => props.actions.toggle('hideDisconnectedClients'),
}, 'Hide disconnected clients'),
]),
toggle() {
this.setState({ open: !this.state.open });
}
r.div([
r(Checkbox, {
checked: props.preferences.hideDisconnectedModules,
onChange: () => props.actions.toggle('hideDisconnectedModules'),
}, 'Hide disconnected modules'),
]),
close() {
this.setState({ open: false });
}
r.div([
r(Checkbox, {
checked: props.preferences.hideDisconnectedSource,
onChange: () => props.actions.toggle('hideDisconnectedSource'),
}, 'Hide disconnected source'),
]),
render() {
const { open } = this.state;
const toggle = this.toggle.bind(this);
r.div([
r(Checkbox, {
checked: props.preferences.hideDisconnectedSinks,
onChange: () => props.actions.toggle('hideDisconnectedSinks'),
}, 'Hide disconnected sinks'),
]),
r.hr(),
r.div([
r(Checkbox, {
checked: props.preferences.hideMonitorSourceEdges,
onChange: () => props.actions.toggle('hideMonitorSourceEdges'),
}, 'Hide monitor source edges'),
]),
r.div([
r(Checkbox, {
checked: props.preferences.hideMonitors,
onChange: () => props.actions.toggle('hideMonitors'),
}, 'Hide monitors'),
]),
r.div([
r(Checkbox, {
checked: props.preferences.hidePulseaudioApps,
onChange: () => props.actions.toggle('hidePulseaudioApps'),
}, 'Hide pulseaudio applications'),
]),
r.hr(),
r.div([
r(Checkbox, {
checked: props.preferences.hideVolumeThumbnails,
onChange: () => props.actions.toggle('hideVolumeThumbnails'),
}, '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 });
return r.div({
classSet: {
panel: true,
preferences: true,
open,
},
}, 'Maximum volume: '),
]),
}, open ? [
r.div([
r(Button, {
style: { width: '100%' },
autoFocus: true,
onClick: toggle,
}, 'Close'),
]),
r.hr(),
r.hr(),
r.div([
r(Checkbox, {
checked: props.preferences.showDebugInfo,
onChange: () => props.actions.toggle('showDebugInfo'),
}, 'Show debug info'),
]),
r.div([
r(Checkbox, {
checked: this.props.preferences.hideDisconnectedClients,
onChange: () => this.props.actions.toggle('hideDisconnectedClients'),
}, 'Hide disconnected clients'),
]),
r.hr(),
r.div([
r(Checkbox, {
checked: this.props.preferences.hideDisconnectedModules,
onChange: () => this.props.actions.toggle('hideDisconnectedModules'),
}, 'Hide disconnected modules'),
]),
r.div([
r(Button, {
style: { width: '100%' },
onClick: props.actions.resetDefaults,
}, 'Reset to defaults'),
]),
] : [
r(Button, {
autoFocus: true,
onClick: toggle,
}, 'Preferences'),
]));
r.div([
r(Checkbox, {
checked: this.props.preferences.hideDisconnectedSource,
onChange: () => this.props.actions.toggle('hideDisconnectedSource'),
}, 'Hide disconnected source'),
]),
r.div([
r(Checkbox, {
checked: this.props.preferences.hideDisconnectedSinks,
onChange: () => this.props.actions.toggle('hideDisconnectedSinks'),
}, 'Hide disconnected sinks'),
]),
r.hr(),
r.div([
r(Checkbox, {
checked: this.props.preferences.hideMonitorSourceEdges,
onChange: () => this.props.actions.toggle('hideMonitorSourceEdges'),
}, 'Hide monitor source edges'),
]),
r.div([
r(Checkbox, {
checked: this.props.preferences.hideMonitors,
onChange: () => this.props.actions.toggle('hideMonitors'),
}, 'Hide monitors'),
]),
r.div([
r(Checkbox, {
checked: this.props.preferences.hidePulseaudioApps,
onChange: () => this.props.actions.toggle('hidePulseaudioApps'),
}, 'Hide pulseaudio applications'),
]),
r.hr(),
r.div([
r(Checkbox, {
checked: this.props.preferences.hideVolumeThumbnails,
onChange: () => this.props.actions.toggle('hideVolumeThumbnails'),
}, 'Hide volume thumbnails'),
]),
r.div([
r(Checkbox, {
checked: this.props.preferences.lockChannelsTogether,
onChange: () => this.props.actions.toggle('lockChannelsTogether'),
}, 'Lock channels together'),
]),
r.div([
r(NumberInput, {
type: 'number',
value: defaultTo(150, Math.round(this.props.preferences.maxVolume * 100)),
onChange: e => {
const v = defaultTo(150, Math.max(0, parseInt(e.target.value, 10)));
this.props.actions.set({ maxVolume: v / 100 });
},
}, 'Maximum volume: '),
]),
r.hr(),
r.div([
r(Checkbox, {
checked: this.props.preferences.showDebugInfo,
onChange: () => this.props.actions.toggle('showDebugInfo'),
}, 'Show debug info'),
]),
r.hr(),
r.div([
r(Button, {
style: { width: '100%' },
onClick: this.props.actions.resetDefaults,
}, 'Reset to defaults'),
]),
] : [
r(Button, {
autoFocus: true,
onClick: toggle,
}, 'Preferences'),
]);
}
}
module.exports = connect(
state => pick([ 'preferences' ], state),
dispatch => ({
actions: bindActionCreators(preferencesActions, dispatch),
}),
null,
{ withRef: true },
)(Preferences);

View File

@ -9,6 +9,10 @@ div {
box-sizing: border-box;
}
div[tabindex="-1"]:focus {
outline: 0;
}
.button {
background: var(--themeBgColor);
color: var(--themeTextColor);

View File

@ -34,6 +34,7 @@
"react": "^16.6.0",
"react-digraph": "^5.1.3",
"react-dom": "^16.6.0",
"react-hotkeys": "^1.1.4",
"react-redux": "^5.1.0",
"recompose": "^0.30.0",
"redux": "^4.0.1",

View File

@ -1,7 +1,5 @@
/* global document */
const React = require('react');
const r = require('r-dom');
const { render } = require('react-dom');
@ -13,15 +11,16 @@ const createStore = require('./store');
const Graph = require('./components/graph');
const Cards = require('./components/cards');
const Preferences = require('./components/preferences');
const { HotKeys } = require('./components/hot-keys');
const theme = require('./utils/theme');
const Root = () => r(Provider, {
store: createStore(),
}, r(React.Fragment, [
r(Graph),
r(Cards),
r(Preferences),
}, r(HotKeys, {}, ({ graphRef, cardsRef, preferencesRef }) => [
r(Graph, { ref: graphRef }),
r(Cards, { ref: cardsRef }),
r(Preferences, { ref: preferencesRef }),
]));
Object.entries(theme.colors).forEach(([ key, value ]) => {

View File

@ -3921,11 +3921,21 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isobject@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=
lodash.kebabcase@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@ -4251,6 +4261,11 @@ moment@2.x.x:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=
mousetrap@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587"
integrity sha512-jDjhi7wlHwdO6q6DS7YRmSHcuI+RVxadBkLt3KHrhd3C2b+w5pKefg3oj5beTcHZyVFA9Aksf+yEE1y5jxUjVA==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -4900,7 +4915,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.6.1, prop-types@^15.6.2:
prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
@ -5032,6 +5047,17 @@ react-dom@^16.6.0:
prop-types "^15.6.2"
scheduler "^0.10.0"
react-hotkeys@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72"
integrity sha1-oHEqouDAOnWf14hYCFmEl6TaznI=
dependencies:
lodash.isboolean "^3.0.3"
lodash.isequal "^4.5.0"
lodash.isobject "^3.0.2"
mousetrap "^1.5.2"
prop-types "^15.6.0"
react-icon-base@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"