mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-1820: Create UI for viewing, searching, and navigating to key events in session recording player.
This commit is contained in:
21
doc/licenses/fuzzysort-2.0.4/LICENSE
Normal file
21
doc/licenses/fuzzysort-2.0.4/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Stephen Kamenar
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
8
doc/licenses/fuzzysort-2.0.4/README
Normal file
8
doc/licenses/fuzzysort-2.0.4/README
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fuzzysort (https://github.com/farzher/fuzzysort/tree/master)
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
Version: 2.0.4
|
||||||
|
From: 'Stephen Kamenar' (https://github.com/farzher)
|
||||||
|
License(s):
|
||||||
|
MIT (bundled/fuzzysort-2.0.4/LICENSE)
|
||||||
|
|
1
doc/licenses/fuzzysort-2.0.4/dep-coordinates.txt
Normal file
1
doc/licenses/fuzzysort-2.0.4/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fuzzysort:2.0.4
|
@@ -0,0 +1,487 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Guacamole = Guacamole || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that will accept raw key events and produce human readable text
|
||||||
|
* batches, seperated by at least `batchSeperation` milliseconds, which can be
|
||||||
|
* retrieved through the onBatch callback or by calling getCurrentBatch().
|
||||||
|
*
|
||||||
|
* NOTE: The event processing logic and output format is based on the `guaclog`
|
||||||
|
* tool, with the addition of batching support.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {number} [batchSeperation=5000]
|
||||||
|
* The minimum number of milliseconds that must elapse between subsequent
|
||||||
|
* batches of key-event-generated text. If 0 or negative, no splitting will
|
||||||
|
* occur, resulting in a single batch for all provided key events.
|
||||||
|
*/
|
||||||
|
Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to this Guacamole.KeyEventInterpreter.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!Guacamole.SessionRecording}
|
||||||
|
*/
|
||||||
|
var interpreter = this;
|
||||||
|
|
||||||
|
// Default to 5 seconds if the batch seperation was not provided
|
||||||
|
if (batchSeperation === undefined || batchSeperation === null)
|
||||||
|
batchSeperation = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A definition for a known key.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @private
|
||||||
|
* @param {KEY_DEFINITION|object} [template={}]
|
||||||
|
* The object whose properties should be copied within the new
|
||||||
|
* KEY_DEFINITION.
|
||||||
|
*/
|
||||||
|
var KeyDefinition = function KeyDefinition(template) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The X11 keysym of the key.
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
this.keysym = parseInt(template.keysym);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-readable name for the key.
|
||||||
|
* @type {!String}
|
||||||
|
*/
|
||||||
|
this.name = template.name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value which would be typed in a typical text editor, if any. If the
|
||||||
|
* key is not associated with any typable value, or if the typable value is
|
||||||
|
* not generally useful in an auditing context, this will be undefined.
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
this.value = template.value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this key is a modifier which may affect the interpretation of
|
||||||
|
* other keys, and thus should be tracked as it is held down.
|
||||||
|
* @type {!boolean}
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
this.modifier = template.modifier || false;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A precursor array to the KNOWN_KEYS map. The objects contained within
|
||||||
|
* will be constructed into full KeyDefinition objects.
|
||||||
|
*
|
||||||
|
* @constant
|
||||||
|
* @private
|
||||||
|
* @type {Object[]}
|
||||||
|
*/
|
||||||
|
var _KNOWN_KEYS = [
|
||||||
|
{keysym: 0xFE03, name: 'AltGr', value: "", modifier: true },
|
||||||
|
{keysym: 0xFF08, name: 'Backspace' },
|
||||||
|
{keysym: 0xFF09, name: 'Tab' },
|
||||||
|
{keysym: 0xFF0B, name: 'Clear' },
|
||||||
|
{keysym: 0xFF0D, name: 'Return', value: "\n" },
|
||||||
|
{keysym: 0xFF13, name: 'Pause' },
|
||||||
|
{keysym: 0xFF14, name: 'Scroll' },
|
||||||
|
{keysym: 0xFF15, name: 'SysReq' },
|
||||||
|
{keysym: 0xFF1B, name: 'Escape' },
|
||||||
|
{keysym: 0xFF50, name: 'Home' },
|
||||||
|
{keysym: 0xFF51, name: 'Left' },
|
||||||
|
{keysym: 0xFF52, name: 'Up' },
|
||||||
|
{keysym: 0xFF53, name: 'Right' },
|
||||||
|
{keysym: 0xFF54, name: 'Down' },
|
||||||
|
{keysym: 0xFF55, name: 'Page Up' },
|
||||||
|
{keysym: 0xFF56, name: 'Page Down' },
|
||||||
|
{keysym: 0xFF57, name: 'End' },
|
||||||
|
{keysym: 0xFF63, name: 'Insert' },
|
||||||
|
{keysym: 0xFF65, name: 'Undo' },
|
||||||
|
{keysym: 0xFF6A, name: 'Help' },
|
||||||
|
{keysym: 0xFF7F, name: 'Num' },
|
||||||
|
{keysym: 0xFF80, name: 'Space', value: " " },
|
||||||
|
{keysym: 0xFF8D, name: 'Enter', value: "\n" },
|
||||||
|
{keysym: 0xFF95, name: 'Home' },
|
||||||
|
{keysym: 0xFF96, name: 'Left' },
|
||||||
|
{keysym: 0xFF97, name: 'Up' },
|
||||||
|
{keysym: 0xFF98, name: 'Right' },
|
||||||
|
{keysym: 0xFF99, name: 'Down' },
|
||||||
|
{keysym: 0xFF9A, name: 'Page Up' },
|
||||||
|
{keysym: 0xFF9B, name: 'Page Down' },
|
||||||
|
{keysym: 0xFF9C, name: 'End' },
|
||||||
|
{keysym: 0xFF9E, name: 'Insert' },
|
||||||
|
{keysym: 0xFFAA, name: '*', value: "*" },
|
||||||
|
{keysym: 0xFFAB, name: '+', value: "+" },
|
||||||
|
{keysym: 0xFFAD, name: '-', value: "-" },
|
||||||
|
{keysym: 0xFFAE, name: '.', value: "." },
|
||||||
|
{keysym: 0xFFAF, name: '/', value: "/" },
|
||||||
|
{keysym: 0xFFB0, name: '0', value: "0" },
|
||||||
|
{keysym: 0xFFB1, name: '1', value: "1" },
|
||||||
|
{keysym: 0xFFB2, name: '2', value: "2" },
|
||||||
|
{keysym: 0xFFB3, name: '3', value: "3" },
|
||||||
|
{keysym: 0xFFB4, name: '4', value: "4" },
|
||||||
|
{keysym: 0xFFB5, name: '5', value: "5" },
|
||||||
|
{keysym: 0xFFB6, name: '6', value: "6" },
|
||||||
|
{keysym: 0xFFB7, name: '7', value: "7" },
|
||||||
|
{keysym: 0xFFB8, name: '8', value: "8" },
|
||||||
|
{keysym: 0xFFB9, name: '9', value: "9" },
|
||||||
|
{keysym: 0xFFBE, name: 'F1' },
|
||||||
|
{keysym: 0xFFBF, name: 'F2' },
|
||||||
|
{keysym: 0xFFC0, name: 'F3' },
|
||||||
|
{keysym: 0xFFC1, name: 'F4' },
|
||||||
|
{keysym: 0xFFC2, name: 'F5' },
|
||||||
|
{keysym: 0xFFC3, name: 'F6' },
|
||||||
|
{keysym: 0xFFC4, name: 'F7' },
|
||||||
|
{keysym: 0xFFC5, name: 'F8' },
|
||||||
|
{keysym: 0xFFC6, name: 'F9' },
|
||||||
|
{keysym: 0xFFC7, name: 'F10' },
|
||||||
|
{keysym: 0xFFC8, name: 'F11' },
|
||||||
|
{keysym: 0xFFC9, name: 'F12' },
|
||||||
|
{keysym: 0xFFCA, name: 'F13' },
|
||||||
|
{keysym: 0xFFCB, name: 'F14' },
|
||||||
|
{keysym: 0xFFCC, name: 'F15' },
|
||||||
|
{keysym: 0xFFCD, name: 'F16' },
|
||||||
|
{keysym: 0xFFCE, name: 'F17' },
|
||||||
|
{keysym: 0xFFCF, name: 'F18' },
|
||||||
|
{keysym: 0xFFD0, name: 'F19' },
|
||||||
|
{keysym: 0xFFD1, name: 'F20' },
|
||||||
|
{keysym: 0xFFD2, name: 'F21' },
|
||||||
|
{keysym: 0xFFD3, name: 'F22' },
|
||||||
|
{keysym: 0xFFD4, name: 'F23' },
|
||||||
|
{keysym: 0xFFD5, name: 'F24' },
|
||||||
|
{keysym: 0xFFE1, name: 'Shift', value: "", modifier: true },
|
||||||
|
{keysym: 0xFFE2, name: 'Shift', value: "", modifier: true },
|
||||||
|
{keysym: 0xFFE3, name: 'Ctrl', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFE4, name: 'Ctrl', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFE5, name: 'Caps' },
|
||||||
|
{keysym: 0xFFE7, name: 'Meta', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFE8, name: 'Meta', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFE9, name: 'Alt', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFEA, name: 'Alt', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFEB, name: 'Super', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFEC, name: 'Super', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFED, name: 'Hyper', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFEE, name: 'Hyper', value: null, modifier: true },
|
||||||
|
{keysym: 0xFFFF, name: 'Delete' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All known keys, as a map of X11 keysym to KeyDefinition.
|
||||||
|
*
|
||||||
|
* @constant
|
||||||
|
* @private
|
||||||
|
* @type {Object.<String, KeyDefinition>}
|
||||||
|
*/
|
||||||
|
var KNOWN_KEYS = {};
|
||||||
|
_KNOWN_KEYS.forEach(function createKeyDefinitionMap(keyDefinition) {
|
||||||
|
|
||||||
|
// Construct a map of keysym to KeyDefinition object
|
||||||
|
KNOWN_KEYS[keyDefinition.keysym] = new KeyDefinition(keyDefinition)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of X11 keysyms to a KeyDefinition object, if the corresponding
|
||||||
|
* key is currently pressed. If a keysym has no entry in this map at all,
|
||||||
|
* it means that the key is not being pressed. Note that not all keysyms
|
||||||
|
* are necessarily tracked within this map - only those that are explicitly
|
||||||
|
* tracked.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {Object.<String,KeyDefinition> }
|
||||||
|
*/
|
||||||
|
var pressedKeys = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-readable representation of all keys pressed since the last keyframe.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
var currentTypedValue = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the key event that started the most recent batch of
|
||||||
|
* text content. If 0, no key events have been processed yet.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
var lastTextTimestamp = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the most recent key event processed.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
var lastKeyEvent = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the currently-pressed keys are part of a shortcut, or
|
||||||
|
* false otherwise.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {!boolean}
|
||||||
|
* True if the currently-pressed keys are part of a shortcut, or false
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
function isShortcut() {
|
||||||
|
|
||||||
|
// If one of the currently-pressed keys is non-printable, a shortcut
|
||||||
|
// is being typed
|
||||||
|
for (var keysym in pressedKeys) {
|
||||||
|
if (pressedKeys[keysym].value === null)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the provided keysym corresponds to a valid UTF-8 character, return
|
||||||
|
* a KeyDefinition for that keysym. Otherwise, return null.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Number} keysym
|
||||||
|
* The keysym to produce a UTF-8 KeyDefinition for, if valid.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* Return a KeyDefinition for the provided keysym, if it it's a valid
|
||||||
|
* UTF-8 keysym, or null otherwise.
|
||||||
|
*/
|
||||||
|
function getUnicodeKeyDefinition(keysym) {
|
||||||
|
|
||||||
|
// Translate only if keysym maps to Unicode
|
||||||
|
if (keysym < 0x00 || (keysym > 0xFF && (keysym | 0xFFFF) != 0x0100FFFF))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var codepoint = keysym & 0xFFFF;
|
||||||
|
var mask;
|
||||||
|
var bytes;
|
||||||
|
|
||||||
|
/* Determine size and initial byte mask */
|
||||||
|
if (codepoint <= 0x007F) {
|
||||||
|
mask = 0x00;
|
||||||
|
bytes = 1;
|
||||||
|
}
|
||||||
|
else if (codepoint <= 0x7FF) {
|
||||||
|
mask = 0xC0;
|
||||||
|
bytes = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mask = 0xE0;
|
||||||
|
bytes = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
var byteArray = new ArrayBuffer(bytes);
|
||||||
|
var byteView = new Int8Array(byteArray);
|
||||||
|
|
||||||
|
// Add trailing bytes, if any
|
||||||
|
for (var i = 1; i < bytes; i++) {
|
||||||
|
byteView[bytes - i] = 0x80 | (codepoint & 0x3F);
|
||||||
|
codepoint >>= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial byte
|
||||||
|
byteView[0] = mask | codepoint;
|
||||||
|
|
||||||
|
// Convert to UTF8 string
|
||||||
|
var name = new TextDecoder("utf-8").decode(byteArray);
|
||||||
|
|
||||||
|
// Create and return the definition
|
||||||
|
return new KeyDefinition({keysym: keysym.toString(), name: name, value: name, modifier: false});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a KeyDefinition corresponding to the provided keysym.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Number} keysym
|
||||||
|
* The keysym to return a KeyDefinition for.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* A KeyDefinition corresponding to the provided keysym.
|
||||||
|
*/
|
||||||
|
function getKeyDefinitionByKeysym(keysym) {
|
||||||
|
|
||||||
|
// If it's a known type, return the existing definition
|
||||||
|
if (keysym in KNOWN_KEYS)
|
||||||
|
return KNOWN_KEYS[keysym];
|
||||||
|
|
||||||
|
// Return a UTF-8 KeyDefinition, if valid
|
||||||
|
var definition = getUnicodeKeyDefinition(keysym);
|
||||||
|
if (definition != null)
|
||||||
|
return definition;
|
||||||
|
|
||||||
|
// If it's not UTF-8, return an unknown definition, with the name
|
||||||
|
// just set to the hex value of the keysym
|
||||||
|
return new KeyDefinition({
|
||||||
|
keysym: keysym,
|
||||||
|
name: '0x' + String(keysym.toString(16))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired whenever a new batch of typed text extracted from key events
|
||||||
|
* is available. A new batch will be provided every time a new key event
|
||||||
|
* is processed after more than batchSeperation milliseconds after the
|
||||||
|
* previous key event.
|
||||||
|
*
|
||||||
|
* @event
|
||||||
|
* @param {!String} text
|
||||||
|
* The typed text associated with the batch of text.
|
||||||
|
*
|
||||||
|
* @param {!number} timestamp
|
||||||
|
* The raw recording timestamp associated with the first key event
|
||||||
|
* that started this batch of text.
|
||||||
|
*/
|
||||||
|
interpreter.onBatch = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a raw key event, potentially appending typed text to the
|
||||||
|
* current batch, and calling onBatch with the current batch, if the
|
||||||
|
* callback is set and a new batch is about to be started.
|
||||||
|
*
|
||||||
|
* @param {!string[]} args
|
||||||
|
* The arguments of the key event.
|
||||||
|
*/
|
||||||
|
interpreter.handleKeyEvent = function handleKeyEvent(args) {
|
||||||
|
|
||||||
|
// The X11 keysym
|
||||||
|
var keysym = parseInt(args[0]);
|
||||||
|
|
||||||
|
// Either 1 or 0 for pressed or released, respectively
|
||||||
|
var pressed = parseInt(args[1]);
|
||||||
|
|
||||||
|
// The timestamp when this key event occured
|
||||||
|
var timestamp = parseInt(args[2]);
|
||||||
|
|
||||||
|
// If no current batch exists, start a new one now
|
||||||
|
if (!lastTextTimestamp) {
|
||||||
|
lastTextTimestamp = timestamp;
|
||||||
|
lastKeyEvent = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only switch to a new batch of text if sufficient time has passed
|
||||||
|
// since the last key event
|
||||||
|
var newBatch = (batchSeperation >= 0
|
||||||
|
&& (timestamp - lastKeyEvent) >= batchSeperation);
|
||||||
|
lastKeyEvent = timestamp;
|
||||||
|
|
||||||
|
if (newBatch) {
|
||||||
|
|
||||||
|
// Call the handler with the current batch of text and the timestamp
|
||||||
|
// at which the current batch started
|
||||||
|
if (currentTypedValue && interpreter.onBatch)
|
||||||
|
interpreter.onBatch(currentTypedValue, lastTextTimestamp);
|
||||||
|
|
||||||
|
// Move on to the next batch of text
|
||||||
|
currentTypedValue = '';
|
||||||
|
lastTextTimestamp = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyDefinition = getKeyDefinitionByKeysym(keysym);
|
||||||
|
|
||||||
|
// Mark down whether the key was pressed or released
|
||||||
|
if (keyDefinition.modifier) {
|
||||||
|
if (pressed)
|
||||||
|
pressedKeys[keysym] = keyDefinition;
|
||||||
|
else
|
||||||
|
delete pressedKeys[keysym];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to the current typed value when a printable
|
||||||
|
// (non-modifier) key is pressed
|
||||||
|
else if (pressed) {
|
||||||
|
|
||||||
|
if (isShortcut()) {
|
||||||
|
|
||||||
|
currentTypedValue += '<';
|
||||||
|
|
||||||
|
var firstKey = true;
|
||||||
|
|
||||||
|
// Compose entry by inspecting the state of each tracked key.
|
||||||
|
// At least one key must be pressed when in a shortcut.
|
||||||
|
for (var keysym in pressedKeys) {
|
||||||
|
|
||||||
|
var pressedKeyDefinition = pressedKeys[keysym];
|
||||||
|
|
||||||
|
// Print name of key
|
||||||
|
if (firstKey) {
|
||||||
|
currentTypedValue += pressedKeyDefinition.name;
|
||||||
|
firstKey = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
currentTypedValue += ('+' + pressedKeyDefinition.name);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, append the printable key to close the shortcut
|
||||||
|
currentTypedValue += ('+' + keyDefinition.name + '>')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the key itself
|
||||||
|
else {
|
||||||
|
|
||||||
|
// Print the value if explicitly defined
|
||||||
|
if (keyDefinition.value != null)
|
||||||
|
currentTypedValue += keyDefinition.value;
|
||||||
|
|
||||||
|
// Otherwise print the name
|
||||||
|
else
|
||||||
|
currentTypedValue += ('<' + keyDefinition.name + '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current batch of typed text. Note that the batch may be
|
||||||
|
* incomplete, as more key events might be processed before the next
|
||||||
|
* batch starts.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* The current batch of text.
|
||||||
|
*/
|
||||||
|
interpreter.getCurrentText = function getCurrentText() {
|
||||||
|
return currentTypedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the recording timestamp associated with the start of the
|
||||||
|
* current batch of typed text.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* The recording timestamp at which the current batch started.
|
||||||
|
*/
|
||||||
|
interpreter.getCurrentTimestamp = function getCurrentTimestamp() {
|
||||||
|
return lastTextTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -104,6 +104,16 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
|
|||||||
*/
|
*/
|
||||||
var KEYFRAME_TIME_INTERVAL = 5000;
|
var KEYFRAME_TIME_INTERVAL = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum number of milliseconds which must elapse between key events
|
||||||
|
* before text can be split across multiple frames.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @constant
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
var TYPED_TEXT_INTERVAL = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All frames parsed from the provided blob.
|
* All frames parsed from the provided blob.
|
||||||
*
|
*
|
||||||
@@ -375,6 +385,26 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
|
|||||||
// Hide cursor unless mouse position is received
|
// Hide cursor unless mouse position is received
|
||||||
playbackClient.getDisplay().showCursor(false);
|
playbackClient.getDisplay().showCursor(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key event interpreter to split all key events in this recording into
|
||||||
|
* human-readable batches of text.
|
||||||
|
*
|
||||||
|
* @type {!Guacamole.KeyEventInterpreter}
|
||||||
|
*/
|
||||||
|
var keyEventInterpreter = new Guacamole.KeyEventInterpreter();
|
||||||
|
|
||||||
|
// Pass through any received batches to the recording ontext handler
|
||||||
|
keyEventInterpreter.onBatch = function onBatch(text, timestamp) {
|
||||||
|
|
||||||
|
// Don't call the callback if it was never set
|
||||||
|
if (!recording.ontext)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Convert to a recording-relative timestamp and pass through
|
||||||
|
recording.ontext(text, toRelativeTimestamp(timestamp));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a newly-received instruction, whether from the main Blob or a
|
* Handles a newly-received instruction, whether from the main Blob or a
|
||||||
* tunnel, adding new frames and keyframes as necessary. Load progress is
|
* tunnel, adding new frames and keyframes as necessary. Load progress is
|
||||||
@@ -421,6 +451,8 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (opcode === 'key')
|
||||||
|
keyEventInterpreter.handleKeyEvent(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -487,6 +519,13 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
|
|||||||
instructionBuffer = '';
|
instructionBuffer = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's any typed text that's yet to be sent to the ontext
|
||||||
|
// handler, send it now
|
||||||
|
var text = keyEventInterpreter.getCurrentText();
|
||||||
|
var timestamp = keyEventInterpreter.getCurrentTimestamp();
|
||||||
|
if (text && recording.ontext)
|
||||||
|
recording.ontext(text, toRelativeTimestamp(timestamp));
|
||||||
|
|
||||||
// Consider recording loaded if tunnel has closed without errors
|
// Consider recording loaded if tunnel has closed without errors
|
||||||
if (!errorEncountered)
|
if (!errorEncountered)
|
||||||
notifyLoaded();
|
notifyLoaded();
|
||||||
@@ -872,6 +911,19 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
|
|||||||
*/
|
*/
|
||||||
this.onpause = null;
|
this.onpause = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired whenever a new batch of typed text extracted from key events
|
||||||
|
* is available.
|
||||||
|
*
|
||||||
|
* @event
|
||||||
|
* @param {!String} text
|
||||||
|
* The typed text associated with the batch of text.
|
||||||
|
*
|
||||||
|
* @param {!number} timestamp
|
||||||
|
* The relative timestamp associated with the batch of text.
|
||||||
|
*/
|
||||||
|
this.ontext = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired whenever the playback position within the recording changes.
|
* Fired whenever the playback position within the recording changes.
|
||||||
*
|
*
|
||||||
|
11
guacamole/src/main/frontend/package-lock.json
generated
11
guacamole/src/main/frontend/package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"csv": "^6.2.5",
|
"csv": "^6.2.5",
|
||||||
"datalist-polyfill": "^1.25.1",
|
"datalist-polyfill": "^1.25.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"fuzzysort": "^2.0.4",
|
||||||
"jquery": "^3.6.4",
|
"jquery": "^3.6.4",
|
||||||
"jstz": "^2.1.1",
|
"jstz": "^2.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -5632,6 +5633,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuzzysort": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw=="
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -15703,6 +15709,11 @@
|
|||||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fuzzysort": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw=="
|
||||||
|
},
|
||||||
"gensync": {
|
"gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
"csv": "^6.2.5",
|
"csv": "^6.2.5",
|
||||||
"datalist-polyfill": "^1.25.1",
|
"datalist-polyfill": "^1.25.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"fuzzysort": "^2.0.4",
|
||||||
"jquery": "^3.6.4",
|
"jquery": "^3.6.4",
|
||||||
"jstz": "^2.1.1",
|
"jstz": "^2.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@@ -77,6 +77,12 @@
|
|||||||
*/
|
*/
|
||||||
angular.module('player').directive('guacPlayer', ['$injector', function guacPlayer($injector) {
|
angular.module('player').directive('guacPlayer', ['$injector', function guacPlayer($injector) {
|
||||||
|
|
||||||
|
// Required types
|
||||||
|
const TextBatch = $injector.get('TextBatch');
|
||||||
|
|
||||||
|
// Required services
|
||||||
|
const playerTimeService = $injector.get('playerTimeService');
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
restrict : 'E',
|
restrict : 'E',
|
||||||
templateUrl : 'app/player/templates/player.html'
|
templateUrl : 'app/player/templates/player.html'
|
||||||
@@ -142,6 +148,21 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
|||||||
*/
|
*/
|
||||||
$scope.seekPosition = null;
|
$scope.seekPosition = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any batches of text typed during the recording.
|
||||||
|
*
|
||||||
|
* @type {TextBatch[]}
|
||||||
|
*/
|
||||||
|
$scope.textBatches = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the key log viewer should be displayed. False by
|
||||||
|
* default unless explicitly enabled by user interaction.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
$scope.showKeyLog = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a seek request is currently in progress. A seek request is
|
* Whether a seek request is currently in progress. A seek request is
|
||||||
* in progress if the user is attempting to change the current playback
|
* in progress if the user is attempting to change the current playback
|
||||||
@@ -161,57 +182,29 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
|||||||
var resumeAfterSeekRequest = false;
|
var resumeAfterSeekRequest = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats the given number as a decimal string, adding leading zeroes
|
* Return true if any batches of key event logs are available for this
|
||||||
* such that the string contains at least two digits. The given number
|
* recording, or false otherwise.
|
||||||
* MUST NOT be negative.
|
|
||||||
*
|
*
|
||||||
* @param {!number} value
|
* @return
|
||||||
* The number to format.
|
* True if any batches of key event logs are avaiable for this
|
||||||
*
|
* recording, or false otherwise.
|
||||||
* @returns {!string}
|
|
||||||
* The decimal string representation of the given value, padded
|
|
||||||
* with leading zeroes up to a minimum length of two digits.
|
|
||||||
*/
|
*/
|
||||||
const zeroPad = function zeroPad(value) {
|
$scope.hasTextBatches = function hasTextBatches () {
|
||||||
return value > 9 ? value : '0' + value;
|
return $scope.textBatches.length >= 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats the given quantity of milliseconds as days, hours, minutes,
|
* Toggle the visibility of the text key log viewer.
|
||||||
* and whole seconds, separated by colons (DD:HH:MM:SS). Hours are
|
|
||||||
* included only if the quantity is at least one hour, and days are
|
|
||||||
* included only if the quantity is at least one day. All included
|
|
||||||
* groups are zero-padded to two digits with the exception of the
|
|
||||||
* left-most group.
|
|
||||||
*
|
|
||||||
* @param {!number} value
|
|
||||||
* The time to format, in milliseconds.
|
|
||||||
*
|
|
||||||
* @returns {!string}
|
|
||||||
* The given quantity of milliseconds formatted as "DD:HH:MM:SS".
|
|
||||||
*/
|
*/
|
||||||
$scope.formatTime = function formatTime(value) {
|
$scope.toggleKeyLogView = function toggleKeyLogView() {
|
||||||
|
$scope.showKeyLog = !$scope.showKeyLog;
|
||||||
// Round provided value down to whole seconds
|
|
||||||
value = Math.floor((value || 0) / 1000);
|
|
||||||
|
|
||||||
// Separate seconds into logical groups of seconds, minutes,
|
|
||||||
// hours, etc.
|
|
||||||
var groups = [ 1, 24, 60, 60 ];
|
|
||||||
for (var i = groups.length - 1; i >= 0; i--) {
|
|
||||||
var placeValue = groups[i];
|
|
||||||
groups[i] = zeroPad(value % placeValue);
|
|
||||||
value = Math.floor(value / placeValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format groups separated by colons, stripping leading zeroes and
|
|
||||||
// groups which are entirely zeroes, leaving at least minutes and
|
|
||||||
// seconds
|
|
||||||
var formatted = groups.join(':');
|
|
||||||
return /^[0:]*([0-9]{1,2}(?::[0-9]{2})+)$/.exec(formatted)[1];
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @borrows playerTimeService.formatTime
|
||||||
|
*/
|
||||||
|
$scope.formatTime = playerTimeService.formatTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses playback and decouples the position slider from current
|
* Pauses playback and decouples the position slider from current
|
||||||
* playback position, allowing the user to manipulate the slider
|
* playback position, allowing the user to manipulate the slider
|
||||||
@@ -242,32 +235,53 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
|||||||
// If a recording is present and there is an active seek request,
|
// If a recording is present and there is an active seek request,
|
||||||
// restore the playback state at the time that request began and
|
// restore the playback state at the time that request began and
|
||||||
// begin seeking to the requested position
|
// begin seeking to the requested position
|
||||||
if ($scope.recording && pendingSeekRequest) {
|
if ($scope.recording && pendingSeekRequest)
|
||||||
|
$scope.seekToPlaybackPosition();
|
||||||
$scope.seekPosition = null;
|
|
||||||
$scope.operationMessage = 'PLAYER.INFO_SEEK_IN_PROGRESS';
|
|
||||||
$scope.operationProgress = 0;
|
|
||||||
|
|
||||||
// Cancel seek when requested, updating playback position if
|
|
||||||
// that position changed
|
|
||||||
$scope.cancelOperation = function abortSeek() {
|
|
||||||
$scope.recording.cancel();
|
|
||||||
$scope.playbackPosition = $scope.seekPosition || $scope.playbackPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
resumeAfterSeekRequest && $scope.recording.play();
|
|
||||||
$scope.recording.seek($scope.playbackPosition, function seekComplete() {
|
|
||||||
$scope.operationMessage = null;
|
|
||||||
$scope.$evalAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flag seek request as completed
|
// Flag seek request as completed
|
||||||
pendingSeekRequest = false;
|
pendingSeekRequest = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the recording to the specified position within the recording,
|
||||||
|
* in milliseconds.
|
||||||
|
*
|
||||||
|
* @param {Number} timestamp
|
||||||
|
* The position to seek to within the current record,
|
||||||
|
* in milliseconds.
|
||||||
|
*/
|
||||||
|
$scope.seekToTimestamp = function seekToTimestamp(timestamp) {
|
||||||
|
|
||||||
|
// Set the timestamp and seek to it
|
||||||
|
$scope.playbackPosition = timestamp;
|
||||||
|
$scope.seekToPlaybackPosition();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the recording to the current playback position value.
|
||||||
|
*/
|
||||||
|
$scope.seekToPlaybackPosition = function seekToPlaybackPosition() {
|
||||||
|
|
||||||
|
$scope.seekPosition = null;
|
||||||
|
$scope.operationMessage = 'PLAYER.INFO_SEEK_IN_PROGRESS';
|
||||||
|
$scope.operationProgress = 0;
|
||||||
|
|
||||||
|
// Cancel seek when requested, updating playback position if
|
||||||
|
// that position changed
|
||||||
|
$scope.cancelOperation = function abortSeek() {
|
||||||
|
$scope.recording.cancel();
|
||||||
|
$scope.playbackPosition = $scope.seekPosition || $scope.playbackPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
resumeAfterSeekRequest && $scope.recording.play();
|
||||||
|
$scope.recording.seek($scope.playbackPosition, function seekComplete() {
|
||||||
|
$scope.operationMessage = null;
|
||||||
|
$scope.$evalAsync();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the current playback state. If playback is currently paused,
|
* Toggles the current playback state. If playback is currently paused,
|
||||||
* playback is resumed. If playback is currently active, playback is
|
* playback is resumed. If playback is currently active, playback is
|
||||||
@@ -342,6 +356,11 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
|||||||
$scope.$evalAsync();
|
$scope.$evalAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Append any extracted batches of typed text
|
||||||
|
$scope.recording.ontext = function appendTextBatch(text, timestamp) {
|
||||||
|
$scope.textBatches.push({text, timestamp});
|
||||||
|
}
|
||||||
|
|
||||||
// Notify listeners when current position within the recording
|
// Notify listeners when current position within the recording
|
||||||
// has changed
|
// has changed
|
||||||
$scope.recording.onseek = function positionChanged(position, current, total) {
|
$scope.recording.onseek = function positionChanged(position, current, total) {
|
||||||
|
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fuzzysort = require('fuzzysort')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive which plays back session recordings.
|
||||||
|
*/
|
||||||
|
angular.module('player').directive('guacPlayerTextView',
|
||||||
|
['$injector', function guacPlayer($injector) {
|
||||||
|
|
||||||
|
// Required types
|
||||||
|
const TextBatch = $injector.get('TextBatch');
|
||||||
|
|
||||||
|
// Required services
|
||||||
|
const playerTimeService = $injector.get('playerTimeService');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
restrict : 'E',
|
||||||
|
templateUrl : 'app/player/templates/textView.html'
|
||||||
|
};
|
||||||
|
|
||||||
|
config.scope = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the batches of text extracted from this recording.
|
||||||
|
*
|
||||||
|
* @type {!TextBatch[]}
|
||||||
|
*/
|
||||||
|
textBatches : '=',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that accepts a timestamp, and seeks the recording to
|
||||||
|
* that provided timestamp.
|
||||||
|
*
|
||||||
|
* @type {!Function}
|
||||||
|
*/
|
||||||
|
seek: '&',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current position within the recording.
|
||||||
|
*
|
||||||
|
* @type {!Number}
|
||||||
|
*/
|
||||||
|
currentPosition: '='
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config.controller = ['$scope', '$element', '$injector',
|
||||||
|
function guacPlayerController($scope, $element) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The phrase to search within the text batches in order to produce the
|
||||||
|
* filtered list for display.
|
||||||
|
*
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
$scope.searchPhrase = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text batches that match the current search phrase, or all
|
||||||
|
* batches if no search phrase is set.
|
||||||
|
*
|
||||||
|
* @type {!TextBatch[]}
|
||||||
|
*/
|
||||||
|
$scope.filteredBatches = $scope.textBatches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the key log viewer should be full-screen. False by
|
||||||
|
* default unless explicitly enabled by user interaction.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
$scope.fullscreenKeyLog = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle whether the key log viewer should take up the whole screen.
|
||||||
|
*/
|
||||||
|
$scope.toggleKeyLogFullscreen = function toggleKeyLogFullscreen() {
|
||||||
|
$element.toggleClass("fullscreen");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the provided text batches using the provided search phrase to
|
||||||
|
* generate the list of filtered batches, or set to all provided
|
||||||
|
* batches if no search phrase is provided.
|
||||||
|
*
|
||||||
|
* @param {String} searchPhrase
|
||||||
|
* The phrase to search the text batches for. If no phrase is
|
||||||
|
* provided, the list of batches will not be filtered.
|
||||||
|
*/
|
||||||
|
const applyFilter = searchPhrase => {
|
||||||
|
|
||||||
|
// If there's search phrase entered, search the text within the
|
||||||
|
// batches for it
|
||||||
|
if (searchPhrase)
|
||||||
|
$scope.filteredBatches = fuzzysort.go(
|
||||||
|
searchPhrase, $scope.textBatches, {key: 'text'})
|
||||||
|
.map(result => result.obj);
|
||||||
|
|
||||||
|
// Otherwise, do not filter the batches
|
||||||
|
else
|
||||||
|
$scope.filteredBatches = $scope.textBatches;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reapply the filter to the updated text batches
|
||||||
|
$scope.$watch('textBatches', applyFilter);
|
||||||
|
|
||||||
|
// Reapply the filter whenever the search phrase is updated
|
||||||
|
$scope.$watch('searchPhrase', applyFilter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @borrows playerTimeService.formatTime
|
||||||
|
*/
|
||||||
|
$scope.formatTime = playerTimeService.formatTime;
|
||||||
|
|
||||||
|
}];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}]);
|
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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 formatting time, specifically for the recording player.
|
||||||
|
*/
|
||||||
|
angular.module('player').factory('playerTimeService',
|
||||||
|
['$injector', function playerTimeService($injector) {
|
||||||
|
|
||||||
|
const service = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the given number as a decimal string, adding leading zeroes
|
||||||
|
* such that the string contains at least two digits. The given number
|
||||||
|
* MUST NOT be negative.
|
||||||
|
*
|
||||||
|
* @param {!number} value
|
||||||
|
* The number to format.
|
||||||
|
*
|
||||||
|
* @returns {!string}
|
||||||
|
* The decimal string representation of the given value, padded
|
||||||
|
* with leading zeroes up to a minimum length of two digits.
|
||||||
|
*/
|
||||||
|
const zeroPad = function zeroPad(value) {
|
||||||
|
return value > 9 ? value : '0' + value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the given quantity of milliseconds as days, hours, minutes,
|
||||||
|
* and whole seconds, separated by colons (DD:HH:MM:SS). Hours are
|
||||||
|
* included only if the quantity is at least one hour, and days are
|
||||||
|
* included only if the quantity is at least one day. All included
|
||||||
|
* groups are zero-padded to two digits with the exception of the
|
||||||
|
* left-most group.
|
||||||
|
*
|
||||||
|
* @param {!number} value
|
||||||
|
* The time to format, in milliseconds.
|
||||||
|
*
|
||||||
|
* @returns {!string}
|
||||||
|
* The given quantity of milliseconds formatted as "DD:HH:MM:SS".
|
||||||
|
*/
|
||||||
|
service.formatTime = function formatTime(value) {
|
||||||
|
|
||||||
|
// Round provided value down to whole seconds
|
||||||
|
value = Math.floor((value || 0) / 1000);
|
||||||
|
|
||||||
|
// Separate seconds into logical groups of seconds, minutes,
|
||||||
|
// hours, etc.
|
||||||
|
var groups = [ 1, 24, 60, 60 ];
|
||||||
|
for (var i = groups.length - 1; i >= 0; i--) {
|
||||||
|
var placeValue = groups[i];
|
||||||
|
groups[i] = zeroPad(value % placeValue);
|
||||||
|
value = Math.floor(value / placeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format groups separated by colons, stripping leading zeroes and
|
||||||
|
// groups which are entirely zeroes, leaving at least minutes and
|
||||||
|
// seconds
|
||||||
|
var formatted = groups.join(':');
|
||||||
|
return /^[0:]*([0-9]{1,2}(?::[0-9]{2})+)$/.exec(formatted)[1];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
|
||||||
|
}]);
|
@@ -50,7 +50,6 @@ guac-player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guac-player .guac-player-display {
|
guac-player .guac-player-display {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -60,6 +59,7 @@ guac-player .guac-player-display {
|
|||||||
guac-player .guac-player-controls {
|
guac-player .guac-player-controls {
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
padding-bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -107,6 +107,18 @@ guac-player .guac-player-controls {
|
|||||||
background-image: url('images/action-icons/guac-pause.svg');
|
background-image: url('images/action-icons/guac-pause.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guac-player-controls .guac-player-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-controls .guac-player-keys {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
guac-player .guac-player-status {
|
guac-player .guac-player-status {
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -145,3 +157,50 @@ guac-player .guac-player-status {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guac-player-container {
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -moz-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
-ms-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
-ms-flex-pack: space-between;
|
||||||
|
-webkit-box-pack: justify;
|
||||||
|
-webkit-justify-content: space-between;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
guac-player-display {
|
||||||
|
|
||||||
|
flex-grow: 5;
|
||||||
|
|
||||||
|
/* Required for horizontal resizing to work */
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
guac-player-text-view {
|
||||||
|
|
||||||
|
min-width: 25em;
|
||||||
|
flex-basis: 0;
|
||||||
|
|
||||||
|
/* Make room for the control bar at the bottom */
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
guac-player-text-view.fullscreen {
|
||||||
|
|
||||||
|
min-width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
115
guacamole/src/main/frontend/src/app/player/styles/textView.css
Normal file
115
guacamole/src/main/frontend/src/app/player/styles/textView.css
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE: This session recording player implementation is based on the Session
|
||||||
|
* Recording Player for Glyptodon Enterprise which is available at
|
||||||
|
* https://github.com/glyptodon/glyptodon-enterprise-player under the
|
||||||
|
* following license:
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Glyptodon, Inc.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.text-batches {
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-batches .text-batch {
|
||||||
|
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-batches .text-batch .timestamp {
|
||||||
|
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: blue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-text-container {
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-text-container .text-controls {
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-text-container .text-controls .filter {
|
||||||
|
|
||||||
|
flex-grow: 5;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-text-container .text-controls .fullscreen-button {
|
||||||
|
|
||||||
|
background-image: url('images/fullscreen.svg');
|
||||||
|
background-size: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-text-container .result-count {
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.5em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.guac-player-text-container .filter {
|
||||||
|
|
||||||
|
margin: 0.25em;
|
||||||
|
|
||||||
|
}
|
@@ -1,9 +1,25 @@
|
|||||||
<!-- Actual playback display -->
|
<div class="guac-player-container">
|
||||||
<guac-player-display display="recording.getDisplay()"
|
|
||||||
ng-click="togglePlayback()"></guac-player-display>
|
<!-- Actual playback display -->
|
||||||
|
<guac-player-display display="recording.getDisplay()"
|
||||||
|
ng-click="togglePlayback()"></guac-player-display>
|
||||||
|
|
||||||
|
<!-- Show the text viewer only if text exists -->
|
||||||
|
<guac-player-text-view ng-show="showKeyLog"
|
||||||
|
text-batches="textBatches"
|
||||||
|
seek="seekToTimestamp(timestamp)"
|
||||||
|
current-position="seekPosition || playbackPosition"
|
||||||
|
duration="recording.getDuration()"
|
||||||
|
></guac-player-text-view>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Player controls -->
|
<!-- Player controls -->
|
||||||
<div class="guac-player-controls" ng-show="recording">
|
<div class="guac-player-controls" ng-show="recording"
|
||||||
|
ng-class="{
|
||||||
|
'paused' : !recording.isPlaying(),
|
||||||
|
'playing' : recording.isPlaying()
|
||||||
|
}">
|
||||||
|
|
||||||
<!-- Playback position slider -->
|
<!-- Playback position slider -->
|
||||||
<input class="guac-player-seek" type="range" min="0" step="1"
|
<input class="guac-player-seek" type="range" min="0" step="1"
|
||||||
@@ -12,22 +28,33 @@
|
|||||||
ng-model="playbackPosition"
|
ng-model="playbackPosition"
|
||||||
ng-on-change="commitSeekRequest()">
|
ng-on-change="commitSeekRequest()">
|
||||||
|
|
||||||
<!-- Play button -->
|
<div class="guac-player-buttons">
|
||||||
<button class="guac-player-play"
|
|
||||||
ng-attr-title="{{ 'PLAYER.ACTION_PLAY' | translate }}"
|
|
||||||
ng-click="recording.play()"
|
|
||||||
ng-hide="recording.isPlaying()"><span class="play-icon"></span></button>
|
|
||||||
|
|
||||||
<!-- Pause button -->
|
<!-- Play button -->
|
||||||
<button class="guac-player-pause"
|
<button class="guac-player-play"
|
||||||
ng-attr-title="{{ 'PLAYER.ACTION_PAUSE' | translate }}"
|
ng-attr-title="{{ 'PLAYER.ACTION_PLAY' | translate }}"
|
||||||
ng-click="recording.pause()"
|
ng-click="recording.play()"
|
||||||
ng-show="recording.isPlaying()"><span class="pause-icon"></span></button>
|
ng-hide="recording.isPlaying()"><span class="play-icon"></span></button>
|
||||||
|
|
||||||
<!-- Playback position and duration -->
|
<!-- Pause button -->
|
||||||
<span class="guac-player-position">
|
<button class="guac-player-pause"
|
||||||
{{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }}
|
ng-attr-title="{{ 'PLAYER.ACTION_PAUSE' | translate }}"
|
||||||
</span>
|
ng-click="recording.pause()"
|
||||||
|
ng-show="recording.isPlaying()"><span class="pause-icon"></span></button>
|
||||||
|
|
||||||
|
<!-- Playback position and duration -->
|
||||||
|
<span class="guac-player-position">
|
||||||
|
{{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ng-show="hasTextBatches()" class="guac-player-keys" ng-click="toggleKeyLogView()">
|
||||||
|
{{ 'PLAYER.ACTION_SHOW_KEY_LOG' | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ng-show="!hasTextBatches()" class="guac-player-keys disabled">
|
||||||
|
{{ 'PLAYER.INFO_NO_KEY_LOG' | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
<div class="guac-player-text-container">
|
||||||
|
|
||||||
|
<div class="text-controls">
|
||||||
|
<div class="filter">
|
||||||
|
<input class="search-string"
|
||||||
|
placeholder="{{'PLAYER.FIELD_PLACEHOLDER_TEXT_BATCH_FILTER' | translate}}"
|
||||||
|
type="text" ng-model="searchPhrase">
|
||||||
|
</div>
|
||||||
|
<span class="fullscreen-button" ng-click="toggleKeyLogFullscreen()"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-count"
|
||||||
|
ng-show="searchPhrase.length"
|
||||||
|
translate="PLAYER.INFO_NUMBER_OF_RESULTS"
|
||||||
|
translate-values="{RESULTS: filteredBatches.length}"></div>
|
||||||
|
|
||||||
|
<div class="text-batches">
|
||||||
|
<div ng-repeat="batch in filteredBatches" class="text-batch" ng-click="seek({timestamp: batch.timestamp})">
|
||||||
|
<div class="timestamp">{{ formatTime(batch.timestamp) }}</div>
|
||||||
|
<div class="text">{{ batch.text }}</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service which defines the TextBatch class.
|
||||||
|
*/
|
||||||
|
angular.module('player').factory('TextBatch', [function defineTextBatch() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A batch of text associated with a recording. The batch consists of a
|
||||||
|
* string representation of the text that would be typed based on the key
|
||||||
|
* events in the recording, as well as a timestamp when the batch started.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {TextBatch|Object} [template={}]
|
||||||
|
* The object whose properties should be copied within the new TextBatch.
|
||||||
|
*/
|
||||||
|
const TextBatch = function TextBatch(template) {
|
||||||
|
|
||||||
|
// Use empty object by default
|
||||||
|
template = template || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text that was typed in this batch.
|
||||||
|
*
|
||||||
|
* @type String
|
||||||
|
*/
|
||||||
|
this.text = template.text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp at which the batch of text was typed.
|
||||||
|
*
|
||||||
|
* @type Number
|
||||||
|
*/
|
||||||
|
this.timestamp = template.timestamp;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return TextBatch;
|
||||||
|
|
||||||
|
}]);
|
@@ -96,7 +96,7 @@
|
|||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings.connectionHistoryPlayer.playing .guac-player-controls {
|
.settings.connectionHistoryPlayer .guac-player-controls.playing {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
-webkit-transition: opacity 0.25s linear 0.25s;
|
-webkit-transition: opacity 0.25s linear 0.25s;
|
||||||
-moz-transition: opacity 0.25s linear 0.25s;
|
-moz-transition: opacity 0.25s linear 0.25s;
|
||||||
@@ -104,8 +104,8 @@
|
|||||||
transition: opacity 0.25s linear 0.25s;
|
transition: opacity 0.25s linear 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings.connectionHistoryPlayer.paused .guac-player-controls,
|
.settings.connectionHistoryPlayer .guac-player-controls.paused,
|
||||||
.settings.connectionHistoryPlayer.playing:hover .guac-player-controls {
|
.settings.connectionHistoryPlayer .guac-player-controls.playing:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
-webkit-transition-delay: 0s;
|
-webkit-transition-delay: 0s;
|
||||||
-moz-transition-delay: 0s;
|
-moz-transition-delay: 0s;
|
||||||
|
@@ -1,11 +1,6 @@
|
|||||||
<guac-viewport class="settings view connectionHistoryPlayer"
|
<guac-viewport class="settings view connectionHistoryPlayer">
|
||||||
ng-class="{
|
|
||||||
'no-recording' : !selectedRecording,
|
|
||||||
'paused' : !playing,
|
|
||||||
'playing' : playing
|
|
||||||
}">
|
|
||||||
|
|
||||||
<!-- Player for selected recording -->
|
<!-- Player for selected recording -->
|
||||||
<guac-player src="tunnel"></guac-player>
|
<guac-player src="tunnel"></guac-player>
|
||||||
|
|
||||||
</guac-viewport>
|
</guac-viewport>
|
||||||
|
1
guacamole/src/main/frontend/src/images/fullscreen.svg
Normal file
1
guacamole/src/main/frontend/src/images/fullscreen.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="540" height="540" viewBox="0 0 142.875 142.875"><path d="M0 0v160h40V40h120V0H0zm380 0v40h120v120h40V0H380zM0 380v160h160v-40H40V380H0zm500 0v120H380v40h160V380h-40z" style="fill:#000;stroke-width:.999999" transform="scale(.26458)"/><path d="M84.667 68.792h58.208v148.167H84.667z" style="fill:none;stroke-width:.264583" transform="translate(-42.333 -68.792)"/><path d="M52.917 79.375h121.708v121.708H52.917z" style="fill:none;stroke-width:.264583" transform="translate(-42.333 -68.792)"/></svg>
|
After Width: | Height: | Size: 541 B |
@@ -478,12 +478,17 @@
|
|||||||
|
|
||||||
"PLAYER" : {
|
"PLAYER" : {
|
||||||
|
|
||||||
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
|
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
|
||||||
"ACTION_PAUSE" : "@:APP.ACTION_PAUSE",
|
"ACTION_PAUSE" : "@:APP.ACTION_PAUSE",
|
||||||
"ACTION_PLAY" : "@:APP.ACTION_PLAY",
|
"ACTION_PLAY" : "@:APP.ACTION_PLAY",
|
||||||
|
"ACTION_SHOW_KEY_LOG" : "Keystroke Log",
|
||||||
|
|
||||||
"INFO_LOADING_RECORDING" : "Your recording is now being loaded. Please wait...",
|
"INFO_LOADING_RECORDING" : "Your recording is now being loaded. Please wait...",
|
||||||
"INFO_SEEK_IN_PROGRESS" : "Seeking to the requested position. Please wait..."
|
"INFO_NO_KEY_LOG" : "Keystroke Log Unavailable",
|
||||||
|
"INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{Match} other{Matches}}",
|
||||||
|
"INFO_SEEK_IN_PROGRESS" : "Seeking to the requested position. Please wait...",
|
||||||
|
|
||||||
|
"FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user