Merge pull request #40 from glyptodon/managed-client

GUAC-963: Maintain connected clients in the background
This commit is contained in:
James Muehlner
2014-12-31 10:51:41 -08:00
25 changed files with 2088 additions and 937 deletions

View File

@@ -27,12 +27,12 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
function clientController($scope, $routeParams, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ScrollState = $injector.get('ScrollState');
var ManagedClientState = $injector.get('ManagedClientState');
var ScrollState = $injector.get('ScrollState');
// Required services
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var $location = $injector.get('$location');
var guacClientManager = $injector.get('guacClientManager');
/**
* The minimum number of pixels a drag gesture must move to result in the
@@ -142,14 +142,23 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
};
/**
* The reconnect action to be provided along with the object sent to
* showStatus.
* Action which returns the user to the home screen.
*/
var NAVIGATE_BACK_ACTION = {
name : "CLIENT.ACTION_NAVIGATE_BACK",
className : "back button",
callback : function navigateBackCallback() {
$location.path('/');
}
};
/**
* Action which replaces the current client with a newly-connected client.
*/
var RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT",
// Handle reconnect action
callback : function reconnectCallback() {
$scope.id = uniqueId;
name : "CLIENT.ACTION_RECONNECT",
callback : function reconnectCallback() {
$scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params);
$scope.showStatus(false);
}
};
@@ -164,12 +173,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
remaining: 15
};
// Client settings and state
$scope.clientProperties = new ClientProperties();
// Initialize clipboard data to an empty string
$scope.clipboardData = "";
// Hide menu by default
$scope.menuShown = false;
@@ -198,27 +201,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* as well as any extra parameters if set.
*/
var uniqueId = $routeParams.type + '/' + $routeParams.id;
$scope.id = uniqueId;
$scope.connectionParameters = $routeParams.params || '';
// Pull connection name from server
switch ($routeParams.type) {
// Connection
case 'c':
connectionService.getConnection($routeParams.id).success(function (connection) {
$scope.connectionName = $scope.page.title = connection.name;
});
break;
// Connection group
case 'g':
connectionGroupService.getConnectionGroup($routeParams.id).success(function (group) {
$scope.connectionName = $scope.page.title = group.name;
});
break;
}
$scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
var keysCurrentlyPressed = {};
@@ -266,9 +249,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
}
// Scroll display if absolute mouse is in use
else if ($scope.clientProperties.emulateAbsoluteMouse) {
$scope.clientProperties.scrollLeft -= deltaX;
$scope.clientProperties.scrollTop -= deltaY;
else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
$scope.client.clientProperties.scrollLeft -= deltaX;
$scope.client.clientProperties.scrollTop -= deltaY;
}
return false;
@@ -305,7 +288,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
// Do not handle pinch gestures while relative mouse is in use
if (!$scope.clientProperties.emulateAbsoluteMouse)
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
return false;
// Stop gesture if not in progress
@@ -316,26 +299,26 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Set initial scale if gesture has just started
if (!initialScale) {
initialScale = $scope.clientProperties.scale;
initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale;
initialScale = $scope.client.clientProperties.scale;
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
}
// Determine new scale absolutely
var currentScale = initialScale * currentLength / startLength;
// Fix scale within limits - scroll will be miscalculated otherwise
currentScale = Math.max(currentScale, $scope.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.clientProperties.maxScale);
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
// Update scale based on pinch distance
$scope.autoFit = false;
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale = currentScale;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale = currentScale;
// Scroll display to keep original pinch location centered within current pinch
$scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
return false;
@@ -354,10 +337,10 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Send clipboard data if menu is hidden
if (!menuShown && menuShownPreviousState)
$scope.$broadcast('guacClipboard', 'text/plain', $scope.clipboardData);
$scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData);
// Disable client keyboard if the menu is shown
$scope.clientProperties.keyboardEnabled = !menuShown;
$scope.client.clientProperties.keyboardEnabled = !menuShown;
});
@@ -385,7 +368,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
keyboard.reset();
// Toggle the menu
$scope.safeApply(function() {
$scope.$apply(function() {
$scope.menuShown = !$scope.menuShown;
});
}
@@ -397,114 +380,129 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
delete keysCurrentlyPressed[keysym];
});
// Show status dialog when client status changes
$scope.$on('guacClientStateChange', function clientStateChangeListener(event, client, status) {
// Update page title when client name is received
$scope.$watch('client.name', function clientNameChanged(name) {
$scope.page.title = name;
});
// Show new status if not yet connected
if (status !== "connected") {
// Show status dialog when connection status changes
$scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) {
// Hide status if no known state
if (!connectionState) {
$scope.showStatus(false);
return;
}
// Get any associated status code
var status = $scope.client.clientState.statusCode;
// Connecting
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
$scope.showStatus({
title: "CLIENT.DIALOG_HEADER_CONNECTING",
text: "CLIENT.TEXT_CLIENT_STATUS_" + status.toUpperCase()
text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
});
}
// Hide status upon connecting
// Client error
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
// Determine translation name of error
var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
$scope.showStatus({
className: "error",
title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text: "CLIENT.ERROR_CLIENT_" + errorName,
countdown: countdown,
actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ]
});
}
// Tunnel error
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
// Determine translation name of error
var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
$scope.showStatus({
className: "error",
title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text: "CLIENT.ERROR_TUNNEL_" + errorName,
countdown: countdown,
actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ]
});
}
// Disconnected
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
$scope.showStatus({
title: "CLIENT.DIALOG_HEADER_DISCONNECTED",
text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(),
actions: [ NAVIGATE_BACK_ACTION, RECONNECT_ACTION ]
});
}
// Hide status for all other states
else
$scope.showStatus(false);
});
// Show status dialog when client errors occur
$scope.$on('guacClientError', function clientErrorListener(event, client, status) {
// Determine translation name of error
var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Override any existing status
$scope.showStatus(false);
// Show error status
$scope.showStatus({
className: "error",
title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text: "CLIENT.ERROR_CLIENT_" + errorName,
countdown: countdown,
actions: [ RECONNECT_ACTION ]
});
});
// Show status dialog when tunnel status changes
$scope.$on('guacTunnelStateChange', function tunnelStateChangeListener(event, tunnel, status) {
// Show new status only if disconnected
if (status === "closed") {
// Disconnect
$scope.id = null;
$scope.showStatus({
title: "CLIENT.DIALOG_HEADER_DISCONNECTED",
text: "CLIENT.TEXT_TUNNEL_STATUS_" + status.toUpperCase()
});
}
});
// Show status dialog when tunnel errors occur
$scope.$on('guacTunnelError', function tunnelErrorListener(event, tunnel, status) {
// Determine translation name of error
var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Override any existing status
$scope.showStatus(false);
// Show error status
$scope.showStatus({
className: "error",
title: "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text: "CLIENT.ERROR_TUNNEL_" + errorName,
countdown: countdown,
actions: [ RECONNECT_ACTION ]
});
});
$scope.formattedScale = function formattedScale() {
return Math.round($scope.clientProperties.scale * 100);
return Math.round($scope.client.clientProperties.scale * 100);
};
$scope.zoomIn = function zoomIn() {
$scope.autoFit = false;
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale += 0.1;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale += 0.1;
};
$scope.zoomOut = function zoomOut() {
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale -= 0.1;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale -= 0.1;
};
$scope.autoFit = true;
$scope.changeAutoFit = function changeAutoFit() {
if ($scope.autoFit && $scope.clientProperties.minScale) {
$scope.clientProperties.autoFit = true;
if ($scope.autoFit && $scope.client.clientProperties.minScale) {
$scope.client.clientProperties.autoFit = true;
} else {
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale = 1;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale = 1;
}
};
$scope.autoFitDisabled = function() {
return $scope.clientProperties.minZoom >= 1;
return $scope.client.clientProperties.minZoom >= 1;
};
/**
* Immediately disconnects the currently-connected client, if any.
*/
$scope.disconnect = function disconnect() {
// Disconnect if client is available
if ($scope.client)
$scope.client.client.disconnect();
// Hide menu
$scope.menuShown = false;
};
/**
@@ -561,159 +559,27 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
};
// Mapping of download stream index to notification object
var downloadNotifications = {};
// Mapping of download stream index to notification ID
var downloadNotificationIDs = {};
$scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) {
$scope.safeApply(function() {
var notification = {
className : 'download',
title : 'CLIENT.DIALOG_TITLE_FILE_TRANSFER',
text : filename
};
downloadNotifications[streamIndex] = notification;
downloadNotificationIDs[streamIndex] = $scope.addNotification(notification);
});
});
// Clean up when view destroyed
$scope.$on('$destroy', function clientViewDestroyed() {
$scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
var notification = downloadNotifications[streamIndex];
if (notification)
notification.progress = getFileProgress('CLIENT.TEXT_FILE_TRANSFER_PROGRESS', length);
});
});
$scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) {
$scope.safeApply(function() {
// Remove client from client manager if no longer connected
var managedClient = $scope.client;
if (managedClient) {
var notification = downloadNotifications[streamIndex];
var notificationID = downloadNotificationIDs[streamIndex];
/**
* Saves the current file.
*/
var saveFile = function saveFile() {
saveAs(blob, filename);
$scope.removeNotification(notificationID);
delete downloadNotifications[streamIndex];
delete downloadNotificationIDs[streamIndex];
};
// Add download action and remove progress indicator
if (notificationID && notification) {
delete notification.progress;
notification.actions = [
{
name : 'CLIENT.ACTION_SAVE_FILE',
callback : saveFile
}
];
}
// Get current connection state
var connectionState = managedClient.clientState.connectionState;
});
});
// If disconnected, remove from management
if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED
|| connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
guacClientManager.removeManagedClient(managedClient.id);
// Mapping of upload stream index to notification object
var uploadNotifications = {};
// Mapping of upload stream index to notification ID
var uploadNotificationIDs = {};
$scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
var notification = {
className : 'upload',
title : 'CLIENT.DIALOG_TITLE_FILE_TRANSFER',
text : filename
};
uploadNotifications[streamIndex] = notification;
uploadNotificationIDs[streamIndex] = $scope.addNotification(notification);
});
});
}
$scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) {
$scope.safeApply(function() {
var notification = uploadNotifications[streamIndex];
if (notification)
notification.progress = getFileProgress('CLIENT.TEXT_FILE_TRANSFER_PROGRESS', offset, length);
});
});
$scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
// Hide any status dialog
$scope.showStatus(false);
var notification = uploadNotifications[streamIndex];
var notificationID = uploadNotificationIDs[streamIndex];
/**
* Close the notification.
*/
var closeNotification = function closeNotification() {
$scope.removeNotification(notificationID);
delete uploadNotifications[streamIndex];
delete uploadNotificationIDs[streamIndex];
};
// Show that the file has uploaded successfully
if (notificationID && notification) {
delete notification.progress;
notification.actions = [
{
name : 'CLIENT.ACTION_ACKNOWLEDGE',
callback : closeNotification
}
];
}
});
});
$scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) {
$scope.safeApply(function() {
var notification = uploadNotifications[streamIndex];
var notificationID = uploadNotificationIDs[streamIndex];
// Determine translation name of error
var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
/**
* Close the notification.
*/
var closeNotification = function closeNotification() {
$scope.removeNotification(notificationID);
delete uploadNotifications[streamIndex];
delete uploadNotificationIDs[streamIndex];
};
// Show that the file upload has failed
if (notificationID && notification) {
delete notification.progress;
notification.actions = [
{
name : 'CLIENT.ACTION_ACKNOWLEDGE',
callback : closeNotification
}
];
notification.text = "CLIENT.ERROR_UPLOAD_" + errorName;
notification.className = "upload error";
}
});
});
}]);

