diff --git a/lib/units/ios-device/plugins/devicenotifier.js b/lib/units/ios-device/plugins/devicenotifier.js index 0e2db98f4..d39715f3e 100644 --- a/lib/units/ios-device/plugins/devicenotifier.js +++ b/lib/units/ios-device/plugins/devicenotifier.js @@ -11,7 +11,7 @@ module.exports = syrup.serial() const log = logger.createLogger('device:plugins:notifier') const notifier = {} - notifier.setDeviceTemporaryUnavialable = function(err) { + notifier.setDeviceTemporaryUnavailable = function(err) { group.get() .then((currentGroup) => { push.send([ @@ -22,7 +22,7 @@ module.exports = syrup.serial() ]) }) .catch(err => { - log.error('Cannot set device temporary unavialable', err) + log.error('Cannot set device temporary unavailable', err) }) } diff --git a/lib/units/ios-device/plugins/install.js b/lib/units/ios-device/plugins/install.js index 4f54142de..7ba5b5742 100755 --- a/lib/units/ios-device/plugins/install.js +++ b/lib/units/ios-device/plugins/install.js @@ -28,9 +28,6 @@ function execShellCommand(cmd) { } const installApp = (udid, filepath, id, isSimulator) => { - // #565 unable to do go-ios actions vs iOS with dashes in udid - udid = udid.replace("-", "") - const simulatorCommands = [ `cd ${filepath} && unzip * && xcrun simctl install ${udid} *.app`, ]; @@ -54,9 +51,6 @@ const installApp = (udid, filepath, id, isSimulator) => { } const launchApp = (udid, bundleId, isSimulator) => { - // #565 unable to do go-ios actions vs iOS with dashes in udid - udid = udid.replace("-", "") - const simulatorCommands = [ `xcrun simctl launch ${udid} ${bundleId}`, ]; diff --git a/lib/units/ios-device/plugins/reboot.js b/lib/units/ios-device/plugins/reboot.js index 4c947a081..4a46b904d 100755 --- a/lib/units/ios-device/plugins/reboot.js +++ b/lib/units/ios-device/plugins/reboot.js @@ -13,7 +13,7 @@ module.exports = syrup.serial() router.on(wire.RebootMessage, (channel) => { const reply = wireutil.reply(options.serial) - let udid = options.serial.replace("-", "") + let udid = options.serial exec(`ios reboot --udid=${udid}`) // this command that launches restart Promise.delay(5000) .then(() => { diff --git a/lib/units/ios-device/plugins/screen/stream.js b/lib/units/ios-device/plugins/screen/stream.js index 2667401ad..3af02d6d5 100755 --- a/lib/units/ios-device/plugins/screen/stream.js +++ b/lib/units/ios-device/plugins/screen/stream.js @@ -31,7 +31,7 @@ module.exports = syrup.serial() function handleSocketError(err, message) { log.error(message, err) - notifier.setDeviceTemporaryUnavialable(err) + notifier.setDeviceTemporaryUnavailable(err) ws.close() } @@ -92,10 +92,28 @@ module.exports = syrup.serial() ws.on('close', function() { // @TODO handle close event - //stream.socket.onclose() - WdaClient.stopSession() - isConnectionAlive = false - log.important('ws on close event') + // stream.socket.onclose() + const orientation = WdaClient.orientation + + const stoppingSession = () => { + WdaClient.stopSession() + isConnectionAlive = false + log.important('ws on close event') + } + + if (orientation === 'PORTRAIT') { + return stoppingSession() + } + + // #770: Reset rotation to Portrait when closing device + + const rotationPromise = new Promise((resolve, reject) => { + // Ensure that rotation is done, then stop session + WdaClient.rotation({orientation: 'PORTRAIT'}) + resolve() + }) + + rotationPromise.then(() => stoppingSession()) }) ws.on('error', function() { // @TODO handle error event diff --git a/lib/units/ios-device/plugins/util/iosutil.js b/lib/units/ios-device/plugins/util/iosutil.js index 63edbf545..e2a77f4a0 100755 --- a/lib/units/ios-device/plugins/util/iosutil.js +++ b/lib/units/ios-device/plugins/util/iosutil.js @@ -42,6 +42,18 @@ let iosutil = { return 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT' } }, + orientationToDegrees: function(orientation) { + switch (orientation) { + case 'PORTRAIT': + return 0 + case 'LANDSCAPE': + return 90 + case 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN': + return 180 + case 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT': + return 270 + } + }, pressButton: function(key) { switch (key) { case 'volume_up': diff --git a/lib/units/ios-device/plugins/wda/WdaClient.js b/lib/units/ios-device/plugins/wda/WdaClient.js index 809eb4bbc..37cdb4e80 100644 --- a/lib/units/ios-device/plugins/wda/WdaClient.js +++ b/lib/units/ios-device/plugins/wda/WdaClient.js @@ -113,10 +113,11 @@ module.exports = syrup.serial() return Promise.resolve() } - this.handleRequest({ + return this.handleRequest({ method: 'DELETE', uri: `${this.baseUrl}/session/${currentSessionId}` }) + }, typeKey: function(params) { @@ -533,8 +534,17 @@ module.exports = syrup.serial() this.getOrientation() this.size() - }) + const rotationDegrees = iosutil.orientationToDegrees(this.orientation) + + push.send([ + wireutil.global, + wireutil.envelope(new wire.RotationEvent( + options.serial, + rotationDegrees + )) + ]) + }) }, batteryIosEvent: function() { return this.handleRequest({ @@ -636,13 +646,23 @@ module.exports = syrup.serial() return resolve(response) }) .catch(err => { + let errMes = err.error.value.message + // #762: Skip lock error message - if (err.error.value.message.includes('Timed out while waiting until the screen gets locked')) { + if (errMes.includes('Timed out while waiting until the screen gets locked')) { return } - if (err.error.value.message.includes('Unable To Rotate Device')) { + + // #765: Skip rotation error message + if (errMes.includes('Unable To Rotate Device')) { return log.info('The current application does not support rotation') } + + // #770 Skip session crash, immediately restart after Portrait mode reset + if (errMes.includes('Session does not exist')) { + return this.startSession() + } + recoverDevice() // #409: capture wda/appium crash asap and exit with status 1 from stf //notifier.setDeviceTemporaryUnavialable(err) diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 19424eb5a..9ee8bce32 100755 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -400,17 +400,6 @@ module.exports = function(options) { } }) }) - .on(wire.ConnectivityEvent, function(channel, message) { - var {serial} = message - delete message.serial - socket.emit('device.change', { - important: false, - data: { - serial: serial, - network: message - } - }) - }) .on(wire.PhoneStateEvent, function(channel, message) { var {serial} = message delete message.serial @@ -676,7 +665,7 @@ module.exports = function(options) { } catch(e) { log.error(e) } - + }) .on('input.touchMoveIos', function(channel, data) { data.duration = 0.042 diff --git a/res/app/components/stf/common-ui/blur-element/blur-element-directive.js b/res/app/components/stf/common-ui/blur-element/blur-element-directive.js index fc4572e46..ed1f3970c 100755 --- a/res/app/components/stf/common-ui/blur-element/blur-element-directive.js +++ b/res/app/components/stf/common-ui/blur-element/blur-element-directive.js @@ -1,4 +1,8 @@ -module.exports = function blurElementDirective($parse, $timeout) { +module.exports = function blurElementDirective( + $parse, + $rootScope, + $timeout, +) { return { restrict: 'A', link: function(scope, element, attrs) { @@ -13,7 +17,15 @@ module.exports = function blurElementDirective($parse, $timeout) { }) element.bind('blur', function() { - scope.$apply(model.assign(scope, false)) + if(!$rootScope.$$phase) { + scope.$apply(() => { + model.assign(scope, false) + }) + } else { + scope.$applyAsync(() => { + model.assign(scope, false) + }) + } }) } } diff --git a/res/app/components/stf/common-ui/focus-element/focus-element-directive.js b/res/app/components/stf/common-ui/focus-element/focus-element-directive.js index 8c3b503b0..63a943375 100755 --- a/res/app/components/stf/common-ui/focus-element/focus-element-directive.js +++ b/res/app/components/stf/common-ui/focus-element/focus-element-directive.js @@ -1,4 +1,8 @@ -module.exports = function focusElementDirective($parse, $timeout) { +module.exports = function focusElementDirective( + $parse, + $rootScope, + $timeout, +) { return { restrict: 'A', link: function(scope, element, attrs) { @@ -14,7 +18,15 @@ module.exports = function focusElementDirective($parse, $timeout) { element.bind('blur', function() { if (model && model.assign) { - scope.$apply(model.assign(scope, false)) + if(!$rootScope.$$phase) { + scope.$apply(() => { + model.assign(scope, false) + }) + } else { + scope.$applyAsync(() => { + model.assign(scope, false) + }) + } } }) } diff --git a/res/app/components/stf/common-ui/modals/fatal-message/fatal-message-service.js b/res/app/components/stf/common-ui/modals/fatal-message/fatal-message-service.js index 7f689c4f8..f2912ae96 100755 --- a/res/app/components/stf/common-ui/modals/fatal-message/fatal-message-service.js +++ b/res/app/components/stf/common-ui/modals/fatal-message/fatal-message-service.js @@ -1,12 +1,20 @@ module.exports = - function FatalMessageServiceFactory($uibModal, $location, $route, $interval, - StateClassesService) { - var FatalMessageService = {} + function FatalMessageServiceFactory( + $interval, + $location, + $route, + $uibModal, + StateClassesService, + ) { + const FatalMessageService = {} + let intervalDeviceInfo - var intervalDeviceInfo - - var ModalInstanceCtrl = function($scope, $uibModalInstance, device, - tryToReconnect) { + const ModalInstanceCtrl = function( + $scope, + $uibModalInstance, + device, + tryToReconnect, + ) { $scope.ok = function() { $uibModalInstance.close(true) $route.reload() @@ -43,7 +51,7 @@ module.exports = $uibModalInstance.dismiss('cancel') } - var destroyInterval = function() { + const destroyInterval = function() { if (angular.isDefined(intervalDeviceInfo)) { $interval.cancel(intervalDeviceInfo) intervalDeviceInfo = undefined @@ -56,7 +64,7 @@ module.exports = } FatalMessageService.open = function(device, tryToReconnect) { - var modalInstance = $uibModal.open({ + const modalInstance = $uibModal.open({ template: require('./fatal-message.pug'), controller: ModalInstanceCtrl, resolve: { @@ -73,8 +81,9 @@ module.exports = }, function() { }) - } + return modalInstance + } return FatalMessageService } diff --git a/res/app/components/stf/common-ui/modals/temporarily-unavailable/index.js b/res/app/components/stf/common-ui/modals/temporarily-unavailable/index.js new file mode 100644 index 000000000..d721ccb35 --- /dev/null +++ b/res/app/components/stf/common-ui/modals/temporarily-unavailable/index.js @@ -0,0 +1,4 @@ +module.exports = angular.module('stf.temporarily-unavailable', [ + require('stf/common-ui/modals/common').name +]) + .factory('TemporarilyUnavailableService', require('./temporarily-unavailable-service')) diff --git a/res/app/components/stf/common-ui/modals/temporarily-unavialable/temporarily-unavialable-service.js b/res/app/components/stf/common-ui/modals/temporarily-unavailable/temporarily-unavailable-service.js similarity index 67% rename from res/app/components/stf/common-ui/modals/temporarily-unavialable/temporarily-unavialable-service.js rename to res/app/components/stf/common-ui/modals/temporarily-unavailable/temporarily-unavailable-service.js index ffd36640e..5eb9d7016 100644 --- a/res/app/components/stf/common-ui/modals/temporarily-unavialable/temporarily-unavialable-service.js +++ b/res/app/components/stf/common-ui/modals/temporarily-unavailable/temporarily-unavailable-service.js @@ -1,45 +1,48 @@ module.exports = - function TemporarilyUnavialableServiceFactory($uibModal, $location, $window) { + function TemporarilyUnavailableServiceFactory($uibModal, $location, $window, $route) { var service = {} var ModalInstanceCtrl = function($scope, $uibModalInstance, message) { $scope.ok = function() { $uibModalInstance.close(true) - $window.location.reload() + $route.reload() // TODO: check if works and git history } $scope.message = message $scope.cancel = function() { $uibModalInstance.dismiss('cancel') - //document.getElementById('temporarily-unavialable').remove() } $scope.second = function() { $uibModalInstance.dismiss() $location.path('/devices/') - //document.getElementById('temporarily-unavialable').remove() } } service.open = function(message) { - const tempModal = document.getElementById('temporarily-unavialable') + const tempModal = document.getElementById('temporarily-unavailable') if(!tempModal) { var modalInstance = $uibModal.open({ - template: require('./temporarily-unavialable.pug'), + template: require('./temporarily-unavailable.pug'), controller: ModalInstanceCtrl, resolve: { message: function() { return message } - } + }, + openedClass: '_temporarily-unavailable-modal', + windowClass: 'temporarily-unavailable-modal', + windowTopClass: '_top', }) modalInstance.result.then(function() { }, function() { }) + + return modalInstance } } diff --git a/res/app/components/stf/common-ui/modals/temporarily-unavialable/temporarily-unavialable.pug b/res/app/components/stf/common-ui/modals/temporarily-unavailable/temporarily-unavailable.pug similarity index 94% rename from res/app/components/stf/common-ui/modals/temporarily-unavialable/temporarily-unavialable.pug rename to res/app/components/stf/common-ui/modals/temporarily-unavailable/temporarily-unavailable.pug index b354e9f69..cc1bca6f0 100644 --- a/res/app/components/stf/common-ui/modals/temporarily-unavialable/temporarily-unavialable.pug +++ b/res/app/components/stf/common-ui/modals/temporarily-unavailable/temporarily-unavailable.pug @@ -1,4 +1,4 @@ -.stf-modal#temporarily-unavialable +.stf-modal#temporarily-unavailable .modal-header.dialog-header-errorX button(type='button', ng-click='cancel()').close × h4.modal-title.text-danger diff --git a/res/app/components/stf/common-ui/modals/temporarily-unavialable/index.js b/res/app/components/stf/common-ui/modals/temporarily-unavialable/index.js deleted file mode 100644 index 700250f1a..000000000 --- a/res/app/components/stf/common-ui/modals/temporarily-unavialable/index.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = angular.module('stf.temporarily-unavialable', [ - require('stf/common-ui/modals/common').name -]) - .factory('TemporarilyUnavialableService', require('./temporarily-unavialable-service')) diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index 91cb5907b..2464f36fd 100755 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -108,16 +108,17 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi } }.bind(this) - function fetch(data) { - deviceService.load(data.serial) - .then(function(device) { - return changeListener({ - important: true - , data: device - }) - }) - .catch(function() {}) - } + // unused code + // function fetch(data) { + // deviceService.load(data.serial) + // .then(function(device) { + // return changeListener({ + // important: true + // , data: device + // }) + // }) + // .catch(function() {}) + // } function addListener(event) { var device = get(event.data) @@ -247,16 +248,14 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi }) } - deviceService.get = function(serial, $scope) { - var tracker = new Tracker($scope, { - filter: function(device) { - return device.serial === serial - } - , digest: true + deviceService.get = (serial, $scope) => { + const tracker = new Tracker($scope, { + filter: (device) => device.serial === serial, + digest: true, }) return deviceService.load(serial) - .then(function(device) { + .then((device) => { tracker.add(device) return device }) diff --git a/res/app/components/stf/screen/index.js b/res/app/components/stf/screen/index.js index 7ce5f8c83..398a411f4 100755 --- a/res/app/components/stf/screen/index.js +++ b/res/app/components/stf/screen/index.js @@ -6,7 +6,7 @@ module.exports = angular.module('stf/screen', [ , require('stf/page-visibility').name , require('stf/browser-info').name , require('stf/common-ui/nothing-to-show').name -, require('stf/common-ui/modals/temporarily-unavialable').name +, require('stf/common-ui/modals/temporarily-unavailable').name , require('stf/screen/screen-loader').name ]) .directive('deviceScreen', require('./screen-directive')) diff --git a/res/app/components/stf/screen/screen-directive.js b/res/app/components/stf/screen/screen-directive.js index 371be3d73..67e022c73 100755 --- a/res/app/components/stf/screen/screen-directive.js +++ b/res/app/components/stf/screen/screen-directive.js @@ -1,187 +1,371 @@ -var _ = require('lodash') -var rotator = require('./rotator') -var ImagePool = require('./imagepool') +const _ = require('lodash') // TODO: import debounce only +const rotator = require('./rotator') +const ImagePool = require('./imagepool') module.exports = function DeviceScreenDirective( - $document -, ScalingService -, VendorUtil -, PageVisibilityService -, $timeout -, $window -, TemporarilyUnavialableService + $document, + $rootScope, + $route, + $timeout, + $window, + $uibModalStack, + GroupService, + PageVisibilityService, + ScalingService, + ScreenLoaderService, + TemporarilyUnavailableService, + VendorUtil, ) { return { - restrict: 'E' - , template: require('./screen.pug') - , scope: { - control: '&' - , device: '&' - } - , link: function(scope, element) { - var URL = window.URL || window.webkitURL - var BLANK_IMG = - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' - var cssTransform = VendorUtil.style(['transform', 'webkitTransform']) - - var device = scope.device() - var control = scope.control() - - var input = element.find('input') - - - var screen = scope.screen = { - rotation: 0 - , bounds: { - x: 0 - , y: 0 - , w: 0 - , h: 0 - } + restrict: 'E', + template: require('./screen.pug'), + scope: { + control: '<', + device: '<', + }, + link: function($scope, $element) { + // eslint-disable-next-line prefer-destructuring + const element = $element[0] + const URL = window.URL || window.webkitURL + const BLANK_IMG = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + const cssTransform = VendorUtil.style(['transform', 'webkitTransform']) + const input = element.querySelector('.screen__input') + const screen = { + rotation: 0, + bounds: { + x: 0, + y: 0, + w: 0, + h: 0, + }, } - - var scaler = ScalingService.coordinator( - device.display.width - , device.display.height + const cachedScreen = { + rotation: 0, + bounds: { + x: 0, + y: 0, + w: 0, + h: 0, + }, + } + const scaler = ScalingService.coordinator( + $scope.device.display.width, + $scope.device.display.height ) + let cachedImageWidth = 0 + let cachedImageHeight = 0 + let cssRotation = 0 + let alwaysUpright = false + const imagePool = new ImagePool(10) + let canvasAspect = 1 + let parentAspect = 1 + const wsReconnectionInterval = 5000 // 5s + const wsReconnectionMaxAttempts = 3 // 5s * 3 -> 15s total delay + let wsReconnectionAttempt = 0 + let wsReconnectionTimeoutID = null + let wsReconnecting = false + let tempUnavailableModalInstance = null + + $scope.screen = screen + ScreenLoaderService.show() + handleScreen() + handleKeyboard() + handleTouch() + + function closeTempUnavailableModal() { + if (tempUnavailableModalInstance) { + tempUnavailableModalInstance.dismiss(true) + tempUnavailableModalInstance = null + } else { + const modalInstance = $uibModalStack.getTop() + + if (modalInstance && modalInstance.value.openedClass === '_temporarily-unavailable-modal') { + modalInstance.key.dismiss(true) + } + } + } /** * SCREEN HANDLING * * This section should deal with updating the screen ONLY. */ - ;(function() { - function stop() { - try { - ws.onerror = ws.onclose = ws.onmessage = ws.onopen = null - ws.close() - ws = null - } - catch (err) { /* noop */ } + function handleScreen() { + let ws, adjustedBoundSize + const canvas = element.querySelector('.screen__canvas') + const g = canvas.getContext('2d') + // const positioner = element.querySelector('div.positioner') + const devicePixelRatio = window.devicePixelRatio || 1 + const backingStoreRatio = vendorBackingStorePixelRatio(g) + const frontBackRatio = devicePixelRatio / backingStoreRatio + const options = { + autoScaleForRetina: true, + density: Math.max(1, Math.min(1.5, devicePixelRatio || 1)), + minscale: 0.36, } + let cachedEnabled = false - var ws = new WebSocket(device.display.url) + connectWS() + addListeners() + resizeListener() - ws.binaryType = 'blob' + $scope.retryLoadingScreen = function() { + if ($scope.displayError === 'secure') { + $scope.control.home() + } + } - ws.onerror = function errorListener(event) { - // @todo Handle - console.log('errorListener', event) + function addListeners() { + const deviceUsingUnwatch = $scope.$watch('device.using', checkEnabled) + // TODO: for now control-panes controller will reload the state to handle this case + // const deviceStateUnwatch = $scope.$watch('device.state', (newValue, oldValue) => { + // // Try to reconnect after status changed + // if (newValue !== oldValue && newValue === 'available') { + // reconnectWS() + // } + // }) + const showScreenUnwatch = $scope.$watch('$parent.showScreen', checkEnabled) + const visibilitychangeUnwatch = $scope.$on('visibilitychange', checkEnabled) + const debouncedPaneResizeUnwatch = $scope.$on('fa-pane-resize', _.debounce(updateBounds, 1000)) + const paneResizeUnwatch = $scope.$on('fa-pane-resize', resizeListener) + const guestPortraitUnwatch = $scope.$on('guest-portrait', () => $scope.control.rotate(0)) + const guestLandscapeUnwatch = $scope.$on('guest-landscape', () => $scope.control.rotate(90)) + + $window.addEventListener('resize', resizeListener, false) + // remove all listeners + $scope.$on('$destroy', () => { + if (wsReconnectionTimeoutID) { + $timeout.cancel(wsReconnectionTimeoutID) + } + deviceUsingUnwatch() + // deviceStateUnwatch() + showScreenUnwatch() + visibilitychangeUnwatch() + debouncedPaneResizeUnwatch() + paneResizeUnwatch() + guestPortraitUnwatch() + guestLandscapeUnwatch() + stop() + $window.removeEventListener('resize', resizeListener, false) + }) } - ws.onclose = function closeListener(event) { - // @todo Maybe handle - console.log('closeListener', event) - TemporarilyUnavialableService.open('Service is currently unavailable! Try your attempt later.') + function connectWS() { + ws = new WebSocket($scope.device.display.url) + + ws.binaryType = 'blob' + ws.onerror = errorListener + ws.onclose = closeListener + ws.onopen = openListener + ws.onmessage = messageListener } - ws.onopen = function openListener() { - checkEnabled() + function reconnectWS() { + // no need reconnect by WS status (OPEN - no need, CONNECTING - "onclose" will fire reconnection) + if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) { + return + } + // no need reconnect if it is already in progress + if (wsReconnecting || wsReconnectionTimeoutID) { + return + } + + wsReconnecting = true + wsReconnectionAttempt += 1 + stop() + connectWS() } - var canvas = element.find('canvas')[0] - var g = canvas.getContext('2d') - var positioner = element.find('div')[0] + function errorListener() { + // handle if need + // console.log('DeviceScreen::WS connection error') + } - function vendorBackingStorePixelRatio(g) { - return g.webkitBackingStorePixelRatio || - g.mozBackingStorePixelRatio || - g.msBackingStorePixelRatio || - g.oBackingStorePixelRatio || - g.backingStorePixelRatio || 1 + function closeListener() { + wsReconnecting = false + ScreenLoaderService.show() + + if (wsReconnectionAttempt < wsReconnectionMaxAttempts) { + wsReconnectionTimeoutID = $timeout(() => { + wsReconnectionTimeoutID = null + reconnectWS() + }, wsReconnectionInterval) + } else if (!tempUnavailableModalInstance) { + tempUnavailableModalInstance = TemporarilyUnavailableService + .open('Service is currently unavailable! Try your attempt later.') + } } - var devicePixelRatio = window.devicePixelRatio || 1 - var backingStoreRatio = vendorBackingStorePixelRatio(g) - var frontBackRatio = devicePixelRatio / backingStoreRatio + function openListener() { + closeTempUnavailableModal() + checkEnabled() - var options = { - autoScaleForRetina: true - , density: Math.max(1, Math.min(1.5, devicePixelRatio || 1)) - , minscale: 0.36 + if (wsReconnecting) { + wsReconnecting = false + wsReconnectionAttempt = 0 + } } - var adjustedBoundSize - var cachedEnabled = false + function messageListener(message) { + screen.rotation = $scope.device.display.rotation - function updateBounds() { - function adjustBoundedSize(w, h) { - var sw = w * options.density - var sh = h * options.density - var f - - if (sw < (f = device.display.width * options.minscale)) { - sw *= f / sw - sh *= f / sh - } + if (message.data instanceof Blob) { + if (shouldUpdateScreen()) { + if ($scope.displayError) { + $scope.$apply(() => { + $scope.displayError = false + }) + } + if (ScreenLoaderService.isVisible) { + ScreenLoaderService.hide() + } - if (sh < (f = device.display.height * options.minscale)) { - sw *= f / sw - sh *= f / sh - } - return { - w: Math.ceil(sw) - , h: Math.ceil(sh) + let blob = new Blob([message.data], {type: 'image/jpeg'}) + let img = imagePool.next() + let url = URL.createObjectURL(blob) + const cleanData = () => { + img.onload = img.onerror = null + img.src = BLANK_IMG + img = null + blob = null + URL.revokeObjectURL(url) + url = null + } + + img.onload = function() { + updateImageArea(this) + g.drawImage(img, 0, 0, img.width, img.height) + + // Try to forcefully clean everything to get rid of memory + // leaks. Note that despite this effort, Chrome will still + // leak huge amounts of memory when the developer tools are + // open, probably to save the resources for inspection. When + // the developer tools are closed no memory is leaked. + cleanData() + } + + img.onerror = function() { + // Happily ignore. I suppose this shouldn't happen, but + // sometimes it does, presumably when we're loading images + // too quickly. + + // Do the same cleanup here as in onload. + cleanData() + } + + img.src = url } + } else if (/^start /.test(message.data)) { + applyQuirks(JSON.parse(message.data.substr('start '.length))) + } else if (message.data === 'secure_on') { + $scope.$apply(() => { + $scope.displayError = 'secure' + }) + } + } + + function stop() { + try { + ws.onerror = ws.onclose = ws.onmessage = ws.onopen = null + ws.close() + ws = null } + catch (err) { /* noop */ } + } - // FIXME: element is an object HTMLUnknownElement in IE9 - var w = screen.bounds.w = element[0].offsetWidth - var h = screen.bounds.h = element[0].offsetHeight + function vendorBackingStorePixelRatio(g) { + return g.webkitBackingStorePixelRatio + || g.mozBackingStorePixelRatio + || g.msBackingStorePixelRatio + || g.oBackingStorePixelRatio + || g.backingStorePixelRatio + || 1 + } + function updateBounds() { // Developer error, let's try to reduce debug time - if (!w || !h) { - throw new Error( - 'Unable to read bounds; container must have dimensions' - ) + if (!element.offsetWidth || !element.offsetHeight) { + throw new Error('Unable to read bounds; container must have dimensions') } - var newAdjustedBoundSize = (function() { - switch (screen.rotation) { + const w = element.offsetWidth + const h = element.offsetHeight + let newAdjustedBoundSize + + screen.bounds.w = w + screen.bounds.h = h + newAdjustedBoundSize = getNewAdjustedBoundSize(w, h) + + if (!adjustedBoundSize + || newAdjustedBoundSize.w !== adjustedBoundSize.w + || newAdjustedBoundSize.h !== adjustedBoundSize.h) { + adjustedBoundSize = newAdjustedBoundSize + onScreenInterestAreaChanged() + } + } + + function getNewAdjustedBoundSize(w, h) { + switch (screen.rotation) { case 90: case 270: return adjustBoundedSize(h, w) case 0: case 180: - /* falls through */ + /* falls through */ default: return adjustBoundedSize(w, h) - } - })() + } + } - if (!adjustedBoundSize || - newAdjustedBoundSize.w !== adjustedBoundSize.w || - newAdjustedBoundSize.h !== adjustedBoundSize.h) { - adjustedBoundSize = newAdjustedBoundSize - onScreenInterestAreaChanged() + function adjustBoundedSize(w, h) { + let sw = w * options.density + let sh = h * options.density + const scaledW = $scope.device.display.width * options.minscale + const scaledH = $scope.device.display.height * options.minscale + + if (sw < scaledW) { + sw *= scaledW / sw + sh *= scaledW / sh + } + + if (sh < scaledH) { + sw *= scaledH / sw + sh *= scaledH / sh + } + + return { + w: Math.ceil(sw), + h: Math.ceil(sh), } } function shouldUpdateScreen() { return ( // NO if the user has disabled the screen. - scope.$parent.showScreen && + $scope.$parent.showScreen // NO if we're not even using the device anymore. - //device.using && + //$scope.device.using && // NO if the page is not visible (e.g. background tab). - !PageVisibilityService.hidden && + && !PageVisibilityService.hidden // NO if we don't have a connection yet. - ws.readyState === WebSocket.OPEN + && (ws && ws.readyState === WebSocket.OPEN) // YES otherwise ) } - function checkEnabled() { - var newEnabled = shouldUpdateScreen() + function checkEnabled(isReconnected) { + const newEnabled = shouldUpdateScreen() if (newEnabled === cachedEnabled) { updateBounds() - } - else if (newEnabled) { + onScreenInterestGained() + // if (isReconnected) { + // onScreenInterestGained() + // } + } else if (newEnabled) { updateBounds() onScreenInterestGained() - } - else { + } else { g.clearRect(0, 0, canvas.width, canvas.height) onScreenInterestLost() } @@ -190,216 +374,94 @@ module.exports = function DeviceScreenDirective( } function onScreenInterestGained() { - if (ws.readyState === WebSocket.OPEN) { + if (ws && ws.readyState === WebSocket.OPEN) { ws.send('size ' + adjustedBoundSize.w + 'x' + adjustedBoundSize.h) ws.send('on') } } function onScreenInterestAreaChanged() { - if (ws.readyState === WebSocket.OPEN) { + if (ws && ws.readyState === WebSocket.OPEN) { ws.send('size ' + adjustedBoundSize.w + 'x' + adjustedBoundSize.h) } } function onScreenInterestLost() { - if (ws.readyState === WebSocket.OPEN) { + if (ws && ws.readyState === WebSocket.OPEN) { ws.send('off') } } - ws.onmessage = (function() { - var cachedScreen = { - rotation: 0 - , bounds: { - x: 0 - , y: 0 - , w: 0 - , h: 0 - } - } - - var cachedImageWidth = 0 - var cachedImageHeight = 0 - var cssRotation = 0 - var alwaysUpright = false - var imagePool = new ImagePool(10) + function applyQuirks(banner) { + // eslint-disable-next-line prefer-destructuring + alwaysUpright = banner.quirks.alwaysUpright + element.classList.toggle('quirk-always-upright', alwaysUpright) + } - function applyQuirks(banner) { - element[0].classList.toggle( - 'quirk-always-upright', alwaysUpright = banner.quirks.alwaysUpright) - } + function hasImageAreaChanged(img) { + return cachedScreen.bounds.w !== screen.bounds.w + || cachedScreen.bounds.h !== screen.bounds.h + || cachedImageWidth !== img.width + || cachedImageHeight !== img.height + || cachedScreen.rotation !== screen.rotation + } - function hasImageAreaChanged(img) { - return cachedScreen.bounds.w !== screen.bounds.w || - cachedScreen.bounds.h !== screen.bounds.h || - cachedImageWidth !== img.width || - cachedImageHeight !== img.height || - cachedScreen.rotation !== screen.rotation - } + function isRotated() { + return screen.rotation === 90 || screen.rotation === 270 + } - function isRotated() { - return screen.rotation === 90 || screen.rotation === 270 + function updateImageArea(img) { + if (!hasImageAreaChanged(img)) { + return } - function updateImageArea(img) { - if (!hasImageAreaChanged(img)) { - return - } - - cachedImageWidth = img.width - cachedImageHeight = img.height - - if (options.autoScaleForRetina) { - canvas.width = cachedImageWidth * frontBackRatio - canvas.height = cachedImageHeight * frontBackRatio - g.scale(frontBackRatio, frontBackRatio) - } - else { - canvas.width = cachedImageWidth - canvas.height = cachedImageHeight - } - - cssRotation += rotator(cachedScreen.rotation, screen.rotation) - - // canvas.style[cssTransform] = 'rotate(' + cssRotation + 'deg)' + cachedImageWidth = img.width + cachedImageHeight = img.height - cachedScreen.bounds.h = screen.bounds.h - cachedScreen.bounds.w = screen.bounds.w - cachedScreen.rotation = screen.rotation - - canvasAspect = canvas.width / canvas.height - if (isRotated() && !alwaysUpright) { - canvasAspect = img.height / img.width - element[0].classList.add('rotated') - } - else { - canvasAspect = img.width / img.height - element[0].classList.remove('rotated') - } - - if (alwaysUpright) { - // If the screen image is always in upright position (but we - // still want the rotation animation), we need to cancel out - // the rotation by using another rotation. - // positioner.style[cssTransform] = 'rotate(' + -cssRotation + 'deg)' - } - - maybeFlipLetterbox() + if (options.autoScaleForRetina) { + canvas.width = cachedImageWidth * frontBackRatio + canvas.height = cachedImageHeight * frontBackRatio + g.scale(frontBackRatio, frontBackRatio) + } else { + canvas.width = cachedImageWidth + canvas.height = cachedImageHeight } - return function messageListener(message) { - screen.rotation = device.display.rotation - - if (message.data instanceof Blob) { - if (shouldUpdateScreen()) { - if (scope.displayError) { - scope.$apply(function() { - scope.displayError = false - }) - } + cssRotation += rotator(cachedScreen.rotation, screen.rotation) + // canvas.style[cssTransform] = 'rotate(' + cssRotation + 'deg)' - scope.$emit('hide-screen-loader') + cachedScreen.bounds.h = screen.bounds.h + cachedScreen.bounds.w = screen.bounds.w + cachedScreen.rotation = screen.rotation - var blob = new Blob([message.data], { - type: 'image/jpeg' - }) - - var img = imagePool.next() - - img.onload = function() { - updateImageArea(this) - - g.drawImage(img, 0, 0, img.width, img.height) - - // Try to forcefully clean everything to get rid of memory - // leaks. Note that despite this effort, Chrome will still - // leak huge amounts of memory when the developer tools are - // open, probably to save the resources for inspection. When - // the developer tools are closed no memory is leaked. - img.onload = img.onerror = null - img.src = BLANK_IMG - img = null - blob = null - - URL.revokeObjectURL(url) - url = null - } - - img.onerror = function() { - // Happily ignore. I suppose this shouldn't happen, but - // sometimes it does, presumably when we're loading images - // too quickly. - - // Do the same cleanup here as in onload. - img.onload = img.onerror = null - img.src = BLANK_IMG - img = null - blob = null - - URL.revokeObjectURL(url) - url = null - } - - var url = URL.createObjectURL(blob) - img.src = url - } - } - else if (/^start /.test(message.data)) { - applyQuirks(JSON.parse(message.data.substr('start '.length))) - } - else if (message.data === 'secure_on') { - scope.$apply(function() { - scope.displayError = 'secure' - }) - } + canvasAspect = canvas.width / canvas.height + if (isRotated() && !alwaysUpright) { + canvasAspect = img.height / img.width + element.classList.add('rotated') + } else { + canvasAspect = img.width / img.height + element.classList.remove('rotated') } - })() - // NOTE: instead of fa-pane-resize, a fa-child-pane-resize could be better - scope.$on('fa-pane-resize', _.debounce(updateBounds, 1000)) - scope.$watch('device.using', checkEnabled) - scope.$on('visibilitychange', checkEnabled) - scope.$watch('$parent.showScreen', checkEnabled) + // if (alwaysUpright) { + // If the screen image is always in upright position (but we + // still want the rotation animation), we need to cancel out + // the rotation by using another rotation. + // positioner.style[cssTransform] = 'rotate(' + -cssRotation + 'deg)' + // } - scope.retryLoadingScreen = function() { - if (scope.displayError === 'secure') { - control.home() - } + maybeFlipLetterbox() } - scope.$on('guest-portrait', function() { - control.rotate(0) - }) - - scope.$on('guest-landscape', function() { - console.log('rotate gues-landscape : 90') - control.rotate(90) - }) - - var canvasAspect = 1 - var parentAspect = 1 - function resizeListener() { - parentAspect = element[0].offsetWidth / element[0].offsetHeight + parentAspect = element.offsetWidth / element.offsetHeight maybeFlipLetterbox() } function maybeFlipLetterbox() { - element[0].classList.toggle( - 'letterboxed', parentAspect < canvasAspect) + element.classList.toggle('letterboxed', parentAspect < canvasAspect) } - - $window.addEventListener('resize', resizeListener, false) - scope.$on('fa-pane-resize', resizeListener) - - resizeListener() - - scope.$on('$destroy', function() { - stop() - $window.removeEventListener('resize', resizeListener, false) - }) - })() + } /** * KEYBOARD HANDLING @@ -410,7 +472,9 @@ module.exports = function DeviceScreenDirective( * For now, try to keep the whole section as a separate unit as much * as possible. */ - ;(function() { + function handleKeyboard() { + const $input = angular.element(input) + function isChangeCharsetKey(e) { // Add any special key here for changing charset //console.log('e', e) @@ -447,7 +511,7 @@ module.exports = function DeviceScreenDirective( function handleSpecialKeys(e) { if (isChangeCharsetKey(e)) { e.preventDefault() - control.keyPress('switch_charset') + $scope.control.keyPress('switch_charset') return true } @@ -460,12 +524,12 @@ module.exports = function DeviceScreenDirective( if (e.keyCode === 9) { e.preventDefault() } - control.keyDown(e.keyCode) + $scope.control.keyDown(e.keyCode) } function keyupListener(e) { if (!handleSpecialKeys(e)) { - control.keyUp(e.keyCode) + $scope.control.keyUp(e.keyCode) } } @@ -474,7 +538,7 @@ module.exports = function DeviceScreenDirective( // the real value instead of any "\n" -> " " conversions we might see // in the input value. e.preventDefault() - control.paste(e.clipboardData.getData('text/plain')) + $scope.control.paste(e.clipboardData.getData('text/plain')) } function copyListener(e) { @@ -484,9 +548,9 @@ module.exports = function DeviceScreenDirective( // what happens is that on the first copy, it will attempt to fetch // the clipboard contents. Only on the second copy will it actually // copy that to the clipboard. - control.getClipboardContent() - if (control.clipboardContent) { - e.clipboardData.setData('text/plain', control.clipboardContent) + $scope.control.getClipboardContent() + if ($scope.control.clipboardContent) { + e.clipboardData.setData('text/plain', $scope.control.clipboardContent) } } @@ -496,16 +560,16 @@ module.exports = function DeviceScreenDirective( // you use the "Romaji" Kotoeri input method, we'll never get any // keypress events. It also causes us to lose the very first keypress // on the page. Currently I'm not sure if we can fix that one. - control.type(this.value) + $scope.control.type(this.value) this.value = '' } - input.bind('keydown', keydownListener) - input.bind('keyup', keyupListener) - input.bind('input', inputListener) - input.bind('paste', pasteListener) - input.bind('copy', copyListener) - })() + $input.bind('keydown', keydownListener) + $input.bind('keyup', keyupListener) + $input.bind('input', inputListener) + $input.bind('paste', pasteListener) + $input.bind('copy', copyListener) + } /** * TOUCH HANDLING @@ -516,15 +580,15 @@ module.exports = function DeviceScreenDirective( * For now, try to keep the whole section as a separate unit as much * as possible. */ - ;(function() { - var prevCoords = {} - var slots = [] - var slotted = Object.create(null) - var fingers = [] - var seq = -1 - var cycle = 100 - var fakePinch = false - var lastPossiblyBuggyMouseUpEvent = 0 + function handleTouch() { + let prevCoords = {} + const slots = [] + const slotted = Object.create(null) + const fingers = [] + let seq = -1 + const cycle = 100 + let fakePinch = false + let lastPossiblyBuggyMouseUpEvent = 0 function nextSeq() { return ++seq >= cycle ? (seq = 0) : seq @@ -534,8 +598,9 @@ module.exports = function DeviceScreenDirective( // The reverse order is important because slots and fingers are in // opposite sort order. Anyway don't change anything here unless // you understand what it does and why. - for (var i = 9; i >= 0; --i) { - var finger = createFinger(i) + for (let i = 9; i >= 0; --i) { + const finger = createFinger(i) + element.append(finger) slots.push(i) fingers.unshift(finger) @@ -543,11 +608,12 @@ module.exports = function DeviceScreenDirective( } function activateFinger(index, x, y, pressure) { - var scale = 0.5 + pressure + const scale = 0.5 + pressure + const cssTranslate = `translate3d(${x}px,${y}px,0)` + const cssScale = `scale(${scale},${scale})` + fingers[index].classList.add('active') - fingers[index].style[cssTransform] = - 'translate3d(' + x + 'px,' + y + 'px,0) ' + - 'scale(' + scale + ',' + scale + ')' + fingers[index].style[cssTransform] = `${cssTranslate} ${cssScale})` } function deactivateFinger(index) { @@ -555,19 +621,21 @@ module.exports = function DeviceScreenDirective( } function deactivateFingers() { - for (var i = 0, l = fingers.length; i < l; ++i) { + for (let i = 0, l = fingers.length; i < l; ++i) { fingers[i].classList.remove('active') } } function createFinger(index) { - var el = document.createElement('span') - el.className = 'finger finger-' + index + const el = document.createElement('span') + + el.className = `finger finger-${index}` + return el } function calculateBounds() { - var el = element[0] + let el = element screen.bounds.w = el.offsetWidth screen.bounds.h = el.offsetHeight @@ -582,7 +650,8 @@ module.exports = function DeviceScreenDirective( } function mouseDownListener(event) { - var e = event + let e = event + if (e.originalEvent) { e = e.originalEvent } @@ -590,7 +659,6 @@ module.exports = function DeviceScreenDirective( if (e.which === 3) { return } - e.preventDefault() fakePinch = e.altKey @@ -598,65 +666,65 @@ module.exports = function DeviceScreenDirective( calculateBounds() startMousing() - var x = e.pageX - screen.bounds.x - var y = e.pageY - screen.bounds.y - var pressure = 0.5 - var scaled = scaler.coords( - screen.bounds.w - , screen.bounds.h - , x - , y - , screen.rotation - , device.ios - ) + const x = e.pageX - screen.bounds.x + const y = e.pageY - screen.bounds.y + const pressure = 0.5 + const scaled = scaler.coords( + screen.bounds.w, + screen.bounds.h, + x, + y, + screen.rotation, + $scope.device.ios, + ) + prevCoords = { x: scaled.xP, - par: 0, y: scaled.yP, - presure: pressure, - seq: nextSeq(), } - if ( device.ios && device.ios === true ) { - control.touchDownIos(nextSeq(), 0, scaled.xP, scaled.yP, pressure) + + // TODO: can be non boolean? + if ($scope.device.ios && $scope.device.ios === true) { + $scope.control.touchDownIos(nextSeq(), 0, scaled.xP, scaled.yP, pressure) if (fakePinch) { - control.touchDownIos(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, - pressure) + $scope.control.touchDownIos(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) } } else { - control.touchDown(nextSeq(), 0, scaled.xP, scaled.yP, pressure) + $scope.control.touchDown(nextSeq(), 0, scaled.xP, scaled.yP, pressure) if (fakePinch) { - control.touchDown(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, - pressure) + $scope.control.touchDown(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) } } - - control.touchCommit(nextSeq()) - + $scope.control.touchCommit(nextSeq()) activateFinger(0, x, y, pressure) if (fakePinch) { - activateFinger(1, -e.pageX + screen.bounds.x + screen.bounds.w, - -e.pageY + screen.bounds.y + screen.bounds.h, pressure) + activateFinger( + 1, + -e.pageX + screen.bounds.x + screen.bounds.w, + -e.pageY + screen.bounds.y + screen.bounds.h, + pressure + ) } - element.bind('mousemove', mouseMoveListener) + $element.bind('mousemove', mouseMoveListener) $document.bind('mouseup', mouseUpListener) $document.bind('mouseleave', mouseUpListener) - if (lastPossiblyBuggyMouseUpEvent && - lastPossiblyBuggyMouseUpEvent.timeStamp > e.timeStamp) { + if (lastPossiblyBuggyMouseUpEvent + && lastPossiblyBuggyMouseUpEvent.timeStamp > e.timeStamp) { // We got mouseup before mousedown. See mouseUpBugWorkaroundListener // for details. mouseUpListener(lastPossiblyBuggyMouseUpEvent) - } - else { + } else { lastPossiblyBuggyMouseUpEvent = null } } function mouseMoveListener(event) { - var e = event + let e = event + if (e.originalEvent) { e = e.originalEvent } @@ -667,57 +735,58 @@ module.exports = function DeviceScreenDirective( } e.preventDefault() - var addGhostFinger = !fakePinch && e.altKey - var deleteGhostFinger = fakePinch && !e.altKey + const addGhostFinger = !fakePinch && e.altKey + const deleteGhostFinger = fakePinch && !e.altKey fakePinch = e.altKey - var x = e.pageX - screen.bounds.x - var y = e.pageY - screen.bounds.y - var pressure = 0.5 - var scaled = scaler.coords( - screen.bounds.w - , screen.bounds.h - , x - , y - , screen.rotation - , device.ios - ) - - control.touchMove(nextSeq(), 0, scaled.xP, scaled.yP, pressure) - //control.touchMoveIos(nextSeq(), 0, scaled.xP, scaled.yP, pressure) + const x = e.pageX - screen.bounds.x + const y = e.pageY - screen.bounds.y + const pressure = 0.5 + const scaled = scaler.coords( + screen.bounds.w, + screen.bounds.h, + x, + y, + screen.rotation, + $scope.device.ios, + ) + + $scope.control.touchMove(nextSeq(), 0, scaled.xP, scaled.yP, pressure) + //$scope.control.touchMoveIos(nextSeq(), 0, scaled.xP, scaled.yP, pressure) if (addGhostFinger) { - if ( device.ios && device.ios === true) { - control.touchDownIos(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) + // TODO: can be non boolean? + if ($scope.device.ios && $scope.device.ios === true) { + $scope.control.touchDownIos(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) } else { - control.touchDown(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) + $scope.control.touchDown(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) } - - } - else if (deleteGhostFinger) { - control.touchUp(nextSeq(), 1) - } - else if (fakePinch) { - control.touchMove(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) - //control.touchMoveIos(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) + } else if (deleteGhostFinger) { + $scope.control.touchUp(nextSeq(), 1) + } else if (fakePinch) { + $scope.control.touchMove(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) + //$scope.control.touchMoveIos(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure) } - control.touchCommit(nextSeq()) - + $scope.control.touchCommit(nextSeq()) activateFinger(0, x, y, pressure) if (deleteGhostFinger) { deactivateFinger(1) - } - else if (fakePinch) { - activateFinger(1, -e.pageX + screen.bounds.x + screen.bounds.w, - -e.pageY + screen.bounds.y + screen.bounds.h, pressure) + } else if (fakePinch) { + activateFinger( + 1, + -e.pageX + screen.bounds.x + screen.bounds.w, + -e.pageY + screen.bounds.y + screen.bounds.h, + pressure + ) } } function mouseUpListener(event) { - var e = event + let e = event + if (e.originalEvent) { e = e.originalEvent } @@ -727,30 +796,40 @@ module.exports = function DeviceScreenDirective( return } e.preventDefault() - var x = e.pageX - screen.bounds.x - var y = e.pageY - screen.bounds.y - var pressure = 0.5 - var scaled = scaler.coords( - screen.bounds.w - , screen.bounds.h - , x - , y - , screen.rotation - , device.ios + + const x = e.pageX - screen.bounds.x + const y = e.pageY - screen.bounds.y + const pressure = 0.5 + const scaled = scaler.coords( + screen.bounds.w, + screen.bounds.h, + x, + y, + screen.rotation, + $scope.device.ios, ) - if ((Math.abs(prevCoords.x - scaled.xP) >= 0.1 || Math.abs(prevCoords.y - scaled.yP) >= 0.1) && device.ios && device.ios === true) { - control.touchMoveIos(scaled.xP, scaled.yP, prevCoords.x, prevCoords.y, pressure, nextSeq(), 0) + if ((Math.abs(prevCoords.x - scaled.xP) >= 0.1 + || Math.abs(prevCoords.y - scaled.yP) >= 0.1) + && $scope.device.ios && $scope.device.ios === true) { // TODO: can be non boolean? + $scope.control.touchMoveIos( + scaled.xP, + scaled.yP, + prevCoords.x, + prevCoords.y, + pressure, + nextSeq(), + 0 + ) } - control.touchUp(nextSeq(), 0) + $scope.control.touchUp(nextSeq(), 0) if (fakePinch) { - control.touchUp(nextSeq(), 1) + $scope.control.touchUp(nextSeq(), 1) } - control.touchCommit(nextSeq()) - + $scope.control.touchCommit(nextSeq()) deactivateFinger(0) if (fakePinch) { @@ -816,20 +895,20 @@ module.exports = function DeviceScreenDirective( } function startMousing() { - control.gestureStart(nextSeq()) - input[0].focus() + $scope.control.gestureStart(nextSeq()) + input.focus() } function stopMousing() { - element.unbind('mousemove', mouseMoveListener) + $element.unbind('mousemove', mouseMoveListener) $document.unbind('mouseup', mouseUpListener) $document.unbind('mouseleave', mouseUpListener) deactivateFingers() - control.gestureStop(nextSeq()) + $scope.control.gestureStop(nextSeq()) } function touchStartListener(event) { - var e = event + let e = event e.preventDefault() //Make it jQuery compatible also @@ -843,10 +922,9 @@ module.exports = function DeviceScreenDirective( startTouching() } - var currentTouches = Object.create(null) - var i, l + const currentTouches = Object.create(null) - for (i = 0, l = e.touches.length; i < l; ++i) { + for (let i = 0, l = e.touches.length; i < l; ++i) { currentTouches[e.touches[i].identifier] = 1 } @@ -858,12 +936,13 @@ module.exports = function DeviceScreenDirective( // (literally) such as dragging from the bottom of the screen so that // the control center appears. If so, let's ask for a reset. if (Object.keys(slotted).some(maybeLostTouchEnd)) { - Object.keys(slotted).forEach(function(id) { - slots.push(slotted[id]) - delete slotted[id] - }) + Object.keys(slotted) + .forEach((id) => { + slots.push(slotted[id]) + delete slotted[id] + }) slots.sort().reverse() - control.touchReset(nextSeq()) + $scope.control.touchReset(nextSeq()) deactivateFingers() } @@ -872,79 +951,81 @@ module.exports = function DeviceScreenDirective( throw new Error('Ran out of multitouch slots') } - for (i = 0, l = e.changedTouches.length; i < l; ++i) { - var touch = e.changedTouches[i] - var slot = slots.pop() - var x = touch.pageX - screen.bounds.x - var y = touch.pageY - screen.bounds.y - var pressure = touch.force || 0.5 - var scaled = scaler.coords( - screen.bounds.w - , screen.bounds.h - , x - , y - , screen.rotation - , device.ios - ) + for (let i = 0, l = e.changedTouches.length; i < l; ++i) { + const touch = e.changedTouches[i] + const slot = slots.pop() + const x = touch.pageX - screen.bounds.x + const y = touch.pageY - screen.bounds.y + const pressure = touch.force || 0.5 + const scaled = scaler.coords( + screen.bounds.w, + screen.bounds.h, + x, + y, + screen.rotation, + $scope.device.ios, + ) slotted[touch.identifier] = slot - if ( device.ios && device.ios === true) { - control.touchDownIos(nextSeq(), slot, scaled.xP, scaled.yP, pressure) + if ($scope.device.ios && $scope.device.ios === true) { // TODO: can be non boolean? + $scope.control.touchDownIos(nextSeq(), slot, scaled.xP, scaled.yP, pressure) } else { - control.touchDown(nextSeq(), slot, scaled.xP, scaled.yP, pressure) + $scope.control.touchDown(nextSeq(), slot, scaled.xP, scaled.yP, pressure) } activateFinger(slot, x, y, pressure) } - element.bind('touchmove', touchMoveListener) + $element.bind('touchmove', touchMoveListener) $document.bind('touchend', touchEndListener) $document.bind('touchleave', touchEndListener) - control.touchCommit(nextSeq()) + $scope.control.touchCommit(nextSeq()) } function touchMoveListener(event) { - var e = event + let e = event e.preventDefault() if (e.originalEvent) { e = e.originalEvent } - for (var i = 0, l = e.changedTouches.length; i < l; ++i) { - var touch = e.changedTouches[i] - var slot = slotted[touch.identifier] - var x = touch.pageX - screen.bounds.x - var y = touch.pageY - screen.bounds.y - var pressure = touch.force || 0.5 - var scaled = scaler.coords( - screen.bounds.w - , screen.bounds.h - , x - , y - , screen.rotation - , device.ios - ) - - control.touchMove(nextSeq(), slot, scaled.xP, scaled.yP, pressure) - //control.touchMoveIos(nextSeq(), slot, scaled.xP, scaled.yP, pressure) + for (let i = 0, l = e.changedTouches.length; i < l; ++i) { + const touch = e.changedTouches[i] + const slot = slotted[touch.identifier] + const x = touch.pageX - screen.bounds.x + const y = touch.pageY - screen.bounds.y + const pressure = touch.force || 0.5 + const scaled = scaler.coords( + screen.bounds.w, + screen.bounds.h, + x, + y, + screen.rotation, + $scope.device.ios + ) + + $scope.control.touchMove(nextSeq(), slot, scaled.xP, scaled.yP, pressure) + //$scope.control.touchMoveIos(nextSeq(), slot, scaled.xP, scaled.yP, pressure) activateFinger(slot, x, y, pressure) } - control.touchCommit(nextSeq()) + $scope.control.touchCommit(nextSeq()) } function touchEndListener(event) { - var e = event + let e = event + if (e.originalEvent) { e = e.originalEvent } - var foundAny = false + let foundAny = false + + for (let i = 0, l = e.changedTouches.length; i < l; ++i) { + const touch = e.changedTouches[i] + const slot = slotted[touch.identifier] - for (var i = 0, l = e.changedTouches.length; i < l; ++i) { - var touch = e.changedTouches[i] - var slot = slotted[touch.identifier] if (typeof slot === 'undefined') { // We've already disposed of the contact. We may have gotten a // touchend event for the same contact twice. @@ -952,13 +1033,13 @@ module.exports = function DeviceScreenDirective( } delete slotted[touch.identifier] slots.push(slot) - control.touchUp(nextSeq(), slot) + $scope.control.touchUp(nextSeq(), slot) deactivateFinger(slot) foundAny = true } if (foundAny) { - control.touchCommit(nextSeq()) + $scope.control.touchCommit(nextSeq()) if (!e.touches.length) { stopTouching() } @@ -966,23 +1047,23 @@ module.exports = function DeviceScreenDirective( } function startTouching() { - control.gestureStart(nextSeq()) + $scope.control.gestureStart(nextSeq()) } function stopTouching() { - element.unbind('touchmove', touchMoveListener) + $element.unbind('touchmove', touchMoveListener) $document.unbind('touchend', touchEndListener) $document.unbind('touchleave', touchEndListener) deactivateFingers() - control.gestureStop(nextSeq()) + $scope.control.gestureStop(nextSeq()) } - element.on('touchstart', touchStartListener) - element.on('mousedown', mouseDownListener) - element.on('mouseup', mouseUpBugWorkaroundListener) + $element.on('touchstart', touchStartListener) + $element.on('mousedown', mouseDownListener) + $element.on('mouseup', mouseUpBugWorkaroundListener) createSlots() - })() + } } } } diff --git a/res/app/components/stf/screen/screen-loader/index.js b/res/app/components/stf/screen/screen-loader/index.js index ac482bc9b..0000a803a 100644 --- a/res/app/components/stf/screen/screen-loader/index.js +++ b/res/app/components/stf/screen/screen-loader/index.js @@ -2,3 +2,4 @@ require('./screen-loader.css') module.exports = angular.module('stf/sreen-loader', []) .directive('screenLoader', require('./screen-loader-directive')) + .factory('ScreenLoaderService', require('./screen-loader-service')) diff --git a/res/app/components/stf/screen/screen-loader/screen-loader-directive.js b/res/app/components/stf/screen/screen-loader/screen-loader-directive.js index 780fe4d61..d7faecf39 100644 --- a/res/app/components/stf/screen/screen-loader/screen-loader-directive.js +++ b/res/app/components/stf/screen/screen-loader/screen-loader-directive.js @@ -2,15 +2,11 @@ module.exports = function screenLoaderDirective() { return { restrict: 'E', template: require('./screen-loader.pug'), - link: function(scope, element) { - const hideScreenLoaderListener = scope.$on('hide-screen-loader', function() { - hideScreenLoaderListener() - element.remove() - }) - - scope.$on('destroy', function() { - hideScreenLoaderListener() - }) - } + controller: ($scope, ScreenLoaderService) => { + return { + get isVisible() { return ScreenLoaderService.isVisible }, + } + }, + controllerAs: '$ctrl', } } diff --git a/res/app/components/stf/screen/screen-loader/screen-loader-service.js b/res/app/components/stf/screen/screen-loader/screen-loader-service.js new file mode 100755 index 000000000..6a5062d80 --- /dev/null +++ b/res/app/components/stf/screen/screen-loader/screen-loader-service.js @@ -0,0 +1,37 @@ +module.exports = function ScreenLoaderServiceFactory( + $rootScope, +) { + let isVisible = true + + return { + get isVisible() { return isVisible }, + hide, + show, + } + + // in most cases this will be called outside angular + function show() { + if(!$rootScope.$$phase) { + $rootScope.$apply(showLoader) + } else { + $rootScope.$applyAsync(showLoader) + } + } + + function showLoader() { + isVisible = true + } + + // in most cases this will be called outside angular + function hide() { + if(!$rootScope.$$phase) { + $rootScope.$apply(hideLoader) + } else { + $rootScope.$applyAsync(hideLoader) + } + } + + function hideLoader() { + isVisible = false + } +} diff --git a/res/app/components/stf/screen/screen-loader/screen-loader.css b/res/app/components/stf/screen/screen-loader/screen-loader.css index 44454613d..f99033fc5 100644 --- a/res/app/components/stf/screen/screen-loader/screen-loader.css +++ b/res/app/components/stf/screen/screen-loader/screen-loader.css @@ -1,4 +1,14 @@ .lds-roller { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 1; + backdrop-filter: blur(3px); + background-color: rgba(0,0,0,0.7); +} +.lds-roller__container { display: inline-block; position: absolute; width: 80px; @@ -8,11 +18,11 @@ top: 40%; left: 40%; } -.lds-roller div { +.lds-roller__item { animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; transform-origin: 40px 40px; } -.lds-roller div:after { +.lds-roller__item:after { content: " "; display: block; position: absolute; @@ -22,59 +32,59 @@ background: #fff; margin: -4px 0 0 -4px; } -.lds-roller div:nth-child(1) { +.lds-roller__item:nth-child(1) { animation-delay: -0.036s; } -.lds-roller div:nth-child(1):after { +.lds-roller__item:nth-child(1):after { top: 63px; left: 63px; } -.lds-roller div:nth-child(2) { +.lds-roller__item:nth-child(2) { animation-delay: -0.072s; } -.lds-roller div:nth-child(2):after { +.lds-roller__item:nth-child(2):after { top: 68px; left: 56px; } -.lds-roller div:nth-child(3) { +.lds-roller__item:nth-child(3) { animation-delay: -0.108s; } -.lds-roller div:nth-child(3):after { +.lds-roller__item:nth-child(3):after { top: 71px; left: 48px; } -.lds-roller div:nth-child(4) { +.lds-roller__item:nth-child(4) { animation-delay: -0.144s; } -.lds-roller div:nth-child(4):after { +.lds-roller__item:nth-child(4):after { top: 72px; left: 40px; } -.lds-roller div:nth-child(5) { +.lds-roller__item:nth-child(5) { animation-delay: -0.18s; } -.lds-roller div:nth-child(5):after { +.lds-roller__item:nth-child(5):after { top: 71px; left: 32px; } -.lds-roller div:nth-child(6) { +.lds-roller__item:nth-child(6) { animation-delay: -0.216s; } -.lds-roller div:nth-child(6):after { +.lds-roller__item:nth-child(6):after { top: 68px; left: 24px; } -.lds-roller div:nth-child(7) { +.lds-roller__item:nth-child(7) { animation-delay: -0.252s; } -.lds-roller div:nth-child(7):after { +.lds-roller__item:nth-child(7):after { top: 63px; left: 17px; } -.lds-roller div:nth-child(8) { +.lds-roller__item:nth-child(8) { animation-delay: -0.288s; } -.lds-roller div:nth-child(8):after { +.lds-roller__item:nth-child(8):after { top: 56px; left: 12px; } diff --git a/res/app/components/stf/screen/screen-loader/screen-loader.pug b/res/app/components/stf/screen/screen-loader/screen-loader.pug index f632e7ebc..d1dcebefc 100644 --- a/res/app/components/stf/screen/screen-loader/screen-loader.pug +++ b/res/app/components/stf/screen/screen-loader/screen-loader.pug @@ -1,9 +1,10 @@ -.lds-roller - div - div - div - div - div - div - div - div +.lds-roller(ng-hide='!$ctrl.isVisible') + .lds-roller__container + .lds-roller__item + .lds-roller__item + .lds-roller__item + .lds-roller__item + .lds-roller__item + .lds-roller__item + .lds-roller__item + .lds-roller__item diff --git a/res/app/components/stf/screen/screen.pug b/res/app/components/stf/screen/screen.pug index abb3bdde6..c088af7a0 100755 --- a/res/app/components/stf/screen/screen.pug +++ b/res/app/components/stf/screen/screen.pug @@ -1,7 +1,7 @@ div.positioner screen-loader - canvas.screen(ng-show='device') + canvas.screen.screen__canvas(ng-show='device') canvas.hacky-stretcher(width=1, height=1) div(ng-if='displayError').screen-error .screen-error-message @@ -15,4 +15,4 @@ div(ng-if='displayError').screen-error button(ng-click='retryLoadingScreen()', style='text-align: center;').btn.btn-primary.btn-block i.fa.fa-refresh span(translate) Retry -input(type='text', autoComplete='off', tabindex='40', accesskey='C', autocorrect='off', autocapitalize='off', focus-element='$root.screenFocus') +input.screen__input(type='text', autoComplete='off', tabindex='40', accesskey='C', autocorrect='off', autocapitalize='off', focus-element='$root.screenFocus') diff --git a/res/app/components/stf/socket/index.js b/res/app/components/stf/socket/index.js index 728b17919..723bfa7e5 100755 --- a/res/app/components/stf/socket/index.js +++ b/res/app/components/stf/socket/index.js @@ -2,6 +2,6 @@ module.exports = angular.module('stf.socket', [ //TODO: Refactor version update out to its own Ctrl require('stf/app-state').name, require('stf/common-ui/modals/version-update').name, - require('stf/common-ui/modals/temporarily-unavialable').name + require('stf/common-ui/modals/temporarily-unavailable').name ]) .factory('socket', require('./socket-service')) diff --git a/res/app/components/stf/socket/socket-service.js b/res/app/components/stf/socket/socket-service.js index 6fec3b359..01345c987 100755 --- a/res/app/components/stf/socket/socket-service.js +++ b/res/app/components/stf/socket/socket-service.js @@ -3,7 +3,7 @@ var io = require('socket.io') module.exports = function SocketFactory( $rootScope , VersionUpdateService -, TemporarilyUnavialableService +, TemporarilyUnavailableService , AppState ) { var websocketUrl = AppState.config.websocketUrl || '' @@ -43,7 +43,7 @@ module.exports = function SocketFactory( }) socket.once('temporarily-unavailable', function() { - TemporarilyUnavialableService.open('Service is currently unavailable! Try your attempt later') + TemporarilyUnavailableService.open('Service is currently unavailable! Try your attempt later') }) return socket diff --git a/res/app/components/stf/user/group/group-service.js b/res/app/components/stf/user/group/group-service.js index 5c2f8ff1e..20f6e91dd 100755 --- a/res/app/components/stf/user/group/group-service.js +++ b/res/app/components/stf/user/group/group-service.js @@ -9,23 +9,19 @@ module.exports = function GroupServiceFactory( } groupService.invite = function(device) { - if (!true) { - return Promise.reject(new Error('Device is not usable')) - } + const tx = TransactionService.create(device) - var tx = TransactionService.create(device) socket.emit('group.invite', device.channel, tx.channel, { requirements: { serial: { - value: device.serial - , match: 'exact' - } - } + value: device.serial, + match: 'exact', + }, + }, }) + return tx.promise - .then(function(result) { - return result.device - }) + .then(({device}) => device) .catch(TransactionError, function() { throw new Error('Device refused to join the group') }) diff --git a/res/app/control-panes/control-panes-controller.js b/res/app/control-panes/control-panes-controller.js index ca630815d..6ca5f80ad 100755 --- a/res/app/control-panes/control-panes-controller.js +++ b/res/app/control-panes/control-panes-controller.js @@ -5,9 +5,10 @@ module.exports = function ControlPanesController($scope, $http, gettext, $routeParams, $timeout, $location, DeviceService, GroupService, ControlService, - StorageService, FatalMessageService, SettingsService) { + StorageService, FatalMessageService, SettingsService, $route) { - var sharedTabs = [ + let openedModalInstance + const sharedTabs = [ { title: gettext('Screenshots'), icon: 'fa-camera color-skyblue', @@ -82,16 +83,25 @@ module.exports = getDevice($routeParams.serial) - - - $scope.$watch('device.state', function(newValue, oldValue) { + $scope.$watch('device.state', (newValue, oldValue) => { if (newValue !== oldValue) { -/*************** fix bug: it seems automation state was forgotten ? *************/ + // show error message with device status, cause device was disconnected by some reason if (oldValue === 'using' || oldValue === 'automation') { -/******************************************************************************/ - FatalMessageService.open($scope.device, false) + openedModalInstance = FatalMessageService.open($scope.device, false) + // close error modal if device was reconnected + } else if ((newValue === 'using' || newValue === 'automation') && openedModalInstance) { + openedModalInstance.dismiss(true) + openedModalInstance = null + // reconnect to the available device + // TODO: refactor: use API call instead or state reloading (for now it is only working way) + } else if (newValue === 'available' && openedModalInstance) { + $timeout(()=> { + if (openedModalInstance) { + openedModalInstance.dismiss(true) + } + $route.reload() + }, 0) } } }, true) - } diff --git a/res/app/control-panes/logs/logs-controller.js b/res/app/control-panes/logs/logs-controller.js index b5ccd4a2a..e7890a584 100755 --- a/res/app/control-panes/logs/logs-controller.js +++ b/res/app/control-panes/logs/logs-controller.js @@ -38,25 +38,30 @@ module.exports = function LogsCtrl($scope, $rootScope, $routeParams, LogcatServi } function setFiltersPriority() { - if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { - $scope.filters.priority = $scope.filters.levelNumbers[ - LogcatService.deviceEntries[deviceSerial].selectedLogLevel - 2] - } else { - if ($scope.started) { - $scope.filters.priority = $scope.filters.levelNumbers[0] - } + const {levelNumbers} = $scope.filters + const {deviceEntries} = LogcatService + + if (!levelNumbers) { return } + + if (Object.keys(deviceEntries).includes(deviceSerial)) { + $scope.filters.priority = levelNumbers[deviceEntries[deviceSerial].selectedLogLevel - 2] + } else if ($scope.started) { + $scope.filters.priority = levelNumbers[0] } } function restoreFilters() { - if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { - Object.keys(LogcatService.deviceEntries[deviceSerial].filters).forEach(function(entry) { - if ('filter.' + entry !== 'filter.priority') { - $scope.filters[entry] = LogcatService.deviceEntries[deviceSerial].filters[entry] - } else { - setFiltersPriority() - } - }) + const {deviceEntries} = LogcatService + + if (Object.keys(deviceEntries).includes(deviceSerial)) { + Object.keys(deviceEntries[deviceSerial].filters) + .forEach((entry) => { + if (`filter.${entry}` !== 'filter.priority') { + $scope.filters[entry] = deviceEntries[deviceSerial].filters[entry] + } else { + setFiltersPriority() + } + }) } }