GUACAMOLE-1820: Extract and display richer information about key events.

This commit is contained in:
James Muehlner
2023-06-26 18:22:38 +00:00
parent 2a2cef9189
commit 55f1fc2aaa
8 changed files with 244 additions and 183 deletions

View File

@@ -22,18 +22,24 @@ var Guacamole = Guacamole || {};
/** /**
* An object that will accept raw key events and produce human readable text * An object that will accept raw key events and produce human readable text
* batches, seperated by at least `batchSeperation` milliseconds, which can be * batches, seperated by at least `batchSeperation` milliseconds, which can be
* retrieved through the onBatch callback or by calling getCurrentBatch(). * retrieved through the onbatch callback or by calling getCurrentBatch().
* *
* NOTE: The event processing logic and output format is based on the `guaclog` * NOTE: The event processing logic and output format is based on the `guaclog`
* tool, with the addition of batching support. * tool, with the addition of batching support.
* *
* @constructor * @constructor
*
* @param {number} [batchSeperation=5000] * @param {number} [batchSeperation=5000]
* The minimum number of milliseconds that must elapse between subsequent * The minimum number of milliseconds that must elapse between subsequent
* batches of key-event-generated text. If 0 or negative, no splitting will * batches of key-event-generated text. If 0 or negative, no splitting will
* occur, resulting in a single batch for all provided key events. * occur, resulting in a single batch for all provided key events.
*
* @param {number} [startTimestamp=0]
* The starting timestamp for the recording being intepreted. If provided,
* the timestamp of each intepreted event will be relative to this timestamp.
* If not provided, the raw recording timestamp will be used.
*/ */
Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) { Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, startTimestamp) {
/** /**
* Reference to this Guacamole.KeyEventInterpreter. * Reference to this Guacamole.KeyEventInterpreter.
@@ -47,6 +53,10 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
if (batchSeperation === undefined || batchSeperation === null) if (batchSeperation === undefined || batchSeperation === null)
batchSeperation = 5000; batchSeperation = 5000;
// Default to 0 seconds to keep the raw timestamps
if (startTimestamp === undefined || startTimestamp === null)
startTimestamp = 0;
/** /**
* A definition for a known key. * A definition for a known key.
* *
@@ -212,21 +222,14 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
var pressedKeys = {}; var pressedKeys = {};
/** /**
* A human-readable representation of all keys pressed since the last keyframe. * The current key event batch, containing a representation of all key
* events processed since the end of the last batch passed to onbatch.
* Null if no key events have been processed yet.
* *
* @private * @private
* @type {String} * @type {!KeyEventBatch}
*/ */
var currentTypedValue = ''; var currentBatch = null;
/**
* 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. * The timestamp of the most recent key event processed.
@@ -265,9 +268,9 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
* @param {Number} keysym * @param {Number} keysym
* The keysym to produce a UTF-8 KeyDefinition for, if valid. * The keysym to produce a UTF-8 KeyDefinition for, if valid.
* *
* @returns * @returns {KeyDefinition}
* Return a KeyDefinition for the provided keysym, if it it's a valid * A KeyDefinition for the provided keysym, if it's a valid UTF-8
* UTF-8 keysym, or null otherwise. * keysym, or null otherwise.
*/ */
function getUnicodeKeyDefinition(keysym) { function getUnicodeKeyDefinition(keysym) {
@@ -279,7 +282,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
var mask; var mask;
var bytes; var bytes;
/* Determine size and initial byte mask */ // Determine size and initial byte mask
if (codepoint <= 0x007F) { if (codepoint <= 0x007F) {
mask = 0x00; mask = 0x00;
bytes = 1; bytes = 1;
@@ -309,7 +312,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
var name = new TextDecoder("utf-8").decode(byteArray); var name = new TextDecoder("utf-8").decode(byteArray);
// Create and return the definition // Create and return the definition
return new KeyDefinition({keysym: keysym.toString(), name: name, value: name, modifier: false}); return new KeyDefinition({keysym: keysym, name: name, value: name, modifier: false});
} }
@@ -320,7 +323,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
* @param {Number} keysym * @param {Number} keysym
* The keysym to return a KeyDefinition for. * The keysym to return a KeyDefinition for.
* *
* @returns * @returns {KeyDefinition}
* A KeyDefinition corresponding to the provided keysym. * A KeyDefinition corresponding to the provided keysym.
*/ */
function getKeyDefinitionByKeysym(keysym) { function getKeyDefinitionByKeysym(keysym) {
@@ -350,24 +353,19 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
* previous key event. * previous key event.
* *
* @event * @event
* @param {!String} text * @param {!Guacamole.KeyEventInterpreter.KeyEventBatch}
* 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; this.onbatch = null;
/** /**
* Handles a raw key event, potentially appending typed text to the * Handles a raw key event, potentially appending typed text to the
* current batch, and calling onBatch with the current batch, if the * current batch, and calling onbatch with the current batch, if the
* callback is set and a new batch is about to be started. * callback is set and a new batch is about to be started.
* *
* @param {!string[]} args * @param {!string[]} args
* The arguments of the key event. * The arguments of the key event.
*/ */
interpreter.handleKeyEvent = function handleKeyEvent(args) { this.handleKeyEvent = function handleKeyEvent(args) {
// The X11 keysym // The X11 keysym
var keysym = parseInt(args[0]); var keysym = parseInt(args[0]);
@@ -379,10 +377,8 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
var timestamp = parseInt(args[2]); var timestamp = parseInt(args[2]);
// If no current batch exists, start a new one now // If no current batch exists, start a new one now
if (!lastTextTimestamp) { if (!currentBatch)
lastTextTimestamp = timestamp; currentBatch = new Guacamole.KeyEventInterpreter.KeyEventBatch();
lastKeyEvent = timestamp;
}
// Only switch to a new batch of text if sufficient time has passed // Only switch to a new batch of text if sufficient time has passed
// since the last key event // since the last key event
@@ -394,12 +390,11 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
// Call the handler with the current batch of text and the timestamp // Call the handler with the current batch of text and the timestamp
// at which the current batch started // at which the current batch started
if (currentTypedValue && interpreter.onBatch) if (currentBatch.events.length && interpreter.onbatch)
interpreter.onBatch(currentTypedValue, lastTextTimestamp); interpreter.onbatch(currentBatch);
// Move on to the next batch of text // Move on to the next batch of text
currentTypedValue = ''; currentBatch = new Guacamole.KeyEventInterpreter.KeyEventBatch();
lastTextTimestamp = 0;
} }
@@ -417,9 +412,11 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
// (non-modifier) key is pressed // (non-modifier) key is pressed
else if (pressed) { else if (pressed) {
var relativeTimestap = timestamp - startTimestamp;
if (isShortcut()) { if (isShortcut()) {
currentTypedValue += '<'; var shortcutText = '<';
var firstKey = true; var firstKey = true;
@@ -431,30 +428,56 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
// Print name of key // Print name of key
if (firstKey) { if (firstKey) {
currentTypedValue += pressedKeyDefinition.name; shortcutText += pressedKeyDefinition.name;
firstKey = false; firstKey = false;
} }
else else
currentTypedValue += ('+' + pressedKeyDefinition.name); shortcutText += ('+' + pressedKeyDefinition.name);
} }
// Finally, append the printable key to close the shortcut // Finally, append the printable key to close the shortcut
currentTypedValue += ('+' + keyDefinition.name + '>') shortcutText += ('+' + keyDefinition.name + '>')
// Add the shortcut to the current batch
currentBatch.simpleValue += shortcutText;
currentBatch.events.push(new Guacamole.KeyEventInterpreter.KeyEvent(
shortcutText, false, relativeTimestap));
} }
// Print the key itself // Print the key itself
else { else {
var keyText;
var typed;
// Print the value if explicitly defined // Print the value if explicitly defined
if (keyDefinition.value != null) if (keyDefinition.value != null) {
currentTypedValue += keyDefinition.value;
keyText = keyDefinition.value;
typed = true;
}
// Otherwise print the name // Otherwise print the name
else else {
currentTypedValue += ('<' + keyDefinition.name + '>');
keyText = ('<' + keyDefinition.name + '>');
// While this is a representation for a single character,
// the key text is the name of the key, not the actual
// character itself
typed = false;
}
// Add the key to the current batch
currentBatch.simpleValue += keyText;
currentBatch.events.push(new Guacamole.KeyEventInterpreter.KeyEvent(
keyText, typed, relativeTimestap));
} }
} }
@@ -466,22 +489,90 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation) {
* incomplete, as more key events might be processed before the next * incomplete, as more key events might be processed before the next
* batch starts. * batch starts.
* *
* @returns * @returns {Guacamole.KeyEventInterpreter.KeyEventBatch}
* The current batch of text. * The current batch of text.
*/ */
interpreter.getCurrentText = function getCurrentText() { this.getCurrentBatch = function getCurrentBatch() {
return currentTypedValue; return currentBatch;
} }
}
/**
* A granular description of an extracted key event, including a human-readable
* text representation of the event, whether the event is directly typed or not,
* and the timestamp when the event occured.
*
* @constructor
* @param {!String} text
* A human-readable representation of the event.
*
* @param {!boolean} typed
* True if this event represents a directly-typed character, or false
* otherwise.
*
* @param {!Number} timestamp
* The timestamp from the recording when this event occured.
*/
Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(text, typed, timestamp) {
/** /**
* Return the recording timestamp associated with the start of the * A human-readable representation of the event. If a printable character
* current batch of typed text. * was directly typed, this will just be that character. Otherwise it will
* be a string describing the event.
* *
* @returns * @type {!String}
* The recording timestamp at which the current batch started.
*/ */
interpreter.getCurrentTimestamp = function getCurrentTimestamp() { this.text = text;
return lastTextTimestamp;
}
/**
* True if this text of this event is exactly a typed character, or false
* otherwise.
*
* @type {!boolean}
*/
this.typed = typed;
/**
* The timestamp from the recording when this event occured. If a
* `startTimestamp` value was provided to the interpreter constructor, this
* will be relative to start of the recording. If not, it will be the raw
* timestamp from the key event.
*
* @type {!Number}
*/
this.timestamp = timestamp;
};
/**
* A series of intepreted key events, seperated by at least the configured
* batchSeperation value from any other key events in the recording corresponding
* to the interpreted key events. A batch will always consist of at least one key
* event, and an associated simplified representation of the event(s).
*
* @constructor
* @param {!Guacamole.KeyEventInterpreter.KeyEvent[]} events
* The interpreted key events for this batch.
*
* @param {!String} simpleValue
* The simplified, human-readable value representing the key events for
* this batch.
*/
Guacamole.KeyEventInterpreter.KeyEventBatch = function KeyEventBatch(events, simpleValue) {
/**
* All key events for this batch.
*
* @type {!Guacamole.KeyEventInterpreter.KeyEvent[]}
*/
this.events = events || [];
/**
* The simplified, human-readable value representing the key events for
* this batch, equivalent to concatenating the `text` field of all key
* events in the batch.
*
* @type {!String}
*/
this.simpleValue = simpleValue || '';
} }

View File

@@ -104,16 +104,6 @@ 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.
* *
@@ -387,23 +377,35 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
/** /**
* A key event interpreter to split all key events in this recording into * A key event interpreter to split all key events in this recording into
* human-readable batches of text. * human-readable batches of text. Constrcution is deferred until the first
* event is processed, to enable recording-relative timestamps.
* *
* @type {!Guacamole.KeyEventInterpreter} * @type {!Guacamole.KeyEventInterpreter}
*/ */
var keyEventInterpreter = new Guacamole.KeyEventInterpreter(); var keyEventInterpreter = null;
/**
* Initialize the key interpreter. This function should be called only once
* with the first timestamp in the recording as an argument.
*
* @private
* @param {!number} startTimestamp
* The timestamp of the first frame in the recording, i.e. the start of
* the recording.
*/
function initializeKeyInterpreter(startTimestamp) {
keyEventInterpreter = new Guacamole.KeyEventInterpreter(null, startTimestamp);
// Pass through any received batches to the recording ontext handler // Pass through any received batches to the recording ontext handler
keyEventInterpreter.onBatch = function onBatch(text, timestamp) { keyEventInterpreter.onbatch = function onbatch(batch) {
// Don't call the callback if it was never set // Pass the batch through if a handler is set
if (!recording.ontext) if (recording.ontext)
return; recording.ontext(batch);
// 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
@@ -436,6 +438,11 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
frames.push(frame); frames.push(frame);
frameStart = frameEnd; frameStart = frameEnd;
// If this is the first frame, intialize the key event interpreter
// with the timestamp of the first frame
if (frames.length === 1)
initializeKeyInterpreter(timestamp);
// This frame should eventually become a keyframe if enough data // This frame should eventually become a keyframe if enough data
// has been processed and enough recording time has elapsed, or if // has been processed and enough recording time has elapsed, or if
// this is the absolute first frame // this is the absolute first frame
@@ -443,6 +450,7 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
&& timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) { && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) {
frame.keyframe = true; frame.keyframe = true;
lastKeyframe = frames.length - 1; lastKeyframe = frames.length - 1;
} }
// Notify that additional content is available // Notify that additional content is available
@@ -521,10 +529,9 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
// If there's any typed text that's yet to be sent to the ontext // If there's any typed text that's yet to be sent to the ontext
// handler, send it now // handler, send it now
var text = keyEventInterpreter.getCurrentText(); var batch = keyEventInterpreter.getCurrentBatch();
var timestamp = keyEventInterpreter.getCurrentTimestamp(); if (batch && recording.ontext)
if (text && recording.ontext) recording.ontext(batch);
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)
@@ -916,11 +923,8 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
* is available. * is available.
* *
* @event * @event
* @param {!String} text * @param {!Guacamole.KeyEventInterpreter.KeyEventBatch} batch
* The typed text associated with the batch of text. * The batch of extracted text.
*
* @param {!number} timestamp
* The relative timestamp associated with the batch of text.
*/ */
this.ontext = null; this.ontext = null;

View File

@@ -77,9 +77,6 @@
*/ */
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 // Required services
const playerTimeService = $injector.get('playerTimeService'); const playerTimeService = $injector.get('playerTimeService');
@@ -151,7 +148,7 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
/** /**
* Any batches of text typed during the recording. * Any batches of text typed during the recording.
* *
* @type {TextBatch[]} * @type {Guacamole.KeyEventInterpeter.KeyEventBatch[]}
*/ */
$scope.textBatches = []; $scope.textBatches = [];
@@ -277,6 +274,7 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
resumeAfterSeekRequest && $scope.recording.play(); resumeAfterSeekRequest && $scope.recording.play();
$scope.recording.seek($scope.playbackPosition, function seekComplete() { $scope.recording.seek($scope.playbackPosition, function seekComplete() {
$scope.seekPosition = null;
$scope.operationMessage = null; $scope.operationMessage = null;
$scope.$evalAsync(); $scope.$evalAsync();
}); });
@@ -357,8 +355,8 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
}; };
// Append any extracted batches of typed text // Append any extracted batches of typed text
$scope.recording.ontext = function appendTextBatch(text, timestamp) { $scope.recording.ontext = function appendTextBatch(batch) {
$scope.textBatches.push({text, timestamp}); $scope.textBatches.push(batch);
} }
// Notify listeners when current position within the recording // Notify listeners when current position within the recording

