{{'CLIENT.HELP_CLIPBOARD' | translate}}
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 63a35bdc8..c24eaaacc 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -112,24 +112,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams 0x031D: true }; - /** - * All upload error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - var UPLOAD_ERRORS = { - 0x0100: true, - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0204: true, - 0x0205: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - /** * All error codes for which automatic reconnection is appropriate when a * tunnel error occurs. @@ -333,7 +315,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); - $scope.$watch('menuShown', function setKeyboardEnabled(menuShown, menuShownPreviousState) { + $scope.$watch('menuShown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { // Send clipboard data if menu is hidden if (!menuShown && menuShownPreviousState) @@ -385,6 +367,17 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.page.title = name; }); + // Show file transfer section of menu if new file transfers have started + $scope.$watch('client.uploads.length + client.downloads.length', function transfersChanged(count, oldCount) { + + // Show menu and scroll file transfer into view + if (count > oldCount) { + $scope.menuShown = true; + $scope.fileTransferMarker.scrollIntoView(); + } + + }); + // Show status dialog when connection status changes $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) { @@ -505,60 +498,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }; - /** - * Returns a progress object, as required by $scope.addNotification(), which - * contains the given number of bytes as an appropriate combination of - * progress value and associated unit. - * - * @param {String} text - * The translation string to associate with the progress object - * returned. - * - * @param {Number} bytes The number of bytes. - * @param {Number} [length] The file length, in bytes, if known. - * - * @returns {Object} - * A progress object, as required by $scope.addNotification(). - */ - var getFileProgress = function getFileProgress(text, bytes, length) { - - // Gigabytes - if (bytes > 1000000000) - return { - text : text, - value : (bytes / 1000000000).toFixed(1), - ratio : bytes / length, - unit : "gb" - }; - - // Megabytes - if (bytes > 1000000) - return { - text : text, - value : (bytes / 1000000).toFixed(1), - ratio : bytes / length, - unit : "mb" - }; - - // Kilobytes - if (bytes > 1000) - return { - text : text, - value : (bytes / 1000).toFixed(1), - ratio : bytes / length, - unit : "kb" - }; - - // Bytes - return { - text : text, - value : bytes, - ratio : bytes / length, - unit : "b" - }; - - }; - // Clean up when view destroyed $scope.$on('$destroy', function clientViewDestroyed() { diff --git a/guacamole/src/main/webapp/app/client/directives/guacFileTransfer.js b/guacamole/src/main/webapp/app/client/directives/guacFileTransfer.js new file mode 100644 index 000000000..0c232035e --- /dev/null +++ b/guacamole/src/main/webapp/app/client/directives/guacFileTransfer.js @@ -0,0 +1,237 @@ +/* + * 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. + */ + +/** + * Directive which displays an active file transfer, providing links for + * downloads, if applicable. + */ +angular.module('client').directive('guacFileTransfer', [function guacFileTransfer() { + + return { + restrict: 'E', + replace: true, + scope: { + + /** + * The file transfer to display. + * + * @type ManagedFileUpload|ManagedFileDownload + */ + transfer : '=' + + }, + + templateUrl: 'app/client/templates/guacFileTransfer.html', + controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) { + + // Required types + var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); + + /** + * All upload error codes handled and passed off for translation. + * Any error code not present in this list will be represented by + * the "DEFAULT" translation. + */ + var UPLOAD_ERRORS = { + 0x0100: true, + 0x0201: true, + 0x0202: true, + 0x0203: true, + 0x0204: true, + 0x0205: true, + 0x0301: true, + 0x0303: true, + 0x0308: true, + 0x031D: true + }; + + /** + * Returns the unit string that is most appropriate for the + * number of bytes transferred thus far - either 'gb', 'mb', 'kb', + * or 'b'. + * + * @returns {String} + * The unit string that is most appropriate for the number of + * bytes transferred thus far. + */ + $scope.getProgressUnit = function getProgressUnit() { + + var bytes = $scope.transfer.progress; + + // Gigabytes + if (bytes > 1000000000) + return 'gb'; + + // Megabytes + if (bytes > 1000000) + return 'mb'; + + // Kilobytes + if (bytes > 1000) + return 'kb'; + + // Bytes + return 'b'; + + }; + + /** + * Returns the amount of data transferred thus far, in the units + * returned by getProgressUnit(). + * + * @returns {Number} + * The amount of data transferred thus far, in the units + * returned by getProgressUnit(). + */ + $scope.getProgressValue = function getProgressValue() { + + var bytes = $scope.transfer.progress; + if (!bytes) + return bytes; + + // Convert bytes to necessary units + switch ($scope.getProgressUnit()) { + + // Gigabytes + case 'gb': + return (bytes / 1000000000).toFixed(1); + + // Megabytes + case 'mb': + return (bytes / 1000000).toFixed(1); + + // Kilobytes + case 'kb': + return (bytes / 1000).toFixed(1); + + // Bytes + case 'b': + default: + return bytes; + + } + + }; + + /** + * Returns the percentage of bytes transferred thus far, if the + * overall length of the file is known. + * + * @returns {Number} + * The percentage of bytes transferred thus far, if the + * overall length of the file is known. + */ + $scope.getPercentDone = function getPercentDone() { + return $scope.transfer.progress / $scope.transfer.length * 100; + }; + + /** + * Determines whether the associated file transfer is in progress. + * + * @returns {Boolean} + * true if the file transfer is in progress, false othherwise. + */ + $scope.isInProgress = function isInProgress() { + + // Not in progress if there is no transfer + if (!$scope.transfer) + return false; + + // Determine in-progress status based on stream state + switch ($scope.transfer.transferState.streamState) { + + // IDLE or OPEN file transfers are active + case ManagedFileTransferState.StreamState.IDLE: + case ManagedFileTransferState.StreamState.OPEN: + return true; + + // All others are not active + default: + return false; + + } + + }; + + /** + * Returns whether the file associated with this file transfer can + * be saved locally via a call to save(). + * + * @returns {Boolean} + * true if a call to save() will result in the file being + * saved, false otherwise. + */ + $scope.isSavable = function isSavable() { + return !!$scope.transfer.blob; + }; + + /** + * Saves the downloaded file, if any. If this transfer is an upload + * or the download is not yet complete, this function has no + * effect. + */ + $scope.save = function save() { + + // Ignore if no blob exists + if (!$scope.transfer.blob) + return; + + // Save file + saveAs($scope.transfer.blob, $scope.transfer.filename); + + }; + + /** + * Returns whether an error has occurred. If an error has occurred, + * the transfer is no longer active, and the text of the error can + * be read from getErrorText(). + * + * @returns {Boolean} + * true if an error has occurred during transfer, false + * otherwise. + */ + $scope.hasError = function hasError() { + return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR; + }; + + /** + * Returns the text of the current error as a translation string. + * + * @returns {String} + * The name of the translation string containing the text + * associated with the current error. + */ + $scope.getErrorText = function getErrorText() { + + // Determine translation name of error + var status = $scope.transfer.transferState.statusCode; + var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + + // Return translation string + return 'CLIENT.ERROR_UPLOAD_' + errorName; + + }; + + }] // end file transfer controller + + }; +}]); diff --git a/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js b/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js new file mode 100644 index 000000000..d49e94288 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js @@ -0,0 +1,137 @@ +/* + * 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. + */ + +/** + * Directive which displays all active file transfers. + */ +angular.module('client').directive('guacFileTransferManager', [function guacFileTransferManager() { + + return { + restrict: 'E', + replace: true, + scope: { + + /** + * The client whose file transfers should be managed by this + * directive. + * + * @type ManagerClient + */ + client : '=' + + }, + + templateUrl: 'app/client/templates/guacFileTransferManager.html', + controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) { + + // Required types + var ManagedClient = $injector.get('ManagedClient'); + var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); + + /** + * Determines whether the attached client has associated file + * transfers, regardless of those file transfers' state. + * + * @returns {Boolean} + * true if there are any file transfers associated with the + * attached client, false otherise. + */ + $scope.hasTransfers = function hasTransfers() { + + // There are no file transfers if there is no client + if (!$scope.client) + return false; + + return !!($scope.client.uploads.length || $scope.client.downloads.length); + + }; + + /** + * Determines whether the given file transfer state indicates an + * in-progress transfer. + * + * @param {ManagedFileTransferState} transferState + * The file transfer state to check. + * + * @returns {Boolean} + * true if the given file transfer state indicates an in- + * progress transfer, false otherwise. + */ + var isInProgress = function isInProgress(transferState) { + switch (transferState.streamState) { + + // IDLE or OPEN file transfers are active + case ManagedFileTransferState.StreamState.IDLE: + case ManagedFileTransferState.StreamState.OPEN: + return true; + + // All others are not active + default: + return false; + + } + }; + + /** + * Removes all file transfers which are not currently in-progress. + */ + $scope.clearCompletedTransfers = function clearCompletedTransfers() { + + // Nothing to clear if no client attached + if (!$scope.client) + return; + + // Remove completed uploads + $scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) { + return isInProgress(upload.transferState); + }); + + // Remove completed downloads + $scope.client.downloads = $scope.client.downloads.filter(function isDownloadInProgress(download) { + return isInProgress(download.transferState); + }); + + }; + + /** + * Begins a file upload through the attached Guacamole client for + * each file in the given FileList. + * + * @param {FileList} files + * The files to upload. + */ + $scope.uploadFiles = function uploadFiles(files) { + + // Ignore file uploads if no attached client + if (!$scope.client) + return; + + // Upload each file + for (var i = 0; i < files.length; i++) + ManagedClient.uploadFile($scope.client, files[i]); + + }; + + }] + + }; +}]); diff --git a/guacamole/src/main/webapp/app/client/styles/transfer-manager.css b/guacamole/src/main/webapp/app/client/styles/transfer-manager.css new file mode 100644 index 000000000..e2b9c5f0d --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/transfer-manager.css @@ -0,0 +1,41 @@ +/* + * 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. + */ + +.transfer-manager .action-buttons { + text-align: center; +} + +.transfer-manager .no-transfers { + + color: rgba(255, 255, 255, 0.5); + text-shadow: -1px -1px rgba(0, 0, 0, 0.5); + opacity: 0.5; + + font-size: 2em; + font-weight: bolder; + text-align: center; + +} + +.transfer-manager .transfer { + margin: 1em; +} diff --git a/guacamole/src/main/webapp/app/client/styles/transfer.css b/guacamole/src/main/webapp/app/client/styles/transfer.css new file mode 100644 index 000000000..a927ce083 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/transfer.css @@ -0,0 +1,129 @@ +/* + * 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. + */ + +.transfer { + position: relative; + padding: 0.5em; + font-size: 0.75em; +} + +.transfer .filename { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + margin-bottom: 0.5em; + font-family: monospace; + font-weight: bold; +} + +.transfer .text { + position: relative; + text-align: center; + margin-top: 0.5em; +} + +@keyframes transfer-progress { + from {background-position: 0px 0px;} + to {background-position: 64px 0px;} +} + +@-webkit-keyframes transfer-progress { + from {background-position: 0px 0px;} + to {background-position: 64px 0px;} +} + +.transfer .progress { + + width: 100%; + background: #C2C2C2; + padding: 0.25em; + + border: 1px solid gray; + + position: absolute; + top: 0; + left: 0; + bottom: 0; + opacity: 0.25; + +} + +.transfer.in-progress .progress { + + background-image: url('images/progress.png'); + + background-size: 16px 16px; + -moz-background-size: 16px 16px; + -webkit-background-size: 16px 16px; + -khtml-background-size: 16px 16px; + + animation-name: transfer-progress; + animation-duration: 2s; + animation-timing-function: linear; + animation-iteration-count: infinite; + + -webkit-animation-name: transfer-progress; + -webkit-animation-duration: 2s; + -webkit-animation-timing-function: linear; + -webkit-animation-iteration-count: infinite; + +} + +.transfer .progress .bar { + background: #A3D655; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0; +} + +.savable.transfer { + cursor: pointer; +} + +.savable.transfer:hover .progress { + border-color: black; +} + +.savable.transfer .filename { + color: blue; + text-decoration: underline; +} + +.error.transfer { + background: #FDD; +} + +.error.transfer .progress { + border-color: rgba(0, 0, 0, 0.125); +} + +.error.transfer .text, +.error.transfer .progress .bar { + display: none; +} + +.error-text { + margin-bottom: 0; +} diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 8a071ed53..8f4e3687e 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -60,13 +60,21 @@ {{'CLIENT.ACTION_DISCONNECT' | translate}}
{{'CLIENT.HELP_CLIPBOARD' | translate}}
{{'CLIENT.HELP_MOUSE_MODE' | translate}}
@@ -115,6 +124,7 @@{{getErrorText() | translate}}
+ +{{'CLIENT.INFO_NO_FILE_TRANSFERS' | translate}}
+ + +