mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 21:27:40 +00:00
GUAC-963: Proof-of-concept ManagedClient implementation. Remove guacClientFactory and guacTunnelFactory (functionality replaced by ManagedClient).
This commit is contained in:
@@ -33,6 +33,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
// Required services
|
||||
var connectionGroupService = $injector.get('connectionGroupService');
|
||||
var connectionService = $injector.get('connectionService');
|
||||
var guacClientManager = $injector.get('guacClientManager');
|
||||
|
||||
/**
|
||||
* The minimum number of pixels a drag gesture must move to result in the
|
||||
@@ -149,7 +150,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
name : "CLIENT.ACTION_RECONNECT",
|
||||
// Handle reconnect action
|
||||
callback : function reconnectCallback() {
|
||||
$scope.id = uniqueId;
|
||||
$scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params);
|
||||
$scope.showStatus(false);
|
||||
}
|
||||
};
|
||||
@@ -164,12 +165,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
remaining: 15
|
||||
};
|
||||
|
||||
// Client settings and state
|
||||
$scope.clientProperties = new ClientProperties();
|
||||
|
||||
// Initialize clipboard data to an empty string
|
||||
$scope.clipboardData = "";
|
||||
|
||||
// Hide menu by default
|
||||
$scope.menuShown = false;
|
||||
|
||||
@@ -198,8 +193,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
* as well as any extra parameters if set.
|
||||
*/
|
||||
var uniqueId = $routeParams.type + '/' + $routeParams.id;
|
||||
$scope.id = uniqueId;
|
||||
$scope.connectionParameters = $routeParams.params || '';
|
||||
$scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
|
||||
|
||||
// Pull connection name from server
|
||||
switch ($routeParams.type) {
|
||||
@@ -266,9 +260,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
}
|
||||
|
||||
// Scroll display if absolute mouse is in use
|
||||
else if ($scope.clientProperties.emulateAbsoluteMouse) {
|
||||
$scope.clientProperties.scrollLeft -= deltaX;
|
||||
$scope.clientProperties.scrollTop -= deltaY;
|
||||
else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
|
||||
$scope.client.clientProperties.scrollLeft -= deltaX;
|
||||
$scope.client.clientProperties.scrollTop -= deltaY;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -305,7 +299,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
|
||||
|
||||
// Do not handle pinch gestures while relative mouse is in use
|
||||
if (!$scope.clientProperties.emulateAbsoluteMouse)
|
||||
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
|
||||
return false;
|
||||
|
||||
// Stop gesture if not in progress
|
||||
@@ -316,26 +310,26 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
|
||||
// Set initial scale if gesture has just started
|
||||
if (!initialScale) {
|
||||
initialScale = $scope.clientProperties.scale;
|
||||
initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale;
|
||||
initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale;
|
||||
initialScale = $scope.client.clientProperties.scale;
|
||||
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
|
||||
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
|
||||
}
|
||||
|
||||
// Determine new scale absolutely
|
||||
var currentScale = initialScale * currentLength / startLength;
|
||||
|
||||
// Fix scale within limits - scroll will be miscalculated otherwise
|
||||
currentScale = Math.max(currentScale, $scope.clientProperties.minScale);
|
||||
currentScale = Math.min(currentScale, $scope.clientProperties.maxScale);
|
||||
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
|
||||
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
|
||||
|
||||
// Update scale based on pinch distance
|
||||
$scope.autoFit = false;
|
||||
$scope.clientProperties.autoFit = false;
|
||||
$scope.clientProperties.scale = currentScale;
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale = currentScale;
|
||||
|
||||
// Scroll display to keep original pinch location centered within current pinch
|
||||
$scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
|
||||
$scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
|
||||
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
|
||||
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
|
||||
|
||||
return false;
|
||||
|
||||
@@ -357,7 +351,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
$scope.$broadcast('guacClipboard', 'text/plain', $scope.clipboardData);
|
||||
|
||||
// Disable client keyboard if the menu is shown
|
||||
$scope.clientProperties.keyboardEnabled = !menuShown;
|
||||
$scope.client.clientProperties.keyboardEnabled = !menuShown;
|
||||
|
||||
});
|
||||
|
||||
@@ -385,7 +379,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
keyboard.reset();
|
||||
|
||||
// Toggle the menu
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.menuShown = !$scope.menuShown;
|
||||
});
|
||||
}
|
||||
@@ -478,33 +472,33 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
});
|
||||
|
||||
$scope.formattedScale = function formattedScale() {
|
||||
return Math.round($scope.clientProperties.scale * 100);
|
||||
return Math.round($scope.client.clientProperties.scale * 100);
|
||||
};
|
||||
|
||||
$scope.zoomIn = function zoomIn() {
|
||||
$scope.autoFit = false;
|
||||
$scope.clientProperties.autoFit = false;
|
||||
$scope.clientProperties.scale += 0.1;
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale += 0.1;
|
||||
};
|
||||
|
||||
$scope.zoomOut = function zoomOut() {
|
||||
$scope.clientProperties.autoFit = false;
|
||||
$scope.clientProperties.scale -= 0.1;
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale -= 0.1;
|
||||
};
|
||||
|
||||
$scope.autoFit = true;
|
||||
|
||||
$scope.changeAutoFit = function changeAutoFit() {
|
||||
if ($scope.autoFit && $scope.clientProperties.minScale) {
|
||||
$scope.clientProperties.autoFit = true;
|
||||
if ($scope.autoFit && $scope.client.clientProperties.minScale) {
|
||||
$scope.client.clientProperties.autoFit = true;
|
||||
} else {
|
||||
$scope.clientProperties.autoFit = false;
|
||||
$scope.clientProperties.scale = 1;
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale = 1;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.autoFitDisabled = function() {
|
||||
return $scope.clientProperties.minZoom >= 1;
|
||||
return $scope.client.clientProperties.minZoom >= 1;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -568,7 +562,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
var downloadNotificationIDs = {};
|
||||
|
||||
$scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = {
|
||||
className : 'download',
|
||||
@@ -583,7 +577,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
});
|
||||
|
||||
$scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = downloadNotifications[streamIndex];
|
||||
if (notification)
|
||||
@@ -593,7 +587,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
});
|
||||
|
||||
$scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = downloadNotifications[streamIndex];
|
||||
var notificationID = downloadNotificationIDs[streamIndex];
|
||||
@@ -629,7 +623,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
var uploadNotificationIDs = {};
|
||||
|
||||
$scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = {
|
||||
className : 'upload',
|
||||
@@ -644,7 +638,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
});
|
||||
|
||||
$scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = uploadNotifications[streamIndex];
|
||||
if (notification)
|
||||
@@ -654,7 +648,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
});
|
||||
|
||||
$scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = uploadNotifications[streamIndex];
|
||||
var notificationID = uploadNotificationIDs[streamIndex];
|
||||
@@ -683,7 +677,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
||||
});
|
||||
|
||||
$scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) {
|
||||
$scope.safeApply(function() {
|
||||
$scope.$apply(function() {
|
||||
|
||||
var notification = uploadNotifications[streamIndex];
|
||||
var notificationID = uploadNotificationIDs[streamIndex];
|
||||
|
@@ -32,46 +32,19 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* Parameters for controlling client state.
|
||||
* The client to display within this guacClient directive.
|
||||
*
|
||||
* @type ClientProperties|Object
|
||||
* @type ManagedClient
|
||||
*/
|
||||
clientProperties : '=',
|
||||
client : '='
|
||||
|
||||
/**
|
||||
* The ID of the Guacamole connection to connect to.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
id : '=',
|
||||
|
||||
/**
|
||||
* Arbitrary URL-encoded parameters to append to the connection
|
||||
* string when connecting.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
connectionParameters : '='
|
||||
|
||||
},
|
||||
templateUrl: 'app/client/templates/guacClient.html',
|
||||
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
|
||||
|
||||
/*
|
||||
* Safe $apply implementation from Alex Vanston:
|
||||
* https://coderwall.com/p/ngisma
|
||||
*/
|
||||
$scope.safeApply = function(fn) {
|
||||
var phase = this.$root.$$phase;
|
||||
if(phase === '$apply' || phase === '$digest') {
|
||||
if(fn && (typeof(fn) === 'function')) {
|
||||
fn();
|
||||
}
|
||||
} else {
|
||||
this.$apply(fn);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Required services
|
||||
var $window = $injector.get('$window');
|
||||
|
||||
/**
|
||||
* Whether the local, hardware mouse cursor is in use.
|
||||
*
|
||||
@@ -146,14 +119,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
*/
|
||||
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
|
||||
|
||||
var $window = $injector.get('$window'),
|
||||
guacAudio = $injector.get('guacAudio'),
|
||||
guacVideo = $injector.get('guacVideo'),
|
||||
guacHistory = $injector.get('guacHistory'),
|
||||
guacTunnelFactory = $injector.get('guacTunnelFactory'),
|
||||
guacClientFactory = $injector.get('guacClientFactory'),
|
||||
authenticationService = $injector.get('authenticationService');
|
||||
|
||||
/**
|
||||
* Updates the scale of the attached Guacamole.Client based on current window
|
||||
* size and "auto-fit" setting.
|
||||
@@ -163,60 +128,20 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
if (!display) return;
|
||||
|
||||
// Calculate scale to fit screen
|
||||
$scope.clientProperties.minScale = Math.min(
|
||||
$scope.client.clientProperties.minScale = Math.min(
|
||||
main.offsetWidth / Math.max(display.getWidth(), 1),
|
||||
main.offsetHeight / Math.max(display.getHeight(), 1)
|
||||
);
|
||||
|
||||
// Calculate appropriate maximum zoom level
|
||||
$scope.clientProperties.maxScale = Math.max($scope.clientProperties.minScale, 3);
|
||||
$scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
|
||||
|
||||
// Clamp zoom level, maintain auto-fit
|
||||
if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit)
|
||||
$scope.clientProperties.scale = $scope.clientProperties.minScale;
|
||||
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
|
||||
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
|
||||
|
||||
else if (display.getScale() > $scope.clientProperties.maxScale)
|
||||
$scope.clientProperties.scale = $scope.clientProperties.maxScale;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the string of connection parameters to be passed to the
|
||||
* Guacamole client during connection. This string generally
|
||||
* contains the desired connection ID, display resolution, and
|
||||
* supported audio/video codecs.
|
||||
*
|
||||
* @returns {String} The string of connection parameters to be
|
||||
* passed to the Guacamole client.
|
||||
*/
|
||||
var getConnectString = function getConnectString() {
|
||||
|
||||
// Calculate optimal width/height for display
|
||||
var pixel_density = $window.devicePixelRatio || 1;
|
||||
var optimal_dpi = pixel_density * 96;
|
||||
var optimal_width = $window.innerWidth * pixel_density;
|
||||
var optimal_height = $window.innerHeight * pixel_density;
|
||||
|
||||
// Build base connect string
|
||||
var connectString =
|
||||
"id=" + encodeURIComponent($scope.id)
|
||||
+ "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken())
|
||||
+ "&width=" + Math.floor(optimal_width)
|
||||
+ "&height=" + Math.floor(optimal_height)
|
||||
+ "&dpi=" + Math.floor(optimal_dpi)
|
||||
+ ($scope.connectionParameters ? '&' + $scope.connectionParameters : '');
|
||||
|
||||
// Add audio mimetypes to connect_string
|
||||
guacAudio.supported.forEach(function(mimetype) {
|
||||
connectString += "&audio=" + encodeURIComponent(mimetype);
|
||||
});
|
||||
|
||||
// Add video mimetypes to connect_string
|
||||
guacVideo.supported.forEach(function(mimetype) {
|
||||
connectString += "&video=" + encodeURIComponent(mimetype);
|
||||
});
|
||||
|
||||
return connectString;
|
||||
else if (display.getScale() > $scope.client.clientProperties.maxScale)
|
||||
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
|
||||
|
||||
};
|
||||
|
||||
@@ -322,82 +247,36 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
* SCROLLING
|
||||
*/
|
||||
|
||||
$scope.$watch('clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
|
||||
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
|
||||
main.scrollLeft = scrollLeft;
|
||||
$scope.clientProperties.scrollLeft = main.scrollLeft;
|
||||
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
|
||||
});
|
||||
|
||||
$scope.$watch('clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
|
||||
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
|
||||
main.scrollTop = scrollTop;
|
||||
$scope.clientProperties.scrollTop = main.scrollTop;
|
||||
$scope.client.clientProperties.scrollTop = main.scrollTop;
|
||||
});
|
||||
|
||||
/*
|
||||
* CONNECT / RECONNECT
|
||||
*/
|
||||
// Attach any given managed client
|
||||
$scope.$watch('client', function(managedClient) {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Remove any existing display
|
||||
displayContainer.innerHTML = "";
|
||||
|
||||
// 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)
|
||||
// Only proceed if a client is given
|
||||
if (!managedClient)
|
||||
return;
|
||||
|
||||
// Get new client instance
|
||||
var tunnel = guacTunnelFactory.getInstance($scope);
|
||||
client = guacClientFactory.getInstance($scope, tunnel);
|
||||
// Get Guacamole client instance
|
||||
client = managedClient.client;
|
||||
|
||||
// Init display
|
||||
// Attach possibly new display
|
||||
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);
|
||||
$scope.$apply(updateDisplayScale);
|
||||
};
|
||||
|
||||
// Use local cursor if possible, update localCursor flag
|
||||
@@ -407,7 +286,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
|
||||
// Add display element
|
||||
displayElement = display.getElement();
|
||||
displayContainer.innerHTML = "";
|
||||
displayContainer.appendChild(displayElement);
|
||||
|
||||
// Do nothing when the display element is clicked on.
|
||||
@@ -416,17 +294,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Connect
|
||||
client.connect(getConnectString());
|
||||
|
||||
});
|
||||
|
||||
// Clean up when client directive is destroyed
|
||||
$scope.$on('$destroy', function destroyClient() {
|
||||
|
||||
// Update stored thumbnail of current connection
|
||||
updateHistoryEntry($scope.id);
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -434,7 +301,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
*/
|
||||
|
||||
// Watch for changes to mouse emulation mode
|
||||
$scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
|
||||
$scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
|
||||
|
||||
if (!client || !display) return;
|
||||
|
||||
@@ -483,14 +350,14 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
*/
|
||||
|
||||
// Adjust scale if modified externally
|
||||
$scope.$watch('clientProperties.scale', function changeScale(scale) {
|
||||
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
|
||||
|
||||
// Fix scale within limits
|
||||
scale = Math.max(scale, $scope.clientProperties.minScale);
|
||||
scale = Math.min(scale, $scope.clientProperties.maxScale);
|
||||
scale = Math.max(scale, $scope.client.clientProperties.minScale);
|
||||
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
|
||||
|
||||
// If at minimum zoom level, hide scroll bars
|
||||
if (scale === $scope.clientProperties.minScale)
|
||||
if (scale === $scope.client.clientProperties.minScale)
|
||||
main.style.overflow = "hidden";
|
||||
|
||||
// If not at minimum zoom level, show scroll bars
|
||||
@@ -501,15 +368,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
if (display)
|
||||
display.scale(scale);
|
||||
|
||||
if (scale !== $scope.clientProperties.scale)
|
||||
$scope.clientProperties.scale = scale;
|
||||
if (scale !== $scope.client.clientProperties.scale)
|
||||
$scope.client.clientProperties.scale = scale;
|
||||
|
||||
});
|
||||
|
||||
// If autofit is set, the scale should be set to the minimum scale, filling the screen
|
||||
$scope.$watch('clientProperties.autoFit', function changeAutoFit(autoFit) {
|
||||
$scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
|
||||
if(autoFit)
|
||||
$scope.clientProperties.scale = $scope.clientProperties.minScale;
|
||||
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
|
||||
});
|
||||
|
||||
// If the element is resized, attempt to resize client
|
||||
@@ -527,7 +394,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
|
||||
}
|
||||
|
||||
$scope.safeApply(updateDisplayScale);
|
||||
$scope.$apply(updateDisplayScale);
|
||||
|
||||
});
|
||||
|
||||
@@ -537,7 +404,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
|
||||
// Listen for broadcasted keydown events and fire the appropriate listeners
|
||||
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
|
||||
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
|
||||
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
|
||||
client.sendKeyEvent(1, keysym);
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -545,7 +412,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
||||
|
||||
// Listen for broadcasted keyup events and fire the appropriate listeners
|
||||
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
||||
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
|
||||
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
|
||||
client.sendKeyEvent(0, keysym);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@@ -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,97 @@
|
||||
/*
|
||||
* 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 = {};
|
||||
|
||||
/**
|
||||
* 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
|
||||
if (id in service.managedClients)
|
||||
service.managedClients[id].client.disconnect();
|
||||
|
||||
// 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;
|
||||
|
||||
}]);
|
@@ -28,10 +28,7 @@
|
||||
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
|
||||
|
||||
<!-- Client -->
|
||||
<guac-client
|
||||
client-properties="clientProperties"
|
||||
id="id"
|
||||
connection-parameters="connectionParameters"/></guac-client>
|
||||
<guac-client client="client"/></guac-client>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -60,7 +57,7 @@
|
||||
<h2>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h2>
|
||||
<div class="content" id="clipboard-settings">
|
||||
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
|
||||
<textarea ng-model="clipboardData" rows="10" cols="40" id="clipboard"></textarea>
|
||||
<textarea ng-model="client.clipboardData" rows="10" cols="40" id="clipboard"></textarea>
|
||||
</div>
|
||||
|
||||
<h2>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h2>
|
||||
@@ -93,7 +90,7 @@
|
||||
|
||||
<!-- Touchscreen -->
|
||||
<div class="choice">
|
||||
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
|
||||
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
|
||||
<div class="figure">
|
||||
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label>
|
||||
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
|
||||
@@ -102,7 +99,7 @@
|
||||
|
||||
<!-- Touchpad -->
|
||||
<div class="choice">
|
||||
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
|
||||
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
|
||||
<div class="figure">
|
||||
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label>
|
||||
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
|
||||
|
399
guacamole/src/main/webapp/app/client/types/ManagedClient.js
Normal file
399
guacamole/src/main/webapp/app/client/types/ManagedClient.js
Normal file
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
// Required services
|
||||
var $window = $injector.get('$window');
|
||||
var $document = $injector.get('$document');
|
||||
var authenticationService = $injector.get('authenticationService');
|
||||
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 name returned via the Guacamole protocol for this connection, if
|
||||
* any.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The current clipboard contents.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.clipboardData = template.clipboardData;
|
||||
|
||||
/**
|
||||
* 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.$apply(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.$apply(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.DISCONNECTED);
|
||||
break;
|
||||
|
||||
// Connecting, disconnecting, and disconnected are all
|
||||
// either ignored or handled by tunnel state
|
||||
|
||||
case 1: // Connecting
|
||||
case 4: // Disconnecting
|
||||
case 5: // Disconnected
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
// Update stored name if name changes
|
||||
client.onname = function clientNameChanged(name) {
|
||||
$rootScope.$apply(function updateName() {
|
||||
managedClient.name = name;
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/* TODO: Restore file transfer again */
|
||||
|
||||
/*
|
||||
// Handle any received files
|
||||
client.onfile = function onClientFile(stream, mimetype, filename) {
|
||||
|
||||
// Begin file download
|
||||
var guacFileStartEvent = $rootScope.$emit('guacClientFileDownloadStart', client, 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() {
|
||||
$rootScope.$emit('guacClientFileDownloadProgress', client, stream.index, mimetype, filename, blob_reader.getLength());
|
||||
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
|
||||
};
|
||||
|
||||
// When complete, prompt for download
|
||||
blob_reader.onend = function onend() {
|
||||
$rootScope.$emit('guacClientFileDownloadEnd', client, 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);
|
||||
|
||||
};
|
||||
*/
|
||||
|
||||
// Connect the Guacamole client
|
||||
client.connect(getConnectString(id, connectionParameters));
|
||||
|
||||
return managedClient;
|
||||
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
}]);
|
@@ -26,32 +26,17 @@
|
||||
angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||
function indexController($scope, $injector) {
|
||||
|
||||
// Get class dependencies
|
||||
// Required types
|
||||
var PermissionSet = $injector.get("PermissionSet");
|
||||
|
||||
// Get services
|
||||
var permissionService = $injector.get("permissionService"),
|
||||
authenticationService = $injector.get("authenticationService"),
|
||||
$q = $injector.get("$q"),
|
||||
$document = $injector.get("$document"),
|
||||
$window = $injector.get("$window"),
|
||||
$location = $injector.get("$location");
|
||||
// Required services
|
||||
var $document = $injector.get("$document");
|
||||
var $location = $injector.get("$location");
|
||||
var $q = $injector.get("$q");
|
||||
var $window = $injector.get("$window");
|
||||
var authenticationService = $injector.get("authenticationService");
|
||||
var permissionService = $injector.get("permissionService");
|
||||
|
||||
/*
|
||||
* Safe $apply implementation from Alex Vanston:
|
||||
* https://coderwall.com/p/ngisma
|
||||
*/
|
||||
$scope.safeApply = function(fn) {
|
||||
var phase = this.$root.$$phase;
|
||||
if(phase === '$apply' || phase === '$digest') {
|
||||
if(fn && (typeof(fn) === 'function')) {
|
||||
fn();
|
||||
}
|
||||
} else {
|
||||
this.$apply(fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The current status notification, or false if no status is currently
|
||||
* shown.
|
||||
|
Reference in New Issue
Block a user