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.name}}

- + +

{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}

{{'CLIENT.HELP_CLIPBOARD' | translate}}

+ +

{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}

+
+ +
+ +

{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}

@@ -91,6 +99,7 @@
+

{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}

{{'CLIENT.HELP_MOUSE_MODE' | translate}}

@@ -115,6 +124,7 @@
+

{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}

@@ -124,11 +134,5 @@
- - - -
- -
diff --git a/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html b/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html new file mode 100644 index 000000000..26cb1dee7 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html @@ -0,0 +1,36 @@ +
+ + + +
{{transfer.filename}}
+ + +
{{'CLIENT.TEXT_FILE_TRANSFER_PROGRESS' | translate:'{PROGRESS: getProgressValue(), UNIT: getProgressUnit()}'}}
+ + +
+ + +

{{getErrorText() | translate}}

+ +
diff --git a/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html b/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html new file mode 100644 index 000000000..964294e7c --- /dev/null +++ b/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html @@ -0,0 +1,43 @@ +
+ + + +

{{'CLIENT.INFO_NO_FILE_TRANSFERS' | translate}}

+ + +
+ +
+ + +
+ +
+ + +
+ {{'CLIENT.ACTION_UPLOAD_FILES' | translate}} + {{'CLIENT.ACTION_CLEAR_COMPLETED_TRANSFERS' | translate}} +
+ +
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 8c53d7b68..d73b49981 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -99,7 +99,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * * @type String */ - this.clipboardData = template.clipboardData; + this.clipboardData = template.clipboardData || ''; /** * All downloaded files. As files are downloaded, their progress can be diff --git a/guacamole/src/main/webapp/app/element/directives/guacFocus.js b/guacamole/src/main/webapp/app/element/directives/guacFocus.js index 2331bc0e1..b8f8d6cdd 100644 --- a/guacamole/src/main/webapp/app/element/directives/guacFocus.js +++ b/guacamole/src/main/webapp/app/element/directives/guacFocus.js @@ -39,7 +39,7 @@ angular.module('element').directive('guacFocus', ['$parse', function guacFocus($ var guacFocus = $parse($attrs.guacFocus); /** - * The element which will register the drag gesture. + * The element which will be focused / blurred. * * @type Element */ diff --git a/guacamole/src/main/webapp/app/element/directives/guacMarker.js b/guacamole/src/main/webapp/app/element/directives/guacMarker.js new file mode 100644 index 000000000..444cf6814 --- /dev/null +++ b/guacamole/src/main/webapp/app/element/directives/guacMarker.js @@ -0,0 +1,62 @@ +/* + * 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 directive which stores a marker which refers to a specific element, + * allowing that element to be scrolled into view when desired. + */ +angular.module('element').directive('guacMarker', ['$injector', function guacMarker($injector) { + + // Required types + var Marker = $injector.get('Marker'); + + // Required services + var $parse = $injector.get('$parse'); + + return { + restrict: 'A', + + link: function linkGuacMarker($scope, $element, $attrs) { + + /** + * The property in which a new Marker should be stored. The new + * Marker will refer to the element associated with this directive. + * + * @type Marker + */ + var guacMarker = $parse($attrs.guacMarker); + + /** + * The element to associate with the new Marker. + * + * @type Element + */ + var element = $element[0]; + + // Assign new marker + guacMarker.assign($scope, new Marker(element)); + + } + + }; + +}]); diff --git a/guacamole/src/main/webapp/app/element/directives/guacUpload.js b/guacamole/src/main/webapp/app/element/directives/guacUpload.js new file mode 100644 index 000000000..b235e0d2b --- /dev/null +++ b/guacamole/src/main/webapp/app/element/directives/guacUpload.js @@ -0,0 +1,95 @@ +/* + * 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 directive which allows multiple files to be uploaded. Clicking on the + * associated element will result in a file selector dialog, which then calls + * the provided callback function with any chosen files. + */ +angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) { + + return { + restrict: 'A', + + link: function linkGuacUpload($scope, $element, $attrs) { + + /** + * The function to call whenever files are chosen. The callback is + * provided a single parameter: the FileList containing all chosen + * files. + * + * @type Function + */ + var guacUpload = $scope.$eval($attrs.guacUpload); + + /** + * The element which will register the drag gesture. + * + * @type Element + */ + var element = $element[0]; + + /** + * Internal form, containing a single file input element. + * + * @type HTMLFormElement + */ + var form = $document[0].createElement('form'); + + /** + * Internal file input element. + * + * @type HTMLInputElement + */ + var input = $document[0].createElement('input'); + + // Init input element + input.type = 'file'; + input.multiple = true; + + // Add input element to internal form + form.appendChild(input); + + // Notify of any chosen files + input.addEventListener('change', function filesSelected() { + $scope.$apply(function setSelectedFiles() { + + // Only set chosen files selection is not canceled + if (guacUpload && input.files.length > 0) + guacUpload(input.files); + + // Reset selection + form.reset(); + + }); + }); + + // Open file chooser when element is clicked + element.addEventListener('click', function elementClicked() { + input.click(); + }); + + } // end guacUpload link function + + }; + +}]); diff --git a/guacamole/src/main/webapp/app/element/types/Marker.js b/guacamole/src/main/webapp/app/element/types/Marker.js new file mode 100644 index 000000000..c3cada482 --- /dev/null +++ b/guacamole/src/main/webapp/app/element/types/Marker.js @@ -0,0 +1,50 @@ +/* + * 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 Marker class definition. + */ +angular.module('element').factory('Marker', [function defineMarker() { + + /** + * Creates a new Marker which allows its associated element to be scolled + * into view as desired. + * + * @constructor + * @param {Element} element + * The element to associate with this marker. + */ + var Marker = function Marker(element) { + + /** + * Scrolls scrollable elements, or the window, as needed to bring the + * element associated with this marker into view. + */ + this.scrollIntoView = function scrollIntoView() { + element.scrollIntoView(); + }; + + }; + + return Marker; + +}]); diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index cc0fee696..71ec4fe63 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -17,16 +17,17 @@ "CLIENT" : { - "ACTION_RECONNECT" : "Reconnect", - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_DISCONNECT" : "Disconnect", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear Completed Transfers", + "ACTION_DISCONNECT" : "Disconnect", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", + "ACTION_RECONNECT" : "Reconnect", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", + "ACTION_UPLOAD_FILES" : "Upload Files", "DIALOG_HEADER_CONNECTING" : "Connecting", "DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error", "DIALOG_HEADER_DISCONNECTED" : "Disconnected", - "DIALOG_HEADER_FILE_TRANSFER" : "File Transfer", "ERROR_CLIENT_201" : "This connection has been closed because the server is busy. Please wait a few minutes and try again.", "ERROR_CLIENT_202" : "The Guacamole server has closed the connection because the remote desktop is taking too long to respond. Please try again or contact your system administrator.", @@ -69,6 +70,8 @@ "HELP_MOUSE_MODE_ABSOLUTE" : "Tap to click. The click occurs at the location of the touch.", "HELP_MOUSE_MODE_RELATIVE" : "Drag to move the mouse pointer and tap to click. The click occurs at the location of the pointer.", + "INFO_NO_FILE_TRANSFERS" : "No file transfers.", + "NAME_INPUT_METHOD_NONE" : "None", "NAME_INPUT_METHOD_OSK" : "On-screen keyboard", "NAME_INPUT_METHOD_TEXT" : "Text input", @@ -79,10 +82,11 @@ "NAME_MOUSE_MODE_ABSOLUTE" : "Touchscreen", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", - "SECTION_HEADER_CLIPBOARD" : "Clipboard", - "SECTION_HEADER_INPUT_METHOD" : "Input method", - "SECTION_HEADER_DISPLAY" : "Display", - "SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode", + "SECTION_HEADER_CLIPBOARD" : "Clipboard", + "SECTION_HEADER_DISPLAY" : "Display", + "SECTION_HEADER_FILE_TRANSFERS" : "File Transfers", + "SECTION_HEADER_INPUT_METHOD" : "Input method", + "SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode", "TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window", "TEXT_CLIENT_STATUS_IDLE" : "Idle.", @@ -101,10 +105,10 @@ "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE" : "@:APP.ACTION_MANAGE", - "INFO_NO_RECENT_CONNECTIONS" : "No recent Connections.", + "INFO_NO_RECENT_CONNECTIONS" : "No recent connections.", - "SECTION_HEADER_RECENT_CONNECTIONS" : "Recent Connections", - "SECTION_HEADER_ALL_CONNECTIONS" : "All Connections" + "SECTION_HEADER_ALL_CONNECTIONS" : "All Connections", + "SECTION_HEADER_RECENT_CONNECTIONS" : "Recent Connections" },