mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
Merge pull request #40 from glyptodon/managed-client
GUAC-963: Maintain connected clients in the background
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}]);
|
||||
|
@@ -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);
|
||||
|
||||
|
173
guacamole/src/main/webapp/app/client/directives/guacThumbnail.js
Normal file
173
guacamole/src/main/webapp/app/client/directives/guacThumbnail.js
Normal 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);
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
@@ -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);
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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%;
|
||||
|
@@ -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;
|
||||
}
|
@@ -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>
|
||||
|
@@ -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>
|
432
guacamole/src/main/webapp/app/client/types/ManagedClient.js
Normal file
432
guacamole/src/main/webapp/app/client/types/ManagedClient.js
Normal 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;
|
||||
|
||||
}]);
|
159
guacamole/src/main/webapp/app/client/types/ManagedClientState.js
Normal file
159
guacamole/src/main/webapp/app/client/types/ManagedClientState.js
Normal 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;
|
||||
|
||||
}]);
|
177
guacamole/src/main/webapp/app/client/types/ManagedDisplay.js
Normal file
177
guacamole/src/main/webapp/app/client/types/ManagedDisplay.js
Normal 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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
218
guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js
Normal file
218
guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js
Normal 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;
|
||||
|
||||
}]);
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
@@ -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));
|
||||
|
@@ -20,4 +20,4 @@
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
angular.module('home', ['history', 'groupList', 'rest']);
|
||||
angular.module('home', ['client', 'history', 'groupList', 'rest']);
|
||||
|
@@ -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">
|
||||
|
55
guacamole/src/main/webapp/app/home/types/ActiveConnection.js
Normal file
55
guacamole/src/main/webapp/app/home/types/ActiveConnection.js
Normal 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;
|
||||
|
||||
}]);
|
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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"
|
||||
|
||||
|
Reference in New Issue
Block a user