diff --git a/src/js/locale/en.yaml b/src/js/locale/en.yaml index f2894499..8b338653 100755 --- a/src/js/locale/en.yaml +++ b/src/js/locale/en.yaml @@ -569,6 +569,12 @@ modal: title: Now playing title_window: '%{name} by %{artist} (now playing)' lyrics: Lyrics + import_configuration: + title: Import configuration + subtitle: Import third-party authorizations and other settings + subtitle_push: Another user has shared their configuration with you + configurations: Configurations to import + import_now: Import now share_configuration: title: Share configuration subtitle: Push your authorizations and interface settings to another, connected user @@ -580,6 +586,9 @@ modal: snapcast_description: Server connection details interface: Interface settings interface_description: Theme, sorting, filters, etc + server: + title: Server + description: Make available for any user to import import: title: Configuration shared subtitle: 'Another user has shared their configuration with you. This includes:' diff --git a/src/js/services/pusher/middleware.js b/src/js/services/pusher/middleware.js index 9f2d2104..44f70b3c 100755 --- a/src/js/services/pusher/middleware.js +++ b/src/js/services/pusher/middleware.js @@ -92,11 +92,7 @@ const PusherMiddleware = (function () { store.dispatch(spotifyActions.tokenChanged(params.spotify_token)); break; case 'share_configuration_received': - store.dispatch(uiActions.createNotification({ - type: 'share-configuration-received', - configuration: params, - sticky: true, - })); + store.dispatch(uiActions.openModal('import-configuration', params)); break; case 'notification': store.dispatch(uiActions.createNotification(params.notification)); diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index d464ddfb..85874595 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -265,6 +265,14 @@ export function removeNotification(key, manual = false) { }; } +export function openModal(name, data) { + return { + type: 'OPEN_MODAL', + name, + data, + }; +} + /** * Loaders * */ diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js index 6e9bed42..2bd13bf6 100755 --- a/src/js/services/ui/middleware.js +++ b/src/js/services/ui/middleware.js @@ -1,5 +1,3 @@ -import ReactGA from 'react-ga'; - const uiActions = require('./actions.js'); const UIMiddleware = (function () { @@ -39,21 +37,10 @@ const UIMiddleware = (function () { break; case 'OPEN_MODAL': - if (store.getState().ui.allow_reporting) { - ReactGA.event({ category: 'Modal', action: 'Opened', label: action.modal.name }); - } - $('body').addClass('modal-open'); - store.dispatch(uiActions.hideContextMenu()); - store.dispatch(uiActions.hideTouchContextMenu()); - next(action); - break; - - case 'CLOSE_MODAL': - if (store.getState().ui.allow_reporting) { - ReactGA.event({ category: 'Modal', action: 'Closed', label: null }); - } - $('body').removeClass('modal-open'); - next(action); + const { name } = action; + window.location = `/iris/modal/${name}`; + console.debug(action); + next(action); // Save data to state break; case 'HIDE_CONTEXT_MENU': diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index 6bedfa37..f5c792b6 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -76,10 +76,7 @@ export default function reducer(ui = {}, action) { * Modals * */ case 'OPEN_MODAL': - return { ...ui, modal: action.modal }; - - case 'CLOSE_MODAL': - return { ...ui, modal: false }; + return { ...ui, modal: action.data }; /** * Notifications diff --git a/src/js/views/Modals/ImportConfiguration.js b/src/js/views/Modals/ImportConfiguration.js new file mode 100644 index 00000000..a70a25c7 --- /dev/null +++ b/src/js/views/Modals/ImportConfiguration.js @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { pick } from 'lodash'; +import Modal from './Modal'; +import { setWindowTitle, set as setUi, createNotification } from '../../services/ui/actions'; +import * as spotifyActions from '../../services/spotify/actions'; +import * as snapcastActions from '../../services/snapcast/actions'; +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'; + +const ImportConfiguration = () => { + const dispatch = useDispatch(); + const [selected, setSelected] = useState([]); + const configuration = useSelector((state) => state.ui?.modal || {}); + + useEffect(() => { + setWindowTitle(i18n('modal.import_configuration.title')); + }, []); + + const onSelectedChanged = (name) => { + setSelected((prev) => { + if (prev.indexOf(name) > -1) return prev.filter((prevName) => prevName !== name); + + return [...prev, name]; + }); + } + + const onSubmit = (e) => { + e.preventDefault(); + const toImport = pick(configuration, selected); + + if (toImport.ui) { + dispatch(setUi(toImport.ui)); + } + if (toImport.spotify) { + dispatch(spotifyActions.importAuthorization( + toImport.spotify.authorization, + toImport.spotify.me, + )); + } + if (toImport.snapcast) { + dispatch(snapcastActions.set(toImport.snapcast)); + setTimeout(() => dispatch(snapcastActions.connect()), 100); + } + if (toImport.lastfm) { + dispatch(lastfmActions.importAuthorization( + toImport.lastfm.authorization, + toImport.lastfm.me, + )); + } + if (toImport.genius) { + dispatch(geniusActions.importAuthorization( + toImport.genius.authorization, + toImport.genius.me, + )); + } + dispatch(createNotification({ + content: i18n('modal.share_configuration.import.successful'), + })); + + window.history.back(); + } + + return ( + +

+ +

+

+ +

+ +
+
+
+ +
+
+ + {Object.keys(configuration).map((name) => ( +
+ +
+ ))} +
+
+ +
+ +
+
+
+ ); +} + +export default ImportConfiguration; diff --git a/src/js/views/Modals/Modals.js b/src/js/views/Modals/Modals.js index 84918409..b4204835 100644 --- a/src/js/views/Modals/Modals.js +++ b/src/js/views/Modals/Modals.js @@ -7,6 +7,7 @@ import AddToQueue from './AddToQueue'; import InitialSetup from './InitialSetup'; import KioskMode from './KioskMode'; import ShareConfiguration from './ShareConfiguration'; +import ImportConfiguration from './ImportConfiguration'; import AddToPlaylist from './AddToPlaylist'; import ImageZoom from './ImageZoom'; import HotkeysInfo from './HotkeysInfo'; @@ -22,6 +23,7 @@ export default () => ( + diff --git a/src/js/views/Modals/ShareConfiguration.js b/src/js/views/Modals/ShareConfiguration.js index 3cee76f8..98f51614 100755 --- a/src/js/views/Modals/ShareConfiguration.js +++ b/src/js/views/Modals/ShareConfiguration.js @@ -1,319 +1,329 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import Modal from './Modal'; -import * as uiActions from '../../services/ui/actions'; -import * as pusherActions from '../../services/pusher/actions'; +import { setWindowTitle } from '../../services/ui/actions'; +import { deliverMessage } from '../../services/pusher/actions'; import { i18n, I18n } from '../../locale'; import Button from '../../components/Button'; +import { indexToArray } from '../../util/arrays'; -class ShareConfiguration extends React.Component { - constructor(props) { - super(props); +const RecipientsList = ({ + selected, + onChange, +}) => { + const { + connection_id: currentConnectionId, + connections, + } = useSelector((state) => state.pusher); + const connectionsArray = indexToArray(connections).filter( + ({ connection_id }) => connection_id !== currentConnectionId, + ); - this.state = { - recipients: [], - spotify: false, - lastfm: false, - genius: false, - ui: false, - }; + return ( +
+
+ +
+ { + connectionsArray.map(({ connection_id, username, ip }) => ( +
+ +
+ )) + } +
+ ); +} + +const ShareConfiguration = () => { + const dispatch = useDispatch(); + const [recipients, setRecipients] = useState([]); + const [configuration, setConfiguration] = 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; + }); } - componentDidMount() { - this.props.uiActions.setWindowTitle(i18n('modal.share_configuration.title')); + const onConfigurationChanged = (id) => { + setConfiguration((prev) => { + const next = { ...prev }; + + if (next.id) { + delete next[id]; + } else { + switch (id) { + case 'spotify': + next.spotify = { + authorization: spotify.authorization, + me: spotify.me, + }; + break; + case 'genius': + next.genius = { + authorization: genius.authorization, + me: genius.me, + }; + break; + case 'lastfm': + next.lastfm = { + authorization: lastfm.authorization, + me: lastfm.me, + }; + break; + case 'ui': + next.ui = ui; + break; + case 'snapcast': + next.snapcast = { + enabled: snapcast.enabled, + host: snapcast.host, + port: snapcast.port, + }; + break; + default: + break; + } + } + return next; + }); } - toggleRecipient(id) { - const { recipients } = this.state; - if (recipients.includes(id)) { - const index = recipients.indexOf(id); - recipients.splice(index, 1); - } else { - recipients.push(id); - } - this.setState({ recipients }); - } - - handleSubmit(e) { + const onSubmit = (e) => { e.preventDefault(); - const configuration = {}; - if (this.state.spotify) { - configuration.spotify = { - authorization: this.props.spotify_authorization, - me: this.props.spotify_me, - }; - } - if (this.state.genius) { - configuration.genius = { - authorization: this.props.genius_authorization, - me: this.props.genius_me, - }; - } - if (this.state.lastfm) { - configuration.lastfm = { - authorization: this.props.lastfm_authorization, - me: this.props.lastfm_me, - }; - } - if (this.state.ui) { - configuration.ui = this.props.ui; - } - if (this.state.snapcast) { - configuration.snapcast = { - enabled: this.props.snapcast.enabled, - host: this.props.snapcast.host, - port: this.props.snapcast.port, - }; - } - - for (const recipient of this.state.recipients) { - this.props.pusherActions.deliverMessage( - recipient, - 'share_configuration_received', - configuration, + for (const recipient of recipients) { + dispatch( + deliverMessage( + recipient, + 'share_configuration_received', + configuration, + ), ); } window.history.back(); } - render() { - const connections = []; - for (const connection_id in this.props.connections) { - if (this.props.connections.hasOwnProperty(connection_id) && connection_id != this.props.connection_id) { - connections.push(this.props.connections[connection_id]); - } - } + return ( + - if (connections.length > 0) { - var recipients = ( -
- { - connections.map((connection, index) => ( -
- -
- )) - } -
- ); - } else { - var recipients = ( -
- - - -
- ); - } +

+ +

+

+ +

- return ( - - -

- -

-

- -

- -
this.handleSubmit(e)}> -
-
- -
- {recipients} + +
+
+
+ +
-
-
- -
-
- - {this.props.spotify_me && this.props.spotify_authorization && ( -
- -
- )} - - {this.props.lastfm_me && this.props.lastfm_authorization && ( -
- -
- )} - - {this.props.genius_me && this.props.genius_authorization && ( -
- -
- )} +
+
+ +
+
+ {spotify?.authorization && (
+ )} + {lastfm?.authorization && (
+ )} + + {genius?.authorization && ( +
+ +
+ )} + +
+ +
+ +
+
+
-
- -
- - - ); - } +
+ +
+ + + ); } -const mapStateToProps = (state) => ({ - spotify_authorization: state.spotify.authorization, - spotify_me: state.spotify.me, - genius_authorization: state.genius.authorization, - genius_me: state.genius.me, - lastfm_authorization: state.lastfm.authorization, - lastfm_me: state.lastfm.me, - ui: state.ui, - snapcast: state.snapcast, - connection_id: state.pusher.connection_id, - connections: state.pusher.connections, -}); - -const mapDispatchToProps = (dispatch) => ({ - pusherActions: bindActionCreators(pusherActions, dispatch), - uiActions: bindActionCreators(uiActions, dispatch), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ShareConfiguration); +export default ShareConfiguration;