vplameniraket / Duolingo Advanced Learning (w/autoupdate)

// ==UserScript==
// @name         Duolingo Advanced Learning (w/autoupdate)
// @description  A must-have for advanced learners. Improves listening and writing skills by hiding the original sentence in «Translate into English» exercises — you now have to write it by ear first.
// @match        *://www.duolingo.com/*
// @author       dogewithflowers
// @icon         https://res.cloudinary.com/dn6n8yqqh/image/upload/c_scale,h_214/v1555635245/Icon_qqbnzf.png
// @namespace    duo-adv-learning-upd
// @version      4.17
// @copyright    2022, vplameniraket (https://openuserjs.org/users/vplameniraket)
// @license      MIT
// @grant        GM_addStyle
// @updateURL    https://openuserjs.org/install/vplameniraket/Duolingo_Advanced_Learning_(wautoupdate).user.js
// @downloadURL  https://openuserjs.org/install/vplameniraket/Duolingo_Advanced_Learning_(wautoupdate).user.js
// ==/UserScript==

GM_addStyle('.textareasWrap { display: flex !important; flex-direction: row !important } .textareasWrap > * { flex-grow: 1; flex-basis: 50%; max-height: 170px; position: relative; display: block } .textareasWrap textarea { height: 100% } .targetLanguageWrap { margin-right: 15px; min-width: calc(50% - 8px); position: relative; } .secondary { opacity: .6; cursor: default; } .badge { position: absolute; z-index: 10; width: 30px; height: 30px; border-radius: 50%; text-align: center; box-sizing: border-box; padding-top: 5px; font-weight: bold; font-size: 17px; left: -13px; top: -14px; transition: all .2s ease-in-out; } .green + .badge { background-color: #78c713; color: #fff; box-shadow: 0 0 0 2px #78c713 inset; } .badge, textarea[disabled] + .badge { background-color: #f0f0f0; box-shadow: 0 0 0 2px #e5e5e5 inset; color: #c7c7c7; } .typeWhatYouHear.success { color: #5b980e; } .success+.badge { box-shadow: 0 0 0 2px #e8e8e8 inset !important; background-color: #f6f6f6 !important; } @keyframes shake { from, to { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); } 20%, 40%, 60%, 80% { transform: translateX(3px); } } .shake { animation-name: shake; } @keyframes pulse { from { transform: scale3d(1, 1, 1); } 50% { transform: scale3d(1.03, 1.03, 1.03); } to { transform: scale3d(1, 1, 1); } } .pulse { animation-name: pulse } .animated { animation-duration: 1s; animation-fill-mode: both; } .animated.fast { animation-duration: 800ms; } .animated.faster { animation-duration: 500ms; } @media screen and (max-width: 699px) {.badge { display: none } .targetLanguageWrap { margin-right: 0; min-width: auto} .textareasWrap { display: block }}');

var v = '4.15',
	news = ['Duolingo Advanced Learning\n',
			'\n' + v + ' — What is new?\n',
			'— Minor adjustments.'
];
if (!localStorage.getItem('duoadv_v') || localStorage.getItem('duoadv_v') != v) {
	localStorage.setItem('duoadv_v', v);
	alert(news.join(''));
}

var keyboardDefined = localStorage.getItem('keyboard') ? true : false,
	headerSelector = 'h1[data-test="challenge-header"] span',
	nativeLanguageTextareaSelector = '[data-test="challenge-translate-input"]:not([autocorrect]):not([spellcheck]):not(.typeWhatYouHear)',
	challengeContainerSelector = '[data-test="challenge challenge-translate"]',
	toggleInputMethodSelector = '[data-test="player-toggle-keyboard"]',
	sentenceContainerSelector = challengeContainerSelector + ' [dir="ltr"] > span + span',
	audioButtonSelector = challengeContainerSelector + ' span > button',
	challengeContainer,
	sentenceContainer,
	nativeLanguageWrap,
	nativeLanguageTextarea,
	targetLanguageWrap,
	targetLanguageTextarea,
	nativeLanguageTextareaIsFound = false,
	targetSentence,
	targetSentenceRevealed;

GM_addStyle(sentenceContainerSelector + ':not(:hover):not(.reveal) {filter: blur(4px)}' + sentenceContainerSelector + '{filter: blur(0px); transition: all .2s ease-in-out}');

