// ==UserScript== // @name Duolingo: Type more, tap less // @description Replace tapping exercises with full-sentence typing // @namespace neobrain // @author neobrain // @match https://*.duolingo.com/* // @grant GM.XMLHTTPRequest // @grant GM.log // @run-at document-start // @require https://update.greasyfork.org/scripts/472943.js?version=1239323 // @license MIT // @version 0.3 // @downloadURL none // ==/UserScript== middleMan.addHook("*/sessions", { requestHandler(request) { GM.log("Outgoing request: ", request); }, async responseHandler(request, response, error) { GM.log("Incoming response: ", response, error); let learningLanguage = JSON.parse(localStorage.getItem("duo.state")).state.redux.user.learningLanguage; let fromLanguage = JSON.parse(localStorage.getItem("duo.state")).state.redux.user.fromLanguage; const data = await response?.json() ?? { errorMessage: error.message }; for (let challenge of data.challenges) { // Speak exercises always use tapping. Turn this into a translate exercise instead // Disabled for now since translate exercises won't have voice output :( if (false && challenge.type === "speak") { challenge.type = "translate"; challenge.correctSolutions = [ challenge.solutionTranslation ]; delete challenge.solutionTranslation; challenge.targetLanguage = challenge.grader.langage; challenge.sourceLanguage = challenge.grader.langage === learningLanguage ? fromLanguage : learningLanguage; } // Translate exercises use tapping only when translating to the student language. // Trick the frontend into allowing free-form text by swapping the languages if (challenge.type === "translate" && challenge.grader.language == fromLanguage) { challenge.grader.language = learningLanguage; challenge.sourceLanguage = fromLanguage; // NOTE: The frontend uses challenge.targetLanguage for the "Write this in X" text, so we leave it unchanged } } return Response.json(data); } });