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

View File

@@ -22,49 +22,60 @@
*/
angular.module('client').directive('guacClient', [function guacClient() {
return {
// Element only
const directive = {
restrict: 'E',
replace: true,
scope: {
templateUrl: 'app/client/templates/guacClient.html'
};
directive.scope = {
/**
* The client to display within this guacClient directive.
*
* @type ManagedClient
*/
client : '='
client : '=',
},
templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
/**
* 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 guacClientController($scope, $injector, $element) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
const ManagedClient = $injector.get('ManagedClient');
// Required services
var $window = $injector.get('$window');
const $window = $injector.get('$window');
/**
* Whether the local, hardware mouse cursor is in use.
*
* @type Boolean
*/
var localCursor = false;
let localCursor = false;
/**
* The current Guacamole client instance.
*
* @type Guacamole.Client
*/
var client = null;
let client = null;
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
var display = null;
let display = null;
/**
* The element associated with the display of the current
@@ -72,28 +83,21 @@ angular.module('client').directive('guacClient', [function guacClient() {
*
* @type Element
*/
var displayElement = null;
let displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
var displayContainer = $element.find('.display')[0];
const displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
var main = $element[0];
/**
* The element which functions as a detector for size changes.
*
* @type Element
*/
var resizeSensor = $element.find('.resize-sensor')[0];
const main = $element[0];
/**
* Guacamole mouse event object, wrapped around the main client
@@ -101,7 +105,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
*
* @type Guacamole.Mouse
*/
var mouse = new Guacamole.Mouse(displayContainer);
const mouse = new Guacamole.Mouse(displayContainer);
/**
* Guacamole absolute mouse emulation object, wrapped around the
@@ -109,7 +113,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
*
* @type Guacamole.Mouse.Touchscreen
*/
var touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer);
const touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer);
/**
* Guacamole relative mouse emulation object, wrapped around the
@@ -117,7 +121,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
*
* @type Guacamole.Mouse.Touchpad
*/
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
const touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
/**
* Guacamole touch event handling object, wrapped around the main
@@ -125,13 +129,13 @@ angular.module('client').directive('guacClient', [function guacClient() {
*
* @type Guacamole.Touch
*/
var touch = new Guacamole.Touch(displayContainer);
const touch = new Guacamole.Touch(displayContainer);
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
var updateDisplayScale = function updateDisplayScale() {
const updateDisplayScale = function updateDisplayScale() {
if (!display) return;
@@ -159,19 +163,19 @@ angular.module('client').directive('guacClient', [function guacClient() {
* @param {Guacamole.Mouse.State} mouseState The current mouse
* state.
*/
var scrollToMouse = function scrollToMouse(mouseState) {
const scrollToMouse = function scrollToMouse(mouseState) {
// Determine mouse position within view
var mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft;
var mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop;
const mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft;
const mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop;
// Determine viewport dimensions
var view_width = main.offsetWidth;
var view_height = main.offsetHeight;
const view_width = main.offsetWidth;
const view_height = main.offsetHeight;
// Determine scroll amounts based on mouse position relative to document
var scroll_amount_x;
let scroll_amount_x;
if (mouse_view_x > view_width)
scroll_amount_x = mouse_view_x - view_width;
else if (mouse_view_x < 0)
@@ -179,7 +183,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
else
scroll_amount_x = 0;
var scroll_amount_y;
let scroll_amount_y;
if (mouse_view_y > view_height)
scroll_amount_y = mouse_view_y - view_height;
else if (mouse_view_y < 0)
@@ -202,7 +206,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to handle.
*/
var handleMouseEvent = function handleMouseEvent(event) {
const handleMouseEvent = function handleMouseEvent(event) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
@@ -227,7 +231,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to handle.
*/
var handleEmulatedMouseEvent = function handleEmulatedMouseEvent(event) {
const handleEmulatedMouseEvent = function handleEmulatedMouseEvent(event) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
@@ -252,7 +256,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
* @param {Guacamole.Touch.Event} touchEvent
* The touch event.
*/
var handleTouchEvent = function handleTouchEvent(event) {
const handleTouchEvent = function handleTouchEvent(event) {
// Do not attempt to handle touch state changes if the client
// or display are not yet available
@@ -294,7 +298,10 @@ angular.module('client').directive('guacClient', [function guacClient() {
return false;
};
// Size of newly-attached client may be different
// Connect and update interface to match required size, deferring
// connecting until a future element resize if the main element
// size (desired display size) is not known and thus can't be sent
// during the handshake
$scope.mainElementResized();
});
@@ -326,8 +333,8 @@ angular.module('client').directive('guacClient', [function guacClient() {
// support and mouse emulation mode
$scope.$watchGroup([
'client.multiTouchSupport',
'client.clientProperties.emulateAbsoluteMouse'
], function touchBehaviorChanged(emulateAbsoluteMouse) {
'emulateAbsoluteMouse'
], function touchBehaviorChanged() {
// Clear existing event handling
touch.offEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
@@ -340,7 +347,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Switch to touchscreen if mouse emulation is required and
// absolute mouse emulation is preferred
else if ($scope.client.clientProperties.emulateAbsoluteMouse)
else if ($scope.emulateAbsoluteMouse)
touchScreen.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
// Use touchpad for mouse emulation if absolute mouse emulation
@@ -380,15 +387,26 @@ angular.module('client').directive('guacClient', [function guacClient() {
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
});
// If the element is resized, attempt to resize client
/**
* Sends the current size of the main element (the display container)
* to the Guacamole server, requesting that the remote display be
* resized. If the Guacamole client is not yet connected, it will be
* connected and the current size will sent through the initial
* handshake. If the size of the main element is not yet known, this
* function may need to be invoked multiple times until the size is
* known and the client may be connected.
*/
$scope.mainElementResized = function mainElementResized() {
// Send new display size, if changed
if (client && display) {
if (client && display && main.offsetWidth && main.offsetHeight) {
var pixelDensity = $window.devicePixelRatio || 1;
var width = main.offsetWidth * pixelDensity;
var height = main.offsetHeight * pixelDensity;
// Connect, if not already connected
ManagedClient.connect($scope.client, main.offsetWidth, main.offsetHeight);
const pixelDensity = $window.devicePixelRatio || 1;
const width = main.offsetWidth * pixelDensity;
const height = main.offsetHeight * pixelDensity;
if (display.getWidth() !== width || display.getHeight() !== height)
client.sendSize(width, height);
@@ -399,6 +417,91 @@ angular.module('client').directive('guacClient', [function guacClient() {
};
// Scroll client display if absolute mouse is in use (the same drag
// gesture is needed for moving the mouse pointer with relative mouse)
$scope.clientDrag = function clientDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
if ($scope.emulateAbsoluteMouse) {
$scope.client.clientProperties.scrollLeft -= deltaX;
$scope.client.clientProperties.scrollTop -= deltaY;
}
return false;
};
/**
* If a pinch gesture is in progress, the scale of the client display when
* the pinch gesture began.
*
* @type Number
*/
let initialScale = null;
/**
* If a pinch gesture is in progress, the X coordinate of the point on the
* client display that was centered within the pinch at the time the
* gesture began.
*
* @type Number
*/
let initialCenterX = 0;
/**
* If a pinch gesture is in progress, the Y coordinate of the point on the
* client display that was centered within the pinch at the time the
* gesture began.
*
* @type Number
*/
let initialCenterY = 0;
// Zoom and pan client via pinch gestures
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
// Do not handle pinch gestures if they would conflict with remote
// handling of similar gestures
if ($scope.client.multiTouchSupport > 1)
return false;
// Do not handle pinch gestures while relative mouse is in use (2+
// contact point gestures are used by relative mouse emulation to
// support right click, middle click, and scrolling)
if (!$scope.emulateAbsoluteMouse)
return false;
// Stop gesture if not in progress
if (!inProgress) {
initialScale = null;
return false;
}
// Set initial scale if gesture has just started
if (!initialScale) {
initialScale = $scope.client.clientProperties.scale;
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
}
// Determine new scale absolutely
let currentScale = initialScale * currentLength / startLength;
// Fix scale within limits - scroll will be miscalculated otherwise
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
// Update scale based on pinch distance
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale = currentScale;
// Scroll display to keep original pinch location centered within current pinch
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
return false;
};
// Ensure focus is regained via mousedown before forwarding event
mouse.on('mousedown', document.body.focus.bind(document.body));
@@ -413,15 +516,12 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function onClipboard(event, data) {
if (client) {
ManagedClient.setClipboard($scope.client, data);
$scope.client.clipboardData = data;
}
});
// Translate local keydown events to remote keydown events if keyboard is enabled
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
if ($scope.client.clientProperties.focused) {
client.sendKeyEvent(1, keysym);
event.preventDefault();
}
@@ -429,7 +529,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Translate local keyup events to remote keyup events if keyboard is enabled
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
if ($scope.client.clientProperties.focused) {
client.sendKeyEvent(0, keysym);
event.preventDefault();
}
@@ -437,49 +537,85 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Universally handle all synthetic keydown events
$scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) {
if ($scope.client.clientProperties.focused)
client.sendKeyEvent(1, keysym);
});
// Universally handle all synthetic keyup events
$scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) {
if ($scope.client.clientProperties.focused)
client.sendKeyEvent(0, keysym);
});
/**
* Ignores the given event.
* Whether a drag/drop operation is currently in progress (the user has
* dragged a file over the Guacamole connection but has not yet
* dropped it).
*
* @param {Event} e The event to ignore.
* @type boolean
*/
function ignoreEvent(e) {
$scope.dropPending = false;
/**
* Displays a visual indication that dropping the file currently
* being dragged is possible. Further propogation and default behavior
* of the given event is automatically prevented.
*
* @param {Event} e
* The event related to the in-progress drag/drop operation.
*/
const notifyDragStart = function notifyDragStart(e) {
e.preventDefault();
e.stopPropagation();
}
// Handle and ignore dragenter/dragover
displayContainer.addEventListener("dragenter", ignoreEvent, false);
displayContainer.addEventListener("dragover", ignoreEvent, false);
$scope.$apply(() => {
$scope.dropPending = true;
});
};
/**
* Removes the visual indication that dropping the file currently
* being dragged is possible. Further propogation and default behavior
* of the given event is automatically prevented.
*
* @param {Event} e
* The event related to the end of the former drag/drop operation.
*/
const notifyDragEnd = function notifyDragEnd(e) {
e.preventDefault();
e.stopPropagation();
$scope.$apply(() => {
$scope.dropPending = false;
});
};
main.addEventListener('dragenter', notifyDragStart, false);
main.addEventListener('dragover', notifyDragStart, false);
main.addEventListener('dragleave', notifyDragEnd, false);
// File drop event handler
displayContainer.addEventListener("drop", function(e) {
main.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
notifyDragEnd(e);
// Ignore file drops if no attached client
if (!$scope.client)
return;
// Upload each file
var files = e.dataTransfer.files;
for (var i=0; i<files.length; i++)
const files = e.dataTransfer.files;
for (let i = 0; i < files.length; i++)
ManagedClient.uploadFile($scope.client, files[i]);
}, false);
/*
* END CLIENT DIRECTIVE
*/
}];
return directive;
}]
};
}]);

View File

@@ -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;
}]);

View File

@@ -25,11 +25,12 @@
angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
// Required services
var guacClientManager = $injector.get('guacClientManager');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
const guacClientManager = $injector.get('guacClientManager');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
var ManagedClientState = $injector.get('ManagedClientState');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedClientState = $injector.get('ManagedClientState');
/**
* Getter/setter for the boolean flag controlling whether the client panel
@@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
scope: {
/**
* The ManagedClient instances associated with the active
* The ManagedClientGroup instances associated with the active
* connections to be displayed within this panel.
*
* @type ManagedClient[]|Object.<String, ManagedClient>
* @type ManagedClientGroup[]
*/
clients : '='
clientGroups : '='
},
templateUrl: 'app/client/templates/guacClientPanel.html',
@@ -75,30 +76,42 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
$scope.panelHidden = panelHidden;
/**
* Returns whether this panel currently has any clients associated
* with it.
* Returns whether this panel currently has any client groups
* associated with it.
*
* @return {Boolean}
* true if at least one client is associated with this panel,
* false otherwise.
* true if at least one client group is associated with this
* panel, false otherwise.
*/
$scope.hasClients = function hasClients() {
return !!_.find($scope.clients, $scope.isManaged);
$scope.hasClientGroups = function hasClientGroups() {
return $scope.clientGroups && $scope.clientGroups.length;
};
/**
* Returns whether the status of the given client has changed in a
* way that requires the user's attention. This may be due to an
* error, or due to a server-initiated disconnect.
* @borrows ManagedClientGroup.getIdentifier
*/
$scope.getIdentifier = ManagedClientGroup.getIdentifier;
/**
* @borrows ManagedClientGroup.getTitle
*/
$scope.getTitle = ManagedClientGroup.getTitle;
/**
* Returns whether the status of any client within the given client
* group has changed in a way that requires the user's attention.
* This may be due to an error, or due to a server-initiated
* disconnect.
*
* @param {ManagedClient} client
* The client to test.
* @param {ManagedClientGroup} clientGroup
* The client group to test.
*
* @returns {Boolean}
* true if the given client requires the user's attention,
* false otherwise.
*/
$scope.hasStatusUpdate = function hasStatusUpdate(client) {
$scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) {
return _.findIndex(clientGroup.clients, (client) => {
// Test whether the client has encountered an error
switch (client.clientState.connectionState) {
@@ -110,36 +123,21 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
return false;
}) !== -1;
};
/**
* Returns whether the given client is currently being managed by
* the guacClientManager service.
* Initiates an orderly disconnect of all clients within the given
* group. The clients are removed from management such that
* attempting to connect to any of the same connections will result
* in new connections being established, rather than displaying a
* notification that the connection has ended.
*
* @param {ManagedClient} client
* The client to test.
*
* @returns {Boolean}
* true if the given client is being managed by the
* guacClientManager service, false otherwise.
* @param {ManagedClientGroup} clientGroup
* The group of clients to disconnect.
*/
$scope.isManaged = function isManaged(client) {
return !!guacClientManager.getManagedClients()[client.id];
};
/**
* Initiates an orderly disconnect of the given client. The client
* is removed from management such that attempting to connect to
* the same connection will result in a new connection being
* established, rather than displaying a notification that the
* connection has ended.
*
* @param {type} client
* @returns {undefined}
*/
$scope.disconnect = function disconnect(client) {
client.client.disconnect();
guacClientManager.removeManagedClient(client.id);
$scope.disconnect = function disconnect(clientGroup) {
guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
};
/**

View File

@@ -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;
}]);

View File

@@ -28,14 +28,6 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
replace: true,
scope: {
/**
* The client whose file transfers should be managed by this
* directive.
*
* @type ManagedClient
*/
client : '=',
/**
* @type ManagedFilesystem
*/
@@ -116,7 +108,7 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
* The file to download.
*/
$scope.downloadFile = function downloadFile(file) {
ManagedFilesystem.downloadFile($scope.client, $scope.filesystem, file.streamName);
ManagedFilesystem.downloadFile($scope.filesystem, file.streamName);
};
/**

View File

@@ -28,12 +28,12 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
scope: {
/**
* The client whose file transfers should be managed by this
* The client group whose file transfers should be managed by this
* directive.
*
* @type ManagerClient
* @type ManagedClientGroup
*/
client : '='
clientGroup : '='
},
@@ -41,7 +41,9 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Determines whether the given file transfer state indicates an
@@ -74,17 +76,29 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
*/
$scope.clearCompletedTransfers = function clearCompletedTransfers() {
// Nothing to clear if no client attached
if (!$scope.client)
// Nothing to clear if no client group attached
if (!$scope.clientGroup)
return;
// Remove completed uploads
$scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) {
$scope.clientGroup.clients.forEach(client => {
client.uploads = client.uploads.filter(function isUploadInProgress(upload) {
return isInProgress(upload.transferState);
});
});
};
/**
* @borrows ManagedClientGroup.hasMultipleClients
*/
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
/**
* @borrows ManagedClient.hasTransfers
*/
$scope.hasTransfers = ManagedClient.hasTransfers;
}]
};

View File

@@ -43,20 +43,6 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
// Required services
var $window = $injector.get('$window');
/**
* The optimal thumbnail width, in pixels.
*
* @type Number
*/
var THUMBNAIL_WIDTH = 320;
/**
* The optimal thumbnail height, in pixels.
*
* @type Number
*/
var THUMBNAIL_HEIGHT = 240;
/**
* The display of the current Guacamole client instance.
*
@@ -126,32 +112,7 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) {
var width;
var height;
// If no display size yet, assume optimal thumbnail size
if (!size || size.width === 0 || size.height === 0) {
width = THUMBNAIL_WIDTH;
height = THUMBNAIL_HEIGHT;
}
// Otherwise, generate size that fits within thumbnail bounds
else {
var scale = Math.min(THUMBNAIL_WIDTH / size.width, THUMBNAIL_HEIGHT / size.height, 1);
width = size.width * scale;
height = size.height * scale;
}
// Generate dummy background image
var thumbnail = document.createElement("canvas");
thumbnail.width = width;
thumbnail.height = height;
$scope.thumbnail = thumbnail.toDataURL("image/png");
// Init display scale
$scope.$evalAsync($scope.updateDisplayScale);
});
}]

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -24,11 +24,12 @@ angular.module('client').factory('guacClientManager', ['$injector',
function guacClientManager($injector) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
// Required services
var $window = $injector.get('$window');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
const $window = $injector.get('$window');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {};
@@ -56,6 +57,65 @@ angular.module('client').factory('guacClientManager', ['$injector',
return storedManagedClients();
};
/**
* Getter/setter which retrieves or sets the array of all active managed
* client groups.
*
* @type Function
*/
const storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns an array of all managed client groups.
*
* @returns {ManagedClientGroup[]>}
* An array of all active managed client groups.
*/
service.getManagedClientGroups = function getManagedClientGroups() {
return storedManagedClientGroups();
};
/**
* Removes the ManagedClient with the given ID from all
* ManagedClientGroups, automatically adjusting the tile size of the
* clients that remain in each group. All client groups that are empty
* after the client is removed will also be removed.
*
* @param {string} id
* The ID of the ManagedClient to remove.
*/
const ungroupManagedClient = function ungroupManagedClient(id) {
const managedClientGroups = storedManagedClientGroups();
// Remove client from all groups
managedClientGroups.forEach(group => {
const removed = _.remove(group.clients, client => (client.id === id));
if (removed.length) {
// Reset focus state if client is being removed from a group
// that isn't currently attached (focus may otherwise be
// retained and result in a newly added connection unexpectedly
// sharing focus)
if (!group.attached)
removed.forEach(client => { client.clientProperties.focused = false; });
// Recalculate group grid if number of clients is changing
ManagedClientGroup.recalculateTiles(group);
}
});
// Remove any groups that are now empty
_.remove(managedClientGroups, group => !group.clients.length);
};
/**
* Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this
@@ -67,13 +127,16 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @returns {Boolean}
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function replaceManagedClient(id) {
service.removeManagedClient = function removeManagedClient(id) {
var managedClients = storedManagedClients();
// Remove client if it exists
if (id in managedClients) {
// Pull client out of any containing groups
ungroupManagedClient(id);
// Disconnect and remove
managedClients[id].client.disconnect();
delete managedClients[id];
@@ -96,22 +159,37 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) {
service.replaceManagedClient = function replaceManagedClient(id) {
// Disconnect any existing client
service.removeManagedClient(id);
const managedClients = storedManagedClients();
const managedClientGroups = storedManagedClientGroups();
// Set new client
return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters);
// Remove client if it exists
if (id in managedClients) {
const hadFocus = managedClients[id].clientProperties.focused;
managedClients[id].client.disconnect();
delete managedClients[id];
// Remove client from all groups
managedClientGroups.forEach(group => {
const index = _.findIndex(group.clients, client => (client.id === id));
if (index === -1)
return;
group.clients[index] = managedClients[id] = ManagedClient.getInstance(id);
managedClients[id].clientProperties.focused = hadFocus;
});
}
return managedClients[id];
};
@@ -123,22 +201,21 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.getManagedClient = function getManagedClient(id, connectionParameters) {
service.getManagedClient = function getManagedClient(id) {
var managedClients = storedManagedClients();
// Ensure any existing client is removed from its containing group
// prior to being returned
ungroupManagedClient(id);
// Create new managed client if it doesn't already exist
if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
managedClients[id] = ManagedClient.getInstance(id);
// Return existing client
return managedClients[id];
@@ -146,7 +223,88 @@ angular.module('client').factory('guacClientManager', ['$injector',
};
/**
* Disconnects and removes all currently-connected clients.
* Returns the ManagedClientGroup having the given ID. If no such
* ManagedClientGroup exists, a new ManagedClientGroup is created by
* extracting the relevant connections from the ID.
*
* @param {String} id
* The ID of the ManagedClientGroup to retrieve or create.
*
* @returns {ManagedClientGroup}
* The ManagedClientGroup having the given ID.
*/
service.getManagedClientGroup = function getManagedClientGroup(id) {
const managedClientGroups = storedManagedClientGroups();
const existingGroup = _.find(managedClientGroups, (group) => {
return id === ManagedClientGroup.getIdentifier(group);
});
// Prefer to return the existing group if it exactly matches
if (existingGroup)
return existingGroup;
const clients = [];
const clientIds = ManagedClientGroup.getClientIdentifiers(id);
// Separate active clients by whether they should be displayed within
// the current view
clientIds.forEach(function groupClients(id) {
clients.push(service.getManagedClient(id));
});
const group = new ManagedClientGroup({
clients : clients
});
// Focus the first client if there are no clients focused
ManagedClientGroup.verifyFocus(group);
managedClientGroups.push(group);
return group;
};
/**
* Removes the existing ManagedClientGroup having the given ID, if any,
* disconnecting and removing all ManagedClients associated with that
* group. If no such a ManagedClientGroup currently exists, this function
* has no effect.
*
* @param {String} id
* The ID of the ManagedClientGroup to remove.
*
* @returns {Boolean}
* true if a ManagedClientGroup was removed, false otherwise.
*/
service.removeManagedClientGroup = function removeManagedClientGroup(id) {
const managedClients = storedManagedClients();
const managedClientGroups = storedManagedClientGroups();
// Remove all matching groups (there SHOULD only be one)
const removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id);
// Disconnect all clients associated with the removed group(s)
removed.forEach((group) => {
group.clients.forEach((client) => {
const id = client.id;
if (managedClients[id]) {
managedClients[id].client.disconnect();
delete managedClients[id];
}
});
});
return !!removed.length;
};
/**
* Disconnects and removes all currently-connected clients and client
* groups.
*/
service.clear = function clear() {
@@ -156,8 +314,9 @@ angular.module('client').factory('guacClientManager', ['$injector',
for (var id in managedClients)
managedClients[id].client.disconnect();
// Clear managed clients
// Clear managed clients and client groups
storedManagedClients({});
storedManagedClientGroups([]);
};

View File

@@ -103,7 +103,7 @@ body.client {
flex: 0 0 auto;
}
.client-view .client-body .main {
.client-view .client-body .tiled-client-list {
position: absolute;
left: 0;
@@ -125,5 +125,13 @@ body.client {
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
background-image: url('images/x.png');
background-image: url('images/x.svg');
}
.client .drop-pending .display {
background: #3161a9;
}
.client .drop-pending .display > *{
opacity: 0.5;
}

View File

@@ -55,3 +55,8 @@
overflow: hidden;
text-overflow: ellipsis;
}
.connection-select-menu .menu-dropdown .menu-contents .caption .connection,
.connection-select-menu .menu-dropdown .menu-contents .caption .connection-group {
display: inline-block;
}

View File

@@ -48,7 +48,7 @@
height: 100%;
margin: 0 0.375em;
background: url('images/warning.png');
background: url('images/warning.svg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;

View File

@@ -37,13 +37,13 @@
/* Directory / file icons */
.file-browser .normal-file > .caption .icon {
background-image: url('images/file.png');
background-image: url('images/file.svg');
}
.file-browser .directory > .caption .icon {
background-image: url('images/folder-closed.png');
background-image: url('images/folder-closed.svg');
}
.file-browser .directory.previous > .caption .icon {
background-image: url('images/folder-up.png');
background-image: url('images/folder-up.svg');
}

View File

@@ -64,7 +64,7 @@
-khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat;
background-position: center center;
background-image: url('images/drive.png');
background-image: url('images/drive.svg');
width: 2em;
height: 2em;
padding: 0;

View File

@@ -108,51 +108,13 @@
}
#guac-menu #keyboard-settings .figure img {
max-width: 100%;
width: 100%;
}
#guac-menu #zoom-settings {
text-align: center;
}
#guac-menu #zoom-out,
#guac-menu #zoom-in,
#guac-menu #zoom-state {
display: inline-block;
vertical-align: middle;
}
#guac-menu #zoom-out,
#guac-menu #zoom-in {
max-width: 3em;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.1);
border-radius: 2em;
margin: 0.5em;
cursor: pointer;
}
#guac-menu #zoom-out img,
#guac-menu #zoom-in img {
max-width: 100%;
opacity: 0.5;
}
#guac-menu #zoom-out:hover,
#guac-menu #zoom-in:hover {
border: 1px solid rgba(0, 0, 0, 1);
background: #CDA;
}
#guac-menu #zoom-out:hover img,
#guac-menu #zoom-in:hover img {
opacity: 1;
}
#guac-menu #zoom-state {
font-size: 2em;
}
#guac-menu #devices .device {
padding: 1em;
@@ -176,7 +138,7 @@
}
#guac-menu #devices .device.filesystem {
background-image: url('images/drive.png');
background-image: url('images/drive.svg');
}
#guac-menu #share-links {