View File

@@ -32,46 +32,22 @@ angular.module('client').directive('guacClient', [function guacClient() {
scope: {
/**
* Parameters for controlling client state.
* The client to display within this guacClient directive.
*
* @type ClientProperties|Object
* @type ManagedClient
*/
clientProperties : '=',
client : '='
/**
* The ID of the Guacamole connection to connect to.
*
* @type String
*/
id : '=',
/**
* Arbitrary URL-encoded parameters to append to the connection
* string when connecting.
*
* @type String
*/
connectionParameters : '='
},
templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
/*
* Safe $apply implementation from Alex Vanston:
* https://coderwall.com/p/ngisma
*/
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase === '$apply' || phase === '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
// Required types
var ManagedClient = $injector.get('ManagedClient');
// Required services
var $window = $injector.get('$window');
/**
* Whether the local, hardware mouse cursor is in use.
*
@@ -146,14 +122,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
*/
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
var $window = $injector.get('$window'),
guacAudio = $injector.get('guacAudio'),
guacVideo = $injector.get('guacVideo'),
guacHistory = $injector.get('guacHistory'),
guacTunnelFactory = $injector.get('guacTunnelFactory'),
guacClientFactory = $injector.get('guacClientFactory'),
authenticationService = $injector.get('authenticationService');
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
@@ -163,60 +131,20 @@ angular.module('client').directive('guacClient', [function guacClient() {
if (!display) return;
// Calculate scale to fit screen
$scope.clientProperties.minScale = Math.min(
$scope.client.clientProperties.minScale = Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
);
// Calculate appropriate maximum zoom level
$scope.clientProperties.maxScale = Math.max($scope.clientProperties.minScale, 3);
$scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
// Clamp zoom level, maintain auto-fit
if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit)
$scope.clientProperties.scale = $scope.clientProperties.minScale;
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
else if (display.getScale() > $scope.clientProperties.maxScale)
$scope.clientProperties.scale = $scope.clientProperties.maxScale;
};
/**
* Returns the string of connection parameters to be passed to the
* Guacamole client during connection. This string generally
* contains the desired connection ID, display resolution, and
* supported audio/video codecs.
*
* @returns {String} The string of connection parameters to be
* passed to the Guacamole client.
*/
var getConnectString = function getConnectString() {
// Calculate optimal width/height for display
var pixel_density = $window.devicePixelRatio || 1;
var optimal_dpi = pixel_density * 96;
var optimal_width = $window.innerWidth * pixel_density;
var optimal_height = $window.innerHeight * pixel_density;
// Build base connect string
var connectString =
"id=" + encodeURIComponent($scope.id)
+ "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&width=" + Math.floor(optimal_width)
+ "&height=" + Math.floor(optimal_height)
+ "&dpi=" + Math.floor(optimal_dpi)
+ ($scope.connectionParameters ? '&' + $scope.connectionParameters : '');
// Add audio mimetypes to connect_string
guacAudio.supported.forEach(function(mimetype) {
connectString += "&audio=" + encodeURIComponent(mimetype);
});
// Add video mimetypes to connect_string
guacVideo.supported.forEach(function(mimetype) {
connectString += "&video=" + encodeURIComponent(mimetype);
});
return connectString;
else if (display.getScale() > $scope.client.clientProperties.maxScale)
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
};
@@ -283,158 +211,60 @@ angular.module('client').directive('guacClient', [function guacClient() {
};
/*
* MOUSE
*/
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Watch for changes to mouse emulation mode
// Send all received mouse events to the client
mouse.onmousedown =
mouse.onmouseup =
mouse.onmousemove = function(mouseState) {
// Remove any existing display
displayContainer.innerHTML = "";
if (!client || !display)
// Only proceed if a client is given
if (!managedClient)
return;
// Send mouse state, show cursor if necessary
display.showCursor(!localCursor);
sendScaledMouseState(mouseState);
// Get Guacamole client instance
client = managedClient.client;
};
// Hide software cursor when mouse leaves display
mouse.onmouseout = function() {
if (!display) return;
display.showCursor(false);
};
/*
* CLIPBOARD
*/
// Update active client if clipboard changes
$scope.$on('guacClipboard', function onClipboard(event, mimetype, data) {
if (client)
client.setClipboard(data);
});
/*
* SCROLLING
*/
$scope.$watch('clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
$scope.clientProperties.scrollLeft = main.scrollLeft;
});
$scope.$watch('clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
main.scrollTop = scrollTop;
$scope.clientProperties.scrollTop = main.scrollTop;
});
/*
* CONNECT / RECONNECT
*/
/**
* Store the thumbnail of the currently connected client within
* the connection history under the given ID. If the client is not
* connected, or if no ID is given, this function has no effect.
*
* @param {String} id
* The ID of the history entry to update.
*/
var updateHistoryEntry = function updateHistoryEntry(id) {
// Update stored thumbnail of previous connection
if (id && display && display.getWidth() > 0 && display.getHeight() > 0) {
// Get screenshot
var canvas = display.flatten();
// Calculate scale of thumbnail (max 320x240, max zoom 100%)
var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1);
// Create thumbnail canvas
var thumbnail = document.createElement("canvas");
thumbnail.width = canvas.width*scale;
thumbnail.height = canvas.height*scale;
// Scale screenshot to thumbnail
var context = thumbnail.getContext("2d");
context.drawImage(canvas,
0, 0, canvas.width, canvas.height,
0, 0, thumbnail.width, thumbnail.height
);
guacHistory.updateThumbnail(id, thumbnail.toDataURL("image/png"));
}
};
// Connect to given ID whenever ID changes
$scope.$watch('id', function(id, previousID) {
// If a client is already attached, ensure it is disconnected
if (client)
client.disconnect();
// Update stored thumbnail of previous connection
updateHistoryEntry(previousID);
// Only proceed if a new client is attached
if (!id)
return;
// Get new client instance
var tunnel = guacTunnelFactory.getInstance($scope);
client = guacClientFactory.getInstance($scope, tunnel);
// Init display
// Attach possibly new display
display = client.getDisplay();
display.scale($scope.clientProperties.scale);
// Update the scale of the display when the client display size changes.
display.onresize = function() {
$scope.safeApply(updateDisplayScale);
};
// Use local cursor if possible, update localCursor flag
display.oncursor = function(canvas, x, y) {
localCursor = mouse.setCursor(canvas, x, y);
};
display.scale($scope.client.clientProperties.scale);
// Add display element
displayElement = display.getElement();
displayContainer.innerHTML = "";
displayContainer.appendChild(displayElement);
// Do nothing when the display element is clicked on.
displayElement.onclick = function(e) {
// Do nothing when the display element is clicked on
display.getElement().onclick = function(e) {
e.preventDefault();
return false;
};
// Connect
client.connect(getConnectString());
});
// Clean up when client directive is destroyed
$scope.$on('$destroy', function destroyClient() {
// Update stored thumbnail of current connection
updateHistoryEntry($scope.id);
// Update actual view scrollLeft when scroll properties change
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
});
/*
* MOUSE EMULATION
*/
// Watch for changes to mouse emulation mode
$scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
// Update actual view scrollTop when scroll properties change
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
main.scrollTop = scrollTop;
$scope.client.clientProperties.scrollTop = main.scrollTop;
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize() {
$scope.$evalAsync(updateDisplayScale);
});
// Keep local cursor up-to-date
$scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) {
if (cursor)
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
});
// Swap mouse emulation modes depending on absolute mode flag
$scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
if (!client || !display) return;
@@ -478,19 +308,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
});
/*
* DISPLAY SCALE / SIZE
*/
// Adjust scale if modified externally
$scope.$watch('clientProperties.scale', function changeScale(scale) {
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
// Fix scale within limits
scale = Math.max(scale, $scope.clientProperties.minScale);
scale = Math.min(scale, $scope.clientProperties.maxScale);
scale = Math.max(scale, $scope.client.clientProperties.minScale);
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
// If at minimum zoom level, hide scroll bars
if (scale === $scope.clientProperties.minScale)
if (scale === $scope.client.clientProperties.minScale)
main.style.overflow = "hidden";
// If not at minimum zoom level, show scroll bars
@@ -501,15 +327,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
if (display)
display.scale(scale);
if (scale !== $scope.clientProperties.scale)
$scope.clientProperties.scale = scale;
if (scale !== $scope.client.clientProperties.scale)
$scope.client.clientProperties.scale = scale;
});
// If autofit is set, the scale should be set to the minimum scale, filling the screen
$scope.$watch('clientProperties.autoFit', function changeAutoFit(autoFit) {
$scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
if(autoFit)
$scope.clientProperties.scale = $scope.clientProperties.minScale;
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
});
// If the element is resized, attempt to resize client
@@ -527,25 +353,48 @@ angular.module('client').directive('guacClient', [function guacClient() {
}
$scope.safeApply(updateDisplayScale);
$scope.$apply(updateDisplayScale);
});
/*
* KEYBOARD
*/
// Listen for broadcasted keydown events and fire the appropriate listeners
// Watch for changes to mouse emulation mode
// Send all received mouse events to the client
mouse.onmousedown =
mouse.onmouseup =
mouse.onmousemove = function(mouseState) {
if (!client || !display)
return;
// Send mouse state, show cursor if necessary
display.showCursor(!localCursor);
sendScaledMouseState(mouseState);
};
// Hide software cursor when mouse leaves display
mouse.onmouseout = function() {
if (!display) return;
display.showCursor(false);
};
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function onClipboard(event, mimetype, data) {
if (client)
client.setClipboard(data);
});
// Translate local keydown events to remote keydown events if keyboard is enabled
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(1, keysym);
event.preventDefault();
}
});
// Listen for broadcasted keyup events and fire the appropriate listeners
// Translate local keyup events to remote keyup events if keyboard is enabled
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(0, keysym);
event.preventDefault();
}
@@ -561,26 +410,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
client.sendKeyEvent(0, keysym);
});
/**
* Converts the given bytes to a base64-encoded string.
*
* @param {Uint8Array} bytes A Uint8Array which contains the data to be
* encoded as base64.
* @return {String} The base64-encoded string.
*/
function getBase64(bytes) {
var data = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
data += String.fromCharCode(bytes[i]);
// Convert to base64
return $window.btoa(data);
}
/**
* Ignores the given event.
*
@@ -590,68 +419,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
e.preventDefault();
e.stopPropagation();
}
/**
* Uploads the given file to the server.
*
* @param {File} file The file to upload.
*/
function uploadFile(file) {
// Construct reader for file
var reader = new FileReader();
reader.onloadend = function() {
// Open file for writing
var stream = client.createFileStream(file.type, file.name);
var valid = true;
var bytes = new Uint8Array(reader.result);
var offset = 0;
// Add upload notification
$scope.$emit('guacClientFileUploadStart', client, stream.index, file.type, file.name, bytes.length);
// Invalidate stream on all errors
// Continue upload when acknowledged
stream.onack = function(status) {
// Handle errors
if (status.isError()) {
valid = false;
$scope.$emit('guacClientFileUploadError', client, stream.index, file.type, file.name, bytes.length, status.code);
}
// Abort upload if stream is invalid
if (!valid) return false;
// Encode packet as base64
var slice = bytes.subarray(offset, offset+4096);
var base64 = getBase64(slice);
// Write packet
stream.sendBlob(base64);
// Advance to next packet
offset += 4096;
// If at end, stop upload
if (offset >= bytes.length) {
stream.sendEnd();
$scope.$emit('guacClientFileUploadProgress', client, stream.index, file.type, file.name, bytes.length, bytes.length);
$scope.$emit('guacClientFileUploadEnd', client, stream.index, file.type, file.name, bytes.length);
}
// Otherwise, update progress
else
$scope.$emit('guacClientFileUploadProgress', client, stream.index, file.type, file.name, bytes.length, offset);
};
};
reader.readAsArrayBuffer(file);
}
// Handle and ignore dragenter/dragover
displayContainer.addEventListener("dragenter", ignoreEvent, false);
@@ -664,12 +431,13 @@ angular.module('client').directive('guacClient', [function guacClient() {
e.stopPropagation();
// Ignore file drops if no attached client
if (!client) return;
if (!$scope.client)
return;
// Upload each file
var files = e.dataTransfer.files;
for (var i=0; i<files.length; i++)
uploadFile(files[i]);
ManagedClient.uploadFile($scope.client, files[i]);
}, false);

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive for displaying a Guacamole client as a non-interactive
* thumbnail.
*/
angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The client to display within this guacThumbnail directive.
*
* @type ManagedClient
*/
client : '='
},
templateUrl: 'app/client/templates/guacThumbnail.html',
controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) {
// Required services
var $window = $injector.get('$window');
/**
* The optimal thumbnail width, in pixels.
*
* @type Number
*/
var THUMBNAIL_WIDTH = 320;
/**
* The optimal thumbnail height, in pixels.
*
* @type Number
*/
var THUMBNAIL_HEIGHT = 240;
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
var display = null;
/**
* The element associated with the display of the current
* Guacamole client instance.
*
* @type Element
*/
var displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
var displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
var main = $element[0];
/**
* The element which functions as a detector for size changes.
*
* @type Element
*/
var resizeSensor = $element.find('.resize-sensor')[0];
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
var updateDisplayScale = function updateDisplayScale() {
if (!display) return;
// Fit within available area
display.scale(Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
));
};
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Remove any existing display
displayContainer.innerHTML = "";
// Only proceed if a client is given
if (!managedClient)
return;
// Get Guacamole client instance
var client = managedClient.client;
// Attach possibly new display
display = client.getDisplay();
// Add display element
displayElement = display.getElement();
displayContainer.appendChild(displayElement);
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) {
var width;
var height;
// If no display size yet, assume optimal thumbnail size
if (!size || size.width === 0 || size.height === 0) {
width = THUMBNAIL_WIDTH;
height = THUMBNAIL_HEIGHT;
}
// Otherwise, generate size that fits within thumbnail bounds
else {
var scale = Math.min(THUMBNAIL_WIDTH / size.width, THUMBNAIL_HEIGHT / size.height, 1);
width = size.width * scale;
height = size.height * scale;
}
// Generate dummy background image
var thumbnail = document.createElement("canvas");
thumbnail.width = width;
thumbnail.height = height;
$scope.thumbnail = thumbnail.toDataURL("image/png");
$scope.$evalAsync(updateDisplayScale);
});
// If the element is resized, attempt to resize client
resizeSensor.contentWindow.addEventListener('resize', function mainElementResized() {
$scope.$apply(updateDisplayScale);
});
}]
};
}]);

