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

@ -39,11 +39,7 @@ class IrisCore(pykka.ThreadingActor):
"seed_tracks": [],
"results": [],
}
data = {
"commands": [],
"pinned": [],
"client_config": {},
}
data = {}
ioloop = None
@classmethod
@ -885,6 +881,9 @@ class IrisCore(pykka.ThreadingActor):
# Portable configuration template for other users to import
##
def get_shared_config(self, *args, **kwargs):
return self.get_data("shared_config", *args, **kwargs)
def set_shared_config(self, *args, **kwargs):
return self.set_data("shared_config", *args, **kwargs)

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

@ -140,7 +140,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1646809993";
var build = "1651998720";
var version = "3.62.0";
// Construct the script tag

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>
)}