Merge pull request #206 from glyptodon/file-browser

GUAC-1172: Implement file browsing UI
This commit is contained in:
James Muehlner
2015-07-05 14:06:09 -07:00
28 changed files with 1286 additions and 324 deletions

View File

@@ -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() {

View File

@@ -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.<String, ManagedFilesystem.File>} 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
}]
};
}]);

View File

@@ -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]);
};
}]
};

View File

@@ -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');
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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;
}

View File

@@ -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%;
}

View File

@@ -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%;
}

View File

@@ -52,8 +52,13 @@
</div>
</div>
<!-- File transfers -->
<div id="file-transfer-dialog" ng-show="hasTransfers()">
<guac-file-transfer-manager client="client"></guac-file-transfer-manager>
</div>
<!-- Menu -->
<div ng-class="{open: menu.shown}" id="menu">
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
<div class="menu-content">
<!-- Stationary header -->
@@ -74,11 +79,13 @@
</div>
</div>
<!-- File transfers -->
<div class="menu-section" id="file-transfers">
<h3 guac-marker="menu.fileTransferMarker">{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}</h3>
<!-- Devices -->
<div class="menu-section" id="devices" ng-show="client.filesystems.length">
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content">
<guac-file-transfer-manager client="client"></guac-file-transfer-manager>
<div class="device filesystem" ng-repeat="filesystem in client.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
</div>
</div>
</div>
@@ -154,4 +161,32 @@
</div>
</div>
<!-- Filesystem menu -->
<div id="filesystem-menu" class="menu" ng-class="{open: isFilesystemMenuShown()}">
<div class="menu-content">
<!-- Stationary header -->
<div class="header">
<h2>{{filesystemMenuContents.name}}</h2>
<button class="upload button" guac-upload="uploadFiles">{{'CLIENT.ACTION_UPLOAD_FILES' | translate}}</button>
<button class="back" ng-click="hideFilesystemMenu()">{{'CLIENT.ACTION_NAVIGATE_BACK' | translate}}</button>
</div>
<!-- Breadcrumbs -->
<div class="header breadcrumbs"><div
class="breadcrumb root"
ng-click="changeDirectory(filesystemMenuContents, filesystemMenuContents.root)"></div><div
class="breadcrumb"
ng-repeat="file in getPath(filesystemMenuContents.currentDirectory)"
ng-click="changeDirectory(filesystemMenuContents, file)">{{file.name}}</div>
</div>
<!-- Scrollable body -->
<div class="menu-body">
<guac-file-browser client="client" filesystem="filesystemMenuContents"></guac-file-browser>
</div>
</div>
</div>
</guac-viewport>

View File

@@ -0,0 +1,30 @@
<div class="list-item">
<!--
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.
-->
<!-- Filename and icon -->
<div class="caption">
<div class="icon"></div>
{{::name}}
</div>
</div>

View File

@@ -0,0 +1,34 @@
<div class="file-browser">
<!--
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.
-->
<!-- Parent directory -->
<div class="list-item directory previous" ng-show="filesystem.currentDirectory.parent">
<div class="caption" ng-click="changeDirectory(filesystem.currentDirectory.parent)">
<div class="icon"></div>..
</div>
</div>
<!-- Current directory contents -->
<div class="current-directory-contents"></div>
</div>

View File

@@ -21,18 +21,23 @@
THE SOFTWARE.
-->
<!-- Filename -->
<div class="filename">{{transfer.filename}}</div>
<!-- Overall status of transfer -->
<div class="transfer-status">
<!-- Filename and progress bar -->
<div class="filename">
<div class="progress"><div ng-style="{'width': getPercentDone() + '%'}" class="bar"></div></div>
{{transfer.filename}}
</div>
<!-- Error text -->
<p class="error-text">{{getErrorText() | translate}}</p>
</div>
<!-- Progress/status text -->
<div class="text"
translate="CLIENT.TEXT_FILE_TRANSFER_PROGRESS"
translate-values="{PROGRESS: getProgressValue(), UNIT: getProgressUnit()}"></div>
<!-- Progress bar -->
<div class="progress"><div ng-style="{'width': getPercentDone() + '%'}" class="bar"></div></div>
<!-- Error text -->
<p class="error-text" ng-show="hasError()">{{getErrorText() | translate}}</p>
</div>

View File

@@ -21,23 +21,21 @@
THE SOFTWARE.
-->
<!-- No transfers currently present -->
<p class="no-transfers" ng-hide="hasTransfers()">{{'CLIENT.INFO_NO_FILE_TRANSFERS' | translate}}</p>
<!-- Sent files -->
<div ng-repeat="upload in client.uploads">
<guac-file-transfer transfer="upload"></guac-file-transfer>
<!-- File transfer manager header -->
<div class="header">
<h2>{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}</h2>
<button ng-click="clearCompletedTransfers()">{{'CLIENT.ACTION_CLEAR_COMPLETED_TRANSFERS' | translate}}</button>
</div>
<!-- Received files -->
<div ng-repeat="download in client.downloads">
<guac-file-transfer transfer="download"></guac-file-transfer>
</div>
<!-- Form buttons -->
<div class="action-buttons">
<a class="upload button" guac-upload="uploadFiles">{{'CLIENT.ACTION_UPLOAD_FILES' | translate}}</a>
<a class="button" ng-click="clearCompletedTransfers()">{{'CLIENT.ACTION_CLEAR_COMPLETED_TRANSFERS' | translate}}</a>
<!-- Sent/received files files -->
<div class="transfers">
<guac-file-transfer
transfer="upload"
ng-repeat="upload in client.uploads">
</guac-file-transfer><guac-file-transfer
transfer="download"
ng-repeat="download in client.downloads">
</guac-file-transfer>
</div>
</div>

View File

@@ -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;

View File

@@ -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);

View File

@@ -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.<String, ManagedFilesystem.File>
*/
this.files = template.files || {};
};
/**
* All legal type strings for a ManagedFilesystem.File.
*
* @type Object.<String, String>
*/
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;
}]);

View File

@@ -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.<String>}
* 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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",