View File

@@ -28,11 +28,11 @@ angular.module('client').directive('guacViewport', [function guacViewport() {
return {
// Element only
restrict: 'E',
scope: false,
scope: {},
transclude: true,
templateUrl: 'app/client/templates/guacViewport.html',
controller: ['$window', '$document', '$element',
function guacViewportController($window, $document, $element) {
controller: ['$scope', '$window', '$document', '$element',
function guacViewportController($scope, $window, $document, $element) {
/**
* The fullscreen container element.
@@ -55,8 +55,12 @@ angular.module('client').directive('guacViewport', [function guacViewport() {
*/
var currentAdjustedHeight = null;
// Fit container within visible region when window scrolls
$window.onscroll = function fitScrollArea() {
/**
* Resizes the container element inside the guacViewport such that
* it exactly fits within the visible area, even if the browser has
* been scrolled.
*/
var fitVisibleArea = function fitVisibleArea() {
// Pull scroll properties
var scrollLeft = document.body.scrollLeft;
@@ -82,6 +86,14 @@ angular.module('client').directive('guacViewport', [function guacViewport() {
};
// Fit container within visible region when window scrolls
$window.addEventListener('scroll', fitVisibleArea);
// Clean up event listener on destroy
$scope.$on('$destroy', function destroyViewport() {
$window.removeEventListener('scroll', fitVisibleArea);
});
}]
};
}]);

View File

@@ -1,176 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A service for creating Guacamole clients.
*/
angular.module('client').factory('guacClientFactory', ['$rootScope',
function guacClientFactory($rootScope) {
var service = {};
/**
* Returns a new Guacamole client instance which connects using the
* provided tunnel.
*
* @param {Scope} $scope The current scope.
* @param {Guacamole.Tunnel} tunnel The tunnel to connect through.
* @returns {Guacamole.Client} A new Guacamole client instance.
*/
service.getInstance = function getClientInstance($scope, tunnel) {
// Instantiate client
var guacClient = new Guacamole.Client(tunnel);
/*
* Fire guacClientStateChange events when client state changes.
*/
guacClient.onstatechange = function onClientStateChange(clientState) {
$scope.safeApply(function() {
switch (clientState) {
// Idle
case 0:
$scope.$emit('guacClientStateChange', guacClient, "idle");
break;
// Connecting
case 1:
$scope.$emit('guacClientStateChange', guacClient, "connecting");
break;
// Connected + waiting
case 2:
$scope.$emit('guacClientStateChange', guacClient, "waiting");
break;
// Connected
case 3:
$scope.$emit('guacClientStateChange', guacClient, "connected");
break;
// Disconnecting / disconnected are handled by tunnel instead
case 4:
case 5:
break;
}
});
};
/*
* Fire guacClientName events when a new name is received.
*/
guacClient.onname = function onClientName(name) {
$scope.safeApply(function() {
$scope.$emit('guacClientName', guacClient, name);
});
};
/*
* Disconnect and fire guacClientError when the client receives an
* error.
*/
guacClient.onerror = function onClientError(status) {
$scope.safeApply(function() {
// Disconnect, if connected
guacClient.disconnect();
$scope.$emit('guacClientError', guacClient, status.code);
});
};
/*
* Fire guacClientClipboard events after new clipboard data is received.
*/
guacClient.onclipboard = function onClientClipboard(stream, mimetype) {
$scope.safeApply(function() {
// Only text/plain is supported for now
if (mimetype !== "text/plain") {
stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED);
return;
}
var reader = new Guacamole.StringReader(stream);
var data = "";
// Append any received data to buffer
reader.ontext = function clipboard_text_received(text) {
data += text;
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Emit event when done
reader.onend = function clipboard_text_end() {
$scope.$emit('guacClientClipboard', guacClient, mimetype, data);
};
});
};
/*
* Fire guacFileStart, guacFileProgress, and guacFileEnd events during
* the receipt of files.
*/
guacClient.onfile = function onClientFile(stream, mimetype, filename) {
$scope.safeApply(function() {
// Begin file download
var guacFileStartEvent = $scope.$emit('guacClientFileDownloadStart', guacClient, stream.index, mimetype, filename);
if (!guacFileStartEvent.defaultPrevented) {
var blob_reader = new Guacamole.BlobReader(stream, mimetype);
// Update progress as data is received
blob_reader.onprogress = function onprogress() {
$scope.$emit('guacClientFileDownloadProgress', guacClient, stream.index, mimetype, filename, blob_reader.getLength());
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// When complete, prompt for download
blob_reader.onend = function onend() {
$scope.$emit('guacClientFileDownloadEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob());
};
stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS);
}
// Respond with UNSUPPORTED if download (default action) canceled within event handler
else
stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED);
});
};
return guacClient;
};
return service;
}]);

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A service for managing several active Guacamole clients.
*/
angular.module('client').factory('guacClientManager', ['ManagedClient',
function guacClientManager(ManagedClient) {
var service = {};
/**
* Map of all active managed clients. Each key is the ID of the connection
* used by that client.
*
* @type Object.<String, ManagedClient>
*/
service.managedClients = {};
/**
* Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this
* function has no effect.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be removed.
*
* @returns {Boolean}
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function replaceManagedClient(id) {
// Remove client if it exists
if (id in service.managedClients) {
// Disconnect and remove
service.managedClients[id].client.disconnect();
delete service.managedClients[id];
// A client was removed
return true;
}
// No client was removed
return false;
};
/**
* Creates a new ManagedClient associated with the connection having the
* given ID. If such a ManagedClient already exists, it is disconnected and
* replaced.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) {
// Disconnect any existing client
service.removeManagedClient(id);
// Set new client
return service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
};
/**
* Returns the ManagedClient associated with the connection having the
* given ID. If no such ManagedClient exists, a new ManagedClient is
* created.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.getManagedClient = function getManagedClient(id, connectionParameters) {
// Create new managed client if it doesn't already exist
if (!(id in service.managedClients))
service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
// Return existing client
return service.managedClients[id];
};
return service;
}]);

View File

@@ -1,89 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A service for creating Guacamole tunnels.
*/
angular.module('client').factory('guacTunnelFactory', ['$rootScope', '$window',
function guacTunnelFactory($rootScope, $window) {
var service = {};
/**
* Returns a new Guacamole tunnel instance, using an implementation that is
* supported by the web browser.
*
* @param {Scope} $scope The current scope.
* @returns {Guacamole.Tunnel} A new Guacamole tunnel instance.
*/
service.getInstance = function getTunnelInstance($scope) {
var tunnel;
// If WebSocket available, try to use it.
if ($window.WebSocket)
tunnel = new Guacamole.ChainedTunnel(
new Guacamole.WebSocketTunnel('websocket-tunnel'),
new Guacamole.HTTPTunnel('tunnel')
);
// If no WebSocket, then use HTTP.
else
tunnel = new Guacamole.HTTPTunnel('tunnel');
// Fire events for tunnel errors
tunnel.onerror = function onTunnelError(status) {
$scope.safeApply(function() {
$scope.$emit('guacTunnelError', tunnel, status.code);
});
};
// Fire events for tunnel state changes
tunnel.onstatechange = function onTunnelStateChange(state) {
$scope.safeApply(function() {
switch (state) {
case Guacamole.Tunnel.State.CONNECTING:
$scope.$emit('guacTunnelStateChange', tunnel, 'connecting');
break;
case Guacamole.Tunnel.State.OPEN:
$scope.$emit('guacTunnelStateChange', tunnel, 'open');
break;
case Guacamole.Tunnel.State.CLOSED:
$scope.$emit('guacTunnelStateChange', tunnel, 'closed');
break;
}
});
};
return tunnel;
};
return service;
}]);

View File

@@ -37,8 +37,12 @@
transition: left 0.125s, opacity 0.125s;
}
#menu h3 {
margin: 1em;
}
#menu .content {
padding: 1em;
margin: 1em;
}
#menu .content > * {
@@ -65,6 +69,7 @@
border-radius: 0.25em;
white-space: pre;
display: block;
font-size: 1em;
}
#menu #mouse-settings .choice {
@@ -94,11 +99,6 @@
margin: 1em auto;
}
#menu h2 {
padding: 0.25em 0.5em;
font-size: 1em;
}
#menu #keyboard-settings .figure {
float: right;
max-width: 30%;

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2013 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.
*/
div.thumbnail-main {
overflow: hidden;
width: 100%;
height: 100%;
position: relative;
font-size: 0px;
}
.thumbnail-main img {
max-width: 100%;
}
.thumbnail-main .display {
position: absolute;
pointer-events: none;
}

View File

@@ -28,10 +28,7 @@
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<!-- Client -->
<guac-client
client-properties="clientProperties"
id="id"
connection-parameters="connectionParameters"/></guac-client>
<guac-client client="client"/></guac-client>
</div>
@@ -57,13 +54,20 @@
<!-- Menu -->
<div ng-class="{open: menuShown}" id="menu" guac-touch-drag="menuDrag" guac-scroll="menuScrollState">
<h2>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h2>
<div class="logout-panel">
<a class="back button" href="#/">{{'CLIENT.ACTION_NAVIGATE_BACK' | translate}}</a>
<a class="disconnect danger button" ng-click="disconnect()">{{'CLIENT.ACTION_DISCONNECT' | translate}}</a>
</div>
<h2>{{client.name}}</h2>
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content" id="clipboard-settings">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<textarea ng-model="clipboardData" rows="10" cols="40" id="clipboard"></textarea>
<textarea ng-model="client.clipboardData" rows="10" cols="40" id="clipboard"></textarea>
</div>
<h2>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h2>
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
<div class="content" id="keyboard-settings">
<!-- No IME -->
@@ -87,13 +91,13 @@
</div>
<h2>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h2>
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content" id="mouse-settings">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
@@ -102,7 +106,7 @@
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
@@ -111,7 +115,7 @@
</div>
<h2>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h2>
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>

View File

@@ -0,0 +1,34 @@
<div class="thumbnail-main">
<!--
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.
-->
<!-- Resize sensor -->
<iframe class="resize-sensor" src="app/client/templates/blank.html"></iframe>
<!-- Display -->
<div class="display">
</div>
<!-- Dummy background thumbnail -->
<img alt="" ng-src="{{thumbnail}}"/>
</div>

View File

@@ -0,0 +1,432 @@
/*
* 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 ManagedClient class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
function defineManagedClient($rootScope, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFileDownload = $injector.get('ManagedFileDownload');
var ManagedFileUpload = $injector.get('ManagedFileUpload');
// Required services
var $window = $injector.get('$window');
var $document = $injector.get('$document');
var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory');
var guacVideo = $injector.get('guacVideo');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* client while it is active, allowing it to be detached and reattached
* from different client views.
*
* @constructor
* @param {ManagedClient|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClient.
*/
var ManagedClient = function ManagedClient(template) {
// Use empty object by default
template = template || {};
/**
* The ID of the connection associated with this client.
*
* @type String
*/
this.id = template.id;
/**
* The actual underlying Guacamole client.
*
* @type Guacamole.Client
*/
this.client = template.client;
/**
* The tunnel being used by the underlying Guacamole client.
*
* @type Guacamole.Tunnel
*/
this.tunnel = template.tunnel;
/**
* The display associated with the underlying Guacamole client.
*
* @type ManagedDisplay
*/
this.managedDisplay = template.managedDisplay;
/**
* The name returned associated with the connection or connection
* group in use.
*
* @type String
*/
this.name = template.name;
/**
* The current clipboard contents.
*
* @type String
*/
this.clipboardData = template.clipboardData;
/**
* All downloaded files. As files are downloaded, their progress can be
* observed through the elements of this array. It is intended that
* this array be manipulated externally as needed.
*
* @type ManagedFileDownload[]
*/
this.downloads = template.downloads || [];
/**
* All uploaded files. As files are uploaded, their progress can be
* observed through the elements of this array. It is intended that
* this array be manipulated externally as needed.
*
* @type ManagedFileUpload[]
*/
this.uploads = template.uploads || [];
/**
* The current state of the Guacamole client (idle, connecting,
* connected, terminated with error, etc.).
*
* @type ManagedClientState
*/
this.clientState = template.clientState || new ManagedClientState();
/**
* Properties associated with the display and behavior of the Guacamole
* client.
*
* @type ClientProperties
*/
this.clientProperties = template.clientProperties || new ClientProperties();
};
/**
* Returns the string of connection parameters to be passed to the
* Guacamole client during connection. This string generally contains the
* desired connection ID, display resolution, and supported audio/video
* codecs.
*
* @param {String} id
* The ID of the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {String}
* The string of connection parameters to be passed to the Guacamole
* client.
*/
var getConnectString = function getConnectString(id, connectionParameters) {
// Calculate optimal width/height for display
var pixel_density = $window.devicePixelRatio || 1;
var optimal_dpi = pixel_density * 96;
var optimal_width = $window.innerWidth * pixel_density;
var optimal_height = $window.innerHeight * pixel_density;
// Build base connect string
var connectString =
"id=" + encodeURIComponent(id)
+ "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&width=" + Math.floor(optimal_width)
+ "&height=" + Math.floor(optimal_height)
+ "&dpi=" + Math.floor(optimal_dpi)
+ (connectionParameters ? '&' + connectionParameters : '');
// Add audio mimetypes to connect_string
guacAudio.supported.forEach(function(mimetype) {
connectString += "&audio=" + encodeURIComponent(mimetype);
});
// Add video mimetypes to connect_string
guacVideo.supported.forEach(function(mimetype) {
connectString += "&video=" + encodeURIComponent(mimetype);
});
return connectString;
};
/**
* Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this
* function has no effect.
*
* @param {String} managedClient
* The client whose history entry should be updated.
*/
var updateHistoryEntry = function updateHistoryEntry(managedClient) {
var display = managedClient.client.getDisplay();
// Update stored thumbnail of previous connection
if (display && display.getWidth() > 0 && display.getHeight() > 0) {
// Get screenshot
var canvas = display.flatten();
// Calculate scale of thumbnail (max 320x240, max zoom 100%)
var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1);
// Create thumbnail canvas
var thumbnail = $document[0].createElement("canvas");
thumbnail.width = canvas.width*scale;
thumbnail.height = canvas.height*scale;
// Scale screenshot to thumbnail
var context = thumbnail.getContext("2d");
context.drawImage(canvas,
0, 0, canvas.width, canvas.height,
0, 0, thumbnail.width, thumbnail.height
);
guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png"));
}
};
/**
* Creates a new ManagedClient, connecting it to the specified connection
* or group.
*
* @param {String} id
* The ID of the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {ManagedClient}
* A new ManagedClient instance which is connected to the connection or
* connection group having the given ID.
*/
ManagedClient.getInstance = function getInstance(id, connectionParameters) {
var tunnel;
// If WebSocket available, try to use it.
if ($window.WebSocket)
tunnel = new Guacamole.ChainedTunnel(
new Guacamole.WebSocketTunnel('websocket-tunnel'),
new Guacamole.HTTPTunnel('tunnel')
);
// If no WebSocket, then use HTTP.
else
tunnel = new Guacamole.HTTPTunnel('tunnel');
// Get new client instance
var client = new Guacamole.Client(tunnel);
// Associate new managed client with new client and tunnel
var managedClient = new ManagedClient({
id : id,
client : client,
tunnel : tunnel
});
// Fire events for tunnel errors
tunnel.onerror = function tunnelError(status) {
$rootScope.$apply(function handleTunnelError() {
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.TUNNEL_ERROR,
status.code);
});
};
// Update connection state as tunnel state changes
tunnel.onstatechange = function tunnelStateChanged(state) {
$rootScope.$evalAsync(function updateTunnelState() {
switch (state) {
// Connection is being established
case Guacamole.Tunnel.State.CONNECTING:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connection has closed
case Guacamole.Tunnel.State.CLOSED:
updateHistoryEntry(managedClient);
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.DISCONNECTED);
break;
}
});
};
// Update connection state as client state changes
client.onstatechange = function clientStateChanged(clientState) {
$rootScope.$evalAsync(function updateClientState() {
switch (clientState) {
// Idle
case 0:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.IDLE);
break;
// Connected + waiting
case 2:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.WAITING);
break;
// Connected
case 3:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED);
break;
// Connecting, disconnecting, and disconnected are all
// either ignored or handled by tunnel state
case 1: // Connecting
case 4: // Disconnecting
case 5: // Disconnected
break;
}
});
};
// Disconnect and update status when the client receives an error
client.onerror = function clientError(status) {
$rootScope.$apply(function handleClientError() {
// Disconnect, if connected
client.disconnect();
// Update state
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CLIENT_ERROR,
status.code);
});
};
// Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(stream, mimetype) {
// Only text/plain is supported for now
if (mimetype !== "text/plain") {
stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED);
return;
}
var reader = new Guacamole.StringReader(stream);
var data = "";
// Append any received data to buffer
reader.ontext = function clipboard_text_received(text) {
data += text;
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Update state when done
reader.onend = function clipboard_text_end() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = data;
});
};
};
// Handle any received files
client.onfile = function clientFileReceived(stream, mimetype, filename) {
$rootScope.$apply(function startDownload() {
managedClient.downloads.push(ManagedFileDownload.getInstance(stream, mimetype, filename));
});
};
// Manage the client display
managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
// Connect the Guacamole client
client.connect(getConnectString(id, connectionParameters));
// Determine type of connection
var typePrefix = id.substring(0, 2);
// If using a connection, pull connection name
if (typePrefix === 'c/') {
connectionService.getConnection(id.substring(2))
.success(function connectionRetrieved(connection) {
managedClient.name = connection.name;
});
}
// If using a connection group, pull connection name
else if (typePrefix === 'g/') {
connectionGroupService.getConnectionGroup(id.substring(2))
.success(function connectionGroupRetrieved(group) {
managedClient.name = group.name;
});
}
return managedClient;
};
/**
* Uploads the given file to the server through the given Guacamole client.
* The file transfer can be monitored through the corresponding entry in
* the uploads array of the given managedClient.
*
* @param {ManagedClient} managedClient
* The ManagedClient through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*/
ManagedClient.uploadFile = function uploadFile(managedClient, file) {
managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient.client, file));
};
return ManagedClient;
}]);

