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:
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",
|
||||
"datalist-polyfill": "^1.25.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"jquery": "^3.6.4",
|
||||
"jstz": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -5632,6 +5633,11 @@
|
||||
"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": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -15703,6 +15709,11 @@
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"fuzzysort": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.4.tgz",
|
||||
"integrity": "sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw=="
|
||||
},
|
||||
"gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
@@ -15,6 +15,7 @@
|
||||
"csv": "^6.2.5",
|
||||
"datalist-polyfill": "^1.25.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"jquery": "^3.6.4",
|
||||
"jstz": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
|
@@ -77,6 +77,12 @@
|
||||
*/
|
||||
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 = {
|
||||
restrict : 'E',
|
||||
templateUrl : 'app/player/templates/player.html'
|
||||
@@ -142,6 +148,21 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
||||
*/
|
||||
$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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Return true if any batches of key event logs are available for this
|
||||
* recording, or false otherwise.
|
||||
*
|
||||
* @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.
|
||||
* @return
|
||||
* True if any batches of key event logs are avaiable for this
|
||||
* recording, or false otherwise.
|
||||
*/
|
||||
const zeroPad = function zeroPad(value) {
|
||||
return value > 9 ? value : '0' + value;
|
||||
$scope.hasTextBatches = function hasTextBatches () {
|
||||
return $scope.textBatches.length >= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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".
|
||||
* Toggle the visibility of the text key log viewer.
|
||||
*/
|
||||
$scope.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];
|
||||
|
||||
$scope.toggleKeyLogView = function toggleKeyLogView() {
|
||||
$scope.showKeyLog = !$scope.showKeyLog;
|
||||
};
|
||||
|
||||
/**
|
||||
* @borrows playerTimeService.formatTime
|
||||
*/
|
||||
$scope.formatTime = playerTimeService.formatTime;
|
||||
|
||||
/**
|
||||
* Pauses playback and decouples the position slider from current
|
||||
* 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,
|
||||
// restore the playback state at the time that request began and
|
||||
// begin seeking to the requested position
|
||||
if ($scope.recording && pendingSeekRequest) {
|
||||
|
||||
$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();
|
||||
});
|
||||
|
||||
}
|
||||
if ($scope.recording && pendingSeekRequest)
|
||||
$scope.seekToPlaybackPosition();
|
||||
|
||||
// Flag seek request as completed
|
||||
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,
|
||||
* playback is resumed. If playback is currently active, playback is
|
||||
@@ -342,6 +356,11 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
||||
$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
|
||||
// has changed
|
||||
$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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -60,6 +59,7 @@ guac-player .guac-player-display {
|
||||
guac-player .guac-player-controls {
|
||||
|
||||
position: absolute;
|
||||
padding-bottom: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
@@ -107,6 +107,18 @@ guac-player .guac-player-controls {
|
||||
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 {
|
||||
|
||||
position: fixed;
|
||||
@@ -145,3 +157,50 @@ guac-player .guac-player-status {
|
||||
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 -->
|
||||
<guac-player-display display="recording.getDisplay()"
|
||||
ng-click="togglePlayback()"></guac-player-display>
|
||||
<div class="guac-player-container">
|
||||
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<input class="guac-player-seek" type="range" min="0" step="1"
|
||||
@@ -12,22 +28,33 @@
|
||||
ng-model="playbackPosition"
|
||||
ng-on-change="commitSeekRequest()">
|
||||
|
||||
<!-- Play button -->
|
||||
<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>
|
||||
<div class="guac-player-buttons">
|
||||
|
||||
<!-- Pause button -->
|
||||
<button class="guac-player-pause"
|
||||
ng-attr-title="{{ 'PLAYER.ACTION_PAUSE' | translate }}"
|
||||
ng-click="recording.pause()"
|
||||
ng-show="recording.isPlaying()"><span class="pause-icon"></span></button>
|
||||
<!-- Play button -->
|
||||
<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>
|
||||
|
||||
<!-- Playback position and duration -->
|
||||
<span class="guac-player-position">
|
||||
{{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }}
|
||||
</span>
|
||||
<!-- Pause button -->
|
||||
<button class="guac-player-pause"
|
||||
ng-attr-title="{{ 'PLAYER.ACTION_PAUSE' | translate }}"
|
||||
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>
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
.settings.connectionHistoryPlayer.playing .guac-player-controls {
|
||||
.settings.connectionHistoryPlayer .guac-player-controls.playing {
|
||||
opacity: 0;
|
||||
-webkit-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;
|
||||
}
|
||||
|
||||
.settings.connectionHistoryPlayer.paused .guac-player-controls,
|
||||
.settings.connectionHistoryPlayer.playing:hover .guac-player-controls {
|
||||
.settings.connectionHistoryPlayer .guac-player-controls.paused,
|
||||
.settings.connectionHistoryPlayer .guac-player-controls.playing:hover {
|
||||
opacity: 1;
|
||||
-webkit-transition-delay: 0s;
|
||||
-moz-transition-delay: 0s;
|
||||
|
@@ -1,11 +1,6 @@
|
||||
<guac-viewport class="settings view connectionHistoryPlayer"
|
||||
ng-class="{
|
||||
'no-recording' : !selectedRecording,
|
||||
'paused' : !playing,
|
||||
'playing' : playing
|
||||
}">
|
||||
<guac-viewport class="settings view connectionHistoryPlayer">
|
||||
|
||||
<!-- Player for selected recording -->
|
||||
<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" : {
|
||||
|
||||
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
|
||||
"ACTION_PAUSE" : "@:APP.ACTION_PAUSE",
|
||||
"ACTION_PLAY" : "@:APP.ACTION_PLAY",
|
||||
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
|
||||
"ACTION_PAUSE" : "@:APP.ACTION_PAUSE",
|
||||
"ACTION_PLAY" : "@:APP.ACTION_PLAY",
|
||||
"ACTION_SHOW_KEY_LOG" : "Keystroke Log",
|
||||
|
||||
"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