Skip to content

Instantly share code, notes, and snippets.

@nfarina
Last active February 28, 2022 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nfarina/e36fd2a85f44469a5bcccee4c89fe375 to your computer and use it in GitHub Desktop.
Save nfarina/e36fd2a85f44469a5bcccee4c89fe375 to your computer and use it in GitHub Desktop.
UserScript adding useful features to Duolingo, including more keyboard shortcuts, and hiding the "hint text" for translation exercises to focus on listening skills.
// ==UserScript==
// @name Duolingo Helper
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Adds useful features to Duolingo, including more keyboard shortcuts, and hidden "hint text" for translation exercises to concentrate on listening skills.
// @author Nick Farina
// @match https://*.duolingo.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const matchKeys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g"];
const matchCodes = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "Digit0", "KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG"];
let hidden = true;
console.log("Duolingo Helper Loaded.");
function updateDocument() {
const typeInEnglish = document.querySelector('textarea[placeholder="Type in English"]')
const typeInChinese = document.querySelector('textarea[placeholder="Type in Chinese"]')
const hintSentence = document.querySelector("*[data-test=hint-token]")?.parentElement;
const listenTap = document.querySelector("*[data-test='challenge challenge-listenTap']");
const matchPairs = document.querySelector("*[data-test='challenge challenge-characterMatch']");
const nextButton = document.querySelector("*[data-test=player-next]");
const finished = nextButton && nextButton.innerText === "CONTINUE";
if (hintSentence) {
// Does this hint sentence have a corresponding "audio button"? If so, hide the text in favor of the audio.
const buttons = getListenButtons();
if (buttons) {
hintSentence.style.opacity = hidden && !finished ? "0" : "1";
createHintText(buttons[0], getTranslateHintText());
}
else {
hintSentence.style.opacity = "1";
}
}
else if (listenTap) {
// This is a "Type what you hear" challenge - find the corresponding audio button.
const buttons = getListenButtons();
if (buttons) {
createHintText(buttons[0], getListenHintText());
}
}
if (matchPairs) {
const buttons = matchPairs.querySelectorAll("button");
let index = 0;
for (const button of buttons) {
const isOtherLanguage = !!button.querySelector("span");
if (!isOtherLanguage) {
// This turns out to be a bit annoying.
// button.style.opacity = hidden ? "0" : "1";
}
createTip(button, matchKeys[index]);
index++;
}
}
if (typeInEnglish || typeInChinese) {
const textarea = typeInEnglish || typeInChinese;
const typedEnglish = textarea.value.match(/\p{Script=Latin}/u);
const typedChinese = textarea.value.match(/\p{Script=Han}/u);
const invalid = (typeInEnglish && typedChinese) || (typeInChinese && typedEnglish);
textarea.style.color = invalid ? "red" : null;
}
}
function onKeyDown(e) {
if (e.code === "AltRight") {
hidden = false;
const buttons = getListenButtons();
if (buttons && buttons.length > 1) buttons[1].click(); // "Speak slowly"
}
else if (e.code === "MetaRight" || e.code === "ControlRight") {
const buttons = getListenButtons();
if (buttons) buttons[0].click();
}
}
function onKeyUp(e) {
if (e.code === "AltRight") {
hidden = true;
}
}
function onKeyPress(e) {
const matchPairs = document.querySelector("*[data-test='challenge challenge-characterMatch']");
if (matchPairs) {
if (matchCodes.includes(e.code)) {
const index = matchCodes.indexOf(e.code);
const buttons = matchPairs.querySelectorAll("button");
buttons[index].click();
}
}
}
function getListenButtons() {
const tests = ["challenge-translate-prompt", "challenge challenge-listenTap"];
for (const test of tests) {
const buttons = document.querySelectorAll(`*[data-test="${test}"] button`);
if (buttons.length > 0) return buttons;
}
// This recently became obfuscated.
const listenIcon = Array.from(document.querySelectorAll("svg"))
.find(svg => svg.width.baseVal.value == 94 && svg.height.baseVal.value == 73);
const listenButton = listenIcon?.parentElement.parentElement;
return listenButton ? [listenButton] : undefined;
}
function getListenSlowlyButton() {
const tests = ["challenge-translate-prompt", "challenge challenge-listenTap"];
for (const test of tests) {
const button = document.querySelector(`*[data-test="${test}"] button`);
if (button) return button;
}
}
// Creates the little "tip" style numbers that can be placed on Duolingo buttons to indicate which shortcut key to press.
function createTip(button, html, larger) {
const hasTip = !!button.querySelector("x-tip");
if (hasTip) {
return;
}
const tip = document.createElement("x-tip");
tip.innerHTML = html;
tip.style.position = "absolute";
tip.style.top = "-9px";
tip.style.left = "-9px";
tip.style.border = "2px solid white";
tip.style.boxSizing = "content-box";
tip.style.borderRadius = "100px";
tip.style.width = "18px";
tip.style.height = "18px";
tip.style.fontSize = "11px";
tip.style.background = "#1cb0f6";
tip.style.color = "white";
tip.style.display = "flex";
tip.style.alignItems = "center";
tip.style.justifyContent = "center";
tip.style.fontWeight = "bold";
button.style.position = "relative";
button.appendChild(tip);
}
function createHintText(button, html) {
const hasTip = !!button.querySelector("x-tip");
if (hasTip) {
return;
}
const tip = document.createElement("x-tip");
tip.innerHTML = html;
tip.style.position = "absolute";
tip.style.display = "block";
tip.style.bottom = "-34px";
tip.style.left = "0";
tip.style.fontSize = "11px";
tip.style.color = "rgb(185, 185, 185)";
tip.style.fontWeight = "bold";
tip.style.whiteSpace = "nowrap";
button.style.position = "relative";
button.appendChild(tip);
}
function getTranslateHintText() {
const os = getOS();
const isApple = os === "mac" || os === "ios";
return `
Press right 
<span style="border: 1px solid rgb(185, 185, 185);
border-radius: 3px;
padding: ${isApple ? "1px 0 0 4px" : "1px 1px 0px 2px"};
margin: 0 2px 0 -2px;">
${isApple ? "⌘" : "Ctrl"}
</span>
&nbsp;to speak, hold right&nbsp;
<span style="border: 1px solid rgb(185, 185, 185);
border-radius: 3px;
padding: 1px 1px 0px 4px;
margin: 0 2px 0 -2px;">
${isApple ? "Option" : "Alt"}
</span>
&nbsp;to reveal text`;
}
function getListenHintText() {
const os = getOS();
const isApple = os === "mac" || os === "ios";
return `
Press right&nbsp;
<span style="border: 1px solid rgb(185, 185, 185);
border-radius: 3px;
padding: ${isApple ? "1px 0 0 4px" : "1px 1px 0px 2px"};
margin: 0 2px 0 -2px;">
${isApple ? "⌘" : "Ctrl"}
</span>
&nbsp;to speak, press right&nbsp;
<span style="border: 1px solid rgb(185, 185, 185);
border-radius: 3px;
padding: 1px 1px 0px 4px;
margin: 0 2px 0 -2px;">
${isApple ? "Option" : "Alt"}
</span>
&nbsp;to speak slowly`;
}
function getOS() {
var userAgent = window.navigator.userAgent,
platform = window.navigator.platform,
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
iosPlatforms = ['iPhone', 'iPad', 'iPod'];
if (macosPlatforms.indexOf(platform) !== -1) {
return "mac";
} else if (iosPlatforms.indexOf(platform) !== -1) {
return "ios";
} else if (windowsPlatforms.indexOf(platform) !== -1) {
return "win"
} else if (/Android/.test(userAgent)) {
return "android"
} else if (/Linux/.test(platform)) {
return "linux"
}
}
document.addEventListener("keydown", onKeyDown, {capture: true});
document.addEventListener("keypress", onKeyPress, {capture: true});
document.addEventListener("keyup", onKeyUp, {capture: true});
setInterval(updateDocument, 100);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment