From fd1c652a84173ac5dc4845dcaf4e0ecc3e456072 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 17 Jan 2017 10:52:00 -0800 Subject: [PATCH 1/2] GUACAMOLE-190: Update client thumbnail roughly every 5 seconds. --- .../webapp/app/client/types/ManagedClient.js | 140 ++++++++++++------ .../client/types/ManagedClientThumbnail.js | 58 ++++++++ 2 files changed, 149 insertions(+), 49 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/types/ManagedClientThumbnail.js diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 213a42ea8..404b5979c 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -24,14 +24,15 @@ 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 ManagedClientState = $injector.get('ManagedClientState'); - var ManagedDisplay = $injector.get('ManagedDisplay'); - var ManagedFilesystem = $injector.get('ManagedFilesystem'); - var ManagedFileUpload = $injector.get('ManagedFileUpload'); - var ManagedShareLink = $injector.get('ManagedShareLink'); + var ClientProperties = $injector.get('ClientProperties'); + var ClientIdentifier = $injector.get('ClientIdentifier'); + var ClipboardData = $injector.get('ClipboardData'); + 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'); // Required services var $document = $injector.get('$document'); @@ -46,7 +47,15 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var guacHistory = $injector.get('guacHistory'); var guacImage = $injector.get('guacImage'); var guacVideo = $injector.get('guacVideo'); - + + /** + * The minimum amount of time to wait between updates to the client + * thumbnail, in milliseconds. + * + * @type Number + */ + var THUMBNAIL_UPDATE_FREQUENCY = 5000; + /** * Object which serves as a surrogate interface, encapsulating a Guacamole * client while it is active, allowing it to be detached and reattached @@ -98,6 +107,15 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ this.name = template.name; + /** + * The most recently-generated thumbnail for this connection, as + * stored within the local connection history. If no thumbnail is + * stored, this will be null. + * + * @type ManagedClientThumbnail + */ + this.thumbnail = template.thumbnail; + /** * The current clipboard contents. * @@ -227,45 +245,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; - /** - * Store the thumbnail of the given managed client within the connection - * history under its associated ID. If the client is not connected, this - * function has no effect. - * - * @param {String} managedClient - * The client whose history entry should be updated. - */ - var updateHistoryEntry = function updateHistoryEntry(managedClient) { - - var display = managedClient.client.getDisplay(); - - // Update stored thumbnail of previous connection - if (display && display.getWidth() > 0 && display.getHeight() > 0) { - - // Get screenshot - var canvas = display.flatten(); - - // Calculate scale of thumbnail (max 320x240, max zoom 100%) - var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); - - // Create thumbnail canvas - var thumbnail = $document[0].createElement("canvas"); - thumbnail.width = canvas.width*scale; - thumbnail.height = canvas.height*scale; - - // Scale screenshot to thumbnail - var context = thumbnail.getContext("2d"); - context.drawImage(canvas, - 0, 0, canvas.width, canvas.height, - 0, 0, thumbnail.width, thumbnail.height - ); - - guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png")); - - } - - }; - /** * Requests the creation of a new audio stream, recorded from the user's * local audio input device. If audio input is supported by the connection, @@ -403,12 +382,14 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Begin streaming audio input if possible requestAudioStream(client); + // Update thumbnail with initial display contents + ManagedClient.updateThumbnail(managedClient); break; // Update history when disconnecting case 4: // Disconnecting case 5: // Disconnected - updateHistoryEntry(managedClient); + ManagedClient.updateThumbnail(managedClient); break; } @@ -431,6 +412,21 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }); }; + // Automatically update the client thumbnail + client.onsync = function syncReceived() { + + var thumbnail = managedClient.thumbnail; + var timestamp = new Date().getTime(); + + // Update thumbnail if it doesn't exist or is old + if (!thumbnail || timestamp - thumbnail.timestamp >= THUMBNAIL_UPDATE_FREQUENCY) { + $rootScope.$apply(function updateClientThumbnail() { + ManagedClient.updateThumbnail(managedClient); + }); + } + + }; + // Handle any received clipboard data client.onclipboard = function clientClipboardReceived(stream, mimetype) { @@ -651,6 +647,52 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; + /** + * Store the thumbnail of the given managed client within the connection + * history under its associated ID. If the client is not connected, this + * function has no effect. + * + * @param {ManagedClient} managedClient + * The client whose history entry should be updated. + */ + ManagedClient.updateThumbnail = function updateThumbnail(managedClient) { + + var display = managedClient.client.getDisplay(); + + // Update stored thumbnail of previous connection + if (display && display.getWidth() > 0 && display.getHeight() > 0) { + + // Get screenshot + var canvas = display.flatten(); + + // Calculate scale of thumbnail (max 320x240, max zoom 100%) + var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); + + // Create thumbnail canvas + var thumbnail = $document[0].createElement("canvas"); + thumbnail.width = canvas.width*scale; + thumbnail.height = canvas.height*scale; + + // Scale screenshot to thumbnail + var context = thumbnail.getContext("2d"); + context.drawImage(canvas, + 0, 0, canvas.width, canvas.height, + 0, 0, thumbnail.width, thumbnail.height + ); + + // Store updated thumbnail within client + managedClient.thumbnail = new ManagedClientThumbnail({ + timestamp : new Date().getTime(), + canvas : thumbnail + }); + + // Update historical thumbnail + guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png")); + + } + + }; + return ManagedClient; }]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClientThumbnail.js b/guacamole/src/main/webapp/app/client/types/ManagedClientThumbnail.js new file mode 100644 index 000000000..fd9b3dea5 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/types/ManagedClientThumbnail.js @@ -0,0 +1,58 @@ +/* + * 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 ManagedClientThumbnail class used by ManagedClient. + */ +angular.module('client').factory('ManagedClientThumbnail', [function defineManagedClientThumbnail() { + + /** + * Object which represents a thumbnail of the Guacamole client display, + * along with the time that the thumbnail was generated. + * + * @constructor + * @param {ManagedClientThumbnail|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedClientThumbnail. + */ + var ManagedClientThumbnail = function ManagedClientThumbnail(template) { + + // Use empty object by default + template = template || {}; + + /** + * The time that this thumbnail was generated, as the number of + * milliseconds elapsed since midnight of January 1, 1970 UTC. + * + * @type Number + */ + this.timestamp = template.timestamp; + + /** + * The thumbnail of the Guacamole client display. + * + * @type HTMLCanvasElement + */ + this.canvas = template.canvas; + + }; + + return ManagedClientThumbnail; + +}]); \ No newline at end of file From 69a25c4e48f0c4d8fecf9cd7c95a859f30646e4f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 17 Jan 2017 10:52:46 -0800 Subject: [PATCH 2/2] GUACAMOLE-190: Synchronize page icon with client thumbnail. --- .../client/controllers/clientController.js | 6 + .../webapp/app/index/services/iconService.js | 148 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 guacamole/src/main/webapp/app/index/services/iconService.js diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 9827de1e6..a66a0cecc 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -35,6 +35,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var clipboardService = $injector.get('clipboardService'); var guacClientManager = $injector.get('guacClientManager'); var guacNotification = $injector.get('guacNotification'); + var iconService = $injector.get('iconService'); var preferenceService = $injector.get('preferenceService'); var tunnelService = $injector.get('tunnelService'); var userPageService = $injector.get('userPageService'); @@ -403,6 +404,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); + // Update page icon when thumbnail changes + $scope.$watch('client.thumbnail.canvas', function thumbnailChanged(canvas) { + iconService.setIcons(canvas); + }); + // Watch clipboard for new data, associating it with any pressed keys $scope.$watch('client.clipboardData', function clipboardChanged(data) { diff --git a/guacamole/src/main/webapp/app/index/services/iconService.js b/guacamole/src/main/webapp/app/index/services/iconService.js new file mode 100644 index 000000000..ed9e0574f --- /dev/null +++ b/guacamole/src/main/webapp/app/index/services/iconService.js @@ -0,0 +1,148 @@ +/* + * 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 service for updating or resetting the favicon of the current page. + */ +angular.module('index').factory('iconService', ['$rootScope', function iconService($rootScope) { + + var service = {}; + + /** + * The URL of the image used for the low-resolution (64x64) favicon. This + * MUST match the URL which is set statically within index.html. + * + * @constant + * @type String + */ + var DEFAULT_SMALL_ICON_URL = 'images/logo-64.png'; + + /** + * The URL of the image used for the high-resolution (144x144) favicon. This + * MUST match the URL which is set statically within index.html. + * + * @constant + * @type String + */ + var DEFAULT_LARGE_ICON_URL = 'images/logo-144.png'; + + /** + * JQuery-wrapped array of all link tags which point to the small, + * low-resolution page icon. + * + * @type Element[] + */ + var smallIcons = $('link[rel=icon][href="' + DEFAULT_SMALL_ICON_URL + '"]'); + + /** + * JQuery-wrapped array of all link tags which point to the large, + * high-resolution page icon. + * + * @type Element[] + */ + var largeIcons = $('link[rel=icon][href="' + DEFAULT_LARGE_ICON_URL + '"]'); + + /** + * Generates an icon by scaling the provided image to fit the given + * dimensions, returning a canvas containing the generated icon. + * + * @param {HTMLCanvasElement} canvas + * A canvas element containing the image which should be scaled to + * produce the contents of the generated icon. + * + * @param {Number} width + * The width of the icon to generate, in pixels. + * + * @param {Number} height + * The height of the icon to generate, in pixels. + * + * @returns {HTMLCanvasElement} + * A new canvas element having the given dimensions and containing the + * provided image, scaled to fit. + */ + var generateIcon = function generateIcon(canvas, width, height) { + + // Create icon canvas having the provided dimensions + var icon = document.createElement('canvas'); + icon.width = width; + icon.height = height; + + // Calculate the scale factor necessary to fit the provided image + // within the icon dimensions + var scale = Math.min(width / canvas.width, height / canvas.height); + + // Calculate the dimensions and position of the scaled image within + // the icon, offsetting the image such that it is centered + var scaledWidth = canvas.width * scale; + var scaledHeight = canvas.height * scale; + var offsetX = (width - scaledWidth) / 2; + var offsetY = (height - scaledHeight) / 2; + + // Draw the icon, scaling the provided image as necessary + var context = icon.getContext('2d'); + context.drawImage(canvas, offsetX, offsetY, scaledWidth, scaledHeight); + return icon; + + }; + + /** + * Temporarily sets the icon of the current page to the contents of the + * given canvas element. The image within the canvas element will be + * automatically scaled and centered to fit within the dimensions of the + * page icons. The page icons will be automatically reset to their original + * values upon navigation. + * + * @param {HTMLCanvasElement} canvas + * The canvas element containing the icon. If this value is null or + * undefined, this function has no effect. + */ + service.setIcons = function setIcons(canvas) { + + // Do nothing if no canvas provided + if (!canvas) + return; + + // Assign low-resolution (64x64) icon + var smallIcon = generateIcon(canvas, 64, 64); + smallIcons.attr('href', smallIcon.toDataURL('image/png')); + + // Assign high-resolution (144x144) icon + var largeIcon = generateIcon(canvas, 144, 144); + largeIcons.attr('href', largeIcon.toDataURL('image/png')); + + }; + + /** + * Resets the icons of the current page to their original values, undoing + * any previous calls to setIcons(). This function is automatically invoked + * upon navigation. + */ + service.setDefaultIcons = function setDefaultIcons() { + smallIcons.attr('href', DEFAULT_SMALL_ICON_URL); + largeIcons.attr('href', DEFAULT_LARGE_ICON_URL); + }; + + // Automatically reset page icons after navigation + $rootScope.$on('$routeChangeSuccess', function resetIcon() { + service.setDefaultIcons(); + }); + + return service; + +}]);