diff --git a/src/components/connection-modal/auto-scanning-step.jsx b/src/components/connection-modal/auto-scanning-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..623bbdd02aa904ee4d0e60606a80ccd0aa1d2225 --- /dev/null +++ b/src/components/connection-modal/auto-scanning-step.jsx @@ -0,0 +1,155 @@ +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; +import keyMirror from 'keymirror'; +import classNames from 'classnames'; + +import Box from '../box/box.jsx'; +import Dots from './dots.jsx'; + +import closeIcon from '../close-button/icon--close.svg'; + +import radarIcon from './icons/searching.png'; +import bluetoothIcon from './icons/bluetooth-white.svg'; +import backIcon from './icons/back.svg'; + +import styles from './connection-modal.css'; + +const PHASES = keyMirror({ + prescan: null, + pressbutton: null, + notfound: null +}); + +const AutoScanningStep = props => ( + <Box className={styles.body}> + <Box className={styles.activityArea}> + <div className={styles.activityAreaInfo}> + <div className={styles.centeredRow}> + {props.phase === PHASES.prescan && ( + <React.Fragment> + <img + className={styles.radarBig} + src={radarIcon} + /> + <img + className={styles.bluetoothCenteredIcon} + src={bluetoothIcon} + /> + </React.Fragment> + )} + {props.phase === PHASES.pressbutton && ( + <React.Fragment> + <img + className={classNames(styles.radarBig, styles.radarSpin)} + src={radarIcon} + /> + <img + className={styles.deviceButtonImage} + src={props.deviceButtonImage} + /> + </React.Fragment> + )} + {props.phase === PHASES.notfound && ( + <Box className={styles.instructions}> + <FormattedMessage + defaultMessage="No devices found" + description="Text shown when no devices could be found" + id="gui.connection.auto-scanning.noDevicesFound" + /> + </Box> + )} + </div> + </div> + </Box> + <Box className={styles.bottomArea}> + <Box className={styles.instructions}> + {props.phase === PHASES.prescan && ( + <FormattedMessage + defaultMessage="Have your device nearby, then begin searching." + description="Prompt for beginning the search" + id="gui.connection.auto-scanning.prescan" + /> + )} + {props.phase === PHASES.pressbutton && ( + <FormattedMessage + defaultMessage="Press the button on your device." + description="Prompt for pushing the button on the device" + id="gui.connection.auto-scanning.pressbutton" + /> + )} + </Box> + <Dots + counter={0} + total={3} + /> + {props.phase === PHASES.prescan && ( + <button + className={styles.connectionButton} + onClick={props.onStartScan} + > + <FormattedMessage + defaultMessage="Start Searching" + description="Button in prompt for starting a search" + id="gui.connection.auto-scanning.start-search" + /> + </button> + )} + {props.phase === PHASES.pressbutton && ( + <div className={styles.segmentedButton}> + <button + disabled + className={styles.connectionButton} + > + <FormattedMessage + defaultMessage="Searching..." + description="Label indicating that search is in progress" + id="gui.connection.connecting-searchbutton" + /> + </button> + <button + className={styles.connectionButton} + onClick={props.onRefresh} + > + <img + className={styles.abortConnectingIcon} + src={closeIcon} + /> + </button> + </div> + )} + {props.phase === PHASES.notfound && ( + <button + className={styles.connectionButton} + onClick={props.onRefresh} + > + <img + className={styles.buttonIconLeft} + src={backIcon} + /> + <FormattedMessage + defaultMessage="Try again" + description="Button in prompt for trying a device search again" + id="gui.connection.auto-scanning.try-again" + /> + </button> + )} + </Box> + </Box> +); + +AutoScanningStep.propTypes = { + deviceButtonImage: PropTypes.string, + onRefresh: PropTypes.func, + onStartScan: PropTypes.func, + phase: PropTypes.oneOf(Object.keys(PHASES)) +}; + +AutoScanningStep.defaultProps = { + phase: PHASES.prescan +}; + +export { + AutoScanningStep as default, + PHASES +}; diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css index 5660a44eb0834a8e0a25d3be015aa29a182d173e..aec4ce630d51e7bf1bcc352d0b13855366d25b15 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -118,9 +118,18 @@ background-color: $pen-primary; } -.radar { +.radar-small { width: 40px; height: 40px; + margin-right: 0.5rem; +} + +.radar-big { + width: 120px; + height: 120px; +} + +.radar-spin { animation: spin 4s linear infinite; } @@ -147,6 +156,10 @@ height: 80px; */ } +.device-button-image { + position: absolute; +} + .bluetooth-connecting-icon { position: absolute; top: -5px; @@ -167,7 +180,6 @@ } } - .bluetooth-connected-icon { position: absolute; top: -5px; @@ -178,8 +190,6 @@ box-shadow: 0px 0px 0px 4px $pen-transparent; } - - @keyframes wiggle { 0% {transform: rotate(3deg) scale(1.05);} 25% {transform: rotate(-3deg) scale(1.05);} @@ -188,6 +198,14 @@ 100% {transform: rotate(0deg) scale(1.05);} } +.bluetooth-centered-icon { + position: absolute; + padding: 5px 5px; + background-color: $motion-primary; + border-radius: 100%; + box-shadow: 0px 0px 0px 2px $motion-transparent; +} + .device-tile-widgets { display: flex; align-items: center; diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx index b8a5feea7d84137816923816377ee114592f1836..6fb9cd4ff98c924e05d0ea5f35ebbf3aa7f1f348 100644 --- a/src/components/connection-modal/connection-modal.jsx +++ b/src/components/connection-modal/connection-modal.jsx @@ -6,6 +6,7 @@ import Box from '../box/box.jsx'; import Modal from '../../containers/modal.jsx'; import ScanningStep from '../../containers/scanning-step.jsx'; +import AutoScanningStep from '../../containers/auto-scanning-step.jsx'; import ConnectingStep from './connecting-step.jsx'; import ConnectedStep from './connected-step.jsx'; import ErrorStep from './error-step.jsx'; @@ -31,7 +32,8 @@ const ConnectionModalComponent = props => ( onRequestClose={props.onCancel} > <Box className={styles.body}> - {props.phase === PHASES.scanning && <ScanningStep {...props} />} + {props.phase === PHASES.scanning && !props.useAutoScan && <ScanningStep {...props} />} + {props.phase === PHASES.scanning && props.useAutoScan && <AutoScanningStep {...props} />} {props.phase === PHASES.connecting && <ConnectingStep {...props} />} {props.phase === PHASES.connected && <ConnectedStep {...props} />} {props.phase === PHASES.error && <ErrorStep {...props} />} @@ -42,12 +44,14 @@ const ConnectionModalComponent = props => ( ConnectionModalComponent.propTypes = { connectingMessage: PropTypes.node, + deviceButtonImage: PropTypes.string, name: PropTypes.node, onCancel: PropTypes.func.isRequired, onHelp: PropTypes.func.isRequired, phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired, smallDeviceImage: PropTypes.string, - title: PropTypes.string.isRequired + title: PropTypes.string.isRequired, + useAutoScan: PropTypes.bool.isRequired }; export { diff --git a/src/components/connection-modal/icons/searching.png b/src/components/connection-modal/icons/searching.png index 260f3227f2ef09cc1aa6fbca0fc928e2b5be0e28..7a72005a102f57ecaa70a4b74f6d6b538ee19499 100644 Binary files a/src/components/connection-modal/icons/searching.png and b/src/components/connection-modal/icons/searching.png differ diff --git a/src/components/connection-modal/scanning-step.jsx b/src/components/connection-modal/scanning-step.jsx index 0b0b4999d11cdb7ee78e0c0ca59a34676cd44dff..cadee33c3e1a6a5669c7a3232f7f3e11ef7e04b5 100644 --- a/src/components/connection-modal/scanning-step.jsx +++ b/src/components/connection-modal/scanning-step.jsx @@ -1,6 +1,7 @@ import {FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; import React from 'react'; +import classNames from 'classnames'; import Box from '../box/box.jsx'; import DeviceTile from './device-tile.jsx'; @@ -19,7 +20,7 @@ const ScanningStep = props => ( <div className={styles.activityAreaInfo}> <div className={styles.centeredRow}> <img - className={styles.radar} + className={classNames(styles.radarSmall, styles.radarSpin)} src={radarIcon} /> <FormattedMessage diff --git a/src/containers/auto-scanning-step.jsx b/src/containers/auto-scanning-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f8c0360a331dd2e141bef116dc3376c67406796c --- /dev/null +++ b/src/containers/auto-scanning-step.jsx @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import ScanningStepComponent, {PHASES} from '../components/connection-modal/auto-scanning-step.jsx'; +import VM from 'scratch-vm'; + +class AutoScanningStep extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handlePeripheralListUpdate', + 'handlePeripheralScanTimeout', + 'handleStartScan', + 'handleRefresh' + ]); + this.state = { + phase: PHASES.prescan + }; + } + componentWillUnmount () { + // @todo: stop the device scan here + this.unbindPeripheralUpdates(); + } + handlePeripheralScanTimeout () { + this.setState({ + phase: PHASES.notfound + }); + this.unbindPeripheralUpdates(); + } + handlePeripheralListUpdate (newList) { + // TODO: sort peripherals by signal strength? so they don't jump around + const peripheralArray = Object.keys(newList).map(id => + newList[id] + ); + if (peripheralArray.length > 0) { + this.props.onConnecting(peripheralArray[0].peripheralId); + } + } + bindPeripheralUpdates () { + this.props.vm.on( + 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate); + this.props.vm.on( + 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout); + } + unbindPeripheralUpdates () { + this.props.vm.removeListener( + 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate); + this.props.vm.removeListener( + 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout); + } + handleRefresh () { + // @todo: stop the device scan here, it is more important for auto scan + // due to timeout and cancellation + this.setState({ + phase: PHASES.prescan + }); + this.unbindPeripheralUpdates(); + } + handleStartScan () { + this.bindPeripheralUpdates(); + this.props.vm.startDeviceScan(this.props.extensionId); + this.setState({ + phase: PHASES.pressbutton + }); + + } + render () { + return ( + <ScanningStepComponent + deviceButtonImage={this.props.deviceButtonImage} + phase={this.state.phase} + title={this.props.extensionId} + onRefresh={this.handleRefresh} + onStartScan={this.handleStartScan} + /> + ); + } +} + +AutoScanningStep.propTypes = { + deviceButtonImage: PropTypes.string, + extensionId: PropTypes.string.isRequired, + onConnecting: PropTypes.func.isRequired, + vm: PropTypes.instanceOf(VM).isRequired +}; + +export default AutoScanningStep; diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 44d52664e366582e7fa6776e103c9a1993af3ba2..8a6bec5fd7578beff79912dea9b6dd5cc52595ed 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -364,8 +364,10 @@ class Blocks extends React.Component { if (extension) { this.setState({connectionModal: { extensionId: extensionId, + useAutoScan: extension.useAutoScan, deviceImage: extension.deviceImage, smallDeviceImage: extension.smallDeviceImage, + deviceButtonImage: extension.deviceButtonImage, name: extension.name, connectingMessage: extension.connectingMessage, helpLink: extension.helpLink @@ -433,12 +435,7 @@ class Blocks extends React.Component { ) : null} {this.state.connectionModal ? ( <ConnectionModal - connectingMessage={this.state.connectionModal.connectingMessage} - deviceImage={this.state.connectionModal.deviceImage} - extensionId={this.state.connectionModal.extensionId} - helpLink={this.state.connectionModal.helpLink} - name={this.state.connectionModal.name} - smallDeviceImage={this.state.connectionModal.smallDeviceImage} + {...this.state.connectionModal} vm={vm} onCancel={this.handleConnectionModalClose} onStatusButtonUpdate={this.handleStatusButtonUpdate} diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx index 8edf15e439381e6e49ddd0a3b6fe20c1ce3263d6..276d4f72a886e6df3b5f150d1cdd443e5eee9956 100644 --- a/src/containers/connection-modal.jsx +++ b/src/containers/connection-modal.jsx @@ -99,12 +99,14 @@ class ConnectionModal extends React.Component { return ( <ConnectionModalComponent connectingMessage={this.props.connectingMessage} + deviceButtonImage={this.props.deviceButtonImage} deviceImage={this.props.deviceImage} extensionId={this.props.extensionId} name={this.props.name} phase={this.state.phase} smallDeviceImage={this.props.smallDeviceImage} title={this.props.extensionId} + useAutoScan={this.props.useAutoScan} vm={this.props.vm} onCancel={this.props.onCancel} onConnected={this.handleConnected} @@ -119,6 +121,7 @@ class ConnectionModal extends React.Component { ConnectionModal.propTypes = { connectingMessage: PropTypes.node.isRequired, + deviceButtonImage: PropTypes.string, deviceImage: PropTypes.string.isRequired, extensionId: PropTypes.string.isRequired, helpLink: PropTypes.string.isRequired, @@ -126,6 +129,7 @@ ConnectionModal.propTypes = { onCancel: PropTypes.func.isRequired, onStatusButtonUpdate: PropTypes.func.isRequired, smallDeviceImage: PropTypes.string.isRequired, + useAutoScan: PropTypes.bool.isRequired, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/lib/libraries/extensions/device-connection/wedo/wedo-button-illustration.svg b/src/lib/libraries/extensions/device-connection/wedo/wedo-button-illustration.svg new file mode 100644 index 0000000000000000000000000000000000000000..d874b966961b1109fd34847a6dab1aa078703f2e Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/wedo/wedo-button-illustration.svg differ diff --git a/src/lib/libraries/extensions/device-connection/wedo/wedo-illustration.svg b/src/lib/libraries/extensions/device-connection/wedo/wedo-illustration.svg new file mode 100644 index 0000000000000000000000000000000000000000..e82660009ea12826e648a8e538a0e88e320abb32 Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/wedo/wedo-illustration.svg differ diff --git a/src/lib/libraries/extensions/device-connection/wedo/wedo-small.svg b/src/lib/libraries/extensions/device-connection/wedo/wedo-small.svg new file mode 100644 index 0000000000000000000000000000000000000000..70db707eb8a2d7d533c5b0ae5e05ad38f4003ec4 Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/wedo/wedo-small.svg differ diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx index 316980efe165795e21614439d23e32c29a4679c8..0bd3b36ee9a2dd9285139ca554a8ca564e9cf358 100644 --- a/src/lib/libraries/extensions/index.jsx +++ b/src/lib/libraries/extensions/index.jsx @@ -13,6 +13,9 @@ import microbitDeviceImage from './device-connection/microbit/microbit-illustrat import microbitMenuImage from './device-connection/microbit/microbit-small.svg'; import ev3DeviceImage from './device-connection/ev3/ev3-hub-illustration.svg'; import ev3MenuImage from './device-connection/ev3/ev3-small.svg'; +import wedoDeviceImage from './device-connection/wedo/wedo-illustration.svg'; +import wedoMenuImage from './device-connection/wedo/wedo-small.svg'; +import wedoButtonImage from './device-connection/wedo/wedo-button-illustration.svg'; export default [ { @@ -105,6 +108,7 @@ export default [ featured: true, disabled: false, launchDeviceConnectionFlow: true, + useAutoScan: false, deviceImage: microbitDeviceImage, smallDeviceImage: microbitMenuImage, connectingMessage: ( @@ -130,6 +134,7 @@ export default [ featured: true, disabled: false, launchDeviceConnectionFlow: true, + useAutoScan: false, deviceImage: ev3DeviceImage, smallDeviceImage: ev3MenuImage, connectingMessage: ( @@ -153,6 +158,20 @@ export default [ /> ), featured: true, - disabled: true + disabled: true, + launchDeviceConnectionFlow: true, + useAutoScan: true, + deviceImage: wedoDeviceImage, + smallDeviceImage: wedoMenuImage, + deviceButtonImage: wedoButtonImage, + connectingMessage: ( + <FormattedMessage + defaultMessage="Connecting" + description="Message to help people connect to their WeDo." + id="gui.extension.wedo2.connectingMessage" + /> + ), + helpLink: 'https://scratch.mit.edu/wedo' + } ];