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..c4834c076b550c7fcd2e5bf00469b57130c68276 --- /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 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.radarStatic} + src={radarIcon} + /> + <img + className={styles.bluetoothCenteredIcon} + src={bluetoothIcon} + /> + </React.Fragment> + )} + {props.phase === PHASES.pressbutton && ( + <React.Fragment> + <img + className={styles.radarBig} + 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 searching" + 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 Scanning" + 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 starting a search" + 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)), + smallDeviceImage: PropTypes.string +}; + +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 b8ca0634b32c81d1504f6f6bd915e1f2dc6380ea..7fec455ff3afc31df8416c194343a24843afc4ff 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -120,6 +120,16 @@ } } +.radar-static { + width: 120px; + height: 120px; +} + +.radar-big { + width: 120px; + height: 120px; + animation: spin 4s linear infinite; +} .device-activity { position: relative; @@ -130,6 +140,10 @@ height: 80px; */ } +.device-button-image { + position: absolute; +} + .bluetooth-connecting-icon { position: absolute; top: -5px; @@ -149,7 +163,6 @@ } } - .bluetooth-connected-icon { position: absolute; top: -5px; @@ -160,8 +173,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);} @@ -170,6 +181,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 99d9a2367dc729dca05bd75b091891a4ebdadd57..f26e3f5ad30048bde59055f76dd7977b2167b49f 100644 --- a/src/components/connection-modal/connection-modal.jsx +++ b/src/components/connection-modal/connection-modal.jsx @@ -5,8 +5,8 @@ import keyMirror from 'keymirror'; import Box from '../box/box.jsx'; import Modal from '../modal/modal.jsx'; -import PrescanStep from './prescan-step.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'; @@ -15,7 +15,6 @@ import UnavailableStep from './unavailable-step.jsx'; import styles from './connection-modal.css'; const PHASES = keyMirror({ - prescan: null, scanning: null, connecting: null, connected: null, @@ -33,8 +32,8 @@ const ConnectionModalComponent = props => ( onRequestClose={props.onCancel} > <Box className={styles.body}> - {props.phase === PHASES.prescan && <PrescanStep {...props} />} - {props.phase === PHASES.scanning && <ScanningStep {...props} />} + {props.phase === PHASES.scanning && props.useDeviceList && <ScanningStep {...props} />} + {props.phase === PHASES.scanning && !props.useDeviceList && <AutoScanningStep {...props} />} {props.phase === PHASES.connecting && <ConnectingStep {...props} />} {props.phase === PHASES.connected && <ConnectedStep {...props} />} {props.phase === PHASES.error && <ErrorStep {...props} />} @@ -45,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, + useDeviceList: 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/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 3862fa653fbe239a2db9d6e29175bcca64f508b4..0868e5eb34ce648b21cb7775c573d8b1bd50d904 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, + useDeviceList: extension.useDeviceList, deviceImage: extension.deviceImage, smallDeviceImage: extension.smallDeviceImage, + deviceButtonImage: extension.deviceButtonImage, name: extension.name, connectingMessage: extension.connectingMessage, helpLink: extension.helpLink @@ -432,12 +434,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 50862fbd2b31e6e830f756484fcd47e0fcbac0e8..4d472e10b8e8026589cfbb063534c444208f31b7 100644 --- a/src/containers/connection-modal.jsx +++ b/src/containers/connection-modal.jsx @@ -83,12 +83,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} + useDeviceList={this.props.useDeviceList} vm={this.props.vm} onCancel={this.props.onCancel} onConnected={this.handleConnected} @@ -103,6 +105,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, @@ -110,6 +113,7 @@ ConnectionModal.propTypes = { onCancel: PropTypes.func.isRequired, onStatusButtonUpdate: PropTypes.func.isRequired, smallDeviceImage: PropTypes.string.isRequired, + useDeviceList: 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..c343868b8063adc30f76615d187d375307e32e6a 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 a972d4d6c5e4cf322107dc3967e11d68b630cedf..691fac2962e4a47fbc5eee59954baf4c7e7720d8 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, + useDeviceList: true, deviceImage: microbitDeviceImage, smallDeviceImage: microbitMenuImage, connectingMessage: ( @@ -130,6 +134,7 @@ export default [ featured: true, disabled: false, launchDeviceConnectionFlow: true, + useDeviceList: true, deviceImage: ev3DeviceImage, smallDeviceImage: ev3MenuImage, connectingMessage: ( @@ -155,9 +160,10 @@ export default [ featured: true, disabled: false, launchDeviceConnectionFlow: true, - startWithPrescanStep: true, - deviceImage: ev3DeviceImage, - smallDeviceImage: ev3MenuImage, + useDeviceList: false, + deviceImage: wedoDeviceImage, + smallDeviceImage: wedoMenuImage, + deviceButtonImage: wedoButtonImage, connectingMessage: ( <FormattedMessage defaultMessage="Connecting"