diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 63411b4e6..ed853fdca 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -33,6 +33,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Required services var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); + var guacClientManager = $injector.get('guacClientManager'); /** * The minimum number of pixels a drag gesture must move to result in the @@ -149,7 +150,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams name : "CLIENT.ACTION_RECONNECT", // Handle reconnect action callback : function reconnectCallback() { - $scope.id = uniqueId; + $scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params); $scope.showStatus(false); } }; @@ -164,12 +165,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams remaining: 15 }; - // Client settings and state - $scope.clientProperties = new ClientProperties(); - - // Initialize clipboard data to an empty string - $scope.clipboardData = ""; - // Hide menu by default $scope.menuShown = false; @@ -198,8 +193,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * as well as any extra parameters if set. */ var uniqueId = $routeParams.type + '/' + $routeParams.id; - $scope.id = uniqueId; - $scope.connectionParameters = $routeParams.params || ''; + $scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params); // Pull connection name from server switch ($routeParams.type) { @@ -266,9 +260,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams } // Scroll display if absolute mouse is in use - else if ($scope.clientProperties.emulateAbsoluteMouse) { - $scope.clientProperties.scrollLeft -= deltaX; - $scope.clientProperties.scrollTop -= deltaY; + else if ($scope.client.clientProperties.emulateAbsoluteMouse) { + $scope.client.clientProperties.scrollLeft -= deltaX; + $scope.client.clientProperties.scrollTop -= deltaY; } return false; @@ -305,7 +299,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) { // Do not handle pinch gestures while relative mouse is in use - if (!$scope.clientProperties.emulateAbsoluteMouse) + if (!$scope.client.clientProperties.emulateAbsoluteMouse) return false; // Stop gesture if not in progress @@ -316,26 +310,26 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Set initial scale if gesture has just started if (!initialScale) { - initialScale = $scope.clientProperties.scale; - initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale; - initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale; + initialScale = $scope.client.clientProperties.scale; + initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale; + initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale; } // Determine new scale absolutely var currentScale = initialScale * currentLength / startLength; // Fix scale within limits - scroll will be miscalculated otherwise - currentScale = Math.max(currentScale, $scope.clientProperties.minScale); - currentScale = Math.min(currentScale, $scope.clientProperties.maxScale); + currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale); + currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale); // Update scale based on pinch distance $scope.autoFit = false; - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale = currentScale; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale = currentScale; // Scroll display to keep original pinch location centered within current pinch - $scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; - $scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY; + $scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; + $scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY; return false; @@ -357,7 +351,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$broadcast('guacClipboard', 'text/plain', $scope.clipboardData); // Disable client keyboard if the menu is shown - $scope.clientProperties.keyboardEnabled = !menuShown; + $scope.client.clientProperties.keyboardEnabled = !menuShown; }); @@ -385,7 +379,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams keyboard.reset(); // Toggle the menu - $scope.safeApply(function() { + $scope.$apply(function() { $scope.menuShown = !$scope.menuShown; }); } @@ -478,33 +472,33 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.formattedScale = function formattedScale() { - return Math.round($scope.clientProperties.scale * 100); + return Math.round($scope.client.clientProperties.scale * 100); }; $scope.zoomIn = function zoomIn() { $scope.autoFit = false; - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale += 0.1; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale += 0.1; }; $scope.zoomOut = function zoomOut() { - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale -= 0.1; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale -= 0.1; }; $scope.autoFit = true; $scope.changeAutoFit = function changeAutoFit() { - if ($scope.autoFit && $scope.clientProperties.minScale) { - $scope.clientProperties.autoFit = true; + if ($scope.autoFit && $scope.client.clientProperties.minScale) { + $scope.client.clientProperties.autoFit = true; } else { - $scope.clientProperties.autoFit = false; - $scope.clientProperties.scale = 1; + $scope.client.clientProperties.autoFit = false; + $scope.client.clientProperties.scale = 1; } }; $scope.autoFitDisabled = function() { - return $scope.clientProperties.minZoom >= 1; + return $scope.client.clientProperties.minZoom >= 1; }; /** @@ -568,7 +562,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var downloadNotificationIDs = {}; $scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = { className : 'download', @@ -583,7 +577,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = downloadNotifications[streamIndex]; if (notification) @@ -593,7 +587,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = downloadNotifications[streamIndex]; var notificationID = downloadNotificationIDs[streamIndex]; @@ -629,7 +623,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var uploadNotificationIDs = {}; $scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = { className : 'upload', @@ -644,7 +638,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = uploadNotifications[streamIndex]; if (notification) @@ -654,7 +648,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = uploadNotifications[streamIndex]; var notificationID = uploadNotificationIDs[streamIndex]; @@ -683,7 +677,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); $scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) { - $scope.safeApply(function() { + $scope.$apply(function() { var notification = uploadNotifications[streamIndex]; var notificationID = uploadNotificationIDs[streamIndex]; diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 1ef3bcdec..8cc3db40d 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -32,46 +32,19 @@ angular.module('client').directive('guacClient', [function guacClient() { scope: { /** - * Parameters for controlling client state. + * The client to display within this guacClient directive. * - * @type ClientProperties|Object + * @type ManagedClient */ - clientProperties : '=', + client : '=' - /** - * The ID of the Guacamole connection to connect to. - * - * @type String - */ - id : '=', - - /** - * Arbitrary URL-encoded parameters to append to the connection - * string when connecting. - * - * @type String - */ - connectionParameters : '=' - }, templateUrl: 'app/client/templates/guacClient.html', controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) { - - /* - * Safe $apply implementation from Alex Vanston: - * https://coderwall.com/p/ngisma - */ - $scope.safeApply = function(fn) { - var phase = this.$root.$$phase; - if(phase === '$apply' || phase === '$digest') { - if(fn && (typeof(fn) === 'function')) { - fn(); - } - } else { - this.$apply(fn); - } - }; - + + // Required services + var $window = $injector.get('$window'); + /** * Whether the local, hardware mouse cursor is in use. * @@ -146,14 +119,6 @@ angular.module('client').directive('guacClient', [function guacClient() { */ var touchPad = new Guacamole.Mouse.Touchpad(displayContainer); - var $window = $injector.get('$window'), - guacAudio = $injector.get('guacAudio'), - guacVideo = $injector.get('guacVideo'), - guacHistory = $injector.get('guacHistory'), - guacTunnelFactory = $injector.get('guacTunnelFactory'), - guacClientFactory = $injector.get('guacClientFactory'), - authenticationService = $injector.get('authenticationService'); - /** * Updates the scale of the attached Guacamole.Client based on current window * size and "auto-fit" setting. @@ -163,60 +128,20 @@ angular.module('client').directive('guacClient', [function guacClient() { if (!display) return; // Calculate scale to fit screen - $scope.clientProperties.minScale = Math.min( + $scope.client.clientProperties.minScale = Math.min( main.offsetWidth / Math.max(display.getWidth(), 1), main.offsetHeight / Math.max(display.getHeight(), 1) ); // Calculate appropriate maximum zoom level - $scope.clientProperties.maxScale = Math.max($scope.clientProperties.minScale, 3); + $scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3); // Clamp zoom level, maintain auto-fit - if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit) - $scope.clientProperties.scale = $scope.clientProperties.minScale; + if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit) + $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; - else if (display.getScale() > $scope.clientProperties.maxScale) - $scope.clientProperties.scale = $scope.clientProperties.maxScale; - - }; - - /** - * Returns the string of connection parameters to be passed to the - * Guacamole client during connection. This string generally - * contains the desired connection ID, display resolution, and - * supported audio/video codecs. - * - * @returns {String} The string of connection parameters to be - * passed to the Guacamole client. - */ - var getConnectString = function getConnectString() { - - // Calculate optimal width/height for display - var pixel_density = $window.devicePixelRatio || 1; - var optimal_dpi = pixel_density * 96; - var optimal_width = $window.innerWidth * pixel_density; - var optimal_height = $window.innerHeight * pixel_density; - - // Build base connect string - var connectString = - "id=" + encodeURIComponent($scope.id) - + "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) - + "&width=" + Math.floor(optimal_width) - + "&height=" + Math.floor(optimal_height) - + "&dpi=" + Math.floor(optimal_dpi) - + ($scope.connectionParameters ? '&' + $scope.connectionParameters : ''); - - // Add audio mimetypes to connect_string - guacAudio.supported.forEach(function(mimetype) { - connectString += "&audio=" + encodeURIComponent(mimetype); - }); - - // Add video mimetypes to connect_string - guacVideo.supported.forEach(function(mimetype) { - connectString += "&video=" + encodeURIComponent(mimetype); - }); - - return connectString; + else if (display.getScale() > $scope.client.clientProperties.maxScale) + $scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale; }; @@ -322,82 +247,36 @@ angular.module('client').directive('guacClient', [function guacClient() { * SCROLLING */ - $scope.$watch('clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { + $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { main.scrollLeft = scrollLeft; - $scope.clientProperties.scrollLeft = main.scrollLeft; + $scope.client.clientProperties.scrollLeft = main.scrollLeft; }); - $scope.$watch('clientProperties.scrollTop', function scrollTopChanged(scrollTop) { + $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) { main.scrollTop = scrollTop; - $scope.clientProperties.scrollTop = main.scrollTop; + $scope.client.clientProperties.scrollTop = main.scrollTop; }); - /* - * CONNECT / RECONNECT - */ + // Attach any given managed client + $scope.$watch('client', function(managedClient) { - /** - * Store the thumbnail of the currently connected client within - * the connection history under the given ID. If the client is not - * connected, or if no ID is given, this function has no effect. - * - * @param {String} id - * The ID of the history entry to update. - */ - var updateHistoryEntry = function updateHistoryEntry(id) { + // Remove any existing display + displayContainer.innerHTML = ""; - // Update stored thumbnail of previous connection - if (id && display && display.getWidth() > 0 && display.getHeight() > 0) { - - // Get screenshot - var canvas = display.flatten(); - - // Calculate scale of thumbnail (max 320x240, max zoom 100%) - var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); - - // Create thumbnail canvas - var thumbnail = document.createElement("canvas"); - thumbnail.width = canvas.width*scale; - thumbnail.height = canvas.height*scale; - - // Scale screenshot to thumbnail - var context = thumbnail.getContext("2d"); - context.drawImage(canvas, - 0, 0, canvas.width, canvas.height, - 0, 0, thumbnail.width, thumbnail.height - ); - - guacHistory.updateThumbnail(id, thumbnail.toDataURL("image/png")); - - } - - }; - - // Connect to given ID whenever ID changes - $scope.$watch('id', function(id, previousID) { - - // If a client is already attached, ensure it is disconnected - if (client) - client.disconnect(); - - // Update stored thumbnail of previous connection - updateHistoryEntry(previousID); - - // Only proceed if a new client is attached - if (!id) + // Only proceed if a client is given + if (!managedClient) return; - // Get new client instance - var tunnel = guacTunnelFactory.getInstance($scope); - client = guacClientFactory.getInstance($scope, tunnel); + // Get Guacamole client instance + client = managedClient.client; - // Init display + // Attach possibly new display display = client.getDisplay(); - display.scale($scope.clientProperties.scale); + display.scale($scope.client.clientProperties.scale); // Update the scale of the display when the client display size changes. display.onresize = function() { - $scope.safeApply(updateDisplayScale); + $scope.$apply(updateDisplayScale); }; // Use local cursor if possible, update localCursor flag @@ -407,7 +286,6 @@ angular.module('client').directive('guacClient', [function guacClient() { // Add display element displayElement = display.getElement(); - displayContainer.innerHTML = ""; displayContainer.appendChild(displayElement); // Do nothing when the display element is clicked on. @@ -416,17 +294,6 @@ angular.module('client').directive('guacClient', [function guacClient() { return false; }; - // Connect - client.connect(getConnectString()); - - }); - - // Clean up when client directive is destroyed - $scope.$on('$destroy', function destroyClient() { - - // Update stored thumbnail of current connection - updateHistoryEntry($scope.id); - }); /* @@ -434,7 +301,7 @@ angular.module('client').directive('guacClient', [function guacClient() { */ // Watch for changes to mouse emulation mode - $scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { + $scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { if (!client || !display) return; @@ -483,14 +350,14 @@ angular.module('client').directive('guacClient', [function guacClient() { */ // Adjust scale if modified externally - $scope.$watch('clientProperties.scale', function changeScale(scale) { + $scope.$watch('client.clientProperties.scale', function changeScale(scale) { // Fix scale within limits - scale = Math.max(scale, $scope.clientProperties.minScale); - scale = Math.min(scale, $scope.clientProperties.maxScale); + scale = Math.max(scale, $scope.client.clientProperties.minScale); + scale = Math.min(scale, $scope.client.clientProperties.maxScale); // If at minimum zoom level, hide scroll bars - if (scale === $scope.clientProperties.minScale) + if (scale === $scope.client.clientProperties.minScale) main.style.overflow = "hidden"; // If not at minimum zoom level, show scroll bars @@ -501,15 +368,15 @@ angular.module('client').directive('guacClient', [function guacClient() { if (display) display.scale(scale); - if (scale !== $scope.clientProperties.scale) - $scope.clientProperties.scale = scale; + if (scale !== $scope.client.clientProperties.scale) + $scope.client.clientProperties.scale = scale; }); // If autofit is set, the scale should be set to the minimum scale, filling the screen - $scope.$watch('clientProperties.autoFit', function changeAutoFit(autoFit) { + $scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) { if(autoFit) - $scope.clientProperties.scale = $scope.clientProperties.minScale; + $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; }); // If the element is resized, attempt to resize client @@ -527,7 +394,7 @@ angular.module('client').directive('guacClient', [function guacClient() { } - $scope.safeApply(updateDisplayScale); + $scope.$apply(updateDisplayScale); }); @@ -537,7 +404,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Listen for broadcasted keydown events and fire the appropriate listeners $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(1, keysym); event.preventDefault(); } @@ -545,7 +412,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Listen for broadcasted keyup events and fire the appropriate listeners $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(0, keysym); event.preventDefault(); } diff --git a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js b/guacamole/src/main/webapp/app/client/services/guacClientFactory.js deleted file mode 100644 index 692e538ce..000000000 --- a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2014 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * A service for creating Guacamole clients. - */ -angular.module('client').factory('guacClientFactory', ['$rootScope', - function guacClientFactory($rootScope) { - - var service = {}; - - /** - * Returns a new Guacamole client instance which connects using the - * provided tunnel. - * - * @param {Scope} $scope The current scope. - * @param {Guacamole.Tunnel} tunnel The tunnel to connect through. - * @returns {Guacamole.Client} A new Guacamole client instance. - */ - service.getInstance = function getClientInstance($scope, tunnel) { - - // Instantiate client - var guacClient = new Guacamole.Client(tunnel); - - /* - * Fire guacClientStateChange events when client state changes. - */ - guacClient.onstatechange = function onClientStateChange(clientState) { - $scope.safeApply(function() { - - switch (clientState) { - - // Idle - case 0: - $scope.$emit('guacClientStateChange', guacClient, "idle"); - break; - - // Connecting - case 1: - $scope.$emit('guacClientStateChange', guacClient, "connecting"); - break; - - // Connected + waiting - case 2: - $scope.$emit('guacClientStateChange', guacClient, "waiting"); - break; - - // Connected - case 3: - $scope.$emit('guacClientStateChange', guacClient, "connected"); - break; - - // Disconnecting / disconnected are handled by tunnel instead - case 4: - case 5: - break; - - } - - }); - }; - - /* - * Fire guacClientName events when a new name is received. - */ - guacClient.onname = function onClientName(name) { - $scope.safeApply(function() { - $scope.$emit('guacClientName', guacClient, name); - }); - }; - - /* - * Disconnect and fire guacClientError when the client receives an - * error. - */ - guacClient.onerror = function onClientError(status) { - $scope.safeApply(function() { - - // Disconnect, if connected - guacClient.disconnect(); - - $scope.$emit('guacClientError', guacClient, status.code); - - }); - }; - - /* - * Fire guacClientClipboard events after new clipboard data is received. - */ - guacClient.onclipboard = function onClientClipboard(stream, mimetype) { - $scope.safeApply(function() { - - // Only text/plain is supported for now - if (mimetype !== "text/plain") { - stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); - return; - } - - var reader = new Guacamole.StringReader(stream); - var data = ""; - - // Append any received data to buffer - reader.ontext = function clipboard_text_received(text) { - data += text; - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // Emit event when done - reader.onend = function clipboard_text_end() { - $scope.$emit('guacClientClipboard', guacClient, mimetype, data); - }; - - }); - }; - - /* - * Fire guacFileStart, guacFileProgress, and guacFileEnd events during - * the receipt of files. - */ - guacClient.onfile = function onClientFile(stream, mimetype, filename) { - $scope.safeApply(function() { - - // Begin file download - var guacFileStartEvent = $scope.$emit('guacClientFileDownloadStart', guacClient, stream.index, mimetype, filename); - if (!guacFileStartEvent.defaultPrevented) { - - var blob_reader = new Guacamole.BlobReader(stream, mimetype); - - // Update progress as data is received - blob_reader.onprogress = function onprogress() { - $scope.$emit('guacClientFileDownloadProgress', guacClient, stream.index, mimetype, filename, blob_reader.getLength()); - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // When complete, prompt for download - blob_reader.onend = function onend() { - $scope.$emit('guacClientFileDownloadEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob()); - }; - - stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); - - } - - // Respond with UNSUPPORTED if download (default action) canceled within event handler - else - stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED); - - }); - }; - - return guacClient; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/webapp/app/client/services/guacClientManager.js b/guacamole/src/main/webapp/app/client/services/guacClientManager.js new file mode 100644 index 000000000..8e200e3ab --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/guacClientManager.js @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A service for managing several active Guacamole clients. + */ +angular.module('client').factory('guacClientManager', ['ManagedClient', + function guacClientManager(ManagedClient) { + + var service = {}; + + /** + * Map of all active managed clients. Each key is the ID of the connection + * used by that client. + * + * @type Object. + */ + service.managedClients = {}; + + /** + * Creates a new ManagedClient associated with the connection having the + * given ID. If such a ManagedClient already exists, it is disconnected and + * replaced. + * + * @param {String} id + * The ID of the connection whose ManagedClient should be retrieved. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. This + * parameter only has an effect if a new connection is established as + * a result of this function call. + * + * @returns {ManagedClient} + * The ManagedClient associated with the connection having the given + * ID. + */ + service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) { + + // Disconnect any existing client + if (id in service.managedClients) + service.managedClients[id].client.disconnect(); + + // Set new client + return service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters); + + }; + + /** + * Returns the ManagedClient associated with the connection having the + * given ID. If no such ManagedClient exists, a new ManagedClient is + * created. + * + * @param {String} id + * The ID of the connection whose ManagedClient should be retrieved. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. This + * parameter only has an effect if a new connection is established as + * a result of this function call. + * + * @returns {ManagedClient} + * The ManagedClient associated with the connection having the given + * ID. + */ + service.getManagedClient = function getManagedClient(id, connectionParameters) { + + // Create new managed client if it doesn't already exist + if (!(id in service.managedClients)) + service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters); + + // Return existing client + return service.managedClients[id]; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js b/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js deleted file mode 100644 index 52e259be3..000000000 --- a/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2014 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * A service for creating Guacamole tunnels. - */ -angular.module('client').factory('guacTunnelFactory', ['$rootScope', '$window', - function guacTunnelFactory($rootScope, $window) { - - var service = {}; - - /** - * Returns a new Guacamole tunnel instance, using an implementation that is - * supported by the web browser. - * - * @param {Scope} $scope The current scope. - * @returns {Guacamole.Tunnel} A new Guacamole tunnel instance. - */ - service.getInstance = function getTunnelInstance($scope) { - - var tunnel; - - // If WebSocket available, try to use it. - if ($window.WebSocket) - tunnel = new Guacamole.ChainedTunnel( - new Guacamole.WebSocketTunnel('websocket-tunnel'), - new Guacamole.HTTPTunnel('tunnel') - ); - - // If no WebSocket, then use HTTP. - else - tunnel = new Guacamole.HTTPTunnel('tunnel'); - - // Fire events for tunnel errors - tunnel.onerror = function onTunnelError(status) { - $scope.safeApply(function() { - $scope.$emit('guacTunnelError', tunnel, status.code); - }); - }; - - // Fire events for tunnel state changes - tunnel.onstatechange = function onTunnelStateChange(state) { - $scope.safeApply(function() { - - switch (state) { - - case Guacamole.Tunnel.State.CONNECTING: - $scope.$emit('guacTunnelStateChange', tunnel, 'connecting'); - break; - - case Guacamole.Tunnel.State.OPEN: - $scope.$emit('guacTunnelStateChange', tunnel, 'open'); - break; - - case Guacamole.Tunnel.State.CLOSED: - $scope.$emit('guacTunnelStateChange', tunnel, 'closed'); - break; - - } - - }); - }; - - return tunnel; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 879a53cec..401970d00 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -28,10 +28,7 @@
- +
@@ -60,7 +57,7 @@