View File

@@ -25,9 +25,6 @@ const fuzzysort = require('fuzzysort')
angular.module('player').directive('guacPlayerTextView', angular.module('player').directive('guacPlayerTextView',
['$injector', function guacPlayer($injector) { ['$injector', function guacPlayer($injector) {
// Required types
const TextBatch = $injector.get('TextBatch');
// Required services // Required services
const playerTimeService = $injector.get('playerTimeService'); const playerTimeService = $injector.get('playerTimeService');
@@ -41,7 +38,7 @@ angular.module('player').directive('guacPlayerTextView',
/** /**
* All the batches of text extracted from this recording. * All the batches of text extracted from this recording.
* *
* @type {!TextBatch[]} * @type {!Guacamole.KeyEventInterpeter.KeyEventBatch[]}
*/ */
textBatches : '=', textBatches : '=',
@@ -77,7 +74,7 @@ angular.module('player').directive('guacPlayerTextView',
* The text batches that match the current search phrase, or all * The text batches that match the current search phrase, or all
* batches if no search phrase is set. * batches if no search phrase is set.
* *
* @type {!TextBatch[]} * @type {!Guacamole.KeyEventInterpeter.KeyEventBatch[]}
*/ */
$scope.filteredBatches = $scope.textBatches; $scope.filteredBatches = $scope.textBatches;
@@ -111,7 +108,7 @@ angular.module('player').directive('guacPlayerTextView',
// batches for it // batches for it
if (searchPhrase) if (searchPhrase)
$scope.filteredBatches = fuzzysort.go( $scope.filteredBatches = fuzzysort.go(
searchPhrase, $scope.textBatches, {key: 'text'}) searchPhrase, $scope.textBatches, {key: 'simpleValue'})
.map(result => result.obj); .map(result => result.obj);
// Otherwise, do not filter the batches // Otherwise, do not filter the batches

View File

@@ -17,6 +17,33 @@
* under the License. * 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.
*/
/** /**
* A service for formatting time, specifically for the recording player. * A service for formatting time, specifically for the recording player.
*/ */

View File

@@ -17,33 +17,6 @@
* under the License. * 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 { .text-batches {
display: flex; display: flex;
@@ -113,3 +86,25 @@
margin: 0.25em; margin: 0.25em;
} }
.text-batches .text {
display: block;
margin: 0;
padding: 0;
max-width: 100%;
overflow-wrap: break-word;
}
.text-batches .text .not-typed {
font-weight: bold;
}
.text-batches .text .future {
color: dimgray;
}

View File

@@ -15,9 +15,15 @@
translate-values="{RESULTS: filteredBatches.length}"></div> translate-values="{RESULTS: filteredBatches.length}"></div>
<div class="text-batches"> <div class="text-batches">
<div ng-repeat="batch in filteredBatches" class="text-batch" ng-click="seek({timestamp: batch.timestamp})"> <div ng-repeat="batch in filteredBatches" class="text-batch" ng-click="seek({timestamp: batch.events[0].timestamp})">
<div class="timestamp">{{ formatTime(batch.timestamp) }}</div> <div class="timestamp">{{ formatTime(batch.events[0].timestamp) }}</div>
<div class="text">{{ batch.text }}</div> <div class="text">
<span
ng-repeat="event in batch.events"
class="key-event"
ng-class="{ 'not-typed' : !event.typed, 'future': event.timestamp >= currentPosition }"
>{{ event.text }}</span>
</div>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,57 +0,0 @@
/*
* 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;
}]);