View File

@@ -26,7 +26,7 @@
width: 480px;
background: #EEE;
box-shadow: inset -1px 0 2px white, 1px 0 2px black;
z-index: 10;
z-index: 100;
-webkit-transition: left 0.125s, opacity 0.125s;
-moz-transition: left 0.125s, opacity 0.125s;
-ms-transition: left 0.125s, opacity 0.125s;
@@ -134,27 +134,6 @@
padding-top: 1em;
}
.menu-section input.zoom-ctrl {
width: 2em;
font-size: 1em;
padding: 0;
background: transparent;
border-color: rgba(0, 0, 0, 0.125);
}
.menu-section div.zoom-ctrl {
font-size: 1.5em;
display: inline;
align-content: center;
vertical-align: middle;
}
.menu-section .zoom-ctrl::-webkit-inner-spin-button,
.menu-section .zoom-ctrl::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.menu,
.menu.closed {
left: -480px;

View File

@@ -17,7 +17,79 @@
* under the License.
*/
.client .notification .parameters h3,
.client .notification .parameters .password-field .toggle-password {
.client-status-modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: rgba(0, 0, 0, 0.5);
}
.client-status-modal.shown {
display: block;
}
.client-status-modal guac-modal {
position: absolute;
}
.client-status-modal .notification {
background: rgba(40, 40, 40, 0.75);
color: white;
width: 100%;
max-width: 100%;
padding: 1em;
text-align: center;
border: none;
}
.client-status-modal .notification.error {
background: rgba(112, 9, 8, 0.75)
}
.client-status-modal .notification .title-bar {
display: none
}
.client-status-modal .notification .button {
background: transparent;
border: 2px solid white;
box-shadow: none;
text-shadow: none;
font-weight: normal;
}
.client-status-modal .notification .button:hover {
text-decoration: underline;
background: rgba(255, 255, 255, 0.25);
}
.client-status-modal .notification .button:active {
background: rgba(255, 255, 255, 0.5);
}
.client-status-modal .notification .parameters {
width: 100%;
max-width: 5in;
margin: 0 auto;
}
.client-status-modal .notification .parameters h3,
.client-status-modal .notification .parameters .password-field .toggle-password {
display: none;
}
.client-status-modal .notification .parameters input[type=email],
.client-status-modal .notification .parameters input[type=number],
.client-status-modal .notification .parameters input[type=password],
.client-status-modal .notification .parameters input[type=text],
.client-status-modal .notification .parameters textarea {
background: transparent;
border: 2px solid white;
color: white;
}

View File

@@ -53,6 +53,6 @@
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/share.png');
background-image: url('images/share.svg');
}