View File

@@ -0,0 +1,159 @@
/*
* 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 ManagedClient class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClientState', [function defineManagedClientState() {
/**
* Object which represents the state of a Guacamole client and its tunnel,
* including any error conditions.
*
* @constructor
* @param {ManagedClientState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientState.
*/
var ManagedClientState = function ManagedClientState(template) {
// Use empty object by default
template = template || {};
/**
* The current connection state. Valid values are described by
* ManagedClientState.ConnectionState.
*
* @type String
* @default ManagedClientState.ConnectionState.IDLE
*/
this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE;
/**
* The status code of the current error condition, if connectionState
* is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState
* values, this will be @link{Guacamole.Status.Code.SUCCESS}.
*
* @type Number
* @default Guacamole.Status.Code.SUCCESS
*/
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
};
/**
* Valid connection state strings. Each state string is associated with a
* specific state of a Guacamole connection.
*/
ManagedClientState.ConnectionState = {
/**
* The Guacamole connection has not yet been attempted.
*
* @type String
*/
IDLE : "IDLE",
/**
* The Guacamole connection is being established.
*
* @type String
*/
CONNECTING : "CONNECTING",
/**
* The Guacamole connection has been successfully established, and the
* client is now waiting for receipt of initial graphical data.
*
* @type String
*/
WAITING : "WAITING",
/**
* The Guacamole connection has been successfully established, and
* initial graphical data has been received.
*
* @type String
*/
CONNECTED : "CONNECTED",
/**
* The Guacamole connection has terminated successfully. No errors are
* indicated.
*
* @type String
*/
DISCONNECTED : "DISCONNECTED",
/**
* The Guacamole connection has terminated due to an error reported by
* the client. The associated error code is stored in statusCode.
*
* @type String
*/
CLIENT_ERROR : "CLIENT_ERROR",
/**
* The Guacamole connection has terminated due to an error reported by
* the tunnel. The associated error code is stored in statusCode.
*
* @type String
*/
TUNNEL_ERROR : "TUNNEL_ERROR"
};
/**
* Sets the current client state and, if given, the associated status code.
* If an error is already represented, this function has no effect.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
*
* @param {String} connectionState
* The connection state to assign to the given ManagedClientState, as
* listed within ManagedClientState.ConnectionState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedClientState, if any,
* as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedClientState is not touched.
*/
ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) {
// Do not set state after an error is registered
if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
return;
// Update connection state
clientState.connectionState = connectionState;
// Set status code, if given
if (statusCode)
clientState.statusCode = statusCode;
};
return ManagedClientState;
}]);