{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}

{{'CLIENT.HELP_CLIPBOARD' | translate}}

- +

{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}

@@ -93,7 +90,7 @@
- +

@@ -102,7 +99,7 @@
- +

diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js new file mode 100644 index 000000000..ce2d92e5b --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Provides the ManagedClient class used by the guacClientManager service. + */ +angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', + function defineManagedClient($rootScope, $injector) { + + // Required types + var ClientProperties = $injector.get('ClientProperties'); + var ManagedClientState = $injector.get('ManagedClientState'); + + // Required services + var $window = $injector.get('$window'); + var $document = $injector.get('$document'); + var authenticationService = $injector.get('authenticationService'); + var guacAudio = $injector.get('guacAudio'); + var guacHistory = $injector.get('guacHistory'); + var guacVideo = $injector.get('guacVideo'); + + /** + * Object which serves as a surrogate interface, encapsulating a Guacamole + * client while it is active, allowing it to be detached and reattached + * from different client views. + * + * @constructor + * @param {ManagedClient|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedClient. + */ + var ManagedClient = function ManagedClient(template) { + + // Use empty object by default + template = template || {}; + + /** + * The ID of the connection associated with this client. + * + * @type String + */ + this.id = template.id; + + /** + * The actual underlying Guacamole client. + * + * @type Guacamole.Client + */ + this.client = template.client; + + /** + * The tunnel being used by the underlying Guacamole client. + * + * @type Guacamole.Tunnel + */ + this.tunnel = template.tunnel; + + /** + * The name returned via the Guacamole protocol for this connection, if + * any. + * + * @type String + */ + this.name = template.name; + + /** + * The current clipboard contents. + * + * @type String + */ + this.clipboardData = template.clipboardData; + + /** + * The current state of the Guacamole client (idle, connecting, + * connected, terminated with error, etc.). + * + * @type ManagedClientState + */ + this.clientState = template.clientState || new ManagedClientState(); + + /** + * Properties associated with the display and behavior of the Guacamole + * client. + * + * @type ClientProperties + */ + this.clientProperties = template.clientProperties || new ClientProperties(); + + }; + + /** + * Returns the string of connection parameters to be passed to the + * Guacamole client during connection. This string generally contains the + * desired connection ID, display resolution, and supported audio/video + * codecs. + * + * @param {String} id + * The ID of the connection or group to connect to. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. + * + * @returns {String} + * The string of connection parameters to be passed to the Guacamole + * client. + */ + var getConnectString = function getConnectString(id, connectionParameters) { + + // Calculate optimal width/height for display + var pixel_density = $window.devicePixelRatio || 1; + var optimal_dpi = pixel_density * 96; + var optimal_width = $window.innerWidth * pixel_density; + var optimal_height = $window.innerHeight * pixel_density; + + // Build base connect string + var connectString = + "id=" + encodeURIComponent(id) + + "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) + + "&width=" + Math.floor(optimal_width) + + "&height=" + Math.floor(optimal_height) + + "&dpi=" + Math.floor(optimal_dpi) + + (connectionParameters ? '&' + connectionParameters : ''); + + // Add audio mimetypes to connect_string + guacAudio.supported.forEach(function(mimetype) { + connectString += "&audio=" + encodeURIComponent(mimetype); + }); + + // Add video mimetypes to connect_string + guacVideo.supported.forEach(function(mimetype) { + connectString += "&video=" + encodeURIComponent(mimetype); + }); + + return connectString; + + }; + + /** + * Store the thumbnail of the given managed client within the connection + * history under its associated ID. If the client is not connected, this + * function has no effect. + * + * @param {String} managedClient + * The client whose history entry should be updated. + */ + var updateHistoryEntry = function updateHistoryEntry(managedClient) { + + var display = managedClient.client.getDisplay(); + + // Update stored thumbnail of previous connection + if (display && display.getWidth() > 0 && display.getHeight() > 0) { + + // Get screenshot + var canvas = display.flatten(); + + // Calculate scale of thumbnail (max 320x240, max zoom 100%) + var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); + + // Create thumbnail canvas + var thumbnail = $document[0].createElement("canvas"); + thumbnail.width = canvas.width*scale; + thumbnail.height = canvas.height*scale; + + // Scale screenshot to thumbnail + var context = thumbnail.getContext("2d"); + context.drawImage(canvas, + 0, 0, canvas.width, canvas.height, + 0, 0, thumbnail.width, thumbnail.height + ); + + guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png")); + + } + + }; + + /** + * Creates a new ManagedClient, connecting it to the specified connection + * or group. + * + * @param {String} id + * The ID of the connection or group to connect to. + * + * @param {String} [connectionParameters] + * Any additional HTTP parameters to pass while connecting. + * + * @returns {ManagedClient} + * A new ManagedClient instance which is connected to the connection or + * connection group having the given ID. + */ + ManagedClient.getInstance = function getInstance(id, connectionParameters) { + + var tunnel; + + // If WebSocket available, try to use it. + if ($window.WebSocket) + tunnel = new Guacamole.ChainedTunnel( + new Guacamole.WebSocketTunnel('websocket-tunnel'), + new Guacamole.HTTPTunnel('tunnel') + ); + + // If no WebSocket, then use HTTP. + else + tunnel = new Guacamole.HTTPTunnel('tunnel'); + + // Get new client instance + var client = new Guacamole.Client(tunnel); + + // Associate new managed client with new client and tunnel + var managedClient = new ManagedClient({ + id : id, + client : client, + tunnel : tunnel + }); + + // Fire events for tunnel errors + tunnel.onerror = function tunnelError(status) { + $rootScope.$apply(function handleTunnelError() { + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.TUNNEL_ERROR, + status.code); + }); + }; + + // Update connection state as tunnel state changes + tunnel.onstatechange = function tunnelStateChanged(state) { + $rootScope.$apply(function updateTunnelState() { + + switch (state) { + + // Connection is being established + case Guacamole.Tunnel.State.CONNECTING: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CONNECTING); + break; + + // Connection has closed + case Guacamole.Tunnel.State.CLOSED: + + updateHistoryEntry(managedClient); + + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.DISCONNECTED); + break; + + } + + }); + }; + + // Update connection state as client state changes + client.onstatechange = function clientStateChanged(clientState) { + $rootScope.$apply(function updateClientState() { + + switch (clientState) { + + // Idle + case 0: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.IDLE); + break; + + // Connected + waiting + case 2: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.WAITING); + break; + + // Connected + case 3: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.DISCONNECTED); + break; + + // Connecting, disconnecting, and disconnected are all + // either ignored or handled by tunnel state + + case 1: // Connecting + case 4: // Disconnecting + case 5: // Disconnected + break; + + } + + }); + }; + + // Update stored name if name changes + client.onname = function clientNameChanged(name) { + $rootScope.$apply(function updateName() { + managedClient.name = name; + }); + }; + + // Disconnect and update status when the client receives an error + client.onerror = function clientError(status) { + $rootScope.$apply(function handleClientError() { + + // Disconnect, if connected + client.disconnect(); + + // Update state + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CLIENT_ERROR, + status.code); + + }); + }; + + // Handle any received clipboard data + client.onclipboard = function clientClipboardReceived(stream, mimetype) { + + // Only text/plain is supported for now + if (mimetype !== "text/plain") { + stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); + return; + } + + var reader = new Guacamole.StringReader(stream); + var data = ""; + + // Append any received data to buffer + reader.ontext = function clipboard_text_received(text) { + data += text; + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // Update state when done + reader.onend = function clipboard_text_end() { + $rootScope.$apply(function updateClipboard() { + managedClient.clipboardData = data; + }); + }; + + }; + + /* TODO: Restore file transfer again */ + + /* + // Handle any received files + client.onfile = function onClientFile(stream, mimetype, filename) { + + // Begin file download + var guacFileStartEvent = $rootScope.$emit('guacClientFileDownloadStart', client, stream.index, mimetype, filename); + if (!guacFileStartEvent.defaultPrevented) { + + var blob_reader = new Guacamole.BlobReader(stream, mimetype); + + // Update progress as data is received + blob_reader.onprogress = function onprogress() { + $rootScope.$emit('guacClientFileDownloadProgress', client, stream.index, mimetype, filename, blob_reader.getLength()); + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // When complete, prompt for download + blob_reader.onend = function onend() { + $rootScope.$emit('guacClientFileDownloadEnd', client, stream.index, mimetype, filename, blob_reader.getBlob()); + }; + + stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); + + } + + // Respond with UNSUPPORTED if download (default action) canceled within event handler + else + stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED); + + }; + */ + + // Connect the Guacamole client + client.connect(getConnectString(id, connectionParameters)); + + return managedClient; + + }; + + return ManagedClient; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClientState.js b/guacamole/src/main/webapp/app/client/types/ManagedClientState.js new file mode 100644 index 000000000..4ab0922a8 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedClientState.js @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Provides the ManagedClient class used by the guacClientManager service. + */ +angular.module('client').factory('ManagedClientState', [function defineManagedClientState() { + + /** + * Object which represents the state of a Guacamole client and its tunnel, + * including any error conditions. + * + * @constructor + * @param {ManagedClientState|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedClientState. + */ + var ManagedClientState = function ManagedClientState(template) { + + // Use empty object by default + template = template || {}; + + /** + * The current connection state. Valid values are described by + * ManagedClientState.ConnectionState. + * + * @type String + * @default ManagedClientState.ConnectionState.IDLE + */ + this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE; + + /** + * The status code of the current error condition, if connectionState + * is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState + * values, this will be @link{Guacamole.Status.Code.SUCCESS}. + * + * @type Number + * @default Guacamole.Status.Code.SUCCESS + */ + this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; + + }; + + /** + * Valid connection state strings. Each state string is associated with a + * specific state of a Guacamole connection. + */ + ManagedClientState.ConnectionState = { + + /** + * The Guacamole connection has not yet been attempted. + * + * @type String + */ + IDLE : "IDLE", + + /** + * The Guacamole connection is being established. + * + * @type String + */ + CONNECTING : "CONNECTING", + + /** + * The Guacamole connection has been successfully established, and the + * client is now waiting for receipt of initial graphical data. + * + * @type String + */ + WAITING : "WAITING", + + /** + * The Guacamole connection has been successfully established, and + * initial graphical data has been received. + * + * @type String + */ + CONNECTED : "CONNECTED", + + /** + * The Guacamole connection has terminated successfully. No errors are + * indicated. + * + * @type String + */ + DISCONNECTED : "DISCONNECTED", + + /** + * The Guacamole connection has terminated due to an error reported by + * the client. The associated error code is stored in statusCode. + * + * @type String + */ + CLIENT_ERROR : "CLIENT_ERROR", + + /** + * The Guacamole connection has terminated due to an error reported by + * the tunnel. The associated error code is stored in statusCode. + * + * @type String + */ + TUNNEL_ERROR : "TUNNEL_ERROR" + + }; + + /** + * Sets the current client state and, if given, the associated status code. + * If an error is already represented, this function has no effect. + * + * @param {ManagedClientState} clientState + * The ManagedClientState to update. + * + * @param {String} connectionState + * The connection state to assign to the given ManagedClientState, as + * listed within ManagedClientState.ConnectionState. + * + * @param {Number} [statusCode] + * The status code to assign to the given ManagedClientState, if any, + * as listed within Guacamole.Status.Code. If no status code is + * specified, the status code of the ManagedClientState is not touched. + */ + ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) { + + // Do not set state after an error is registered + if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR + || clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) + return; + + // Update connection state + clientState.connectionState = connectionState; + + // Set status code, if given + if (statusCode) + clientState.statusCode = statusCode; + + }; + + return ManagedClientState; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index c927e22e2..6ef71eac5 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -26,32 +26,17 @@ angular.module('index').controller('indexController', ['$scope', '$injector', function indexController($scope, $injector) { - // Get class dependencies + // Required types var PermissionSet = $injector.get("PermissionSet"); - // Get services - var permissionService = $injector.get("permissionService"), - authenticationService = $injector.get("authenticationService"), - $q = $injector.get("$q"), - $document = $injector.get("$document"), - $window = $injector.get("$window"), - $location = $injector.get("$location"); + // Required services + var $document = $injector.get("$document"); + var $location = $injector.get("$location"); + var $q = $injector.get("$q"); + var $window = $injector.get("$window"); + var authenticationService = $injector.get("authenticationService"); + var permissionService = $injector.get("permissionService"); - /* - * Safe $apply implementation from Alex Vanston: - * https://coderwall.com/p/ngisma - */ - $scope.safeApply = function(fn) { - var phase = this.$root.$$phase; - if(phase === '$apply' || phase === '$digest') { - if(fn && (typeof(fn) === 'function')) { - fn(); - } - } else { - this.$apply(fn); - } - }; - /** * The current status notification, or false if no status is currently * shown.