View File

@@ -25,11 +25,6 @@ div.thumbnail-main {
font-size: 0px;
}
.thumbnail-main img {
max-width: 100%;
}
.thumbnail-main .display {
position: absolute;
pointer-events: none;
}

View File

@@ -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;
}

View File

@@ -36,6 +36,14 @@
align-items: center;
}
.transfer-manager h3 {
margin: 0.25em;
font-size: 1em;
margin-bottom: 0;
opacity: 0.5;
text-align: center;
}
.transfer-manager .transfers {
display: table;
padding: 0.25em;

View File

@@ -69,7 +69,7 @@
.transfer.in-progress .progress {
background-color: #EEE;
background-image: url('images/progress.png');
background-image: url('images/progress.svg');
background-size: 16px 16px;
-moz-background-size: 16px 16px;

View File

@@ -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;
}

View File

@@ -6,15 +6,14 @@
<div class="client-view-content">
<!-- Central portion of view -->
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<div class="client-body" guac-touch-drag="menuDrag">
<!-- Client for current connection -->
<guac-client client="client"></guac-client>
<!-- All other active connections -->
<div id="other-connections">
<guac-client-panel clients="otherClients"></guac-client-panel>
</div>
<!-- All connections in current display -->
<guac-tiled-clients
on-close="closeClientTile($client)"
client-group="clientGroup"
emulate-absolute-mouse="menu.emulateAbsoluteMouse">
</guac-tiled-clients>
</div>
@@ -38,7 +37,7 @@
<!-- File transfers -->
<div id="file-transfer-dialog" ng-show="hasTransfers()">
<guac-file-transfer-manager client="client"></guac-file-transfer-manager>
<guac-file-transfer-manager client-group="clientGroup"></guac-file-transfer-manager>
</div>
<!-- Connection stability warning -->
@@ -48,13 +47,13 @@
<!-- Menu -->
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
<div class="menu-content" ng-if="menu.shown">
<div class="menu-content" ng-if="menu.shown" guac-touch-drag="menuDrag">
<!-- Stationary header -->
<div class="header">
<h2 ng-hide="rootConnectionGroups">{{client.name}}</h2>
<h2 ng-hide="rootConnectionGroups">{{ getName(clientGroup) }}</h2>
<h2 class="connection-select-menu" ng-show="rootConnectionGroups">
<guac-menu menu-title="client.name" interactive="true">
<guac-menu menu-title="getName(clientGroup)" interactive="true">
<div class="all-connections">
<guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups"
@@ -63,6 +62,7 @@
connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
<guac-group-list
connection-groups="filteredRootConnectionGroups"
context="connectionListContext"
templates="{
'connection' : 'app/client/templates/connection.html',
'connection-group' : 'app/client/templates/connectionGroup.html'
@@ -82,7 +82,7 @@
</div>
<!-- Scrollable body -->
<div class="menu-body" guac-touch-drag="menuDrag" guac-scroll="menu.scrollState">
<div class="menu-body" guac-touch-drag="visibleMenuDrag" guac-scroll="menu.scrollState">
<!-- Connection sharing -->
<div class="menu-section" id="share-links" ng-show="isShared()">
@@ -92,7 +92,7 @@
translate="CLIENT.HELP_SHARE_LINK"
translate-values="{LINKS : getShareLinkCount()}"></p>
<table>
<tr ng-repeat="link in client.shareLinks | toArray | orderBy: value.name">
<tr ng-repeat="link in focusedClient.shareLinks | toArray | orderBy: value.name">
<th>{{link.value.name}}</th>
<td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td>
</tr>
@@ -105,24 +105,24 @@
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<guac-clipboard data="client.clipboardData"></guac-clipboard>
<guac-clipboard></guac-clipboard>
</div>
</div>
<!-- Devices -->
<div class="menu-section" id="devices" ng-show="client.filesystems.length">
<div class="menu-section" id="devices" ng-if="focusedClient.filesystems.length">
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content">
<div class="device filesystem" ng-repeat="filesystem in client.filesystems" ng-click="showFilesystemMenu(filesystem)">
<div class="device filesystem" ng-repeat="filesystem in focusedClient.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
</div>
</div>
</div>
<!-- Connection parameters which may be modified while the connection is open -->
<div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
<guac-form namespace="getProtocolNamespace(client.protocol)"
content="client.forms"
<div class="menu-section connection-parameters" id="connection-settings" ng-if="focusedClient.protocol">
<guac-form namespace="getProtocolNamespace(focusedClient.protocol)"
content="focusedClient.forms"
model="menu.connectionParameters"
model-only="true"></guac-form>
</div>
@@ -140,7 +140,7 @@
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.png" alt=""></label></div>
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.svg" alt=""></label></div>
<label><input id="ime-text" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="text"> {{'CLIENT.NAME_INPUT_METHOD_TEXT' | translate}}</label>
<p class="caption"><label for="ime-text">{{'CLIENT.HELP_INPUT_METHOD_TEXT' | translate}} </label></p>
</div>
@@ -155,25 +155,25 @@
</div>
<!-- Mouse mode -->
<div class="menu-section" id="mouse-settings" ng-hide="client.multiTouchSupport">
<div class="menu-section" id="mouse-settings">
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div>
</div>
@@ -182,20 +182,12 @@
</div>
<!-- Display options -->
<div class="menu-section" id="display-settings">
<div class="menu-section" id="display-settings" ng-if="focusedClient">
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"></div>
<div class="zoom-ctrl">
<input type="number" class="zoom-ctrl" guac-zoom-ctrl
ng-model="client.clientProperties.scale"
ng-model-options="{ updateOn: 'blur submit' }"
ng-change="zoomSet()">%
<guac-client-zoom client="focusedClient"></guac-client-zoom>
</div>
<div ng-click="zoomIn()" id="zoom-in"><img src="images/settings/zoom-in.png" alt="+"></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>

View File

@@ -1,4 +1,9 @@
<a class="connection" ng-href="{{ item.getClientURL() }}">
<div class="connection">
<input type="checkbox"
ng-model="context.attachedClients[item.getClientIdentifier()]"
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
<a ng-href="{{ item.getClientURL() }}">
<div class="icon type" ng-class="item.protocol"></div>
<span class="name">{{item.name}}</span>
</a>
</div>

View File

@@ -1,4 +1,10 @@
<a class="connection-group" ng-href="{{ item.getClientURL() }}">
<div class="connection-group">
<input type="checkbox"
ng-show="item.balancing"
ng-model="context.attachedClients[item.getClientIdentifier()]"
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
<a ng-href="{{ item.getClientURL() }}">
<div ng-show="item.balancing" class="icon type balancer"></div>
<span class="name">{{item.name}}</span>
</a>
</div>

View File

@@ -1,4 +1,8 @@
<div class="main" guac-resize="mainElementResized">
<div class="main"
ng-class="{ 'drop-pending': dropPending }"
guac-resize="mainElementResized"
guac-touch-drag="clientDrag"
guac-touch-pinch="clientPinch">
<!-- Display -->
<div class="displayOuter">

View File

@@ -0,0 +1,5 @@
<div class="client-status-modal" ng-class="{ shown: status }">
<guac-modal>
<guac-notification notification="status"></guac-notification>
</guac-modal>
</div>

View File

@@ -1,29 +1,27 @@
<div class="client-panel"
ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }">
ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
<!-- Toggle panel visibility -->
<div class="client-panel-handle" ng-click="togglePanel()"></div>
<!-- List of connection thumbnails -->
<ul class="client-panel-connection-list">
<li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]"
ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }"
ng-show="isManaged(client.value)"
<li ng-repeat="clientGroup in clientGroups | orderBy: '-lastUsed'"
ng-if="!clientGroup.attached"
ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
class="client-panel-connection">
<!-- Close connection -->
<button class="close-other-connection" ng-click="disconnect(client.value)">
<button class="close-other-connection" ng-click="disconnect(clientGroup)">
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.png">
src="images/x.svg">
</button>
<!-- Thumbnail -->
<a href="#/client/{{client.value.id}}">
<div class="thumbnail">
<guac-thumbnail client="client.value"></guac-thumbnail>
</div>
<div class="name">{{ client.value.title }}</div>
<a href="#/client/{{ getIdentifier(clientGroup) }}">
<guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
<div class="name">{{ getTitle(clientGroup) }}</div>
</a>
</li>

