mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 13:41:21 +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) {
|
function clientController($scope, $routeParams, $injector) {
|
||||||
|
|
||||||
// Required types
|
// Required types
|
||||||
var ClientProperties = $injector.get('ClientProperties');
|
var ManagedClientState = $injector.get('ManagedClientState');
|
||||||
var ScrollState = $injector.get('ScrollState');
|
var ScrollState = $injector.get('ScrollState');
|
||||||
|
|
||||||
// Required services
|
// Required services
|
||||||
var connectionGroupService = $injector.get('connectionGroupService');
|
var $location = $injector.get('$location');
|
||||||
var connectionService = $injector.get('connectionService');
|
var guacClientManager = $injector.get('guacClientManager');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum number of pixels a drag gesture must move to result in the
|
* 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
|
* Action which returns the user to the home screen.
|
||||||
* showStatus.
|
*/
|
||||||
|
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 = {
|
var RECONNECT_ACTION = {
|
||||||
name : "CLIENT.ACTION_RECONNECT",
|
name : "CLIENT.ACTION_RECONNECT",
|
||||||
// Handle reconnect action
|
callback : function reconnectCallback() {
|
||||||
callback : function reconnectCallback() {
|
$scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params);
|
||||||
$scope.id = uniqueId;
|
|
||||||
$scope.showStatus(false);
|
$scope.showStatus(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -164,12 +173,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
remaining: 15
|
remaining: 15
|
||||||
};
|
};
|
||||||
|
|
||||||
// Client settings and state
|
|
||||||
$scope.clientProperties = new ClientProperties();
|
|
||||||
|
|
||||||
// Initialize clipboard data to an empty string
|
|
||||||
$scope.clipboardData = "";
|
|
||||||
|
|
||||||
// Hide menu by default
|
// Hide menu by default
|
||||||
$scope.menuShown = false;
|
$scope.menuShown = false;
|
||||||
|
|
||||||
@@ -198,27 +201,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
* as well as any extra parameters if set.
|
* as well as any extra parameters if set.
|
||||||
*/
|
*/
|
||||||
var uniqueId = $routeParams.type + '/' + $routeParams.id;
|
var uniqueId = $routeParams.type + '/' + $routeParams.id;
|
||||||
$scope.id = uniqueId;
|
$scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
|
||||||
$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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var keysCurrentlyPressed = {};
|
var keysCurrentlyPressed = {};
|
||||||
|
|
||||||
@@ -266,9 +249,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scroll display if absolute mouse is in use
|
// Scroll display if absolute mouse is in use
|
||||||
else if ($scope.clientProperties.emulateAbsoluteMouse) {
|
else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
|
||||||
$scope.clientProperties.scrollLeft -= deltaX;
|
$scope.client.clientProperties.scrollLeft -= deltaX;
|
||||||
$scope.clientProperties.scrollTop -= deltaY;
|
$scope.client.clientProperties.scrollTop -= deltaY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -305,7 +288,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
|
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
|
||||||
|
|
||||||
// Do not handle pinch gestures while relative mouse is in use
|
// Do not handle pinch gestures while relative mouse is in use
|
||||||
if (!$scope.clientProperties.emulateAbsoluteMouse)
|
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Stop gesture if not in progress
|
// 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
|
// Set initial scale if gesture has just started
|
||||||
if (!initialScale) {
|
if (!initialScale) {
|
||||||
initialScale = $scope.clientProperties.scale;
|
initialScale = $scope.client.clientProperties.scale;
|
||||||
initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale;
|
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
|
||||||
initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale;
|
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine new scale absolutely
|
// Determine new scale absolutely
|
||||||
var currentScale = initialScale * currentLength / startLength;
|
var currentScale = initialScale * currentLength / startLength;
|
||||||
|
|
||||||
// Fix scale within limits - scroll will be miscalculated otherwise
|
// Fix scale within limits - scroll will be miscalculated otherwise
|
||||||
currentScale = Math.max(currentScale, $scope.clientProperties.minScale);
|
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
|
||||||
currentScale = Math.min(currentScale, $scope.clientProperties.maxScale);
|
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
|
||||||
|
|
||||||
// Update scale based on pinch distance
|
// Update scale based on pinch distance
|
||||||
$scope.autoFit = false;
|
$scope.autoFit = false;
|
||||||
$scope.clientProperties.autoFit = false;
|
$scope.client.clientProperties.autoFit = false;
|
||||||
$scope.clientProperties.scale = currentScale;
|
$scope.client.clientProperties.scale = currentScale;
|
||||||
|
|
||||||
// Scroll display to keep original pinch location centered within current pinch
|
// Scroll display to keep original pinch location centered within current pinch
|
||||||
$scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
|
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
|
||||||
$scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
|
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -354,10 +337,10 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
|
|
||||||
// Send clipboard data if menu is hidden
|
// Send clipboard data if menu is hidden
|
||||||
if (!menuShown && menuShownPreviousState)
|
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
|
// 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();
|
keyboard.reset();
|
||||||
|
|
||||||
// Toggle the menu
|
// Toggle the menu
|
||||||
$scope.safeApply(function() {
|
$scope.$apply(function() {
|
||||||
$scope.menuShown = !$scope.menuShown;
|
$scope.menuShown = !$scope.menuShown;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -397,114 +380,129 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
delete keysCurrentlyPressed[keysym];
|
delete keysCurrentlyPressed[keysym];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show status dialog when client status changes
|
// Update page title when client name is received
|
||||||
$scope.$on('guacClientStateChange', function clientStateChangeListener(event, client, status) {
|
$scope.$watch('client.name', function clientNameChanged(name) {
|
||||||
|
$scope.page.title = name;
|
||||||
|
});
|
||||||
|
|
||||||
// Show new status if not yet connected
|
// Show status dialog when connection status changes
|
||||||
if (status !== "connected") {
|
$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({
|
$scope.showStatus({
|
||||||
title: "CLIENT.DIALOG_HEADER_CONNECTING",
|
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
|
else
|
||||||
$scope.showStatus(false);
|
$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() {
|
$scope.formattedScale = function formattedScale() {
|
||||||
return Math.round($scope.clientProperties.scale * 100);
|
return Math.round($scope.client.clientProperties.scale * 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.zoomIn = function zoomIn() {
|
$scope.zoomIn = function zoomIn() {
|
||||||
$scope.autoFit = false;
|
$scope.autoFit = false;
|
||||||
$scope.clientProperties.autoFit = false;
|
$scope.client.clientProperties.autoFit = false;
|
||||||
$scope.clientProperties.scale += 0.1;
|
$scope.client.clientProperties.scale += 0.1;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.zoomOut = function zoomOut() {
|
$scope.zoomOut = function zoomOut() {
|
||||||
$scope.clientProperties.autoFit = false;
|
$scope.client.clientProperties.autoFit = false;
|
||||||
$scope.clientProperties.scale -= 0.1;
|
$scope.client.clientProperties.scale -= 0.1;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.autoFit = true;
|
$scope.autoFit = true;
|
||||||
|
|
||||||
$scope.changeAutoFit = function changeAutoFit() {
|
$scope.changeAutoFit = function changeAutoFit() {
|
||||||
if ($scope.autoFit && $scope.clientProperties.minScale) {
|
if ($scope.autoFit && $scope.client.clientProperties.minScale) {
|
||||||
$scope.clientProperties.autoFit = true;
|
$scope.client.clientProperties.autoFit = true;
|
||||||
} else {
|
} else {
|
||||||
$scope.clientProperties.autoFit = false;
|
$scope.client.clientProperties.autoFit = false;
|
||||||
$scope.clientProperties.scale = 1;
|
$scope.client.clientProperties.scale = 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.autoFitDisabled = function() {
|
$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
|
// Clean up when view destroyed
|
||||||
var downloadNotifications = {};
|
$scope.$on('$destroy', function clientViewDestroyed() {
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) {
|
// Remove client from client manager if no longer connected
|
||||||
$scope.safeApply(function() {
|
var managedClient = $scope.client;
|
||||||
|
if (managedClient) {
|
||||||
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() {
|
|
||||||
|
|
||||||
var notification = downloadNotifications[streamIndex];
|
// Get current connection state
|
||||||
var notificationID = downloadNotificationIDs[streamIndex];
|
var connectionState = managedClient.clientState.connectionState;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
// 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) {
|
// Hide any status dialog
|
||||||
$scope.safeApply(function() {
|
$scope.showStatus(false);
|
||||||
|
|
||||||
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() {
|
|
||||||
|
|
||||||
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: {
|
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',
|
templateUrl: 'app/client/templates/guacClient.html',
|
||||||
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
|
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
|
||||||
|
|
||||||
/*
|
// Required types
|
||||||
* Safe $apply implementation from Alex Vanston:
|
var ManagedClient = $injector.get('ManagedClient');
|
||||||
* https://coderwall.com/p/ngisma
|
|
||||||
*/
|
// Required services
|
||||||
$scope.safeApply = function(fn) {
|
var $window = $injector.get('$window');
|
||||||
var phase = this.$root.$$phase;
|
|
||||||
if(phase === '$apply' || phase === '$digest') {
|
|
||||||
if(fn && (typeof(fn) === 'function')) {
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$apply(fn);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the local, hardware mouse cursor is in use.
|
* 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 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
|
* Updates the scale of the attached Guacamole.Client based on current window
|
||||||
* size and "auto-fit" setting.
|
* size and "auto-fit" setting.
|
||||||
@@ -163,60 +131,20 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
if (!display) return;
|
if (!display) return;
|
||||||
|
|
||||||
// Calculate scale to fit screen
|
// Calculate scale to fit screen
|
||||||
$scope.clientProperties.minScale = Math.min(
|
$scope.client.clientProperties.minScale = Math.min(
|
||||||
main.offsetWidth / Math.max(display.getWidth(), 1),
|
main.offsetWidth / Math.max(display.getWidth(), 1),
|
||||||
main.offsetHeight / Math.max(display.getHeight(), 1)
|
main.offsetHeight / Math.max(display.getHeight(), 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate appropriate maximum zoom level
|
// 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
|
// Clamp zoom level, maintain auto-fit
|
||||||
if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit)
|
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
|
||||||
$scope.clientProperties.scale = $scope.clientProperties.minScale;
|
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
|
||||||
|
|
||||||
else if (display.getScale() > $scope.clientProperties.maxScale)
|
else if (display.getScale() > $scope.client.clientProperties.maxScale)
|
||||||
$scope.clientProperties.scale = $scope.clientProperties.maxScale;
|
$scope.client.clientProperties.scale = $scope.client.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;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,158 +211,60 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
// Attach any given managed client
|
||||||
* MOUSE
|
$scope.$watch('client', function attachManagedClient(managedClient) {
|
||||||
*/
|
|
||||||
|
|
||||||
// Watch for changes to mouse emulation mode
|
// Remove any existing display
|
||||||
// Send all received mouse events to the client
|
displayContainer.innerHTML = "";
|
||||||
mouse.onmousedown =
|
|
||||||
mouse.onmouseup =
|
|
||||||
mouse.onmousemove = function(mouseState) {
|
|
||||||
|
|
||||||
if (!client || !display)
|
// Only proceed if a client is given
|
||||||
|
if (!managedClient)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Send mouse state, show cursor if necessary
|
// Get Guacamole client instance
|
||||||
display.showCursor(!localCursor);
|
client = managedClient.client;
|
||||||
sendScaledMouseState(mouseState);
|
|
||||||
|
|
||||||
};
|
// Attach possibly new display
|
||||||
|
|
||||||
// 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
|
|
||||||
display = client.getDisplay();
|
display = client.getDisplay();
|
||||||
display.scale($scope.clientProperties.scale);
|
display.scale($scope.client.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);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add display element
|
// Add display element
|
||||||
displayElement = display.getElement();
|
displayElement = display.getElement();
|
||||||
displayContainer.innerHTML = "";
|
|
||||||
displayContainer.appendChild(displayElement);
|
displayContainer.appendChild(displayElement);
|
||||||
|
|
||||||
// Do nothing when the display element is clicked on.
|
// Do nothing when the display element is clicked on
|
||||||
displayElement.onclick = function(e) {
|
display.getElement().onclick = function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect
|
|
||||||
client.connect(getConnectString());
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up when client directive is destroyed
|
// Update actual view scrollLeft when scroll properties change
|
||||||
$scope.$on('$destroy', function destroyClient() {
|
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
|
||||||
|
main.scrollLeft = scrollLeft;
|
||||||
// Update stored thumbnail of current connection
|
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
|
||||||
updateHistoryEntry($scope.id);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
// Update actual view scrollTop when scroll properties change
|
||||||
* MOUSE EMULATION
|
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
|
||||||
*/
|
main.scrollTop = scrollTop;
|
||||||
|
$scope.client.clientProperties.scrollTop = main.scrollTop;
|
||||||
// Watch for changes to mouse emulation mode
|
});
|
||||||
$scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
|
|
||||||
|
// 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;
|
if (!client || !display) return;
|
||||||
|
|
||||||
@@ -478,19 +308,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
* DISPLAY SCALE / SIZE
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Adjust scale if modified externally
|
// Adjust scale if modified externally
|
||||||
$scope.$watch('clientProperties.scale', function changeScale(scale) {
|
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
|
||||||
|
|
||||||
// Fix scale within limits
|
// Fix scale within limits
|
||||||
scale = Math.max(scale, $scope.clientProperties.minScale);
|
scale = Math.max(scale, $scope.client.clientProperties.minScale);
|
||||||
scale = Math.min(scale, $scope.clientProperties.maxScale);
|
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
|
||||||
|
|
||||||
// If at minimum zoom level, hide scroll bars
|
// If at minimum zoom level, hide scroll bars
|
||||||
if (scale === $scope.clientProperties.minScale)
|
if (scale === $scope.client.clientProperties.minScale)
|
||||||
main.style.overflow = "hidden";
|
main.style.overflow = "hidden";
|
||||||
|
|
||||||
// If not at minimum zoom level, show scroll bars
|
// If not at minimum zoom level, show scroll bars
|
||||||
@@ -501,15 +327,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
if (display)
|
if (display)
|
||||||
display.scale(scale);
|
display.scale(scale);
|
||||||
|
|
||||||
if (scale !== $scope.clientProperties.scale)
|
if (scale !== $scope.client.clientProperties.scale)
|
||||||
$scope.clientProperties.scale = scale;
|
$scope.client.clientProperties.scale = scale;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If autofit is set, the scale should be set to the minimum scale, filling the screen
|
// 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)
|
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
|
// 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);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
// Watch for changes to mouse emulation mode
|
||||||
* KEYBOARD
|
// Send all received mouse events to the client
|
||||||
*/
|
mouse.onmousedown =
|
||||||
|
mouse.onmouseup =
|
||||||
// Listen for broadcasted keydown events and fire the appropriate listeners
|
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) {
|
$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);
|
client.sendKeyEvent(1, keysym);
|
||||||
event.preventDefault();
|
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) {
|
$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);
|
client.sendKeyEvent(0, keysym);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -561,26 +410,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
client.sendKeyEvent(0, keysym);
|
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.
|
* Ignores the given event.
|
||||||
*
|
*
|
||||||
@@ -590,68 +419,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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
|
// Handle and ignore dragenter/dragover
|
||||||
displayContainer.addEventListener("dragenter", ignoreEvent, false);
|
displayContainer.addEventListener("dragenter", ignoreEvent, false);
|
||||||
@@ -664,12 +431,13 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Ignore file drops if no attached client
|
// Ignore file drops if no attached client
|
||||||
if (!client) return;
|
if (!$scope.client)
|
||||||
|
return;
|
||||||
|
|
||||||
// Upload each file
|
// Upload each file
|
||||||
var files = e.dataTransfer.files;
|
var files = e.dataTransfer.files;
|
||||||
for (var i=0; i<files.length; i++)
|
for (var i=0; i<files.length; i++)
|
||||||
uploadFile(files[i]);
|
ManagedClient.uploadFile($scope.client, files[i]);
|
||||||
|
|
||||||
}, false);
|
}, 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 {
|
return {
|
||||||
// Element only
|
// Element only
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: false,
|
scope: {},
|
||||||
transclude: true,
|
transclude: true,
|
||||||
templateUrl: 'app/client/templates/guacViewport.html',
|
templateUrl: 'app/client/templates/guacViewport.html',
|
||||||
controller: ['$window', '$document', '$element',
|
controller: ['$scope', '$window', '$document', '$element',
|
||||||
function guacViewportController($window, $document, $element) {
|
function guacViewportController($scope, $window, $document, $element) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The fullscreen container element.
|
* The fullscreen container element.
|
||||||
@@ -55,8 +55,12 @@ angular.module('client').directive('guacViewport', [function guacViewport() {
|
|||||||
*/
|
*/
|
||||||
var currentAdjustedHeight = null;
|
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
|
// Pull scroll properties
|
||||||
var scrollLeft = document.body.scrollLeft;
|
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;
|
transition: left 0.125s, opacity 0.125s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu h3 {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
#menu .content {
|
#menu .content {
|
||||||
padding: 1em;
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu .content > * {
|
#menu .content > * {
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
display: block;
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu #mouse-settings .choice {
|
#menu #mouse-settings .choice {
|
||||||
@@ -94,11 +99,6 @@
|
|||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu h2 {
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu #keyboard-settings .figure {
|
#menu #keyboard-settings .figure {
|
||||||
float: right;
|
float: right;
|
||||||
max-width: 30%;
|
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">
|
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
|
||||||
|
|
||||||
<!-- Client -->
|
<!-- Client -->
|
||||||
<guac-client
|
<guac-client client="client"/></guac-client>
|
||||||
client-properties="clientProperties"
|
|
||||||
id="id"
|
|
||||||
connection-parameters="connectionParameters"/></guac-client>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,13 +54,20 @@
|
|||||||
|
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<div ng-class="{open: menuShown}" id="menu" guac-touch-drag="menuDrag" guac-scroll="menuScrollState">
|
<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">
|
<div class="content" id="clipboard-settings">
|
||||||
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<h2>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h2>
|
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
|
||||||
<div class="content" id="keyboard-settings">
|
<div class="content" id="keyboard-settings">
|
||||||
|
|
||||||
<!-- No IME -->
|
<!-- No IME -->
|
||||||
@@ -87,13 +91,13 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h2>
|
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
|
||||||
<div class="content" id="mouse-settings">
|
<div class="content" id="mouse-settings">
|
||||||
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
|
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
|
||||||
|
|
||||||
<!-- Touchscreen -->
|
<!-- Touchscreen -->
|
||||||
<div class="choice">
|
<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">
|
<div class="figure">
|
||||||
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label>
|
<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>
|
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
|
||||||
@@ -102,7 +106,7 @@
|
|||||||
|
|
||||||
<!-- Touchpad -->
|
<!-- Touchpad -->
|
||||||
<div class="choice">
|
<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">
|
<div class="figure">
|
||||||
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label>
|
<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>
|
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
|
||||||
@@ -111,7 +115,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h2>
|
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div id="zoom-settings">
|
<div id="zoom-settings">
|
||||||
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>
|
<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.
|
* 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 {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
@@ -47,7 +47,7 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function
|
|||||||
|
|
||||||
// Set/unset focus depending on value of guacFocus
|
// Set/unset focus depending on value of guacFocus
|
||||||
$scope.$watch(guacFocus, function updateFocus(value) {
|
$scope.$watch(guacFocus, function updateFocus(value) {
|
||||||
$timeout(function updateFocusAsync() {
|
$scope.$evalAsync(function updateFocusAsync() {
|
||||||
if (value)
|
if (value)
|
||||||
element.focus();
|
element.focus();
|
||||||
else
|
else
|
||||||
@@ -57,14 +57,14 @@ angular.module('element').directive('guacFocus', ['$timeout', '$parse', function
|
|||||||
|
|
||||||
// Set focus flag when focus is received
|
// Set focus flag when focus is received
|
||||||
element.addEventListener('focus', function focusReceived() {
|
element.addEventListener('focus', function focusReceived() {
|
||||||
$scope.$apply(function setGuacFocus() {
|
$scope.$evalAsync(function setGuacFocusAsync() {
|
||||||
guacFocus.assign($scope, true);
|
guacFocus.assign($scope, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Unset focus flag when focus is lost
|
// Unset focus flag when focus is lost
|
||||||
element.addEventListener('blur', function focusLost() {
|
element.addEventListener('blur', function focusLost() {
|
||||||
$scope.$apply(function unsetGuacFocus() {
|
$scope.$evalAsync(function unsetGuacFocusAsync() {
|
||||||
guacFocus.assign($scope, false);
|
guacFocus.assign($scope, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -42,9 +42,50 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
|||||||
},
|
},
|
||||||
|
|
||||||
templateUrl: 'app/home/templates/guacRecentConnections.html',
|
templateUrl: 'app/home/templates/guacRecentConnections.html',
|
||||||
controller: ['$scope', '$injector', 'guacHistory', 'RecentConnection',
|
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
|
||||||
function guacRecentConnectionsController($scope, $injector, guacHistory, RecentConnection) {
|
|
||||||
|
|
||||||
|
// 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 = {};
|
var visibleObjects = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,6 +128,8 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
|||||||
// Update visible objects when root group is set
|
// Update visible objects when root group is set
|
||||||
$scope.$watch("rootGroup", function setRootGroup(rootGroup) {
|
$scope.$watch("rootGroup", function setRootGroup(rootGroup) {
|
||||||
|
|
||||||
|
// Clear connection arrays
|
||||||
|
$scope.activeConnections = [];
|
||||||
$scope.recentConnections = [];
|
$scope.recentConnections = [];
|
||||||
|
|
||||||
// Produce collection of visible objects
|
// Produce collection of visible objects
|
||||||
@@ -94,11 +137,27 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
|||||||
if (rootGroup)
|
if (rootGroup)
|
||||||
addVisibleConnectionGroup(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
|
// Add any recent connections that are visible
|
||||||
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
|
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
|
||||||
|
|
||||||
// Add recent connections for history entries with associated visible objects
|
// 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];
|
var object = visibleObjects[historyEntry.id];
|
||||||
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));
|
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));
|
||||||
|
@@ -20,4 +20,4 @@
|
|||||||
* THE SOFTWARE.
|
* 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 -->
|
<!-- 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 -->
|
<!-- All recent connections -->
|
||||||
<div ng-repeat="recentConnection in recentConnections" class="connection">
|
<div ng-repeat="recentConnection in recentConnections" class="connection">
|
||||||
<a href="#/client/{{recentConnection.entry.id}}/{{recentConnection.name}}">
|
<a href="#/client/{{recentConnection.entry.id}}">
|
||||||
|
|
||||||
<!-- Connection thumbnail -->
|
<!-- Connection thumbnail -->
|
||||||
<div class="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',
|
angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||||
function indexController($scope, $injector) {
|
function indexController($scope, $injector) {
|
||||||
|
|
||||||
// Get class dependencies
|
// Required types
|
||||||
var PermissionSet = $injector.get("PermissionSet");
|
var PermissionSet = $injector.get("PermissionSet");
|
||||||
|
|
||||||
// Get services
|
// Required services
|
||||||
var permissionService = $injector.get("permissionService"),
|
var $document = $injector.get("$document");
|
||||||
authenticationService = $injector.get("authenticationService"),
|
var $location = $injector.get("$location");
|
||||||
$q = $injector.get("$q"),
|
var $q = $injector.get("$q");
|
||||||
$document = $injector.get("$document"),
|
var $window = $injector.get("$window");
|
||||||
$window = $injector.get("$window"),
|
var authenticationService = $injector.get("authenticationService");
|
||||||
$location = $injector.get("$location");
|
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
|
* The current status notification, or false if no status is currently
|
||||||
* shown.
|
* shown.
|
||||||
|
@@ -55,10 +55,12 @@
|
|||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection .thumbnail img {
|
.connection .thumbnail > * {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
|
background: black;
|
||||||
box-shadow: 1px 1px 5px black;
|
box-shadow: 1px 1px 5px black;
|
||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.recent-connections .connection .thumbnail {
|
div.recent-connections .connection .thumbnail {
|
||||||
|
@@ -17,9 +17,11 @@
|
|||||||
|
|
||||||
"CLIENT" : {
|
"CLIENT" : {
|
||||||
|
|
||||||
"ACTION_RECONNECT" : "Reconnect",
|
"ACTION_RECONNECT" : "Reconnect",
|
||||||
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
|
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
|
||||||
"ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE",
|
"ACTION_DISCONNECT" : "Disconnect",
|
||||||
|
"ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK",
|
||||||
|
"ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE",
|
||||||
|
|
||||||
"DIALOG_HEADER_CONNECTING" : "Connecting",
|
"DIALOG_HEADER_CONNECTING" : "Connecting",
|
||||||
"DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error",
|
"DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error",
|
||||||
@@ -82,13 +84,13 @@
|
|||||||
"SECTION_HEADER_DISPLAY" : "Display",
|
"SECTION_HEADER_DISPLAY" : "Display",
|
||||||
"SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode",
|
"SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode",
|
||||||
|
|
||||||
"TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window",
|
"TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window",
|
||||||
"TEXT_CLIENT_STATUS_IDLE" : "Idle.",
|
"TEXT_CLIENT_STATUS_IDLE" : "Idle.",
|
||||||
"TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...",
|
"TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...",
|
||||||
"TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...",
|
"TEXT_CLIENT_STATUS_DISCONNECTED" : "You have been disconnected.",
|
||||||
"TEXT_TUNNEL_STATUS_CLOSED" : "You have been disconnected. Reload the page to reconnect.",
|
"TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...",
|
||||||
"TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...",
|
"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_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
|
||||||
|
|
||||||
"URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml"
|
"URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user