// ==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.4 // @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 reduxState; try { reduxState = JSON.parse(localStorage.getItem("duo.state")).state.redux } catch { // NOTE: This is now a base64-encoded gz archive let str = localStorage.getItem("duo.state"); const str2 = atob(str.slice(1, -1)); // Remove wrapping quotes const ds = new DecompressionStream("gzip"); let uint8Array = new Uint8Array(str2.split("").map(c => c.charCodeAt(0))); let readableStream = new ReadableStream({ start(controller) { controller.enqueue(uint8Array); controller.close(); } }); let outstream = readableStream.pipeThrough(ds); let result = ""; const reader = outstream.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { let value = await reader.read(); result += decoder.decode(value.value); if (value.done) { break; } } reduxState = JSON.parse(result).state.redux; } const learningLanguage = reduxState.user.learningLanguage; const fromLanguage = reduxState.user.fromLanguage; const data = await response?.json() ?? { errorMessage: error.message }; for (let challenge of data.challenges) { // 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); } });