View File

@@ -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>

View File

@@ -8,15 +8,15 @@
<!-- Sent/received files -->
<div class="transfer-manager-body">
<div class="transfer-manager-body-section" ng-repeat="client in clientGroup.clients" ng-show="hasTransfers(client)">
<h3 ng-show="hasMultipleClients(clientGroup)">{{ client.name }}</h3>
<div class="transfers">
<guac-file-transfer
transfer="upload"
ng-repeat="upload in client.uploads">
</guac-file-transfer><guac-file-transfer
transfer="download"
ng-repeat="download in client.downloads">
</guac-file-transfer>
</div>
</div>
</div>
</div>

View File

@@ -1,10 +1,11 @@
<div class="thumbnail-main" guac-resize="updateDisplayScale">
<!-- Display -->
<div class="displayOuter">
<div class="displayMiddle">
<div class="display">
</div>
<!-- Dummy background thumbnail -->
<img alt="" ng-src="{{thumbnail}}">
</div>
</div>
</div>

View File

@@ -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>

View File

@@ -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>

View File

@@ -22,9 +22,6 @@
*/
angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
// Required services
var preferenceService = $injector.get('preferenceService');
/**
* Object used for interacting with a guacClient directive.
*
@@ -69,19 +66,11 @@ angular.module('client').factory('ClientProperties', ['$injector', function defi
this.maxScale = template.maxScale || 3;
/**
* Whether or not the client should listen to keyboard events.
* Whether this client should receive keyboard events.
*
* @type Boolean
*/
this.keyboardEnabled = template.keyboardEnabled || true;
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type Boolean
*/
this.emulateAbsoluteMouse = template.emulateAbsoluteMouse || preferenceService.preferences.emulateAbsoluteMouse;
this.focused = template.focused || false;
/**
* The relative Y coordinate of the scroll offset of the display within

View File

@@ -24,34 +24,34 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
function defineManagedClient($rootScope, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData');
var ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFilesystem = $injector.get('ManagedFilesystem');
var ManagedFileUpload = $injector.get('ManagedFileUpload');
var ManagedShareLink = $injector.get('ManagedShareLink');
const ClientProperties = $injector.get('ClientProperties');
const ClientIdentifier = $injector.get('ClientIdentifier');
const ClipboardData = $injector.get('ClipboardData');
const ManagedArgument = $injector.get('ManagedArgument');
const ManagedClientState = $injector.get('ManagedClientState');
const ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
const ManagedDisplay = $injector.get('ManagedDisplay');
const ManagedFilesystem = $injector.get('ManagedFilesystem');
const ManagedFileUpload = $injector.get('ManagedFileUpload');
const ManagedShareLink = $injector.get('ManagedShareLink');
// Required services
var $document = $injector.get('$document');
var $q = $injector.get('$q');
var $rootScope = $injector.get('$rootScope');
var $window = $injector.get('$window');
var activeConnectionService = $injector.get('activeConnectionService');
var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var preferenceService = $injector.get('preferenceService');
var requestService = $injector.get('requestService');
var schemaService = $injector.get('schemaService');
var tunnelService = $injector.get('tunnelService');
var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory');
var guacImage = $injector.get('guacImage');
var guacVideo = $injector.get('guacVideo');
const $document = $injector.get('$document');
const $q = $injector.get('$q');
const $rootScope = $injector.get('$rootScope');
const $window = $injector.get('$window');
const activeConnectionService = $injector.get('activeConnectionService');
const authenticationService = $injector.get('authenticationService');
const clipboardService = $injector.get('clipboardService');
const connectionGroupService = $injector.get('connectionGroupService');
const connectionService = $injector.get('connectionService');
const preferenceService = $injector.get('preferenceService');
const requestService = $injector.get('requestService');
const tunnelService = $injector.get('tunnelService');
const guacAudio = $injector.get('guacAudio');
const guacHistory = $injector.get('guacHistory');
const guacImage = $injector.get('guacImage');
const guacVideo = $injector.get('guacVideo');
/**
* The minimum amount of time to wait between updates to the client
@@ -63,8 +63,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* client while it is active, allowing it to be detached and reattached
* from different client views.
* client while it is active, allowing it to be maintained in the
* background. One or more ManagedClients are grouped within
* ManagedClientGroups before being attached to the client view.
*
* @constructor
* @param {ManagedClient|Object} [template={}]
@@ -83,16 +84,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
this.id = template.id;
/**
* The time that the connection was last brought to the foreground of
* the current tab, as the number of milliseconds elapsed since
* midnight of January 1, 1970 UTC. If the connection has not yet been
* viewed, this will be 0.
*
* @type Number
*/
this.lastUsed = template.lastUsed || 0;
/**
* The actual underlying Guacamole client.
*
@@ -156,16 +147,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
this.thumbnail = template.thumbnail;
/**
* The current clipboard contents.
*
* @type ClipboardData
*/
this.clipboardData = template.clipboardData || new ClipboardData({
type : 'text/plain',
data : ''
});
/**
* The current state of all parameters requested by the server via
* "required" instructions, where each object key is the name of a
@@ -260,25 +241,30 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
* @param {ClientIdentifier} identifier
* The identifier representing the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
* @param {number} [width]
* The optimal display width, in local CSS pixels. If omitted, the
* browser window width will be used.
*
* @param {number} [height]
* The optimal display height, in local CSS pixels. If omitted, the
* browser window height will be used.
*
* @returns {Promise.<String>}
* A promise which resolves with the string of connection parameters to
* be passed to the Guacamole client, once the string is ready.
*/
var getConnectString = function getConnectString(identifier, connectionParameters) {
const getConnectString = function getConnectString(identifier, width, height) {
var deferred = $q.defer();
const deferred = $q.defer();
// Calculate optimal width/height for display
var pixel_density = $window.devicePixelRatio || 1;
var optimal_dpi = pixel_density * 96;
var optimal_width = $window.innerWidth * pixel_density;
var optimal_height = $window.innerHeight * pixel_density;
const pixel_density = $window.devicePixelRatio || 1;
const optimal_dpi = pixel_density * 96;
const optimal_width = width * pixel_density;
const optimal_height = height * pixel_density;
// Build base connect string
var connectString =
let connectString =
"token=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource)
+ "&GUAC_ID=" + encodeURIComponent(identifier.id)
@@ -286,8 +272,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
+ "&GUAC_WIDTH=" + Math.floor(optimal_width)
+ "&GUAC_HEIGHT=" + Math.floor(optimal_height)
+ "&GUAC_DPI=" + Math.floor(optimal_dpi)
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone)
+ (connectionParameters ? '&' + connectionParameters : '');
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone);
// Add audio mimetypes to connect string
guacAudio.supported.forEach(function(mimetype) {
@@ -347,22 +332,20 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
/**
* Creates a new ManagedClient, connecting it to the specified connection
* or group.
* Creates a new ManagedClient representing the specified connection or
* connection group. The ManagedClient will not initially be connected,
* and must be explicitly connected by invoking ManagedClient.connect().
*
* @param {String} id
* The ID of the connection or group to connect to. This String must be
* a valid ClientIdentifier string, as would be generated by
* ClientIdentifier.toString().
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {ManagedClient}
* A new ManagedClient instance which is connected to the connection or
* A new ManagedClient instance which represents the connection or
* connection group having the given ID.
*/
ManagedClient.getInstance = function getInstance(id, connectionParameters) {
ManagedClient.getInstance = function getInstance(id) {
var tunnel;
@@ -450,8 +433,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.ConnectionState.IDLE);
break;
// Ignore "connecting" state
case 1: // Connecting
// Conneccting
case 1:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connected + waiting
@@ -465,9 +450,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED);
// Send any clipboard data already provided
if (managedClient.clipboardData)
ManagedClient.setClipboard(managedClient, managedClient.clipboardData);
// Sync current clipboard data
clipboardService.getClipboard().then((data) => {
ManagedClient.setClipboard(managedClient, data);
}, angular.noop);
// Begin streaming audio input if possible
requestAudioStream(client);
@@ -562,12 +548,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Set clipboard contents once stream is finished
reader.onend = function textComplete() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = new ClipboardData({
clipboardService.setClipboard(new ClipboardData({
source : managedClient.id,
type : mimetype,
data : data
});
});
}))['catch'](angular.noop);
};
}
@@ -576,12 +561,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = function blobComplete() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = new ClipboardData({
clipboardService.setClipboard(new ClipboardData({
source : managedClient.id,
type : mimetype,
data : reader.getBlob()
});
});
}))['catch'](angular.noop);
};
}
@@ -607,7 +591,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Handle any received filesystem objects
client.onfilesystem = function fileSystemReceived(object, name) {
$rootScope.$apply(function exposeFilesystem() {
managedClient.filesystems.push(ManagedFilesystem.getInstance(object, name));
managedClient.filesystems.push(ManagedFilesystem.getInstance(managedClient, object, name));
});
};
@@ -627,11 +611,8 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Parse connection details from ID
var clientIdentifier = ClientIdentifier.fromString(id);
// Connect the Guacamole client
getConnectString(clientIdentifier, connectionParameters)
.then(function connectClient(connectString) {
client.connect(connectString);
});
// Defer actually connecting the Guacamole client until
// ManagedClient.connect() is explicitly invoked
// If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
@@ -671,6 +652,40 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
/**
* Connects the given ManagedClient instance to its associated connection
* or connection group. If the ManagedClient has already been connected,
* including if connected but subsequently disconnected, this function has
* no effect.
*
* @param {ManagedClient} managedClient
* The ManagedClient to connect.
*
* @param {number} [width]
* The optimal display width, in local CSS pixels. If omitted, the
* browser window width will be used.
*
* @param {number} [height]
* The optimal display height, in local CSS pixels. If omitted, the
* browser window height will be used.
*/
ManagedClient.connect = function connect(managedClient, width, height) {
// Ignore if already connected
if (managedClient.clientState.connectionState !== ManagedClientState.ConnectionState.IDLE)
return;
// Parse connection details from ID
const clientIdentifier = ClientIdentifier.fromString(managedClient.id);
// Connect the Guacamole client
getConnectString(clientIdentifier, width, height)
.then(function connectClient(connectString) {
managedClient.client.connect(connectString);
});
};
/**
* Uploads the given file to the server through the given Guacamole client.
* The file transfer can be monitored through the corresponding entry in
@@ -710,7 +725,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
/**
* Sends the given clipboard data over the given Guacamole client, setting
* the contents of the remote clipboard to the data provided.
* the contents of the remote clipboard to the data provided. If the given
* clipboard data was originally received from that client, the data is
* ignored and this function has no effect.
*
* @param {ManagedClient} managedClient
* The ManagedClient over which the given clipboard data is to be sent.
@@ -720,6 +737,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
ManagedClient.setClipboard = function setClipboard(managedClient, data) {
// Ignore clipboard data that was received from this connection
if (data.source === managedClient.id)
return;
var writer;
// Create stream with proper mimetype
@@ -873,6 +894,18 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
};
/**
* Returns whether the given client has any associated file transfers,
* regardless of those file transfers' state.
*
* @returns {boolean}
* true if there are any file transfers associated with the
* given client, false otherise.
*/
ManagedClient.hasTransfers = function hasTransfers(client) {
return !!(client && client.uploads && client.uploads.length);
};
/**
* Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this

View File

@@ -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;
}]);

View File

@@ -42,6 +42,14 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
// Use empty object by default
template = template || {};
/**
* The client that originally received the "filesystem" instruction
* that resulted in the creation of this ManagedFilesystem.
*
* @type ManagedClient
*/
this.client = template.client;
/**
* The Guacamole filesystem object, as received via a "filesystem"
* instruction.
@@ -162,6 +170,10 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* and human-readable name. Upon creation, a request to populate the
* contents of the root directory will be automatically dispatched.
*
* @param {ManagedClient} client
* The client that originally received the "filesystem" instruction
* that resulted in the creation of this ManagedFilesystem.
*
* @param {Guacamole.Object} object
* The Guacamole.Object defining the filesystem.
*
@@ -171,10 +183,11 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* @returns {ManagedFilesystem}
* The newly-created ManagedFilesystem.
*/
ManagedFilesystem.getInstance = function getInstance(object, name) {
ManagedFilesystem.getInstance = function getInstance(client, object, name) {
// Init new filesystem object
var managedFilesystem = new ManagedFilesystem({
client : client,
object : object,
name : name,
root : new ManagedFilesystem.File({
@@ -196,9 +209,6 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* client and filesystem. The browser will automatically start the
* download upon completion of this function.
*
* @param {ManagedClient} managedClient
* The ManagedClient from which the file is to be downloaded.
*
* @param {ManagedFilesystem} managedFilesystem
* The ManagedFilesystem from which the file is to be downloaded. Any
* path information provided must be relative to this filesystem.
@@ -206,7 +216,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
* @param {String} path
* The full, absolute path of the file to download.
*/
ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) {
ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) {
// Request download
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
@@ -215,7 +225,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
var filename = path.match(/(.*[\\/])?(.*)/)[2];
// Start download
tunnelService.downloadStream(managedClient.tunnel.uuid, stream, mimetype, filename);
tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid, stream, mimetype, filename);
});

View File

@@ -18,16 +18,19 @@
*/
/**
* A directive provides an editor whose contents are exposed via a
* ClipboardData object via the "data" attribute. If this data should also be
* synced to the local clipboard, or sent via a connected Guacamole client
* using a "guacClipboard" event, it is up to external code to do so.
* A directive provides an editor for the clipboard content maintained by
* clipboardService. Changes to the clipboard by clipboardService will
* automatically be reflected in the editor, and changes in the editor will
* automatically be reflected in the clipboard by clipboardService.
*/
angular.module('clipboard').directive('guacClipboard', ['$injector',
function guacClipboard($injector) {
// Required types
var ClipboardData = $injector.get('ClipboardData');
const ClipboardData = $injector.get('ClipboardData');
// Required services
const clipboardService = $injector.get('clipboardService');
/**
* Configuration object for the guacClipboard directive.
@@ -40,20 +43,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
templateUrl : 'app/clipboard/templates/guacClipboard.html'
};
// Scope properties exposed by the guacClipboard directive
config.scope = {
/**
* The data to display within the field provided by this directive. This
* data will modified or replaced when the user manually alters the
* contents of the field.
*
* @type ClipboardData
*/
data : '='
};
// guacClipboard directive controller
config.controller = ['$scope', '$injector', '$element',
function guacClipboardController($scope, $injector, $element) {
@@ -75,12 +64,27 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
var updateClipboardData = function updateClipboardData() {
// Read contents of clipboard textarea
$scope.$evalAsync(function assignClipboardText() {
$scope.data = new ClipboardData({
clipboardService.setClipboard(new ClipboardData({
type : 'text/plain',
data : element.value
});
});
}));
};
/**
* Updates the contents of the clipboard editor to the given data.
*
* @param {ClipboardData} data
* The ClipboardData to display within the clipboard editor for
* editing.
*/
const updateClipboardEditor = function updateClipboardEditor(data) {
// If the clipboard data is a string, render it as text
if (typeof data.data === 'string')
element.value = data.data;
// Ignore other data types for now
};
@@ -89,17 +93,15 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
element.addEventListener('input', updateClipboardData);
element.addEventListener('change', updateClipboardData);
// Watch clipboard for new data, updating the clipboard textarea as
// necessary
$scope.$watch('data', function clipboardDataChanged(data) {
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function clipboardChanged(event, data) {
updateClipboardEditor(data);
});
// If the clipboard data is a string, render it as text
if (typeof data.data === 'string')
element.value = data.data;
// Ignore other data types for now
}); // end $scope.data watch
// Init clipboard editor with current clipboard contents
clipboardService.getClipboard().then((data) => {
updateClipboardEditor(data);
}, angular.noop);
}];

View File

@@ -18,17 +18,31 @@
*/
/**
* A service for accessing local clipboard data.
* A service for maintaining and accessing clipboard data. If possible, this
* service will leverage the local clipboard. If the local clipboard is not
* available, an internal in-memory clipboard will be used instead.
*/
angular.module('clipboard').factory('clipboardService', ['$injector',
function clipboardService($injector) {
// Get required services
var $q = $injector.get('$q');
var $window = $injector.get('$window');
const $q = $injector.get('$q');
const $window = $injector.get('$window');
const $rootScope = $injector.get('$rootScope');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
var ClipboardData = $injector.get('ClipboardData');
const ClipboardData = $injector.get('ClipboardData');
/**
* Getter/setter which retrieves or sets the current stored clipboard
* contents. The stored clipboard contents are strictly internal to
* Guacamole, and may not reflect the local clipboard if local clipboard
* access is unavailable.
*
* @type Function
*/
const storedClipboardData = sessionStorageFactory.create(new ClipboardData());
var service = {};
@@ -175,7 +189,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
* A promise that will resolve if setting the clipboard was successful,
* and will reject if it failed.
*/
service.setLocalClipboard = function setLocalClipboard(data) {
const setLocalClipboard = function setLocalClipboard(data) {
var deferred = $q.defer();
@@ -423,7 +437,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
* if getting the clipboard was successful, and will reject if it
* failed.
*/
service.getLocalClipboard = function getLocalClipboard() {
const getLocalClipboard = function getLocalClipboard() {
// If the clipboard is already being read, do not overlap the read
// attempts; instead share the result across all requests
@@ -548,6 +562,64 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
};
/**
* Returns the current value of the internal clipboard shared across all
* active Guacamole connections running within the current browser tab. If
* access to the local clipboard is available, the internal clipboard is
* first synchronized with the current local clipboard contents. If access
* to the local clipboard is unavailable, only the internal clipboard will
* be used.
*
* @return {Promise.<ClipboardData>}
* A promise that will resolve with the contents of the internal
* clipboard, first retrieving those contents from the local clipboard
* if permission to do so has been granted. This promise is always
* resolved.
*/
service.getClipboard = function getClipboard() {
return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData());
};
/**
* Sets the content of the internal clipboard shared across all active
* Guacamole connections running within the current browser tab. If
* access to the local clipboard is available, the local clipboard is
* first set to the provided clipboard content. If access to the local
* clipboard is unavailable, only the internal clipboard will be used. A
* "guacClipboard" event will be broadcast with the assigned data once the
* operation has completed.
*
* @param {ClipboardData} data
* The data to assign to the clipboard.
*
* @return {Promise}
* A promise that will resolve after the clipboard content has been
* set. This promise is always resolved.
*/
service.setClipboard = function setClipboard(data) {
return setLocalClipboard(data)['catch'](angular.noop).finally(() => {
// Update internal clipboard and broadcast event notifying of
// updated contents
storedClipboardData(data);
$rootScope.$broadcast('guacClipboard', data);
});
};
/**
* Resynchronizes the local and internal clipboards, setting the contents
* of the internal clipboard to that of the local clipboard (if local
* clipboard access is granted) and broadcasting a "guacClipboard" event
* with the current internal clipboard contents for consumption by external
* components like the "guacClient" directive.
*/
service.resyncClipboard = function resyncClipboard() {
service.getClipboard().then(function clipboardRead(data) {
return service.setClipboard(data);
}, angular.noop);
};
return service;
}]);

View File

@@ -47,7 +47,7 @@
display: block;
margin: 0 auto;
border: 1px solid black;
background: url('images/checker.png');
background: url('images/checker.svg');
}
.clipboard-service-target {

View File

@@ -36,6 +36,15 @@ angular.module('clipboard').factory('ClipboardData', [function defineClipboardDa
// Use empty object by default
template = template || {};
/**
* The ID of the ManagedClient handling the remote desktop connection
* that originated this clipboard data, or null if the data originated
* from the clipboard editor or local clipboard.
*
* @type {string}
*/
this.source = template.source;
/**
* The mimetype of the data currently stored within the clipboard.
*

View File

@@ -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
};
}]);

View File

@@ -38,10 +38,10 @@
/* Icon for unmasking passwords */
.form-field .password-field input[type=password] ~ .icon.toggle-password {
background-image: url('images/action-icons/guac-show-pass.png');
background-image: url('images/action-icons/guac-show-pass.svg');
}
/* Icon for masking passwords */
.form-field .password-field input[type=text] ~ .icon.toggle-password {
background-image: url('images/action-icons/guac-hide-pass.png');
background-image: url('images/action-icons/guac-hide-pass.svg');
}

View File

@@ -18,7 +18,8 @@
*/
/**
* A directive which displays the contents of a connection group.
* A directive which displays the recently-accessed connections nested beneath
* each of the given connection groups.
*/
angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() {
@@ -44,21 +45,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
// Required types
var ActiveConnection = $injector.get('ActiveConnection');
var ClientIdentifier = $injector.get('ClientIdentifier');
var RecentConnection = $injector.get('RecentConnection');
// Required services
var guacClientManager = $injector.get('guacClientManager');
var guacHistory = $injector.get('guacHistory');
/**
* Array of all known and visible active connections.
*
* @type ActiveConnection[]
*/
$scope.activeConnections = [];
/**
* Array of all known and visible recently-used connections.
*
@@ -68,16 +60,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
/**
* Returns whether recent connections are available for display.
* Note that, for the sake of this directive, recent connections
* include any currently-active connections, even if they are not
* yet in the history.
*
* @returns {Boolean}
* true if recent (or active) connections are present, false
* otherwise.
* true if recent connections are present, false otherwise.
*/
$scope.hasRecentConnections = function hasRecentConnections() {
return !!($scope.activeConnections.length || $scope.recentConnections.length);
return !!$scope.recentConnections.length;
};
/**
@@ -149,7 +137,6 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
$scope.$watch("rootGroups", function setRootGroups(rootGroups) {
// Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = [];
// Produce collection of visible objects
@@ -160,29 +147,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
});
}
var managedClients = guacClientManager.getManagedClients();
// Add all active connections
for (var id in managedClients) {
// Get corresponding managed client
var client = managedClients[id];
// Add active connections for clients with associated visible objects
if (id in visibleObjects) {
var object = visibleObjects[id];
$scope.activeConnections.push(new ActiveConnection(object.name, client));
}
}
// Add any recent connections that are visible
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects
if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) {
if (historyEntry.id in visibleObjects) {
var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));

View File

@@ -3,23 +3,6 @@
<!-- Text displayed if no recent connections exist -->
<p class="placeholder" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
<!-- All active connections -->
<div ng-repeat="activeConnection in activeConnections" class="connection">
<a href="#/client/{{activeConnection.client.id}}">
<!-- Connection thumbnail -->
<div class="thumbnail">
<guac-thumbnail client="activeConnection.client"></guac-thumbnail>
</div>
<!-- Connection name -->
<div class="caption">
<span class="name">{{activeConnection.name}}</span>
</div>
</a>
</div>
<!-- All recent connections -->
<div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}">

View File

@@ -181,10 +181,11 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
})
// Client view
.when('/client/:id/:params?', {
.when('/client/:id', {
bodyClassName : 'client',
templateUrl : 'app/client/templates/client.html',
controller : 'clientController',
reloadOnUrl : false,
resolve : { updateCurrentToken: updateCurrentToken }
})

View File

@@ -24,11 +24,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) {
// Required services
var $document = $injector.get('$document');
var $route = $injector.get('$route');
var $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification');
const $document = $injector.get('$document');
const $route = $injector.get('$route');
const $window = $injector.get('$window');
const clipboardService = $injector.get('clipboardService');
const guacNotification = $injector.get('guacNotification');
const guacClientManager = $injector.get('guacClientManager');
/**
* The error that prevents the current page from rendering at all. If no
@@ -43,6 +44,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/
$scope.guacNotification = guacNotification;
/**
* All currently-active connections, grouped into their corresponding
* tiled views.
*
* @type ManagedClientGroup[]
*/
$scope.getManagedClientGroups = guacClientManager.getManagedClientGroups;
/**
* The message to display to the user as instructions for the login
* process.
@@ -154,9 +163,8 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Broadcast keydown events
keyboard.onkeydown = function onkeydown(keysym) {
// Do not handle key events if not logged in or if a notification is
// shown
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
// Do not handle key events if not logged in
if ($scope.applicationState !== ApplicationState.READY)
return true;
// Warn of pending keydown
@@ -175,7 +183,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Do not handle key events if not logged in or if a notification is
// shown
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
if ($scope.applicationState !== ApplicationState.READY)
return;
// Warn of pending keyup
@@ -199,25 +207,15 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
keyboard.reset();
});
/**
* Checks whether the clipboard data has changed, firing a new
* "guacClipboard" event if it has.
*/
var checkClipboard = function checkClipboard() {
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
$scope.$broadcast('guacClipboard', data);
}, angular.noop);
};
// Attempt to read the clipboard if it may have changed
$window.addEventListener('load', checkClipboard, true);
$window.addEventListener('copy', checkClipboard);
$window.addEventListener('cut', checkClipboard);
$window.addEventListener('load', clipboardService.resyncClipboard, true);
$window.addEventListener('copy', clipboardService.resyncClipboard);
$window.addEventListener('cut', clipboardService.resyncClipboard);
$window.addEventListener('focus', function focusGained(e) {
// Only recheck clipboard if it's the window itself that gained focus
if (e.target === $window)
checkClipboard();
clipboardService.resyncClipboard();
}, true);

View File

@@ -48,3 +48,12 @@
from { opacity: 1; }
to { opacity: 0; }
}
/**
* popin: Increase in size and opacity from invisibly tiny and transparent to
* full size and opaque.
*/
@keyframes popin {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); }
}

View File

@@ -105,30 +105,30 @@ input[type="submit"]:disabled, button:disabled, button.danger:disabled {
.button.logout::before,
button.logout::before {
background-image: url('images/action-icons/guac-logout.png');
background-image: url('images/action-icons/guac-logout.svg');
}
.button.reconnect::before,
button.reconnect::before {
background-image: url('images/circle-arrows.png');
background-image: url('images/circle-arrows.svg');
}
.button.manage::before,
button.manage::before {
background-image: url('images/action-icons/guac-config.png');
background-image: url('images/action-icons/guac-config.svg');
}
.button.back::before,
button.back::before {
background-image: url('images/action-icons/guac-back.png');
background-image: url('images/action-icons/guac-back.svg');
}
.button.home::before,
button.home::before {
background-image: url('images/action-icons/guac-home.png');
background-image: url('images/action-icons/guac-home.svg');
}
.button.change-password::before,
button.change-password::before {
background-image: url('images/action-icons/guac-key.png');
background-image: url('images/action-icons/guac-key.svg');
}

View File

@@ -38,7 +38,7 @@
.fatal-page-error h1::before {
content: ' ';
display: inline-block;
background: url('images/warning.png');
background: url('images/warning.svg');
background-repeat: no-repeat;
height: 1em;
width: 1em;

View File

@@ -49,7 +49,7 @@
top: 50%;
left: 50%;
background-image: url('images/cog.png');
background-image: url('images/cog.svg');
background-size: 96px 96px;
background-position: center center;
background-repeat: no-repeat;

View File

@@ -20,7 +20,7 @@
#other-connections .client-panel {
display: none;
position: absolute;
position: fixed;
right: 0;
bottom: 0;
@@ -56,7 +56,7 @@
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
background-image: url(images/arrows/right.png);
background-image: url(images/arrows/right.svg);
opacity: 0.5;
}
@@ -66,7 +66,7 @@
}
#other-connections .client-panel.hidden .client-panel-handle {
background-image: url(images/arrows/left.png);
background-image: url(images/arrows/left.svg);
}
#other-connections .client-panel-connection-list {
@@ -92,6 +92,7 @@
background: black;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
animation: 0.1s linear 0s popin;
opacity: 0.5;
transition: opacity 0.25s;
@@ -101,11 +102,6 @@
}
#other-connections .client-panel-connection .thumbnail-main img {
max-width: none;
max-height: 128px;
}
#other-connections .client-panel-connection a[href]::before {
display: block;
@@ -118,7 +114,7 @@
width: 100%;
z-index: 1;
background: url('images/warning-white.png');
background: url('images/warning-white.svg');
background-size: 48px;
background-position: center;
background-repeat: no-repeat;
@@ -160,6 +156,7 @@
#other-connections button.close-other-connection img {
background: #A43;
border-radius: 18px;
width: 100%;
max-width: 18px;
padding: 3px;
}

