mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-31 12:28:50 +08:00
Added ability to watch replay
This commit is contained in:
parent
7acd5d9f2b
commit
d4e954069e
9 changed files with 14455 additions and 42 deletions
3644
functions/package-lock.json
generated
3644
functions/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -161,6 +161,7 @@ const refactoredSrc = [
|
|||
"./src/js/test/test-timer.js",
|
||||
"./src/js/test/test-config.js",
|
||||
"./src/js/test/layout-emulator.js",
|
||||
"./src/js/replay.js",
|
||||
];
|
||||
|
||||
//legacy files
|
||||
|
|
|
|||
10519
package-lock.json
generated
10519
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,7 @@ import * as TimerProgress from "./timer-progress";
|
|||
import * as TestTimer from "./test-timer";
|
||||
import * as Focus from "./focus";
|
||||
import * as ShiftTracker from "./shift-tracker";
|
||||
import * as Replay from "./replay.js";
|
||||
|
||||
$("#wordsInput").keypress((event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -130,6 +131,7 @@ function handleBackspace(event) {
|
|||
}
|
||||
}
|
||||
TestLogic.words.decreaseCurrentIndex();
|
||||
Replay.addReplayEvent("backWord");
|
||||
TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex - 1);
|
||||
TestUI.updateActiveElement(true);
|
||||
Funbox.toggleScript(TestLogic.words.getCurrent());
|
||||
|
|
@ -138,6 +140,7 @@ function handleBackspace(event) {
|
|||
} else {
|
||||
if (Config.confidenceMode === "max") return;
|
||||
if (event["ctrlKey"] || event["altKey"]) {
|
||||
Replay.addReplayEvent("clearWord");
|
||||
let limiter = " ";
|
||||
if (
|
||||
TestLogic.input.current.lastIndexOf("-") >
|
||||
|
|
@ -165,6 +168,7 @@ function handleBackspace(event) {
|
|||
TestLogic.input.setCurrent(
|
||||
TestLogic.input.current.substring(0, TestLogic.input.current.length - 1)
|
||||
);
|
||||
Replay.addReplayEvent("deleteLetter");
|
||||
}
|
||||
TestUI.updateWordElement(!Config.blindMode);
|
||||
}
|
||||
|
|
@ -230,6 +234,7 @@ function handleSpace(event, isEnter) {
|
|||
dontInsertSpace = true;
|
||||
if (currentWord == TestLogic.input.current || Config.mode == "zen") {
|
||||
//correct word or in zen mode
|
||||
Replay.addReplayEvent("submitCorrectWord");
|
||||
PaceCaret.handleSpace(true, currentWord);
|
||||
TestStats.incrementAccuracy(true);
|
||||
TestLogic.input.pushHistory();
|
||||
|
|
@ -247,6 +252,7 @@ function handleSpace(event, isEnter) {
|
|||
}
|
||||
} else {
|
||||
//incorrect word
|
||||
Replay.addReplayEvent("submitErrorWord");
|
||||
PaceCaret.handleSpace(false, currentWord);
|
||||
if (Funbox.active !== "nospace") {
|
||||
if (!Config.playSoundOnError || Config.blindMode) {
|
||||
|
|
@ -566,6 +572,7 @@ function handleAlpha(event) {
|
|||
}
|
||||
|
||||
if (!thisCharCorrect) {
|
||||
Replay.addReplayEvent("incorrectLetter", event.key);
|
||||
TestStats.incrementAccuracy(false);
|
||||
TestStats.incrementKeypressErrors();
|
||||
// currentError.count++;
|
||||
|
|
@ -573,6 +580,7 @@ function handleAlpha(event) {
|
|||
thisCharCorrect = false;
|
||||
TestStats.pushMissedWord(TestLogic.words.getCurrent());
|
||||
} else {
|
||||
Replay.addReplayEvent("correctLetter", event.key);
|
||||
TestStats.incrementAccuracy(true);
|
||||
thisCharCorrect = true;
|
||||
if (Config.mode == "zen") {
|
||||
|
|
|
|||
267
src/js/replay.js
Normal file
267
src/js/replay.js
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
TODO:
|
||||
Export replay as video
|
||||
Export replay as typing test file?
|
||||
.ttr file extension (stands for typing test record)
|
||||
Should just be json, but fields should be specified by some format
|
||||
metadata field with rules, website source, mode, name of typist
|
||||
data field should be a list of objects, like monkeytype replay uses
|
||||
signature or verfication field should be able to check file validity with server
|
||||
And add ability to upload file to watch replay
|
||||
*/
|
||||
let wordsList = [];
|
||||
let replayData = [];
|
||||
let replayStartTime = 0;
|
||||
let replayRecording = true;
|
||||
let wordPos = 0;
|
||||
let curPos = 0;
|
||||
let targetWordPos = 0;
|
||||
let targetCurPos = 0;
|
||||
let timeoutList = [];
|
||||
const toggleButton = document.getElementById("playpauseReplayButton")
|
||||
.children[0];
|
||||
|
||||
function replayGetWordsList(wordsListFromScript) {
|
||||
wordsList = wordsListFromScript;
|
||||
}
|
||||
|
||||
function initializeReplayPrompt() {
|
||||
const replayWordsElement = document.getElementById("replayWords");
|
||||
replayWordsElement.innerHTML = "";
|
||||
let wordCount = 0;
|
||||
replayData.forEach((item, i) => {
|
||||
//trim wordsList for timed tests
|
||||
if (item.action === "backWord") {
|
||||
wordCount--;
|
||||
} else if (
|
||||
item.action === "submitCorrectWord" ||
|
||||
item.action === "submitErrorWord"
|
||||
) {
|
||||
wordCount++;
|
||||
}
|
||||
});
|
||||
wordsList.forEach((item, i) => {
|
||||
if (i > wordCount) return;
|
||||
let x = document.createElement("div");
|
||||
x.className = "word";
|
||||
for (i = 0; i < item.length; i++) {
|
||||
let letter = document.createElement("LETTER");
|
||||
letter.innerHTML = item[i];
|
||||
x.appendChild(letter);
|
||||
}
|
||||
replayWordsElement.appendChild(x);
|
||||
});
|
||||
}
|
||||
|
||||
function startReplayRecording() {
|
||||
if (!$("#resultReplay").stop(true, true).hasClass("hidden")) {
|
||||
//hide replay display if user left it open
|
||||
toggleReplayDisplay();
|
||||
}
|
||||
replayData = [];
|
||||
replayStartTime = performance.now();
|
||||
replayRecording = true;
|
||||
targetCurPos = 0;
|
||||
targetWordPos = 0;
|
||||
}
|
||||
|
||||
function stopReplayRecording() {
|
||||
replayRecording = false;
|
||||
}
|
||||
|
||||
function addReplayEvent(action, letter = undefined) {
|
||||
if (replayRecording === false) {
|
||||
return;
|
||||
}
|
||||
let timeDelta = performance.now() - replayStartTime;
|
||||
if (action === "incorrectLetter" || action === "correctLetter") {
|
||||
replayData.push({ action: action, letter: letter, time: timeDelta });
|
||||
} else {
|
||||
replayData.push({ action: action, time: timeDelta });
|
||||
}
|
||||
}
|
||||
|
||||
function pauseReplay() {
|
||||
timeoutList.forEach((item, i) => {
|
||||
clearTimeout(item);
|
||||
});
|
||||
timeoutList = [];
|
||||
targetCurPos = curPos;
|
||||
targetWordPos = wordPos;
|
||||
toggleButton.className = "fas fa-play";
|
||||
toggleButton.parentNode.setAttribute("aria-label", "Resume replay");
|
||||
}
|
||||
|
||||
function loadOldReplay() {
|
||||
let startingIndex = 0;
|
||||
curPos = 0;
|
||||
wordPos = 0;
|
||||
replayData.forEach((item, i) => {
|
||||
if (
|
||||
wordPos < targetWordPos ||
|
||||
(wordPos === targetWordPos && curPos < targetCurPos)
|
||||
) {
|
||||
//quickly display everything up to the target
|
||||
handleDisplayLogic(item);
|
||||
startingIndex = i + 1;
|
||||
}
|
||||
});
|
||||
return startingIndex;
|
||||
}
|
||||
|
||||
function playReplay() {
|
||||
curPos = 0;
|
||||
wordPos = 0;
|
||||
toggleButton.className = "fas fa-pause";
|
||||
toggleButton.parentNode.setAttribute("aria-label", "Pause replay");
|
||||
initializeReplayPrompt();
|
||||
let startingIndex = loadOldReplay();
|
||||
let lastTime = replayData[startingIndex].time;
|
||||
replayData.forEach((item, i) => {
|
||||
if (i < startingIndex) return;
|
||||
timeoutList.push(
|
||||
setTimeout(() => {
|
||||
handleDisplayLogic(item);
|
||||
}, item.time - lastTime)
|
||||
);
|
||||
});
|
||||
timeoutList.push(
|
||||
setTimeout(() => {
|
||||
//after the replay has finished, this will run
|
||||
targetCurPos = 0;
|
||||
targetWordPos = 0;
|
||||
toggleButton.className = "fas fa-play";
|
||||
toggleButton.parentNode.setAttribute("aria-label", "Start replay");
|
||||
}, replayData[replayData.length - 1].time - lastTime)
|
||||
);
|
||||
}
|
||||
|
||||
function handleDisplayLogic(item) {
|
||||
let activeWord = document.getElementById("replayWords").children[wordPos];
|
||||
if (item.action === "correctLetter") {
|
||||
activeWord.children[curPos].classList.add("correct");
|
||||
curPos++;
|
||||
} else if (item.action === "incorrectLetter") {
|
||||
let myElement;
|
||||
if (curPos >= activeWord.children.length) {
|
||||
//if letter is an extra
|
||||
myElement = document.createElement("letter");
|
||||
myElement.classList.add("extra");
|
||||
myElement.innerHTML = item.letter;
|
||||
activeWord.appendChild(myElement);
|
||||
}
|
||||
myElement = activeWord.children[curPos];
|
||||
myElement.classList.add("incorrect");
|
||||
curPos++;
|
||||
} else if (item.action === "deleteLetter") {
|
||||
let myElement = activeWord.children[curPos - 1];
|
||||
if (myElement.classList.contains("extra")) {
|
||||
myElement.remove();
|
||||
} else {
|
||||
myElement.className = "";
|
||||
}
|
||||
curPos--;
|
||||
} else if (item.action === "submitCorrectWord") {
|
||||
wordPos++;
|
||||
curPos = 0;
|
||||
} else if (item.action === "submitErrorWord") {
|
||||
activeWord.classList.add("error");
|
||||
wordPos++;
|
||||
curPos = 0;
|
||||
} else if (item.action === "clearWord") {
|
||||
let promptWord = document.createElement("div");
|
||||
let wordArr = wordsList[wordPos].split("");
|
||||
wordArr.forEach((letter, i) => {
|
||||
promptWord.innerHTML += `<letter>${letter}</letter>`;
|
||||
});
|
||||
activeWord.innerHTML = promptWord.innerHTML;
|
||||
curPos = 0;
|
||||
} else if (item.action === "backWord") {
|
||||
wordPos--;
|
||||
activeWord = document.getElementById("replayWords").children[wordPos];
|
||||
curPos = activeWord.children.length;
|
||||
while (activeWord.children[curPos - 1].className === "") curPos--;
|
||||
activeWord.classList.remove("error");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleReplayDisplay() {
|
||||
if ($("#resultReplay").stop(true, true).hasClass("hidden")) {
|
||||
initializeReplayPrompt();
|
||||
loadOldReplay();
|
||||
//show
|
||||
if (!$("#watchReplayButton").hasClass("loaded")) {
|
||||
$("#words").html(
|
||||
`<div class="preloader"><i class="fas fa-fw fa-spin fa-circle-notch"></i></div>`
|
||||
);
|
||||
$("#resultReplay")
|
||||
.removeClass("hidden")
|
||||
.css("display", "none")
|
||||
.slideDown(250);
|
||||
} else {
|
||||
$("#resultReplay")
|
||||
.removeClass("hidden")
|
||||
.css("display", "none")
|
||||
.slideDown(250);
|
||||
}
|
||||
} else {
|
||||
//hide
|
||||
if (toggleButton.parentNode.getAttribute("aria-label") != "Start replay") {
|
||||
pauseReplay();
|
||||
}
|
||||
$("#resultReplay").slideUp(250, () => {
|
||||
$("#resultReplay").addClass("hidden");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(".pageTest #playpauseReplayButton").click(async (event) => {
|
||||
if (toggleButton.className === "fas fa-play") {
|
||||
playReplay();
|
||||
} else if (toggleButton.className === "fas fa-pause") {
|
||||
pauseReplay();
|
||||
}
|
||||
});
|
||||
|
||||
$("#replayWords").click((event) => {
|
||||
//allows user to click on the place they want to start their replay at
|
||||
pauseReplay();
|
||||
const replayWords = document.querySelector("#replayWords");
|
||||
let range;
|
||||
let textNode;
|
||||
|
||||
if (document.caretPositionFromPoint) {
|
||||
// standard
|
||||
range = document.caretPositionFromPoint(event.pageX, event.pageY);
|
||||
textNode = range.offsetNode;
|
||||
} else if (document.caretRangeFromPoint) {
|
||||
// WebKit
|
||||
range = document.caretRangeFromPoint(event.pageX, event.pageY);
|
||||
textNode = range.startContainer;
|
||||
}
|
||||
|
||||
const words = [...replayWords.children];
|
||||
targetWordPos = words.indexOf(textNode.parentNode.parentNode);
|
||||
const letters = [...words[targetWordPos].children];
|
||||
targetCurPos = letters.indexOf(textNode.parentNode);
|
||||
|
||||
initializeReplayPrompt();
|
||||
loadOldReplay();
|
||||
});
|
||||
|
||||
$(document).on("keypress", "#watchReplayButton", (event) => {
|
||||
if (event.keyCode == 13) {
|
||||
toggleReplayDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
$(document.body).on("click", "#watchReplayButton", () => {
|
||||
toggleReplayDisplay();
|
||||
});
|
||||
|
||||
export {
|
||||
startReplayRecording,
|
||||
stopReplayRecording,
|
||||
addReplayEvent,
|
||||
replayGetWordsList,
|
||||
};
|
||||
|
|
@ -27,6 +27,7 @@ import * as DB from "./db";
|
|||
import * as ThemeColors from "./theme-colors";
|
||||
import * as CloudFunctions from "./cloud-functions";
|
||||
import * as TestLeaderboards from "./test-leaderboards";
|
||||
import * as Replay from "./replay.js";
|
||||
|
||||
export let notSignedInLastResult = null;
|
||||
|
||||
|
|
@ -318,6 +319,8 @@ export function startTest() {
|
|||
console.log("Analytics unavailable");
|
||||
}
|
||||
setActive(true);
|
||||
Replay.startReplayRecording();
|
||||
Replay.replayGetWordsList(words.list);
|
||||
TestStats.resetKeypressTimings();
|
||||
TimerProgress.restart();
|
||||
TimerProgress.show();
|
||||
|
|
@ -343,6 +346,7 @@ export function startTest() {
|
|||
|
||||
export async function init() {
|
||||
setActive(false);
|
||||
Replay.stopReplayRecording();
|
||||
words.reset();
|
||||
TestUI.setCurrentWordElementIndex(0);
|
||||
// accuracy = {
|
||||
|
|
@ -678,6 +682,7 @@ export function restart(withSameWordset = false, nosave = false, event) {
|
|||
Focus.set(false);
|
||||
Caret.hide();
|
||||
setActive(false);
|
||||
Replay.stopReplayRecording();
|
||||
LiveWpm.hide();
|
||||
LiveAcc.hide();
|
||||
TimerProgress.hide();
|
||||
|
|
@ -727,6 +732,7 @@ export function restart(withSameWordset = false, nosave = false, event) {
|
|||
} else {
|
||||
setRepeated(true);
|
||||
setActive(false);
|
||||
Replay.stopReplayRecording();
|
||||
words.resetCurrentIndex();
|
||||
input.reset();
|
||||
PaceCaret.init();
|
||||
|
|
@ -952,6 +958,7 @@ export function finish(difficultyFailed = false) {
|
|||
if (Config.mode == "zen" && input.current.length != 0) {
|
||||
input.pushHistory();
|
||||
corrected.pushHistory();
|
||||
Replay.replayGetWordsList(input.history);
|
||||
}
|
||||
|
||||
TestStats.recordKeypressSpacing();
|
||||
|
|
@ -960,6 +967,7 @@ export function finish(difficultyFailed = false) {
|
|||
TestUI.setResultVisible(true);
|
||||
TestStats.setEnd(performance.now());
|
||||
setActive(false);
|
||||
Replay.stopReplayRecording();
|
||||
Focus.set(false);
|
||||
Caret.hide();
|
||||
LiveWpm.hide();
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
|
||||
#result {
|
||||
.buttons {
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
#nextTestButton {
|
||||
grid-column: 1/5;
|
||||
width: 100%;
|
||||
|
|
@ -219,7 +219,7 @@
|
|||
}
|
||||
#result {
|
||||
.buttons {
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr 1fr;
|
||||
#nextTestButton {
|
||||
grid-column: 1/3;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1864,7 +1864,8 @@ key {
|
|||
grid-column: 1/3;
|
||||
}
|
||||
|
||||
#resultWordsHistory {
|
||||
#resultWordsHistory,
|
||||
#resultReplay {
|
||||
// grid-area: wordsHistory;
|
||||
color: var(--sub-color);
|
||||
grid-column: 1/3;
|
||||
|
|
@ -1876,8 +1877,18 @@ key {
|
|||
user-select: none;
|
||||
.word {
|
||||
position: relative;
|
||||
margin: 0.18rem 0.6rem 0.15rem 0;
|
||||
}
|
||||
}
|
||||
.correct {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.incorrect {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.incorrect.extra {
|
||||
color: var(--error-extra-color);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
|
|
@ -2276,7 +2287,8 @@ key {
|
|||
#copyResultToClipboardButton,
|
||||
#restartTestButtonWithSameWordset,
|
||||
#nextTestButton,
|
||||
#practiseMissedWordsButton {
|
||||
#practiseMissedWordsButton,
|
||||
#watchReplayButton {
|
||||
position: relative;
|
||||
border-radius: var(--roundness);
|
||||
padding: 1rem 2rem;
|
||||
|
|
@ -2297,6 +2309,10 @@ key {
|
|||
}
|
||||
}
|
||||
|
||||
#replayWords {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#restartTestButton {
|
||||
margin: 0 auto;
|
||||
margin-top: 1rem;
|
||||
|
|
|
|||
|
|
@ -1424,6 +1424,23 @@
|
|||
</div>
|
||||
<div class="words"></div>
|
||||
</div>
|
||||
<div id="resultReplay" class="hidden">
|
||||
<div class="title">
|
||||
watch replay
|
||||
<span
|
||||
id="playpauseReplayButton"
|
||||
class="icon-button"
|
||||
aria-label="Start replay"
|
||||
data-balloon-pos="up"
|
||||
style="display: inline-block"
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div id="wordsWrapper">
|
||||
<div id="replayWords" class="words"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loginTip">
|
||||
<a href="/login" tabindex="9">Sign in</a>
|
||||
to save your results
|
||||
|
|
@ -1475,6 +1492,15 @@
|
|||
>
|
||||
<i class="far fa-fw fa-image"></i>
|
||||
</div>
|
||||
<div
|
||||
id="watchReplayButton"
|
||||
aria-label="Watch Replay"
|
||||
data-balloon-pos="down"
|
||||
tabindex="0"
|
||||
onclick="this.blur();"
|
||||
>
|
||||
<i class="fas fa-fw fa-backward"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue