From 0916d0eb4426f325d90835e82d12c39dee36d7fb Mon Sep 17 00:00:00 2001 From: futpib Date: Mon, 19 Nov 2018 18:11:17 +0300 Subject: [PATCH] Add default sinks/sources, fix server info updates --- actions/icons.js | 1 + actions/pulse.js | 5 ++ components/graph/index.js | 109 ++++++++++++++++++++++++++++++++--- components/hot-keys/index.js | 2 + index.css | 8 +++ package.json | 4 +- reducers/pulse.js | 11 ++++ store/pulse-middleware.js | 30 ++++++++-- todo.md | 2 - yarn.lock | 82 ++++++++++++++------------ 10 files changed, 199 insertions(+), 55 deletions(-) diff --git a/actions/icons.js b/actions/icons.js index 24ead40..f668350 100644 --- a/actions/icons.js +++ b/actions/icons.js @@ -8,6 +8,7 @@ const { iconThemeNames } = require('../utils/theme'); const fallbacks = new Map(Object.entries({ 'audio-card-pci': 'audio-card', 'audio-card-usb': 'audio-card', + starred: 'starred-symbolic', })); const cache = new Map(); diff --git a/actions/pulse.js b/actions/pulse.js index c407f1a..a88b705 100644 --- a/actions/pulse.js +++ b/actions/pulse.js @@ -14,6 +14,8 @@ module.exports = createActionCreators({ INFO: null, + SERVER_INFO: null, + MOVE_SINK_INPUT: (sinkInputIndex, destSinkIndex) => ({ sinkInputIndex, destSinkIndex }), MOVE_SOURCE_OUTPUT: (sourceOutputIndex, destSourceIndex) => ({ sourceOutputIndex, destSourceIndex }), @@ -41,5 +43,8 @@ module.exports = createActionCreators({ SET_SOURCE_MUTE: (index, muted) => ({ index, muted }), SET_SINK_INPUT_MUTE_BY_INDEX: (index, muted) => ({ index, muted }), SET_SOURCE_OUTPUT_MUTE_BY_INDEX: (index, muted) => ({ index, muted }), + + SET_DEFAULT_SINK_BY_NAME: name => ({ name }), + SET_DEFAULT_SOURCE_BY_NAME: name => ({ name }), }, }); diff --git a/components/graph/index.js b/components/graph/index.js index 0384f60..450fb78 100644 --- a/components/graph/index.js +++ b/components/graph/index.js @@ -15,6 +15,7 @@ const { max, merge, min, + omit, path, pick, prop, @@ -419,6 +420,20 @@ const VolumeControls = ({ pai, state }) => { ]); }; +const Icon = ({ state, name, ...props }) => { + const src = state.icons[name]; + + if (!src) { + return r(React.Fragment); + } + + return r.img({ + className: 'node-name-icon', + src, + ...props, + }); +}; + const DebugText = ({ dgo, pai, state }) => r.div({ style: { fontSize: '50%', @@ -431,8 +446,19 @@ const DebugText = ({ dgo, pai, state }) => r.div({ const SinkText = ({ dgo, pai, state, selected }) => r.div([ r.div({ className: 'node-name', - title: pai.name, - }, pai.description), + }, [ + state.serverInfo.defaultSinkName === pai.name && r(React.Fragment, [ + r(Icon, { + state, + name: 'starred', + title: 'Default sink', + }), + ' ', + ]), + r.span({ + title: pai.name, + }, pai.description), + ]), !selected && r(VolumeThumbnail, { pai, state }), selected && r(VolumeControls, { pai, state }), r(DebugText, { dgo, pai, state }), @@ -441,8 +467,19 @@ const SinkText = ({ dgo, pai, state, selected }) => r.div([ const SourceText = ({ dgo, pai, state, selected }) => r.div([ r.div({ className: 'node-name', - title: pai.name, - }, pai.description), + }, [ + state.serverInfo.defaultSourceName === pai.name && r(React.Fragment, [ + r(Icon, { + state, + name: 'starred', + title: 'Default source', + }), + ' ', + ]), + r.span({ + title: pai.name, + }, pai.description), + ]), !selected && r(VolumeThumbnail, { pai, state }), selected && r(VolumeControls, { pai, state }), r(DebugText, { dgo, pai, state }), @@ -527,6 +564,14 @@ class GraphContextMenu extends React.PureComponent { return r(PopupMenu, { onClose: this.props.onClose, }, [ + this.props.canSetAsDefault() && r(React.Fragment, [ + r(MenuItem, { + label: 'Set as default', + onClick: this.props.onSetAsDefault, + }), + r(MenuItem.Separator), + ]), + r(MenuItem, { label: 'Delete', onClick: this.props.onDelete, @@ -560,8 +605,12 @@ class Graph extends React.Component { onDeleteEdge: this.onDeleteEdge.bind(this), onEdgeMouseDown: this.onEdgeMouseDown.bind(this), - onContextMenuDelete: this.onContextMenuDelete.bind(this), onContextMenuClose: this.onContextMenuClose.bind(this), + + canContextMenuSetAsDefault: this.canContextMenuSetAsDefault.bind(this), + onContextMenuSetAsDefault: this.onContextMenuSetAsDefault.bind(this), + + onContextMenuDelete: this.onContextMenuDelete.bind(this), }); } @@ -674,7 +723,8 @@ class Graph extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !( - (nextProps.objects === this.props.objects) && + (nextProps.serverInfo === this.props.serverInfo) && + (nextProps.objects === this.props.objects) && (nextProps.infos === this.props.infos) && (nextProps.preferences === this.props.preferences) && (nextProps.icons === this.props.icons) && @@ -685,7 +735,7 @@ class Graph extends React.Component { } componentDidMount() { - this.getIconPath('audio-volume-muted'); + this.getIconPath('starred'); this.graphViewElement = document.querySelector('#graph .view-wrapper'); this.graphViewElement.setAttribute('tabindex', '-1'); @@ -873,6 +923,41 @@ class Graph extends React.Component { }); } + canContextMenuSetAsDefault() { + const { contexted } = this.state; + const pai = dgoToPai.get(contexted); + + if (pai && pai.type === 'sink' && pai.name !== this.props.serverInfo.defaultSinkName) { + return true; + } + + if (pai && pai.type === 'source' && pai.name !== this.props.serverInfo.defaultSourceName) { + return true; + } + + return false; + } + + setAsDefault(data) { + const pai = dgoToPai.get(data); + + if (pai.type === 'sink') { + this.props.setDefaultSinkByName(pai.name); + } + + if (pai.type === 'source') { + this.props.setDefaultSourceByName(pai.name); + } + } + + onContextMenuSetAsDefault() { + this.setAsDefault(this.state.contexted); + } + + hotKeySetAsDefault() { + this.setAsDefault(this.state.selected); + } + focus() { this.graphViewElement.focus(); } @@ -1176,6 +1261,10 @@ class Graph extends React.Component { this.state.contexted && r(GraphContextMenu, { onClose: this.onContextMenuClose, + + canSetAsDefault: this.canContextMenuSetAsDefault, + onSetAsDefault: this.onContextMenuSetAsDefault, + onDelete: this.onContextMenuDelete, }), ])); @@ -1184,6 +1273,8 @@ class Graph extends React.Component { module.exports = connect( state => ({ + serverInfo: state.pulse.serverInfo, + objects: state.pulse.objects, infos: state.pulse.infos, @@ -1195,7 +1286,9 @@ module.exports = connect( preferences: state.preferences, }), - dispatch => bindActionCreators(merge(pulseActions, iconsActions), dispatch), + dispatch => bindActionCreators(omit([ + 'serverInfo', + ], merge(pulseActions, iconsActions)), dispatch), null, { withRef: true }, )(Graph); diff --git a/components/hot-keys/index.js b/components/hot-keys/index.js index fa1240f..14ff598 100644 --- a/components/hot-keys/index.js +++ b/components/hot-keys/index.js @@ -31,6 +31,8 @@ const keyMap = { hotKeyMute: 'space', hotKeyShiftMute: 'shift+space', + + hotKeySetAsDefault: 'f', }; class MyHotKeys extends React.Component { diff --git a/index.css b/index.css index a315f8a..bfebd08 100644 --- a/index.css +++ b/index.css @@ -255,10 +255,18 @@ div[tabindex="-1"]:focus { } .node-name { + pointer-events: initial; + user-select: none; + overflow: hidden; text-overflow: ellipsis; } +.node-name-icon { + height: 1em; + vertical-align: text-top; +} + .volume-thumbnail-ruler-line { stroke-width: 2px; stroke: var(--borders); diff --git a/package.json b/package.json index f785c49..0b01b11 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ ] }, "dependencies": { - "@futpib/paclient": "^0.0.7", - "@futpib/react-electron-menu": "^0.3.0", + "@futpib/paclient": "^0.0.8", + "@futpib/react-electron-menu": "^0.3.1", "bluebird": "^3.5.3", "camelcase": "^5.0.0", "d3": "^5.7.0", diff --git a/reducers/pulse.js b/reducers/pulse.js index d01abb1..158e6a9 100644 --- a/reducers/pulse.js +++ b/reducers/pulse.js @@ -21,6 +21,8 @@ const { things } = require('../constants/pulse'); const initialState = { state: 'closed', + serverInfo: {}, + objects: fromPairs(map(({ key }) => [ key, {} ], things)), infos: fromPairs(map(({ key }) => [ key, {} ], things)), @@ -33,6 +35,15 @@ const reducer = combineReducers({ [pulse.close]: always('closed'), }, initialState.state), + serverInfo: handleActions({ + [pulse.serverInfo]: (state, { payload }) => { + return equals(state, payload) ? + state : + payload; + }, + [pulse.close]: always(initialState.serverInfo), + }, initialState.serverInfo), + objects: combineReducers(fromPairs(map(({ key, type }) => [ key, handleActions({ [pulse.new]: (state, { payload }) => { if (payload.type !== type) { diff --git a/store/pulse-middleware.js b/store/pulse-middleware.js index 2e4f808..4fc0c55 100644 --- a/store/pulse-middleware.js +++ b/store/pulse-middleware.js @@ -80,11 +80,12 @@ module.exports = store => { .on('ready', () => { store.dispatch(pulseActions.ready()); pa.subscribe('all'); + + getServerInfo(); + things.forEach(({ method, type }) => { pa[method]((err, infos) => { - if (err) { - throw err; - } + handleError(err); infos.forEach(info => { const { index } = info; info.type = info.type || type; @@ -100,7 +101,7 @@ module.exports = store => { }) .on('new', (type, index) => { if (type === 'server') { - pa.end(); // Reconnect + getServerInfo(); return; } store.dispatch(pulseActions.new({ type, index })); @@ -108,7 +109,7 @@ module.exports = store => { }) .on('change', (type, index) => { if (type === 'server') { - pa.end(); // Reconnect + getServerInfo(); return; } store.dispatch(pulseActions.change({ type, index })); @@ -134,6 +135,16 @@ module.exports = store => { reconnect(); + const getServerInfo = () => { + pa.getServerInfo((err, info) => { + if (err) { + handleError(err); + } else { + store.dispatch(pulseActions.serverInfo(info)); + } + }); + }; + const handleError = error => { if (!error) { return; @@ -228,6 +239,15 @@ module.exports = store => { pa.setSourceOutputMuteByIndex(index, muted, handleError); return state; }, + + [pulseActions.setDefaultSinkByName]: (state, { payload: { name } }) => { + pa.setDefaultSinkByName(name, handleError); + return state; + }, + [pulseActions.setDefaultSourceByName]: (state, { payload: { name } }) => { + pa.setDefaultSourceByName(name, handleError); + return state; + }, }, null); return next => action => { diff --git a/todo.md b/todo.md index 836b8c7..7ad9313 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,5 @@ 1 exit Terminate the daemon 1 load-module Load a module (args: name, arguments) -1 set-default-sink Set the default sink (args: index|name) -1 set-default-source Set the default source (args: index|name) 2 set-sink-port Change the port of a sink (args: index|name, port-name) 2 set-source-port Change the port of a source (args: index|name, port-name) 3 describe-module Describe a module (arg: name) diff --git a/yarn.lock b/yarn.lock index 0cdfa12..efa1758 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,18 +84,19 @@ dependencies: arrify "^1.0.1" -"@futpib/paclient@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@futpib/paclient/-/paclient-0.0.7.tgz#d8957135ba81888f5e92812d8e9e4e8e1ebf935f" - integrity sha512-fjpJaS3LHuo+51/7g3dqpZBGO2wZtnLAWYKVk5CIBsfqn3345xJaEe0HfLpBxPAdpAHRTcTz5aWXlhOWsBClHA== +"@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/react-electron-menu@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@futpib/react-electron-menu/-/react-electron-menu-0.3.0.tgz#33a9f5bb21823805a9782daf8ba2f8df02cde8f7" - integrity sha512-RqlD74LUvDTP5gvHwUy1qGwBLvNnjPmgbDzl8+n2t9Z5WkHsCMAXnsYfEyuKxWMkivCrEYeLBwYEardtm6hRiA== +"@futpib/react-electron-menu@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@futpib/react-electron-menu/-/react-electron-menu-0.3.1.tgz#0ef7c97c0c8c4b1f112d62609d0411155493ad90" + integrity sha512-qhQqCJr4vLjipkLdUPCm0zkL/6sKoXVv3DMUgbIuB6sMy+mBIqpuADDQUf4A2Uz1o/4zViHhNVN4ZY/y/M3GHA== dependencies: - react "^15.4.2" - react-test-renderer "^15.4.2" + prop-types "^15.6.2" + react "^16.6.3" + react-test-renderer "^16.6.3" "@ladjs/time-require@^0.1.4": version "0.1.4" @@ -1407,15 +1408,6 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -create-react-class@^15.6.0: - version "15.6.3" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" - integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2531,7 +2523,7 @@ fast-levenshtein@~2.0.4: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fbjs@^0.8.1, fbjs@^0.8.9: +fbjs@^0.8.1: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= @@ -4944,7 +4936,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@^15.5.6, 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== @@ -5111,6 +5103,11 @@ react-is@^16.3.2, react-is@^16.6.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.0.tgz#456645144581a6e99f6816ae2bd24ee94bdd0c01" integrity sha512-q8U7k0Fi7oxF1HvQgyBjPwDXeMplEsArnKt2iYhuIF86+GBbgLHdAmokL3XUFjTd7Q363OSNG55FOGUdONVn1g== +react-is@^16.6.3: + version "16.6.3" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0" + integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA== + react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -5129,13 +5126,15 @@ react-redux@^5.1.0: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react-test-renderer@^15.4.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8" - integrity sha1-0DM0NPwsQ4CSaWyncNpe1IA376g= +react-test-renderer@^16.6.3: + version "16.6.3" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.6.3.tgz#5f3a1a7d5c3379d46f7052b848b4b72e47c89f38" + integrity sha512-B5bCer+qymrQz/wN03lT0LppbZUDRq6AMfzMKrovzkGzfO81a9T+PWQW6MzkWknbwODQH/qpJno/yFQLX5IWrQ== dependencies: - fbjs "^0.8.9" - object-assign "^4.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.6.3" + scheduler "^0.11.2" react-transition-group@^1.2.0: version "1.2.1" @@ -5148,17 +5147,6 @@ react-transition-group@^1.2.0: prop-types "^15.5.6" warning "^3.0.0" -react@^15.4.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" - integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= - dependencies: - create-react-class "^15.6.0" - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - react@^16.6.0: version "16.6.0" resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246" @@ -5169,6 +5157,16 @@ react@^16.6.0: prop-types "^15.6.2" scheduler "^0.10.0" +react@^16.6.3: + version "16.6.3" + resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c" + integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.11.2" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -5745,6 +5743,14 @@ scheduler@^0.10.0: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.2.tgz#a8db5399d06eba5abac51b705b7151d2319d33d3" + integrity sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + seed-random@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"