View File

@@ -52,10 +52,10 @@ table.sorted th.sort-primary:after {
background-size: 1em 1em;
background-position: right center;
background-repeat: no-repeat;
background-image: url('images/arrows/down.png');
background-image: url('images/arrows/down.svg');
}
table.sorted th.sort-primary.sort-descending:after {
background-image: url('images/arrows/up.png');
background-image: url('images/arrows/up.svg');
}

View File

@@ -149,27 +149,27 @@ div.section {
*/
.icon.user {
background-image: url('images/user-icons/guac-user.png');
background-image: url('images/user-icons/guac-user.svg');
}
.icon.user.add {
background-image: url('images/action-icons/guac-user-add.png');
background-image: url('images/action-icons/guac-user-add.svg');
}
.icon.user-group {
background-image: url('images/user-icons/guac-user-group.png');
background-image: url('images/user-icons/guac-user-group.svg');
}
.icon.user-group.add {
background-image: url('images/action-icons/guac-user-group-add.png');
background-image: url('images/action-icons/guac-user-group-add.svg');
}
.icon.connection {
background-image: url('images/protocol-icons/guac-plug.png');
background-image: url('images/protocol-icons/guac-plug.svg');
}
.icon.connection.add {
background-image: url('images/action-icons/guac-monitor-add.png');
background-image: url('images/action-icons/guac-monitor-add.svg');
}
.connection .icon,
@@ -187,30 +187,30 @@ div.section {
}
.connection-group > .caption .icon {
background-image: url('images/folder-closed.png');
background-image: url('images/folder-closed.svg');
}
.connection-group.expanded > .caption .icon {
background-image: url('images/folder-open.png');
background-image: url('images/folder-open.svg');
}
.connection .icon {
background-image: url('images/protocol-icons/guac-plug.png');
background-image: url('images/protocol-icons/guac-plug.svg');
}
.connection .icon.kubernetes,
.connection .icon.ssh,
.connection .icon.telnet {
background-image: url('images/protocol-icons/guac-text.png');
background-image: url('images/protocol-icons/guac-text.svg');
}
.connection .icon.vnc,
.connection .icon.rdp {
background-image: url('images/protocol-icons/guac-monitor.png');
background-image: url('images/protocol-icons/guac-monitor.svg');
}
.sharing-profile .icon {
background-image: url('images/share.png');
background-image: url('images/share.svg');
}
/*
@@ -223,7 +223,7 @@ div.section {
}
.connection-group.empty.balancer .icon {
background-image: url('images/protocol-icons/guac-monitor.png');
background-image: url('images/protocol-icons/guac-monitor.svg');
}
.expandable.expanded > .children > .list-item {
@@ -259,16 +259,16 @@ div.section {
}
.expandable > .caption .icon.expand {
background-image: url('images/group-icons/guac-closed.png');
background-image: url('images/group-icons/guac-closed.svg');
}
.expandable.expanded > .caption .icon.expand {
background-image: url('images/group-icons/guac-open.png');
background-image: url('images/group-icons/guac-open.svg');
}
.expandable.empty > .caption .icon.expand {
opacity: 0.25;
background-image: url('images/group-icons/guac-open.png');
background-image: url('images/group-icons/guac-open.svg');
}
.history th,

View File

@@ -22,7 +22,7 @@
}
.filter .search-string {
background-image: url('images/magnifier.png');
background-image: url('images/magnifier.svg');
background-repeat: no-repeat;
background-size: 1.75em;
background-position: 0.25em center;

View File

@@ -71,17 +71,17 @@
}
.pager .icon.first-page {
background-image: url('images/action-icons/guac-first-page.png');
background-image: url('images/action-icons/guac-first-page.svg');
}
.pager .icon.prev-page {
background-image: url('images/action-icons/guac-prev-page.png');
background-image: url('images/action-icons/guac-prev-page.svg');
}
.pager .icon.next-page {
background-image: url('images/action-icons/guac-next-page.png');
background-image: url('images/action-icons/guac-next-page.svg');
}
.pager .icon.last-page {
background-image: url('images/action-icons/guac-last-page.png');
background-image: url('images/action-icons/guac-last-page.svg');
}

View File

@@ -103,7 +103,7 @@
-moz-background-size: 3em 3em;
-webkit-background-size: 3em 3em;
-khtml-background-size: 3em 3em;
background-image: url("images/guac-tricolor.png");
background-image: url("images/guac-tricolor.svg");
}
.login-ui.continuation .login-dialog {

View File

@@ -39,11 +39,11 @@
}
.manage-user-group .page-tabs .page-list li.read-only a[href]:before {
background-image: url('images/lock.png');
background-image: url('images/lock.svg');
}
.manage-user-group .page-tabs .page-list li.unlinked a[href]:before {
background-image: url('images/plus.png');
background-image: url('images/plus.svg');
}
.manage-user-group .page-tabs .page-list li.unlinked a[href] {
@@ -56,7 +56,7 @@
}
.manage-user-group .page-tabs .page-list li.linked a[href]:before {
background-image: url('images/checkmark.png');
background-image: url('images/checkmark.svg');
}
.manage-user-group .notice.read-only {

View File

@@ -39,11 +39,11 @@
}
.manage-user .page-tabs .page-list li.read-only a[href]:before {
background-image: url('images/lock.png');
background-image: url('images/lock.svg');
}
.manage-user .page-tabs .page-list li.unlinked a[href]:before {
background-image: url('images/plus.png');
background-image: url('images/plus.svg');
}
.manage-user .page-tabs .page-list li.unlinked a[href] {
@@ -56,7 +56,7 @@
}
.manage-user .page-tabs .page-list li.linked a[href]:before {
background-image: url('images/checkmark.png');
background-image: url('images/checkmark.svg');
}
.manage-user .notice.read-only {

View File

@@ -12,12 +12,12 @@
<!-- Abbreviated list of only the currently selected objects -->
<div class="abbreviated-related-objects">
<img src="images/arrows/right.png" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()">
<img src="images/arrows/down.png" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
<img src="images/arrows/right.svg" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()">
<img src="images/arrows/down.svg" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
<p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p>
<ul>
<li ng-repeat="identifier in identifiers | filter: filterString">
<label><img src="images/x-red.png" alt="Remove" class="remove"
<label><img src="images/x-red.svg" alt="Remove" class="remove"
ng-click="removeIdentifier(identifier)"
ng-show="isEditable[identifier]"><span class="identifier">{{ identifier }}</span>
</label>

View File

@@ -92,7 +92,7 @@
background-repeat: no-repeat;
background-size: 1em;
background-position: center center;
background-image: url('images/arrows/down.png');
background-image: url('images/arrows/down.svg');
}

View File

@@ -54,7 +54,7 @@
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/user-icons/guac-user.png');
background-image: url('images/user-icons/guac-user.svg');
}
@@ -64,23 +64,23 @@
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
background-image: url('images/protocol-icons/guac-monitor.png');
background-image: url('images/protocol-icons/guac-monitor.svg');
}
.user-menu .menu-dropdown .menu-contents li a[href="#/"] {
background-image: url('images/action-icons/guac-home-dark.png');
background-image: url('images/action-icons/guac-home-dark.svg');
}
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/users"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/connections"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/sessions"],
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/preferences"] {
background-image: url('images/action-icons/guac-config-dark.png');
background-image: url('images/action-icons/guac-config-dark.svg');
}
.user-menu .menu-dropdown .menu-contents li a.logout {
background-image: url('images/action-icons/guac-logout-dark.png');
background-image: url('images/action-icons/guac-logout-dark.svg');
}
.user-menu .menu-dropdown .menu-contents .profile {

View File

@@ -18,20 +18,30 @@
*/
guac-modal {
display: table;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 10;
overflow: hidden;
}
guac-modal .modal-contents {
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
width: 100%;
text-align: center;
display: table-cell;
vertical-align: middle;
}
guac-modal {

View File

@@ -74,7 +74,7 @@
.notification .progress {
width: 100%;
background: #C2C2C2 url('images/progress.png');
background: #C2C2C2 url('images/progress.svg');
background-size: 16px 16px;
-moz-background-size: 16px 16px;
-webkit-background-size: 16px 16px;

View File

@@ -45,17 +45,17 @@ a.button.add-connection-group::before {
}
a.button.add-user::before {
background-image: url('images/action-icons/guac-user-add.png');
background-image: url('images/action-icons/guac-user-add.svg');
}
a.button.add-user-group::before {
background-image: url('images/action-icons/guac-user-group-add.png');
background-image: url('images/action-icons/guac-user-group-add.svg');
}
a.button.add-connection::before {
background-image: url('images/action-icons/guac-monitor-add.png');
background-image: url('images/action-icons/guac-monitor-add.svg');
}
a.button.add-connection-group::before {
background-image: url('images/action-icons/guac-group-add.png');
background-image: url('images/action-icons/guac-group-add.svg');
}

View File

@@ -72,7 +72,7 @@
<div class="choice">
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<p class="caption"><label for="absolute">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div>
</div>
@@ -81,7 +81,7 @@
<div class="choice">
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<p class="caption"><label for="relative">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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