GUACAMOLE-724: Merge multiple-connection tile support/view.
@@ -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;
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
||||
|
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for displaying a non-global notification describing the status
|
||||
* of a specific Guacamole client, including prompts for any information
|
||||
* necessary to continue the connection.
|
||||
*/
|
||||
angular.module('client').directive('guacClientNotification', [function guacClientNotification() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacClientNotification.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The client whose status should be displayed.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacClientNotificationController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientState = $injector.get('ManagedClientState');
|
||||
const Protocol = $injector.get('Protocol');
|
||||
|
||||
// Required services
|
||||
const $location = $injector.get('$location');
|
||||
const authenticationService = $injector.get('authenticationService');
|
||||
const guacClientManager = $injector.get('guacClientManager');
|
||||
const requestService = $injector.get('requestService');
|
||||
const userPageService = $injector.get('userPageService');
|
||||
|
||||
/**
|
||||
* A Notification object describing the client status to display as a
|
||||
* dialog or prompt, as would be accepted by guacNotification.showStatus(),
|
||||
* or false if no status should be shown.
|
||||
*
|
||||
* @type {Notification|Object|Boolean}
|
||||
*/
|
||||
$scope.status = false;
|
||||
|
||||
/**
|
||||
* All client error codes handled and passed off for translation. Any error
|
||||
* code not present in this list will be represented by the "DEFAULT"
|
||||
* translation.
|
||||
*/
|
||||
const CLIENT_ERRORS = {
|
||||
0x0201: true,
|
||||
0x0202: true,
|
||||
0x0203: true,
|
||||
0x0207: true,
|
||||
0x0208: true,
|
||||
0x0209: true,
|
||||
0x020A: true,
|
||||
0x020B: true,
|
||||
0x0301: true,
|
||||
0x0303: true,
|
||||
0x0308: true,
|
||||
0x031D: true
|
||||
};
|
||||
|
||||
/**
|
||||
* All error codes for which automatic reconnection is appropriate when a
|
||||
* client error occurs.
|
||||
*/
|
||||
const CLIENT_AUTO_RECONNECT = {
|
||||
0x0200: true,
|
||||
0x0202: true,
|
||||
0x0203: true,
|
||||
0x0207: true,
|
||||
0x0208: true,
|
||||
0x0301: true,
|
||||
0x0308: true
|
||||
};
|
||||
|
||||
/**
|
||||
* All tunnel error codes handled and passed off for translation. Any error
|
||||
* code not present in this list will be represented by the "DEFAULT"
|
||||
* translation.
|
||||
*/
|
||||
const TUNNEL_ERRORS = {
|
||||
0x0201: true,
|
||||
0x0202: true,
|
||||
0x0203: true,
|
||||
0x0204: true,
|
||||
0x0205: true,
|
||||
0x0207: true,
|
||||
0x0208: true,
|
||||
0x0301: true,
|
||||
0x0303: true,
|
||||
0x0308: true,
|
||||
0x031D: true
|
||||
};
|
||||
|
||||
/**
|
||||
* All error codes for which automatic reconnection is appropriate when a
|
||||
* tunnel error occurs.
|
||||
*/
|
||||
const TUNNEL_AUTO_RECONNECT = {
|
||||
0x0200: true,
|
||||
0x0202: true,
|
||||
0x0203: true,
|
||||
0x0207: true,
|
||||
0x0208: true,
|
||||
0x0308: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which logs out from Guacamole entirely.
|
||||
*/
|
||||
const LOGOUT_ACTION = {
|
||||
name : "CLIENT.ACTION_LOGOUT",
|
||||
className : "logout button",
|
||||
callback : function logoutCallback() {
|
||||
authenticationService.logout()
|
||||
['catch'](requestService.IGNORE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which returns the user to the home screen. If the home page has
|
||||
* not yet been determined, this will be null.
|
||||
*/
|
||||
let NAVIGATE_HOME_ACTION = null;
|
||||
|
||||
// Assign home page action once user's home page has been determined
|
||||
userPageService.getHomePage()
|
||||
.then(function homePageRetrieved(homePage) {
|
||||
|
||||
// Define home action only if different from current location
|
||||
if ($location.path() !== homePage.url) {
|
||||
NAVIGATE_HOME_ACTION = {
|
||||
name : "CLIENT.ACTION_NAVIGATE_HOME",
|
||||
className : "home button",
|
||||
callback : function navigateHomeCallback() {
|
||||
$location.url(homePage.url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}, requestService.WARN);
|
||||
|
||||
/**
|
||||
* Action which replaces the current client with a newly-connected client.
|
||||
*/
|
||||
const RECONNECT_ACTION = {
|
||||
name : "CLIENT.ACTION_RECONNECT",
|
||||
className : "reconnect button",
|
||||
callback : function reconnectCallback() {
|
||||
$scope.client = guacClientManager.replaceManagedClient($scope.client.id);
|
||||
$scope.status = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The reconnect countdown to display if an error or status warrants an
|
||||
* automatic, timed reconnect.
|
||||
*/
|
||||
const RECONNECT_COUNTDOWN = {
|
||||
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
|
||||
callback: RECONNECT_ACTION.callback,
|
||||
remaining: 15
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a notification at the end of a Guacamole connection, whether
|
||||
* that connection is ending normally or due to an error. As the end of
|
||||
* a Guacamole connection may be due to changes in authentication status,
|
||||
* this will also implicitly peform a re-authentication attempt to check
|
||||
* for such changes, possibly resulting in auth-related events like
|
||||
* guacInvalidCredentials.
|
||||
*
|
||||
* @param {Notification|Boolean|Object} status
|
||||
* The status notification to show, as would be accepted by
|
||||
* guacNotification.showStatus().
|
||||
*/
|
||||
const notifyConnectionClosed = function notifyConnectionClosed(status) {
|
||||
|
||||
// Re-authenticate to verify auth status at end of connection
|
||||
authenticationService.updateCurrentToken($location.search())
|
||||
['catch'](requestService.IGNORE)
|
||||
|
||||
// Show the requested status once the authentication check has finished
|
||||
['finally'](function authenticationCheckComplete() {
|
||||
$scope.status = status;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies the user that the connection state has changed.
|
||||
*
|
||||
* @param {String} connectionState
|
||||
* The current connection state, as defined by
|
||||
* ManagedClientState.ConnectionState.
|
||||
*/
|
||||
const notifyConnectionState = function notifyConnectionState(connectionState) {
|
||||
|
||||
// Hide any existing status
|
||||
$scope.status = false;
|
||||
|
||||
// Do not display status if status not known
|
||||
if (!connectionState)
|
||||
return;
|
||||
|
||||
// Build array of available actions
|
||||
let actions;
|
||||
if (NAVIGATE_HOME_ACTION)
|
||||
actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
|
||||
else
|
||||
actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
|
||||
|
||||
// Get any associated status code
|
||||
const status = $scope.client.clientState.statusCode;
|
||||
|
||||
// Connecting
|
||||
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|
||||
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
|
||||
$scope.status = {
|
||||
title: "CLIENT.DIALOG_HEADER_CONNECTING",
|
||||
text: {
|
||||
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Client error
|
||||
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
|
||||
|
||||
// Determine translation name of error
|
||||
const errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
|
||||
|
||||
// Determine whether the reconnect countdown applies
|
||||
const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
|
||||
|
||||
// Show error status
|
||||
notifyConnectionClosed({
|
||||
className : "error",
|
||||
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
|
||||
text : {
|
||||
key : "CLIENT.ERROR_CLIENT_" + errorName
|
||||
},
|
||||
countdown : countdown,
|
||||
actions : actions
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Tunnel error
|
||||
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
|
||||
|
||||
// Determine translation name of error
|
||||
const errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
|
||||
|
||||
// Determine whether the reconnect countdown applies
|
||||
const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
|
||||
|
||||
// Show error status
|
||||
notifyConnectionClosed({
|
||||
className : "error",
|
||||
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
|
||||
text : {
|
||||
key : "CLIENT.ERROR_TUNNEL_" + errorName
|
||||
},
|
||||
countdown : countdown,
|
||||
actions : actions
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Disconnected
|
||||
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
|
||||
notifyConnectionClosed({
|
||||
title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
|
||||
text : {
|
||||
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
|
||||
},
|
||||
actions : actions
|
||||
});
|
||||
}
|
||||
|
||||
// Hide status for all other states
|
||||
else
|
||||
$scope.status = false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompts the user to enter additional connection parameters. If the
|
||||
* protocol and associated parameters of the underlying connection are not
|
||||
* yet known, this function has no effect and should be re-invoked once
|
||||
* the parameters are known.
|
||||
*
|
||||
* @param {Object.<String, String>} requiredParameters
|
||||
* The set of all parameters requested by the server via "required"
|
||||
* instructions, where each object key is the name of a requested
|
||||
* parameter and each value is the current value entered by the user.
|
||||
*/
|
||||
const notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
|
||||
|
||||
/**
|
||||
* Action which submits the current set of parameter values, requesting
|
||||
* that the connection continue.
|
||||
*/
|
||||
const SUBMIT_PARAMETERS = {
|
||||
name : "CLIENT.ACTION_CONTINUE",
|
||||
className : "button",
|
||||
callback : function submitParameters() {
|
||||
if ($scope.client) {
|
||||
const params = $scope.client.requiredParameters;
|
||||
$scope.client.requiredParameters = null;
|
||||
ManagedClient.sendArguments($scope.client, params);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which cancels submission of additional parameters and
|
||||
* disconnects from the current connection.
|
||||
*/
|
||||
const CANCEL_PARAMETER_SUBMISSION = {
|
||||
name : "CLIENT.ACTION_CANCEL",
|
||||
className : "button",
|
||||
callback : function cancelSubmission() {
|
||||
$scope.client.requiredParameters = null;
|
||||
$scope.client.client.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
// Attempt to prompt for parameters only if the parameters that apply
|
||||
// to the underlying connection are known
|
||||
if (!$scope.client.protocol || !$scope.client.forms)
|
||||
return;
|
||||
|
||||
// Prompt for parameters
|
||||
$scope.status = {
|
||||
formNamespace : Protocol.getNamespace($scope.client.protocol),
|
||||
forms : $scope.client.forms,
|
||||
formModel : requiredParameters,
|
||||
formSubmitCallback : SUBMIT_PARAMETERS.callback,
|
||||
actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given connection state allows for submission of
|
||||
* connection parameters via "argv" instructions.
|
||||
*
|
||||
* @param {String} connectionState
|
||||
* The connection state to test, as defined by
|
||||
* ManagedClientState.ConnectionState.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if the given connection state allows submission of connection
|
||||
* parameters via "argv" instructions, false otherwise.
|
||||
*/
|
||||
const canSubmitParameters = function canSubmitParameters(connectionState) {
|
||||
return (connectionState === ManagedClientState.ConnectionState.WAITING ||
|
||||
connectionState === ManagedClientState.ConnectionState.CONNECTED);
|
||||
};
|
||||
|
||||
// Show status dialog when connection status changes
|
||||
$scope.$watchGroup([
|
||||
'client.clientState.connectionState',
|
||||
'client.requiredParameters',
|
||||
'client.protocol',
|
||||
'client.forms'
|
||||
], function clientStateChanged(newValues) {
|
||||
|
||||
const connectionState = newValues[0];
|
||||
const requiredParameters = newValues[1];
|
||||
|
||||
// Prompt for parameters only if parameters can actually be submitted
|
||||
if (requiredParameters && canSubmitParameters(connectionState))
|
||||
notifyParametersRequired(requiredParameters);
|
||||
|
||||
// Otherwise, just show general connection state
|
||||
else
|
||||
notifyConnectionState(connectionState);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Prevents the default behavior of the given AngularJS event if a
|
||||
* notification is currently shown and the client is focused.
|
||||
*
|
||||
* @param {event} e
|
||||
* The AngularJS event to selectively prevent.
|
||||
*/
|
||||
const preventDefaultDuringNotification = function preventDefaultDuringNotification(e) {
|
||||
if ($scope.status && $scope.client.clientProperties.focused)
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Block internal handling of key events (by the client) if a
|
||||
// notification is visible
|
||||
$scope.$on('guacBeforeKeydown', preventDefaultDuringNotification);
|
||||
$scope.$on('guacBeforeKeyup', preventDefaultDuringNotification);
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
@@ -25,11 +25,12 @@
|
||||
angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
|
||||
|
||||
// Required services
|
||||
var guacClientManager = $injector.get('guacClientManager');
|
||||
var sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
const guacClientManager = $injector.get('guacClientManager');
|
||||
const sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
|
||||
// Required types
|
||||
var ManagedClientState = $injector.get('ManagedClientState');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
const ManagedClientState = $injector.get('ManagedClientState');
|
||||
|
||||
/**
|
||||
* Getter/setter for the boolean flag controlling whether the client panel
|
||||
@@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The ManagedClient instances associated with the active
|
||||
* The ManagedClientGroup instances associated with the active
|
||||
* connections to be displayed within this panel.
|
||||
*
|
||||
* @type ManagedClient[]|Object.<String, ManagedClient>
|
||||
* @type ManagedClientGroup[]
|
||||
*/
|
||||
clients : '='
|
||||
clientGroups : '='
|
||||
|
||||
},
|
||||
templateUrl: 'app/client/templates/guacClientPanel.html',
|
||||
@@ -75,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));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for controlling the zoom level and scale-to-fit behavior of a
|
||||
* a single Guacamole client.
|
||||
*/
|
||||
angular.module('client').directive('guacClientZoom', [function guacClientZoom() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacClientZoom.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The client to control the zoom/autofit of.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacClientZoomController($scope, $injector, $element) {
|
||||
|
||||
/**
|
||||
* Zooms in by 10%, automatically disabling autofit.
|
||||
*/
|
||||
$scope.zoomIn = function zoomIn() {
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale += 0.1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Zooms out by 10%, automatically disabling autofit.
|
||||
*/
|
||||
$scope.zoomOut = function zoomOut() {
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale -= 0.1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the client autofit setting to false.
|
||||
*/
|
||||
$scope.clearAutoFit = function clearAutoFit() {
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies that the autofit setting has been manually changed by the
|
||||
* user.
|
||||
*/
|
||||
$scope.autoFitChanged = function autoFitChanged() {
|
||||
|
||||
// Reset to 100% scale when autofit is first disabled
|
||||
if (!$scope.client.clientProperties.autoFit)
|
||||
$scope.client.clientProperties.scale = 1;
|
||||
|
||||
};
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
@@ -28,14 +28,6 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The client whose file transfers should be managed by this
|
||||
* directive.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '=',
|
||||
|
||||
/**
|
||||
* @type ManagedFilesystem
|
||||
*/
|
||||
@@ -116,7 +108,7 @@ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser(
|
||||
* The file to download.
|
||||
*/
|
||||
$scope.downloadFile = function downloadFile(file) {
|
||||
ManagedFilesystem.downloadFile($scope.client, $scope.filesystem, file.streamName);
|
||||
ManagedFilesystem.downloadFile($scope.filesystem, file.streamName);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -28,12 +28,12 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The client whose file transfers should be managed by this
|
||||
* The client group whose file transfers should be managed by this
|
||||
* directive.
|
||||
*
|
||||
* @type ManagerClient
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
client : '='
|
||||
clientGroup : '='
|
||||
|
||||
},
|
||||
|
||||
@@ -41,7 +41,9 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
|
||||
controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
|
||||
|
||||
// Required types
|
||||
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
const ManagedFileTransferState = $injector.get('ManagedFileTransferState');
|
||||
|
||||
/**
|
||||
* Determines whether the given file transfer state indicates an
|
||||
@@ -74,17 +76,29 @@ angular.module('client').directive('guacFileTransferManager', [function guacFile
|
||||
*/
|
||||
$scope.clearCompletedTransfers = function clearCompletedTransfers() {
|
||||
|
||||
// Nothing to clear if no client attached
|
||||
if (!$scope.client)
|
||||
// Nothing to clear if no client group attached
|
||||
if (!$scope.clientGroup)
|
||||
return;
|
||||
|
||||
// Remove completed uploads
|
||||
$scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) {
|
||||
$scope.clientGroup.clients.forEach(client => {
|
||||
client.uploads = client.uploads.filter(function isUploadInProgress(upload) {
|
||||
return isInProgress(upload.transferState);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.hasMultipleClients
|
||||
*/
|
||||
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClient.hasTransfers
|
||||
*/
|
||||
$scope.hasTransfers = ManagedClient.hasTransfers;
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
|
@@ -43,20 +43,6 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
|
||||
// Required services
|
||||
var $window = $injector.get('$window');
|
||||
|
||||
/**
|
||||
* The optimal thumbnail width, in pixels.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var THUMBNAIL_WIDTH = 320;
|
||||
|
||||
/**
|
||||
* The optimal thumbnail height, in pixels.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var THUMBNAIL_HEIGHT = 240;
|
||||
|
||||
/**
|
||||
* The display of the current Guacamole client instance.
|
||||
*
|
||||
@@ -126,32 +112,7 @@ angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
|
||||
|
||||
// Update scale when display is resized
|
||||
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) {
|
||||
|
||||
var width;
|
||||
var height;
|
||||
|
||||
// If no display size yet, assume optimal thumbnail size
|
||||
if (!size || size.width === 0 || size.height === 0) {
|
||||
width = THUMBNAIL_WIDTH;
|
||||
height = THUMBNAIL_HEIGHT;
|
||||
}
|
||||
|
||||
// Otherwise, generate size that fits within thumbnail bounds
|
||||
else {
|
||||
var scale = Math.min(THUMBNAIL_WIDTH / size.width, THUMBNAIL_HEIGHT / size.height, 1);
|
||||
width = size.width * scale;
|
||||
height = size.height * scale;
|
||||
}
|
||||
|
||||
// Generate dummy background image
|
||||
var thumbnail = document.createElement("canvas");
|
||||
thumbnail.width = width;
|
||||
thumbnail.height = height;
|
||||
$scope.thumbnail = thumbnail.toDataURL("image/png");
|
||||
|
||||
// Init display scale
|
||||
$scope.$evalAsync($scope.updateDisplayScale);
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
|
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which displays one or more Guacamole clients in an evenly-tiled
|
||||
* view. The number of rows and columns used for the arrangement of tiles is
|
||||
* automatically determined by the number of clients present.
|
||||
*/
|
||||
angular.module('client').directive('guacTiledClients', [function guacTiledClients() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/client/templates/guacTiledClients.html',
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The function to invoke when the "close" button in the header of a
|
||||
* client tile is clicked. The ManagedClient that is closed will be
|
||||
* made available to the Angular expression defining the callback as
|
||||
* "$client".
|
||||
*
|
||||
* @type function
|
||||
*/
|
||||
onClose : '&',
|
||||
|
||||
/**
|
||||
* The group of Guacamole clients that should be displayed in an
|
||||
* evenly-tiled grid arrangement.
|
||||
*
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
clientGroup : '=',
|
||||
|
||||
/**
|
||||
* Whether translation of touch to mouse events should emulate an
|
||||
* absolute pointer device, or a relative pointer device.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
emulateAbsoluteMouse : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacTiledClientsController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
|
||||
/**
|
||||
* Returns the currently-focused ManagedClient. If there is no such
|
||||
* client, or multiple clients are focused, null is returned.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* The currently-focused client, or null if there are no focused
|
||||
* clients or if multiple clients are focused.
|
||||
*/
|
||||
$scope.getFocusedClient = function getFocusedClient() {
|
||||
|
||||
const managedClientGroup = $scope.clientGroup;
|
||||
if (managedClientGroup) {
|
||||
const focusedClients = _.filter(managedClientGroup.clients, client => client.clientProperties.focused);
|
||||
if (focusedClients.length === 1)
|
||||
return focusedClients[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
// Notify whenever identify of currently-focused client changes
|
||||
$scope.$watch('getFocusedClient()', function focusedClientChanged(focusedClient) {
|
||||
$scope.$emit('guacClientFocused', focusedClient);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a callback for guacClick that assigns or updates keyboard
|
||||
* focus to the given client, allowing that client to receive and
|
||||
* handle keyboard events. Multiple clients may have keyboard focus
|
||||
* simultaneously.
|
||||
*
|
||||
* @param {ManagedClient} client
|
||||
* The client that should receive keyboard focus.
|
||||
*
|
||||
* @return {guacClick~callback}
|
||||
* The callback that guacClient should invoke when the given client
|
||||
* has been clicked.
|
||||
*/
|
||||
$scope.getFocusAssignmentCallback = function getFocusAssignmentCallback(client) {
|
||||
return (shift, ctrl) => {
|
||||
|
||||
// Clear focus of all other clients if not selecting multiple
|
||||
if (!shift && !ctrl) {
|
||||
$scope.clientGroup.clients.forEach(client => {
|
||||
client.clientProperties.focused = false;
|
||||
});
|
||||
}
|
||||
|
||||
client.clientProperties.focused = true;
|
||||
|
||||
// Fill in any gaps if performing rectangular multi-selection
|
||||
// via shift-click
|
||||
if (shift) {
|
||||
|
||||
let minRow = $scope.clientGroup.rows - 1;
|
||||
let minColumn = $scope.clientGroup.columns - 1;
|
||||
let maxRow = 0;
|
||||
let maxColumn = 0;
|
||||
|
||||
// Determine extents of selected area
|
||||
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
|
||||
if (client.clientProperties.focused) {
|
||||
minRow = Math.min(minRow, row);
|
||||
minColumn = Math.min(minColumn, column);
|
||||
maxRow = Math.max(maxRow, row);
|
||||
maxColumn = Math.max(maxColumn, column);
|
||||
}
|
||||
});
|
||||
|
||||
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
|
||||
client.clientProperties.focused =
|
||||
row >= minRow
|
||||
&& row <= maxRow
|
||||
&& column >= minColumn
|
||||
&& column <= maxColumn;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.hasMultipleClients
|
||||
*/
|
||||
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getClientGrid
|
||||
*/
|
||||
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClient.isShared
|
||||
*/
|
||||
$scope.isShared = ManagedClient.isShared;
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for displaying a group of Guacamole clients as a non-interactive
|
||||
* thumbnail of tiled client displays.
|
||||
*/
|
||||
angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacTiledThumbnails.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The group of clients to display as a thumbnail of tiled client
|
||||
* displays.
|
||||
*
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
clientGroup : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacTiledThumbnailsController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
|
||||
/**
|
||||
* The overall height of the thumbnail view of the tiled grid of
|
||||
* clients within the client group, in pixels. This value is
|
||||
* intentionally based off a snapshot of the current browser size at
|
||||
* the time the directive comes into existence to ensure the contents
|
||||
* of the thumbnail are familiar in appearance and aspect ratio.
|
||||
*/
|
||||
$scope.height = Math.min(window.innerHeight, 128);
|
||||
|
||||
/**
|
||||
* The overall width of the thumbnail view of the tiled grid of
|
||||
* clients within the client group, in pixels. This value is
|
||||
* intentionally based off a snapshot of the current browser size at
|
||||
* the time the directive comes into existence to ensure the contents
|
||||
* of the thumbnail are familiar in appearance and aspect ratio.
|
||||
*/
|
||||
$scope.width = window.innerWidth / window.innerHeight * $scope.height;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getClientGrid
|
||||
*/
|
||||
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
@@ -24,11 +24,12 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
function guacClientManager($injector) {
|
||||
|
||||
// Required types
|
||||
var ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
|
||||
// Required services
|
||||
var $window = $injector.get('$window');
|
||||
var sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
const $window = $injector.get('$window');
|
||||
const sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
|
||||
var service = {};
|
||||
|
||||
@@ -56,6 +57,65 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
return storedManagedClients();
|
||||
};
|
||||
|
||||
/**
|
||||
* Getter/setter which retrieves or sets the array of all active managed
|
||||
* client groups.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() {
|
||||
|
||||
// Disconnect all clients when storage is destroyed
|
||||
service.clear();
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns an array of all managed client groups.
|
||||
*
|
||||
* @returns {ManagedClientGroup[]>}
|
||||
* An array of all active managed client groups.
|
||||
*/
|
||||
service.getManagedClientGroups = function getManagedClientGroups() {
|
||||
return storedManagedClientGroups();
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the ManagedClient with the given ID from all
|
||||
* ManagedClientGroups, automatically adjusting the tile size of the
|
||||
* clients that remain in each group. All client groups that are empty
|
||||
* after the client is removed will also be removed.
|
||||
*
|
||||
* @param {string} id
|
||||
* The ID of the ManagedClient to remove.
|
||||
*/
|
||||
const ungroupManagedClient = function ungroupManagedClient(id) {
|
||||
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
|
||||
// Remove client from all groups
|
||||
managedClientGroups.forEach(group => {
|
||||
const removed = _.remove(group.clients, client => (client.id === id));
|
||||
if (removed.length) {
|
||||
|
||||
// Reset focus state if client is being removed from a group
|
||||
// that isn't currently attached (focus may otherwise be
|
||||
// retained and result in a newly added connection unexpectedly
|
||||
// sharing focus)
|
||||
if (!group.attached)
|
||||
removed.forEach(client => { client.clientProperties.focused = false; });
|
||||
|
||||
// Recalculate group grid if number of clients is changing
|
||||
ManagedClientGroup.recalculateTiles(group);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Remove any groups that are now empty
|
||||
_.remove(managedClientGroups, group => !group.clients.length);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the existing ManagedClient associated with the connection having
|
||||
* the given ID, if any. If no such a ManagedClient already exists, this
|
||||
@@ -67,13 +127,16 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
* @returns {Boolean}
|
||||
* true if an existing client was removed, false otherwise.
|
||||
*/
|
||||
service.removeManagedClient = function replaceManagedClient(id) {
|
||||
service.removeManagedClient = function removeManagedClient(id) {
|
||||
|
||||
var managedClients = storedManagedClients();
|
||||
|
||||
// Remove client if it exists
|
||||
if (id in managedClients) {
|
||||
|
||||
// Pull client out of any containing groups
|
||||
ungroupManagedClient(id);
|
||||
|
||||
// Disconnect and remove
|
||||
managedClients[id].client.disconnect();
|
||||
delete managedClients[id];
|
||||
@@ -96,22 +159,37 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
* @param {String} id
|
||||
* The ID of the connection whose ManagedClient should be retrieved.
|
||||
*
|
||||
* @param {String} [connectionParameters]
|
||||
* Any additional HTTP parameters to pass while connecting. This
|
||||
* parameter only has an effect if a new connection is established as
|
||||
* a result of this function call.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* The ManagedClient associated with the connection having the given
|
||||
* ID.
|
||||
*/
|
||||
service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) {
|
||||
service.replaceManagedClient = function replaceManagedClient(id) {
|
||||
|
||||
// Disconnect any existing client
|
||||
service.removeManagedClient(id);
|
||||
const managedClients = storedManagedClients();
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
|
||||
// Set new client
|
||||
return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters);
|
||||
// Remove client if it exists
|
||||
if (id in managedClients) {
|
||||
|
||||
const hadFocus = managedClients[id].clientProperties.focused;
|
||||
managedClients[id].client.disconnect();
|
||||
delete managedClients[id];
|
||||
|
||||
// Remove client from all groups
|
||||
managedClientGroups.forEach(group => {
|
||||
|
||||
const index = _.findIndex(group.clients, client => (client.id === id));
|
||||
if (index === -1)
|
||||
return;
|
||||
|
||||
group.clients[index] = managedClients[id] = ManagedClient.getInstance(id);
|
||||
managedClients[id].clientProperties.focused = hadFocus;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return managedClients[id];
|
||||
|
||||
};
|
||||
|
||||
@@ -123,22 +201,21 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
* @param {String} id
|
||||
* The ID of the connection whose ManagedClient should be retrieved.
|
||||
*
|
||||
* @param {String} [connectionParameters]
|
||||
* Any additional HTTP parameters to pass while connecting. This
|
||||
* parameter only has an effect if a new connection is established as
|
||||
* a result of this function call.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* The ManagedClient associated with the connection having the given
|
||||
* ID.
|
||||
*/
|
||||
service.getManagedClient = function getManagedClient(id, connectionParameters) {
|
||||
service.getManagedClient = function getManagedClient(id) {
|
||||
|
||||
var managedClients = storedManagedClients();
|
||||
|
||||
// Ensure any existing client is removed from its containing group
|
||||
// prior to being returned
|
||||
ungroupManagedClient(id);
|
||||
|
||||
// Create new managed client if it doesn't already exist
|
||||
if (!(id in managedClients))
|
||||
managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
|
||||
managedClients[id] = ManagedClient.getInstance(id);
|
||||
|
||||
// Return existing client
|
||||
return managedClients[id];
|
||||
@@ -146,7 +223,88 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnects and removes all currently-connected clients.
|
||||
* Returns the ManagedClientGroup having the given ID. If no such
|
||||
* ManagedClientGroup exists, a new ManagedClientGroup is created by
|
||||
* extracting the relevant connections from the ID.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the ManagedClientGroup to retrieve or create.
|
||||
*
|
||||
* @returns {ManagedClientGroup}
|
||||
* The ManagedClientGroup having the given ID.
|
||||
*/
|
||||
service.getManagedClientGroup = function getManagedClientGroup(id) {
|
||||
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
const existingGroup = _.find(managedClientGroups, (group) => {
|
||||
return id === ManagedClientGroup.getIdentifier(group);
|
||||
});
|
||||
|
||||
// Prefer to return the existing group if it exactly matches
|
||||
if (existingGroup)
|
||||
return existingGroup;
|
||||
|
||||
const clients = [];
|
||||
const clientIds = ManagedClientGroup.getClientIdentifiers(id);
|
||||
|
||||
// Separate active clients by whether they should be displayed within
|
||||
// the current view
|
||||
clientIds.forEach(function groupClients(id) {
|
||||
clients.push(service.getManagedClient(id));
|
||||
});
|
||||
|
||||
const group = new ManagedClientGroup({
|
||||
clients : clients
|
||||
});
|
||||
|
||||
// Focus the first client if there are no clients focused
|
||||
ManagedClientGroup.verifyFocus(group);
|
||||
|
||||
managedClientGroups.push(group);
|
||||
return group;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the existing ManagedClientGroup having the given ID, if any,
|
||||
* disconnecting and removing all ManagedClients associated with that
|
||||
* group. If no such a ManagedClientGroup currently exists, this function
|
||||
* has no effect.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the ManagedClientGroup to remove.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if a ManagedClientGroup was removed, false otherwise.
|
||||
*/
|
||||
service.removeManagedClientGroup = function removeManagedClientGroup(id) {
|
||||
|
||||
const managedClients = storedManagedClients();
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
|
||||
// Remove all matching groups (there SHOULD only be one)
|
||||
const removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id);
|
||||
|
||||
// Disconnect all clients associated with the removed group(s)
|
||||
removed.forEach((group) => {
|
||||
group.clients.forEach((client) => {
|
||||
|
||||
const id = client.id;
|
||||
if (managedClients[id]) {
|
||||
managedClients[id].client.disconnect();
|
||||
delete managedClients[id];
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return !!removed.length;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnects and removes all currently-connected clients and client
|
||||
* groups.
|
||||
*/
|
||||
service.clear = function clear() {
|
||||
|
||||
@@ -156,8 +314,9 @@ angular.module('client').factory('guacClientManager', ['$injector',
|
||||
for (var id in managedClients)
|
||||
managedClients[id].client.disconnect();
|
||||
|
||||
// Clear managed clients
|
||||
// Clear managed clients and client groups
|
||||
storedManagedClients({});
|
||||
storedManagedClientGroups([]);
|
||||
|
||||
};
|
||||
|
||||
|
@@ -103,7 +103,7 @@ body.client {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.client-view .client-body .main {
|
||||
.client-view .client-body .tiled-client-list {
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -125,5 +125,13 @@ body.client {
|
||||
background-size: 1em;
|
||||
background-position: 0.75em center;
|
||||
padding-left: 2.5em;
|
||||
background-image: url('images/x.png');
|
||||
background-image: url('images/x.svg');
|
||||
}
|
||||
|
||||
.client .drop-pending .display {
|
||||
background: #3161a9;
|
||||
}
|
||||
|
||||
.client .drop-pending .display > *{
|
||||
opacity: 0.5;
|
||||
}
|
@@ -55,3 +55,8 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown .menu-contents .caption .connection,
|
||||
.connection-select-menu .menu-dropdown .menu-contents .caption .connection-group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@
|
||||
height: 100%;
|
||||
margin: 0 0.375em;
|
||||
|
||||
background: url('images/warning.png');
|
||||
background: url('images/warning.svg');
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
@@ -37,13 +37,13 @@
|
||||
/* Directory / file icons */
|
||||
|
||||
.file-browser .normal-file > .caption .icon {
|
||||
background-image: url('images/file.png');
|
||||
background-image: url('images/file.svg');
|
||||
}
|
||||
|
||||
.file-browser .directory > .caption .icon {
|
||||
background-image: url('images/folder-closed.png');
|
||||
background-image: url('images/folder-closed.svg');
|
||||
}
|
||||
|
||||
.file-browser .directory.previous > .caption .icon {
|
||||
background-image: url('images/folder-up.png');
|
||||
background-image: url('images/folder-up.svg');
|
||||
}
|
||||
|
@@ -64,7 +64,7 @@
|
||||
-khtml-background-size: 1.5em 1.5em;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('images/drive.png');
|
||||
background-image: url('images/drive.svg');
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0;
|
||||
|
@@ -108,51 +108,13 @@
|
||||
}
|
||||
|
||||
#guac-menu #keyboard-settings .figure img {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-settings {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-out,
|
||||
#guac-menu #zoom-in,
|
||||
#guac-menu #zoom-state {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-out,
|
||||
#guac-menu #zoom-in {
|
||||
max-width: 3em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2em;
|
||||
margin: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-out img,
|
||||
#guac-menu #zoom-in img {
|
||||
max-width: 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-out:hover,
|
||||
#guac-menu #zoom-in:hover {
|
||||
border: 1px solid rgba(0, 0, 0, 1);
|
||||
background: #CDA;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-out:hover img,
|
||||
#guac-menu #zoom-in:hover img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-state {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#guac-menu #devices .device {
|
||||
|
||||
padding: 1em;
|
||||
@@ -176,7 +138,7 @@
|
||||
}
|
||||
|
||||
#guac-menu #devices .device.filesystem {
|
||||
background-image: url('images/drive.png');
|
||||
background-image: url('images/drive.svg');
|
||||
}
|
||||
|
||||
#guac-menu #share-links {
|
||||
|
@@ -26,7 +26,7 @@
|
||||
width: 480px;
|
||||
background: #EEE;
|
||||
box-shadow: inset -1px 0 2px white, 1px 0 2px black;
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
-webkit-transition: left 0.125s, opacity 0.125s;
|
||||
-moz-transition: left 0.125s, opacity 0.125s;
|
||||
-ms-transition: left 0.125s, opacity 0.125s;
|
||||
@@ -134,27 +134,6 @@
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.menu-section input.zoom-ctrl {
|
||||
width: 2em;
|
||||
font-size: 1em;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-color: rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.menu-section div.zoom-ctrl {
|
||||
font-size: 1.5em;
|
||||
display: inline;
|
||||
align-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.menu-section .zoom-ctrl::-webkit-inner-spin-button,
|
||||
.menu-section .zoom-ctrl::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu,
|
||||
.menu.closed {
|
||||
left: -480px;
|
||||
|
@@ -17,7 +17,79 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.client .notification .parameters h3,
|
||||
.client .notification .parameters .password-field .toggle-password {
|
||||
.client-status-modal {
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: none;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
}
|
||||
|
||||
.client-status-modal.shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.client-status-modal guac-modal {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.client-status-modal .notification {
|
||||
background: rgba(40, 40, 40, 0.75);
|
||||
color: white;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.client-status-modal .notification.error {
|
||||
background: rgba(112, 9, 8, 0.75)
|
||||
}
|
||||
|
||||
.client-status-modal .notification .title-bar {
|
||||
display: none
|
||||
}
|
||||
|
||||
.client-status-modal .notification .button {
|
||||
background: transparent;
|
||||
border: 2px solid white;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.client-status-modal .notification .button:hover {
|
||||
text-decoration: underline;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.client-status-modal .notification .button:active {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.client-status-modal .notification .parameters {
|
||||
width: 100%;
|
||||
max-width: 5in;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.client-status-modal .notification .parameters h3,
|
||||
.client-status-modal .notification .parameters .password-field .toggle-password {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.client-status-modal .notification .parameters input[type=email],
|
||||
.client-status-modal .notification .parameters input[type=number],
|
||||
.client-status-modal .notification .parameters input[type=password],
|
||||
.client-status-modal .notification .parameters input[type=text],
|
||||
.client-status-modal .notification .parameters textarea {
|
||||
background: transparent;
|
||||
border: 2px solid white;
|
||||
color: white;
|
||||
}
|
||||
|
@@ -53,6 +53,6 @@
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em;
|
||||
background-position: 0.5em center;
|
||||
background-image: url('images/share.png');
|
||||
background-image: url('images/share.svg');
|
||||
|
||||
}
|
||||
|
@@ -25,11 +25,6 @@ div.thumbnail-main {
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
.thumbnail-main img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.thumbnail-main .display {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Overall tiled grid layout.
|
||||
*/
|
||||
|
||||
.tiled-client-grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tiled-client-grid,
|
||||
.tiled-client-grid .tiled-client-row,
|
||||
.tiled-client-grid .tiled-client-cell,
|
||||
.tiled-client-grid .client-tile {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tiled-client-grid {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tiled-client-grid .tiled-client-row {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/*
|
||||
* Rendering of individual clients within tiles.
|
||||
*/
|
||||
|
||||
.tiled-client-grid .client-tile {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .client-tile-header {
|
||||
|
||||
display: -webkit-box;
|
||||
|
||||
display: -webkit-flex;
|
||||
|
||||
display: -ms-flexbox;
|
||||
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
background: #444;
|
||||
padding: 0 0.25em;
|
||||
font-size: 0.8em;
|
||||
color: white;
|
||||
z-index: 30;
|
||||
min-height: 1.5em;
|
||||
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile.focused .client-tile-header {
|
||||
background-color: #3161a9;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .client-tile-header > * {
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0;
|
||||
-ms-flex: 0;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .client-tile-header .client-tile-name {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .main {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile-disconnect,
|
||||
.tiled-client-grid .client-tile-shared-indicator {
|
||||
max-height: 1em;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile-shared-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tiled-client-grid .shared .client-tile-shared-indicator {
|
||||
display: inline;
|
||||
}
|
@@ -36,6 +36,14 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.transfer-manager h3 {
|
||||
margin: 0.25em;
|
||||
font-size: 1em;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.transfer-manager .transfers {
|
||||
display: table;
|
||||
padding: 0.25em;
|
||||
|
@@ -69,7 +69,7 @@
|
||||
.transfer.in-progress .progress {
|
||||
|
||||
background-color: #EEE;
|
||||
background-image: url('images/progress.png');
|
||||
background-image: url('images/progress.svg');
|
||||
|
||||
background-size: 16px 16px;
|
||||
-moz-background-size: 16px 16px;
|
||||
|
75
guacamole/src/main/frontend/src/app/client/styles/zoom.css
Normal 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;
|
||||
}
|
@@ -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>
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -1,4 +1,8 @@
|
||||
<div class="main" guac-resize="mainElementResized">
|
||||
<div class="main"
|
||||
ng-class="{ 'drop-pending': dropPending }"
|
||||
guac-resize="mainElementResized"
|
||||
guac-touch-drag="clientDrag"
|
||||
guac-touch-pinch="clientPinch">
|
||||
|
||||
<!-- Display -->
|
||||
<div class="displayOuter">
|
||||
|
@@ -0,0 +1,5 @@
|
||||
<div class="client-status-modal" ng-class="{ shown: status }">
|
||||
<guac-modal>
|
||||
<guac-notification notification="status"></guac-notification>
|
||||
</guac-modal>
|
||||
</div>
|
@@ -1,29 +1,27 @@
|
||||
<div class="client-panel"
|
||||
ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }">
|
||||
ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
|
||||
|
||||
<!-- Toggle panel visibility -->
|
||||
<div class="client-panel-handle" ng-click="togglePanel()"></div>
|
||||
|
||||
<!-- List of connection thumbnails -->
|
||||
<ul class="client-panel-connection-list">
|
||||
<li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]"
|
||||
ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }"
|
||||
ng-show="isManaged(client.value)"
|
||||
<li ng-repeat="clientGroup in clientGroups | orderBy: '-lastUsed'"
|
||||
ng-if="!clientGroup.attached"
|
||||
ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
|
||||
class="client-panel-connection">
|
||||
|
||||
<!-- Close connection -->
|
||||
<button class="close-other-connection" ng-click="disconnect(client.value)">
|
||||
<button class="close-other-connection" ng-click="disconnect(clientGroup)">
|
||||
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
src="images/x.png">
|
||||
src="images/x.svg">
|
||||
</button>
|
||||
|
||||
<!-- Thumbnail -->
|
||||
<a href="#/client/{{client.value.id}}">
|
||||
<div class="thumbnail">
|
||||
<guac-thumbnail client="client.value"></guac-thumbnail>
|
||||
</div>
|
||||
<div class="name">{{ client.value.title }}</div>
|
||||
<a href="#/client/{{ getIdentifier(clientGroup) }}">
|
||||
<guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
|
||||
<div class="name">{{ getTitle(clientGroup) }}</div>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
@@ -0,0 +1,18 @@
|
||||
<div class="client-zoom">
|
||||
<div class="client-zoom-editor">
|
||||
<div ng-click="zoomOut()" class="client-zoom-out"><img src="images/settings/zoom-out.svg" alt="-"></div>
|
||||
<div class="client-zoom-state">
|
||||
<input type="number" guac-zoom-ctrl
|
||||
ng-model="client.clientProperties.scale"
|
||||
ng-model-options="{ updateOn: 'blur submit' }"
|
||||
ng-change="zoomSet()">%
|
||||
</div>
|
||||
<div ng-click="zoomIn()" class="client-zoom-in"><img src="images/settings/zoom-in.svg" alt="+"></div>
|
||||
</div>
|
||||
<div class="client-zoom-autofit">
|
||||
<label><input ng-model="client.clientProperties.autoFit"
|
||||
ng-change="changeAutoFit()"
|
||||
ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit">
|
||||
{{'CLIENT.TEXT_ZOOM_AUTO_FIT' | translate}}</label>
|
||||
</div>
|
||||
</div>
|
@@ -8,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>
|
||||
|
@@ -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>
|
@@ -0,0 +1,28 @@
|
||||
<div class="tiled-client-grid">
|
||||
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
|
||||
<div class="tiled-client-cell" ng-repeat="client in clientRow">
|
||||
|
||||
<div class="client-tile" ng-if="client"
|
||||
ng-class="{
|
||||
'focused' : client.clientProperties.focused,
|
||||
'shared' : isShared(client)
|
||||
}"
|
||||
guac-click="getFocusAssignmentCallback(client)">
|
||||
<h3 class="client-tile-header" ng-if="hasMultipleClients(clientGroup)">
|
||||
<img class="client-tile-shared-indicator" src="images/share-white.svg">
|
||||
<span class="client-tile-name">{{ client.title }}</span>
|
||||
<img ng-click="onClose({ '$client' : client })"
|
||||
class="client-tile-disconnect"
|
||||
ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
src="images/x.svg">
|
||||
</h3>
|
||||
<guac-client client="client" emulate-absolute-mouse="emulateAbsoluteMouse"></guac-client>
|
||||
|
||||
<!-- Client-specific status/error dialog -->
|
||||
<guac-client-notification client="client"></guac-client-notification>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,14 @@
|
||||
<div class="tiled-client-grid" ng-style="{
|
||||
'width' : width + 'px',
|
||||
'height' : height + 'px',
|
||||
}">
|
||||
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
|
||||
<div class="tiled-client-cell" ng-repeat="client in clientRow">
|
||||
|
||||
<div class="client-tile" ng-if="client">
|
||||
<guac-thumbnail client="client"></guac-thumbnail>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -22,9 +22,6 @@
|
||||
*/
|
||||
angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
|
||||
|
||||
// Required services
|
||||
var preferenceService = $injector.get('preferenceService');
|
||||
|
||||
/**
|
||||
* Object used for interacting with a guacClient directive.
|
||||
*
|
||||
@@ -69,19 +66,11 @@ angular.module('client').factory('ClientProperties', ['$injector', function defi
|
||||
this.maxScale = template.maxScale || 3;
|
||||
|
||||
/**
|
||||
* Whether or not the client should listen to keyboard events.
|
||||
* Whether this client should receive keyboard events.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
this.keyboardEnabled = template.keyboardEnabled || true;
|
||||
|
||||
/**
|
||||
* Whether translation of touch to mouse events should emulate an
|
||||
* absolute pointer device, or a relative pointer device.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
this.emulateAbsoluteMouse = template.emulateAbsoluteMouse || preferenceService.preferences.emulateAbsoluteMouse;
|
||||
this.focused = template.focused || false;
|
||||
|
||||
/**
|
||||
* The relative Y coordinate of the scroll offset of the display within
|
||||
|
@@ -24,34 +24,34 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
function defineManagedClient($rootScope, $injector) {
|
||||
|
||||
// Required types
|
||||
var ClientProperties = $injector.get('ClientProperties');
|
||||
var ClientIdentifier = $injector.get('ClientIdentifier');
|
||||
var ClipboardData = $injector.get('ClipboardData');
|
||||
var ManagedArgument = $injector.get('ManagedArgument');
|
||||
var ManagedClientState = $injector.get('ManagedClientState');
|
||||
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
|
||||
var ManagedDisplay = $injector.get('ManagedDisplay');
|
||||
var ManagedFilesystem = $injector.get('ManagedFilesystem');
|
||||
var ManagedFileUpload = $injector.get('ManagedFileUpload');
|
||||
var ManagedShareLink = $injector.get('ManagedShareLink');
|
||||
const ClientProperties = $injector.get('ClientProperties');
|
||||
const ClientIdentifier = $injector.get('ClientIdentifier');
|
||||
const ClipboardData = $injector.get('ClipboardData');
|
||||
const ManagedArgument = $injector.get('ManagedArgument');
|
||||
const ManagedClientState = $injector.get('ManagedClientState');
|
||||
const ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
|
||||
const ManagedDisplay = $injector.get('ManagedDisplay');
|
||||
const ManagedFilesystem = $injector.get('ManagedFilesystem');
|
||||
const ManagedFileUpload = $injector.get('ManagedFileUpload');
|
||||
const ManagedShareLink = $injector.get('ManagedShareLink');
|
||||
|
||||
// Required services
|
||||
var $document = $injector.get('$document');
|
||||
var $q = $injector.get('$q');
|
||||
var $rootScope = $injector.get('$rootScope');
|
||||
var $window = $injector.get('$window');
|
||||
var activeConnectionService = $injector.get('activeConnectionService');
|
||||
var authenticationService = $injector.get('authenticationService');
|
||||
var connectionGroupService = $injector.get('connectionGroupService');
|
||||
var connectionService = $injector.get('connectionService');
|
||||
var preferenceService = $injector.get('preferenceService');
|
||||
var requestService = $injector.get('requestService');
|
||||
var schemaService = $injector.get('schemaService');
|
||||
var tunnelService = $injector.get('tunnelService');
|
||||
var guacAudio = $injector.get('guacAudio');
|
||||
var guacHistory = $injector.get('guacHistory');
|
||||
var guacImage = $injector.get('guacImage');
|
||||
var guacVideo = $injector.get('guacVideo');
|
||||
const $document = $injector.get('$document');
|
||||
const $q = $injector.get('$q');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
const $window = $injector.get('$window');
|
||||
const activeConnectionService = $injector.get('activeConnectionService');
|
||||
const authenticationService = $injector.get('authenticationService');
|
||||
const clipboardService = $injector.get('clipboardService');
|
||||
const connectionGroupService = $injector.get('connectionGroupService');
|
||||
const connectionService = $injector.get('connectionService');
|
||||
const preferenceService = $injector.get('preferenceService');
|
||||
const requestService = $injector.get('requestService');
|
||||
const tunnelService = $injector.get('tunnelService');
|
||||
const guacAudio = $injector.get('guacAudio');
|
||||
const guacHistory = $injector.get('guacHistory');
|
||||
const guacImage = $injector.get('guacImage');
|
||||
const guacVideo = $injector.get('guacVideo');
|
||||
|
||||
/**
|
||||
* The minimum amount of time to wait between updates to the client
|
||||
@@ -63,8 +63,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
|
||||
/**
|
||||
* Object which serves as a surrogate interface, encapsulating a Guacamole
|
||||
* client while it is active, allowing it to be detached and reattached
|
||||
* from different client views.
|
||||
* client while it is active, allowing it to be maintained in the
|
||||
* background. One or more ManagedClients are grouped within
|
||||
* ManagedClientGroups before being attached to the client view.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedClient|Object} [template={}]
|
||||
@@ -83,16 +84,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
*/
|
||||
this.id = template.id;
|
||||
|
||||
/**
|
||||
* The time that the connection was last brought to the foreground of
|
||||
* the current tab, as the number of milliseconds elapsed since
|
||||
* midnight of January 1, 1970 UTC. If the connection has not yet been
|
||||
* viewed, this will be 0.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.lastUsed = template.lastUsed || 0;
|
||||
|
||||
/**
|
||||
* The actual underlying Guacamole client.
|
||||
*
|
||||
@@ -156,16 +147,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
*/
|
||||
this.thumbnail = template.thumbnail;
|
||||
|
||||
/**
|
||||
* The current clipboard contents.
|
||||
*
|
||||
* @type ClipboardData
|
||||
*/
|
||||
this.clipboardData = template.clipboardData || new ClipboardData({
|
||||
type : 'text/plain',
|
||||
data : ''
|
||||
});
|
||||
|
||||
/**
|
||||
* The current state of all parameters requested by the server via
|
||||
* "required" instructions, where each object key is the name of a
|
||||
@@ -260,25 +241,30 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
* @param {ClientIdentifier} identifier
|
||||
* The identifier representing the connection or group to connect to.
|
||||
*
|
||||
* @param {String} [connectionParameters]
|
||||
* Any additional HTTP parameters to pass while connecting.
|
||||
* @param {number} [width]
|
||||
* The optimal display width, in local CSS pixels. If omitted, the
|
||||
* browser window width will be used.
|
||||
*
|
||||
* @param {number} [height]
|
||||
* The optimal display height, in local CSS pixels. If omitted, the
|
||||
* browser window height will be used.
|
||||
*
|
||||
* @returns {Promise.<String>}
|
||||
* A promise which resolves with the string of connection parameters to
|
||||
* be passed to the Guacamole client, once the string is ready.
|
||||
*/
|
||||
var getConnectString = function getConnectString(identifier, connectionParameters) {
|
||||
const getConnectString = function getConnectString(identifier, width, height) {
|
||||
|
||||
var deferred = $q.defer();
|
||||
const deferred = $q.defer();
|
||||
|
||||
// Calculate optimal width/height for display
|
||||
var pixel_density = $window.devicePixelRatio || 1;
|
||||
var optimal_dpi = pixel_density * 96;
|
||||
var optimal_width = $window.innerWidth * pixel_density;
|
||||
var optimal_height = $window.innerHeight * pixel_density;
|
||||
const pixel_density = $window.devicePixelRatio || 1;
|
||||
const optimal_dpi = pixel_density * 96;
|
||||
const optimal_width = width * pixel_density;
|
||||
const optimal_height = height * pixel_density;
|
||||
|
||||
// Build base connect string
|
||||
var connectString =
|
||||
let connectString =
|
||||
"token=" + encodeURIComponent(authenticationService.getCurrentToken())
|
||||
+ "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource)
|
||||
+ "&GUAC_ID=" + encodeURIComponent(identifier.id)
|
||||
@@ -286,8 +272,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
+ "&GUAC_WIDTH=" + Math.floor(optimal_width)
|
||||
+ "&GUAC_HEIGHT=" + Math.floor(optimal_height)
|
||||
+ "&GUAC_DPI=" + Math.floor(optimal_dpi)
|
||||
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone)
|
||||
+ (connectionParameters ? '&' + connectionParameters : '');
|
||||
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone);
|
||||
|
||||
// Add audio mimetypes to connect string
|
||||
guacAudio.supported.forEach(function(mimetype) {
|
||||
@@ -347,22 +332,20 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new ManagedClient, connecting it to the specified connection
|
||||
* or group.
|
||||
* Creates a new ManagedClient representing the specified connection or
|
||||
* connection group. The ManagedClient will not initially be connected,
|
||||
* and must be explicitly connected by invoking ManagedClient.connect().
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the connection or group to connect to. This String must be
|
||||
* a valid ClientIdentifier string, as would be generated by
|
||||
* ClientIdentifier.toString().
|
||||
*
|
||||
* @param {String} [connectionParameters]
|
||||
* Any additional HTTP parameters to pass while connecting.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* A new ManagedClient instance which is connected to the connection or
|
||||
* A new ManagedClient instance which represents the connection or
|
||||
* connection group having the given ID.
|
||||
*/
|
||||
ManagedClient.getInstance = function getInstance(id, connectionParameters) {
|
||||
ManagedClient.getInstance = function getInstance(id) {
|
||||
|
||||
var tunnel;
|
||||
|
||||
@@ -450,8 +433,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
ManagedClientState.ConnectionState.IDLE);
|
||||
break;
|
||||
|
||||
// Ignore "connecting" state
|
||||
case 1: // Connecting
|
||||
// Conneccting
|
||||
case 1:
|
||||
ManagedClientState.setConnectionState(managedClient.clientState,
|
||||
ManagedClientState.ConnectionState.CONNECTING);
|
||||
break;
|
||||
|
||||
// Connected + waiting
|
||||
@@ -465,9 +450,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
ManagedClientState.setConnectionState(managedClient.clientState,
|
||||
ManagedClientState.ConnectionState.CONNECTED);
|
||||
|
||||
// Send any clipboard data already provided
|
||||
if (managedClient.clipboardData)
|
||||
ManagedClient.setClipboard(managedClient, managedClient.clipboardData);
|
||||
// Sync current clipboard data
|
||||
clipboardService.getClipboard().then((data) => {
|
||||
ManagedClient.setClipboard(managedClient, data);
|
||||
}, angular.noop);
|
||||
|
||||
// Begin streaming audio input if possible
|
||||
requestAudioStream(client);
|
||||
@@ -562,12 +548,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
||||
|
||||
// Set clipboard contents once stream is finished
|
||||
reader.onend = function textComplete() {
|
||||
$rootScope.$apply(function updateClipboard() {
|
||||
managedClient.clipboardData = new ClipboardData({
|
||||
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
|
||||
|
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedClientGroup class used by the guacClientManager service.
|
||||
*/
|
||||
angular.module('client').factory('ManagedClientGroup', ['$injector', function defineManagedClientGroup($injector) {
|
||||
|
||||
/**
|
||||
* Object which serves as a grouping of ManagedClients. Each
|
||||
* ManagedClientGroup may be attached, detached, and reattached dynamically
|
||||
* from different client views, with its contents automatically displayed
|
||||
* in a tiled arrangment if needed.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedClientGroup|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
const ManagedClientGroup = function ManagedClientGroup(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The time that this group was last brought to the foreground of
|
||||
* the current tab, as the number of milliseconds elapsed since
|
||||
* midnight of January 1, 1970 UTC. If the group has not yet been
|
||||
* viewed, this will be 0.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.lastUsed = template.lastUsed || 0;
|
||||
|
||||
/**
|
||||
* Whether this ManagedClientGroup is currently attached to the client
|
||||
* interface (true) or is running in the background (false).
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.attached = template.attached || false;
|
||||
|
||||
/**
|
||||
* The clients that should be displayed within the client interface
|
||||
* when this group is attached.
|
||||
*
|
||||
* @type {ManagedClient[]}
|
||||
* @default []
|
||||
*/
|
||||
this.clients = template.clients || [];
|
||||
|
||||
/**
|
||||
* The number of rows that should be used when arranging the clients
|
||||
* within this group in a grid. By default, this value is automatically
|
||||
* calculated from the number of clients.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.rows = template.rows || ManagedClientGroup.getRows(this);
|
||||
|
||||
/**
|
||||
* The number of columns that should be used when arranging the clients
|
||||
* within this group in a grid. By default, this value is automatically
|
||||
* calculated from the number of clients.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.columns = template.columns || ManagedClientGroup.getColumns(this);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the number of rows and columns stored within the given
|
||||
* ManagedClientGroup such that the clients within the group are evenly
|
||||
* distributed. This function should be called whenever the size of a
|
||||
* group changes.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup that should be updated.
|
||||
*/
|
||||
ManagedClientGroup.recalculateTiles = function recalculateTiles(group) {
|
||||
|
||||
const recalculated = new ManagedClientGroup({
|
||||
clients : group.clients
|
||||
});
|
||||
|
||||
group.rows = recalculated.rows;
|
||||
group.columns = recalculated.columns;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the unique ID representing the given ManagedClientGroup or set
|
||||
* of client IDs. The ID of a ManagedClientGroup consists simply of the
|
||||
* IDs of all its ManagedClients, separated by periods.
|
||||
*
|
||||
* @param {ManagedClientGroup|string[]} group
|
||||
* The ManagedClientGroup or array of client IDs to determine the
|
||||
* ManagedClientGroup ID of.
|
||||
*
|
||||
* @returns {string}
|
||||
* The unique ID representing the given ManagedClientGroup, or the
|
||||
* unique ID that would represent a ManagedClientGroup containing the
|
||||
* clients with the given IDs.
|
||||
*/
|
||||
ManagedClientGroup.getIdentifier = function getIdentifier(group) {
|
||||
|
||||
if (!_.isArray(group))
|
||||
group = _.map(group.clients, client => client.id);
|
||||
|
||||
return group.join('.');
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of client identifiers for all clients contained within
|
||||
* the given ManagedClientGroup. Order of the identifiers is preserved
|
||||
* with respect to the order of the clients within the group.
|
||||
*
|
||||
* @param {ManagedClientGroup|string} group
|
||||
* The ManagedClientGroup to retrieve the client identifiers from,
|
||||
* or its ID.
|
||||
*
|
||||
* @returns {string[]}
|
||||
* The client identifiers of all clients contained within the given
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) {
|
||||
|
||||
if (_.isString(group))
|
||||
return group.split(/\./);
|
||||
|
||||
return group.clients.map(client => client.id);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the number of columns that should be used to evenly arrange
|
||||
* all provided clients in a tiled grid.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The number of columns that should be used for the grid of
|
||||
* clients.
|
||||
*/
|
||||
ManagedClientGroup.getColumns = function getColumns(group) {
|
||||
|
||||
if (!group.clients.length)
|
||||
return 0;
|
||||
|
||||
return Math.ceil(Math.sqrt(group.clients.length));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the number of rows that should be used to evenly arrange all
|
||||
* provided clients in a tiled grid.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The number of rows that should be used for the grid of clients.
|
||||
*/
|
||||
ManagedClientGroup.getRows = function getRows(group) {
|
||||
|
||||
if (!group.clients.length)
|
||||
return 0;
|
||||
|
||||
return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the title which should be displayed as the page title if the
|
||||
* given client group is attached to the interface.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to determine the title of.
|
||||
*
|
||||
* @returns {string}
|
||||
* The title of the given ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.getTitle = function getTitle(group) {
|
||||
|
||||
// Use client-specific title if only one client
|
||||
if (group.clients.length === 1)
|
||||
return group.clients[0].title;
|
||||
|
||||
// With multiple clients, somehow combining multiple page titles would
|
||||
// be confusing. Instead, use the combined names.
|
||||
return ManagedClientGroup.getName(group);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the combined names of all clients within the given
|
||||
* ManagedClientGroup, as determined by the names of the associated
|
||||
* connections or connection groups.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to determine the name of.
|
||||
*
|
||||
* @returns {string}
|
||||
* The combined names of all clients within the given
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.getName = function getName(group) {
|
||||
|
||||
// Generate a name from ONLY the focused clients, unless there are no
|
||||
// focused clients
|
||||
let relevantClients = _.filter(group.clients, client => client.clientProperties.focused);
|
||||
if (!relevantClients.length)
|
||||
relevantClients = group.clients;
|
||||
|
||||
return _.filter(relevantClients, (client => !!client.name)).map(client => client.name).join(', ') || '...';
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A callback that is invoked for a ManagedClient within a ManagedClientGroup.
|
||||
*
|
||||
* @callback ManagedClientGroup~clientCallback
|
||||
* @param {ManagedClient} client
|
||||
* The relevant ManagedClient.
|
||||
*
|
||||
* @param {number} row
|
||||
* The row number of the client within the tiled grid, where 0 is the
|
||||
* first row.
|
||||
*
|
||||
* @param {number} column
|
||||
* The column number of the client within the tiled grid, where 0 is
|
||||
* the first column.
|
||||
*
|
||||
* @param {number} index
|
||||
* The index of the client within the relevant
|
||||
* {@link ManagedClientGroup#clients} array.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Loops through each of the clients associated with the given
|
||||
* ManagedClientGroup, invoking the given callback for each client.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to loop through.
|
||||
*
|
||||
* @param {ManagedClientGroup~clientCallback} callback
|
||||
* The callback to invoke for each of the clients within the given
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.forEach = function forEach(group, callback) {
|
||||
let current = 0;
|
||||
for (let row = 0; row < group.rows; row++) {
|
||||
for (let column = 0; column < group.columns; column++) {
|
||||
|
||||
callback(group.clients[current], row, column, current);
|
||||
current++;
|
||||
|
||||
if (current >= group.clients.length)
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given ManagedClientGroup contains more than one
|
||||
* client.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to test.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if two or more clients are currently present in the given
|
||||
* group, false otherwise.
|
||||
*/
|
||||
ManagedClientGroup.hasMultipleClients = function hasMultipleClients(group) {
|
||||
return group && group.clients.length > 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a two-dimensional array of all ManagedClients within the given
|
||||
* group, arranged in the grid defined by {@link ManagedClientGroup#rows}
|
||||
* and {@link ManagedClientGroup#columns}. If any grid cell lacks a
|
||||
* corresponding client (because the number of clients does not divide
|
||||
* evenly into a grid), that cell will be null.
|
||||
*
|
||||
* For the sake of AngularJS scope watches, the results of calling this
|
||||
* function are cached and will always favor modifying an existing array
|
||||
* over creating a new array, even for nested arrays.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup defining the tiled grid arrangement of
|
||||
* ManagedClients.
|
||||
*
|
||||
* @returns {ManagedClient[][]}
|
||||
* A two-dimensional array of all ManagedClients within the given
|
||||
* group.
|
||||
*/
|
||||
ManagedClientGroup.getClientGrid = function getClientGrid(group) {
|
||||
|
||||
let index = 0;
|
||||
|
||||
// Operate on cached copy of grid
|
||||
const clientGrid = group._grid || (group._grid = []);
|
||||
|
||||
// Delete any rows in excess of the required size
|
||||
clientGrid.splice(group.rows);
|
||||
|
||||
for (let row = 0; row < group.rows; row++) {
|
||||
|
||||
// Prefer to use existing column arrays, deleting any columns in
|
||||
// excess of the required size
|
||||
const currentRow = clientGrid[row] || (clientGrid[row] = []);
|
||||
currentRow.splice(group.columns);
|
||||
|
||||
for (let column = 0; column < group.columns; column++) {
|
||||
currentRow[column] = group.clients[index++] || null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return clientGrid;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies that focus is assigned to at least one client in the given
|
||||
* group. If no client has focus, focus is assigned to the first client in
|
||||
* the group.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The group to verify.
|
||||
*/
|
||||
ManagedClientGroup.verifyFocus = function verifyFocus(group) {
|
||||
|
||||
// Focus the first client if there are no clients focused
|
||||
if (group.clients.length >= 1 && _.findIndex(group.clients, client => client.clientProperties.focused) === -1) {
|
||||
group.clients[0].clientProperties.focused = true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return ManagedClientGroup;
|
||||
|
||||
}]);
|
@@ -42,6 +42,14 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The client that originally received the "filesystem" instruction
|
||||
* that resulted in the creation of this ManagedFilesystem.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
this.client = template.client;
|
||||
|
||||
/**
|
||||
* The Guacamole filesystem object, as received via a "filesystem"
|
||||
* instruction.
|
||||
@@ -162,6 +170,10 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
|
||||
* and human-readable name. Upon creation, a request to populate the
|
||||
* contents of the root directory will be automatically dispatched.
|
||||
*
|
||||
* @param {ManagedClient} client
|
||||
* The client that originally received the "filesystem" instruction
|
||||
* that resulted in the creation of this ManagedFilesystem.
|
||||
*
|
||||
* @param {Guacamole.Object} object
|
||||
* The Guacamole.Object defining the filesystem.
|
||||
*
|
||||
@@ -171,10 +183,11 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
|
||||
* @returns {ManagedFilesystem}
|
||||
* The newly-created ManagedFilesystem.
|
||||
*/
|
||||
ManagedFilesystem.getInstance = function getInstance(object, name) {
|
||||
ManagedFilesystem.getInstance = function getInstance(client, object, name) {
|
||||
|
||||
// Init new filesystem object
|
||||
var managedFilesystem = new ManagedFilesystem({
|
||||
client : client,
|
||||
object : object,
|
||||
name : name,
|
||||
root : new ManagedFilesystem.File({
|
||||
@@ -196,9 +209,6 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
|
||||
* client and filesystem. The browser will automatically start the
|
||||
* download upon completion of this function.
|
||||
*
|
||||
* @param {ManagedClient} managedClient
|
||||
* The ManagedClient from which the file is to be downloaded.
|
||||
*
|
||||
* @param {ManagedFilesystem} managedFilesystem
|
||||
* The ManagedFilesystem from which the file is to be downloaded. Any
|
||||
* path information provided must be relative to this filesystem.
|
||||
@@ -206,7 +216,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
|
||||
* @param {String} path
|
||||
* The full, absolute path of the file to download.
|
||||
*/
|
||||
ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) {
|
||||
ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) {
|
||||
|
||||
// Request download
|
||||
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
|
||||
@@ -215,7 +225,7 @@ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector'
|
||||
var filename = path.match(/(.*[\\/])?(.*)/)[2];
|
||||
|
||||
// Start download
|
||||
tunnelService.downloadStream(managedClient.tunnel.uuid, stream, mimetype, filename);
|
||||
tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid, stream, mimetype, filename);
|
||||
|
||||
});
|
||||
|
||||
|
@@ -18,16 +18,19 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive provides an editor whose contents are exposed via a
|
||||
* ClipboardData object via the "data" attribute. If this data should also be
|
||||
* synced to the local clipboard, or sent via a connected Guacamole client
|
||||
* using a "guacClipboard" event, it is up to external code to do so.
|
||||
* A directive provides an editor for the clipboard content maintained by
|
||||
* clipboardService. Changes to the clipboard by clipboardService will
|
||||
* automatically be reflected in the editor, and changes in the editor will
|
||||
* automatically be reflected in the clipboard by clipboardService.
|
||||
*/
|
||||
angular.module('clipboard').directive('guacClipboard', ['$injector',
|
||||
function guacClipboard($injector) {
|
||||
|
||||
// Required types
|
||||
var ClipboardData = $injector.get('ClipboardData');
|
||||
const ClipboardData = $injector.get('ClipboardData');
|
||||
|
||||
// Required services
|
||||
const clipboardService = $injector.get('clipboardService');
|
||||
|
||||
/**
|
||||
* Configuration object for the guacClipboard directive.
|
||||
@@ -40,20 +43,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
|
||||
templateUrl : 'app/clipboard/templates/guacClipboard.html'
|
||||
};
|
||||
|
||||
// Scope properties exposed by the guacClipboard directive
|
||||
config.scope = {
|
||||
|
||||
/**
|
||||
* The data to display within the field provided by this directive. This
|
||||
* data will modified or replaced when the user manually alters the
|
||||
* contents of the field.
|
||||
*
|
||||
* @type ClipboardData
|
||||
*/
|
||||
data : '='
|
||||
|
||||
};
|
||||
|
||||
// guacClipboard directive controller
|
||||
config.controller = ['$scope', '$injector', '$element',
|
||||
function guacClipboardController($scope, $injector, $element) {
|
||||
@@ -75,12 +64,27 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
|
||||
var updateClipboardData = function updateClipboardData() {
|
||||
|
||||
// Read contents of clipboard textarea
|
||||
$scope.$evalAsync(function assignClipboardText() {
|
||||
$scope.data = new ClipboardData({
|
||||
clipboardService.setClipboard(new ClipboardData({
|
||||
type : 'text/plain',
|
||||
data : element.value
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the contents of the clipboard editor to the given data.
|
||||
*
|
||||
* @param {ClipboardData} data
|
||||
* The ClipboardData to display within the clipboard editor for
|
||||
* editing.
|
||||
*/
|
||||
const updateClipboardEditor = function updateClipboardEditor(data) {
|
||||
|
||||
// If the clipboard data is a string, render it as text
|
||||
if (typeof data.data === 'string')
|
||||
element.value = data.data;
|
||||
|
||||
// Ignore other data types for now
|
||||
|
||||
};
|
||||
|
||||
@@ -89,17 +93,15 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
|
||||
element.addEventListener('input', updateClipboardData);
|
||||
element.addEventListener('change', updateClipboardData);
|
||||
|
||||
// Watch clipboard for new data, updating the clipboard textarea as
|
||||
// necessary
|
||||
$scope.$watch('data', function clipboardDataChanged(data) {
|
||||
// Update remote clipboard if local clipboard changes
|
||||
$scope.$on('guacClipboard', function clipboardChanged(event, data) {
|
||||
updateClipboardEditor(data);
|
||||
});
|
||||
|
||||
// If the clipboard data is a string, render it as text
|
||||
if (typeof data.data === 'string')
|
||||
element.value = data.data;
|
||||
|
||||
// Ignore other data types for now
|
||||
|
||||
}); // end $scope.data watch
|
||||
// Init clipboard editor with current clipboard contents
|
||||
clipboardService.getClipboard().then((data) => {
|
||||
updateClipboardEditor(data);
|
||||
}, angular.noop);
|
||||
|
||||
}];
|
||||
|
||||
|
@@ -18,17 +18,31 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for accessing local clipboard data.
|
||||
* A service for maintaining and accessing clipboard data. If possible, this
|
||||
* service will leverage the local clipboard. If the local clipboard is not
|
||||
* available, an internal in-memory clipboard will be used instead.
|
||||
*/
|
||||
angular.module('clipboard').factory('clipboardService', ['$injector',
|
||||
function clipboardService($injector) {
|
||||
|
||||
// Get required services
|
||||
var $q = $injector.get('$q');
|
||||
var $window = $injector.get('$window');
|
||||
const $q = $injector.get('$q');
|
||||
const $window = $injector.get('$window');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
const sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
|
||||
// Required types
|
||||
var ClipboardData = $injector.get('ClipboardData');
|
||||
const ClipboardData = $injector.get('ClipboardData');
|
||||
|
||||
/**
|
||||
* Getter/setter which retrieves or sets the current stored clipboard
|
||||
* contents. The stored clipboard contents are strictly internal to
|
||||
* Guacamole, and may not reflect the local clipboard if local clipboard
|
||||
* access is unavailable.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const storedClipboardData = sessionStorageFactory.create(new ClipboardData());
|
||||
|
||||
var service = {};
|
||||
|
||||
@@ -175,7 +189,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
|
||||
* A promise that will resolve if setting the clipboard was successful,
|
||||
* and will reject if it failed.
|
||||
*/
|
||||
service.setLocalClipboard = function setLocalClipboard(data) {
|
||||
const setLocalClipboard = function setLocalClipboard(data) {
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
@@ -423,7 +437,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
|
||||
* if getting the clipboard was successful, and will reject if it
|
||||
* failed.
|
||||
*/
|
||||
service.getLocalClipboard = function getLocalClipboard() {
|
||||
const getLocalClipboard = function getLocalClipboard() {
|
||||
|
||||
// If the clipboard is already being read, do not overlap the read
|
||||
// attempts; instead share the result across all requests
|
||||
@@ -548,6 +562,64 @@ angular.module('clipboard').factory('clipboardService', ['$injector',
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current value of the internal clipboard shared across all
|
||||
* active Guacamole connections running within the current browser tab. If
|
||||
* access to the local clipboard is available, the internal clipboard is
|
||||
* first synchronized with the current local clipboard contents. If access
|
||||
* to the local clipboard is unavailable, only the internal clipboard will
|
||||
* be used.
|
||||
*
|
||||
* @return {Promise.<ClipboardData>}
|
||||
* A promise that will resolve with the contents of the internal
|
||||
* clipboard, first retrieving those contents from the local clipboard
|
||||
* if permission to do so has been granted. This promise is always
|
||||
* resolved.
|
||||
*/
|
||||
service.getClipboard = function getClipboard() {
|
||||
return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData());
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the content of the internal clipboard shared across all active
|
||||
* Guacamole connections running within the current browser tab. If
|
||||
* access to the local clipboard is available, the local clipboard is
|
||||
* first set to the provided clipboard content. If access to the local
|
||||
* clipboard is unavailable, only the internal clipboard will be used. A
|
||||
* "guacClipboard" event will be broadcast with the assigned data once the
|
||||
* operation has completed.
|
||||
*
|
||||
* @param {ClipboardData} data
|
||||
* The data to assign to the clipboard.
|
||||
*
|
||||
* @return {Promise}
|
||||
* A promise that will resolve after the clipboard content has been
|
||||
* set. This promise is always resolved.
|
||||
*/
|
||||
service.setClipboard = function setClipboard(data) {
|
||||
return setLocalClipboard(data)['catch'](angular.noop).finally(() => {
|
||||
|
||||
// Update internal clipboard and broadcast event notifying of
|
||||
// updated contents
|
||||
storedClipboardData(data);
|
||||
$rootScope.$broadcast('guacClipboard', data);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resynchronizes the local and internal clipboards, setting the contents
|
||||
* of the internal clipboard to that of the local clipboard (if local
|
||||
* clipboard access is granted) and broadcasting a "guacClipboard" event
|
||||
* with the current internal clipboard contents for consumption by external
|
||||
* components like the "guacClient" directive.
|
||||
*/
|
||||
service.resyncClipboard = function resyncClipboard() {
|
||||
service.getClipboard().then(function clipboardRead(data) {
|
||||
return service.setClipboard(data);
|
||||
}, angular.noop);
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
|
@@ -47,7 +47,7 @@
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border: 1px solid black;
|
||||
background: url('images/checker.png');
|
||||
background: url('images/checker.svg');
|
||||
}
|
||||
|
||||
.clipboard-service-target {
|
||||
|
@@ -36,6 +36,15 @@ angular.module('clipboard').factory('ClipboardData', [function defineClipboardDa
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The ID of the ManagedClient handling the remote desktop connection
|
||||
* that originated this clipboard data, or null if the data originated
|
||||
* from the clipboard editor or local clipboard.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.source = template.source;
|
||||
|
||||
/**
|
||||
* The mimetype of the data currently stored within the clipboard.
|
||||
*
|
||||
|
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which provides handling of click and click-like touch events.
|
||||
* The state of Shift and Ctrl modifiers is tracked through these click events
|
||||
* to allow for specific handling of Shift+Click and Ctrl+Click.
|
||||
*/
|
||||
angular.module('element').directive('guacClick', [function guacClick() {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacClick($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* A callback that is invoked by the guacClick directive when a
|
||||
* click or click-like event is received.
|
||||
*
|
||||
* @callback guacClick~callback
|
||||
* @param {boolean} shift
|
||||
* Whether Shift was held down at the time the click occurred.
|
||||
*
|
||||
* @param {boolean} ctrl
|
||||
* Whether Ctrl or Meta (the Mac "Command" key) was held down
|
||||
* at the time the click occurred.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The callback to invoke when a click or click-like event is
|
||||
* received on the assocaited element.
|
||||
*
|
||||
* @type guacClick~callback
|
||||
*/
|
||||
const guacClick = $scope.$eval($attrs.guacClick);
|
||||
|
||||
/**
|
||||
* The element which will register the click.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Whether either Shift key is currently pressed.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
let shift = false;
|
||||
|
||||
/**
|
||||
* Whether either Ctrl key is currently pressed. To allow the
|
||||
* Command key to be used on Mac platforms, this flag also
|
||||
* considers the state of either Meta key.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
let ctrl = false;
|
||||
|
||||
/**
|
||||
* Updates the state of the {@link shift} and {@link ctrl} flags
|
||||
* based on which keys are currently marked as held down by the
|
||||
* given Guacamole.Keyboard.
|
||||
*
|
||||
* @param {Guacamole.Keyboard} keyboard
|
||||
* The Guacamole.Keyboard instance to read key states from.
|
||||
*/
|
||||
const updateModifiers = function updateModifiers(keyboard) {
|
||||
|
||||
shift = !!(
|
||||
keyboard.pressed[0xFFE1] // Left shift
|
||||
|| keyboard.pressed[0xFFE2] // Right shift
|
||||
);
|
||||
|
||||
ctrl = !!(
|
||||
keyboard.pressed[0xFFE3] // Left ctrl
|
||||
|| keyboard.pressed[0xFFE4] // Right ctrl
|
||||
|| keyboard.pressed[0xFFE7] // Left meta (command)
|
||||
|| keyboard.pressed[0xFFE8] // Right meta (command)
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Update tracking of modifier states for each key press
|
||||
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
|
||||
updateModifiers(keyboard);
|
||||
});
|
||||
|
||||
// Update tracking of modifier states for each key release
|
||||
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
||||
updateModifiers(keyboard);
|
||||
});
|
||||
|
||||
// Fire provided callback for each mouse-initiated "click" event ...
|
||||
element.addEventListener('click', function elementClicked(e) {
|
||||
if (element.contains(e.target))
|
||||
$scope.$apply(() => guacClick(shift, ctrl));
|
||||
});
|
||||
|
||||
// ... and for touch-initiated click-like events
|
||||
element.addEventListener('touchstart', function elementClicked(e) {
|
||||
if (element.contains(e.target))
|
||||
$scope.$apply(() => guacClick(shift, ctrl));
|
||||
});
|
||||
|
||||
} // end guacClick link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -38,10 +38,10 @@
|
||||
|
||||
/* Icon for unmasking passwords */
|
||||
.form-field .password-field input[type=password] ~ .icon.toggle-password {
|
||||
background-image: url('images/action-icons/guac-show-pass.png');
|
||||
background-image: url('images/action-icons/guac-show-pass.svg');
|
||||
}
|
||||
|
||||
/* Icon for masking passwords */
|
||||
.form-field .password-field input[type=text] ~ .icon.toggle-password {
|
||||
background-image: url('images/action-icons/guac-hide-pass.png');
|
||||
background-image: url('images/action-icons/guac-hide-pass.svg');
|
||||
}
|
||||
|
@@ -18,7 +18,8 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which displays the contents of a connection group.
|
||||
* A directive which displays the recently-accessed connections nested beneath
|
||||
* each of the given connection groups.
|
||||
*/
|
||||
angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() {
|
||||
|
||||
@@ -44,21 +45,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
||||
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
|
||||
|
||||
// Required types
|
||||
var ActiveConnection = $injector.get('ActiveConnection');
|
||||
var ClientIdentifier = $injector.get('ClientIdentifier');
|
||||
var RecentConnection = $injector.get('RecentConnection');
|
||||
|
||||
// Required services
|
||||
var guacClientManager = $injector.get('guacClientManager');
|
||||
var guacHistory = $injector.get('guacHistory');
|
||||
|
||||
/**
|
||||
* Array of all known and visible active connections.
|
||||
*
|
||||
* @type ActiveConnection[]
|
||||
*/
|
||||
$scope.activeConnections = [];
|
||||
|
||||
/**
|
||||
* Array of all known and visible recently-used connections.
|
||||
*
|
||||
@@ -68,16 +60,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
||||
|
||||
/**
|
||||
* Returns whether recent connections are available for display.
|
||||
* Note that, for the sake of this directive, recent connections
|
||||
* include any currently-active connections, even if they are not
|
||||
* yet in the history.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if recent (or active) connections are present, false
|
||||
* otherwise.
|
||||
* true if recent connections are present, false otherwise.
|
||||
*/
|
||||
$scope.hasRecentConnections = function hasRecentConnections() {
|
||||
return !!($scope.activeConnections.length || $scope.recentConnections.length);
|
||||
return !!$scope.recentConnections.length;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -149,7 +137,6 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
||||
$scope.$watch("rootGroups", function setRootGroups(rootGroups) {
|
||||
|
||||
// Clear connection arrays
|
||||
$scope.activeConnections = [];
|
||||
$scope.recentConnections = [];
|
||||
|
||||
// Produce collection of visible objects
|
||||
@@ -160,29 +147,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
|
||||
});
|
||||
}
|
||||
|
||||
var managedClients = guacClientManager.getManagedClients();
|
||||
|
||||
// Add all active connections
|
||||
for (var id in managedClients) {
|
||||
|
||||
// Get corresponding managed client
|
||||
var client = managedClients[id];
|
||||
|
||||
// Add active connections for clients with associated visible objects
|
||||
if (id in visibleObjects) {
|
||||
|
||||
var object = visibleObjects[id];
|
||||
$scope.activeConnections.push(new ActiveConnection(object.name, client));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add any recent connections that are visible
|
||||
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
|
||||
|
||||
// Add recent connections for history entries with associated visible objects
|
||||
if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) {
|
||||
if (historyEntry.id in visibleObjects) {
|
||||
|
||||
var object = visibleObjects[historyEntry.id];
|
||||
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));
|
||||
|
@@ -3,23 +3,6 @@
|
||||
<!-- Text displayed if no recent connections exist -->
|
||||
<p class="placeholder" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
|
||||
|
||||
<!-- All active connections -->
|
||||
<div ng-repeat="activeConnection in activeConnections" class="connection">
|
||||
<a href="#/client/{{activeConnection.client.id}}">
|
||||
|
||||
<!-- Connection thumbnail -->
|
||||
<div class="thumbnail">
|
||||
<guac-thumbnail client="activeConnection.client"></guac-thumbnail>
|
||||
</div>
|
||||
|
||||
<!-- Connection name -->
|
||||
<div class="caption">
|
||||
<span class="name">{{activeConnection.name}}</span>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- All recent connections -->
|
||||
<div ng-repeat="recentConnection in recentConnections" class="connection">
|
||||
<a href="#/client/{{recentConnection.entry.id}}">
|
||||
|
@@ -181,10 +181,11 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
|
||||
})
|
||||
|
||||
// Client view
|
||||
.when('/client/:id/:params?', {
|
||||
.when('/client/:id', {
|
||||
bodyClassName : 'client',
|
||||
templateUrl : 'app/client/templates/client.html',
|
||||
controller : 'clientController',
|
||||
reloadOnUrl : false,
|
||||
resolve : { updateCurrentToken: updateCurrentToken }
|
||||
})
|
||||
|
||||
|
@@ -24,11 +24,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||
function indexController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
var $document = $injector.get('$document');
|
||||
var $route = $injector.get('$route');
|
||||
var $window = $injector.get('$window');
|
||||
var clipboardService = $injector.get('clipboardService');
|
||||
var guacNotification = $injector.get('guacNotification');
|
||||
const $document = $injector.get('$document');
|
||||
const $route = $injector.get('$route');
|
||||
const $window = $injector.get('$window');
|
||||
const clipboardService = $injector.get('clipboardService');
|
||||
const guacNotification = $injector.get('guacNotification');
|
||||
const guacClientManager = $injector.get('guacClientManager');
|
||||
|
||||
/**
|
||||
* The error that prevents the current page from rendering at all. If no
|
||||
@@ -43,6 +44,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||
*/
|
||||
$scope.guacNotification = guacNotification;
|
||||
|
||||
/**
|
||||
* All currently-active connections, grouped into their corresponding
|
||||
* tiled views.
|
||||
*
|
||||
* @type ManagedClientGroup[]
|
||||
*/
|
||||
$scope.getManagedClientGroups = guacClientManager.getManagedClientGroups;
|
||||
|
||||
/**
|
||||
* The message to display to the user as instructions for the login
|
||||
* process.
|
||||
@@ -154,9 +163,8 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||
// Broadcast keydown events
|
||||
keyboard.onkeydown = function onkeydown(keysym) {
|
||||
|
||||
// Do not handle key events if not logged in or if a notification is
|
||||
// shown
|
||||
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
|
||||
// Do not handle key events if not logged in
|
||||
if ($scope.applicationState !== ApplicationState.READY)
|
||||
return true;
|
||||
|
||||
// Warn of pending keydown
|
||||
@@ -175,7 +183,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||
|
||||
// Do not handle key events if not logged in or if a notification is
|
||||
// shown
|
||||
if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
|
||||
if ($scope.applicationState !== ApplicationState.READY)
|
||||
return;
|
||||
|
||||
// Warn of pending keyup
|
||||
@@ -199,25 +207,15 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
||||
keyboard.reset();
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks whether the clipboard data has changed, firing a new
|
||||
* "guacClipboard" event if it has.
|
||||
*/
|
||||
var checkClipboard = function checkClipboard() {
|
||||
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
|
||||
$scope.$broadcast('guacClipboard', data);
|
||||
}, angular.noop);
|
||||
};
|
||||
|
||||
// Attempt to read the clipboard if it may have changed
|
||||
$window.addEventListener('load', checkClipboard, true);
|
||||
$window.addEventListener('copy', checkClipboard);
|
||||
$window.addEventListener('cut', checkClipboard);
|
||||
$window.addEventListener('load', clipboardService.resyncClipboard, true);
|
||||
$window.addEventListener('copy', clipboardService.resyncClipboard);
|
||||
$window.addEventListener('cut', clipboardService.resyncClipboard);
|
||||
$window.addEventListener('focus', function focusGained(e) {
|
||||
|
||||
// Only recheck clipboard if it's the window itself that gained focus
|
||||
if (e.target === $window)
|
||||
checkClipboard();
|
||||
clipboardService.resyncClipboard();
|
||||
|
||||
}, true);
|
||||
|
||||
|
@@ -48,3 +48,12 @@
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/**
|
||||
* popin: Increase in size and opacity from invisibly tiny and transparent to
|
||||
* full size and opaque.
|
||||
*/
|
||||
@keyframes popin {
|
||||
from { transform: scale(0); opacity: 0; }
|
||||
to { transform: scale(1); }
|
||||
}
|
||||
|
@@ -105,30 +105,30 @@ input[type="submit"]:disabled, button:disabled, button.danger:disabled {
|
||||
|
||||
.button.logout::before,
|
||||
button.logout::before {
|
||||
background-image: url('images/action-icons/guac-logout.png');
|
||||
background-image: url('images/action-icons/guac-logout.svg');
|
||||
}
|
||||
|
||||
.button.reconnect::before,
|
||||
button.reconnect::before {
|
||||
background-image: url('images/circle-arrows.png');
|
||||
background-image: url('images/circle-arrows.svg');
|
||||
}
|
||||
|
||||
.button.manage::before,
|
||||
button.manage::before {
|
||||
background-image: url('images/action-icons/guac-config.png');
|
||||
background-image: url('images/action-icons/guac-config.svg');
|
||||
}
|
||||
|
||||
.button.back::before,
|
||||
button.back::before {
|
||||
background-image: url('images/action-icons/guac-back.png');
|
||||
background-image: url('images/action-icons/guac-back.svg');
|
||||
}
|
||||
|
||||
.button.home::before,
|
||||
button.home::before {
|
||||
background-image: url('images/action-icons/guac-home.png');
|
||||
background-image: url('images/action-icons/guac-home.svg');
|
||||
}
|
||||
|
||||
.button.change-password::before,
|
||||
button.change-password::before {
|
||||
background-image: url('images/action-icons/guac-key.png');
|
||||
background-image: url('images/action-icons/guac-key.svg');
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@
|
||||
.fatal-page-error h1::before {
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
background: url('images/warning.png');
|
||||
background: url('images/warning.svg');
|
||||
background-repeat: no-repeat;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
@@ -49,7 +49,7 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
background-image: url('images/cog.png');
|
||||
background-image: url('images/cog.svg');
|
||||
background-size: 96px 96px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
|
@@ -20,7 +20,7 @@
|
||||
#other-connections .client-panel {
|
||||
|
||||
display: none;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
background-image: url(images/arrows/right.png);
|
||||
background-image: url(images/arrows/right.svg);
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
@@ -66,7 +66,7 @@
|
||||
}
|
||||
|
||||
#other-connections .client-panel.hidden .client-panel-handle {
|
||||
background-image: url(images/arrows/left.png);
|
||||
background-image: url(images/arrows/left.svg);
|
||||
}
|
||||
|
||||
#other-connections .client-panel-connection-list {
|
||||
@@ -92,6 +92,7 @@
|
||||
background: black;
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
|
||||
|
||||
animation: 0.1s linear 0s popin;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.25s;
|
||||
|
||||
@@ -101,11 +102,6 @@
|
||||
|
||||
}
|
||||
|
||||
#other-connections .client-panel-connection .thumbnail-main img {
|
||||
max-width: none;
|
||||
max-height: 128px;
|
||||
}
|
||||
|
||||
#other-connections .client-panel-connection a[href]::before {
|
||||
|
||||
display: block;
|
||||
@@ -118,7 +114,7 @@
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
background: url('images/warning-white.png');
|
||||
background: url('images/warning-white.svg');
|
||||
background-size: 48px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
@@ -160,6 +156,7 @@
|
||||
#other-connections button.close-other-connection img {
|
||||
background: #A43;
|
||||
border-radius: 18px;
|
||||
width: 100%;
|
||||
max-width: 18px;
|
||||
padding: 3px;
|
||||
}
|
@@ -52,10 +52,10 @@ table.sorted th.sort-primary:after {
|
||||
background-size: 1em 1em;
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('images/arrows/down.png');
|
||||
background-image: url('images/arrows/down.svg');
|
||||
|
||||
}
|
||||
|
||||
table.sorted th.sort-primary.sort-descending:after {
|
||||
background-image: url('images/arrows/up.png');
|
||||
background-image: url('images/arrows/up.svg');
|
||||
}
|
||||
|
@@ -149,27 +149,27 @@ div.section {
|
||||
*/
|
||||
|
||||
.icon.user {
|
||||
background-image: url('images/user-icons/guac-user.png');
|
||||
background-image: url('images/user-icons/guac-user.svg');
|
||||
}
|
||||
|
||||
.icon.user.add {
|
||||
background-image: url('images/action-icons/guac-user-add.png');
|
||||
background-image: url('images/action-icons/guac-user-add.svg');
|
||||
}
|
||||
|
||||
.icon.user-group {
|
||||
background-image: url('images/user-icons/guac-user-group.png');
|
||||
background-image: url('images/user-icons/guac-user-group.svg');
|
||||
}
|
||||
|
||||
.icon.user-group.add {
|
||||
background-image: url('images/action-icons/guac-user-group-add.png');
|
||||
background-image: url('images/action-icons/guac-user-group-add.svg');
|
||||
}
|
||||
|
||||
.icon.connection {
|
||||
background-image: url('images/protocol-icons/guac-plug.png');
|
||||
background-image: url('images/protocol-icons/guac-plug.svg');
|
||||
}
|
||||
|
||||
.icon.connection.add {
|
||||
background-image: url('images/action-icons/guac-monitor-add.png');
|
||||
background-image: url('images/action-icons/guac-monitor-add.svg');
|
||||
}
|
||||
|
||||
.connection .icon,
|
||||
@@ -187,30 +187,30 @@ div.section {
|
||||
}
|
||||
|
||||
.connection-group > .caption .icon {
|
||||
background-image: url('images/folder-closed.png');
|
||||
background-image: url('images/folder-closed.svg');
|
||||
}
|
||||
|
||||
.connection-group.expanded > .caption .icon {
|
||||
background-image: url('images/folder-open.png');
|
||||
background-image: url('images/folder-open.svg');
|
||||
}
|
||||
|
||||
.connection .icon {
|
||||
background-image: url('images/protocol-icons/guac-plug.png');
|
||||
background-image: url('images/protocol-icons/guac-plug.svg');
|
||||
}
|
||||
|
||||
.connection .icon.kubernetes,
|
||||
.connection .icon.ssh,
|
||||
.connection .icon.telnet {
|
||||
background-image: url('images/protocol-icons/guac-text.png');
|
||||
background-image: url('images/protocol-icons/guac-text.svg');
|
||||
}
|
||||
|
||||
.connection .icon.vnc,
|
||||
.connection .icon.rdp {
|
||||
background-image: url('images/protocol-icons/guac-monitor.png');
|
||||
background-image: url('images/protocol-icons/guac-monitor.svg');
|
||||
}
|
||||
|
||||
.sharing-profile .icon {
|
||||
background-image: url('images/share.png');
|
||||
background-image: url('images/share.svg');
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -223,7 +223,7 @@ div.section {
|
||||
}
|
||||
|
||||
.connection-group.empty.balancer .icon {
|
||||
background-image: url('images/protocol-icons/guac-monitor.png');
|
||||
background-image: url('images/protocol-icons/guac-monitor.svg');
|
||||
}
|
||||
|
||||
.expandable.expanded > .children > .list-item {
|
||||
@@ -259,16 +259,16 @@ div.section {
|
||||
}
|
||||
|
||||
.expandable > .caption .icon.expand {
|
||||
background-image: url('images/group-icons/guac-closed.png');
|
||||
background-image: url('images/group-icons/guac-closed.svg');
|
||||
}
|
||||
|
||||
.expandable.expanded > .caption .icon.expand {
|
||||
background-image: url('images/group-icons/guac-open.png');
|
||||
background-image: url('images/group-icons/guac-open.svg');
|
||||
}
|
||||
|
||||
.expandable.empty > .caption .icon.expand {
|
||||
opacity: 0.25;
|
||||
background-image: url('images/group-icons/guac-open.png');
|
||||
background-image: url('images/group-icons/guac-open.svg');
|
||||
}
|
||||
|
||||
.history th,
|
||||
|
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.filter .search-string {
|
||||
background-image: url('images/magnifier.png');
|
||||
background-image: url('images/magnifier.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.75em;
|
||||
background-position: 0.25em center;
|
||||
|
@@ -71,17 +71,17 @@
|
||||
}
|
||||
|
||||
.pager .icon.first-page {
|
||||
background-image: url('images/action-icons/guac-first-page.png');
|
||||
background-image: url('images/action-icons/guac-first-page.svg');
|
||||
}
|
||||
|
||||
.pager .icon.prev-page {
|
||||
background-image: url('images/action-icons/guac-prev-page.png');
|
||||
background-image: url('images/action-icons/guac-prev-page.svg');
|
||||
}
|
||||
|
||||
.pager .icon.next-page {
|
||||
background-image: url('images/action-icons/guac-next-page.png');
|
||||
background-image: url('images/action-icons/guac-next-page.svg');
|
||||
}
|
||||
|
||||
.pager .icon.last-page {
|
||||
background-image: url('images/action-icons/guac-last-page.png');
|
||||
background-image: url('images/action-icons/guac-last-page.svg');
|
||||
}
|
||||
|
@@ -103,7 +103,7 @@
|
||||
-moz-background-size: 3em 3em;
|
||||
-webkit-background-size: 3em 3em;
|
||||
-khtml-background-size: 3em 3em;
|
||||
background-image: url("images/guac-tricolor.png");
|
||||
background-image: url("images/guac-tricolor.svg");
|
||||
}
|
||||
|
||||
.login-ui.continuation .login-dialog {
|
||||
|
@@ -39,11 +39,11 @@
|
||||
}
|
||||
|
||||
.manage-user-group .page-tabs .page-list li.read-only a[href]:before {
|
||||
background-image: url('images/lock.png');
|
||||
background-image: url('images/lock.svg');
|
||||
}
|
||||
|
||||
.manage-user-group .page-tabs .page-list li.unlinked a[href]:before {
|
||||
background-image: url('images/plus.png');
|
||||
background-image: url('images/plus.svg');
|
||||
}
|
||||
|
||||
.manage-user-group .page-tabs .page-list li.unlinked a[href] {
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
|
||||
.manage-user-group .page-tabs .page-list li.linked a[href]:before {
|
||||
background-image: url('images/checkmark.png');
|
||||
background-image: url('images/checkmark.svg');
|
||||
}
|
||||
|
||||
.manage-user-group .notice.read-only {
|
||||
|
@@ -39,11 +39,11 @@
|
||||
}
|
||||
|
||||
.manage-user .page-tabs .page-list li.read-only a[href]:before {
|
||||
background-image: url('images/lock.png');
|
||||
background-image: url('images/lock.svg');
|
||||
}
|
||||
|
||||
.manage-user .page-tabs .page-list li.unlinked a[href]:before {
|
||||
background-image: url('images/plus.png');
|
||||
background-image: url('images/plus.svg');
|
||||
}
|
||||
|
||||
.manage-user .page-tabs .page-list li.unlinked a[href] {
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
|
||||
.manage-user .page-tabs .page-list li.linked a[href]:before {
|
||||
background-image: url('images/checkmark.png');
|
||||
background-image: url('images/checkmark.svg');
|
||||
}
|
||||
|
||||
.manage-user .notice.read-only {
|
||||
|
@@ -12,12 +12,12 @@
|
||||
|
||||
<!-- Abbreviated list of only the currently selected objects -->
|
||||
<div class="abbreviated-related-objects">
|
||||
<img src="images/arrows/right.png" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()">
|
||||
<img src="images/arrows/down.png" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
|
||||
<img src="images/arrows/right.svg" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()">
|
||||
<img src="images/arrows/down.svg" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()">
|
||||
<p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p>
|
||||
<ul>
|
||||
<li ng-repeat="identifier in identifiers | filter: filterString">
|
||||
<label><img src="images/x-red.png" alt="Remove" class="remove"
|
||||
<label><img src="images/x-red.svg" alt="Remove" class="remove"
|
||||
ng-click="removeIdentifier(identifier)"
|
||||
ng-show="isEditable[identifier]"><span class="identifier">{{ identifier }}</span>
|
||||
</label>
|
||||
|
@@ -92,7 +92,7 @@
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em;
|
||||
background-position: center center;
|
||||
background-image: url('images/arrows/down.png');
|
||||
background-image: url('images/arrows/down.svg');
|
||||
|
||||
}
|
||||
|
||||
|
@@ -54,7 +54,7 @@
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em;
|
||||
background-position: 0.5em center;
|
||||
background-image: url('images/user-icons/guac-user.png');
|
||||
background-image: url('images/user-icons/guac-user.svg');
|
||||
|
||||
}
|
||||
|
||||
@@ -64,23 +64,23 @@
|
||||
background-size: 1em;
|
||||
background-position: 0.75em center;
|
||||
padding-left: 2.5em;
|
||||
background-image: url('images/protocol-icons/guac-monitor.png');
|
||||
background-image: url('images/protocol-icons/guac-monitor.svg');
|
||||
|
||||
}
|
||||
|
||||
.user-menu .menu-dropdown .menu-contents li a[href="#/"] {
|
||||
background-image: url('images/action-icons/guac-home-dark.png');
|
||||
background-image: url('images/action-icons/guac-home-dark.svg');
|
||||
}
|
||||
|
||||
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/users"],
|
||||
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/connections"],
|
||||
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/sessions"],
|
||||
.user-menu .menu-dropdown .menu-contents li a[href="#/settings/preferences"] {
|
||||
background-image: url('images/action-icons/guac-config-dark.png');
|
||||
background-image: url('images/action-icons/guac-config-dark.svg');
|
||||
}
|
||||
|
||||
.user-menu .menu-dropdown .menu-contents li a.logout {
|
||||
background-image: url('images/action-icons/guac-logout-dark.png');
|
||||
background-image: url('images/action-icons/guac-logout-dark.svg');
|
||||
}
|
||||
|
||||
.user-menu .menu-dropdown .menu-contents .profile {
|
||||
|
@@ -18,20 +18,30 @@
|
||||
*/
|
||||
|
||||
guac-modal {
|
||||
display: table;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
guac-modal .modal-contents {
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
-ms-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
guac-modal {
|
||||
|
@@ -74,7 +74,7 @@
|
||||
.notification .progress {
|
||||
|
||||
width: 100%;
|
||||
background: #C2C2C2 url('images/progress.png');
|
||||
background: #C2C2C2 url('images/progress.svg');
|
||||
background-size: 16px 16px;
|
||||
-moz-background-size: 16px 16px;
|
||||
-webkit-background-size: 16px 16px;
|
||||
|
@@ -45,17 +45,17 @@ a.button.add-connection-group::before {
|
||||
}
|
||||
|
||||
a.button.add-user::before {
|
||||
background-image: url('images/action-icons/guac-user-add.png');
|
||||
background-image: url('images/action-icons/guac-user-add.svg');
|
||||
}
|
||||
|
||||
a.button.add-user-group::before {
|
||||
background-image: url('images/action-icons/guac-user-group-add.png');
|
||||
background-image: url('images/action-icons/guac-user-group-add.svg');
|
||||
}
|
||||
|
||||
a.button.add-connection::before {
|
||||
background-image: url('images/action-icons/guac-monitor-add.png');
|
||||
background-image: url('images/action-icons/guac-monitor-add.svg');
|
||||
}
|
||||
|
||||
a.button.add-connection-group::before {
|
||||
background-image: url('images/action-icons/guac-group-add.png');
|
||||
background-image: url('images/action-icons/guac-group-add.svg');
|
||||
}
|
||||
|
@@ -72,7 +72,7 @@
|
||||
<div class="choice">
|
||||
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
|
||||
<div class="figure">
|
||||
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
|
||||
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
|
||||
<p class="caption"><label for="absolute">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="choice">
|
||||
<input name="mouse-mode" ng-model="preferences.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
|
||||
<div class="figure">
|
||||
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
|
||||
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'SETTINGS_PREFERENCES.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
|
||||
<p class="caption"><label for="relative">{{'SETTINGS_PREFERENCES.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
|
||||
</div>
|
||||
</div>
|
||||
|
Before Width: | Height: | Size: 586 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;stroke:none" d="M41.249 6.437a9593.495 9593.495 0 0 1-25.563 25.52c8.532 8.525 17.045 17.068 25.563 25.606l7.065-7.065L29.816 32l18.498-18.498z"/></svg>
|
After Width: | Height: | Size: 258 B |
Before Width: | Height: | Size: 966 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="m28.031 4.813-2 5.25a22.529 22.529 0 0 0-5.312 2.218L15 10.562 10.562 15l1.72 5.719a22.529 22.529 0 0 0-2.22 5.312l-5.25 2v7.938l5.25 2a22.529 22.529 0 0 0 2.22 5.312L10.561 49 15 53.438l5.719-1.72a22.529 22.529 0 0 0 5.312 2.22l2 5.25h7.938l2-5.25a22.529 22.529 0 0 0 5.312-2.22L49 53.439 53.438 49l-1.72-5.719a22.529 22.529 0 0 0 2.22-5.312l5.25-2V28.03l-5.25-2a22.529 22.529 0 0 0-2.22-5.312L53.439 15 49 10.562l-5.719 1.72a22.529 22.529 0 0 0-5.312-2.22l-2-5.25zM32 16.5c8.552 0 15.5 6.948 15.5 15.5S40.552 47.5 32 47.5 16.5 40.552 16.5 32 23.448 16.5 32 16.5z"/><path transform="translate(.31 .77)" d="M42.854 31.23a11.164 11.164 0 1 1-22.327 0 11.164 11.164 0 1 1 22.327 0z" style="fill:#000;stroke:none"/></svg>
|
After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;stroke:none" d="m28.031 4.813-2 5.25a22.529 22.529 0 0 0-5.312 2.218L15 10.562 10.562 15l1.72 5.719a22.529 22.529 0 0 0-2.22 5.312l-5.25 2v7.938l5.25 2a22.529 22.529 0 0 0 2.22 5.312L10.561 49 15 53.438l5.719-1.72a22.529 22.529 0 0 0 5.312 2.22l2 5.25h7.938l2-5.25a22.529 22.529 0 0 0 5.312-2.22L49 53.439 53.438 49l-1.72-5.719a22.529 22.529 0 0 0 2.22-5.312l5.25-2V28.03l-5.25-2a22.529 22.529 0 0 0-2.22-5.312L53.439 15 49 10.562l-5.719 1.72a22.529 22.529 0 0 0-5.312-2.22l-2-5.25zM32 16.5c8.552 0 15.5 6.948 15.5 15.5S40.552 47.5 32 47.5 16.5 40.552 16.5 32 23.448 16.5 32 16.5z"/><path transform="translate(.31 .77)" d="M42.854 31.23a11.164 11.164 0 1 1-22.327 0 11.164 11.164 0 1 1 22.327 0z" style="fill:#fff;stroke:none"/></svg>
|
After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 611 B |
Before Width: | Height: | Size: 690 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="M-88.192 11.032h7.29v37.312h-7.29zm32.66 0a7001.475 7001.475 0 0 1-18.656 18.624c6.227 6.222 12.44 12.457 18.657 18.688l5.156-5.156-13.5-13.5 13.5-13.5z" transform="translate(126.933 -8.678) scale(1.3702)"/></svg>
|
After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 525 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.05000003;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" d="M328.653 258.485c-8.817 0-15.841 13.705-17.413 31.686H195.06c-11.575 0-20.838 10.086-20.838 22.551v224.94c0 12.466 9.263 22.266 20.838 22.266h292.594c11.575 0 21.124-9.8 21.124-22.265v-224.94c0-8.152.026-54.131-18.84-54.238zm-79.643 88.778h184.977c7.313 0 13.131 5.656 13.131 12.845v129.883c0 7.19-5.818 12.846-13.131 12.846H249.01c-7.313 0-13.13-5.657-13.13-12.846V360.108c0-7.189 5.817-12.845 13.13-12.845z" transform="translate(-19.073 1.734) scale(.10947)"/><path style="fill:#fff;stroke:none" d="M57.322 8.871h7.518v22.135h-7.518z" transform="translate(-15.013 -2.006)"/><path transform="rotate(90 -6.504 -8.51)" style="fill:#fff;stroke:none" d="M16.179-72.149h7.518v22.135h-7.518z"/></svg>
|
After Width: | Height: | Size: 977 B |
Before Width: | Height: | Size: 721 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g transform="translate(-31.663 -41) scale(.19138)" style="fill:#000"><rect ry="10" rx="10" y="359.24" x="196.801" height="158.064" width="184.408" style="fill:#000;fill-opacity:1;stroke:none"/><path d="M391.226 235.731c-36.12 0-65.643 29.524-65.643 65.643v69.887c0 .55-.013 1.087 0 1.633h31.352c-.026-.564 0-1.06 0-1.633v-69.887c0-19.281 15.01-34.291 34.29-34.291h4.573c19.28 0 34.29 15.01 34.29 34.29v69.888c0 .573.027 1.069 0 1.633h31.352c.014-.546 0-1.083 0-1.633v-69.887c0-36.12-29.523-65.643-65.642-65.643z" style="font-size:medium;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000;fill:#000;fill-opacity:1;stroke:none;stroke-width:31.37258148;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 780 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path d="m-69 10.156-19.063 14.5h5.313V41.25h11.063v-6.875h5.374v6.875h11.063V24.656h5.313L-69 10.156zm5.406 15.719h4.282v4.281h-4.282v-4.281zm-15.093.094h4.28v4.281h-4.28v-4.281z" style="fill:#000;fill-opacity:1;stroke:none" transform="translate(147.925 -11.148) scale(1.67869)"/><path style="fill:#000;fill-opacity:1;stroke:none" d="M-61 13.5h4.75v7.75H-61z" transform="translate(147.925 -11.148) scale(1.67869)"/></svg>
|
After Width: | Height: | Size: 505 B |
Before Width: | Height: | Size: 874 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path d="m-69 10.156-19.063 14.5h5.313V41.25h11.063v-6.875h5.374v6.875h11.063V24.656h5.313L-69 10.156zm5.406 15.719h4.282v4.281h-4.282v-4.281zm-15.093.094h4.28v4.281h-4.28v-4.281z" style="fill:#fff;fill-opacity:1;stroke:none" transform="translate(147.925 -11.148) scale(1.67869)"/><path style="fill:#fff;fill-opacity:1;stroke:none" d="M-61 13.5h4.75v7.75H-61z" transform="translate(147.925 -11.148) scale(1.67869)"/></svg>
|
After Width: | Height: | Size: 505 B |
Before Width: | Height: | Size: 728 B |
Before Width: | Height: | Size: 702 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;fill-opacity:1;stroke:none" d="M31.844 9.594c-9.553.074-13.219-.126-13.219 4.656v15a2.716 2.716 0 0 0 2.719 2.719H41.75a2.716 2.716 0 0 0 2.719-2.719v-15c0-3.789-3.073-4.73-12.625-4.656zm-.407 3.719c4.165-.02 4.965 2.797 4.407 4.718-.559 1.922-2.906 1.856-4.375 1.844-1.47-.012-3.553.155-4.188-1.844-.635-1.998-.008-4.7 4.157-4.718z" transform="translate(.453)"/><path style="fill:#fff;fill-opacity:1;stroke:none" d="M77.702 20.455h7.305s.104 17.639 0 17.86c-.105.222-2.182 1.792-3.074 1.725-.892-.068-4.13-1.37-4.231-1.724-.101-.355 1.51-2.698 1.498-3.194-.011-.497-1.522-2.735-1.498-3.381.023-.646 1.492-2.338 1.498-2.727.006-.39-1.548-2.375-1.498-2.805.05-.43 1.57-2.067 1.498-2.497-.071-.43-1.498-3.257-1.498-3.257z" transform="translate(-94.13 -2.653) scale(1.54998)"/></svg>
|
After Width: | Height: | Size: 886 B |
Before Width: | Height: | Size: 707 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="M-88.192 11.032h7.29v37.312h-7.29zm32.66 0a7001.475 7001.475 0 0 1-18.656 18.624c6.227 6.222 12.44 12.457 18.657 18.688l5.156-5.156-13.5-13.5 13.5-13.5z" transform="matrix(-1.3702 0 0 1.3702 -62.933 -8.678)"/></svg>
|
After Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;fill-opacity:1;stroke:#000;stroke-width:.00547374;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M2.094 1.486C.934 1.486 0 2.496 0 3.744v42.66c0 1.249.935 2.242 2.094 2.242h.302V5.17l19.272 14v29.476h7.9c1.16 0 2.094-.991 2.094-2.24V3.744c0-1.248-.934-2.258-2.094-2.258H2.094z"/><path d="M208.19 52.448c-10.236 0-18.485 9.336-18.485 20.894V468.33c0 11.558 8.249 20.743 18.486 20.743h242.637c10.237 0 18.486-9.185 18.486-20.743V73.342c0-11.558-8.249-20.894-18.486-20.894z" style="fill:none;fill-opacity:1;stroke:#000;stroke-width:47.44955063;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" transform="matrix(.07096 .05263 0 .10015 -11.461 -11.512)"/><path transform="matrix(.64819 .48075 0 .91482 .415 -3.146)" d="M26.5 30.125a1.625 1.625 0 1 1-3.25 0 1.625 1.625 0 0 1 3.25 0z" style="fill:#000;fill-opacity:1;stroke:#000;stroke-width:5.19446087;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/><path transform="rotate(90 -12.504 -.51)" style="fill:#000;stroke:none" d="M16.179-72.149h7.518v12.885h-7.518z"/><path style="fill:#000;fill-opacity:1;stroke:none" d="M48.023 17.5 29.988 27.913V7.087z" transform="matrix(-.6396 0 0 1 68.454 14.5)"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.00547374;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" d="M2.094 1.486C.934 1.486 0 2.496 0 3.744v42.66c0 1.249.935 2.242 2.094 2.242h.302V5.17l19.272 14v29.476h7.9c1.16 0 2.094-.991 2.094-2.24V3.744c0-1.248-.934-2.258-2.094-2.258H2.094z"/><path d="M208.19 52.448c-10.236 0-18.485 9.336-18.485 20.894V468.33c0 11.558 8.249 20.743 18.486 20.743h242.637c10.237 0 18.486-9.185 18.486-20.743V73.342c0-11.558-8.249-20.894-18.486-20.894z" style="fill:none;fill-opacity:1;stroke:#fff;stroke-width:47.44955063;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" transform="matrix(.07096 .05263 0 .10015 -11.461 -11.512)"/><path transform="matrix(.64819 .48075 0 .91482 .415 -3.146)" d="M26.5 30.125a1.625 1.625 0 1 1-3.25 0 1.625 1.625 0 0 1 3.25 0z" style="fill:#fff;fill-opacity:1;stroke:#fff;stroke-width:5.19446087;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/><path transform="rotate(90 -12.504 -.51)" style="fill:#fff;stroke:none" d="M16.179-72.149h7.518v12.885h-7.518z"/><path style="fill:#fff;fill-opacity:1;stroke:none" d="M48.023 17.5 29.988 27.913V7.087z" transform="matrix(-.6396 0 0 1 68.454 14.5)"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 560 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g transform="translate(-19.073 1.734) scale(.10947)" style="fill:#fff"><path style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.00956892;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" d="M4 .656c-2.215 0-4 1.927-4 4.313V48.03c0 2.386 1.785 4.282 4 4.282h56c2.215 0 4-1.896 4-4.282V4.97c0-2.386-1.785-4.313-4-4.313H4zm10.313 10.938h35.374c1.4 0 2.532 1.093 2.532 2.469v24.843c0 1.376-1.132 2.469-2.532 2.469H14.313c-1.4 0-2.532-1.093-2.532-2.469V14.063c0-1.376 1.132-2.47 2.531-2.47z" transform="translate(174.22 231.813) scale(5.22525)"/><rect ry="18" rx="18" y="524.187" x="244.268" height="38.636" width="197.179" style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.05000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"/></g><path style="fill:#fff;stroke:none" d="M57.322 8.871h7.518v22.135h-7.518z" transform="translate(-15.013 -2.006)"/><path transform="rotate(90 -6.504 -8.51)" style="fill:#fff;stroke:none" d="M16.179-72.149h7.518v22.135h-7.518z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 626 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path style="fill:#000;stroke:none" d="M22.751 6.437a9593.495 9593.495 0 0 0 25.563 25.52c-8.532 8.525-17.045 17.068-25.563 25.606l-7.065-7.065L34.184 32 15.686 13.502z"/></svg>
|
After Width: | Height: | Size: 260 B |
Before Width: | Height: | Size: 648 B |