View File

@@ -0,0 +1,177 @@
/*
* 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 ManagedDisplay class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedDisplay', ['$rootScope',
function defineManagedDisplay($rootScope) {
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* display while it is active, allowing it to be detached and reattached
* from different client views.
*
* @constructor
* @param {ManagedDisplay|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedDisplay.
*/
var ManagedDisplay = function ManagedDisplay(template) {
// Use empty object by default
template = template || {};
/**
* The underlying Guacamole display.
*
* @type Guacamole.Display
*/
this.display = template.display;
/**
* The current size of the Guacamole display.
*
* @type ManagedDisplay.Dimensions
*/
this.size = new ManagedDisplay.Dimensions(template.size);
/**
* The current mouse cursor, if any.
*
* @type ManagedDisplay.Cursor
*/
this.cursor = template.cursor;
};
/**
* Object which represents the size of the Guacamole display.
*
* @constructor
* @param {ManagedDisplay.Dimensions|Object} template
* The object whose properties should be copied within the new
* ManagedDisplay.Dimensions.
*/
ManagedDisplay.Dimensions = function Dimensions(template) {
// Use empty object by default
template = template || {};
/**
* The current width of the Guacamole display, in pixels.
*
* @type Number
*/
this.width = template.width || 0;
/**
* The current width of the Guacamole display, in pixels.
*
* @type Number
*/
this.height = template.height || 0;
};
/**
* Object which represents a mouse cursor used by the Guacamole display.
*
* @constructor
* @param {ManagedDisplay.Cursor|Object} template
* The object whose properties should be copied within the new
* ManagedDisplay.Cursor.
*/
ManagedDisplay.Cursor = function Cursor(template) {
// Use empty object by default
template = template || {};
/**
* The actual mouse cursor image.
*
* @type HTMLCanvasElement
*/
this.canvas = template.canvas;
/**
* The X coordinate of the cursor hotspot.
*
* @type Number
*/
this.x = template.x;
/**
* The Y coordinate of the cursor hotspot.
*
* @type Number
*/
this.y = template.y;
};
/**
* Creates a new ManagedDisplay which represents the current state of the
* given Guacamole display.
*
* @param {Guacamole.Display} display
* The Guacamole display to represent. Changes to this display will
* affect this ManagedDisplay.
*
* @returns {ManagedDisplay}
* A new ManagedDisplay which represents the current state of the
* given Guacamole display.
*/
ManagedDisplay.getInstance = function getInstance(display) {
var managedDisplay = new ManagedDisplay({
display : display
});
// Store changes to display size
display.onresize = function setClientSize() {
$rootScope.$apply(function updateClientSize() {
managedDisplay.size = new ManagedDisplay.Dimensions({
width : display.getWidth(),
height : display.getHeight()
});
});
};
// Store changes to display cursor
display.oncursor = function setClientCursor(canvas, x, y) {
$rootScope.$apply(function updateClientCursor() {
managedDisplay.cursor = new ManagedDisplay.Cursor({
canvas : canvas,
x : x,
y : y
});
});
};
return managedDisplay;
};
return ManagedDisplay;
}]);

