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) { angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
// Required services // Required services
var guacClientManager = $injector.get('guacClientManager'); const guacClientManager = $injector.get('guacClientManager');
var sessionStorageFactory = $injector.get('sessionStorageFactory'); const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types // 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 * Getter/setter for the boolean flag controlling whether the client panel
@@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
scope: { scope: {
/** /**
* The ManagedClient instances associated with the active * The ManagedClientGroup instances associated with the active
* connections to be displayed within this panel. * connections to be displayed within this panel.
* *
* @type ManagedClient[]|Object.<String, ManagedClient> * @type ManagedClientGroup[]
*/ */
clients : '=' clientGroups : '='
}, },
templateUrl: 'app/client/templates/guacClientPanel.html', templateUrl: 'app/client/templates/guacClientPanel.html',
@@ -75,71 +76,68 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
$scope.panelHidden = panelHidden; $scope.panelHidden = panelHidden;
/** /**
* Returns whether this panel currently has any clients associated * Returns whether this panel currently has any client groups
* with it. * associated with it.
* *
* @return {Boolean} * @return {Boolean}
* true if at least one client is associated with this panel, * true if at least one client group is associated with this
* false otherwise. * panel, false otherwise.
*/ */
$scope.hasClients = function hasClients() { $scope.hasClientGroups = function hasClientGroups() {
return !!_.find($scope.clients, $scope.isManaged); return $scope.clientGroups && $scope.clientGroups.length;
}; };
/** /**
* Returns whether the status of the given client has changed in a * @borrows ManagedClientGroup.getIdentifier
* way that requires the user's attention. This may be due to an */
* error, or due to a server-initiated disconnect. $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 * @param {ManagedClientGroup} clientGroup
* The client to test. * The client group to test.
* *
* @returns {Boolean} * @returns {Boolean}
* true if the given client requires the user's attention, * true if the given client requires the user's attention,
* false otherwise. * 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 // Test whether the client has encountered an error
switch (client.clientState.connectionState) { switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR: case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR: case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED: case ManagedClientState.ConnectionState.DISCONNECTED:
return true; return true;
} }
return false; return false;
}) !== -1;
}; };
/** /**
* Returns whether the given client is currently being managed by * Initiates an orderly disconnect of all clients within the given
* the guacClientManager service. * 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 * @param {ManagedClientGroup} clientGroup
* The client to test. * The group of clients to disconnect.
*
* @returns {Boolean}
* true if the given client is being managed by the
* guacClientManager service, false otherwise.
*/ */
$scope.isManaged = function isManaged(client) { $scope.disconnect = function disconnect(clientGroup) {
return !!guacClientManager.getManagedClients()[client.id]; guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
};
/**
* 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);
}; };
/** /**
@@ -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, replace: true,
scope: { scope: {
/**
* The client whose file transfers should be managed by this
* directive.
*
* @type ManagedClient
*/
client : '=',
/** /**
* @type ManagedFilesystem * @type ManagedFilesystem
*/ */
@@ -116,7 +108,7 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
* The file to download. * The file to download.
*/ */
$scope.downloadFile = function downloadFile(file) { $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: { scope: {
/** /**
* The client whose file transfers should be managed by this * The client group whose file transfers should be managed by this
* directive. * 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) { controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
// Required types // 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 * Determines whether the given file transfer state indicates an
@@ -74,17 +76,29 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
*/ */
$scope.clearCompletedTransfers = function clearCompletedTransfers() { $scope.clearCompletedTransfers = function clearCompletedTransfers() {
// Nothing to clear if no client attached // Nothing to clear if no client group attached
if (!$scope.client) if (!$scope.clientGroup)
return; return;
// Remove completed uploads // Remove completed uploads
$scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) { $scope.clientGroup.clients.forEach(client => {
return isInProgress(upload.transferState); 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 // Required services
var $window = $injector.get('$window'); 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. * 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 // Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) { $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); $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) { function guacClientManager($injector) {
// Required types // Required types
var ManagedClient = $injector.get('ManagedClient'); const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
// Required services // Required services
var $window = $injector.get('$window'); const $window = $injector.get('$window');
var sessionStorageFactory = $injector.get('sessionStorageFactory'); const sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {}; var service = {};
@@ -56,6 +57,65 @@ angular.module('client').factory('guacClientManager', ['$injector',
return storedManagedClients(); 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 * Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this * 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} * @returns {Boolean}
* true if an existing client was removed, false otherwise. * true if an existing client was removed, false otherwise.
*/ */
service.removeManagedClient = function replaceManagedClient(id) { service.removeManagedClient = function removeManagedClient(id) {
var managedClients = storedManagedClients(); var managedClients = storedManagedClients();
// Remove client if it exists // Remove client if it exists
if (id in managedClients) { if (id in managedClients) {
// Pull client out of any containing groups
ungroupManagedClient(id);
// Disconnect and remove // Disconnect and remove
managedClients[id].client.disconnect(); managedClients[id].client.disconnect();
delete managedClients[id]; delete managedClients[id];
@@ -96,22 +159,37 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @param {String} id * @param {String} id
* The ID of the connection whose ManagedClient should be retrieved. * 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} * @returns {ManagedClient}
* The ManagedClient associated with the connection having the given * The ManagedClient associated with the connection having the given
* ID. * ID.
*/ */
service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) { service.replaceManagedClient = function replaceManagedClient(id) {
// Disconnect any existing client const managedClients = storedManagedClients();
service.removeManagedClient(id); const managedClientGroups = storedManagedClientGroups();
// Set new client // Remove client if it exists
return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters); 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 * @param {String} id
* The ID of the connection whose ManagedClient should be retrieved. * 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} * @returns {ManagedClient}
* The ManagedClient associated with the connection having the given * The ManagedClient associated with the connection having the given
* ID. * ID.
*/ */
service.getManagedClient = function getManagedClient(id, connectionParameters) { service.getManagedClient = function getManagedClient(id) {
var managedClients = storedManagedClients(); 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 // Create new managed client if it doesn't already exist
if (!(id in managedClients)) if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id, connectionParameters); managedClients[id] = ManagedClient.getInstance(id);
// Return existing client // Return existing client
return managedClients[id]; 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() { service.clear = function clear() {
@@ -156,8 +314,9 @@ angular.module('client').factory('guacClientManager', ['$injector',
for (var id in managedClients) for (var id in managedClients)
managedClients[id].client.disconnect(); managedClients[id].client.disconnect();
// Clear managed clients // Clear managed clients and client groups
storedManagedClients({}); storedManagedClients({});
storedManagedClientGroups([]);
}; };
@@ -103,7 +103,7 @@ body.client {
flex: 0 0 auto; flex: 0 0 auto;
} }
.client-view .client-body .main { .client-view .client-body .tiled-client-list {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -125,5 +125,13 @@ body.client {
background-size: 1em; background-size: 1em;
background-position: 0.75em center; background-position: 0.75em center;
padding-left: 2.5em; 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; overflow: hidden;
text-overflow: ellipsis; 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%; height: 100%;
margin: 0 0.375em; margin: 0 0.375em;
background: url('images/warning.png'); background: url('images/warning.svg');
background-size: contain; background-size: contain;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -37,13 +37,13 @@
/* Directory / file icons */ /* Directory / file icons */
.file-browser .normal-file > .caption .icon { .file-browser .normal-file > .caption .icon {
background-image: url('images/file.png'); background-image: url('images/file.svg');
} }
.file-browser .directory > .caption .icon { .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 { .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; -khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center center; background-position: center center;
background-image: url('images/drive.png'); background-image: url('images/drive.svg');
width: 2em; width: 2em;
height: 2em; height: 2em;
padding: 0; padding: 0;
@@ -108,51 +108,13 @@
} }
#guac-menu #keyboard-settings .figure img { #guac-menu #keyboard-settings .figure img {
max-width: 100%; width: 100%;
} }
#guac-menu #zoom-settings { #guac-menu #zoom-settings {
text-align: center; 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 { #guac-menu #devices .device {
padding: 1em; padding: 1em;
@@ -176,7 +138,7 @@
} }
#guac-menu #devices .device.filesystem { #guac-menu #devices .device.filesystem {
background-image: url('images/drive.png'); background-image: url('images/drive.svg');
} }
#guac-menu #share-links { #guac-menu #share-links {
@@ -26,7 +26,7 @@
width: 480px; width: 480px;
background: #EEE; background: #EEE;
box-shadow: inset -1px 0 2px white, 1px 0 2px black; 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; -webkit-transition: left 0.125s, opacity 0.125s;
-moz-transition: left 0.125s, opacity 0.125s; -moz-transition: left 0.125s, opacity 0.125s;
-ms-transition: left 0.125s, opacity 0.125s; -ms-transition: left 0.125s, opacity 0.125s;
@@ -134,27 +134,6 @@
padding-top: 1em; 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,
.menu.closed { .menu.closed {
left: -480px; left: -480px;
@@ -17,7 +17,79 @@
* under the License. * under the License.
*/ */
.client .notification .parameters h3, .client-status-modal {
.client .notification .parameters .password-field .toggle-password {
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; 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-repeat: no-repeat;
background-size: 1em; background-size: 1em;
background-position: 0.5em center; 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; font-size: 0px;
} }
.thumbnail-main img {
max-width: 100%;
}
.thumbnail-main .display { .thumbnail-main .display {
position: absolute;
pointer-events: none; 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; align-items: center;
} }
.transfer-manager h3 {
margin: 0.25em;
font-size: 1em;
margin-bottom: 0;
opacity: 0.5;
text-align: center;
}
.transfer-manager .transfers { .transfer-manager .transfers {
display: table; display: table;
padding: 0.25em; padding: 0.25em;
@@ -69,7 +69,7 @@
.transfer.in-progress .progress { .transfer.in-progress .progress {
background-color: #EEE; background-color: #EEE;
background-image: url('images/progress.png'); background-image: url('images/progress.svg');
background-size: 16px 16px; background-size: 16px 16px;
-moz-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"> <div class="client-view-content">
<!-- Central portion of view --> <!-- 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 --> <!-- All connections in current display -->
<guac-client client="client"></guac-client> <guac-tiled-clients
on-close="closeClientTile($client)"
<!-- All other active connections --> client-group="clientGroup"
<div id="other-connections"> emulate-absolute-mouse="menu.emulateAbsoluteMouse">
<guac-client-panel clients="otherClients"></guac-client-panel> </guac-tiled-clients>
</div>
</div> </div>
@@ -38,7 +37,7 @@
<!-- File transfers --> <!-- File transfers -->
<div id="file-transfer-dialog" ng-show="hasTransfers()"> <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> </div>
<!-- Connection stability warning --> <!-- Connection stability warning -->
@@ -48,13 +47,13 @@
<!-- Menu --> <!-- Menu -->
<div class="menu" ng-class="{open: menu.shown}" id="guac-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 --> <!-- Stationary header -->
<div class="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"> <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"> <div class="all-connections">
<guac-group-list-filter connection-groups="rootConnectionGroups" <guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups" filtered-connection-groups="filteredRootConnectionGroups"
@@ -63,6 +62,7 @@
connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter> connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
<guac-group-list <guac-group-list
connection-groups="filteredRootConnectionGroups" connection-groups="filteredRootConnectionGroups"
context="connectionListContext"
templates="{ templates="{
'connection' : 'app/client/templates/connection.html', 'connection' : 'app/client/templates/connection.html',
'connection-group' : 'app/client/templates/connectionGroup.html' 'connection-group' : 'app/client/templates/connectionGroup.html'
@@ -82,7 +82,7 @@
</div> </div>
<!-- Scrollable body --> <!-- 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 --> <!-- Connection sharing -->
<div class="menu-section" id="share-links" ng-show="isShared()"> <div class="menu-section" id="share-links" ng-show="isShared()">
@@ -92,7 +92,7 @@
translate="CLIENT.HELP_SHARE_LINK" translate="CLIENT.HELP_SHARE_LINK"
translate-values="{LINKS : getShareLinkCount()}"></p> translate-values="{LINKS : getShareLinkCount()}"></p>
<table> <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> <th>{{link.value.name}}</th>
<td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td> <td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td>
</tr> </tr>
@@ -105,24 +105,24 @@
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3> <h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content"> <div class="content">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p> <p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<guac-clipboard data="client.clipboardData"></guac-clipboard> <guac-clipboard></guac-clipboard>
</div> </div>
</div> </div>
<!-- Devices --> <!-- 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> <h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content"> <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}} {{filesystem.name}}
</div> </div>
</div> </div>
</div> </div>
<!-- Connection parameters which may be modified while the connection is open --> <!-- Connection parameters which may be modified while the connection is open -->
<div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol"> <div class="menu-section connection-parameters" id="connection-settings" ng-if="focusedClient.protocol">
<guac-form namespace="getProtocolNamespace(client.protocol)" <guac-form namespace="getProtocolNamespace(focusedClient.protocol)"
content="client.forms" content="focusedClient.forms"
model="menu.connectionParameters" model="menu.connectionParameters"
model-only="true"></guac-form> model-only="true"></guac-form>
</div> </div>
@@ -140,7 +140,7 @@
<!-- Text input --> <!-- Text input -->
<div class="choice"> <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> <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> <p class="caption"><label for="ime-text">{{'CLIENT.HELP_INPUT_METHOD_TEXT' | translate}} </label></p>
</div> </div>
@@ -155,25 +155,25 @@
</div> </div>
<!-- Mouse mode --> <!-- 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> <h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content"> <div class="content">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p> <p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen --> <!-- Touchscreen -->
<div class="choice"> <div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="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"> <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> <p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div> </div>
</div> </div>
<!-- Touchpad --> <!-- Touchpad -->
<div class="choice"> <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"> <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> <p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div> </div>
</div> </div>
@@ -182,20 +182,12 @@
</div> </div>
<!-- Display options --> <!-- 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> <h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content"> <div class="content">
<div id="zoom-settings"> <div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"></div> <guac-client-zoom client="focusedClient"></guac-client-zoom>
<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>
</div> </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>
</div> </div>
@@ -1,4 +1,9 @@
<a class="connection" ng-href="{{ item.getClientURL() }}"> <div class="connection">
<div class="icon type" ng-class="item.protocol"></div> <input type="checkbox"
<span class="name">{{item.name}}</span> ng-model="context.attachedClients[item.getClientIdentifier()]"
</a> 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 class="connection-group">
<div ng-show="item.balancing" class="icon type balancer"></div> <input type="checkbox"
<span class="name">{{item.name}}</span> ng-show="item.balancing"
</a> 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 --> <!-- Display -->
<div class="displayOuter"> <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" <div class="client-panel"
ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }"> ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
<!-- Toggle panel visibility --> <!-- Toggle panel visibility -->
<div class="client-panel-handle" ng-click="togglePanel()"></div> <div class="client-panel-handle" ng-click="togglePanel()"></div>
<!-- List of connection thumbnails --> <!-- List of connection thumbnails -->
<ul class="client-panel-connection-list"> <ul class="client-panel-connection-list">
<li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]" <li ng-repeat="clientGroup in clientGroups | orderBy: '-lastUsed'"
ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }" ng-if="!clientGroup.attached"
ng-show="isManaged(client.value)" ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
class="client-panel-connection"> class="client-panel-connection">
<!-- Close 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 }}" <img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}" ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.png"> src="images/x.svg">
</button> </button>
<!-- Thumbnail --> <!-- Thumbnail -->
<a href="#/client/{{client.value.id}}"> <a href="#/client/{{ getIdentifier(clientGroup) }}">
<div class="thumbnail"> <guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
<guac-thumbnail client="client.value"></guac-thumbnail> <div class="name">{{ getTitle(clientGroup) }}</div>
</div>
<div class="name">{{ client.value.title }}</div>
</a> </a>
</li> </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 --> <!-- Sent/received files -->
<div class="transfer-manager-body"> <div class="transfer-manager-body">
<div class="transfers"> <div class="transfer-manager-body-section" ng-repeat="client in clientGroup.clients" ng-show="hasTransfers(client)">
<guac-file-transfer <h3 ng-show="hasMultipleClients(clientGroup)">{{ client.name }}</h3>
transfer="upload" <div class="transfers">
ng-repeat="upload in client.uploads"> <guac-file-transfer
</guac-file-transfer><guac-file-transfer transfer="upload"
transfer="download" ng-repeat="upload in client.uploads">
ng-repeat="download in client.downloads"> </guac-file-transfer>
</guac-file-transfer> </div>
</div> </div>
</div> </div>
@@ -1,10 +1,11 @@
<div class="thumbnail-main" guac-resize="updateDisplayScale"> <div class="thumbnail-main" guac-resize="updateDisplayScale">
<!-- Display --> <!-- Display -->
<div class="display"> <div class="displayOuter">
<div class="displayMiddle">
<div class="display">
</div>
</div>
</div> </div>
<!-- Dummy background thumbnail -->
<img alt="" ng-src="{{thumbnail}}">
</div> </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) { angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
// Required services
var preferenceService = $injector.get('preferenceService');
/** /**
* Object used for interacting with a guacClient directive. * 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; this.maxScale = template.maxScale || 3;
/** /**
* Whether or not the client should listen to keyboard events. * Whether this client should receive keyboard events.
* *
* @type Boolean * @type Boolean
*/ */
this.keyboardEnabled = template.keyboardEnabled || true; this.focused = template.focused || false;
/**
* 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;
/** /**
* The relative Y coordinate of the scroll offset of the display within * 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) { function defineManagedClient($rootScope, $injector) {
// Required types // Required types
var ClientProperties = $injector.get('ClientProperties'); const ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier'); const ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData'); const ClipboardData = $injector.get('ClipboardData');
var ManagedArgument = $injector.get('ManagedArgument'); const ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState'); const ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail'); const ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay'); const ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFilesystem = $injector.get('ManagedFilesystem'); const ManagedFilesystem = $injector.get('ManagedFilesystem');
var ManagedFileUpload = $injector.get('ManagedFileUpload'); const ManagedFileUpload = $injector.get('ManagedFileUpload');
var ManagedShareLink = $injector.get('ManagedShareLink'); const ManagedShareLink = $injector.get('ManagedShareLink');
// Required services // Required services
var $document = $injector.get('$document'); const $document = $injector.get('$document');
var $q = $injector.get('$q'); const $q = $injector.get('$q');
var $rootScope = $injector.get('$rootScope'); const $rootScope = $injector.get('$rootScope');
var $window = $injector.get('$window'); const $window = $injector.get('$window');
var activeConnectionService = $injector.get('activeConnectionService'); const activeConnectionService = $injector.get('activeConnectionService');
var authenticationService = $injector.get('authenticationService'); const authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService'); const clipboardService = $injector.get('clipboardService');
var connectionService = $injector.get('connectionService'); const connectionGroupService = $injector.get('connectionGroupService');
var preferenceService = $injector.get('preferenceService'); const connectionService = $injector.get('connectionService');
var requestService = $injector.get('requestService'); const preferenceService = $injector.get('preferenceService');
var schemaService = $injector.get('schemaService'); const requestService = $injector.get('requestService');
var tunnelService = $injector.get('tunnelService'); const tunnelService = $injector.get('tunnelService');
var guacAudio = $injector.get('guacAudio'); const guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory'); const guacHistory = $injector.get('guacHistory');
var guacImage = $injector.get('guacImage'); const guacImage = $injector.get('guacImage');
var guacVideo = $injector.get('guacVideo'); const guacVideo = $injector.get('guacVideo');
/** /**
* The minimum amount of time to wait between updates to the client * 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 * Object which serves as a surrogate interface, encapsulating a Guacamole
* client while it is active, allowing it to be detached and reattached * client while it is active, allowing it to be maintained in the
* from different client views. * background. One or more ManagedClients are grouped within
* ManagedClientGroups before being attached to the client view.
* *
* @constructor * @constructor
* @param {ManagedClient|Object} [template={}] * @param {ManagedClient|Object} [template={}]
@@ -83,16 +84,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/ */
this.id = template.id; 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. * The actual underlying Guacamole client.
* *
@@ -156,16 +147,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/ */
this.thumbnail = template.thumbnail; 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 * The current state of all parameters requested by the server via
* "required" instructions, where each object key is the name of a * "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 * @param {ClientIdentifier} identifier
* The identifier representing the connection or group to connect to. * The identifier representing the connection or group to connect to.
* *
* @param {String} [connectionParameters] * @param {number} [width]
* Any additional HTTP parameters to pass while connecting. * 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>} * @returns {Promise.<String>}
* A promise which resolves with the string of connection parameters to * A promise which resolves with the string of connection parameters to
* be passed to the Guacamole client, once the string is ready. * 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 // Calculate optimal width/height for display
var pixel_density = $window.devicePixelRatio || 1; const pixel_density = $window.devicePixelRatio || 1;
var optimal_dpi = pixel_density * 96; const optimal_dpi = pixel_density * 96;
var optimal_width = $window.innerWidth * pixel_density; const optimal_width = width * pixel_density;
var optimal_height = $window.innerHeight * pixel_density; const optimal_height = height * pixel_density;
// Build base connect string // Build base connect string
var connectString = let connectString =
"token=" + encodeURIComponent(authenticationService.getCurrentToken()) "token=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource) + "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource)
+ "&GUAC_ID=" + encodeURIComponent(identifier.id) + "&GUAC_ID=" + encodeURIComponent(identifier.id)
@@ -286,8 +272,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
+ "&GUAC_WIDTH=" + Math.floor(optimal_width) + "&GUAC_WIDTH=" + Math.floor(optimal_width)
+ "&GUAC_HEIGHT=" + Math.floor(optimal_height) + "&GUAC_HEIGHT=" + Math.floor(optimal_height)
+ "&GUAC_DPI=" + Math.floor(optimal_dpi) + "&GUAC_DPI=" + Math.floor(optimal_dpi)
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone) + "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone);
+ (connectionParameters ? '&' + connectionParameters : '');
// Add audio mimetypes to connect string // Add audio mimetypes to connect string
guacAudio.supported.forEach(function(mimetype) { 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 * Creates a new ManagedClient representing the specified connection or
* or group. * connection group. The ManagedClient will not initially be connected,
* and must be explicitly connected by invoking ManagedClient.connect().
* *
* @param {String} id * @param {String} id
* The ID of the connection or group to connect to. This String must be * The ID of the connection or group to connect to. This String must be
* a valid ClientIdentifier string, as would be generated by * a valid ClientIdentifier string, as would be generated by
* ClientIdentifier.toString(). * ClientIdentifier.toString().
* *
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {ManagedClient} * @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. * connection group having the given ID.
*/ */
ManagedClient.getInstance = function getInstance(id, connectionParameters) { ManagedClient.getInstance = function getInstance(id) {
var tunnel; var tunnel;
@@ -450,8 +433,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.ConnectionState.IDLE); ManagedClientState.ConnectionState.IDLE);
break; break;
// Ignore "connecting" state // Conneccting
case 1: // Connecting case 1:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTING);
break; break;
// Connected + waiting // Connected + waiting
@@ -465,9 +450,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED); ManagedClientState.ConnectionState.CONNECTED);
// Send any clipboard data already provided // Sync current clipboard data
if (managedClient.clipboardData) clipboardService.getClipboard().then((data) => {
ManagedClient.setClipboard(managedClient, managedClient.clipboardData); ManagedClient.setClipboard(managedClient, data);
}, angular.noop);
// Begin streaming audio input if possible // Begin streaming audio input if possible
requestAudioStream(client); requestAudioStream(client);
@@ -562,12 +548,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Set clipboard contents once stream is finished // Set clipboard contents once stream is finished
reader.onend = function textComplete() { reader.onend = function textComplete() {
$rootScope.$apply(function updateClipboard() { clipboardService.setClipboard(new ClipboardData({
managedClient.clipboardData = new ClipboardData({ source : managedClient.id,
type : mimetype, type : mimetype,
data : data data : data
}); }))['catch'](angular.noop);
});
}; };
} }
@@ -576,12 +561,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
else { else {
reader = new Guacamole.BlobReader(stream, mimetype); reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = function blobComplete() { reader.onend = function blobComplete() {
$rootScope.$apply(function updateClipboard() { clipboardService.setClipboard(new ClipboardData({
managedClient.clipboardData = new ClipboardData({ source : managedClient.id,
type : mimetype, type : mimetype,
data : reader.getBlob() data : reader.getBlob()
}); }))['catch'](angular.noop);
});
}; };
} }
@@ -607,7 +591,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Handle any received filesystem objects // Handle any received filesystem objects
client.onfilesystem = function fileSystemReceived(object, name) { client.onfilesystem = function fileSystemReceived(object, name) {
$rootScope.$apply(function exposeFilesystem() { $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 // Parse connection details from ID
var clientIdentifier = ClientIdentifier.fromString(id); var clientIdentifier = ClientIdentifier.fromString(id);
// Connect the Guacamole client // Defer actually connecting the Guacamole client until
getConnectString(clientIdentifier, connectionParameters) // ManagedClient.connect() is explicitly invoked
.then(function connectClient(connectString) {
client.connect(connectString);
});
// If using a connection, pull connection name and protocol information // If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { 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. * Uploads the given file to the server through the given Guacamole client.
* The file transfer can be monitored through the corresponding entry in * 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 * 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 * @param {ManagedClient} managedClient
* The ManagedClient over which the given clipboard data is to be sent. * 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) { ManagedClient.setClipboard = function setClipboard(managedClient, data) {
// Ignore clipboard data that was received from this connection
if (data.source === managedClient.id)
return;
var writer; var writer;
// Create stream with proper mimetype // 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 * Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this * 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 // Use empty object by default
template = template || {}; 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" * The Guacamole filesystem object, as received via a "filesystem"
* instruction. * instruction.
@@ -162,6 +170,10 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* and human-readable name. Upon creation, a request to populate the * and human-readable name. Upon creation, a request to populate the
* contents of the root directory will be automatically dispatched. * 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 * @param {Guacamole.Object} object
* The Guacamole.Object defining the filesystem. * The Guacamole.Object defining the filesystem.
* *
@@ -171,10 +183,11 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* @returns {ManagedFilesystem} * @returns {ManagedFilesystem}
* The newly-created ManagedFilesystem. * The newly-created ManagedFilesystem.
*/ */
ManagedFilesystem.getInstance = function getInstance(object, name) { ManagedFilesystem.getInstance = function getInstance(client, object, name) {
// Init new filesystem object // Init new filesystem object
var managedFilesystem = new ManagedFilesystem({ var managedFilesystem = new ManagedFilesystem({
client : client,
object : object, object : object,
name : name, name : name,
root : new ManagedFilesystem.File({ root : new ManagedFilesystem.File({
@@ -196,9 +209,6 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* client and filesystem. The browser will automatically start the * client and filesystem. The browser will automatically start the
* download upon completion of this function. * download upon completion of this function.
* *
* @param {ManagedClient} managedClient
* The ManagedClient from which the file is to be downloaded.
*
* @param {ManagedFilesystem} managedFilesystem * @param {ManagedFilesystem} managedFilesystem
* The ManagedFilesystem from which the file is to be downloaded. Any * The ManagedFilesystem from which the file is to be downloaded. Any
* path information provided must be relative to this filesystem. * path information provided must be relative to this filesystem.
@@ -206,7 +216,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* @param {String} path * @param {String} path
* The full, absolute path of the file to download. * The full, absolute path of the file to download.
*/ */
ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) { ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) {
// Request download // Request download
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) { managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
@@ -215,7 +225,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
var filename = path.match(/(.*[\\/])?(.*)/)[2]; var filename = path.match(/(.*[\\/])?(.*)/)[2];
// Start download // 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 * A directive provides an editor for the clipboard content maintained by
* ClipboardData object via the "data" attribute. If this data should also be * clipboardService. Changes to the clipboard by clipboardService will
* synced to the local clipboard, or sent via a connected Guacamole client * automatically be reflected in the editor, and changes in the editor will
* using a "guacClipboard" event, it is up to external code to do so. * automatically be reflected in the clipboard by clipboardService.
*/ */
angular.module('clipboard').directive('guacClipboard', ['$injector', angular.module('clipboard').directive('guacClipboard', ['$injector',
function guacClipboard($injector) { function guacClipboard($injector) {
// Required types // 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. * Configuration object for the guacClipboard directive.
@@ -40,20 +43,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
templateUrl : 'app/clipboard/templates/guacClipboard.html' 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 // guacClipboard directive controller
config.controller = ['$scope', '$injector', '$element', config.controller = ['$scope', '$injector', '$element',
function guacClipboardController($scope, $injector, $element) { function guacClipboardController($scope, $injector, $element) {
@@ -75,12 +64,27 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
var updateClipboardData = function updateClipboardData() { var updateClipboardData = function updateClipboardData() {
// Read contents of clipboard textarea // Read contents of clipboard textarea
$scope.$evalAsync(function assignClipboardText() { clipboardService.setClipboard(new ClipboardData({
$scope.data = new ClipboardData({ type : 'text/plain',
type : 'text/plain', data : element.value
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('input', updateClipboardData);
element.addEventListener('change', updateClipboardData); element.addEventListener('change', updateClipboardData);
// Watch clipboard for new data, updating the clipboard textarea as // Update remote clipboard if local clipboard changes
// necessary $scope.$on('guacClipboard', function clipboardChanged(event, data) {
$scope.$watch('data', function clipboardDataChanged(data) { updateClipboardEditor(data);
});
// If the clipboard data is a string, render it as text // Init clipboard editor with current clipboard contents
if (typeof data.data === 'string') clipboardService.getClipboard().then((data) => {
element.value = data.data; updateClipboardEditor(data);
}, angular.noop);
// Ignore other data types for now
}); // end $scope.data watch
}]; }];
@@ -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', angular.module('clipboard').factory('clipboardService', ['$injector',
function clipboardService($injector) { function clipboardService($injector) {
// Get required services // Get required services
var $q = $injector.get('$q'); const $q = $injector.get('$q');
var $window = $injector.get('$window'); const $window = $injector.get('$window');
const $rootScope = $injector.get('$rootScope');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types // 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 = {}; var service = {};
@@ -175,7 +189,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
* A promise that will resolve if setting the clipboard was successful, * A promise that will resolve if setting the clipboard was successful,
* and will reject if it failed. * and will reject if it failed.
*/ */
service.setLocalClipboard = function setLocalClipboard(data) { const setLocalClipboard = function setLocalClipboard(data) {
var deferred = $q.defer(); 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 * if getting the clipboard was successful, and will reject if it
* failed. * failed.
*/ */
service.getLocalClipboard = function getLocalClipboard() { const getLocalClipboard = function getLocalClipboard() {
// If the clipboard is already being read, do not overlap the read // If the clipboard is already being read, do not overlap the read
// attempts; instead share the result across all requests // 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; return service;
}]); }]);
@@ -47,7 +47,7 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
border: 1px solid black; border: 1px solid black;
background: url('images/checker.png'); background: url('images/checker.svg');
} }
.clipboard-service-target { .clipboard-service-target {
@@ -36,6 +36,15 @@ angular.module('clipboard').factory('ClipboardData', [function defineClipboardDa
// Use empty object by default // Use empty object by default
template = template || {}; 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. * 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 */ /* Icon for unmasking passwords */
.form-field .password-field input[type=password] ~ .icon.toggle-password { .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 */ /* Icon for masking passwords */
.form-field .password-field input[type=text] ~ .icon.toggle-password { .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() { 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) { controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
// Required types // Required types
var ActiveConnection = $injector.get('ActiveConnection');
var ClientIdentifier = $injector.get('ClientIdentifier'); var ClientIdentifier = $injector.get('ClientIdentifier');
var RecentConnection = $injector.get('RecentConnection'); var RecentConnection = $injector.get('RecentConnection');
// Required services // Required services
var guacClientManager = $injector.get('guacClientManager');
var guacHistory = $injector.get('guacHistory'); 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. * 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. * 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} * @returns {Boolean}
* true if recent (or active) connections are present, false * true if recent connections are present, false otherwise.
* otherwise.
*/ */
$scope.hasRecentConnections = function hasRecentConnections() { $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) { $scope.$watch("rootGroups", function setRootGroups(rootGroups) {
// Clear connection arrays // Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = []; $scope.recentConnections = [];
// Produce collection of visible objects // 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 // Add any recent connections that are visible
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects // Add recent connections for history entries with associated visible objects
if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) { if (historyEntry.id in visibleObjects) {
var object = visibleObjects[historyEntry.id]; var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); $scope.recentConnections.push(new RecentConnection(object.name, historyEntry));
@@ -3,23 +3,6 @@
<!-- Text displayed if no recent connections exist --> <!-- Text displayed if no recent connections exist -->
<p class="placeholder" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p> <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 --> <!-- All recent connections -->
<div ng-repeat="recentConnection in recentConnections" class="connection"> <div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}"> <a href="#/client/{{recentConnection.entry.id}}">
@@ -181,10 +181,11 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// Client view // Client view
.when('/client/:id/:params?', { .when('/client/:id', {
bodyClassName : 'client', bodyClassName : 'client',
templateUrl : 'app/client/templates/client.html', templateUrl : 'app/client/templates/client.html',
controller : 'clientController', controller : 'clientController',
reloadOnUrl : false,
resolve : { updateCurrentToken: updateCurrentToken } resolve : { updateCurrentToken: updateCurrentToken }
}) })
@@ -24,11 +24,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) { function indexController($scope, $injector) {
// Required services // Required services
var $document = $injector.get('$document'); const $document = $injector.get('$document');
var $route = $injector.get('$route'); const $route = $injector.get('$route');
var $window = $injector.get('$window'); const $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService'); const clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification'); const guacNotification = $injector.get('guacNotification');
const guacClientManager = $injector.get('guacClientManager');
/** /**
* The error that prevents the current page from rendering at all. If no * 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; $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 * The message to display to the user as instructions for the login
* process. * process.
@@ -154,9 +163,8 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Broadcast keydown events // Broadcast keydown events
keyboard.onkeydown = function onkeydown(keysym) { keyboard.onkeydown = function onkeydown(keysym) {
// Do not handle key events if not logged in or if a notification is // Do not handle key events if not logged in
// shown if ($scope.applicationState !== ApplicationState.READY)
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
return true; return true;
// Warn of pending keydown // 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 // Do not handle key events if not logged in or if a notification is
// shown // shown
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus()) if ($scope.applicationState !== ApplicationState.READY)
return; return;
// Warn of pending keyup // Warn of pending keyup
@@ -199,25 +207,15 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
keyboard.reset(); 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 // Attempt to read the clipboard if it may have changed
$window.addEventListener('load', checkClipboard, true); $window.addEventListener('load', clipboardService.resyncClipboard, true);
$window.addEventListener('copy', checkClipboard); $window.addEventListener('copy', clipboardService.resyncClipboard);
$window.addEventListener('cut', checkClipboard); $window.addEventListener('cut', clipboardService.resyncClipboard);
$window.addEventListener('focus', function focusGained(e) { $window.addEventListener('focus', function focusGained(e) {
// Only recheck clipboard if it's the window itself that gained focus // Only recheck clipboard if it's the window itself that gained focus
if (e.target === $window) if (e.target === $window)
checkClipboard(); clipboardService.resyncClipboard();
}, true); }, true);
@@ -48,3 +48,12 @@
from { opacity: 1; } from { opacity: 1; }
to { opacity: 0; } 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,
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,
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,
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,
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,
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,
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 { .fatal-page-error h1::before {
content: ' '; content: ' ';
display: inline-block; display: inline-block;
background: url('images/warning.png'); background: url('images/warning.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
height: 1em; height: 1em;
width: 1em; width: 1em;
@@ -49,7 +49,7 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
background-image: url('images/cog.png'); background-image: url('images/cog.svg');
background-size: 96px 96px; background-size: 96px 96px;
background-position: center center; background-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -20,7 +20,7 @@
#other-connections .client-panel { #other-connections .client-panel {
display: none; display: none;
position: absolute; position: fixed;
right: 0; right: 0;
bottom: 0; bottom: 0;
@@ -56,7 +56,7 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
background-position: center center; background-position: center center;
background-image: url(images/arrows/right.png); background-image: url(images/arrows/right.svg);
opacity: 0.5; opacity: 0.5;
} }
@@ -66,7 +66,7 @@
} }
#other-connections .client-panel.hidden .client-panel-handle { #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 { #other-connections .client-panel-connection-list {
@@ -92,6 +92,7 @@
background: black; background: black;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
animation: 0.1s linear 0s popin;
opacity: 0.5; opacity: 0.5;
transition: opacity 0.25s; 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 { #other-connections .client-panel-connection a[href]::before {
display: block; display: block;
@@ -118,7 +114,7 @@
width: 100%; width: 100%;
z-index: 1; z-index: 1;
background: url('images/warning-white.png'); background: url('images/warning-white.svg');
background-size: 48px; background-size: 48px;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -160,6 +156,7 @@
#other-connections button.close-other-connection img { #other-connections button.close-other-connection img {
background: #A43; background: #A43;
border-radius: 18px; border-radius: 18px;
width: 100%;
max-width: 18px; max-width: 18px;
padding: 3px; padding: 3px;
} }
@@ -52,10 +52,10 @@ table.sorted th.sort-primary:after {
background-size: 1em 1em; background-size: 1em 1em;
background-position: right center; background-position: right center;
background-repeat: no-repeat; 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 { 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 { .icon.user {
background-image: url('images/user-icons/guac-user.png'); background-image: url('images/user-icons/guac-user.svg');
} }
.icon.user.add { .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 { .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 { .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 { .icon.connection {
background-image: url('images/protocol-icons/guac-plug.png'); background-image: url('images/protocol-icons/guac-plug.svg');
} }
.icon.connection.add { .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, .connection .icon,
@@ -187,30 +187,30 @@ div.section {
} }
.connection-group > .caption .icon { .connection-group > .caption .icon {
background-image: url('images/folder-closed.png'); background-image: url('images/folder-closed.svg');
} }
.connection-group.expanded > .caption .icon { .connection-group.expanded > .caption .icon {
background-image: url('images/folder-open.png'); background-image: url('images/folder-open.svg');
} }
.connection .icon { .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.kubernetes,
.connection .icon.ssh, .connection .icon.ssh,
.connection .icon.telnet { .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.vnc,
.connection .icon.rdp { .connection .icon.rdp {
background-image: url('images/protocol-icons/guac-monitor.png'); background-image: url('images/protocol-icons/guac-monitor.svg');
} }
.sharing-profile .icon { .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 { .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 { .expandable.expanded > .children > .list-item {
@@ -259,16 +259,16 @@ div.section {
} }
.expandable > .caption .icon.expand { .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 { .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 { .expandable.empty > .caption .icon.expand {
opacity: 0.25; opacity: 0.25;
background-image: url('images/group-icons/guac-open.png'); background-image: url('images/group-icons/guac-open.svg');
} }
.history th, .history th,
@@ -22,7 +22,7 @@
} }
.filter .search-string { .filter .search-string {
background-image: url('images/magnifier.png'); background-image: url('images/magnifier.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 1.75em; background-size: 1.75em;
background-position: 0.25em center; background-position: 0.25em center;
@@ -71,17 +71,17 @@
} }
.pager .icon.first-page { .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 { .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 { .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 { .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; -moz-background-size: 3em 3em;
-webkit-background-size: 3em 3em; -webkit-background-size: 3em 3em;
-khtml-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 { .login-ui.continuation .login-dialog {
@@ -39,11 +39,11 @@
} }
.manage-user-group .page-tabs .page-list li.read-only a[href]:before { .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 { .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] { .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 { .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 { .manage-user-group .notice.read-only {
@@ -39,11 +39,11 @@
} }
.manage-user .page-tabs .page-list li.read-only a[href]:before { .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 { .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] { .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 { .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 { .manage-user .notice.read-only {
@@ -12,12 +12,12 @@
<!-- Abbreviated list of only the currently selected objects --> <!-- Abbreviated list of only the currently selected objects -->
<div class="abbreviated-related-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/right.svg" 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/down.svg" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
<p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p> <p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p>
<ul> <ul>
<li ng-repeat="identifier in identifiers | filter: filterString"> <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-click="removeIdentifier(identifier)"
ng-show="isEditable[identifier]"><span class="identifier">{{ identifier }}</span> ng-show="isEditable[identifier]"><span class="identifier">{{ identifier }}</span>
</label> </label>
@@ -92,7 +92,7 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 1em; background-size: 1em;
background-position: center center; 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-repeat: no-repeat;
background-size: 1em; background-size: 1em;
background-position: 0.5em center; 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-size: 1em;
background-position: 0.75em center; background-position: 0.75em center;
padding-left: 2.5em; 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="#/"] { .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/users"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/connections"], .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/sessions"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/preferences"] { .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 { .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 { .user-menu .menu-dropdown .menu-contents .profile {
@@ -18,20 +18,30 @@
*/ */
guac-modal { 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%; height: 100%;
width: 100%; width: 100%;
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
z-index: 10; z-index: 10;
overflow: hidden;
} }
guac-modal .modal-contents { guac-modal .modal-contents {
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
width: 100%; width: 100%;
text-align: center; text-align: center;
display: table-cell;
vertical-align: middle;
} }
guac-modal { guac-modal {
@@ -74,7 +74,7 @@
.notification .progress { .notification .progress {
width: 100%; width: 100%;
background: #C2C2C2 url('images/progress.png'); background: #C2C2C2 url('images/progress.svg');
background-size: 16px 16px; background-size: 16px 16px;
-moz-background-size: 16px 16px; -moz-background-size: 16px 16px;
-webkit-background-size: 16px 16px; -webkit-background-size: 16px 16px;
@@ -45,17 +45,17 @@ a.button.add-connection-group::before {
} }
a.button.add-user::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 { 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 { 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 { 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"> <div class="choice">
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"> <input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<div class="figure"> <div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'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> <p class="caption"><label for="absolute">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div> </div>
</div> </div>
@@ -81,7 +81,7 @@
<div class="choice"> <div class="choice">
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"> <input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<div class="figure"> <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> <p class="caption"><label for="relative">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div> </div>
</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