setInterval(function(){
	if (!nativeLanguageTextareaIsFound) {
		nativeLanguageTextarea = document.querySelectorAll(challengeContainerSelector + ' ' + nativeLanguageTextareaSelector)[0];
		if (document.body.contains(nativeLanguageTextarea)) {
			nativeLanguageTextareaIsFound = true;
			var correct = false;

			challengeContainer = document.querySelectorAll(challengeContainerSelector)[0];
			challengeContainer.classList.add('challengeContainer');

			nativeLanguageWrap = nativeLanguageTextarea.parentNode;

			if (!challengeContainer.classList.contains('processed')) {
				challengeContainer.classList.add('processed');

				var header = challengeContainer.querySelectorAll(headerSelector)[0];
				if (header.innerText == 'Write this in English') {
					header.innerText = 'Write down and translate what you hear';
				}

				nativeLanguageTextarea.parentNode.insertAdjacentHTML('beforebegin','<div class="targetLanguageWrap animated fast"><textarea data-test="challenge-translate-input" class="' + nativeLanguageTextarea.getAttribute('class') + ' typeWhatYouHear" dir="ltr" placeholder="Type what you hear" style="height: 100%"></textarea><div class="badge">1</div><audio class="successSound"><source src="https://res.cloudinary.com/dn6n8yqqh/video/upload/v1559766656/DuolingoSuccessMP3.mp3" type="audio/mpeg"><source src="https://res.cloudinary.com/dn6n8yqqh/video/upload/v1555571639/DuolingoSuccessOGG.ogg" type="audio/ogg"><source src="https://res.cloudinary.com/dn6n8yqqh/video/upload/v1555571640/DuolingoSuccessWAV.wav" type="audio/wav"></audio></div>');
				nativeLanguageTextarea.insertAdjacentHTML('afterend','<div class="badge">2</div>');

				targetLanguageWrap = challengeContainer.querySelectorAll('.targetLanguageWrap')[0];
				targetLanguageWrap.parentNode.classList.add('textareasWrap');

				targetLanguageTextarea = targetLanguageWrap.querySelectorAll('.typeWhatYouHear')[0];
				targetLanguageTextarea.classList.add('green');
				targetLanguageTextarea.focus();

				nativeLanguageTextarea.classList.add('secondary');
				nativeLanguageTextarea.disabled = true;

				sentenceContainer = challengeContainer.querySelectorAll(sentenceContainerSelector)[0];
				targetSentenceRevealed = false;
				targetSentence = sentenceContainer.innerText.toLowerCase();

				sentenceContainer.classList.add('sentence');

				if (!challengeContainer.classList.contains('events-added')) {
					challengeContainer.classList.add('events-added');
					challengeContainer.addEventListener('keydown', function (e) {
						switch (e.keyCode) {
							case 17:
								document.querySelectorAll(audioButtonSelector)[0].click();
								break;
							case 9:
								e.preventDefault();
								if (targetSentenceRevealed == false) {
									sentenceContainer.classList.add('reveal');
									challengeContainer.classList.add('reveal');
									targetSentenceRevealed = true;
								} return;
								break;
						}
					});
					challengeContainer.addEventListener('keyup', function (e) {
						if (e.keyCode === 9) {
							e.preventDefault();
							sentenceContainer.classList.remove('reveal');
							challengeContainer.classList.remove('reveal');
							targetSentenceRevealed = false;
						}
					});
				}
				targetLanguageTextarea.addEventListener('keydown', function (e) {
					switch (e.keyCode) {
						case 13:
							validate(e);
							break;
						case 27:
							nativeLanguageTextarea.disabled = false;
							nativeLanguageTextarea.classList.replace('secondary','green');
							nativeLanguageTextarea.focus();

							targetLanguageTextarea.disabled = true;
							targetLanguageTextarea.classList.add('secondary');

							toggleTargetKeyboard('disabled');
							toggleNativeKeyboard('enabled');
							break;
					}
				});
				nativeLanguageTextarea.addEventListener('keydown', function (e) {
					if (e.keyCode === 27 && !correct) {
						nativeLanguageTextarea.disabled = true;
						nativeLanguageTextarea.classList.replace('green','secondary');

						targetLanguageTextarea.disabled = false;
						targetLanguageTextarea.classList.remove('secondary');
						targetLanguageTextarea.focus();

						toggleTargetKeyboard('enabled');
						toggleNativeKeyboard('disabled');
					}
				});
				function validate(e) {
					var text = e.target.value.replace(/[ ]$/g, '').replace(/^[ ]/g, '').replace(/[/:]/g, '').replace(/[ ][ ]/g, ' ').toLowerCase(),
						noCommasAndColons = targetSentence.replace(/[,][ ]/g, ' ').replace(/[、]/g, '').replace(/[:]/g, ''),
						filteredSentence = removeDotAndExcMarks(targetSentence),
						filterednoCommasAndColons = removeDotAndExcMarks(noCommasAndColons),

						everyPossibleSentenceVariation = [targetSentence, filteredSentence,
							replaceNonLatin(targetSentence), normalize(targetSentence), normalize(replaceNonLatin(targetSentence)),
							replaceNonLatin(filteredSentence), normalize(filteredSentence), normalize(replaceNonLatin(filteredSentence)),
							noCommasAndColons, filterednoCommasAndColons,
							replaceNonLatin(noCommasAndColons), normalize(noCommasAndColons), normalize(replaceNonLatin(noCommasAndColons)),
							replaceNonLatin(filterednoCommasAndColons), normalize(filterednoCommasAndColons), normalize(replaceNonLatin(filterednoCommasAndColons))];

					if (everyPossibleSentenceVariation.includes(text)) {
						nativeLanguageTextarea.classList.replace('secondary','green');
						nativeLanguageTextarea.disabled = false;
						nativeLanguageTextarea.focus();

						targetLanguageTextarea.disabled = true;
						targetLanguageTextarea.classList.add('secondary','success');

						targetLanguageWrap.classList.replace('fast','faster');
						targetLanguageWrap.classList.add('pulse');

						var player = targetLanguageWrap.querySelectorAll('.successSound')[0];
						player.volume = 0.3;
						player.play();

						toggleTargetKeyboard('disabled');
						toggleNativeKeyboard('enabled');

						correct = true;
					} else {
						targetLanguageWrap.classList.remove('shake');
						targetLanguageWrap.classList.add('shake');
						setTimeout(function(){
							targetLanguageWrap.classList.remove('shake');
						},800);
					}
				}
				if (keyboardDefined && localStorage.getItem('keyboard') !== '') {
					targetLanguageWrap.insertAdjacentHTML('beforeend', localStorage.getItem('keyboard'));
					targetLanguageWrap.querySelectorAll('audio + div')[0].classList.add('keyboard');
					var keybuttons = targetLanguageWrap.querySelectorAll('textarea ~ .keyboard button');

					function lowerCase() {
						keybuttons[0].innerText = '↑';
						keybuttons.forEach(function(keybutton){
							keybutton.innerText = keybutton.innerText.toLowerCase();
						});
					}
					function upperCase() {
						keybuttons[0].innerText = '↓';
						keybuttons.forEach(function(keybutton){
							keybutton.innerText = keybutton.innerText.toUpperCase();
						});
					}

					if (keybuttons[0].innerText == '↓') {
						lowerCase();
					}

					keybuttons.forEach(function(keybutton, i){
						keybutton.removeAttribute('disabled');
						keybutton.onclick = !i ? function(){
							this.innerText == '↓' ? lowerCase() : upperCase();
						} : function(){
							insertAtCursor(targetLanguageTextarea, this.innerText);
							targetLanguageTextarea.focus();
						}
					});
				}

				var toggleInputMethodButton = document.querySelectorAll(toggleInputMethodSelector)[0];
				if (!!toggleInputMethodButton) {
					toggleInputMethodButton.onclick = function(){
						if (!!targetLanguageWrap) {
							targetLanguageWrap.remove();
						}
					}
				}

				var targetKeyboardButtons = targetLanguageWrap.querySelectorAll('textarea ~ div button');
				function toggleTargetKeyboard(state) {
					if (!!targetKeyboardButtons) {
						targetKeyboardButtons.forEach(function(keybutton){
							keybutton.disabled = state == 'disabled';
						});
					}
				}

				var nativeKeyboardButtons = nativeLanguageWrap.querySelectorAll('textarea ~ div button');
				function toggleNativeKeyboard(state) {
					if (!!nativeKeyboardButtons) {
						nativeKeyboardButtons.forEach(function(keybutton){
							keybutton.disabled = state == 'disabled';
						});
					}
				}

				toggleTargetKeyboard('enabled');
				toggleNativeKeyboard('disabled');
			}
		} else {
			if (document.body.contains(targetLanguageWrap)) {
				targetLanguageWrap.remove();
			}
			if (document.body.contains(challengeContainer)) {
				challengeContainer.classList.remove('processed');
			}
			nativeLanguageTextareaIsFound = false;
		}
	} else if (!document.body.contains(nativeLanguageTextarea)) {
		nativeLanguageTextareaIsFound = false;
	}

	if (!!document.querySelectorAll('textarea[autocorrect][spellcheck]')[0]) {
		var keyboard = document.querySelectorAll('textarea[autocorrect][spellcheck] + div')[0];
		if (!!keyboard) {
			if (!keyboardDefined || keyboard.outerHTML !== localStorage.getItem('keyboard')) {
			   localStorage.setItem('keyboard', keyboard.outerHTML);
			}
		} else {
			localStorage.setItem('keyboard', '');
		}
	}
}, 50);

function insertAtCursor(field, val) {
	if (document.selection) {
		field.focus();
		var sel = document.selection.createRange();
		sel.text = val;
	} else if (field.selectionStart || field.selectionStart == '0') {
		var start = field.selectionStart,
			end = field.selectionEnd;

		field.value = field.value.substring(0, start)
			+ val
			+ field.value.substring(end, field.value.length);
		field.selectionStart = start + val.length;
		field.selectionEnd = start + val.length;
	} else field.value += val;
}
function removeDotAndExcMarks(string) { return string.replace(/[ ][.?!]$/g, '').replace(/[.?!]$/g, '').replace(/[¿]/g, '').replace(/[¡]/g, '').replace(/[。!?]$/g, '') }
function replaceNonLatin(string) { return string.replace(/[æ]/g, 'ae').replace(/[ø]/g, 'oe').replace(/[å]/g, 'aa').replace(/[ß]/g, 'ss').replace(/[œ]/g, 'oe').replace(/[ñ]/g, 'n\'').replace(/[dž]/g, 'dzh').replace(/[ĉ]/g, 'cx').replace(/[ĝ]/g, 'gx') }
function normalize(string) { return string.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[\u0591-\u05C7]/g, '') }