View File

@@ -0,0 +1,153 @@
/*
* 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 ManagedFileDownload class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedFileDownload', ['$rootScope', '$injector',
function defineManagedFileDownload($rootScope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* file download while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFileDownload|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileDownload.
*/
var ManagedFileDownload = function ManagedFileDownload(template) {
// Use empty object by default
template = template || {};
/**
* The current state of the file transfer stream.
*
* @type ManagedFileTransferState
*/
this.transferState = template.transferState || new ManagedFileTransferState();
/**
* The mimetype of the file being transferred.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The filename of the file being transferred.
*
* @type String
*/
this.filename = template.filename;
/**
* The number of bytes transferred so far.
*
* @type Number
*/
this.progress = template.progress;
/**
* A blob containing the complete downloaded file. This is available
* only after the download has finished.
*
* @type Blob
*/
this.blob = template.blob;
};
/**
* Creates a new ManagedFileDownload which downloads the contents of the
* given stream as a file having the given mimetype and filename.
*
* @param {Guacamole.InputStream} stream
* The stream whose contents should be downloaded as a file.
*
* @param {String} mimetype
* The mimetype of the stream contents.
*
* @param {String} filename
* The filename of the file being received over the steram.
*
* @return {ManagedFileDownload}
* A new ManagedFileDownload object which can be used to track the
* progress of the download.
*/
ManagedFileDownload.getInstance = function getInstance(stream, mimetype, filename) {
// Init new file download object
var managedFileDownload = new ManagedFileDownload({
mimetype : mimetype,
filename : filename,
progress : 0,
transferState : new ManagedFileTransferState({
streamState : ManagedFileTransferState.StreamState.OPEN
})
});
// Begin file download
var blob_reader = new Guacamole.BlobReader(stream, mimetype);
// Update progress as data is received
blob_reader.onprogress = function onprogress() {
// Update progress
$rootScope.$apply(function downloadStreamProgress() {
managedFileDownload.progress = blob_reader.getLength();
});
// Signal server that data was received
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Save blob and close stream when complete
blob_reader.onend = function onend() {
$rootScope.$apply(function downloadStreamEnd() {
// Save blob
managedFileDownload.blob = blob_reader.getBlob();
// Mark stream as closed
ManagedFileTransferState.setStreamState(managedFileDownload.transferState,
ManagedFileTransferState.StreamState.CLOSED);
});
};
// Signal server that data is ready to be received
stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS);
return managedFileDownload;
};
return ManagedFileDownload;
}]);

