Refactor of modals and storing server-side

This commit is contained in:
James Barnsley
2022-05-08 20:33:53 +12:00
parent c4f45574f7
commit e561cce2af
15 changed files with 1208 additions and 1299 deletions

View File

@ -45,56 +45,6 @@ const Notification = ({
</div>
);
case 'share-configuration-received':
return (
<div className="notification__wrapper">
<div className="notification notification--info" data-duration={duration}>
<Icon
name="close"
className="notification__close-button"
onClick={() => dispatch(removeNotification(key, true))}
/>
<h4 className="notification__title">
<I18n path="modal.share_configuration.import.title" />
</h4>
<div className="notification__content">
<p>
<I18n path="modal.share_configuration.import.subtitle" />
</p>
<ul>
{configuration.ui && (
<li><I18n path="modal.share_configuration.interface" /></li>
)}
{configuration.spotify && (
<li><I18n path="services.spotify.title" /></li>
)}
{configuration.lastfm && (
<li><I18n path="services.lastfm.title" /></li>
)}
{configuration.genius && (
<li><I18n path="services.genius.title" /></li>
)}
{configuration.snapcast && (
<li><I18n path="services.snapcast.title" /></li>
)}
</ul>
<p>
<I18n path="modal.share_configuration.import.do_you_want_to_import" />
</p>
</div>
<div className="notification__actions">
<Button
className="notification__actions__item"
onClick={() => importConfiguration(key, configuration)}
tracking={{ category: 'ShareConfiguration', action: 'Import' }}
>
<I18n path="modal.share_configuration.import.import_now" />
</Button>
</div>
</div>
</div>
);
default:
return (
<div className="notification__wrapper" key={key}>

View File

@ -574,20 +574,45 @@ modal:
lyrics: Lyrics
import_configuration:
title: Import configuration
configurations: Configurations to import
import_now: Import now
shared_config:
no_peers: No peer connections
recipients: Recipients
configurations: Configurations
import_now: Import now
imported: Import successful
share: Share now
shared: Share successful
push:
title: Share configuration
subtitle: Push your authorizations and interface settings to another, connected user
pushed:
title: Configuration received
subtitle: Another user has shared their configuration with you
server:
title: Import configuration
subtitle: Import third-party authorizations and other settings
configurations: Configurations to import
import_now: Import now
config:
label: Configurations
ui:
label: User interface
description: Theme, sorting, filters, etc
spotify:
label: Spotify authorisation
description: 'Logged in as %{name}'
lastfm:
label: LastFM authorisation
description: 'Logged in as %{name}'
genius:
label: Genius authorisation
description: 'Logged in as %{name}'
snapcast:
label: Snapcast
description: Server connection details
share_configuration:
title: Share configuration
subtitle: Push your authorizations and interface settings to another, connected user
no_peers: No peer connections
recipients: Recipients
configurations: Configurations
authorization: '%{service} authorization'
logged_in_as: 'Logged in as %{name}'
snapcast_description: Server connection details

View File

@ -113,6 +113,19 @@ export function setSharedConfig(shared_config) {
};
}
export function getSharedConfig() {
return {
type: 'PUSHER_GET_SHARED_CONFIG',
};
}
export function sharedConfig(shared_config) {
return {
type: 'PUSHER_SHARED_CONFIG',
shared_config,
};
}
export function getVersion() {
return {
type: 'PUSHER_GET_VERSION',

View File

@ -91,14 +91,17 @@ const PusherMiddleware = (function () {
case 'spotify_token_changed':
store.dispatch(spotifyActions.tokenChanged(params.spotify_token));
break;
case 'share_configuration_received':
case 'share_config_received':
store.dispatch(
uiActions.openModal(
'import-configuration',
{ context: 'pushed', configuration: params },
'import-config',
{ context: 'pushed', config: params },
),
);
break;
case 'shared_config_changed':
store.dispatch(pusherActions.sharedConfig(params.shared_config));
break;
case 'notification':
store.dispatch(uiActions.createNotification(params.notification));
break;
@ -326,6 +329,7 @@ const PusherMiddleware = (function () {
clearTimeout(reconnectTimer);
store.dispatch(pusherActions.updateConnection());
store.dispatch(pusherActions.getConfig());
store.dispatch(pusherActions.getSharedConfig());
store.dispatch(pusherActions.getRadio());
store.dispatch(pusherActions.getCommands());
store.dispatch(pusherActions.getPinned());
@ -476,6 +480,24 @@ const PusherMiddleware = (function () {
);
break;
case 'PUSHER_GET_SHARED_CONFIG':
request(store, 'get_shared_config')
.then(
(response) => {
store.dispatch({
type: 'PUSHER_SHARED_CONFIG',
shared_config: response.shared_config,
});
},
(error) => {
store.dispatch(coreActions.handleException(
'Could not load shared config',
error,
));
},
);
break;
case 'PUSHER_SET_SHARED_CONFIG': {
const { shared_config } = action;
request(store, 'set_shared_config', { shared_config })

View File

@ -65,6 +65,9 @@ export default function reducer(pusher = {}, action) {
case 'PUSHER_CONFIG':
return { ...pusher, config: action.config };
case 'PUSHER_SHARED_CONFIG':
return { ...pusher, shared_config: action.shared_config };
case 'PUSHER_PINNED_UPDATED':
return { ...pusher, pinned: action.pinned };

View File

@ -14,18 +14,18 @@ import * as lastfmActions from '../../services/lastfm/actions';
import * as geniusActions from '../../services/genius/actions';
import { i18n, I18n } from '../../locale';
import Button from '../../components/Button';
import { titleCase } from '../../util/helpers';
import { useParams } from 'react-router';
const ImportConfiguration = () => {
const ImportConfig = () => {
const { source = 'pushed' } = useParams();
const dispatch = useDispatch();
const [selected, setSelected] = useState([]);
const {
configuration = {},
context = 'server',
} = useSelector((state) => state.ui.modal?.props || {});
const configFromState = useSelector((state) => state.ui.modal?.props?.config || {});
const configFromServer = useSelector((state) => state.pusher.shared_config || {});
const config = source === 'server' ? configFromServer : configFromState;
useEffect(() => {
setWindowTitle(i18n('modal.import_configuration.title'));
setWindowTitle(i18n(`modal.shared_config.${source}.title`));
}, []);
const onSelectedChanged = (name) => {
@ -38,7 +38,7 @@ const ImportConfiguration = () => {
const onSubmit = (e) => {
e.preventDefault();
const toImport = pick(configuration, selected);
const toImport = pick(config, selected);
if (toImport.ui) {
dispatch(setUi(toImport.ui));
@ -66,7 +66,7 @@ const ImportConfiguration = () => {
));
}
dispatch(createNotification({
content: i18n('modal.share_configuration.import.successful'),
content: i18n('modal.shared_config.imported'),
}));
dispatch(closeModal());
@ -75,20 +75,20 @@ const ImportConfiguration = () => {
return (
<Modal className="modal--share-configuration">
<h1>
<I18n path={`modal.import_configuration.${context}.title`} />
<I18n path={`modal.shared_config.${source}.title`} />
</h1>
<h2>
<I18n path={`modal.import_configuration.${context}.subtitle`} />
<I18n path={`modal.shared_config.${source}.subtitle`} />
</h2>
<form onSubmit={onSubmit}>
<div className="field checkbox checkbox--block">
<div className="name">
<I18n path="modal.import_configuration.configurations" />
<I18n path="modal.shared_config.config.label" />
</div>
<div className="input">
{Object.keys(configuration).filter((i) => i !== 'name').map((name) => (
{Object.keys(config).map((name) => (
<div className="checkbox-group__item" key={`configuration_${name}`}>
<label>
<input
@ -100,7 +100,13 @@ const ImportConfiguration = () => {
<div className="label">
<div>
<div className="title">
{titleCase(name)}
<I18n path={`modal.shared_config.config.${name}.label`} />
</div>
<div className="description mid_grey-text">
<I18n
path={`modal.shared_config.config.${name}.description`}
name={config[name].me?.name || 'Unknown'}
/>
</div>
</div>
</div>
@ -118,7 +124,7 @@ const ImportConfiguration = () => {
onClick={onSubmit}
tracking={{ category: 'ImportConfiguration', action: 'Import' }}
>
<I18n path="modal.import_configuration.import_now" />
<I18n path="modal.shared_config.import_now" />
</Button>
</div>
</form>
@ -126,4 +132,4 @@ const ImportConfiguration = () => {
);
}
export default ImportConfiguration;
export default ImportConfig;

View File

@ -6,8 +6,8 @@ import EditRadio from './EditRadio';
import AddToQueue from './AddToQueue';
import InitialSetup from './InitialSetup';
import KioskMode from './KioskMode';
import ShareConfiguration from './ShareConfiguration';
import ImportConfiguration from './ImportConfiguration';
import ShareConfig from './ShareConfig';
import ImportConfig from './ImportConfig';
import AddToPlaylist from './AddToPlaylist';
import ImageZoom from './ImageZoom';
import HotkeysInfo from './HotkeysInfo';
@ -22,8 +22,8 @@ export default () => (
<Route path="/modal/add-to-playlist/:uris" component={AddToPlaylist} />
<Route path="/modal/image-zoom" component={ImageZoom} />
<Route path="/modal/hotkeys" component={HotkeysInfo} />
<Route path="/modal/share-configuration" component={ShareConfiguration} />
<Route path="/modal/import-configuration" component={ImportConfiguration} />
<Route path="/modal/share-config" component={ShareConfig} />
<Route path="/modal/import-config/:source?" component={ImportConfig} />
<Route path="/modal/reset" component={Reset} />
<Route path="/modal/servers" component={Servers} />
<Route path="/modal/edit-command/:id?" component={EditCommand} />

View File

@ -0,0 +1,230 @@
import React, { useState, useEffect } from 'react';
import { pick, map } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import Modal from './Modal';
import {
setWindowTitle,
closeModal,
createNotification,
} from '../../services/ui/actions';
import { deliverMessage, setSharedConfig } from '../../services/pusher/actions';
import { i18n, I18n } from '../../locale';
import Button from '../../components/Button';
import { indexToArray } from '../../util/arrays';
const RecipientsList = ({
selected,
onChange,
}) => {
const {
connection_id: currentConnectionId,
connections,
} = useSelector((state) => state.pusher);
const connectionsArray = indexToArray(connections).filter(
({ connection_id }) => connection_id !== currentConnectionId,
);
return (
<div className="input checkbox-group">
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="connection_server"
checked={selected.includes('SERVER')}
onChange={() => onChange('SERVER')}
/>
<div className="label">
<div>
<div className="title">
<I18n path="modal.share_configuration.server.title" />
</div>
<div className="description mid_grey-text">
<I18n path="modal.share_configuration.server.description" />
</div>
</div>
</div>
</label>
</div>
{
connectionsArray.map(({ connection_id, username, ip }) => (
<div key={connection_id} className="checkbox-group__item">
<label>
<input
type="checkbox"
name={`connection_${connection_id}`}
checked={selected.includes(connection_id)}
onChange={() => onChange(connection_id)}
/>
<div className="label">
<div>
<div className="title">{username}</div>
<div className="description mid_grey-text">{ip}</div>
</div>
</div>
</label>
</div>
))
}
</div>
);
}
const ShareConfig = () => {
const dispatch = useDispatch();
const [recipients, setRecipients] = useState([]);
const [selectedConfigs, setSelectedConfigs] = useState([]);
const spotify = useSelector((state) => state.spotify);
const genius = useSelector((state) => state.genius);
const lastfm = useSelector((state) => state.lastfm);
const ui = useSelector((state) => state.ui);
const snapcast = useSelector((state) => state.snapcast);
useEffect(() => {
setWindowTitle(i18n('modal.share_configuration.title'));
}, []);
const onRecipientChanged = (id) => {
setRecipients((prev) => {
const next = [...prev];
if (next.includes(id)) {
const index = next.indexOf(id);
next.splice(index, 1);
} else {
next.push(id);
}
return next;
});
}
const onSelectedConfigChanged = (id) => {
setSelectedConfigs((prev) => {
const next = [...prev];
if (next.includes(id)) {
const index = next.indexOf(id);
next.splice(index, 1);
} else {
next.push(id);
}
return next;
});
}
const allConfigs = {
ui: pick(
ui,
[
'language',
'theme',
'smooth_scrolling_enabled',
'hotkeys_enabled',
'allow_reporting',
'wide_scrollbars_enabled',
'hide_scrollbars',
'grid_glow_enabled',
'sort',
'initial_setup_complete',
'uri_schemes_search_enabled',
],
),
snapcast: pick(snapcast, ['enabled', 'host', 'port', 'ssl']),
spotify: pick(spotify, ['authorization', 'me']),
genius: pick(genius, ['authorization', 'me']),
lastfm: pick(lastfm, ['authorization', 'me']),
}
const onSubmit = (e) => {
e.preventDefault();
const config = pick(allConfigs, selectedConfigs)
for (const recipient of recipients) {
if (recipient === 'SERVER') {
dispatch(setSharedConfig(config));
} else {
dispatch(
deliverMessage(
recipient,
'share_config_received',
config,
),
);
}
}
dispatch(createNotification({
content: i18n('modal.shared_config.shared'),
}));
dispatch(closeModal());
}
return (
<Modal className="modal--share-configuration">
<h1>
<I18n path="modal.share_configuration.title" />
</h1>
<h2>
<I18n path="modal.share_configuration.subtitle" />
</h2>
<form onSubmit={onSubmit}>
<div className="field checkbox white">
<div className="name">
<I18n path="modal.shared_config.recipients" />
</div>
<RecipientsList
selected={recipients}
onChange={onRecipientChanged}
/>
</div>
<div className="field checkbox checkbox--block">
<div className="name">
<I18n path="modal.shared_config.config.label" />
</div>
<div className="input">
{map(allConfigs, (data, name) => (
<div className="checkbox-group__item" key={name}>
<label>
<input
type="checkbox"
name={name}
checked={selectedConfigs.indexOf(name) > -1}
onChange={() => onSelectedConfigChanged(name)}
/>
<div className="label">
<div>
<div className="title">
<I18n path={`modal.shared_config.config.${name}.label`} />
</div>
<div className="description mid_grey-text">
<I18n
path={`modal.shared_config.config.${name}.description`}
name={data.me?.name || 'Unknown'}
/>
</div>
</div>
</div>
</label>
</div>
))}
</div>
</div>
<div className="actions centered-text">
<Button
type="primary"
size="large"
disabled={recipients.length <= 0}
onClick={onSubmit}
tracking={{ category: 'ShareConfiguration', action: 'Send' }}
>
<I18n path="actions.send" />
</Button>
</div>
</form>
</Modal>
);
}
export default ShareConfig;

View File

@ -1,345 +0,0 @@
import React, { useState, useEffect } from 'react';
import { pick } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import Modal from './Modal';
import { setWindowTitle, closeModal } from '../../services/ui/actions';
import { deliverMessage, setSharedConfig } from '../../services/pusher/actions';
import { i18n, I18n } from '../../locale';
import Button from '../../components/Button';
import { indexToArray } from '../../util/arrays';
const RecipientsList = ({
selected,
onChange,
}) => {
const {
connection_id: currentConnectionId,
connections,
} = useSelector((state) => state.pusher);
const connectionsArray = indexToArray(connections).filter(
({ connection_id }) => connection_id !== currentConnectionId,
);
return (
<div className="input checkbox-group">
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="connection_server"
checked={selected.includes('SERVER')}
onChange={() => onChange('SERVER')}
/>
<div className="label">
<div>
<div className="title">
<I18n path="modal.share_configuration.server.title" />
</div>
<div className="description mid_grey-text">
<I18n path="modal.share_configuration.server.description" />
</div>
</div>
</div>
</label>
</div>
{
connectionsArray.map(({ connection_id, username, ip }) => (
<div key={connection_id} className="checkbox-group__item">
<label>
<input
type="checkbox"
name={`connection_${connection_id}`}
checked={selected.includes(connection_id)}
onChange={() => onChange(connection_id)}
/>
<div className="label">
<div>
<div className="title">{username}</div>
<div className="description mid_grey-text">{ip}</div>
</div>
</div>
</label>
</div>
))
}
</div>
);
}
const ShareConfiguration = () => {
const dispatch = useDispatch();
const [recipients, setRecipients] = useState([]);
const [selectedConfigs, setSelectedConfigs] = useState([]);
const spotify = useSelector((state) => state.spotify);
const genius = useSelector((state) => state.genius);
const lastfm = useSelector((state) => state.lastfm);
const ui = useSelector((state) => state.ui);
const snapcast = useSelector((state) => state.snapcast);
useEffect(() => {
setWindowTitle(i18n('modal.share_configuration.title'));
}, []);
const onRecipientChanged = (id) => {
setRecipients((prev) => {
const next = [...prev];
if (next.includes(id)) {
const index = next.indexOf(id);
next.splice(index, 1);
} else {
next.push(id);
}
return next;
});
}
const onSelectedConfigChanged = (id) => {
setSelectedConfigs((prev) => {
const next = [...prev];
if (next.includes(id)) {
const index = next.indexOf(id);
next.splice(index, 1);
} else {
next.push(id);
}
return next;
});
}
const collateConfig = () => {
const obj = {};
selectedConfigs.forEach((name) => {
switch (name) {
case 'spotify':
obj.spotify = pick(spotify, ['authorization', 'me']);
break;
case 'genius':
obj.genius = pick(genius, ['authorization', 'me']);
break;
case 'lastfm':
obj.lastfm = pick(lastfm, ['authorization', 'me']);
break;
case 'snapcast':
obj.snapcast = pick(snapcast, ['enabled', 'host', 'port', 'ssl']);
break;
case 'ui':
obj.ui = pick(
ui,
[
'language',
'theme',
'smooth_scrolling_enabled',
'hotkeys_enabled',
'allow_reporting',
'wide_scrollbars_enabled',
'hide_scrollbars',
'grid_glow_enabled',
'sort',
'initial_setup_complete',
'uri_schemes_search_enabled',
],
);
break;
default:
break;
}
});
return obj;
};
const onSubmit = (e) => {
e.preventDefault();
const configuration = collateConfig();
for (const recipient of recipients) {
if (recipient === 'SERVER') {
dispatch(setSharedConfig(configuration));
} else {
dispatch(
deliverMessage(
recipient,
'share_configuration_received',
configuration,
),
);
}
}
dispatch(closeModal());
}
return (
<Modal className="modal--share-configuration">
<h1>
<I18n path="modal.share_configuration.title" />
</h1>
<h2>
<I18n path="modal.share_configuration.subtitle" />
</h2>
<form onSubmit={onSubmit}>
<div className="field checkbox white">
<div className="name">
<I18n path="modal.share_configuration.recipients" />
</div>
<RecipientsList
selected={recipients}
onChange={onRecipientChanged}
/>
</div>
<div className="field checkbox checkbox--block">
<div className="name">
<I18n path="modal.share_configuration.configurations" />
</div>
<div className="input">
{spotify?.authorization && (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="spotify"
checked={selectedConfigs.indexOf('spotify') > -1}
onChange={() => onSelectedConfigChanged('spotify')}
/>
<div className="label">
<div>
<div className="title">
<I18n
path="modal.share_configuration.authorization"
service={i18n('services.spotify.title')}
/>
</div>
<div className="description mid_grey-text">
<I18n
path="modal.share_configuration.logged_in_as"
name={spotify.me?.name}
/>
</div>
</div>
</div>
</label>
</div>
)}
{lastfm?.authorization && (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="lastfm_authorization"
checked={selectedConfigs.indexOf('lastfm') > -1}
onChange={() => onSelectedConfigChanged('lastfm')}
/>
<div className="label">
<div>
<div className="title">
<I18n
path="modal.share_configuration.authorization"
service={i18n('services.lastfm.title')}
/>
</div>
<div className="description mid_grey-text">
<I18n
path="modal.share_configuration.logged_in_as"
name={lastfm.me?.name}
/>
</div>
</div>
</div>
</label>
</div>
)}
{genius?.authorization && (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="genius_authorization"
checked={selectedConfigs.indexOf('genius') > -1}
onChange={() => onSelectedConfigChanged('genius')}
/>
<div className="label">
<div>
<div className="title">
<I18n
path="modal.share_configuration.authorization"
service={i18n('services.genius.title')}
/>
</div>
<div className="description mid_grey-text">
<I18n
path="modal.share_configuration.logged_in_as"
name={genius?.me?.name || 'Unknown'}
/>
</div>
</div>
</div>
</label>
</div>
)}
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="snapcast"
checked={selectedConfigs.indexOf('snapcast') > -1}
onChange={() => onSelectedConfigChanged('snapcast')}
/>
<div className="label">
<div>
<div className="title">
<I18n path="services.snapcast.title" />
</div>
<div className="description mid_grey-text">
<I18n path="modal.share_configuration.snapcast_description" />
</div>
</div>
</div>
</label>
</div>
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="interface"
checked={selectedConfigs.indexOf('ui') > -1}
onChange={() => onSelectedConfigChanged('ui')}
/>
<div className="label">
<div>
<div className="title">
<I18n path="modal.share_configuration.interface" />
</div>
<div className="description mid_grey-text">
<I18n path="modal.share_configuration.interface_description" />
</div>
</div>
</div>
</label>
</div>
</div>
</div>
<div className="actions centered-text">
<Button
type="primary"
size="large"
disabled={recipients.length <= 0}
onClick={onSubmit}
tracking={{ category: 'ShareConfiguration', action: 'Send' }}
>
<I18n path="actions.send" />
</Button>
</div>
</form>
</Modal>
);
}
export default ShareConfiguration;

View File

@ -113,20 +113,6 @@ class Settings extends React.Component {
localScan();
}
doImportSharedConfig = () => {
const {
uiActions: {
openModal,
},
pusher: {
config: {
shared_config: configuration,
},
},
} = this.props;
openModal('import-configuration', { context: 'server', configuration });
}
renderLocalScanButton = () => {
const {
ui: { processes },
@ -525,11 +511,11 @@ class Settings extends React.Component {
<I18n path="settings.advanced.shared_configuration.label" />
</div>
<div className="input">
<Button to="/modal/share-configuration">
<Button to="/modal/share-config">
<I18n path="settings.advanced.shared_configuration.share" />
</Button>
{pusher.config?.shared_config && (
<Button onClick={this.doImportSharedConfig}>
{pusher.shared_config && (
<Button to="/modal/import-config/server">
<I18n path="settings.advanced.shared_configuration.import" />
</Button>
)}