diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index af6572b04..350aa3840 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -67,6 +67,19 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', 0x0308: true, 0x031D: true }; + + /** + * The reconnect action to be provided along with the object sent to + * showStatus. + */ + var RECONNECT_ACTION = { + name : "client.action.reconnect", + // Handle reconnect action + callback : function reconnectCallback() { + $scope.id = uniqueId; + $scope.showStatus(false); + } + }; // Get DAO for reading connections and groups var connectionGroupDAO = $injector.get('connectionGroupDAO'); @@ -215,7 +228,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', className: "error", title: "client.error.connectionErrorTitle", text: "client.error.clientErrors." + errorName, - actions: [ "client.action.reconnect" ] + actions: [ RECONNECT_ACTION ] }); }); @@ -250,19 +263,11 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', className: "error", title: "client.error.connectionErrorTitle", text: "client.error.tunnelErrors." + errorName, - actions: [ "client.action.reconnect" ] + actions: [ RECONNECT_ACTION ] }); }); - // Handle reconnect action - $scope.$on('guacAction', function actionListener(event, action) { - - if (action === "client.action.reconnect") - $scope.id = uniqueId; - - }); - $scope.formattedScale = function formattedScale() { return Math.round($scope.clientProperties.scale * 100); }; @@ -292,5 +297,66 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', $scope.autoFitDisabled = function() { return $scope.clientProperties.minZoom >= 1; }; + + // Mapping of stream index to notification object + var downloadNotifications = {}; + + // Mapping of stream index to notification ID + var downloadNotificationIDs = {}; + + $scope.$on('guacClientFileStart', function handleClientFileStart(event, guacClient, streamIndex, mimetype, filename) { + $scope.safeApply(function() { + + var notification = { + className : 'download', + title : 'client.fileTransfer.title', + text : filename + }; + + downloadNotifications[streamIndex] = notification; + downloadNotificationIDs[streamIndex] = $scope.addNotification(notification); + + }); + }); + + $scope.$on('guacClientFileProgress', function handleClientFileProgress(event, guacClient, streamIndex, mimetype, filename, length) { + $scope.safeApply(function() { + + var notification = downloadNotifications[streamIndex]; + if (notification) + notification.progress = length; + + }); + }); + + $scope.$on('guacClientFileEnd', function handleClientFileEnd(event, guacClient, streamIndex, mimetype, filename, blob) { + $scope.safeApply(function() { + + var notification = downloadNotifications[streamIndex]; + var notificationID = downloadNotificationIDs[streamIndex]; + + /** + * Saves the current file. + */ + var saveFile = function saveFile() { + saveAs(blob, filename); + $scope.removeNotification(notificationID); + delete downloadNotifications[streamIndex]; + delete downloadNotificationIDs[streamIndex]; + }; + + // Add download action and remove progress indicator + if (notificationID && notification) { + delete notification.progress; + notification.actions = [ + { + name : 'client.fileTransfer.save', + callback : saveFile + } + ]; + } + + }); + }); }]); diff --git a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js b/guacamole/src/main/webapp/app/client/services/guacClientFactory.js index f5bd50c12..5018a1474 100644 --- a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js +++ b/guacamole/src/main/webapp/app/client/services/guacClientFactory.js @@ -147,13 +147,13 @@ angular.module('client').factory('guacClientFactory', ['$rootScope', // Update progress as data is received blob_reader.onprogress = function onprogress() { - $scope.$emit('guacClientFileProgress', guacClient, stream.index, mimetype, filename); + $scope.$emit('guacClientFileProgress', 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('guacClientFileEnd', guacClient, stream.index, mimetype, filename); + $scope.$emit('guacClientFileEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob()); }; stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); diff --git a/guacamole/src/main/webapp/app/client/styles/notification.css b/guacamole/src/main/webapp/app/client/styles/notification.css index 65b4478b5..9b559efd5 100644 --- a/guacamole/src/main/webapp/app/client/styles/notification.css +++ b/guacamole/src/main/webapp/app/client/styles/notification.css @@ -33,56 +33,21 @@ font-size: 0.7em; text-align: center; - border: 1px solid rgba(0, 0, 0, 0.75); - -moz-border-radius: 0.2em; - -webkit-border-radius: 0.2em; - -khtml-border-radius: 0.2em; - border-radius: 0.2em; + border: 1px solid rgba(0, 0, 0, 0.125); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.125); background: white; color: black; padding: 0.5em; margin: 1em; + width: 2in; + max-width: 75%; overflow: hidden; - - box-shadow: 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.25); - } -.notification div { - display: inline-block; - text-align: left; -} - -.notification .title-bar { - display: block; - white-space: nowrap; - font-weight: bold; - - border-bottom: 1px solid black; - padding-bottom: 0.5em; - margin-bottom: 0.5em; -} - -.notification .title-bar * { - vertical-align: middle; -} - -.notification .close { - - background: url('images/action-icons/guac-close.png'); - background-size: 10px 10px; - -moz-background-size: 10px 10px; - -webkit-background-size: 10px 10px; - -khtml-background-size: 10px 10px; - - width: 10px; - height: 10px; - - float: right; - cursor: pointer; - +.notification .buttons { + margin: 0; } @keyframes notification-progress { @@ -95,43 +60,25 @@ to {background-position: 64px 0px;} } -.notification .caption, -.notification.download .caption { +.notification .title-bar { + font-size: 1.25em; + font-weight: bold; + + text-transform: uppercase; + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); + background: rgba(0, 0, 0, 0.04); + + margin-bottom: 1em; +} + +.notification .text { width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.notification.upload .status, -.notification.download .status { - color: red; - font-size: 1em; - padding: 1em; -} - -.notification.download .progress, -.notification.upload .progress, -.notification.download .download { - - margin-top: 1em; - margin-left: 0.75em; - padding: 0.25em; - min-width: 5em; - - border: 1px solid gray; - -moz-border-radius: 0.2em; - -webkit-border-radius: 0.2em; - -khtml-border-radius: 0.2em; - border-radius: 0.2em; - - text-align: center; - float: right; - - position: relative; - -} - .notification.upload .progress { float: none; width: 80%; @@ -160,6 +107,7 @@ .notification.upload .progress, .notification.download .progress { + width: 100%; background: #C2C2C2 url('images/progress.png'); background-size: 16px 16px; -moz-background-size: 16px 16px; @@ -176,6 +124,20 @@ -webkit-animation-timing-function: linear; -webkit-animation-iteration-count: infinite; + padding: 0.25em; + min-width: 5em; + + border: 1px solid gray; + -moz-border-radius: 0.2em; + -webkit-border-radius: 0.2em; + -khtml-border-radius: 0.2em; + border-radius: 0.2em; + + text-align: center; + float: right; + + position: relative; + } .notification.download .download { diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index ea49c6b03..5dc066c19 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -59,6 +59,8 @@ angular.module('index').controller('indexController', ['$scope', '$injector', $scope.currentUserIsAdmin = false; $scope.currentUserHasUpdate = false; $scope.currentUserPermissions = null; + $scope.notifications = []; + var notificationUniqueID = 0; // A promise to be fulfilled when all basic user permissions are loaded. var permissionsLoaded= $q.defer(); @@ -71,43 +73,93 @@ angular.module('index').controller('indexController', ['$scope', '$injector', $location.path('/login'); /** - * Shows or hides the status modal. If a status modal is currently shown, - * no further status modals will be shown until the current status is + * Shows or hides the status. If a status is currently shown, + * no further statuses will be shown until the current status is * hidden. * * @param {Boolean|Object} status The status to show, or false to hide the * current status. - * @param {String} [status.title] The title of the status modal. - * @param {String} [status.text] The body text of the status modal. - * @param {String} [status.className] The CSS class name to apply to the - * modal, in addition to the default - * "dialog" and "status" classes. - * @param {String[]} [status.actions] Array of action names which - * correspond to button captions. Each - * action will be displayed as a button - * within the status modal. Clickin a - * button will fire a guacStatusAction - * event. + * @param {String} [status.title] The title of the status. + * @param {String} [status.text] The body text of the status. + * @param {String} [status.className] The CSS class name to apply. + * @param {Object[]} [status.actions] Array of action objects which + * contain an action name and callback to + * be executed when that action is + * invoked. + * + * @example + * + * // To show a status message with actions + * $scope.showStatus({ + * 'title' : 'Disconnected', + * 'text' : 'You have been disconnected!', + * 'actions' : { + * 'name' : 'reconnect', + * 'callback' : function () { + * // Reconnection code goes here + * } + * } + * }); + * + * // To hide the status message + * $scope.showStatus(false); */ $scope.showStatus = function showStatus(status) { if (!$scope.status || !status) $scope.status = status; }; - + /** - * Fires a guacStatusAction event signalling a chosen action. The status - * modal will be cloased prior to firing the action event. - * - * @param {String} action The name of the action. + * Adds a notification to the the list of notifications shown. + * + * @param {Object} notification The notification to add. + * @param {String} [notification.title] The title of the notification. + * @param {String} [notification.text] The body text of the status modal. + * @param {String} [notification.className] The CSS class name to apply. + * @param {Object[]} [notification.actions] Array of action objects which + * contain an action name and callback to + * be executed when that action is + * invoked. + * @returns {Number} A unique ID for the notification that's just been added. + * + * + * @example + * + * var id = $scope.addNotification({ + * 'title' : 'Download', + * 'text' : 'You have a file ready for download!', + * 'actions' : { + * 'name' : 'download', + * 'callback' : function () { + * // download the file and remove the notification here + * } + * } + * }); */ - $scope.fireAction = function fireAction(action) { - - // Hide status modal - $scope.status = false; - - // Fire action event - $scope.$broadcast('guacAction', action); - + $scope.addNotification = function addNotification(notification) { + var id = ++notificationUniqueID; + + $scope.notifications.push({ + notification : notification, + id : id + }); + + return id; + }; + + /** + * Remove a notification by unique ID. + * + * @param {type} id The unique ID of the notification to remove. This ID is + * retrieved from the initial call to addNotification. + */ + $scope.removeNotification = function removeNotification(id) { + for(var i = 0; i < $scope.notifications.length; i++) { + if($scope.notifications[i].id === id) { + $scope.notifications.splice(i, 1); + return; + } + } }; // Allow the permissions to be reloaded elsewhere if needed diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html index 9490b98aa..f4e61cc28 100644 --- a/guacamole/src/main/webapp/index.html +++ b/guacamole/src/main/webapp/index.html @@ -47,7 +47,7 @@ THE SOFTWARE.
- +
@@ -57,6 +57,28 @@ THE SOFTWARE.
+ +
+
+ + +
+
{{wrapper.notification.title | translate}}
+
+ + +

{{wrapper.notification.text}}

+ +
+ {{wrapper.notification.progress}} +
+ +
+ +
+
+
+ diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index c1b90caac..53635a75f 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -268,6 +268,10 @@ }, "action" : { "reconnect" : "Reconnect" + }, + "fileTransfer" : { + "title" : "File Transfer", + "save" : "Save" } } }