GUACAMOLE-724: Merge multiple-connection tile support/view.

This commit is contained in:
James Muehlner
2021-07-14 13:41:03 -07:00
committed by GitHub
182 changed files with 3273 additions and 1652 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,431 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying a non-global notification describing the status
* of a specific Guacamole client, including prompts for any information
* necessary to continue the connection.
*/
angular.module('client').directive('guacClientNotification', [function guacClientNotification() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClientNotification.html'
};
directive.scope = {
/**
* The client whose status should be displayed.
*
* @type ManagedClient
*/
client : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientNotificationController($scope, $injector, $element) {
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientState = $injector.get('ManagedClientState');
const Protocol = $injector.get('Protocol');
// Required services
const $location = $injector.get('$location');
const authenticationService = $injector.get('authenticationService');
const guacClientManager = $injector.get('guacClientManager');
const requestService = $injector.get('requestService');
const userPageService = $injector.get('userPageService');
/**
* A Notification object describing the client status to display as a
* dialog or prompt, as would be accepted by guacNotification.showStatus(),
* or false if no status should be shown.
*
* @type {Notification|Object|Boolean}
*/
$scope.status = false;
/**
* All client error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
const CLIENT_ERRORS = {
0x0201: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0209: true,
0x020A: true,
0x020B: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* client error occurs.
*/
const CLIENT_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0308: true
};
/**
* All tunnel error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
const TUNNEL_ERRORS = {
0x0201: true,
0x0202: true,
0x0203: true,
0x0204: true,
0x0205: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* tunnel error occurs.
*/
const TUNNEL_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0308: true
};
/**
* Action which logs out from Guacamole entirely.
*/
const LOGOUT_ACTION = {
name : "CLIENT.ACTION_LOGOUT",
className : "logout button",
callback : function logoutCallback() {
authenticationService.logout()
['catch'](requestService.IGNORE);
}
};
/**
* Action which returns the user to the home screen. If the home page has
* not yet been determined, this will be null.
*/
let NAVIGATE_HOME_ACTION = null;
// Assign home page action once user's home page has been determined
userPageService.getHomePage()
.then(function homePageRetrieved(homePage) {
// Define home action only if different from current location
if ($location.path() !== homePage.url) {
NAVIGATE_HOME_ACTION = {
name : "CLIENT.ACTION_NAVIGATE_HOME",
className : "home button",
callback : function navigateHomeCallback() {
$location.url(homePage.url);
}
};
}
}, requestService.WARN);
/**
* Action which replaces the current client with a newly-connected client.
*/
const RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT",
className : "reconnect button",
callback : function reconnectCallback() {
$scope.client = guacClientManager.replaceManagedClient($scope.client.id);
$scope.status = false;
}
};
/**
* The reconnect countdown to display if an error or status warrants an
* automatic, timed reconnect.
*/
const RECONNECT_COUNTDOWN = {
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
callback: RECONNECT_ACTION.callback,
remaining: 15
};
/**
* Displays a notification at the end of a Guacamole connection, whether
* that connection is ending normally or due to an error. As the end of
* a Guacamole connection may be due to changes in authentication status,
* this will also implicitly peform a re-authentication attempt to check
* for such changes, possibly resulting in auth-related events like
* guacInvalidCredentials.
*
* @param {Notification|Boolean|Object} status
* The status notification to show, as would be accepted by
* guacNotification.showStatus().
*/
const notifyConnectionClosed = function notifyConnectionClosed(status) {
// Re-authenticate to verify auth status at end of connection
authenticationService.updateCurrentToken($location.search())
['catch'](requestService.IGNORE)
// Show the requested status once the authentication check has finished
['finally'](function authenticationCheckComplete() {
$scope.status = status;
});
};
/**
* Notifies the user that the connection state has changed.
*
* @param {String} connectionState
* The current connection state, as defined by
* ManagedClientState.ConnectionState.
*/
const notifyConnectionState = function notifyConnectionState(connectionState) {
// Hide any existing status
$scope.status = false;
// Do not display status if status not known
if (!connectionState)
return;
// Build array of available actions
let actions;
if (NAVIGATE_HOME_ACTION)
actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
else
actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
// Get any associated status code
const status = $scope.client.clientState.statusCode;
// Connecting
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
$scope.status = {
title: "CLIENT.DIALOG_HEADER_CONNECTING",
text: {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
}
};
}
// Client error
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
// Determine translation name of error
const errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : "CLIENT.ERROR_CLIENT_" + errorName
},
countdown : countdown,
actions : actions
});
}
// Tunnel error
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
// Determine translation name of error
const errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : "CLIENT.ERROR_TUNNEL_" + errorName
},
countdown : countdown,
actions : actions
});
}
// Disconnected
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
notifyConnectionClosed({
title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
text : {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
},
actions : actions
});
}
// Hide status for all other states
else
$scope.status = false;
};
/**
* Prompts the user to enter additional connection parameters. If the
* protocol and associated parameters of the underlying connection are not
* yet known, this function has no effect and should be re-invoked once
* the parameters are known.
*
* @param {Object.<String, String>} requiredParameters
* The set of all parameters requested by the server via "required"
* instructions, where each object key is the name of a requested
* parameter and each value is the current value entered by the user.
*/
const notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
/**
* Action which submits the current set of parameter values, requesting
* that the connection continue.
*/
const SUBMIT_PARAMETERS = {
name : "CLIENT.ACTION_CONTINUE",
className : "button",
callback : function submitParameters() {
if ($scope.client) {
const params = $scope.client.requiredParameters;
$scope.client.requiredParameters = null;
ManagedClient.sendArguments($scope.client, params);
}
}
};
/**
* Action which cancels submission of additional parameters and
* disconnects from the current connection.
*/
const CANCEL_PARAMETER_SUBMISSION = {
name : "CLIENT.ACTION_CANCEL",
className : "button",
callback : function cancelSubmission() {
$scope.client.requiredParameters = null;
$scope.client.client.disconnect();
}
};
// Attempt to prompt for parameters only if the parameters that apply
// to the underlying connection are known
if (!$scope.client.protocol || !$scope.client.forms)
return;
// Prompt for parameters
$scope.status = {
formNamespace : Protocol.getNamespace($scope.client.protocol),
forms : $scope.client.forms,
formModel : requiredParameters,
formSubmitCallback : SUBMIT_PARAMETERS.callback,
actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
};
};
/**
* Returns whether the given connection state allows for submission of
* connection parameters via "argv" instructions.
*
* @param {String} connectionState
* The connection state to test, as defined by
* ManagedClientState.ConnectionState.
*
* @returns {boolean}
* true if the given connection state allows submission of connection
* parameters via "argv" instructions, false otherwise.
*/
const canSubmitParameters = function canSubmitParameters(connectionState) {
return (connectionState === ManagedClientState.ConnectionState.WAITING ||
connectionState === ManagedClientState.ConnectionState.CONNECTED);
};
// Show status dialog when connection status changes
$scope.$watchGroup([
'client.clientState.connectionState',
'client.requiredParameters',
'client.protocol',
'client.forms'
], function clientStateChanged(newValues) {
const connectionState = newValues[0];
const requiredParameters = newValues[1];
// Prompt for parameters only if parameters can actually be submitted
if (requiredParameters && canSubmitParameters(connectionState))
notifyParametersRequired(requiredParameters);
// Otherwise, just show general connection state
else
notifyConnectionState(connectionState);
});
/**
* Prevents the default behavior of the given AngularJS event if a
* notification is currently shown and the client is focused.
*
* @param {event} e
* The AngularJS event to selectively prevent.
*/
const preventDefaultDuringNotification = function preventDefaultDuringNotification(e) {
if ($scope.status && $scope.client.clientProperties.focused)
e.preventDefault();
};
// Block internal handling of key events (by the client) if a
// notification is visible
$scope.$on('guacBeforeKeydown', preventDefaultDuringNotification);
$scope.$on('guacBeforeKeyup', preventDefaultDuringNotification);
}];
return directive;
}]);
@@ -25,11 +25,12 @@
angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
// Required services
var guacClientManager = $injector.get('guacClientManager');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
const guacClientManager = $injector.get('guacClientManager');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
var ManagedClientState = $injector.get('ManagedClientState');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedClientState = $injector.get('ManagedClientState');
/**
* Getter/setter for the boolean flag controlling whether the client panel
@@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
scope: {
/**
* The ManagedClient instances associated with the active
* The ManagedClientGroup instances associated with the active
* connections to be displayed within this panel.
*
* @type ManagedClient[]|Object.<String, ManagedClient>
* @type ManagedClientGroup[]
*/
clients : '='
clientGroups : '='
},
templateUrl: 'app/client/templates/guacClientPanel.html',
@@ -75,71 +76,68 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
$scope.panelHidden = panelHidden;
/**
* Returns whether this panel currently has any clients associated
* with it.
* Returns whether this panel currently has any client groups
* associated with it.
*
* @return {Boolean}
* true if at least one client is associated with this panel,
* false otherwise.
* true if at least one client group is associated with this
* panel, false otherwise.
*/
$scope.hasClients = function hasClients() {
return !!_.find($scope.clients, $scope.isManaged);
$scope.hasClientGroups = function hasClientGroups() {
return $scope.clientGroups && $scope.clientGroups.length;
};
/**
* Returns whether the status of the given client has changed in a
* way that requires the user's attention. This may be due to an
* error, or due to a server-initiated disconnect.
* @borrows ManagedClientGroup.getIdentifier
*/
$scope.getIdentifier = ManagedClientGroup.getIdentifier;
/**
* @borrows ManagedClientGroup.getTitle
*/
$scope.getTitle = ManagedClientGroup.getTitle;
/**
* Returns whether the status of any client within the given client
* group has changed in a way that requires the user's attention.
* This may be due to an error, or due to a server-initiated
* disconnect.
*
* @param {ManagedClient} client
* The client to test.
* @param {ManagedClientGroup} clientGroup
* The client group to test.
*
* @returns {Boolean}
* true if the given client requires the user's attention,
* false otherwise.
*/
$scope.hasStatusUpdate = function hasStatusUpdate(client) {
$scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) {
return _.findIndex(clientGroup.clients, (client) => {
// Test whether the client has encountered an error
switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED:
return true;
}
// Test whether the client has encountered an error
switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED:
return true;
}
return false;
return false;
}) !== -1;
};
/**
* Returns whether the given client is currently being managed by
* the guacClientManager service.
* Initiates an orderly disconnect of all clients within the given
* group. The clients are removed from management such that
* attempting to connect to any of the same connections will result
* in new connections being established, rather than displaying a
* notification that the connection has ended.
*
* @param {ManagedClient} client
* The client to test.
*
* @returns {Boolean}
* true if the given client is being managed by the
* guacClientManager service, false otherwise.
* @param {ManagedClientGroup} clientGroup
* The group of clients to disconnect.
*/
$scope.isManaged = function isManaged(client) {
return !!guacClientManager.getManagedClients()[client.id];
};
/**
* Initiates an orderly disconnect of the given client. The client
* is removed from management such that attempting to connect to
* the same connection will result in a new connection being
* established, rather than displaying a notification that the
* connection has ended.
*
* @param {type} client
* @returns {undefined}
*/
$scope.disconnect = function disconnect(client) {
client.client.disconnect();
guacClientManager.removeManagedClient(client.id);
$scope.disconnect = function disconnect(clientGroup) {
guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
};
/**
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for controlling the zoom level and scale-to-fit behavior of a
* a single Guacamole client.
*/
angular.module('client').directive('guacClientZoom', [function guacClientZoom() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClientZoom.html'
};
directive.scope = {
/**
* The client to control the zoom/autofit of.
*
* @type ManagedClient
*/
client : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientZoomController($scope, $injector, $element) {
/**
* Zooms in by 10%, automatically disabling autofit.
*/
$scope.zoomIn = function zoomIn() {
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale += 0.1;
};
/**
* Zooms out by 10%, automatically disabling autofit.
*/
$scope.zoomOut = function zoomOut() {
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale -= 0.1;
};
/**
* Resets the client autofit setting to false.
*/
$scope.clearAutoFit = function clearAutoFit() {
$scope.client.clientProperties.autoFit = false;
};
/**
* Notifies that the autofit setting has been manually changed by the
* user.
*/
$scope.autoFitChanged = function autoFitChanged() {
// Reset to 100% scale when autofit is first disabled
if (!$scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = 1;
};
}];
return directive;
}]);
@@ -28,14 +28,6 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
replace: true,
scope: {
/**
* The client whose file transfers should be managed by this
* directive.
*
* @type ManagedClient
*/
client : '=',
/**
* @type ManagedFilesystem
*/
@@ -116,7 +108,7 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
* The file to download.
*/
$scope.downloadFile = function downloadFile(file) {
ManagedFilesystem.downloadFile($scope.client, $scope.filesystem, file.streamName);
ManagedFilesystem.downloadFile($scope.filesystem, file.streamName);
};
/**
@@ -28,12 +28,12 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
scope: {
/**
* The client whose file transfers should be managed by this
* The client group whose file transfers should be managed by this
* directive.
*
* @type ManagerClient
* @type ManagedClientGroup
*/
client : '='
clientGroup : '='
},
@@ -41,7 +41,9 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Determines whether the given file transfer state indicates an
@@ -74,17 +76,29 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
*/
$scope.clearCompletedTransfers = function clearCompletedTransfers() {
// Nothing to clear if no client attached
if (!$scope.client)
// Nothing to clear if no client group attached
if (!$scope.clientGroup)
return;
// Remove completed uploads
$scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) {
return isInProgress(upload.transferState);
$scope.clientGroup.clients.forEach(client => {
client.uploads = client.uploads.filter(function isUploadInProgress(upload) {
return isInProgress(upload.transferState);
});
});
};
/**
* @borrows ManagedClientGroup.hasMultipleClients
*/
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
/**
* @borrows ManagedClient.hasTransfers
*/
$scope.hasTransfers = ManagedClient.hasTransfers;
}]
};
@@ -43,20 +43,6 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
// 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.
*
@@ -126,32 +112,7 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
// 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");
// Init display scale
$scope.$evalAsync($scope.updateDisplayScale);
});
}]
@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which displays one or more Guacamole clients in an evenly-tiled
* view. The number of rows and columns used for the arrangement of tiles is
* automatically determined by the number of clients present.
*/
angular.module('client').directive('guacTiledClients', [function guacTiledClients() {
const directive = {
restrict: 'E',
templateUrl: 'app/client/templates/guacTiledClients.html',
};
directive.scope = {
/**
* The function to invoke when the "close" button in the header of a
* client tile is clicked. The ManagedClient that is closed will be
* made available to the Angular expression defining the callback as
* "$client".
*
* @type function
*/
onClose : '&',
/**
* The group of Guacamole clients that should be displayed in an
* evenly-tiled grid arrangement.
*
* @type ManagedClientGroup
*/
clientGroup : '=',
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type boolean
*/
emulateAbsoluteMouse : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacTiledClientsController($scope, $injector, $element) {
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
/**
* Returns the currently-focused ManagedClient. If there is no such
* client, or multiple clients are focused, null is returned.
*
* @returns {ManagedClient}
* The currently-focused client, or null if there are no focused
* clients or if multiple clients are focused.
*/
$scope.getFocusedClient = function getFocusedClient() {
const managedClientGroup = $scope.clientGroup;
if (managedClientGroup) {
const focusedClients = _.filter(managedClientGroup.clients, client => client.clientProperties.focused);
if (focusedClients.length === 1)
return focusedClients[0];
}
return null;
};
// Notify whenever identify of currently-focused client changes
$scope.$watch('getFocusedClient()', function focusedClientChanged(focusedClient) {
$scope.$emit('guacClientFocused', focusedClient);
});
/**
* Returns a callback for guacClick that assigns or updates keyboard
* focus to the given client, allowing that client to receive and
* handle keyboard events. Multiple clients may have keyboard focus
* simultaneously.
*
* @param {ManagedClient} client
* The client that should receive keyboard focus.
*
* @return {guacClick~callback}
* The callback that guacClient should invoke when the given client
* has been clicked.
*/
$scope.getFocusAssignmentCallback = function getFocusAssignmentCallback(client) {
return (shift, ctrl) => {
// Clear focus of all other clients if not selecting multiple
if (!shift && !ctrl) {
$scope.clientGroup.clients.forEach(client => {
client.clientProperties.focused = false;
});
}
client.clientProperties.focused = true;
// Fill in any gaps if performing rectangular multi-selection
// via shift-click
if (shift) {
let minRow = $scope.clientGroup.rows - 1;
let minColumn = $scope.clientGroup.columns - 1;
let maxRow = 0;
let maxColumn = 0;
// Determine extents of selected area
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
if (client.clientProperties.focused) {
minRow = Math.min(minRow, row);
minColumn = Math.min(minColumn, column);
maxRow = Math.max(maxRow, row);
maxColumn = Math.max(maxColumn, column);
}
});
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
client.clientProperties.focused =
row >= minRow
&& row <= maxRow
&& column >= minColumn
&& column <= maxColumn;
});
}
};
};
/**
* @borrows ManagedClientGroup.hasMultipleClients
*/
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
/**
* @borrows ManagedClientGroup.getClientGrid
*/
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
/**
* @borrows ManagedClient.isShared
*/
$scope.isShared = ManagedClient.isShared;
}];
return directive;
}]);
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying a group of Guacamole clients as a non-interactive
* thumbnail of tiled client displays.
*/
angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacTiledThumbnails.html'
};
directive.scope = {
/**
* The group of clients to display as a thumbnail of tiled client
* displays.
*
* @type ManagedClientGroup
*/
clientGroup : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacTiledThumbnailsController($scope, $injector, $element) {
// Required types
const ManagedClientGroup = $injector.get('ManagedClientGroup');
/**
* The overall height of the thumbnail view of the tiled grid of
* clients within the client group, in pixels. This value is
* intentionally based off a snapshot of the current browser size at
* the time the directive comes into existence to ensure the contents
* of the thumbnail are familiar in appearance and aspect ratio.
*/
$scope.height = Math.min(window.innerHeight, 128);
/**
* The overall width of the thumbnail view of the tiled grid of
* clients within the client group, in pixels. This value is
* intentionally based off a snapshot of the current browser size at
* the time the directive comes into existence to ensure the contents
* of the thumbnail are familiar in appearance and aspect ratio.
*/
$scope.width = window.innerWidth / window.innerHeight * $scope.height;
/**
* @borrows ManagedClientGroup.getClientGrid
*/
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
}];
return directive;
}]);
@@ -24,11 +24,12 @@ angular.module('client').factory('guacClientManager', ['$injector',
function guacClientManager($injector) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
// Required services
var $window = $injector.get('$window');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
const $window = $injector.get('$window');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {};
@@ -56,6 +57,65 @@ angular.module('client').factory('guacClientManager', ['$injector',
return storedManagedClients();
};
/**
* Getter/setter which retrieves or sets the array of all active managed
* client groups.
*
* @type Function
*/
const storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns an array of all managed client groups.
*
* @returns {ManagedClientGroup[]>}
* An array of all active managed client groups.
*/
service.getManagedClientGroups = function getManagedClientGroups() {
return storedManagedClientGroups();
};
/**
* Removes the ManagedClient with the given ID from all
* ManagedClientGroups, automatically adjusting the tile size of the
* clients that remain in each group. All client groups that are empty
* after the client is removed will also be removed.
*
* @param {string} id
* The ID of the ManagedClient to remove.
*/
const ungroupManagedClient = function ungroupManagedClient(id) {
const managedClientGroups = storedManagedClientGroups();
// Remove client from all groups
managedClientGroups.forEach(group => {
const removed = _.remove(group.clients, client => (client.id === id));
if (removed.length) {
// Reset focus state if client is being removed from a group
// that isn't currently attached (focus may otherwise be
// retained and result in a newly added connection unexpectedly
// sharing focus)
if (!group.attached)
removed.forEach(client => { client.clientProperties.focused = false; });
// Recalculate group grid if number of clients is changing
ManagedClientGroup.recalculateTiles(group);
}
});
// Remove any groups that are now empty
_.remove(managedClientGroups, group => !group.clients.length);
};
/**
* Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this
@@ -67,13 +127,16 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @returns {Boolean}
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function replaceManagedClient(id) {
service.removeManagedClient = function removeManagedClient(id) {
var managedClients = storedManagedClients();
// Remove client if it exists
if (id in managedClients) {
// Pull client out of any containing groups
ungroupManagedClient(id);
// Disconnect and remove
managedClients[id].client.disconnect();
delete managedClients[id];
@@ -96,22 +159,37 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @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) {
service.replaceManagedClient = function replaceManagedClient(id) {
// Disconnect any existing client
service.removeManagedClient(id);
const managedClients = storedManagedClients();
const managedClientGroups = storedManagedClientGroups();
// Set new client
return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters);
// Remove client if it exists
if (id in managedClients) {
const hadFocus = managedClients[id].clientProperties.focused;
managedClients[id].client.disconnect();
delete managedClients[id];
// Remove client from all groups
managedClientGroups.forEach(group => {
const index = _.findIndex(group.clients, client => (client.id === id));
if (index === -1)
return;
group.clients[index] = managedClients[id] = ManagedClient.getInstance(id);
managedClients[id].clientProperties.focused = hadFocus;
});
}
return managedClients[id];
};
@@ -123,22 +201,21 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @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) {
service.getManagedClient = function getManagedClient(id) {
var managedClients = storedManagedClients();
// Ensure any existing client is removed from its containing group
// prior to being returned
ungroupManagedClient(id);
// Create new managed client if it doesn't already exist
if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
managedClients[id] = ManagedClient.getInstance(id);
// Return existing client
return managedClients[id];
@@ -146,7 +223,88 @@ angular.module('client').factory('guacClientManager', ['$injector',
};
/**
* Disconnects and removes all currently-connected clients.
* Returns the ManagedClientGroup having the given ID. If no such
* ManagedClientGroup exists, a new ManagedClientGroup is created by
* extracting the relevant connections from the ID.
*
* @param {String} id
* The ID of the ManagedClientGroup to retrieve or create.
*
* @returns {ManagedClientGroup}
* The ManagedClientGroup having the given ID.
*/
service.getManagedClientGroup = function getManagedClientGroup(id) {
const managedClientGroups = storedManagedClientGroups();
const existingGroup = _.find(managedClientGroups, (group) => {
return id === ManagedClientGroup.getIdentifier(group);
});
// Prefer to return the existing group if it exactly matches
if (existingGroup)
return existingGroup;
const clients = [];
const clientIds = ManagedClientGroup.getClientIdentifiers(id);
// Separate active clients by whether they should be displayed within
// the current view
clientIds.forEach(function groupClients(id) {
clients.push(service.getManagedClient(id));
});
const group = new ManagedClientGroup({
clients : clients
});
// Focus the first client if there are no clients focused
ManagedClientGroup.verifyFocus(group);
managedClientGroups.push(group);
return group;
};
/**
* Removes the existing ManagedClientGroup having the given ID, if any,
* disconnecting and removing all ManagedClients associated with that
* group. If no such a ManagedClientGroup currently exists, this function
* has no effect.
*
* @param {String} id
* The ID of the ManagedClientGroup to remove.
*
* @returns {Boolean}
* true if a ManagedClientGroup was removed, false otherwise.
*/
service.removeManagedClientGroup = function removeManagedClientGroup(id) {
const managedClients = storedManagedClients();
const managedClientGroups = storedManagedClientGroups();
// Remove all matching groups (there SHOULD only be one)
const removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id);
// Disconnect all clients associated with the removed group(s)
removed.forEach((group) => {
group.clients.forEach((client) => {
const id = client.id;
if (managedClients[id]) {
managedClients[id].client.disconnect();
delete managedClients[id];
}
});
});
return !!removed.length;
};
/**
* Disconnects and removes all currently-connected clients and client
* groups.
*/
service.clear = function clear() {
@@ -156,8 +314,9 @@ angular.module('client').factory('guacClientManager', ['$injector',
for (var id in managedClients)
managedClients[id].client.disconnect();
// Clear managed clients
// Clear managed clients and client groups
storedManagedClients({});
storedManagedClientGroups([]);
};
@@ -103,7 +103,7 @@ body.client {
flex: 0 0 auto;
}
.client-view .client-body .main {
.client-view .client-body .tiled-client-list {
position: absolute;
left: 0;
@@ -125,5 +125,13 @@ body.client {
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
background-image: url('images/x.png');
background-image: url('images/x.svg');
}
.client .drop-pending .display {
background: #3161a9;
}
.client .drop-pending .display > *{
opacity: 0.5;
}
@@ -55,3 +55,8 @@
overflow: hidden;
text-overflow: ellipsis;
}
.connection-select-menu .menu-dropdown .menu-contents .caption .connection,
.connection-select-menu .menu-dropdown .menu-contents .caption .connection-group {
display: inline-block;
}
@@ -48,7 +48,7 @@
height: 100%;
margin: 0 0.375em;
background: url('images/warning.png');
background: url('images/warning.svg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
@@ -37,13 +37,13 @@
/* Directory / file icons */
.file-browser .normal-file > .caption .icon {
background-image: url('images/file.png');
background-image: url('images/file.svg');
}
.file-browser .directory > .caption .icon {
background-image: url('images/folder-closed.png');
background-image: url('images/folder-closed.svg');
}
.file-browser .directory.previous > .caption .icon {
background-image: url('images/folder-up.png');
background-image: url('images/folder-up.svg');
}
@@ -64,7 +64,7 @@
-khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat;
background-position: center center;
background-image: url('images/drive.png');
background-image: url('images/drive.svg');
width: 2em;
height: 2em;
padding: 0;
@@ -108,51 +108,13 @@
}
#guac-menu #keyboard-settings .figure img {
max-width: 100%;
width: 100%;
}
#guac-menu #zoom-settings {
text-align: center;
}
#guac-menu #zoom-out,
#guac-menu #zoom-in,
#guac-menu #zoom-state {
display: inline-block;
vertical-align: middle;
}
#guac-menu #zoom-out,
#guac-menu #zoom-in {
max-width: 3em;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.1);
border-radius: 2em;
margin: 0.5em;
cursor: pointer;
}
#guac-menu #zoom-out img,
#guac-menu #zoom-in img {
max-width: 100%;
opacity: 0.5;
}
#guac-menu #zoom-out:hover,
#guac-menu #zoom-in:hover {
border: 1px solid rgba(0, 0, 0, 1);
background: #CDA;
}
#guac-menu #zoom-out:hover img,
#guac-menu #zoom-in:hover img {
opacity: 1;
}
#guac-menu #zoom-state {
font-size: 2em;
}
#guac-menu #devices .device {
padding: 1em;
@@ -176,7 +138,7 @@
}
#guac-menu #devices .device.filesystem {
background-image: url('images/drive.png');
background-image: url('images/drive.svg');
}
#guac-menu #share-links {
@@ -26,7 +26,7 @@
width: 480px;
background: #EEE;
box-shadow: inset -1px 0 2px white, 1px 0 2px black;
z-index: 10;
z-index: 100;
-webkit-transition: left 0.125s, opacity 0.125s;
-moz-transition: left 0.125s, opacity 0.125s;
-ms-transition: left 0.125s, opacity 0.125s;
@@ -134,27 +134,6 @@
padding-top: 1em;
}
.menu-section input.zoom-ctrl {
width: 2em;
font-size: 1em;
padding: 0;
background: transparent;
border-color: rgba(0, 0, 0, 0.125);
}
.menu-section div.zoom-ctrl {
font-size: 1.5em;
display: inline;
align-content: center;
vertical-align: middle;
}
.menu-section .zoom-ctrl::-webkit-inner-spin-button,
.menu-section .zoom-ctrl::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.menu,
.menu.closed {
left: -480px;
@@ -17,7 +17,79 @@
* under the License.
*/
.client .notification .parameters h3,
.client .notification .parameters .password-field .toggle-password {
.client-status-modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: rgba(0, 0, 0, 0.5);
}
.client-status-modal.shown {
display: block;
}
.client-status-modal guac-modal {
position: absolute;
}
.client-status-modal .notification {
background: rgba(40, 40, 40, 0.75);
color: white;
width: 100%;
max-width: 100%;
padding: 1em;
text-align: center;
border: none;
}
.client-status-modal .notification.error {
background: rgba(112, 9, 8, 0.75)
}
.client-status-modal .notification .title-bar {
display: none
}
.client-status-modal .notification .button {
background: transparent;
border: 2px solid white;
box-shadow: none;
text-shadow: none;
font-weight: normal;
}
.client-status-modal .notification .button:hover {
text-decoration: underline;
background: rgba(255, 255, 255, 0.25);
}
.client-status-modal .notification .button:active {
background: rgba(255, 255, 255, 0.5);
}
.client-status-modal .notification .parameters {
width: 100%;
max-width: 5in;
margin: 0 auto;
}
.client-status-modal .notification .parameters h3,
.client-status-modal .notification .parameters .password-field .toggle-password {
display: none;
}
.client-status-modal .notification .parameters input[type=email],
.client-status-modal .notification .parameters input[type=number],
.client-status-modal .notification .parameters input[type=password],
.client-status-modal .notification .parameters input[type=text],
.client-status-modal .notification .parameters textarea {
background: transparent;
border: 2px solid white;
color: white;
}
@@ -53,6 +53,6 @@
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/share.png');
background-image: url('images/share.svg');
}
@@ -25,11 +25,6 @@ div.thumbnail-main {
font-size: 0px;
}
.thumbnail-main img {
max-width: 100%;
}
.thumbnail-main .display {
position: absolute;
pointer-events: none;
}
@@ -0,0 +1,138 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Overall tiled grid layout.
*/
.tiled-client-grid {
width: 100%;
height: 100%;
}
.tiled-client-grid,
.tiled-client-grid .tiled-client-row,
.tiled-client-grid .tiled-client-cell,
.tiled-client-grid .client-tile {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.tiled-client-grid {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.tiled-client-grid .tiled-client-row {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
/*
* Rendering of individual clients within tiles.
*/
.tiled-client-grid .client-tile {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
line-height: 1.5;
}
.tiled-client-grid .client-tile .client-tile-header {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
margin: 0;
background: #444;
padding: 0 0.25em;
font-size: 0.8em;
color: white;
z-index: 30;
min-height: 1.5em;
}
.tiled-client-grid .client-tile.focused .client-tile-header {
background-color: #3161a9;
}
.tiled-client-grid .client-tile .client-tile-header > * {
-webkit-box-flex: 0;
-webkit-flex: 0;
-ms-flex: 0;
flex: 0;
}
.tiled-client-grid .client-tile .client-tile-header .client-tile-name {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.tiled-client-grid .client-tile .main {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.tiled-client-grid .client-tile-disconnect,
.tiled-client-grid .client-tile-shared-indicator {
max-height: 1em;
height: 100%;
}
.tiled-client-grid .client-tile-shared-indicator {
display: none;
}
.tiled-client-grid .shared .client-tile-shared-indicator {
display: inline;
}
@@ -36,6 +36,14 @@
align-items: center;
}
.transfer-manager h3 {
margin: 0.25em;
font-size: 1em;
margin-bottom: 0;
opacity: 0.5;
text-align: center;
}
.transfer-manager .transfers {
display: table;
padding: 0.25em;
@@ -69,7 +69,7 @@
.transfer.in-progress .progress {
background-color: #EEE;
background-image: url('images/progress.png');
background-image: url('images/progress.svg');
background-size: 16px 16px;
-moz-background-size: 16px 16px;
@@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.client-zoom .client-zoom-out,
.client-zoom .client-zoom-in,
.client-zoom .client-zoom-state {
display: inline-block;
vertical-align: middle;
}
.client-zoom .client-zoom-out,
.client-zoom .client-zoom-in {
max-width: 3em;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.1);
border-radius: 2em;
margin: 0.5em;
cursor: pointer;
}
.client-zoom .client-zoom-out img,
.client-zoom .client-zoom-in img {
width: 100%;
opacity: 0.5;
}
.client-zoom .client-zoom-out:hover,
.client-zoom .client-zoom-in:hover {
border: 1px solid rgba(0, 0, 0, 1);
background: #CDA;
}
.client-zoom .client-zoom-out:hover img,
.client-zoom .client-zoom-in:hover img {
opacity: 1;
}
.client-zoom .client-zoom-state {
font-size: 1.5em;
}
.client-zoom .client-zoom-autofit {
text-align: left;
margin-top: 1em;
}
.client-zoom .client-zoom-state input {
width: 2em;
font-size: 1em;
padding: 0;
background: transparent;
border-color: rgba(0, 0, 0, 0.125);
}
.client-zoom .client-zoom-state input::-webkit-inner-spin-button,
.client-zoom .client-zoom-state input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
@@ -6,15 +6,14 @@
<div class="client-view-content">
<!-- Central portion of view -->
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<div class="client-body" guac-touch-drag="menuDrag">
<!-- Client for current connection -->
<guac-client client="client"></guac-client>
<!-- All other active connections -->
<div id="other-connections">
<guac-client-panel clients="otherClients"></guac-client-panel>
</div>
<!-- All connections in current display -->
<guac-tiled-clients
on-close="closeClientTile($client)"
client-group="clientGroup"
emulate-absolute-mouse="menu.emulateAbsoluteMouse">
</guac-tiled-clients>
</div>
@@ -38,7 +37,7 @@
<!-- File transfers -->
<div id="file-transfer-dialog" ng-show="hasTransfers()">
<guac-file-transfer-manager client="client"></guac-file-transfer-manager>
<guac-file-transfer-manager client-group="clientGroup"></guac-file-transfer-manager>
</div>
<!-- Connection stability warning -->
@@ -48,13 +47,13 @@
<!-- Menu -->
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
<div class="menu-content" ng-if="menu.shown">
<div class="menu-content" ng-if="menu.shown" guac-touch-drag="menuDrag">
<!-- Stationary header -->
<div class="header">
<h2 ng-hide="rootConnectionGroups">{{client.name}}</h2>
<h2 ng-hide="rootConnectionGroups">{{ getName(clientGroup) }}</h2>
<h2 class="connection-select-menu" ng-show="rootConnectionGroups">
<guac-menu menu-title="client.name" interactive="true">
<guac-menu menu-title="getName(clientGroup)" interactive="true">
<div class="all-connections">
<guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups"
@@ -63,6 +62,7 @@
connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
<guac-group-list
connection-groups="filteredRootConnectionGroups"
context="connectionListContext"
templates="{
'connection' : 'app/client/templates/connection.html',
'connection-group' : 'app/client/templates/connectionGroup.html'
@@ -82,7 +82,7 @@
</div>
<!-- Scrollable body -->
<div class="menu-body" guac-touch-drag="menuDrag" guac-scroll="menu.scrollState">
<div class="menu-body" guac-touch-drag="visibleMenuDrag" guac-scroll="menu.scrollState">
<!-- Connection sharing -->
<div class="menu-section" id="share-links" ng-show="isShared()">
@@ -92,7 +92,7 @@
translate="CLIENT.HELP_SHARE_LINK"
translate-values="{LINKS : getShareLinkCount()}"></p>
<table>
<tr ng-repeat="link in client.shareLinks | toArray | orderBy: value.name">
<tr ng-repeat="link in focusedClient.shareLinks | toArray | orderBy: value.name">
<th>{{link.value.name}}</th>
<td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td>
</tr>
@@ -105,24 +105,24 @@
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<guac-clipboard data="client.clipboardData"></guac-clipboard>
<guac-clipboard></guac-clipboard>
</div>
</div>
<!-- Devices -->
<div class="menu-section" id="devices" ng-show="client.filesystems.length">
<div class="menu-section" id="devices" ng-if="focusedClient.filesystems.length">
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content">
<div class="device filesystem" ng-repeat="filesystem in client.filesystems" ng-click="showFilesystemMenu(filesystem)">
<div class="device filesystem" ng-repeat="filesystem in focusedClient.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
</div>
</div>
</div>
<!-- Connection parameters which may be modified while the connection is open -->
<div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
<guac-form namespace="getProtocolNamespace(client.protocol)"
content="client.forms"
<div class="menu-section connection-parameters" id="connection-settings" ng-if="focusedClient.protocol">
<guac-form namespace="getProtocolNamespace(focusedClient.protocol)"
content="focusedClient.forms"
model="menu.connectionParameters"
model-only="true"></guac-form>
</div>
@@ -140,7 +140,7 @@
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.png" alt=""></label></div>
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.svg" alt=""></label></div>
<label><input id="ime-text" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="text"> {{'CLIENT.NAME_INPUT_METHOD_TEXT' | translate}}</label>
<p class="caption"><label for="ime-text">{{'CLIENT.HELP_INPUT_METHOD_TEXT' | translate}} </label></p>
</div>
@@ -155,25 +155,25 @@
</div>
<!-- Mouse mode -->
<div class="menu-section" id="mouse-settings" ng-hide="client.multiTouchSupport">
<div class="menu-section" id="mouse-settings">
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.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>
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.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>
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div>
</div>
@@ -182,20 +182,12 @@
</div>
<!-- Display options -->
<div class="menu-section" id="display-settings">
<div class="menu-section" id="display-settings" ng-if="focusedClient">
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"></div>
<div class="zoom-ctrl">
<input type="number" class="zoom-ctrl" guac-zoom-ctrl
ng-model="client.clientProperties.scale"
ng-model-options="{ updateOn: 'blur submit' }"
ng-change="zoomSet()">%
</div>
<div ng-click="zoomIn()" id="zoom-in"><img src="images/settings/zoom-in.png" alt="+"></div>
<guac-client-zoom client="focusedClient"></guac-client-zoom>
</div>
<div><label><input ng-model="menu.autoFit" ng-change="changeAutoFit()" ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit"> {{'CLIENT.TEXT_ZOOM_AUTO_FIT' | translate}}</label></div>
</div>
</div>
@@ -1,4 +1,9 @@
<a class="connection" ng-href="{{ item.getClientURL() }}">
<div class="icon type" ng-class="item.protocol"></div>
<span class="name">{{item.name}}</span>
</a>
<div class="connection">
<input type="checkbox"
ng-model="context.attachedClients[item.getClientIdentifier()]"
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
<a ng-href="{{ item.getClientURL() }}">
<div class="icon type" ng-class="item.protocol"></div>
<span class="name">{{item.name}}</span>
</a>
</div>
@@ -1,4 +1,10 @@
<a class="connection-group" ng-href="{{ item.getClientURL() }}">
<div ng-show="item.balancing" class="icon type balancer"></div>
<span class="name">{{item.name}}</span>
</a>
<div class="connection-group">
<input type="checkbox"
ng-show="item.balancing"
ng-model="context.attachedClients[item.getClientIdentifier()]"
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
<a ng-href="{{ item.getClientURL() }}">
<div ng-show="item.balancing" class="icon type balancer"></div>
<span class="name">{{item.name}}</span>
</a>
</div>
@@ -1,4 +1,8 @@
<div class="main" guac-resize="mainElementResized">
<div class="main"
ng-class="{ 'drop-pending': dropPending }"
guac-resize="mainElementResized"
guac-touch-drag="clientDrag"
guac-touch-pinch="clientPinch">
<!-- Display -->
<div class="displayOuter">
@@ -0,0 +1,5 @@
<div class="client-status-modal" ng-class="{ shown: status }">
<guac-modal>
<guac-notification notification="status"></guac-notification>
</guac-modal>
</div>
@@ -1,29 +1,27 @@
<div class="client-panel"
ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }">
ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
<!-- Toggle panel visibility -->
<div class="client-panel-handle" ng-click="togglePanel()"></div>
<!-- List of connection thumbnails -->
<ul class="client-panel-connection-list">
<li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]"
ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }"
ng-show="isManaged(client.value)"
<li ng-repeat="clientGroup in clientGroups | orderBy: '-lastUsed'"
ng-if="!clientGroup.attached"
ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
class="client-panel-connection">
<!-- Close connection -->
<button class="close-other-connection" ng-click="disconnect(client.value)">
<button class="close-other-connection" ng-click="disconnect(clientGroup)">
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.png">
src="images/x.svg">
</button>
<!-- Thumbnail -->
<a href="#/client/{{client.value.id}}">
<div class="thumbnail">
<guac-thumbnail client="client.value"></guac-thumbnail>
</div>
<div class="name">{{ client.value.title }}</div>
<a href="#/client/{{ getIdentifier(clientGroup) }}">
<guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
<div class="name">{{ getTitle(clientGroup) }}</div>
</a>
</li>
@@ -0,0 +1,18 @@
<div class="client-zoom">
<div class="client-zoom-editor">
<div ng-click="zoomOut()" class="client-zoom-out"><img src="images/settings/zoom-out.svg" alt="-"></div>
<div class="client-zoom-state">
<input type="number" guac-zoom-ctrl
ng-model="client.clientProperties.scale"
ng-model-options="{ updateOn: 'blur submit' }"
ng-change="zoomSet()">%
</div>
<div ng-click="zoomIn()" class="client-zoom-in"><img src="images/settings/zoom-in.svg" alt="+"></div>
</div>
<div class="client-zoom-autofit">
<label><input ng-model="client.clientProperties.autoFit"
ng-change="changeAutoFit()"
ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit">
{{'CLIENT.TEXT_ZOOM_AUTO_FIT' | translate}}</label>
</div>
</div>
@@ -8,14 +8,14 @@
<!-- Sent/received files -->
<div class="transfer-manager-body">
<div class="transfers">
<guac-file-transfer
transfer="upload"
ng-repeat="upload in client.uploads">
</guac-file-transfer><guac-file-transfer
transfer="download"
ng-repeat="download in client.downloads">
</guac-file-transfer>
<div class="transfer-manager-body-section" ng-repeat="client in clientGroup.clients" ng-show="hasTransfers(client)">
<h3 ng-show="hasMultipleClients(clientGroup)">{{ client.name }}</h3>
<div class="transfers">
<guac-file-transfer
transfer="upload"
ng-repeat="upload in client.uploads">
</guac-file-transfer>
</div>
</div>
</div>
@@ -1,10 +1,11 @@
<div class="thumbnail-main" guac-resize="updateDisplayScale">
<!-- Display -->
<div class="display">
<div class="displayOuter">
<div class="displayMiddle">
<div class="display">
</div>
</div>
</div>
<!-- Dummy background thumbnail -->
<img alt="" ng-src="{{thumbnail}}">
</div>
@@ -0,0 +1,28 @@
<div class="tiled-client-grid">
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
<div class="tiled-client-cell" ng-repeat="client in clientRow">
<div class="client-tile" ng-if="client"
ng-class="{
'focused' : client.clientProperties.focused,
'shared' : isShared(client)
}"
guac-click="getFocusAssignmentCallback(client)">
<h3 class="client-tile-header" ng-if="hasMultipleClients(clientGroup)">
<img class="client-tile-shared-indicator" src="images/share-white.svg">
<span class="client-tile-name">{{ client.title }}</span>
<img ng-click="onClose({ '$client' : client })"
class="client-tile-disconnect"
ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.svg">
</h3>
<guac-client client="client" emulate-absolute-mouse="emulateAbsoluteMouse"></guac-client>
<!-- Client-specific status/error dialog -->
<guac-client-notification client="client"></guac-client-notification>
</div>
</div>
</div>
</div>
@@ -0,0 +1,14 @@
<div class="tiled-client-grid" ng-style="{
'width' : width + 'px',
'height' : height + 'px',
}">
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
<div class="tiled-client-cell" ng-repeat="client in clientRow">
<div class="client-tile" ng-if="client">
<guac-thumbnail client="client"></guac-thumbnail>
</div>
</div>
</div>
</div>
@@ -22,9 +22,6 @@
*/
angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
// Required services
var preferenceService = $injector.get('preferenceService');
/**
* Object used for interacting with a guacClient directive.
*
@@ -69,19 +66,11 @@ angular.module('client').factory('ClientProperties', ['$injector', function defi
this.maxScale = template.maxScale || 3;
/**
* Whether or not the client should listen to keyboard events.
*
* Whether this client should receive keyboard events.
*
* @type Boolean
*/
this.keyboardEnabled = template.keyboardEnabled || true;
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type Boolean
*/
this.emulateAbsoluteMouse = template.emulateAbsoluteMouse || preferenceService.preferences.emulateAbsoluteMouse;
this.focused = template.focused || false;
/**
* The relative Y coordinate of the scroll offset of the display within
@@ -24,34 +24,34 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
function defineManagedClient($rootScope, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData');
var ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFilesystem = $injector.get('ManagedFilesystem');
var ManagedFileUpload = $injector.get('ManagedFileUpload');
var ManagedShareLink = $injector.get('ManagedShareLink');
const ClientProperties = $injector.get('ClientProperties');
const ClientIdentifier = $injector.get('ClientIdentifier');
const ClipboardData = $injector.get('ClipboardData');
const ManagedArgument = $injector.get('ManagedArgument');
const ManagedClientState = $injector.get('ManagedClientState');
const ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
const ManagedDisplay = $injector.get('ManagedDisplay');
const ManagedFilesystem = $injector.get('ManagedFilesystem');
const ManagedFileUpload = $injector.get('ManagedFileUpload');
const ManagedShareLink = $injector.get('ManagedShareLink');
// Required services
var $document = $injector.get('$document');
var $q = $injector.get('$q');
var $rootScope = $injector.get('$rootScope');
var $window = $injector.get('$window');
var activeConnectionService = $injector.get('activeConnectionService');
var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var preferenceService = $injector.get('preferenceService');
var requestService = $injector.get('requestService');
var schemaService = $injector.get('schemaService');
var tunnelService = $injector.get('tunnelService');
var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory');
var guacImage = $injector.get('guacImage');
var guacVideo = $injector.get('guacVideo');
const $document = $injector.get('$document');
const $q = $injector.get('$q');
const $rootScope = $injector.get('$rootScope');
const $window = $injector.get('$window');
const activeConnectionService = $injector.get('activeConnectionService');
const authenticationService = $injector.get('authenticationService');
const clipboardService = $injector.get('clipboardService');
const connectionGroupService = $injector.get('connectionGroupService');
const connectionService = $injector.get('connectionService');
const preferenceService = $injector.get('preferenceService');
const requestService = $injector.get('requestService');
const tunnelService = $injector.get('tunnelService');
const guacAudio = $injector.get('guacAudio');
const guacHistory = $injector.get('guacHistory');
const guacImage = $injector.get('guacImage');
const guacVideo = $injector.get('guacVideo');
/**
* The minimum amount of time to wait between updates to the client
@@ -63,8 +63,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
/**
* 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.
* client while it is active, allowing it to be maintained in the
* background. One or more ManagedClients are grouped within
* ManagedClientGroups before being attached to the client view.
*
* @constructor
* @param {ManagedClient|Object} [template={}]
@@ -83,16 +84,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
this.id = template.id;
/**
* The time that the connection was last brought to the foreground of
* the current tab, as the number of milliseconds elapsed since
* midnight of January 1, 1970 UTC. If the connection has not yet been
* viewed, this will be 0.
*
* @type Number
*/
this.lastUsed = template.lastUsed || 0;
/**
* The actual underlying Guacamole client.
*
@@ -156,16 +147,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
this.thumbnail = template.thumbnail;
/**
* The current clipboard contents.
*
* @type ClipboardData
*/
this.clipboardData = template.clipboardData || new ClipboardData({
type : 'text/plain',
data : ''
});
/**
* The current state of all parameters requested by the server via
* "required" instructions, where each object key is the name of a
@@ -260,25 +241,30 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
* @param {ClientIdentifier} identifier
* The identifier representing the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @param {number} [width]
* The optimal display width, in local CSS pixels. If omitted, the
* browser window width will be used.
*
* @param {number} [height]
* The optimal display height, in local CSS pixels. If omitted, the
* browser window height will be used.
*
* @returns {Promise.<String>}
* A promise which resolves with the string of connection parameters to
* be passed to the Guacamole client, once the string is ready.
*/
var getConnectString = function getConnectString(identifier, connectionParameters) {
const getConnectString = function getConnectString(identifier, width, height) {
var deferred = $q.defer();
const deferred = $q.defer();
// 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;
const pixel_density = $window.devicePixelRatio || 1;
const optimal_dpi = pixel_density * 96;
const optimal_width = width * pixel_density;
const optimal_height = height * pixel_density;
// Build base connect string
var connectString =
let connectString =
"token=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource)
+ "&GUAC_ID=" + encodeURIComponent(identifier.id)
@@ -286,8 +272,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
+ "&GUAC_WIDTH=" + Math.floor(optimal_width)
+ "&GUAC_HEIGHT=" + Math.floor(optimal_height)
+ "&GUAC_DPI=" + Math.floor(optimal_dpi)
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone)
+ (connectionParameters ? '&' + connectionParameters : '');
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone);
// Add audio mimetypes to connect string
guacAudio.supported.forEach(function(mimetype) {
@@ -347,22 +332,20 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
/**
* Creates a new ManagedClient, connecting it to the specified connection
* or group.
* Creates a new ManagedClient representing the specified connection or
* connection group. The ManagedClient will not initially be connected,
* and must be explicitly connected by invoking ManagedClient.connect().
*
* @param {String} id
* The ID of the connection or group to connect to. This String must be
* a valid ClientIdentifier string, as would be generated by
* ClientIdentifier.toString().
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {ManagedClient}
* A new ManagedClient instance which is connected to the connection or
* A new ManagedClient instance which represents the connection or
* connection group having the given ID.
*/
ManagedClient.getInstance = function getInstance(id, connectionParameters) {
ManagedClient.getInstance = function getInstance(id) {
var tunnel;
@@ -450,8 +433,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.ConnectionState.IDLE);
break;
// Ignore "connecting" state
case 1: // Connecting
// Conneccting
case 1:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connected + waiting
@@ -465,9 +450,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED);
// Send any clipboard data already provided
if (managedClient.clipboardData)
ManagedClient.setClipboard(managedClient, managedClient.clipboardData);
// Sync current clipboard data
clipboardService.getClipboard().then((data) => {
ManagedClient.setClipboard(managedClient, data);
}, angular.noop);
// Begin streaming audio input if possible
requestAudioStream(client);
@@ -562,12 +548,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Set clipboard contents once stream is finished
reader.onend = function textComplete() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = new ClipboardData({
type : mimetype,
data : data
});
});
clipboardService.setClipboard(new ClipboardData({
source : managedClient.id,
type : mimetype,
data : data
}))['catch'](angular.noop);
};
}
@@ -576,12 +561,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = function blobComplete() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = new ClipboardData({
type : mimetype,
data : reader.getBlob()
});
});
clipboardService.setClipboard(new ClipboardData({
source : managedClient.id,
type : mimetype,
data : reader.getBlob()
}))['catch'](angular.noop);
};
}
@@ -607,7 +591,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Handle any received filesystem objects
client.onfilesystem = function fileSystemReceived(object, name) {
$rootScope.$apply(function exposeFilesystem() {
managedClient.filesystems.push(ManagedFilesystem.getInstance(object, name));
managedClient.filesystems.push(ManagedFilesystem.getInstance(managedClient, object, name));
});
};
@@ -627,11 +611,8 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Parse connection details from ID
var clientIdentifier = ClientIdentifier.fromString(id);
// Connect the Guacamole client
getConnectString(clientIdentifier, connectionParameters)
.then(function connectClient(connectString) {
client.connect(connectString);
});
// Defer actually connecting the Guacamole client until
// ManagedClient.connect() is explicitly invoked
// If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
@@ -671,6 +652,40 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
/**
* Connects the given ManagedClient instance to its associated connection
* or connection group. If the ManagedClient has already been connected,
* including if connected but subsequently disconnected, this function has
* no effect.
*
* @param {ManagedClient} managedClient
* The ManagedClient to connect.
*
* @param {number} [width]
* The optimal display width, in local CSS pixels. If omitted, the
* browser window width will be used.
*
* @param {number} [height]
* The optimal display height, in local CSS pixels. If omitted, the
* browser window height will be used.
*/
ManagedClient.connect = function connect(managedClient, width, height) {
// Ignore if already connected
if (managedClient.clientState.connectionState !== ManagedClientState.ConnectionState.IDLE)
return;
// Parse connection details from ID
const clientIdentifier = ClientIdentifier.fromString(managedClient.id);
// Connect the Guacamole client
getConnectString(clientIdentifier, width, height)
.then(function connectClient(connectString) {
managedClient.client.connect(connectString);
});
};
/**
* Uploads the given file to the server through the given Guacamole client.
* The file transfer can be monitored through the corresponding entry in
@@ -710,7 +725,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
/**
* Sends the given clipboard data over the given Guacamole client, setting
* the contents of the remote clipboard to the data provided.
* the contents of the remote clipboard to the data provided. If the given
* clipboard data was originally received from that client, the data is
* ignored and this function has no effect.
*
* @param {ManagedClient} managedClient
* The ManagedClient over which the given clipboard data is to be sent.
@@ -720,6 +737,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
ManagedClient.setClipboard = function setClipboard(managedClient, data) {
// Ignore clipboard data that was received from this connection
if (data.source === managedClient.id)
return;
var writer;
// Create stream with proper mimetype
@@ -873,6 +894,18 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
/**
* Returns whether the given client has any associated file transfers,
* regardless of those file transfers' state.
*
* @returns {boolean}
* true if there are any file transfers associated with the
* given client, false otherise.
*/
ManagedClient.hasTransfers = function hasTransfers(client) {
return !!(client && client.uploads && client.uploads.length);
};
/**
* Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this
@@ -0,0 +1,359 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedClientGroup class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClientGroup', ['$injector', function defineManagedClientGroup($injector) {
/**
* Object which serves as a grouping of ManagedClients. Each
* ManagedClientGroup may be attached, detached, and reattached dynamically
* from different client views, with its contents automatically displayed
* in a tiled arrangment if needed.
*
* @constructor
* @param {ManagedClientGroup|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientGroup.
*/
const ManagedClientGroup = function ManagedClientGroup(template) {
// Use empty object by default
template = template || {};
/**
* The time that this group was last brought to the foreground of
* the current tab, as the number of milliseconds elapsed since
* midnight of January 1, 1970 UTC. If the group has not yet been
* viewed, this will be 0.
*
* @type Number
*/
this.lastUsed = template.lastUsed || 0;
/**
* Whether this ManagedClientGroup is currently attached to the client
* interface (true) or is running in the background (false).
*
* @type {boolean}
* @default false
*/
this.attached = template.attached || false;
/**
* The clients that should be displayed within the client interface
* when this group is attached.
*
* @type {ManagedClient[]}
* @default []
*/
this.clients = template.clients || [];
/**
* The number of rows that should be used when arranging the clients
* within this group in a grid. By default, this value is automatically
* calculated from the number of clients.
*
* @type {number}
*/
this.rows = template.rows || ManagedClientGroup.getRows(this);
/**
* The number of columns that should be used when arranging the clients
* within this group in a grid. By default, this value is automatically
* calculated from the number of clients.
*
* @type {number}
*/
this.columns = template.columns || ManagedClientGroup.getColumns(this);
};
/**
* Updates the number of rows and columns stored within the given
* ManagedClientGroup such that the clients within the group are evenly
* distributed. This function should be called whenever the size of a
* group changes.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup that should be updated.
*/
ManagedClientGroup.recalculateTiles = function recalculateTiles(group) {
const recalculated = new ManagedClientGroup({
clients : group.clients
});
group.rows = recalculated.rows;
group.columns = recalculated.columns;
};
/**
* Returns the unique ID representing the given ManagedClientGroup or set
* of client IDs. The ID of a ManagedClientGroup consists simply of the
* IDs of all its ManagedClients, separated by periods.
*
* @param {ManagedClientGroup|string[]} group
* The ManagedClientGroup or array of client IDs to determine the
* ManagedClientGroup ID of.
*
* @returns {string}
* The unique ID representing the given ManagedClientGroup, or the
* unique ID that would represent a ManagedClientGroup containing the
* clients with the given IDs.
*/
ManagedClientGroup.getIdentifier = function getIdentifier(group) {
if (!_.isArray(group))
group = _.map(group.clients, client => client.id);
return group.join('.');
};
/**
* Returns an array of client identifiers for all clients contained within
* the given ManagedClientGroup. Order of the identifiers is preserved
* with respect to the order of the clients within the group.
*
* @param {ManagedClientGroup|string} group
* The ManagedClientGroup to retrieve the client identifiers from,
* or its ID.
*
* @returns {string[]}
* The client identifiers of all clients contained within the given
* ManagedClientGroup.
*/
ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) {
if (_.isString(group))
return group.split(/\./);
return group.clients.map(client => client.id);
};
/**
* Returns the number of columns that should be used to evenly arrange
* all provided clients in a tiled grid.
*
* @returns {Number}
* The number of columns that should be used for the grid of
* clients.
*/
ManagedClientGroup.getColumns = function getColumns(group) {
if (!group.clients.length)
return 0;
return Math.ceil(Math.sqrt(group.clients.length));
};
/**
* Returns the number of rows that should be used to evenly arrange all
* provided clients in a tiled grid.
*
* @returns {Number}
* The number of rows that should be used for the grid of clients.
*/
ManagedClientGroup.getRows = function getRows(group) {
if (!group.clients.length)
return 0;
return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group));
};
/**
* Returns the title which should be displayed as the page title if the
* given client group is attached to the interface.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the title of.
*
* @returns {string}
* The title of the given ManagedClientGroup.
*/
ManagedClientGroup.getTitle = function getTitle(group) {
// Use client-specific title if only one client
if (group.clients.length === 1)
return group.clients[0].title;
// With multiple clients, somehow combining multiple page titles would
// be confusing. Instead, use the combined names.
return ManagedClientGroup.getName(group);
};
/**
* Returns the combined names of all clients within the given
* ManagedClientGroup, as determined by the names of the associated
* connections or connection groups.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the name of.
*
* @returns {string}
* The combined names of all clients within the given
* ManagedClientGroup.
*/
ManagedClientGroup.getName = function getName(group) {
// Generate a name from ONLY the focused clients, unless there are no
// focused clients
let relevantClients = _.filter(group.clients, client => client.clientProperties.focused);
if (!relevantClients.length)
relevantClients = group.clients;
return _.filter(relevantClients, (client => !!client.name)).map(client => client.name).join(', ') || '...';
};
/**
* A callback that is invoked for a ManagedClient within a ManagedClientGroup.
*
* @callback ManagedClientGroup~clientCallback
* @param {ManagedClient} client
* The relevant ManagedClient.
*
* @param {number} row
* The row number of the client within the tiled grid, where 0 is the
* first row.
*
* @param {number} column
* The column number of the client within the tiled grid, where 0 is
* the first column.
*
* @param {number} index
* The index of the client within the relevant
* {@link ManagedClientGroup#clients} array.
*/
/**
* Loops through each of the clients associated with the given
* ManagedClientGroup, invoking the given callback for each client.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to loop through.
*
* @param {ManagedClientGroup~clientCallback} callback
* The callback to invoke for each of the clients within the given
* ManagedClientGroup.
*/
ManagedClientGroup.forEach = function forEach(group, callback) {
let current = 0;
for (let row = 0; row < group.rows; row++) {
for (let column = 0; column < group.columns; column++) {
callback(group.clients[current], row, column, current);
current++;
if (current >= group.clients.length)
return;
}
}
};
/**
* Returns whether the given ManagedClientGroup contains more than one
* client.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to test.
*
* @returns {boolean}
* true if two or more clients are currently present in the given
* group, false otherwise.
*/
ManagedClientGroup.hasMultipleClients = function hasMultipleClients(group) {
return group && group.clients.length > 1;
};
/**
* Returns a two-dimensional array of all ManagedClients within the given
* group, arranged in the grid defined by {@link ManagedClientGroup#rows}
* and {@link ManagedClientGroup#columns}. If any grid cell lacks a
* corresponding client (because the number of clients does not divide
* evenly into a grid), that cell will be null.
*
* For the sake of AngularJS scope watches, the results of calling this
* function are cached and will always favor modifying an existing array
* over creating a new array, even for nested arrays.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup defining the tiled grid arrangement of
* ManagedClients.
*
* @returns {ManagedClient[][]}
* A two-dimensional array of all ManagedClients within the given
* group.
*/
ManagedClientGroup.getClientGrid = function getClientGrid(group) {
let index = 0;
// Operate on cached copy of grid
const clientGrid = group._grid || (group._grid = []);
// Delete any rows in excess of the required size
clientGrid.splice(group.rows);
for (let row = 0; row < group.rows; row++) {
// Prefer to use existing column arrays, deleting any columns in
// excess of the required size
const currentRow = clientGrid[row] || (clientGrid[row] = []);
currentRow.splice(group.columns);
for (let column = 0; column < group.columns; column++) {
currentRow[column] = group.clients[index++] || null;
}
}
return clientGrid;
};
/**
* Verifies that focus is assigned to at least one client in the given
* group. If no client has focus, focus is assigned to the first client in
* the group.
*
* @param {ManagedClientGroup} group
* The group to verify.
*/
ManagedClientGroup.verifyFocus = function verifyFocus(group) {
// Focus the first client if there are no clients focused
if (group.clients.length >= 1 && _.findIndex(group.clients, client => client.clientProperties.focused) === -1) {
group.clients[0].clientProperties.focused = true;
}
};
return ManagedClientGroup;
}]);
@@ -42,6 +42,14 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
// Use empty object by default
template = template || {};
/**
* The client that originally received the "filesystem" instruction
* that resulted in the creation of this ManagedFilesystem.
*
* @type ManagedClient
*/
this.client = template.client;
/**
* The Guacamole filesystem object, as received via a "filesystem"
* instruction.
@@ -162,6 +170,10 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* and human-readable name. Upon creation, a request to populate the
* contents of the root directory will be automatically dispatched.
*
* @param {ManagedClient} client
* The client that originally received the "filesystem" instruction
* that resulted in the creation of this ManagedFilesystem.
*
* @param {Guacamole.Object} object
* The Guacamole.Object defining the filesystem.
*
@@ -171,10 +183,11 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* @returns {ManagedFilesystem}
* The newly-created ManagedFilesystem.
*/
ManagedFilesystem.getInstance = function getInstance(object, name) {
ManagedFilesystem.getInstance = function getInstance(client, object, name) {
// Init new filesystem object
var managedFilesystem = new ManagedFilesystem({
client : client,
object : object,
name : name,
root : new ManagedFilesystem.File({
@@ -196,9 +209,6 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* client and filesystem. The browser will automatically start the
* download upon completion of this function.
*
* @param {ManagedClient} managedClient
* The ManagedClient from which the file is to be downloaded.
*
* @param {ManagedFilesystem} managedFilesystem
* The ManagedFilesystem from which the file is to be downloaded. Any
* path information provided must be relative to this filesystem.
@@ -206,7 +216,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* @param {String} path
* The full, absolute path of the file to download.
*/
ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) {
ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) {
// Request download
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
@@ -215,7 +225,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
var filename = path.match(/(.*[\\/])?(.*)/)[2];
// Start download
tunnelService.downloadStream(managedClient.tunnel.uuid, stream, mimetype, filename);
tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid, stream, mimetype, filename);
});
@@ -18,16 +18,19 @@
*/
/**
* A directive provides an editor whose contents are exposed via a
* ClipboardData object via the "data" attribute. If this data should also be
* synced to the local clipboard, or sent via a connected Guacamole client
* using a "guacClipboard" event, it is up to external code to do so.
* A directive provides an editor for the clipboard content maintained by
* clipboardService. Changes to the clipboard by clipboardService will
* automatically be reflected in the editor, and changes in the editor will
* automatically be reflected in the clipboard by clipboardService.
*/
angular.module('clipboard').directive('guacClipboard', ['$injector',
function guacClipboard($injector) {
// Required types
var ClipboardData = $injector.get('ClipboardData');
const ClipboardData = $injector.get('ClipboardData');
// Required services
const clipboardService = $injector.get('clipboardService');
/**
* Configuration object for the guacClipboard directive.
@@ -40,20 +43,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
templateUrl : 'app/clipboard/templates/guacClipboard.html'
};
// Scope properties exposed by the guacClipboard directive
config.scope = {
/**
* The data to display within the field provided by this directive. This
* data will modified or replaced when the user manually alters the
* contents of the field.
*
* @type ClipboardData
*/
data : '='
};
// guacClipboard directive controller
config.controller = ['$scope', '$injector', '$element',
function guacClipboardController($scope, $injector, $element) {
@@ -75,12 +64,27 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
var updateClipboardData = function updateClipboardData() {
// Read contents of clipboard textarea
$scope.$evalAsync(function assignClipboardText() {
$scope.data = new ClipboardData({
type : 'text/plain',
data : element.value
});
});
clipboardService.setClipboard(new ClipboardData({
type : 'text/plain',
data : element.value
}));
};
/**
* Updates the contents of the clipboard editor to the given data.
*
* @param {ClipboardData} data
* The ClipboardData to display within the clipboard editor for
* editing.
*/
const updateClipboardEditor = function updateClipboardEditor(data) {
// If the clipboard data is a string, render it as text
if (typeof data.data === 'string')
element.value = data.data;
// Ignore other data types for now
};
@@ -89,17 +93,15 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
element.addEventListener('input', updateClipboardData);
element.addEventListener('change', updateClipboardData);
// Watch clipboard for new data, updating the clipboard textarea as
// necessary
$scope.$watch('data', function clipboardDataChanged(data) {
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function clipboardChanged(event, data) {
updateClipboardEditor(data);
});
// If the clipboard data is a string, render it as text
if (typeof data.data === 'string')
element.value = data.data;
// Ignore other data types for now
}); // end $scope.data watch
// Init clipboard editor with current clipboard contents
clipboardService.getClipboard().then((data) => {
updateClipboardEditor(data);
}, angular.noop);
}];
@@ -18,17 +18,31 @@
*/
/**
* A service for accessing local clipboard data.
* A service for maintaining and accessing clipboard data. If possible, this
* service will leverage the local clipboard. If the local clipboard is not
* available, an internal in-memory clipboard will be used instead.
*/
angular.module('clipboard').factory('clipboardService', ['$injector',
function clipboardService($injector) {
// Get required services
var $q = $injector.get('$q');
var $window = $injector.get('$window');
const $q = $injector.get('$q');
const $window = $injector.get('$window');
const $rootScope = $injector.get('$rootScope');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
var ClipboardData = $injector.get('ClipboardData');
const ClipboardData = $injector.get('ClipboardData');
/**
* Getter/setter which retrieves or sets the current stored clipboard
* contents. The stored clipboard contents are strictly internal to
* Guacamole, and may not reflect the local clipboard if local clipboard
* access is unavailable.
*
* @type Function
*/
const storedClipboardData = sessionStorageFactory.create(new ClipboardData());
var service = {};
@@ -175,7 +189,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
* A promise that will resolve if setting the clipboard was successful,
* and will reject if it failed.
*/
service.setLocalClipboard = function setLocalClipboard(data) {
const setLocalClipboard = function setLocalClipboard(data) {
var deferred = $q.defer();
@@ -423,7 +437,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
* if getting the clipboard was successful, and will reject if it
* failed.
*/
service.getLocalClipboard = function getLocalClipboard() {
const getLocalClipboard = function getLocalClipboard() {
// If the clipboard is already being read, do not overlap the read
// attempts; instead share the result across all requests
@@ -548,6 +562,64 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
};
/**
* Returns the current value of the internal clipboard shared across all
* active Guacamole connections running within the current browser tab. If
* access to the local clipboard is available, the internal clipboard is
* first synchronized with the current local clipboard contents. If access
* to the local clipboard is unavailable, only the internal clipboard will
* be used.
*
* @return {Promise.<ClipboardData>}
* A promise that will resolve with the contents of the internal
* clipboard, first retrieving those contents from the local clipboard
* if permission to do so has been granted. This promise is always
* resolved.
*/
service.getClipboard = function getClipboard() {
return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData());
};
/**
* Sets the content of the internal clipboard shared across all active
* Guacamole connections running within the current browser tab. If
* access to the local clipboard is available, the local clipboard is
* first set to the provided clipboard content. If access to the local
* clipboard is unavailable, only the internal clipboard will be used. A
* "guacClipboard" event will be broadcast with the assigned data once the
* operation has completed.
*
* @param {ClipboardData} data
* The data to assign to the clipboard.
*
* @return {Promise}
* A promise that will resolve after the clipboard content has been
* set. This promise is always resolved.
*/
service.setClipboard = function setClipboard(data) {
return setLocalClipboard(data)['catch'](angular.noop).finally(() => {
// Update internal clipboard and broadcast event notifying of
// updated contents
storedClipboardData(data);
$rootScope.$broadcast('guacClipboard', data);
});
};
/**
* Resynchronizes the local and internal clipboards, setting the contents
* of the internal clipboard to that of the local clipboard (if local
* clipboard access is granted) and broadcasting a "guacClipboard" event
* with the current internal clipboard contents for consumption by external
* components like the "guacClient" directive.
*/
service.resyncClipboard = function resyncClipboard() {
service.getClipboard().then(function clipboardRead(data) {
return service.setClipboard(data);
}, angular.noop);
};
return service;
}]);
@@ -47,7 +47,7 @@
display: block;
margin: 0 auto;
border: 1px solid black;
background: url('images/checker.png');
background: url('images/checker.svg');
}
.clipboard-service-target {
@@ -36,6 +36,15 @@ angular.module('clipboard').factory('ClipboardData', [function defineClipboardDa
// Use empty object by default
template = template || {};
/**
* The ID of the ManagedClient handling the remote desktop connection
* that originated this clipboard data, or null if the data originated
* from the clipboard editor or local clipboard.
*
* @type {string}
*/
this.source = template.source;
/**
* The mimetype of the data currently stored within the clipboard.
*
@@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which provides handling of click and click-like touch events.
* The state of Shift and Ctrl modifiers is tracked through these click events
* to allow for specific handling of Shift+Click and Ctrl+Click.
*/
angular.module('element').directive('guacClick', [function guacClick() {
return {
restrict: 'A',
link: function linkGuacClick($scope, $element, $attrs) {
/**
* A callback that is invoked by the guacClick directive when a
* click or click-like event is received.
*
* @callback guacClick~callback
* @param {boolean} shift
* Whether Shift was held down at the time the click occurred.
*
* @param {boolean} ctrl
* Whether Ctrl or Meta (the Mac "Command" key) was held down
* at the time the click occurred.
*/
/**
* The callback to invoke when a click or click-like event is
* received on the assocaited element.
*
* @type guacClick~callback
*/
const guacClick = $scope.$eval($attrs.guacClick);
/**
* The element which will register the click.
*
* @type Element
*/
const element = $element[0];
/**
* Whether either Shift key is currently pressed.
*
* @type boolean
*/
let shift = false;
/**
* Whether either Ctrl key is currently pressed. To allow the
* Command key to be used on Mac platforms, this flag also
* considers the state of either Meta key.
*
* @type boolean
*/
let ctrl = false;
/**
* Updates the state of the {@link shift} and {@link ctrl} flags
* based on which keys are currently marked as held down by the
* given Guacamole.Keyboard.
*
* @param {Guacamole.Keyboard} keyboard
* The Guacamole.Keyboard instance to read key states from.
*/
const updateModifiers = function updateModifiers(keyboard) {
shift = !!(
keyboard.pressed[0xFFE1] // Left shift
|| keyboard.pressed[0xFFE2] // Right shift
);
ctrl = !!(
keyboard.pressed[0xFFE3] // Left ctrl
|| keyboard.pressed[0xFFE4] // Right ctrl
|| keyboard.pressed[0xFFE7] // Left meta (command)
|| keyboard.pressed[0xFFE8] // Right meta (command)
);
};
// Update tracking of modifier states for each key press
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
updateModifiers(keyboard);
});
// Update tracking of modifier states for each key release
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
updateModifiers(keyboard);
});
// Fire provided callback for each mouse-initiated "click" event ...
element.addEventListener('click', function elementClicked(e) {
if (element.contains(e.target))
$scope.$apply(() => guacClick(shift, ctrl));
});
// ... and for touch-initiated click-like events
element.addEventListener('touchstart', function elementClicked(e) {
if (element.contains(e.target))
$scope.$apply(() => guacClick(shift, ctrl));
});
} // end guacClick link function
};
}]);
@@ -38,10 +38,10 @@
/* Icon for unmasking passwords */
.form-field .password-field input[type=password] ~ .icon.toggle-password {
background-image: url('images/action-icons/guac-show-pass.png');
background-image: url('images/action-icons/guac-show-pass.svg');
}
/* Icon for masking passwords */
.form-field .password-field input[type=text] ~ .icon.toggle-password {
background-image: url('images/action-icons/guac-hide-pass.png');
background-image: url('images/action-icons/guac-hide-pass.svg');
}
@@ -18,7 +18,8 @@
*/
/**
* A directive which displays the contents of a connection group.
* A directive which displays the recently-accessed connections nested beneath
* each of the given connection groups.
*/
angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() {
@@ -44,21 +45,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
// Required types
var ActiveConnection = $injector.get('ActiveConnection');
var ClientIdentifier = $injector.get('ClientIdentifier');
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.
*
@@ -68,16 +60,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
/**
* 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.
* true if recent connections are present, false otherwise.
*/
$scope.hasRecentConnections = function hasRecentConnections() {
return !!($scope.activeConnections.length || $scope.recentConnections.length);
return !!$scope.recentConnections.length;
};
/**
@@ -149,7 +137,6 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
$scope.$watch("rootGroups", function setRootGroups(rootGroups) {
// Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = [];
// Produce collection of visible objects
@@ -160,29 +147,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
});
}
var managedClients = guacClientManager.getManagedClients();
// Add all active connections
for (var id in managedClients) {
// Get corresponding managed client
var client = managedClients[id];
// Add active connections for clients with associated visible objects
if (id in visibleObjects) {
var object = visibleObjects[id];
$scope.activeConnections.push(new ActiveConnection(object.name, client));
}
}
// Add any recent connections that are visible
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects
if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) {
if (historyEntry.id in visibleObjects) {
var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));
@@ -3,23 +3,6 @@
<!-- Text displayed if no recent connections exist -->
<p class="placeholder" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
<!-- All active connections -->
<div ng-repeat="activeConnection in activeConnections" class="connection">
<a href="#/client/{{activeConnection.client.id}}">
<!-- Connection thumbnail -->
<div class="thumbnail">
<guac-thumbnail client="activeConnection.client"></guac-thumbnail>
</div>
<!-- Connection name -->
<div class="caption">
<span class="name">{{activeConnection.name}}</span>
</div>
</a>
</div>
<!-- All recent connections -->
<div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}">
@@ -181,10 +181,11 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
})
// Client view
.when('/client/:id/:params?', {
.when('/client/:id', {
bodyClassName : 'client',
templateUrl : 'app/client/templates/client.html',
controller : 'clientController',
reloadOnUrl : false,
resolve : { updateCurrentToken: updateCurrentToken }
})
@@ -24,11 +24,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) {
// Required services
var $document = $injector.get('$document');
var $route = $injector.get('$route');
var $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification');
const $document = $injector.get('$document');
const $route = $injector.get('$route');
const $window = $injector.get('$window');
const clipboardService = $injector.get('clipboardService');
const guacNotification = $injector.get('guacNotification');
const guacClientManager = $injector.get('guacClientManager');
/**
* The error that prevents the current page from rendering at all. If no
@@ -43,6 +44,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/
$scope.guacNotification = guacNotification;
/**
* All currently-active connections, grouped into their corresponding
* tiled views.
*
* @type ManagedClientGroup[]
*/
$scope.getManagedClientGroups = guacClientManager.getManagedClientGroups;
/**
* The message to display to the user as instructions for the login
* process.
@@ -154,9 +163,8 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Broadcast keydown events
keyboard.onkeydown = function onkeydown(keysym) {
// Do not handle key events if not logged in or if a notification is
// shown
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
// Do not handle key events if not logged in
if ($scope.applicationState !== ApplicationState.READY)
return true;
// Warn of pending keydown
@@ -175,7 +183,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Do not handle key events if not logged in or if a notification is
// shown
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
if ($scope.applicationState !== ApplicationState.READY)
return;
// Warn of pending keyup
@@ -199,25 +207,15 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
keyboard.reset();
});
/**
* Checks whether the clipboard data has changed, firing a new
* "guacClipboard" event if it has.
*/
var checkClipboard = function checkClipboard() {
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
$scope.$broadcast('guacClipboard', data);
}, angular.noop);
};
// Attempt to read the clipboard if it may have changed
$window.addEventListener('load', checkClipboard, true);
$window.addEventListener('copy', checkClipboard);
$window.addEventListener('cut', checkClipboard);
$window.addEventListener('load', clipboardService.resyncClipboard, true);
$window.addEventListener('copy', clipboardService.resyncClipboard);
$window.addEventListener('cut', clipboardService.resyncClipboard);
$window.addEventListener('focus', function focusGained(e) {
// Only recheck clipboard if it's the window itself that gained focus
if (e.target === $window)
checkClipboard();
clipboardService.resyncClipboard();
}, true);
@@ -48,3 +48,12 @@
from { opacity: 1; }
to { opacity: 0; }
}
/**
* popin: Increase in size and opacity from invisibly tiny and transparent to
* full size and opaque.
*/
@keyframes popin {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); }
}
@@ -105,30 +105,30 @@ input[type="submit"]:disabled, button:disabled, button.danger:disabled {
.button.logout::before,
button.logout::before {
background-image: url('images/action-icons/guac-logout.png');
background-image: url('images/action-icons/guac-logout.svg');
}
.button.reconnect::before,
button.reconnect::before {
background-image: url('images/circle-arrows.png');
background-image: url('images/circle-arrows.svg');
}
.button.manage::before,
button.manage::before {
background-image: url('images/action-icons/guac-config.png');
background-image: url('images/action-icons/guac-config.svg');
}
.button.back::before,
button.back::before {
background-image: url('images/action-icons/guac-back.png');
background-image: url('images/action-icons/guac-back.svg');
}
.button.home::before,
button.home::before {
background-image: url('images/action-icons/guac-home.png');
background-image: url('images/action-icons/guac-home.svg');
}
.button.change-password::before,
button.change-password::before {
background-image: url('images/action-icons/guac-key.png');
background-image: url('images/action-icons/guac-key.svg');
}
@@ -38,7 +38,7 @@
.fatal-page-error h1::before {
content: ' ';
display: inline-block;
background: url('images/warning.png');
background: url('images/warning.svg');
background-repeat: no-repeat;
height: 1em;
width: 1em;
@@ -49,7 +49,7 @@
top: 50%;
left: 50%;
background-image: url('images/cog.png');
background-image: url('images/cog.svg');
background-size: 96px 96px;
background-position: center center;
background-repeat: no-repeat;
@@ -20,7 +20,7 @@
#other-connections .client-panel {
display: none;
position: absolute;
position: fixed;
right: 0;
bottom: 0;
@@ -56,7 +56,7 @@
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
background-image: url(images/arrows/right.png);
background-image: url(images/arrows/right.svg);
opacity: 0.5;
}
@@ -66,7 +66,7 @@
}
#other-connections .client-panel.hidden .client-panel-handle {
background-image: url(images/arrows/left.png);
background-image: url(images/arrows/left.svg);
}
#other-connections .client-panel-connection-list {
@@ -92,6 +92,7 @@
background: black;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
animation: 0.1s linear 0s popin;
opacity: 0.5;
transition: opacity 0.25s;
@@ -101,11 +102,6 @@
}
#other-connections .client-panel-connection .thumbnail-main img {
max-width: none;
max-height: 128px;
}
#other-connections .client-panel-connection a[href]::before {
display: block;
@@ -118,7 +114,7 @@
width: 100%;
z-index: 1;
background: url('images/warning-white.png');
background: url('images/warning-white.svg');
background-size: 48px;
background-position: center;
background-repeat: no-repeat;
@@ -160,6 +156,7 @@
#other-connections button.close-other-connection img {
background: #A43;
border-radius: 18px;
width: 100%;
max-width: 18px;
padding: 3px;
}
@@ -52,10 +52,10 @@ table.sorted th.sort-primary:after {
background-size: 1em 1em;
background-position: right center;
background-repeat: no-repeat;
background-image: url('images/arrows/down.png');
background-image: url('images/arrows/down.svg');
}
table.sorted th.sort-primary.sort-descending:after {
background-image: url('images/arrows/up.png');
background-image: url('images/arrows/up.svg');
}
@@ -149,27 +149,27 @@ div.section {
*/
.icon.user {
background-image: url('images/user-icons/guac-user.png');
background-image: url('images/user-icons/guac-user.svg');
}
.icon.user.add {
background-image: url('images/action-icons/guac-user-add.png');
background-image: url('images/action-icons/guac-user-add.svg');
}
.icon.user-group {
background-image: url('images/user-icons/guac-user-group.png');
background-image: url('images/user-icons/guac-user-group.svg');
}
.icon.user-group.add {
background-image: url('images/action-icons/guac-user-group-add.png');
background-image: url('images/action-icons/guac-user-group-add.svg');
}
.icon.connection {
background-image: url('images/protocol-icons/guac-plug.png');
background-image: url('images/protocol-icons/guac-plug.svg');
}
.icon.connection.add {
background-image: url('images/action-icons/guac-monitor-add.png');
background-image: url('images/action-icons/guac-monitor-add.svg');
}
.connection .icon,
@@ -187,30 +187,30 @@ div.section {
}
.connection-group > .caption .icon {
background-image: url('images/folder-closed.png');
background-image: url('images/folder-closed.svg');
}
.connection-group.expanded > .caption .icon {
background-image: url('images/folder-open.png');
background-image: url('images/folder-open.svg');
}
.connection .icon {
background-image: url('images/protocol-icons/guac-plug.png');
background-image: url('images/protocol-icons/guac-plug.svg');
}
.connection .icon.kubernetes,
.connection .icon.ssh,
.connection .icon.telnet {
background-image: url('images/protocol-icons/guac-text.png');
background-image: url('images/protocol-icons/guac-text.svg');
}
.connection .icon.vnc,
.connection .icon.rdp {
background-image: url('images/protocol-icons/guac-monitor.png');
background-image: url('images/protocol-icons/guac-monitor.svg');
}
.sharing-profile .icon {
background-image: url('images/share.png');
background-image: url('images/share.svg');
}
/*
@@ -223,7 +223,7 @@ div.section {
}
.connection-group.empty.balancer .icon {
background-image: url('images/protocol-icons/guac-monitor.png');
background-image: url('images/protocol-icons/guac-monitor.svg');
}
.expandable.expanded > .children > .list-item {
@@ -259,16 +259,16 @@ div.section {
}
.expandable > .caption .icon.expand {
background-image: url('images/group-icons/guac-closed.png');
background-image: url('images/group-icons/guac-closed.svg');
}
.expandable.expanded > .caption .icon.expand {
background-image: url('images/group-icons/guac-open.png');
background-image: url('images/group-icons/guac-open.svg');
}
.expandable.empty > .caption .icon.expand {
opacity: 0.25;
background-image: url('images/group-icons/guac-open.png');
background-image: url('images/group-icons/guac-open.svg');
}
.history th,
@@ -22,7 +22,7 @@
}
.filter .search-string {
background-image: url('images/magnifier.png');
background-image: url('images/magnifier.svg');
background-repeat: no-repeat;
background-size: 1.75em;
background-position: 0.25em center;
@@ -71,17 +71,17 @@
}
.pager .icon.first-page {
background-image: url('images/action-icons/guac-first-page.png');
background-image: url('images/action-icons/guac-first-page.svg');
}
.pager .icon.prev-page {
background-image: url('images/action-icons/guac-prev-page.png');
background-image: url('images/action-icons/guac-prev-page.svg');
}
.pager .icon.next-page {
background-image: url('images/action-icons/guac-next-page.png');
background-image: url('images/action-icons/guac-next-page.svg');
}
.pager .icon.last-page {
background-image: url('images/action-icons/guac-last-page.png');
background-image: url('images/action-icons/guac-last-page.svg');
}
@@ -103,7 +103,7 @@
-moz-background-size: 3em 3em;
-webkit-background-size: 3em 3em;
-khtml-background-size: 3em 3em;
background-image: url("images/guac-tricolor.png");
background-image: url("images/guac-tricolor.svg");
}
.login-ui.continuation .login-dialog {
@@ -39,11 +39,11 @@
}
.manage-user-group .page-tabs .page-list li.read-only a[href]:before {
background-image: url('images/lock.png');
background-image: url('images/lock.svg');
}
.manage-user-group .page-tabs .page-list li.unlinked a[href]:before {
background-image: url('images/plus.png');
background-image: url('images/plus.svg');
}
.manage-user-group .page-tabs .page-list li.unlinked a[href] {
@@ -56,7 +56,7 @@
}
.manage-user-group .page-tabs .page-list li.linked a[href]:before {
background-image: url('images/checkmark.png');
background-image: url('images/checkmark.svg');
}
.manage-user-group .notice.read-only {
@@ -39,11 +39,11 @@
}
.manage-user .page-tabs .page-list li.read-only a[href]:before {
background-image: url('images/lock.png');
background-image: url('images/lock.svg');
}
.manage-user .page-tabs .page-list li.unlinked a[href]:before {
background-image: url('images/plus.png');
background-image: url('images/plus.svg');
}
.manage-user .page-tabs .page-list li.unlinked a[href] {
@@ -56,7 +56,7 @@
}
.manage-user .page-tabs .page-list li.linked a[href]:before {
background-image: url('images/checkmark.png');
background-image: url('images/checkmark.svg');
}
.manage-user .notice.read-only {
@@ -12,12 +12,12 @@
<!-- Abbreviated list of only the currently selected objects -->
<div class="abbreviated-related-objects">
<img src="images/arrows/right.png" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()">
<img src="images/arrows/down.png" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
<img src="images/arrows/right.svg" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()">
<img src="images/arrows/down.svg" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
<p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p>
<ul>
<li ng-repeat="identifier in identifiers | filter: filterString">
<label><img src="images/x-red.png" alt="Remove" class="remove"
<label><img src="images/x-red.svg" alt="Remove" class="remove"
ng-click="removeIdentifier(identifier)"
ng-show="isEditable[identifier]"><span class="identifier">{{ identifier }}</span>
</label>
@@ -92,7 +92,7 @@
background-repeat: no-repeat;
background-size: 1em;
background-position: center center;
background-image: url('images/arrows/down.png');
background-image: url('images/arrows/down.svg');
}
@@ -54,7 +54,7 @@
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/user-icons/guac-user.png');
background-image: url('images/user-icons/guac-user.svg');
}
@@ -64,23 +64,23 @@
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
background-image: url('images/protocol-icons/guac-monitor.png');
background-image: url('images/protocol-icons/guac-monitor.svg');
}
.user-menu .menu-dropdown .menu-contents li a[href="#/"] {
background-image: url('images/action-icons/guac-home-dark.png');
background-image: url('images/action-icons/guac-home-dark.svg');
}
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/users"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/connections"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/sessions"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/preferences"] {
background-image: url('images/action-icons/guac-config-dark.png');
background-image: url('images/action-icons/guac-config-dark.svg');
}
.user-menu .menu-dropdown .menu-contents li a.logout {
background-image: url('images/action-icons/guac-logout-dark.png');
background-image: url('images/action-icons/guac-logout-dark.svg');
}
.user-menu .menu-dropdown .menu-contents .profile {
@@ -18,20 +18,30 @@
*/
guac-modal {
display: table;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 10;
overflow: hidden;
}
guac-modal .modal-contents {
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
width: 100%;
text-align: center;
display: table-cell;
vertical-align: middle;
}
guac-modal {
@@ -74,7 +74,7 @@
.notification .progress {
width: 100%;
background: #C2C2C2 url('images/progress.png');
background: #C2C2C2 url('images/progress.svg');
background-size: 16px 16px;
-moz-background-size: 16px 16px;
-webkit-background-size: 16px 16px;
@@ -45,17 +45,17 @@ a.button.add-connection-group::before {
}
a.button.add-user::before {
background-image: url('images/action-icons/guac-user-add.png');
background-image: url('images/action-icons/guac-user-add.svg');
}
a.button.add-user-group::before {
background-image: url('images/action-icons/guac-user-group-add.png');
background-image: url('images/action-icons/guac-user-group-add.svg');
}
a.button.add-connection::before {
background-image: url('images/action-icons/guac-monitor-add.png');
background-image: url('images/action-icons/guac-monitor-add.svg');
}
a.button.add-connection-group::before {
background-image: url('images/action-icons/guac-group-add.png');
background-image: url('images/action-icons/guac-group-add.svg');
}
@@ -72,7 +72,7 @@
<div class="choice">
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<p class="caption"><label for="absolute">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div>
</div>
@@ -81,7 +81,7 @@
<div class="choice">
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<p class="caption"><label for="relative">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;stroke:none" d="M41.249 6.437a9593.495 9593.495 0 0 1-25.563 25.52c8.532 8.525 17.045 17.068 25.563 25.606l7.065-7.065L29.816 32l18.498-18.498z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="m28.031 4.813-2 5.25a22.529 22.529 0 0 0-5.312 2.218L15 10.562 10.562 15l1.72 5.719a22.529 22.529 0 0 0-2.22 5.312l-5.25 2v7.938l5.25 2a22.529 22.529 0 0 0 2.22 5.312L10.561 49 15 53.438l5.719-1.72a22.529 22.529 0 0 0 5.312 2.22l2 5.25h7.938l2-5.25a22.529 22.529 0 0 0 5.312-2.22L49 53.439 53.438 49l-1.72-5.719a22.529 22.529 0 0 0 2.22-5.312l5.25-2V28.03l-5.25-2a22.529 22.529 0 0 0-2.22-5.312L53.439 15 49 10.562l-5.719 1.72a22.529 22.529 0 0 0-5.312-2.22l-2-5.25zM32 16.5c8.552 0 15.5 6.948 15.5 15.5S40.552 47.5 32 47.5 16.5 40.552 16.5 32 23.448 16.5 32 16.5z"/><path transform="translate(.31 .77)" d="M42.854 31.23a11.164 11.164 0 1 1-22.327 0 11.164 11.164 0 1 1 22.327 0z" style="fill:#000;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;stroke:none" d="m28.031 4.813-2 5.25a22.529 22.529 0 0 0-5.312 2.218L15 10.562 10.562 15l1.72 5.719a22.529 22.529 0 0 0-2.22 5.312l-5.25 2v7.938l5.25 2a22.529 22.529 0 0 0 2.22 5.312L10.561 49 15 53.438l5.719-1.72a22.529 22.529 0 0 0 5.312 2.22l2 5.25h7.938l2-5.25a22.529 22.529 0 0 0 5.312-2.22L49 53.439 53.438 49l-1.72-5.719a22.529 22.529 0 0 0 2.22-5.312l5.25-2V28.03l-5.25-2a22.529 22.529 0 0 0-2.22-5.312L53.439 15 49 10.562l-5.719 1.72a22.529 22.529 0 0 0-5.312-2.22l-2-5.25zM32 16.5c8.552 0 15.5 6.948 15.5 15.5S40.552 47.5 32 47.5 16.5 40.552 16.5 32 23.448 16.5 32 16.5z"/><path transform="translate(.31 .77)" d="M42.854 31.23a11.164 11.164 0 1 1-22.327 0 11.164 11.164 0 1 1 22.327 0z" style="fill:#fff;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="M-88.192 11.032h7.29v37.312h-7.29zm32.66 0a7001.475 7001.475 0 0 1-18.656 18.624c6.227 6.222 12.44 12.457 18.657 18.688l5.156-5.156-13.5-13.5 13.5-13.5z" transform="translate(126.933 -8.678) scale(1.3702)"/></svg>

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.05000003;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" d="M328.653 258.485c-8.817 0-15.841 13.705-17.413 31.686H195.06c-11.575 0-20.838 10.086-20.838 22.551v224.94c0 12.466 9.263 22.266 20.838 22.266h292.594c11.575 0 21.124-9.8 21.124-22.265v-224.94c0-8.152.026-54.131-18.84-54.238zm-79.643 88.778h184.977c7.313 0 13.131 5.656 13.131 12.845v129.883c0 7.19-5.818 12.846-13.131 12.846H249.01c-7.313 0-13.13-5.657-13.13-12.846V360.108c0-7.189 5.817-12.845 13.13-12.845z" transform="translate(-19.073 1.734) scale(.10947)"/><path style="fill:#fff;stroke:none" d="M57.322 8.871h7.518v22.135h-7.518z" transform="translate(-15.013 -2.006)"/><path transform="rotate(90 -6.504 -8.51)" style="fill:#fff;stroke:none" d="M16.179-72.149h7.518v22.135h-7.518z"/></svg>

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g transform="translate(-31.663 -41) scale(.19138)" style="fill:#000"><rect ry="10" rx="10" y="359.24" x="196.801" height="158.064" width="184.408" style="fill:#000;fill-opacity:1;stroke:none"/><path d="M391.226 235.731c-36.12 0-65.643 29.524-65.643 65.643v69.887c0 .55-.013 1.087 0 1.633h31.352c-.026-.564 0-1.06 0-1.633v-69.887c0-19.281 15.01-34.291 34.29-34.291h4.573c19.28 0 34.29 15.01 34.29 34.29v69.888c0 .573.027 1.069 0 1.633h31.352c.014-.546 0-1.083 0-1.633v-69.887c0-36.12-29.523-65.643-65.642-65.643z" style="font-size:medium;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000;fill:#000;fill-opacity:1;stroke:none;stroke-width:31.37258148;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path d="m-69 10.156-19.063 14.5h5.313V41.25h11.063v-6.875h5.374v6.875h11.063V24.656h5.313L-69 10.156zm5.406 15.719h4.282v4.281h-4.282v-4.281zm-15.093.094h4.28v4.281h-4.28v-4.281z" style="fill:#000;fill-opacity:1;stroke:none" transform="translate(147.925 -11.148) scale(1.67869)"/><path style="fill:#000;fill-opacity:1;stroke:none" d="M-61 13.5h4.75v7.75H-61z" transform="translate(147.925 -11.148) scale(1.67869)"/></svg>

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path d="m-69 10.156-19.063 14.5h5.313V41.25h11.063v-6.875h5.374v6.875h11.063V24.656h5.313L-69 10.156zm5.406 15.719h4.282v4.281h-4.282v-4.281zm-15.093.094h4.28v4.281h-4.28v-4.281z" style="fill:#fff;fill-opacity:1;stroke:none" transform="translate(147.925 -11.148) scale(1.67869)"/><path style="fill:#fff;fill-opacity:1;stroke:none" d="M-61 13.5h4.75v7.75H-61z" transform="translate(147.925 -11.148) scale(1.67869)"/></svg>

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;fill-opacity:1;stroke:none" d="M31.844 9.594c-9.553.074-13.219-.126-13.219 4.656v15a2.716 2.716 0 0 0 2.719 2.719H41.75a2.716 2.716 0 0 0 2.719-2.719v-15c0-3.789-3.073-4.73-12.625-4.656zm-.407 3.719c4.165-.02 4.965 2.797 4.407 4.718-.559 1.922-2.906 1.856-4.375 1.844-1.47-.012-3.553.155-4.188-1.844-.635-1.998-.008-4.7 4.157-4.718z" transform="translate(.453)"/><path style="fill:#fff;fill-opacity:1;stroke:none" d="M77.702 20.455h7.305s.104 17.639 0 17.86c-.105.222-2.182 1.792-3.074 1.725-.892-.068-4.13-1.37-4.231-1.724-.101-.355 1.51-2.698 1.498-3.194-.011-.497-1.522-2.735-1.498-3.381.023-.646 1.492-2.338 1.498-2.727.006-.39-1.548-2.375-1.498-2.805.05-.43 1.57-2.067 1.498-2.497-.071-.43-1.498-3.257-1.498-3.257z" transform="translate(-94.13 -2.653) scale(1.54998)"/></svg>

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="M-88.192 11.032h7.29v37.312h-7.29zm32.66 0a7001.475 7001.475 0 0 1-18.656 18.624c6.227 6.222 12.44 12.457 18.657 18.688l5.156-5.156-13.5-13.5 13.5-13.5z" transform="matrix(-1.3702 0 0 1.3702 -62.933 -8.678)"/></svg>

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;fill-opacity:1;stroke:#000;stroke-width:.00547374;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M2.094 1.486C.934 1.486 0 2.496 0 3.744v42.66c0 1.249.935 2.242 2.094 2.242h.302V5.17l19.272 14v29.476h7.9c1.16 0 2.094-.991 2.094-2.24V3.744c0-1.248-.934-2.258-2.094-2.258H2.094z"/><path d="M208.19 52.448c-10.236 0-18.485 9.336-18.485 20.894V468.33c0 11.558 8.249 20.743 18.486 20.743h242.637c10.237 0 18.486-9.185 18.486-20.743V73.342c0-11.558-8.249-20.894-18.486-20.894z" style="fill:none;fill-opacity:1;stroke:#000;stroke-width:47.44955063;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" transform="matrix(.07096 .05263 0 .10015 -11.461 -11.512)"/><path transform="matrix(.64819 .48075 0 .91482 .415 -3.146)" d="M26.5 30.125a1.625 1.625 0 1 1-3.25 0 1.625 1.625 0 0 1 3.25 0z" style="fill:#000;fill-opacity:1;stroke:#000;stroke-width:5.19446087;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/><path transform="rotate(90 -12.504 -.51)" style="fill:#000;stroke:none" d="M16.179-72.149h7.518v12.885h-7.518z"/><path style="fill:#000;fill-opacity:1;stroke:none" d="M48.023 17.5 29.988 27.913V7.087z" transform="matrix(-.6396 0 0 1 68.454 14.5)"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.00547374;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M2.094 1.486C.934 1.486 0 2.496 0 3.744v42.66c0 1.249.935 2.242 2.094 2.242h.302V5.17l19.272 14v29.476h7.9c1.16 0 2.094-.991 2.094-2.24V3.744c0-1.248-.934-2.258-2.094-2.258H2.094z"/><path d="M208.19 52.448c-10.236 0-18.485 9.336-18.485 20.894V468.33c0 11.558 8.249 20.743 18.486 20.743h242.637c10.237 0 18.486-9.185 18.486-20.743V73.342c0-11.558-8.249-20.894-18.486-20.894z" style="fill:none;fill-opacity:1;stroke:#fff;stroke-width:47.44955063;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" transform="matrix(.07096 .05263 0 .10015 -11.461 -11.512)"/><path transform="matrix(.64819 .48075 0 .91482 .415 -3.146)" d="M26.5 30.125a1.625 1.625 0 1 1-3.25 0 1.625 1.625 0 0 1 3.25 0z" style="fill:#fff;fill-opacity:1;stroke:#fff;stroke-width:5.19446087;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/><path transform="rotate(90 -12.504 -.51)" style="fill:#fff;stroke:none" d="M16.179-72.149h7.518v12.885h-7.518z"/><path style="fill:#fff;fill-opacity:1;stroke:none" d="M48.023 17.5 29.988 27.913V7.087z" transform="matrix(-.6396 0 0 1 68.454 14.5)"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g transform="translate(-19.073 1.734) scale(.10947)" style="fill:#fff"><path style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.00956892;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" d="M4 .656c-2.215 0-4 1.927-4 4.313V48.03c0 2.386 1.785 4.282 4 4.282h56c2.215 0 4-1.896 4-4.282V4.97c0-2.386-1.785-4.313-4-4.313H4zm10.313 10.938h35.374c1.4 0 2.532 1.093 2.532 2.469v24.843c0 1.376-1.132 2.469-2.532 2.469H14.313c-1.4 0-2.532-1.093-2.532-2.469V14.063c0-1.376 1.132-2.47 2.531-2.47z" transform="translate(174.22 231.813) scale(5.22525)"/><rect ry="18" rx="18" y="524.187" x="244.268" height="38.636" width="197.179" style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.05000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"/></g><path style="fill:#fff;stroke:none" d="M57.322 8.871h7.518v22.135h-7.518z" transform="translate(-15.013 -2.006)"/><path transform="rotate(90 -6.504 -8.51)" style="fill:#fff;stroke:none" d="M16.179-72.149h7.518v22.135h-7.518z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="M22.751 6.437a9593.495 9593.495 0 0 0 25.563 25.52c-8.532 8.525-17.045 17.068-25.563 25.606l-7.065-7.065L34.184 32 15.686 13.502z"/></svg>

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 B

Some files were not shown because too many files have changed in this diff Show More