diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 6e4f27509..e13a4cdf5 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -27,7 +27,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams function clientController($scope, $routeParams, $injector) { // Required types + var ManagedClient = $injector.get('ManagedClient'); var ManagedClientState = $injector.get('ManagedClientState'); + var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ScrollState = $injector.get('ScrollState'); // Required services @@ -407,17 +409,6 @@ 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.menu.shown = true; - $scope.menu.fileTransferMarker.scrollIntoView(); - } - - }); - // Show status dialog when connection status changes $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) { @@ -558,6 +549,119 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Set client-specific menu actions $scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ]; + /** + * The currently-visible filesystem within the filesystem menu, if the + * filesystem menu is open. If no filesystem is currently visible, this + * will be null. + * + * @type ManagedFilesystem + */ + $scope.filesystemMenuContents = null; + + /** + * Hides the filesystem menu. + */ + $scope.hideFilesystemMenu = function hideFilesystemMenu() { + $scope.filesystemMenuContents = null; + }; + + /** + * Shows the filesystem menu, displaying the contents of the given + * filesystem within it. + * + * @param {ManagedFilesystem} filesystem + * The filesystem to show within the filesystem menu. + */ + $scope.showFilesystemMenu = function showFilesystemMenu(filesystem) { + $scope.filesystemMenuContents = filesystem; + }; + + /** + * Returns whether the filesystem menu should be visible. + * + * @returns {Boolean} + * true if the filesystem menu is shown, false otherwise. + */ + $scope.isFilesystemMenuShown = function isFilesystemMenuShown() { + return !!$scope.filesystemMenuContents && $scope.menu.shown; + }; + + /** + * Returns the full path to the given file as an ordered array of parent + * directories. + * + * @param {ManagedFilesystem.File} file + * The file whose full path should be retrieved. + * + * @returns {ManagedFilesystem.File[]} + * An array of directories which make up the hierarchy containing the + * given file, in order of increasing depth. + */ + $scope.getPath = function getPath(file) { + + var path = []; + + // Add all files to path in ascending order of depth + while (file && file.parent) { + path.unshift(file); + file = file.parent; + } + + return path; + + }; + + /** + * Changes the current directory of the given filesystem to the given + * directory. + * + * @param {ManagedFilesystem} filesystem + * The filesystem whose current directory should be changed. + * + * @param {ManagedFilesystem.File} file + * The directory to change to. + */ + $scope.changeDirectory = function changeDirectory(filesystem, file) { + ManagedFilesystem.changeDirectory(filesystem, file); + }; + + /** + * 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], $scope.filesystemMenuContents); + + }; + + /** + * 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); + + }; + // Clean up when view destroyed $scope.$on('$destroy', function clientViewDestroyed() { diff --git a/guacamole/src/main/webapp/app/client/directives/guacFileBrowser.js b/guacamole/src/main/webapp/app/client/directives/guacFileBrowser.js new file mode 100644 index 000000000..d612dfdfc --- /dev/null +++ b/guacamole/src/main/webapp/app/client/directives/guacFileBrowser.js @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2015 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 displays the contents of a filesystem received through the + * Guacamole client. + */ +angular.module('client').directive('guacFileBrowser', [function guacFileBrowser() { + + return { + restrict: 'E', + replace: true, + scope: { + + /** + * The client whose file transfers should be managed by this + * directive. + * + * @type ManagedClient + */ + client : '=', + + /** + * @type ManagedFilesystem + */ + filesystem : '=' + + }, + + templateUrl: 'app/client/templates/guacFileBrowser.html', + controller: ['$scope', '$element', '$injector', function guacFileBrowserController($scope, $element, $injector) { + + // Required types + var ManagedFilesystem = $injector.get('ManagedFilesystem'); + + // Required services + var $interpolate = $injector.get('$interpolate'); + var $templateRequest = $injector.get('$templateRequest'); + + /** + * The jQuery-wrapped element representing the contents of the + * current directory within the file browser. + * + * @type Element[] + */ + var currentDirectoryContents = $element.find('.current-directory-contents'); + + /** + * Statically-cached template HTML used to render each file within + * a directory. Once available, this will be used through + * createFileElement() to generate the DOM elements which make up + * a directory listing. + * + * @type String + */ + var fileTemplate = null; + + /** + * Returns whether the given file is a normal file. + * + * @param {ManagedFilesystem.File} file + * The file to test. + * + * @returns {Boolean} + * true if the given file is a normal file, false otherwise. + */ + $scope.isNormalFile = function isNormalFile(file) { + return file.type === ManagedFilesystem.File.Type.NORMAL; + }; + + /** + * Returns whether the given file is a directory. + * + * @param {ManagedFilesystem.File} file + * The file to test. + * + * @returns {Boolean} + * true if the given file is a directory, false otherwise. + */ + $scope.isDirectory = function isDirectory(file) { + return file.type === ManagedFilesystem.File.Type.DIRECTORY; + }; + + /** + * Changes the currently-displayed directory to the given + * directory. + * + * @param {ManagedFilesystem.File} file + * The directory to change to. + */ + $scope.changeDirectory = function changeDirectory(file) { + ManagedFilesystem.changeDirectory($scope.filesystem, file); + }; + + /** + * Initiates a download of the given file. The progress of the + * download can be observed through guacFileTransferManager. + * + * @param {ManagedFilesystem.File} file + * The file to download. + */ + $scope.downloadFile = function downloadFile(file) { + ManagedFilesystem.downloadFile($scope.client, $scope.filesystem, file.streamName); + }; + + /** + * Creates a new element representing the given file and properly + * handling user events, bypassing the overhead incurred through + * use of ngRepeat and related techniques. + * + * Note that this function depends on the availability of the + * statically-cached fileTemplate. + * + * @param {ManagedFilesystem.File} file + * The file to generate an element for. + * + * @returns {Element[]} + * A jQuery-wrapped array containing a single DOM element + * representing the given file. + */ + var createFileElement = function createFileElement(file) { + + // Create from internal template + var element = angular.element($interpolate(fileTemplate)(file)); + + // Change current directory when directories are clicked + if ($scope.isDirectory(file)) { + element.addClass('directory'); + element.on('click', function changeDirectory() { + $scope.changeDirectory(file); + }); + } + + // Initiate downloads when normal files are clicked + else if ($scope.isNormalFile(file)) { + element.addClass('normal-file'); + element.on('click', function downloadFile() { + $scope.downloadFile(file); + }); + } + + return element; + + }; + + /** + * Sorts the given map of files, returning an array of those files + * grouped by file type (directories first, followed by non- + * directories) and sorted lexicographically. + * + * @param {Object.} files + * The map of files to sort. + * + * @returns {ManagedFilesystem.File[]} + * An array of all files in the given map, sorted + * lexicographically with directories first, followed by non- + * directories. + */ + var sortFiles = function sortFiles(files) { + + // Get all given files as an array + var unsortedFiles = []; + for (var name in files) + unsortedFiles.push(files[name]); + + // Sort files - directories first, followed by all other files + // sorted by name + return unsortedFiles.sort(function fileComparator(a, b) { + + // Directories come before non-directories + if ($scope.isDirectory(a) && !$scope.isDirectory(b)) + return -1; + + // Non-directories come after directories + if (!$scope.isDirectory(a) && $scope.isDirectory(b)) + return 1; + + // All other combinations are sorted by name + return a.name.localeCompare(b.name); + + }); + + }; + + // Watch directory contents once file template is available + $templateRequest('app/client/templates/file.html').then(function fileTemplateRetrieved(html) { + + // Store file template statically + fileTemplate = html; + + // Update the contents of the file browser whenever the current directory (or its contents) changes + $scope.$watch('filesystem.currentDirectory.files', function currentDirectoryChanged(files) { + + // Clear current content + currentDirectoryContents.html(''); + + // Display all files within current directory, sorted + angular.forEach(sortFiles(files), function displayFile(file) { + currentDirectoryContents.append(createFileElement(file)); + }); + + }); + + }); // end retrieve file template + + }] + + }; +}]); diff --git a/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js b/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js index d49e94288..48cfaf06a 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js +++ b/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js @@ -44,27 +44,8 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile 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. @@ -112,25 +93,6 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile }; - /** - * 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/file-browser.css b/guacamole/src/main/webapp/app/client/styles/file-browser.css new file mode 100644 index 000000000..73fce34ad --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/file-browser.css @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 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. + */ + +/* Hide directory contents by default */ + +.file-browser .directory > .children { + padding-left: 1em; + display: none; +} + +.file-browser .list-item .caption { + white-space: nowrap; +} + +/* Directory / file icons */ + +.file-browser .normal-file > .caption .icon { + background-image: url('images/file.png'); +} + +.file-browser .directory > .caption .icon { + background-image: url('images/folder-closed.png'); +} + +.file-browser .directory.previous > .caption .icon { + background-image: url('images/folder-up.png'); +} diff --git a/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css b/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css new file mode 100644 index 000000000..52c00d65c --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 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. + */ + +#file-transfer-dialog { + + position: absolute; + right: 0; + bottom: 0; + + font-size: 0.8em; + padding: 0.5em; + + width: 4in; + max-width: 100%; + +} + +#file-transfer-dialog .transfer-manager { + border: 1px solid rgba(0, 0, 0, 0.125); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.125); +} diff --git a/guacamole/src/main/webapp/app/client/styles/filesystem-menu.css b/guacamole/src/main/webapp/app/client/styles/filesystem-menu.css new file mode 100644 index 000000000..1f51b88b9 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/filesystem-menu.css @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015 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. + */ + +#filesystem-menu .header h2 { + font-size: 1em; + font-weight: normal; + padding-top: 0; + padding-bottom: 0; +} + +#filesystem-menu .header { + -ms-flex-align: center; + -moz-box-align: center; + -webkit-box-align: center; + -webkit-align-items: center; + align-items: center; +} + +#filesystem-menu .menu-body { + padding: 0.25em; +} + +#filesystem-menu .header.breadcrumbs { + display: block; + background: rgba(0,0,0,0.0125); + border-bottom: 1px solid rgba(0,0,0,0.05); + box-shadow: none; + margin-top: 0; + border-top: none; +} + +#filesystem-menu .header.breadcrumbs .breadcrumb { + display: inline-block; + padding: 0.5em; + font-size: 0.8em; + font-weight: bold; +} + +#filesystem-menu .header.breadcrumbs .breadcrumb:hover { + background-color: #CDA; + cursor: pointer; +} + +#filesystem-menu .header.breadcrumbs .breadcrumb.root { + background-size: 1.5em 1.5em; + -moz-background-size: 1.5em 1.5em; + -webkit-background-size: 1.5em 1.5em; + -khtml-background-size: 1.5em 1.5em; + background-repeat: no-repeat; + background-position: center center; + background-image: url('images/drive.png'); + width: 2em; + height: 2em; + padding: 0; + vertical-align: middle; +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/styles/guac-menu.css b/guacamole/src/main/webapp/app/client/styles/guac-menu.css new file mode 100644 index 000000000..11df28353 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/guac-menu.css @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2015 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. + */ + +#guac-menu .content { + + padding: 0; + margin: 0; + + /* IE10 */ + display: -ms-flexbox; + -ms-flex-align: stretch; + -ms-flex-direction: column; + + /* Ancient Mozilla */ + display: -moz-box; + -moz-box-align: stretch; + -moz-box-orient: vertical; + + /* Ancient WebKit */ + display: -webkit-box; + -webkit-box-align: stretch; + -webkit-box-orient: vertical; + + /* Old WebKit */ + display: -webkit-flex; + -webkit-align-items: stretch; + -webkit-flex-direction: column; + + /* W3C */ + display: flex; + align-items: stretch; + flex-direction: column; + +} + +#guac-menu .content > * { + + margin: 0; + + -ms-flex: 0 0 auto; + -moz-box-flex: 0; + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + +} + +#guac-menu .content > * + * { + margin-top: 1em; +} + +#guac-menu #clipboard-settings textarea { + width: 100%; + border: 1px solid #AAA; + -moz-border-radius: 0.25em; + -webkit-border-radius: 0.25em; + -khtml-border-radius: 0.25em; + border-radius: 0.25em; + white-space: pre; + display: block; + font-size: 1em; +} + +#guac-menu #mouse-settings .choice { + text-align: center; +} + +#guac-menu #mouse-settings .choice .figure { + display: inline-block; + vertical-align: middle; + width: 75%; + max-width: 320px; +} + +#guac-menu #keyboard-settings .caption { + font-size: 0.9em; + margin-left: 2em; + margin-right: 2em; +} + +#guac-menu #mouse-settings .figure .caption { + text-align: center; + font-size: 0.9em; +} + +#guac-menu #mouse-settings .figure img { + display: block; + width: 100%; + max-width: 320px; + margin: 1em auto; +} + +#guac-menu #keyboard-settings .figure { + float: right; + max-width: 30%; + margin: 1em; +} + +#guac-menu #keyboard-settings .figure img { + max-width: 100%; +} + +#guac-menu #zoom-settings { + text-align: center; +} + +#guac-menu #zoom-out, +#guac-menu #zoom-in, +#guac-menu #zoom-state { + display: inline-block; + vertical-align: middle; +} + +#guac-menu #zoom-out, +#guac-menu #zoom-in { + max-width: 3em; + border: 1px solid rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.1); + border-radius: 2em; + margin: 0.5em; + cursor: pointer; +} + +#guac-menu #zoom-out img, +#guac-menu #zoom-in img { + max-width: 100%; + opacity: 0.5; +} + +#guac-menu #zoom-out:hover, +#guac-menu #zoom-in:hover { + border: 1px solid rgba(0, 0, 0, 1); + background: #CDA; +} + +#guac-menu #zoom-out:hover img, +#guac-menu #zoom-in:hover img { + opacity: 1; +} + +#guac-menu #zoom-state { + font-size: 2em; +} + +#guac-menu #devices .device { + + padding: 1em; + border: 1px solid rgba(0, 0, 0, 0.125); + background: rgba(0, 0, 0, 0.04); + + padding-left: 3.5em; + background-size: 1.5em 1.5em; + -moz-background-size: 1.5em 1.5em; + -webkit-background-size: 1.5em 1.5em; + -khtml-background-size: 1.5em 1.5em; + + background-repeat: no-repeat; + background-position: 1em center; + +} + +#guac-menu #devices .device:hover { + cursor: pointer; + border-color: black; +} + +#guac-menu #devices .device.filesystem { + background-image: url('images/drive.png'); +} diff --git a/guacamole/src/main/webapp/app/client/styles/menu.css b/guacamole/src/main/webapp/app/client/styles/menu.css index 107b397ca..a5bc2c43d 100644 --- a/guacamole/src/main/webapp/app/client/styles/menu.css +++ b/guacamole/src/main/webapp/app/client/styles/menu.css @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2015 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 @@ -20,7 +20,7 @@ * THE SOFTWARE. */ -#menu { +.menu { overflow: hidden; position: absolute; top: 0; @@ -127,38 +127,6 @@ flex: 0 0 auto; } -#menu .content { - - padding: 0; - margin: 0; - - /* IE10 */ - display: -ms-flexbox; - -ms-flex-align: stretch; - -ms-flex-direction: column; - - /* Ancient Mozilla */ - display: -moz-box; - -moz-box-align: stretch; - -moz-box-orient: vertical; - - /* Ancient WebKit */ - display: -webkit-box; - -webkit-box-align: stretch; - -webkit-box-orient: vertical; - - /* Old WebKit */ - display: -webkit-flex; - -webkit-align-items: stretch; - -webkit-flex-direction: column; - - /* W3C */ - display: flex; - align-items: stretch; - flex-direction: column; - -} - .menu-section h3 { margin: 0; padding: 0; @@ -169,122 +137,13 @@ padding-top: 1em; } -#menu .content > * { - - margin: 0; - - -ms-flex: 0 0 auto; - -moz-box-flex: 0; - -webkit-box-flex: 0; - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - -} - -#menu .content > * + * { - margin-top: 1em; -} - -#menu, -#menu.closed { +.menu, +.menu.closed { left: -480px; opacity: 0; } -#menu.open { +.menu.open { left: 0px; opacity: 1; } - -#menu #clipboard-settings textarea { - width: 100%; - border: 1px solid #AAA; - -moz-border-radius: 0.25em; - -webkit-border-radius: 0.25em; - -khtml-border-radius: 0.25em; - border-radius: 0.25em; - white-space: pre; - display: block; - font-size: 1em; -} - -#menu #mouse-settings .choice { - text-align: center; -} - -#menu #mouse-settings .choice .figure { - display: inline-block; - vertical-align: middle; - width: 75%; - max-width: 320px; -} - -#menu #keyboard-settings .caption { - font-size: 0.9em; - margin-left: 2em; - margin-right: 2em; -} - -#menu #mouse-settings .figure .caption { - text-align: center; - font-size: 0.9em; -} - -#menu #mouse-settings .figure img { - display: block; - width: 100%; - max-width: 320px; - margin: 1em auto; -} - -#menu #keyboard-settings .figure { - float: right; - max-width: 30%; - margin: 1em; -} - -#menu #keyboard-settings .figure img { - max-width: 100%; -} - -#menu #zoom-settings { - text-align: center; -} - -#menu #zoom-out, -#menu #zoom-in, -#menu #zoom-state { - display: inline-block; - vertical-align: middle; -} - -#menu #zoom-out, -#menu #zoom-in { - max-width: 3em; - border: 1px solid rgba(0, 0, 0, 0.5); - background: rgba(0, 0, 0, 0.1); - border-radius: 2em; - margin: 0.5em; - cursor: pointer; -} - -#menu #zoom-out img, -#menu #zoom-in img { - max-width: 100%; - opacity: 0.5; -} - -#menu #zoom-out:hover, -#menu #zoom-in:hover { - border: 1px solid rgba(0, 0, 0, 1); - background: #CDA; -} - -#menu #zoom-out:hover img, -#menu #zoom-in:hover img { - opacity: 1; -} - -#menu #zoom-state { - font-size: 2em; -} diff --git a/guacamole/src/main/webapp/app/client/styles/transfer-manager.css b/guacamole/src/main/webapp/app/client/styles/transfer-manager.css index e2b9c5f0d..3d2eb8045 100644 --- a/guacamole/src/main/webapp/app/client/styles/transfer-manager.css +++ b/guacamole/src/main/webapp/app/client/styles/transfer-manager.css @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 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 @@ -20,22 +20,27 @@ * THE SOFTWARE. */ -.transfer-manager .action-buttons { - text-align: center; +.transfer-manager { + background: white; } -.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 .header h2 { + font-size: 1em; + padding-top: 0; + padding-bottom: 0; } -.transfer-manager .transfer { - margin: 1em; +.transfer-manager .header { + margin: 0; + -ms-flex-align: center; + -moz-box-align: center; + -webkit-box-align: center; + -webkit-align-items: center; + align-items: center; +} + +.transfer-manager .transfers { + display: table; + padding: 0.25em; + width: 100%; } diff --git a/guacamole/src/main/webapp/app/client/styles/transfer.css b/guacamole/src/main/webapp/app/client/styles/transfer.css index a927ce083..b3b7c427a 100644 --- a/guacamole/src/main/webapp/app/client/styles/transfer.css +++ b/guacamole/src/main/webapp/app/client/styles/transfer.css @@ -21,25 +21,29 @@ */ .transfer { + display: table-row; +} + +.transfer .transfer-status { + display: table-cell; + padding: 0.25em; position: relative; - padding: 0.5em; - font-size: 0.75em; +} + +.transfer .text { + display: table-cell; + text-align: right; + padding: 0.25em } .transfer .filename { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - width: 100%; - margin-bottom: 0.5em; + position: relative; font-family: monospace; font-weight: bold; -} - -.transfer .text { - position: relative; - text-align: center; - margin-top: 0.5em; + padding: 0.125em; } @keyframes transfer-progress { @@ -55,11 +59,8 @@ .transfer .progress { width: 100%; - background: #C2C2C2; padding: 0.25em; - border: 1px solid gray; - position: absolute; top: 0; left: 0; @@ -70,6 +71,7 @@ .transfer.in-progress .progress { + background-color: #EEE; background-image: url('images/progress.png'); background-size: 16px 16px; @@ -90,6 +92,7 @@ } .transfer .progress .bar { + display: none; background: #A3D655; position: absolute; top: 0; @@ -98,32 +101,35 @@ width: 0; } -.savable.transfer { +.transfer.in-progress .progress .bar { + display: initial; +} + +.transfer.savable { cursor: pointer; } -.savable.transfer:hover .progress { - border-color: black; -} - -.savable.transfer .filename { +.transfer.savable .filename { color: blue; text-decoration: underline; } -.error.transfer { +.transfer.error { background: #FDD; } -.error.transfer .progress { - border-color: rgba(0, 0, 0, 0.125); -} - -.error.transfer .text, -.error.transfer .progress .bar { +.transfer.error .text, +.transfer.error .progress .bar { display: none; } -.error-text { - margin-bottom: 0; +.transfer .error-text { + display: none; +} + +.transfer.error .error-text { + display: block; + margin: 0; + margin-top: 0.5em; + width: 100%; } diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 2a2a0cde0..b2292ed6e 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -52,8 +52,13 @@ + +
+ +
+ - + + + diff --git a/guacamole/src/main/webapp/app/client/templates/file.html b/guacamole/src/main/webapp/app/client/templates/file.html new file mode 100644 index 000000000..2bd86ea34 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/templates/file.html @@ -0,0 +1,30 @@ +
+ + + +
+
+ {{::name}} +
+ +
diff --git a/guacamole/src/main/webapp/app/client/templates/guacFileBrowser.html b/guacamole/src/main/webapp/app/client/templates/guacFileBrowser.html new file mode 100644 index 000000000..196a18dcd --- /dev/null +++ b/guacamole/src/main/webapp/app/client/templates/guacFileBrowser.html @@ -0,0 +1,34 @@ +
+ + + + + + +
+ +
diff --git a/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html b/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html index 2587421ca..8af545bd7 100644 --- a/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html +++ b/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html @@ -21,18 +21,23 @@ THE SOFTWARE. --> - -
{{transfer.filename}}
+ +
+ + +
+
+ {{transfer.filename}} +
+ + +

{{getErrorText() | translate}}

+ +
- -
- - -

{{getErrorText() | translate}}

- diff --git a/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html b/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html index 964294e7c..7de41078a 100644 --- a/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html +++ b/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html @@ -21,23 +21,21 @@ THE SOFTWARE. --> - -

{{'CLIENT.INFO_NO_FILE_TRANSFERS' | translate}}

- - -
- + +
+

{{'CLIENT.SECTION_HEADER_FILE_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 26fe249b1..822a80870 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -31,6 +31,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var ManagedClientState = $injector.get('ManagedClientState'); var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedFileDownload = $injector.get('ManagedFileDownload'); + var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ManagedFileUpload = $injector.get('ManagedFileUpload'); // Required services @@ -119,6 +120,15 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ this.uploads = template.uploads || []; + /** + * All currently-exposed filesystems. When the Guacamole server exposes + * a filesystem object, that object will be made available as a + * ManagedFilesystem within this array. + * + * @type ManagedFilesystem[] + */ + this.filesystems = template.filesystems || []; + /** * The current state of the Guacamole client (idle, connecting, * connected, terminated with error, etc.). @@ -382,6 +392,13 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }); }; + // Handle any received filesystem objects + client.onfilesystem = function fileSystemReceived(object, name) { + $rootScope.$apply(function exposeFilesystem() { + managedClient.filesystems.push(ManagedFilesystem.getInstance(object, name)); + }); + }; + // Manage the client display managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); @@ -421,9 +438,31 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * * @param {File} file * The file to upload. + * + * @param {ManagedFilesystem} [filesystem] + * The filesystem to upload the file to, if any. If not specified, the + * file will be sent as a generic Guacamole file stream. + * + * @param {ManagedFilesystem.File} [directory=filesystem.currentDirectory] + * The directory within the given filesystem to upload the file to. If + * not specified, but a filesystem is given, the current directory of + * that filesystem will be used. */ - ManagedClient.uploadFile = function uploadFile(managedClient, file) { - managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient.client, file)); + ManagedClient.uploadFile = function uploadFile(managedClient, file, filesystem, directory) { + + // Use generic Guacamole file streams by default + var object = null; + var streamName = null; + + // If a filesystem is given, determine the destination object and stream + if (filesystem) { + object = filesystem.object; + streamName = (directory || filesystem.currentDirectory).streamName + '/' + file.name; + } + + // Start and manage file upload + managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient.client, file, object, streamName)); + }; return ManagedClient; diff --git a/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js b/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js index 01a946806..6cd51bba9 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js @@ -124,11 +124,19 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector' * @param {File} file * The file to upload. * + * @param {Object} [object] + * The object to upload the file to, if any, such as a filesystem + * object. + * + * @param {String} [streamName] + * The name of the stream to upload the file to. If an object is given, + * this must be specified. + * * @return {ManagedFileUpload} * A new ManagedFileUpload object which can be used to track the * progress of the upload. */ - ManagedFileUpload.getInstance = function getInstance(client, file) { + ManagedFileUpload.getInstance = function getInstance(client, file, object, streamName) { var managedFileUpload = new ManagedFileUpload(); @@ -137,7 +145,14 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector' reader.onloadend = function fileContentsLoaded() { // Open file for writing - var stream = client.createFileStream(file.type, file.name); + var stream; + if (!object) + stream = client.createFileStream(file.type, file.name); + + // If object/streamName specified, upload to that instead of a file + // stream + else + stream = object.createOutputStream(file.type, streamName); var valid = true; var bytes = new Uint8Array(reader.result); diff --git a/guacamole/src/main/webapp/app/client/types/ManagedFilesystem.js b/guacamole/src/main/webapp/app/client/types/ManagedFilesystem.js new file mode 100644 index 000000000..65205def0 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedFilesystem.js @@ -0,0 +1,334 @@ +/* + * 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 ManagedFilesystem class used by ManagedClient to represent + * available remote filesystems. + */ +angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector', + function defineManagedFilesystem($rootScope, $injector) { + + // Required types + var ManagedFileDownload = $injector.get('ManagedFileDownload'); + var ManagedFileUpload = $injector.get('ManagedFileUpload'); + + /** + * Object which serves as a surrogate interface, encapsulating a Guacamole + * filesystem object while it is active, allowing it to be detached and + * reattached from different client views. + * + * @constructor + * @param {ManagedFilesystem|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedFilesystem. + */ + var ManagedFilesystem = function ManagedFilesystem(template) { + + // Use empty object by default + template = template || {}; + + /** + * The Guacamole filesystem object, as received via a "filesystem" + * instruction. + * + * @type Guacamole.Object + */ + this.object = template.object; + + /** + * The declared, human-readable name of the filesystem + * + * @type String + */ + this.name = template.name; + + /** + * The root directory of the filesystem. + * + * @type ManagedFilesystem.File + */ + this.root = template.root; + + /** + * The current directory being viewed or manipulated within the + * filesystem. + * + * @type ManagedFilesystem.File + */ + this.currentDirectory = template.currentDirectory || template.root; + + }; + + /** + * Refreshes the contents of the given file, if that file is a directory. + * Only the immediate children of the file are refreshed. Files further + * down the directory tree are not refreshed. + * + * @param {ManagedFilesystem} filesystem + * The filesystem associated with the file being refreshed. + * + * @param {ManagedFilesystem.File} file + * The file being refreshed. + */ + ManagedFilesystem.refresh = function updateDirectory(filesystem, file) { + + // Do not attempt to refresh the contents of directories + if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) + return; + + // Request contents of given file + filesystem.object.requestInputStream(file.streamName, function handleStream(stream, mimetype) { + + // Ignore stream if mimetype is wrong + if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) { + stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED); + return; + } + + // Signal server that data is ready to be received + stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS); + + // Read stream as JSON + var reader = new Guacamole.JSONReader(stream); + + // Acknowledge received JSON blobs + reader.onprogress = function onprogress() { + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // Reset contents of directory + reader.onend = function jsonReady() { + $rootScope.$evalAsync(function updateFileContents() { + + // Empty contents + file.files = {}; + + // Determine the expected filename prefix of each stream + var expectedPrefix = file.streamName; + if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/') + expectedPrefix += '/'; + + // For each received stream name + var mimetypes = reader.getJSON(); + for (var name in mimetypes) { + + // Assert prefix is correct + if (name.substring(0, expectedPrefix.length) !== expectedPrefix) + continue; + + // Extract filename from stream name + var filename = name.substring(expectedPrefix.length); + + // Deduce type from mimetype + var type = ManagedFilesystem.File.Type.NORMAL; + if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) + type = ManagedFilesystem.File.Type.DIRECTORY; + + // Add file entry + file.files[filename] = new ManagedFilesystem.File({ + mimetype : mimetypes[name], + streamName : name, + type : type, + parent : file, + name : filename + }); + + } + + }); + }; + + }); + + }; + + /** + * Creates a new ManagedFilesystem instance from the given Guacamole.Object + * and human-readable name. Upon creation, a request to populate the + * contents of the root directory will be automatically dispatched. + * + * @param {Guacamole.Object} object + * The Guacamole.Object defining the filesystem. + * + * @param {String} name + * A human-readable name for the filesystem. + * + * @returns {ManagedFilesystem} + * The newly-created ManagedFilesystem. + */ + ManagedFilesystem.getInstance = function getInstance(object, name) { + + // Init new filesystem object + var managedFilesystem = new ManagedFilesystem({ + object : object, + name : name, + root : new ManagedFilesystem.File({ + mimetype : Guacamole.Object.STREAM_INDEX_MIMETYPE, + streamName : Guacamole.Object.ROOT_STREAM, + type : ManagedFilesystem.File.Type.DIRECTORY + }) + }); + + // Retrieve contents of root + ManagedFilesystem.refresh(managedFilesystem, managedFilesystem.root); + + return managedFilesystem; + + }; + + /** + * Downloads the given file from the server using the given Guacamole + * client and filesystem. The file transfer can be monitored through the + * corresponding entry in the downloads array of the given ManagedClient. + * + * @param {ManagedClient} managedClient + * The ManagedClient from which the file is to be downloaded. + * + * @param {ManagedFilesystem} managedFilesystem + * The ManagedFilesystem from which the file is to be downloaded. Any + * path information provided must be relative to this filesystem. + * + * @param {String} path + * The full, absolute path of the file to download. + */ + ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) { + + // Request download + managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) { + + // Parse filename from string + var filename = path.match(/(.*[\\/])?(.*)/)[2]; + + // Start and track download + managedClient.downloads.push(ManagedFileDownload.getInstance(stream, mimetype, filename)); + + }); + + }; + + /** + * Changes the current directory of the given filesystem, automatically + * refreshing the contents of that directory. + * + * @param {ManagedFilesystem} filesystem + * The filesystem whose current directory should be changed. + * + * @param {ManagedFilesystem.File} file + * The directory to change to. + */ + ManagedFilesystem.changeDirectory = function changeDirectory(filesystem, file) { + + // Refresh contents + ManagedFilesystem.refresh(filesystem, file); + + // Set current directory + filesystem.currentDirectory = file; + + }; + + /** + * A file within a ManagedFilesystem. Each ManagedFilesystem.File provides + * sufficient information for retrieval or replacement of the file's + * contents, as well as the file's name and type. + * + * @param {ManagedFilesystem|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedFilesystem.File. + */ + ManagedFilesystem.File = function File(template) { + + /** + * The mimetype of the data contained within this file. + * + * @type String + */ + this.mimetype = template.mimetype; + + /** + * The name of the stream representing this files contents within its + * associated filesystem object. + * + * @type String + */ + this.streamName = template.streamName; + + /** + * The type of this file. All legal file type strings are defined + * within ManagedFilesystem.File.Type. + * + * @type String + */ + this.type = template.type; + + /** + * The name of this file. + * + * @type String + */ + this.name = template.name; + + /** + * The parent directory of this file. In the case of the root + * directory, this will be null. + * + * @type ManagedFilesystem.File + */ + this.parent = template.parent; + + /** + * Map of all known files containined within this file by name. This is + * only applicable to directories. + * + * @type Object. + */ + this.files = template.files || {}; + + }; + + /** + * All legal type strings for a ManagedFilesystem.File. + * + * @type Object. + */ + ManagedFilesystem.File.Type = { + + /** + * A normal file. As ManagedFilesystem does not currently represent any + * other non-directory types of files, like symbolic links, this type + * string may be used for any non-directory file. + * + * @type String + */ + NORMAL : 'NORMAL', + + /** + * A directory. + * + * @type String + */ + DIRECTORY : 'DIRECTORY' + + }; + + return ManagedFilesystem; + +}]); diff --git a/guacamole/src/main/webapp/app/form/services/formService.js b/guacamole/src/main/webapp/app/form/services/formService.js index e8a1045b3..b598347ab 100644 --- a/guacamole/src/main/webapp/app/form/services/formService.js +++ b/guacamole/src/main/webapp/app/form/services/formService.js @@ -143,60 +143,14 @@ angular.module('form').provider('formService', function formServiceProvider() { this.$get = ['$injector', function formServiceFactory($injector) { // Required services - var $compile = $injector.get('$compile'); - var $http = $injector.get('$http'); - var $q = $injector.get('$q'); - var $templateCache = $injector.get('$templateCache'); + var $compile = $injector.get('$compile'); + var $q = $injector.get('$q'); + var $templateRequest = $injector.get('$templateRequest'); var service = {}; service.fieldTypes = provider.fieldTypes; - /** - * Returns a Promise which resolves with the HTML contents of the - * template at the given URL. The template contents will be retrieved from - * the $templateCache if possible. - * - * @param {String} url - * The URL of the template to retrieve. - * - * @returns {Promise.} - * A Promise which resolves with the HTML contents of the template at - * the given URL. - */ - var templateRequest = function templateRequest(url) { - - // Pull template from cache if present - var template = $templateCache.get(url); - if (template) - return $q.when(template); - - // Defer retrieval of template - var templateContent = $q.defer(); - - // Retrieve template manually - $http({ - method : 'GET', - url : url, - cache : true - }) - - // Upon success, resolve promise and update template cache - .success(function templateRetrieved(html) { - $templateCache.put(url, html); - templateContent.resolve(html); - }) - - // Fail if template cannot be retrieved - .error(function templateError() { - templateContent.reject(); - }); - - // Return promise which will resolve with the retrieved template - return templateContent.promise; - - }; - /** * Compiles and links the field associated with the given name to the given * scope, producing a distinct and independent DOM Element which functions @@ -249,7 +203,7 @@ angular.module('form').provider('formService', function formServiceProvider() { else { // Attempt to retrieve template HTML - templateRequest(fieldType.templateUrl) + $templateRequest(fieldType.templateUrl) // Resolve with compiled HTML upon success .then(function templateRetrieved(html) { diff --git a/guacamole/src/main/webapp/images/drive.png b/guacamole/src/main/webapp/images/drive.png new file mode 100644 index 000000000..916d58e82 Binary files /dev/null and b/guacamole/src/main/webapp/images/drive.png differ diff --git a/guacamole/src/main/webapp/images/file.png b/guacamole/src/main/webapp/images/file.png new file mode 100644 index 000000000..d45e227d3 Binary files /dev/null and b/guacamole/src/main/webapp/images/file.png differ diff --git a/guacamole/src/main/webapp/images/folder-closed.png b/guacamole/src/main/webapp/images/folder-closed.png new file mode 100644 index 000000000..46a918ad5 Binary files /dev/null and b/guacamole/src/main/webapp/images/folder-closed.png differ diff --git a/guacamole/src/main/webapp/images/folder-open.png b/guacamole/src/main/webapp/images/folder-open.png new file mode 100644 index 000000000..f04b0be73 Binary files /dev/null and b/guacamole/src/main/webapp/images/folder-open.png differ diff --git a/guacamole/src/main/webapp/images/folder-up.png b/guacamole/src/main/webapp/images/folder-up.png new file mode 100644 index 000000000..5271b2eb8 Binary files /dev/null and b/guacamole/src/main/webapp/images/folder-up.png differ diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index a243a956a..6b15d2a47 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -41,8 +41,9 @@ "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear Completed Transfers", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear", "ACTION_DISCONNECT" : "Disconnect", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Reconnect", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", @@ -106,6 +107,7 @@ "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", "SECTION_HEADER_CLIPBOARD" : "Clipboard", + "SECTION_HEADER_DEVICES" : "Devices", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_FILE_TRANSFERS" : "File Transfers", "SECTION_HEADER_INPUT_METHOD" : "Input method", diff --git a/guacamole/src/main/webapp/translations/fr.json b/guacamole/src/main/webapp/translations/fr.json index 909514451..e1f5f1908 100644 --- a/guacamole/src/main/webapp/translations/fr.json +++ b/guacamole/src/main/webapp/translations/fr.json @@ -39,8 +39,9 @@ "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vider transferts terminés", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vider", "ACTION_DISCONNECT" : "Déconnecter", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Reconnecter", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", diff --git a/guacamole/src/main/webapp/translations/nl.json b/guacamole/src/main/webapp/translations/nl.json index 3a7ee046a..40021f140 100644 --- a/guacamole/src/main/webapp/translations/nl.json +++ b/guacamole/src/main/webapp/translations/nl.json @@ -41,8 +41,9 @@ "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Wis lijst Voltooide Overdrachten", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Wis lijst", "ACTION_DISCONNECT" : "Verbreek Verbinding", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Verbind Opnieuw", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", diff --git a/guacamole/src/main/webapp/translations/ru.json b/guacamole/src/main/webapp/translations/ru.json index dd3947b83..9d2def73e 100644 --- a/guacamole/src/main/webapp/translations/ru.json +++ b/guacamole/src/main/webapp/translations/ru.json @@ -40,8 +40,9 @@ "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Очистить завершенные загрузки", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Очистить", "ACTION_DISCONNECT" : "Отключиться", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Переподключиться", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE",