Fixing Discover Categories; Upgrading hotkeys to slicker plugin, adding Snapcast volume hotkeys

This commit is contained in:
James Barnsley
2021-03-30 21:27:18 +13:00
parent 875f64b616
commit 86f63b89b4
13 changed files with 1237 additions and 19848 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1616889214";
var build = "1617087113";
var version = "3.57.3";
// Construct the script tag

19125
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@
"react-dom": "^16.14.0",
"react-fontawesome": "^1.7.1",
"react-ga": "^3.3.0",
"react-hotkeys-hook": "^3.3.0",
"react-in-viewport": "^1.0.0-alpha.16",
"react-input-range": "1.3.0",
"react-redux": "^7.2.2",

View File

@ -72,7 +72,7 @@ class OutputControl extends React.Component {
return (
<div>
{
groups.map((simpleGroup) => {
sortItems(groups, 'name').map((simpleGroup) => {
const group = collate(simpleGroup, { clients });
let { clients: groupClients = [] } = group;
if (!show_disconnected_clients) {
@ -202,7 +202,7 @@ class OutputControl extends React.Component {
const mapStateToProps = (state) => ({
pusher_connected: state.pusher.connected,
snapcast_enabled: (state.pusher.config ? state.pusher.config.snapcast_enabled : null),
show_disconnected_clients: (state.ui.snapcast_show_disconnected_clients !== undefined ? state.ui.snapcast_show_disconnected_clients : false),
show_disconnected_clients: state.ui.snapcast_show_disconnected_clients || false,
snapcast_clients: state.snapcast.clients,
snapcast_groups: state.snapcast.groups,
snapcast_streams: state.snapcast.streams,

View File

@ -1,47 +1,38 @@
import React from 'react';
import ReactGA from 'react-ga';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { useHotkeys, useIsHotkeyPressed } from 'react-hotkeys-hook';
import { indexToArray, sortItems, applyFilter } from '../util/arrays';
import { collate } from '../util/format';
import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as snapcastActions from '../services/snapcast/actions';
class Hotkeys extends React.Component {
constructor(props) {
super(props);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown, false);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown, false);
}
handleKeyDown(e) {
const {
play_state,
mopidyActions,
uiActions,
mute,
play_time_position,
history,
dragging,
allow_reporting,
} = this.props;
let { volume } = this.props;
let { key } = e;
const Hotkeys = ({
play_state,
mopidyActions,
uiActions,
snapcastActions,
mute,
play_time_position,
history,
dragging,
allow_reporting,
volume,
snapcast_groups,
snapcast_clients,
show_disconnected_clients,
}) => {
const prepare = ({ e, label, callback }) => {
const {
target,
altKey,
ctrlKey,
metaKey,
shiftKey,
key,
} = e;
key = key.toLowerCase();
// Ignore text input fields
if (
(target.nodeName === 'INPUT' && (target.type === 'text' || target.type === 'number'))
@ -56,157 +47,185 @@ class Hotkeys extends React.Component {
return;
}
switch (key) {
case 'i': {
history.push('/hotkeys');
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Hotkey info' });
break;
}
case ' ':
case 'p': // Super-useful once you get used to it. This negates the issue where interactive elements
// are in focus (ie slider) and <space> is reserved for that field's interactivity.
if (play_state === 'playing') {
mopidyActions.pause();
uiActions.createNotification({ content: 'pause', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Pause' });
} else {
mopidyActions.play();
uiActions.createNotification({ content: 'play_arrow', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Play' });
}
e.preventDefault();
break;
// Stop
case 's': {
mopidyActions.stop();
uiActions.createNotification({ content: 'stop', type: 'shortcut' });
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Stop' });
break;
}
// Seek backwards 30 sec
case 'r': {
let new_position = play_time_position - 30000;
if (new_position < 0) {
new_position = 0;
}
mopidyActions.setTimePosition(new_position);
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
break;
}
// Seek forwards 30 sec
case 'f':
mopidyActions.setTimePosition(play_time_position + 30000);
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
break;
case ',':
case '<': // Previous track
mopidyActions.previous();
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
break;
case '.':
case '>': // Next track
mopidyActions.next();
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
break;
case '=':
case '+': // Volume up
if (volume !== 'false') {
volume += 5;
if (volume > 100) {
volume = 100;
}
mopidyActions.setVolume(volume);
if (mute) {
mopidyActions.setMute(false);
}
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
}
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume up' });
break;
case '-': // Volume down
if (volume !== 'false') {
volume -= 5;
if (volume < 0) {
volume = 0;
}
mopidyActions.setVolume(volume);
if (mute) {
mopidyActions.setMute(false);
}
}
uiActions.createNotification({ content: 'volume_down', type: 'shortcut' });
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume down' });
break;
case '0': // Mute
if (mute) {
mopidyActions.setMute(false);
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Unmute' });
} else {
mopidyActions.setMute(true);
uiActions.createNotification({ content: 'volume_off', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Mute' });
}
e.preventDefault();
break;
case 'escape': // Cancel current action/context
if (dragging) {
uiActions.dragEnd();
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
} else if ($('body').hasClass('modal-open')) {
window.history.back();
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
}
break;
case '1': // Navigation: Queue
history.push('/queue');
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
break;
case '2': // Navigation: Search
history.push('/search');
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
break;
case '3': // Navigation: Kiosk mode
history.push('/kiosk-mode');
e.preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
break;
default:
break;
if (allow_reporting) {
ReactGA.event({ category: 'Hotkey', action: e.key, label });
}
e.preventDefault();
callback();
};
const isPressed = useIsHotkeyPressed();
const onePressed = isPressed('1');
const twoPressed = isPressed('2');
const threePressed = isPressed('3');
const fourPressed = isPressed('4');
const fivePressed = isPressed('5');
useHotkeys('i', (e) => prepare({
e,
label: 'Hotkey info',
callback: () => {
history.push('/hotkeys');
},
}));
useHotkeys('space,p', (e) => prepare({
e,
label: 'Play/pause',
callback: () => {
if (play_state === 'playing') {
mopidyActions.pause();
uiActions.createNotification({ content: 'pause', type: 'shortcut' });
} else {
mopidyActions.play();
uiActions.createNotification({ content: 'play_arrow', type: 'shortcut' });
}
},
}), {}, [play_state]);
useHotkeys('s', (e) => prepare({
e,
label: 'Stop',
callback: () => {
mopidyActions.stop();
uiActions.createNotification({ content: 'stop', type: 'shortcut' });
},
}));
useHotkeys('r', (e) => prepare({
e,
label: 'Rewind',
callback: () => {
let new_position = play_time_position - 30000;
if (new_position < 0) {
new_position = 0;
}
mopidyActions.setTimePosition(new_position);
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
},
}));
useHotkeys('f', (e) => prepare({
e,
label: 'Fastforward',
callback: () => {
mopidyActions.setTimePosition(play_time_position + 30000);
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
},
}));
useHotkeys(',', (e) => prepare({
e,
label: 'Previous',
callback: () => {
mopidyActions.previous();
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
},
}));
useHotkeys('.', (e) => prepare({
e,
label: 'Next',
callback: () => {
mopidyActions.next();
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
},
}));
useHotkeys('=,1+=,2+=,3+=,4+=,5+=', (e, handler) => prepare({
e,
label: 'Volume up',
callback: () => {
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
if (handler.key === '=') {
if (volume !== 'false') {
let nextVolume = volume + 5;
if (nextVolume > 100) nextVolume = 100;
mopidyActions.setVolume(nextVolume);
if (mute) {
mopidyActions.setMute(false);
}
}
} else {
const index = parseInt(handler.key.replace('+='), 10);
setSnapcastVolume(index - 1, 5);
}
},
}), {}, [volume, mute]);
useHotkeys('-,1+-,2+-,3+-,4+-,5+-', (e, handler) => prepare({
e,
label: 'Volume down',
callback: () => {
uiActions.createNotification({ content: 'volume_down', type: 'shortcut' });
if (handler.key === '-') {
if (volume !== 'false') {
let nextVolume = volume - 5;
if (nextVolume < 0) nextVolume = 0;
mopidyActions.setVolume(nextVolume);
if (mute) {
mopidyActions.setMute(false);
}
}
} else {
const index = parseInt(handler.key.replace('+-'), 10);
setSnapcastVolume(index - 1, -5);
}
},
}), {}, [volume, mute]);
useHotkeys('0', (e) => prepare({
e,
label: 'Mute/unmute',
callback: () => {
if (mute) {
mopidyActions.setMute(false);
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
} else {
mopidyActions.setMute(true);
uiActions.createNotification({ content: 'volume_off', type: 'shortcut' });
}
},
}), {}, [mute]);
useHotkeys('escape', (e) => prepare({
e,
label: 'Escape',
callback: () => {
if (dragging) {
uiActions.dragEnd();
e.preventDefault();
} else if ($('body').hasClass('modal-open')) {
window.history.back();
e.preventDefault();
}
},
}), {}, [dragging]);
const setSnapcastVolume = (index, adjustment) => {
console.debug({ index, adjustment });
const simpleGroups = indexToArray(snapcast_groups);
if (simpleGroups.length <= 0) return false;
let group = sortItems(simpleGroups, 'name')[index];
if (!group) return false;
group = collate(group, { clients: snapcast_clients });
let { clients: groupClients } = group;
if (!groupClients) return false;
if (!show_disconnected_clients) {
groupClients = groupClients.filter((c) => c.connected);
}
const volume = groupClients.reduce(
(acc, client) => acc + (client.volume || 0),
0,
) / groupClients.length;
snapcastActions.setGroupVolume(group.id, volume + adjustment, volume);
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
}
render() {
return null;
}
return null;
}
const mapStateToProps = (state) => ({
@ -216,11 +235,15 @@ const mapStateToProps = (state) => ({
play_time_position: parseInt(state.mopidy.time_position, 10),
dragging: state.ui.dragger && state.ui.dragger.dragging,
allow_reporting: state.ui.allow_reporting,
snapcast_groups: state.snapcast.groups,
snapcast_clients: state.snapcast.clients,
show_disconnected_clients: state.ui.snapcast_show_disconnected_clients || false,
});
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
snapcastActions: bindActionCreators(snapcastActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Hotkeys);

View File

@ -215,28 +215,30 @@ export function getTrack(uri) {
};
}
export function getArtist(uri, name, mbid = false, nolang=false) {
export function getArtist(uri, name, mbid = false, noLanguage = false) {
return (dispatch, getState) => {
let params = 'method=artist.getInfo&';
if (mbid) {
var params = `method=artist.getInfo&mbid=${mbid}`;
params += `mbid=${mbid}`;
} else {
name = name.replace('&', 'and');
name = encodeURIComponent(name);
var params = `method=artist.getInfo&artist=${name}`;
params += `&artist=${encodeURIComponent(name.replace('&', 'and'))}`;
}
const lang = !nolang ? window.language : undefined;
if (lang !== undefined) {
params += `&lang=${lang}`;
const language = !noLanguage ? window.language : undefined;
if (language !== undefined) {
params += `&lang=${language}`;
}
sendRequest(dispatch, getState, params)
.then(
(response) => {
if (response.artist) {
if (!nolang) {
if (lang !== undefined && lang !== 'en' &&
(!response.artist.bio || response.artist.bio.content == '')) {
getArtist(uri, name, mbid, true)(dispatch, getState);
return;
if (!noLanguage) {
if (
language !== undefined
&& language !== 'en'
&& (!response.artist.bio || response.artist.bio.content === '')
) {
getArtist(uri, name, mbid, true)(dispatch, getState);
return;
}
}
dispatch(
@ -247,7 +249,7 @@ export function getArtist(uri, name, mbid = false, nolang=false) {
biography: response.artist.bio.content,
biography_publish_date: response.artist.bio.published,
biography_link: response.artist.bio.links.link.href,
listeners: parseInt(response.artist.stats.listeners),
listeners: parseInt(response.artist.stats.listeners, 10),
}),
),
);
@ -394,9 +396,9 @@ export function loveTrack(uri) {
userloved: true,
}));
dispatch(uiActions.createNotification({
content: <span>
Loved
<URILink type="track" uri={uri}>{asset ? asset.name : type}</URILink>
content: <span>
Loved
<URILink type="track" uri={uri}>{asset ? asset.name : type}</URILink>
</span>,
}));
},
@ -434,9 +436,9 @@ export function unloveTrack(uri) {
userloved: false,
}));
dispatch(uiActions.createNotification({
content: <span>
Unloved
<URILink uri={uri}>{asset ? asset.name : type}</URILink>
content: <span>
Unloved
<URILink uri={uri}>{asset ? asset.name : type}</URILink>
</span>,
}));
},

View File

@ -38,7 +38,7 @@ const CheckboxSetting = ({
checked={checked}
onChange={() => dispatch(uiActions.set({ [name]: !checked }))}
/>
<span className={`label ${tooltip ? 'tooltip' :''}`}>
<span className={`label ${tooltip ? 'tooltip' : ''}`}>
{label}
{tooltip && (
<span className="tooltip__content">

View File

@ -15,6 +15,7 @@ import {
makeItemSelector,
makeLoadingSelector,
} from '../../util/selectors';
import { decodeUri } from '../../util/format';
class DiscoverCategory extends React.Component {
componentDidMount() {
@ -126,7 +127,7 @@ class DiscoverCategory extends React.Component {
}
const mapStateToProps = (state, ownProps) => {
const uri = decodeURIComponent(ownProps.match.params.uri);
const uri = decodeUri(ownProps.match.params.uri);
const loadingSelector = makeLoadingSelector([`spotify_category_${uri}`]);
const categorySelector = makeItemSelector(uri);
const category = categorySelector(state);

View File

@ -13,8 +13,8 @@ const hotkeys = [
{ label: 'fastforward', keys: ['f'] },
{ label: 'next', keys: ['.', '>'] },
{ label: 'previous', keys: [',', '<'] },
{ label: 'volume_up', keys: ['=', '+'] },
{ label: 'volume_down', keys: ['-', '_'] },
{ label: 'volume_up', keys: ['='] },
{ label: 'volume_down', keys: ['-'] },
{ label: 'mute', keys: ['0'] },
{ label: 'exit', keys: ['esc'] },
{ label: 'now_playing', keys: ['1'] },