diff --git a/.firebaserc_example b/.firebaserc_example new file mode 100644 index 000000000..0de16a603 --- /dev/null +++ b/.firebaserc_example @@ -0,0 +1,5 @@ + { + "projects": { + "default": "your-firebase-project-id" + } + } diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 94a467522..1fe9cd7b1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ -Adding a language or a theme? Make sure to edit the `list.json` file as well, otherwise, it will not work! +Adding a language or a theme? Make sure to edit the `_list.json` file as well, otherwise, it will not work! Please reference any issues related to your pull request. diff --git a/.prettierignore b/.prettierignore index f568baa07..44e1ccc46 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,10 +1,10 @@ *.min.js layouts.js -english_quotes.json +quotes/english.json chartjs-plugin-*.js sound/* node_modules css/balloon.css css/fa.css css/style.min.css -list.json \ No newline at end of file +_list.json diff --git a/functions/index.js b/functions/index.js index a037df129..38539a0b8 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,9 +1,11 @@ const functions = require("firebase-functions"); const admin = require("firebase-admin"); let key = "./serviceAccountKey.json"; +let origin = "http://localhost:5000"; if (process.env.GCLOUD_PROJECT === "monkey-type") { key = "./serviceAccountKey_live.json"; + origin = "https://monkeytype.com"; } var serviceAccount = require(key); @@ -133,14 +135,36 @@ exports.clearName = functions.auth.user().onDelete((user) => { db.collection("users").doc(user.uid).delete(); }); -exports.checkNameAvailability = functions.https.onCall( +exports.checkNameAvailability = functions.https.onRequest( async (request, response) => { + response.set("Access-Control-Allow-Origin", origin); + if (request.method === "OPTIONS") { + // Send response to OPTIONS requests + response.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + response.set( + "Access-Control-Allow-Headers", + "Authorization,Content-Type" + ); + response.set("Access-Control-Max-Age", "3600"); + response.status(204).send(""); + return; + } + request = request.body.data; + // 1 - available // -1 - unavailable (taken) // -2 - not valid name // -999 - unknown error try { - if (!isUsernameValid(request.name)) return -2; + if (!isUsernameValid(request.name)) { + response.status(200).send({ + data: { + resultCode: -2, + message: "Username is not valid", + }, + }); + return; + } let takendata = await db .collection("takenNames") @@ -150,9 +174,21 @@ exports.checkNameAvailability = functions.https.onCall( takendata = takendata.data(); if (takendata !== undefined && takendata.taken) { - return -1; + response.status(200).send({ + data: { + resultCode: -1, + message: "Username is taken", + }, + }); + return; } else { - return 1; + response.status(200).send({ + data: { + resultCode: 1, + message: "Username is available", + }, + }); + return; } // return getAllNames().then((data) => { @@ -167,8 +203,17 @@ exports.checkNameAvailability = functions.https.onCall( // return available; // }); } catch (e) { - console.log(e.message); - return -999; + console.error( + `Error while checking name availability for ${request.name}:` + + e.message + ); + response.status(200).send({ + data: { + resultCode: -999, + message: "Unexpected error: " + e, + }, + }); + return; } } ); @@ -456,7 +501,7 @@ function checkIfPB(uid, obj, userdata) { } } -async function checkIfTagPB(uid, obj) { +async function checkIfTagPB(uid, obj, userdata) { if (obj.tags.length === 0) { return []; } @@ -471,24 +516,152 @@ async function checkIfTagPB(uid, obj) { dbtags.push(data); } }); - } catch (e) { + } catch { return []; } - let wpm = obj.wpm; + let ret = []; for (let i = 0; i < dbtags.length; i++) { - let dbtag = dbtags[i]; - if (dbtag.pb === undefined || dbtag.pb < wpm) { - //no pb found, meaning this one is a pb - await db.collection(`users/${uid}/tags`).doc(dbtag.id).update({ - pb: wpm, + let pbs = null; + try { + pbs = dbtags[i].personalBests; + if (pbs === undefined) { + throw new Error("pb is undefined"); + } + } catch (e) { + //undefined personal best = new personal best + db.collection(`users/${uid}/tags`) + .doc(dbtags[i].id) + .set( + { + personalBests: { + [obj.mode]: { + [obj.mode2]: [ + { + language: obj.language, + difficulty: obj.difficulty, + punctuation: obj.punctuation, + wpm: obj.wpm, + acc: obj.acc, + raw: obj.rawWpm, + timestamp: Date.now(), + consistency: obj.consistency, + }, + ], + }, + }, + }, + { merge: true } + ) + .then((e) => { + ret.push(dbtags[i].id); + }); + continue; + } + let toUpdate = false; + let found = false; + try { + if (pbs[obj.mode][obj.mode2] === undefined) { + pbs[obj.mode][obj.mode2] = []; + } + pbs[obj.mode][obj.mode2].forEach((pb) => { + if ( + pb.punctuation === obj.punctuation && + pb.difficulty === obj.difficulty && + pb.language === obj.language + ) { + //entry like this already exists, compare wpm + found = true; + if (pb.wpm < obj.wpm) { + //new pb + pb.wpm = obj.wpm; + pb.acc = obj.acc; + pb.raw = obj.rawWpm; + pb.timestamp = Date.now(); + pb.consistency = obj.consistency; + toUpdate = true; + } else { + //no pb + return false; + } + } }); - ret.push(dbtag.id); + //checked all pbs, nothing found - meaning this is a new pb + if (!found) { + pbs[obj.mode][obj.mode2].push({ + language: obj.language, + difficulty: obj.difficulty, + punctuation: obj.punctuation, + wpm: obj.wpm, + acc: obj.acc, + raw: obj.rawWpm, + timestamp: Date.now(), + consistency: obj.consistency, + }); + toUpdate = true; + } + } catch (e) { + // console.log(e); + pbs[obj.mode] = {}; + pbs[obj.mode][obj.mode2] = [ + { + language: obj.language, + difficulty: obj.difficulty, + punctuation: obj.punctuation, + wpm: obj.wpm, + acc: obj.acc, + raw: obj.rawWpm, + timestamp: Date.now(), + consistency: obj.consistency, + }, + ]; + toUpdate = true; + } + + if (toUpdate) { + db.collection(`users/${uid}/tags`) + .doc(dbtags[i].id) + .update({ personalBests: pbs }); + ret.push(dbtags[i].id); } } return ret; } +//old +// async function checkIfTagPB(uid, obj) { +// if (obj.tags.length === 0) { +// return []; +// } +// let dbtags = []; +// let restags = obj.tags; +// try { +// let snap = await db.collection(`users/${uid}/tags`).get(); +// snap.forEach((doc) => { +// if (restags.includes(doc.id)) { +// let data = doc.data(); +// data.id = doc.id; +// dbtags.push(data); +// } +// }); +// } catch (e) { +// return []; +// } +// let wpm = obj.wpm; +// let ret = []; +// for (let i = 0; i < dbtags.length; i++) { +// let dbtag = dbtags[i]; +// if (dbtag.pb === undefined || dbtag.pb < wpm) { +// //no pb found, meaning this one is a pb +// await db.collection(`users/${uid}/tags`).doc(dbtag.id).update({ +// pb: wpm, +// }); +// ret.push(dbtag.id); +// } +// } +// return ret; +// } + exports.clearTagPb = functions.https.onCall((request, response) => { try { return db @@ -588,14 +761,14 @@ function validateResult(result) { } exports.requestTest = functions.https.onRequest((request, response) => { - response.set("Access-Control-Allow-Origin", "*"); + response.set("Access-Control-Allow-Origin", origin); response.set("Access-Control-Allow-Headers", "*"); response.set("Access-Control-Allow-Credentials", "true"); response.status(200).send({ data: "test" }); }); exports.getPatreons = functions.https.onRequest(async (request, response) => { - response.set("Access-Control-Allow-Origin", "*"); + response.set("Access-Control-Allow-Origin", origin); response.set("Access-Control-Allow-Headers", "*"); response.set("Access-Control-Allow-Credentials", "true"); if (request.method === "OPTIONS") { @@ -629,7 +802,7 @@ exports.getPatreons = functions.https.onRequest(async (request, response) => { }); exports.verifyUser = functions.https.onRequest(async (request, response) => { - response.set("Access-Control-Allow-Origin", "*"); + response.set("Access-Control-Allow-Origin", origin); response.set("Access-Control-Allow-Headers", "*"); response.set("Access-Control-Allow-Credentials", "true"); if (request.method === "OPTIONS") { @@ -1002,7 +1175,7 @@ async function incrementTimeSpentTyping(uid, res, userData) { } exports.testCompleted = functions.https.onRequest(async (request, response) => { - response.set("Access-Control-Allow-Origin", "*"); + response.set("Access-Control-Allow-Origin", origin); if (request.method === "OPTIONS") { // Send response to OPTIONS requests response.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); @@ -1055,7 +1228,13 @@ exports.testCompleted = functions.https.onRequest(async (request, response) => { return; } - if (obj.wpm <= 0 || obj.wpm > 350 || obj.acc < 50 || obj.acc > 100) { + if ( + obj.wpm <= 0 || + obj.wpm > 350 || + obj.acc < 50 || + obj.acc > 100 || + obj.consistency > 100 + ) { response.status(200).send({ data: { resultCode: -1 } }); return; } @@ -1426,12 +1605,12 @@ exports.addTag = functions.https.onCall((request, response) => { console.error( `error while creating tag for user ${request.uid}: ${e.message}` ); - return { resultCode: -999 }; + return { resultCode: -999, message: e.message }; }); } } catch (e) { console.error(`error adding tag for ${request.uid} - ${e}`); - return { resultCode: -999 }; + return { resultCode: -999, message: e.message }; } }); @@ -1456,12 +1635,12 @@ exports.editTag = functions.https.onCall((request, response) => { console.error( `error while updating tag for user ${request.uid}: ${e.message}` ); - return { resultCode: -999 }; + return { resultCode: -999, message: e.message }; }); } } catch (e) { console.error(`error updating tag for ${request.uid} - ${e}`); - return { resultCode: -999 }; + return { resultCode: -999, message: e.message }; } }); @@ -1522,7 +1701,7 @@ exports.updateResultTags = functions.https.onCall((request, response) => { } } catch (e) { console.error(`error updating tags by ${request.uid} - ${e}`); - return { resultCode: -999 }; + return { resultCode: -999, message: e }; } }); @@ -1984,7 +2163,7 @@ class Leaderboard { // }); exports.unlinkDiscord = functions.https.onRequest((request, response) => { - response.set("Access-Control-Allow-Origin", "*"); + response.set("Access-Control-Allow-Origin", origin); if (request.method === "OPTIONS") { // Send response to OPTIONS requests response.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); diff --git a/gulpfile.js b/gulpfile.js index e4f1bda03..e73ad8398 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -93,6 +93,7 @@ const refactoredSrc = [ "./src/js/layouts.js", "./src/js/monkey.js", "./src/js/result-filters.js", + "./src/js/notification-center.js", ]; //legacy files diff --git a/src/js/account.js b/src/js/account.js index 997d40c89..c646a1ff9 100644 --- a/src/js/account.js +++ b/src/js/account.js @@ -24,7 +24,7 @@ function signIn() { changePage("test"); }) .catch(function (error) { - Misc.showNotification(error.message, 5000); + Notifications.add(error.message, -1); $(".pageLogin .preloader").addClass("hidden"); }); }); @@ -41,7 +41,7 @@ function signIn() { changePage("test"); }) .catch(function (error) { - Misc.showNotification(error.message, 5000); + Notifications.add(error.message, -1); $(".pageLogin .preloader").addClass("hidden"); }); }); @@ -59,27 +59,27 @@ function signUp() { let passwordVerify = $(".pageLogin .register input")[3].value; if (password != passwordVerify) { - Misc.showNotification("Passwords do not match", 3000); + Notifications.add("Passwords do not match", 0, 3); $(".pageLogin .preloader").addClass("hidden"); $(".pageLogin .register .button").removeClass("disabled"); return; } CloudFunctions.namecheck({ name: nname }).then((d) => { - if (d.data === -1) { - Misc.showNotification("Name unavailable", 3000); + if (d.data.resultCode === -1) { + Notifications.add("Name unavailable", -1); $(".pageLogin .preloader").addClass("hidden"); $(".pageLogin .register .button").removeClass("disabled"); return; - } else if (d.data === -2) { - Misc.showNotification( + } else if (d.data.resultCode === -2) { + Notifications.add( "Name cannot contain special characters or contain more than 14 characters. Can include _ . and -", - 8000 + -1 ); $(".pageLogin .preloader").addClass("hidden"); $(".pageLogin .register .button").removeClass("disabled"); return; - } else if (d.data === 1) { + } else if (d.data.resultCode === 1) { firebase .auth() .createUserWithEmailAndPassword(email, password) @@ -106,7 +106,7 @@ function signUp() { ); usr.sendEmailVerification(); clearGlobalStats(); - Misc.showNotification("Account created", 2000); + Notifications.add("Account created", 1, 3); $("#menu .icon-button.account .text").text(nname); try { firebase.analytics().logEvent("accountCreated", usr.uid); @@ -144,15 +144,19 @@ function signUp() { .delete() .then(function () { // User deleted. - Misc.showNotification( - "An error occured. Account not created.", - 2000 + Notifications.add( + "Account not created. " + error.message, + -1 ); $(".pageLogin .preloader").addClass("hidden"); }) .catch(function (error) { // An error happened. $(".pageLogin .preloader").addClass("hidden"); + Notifications.add( + "Something went wrong. " + error.message, + -1 + ); console.error(error); }); }); @@ -160,10 +164,14 @@ function signUp() { .catch(function (error) { // Handle Errors here. $(".pageLogin .register .button").removeClass("disabled"); - var errorMessage = error.message; - Misc.showNotification(errorMessage, 5000); + Notifications.add(error.message, -1); $(".pageLogin .preloader").addClass("hidden"); }); + } else { + Notifications.add( + "Something went wrong when checking name: " + d.data.message, + -1 + ); } }); } @@ -173,7 +181,7 @@ function signOut() { .auth() .signOut() .then(function () { - Misc.showNotification("Signed out", 2000); + Notifications.add("Signed out", 0, 2); clearGlobalStats(); hideAccountSettingsSection(); updateAccountLoginButton(); @@ -181,7 +189,7 @@ function signOut() { db_setSnapshot(null); }) .catch(function (error) { - Misc.showNotification(error.message, 5000); + Notifications.add(error.message, -1); }); } @@ -221,13 +229,15 @@ firebase.auth().onAuthStateChanged(function (user) { $(".pageAccount .group.createdDate").text(text); if (verifyUserWhenLoggedIn !== null) { - Misc.showNotification("Verifying", 1000); + Notifications.add("Verifying", 0, 3); verifyUserWhenLoggedIn.uid = user.uid; CloudFunctions.verifyUser(verifyUserWhenLoggedIn).then((data) => { - Misc.showNotification(data.data.message, 3000); if (data.data.status === 1) { + Notifications.add(data.data.message, 1); db_getSnapshot().discordId = data.data.did; updateDiscordSettingsSection(); + } else { + Notifications.add(data.data.message, -1); } }); } @@ -237,11 +247,11 @@ firebase.auth().onAuthStateChanged(function (user) { try { theme = theme.split(","); config.customThemeColors = theme; - Misc.showNotification("Custom theme applied.", 1000); + Notifications.add("Custom theme applied.", 1); } catch (e) { - Misc.showNotification( + Notifications.add( "Something went wrong. Reverting to default custom colors.", - 3000 + 0 ); config.customThemeColors = defaultConfig.customThemeColors; } @@ -366,9 +376,9 @@ function getAccountDataAndInit() { .catch((e) => { accountIconLoading(false); console.error(e); - Misc.showNotification( - "Error downloading user data. Refresh to try again. If error persists contact Miodec.", - 5000 + Notifications.add( + "Error downloading user data - refresh to try again. Client likely could not connect to the backend, if error persists contact Miodec.", + -1 ); $("#top #menu .account .icon").html(''); $("#top #menu .account").css("opacity", 1); @@ -1004,9 +1014,9 @@ function toggleFilter(group, filter) { ResultFilters.toggleFilter(group, filter); ResultFilters.save(); } catch (e) { - Misc.showNotification( + Notifications.add( "Something went wrong toggling filter. Reverting to defaults", - 3000 + 0 ); console.log("toggling filter error"); console.error(e); @@ -1851,9 +1861,9 @@ function refreshAccountPage() { filteredResults.push(result); } catch (e) { - Misc.showNotification( + Notifications.add( "Something went wrong when filtering. Resetting filters.", - 5000 + 0 ); console.log(result); console.error(e); @@ -2206,7 +2216,7 @@ function refreshAccountPage() { swapElements($(".pageAccount .preloader"), $(".pageAccount .content"), 250); } if (db_getSnapshot() === null) { - Misc.showNotification(`Missing account data. Please refresh.`, 5000); + Notifications.add(`Missing account data. Please refresh.`, -1); $(".pageAccount .preloader").html("Missing account data. Please refresh."); } else if (db_getSnapshot().results === undefined) { db_getUserResults().then((d) => { @@ -2224,7 +2234,7 @@ function refreshAccountPage() { cont(); } catch (e) { console.error(e); - Misc.showNotification(`Something went wrong: ${e}`, 5000); + Notifications.add(`Something went wrong: ${e}`, -1); } } } @@ -2326,7 +2336,7 @@ $("#resultEditTagsPanel .confirmButton").click((f) => { }).then((r) => { hideBackgroundLoader(); if (r.data.resultCode === 1) { - Misc.showNotification("Tags updated.", 3000); + Notifications.add("Tags updated.", 1, 2); db_getSnapshot().results.forEach((result) => { if (result.id === resultid) { result.tags = newtags; @@ -2377,7 +2387,7 @@ $("#resultEditTagsPanel .confirmButton").click((f) => { ); } } else { - Misc.showNotification("Error updating tags", 3000); + Notifications.add("Error updating tags: " + r.data.message, -1); } }); }); @@ -2425,11 +2435,11 @@ $(".pageLogin #forgotPasswordButton").click((e) => { .sendPasswordResetEmail(email) .then(function () { // Email sent. - Misc.showNotification("Email sent", 2000); + Notifications.add("Email sent", 1, 2); }) .catch(function (error) { // An error happened. - Misc.showNotification(error.message, 5000); + Notifications.add(error.message, -1); }); } }); diff --git a/src/js/commandline.js b/src/js/commandline.js index bb5520215..3335b6640 100644 --- a/src/js/commandline.js +++ b/src/js/commandline.js @@ -1,11 +1,15 @@ function canBailOut() { return ( (config.mode === "custom" && - customTextIsRandom && - customTextWordCount >= 5000) || + customText.isWordRandom && + customText.word >= 5000) || (config.mode === "custom" && - !customTextIsRandom && - customText.length >= 5000) || + !customText.isWordRandom && + !customText.isTimeRandom && + customText.text.length >= 5000) || + (config.mode === "custom" && + customText.isTimeRandom && + customText.time >= 3600) || (config.mode === "words" && config.words >= 5000) || config.words === 0 || (config.mode === "time" && (config.time >= 3600 || config.time === 0)) || @@ -885,7 +889,7 @@ let commandsEnableAds = { display: "off", exec: () => { setEnableAds("off"); - Misc.showNotification("Don't forget to refresh the page!", 3000); + Notifications.add("Don't forget to refresh the page!", 0); }, }, { @@ -893,7 +897,7 @@ let commandsEnableAds = { display: "on", exec: () => { setEnableAds("on"); - Misc.showNotification("Don't forget to refresh the page!", 3000); + Notifications.add("Don't forget to refresh the page!", 0); }, }, { @@ -901,7 +905,7 @@ let commandsEnableAds = { display: "Sellout", exec: () => { setEnableAds("max"); - Misc.showNotification("Don't forget to refresh the page!", 3000); + Notifications.add("Don't forget to refresh the page!", 0); }, }, ], diff --git a/src/js/db.js b/src/js/db.js index bdf8d53b6..37be3d0ef 100644 --- a/src/js/db.js +++ b/src/js/db.js @@ -45,6 +45,9 @@ export async function db_getUserSnapshot() { data.docs.forEach((doc) => { let tag = doc.data(); tag.id = doc.id; + if (tag.personalBests === undefined) { + tag.personalBests = {}; + } snap.tags.push(tag); }); snap.tags = snap.tags.sort((a, b) => { @@ -305,14 +308,27 @@ export async function db_saveLocalPB( } } -export async function db_getLocalTagPB(tagId) { +export async function db_getLocalTagPB( + tagId, + mode, + mode2, + punctuation, + language, + difficulty +) { function cont() { let ret = 0; + let filteredtag = dbSnapshot.tags.filter((t) => t.id === tagId)[0]; try { - ret = dbSnapshot.tags.filter((t) => t.id === tagId)[0].pb; - if (ret == undefined) { - ret = 0; - } + filteredtag.personalBests[mode][mode2].forEach((pb) => { + if ( + pb.punctuation == punctuation && + pb.difficulty == difficulty && + pb.language == language + ) { + ret = pb.wpm; + } + }); return ret; } catch (e) { return ret; @@ -320,22 +336,114 @@ export async function db_getLocalTagPB(tagId) { } let retval; - if (dbSnapshot != null) { + if (dbSnapshot == null) { + retval = 0; + } else { retval = cont(); } return retval; } -export async function db_saveLocalTagPB(tagId, wpm) { +export async function db_saveLocalTagPB( + tagId, + mode, + mode2, + punctuation, + language, + difficulty, + wpm, + acc, + raw, + consistency +) { function cont() { - dbSnapshot.tags.forEach((tag) => { - if (tag.id === tagId) { - tag.pb = wpm; + let filteredtag = dbSnapshot.tags.filter((t) => t.id === tagId)[0]; + try { + let found = false; + if (filteredtag.personalBests[mode][mode2] === undefined) { + filteredtag.personalBests[mode][mode2] = []; } - }); + filteredtag.personalBests[mode][mode2].forEach((pb) => { + if ( + pb.punctuation == punctuation && + pb.difficulty == difficulty && + pb.language == language + ) { + found = true; + pb.wpm = wpm; + pb.acc = acc; + pb.raw = raw; + pb.timestamp = Date.now(); + pb.consistency = consistency; + } + }); + if (!found) { + //nothing found + filteredtag.personalBests[mode][mode2].push({ + language: language, + difficulty: difficulty, + punctuation: punctuation, + wpm: wpm, + acc: acc, + raw: raw, + timestamp: Date.now(), + consistency: consistency, + }); + } + } catch (e) { + //that mode or mode2 is not found + filteredtag.personalBests[mode] = {}; + filteredtag.personalBests[mode][mode2] = [ + { + language: language, + difficulty: difficulty, + punctuation: punctuation, + wpm: wpm, + acc: acc, + raw: raw, + timestamp: Date.now(), + consistency: consistency, + }, + ]; + } } if (dbSnapshot != null) { cont(); } } + +// export async function db_getLocalTagPB(tagId) { +// function cont() { +// let ret = 0; +// try { +// ret = dbSnapshot.tags.filter((t) => t.id === tagId)[0].pb; +// if (ret == undefined) { +// ret = 0; +// } +// return ret; +// } catch (e) { +// return ret; +// } +// } + +// let retval; +// if (dbSnapshot != null) { +// retval = cont(); +// } +// return retval; +// } + +// export async function db_saveLocalTagPB(tagId, wpm) { +// function cont() { +// dbSnapshot.tags.forEach((tag) => { +// if (tag.id === tagId) { +// tag.pb = wpm; +// } +// }); +// } + +// if (dbSnapshot != null) { +// cont(); +// } +// } diff --git a/src/js/exports.js b/src/js/exports.js index 8f8082626..369fd5ce6 100644 --- a/src/js/exports.js +++ b/src/js/exports.js @@ -7,3 +7,4 @@ global.sendVerificationEmail = Misc.sendVerificationEmail; //these exports are just for debugging in the browser global.snapshot = db_getSnapshot; global.config = config; +global.addnotif = Notifications.add; diff --git a/src/js/global-dependencies.js b/src/js/global-dependencies.js index dc414488a..2ce2dfd6a 100644 --- a/src/js/global-dependencies.js +++ b/src/js/global-dependencies.js @@ -25,4 +25,5 @@ import * as Misc from "./misc"; import * as CloudFunctions from "./cloud-functions"; import layouts from "./layouts"; import * as Monkey from "./monkey"; +import * as Notifications from "./notification-center"; import * as ResultFilters from "./result-filters"; diff --git a/src/js/leaderboards.js b/src/js/leaderboards.js index bbfe21874..2a7feb6a9 100644 --- a/src/js/leaderboards.js +++ b/src/js/leaderboards.js @@ -217,7 +217,8 @@ function updateLeaderboards() { } }) .catch((e) => { - Misc.showNotification("Something went wrong", 3000); + hideBackgroundLoader(); + Notifications.add("Something went wrong: " + e.message, -1); }); } diff --git a/src/js/misc.js b/src/js/misc.js index 2e8d4c240..92a4cf5dd 100644 --- a/src/js/misc.js +++ b/src/js/misc.js @@ -50,7 +50,7 @@ function hexToHSL(H) { let themesList = null; export async function getThemesList() { if (themesList == null) { - return $.getJSON("themes/list.json", function (data) { + return $.getJSON("themes/_list.json", function (data) { const list = data.sort(function (a, b) { const nameA = a.name.toLowerCase(); const nameB = b.name.toLowerCase(); @@ -87,7 +87,7 @@ export async function getSortedThemesList() { let funboxList = null; export async function getFunboxList() { if (funboxList == null) { - return $.getJSON("funbox/list.json", function (data) { + return $.getJSON("funbox/_list.json", function (data) { funboxList = data.sort(function (a, b) { const nameA = a.name.toLowerCase(); const nameB = b.name.toLowerCase(); @@ -105,7 +105,7 @@ export async function getFunboxList() { let fontsList = null; export async function getFontsList() { if (fontsList == null) { - return $.getJSON("js/fonts.json", function (data) { + return $.getJSON("fonts/_list.json", function (data) { fontsList = data.sort(function (a, b) { const nameA = a.name.toLowerCase(); const nameB = b.name.toLowerCase(); @@ -123,7 +123,7 @@ export async function getFontsList() { let languageList = null; export async function getLanguageList() { if (languageList == null) { - return $.getJSON("languages/list.json", function (data) { + return $.getJSON("languages/_list.json", function (data) { languageList = data; return languageList; }); @@ -135,7 +135,7 @@ export async function getLanguageList() { let challengeList = null; export async function getChallengeList() { if (challengeList == null) { - return $.getJSON("challenges/list.json", function (data) { + return $.getJSON("challenges/_list.json", function (data) { challengeList = data; return challengeList; }); diff --git a/src/js/notification-center.js b/src/js/notification-center.js new file mode 100644 index 000000000..c0f3be1fe --- /dev/null +++ b/src/js/notification-center.js @@ -0,0 +1,131 @@ +const notificationHistory = []; +let id = 0; +class Notification { + constructor(message, level, duration, customTitle, customIcon) { + this.message = message; + this.level = level; + if (duration == undefined) { + if (level === -1) { + this.duration = 0; + } else { + this.duration = 3000; + } + } else { + this.duration = duration * 1000; + } + this.customTitle = customTitle; + this.customIcon = customIcon; + this.id = id++; + } + //level + //0 - notice + //1 - good + //-1 - bad + show() { + let cls = "notice"; + let icon = ``; + let title = "Notice"; + if (this.level === 1) { + cls = "good"; + icon = ``; + title = "Success"; + } else if (this.level === -1) { + cls = "bad"; + icon = ``; + title = "Error"; + console.error(this.message); + } + + if (this.customTitle != undefined) { + title = this.customTitle; + } + + if (this.customIcon != undefined) { + icon = ``; + } + + // moveCurrentToHistory(); + let oldHeight = $("#notificationCenter .history").height(); + $("#notificationCenter .history").prepend(` + +
+
${icon}
+
${title}
${this.message}
+
+ + `); + let newHeight = $("#notificationCenter .history").height(); + $(`#notificationCenter .notif[id='${this.id}']`).remove(); + $("#notificationCenter .history") + .css("margin-top", 0) + .animate( + { + marginTop: newHeight - oldHeight, + }, + 125, + () => { + $("#notificationCenter .history").css("margin-top", 0); + $("#notificationCenter .history").prepend(` + +
+
${icon}
+
${title}
${this.message}
+
+ + `); + $(`#notificationCenter .notif[id='${this.id}']`) + .css("opacity", 0) + .animate( + { + opacity: 1, + }, + 125, + () => { + $(`#notificationCenter .notif[id='${this.id}']`).css( + "opacity", + "" + ); + } + ); + $(`#notificationCenter .notif[id='${this.id}']`).click((e) => { + this.hide(); + }); + } + ); + if (this.duration > 0) { + setTimeout(() => { + this.hide(); + }, this.duration + 250); + } + $(`#notificationCenter .notif[id='${this.id}']`).hover((e) => { + $(`#notificationCenter .notif[id='${this.id}']`).toggleClass("hover"); + }); + } + hide() { + $(`#notificationCenter .notif[id='${this.id}']`) + .css("opacity", 1) + .animate( + { + opacity: 0, + }, + 125, + () => { + $(`#notificationCenter .notif[id='${this.id}']`).animate( + { + height: 0, + }, + 125, + () => { + $(`#notificationCenter .notif[id='${this.id}']`).remove(); + } + ); + } + ); + } +} + +export function add(message, level, duration, customTitle, customIcon) { + notificationHistory.push( + new Notification(message, level, duration, customTitle, customIcon).show() + ); +} diff --git a/src/js/script.js b/src/js/script.js index a27aac2c3..3b6bf3a35 100644 --- a/src/js/script.js +++ b/src/js/script.js @@ -45,8 +45,11 @@ let paceCaret = null; let missedWords = []; let verifyUserWhenLoggedIn = null; let modeBeforePractise = null; +let punctuationBeforePractise = null; +let numbersBeforePractise = null; let memoryFunboxTimer = null; let memoryFunboxInterval = null; +let textHasTab = false; let themeColors = { bg: "#323437", @@ -199,9 +202,17 @@ function initClickSounds() { }; } -let customText = "The quick brown fox jumps over the lazy dog".split(" "); -let customTextIsRandom = false; -let customTextWordCount = 1; +let customText = { + text: "The quick brown fox jumps over the lazy dog".split(" "), + isWordRandom: false, + isTimeRandom: false, + word: "", + time: "", +}; + +// let customText = "The quick brown fox jumps over the lazy dog".split(" "); +// let customText.isWordRandom = false; +// let customText.word = 1; let randomQuote = null; function refreshThemeColorObject() { @@ -256,7 +267,7 @@ function copyResultToClipboard() { ]) .then((f) => { $(".notification").removeClass("hidden"); - Misc.showNotification("Copied to clipboard", 1000); + Notifications.add("Copied to clipboard", 1, 2); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); if (firebase.auth().currentUser == null) @@ -265,7 +276,10 @@ function copyResultToClipboard() { .catch((f) => { open(URL.createObjectURL(blob)); $(".notification").removeClass("hidden"); - Misc.showNotification("Error saving image to clipboard", 2000); + Notifications.add( + "Error saving image to clipboard: " + f.message, + -1 + ); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); if (firebase.auth().currentUser == null) @@ -274,7 +288,10 @@ function copyResultToClipboard() { } catch (e) { open(URL.createObjectURL(blob)); $(".notification").removeClass("hidden"); - Misc.showNotification("Error saving image to clipboard", 2000); + Notifications.add( + "Error saving image to clipboard: " + e.message, + -1 + ); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); if (firebase.auth().currentUser == null) @@ -284,7 +301,7 @@ function copyResultToClipboard() { }); } catch (e) { $(".notification").removeClass("hidden"); - Misc.showNotification("Error creating image", 2000); + Notifications.add("Error creating image: " + e.message, -1); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); if (firebase.auth().currentUser == null) @@ -294,17 +311,17 @@ function copyResultToClipboard() { async function activateFunbox(funbox, mode) { if (testActive || resultVisible) { - Misc.showNotification( + Notifications.add( "You can only change the funbox before starting a test.", - 4000 + 0 ); return false; } if (Misc.getCurrentLanguage().ligatures) { if (funbox == "choo_choo" || funbox == "earthquake") { - Misc.showNotification( + Notifications.add( "Current language does not support this funbox mode", - 3000 + 0 ); activateFunbox("none", null); return; @@ -439,27 +456,56 @@ async function initWords() { config.language = "english"; } - if (config.mode === "quote" && quotes === null) { + if ( + config.mode === "quote" && + (quotes === null || + quotes.language !== config.language.replace(/_\d*k$/g, "")) + ) { + // if (config.language.split("_")[0] !== "code") { + setLanguage(config.language.replace(/_\d*k$/g, ""), true); + // } showBackgroundLoader(); $.ajax({ - url: "js/english_quotes.json", + url: `quotes/${config.language}.json`, async: false, success: function (data) { hideBackgroundLoader(); - quotes = data; - quotes.groups.forEach((qg, i) => { - let lower = qg[0]; - let upper = qg[1]; - quotes.groups[i] = quotes.quotes.filter((q) => { - if (q.length >= lower && q.length <= upper) { - q.group = i; - return true; - } else { - return false; - } + try { + if (data.quotes.length === 0) { + throw new Error("No quotes"); + } + quotes = data; + quotes.groups.forEach((qg, i) => { + let lower = qg[0]; + let upper = qg[1]; + quotes.groups[i] = quotes.quotes.filter((q) => { + if (q.length >= lower && q.length <= upper) { + q.group = i; + return true; + } else { + return false; + } + }); }); - }); - quotes.quotes = []; + quotes.quotes = []; + } catch (e) { + console.error(e); + Notifications.add( + `No ${config.language.replace(/_\d*k$/g, "")} quotes found`, + 0 + ); + return; + } + }, + error: (e) => { + Notifications.add( + `Error while loading ${config.language.replace( + /_\d*k$/g, + "" + )} quotes: ${e}`, + -1 + ); + return; }, }); } @@ -477,10 +523,12 @@ async function initWords() { let wordsBound = 100; if (config.showAllLines) { if (config.mode === "custom") { - if (customTextIsRandom) { - wordsBound = customTextWordCount; + if (customText.isWordRandom) { + wordsBound = customText.word; + } else if (customText.isTimeRandom) { + wordsBound = 100; } else { - wordsBound = customText.length; + wordsBound = customText.text.length; } } else if (config.mode != "time") { wordsBound = config.words; @@ -491,19 +539,38 @@ async function initWords() { } if ( config.mode == "custom" && - customTextIsRandom && - customTextWordCount < wordsBound + customText.isWordRandom && + customText.word < wordsBound ) { - wordsBound = customTextWordCount; + wordsBound = customText.word; } if ( config.mode == "custom" && - !customTextIsRandom && - customText.length < wordsBound + customText.isTimeRandom && + customText.time < wordsBound ) { - wordsBound = customText.length; + wordsBound = 100; + } + if ( + config.mode == "custom" && + !customText.isWordRandom && + customText.text.length < wordsBound + ) { + wordsBound = customText.text.length; } } + + if ( + (config.mode === "custom" && + customText.isWordRandom && + customText.word == 0) || + (config.mode === "custom" && + customText.isTimeRandom && + customText.time == 0) + ) { + wordsBound = 100; + } + if (config.mode === "words" && config.words === 0) { wordsBound = 100; } @@ -512,16 +579,19 @@ async function initWords() { } let wordset = language.words; if (config.mode == "custom") { - wordset = customText; + wordset = customText.text; } for (let i = 0; i < wordsBound; i++) { let randomWord = wordset[Math.floor(Math.random() * wordset.length)]; const previousWord = wordsList[i - 1]; const previousWord2 = wordsList[i - 2]; - if (config.mode == "custom" && customTextIsRandom) { + if ( + config.mode == "custom" && + (customText.isWordRandom || customText.isTimeRandom) + ) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; - } else if (config.mode == "custom" && !customTextIsRandom) { - randomWord = customText[i]; + } else if (config.mode == "custom" && !customText.isWordRandom) { + randomWord = customText.text[i]; } else { while ( randomWord == previousWord || @@ -556,15 +626,19 @@ async function initWords() { randomWord = Misc.getASCII(); } - if (config.punctuation && config.mode != "custom") { + if (config.punctuation) { randomWord = punctuateWord(previousWord, randomWord, i, wordsBound); } - if (config.numbers && config.mode != "custom") { + if (config.numbers) { if (Math.random() < 0.1) { randomWord = Misc.getNumbers(4); } } + if (/\t/g.test(randomWord)) { + textHasTab = true; + } + wordsList.push(randomWord); } } else if (config.mode == "quote") { @@ -572,6 +646,14 @@ async function initWords() { if (config.quoteLength === -1) { group = Math.floor(Math.random() * quotes.groups.length); + while (quotes.groups[group].length === 0) { + group = Math.floor(Math.random() * quotes.groups.length); + } + } else { + if (quotes.groups[group].length === 0) { + Notifications.add("No quotes found for selected quote length", 0); + return; + } } let rq = @@ -585,8 +667,17 @@ async function initWords() { ]; } randomQuote = rq; - let w = rq.text.trim().split(" "); + randomQuote.text = randomQuote.text.replace(/ +/gm, " "); + randomQuote.text = randomQuote.text.replace(/\\\\t/gm, "\t"); + randomQuote.text = randomQuote.text.replace(/\\\\n/gm, "\n"); + randomQuote.text = randomQuote.text.replace(/\\t/gm, "\t"); + randomQuote.text = randomQuote.text.replace(/\\n/gm, "\n"); + randomQuote.text = randomQuote.text.replace(/( *(\r\n|\r|\n) *)/g, "\n "); + let w = randomQuote.text.trim().split(" "); for (let i = 0; i < w.length; i++) { + if (/\t/g.test(w[i])) { + textHasTab = true; + } wordsList.push(w[i]); } } @@ -835,11 +926,12 @@ function addWord() { wordsList.length >= config.words && config.words > 0) || (config.mode === "custom" && - customTextIsRandom && - wordsList.length >= customTextWordCount) || + customText.isWordRandom && + wordsList.length >= customText.word && + customText.word != 0) || (config.mode === "custom" && - !customTextIsRandom && - wordsList.length >= customText.length) + !customText.isWordRandom && + wordsList.length >= customText.text.length) ) return; const language = @@ -848,7 +940,7 @@ function addWord() { : { //borrow the direction of the current language leftToRight: Misc.getCurrentLanguage().leftToRight, - words: customText, + words: customText.text, }; const wordset = language.words; let randomWord = wordset[Math.floor(Math.random() * wordset.length)]; @@ -860,10 +952,14 @@ function addWord() { .replace(/[.?!":\-,]/g, "") .toLowerCase(); - if (config.mode === "custom" && customTextIsRandom && wordset.length < 3) { + if ( + config.mode === "custom" && + customText.isWordRandom && + wordset.length < 3 + ) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; - } else if (config.mode == "custom" && !customTextIsRandom) { - randomWord = customText[wordsList.length]; + } else if (config.mode == "custom" && !customText.isWordRandom) { + randomWord = customText.text[wordsList.length]; } else { while ( previousWordStripped == randomWord || @@ -918,19 +1014,34 @@ function showWords() { $("#words").empty(); let wordsHTML = ""; + let newlineafter = false; for (let i = 0; i < wordsList.length; i++) { - wordsHTML += "
"; + newlineafter = false; + wordsHTML += `
`; for (let c = 0; c < wordsList[i].length; c++) { - wordsHTML += "" + wordsList[i].charAt(c) + ""; + if (wordsList[i].charAt(c) === "\t") { + wordsHTML += ``; + } else if (wordsList[i].charAt(c) === "\n") { + newlineafter = true; + wordsHTML += ``; + } else { + wordsHTML += "" + wordsList[i].charAt(c) + ""; + } } wordsHTML += "
"; + if (newlineafter) wordsHTML += "
"; } $("#words").html(wordsHTML); $("#wordsWrapper").removeClass("hidden"); const wordHeight = $(document.querySelector(".word")).outerHeight(true); const wordsHeight = $(document.querySelector("#words")).outerHeight(true); - if (config.showAllLines && config.mode != "time") { + if ( + config.showAllLines && + config.mode != "time" && + !(customText.isWordRandom && customText.word == 0) && + !customText.isTimeRandom + ) { $("#words").css("height", "auto"); $("#wordsWrapper").css("height", "auto"); let nh = wordHeight * 3; @@ -957,26 +1068,6 @@ function showWords() { updateCaretPosition(); } -$("#restartTestButton, #startTestButton").on("click", function () { - if (activeFunBox === "memory") { - memoryFunboxInterval = clearInterval(memoryFunboxInterval); - memoryFunboxTimer = Math.round(Math.pow(wordsList.length, 1.2)); - memoryFunboxInterval = setInterval(() => { - memoryFunboxTimer -= 1; - Misc.showNotification(memoryFunboxTimer); - if (memoryFunboxTimer < 0) { - memoryFunboxInterval = clearInterval(memoryFunboxInterval); - memoryFunboxTimer = null; - $("#wordsWrapper").addClass("hidden"); - } - }, 1000); - - if (config.keymapMode === "next") { - setKeymapMode("react"); - } - } -}); - (function (history) { var pushState = history.pushState; history.pushState = function (state) { @@ -1076,8 +1167,19 @@ function updateWordElement(showError) { } } catch (e) {} + let currentLetter = currentWord[i]; + let tabChar = ""; + let nlChar = ""; + if (currentLetter === "\t") { + tabChar = "tabChar"; + currentLetter = ``; + } else if (currentLetter === "\n") { + nlChar = "nlChar"; + currentLetter = ``; + } + if (charCorrect) { - ret += '' + currentWord[i] + ""; + ret += `${currentLetter}`; } else { // if (config.difficulty == "master") { // if (!resultVisible) { @@ -1085,23 +1187,23 @@ function updateWordElement(showError) { // } // } if (!showError) { - if (currentWord[i] == undefined) { + if (currentLetter == undefined) { } else { - ret += '' + currentWord[i] + ""; + ret += `${currentLetter}`; } } else { - if (currentWord[i] == undefined) { + if (currentLetter == undefined) { if (!config.hideExtraLetters) { let letter = input[i]; - if (letter == " ") { + if (letter == " " || letter == "\t" || letter == "\n") { letter = "_"; } - ret += `${letter}`; + ret += `${letter}`; } } else { ret += - '' + - currentWord[i] + + `` + + currentLetter + (config.indicateTypos ? `${input[i]}` : "") + ""; } @@ -1111,7 +1213,13 @@ function updateWordElement(showError) { if (input.length < currentWord.length) { for (let i = input.length; i < currentWord.length; i++) { - ret += "" + currentWord[i] + ""; + if (currentWord[i] === "\t") { + ret += ``; + } else if (currentWord[i] === "\n") { + ret += ``; + } else { + ret += "" + currentWord[i] + ""; + } } } } @@ -1206,9 +1314,16 @@ function restartTimer() { function updateTimer() { if (!config.showTimerProgress) return; - if (config.mode === "time") { + if ( + config.mode === "time" || + (config.mode === "custom" && customText.isTimeRandom) + ) { + let maxtime = config.time; + if (config.mode === "custom" && customText.isTimeRandom) { + maxtime = customText.time; + } if (config.timerStyle === "bar") { - let percent = 100 - ((time + 1) / config.time) * 100; + let percent = 100 - ((time + 1) / maxtime) * 100; $("#timer") .stop(true, true) .animate( @@ -1219,14 +1334,14 @@ function updateTimer() { "linear" ); } else if (config.timerStyle === "text") { - let displayTime = Misc.secondsToString(config.time - time); - if (config.time === 0) { + let displayTime = Misc.secondsToString(maxtime - time); + if (maxtime === 0) { displayTime = Misc.secondsToString(time); } $("#timerNumber").html("
" + displayTime + "
"); } else if (config.timerStyle === "mini") { - let displayTime = Misc.secondsToString(config.time - time); - if (config.time === 0) { + let displayTime = Misc.secondsToString(maxtime - time); + if (maxtime === 0) { displayTime = Misc.secondsToString(time); } $("#miniTimerAndLiveWpm .time").html(displayTime); @@ -1242,10 +1357,10 @@ function updateTimer() { outof = config.words; } if (config.mode === "custom") { - if (customTextIsRandom) { - outof = customTextWordCount; + if (customText.isWordRandom) { + outof = customText.word; } else { - outof = customText.length; + outof = customText.text.length; } } let percent = Math.floor(((currentWordIndex + 1) / outof) * 100); @@ -1263,13 +1378,13 @@ function updateTimer() { outof = config.words; } if (config.mode === "custom") { - if (customTextIsRandom) { - outof = customTextWordCount; + if (customText.isWordRandom) { + outof = customText.word; } else { - outof = customText.length; + outof = customText.text.length; } } - if (config.words === 0) { + if (outof === 0) { $("#timerNumber").html("
" + `${inputHistory.length}` + "
"); } else { $("#timerNumber").html( @@ -1282,10 +1397,10 @@ function updateTimer() { outof = config.words; } if (config.mode === "custom") { - if (customTextIsRandom) { - outof = customTextWordCount; + if (customText.isWordRandom) { + outof = customText.word; } else { - outof = customText.length; + outof = customText.text.length; } } if (config.words === 0) { @@ -1584,23 +1699,27 @@ function countChars() { let spaces = 0; let correctspaces = 0; for (let i = 0; i < inputHistory.length; i++) { + let word = config.mode == "zen" ? inputHistory[i] : wordsList[i]; if (inputHistory[i] === "") { //last word that was not started continue; } - if (config.mode == "zen" || inputHistory[i] == wordsList[i]) { + if (inputHistory[i] == word) { //the word is correct - correctWordChars += inputHistory[i].length; - correctChars += inputHistory[i].length; - if (i < inputHistory.length - 1) { + correctWordChars += word.length; + correctChars += word.length; + if ( + i < inputHistory.length - 1 && + Misc.getLastChar(inputHistory[i]) !== "\n" + ) { correctspaces++; } - } else if (inputHistory[i].length >= wordsList[i].length) { + } else if (inputHistory[i].length >= word.length) { //too many chars for (let c = 0; c < inputHistory[i].length; c++) { - if (c < wordsList[i].length) { + if (c < word.length) { //on char that still has a word list pair - if (inputHistory[i][c] == wordsList[i][c]) { + if (inputHistory[i][c] == word[c]) { correctChars++; } else { incorrectChars++; @@ -1617,10 +1736,10 @@ function countChars() { incorrect: 0, missed: 0, }; - for (let c = 0; c < wordsList[i].length; c++) { + for (let c = 0; c < word.length; c++) { if (c < inputHistory[i].length) { //on char that still has a word list pair - if (inputHistory[i][c] == wordsList[i][c]) { + if (inputHistory[i][c] == word[c]) { toAdd.correct++; } else { toAdd.incorrect++; @@ -2032,11 +2151,11 @@ function showResult(difficultyFailed = false) { if (bailout) afkDetected = false; if (difficultyFailed) { - Misc.showNotification("Test failed", 2000); + Notifications.add("Test failed", 0); } else if (afkDetected) { - Misc.showNotification("Test invalid - AFK detected", 2000); + Notifications.add("Test invalid - AFK detected", 0); } else if (sameWordset) { - Misc.showNotification("Test invalid - repeated", 2000); + Notifications.add("Test invalid - repeated", 0); } else { let activeTags = []; let activeTagsIds = []; @@ -2061,11 +2180,7 @@ function showResult(difficultyFailed = false) { keypressStats.duration.array = "toolong"; } - // REMOVE THIS WHEN MULTI LANGUAGE QUOTES ARE ADDED let lang = config.language; - if (config.mode === "quote") { - lang = "english"; - } let quoteLength = -1; if (config.mode === "quote") { @@ -2200,14 +2315,33 @@ function showResult(difficultyFailed = false) { $("#result .stats .tags").removeClass("hidden"); } $("#result .stats .tags .bottom").text(""); + let annotationSide = "left"; activeTags.forEach(async (tag) => { - let tpb = await db_getLocalTagPB(tag.id); + let tpb = await db_getLocalTagPB( + tag.id, + config.mode, + mode2, + config.punctuation, + config.language, + config.difficulty + ); $("#result .stats .tags .bottom").append(`
${tag.name}
`); if (tpb < stats.wpm) { //new pb for that tag - db_saveLocalTagPB(tag.id, stats.wpm); + db_saveLocalTagPB( + tag.id, + config.mode, + mode2, + config.punctuation, + config.language, + config.difficulty, + stats.wpm, + stats.acc, + stats.wpmRaw, + consistency + ); $( `#result .stats .tags .bottom div[tagid="${tag.id}"] .fas` ).removeClass("hidden"); @@ -2235,11 +2369,16 @@ function showResult(difficultyFailed = false) { xPadding: 6, yPadding: 6, cornerRadius: 3, - position: "center", + position: annotationSide, enabled: true, content: `${tag.name} PB: ${tpb}`, }, }); + if (annotationSide === "left") { + annotationSide = "right"; + } else { + annotationSide = "left"; + } } }); @@ -2250,35 +2389,35 @@ function showResult(difficultyFailed = false) { .then((e) => { accountIconLoading(false); if (e.data == null) { - Misc.showNotification( - "Unexpected response from the server.", - 4000 + Notifications.add( + "Unexpected response from the server: " + e.data, + -1 ); return; } if (e.data.resultCode === -1) { - Misc.showNotification("Could not save result", 3000); + Notifications.add("Could not save result", -1); } else if (e.data.resultCode === -2) { - Misc.showNotification( + Notifications.add( "Possible bot detected. Result not saved.", - 4000 + -1 ); } else if (e.data.resultCode === -3) { - Misc.showNotification( + Notifications.add( "Could not verify keypress stats. Result not saved.", - 4000 + -1 ); } else if (e.data.resultCode === -4) { - Misc.showNotification( + Notifications.add( "Result data does not make sense. Result not saved.", - 4000 + -1 ); } else if (e.data.resultCode === -999) { console.error("internal error: " + e.data.message); - Misc.showNotification( + Notifications.add( "Internal error. Result might not be saved. " + e.data.message, - 6000 + -1 ); } else if (e.data.resultCode === 1 || e.data.resultCode === 2) { completedEvent.id = e.data.createdId; @@ -2451,7 +2590,7 @@ function showResult(difficultyFailed = false) { // }).then((d) => { // if (d.data.returnCode === 1) { // } else { - // Misc.showNotification( + // Notifications.add( // `Error saving lb memory ${d.data.message}`, // 4000 // ); @@ -2503,7 +2642,7 @@ function showResult(difficultyFailed = false) { } else if (e.data.resultCode === 1) { hideCrown(); // if (localPb) { - // Misc.showNotification( + // Notifications.add( // "Local PB data is out of sync! Refresh the page to resync it or contact Miodec on Discord.", // 15000 // ); @@ -2513,7 +2652,7 @@ function showResult(difficultyFailed = false) { }) .catch((e) => { console.error(e); - Misc.showNotification("Could not save result. " + e, 5000); + Notifications.add("Could not save result. " + e, -1); }); }); }); @@ -2526,7 +2665,7 @@ function showResult(difficultyFailed = false) { notSignedInLastResult = completedEvent; } } else { - Misc.showNotification("Test invalid", 3000); + Notifications.add("Test invalid", 0); testInvalid = true; try { firebase.analytics().logEvent("testCompletedInvalid", completedEvent); @@ -2704,7 +2843,10 @@ function startTest() { timer = setTimeout(function () { time++; $(".pageTest #premidSecondsLeft").text(config.time - time); - if (config.mode === "time") { + if ( + config.mode === "time" || + (config.mode === "custom" && customText.isTimeRandom) + ) { updateTimer(); } let wpmAndRaw = liveWpmAndRaw(); @@ -2718,8 +2860,6 @@ function startTest() { 100 ); - updateLiveAcc(acc); - if (activeFunBox === "layoutfluid" && config.mode === "time") { const layouts = ["qwerty", "dvorak", "colemak"]; let index = 0; @@ -2729,23 +2869,23 @@ function startTest() { time == Math.floor(config.time / 3) - 3 || time == (config.time / 3) * 2 - 3 ) { - Misc.showNotification("3", 1000); + Notifications.add("3", 0, 1); } if ( time == Math.floor(config.time / 3) - 2 || time == Math.floor(config.time / 3) * 2 - 2 ) { - Misc.showNotification("2", 1000); + Notifications.add("2", 0, 1); } if ( time == Math.floor(config.time / 3) - 1 || time == Math.floor(config.time / 3) * 2 - 1 ) { - Misc.showNotification("1", 1000); + Notifications.add("1", 0, 1); } if (config.layout !== layouts[index] && layouts[index] !== undefined) { - Misc.showNotification(`--- !!! ${layouts[index]} !!! ---`, 3000); + Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); } setLayout(layouts[index]); setKeymapLayout(layouts[index]); @@ -2774,8 +2914,18 @@ function startTest() { failTest(); return; } - if (config.mode == "time") { - if (time >= config.time && config.time !== 0) { + if ( + config.mode == "time" || + (config.mode === "custom" && customText.isTimeRandom) + ) { + if ( + (time >= config.time && + config.time !== 0 && + config.mode === "time") || + (time >= customText.time && + customText.time !== 0 && + config.mode === "custom") + ) { //times up clearTimeout(timer); hideCaret(); @@ -2798,17 +2948,22 @@ function restartTest(withSameWordset = false, nosave = false) { (config.mode === "time" && config.time < 3600 && config.time > 0) || config.mode === "quote" || (config.mode === "custom" && - customTextIsRandom && - customTextWordCount < 1000) || + customText.isWordRandom && + customText.word < 1000 && + customText.word != 0) || (config.mode === "custom" && - !customTextIsRandom && - customText.length < 1000) + customText.isTimeRandom && + customText.time < 3600 && + customText.time != 0) || + (config.mode === "custom" && + !customText.isWordRandom && + customText.text.length < 1000) ) { } else { if (testActive) { - Misc.showNotification( + Notifications.add( "Restart disabled for long tests. Use your mouse to confirm.", - 4000 + 0 ); return; } @@ -2820,9 +2975,13 @@ function restartTest(withSameWordset = false, nosave = false) { } if (modeBeforePractise !== null && !withSameWordset) { - Misc.showNotification("Reverting to previous settings.", 1500); + Notifications.add("Reverting to previous settings.", 0); setMode(modeBeforePractise); + setPunctuation(punctuationBeforePractise); + setNumbers(numbersBeforePractise); modeBeforePractise = null; + punctuationBeforePractise = null; + numbersBeforePractise = null; } manualRestart = false; @@ -2897,6 +3056,7 @@ function restartTest(withSameWordset = false, nosave = false) { $("#typingTest").css("opacity", 0).removeClass("hidden"); if (!withSameWordset) { sameWordset = false; + textHasTab = false; await initWords(); initPaceCaret(nosave); } else { @@ -2923,6 +3083,28 @@ function restartTest(withSameWordset = false, nosave = false) { document.querySelector("#liveWpm").innerHTML = "0"; document.querySelector("#liveAcc").innerHTML = "100%"; + if (activeFunBox === "memory") { + memoryFunboxInterval = clearInterval(memoryFunboxInterval); + memoryFunboxTimer = Math.round(Math.pow(wordsList.length, 1.2)); + memoryFunboxInterval = setInterval(() => { + memoryFunboxTimer -= 1; + Notifications.add( + memoryFunboxTimer == 0 ? "Times up" : memoryFunboxTimer, + 0, + 1 + ); + if (memoryFunboxTimer <= 0) { + memoryFunboxInterval = clearInterval(memoryFunboxInterval); + memoryFunboxTimer = null; + $("#wordsWrapper").addClass("hidden"); + } + }, 1000); + + if (config.keymapMode === "next") { + setKeymapMode("react"); + } + } + let mode2 = ""; if (config.mode === "time") { mode2 = config.time; @@ -2984,17 +3166,14 @@ function focusWords() { } function setCustomText() { - customText = prompt("Custom text").trim(); - customText = customText.replace(/[\n\r\t ]/gm, " "); - customText = customText.replace(/ +/gm, " "); - customText = customText.split(" "); - if (customText.length >= 10000) { - Misc.showNotification( - "Custom text cannot be longer than 10000 words.", - 4000 - ); + customText.text = prompt("Custom text").trim(); + customText.text = customText.text.replace(/[\n\r\t ]/gm, " "); + customText.text = customText.text.replace(/ +/gm, " "); + customText.text = customText.text.split(" "); + if (customText.text.text.length >= 10000) { + Notifications.add("Custom text cannot be longer than 10000 words.", 0); setMode("time"); - customText = "The quick brown fox jumped over the lazy dog".split(" "); + customText.text = "The quick brown fox jumped over the lazy dog".split(" "); } } @@ -3074,7 +3253,7 @@ function changePage(page) { function setMode(mode, nosave) { if (mode !== "words" && activeFunBox === "memory") { - Misc.showNotification("Memory funbox can only be used with words mode."); + Notifications.add("Memory funbox can only be used with words mode.", 0); return; } @@ -3112,8 +3291,8 @@ function setMode(mode, nosave) { $("#top .config .wordCount").addClass("hidden"); $("#top .config .time").addClass("hidden"); $("#top .config .customText").removeClass("hidden"); - $("#top .config .punctuationMode").addClass("hidden"); - $("#top .config .numbersMode").addClass("hidden"); + $("#top .config .punctuationMode").removeClass("hidden"); + $("#top .config .numbersMode").removeClass("hidden"); $("#top .config .quoteLength").addClass("hidden"); setPunctuation(false, true); setNumbers(false, true); @@ -3144,11 +3323,15 @@ function liveWpmAndRaw() { let correctWordChars = 0; let spaces = 0; for (let i = 0; i < inputHistory.length; i++) { - if (config.mode == "zen" || inputHistory[i] == wordsList[i]) { + let word = config.mode == "zen" ? inputHistory[i] : wordsList[i]; + if (inputHistory[i] == word) { //the word is correct //+1 for space - correctWordChars += inputHistory[i].length; - if (i < inputHistory.length) { + correctWordChars += word.length; + if ( + i < inputHistory.length - 1 && + Misc.getLastChar(inputHistory[i]) !== "\n" + ) { spaces++; } } @@ -3426,6 +3609,7 @@ async function loadWordsHistory() { let wordsHTML = ""; for (let i = 0; i < inputHistory.length + 2; i++) { let input = inputHistory[i]; + let word = wordsList[i]; let wordEl = ""; try { if (input === "") throw new Error("empty input word"); @@ -3440,34 +3624,34 @@ async function loadWordsHistory() { } if (i === inputHistory.length - 1) { //last word - let word = { + let wordstats = { correct: 0, incorrect: 0, missed: 0, }; - let length = config.mode == "zen" ? input.length : wordsList[i].length; + let length = config.mode == "zen" ? input.length : word.length; for (let c = 0; c < length; c++) { - if (c < inputHistory[i].length) { + if (c < input.length) { //on char that still has a word list pair - if (config.mode == "zen" || inputHistory[i][c] == wordsList[i][c]) { - word.correct++; + if (config.mode == "zen" || input[c] == word[c]) { + wordstats.correct++; } else { - word.incorrect++; + wordstats.incorrect++; } } else { //on char that is extra - word.missed++; + wordstats.missed++; } } - if (word.incorrect !== 0 || config.mode !== "time") { - if (config.mode != "zen" && input !== wordsList[i]) { + if (wordstats.incorrect !== 0 || config.mode !== "time") { + if (config.mode != "zen" && input !== word) { wordEl = `
`; } } } else { - if (config.mode != "zen" && input !== wordsList[i]) { + if (config.mode != "zen" && input !== word) { wordEl = `
`; @@ -3475,12 +3659,12 @@ async function loadWordsHistory() { } let loop; - if (config.mode == "zen" || input.length > wordsList[i].length) { + if (config.mode == "zen" || input.length > word.length) { //input is longer - extra characters possible (loop over input) loop = input.length; } else { //input is shorter or equal (loop over word list) - loop = wordsList[i].length; + loop = word.length; } for (let c = 0; c < loop; c++) { @@ -3498,8 +3682,8 @@ async function loadWordsHistory() { ) { extraCorrected = "extraCorrected"; } - if (config.mode == "zen" || wordsList[i][c] !== undefined) { - if (config.mode == "zen" || input[c] === wordsList[i][c]) { + if (config.mode == "zen" || wor[c] !== undefined) { + if (config.mode == "zen" || input[c] === word[c]) { if (correctedChar === input[c] || correctedChar === undefined) { wordEl += `${input[c]}`; } else { @@ -3511,13 +3695,15 @@ async function loadWordsHistory() { } else { if (input[c] === currentInput) { wordEl += - "" + wordsList[i][c] + ""; + `` + + word[c] + + ""; } else if (input[c] === undefined) { - wordEl += "" + wordsList[i][c] + ""; + wordEl += "" + word[c] + ""; } else { wordEl += `` + - wordsList[i][c] + + word[c] + ""; } } @@ -3529,8 +3715,8 @@ async function loadWordsHistory() { } catch (e) { try { wordEl = "
"; - for (let c = 0; c < wordsList[i].length; c++) { - wordEl += "" + wordsList[i][c] + ""; + for (let c = 0; c < word.length; c++) { + wordEl += "" + word[c] + ""; } wordEl += "
"; } catch {} @@ -3622,6 +3808,12 @@ function updateTestModesNotice() { ); } + if (textHasTab) { + $(".pageTest #testModesNotice").append( + `
shift + tab to restart
` + ); + } + if (config.language === "english_1k" || config.language === "english_10k") { $(".pageTest #testModesNotice").append( `
${config.language.replace( @@ -3771,7 +3963,7 @@ function tagsEdit() { hideBackgroundLoader(); let status = e.data.resultCode; if (status === 1) { - Misc.showNotification("Tag added", 2000); + Notifications.add("Tag added", 1, 2); db_getSnapshot().tags.push({ name: inputVal, id: e.data.id, @@ -3780,9 +3972,9 @@ function tagsEdit() { updateSettingsPage(); updateFilterTags(); } else if (status === -1) { - Misc.showNotification("Invalid tag name", 3000); + Notifications.add("Invalid tag name", 0); } else if (status < -1) { - Misc.showNotification("Unknown error", 3000); + Notifications.add("Unknown error: " + e.data.message, -1); } }); } else if (action === "edit") { @@ -3795,7 +3987,7 @@ function tagsEdit() { hideBackgroundLoader(); let status = e.data.resultCode; if (status === 1) { - Misc.showNotification("Tag updated", 2000); + Notifications.add("Tag updated", 1); db_getSnapshot().tags.forEach((tag) => { if (tag.id === tagid) { tag.name = inputVal; @@ -3805,9 +3997,9 @@ function tagsEdit() { updateSettingsPage(); updateFilterTags(); } else if (status === -1) { - Misc.showNotification("Invalid tag name", 3000); + Notifications.add("Invalid tag name", 0); } else if (status < -1) { - Misc.showNotification("Unknown error", 3000); + Notifications.add("Unknown error: " + e.data.message, -1); } }); } else if (action === "remove") { @@ -3819,7 +4011,7 @@ function tagsEdit() { hideBackgroundLoader(); let status = e.data.resultCode; if (status === 1) { - Misc.showNotification("Tag removed", 2000); + Notifications.add("Tag removed", 1); db_getSnapshot().tags.forEach((tag, index) => { if (tag.id === tagid) { db_getSnapshot().tags.splice(index, 1); @@ -3829,7 +4021,7 @@ function tagsEdit() { updateSettingsPage(); updateFilterTags(); } else if (status < -1) { - Misc.showNotification("Unknown error", 3000); + Notifications.add("Unknown error: " + e.data.message, -1); } }); } @@ -3850,17 +4042,20 @@ function hideCapsWarning() { function showCustomTextPopup() { if ($("#customTextPopupWrapper").hasClass("hidden")) { if ($("#customTextPopup .check input").prop("checked")) { - $("#customTextPopup .inputs .wordcount").removeClass("hidden"); + $("#customTextPopup .inputs .randomInputFields").removeClass("hidden"); } else { - $("#customTextPopup .inputs .wordcount").addClass("hidden"); + $("#customTextPopup .inputs .randomInputFields").addClass("hidden"); } $("#customTextPopupWrapper") .stop(true, true) .css("opacity", 0) .removeClass("hidden") .animate({ opacity: 1 }, 100, () => { - $("#customTextPopup textarea").val(customText.join(" ")); - $("#customTextPopup .wordcount input").val(customTextWordCount); + let newtext = customText.text.join(" "); + newtext = newtext.replace(/\n /g, "\n"); + $("#customTextPopup textarea").val(newtext); + $("#customTextPopup .wordcount input").val(customText.word); + $("#customTextPopup .time input").val(customText.time); $("#customTextPopup textarea").focus(); }); } @@ -3891,9 +4086,9 @@ $("#customTextPopupWrapper").mousedown((e) => { $("#customTextPopup .inputs .check input").change(() => { if ($("#customTextPopup .check input").prop("checked")) { - $("#customTextPopup .inputs .wordcount").removeClass("hidden"); + $("#customTextPopup .inputs .randomInputFields").removeClass("hidden"); } else { - $("#customTextPopup .inputs .wordcount").addClass("hidden"); + $("#customTextPopup .inputs .randomInputFields").addClass("hidden"); } }); @@ -3903,20 +4098,81 @@ $("#customTextPopup textarea").keypress((e) => { } }); +$("#customTextPopup .randomInputFields .wordcount input").keypress((e) => { + $("#customTextPopup .randomInputFields .time input").val(""); +}); + +$("#customTextPopup .randomInputFields .time input").keypress((e) => { + $("#customTextPopup .randomInputFields .wordcount input").val(""); +}); + $("#customTextPopup .button").click(() => { let text = $("#customTextPopup textarea").val(); text = text.trim(); - text = text.replace(/[\n\r\t ]/gm, " "); + // text = text.replace(/[\r]/gm, " "); + text = text.replace(/\\\\t/gm, "\t"); + text = text.replace(/\\\\n/gm, "\n"); + text = text.replace(/\\t/gm, "\t"); + text = text.replace(/\\n/gm, "\n"); text = text.replace(/ +/gm, " "); + // text = text.replace(/(\r\n)+/g, "\r\n"); + // text = text.replace(/(\n)+/g, "\n"); + // text = text.replace(/(\r)+/g, "\r"); + text = text.replace(/( *(\r\n|\r|\n) *)/g, "\n "); if ($("#customTextPopup .typographyCheck input").prop("checked")) { text = Misc.cleanTypographySymbols(text); } // text = Misc.remove_non_ascii(text); text = text.replace(/[\u2060]/g, ""); text = text.split(" "); - customText = text; - customTextIsRandom = $("#customTextPopup .check input").prop("checked"); - customTextWordCount = $("#customTextPopup .wordcount input").val(); + customText.text = text; + customText.word = parseInt($("#customTextPopup .wordcount input").val()); + customText.time = parseInt($("#customTextPopup .time input").val()); + + customText.isWordRandom = + $("#customTextPopup .check input").prop("checked") && + !isNaN(customText.word); + customText.isTimeRandom = + $("#customTextPopup .check input").prop("checked") && + !isNaN(customText.time); + + if ( + isNaN(customText.word) && + isNaN(customText.time) && + (customText.isTimeRandom || customText.isWordRandom) + ) { + Notifications.add( + "You need to specify word count or time in seconds to start a random custom test.", + 0, + 5 + ); + return; + } + + if ( + !isNaN(customText.word) && + !isNaN(customText.time) && + (customText.isTimeRandom || customText.isWordRandom) + ) { + Notifications.add( + "You need to pick between word count or time in seconds to start a random custom test.", + 0, + 5 + ); + return; + } + + if ( + (customText.isWordRandom && parseInt(customText.word) === 0) || + (customText.isTimeRandom && parseInt(customText.time) === 0) + ) { + Notifications.add( + "Infinite words! Make sure to use Bail Out from the command line to save your result.", + 0, + 7 + ); + } + manualRestart = true; restartTest(); hideCustomTextPopup(); @@ -4242,15 +4498,16 @@ function applyMode2Popup() { manualRestart = true; restartTest(); if (val >= 1800) { - Misc.showNotification("Stay safe and take breaks!", 3000); + Notifications.add("Stay safe and take breaks!", 0); } else if (val == 0) { - Misc.showNotification( + Notifications.add( "Infinite time! Make sure to use Bail Out from the command line to save your result.", - 5000 + 0, + 7 ); } } else { - Misc.showNotification("Custom time must be at least 1", 3000); + Notifications.add("Custom time must be at least 1", 0); } } else if (mode == "words") { if (val !== null && !isNaN(val) && val >= 0) { @@ -4258,15 +4515,16 @@ function applyMode2Popup() { manualRestart = true; restartTest(); if (val > 2000) { - Misc.showNotification("Stay safe and take breaks!", 3000); + Notifications.add("Stay safe and take breaks!", 0); } else if (val == 0) { - Misc.showNotification( + Notifications.add( "Infinite words! Make sure to use Bail Out from the command line to save your result.", - 5000 + 0, + 7 ); } } else { - Misc.showNotification("Custom word amount must be at least 1", 3000); + Notifications.add("Custom word amount must be at least 1", 0); } } @@ -4436,11 +4694,14 @@ $(document).on("keypress", "#restartTestButton", (event) => { (config.mode === "time" && config.time < 3600) || config.mode === "quote" || (config.mode === "custom" && - customTextIsRandom && - customTextWordCount < 1000) || + customText.isWordRandom && + customText.word < 1000) || (config.mode === "custom" && - !customTextIsRandom && - customText.length < 1000) + customText.isTimeRandom && + customText.time < 3600) || + (config.mode === "custom" && + !customText.isWordRandom && + customText.text.length < 1000) ) { if (testActive) { let testNow = performance.now(); @@ -4454,7 +4715,7 @@ $(document).on("keypress", "#restartTestButton", (event) => { if (resultCalculating) return; restartTest(); } else { - Misc.showNotification("Quick restart disabled for long tests", 2000); + Notifications.add("Quick restart disabled for long tests", 0); } } }); @@ -4466,7 +4727,13 @@ $(document.body).on("click", "#restartTestButton", () => { }); function initPractiseMissedWords() { - let currentMode = config.mode; + let mode = modeBeforePractise === null ? config.mode : modeBeforePractise; + let punctuation = + punctuationBeforePractise === null + ? config.punctuation + : punctuationBeforePractise; + let numbers = + numbersBeforePractise === null ? config.numbers : numbersBeforePractise; setMode("custom"); let newCustomText = []; Object.keys(missedWords).forEach((missedWord) => { @@ -4474,13 +4741,17 @@ function initPractiseMissedWords() { newCustomText.push(missedWord); } }); - customText = newCustomText; - customTextIsRandom = true; - customTextWordCount = 50; - let mode = modeBeforePractise === null ? currentMode : modeBeforePractise; + customText.text = newCustomText; + customText.isWordRandom = true; + customText.word = 50; + modeBeforePractise = null; + punctuationBeforePractise = null; + numbersBeforePractise = null; restartTest(); modeBeforePractise = mode; + punctuationBeforePractise = punctuation; + numbersBeforePractise = numbers; } $(document).on("keypress", "#practiseMissedWordsButton", (event) => { @@ -4488,7 +4759,7 @@ $(document).on("keypress", "#practiseMissedWordsButton", (event) => { if (Object.keys(missedWords).length > 0) { initPractiseMissedWords(); } else { - Misc.showNotification("You haven't missed any words.", 2000); + Notifications.add("You haven't missed any words.", 0); } } }); @@ -4497,7 +4768,7 @@ $(document.body).on("click", "#practiseMissedWordsButton", () => { if (Object.keys(missedWords).length > 0) { initPractiseMissedWords(); } else { - Misc.showNotification("You haven't missed any words.", 2000); + Notifications.add("You haven't missed any words.", 0); } }); @@ -4678,16 +4949,36 @@ $(document).keydown(function (event) { Monkey.type(); + //autofocus + let pageTestActive = !$(".pageTest").hasClass("hidden"); + let commandLineVisible = !$("#commandLineWrapper").hasClass("hidden"); + let wordsFocused = $("#wordsInput").is(":focus"); + let modePopupVisible = + !$("#customTextPopupWrapper").hasClass("hidden") || + !$("#customMode2PopupWrapper").hasClass("hidden"); + if ( + pageTestActive && + !commandLineVisible && + !modePopupVisible && + !resultVisible && + !wordsFocused && + event.key !== "Enter" + ) { + focusWords(); + if (config.showOutOfFocusWarning) return; + } + //tab if ( (event.key == "Tab" && !config.swapEscAndTab) || (event.key == "Escape" && config.swapEscAndTab) ) { handleTab(event); + // event.preventDefault(); } //blocking firefox from going back in history with backspace - if (event.key === "Backspace") { + if (event.key === "Backspace" && wordsFocused) { let t = /INPUT|SELECT|TEXTAREA/i; if ( !t.test(event.target.tagName) || @@ -4710,44 +5001,64 @@ $(document).keydown(function (event) { } } catch {} - //autofocus - let pageTestActive = !$(".pageTest").hasClass("hidden"); - let commandLineVisible = !$("#commandLineWrapper").hasClass("hidden"); - let wordsFocused = $("#wordsInput").is(":focus"); - let modePopupVisible = - !$("#customTextPopupWrapper").hasClass("hidden") || - !$("#customMode2PopupWrapper").hasClass("hidden"); - if (pageTestActive && !commandLineVisible && !modePopupVisible) { - if (!wordsFocused && event.key !== "Enter") { - focusWords(); - if (config.showOutOfFocusWarning) return; - } - } else { - return; - } - //backspace const isBackspace = event.key === "Backspace" || (config.capsLockBackspace && event.key === "CapsLock"); - if (isBackspace) { + if (isBackspace && wordsFocused) { handleBackspace(event); } - //space - if (event.key === " " || (activeFunBox == "58008" && event.key === "Enter")) { - handleSpace(event); + if (event.key === "Enter" && activeFunBox === "58008" && wordsFocused) { + event.key = " "; } - handleAlpha(event); + //space or enter + if (event.key === " " && wordsFocused) { + handleSpace(event, false); + } + + if (wordsFocused && !commandLineVisible) { + handleAlpha(event); + } + + let acc = Misc.roundTo2( + (accuracyStats.correct / + (accuracyStats.correct + accuracyStats.incorrect)) * + 100 + ); + updateLiveAcc(acc); }); function handleTab(event) { if (resultCalculating) { event.preventDefault(); } - if ( + if ($("#customTextPopup .textarea").is(":focus")) { + event.preventDefault(); + + let area = $("#customTextPopup .textarea")[0]; + + var start = area.selectionStart; + var end = area.selectionEnd; + + // set textarea value to: text before caret + tab + text after caret + area.value = + area.value.substring(0, start) + "\t" + area.value.substring(end); + + // put caret at right position again + area.selectionStart = area.selectionEnd = start + 1; + + // event.preventDefault(); + // $("#customTextPopup .textarea").val( + // $("#customTextPopup .textarea").val() + "\t" + // ); + return; + } else if ( !event.ctrlKey && + ((!event.shiftKey && !textHasTab) || + (event.shiftKey && textHasTab) || + resultVisible) && config.quickTab && !$(".pageLogin").hasClass("active") && !resultCalculating && @@ -4761,11 +5072,14 @@ function handleTab(event) { (config.mode === "time" && config.time < 3600) || config.mode === "quote" || (config.mode === "custom" && - customTextIsRandom && - customTextWordCount < 1000) || + customText.isWordRandom && + customText.word < 1000) || (config.mode === "custom" && - !customTextIsRandom && - customText.length < 1000) + customText.isTimeRandom && + customText.time < 3600) || + (config.mode === "custom" && + !customText.isWordRandom && + customText.text.length < 1000) ) { if (testActive) { let testNow = performance.now(); @@ -4778,11 +5092,19 @@ function handleTab(event) { } restartTest(); } else { - Misc.showNotification("Quick restart disabled for long tests", 2000); + Notifications.add("Quick restart disabled for long tests", 0); } } else { changePage("test"); } + } else if ( + !config.quickTab && + textHasTab && + event.shiftKey && + !resultVisible + ) { + event.preventDefault(); + $("#restartTestButton").focus(); } } @@ -4794,6 +5116,7 @@ function handleBackspace(event) { inputHistory.length > 0 && currentWordElementIndex > 0 ) { + //if nothing is inputted and its not the first word if ( (inputHistory[currentWordIndex - 1] == wordsList[currentWordIndex - 1] && !config.freedomMode) || @@ -4856,9 +5179,12 @@ function handleBackspace(event) { updateCaretPosition(); } -function handleSpace(event) { +function handleSpace(event, isEnter) { if (!testActive) return; if (currentInput === "") return; + let nextWord = wordsList[currentWordIndex + 1]; + // if ((isEnter && nextWord !== "\n") && (isEnter && activeFunBox !== "58008")) return; + // if (!isEnter && nextWord === "\n") return; event.preventDefault(); if (config.mode == "zen") { @@ -4873,7 +5199,7 @@ function handleSpace(event) { let outof = wordsList.length; index = Math.floor((inputHistory.length + 1) / (outof / 3)); if (config.layout !== layouts[index] && layouts[index] !== undefined) { - Misc.showNotification(`--- !!! ${layouts[index]} !!! ---`, 3000); + Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); } setLayout(layouts[index]); setKeymapLayout(layouts[index]); @@ -4923,7 +5249,7 @@ function handleSpace(event) { } accuracyStats.incorrect++; let cil = currentInput.length; - if (cil < wordsList[currentWordIndex].length) { + if (cil <= wordsList[currentWordIndex].length) { if (cil >= currentCorrected.length) { currentCorrected += "_"; } else { @@ -4969,7 +5295,12 @@ function handleSpace(event) { correctedHistory.push(currentCorrected); currentCorrected = ""; - if (!config.showAllLines || config.mode == "time") { + if ( + !config.showAllLines || + config.mode == "time" || + (customText.isWordRandom && customText.word == 0) || + customText.isTimeRandom + ) { let currentTop = Math.floor( document.querySelectorAll("#words .word")[currentWordElementIndex - 1] .offsetTop @@ -5015,7 +5346,6 @@ function handleSpace(event) { function handleAlpha(event) { if ( [ - "Tab", "ContextMenu", "Escape", "Shift", @@ -5025,7 +5355,6 @@ function handleAlpha(event) { "AltGraph", "CapsLock", "Backspace", - "Enter", "PageUp", "PageDown", "Home", @@ -5047,6 +5376,8 @@ function handleAlpha(event) { "SymbolLock", "Super", "Unidentified", + "Process", + "Delete", undefined, ].includes(event.key) ) { @@ -5067,11 +5398,28 @@ function handleAlpha(event) { } } - if (event.key.length > 1) return; + if (event.key === "Tab") { + if (!textHasTab || (textHasTab && event.shiftKey)) { + return; + } + event.key = "\t"; + event.preventDefault(); + } + + if (event.key === "Enter") { + event.key = "\n"; + } + + // if (event.key.length > 1) return; if (/F\d+/.test(event.key)) return; if (/Numpad/.test(event.key)) return; if (/Volume/.test(event.key)) return; - if (event.ctrlKey && !event.altKey) return; + if (/Media/.test(event.key)) return; + if ( + event.ctrlKey != event.altKey && + (event.ctrlKey || /Linux/.test(window.navigator.platform)) + ) + return; if (event.metaKey) return; event = emulateLayout(event); @@ -5231,8 +5579,9 @@ function handleAlpha(event) { //simulate space press in nospace funbox if ( - activeFunBox === "nospace" && - currentInput.length === wordsList[currentWordIndex].length + (activeFunBox === "nospace" && + currentInput.length === wordsList[currentWordIndex].length) || + (event.key === "\n" && thisCharCorrect) ) { $.event.trigger({ type: "keydown", @@ -5274,11 +5623,14 @@ window.addEventListener("beforeunload", (event) => { (config.mode === "time" && config.time < 3600) || config.mode === "quote" || (config.mode === "custom" && - customTextIsRandom && - customTextWordCount < 1000) || + customText.isWordRandom && + customText.word < 1000) || (config.mode === "custom" && - !customTextIsRandom && - customText.length < 1000) + customText.isTimeRandom && + customText.time < 1000) || + (config.mode === "custom" && + !customText.isWordRandom && + customText.text.length < 1000) ) { } else { if (testActive) { @@ -5299,7 +5651,7 @@ if (firebase.app().options.projectId === "monkey-type-dev-67af4") { if (window.location.hostname === "localhost") { window.onerror = function (error) { - Misc.showNotification(error, 3000); + Notifications.add(error, -1); }; $("#top .logo .top").text("localhost"); $("head title").text($("head title").text() + " (localhost)"); @@ -5318,7 +5670,11 @@ $(document).on("mouseenter", "#resultWordsHistory .words .word", (e) => { if (resultVisible) { let input = $(e.currentTarget).attr("input"); if (input != undefined) - $(e.currentTarget).append(`
${input}
`); + $(e.currentTarget).append( + `
${input + .replace(/\t/g, "_") + .replace(/\n/g, "_")}
` + ); } }); @@ -5397,23 +5753,25 @@ $(".merchBanner a").click((event) => { $(".merchBanner .fas").click((event) => { $(".merchBanner").remove(); Misc.setCookie("merchbannerclosed", true, 365); - Misc.showNotification( - "Won't remind you anymore :) Thanks for continued support <3", - 2500 + Notifications.add( + "Won't remind you anymore. Thanks for continued support <3", + 0, + 5 ); }); $(".pageTest #copyWordsListButton").click(async (event) => { try { let words; - if (config.mode == "zen") words = inputHistory.join(" "); - if (config.mode != "zen") + if (config.mode == "zen") { + words = inputHistory.join(" "); + } else { words = wordsList.slice(0, inputHistory.length).join(" "); - + } await navigator.clipboard.writeText(words); - Misc.showNotification("Copied to clipboard", 1000); + Notifications.add("Copied to clipboard", 0, 2); } catch (e) { - Misc.showNotification("Could not copy to clipboard: " + e, 5000); + Notifications.add("Could not copy to clipboard: " + e, -1); } }); @@ -5447,9 +5805,9 @@ async function setupChallenge(challengeName) { setMode("words", true); setDifficulty("normal", true); } else if (challenge.type === "customText") { - customText = challenge.parameters[0].split(" "); - customTextIsRandom = challenge.parameters[1]; - customTextWordCount = challenge.parameters[2]; + customText.text = challenge.parameters[0].split(" "); + customText.isWordRandom = challenge.parameters[1]; + customText.word = parseInt(challenge.parameters[2]); setMode("custom", true); setDifficulty("normal", true); } else if (challenge.type === "script") { @@ -5458,8 +5816,8 @@ async function setupChallenge(challengeName) { let text = scriptdata.trim(); text = text.replace(/[\n\r\t ]/gm, " "); text = text.replace(/ +/gm, " "); - customText = text.split(" "); - customTextIsRandom = false; + customText.text = text.split(" "); + customText.isWordRandom = false; setMode("custom", true); setDifficulty("normal", true); if (challenge.parameters[1] != null) { @@ -5489,15 +5847,13 @@ async function setupChallenge(challengeName) { restartTest(false, true); notitext = challenge.message; if (notitext === undefined) { - Misc.showNotification(`Challenge '${challengeName}' loaded.`, 3000); + Notifications.add(`Challenge '${challengeName}' loaded.`, 0); } else { - Misc.showNotification("Challenge loaded. " + notitext, 5000); + Notifications.add("Challenge loaded. " + notitext, 0); } } catch (e) { - Misc.showNotification("Something went wrong: " + e, 5000); - console.log(e); + Notifications.add("Something went wrong: " + e, -1); } - // Misc.showNotification("Challenge loaded", 2000); } let ctx = $("#wpmChart"); diff --git a/src/js/settings.js b/src/js/settings.js index 9252b3dc2..ffad5d3f1 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -473,9 +473,10 @@ function hideCustomThemeShare() { $("#customThemeShareWrapper input").val() ); } catch (e) { - Misc.showNotification( + Notifications.add( "Something went wrong. Reverting to default custom colors.", - 3000 + 0, + 4 ); config.customThemeColors = defaultConfig.customThemeColors; } @@ -524,12 +525,12 @@ $("#shareCustomThemeButton").click((e) => { Misc.objectToQueryString({ customTheme: share }); navigator.clipboard.writeText(url).then( function () { - Misc.showNotification("URL Copied to clipboard", 2000); + Notifications.add("URL Copied to clipboard", 0); }, function (err) { - Misc.showNotification( + Notifications.add( "Something went wrong when copying the URL: " + err, - 5000 + -1 ); } ); @@ -583,7 +584,7 @@ function refreshTagsSettingsSection() {
${tag.name}
-
+
@@ -597,7 +598,7 @@ function refreshTagsSettingsSection() {
${tag.name}
-
+
@@ -766,7 +767,7 @@ $( }) .catch((e) => { hideBackgroundLoader(); - Misc.showNotification("Something went wrong. Error: " + e.message, 4000); + Notifications.add("Something went wrong. Error: " + e.message, -1); }); }); @@ -781,13 +782,10 @@ $(".pageSettings .section.discordIntegration #unlinkDiscordButton").click( console.log(ret); if (ret.data.status === 1) { db_getSnapshot().discordId = null; - Misc.showNotification("Accounts unlinked", 2000); + Notifications.add("Accounts unlinked", 0); updateDiscordSettingsSection(); } else { - Misc.showNotification( - "Something went wrong: " + ret.data.message, - 5000 - ); + Notifications.add("Something went wrong: " + ret.data.message, -1); updateDiscordSettingsSection(); } }); @@ -908,7 +906,7 @@ $(".pageSettings .saveCustomThemeButton").click((e) => { } ); setCustomThemeColors(save); - Misc.showNotification("Custom theme colors saved", 1000); + Notifications.add("Custom theme colors saved", 0); }); $(".pageSettings #loadCustomColorsFromPreset").click((e) => { @@ -960,12 +958,12 @@ $("#exportSettingsButton").click((e) => { let configJSON = JSON.stringify(config); navigator.clipboard.writeText(configJSON).then( function () { - Misc.showNotification("JSON Copied to clipboard", 2000); + Notifications.add("JSON Copied to clipboard", 0); }, function (err) { - Misc.showNotification( + Notifications.add( "Something went wrong when copying the settings JSON: " + err, - 5000 + -1 ); } ); @@ -991,9 +989,9 @@ function hideSettingsImport() { try { applyConfig(JSON.parse($("#settingsImportWrapper input").val())); } catch (e) { - Misc.showNotification( + Notifications.add( "An error occured while importing settings: " + e, - 5000 + -1 ); } saveConfigToCookie(); diff --git a/src/js/simple-popups.js b/src/js/simple-popups.js index 2f2d64d3c..c531054b4 100644 --- a/src/js/simple-popups.js +++ b/src/js/simple-popups.js @@ -150,21 +150,21 @@ simplePopups.updateEmail = new SimplePopup( }).then((data) => { hideBackgroundLoader(); if (data.data.resultCode === 1) { - Misc.showNotification("Email updated", 2000); + Notifications.add("Email updated", 0); setTimeout(() => { signOut(); }, 1000); } else if (data.data.resultCode === -1) { - Misc.showNotification("Current email doesn't match", 2000); + Notifications.add("Current email doesn't match", 0); } else { - Misc.showNotification( + Notifications.add( "Something went wrong: " + JSON.stringify(data.data), - 7000 + -1 ); } }); } catch (e) { - Misc.showNotification("Something went wrong: " + e, 5000); + Notifications.add("Something went wrong: " + e, -1); } }, () => {} @@ -192,21 +192,16 @@ simplePopups.clearTagPb = new SimplePopup( $( `.pageSettings .section.tags .tagsList .tag[id="${tagid}"] .clearPbButton` ).attr("aria-label", "No PB found"); - Misc.showNotification("Tag PB cleared.", 1000); + Notifications.add("Tag PB cleared.", 0); } else { - console.error(res.data.message); - Misc.showNotification( - "Something went wrong: " + res.data.message, - 5000 - ); + Notifications.add("Something went wrong: " + res.data.message, -1); } }) .catch((e) => { hideBackgroundLoader(); - console.error(e); - Misc.showNotification( + Notifications.add( "Something went wrong while clearing tag pb " + e, - 5000 + -1 ); }); // console.log(`clearing for ${eval("this.parameters[0]")} ${eval("this.parameters[1]")}`); diff --git a/src/js/userconfig.js b/src/js/userconfig.js index a526767c6..1cb2feaf8 100644 --- a/src/js/userconfig.js +++ b/src/js/userconfig.js @@ -104,23 +104,20 @@ async function saveConfigToCookie(noDbCheck = false) { } async function saveConfigToDB() { - // if (firebase.auth().currentUser !== null) { - // accountIconLoading(true); - // CloudFunctions.saveConfig({ - // uid: firebase.auth().currentUser.uid, - // obj: config, - // }).then((d) => { - // accountIconLoading(false); - // if (d.data.returnCode === 1) { - // } else { - // Misc.showNotification( - // `Error saving config to DB! ${d.data.message}`, - // 4000 - // ); - // } - // return; - // }); - // } + if (firebase.auth().currentUser !== null) { + accountIconLoading(true); + CloudFunctions.saveConfig({ + uid: firebase.auth().currentUser.uid, + obj: config, + }).then((d) => { + accountIconLoading(false); + if (d.data.returnCode === 1) { + } else { + Notifications.add(`Error saving config to DB! ${d.data.message}`, 4000); + } + return; + }); + } } function resetConfig() { @@ -680,7 +677,7 @@ function setHighlightMode(mode, nosave) { mode === "word" && (activeFunBox === "nospace" || activeFunBox === "read_ahead") ) { - Misc.showNotification("Can't use word highlight with this funbox", 3000); + Notifications.add("Can't use word highlight with this funbox", 0); return; } if (mode == null || mode == undefined) { @@ -1145,7 +1142,7 @@ function randomiseTheme() { randomList = config.favThemes; randomTheme = randomList[Math.floor(Math.random() * randomList.length)]; setTheme(randomTheme, true); - Misc.showNotification(randomTheme.replace(/_/g, " "), 1500); + Notifications.add(randomTheme.replace(/_/g, " "), 0); }); } @@ -1482,8 +1479,7 @@ function setFontSize(fontSize, nosave) { function applyConfig(configObj) { if (configObj == null || configObj == undefined) { - Misc.showNotification("Could not apply config", 1000); - console.error("configobj is null or undefined"); + Notifications.add("Could not apply config", -1); return; } Object.keys(defaultConfig).forEach((configKey) => { diff --git a/src/sass/style.scss b/src/sass/style.scss index f7ec33f28..e5a812fba 100644 --- a/src/sass/style.scss +++ b/src/sass/style.scss @@ -415,6 +415,13 @@ a:hover { justify-items: left; } + .randomInputFields { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + text-align: center; + align-items: center; + } + .check { span { display: block; @@ -697,6 +704,82 @@ a:hover { } } +#notificationCenter { + width: 350px; + z-index: 99999999; + display: grid; + gap: 1rem; + position: fixed; + right: 1rem; + top: 1rem; + .history { + display: grid; + gap: 1rem; + } + .notif { + .icon { + color: var(--bg-color); + opacity: 0.5; + padding: 1rem 1rem; + align-items: center; + display: grid; + font-size: 1.25rem; + } + .message { + padding: 1rem 1rem 1rem 0; + .title { + color: var(--bg-color); + font-size: 0.75em; + opacity: 0.5; + line-height: 0.75rem; + } + } + + position: relative; + background: var(--sub-color); + color: var(--bg-color); + display: grid; + grid-template-columns: min-content auto min-content; + border-radius: var(--roundness); + border-width: 0.25rem; + + &.bad { + background-color: var(--error-color); + } + + &.good { + background-color: var(--main-color); + } + + &:hover { + // opacity: .5; + // box-shadow: 0 0 20px rgba(0,0,0,.25); + cursor: pointer; + &::after { + opacity: 1; + } + } + &::after { + transition: 0.125s; + font-family: "Font Awesome 5 Free"; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + font-weight: 900; + content: "\f00d"; + position: absolute; + width: 100%; + height: 100%; + color: var(--bg-color); + font-size: 2.5rem; + display: grid; + /* align-self: center; */ + align-items: center; + text-align: center; + border-radius: var(--roundness); + } + } +} + #supportMeWrapper { width: 100%; height: 100%; @@ -1704,6 +1787,10 @@ key { user-select: none; padding-bottom: 1em; + .newline { + width: inherit; + } + letter { border-bottom-style: solid; border-bottom-width: 0.05em; @@ -1712,6 +1799,11 @@ key { border-bottom-width: 0.05em; border-bottom-color: var(--sub-color); } + &.tabChar, + &.nlChar { + margin: 0 0.25rem; + opacity: 0.2; + } } /* a little hack for right-to-left languages */ @@ -1970,6 +2062,14 @@ key { display: inline-block; } + &.lastbeforenewline::after { + font-family: "Font Awesome 5 Free"; + font-weight: 600; + content: "\f107"; + margin-left: 0.5rem; + opacity: 0.25; + } + // transition: .25s; .wordInputAfter { opacity: 1; @@ -2045,6 +2145,10 @@ key { border-bottom: 2px dotted var(--main-color); } +.word letter.extraCorrected { + border-right: 2px dotted var(--main-color); +} + .word letter.incorrect { color: var(--error-color); position: relative; diff --git a/static/challenges/list.json b/static/challenges/_list.json similarity index 100% rename from static/challenges/list.json rename to static/challenges/_list.json diff --git a/static/js/fonts.json b/static/fonts/_list.json similarity index 100% rename from static/js/fonts.json rename to static/fonts/_list.json diff --git a/static/funbox/list.json b/static/funbox/_list.json similarity index 100% rename from static/funbox/list.json rename to static/funbox/_list.json diff --git a/static/index.html b/static/index.html index ce96e28a4..af25b0951 100644 --- a/static/index.html +++ b/static/index.html @@ -49,6 +49,9 @@ +
+
+
- Word count - - +
-
- For testing purposes, account config synchronisation has been - temporarily disabled (only local config will be remembered). Sorry - for the inconvenience. -