View File

@@ -0,0 +1,136 @@
/*
* 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 ManagedFileTransferState class used by the guacClientManager
* service.
*/
angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() {
/**
* Object which represents the state of a Guacamole stream, including any
* error conditions.
*
* @constructor
* @param {ManagedFileTransferState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileTransferState.
*/
var ManagedFileTransferState = function ManagedFileTransferState(template) {
// Use empty object by default
template = template || {};
/**
* The current stream state. Valid values are described by
* ManagedFileTransferState.StreamState.
*
* @type String
* @default ManagedFileTransferState.StreamState.IDLE
*/
this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE;
/**
* The status code of the current error condition, if streamState
* is ERROR. For all other streamState values, this will be
* @link{Guacamole.Status.Code.SUCCESS}.
*
* @type Number
* @default Guacamole.Status.Code.SUCCESS
*/
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
};
/**
* Valid stream state strings. Each state string is associated with a
* specific state of a Guacamole stream.
*/
ManagedFileTransferState.StreamState = {
/**
* The stream has not yet been opened.
*
* @type String
*/
IDLE : "IDLE",
/**
* The stream has been successfully established. Data can be sent or
* received.
*
* @type String
*/
OPEN : "OPEN",
/**
* The stream has terminated successfully. No errors are indicated.
*
* @type String
*/
CLOSED : "CLOSED",
/**
* The stream has terminated due to an error. The associated error code
* is stored in statusCode.
*
* @type String
*/
ERROR : "ERROR"
};
/**
* Sets the current transfer state and, if given, the associated status
* code. If an error is already represented, this function has no effect.
*
* @param {ManagedFileTransferState} transferState
* The ManagedFileTransferState to update.
*
* @param {String} streamState
* The stream state to assign to the given ManagedFileTransferState, as
* listed within ManagedFileTransferState.StreamState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedFileTransferState, if
* any, as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedFileTransferState is not
* touched.
*/
ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) {
// Do not set state after an error is registered
if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR)
return;
// Update stream state
transferState.streamState = streamState;
// Set status code, if given
if (statusCode)
transferState.statusCode = statusCode;
};
return ManagedFileTransferState;
}]);

View File

@@ -0,0 +1,218 @@
/*
* 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 ManagedFileUpload class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector',
function defineManagedFileUpload($rootScope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
// Required services
var $window = $injector.get('$window');
/**
* The maximum number of bytes to include in each blob for the Guacamole
* file stream. Note that this, along with instruction opcode and protocol-
* related overhead, must not exceed the 8192 byte maximum imposed by the
* Guacamole protocol.
*
* @type Number
*/
var STREAM_BLOB_SIZE = 4096;
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* file upload while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFileUpload|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileUpload.
*/
var ManagedFileUpload = function ManagedFileUpload(template) {
// Use empty object by default
template = template || {};
/**
* The current state of the file transfer stream.
*
* @type ManagedFileTransferState
*/
this.transferState = template.transferState || new ManagedFileTransferState();
/**
* The mimetype of the file being transferred.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The filename of the file being transferred.
*
* @type String
*/
this.filename = template.filename;
/**
* The number of bytes transferred so far.
*
* @type Number
*/
this.progress = template.progress;
/**
* The total number of bytes in the file.
*
* @type Number
*/
this.length = template.length;
};
/**
* Converts the given bytes to a base64-encoded string.
*
* @param {Uint8Array} bytes A Uint8Array which contains the data to be
* encoded as base64.
* @return {String} The base64-encoded string.
*/
var getBase64 = function getBase64(bytes) {
var data = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
data += String.fromCharCode(bytes[i]);
// Convert to base64
return $window.btoa(data);
};
/**
* Creates a new ManagedFileUpload which uploads the given file to the
* server through the given Guacamole client.
*
* @param {Guacamole.Client} client
* The Guacamole client through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*
* @return {ManagedFileUpload}
* A new ManagedFileUpload object which can be used to track the
* progress of the upload.
*/
ManagedFileUpload.getInstance = function getInstance(client, file) {
var managedFileUpload = new ManagedFileUpload();
// Construct reader for file
var reader = new FileReader();
reader.onloadend = function fileContentsLoaded() {
// Open file for writing
var stream = client.createFileStream(file.type, file.name);
var valid = true;
var bytes = new Uint8Array(reader.result);
var offset = 0;
$rootScope.$apply(function uploadStreamOpen() {
// Init managed upload
managedFileUpload.filename = file.name;
managedFileUpload.mimetype = file.type;
managedFileUpload.progress = 0;
managedFileUpload.length = bytes.length;
// Notify that stream is open
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.OPEN);
});
// Invalidate stream on all errors
// Continue upload when acknowledged
stream.onack = function ackReceived(status) {
// Handle errors
if (status.isError()) {
valid = false;
$rootScope.$apply(function uploadStreamError() {
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
status.code);
});
}
// Abort upload if stream is invalid
if (!valid)
return false;
// Encode packet as base64
var slice = bytes.subarray(offset, offset + STREAM_BLOB_SIZE);
var base64 = getBase64(slice);
// Write packet
stream.sendBlob(base64);
// Advance to next packet
offset += STREAM_BLOB_SIZE;
$rootScope.$apply(function uploadStreamProgress() {
// If at end, stop upload
if (offset >= bytes.length) {
stream.sendEnd();
managedFileUpload.progress = bytes.length;
// Upload complete
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.CLOSED);
}
// Otherwise, update progress
else
managedFileUpload.progress = offset;
});
}; // end ack handler
};
reader.readAsArrayBuffer(file);
return managedFileUpload;
};
return ManagedFileUpload;
}]);

View File

@@ -23,7 +23,7 @@
/**
* A directive which allows elements to be manually focused / blurred.
*/
angular.module('element').directive('guacFocus', ['$timeout', '$parse', function guacFocus($timeout, $parse) {
angular.module('element').directive('guacFocus', ['$parse', function guacFocus($parse) {
return {
restrict: 'A',
@@ -47,7 +47,7 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function
// Set/unset focus depending on value of guacFocus
$scope.$watch(guacFocus, function updateFocus(value) {
$timeout(function updateFocusAsync() {
$scope.$evalAsync(function updateFocusAsync() {
if (value)
element.focus();
else
@@ -57,14 +57,14 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function
// Set focus flag when focus is received
element.addEventListener('focus', function focusReceived() {
$scope.$apply(function setGuacFocus() {
$scope.$evalAsync(function setGuacFocusAsync() {
guacFocus.assign($scope, true);
});
});
// Unset focus flag when focus is lost
element.addEventListener('blur', function focusLost() {
$scope.$apply(function unsetGuacFocus() {
$scope.$evalAsync(function unsetGuacFocusAsync() {
guacFocus.assign($scope, false);
});
});

View File

@@ -42,9 +42,50 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
},
templateUrl: 'app/home/templates/guacRecentConnections.html',
controller: ['$scope', '$injector', 'guacHistory', 'RecentConnection',
function guacRecentConnectionsController($scope, $injector, guacHistory, RecentConnection) {
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
// Required types
var ActiveConnection = $injector.get('ActiveConnection');
var RecentConnection = $injector.get('RecentConnection');
// Required services
var guacClientManager = $injector.get('guacClientManager');
var guacHistory = $injector.get('guacHistory');
/**
* Array of all known and visible active connections.
*
* @type ActiveConnection[]
*/
$scope.activeConnections = [];
/**
* Array of all known and visible recently-used connections.
*
* @type RecentConnection[]
*/
$scope.recentConnections = [];
/**
* Returns whether recent connections are available for display.
* Note that, for the sake of this directive, recent connections
* include any currently-active connections, even if they are not
* yet in the history.
*
* @returns {Boolean}
* true if recent (or active) connections are present, false
* otherwise.
*/
$scope.hasRecentConnections = function hasRecentConnections() {
return !!($scope.activeConnections.length || $scope.recentConnections.length);
};
/**
* Map of all visible objects, connections or connection groups, by
* object identifier.
*
* @type Object.<String, Connection|ConnectionGroup>
*/
var visibleObjects = {};
/**
@@ -87,6 +128,8 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
// Update visible objects when root group is set
$scope.$watch("rootGroup", function setRootGroup(rootGroup) {
// Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = [];
// Produce collection of visible objects
@@ -94,11 +137,27 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
if (rootGroup)
addVisibleConnectionGroup(rootGroup);
// Add all active connections
for (var id in guacClientManager.managedClients) {
// Get corresponding managed client
var client = guacClientManager.managedClients[id];
// Add active connections for clients with associated visible objects
if (id in visibleObjects) {
var object = visibleObjects[id];
$scope.activeConnections.push(new ActiveConnection(object.name, client));
}
}
// Add any recent connections that are visible
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects
if (historyEntry.id in visibleObjects) {
if (historyEntry.id in visibleObjects && !(historyEntry.id in guacClientManager.managedClients)) {
var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));

View File

@@ -20,4 +20,4 @@
* THE SOFTWARE.
*/
angular.module('home', ['history', 'groupList', 'rest']);
angular.module('home', ['client', 'history', 'groupList', 'rest']);

View File

@@ -22,11 +22,28 @@
-->
<!-- Text displayed if no recent connections exist -->
<p class="no-recent" ng-hide="recentConnections.length">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
<p class="no-recent" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
<!-- All active connections -->
<div ng-repeat="activeConnection in activeConnections" class="connection">
<a href="#/client/{{activeConnection.client.id}}">
<!-- Connection thumbnail -->
<div class="thumbnail">
<guac-thumbnail client="activeConnection.client"/></guac-thumbnail>
</div>
<!-- Connection name -->
<div class="caption">
<span class="name">{{activeConnection.name}}</span>
</div>
</a>
</div>
<!-- All recent connections -->
<div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}/{{recentConnection.name}}">
<a href="#/client/{{recentConnection.entry.id}}">
<!-- Connection thumbnail -->
<div class="thumbnail">

View File

@@ -0,0 +1,55 @@
/*
* 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 ActiveConnection class used by the guacRecentConnections
* directive.
*/
angular.module('home').factory('ActiveConnection', [function defineActiveConnection() {
/**
* A recently-user connection, visible to the current user, with an
* associated history entry.
*
* @constructor
*/
var ActiveConnection = function ActiveConnection(name, client) {
/**
* The human-readable name of this connection.
*
* @type String
*/
this.name = name;
/**
* The client associated with this active connection.
*
* @type ManagedClient
*/
this.client = client;
};
return ActiveConnection;
}]);

View File

@@ -26,32 +26,17 @@
angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) {
// Get class dependencies
// Required types
var PermissionSet = $injector.get("PermissionSet");
// Get services
var permissionService = $injector.get("permissionService"),
authenticationService = $injector.get("authenticationService"),
$q = $injector.get("$q"),
$document = $injector.get("$document"),
$window = $injector.get("$window"),
$location = $injector.get("$location");
// Required services
var $document = $injector.get("$document");
var $location = $injector.get("$location");
var $q = $injector.get("$q");
var $window = $injector.get("$window");
var authenticationService = $injector.get("authenticationService");
var permissionService = $injector.get("permissionService");
/*
* Safe $apply implementation from Alex Vanston:
* https://coderwall.com/p/ngisma
*/
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase === '$apply' || phase === '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
/**
* The current status notification, or false if no status is currently
* shown.

View File

@@ -55,10 +55,12 @@
margin: 0.5em;
}
.connection .thumbnail img {
.connection .thumbnail > * {
border: 1px solid black;
background: black;
box-shadow: 1px 1px 5px black;
max-width: 75%;
display: inline-block;
}
div.recent-connections .connection .thumbnail {

View File

@@ -17,9 +17,11 @@
"CLIENT" : {
"ACTION_RECONNECT" : "Reconnect",
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE",
"ACTION_RECONNECT" : "Reconnect",
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_DISCONNECT" : "Disconnect",
"ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK",
"ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONNECTING" : "Connecting",
"DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error",
@@ -82,13 +84,13 @@
"SECTION_HEADER_DISPLAY" : "Display",
"SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode",
"TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window",
"TEXT_CLIENT_STATUS_IDLE" : "Idle.",
"TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...",
"TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...",
"TEXT_TUNNEL_STATUS_CLOSED" : "You have been disconnected. Reload the page to reconnect.",
"TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...",
"TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
"TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window",
"TEXT_CLIENT_STATUS_IDLE" : "Idle.",
"TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...",
"TEXT_CLIENT_STATUS_DISCONNECTED" : "You have been disconnected.",
"TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...",
"TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...",
"TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
"URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml"