Repository: anonymoushackerIV/Duolingo-Pro-BETA
Branch: main
Commit: 3bcb8cda87e5
Files: 3
Total size: 528.0 KB
Directory structure:
gitextract_917tfqi7/
├── Duolingo-PRO-BETA.user.js
├── LICENSE
└── README.md
================================================
FILE CONTENTS
================================================
================================================
FILE: Duolingo-PRO-BETA.user.js
================================================
// ==UserScript==
// @name Duolingo PRO
// @namespace http://duolingopro.net
// @version 3.1BETA.04.4
// @description The fastest Duolingo XP farmer, with free gems, Duolingo Max & more. Working as of April 2026.
// @author anonymousHackerIV
// @match *://*.duolingo.com/*
// @match *://*.duolingo.cn/*
// @icon https://www.duolingopro.net/static/favicons/dlp/128/light/primary.png
// @grant GM_log
// ==/UserScript==
const VERSION_NUMBER = "09";
const STORAGE_LOCAL_VERSION = "09";
const STORAGE_SESSION_VERSION = "09";
const VERSION_NAME = "BETA.04.4";
const VERSION_FULL = "3.1BETA.04.4";
const VERSION_FORMAL = "3.1 BETA.04.4";
let serverURL = "https://www.duolingopro.net";
let apiURL = "https://api.duolingopro.net";
let greasyfork = true;
let alpha = false;
let storageLocal;
let storageSession;
let hidden = false;
let pageHistory = [1];
let windowBlurState = true;
let multipleScriptsDetected = false;
let recentUpdateDetected = false;
let solvingLoopRunning = false;
let isAutoMode = false;
const DEFAULT_REACT_MAIN_ELEMENT_CLASS = '_3yE3H';
const DEFAULT_REACT_TRAVERSE_UP = 1;
const STORY_REACT_MAIN_ELEMENT_CLASS = '_3TJzR';
const STORY_REACT_TRAVERSE_UP = 0;
let findReactMainElementClass = DEFAULT_REACT_MAIN_ELEMENT_CLASS;
let reactTraverseUp = DEFAULT_REACT_TRAVERSE_UP;
if (["blog", "simg-ssl", "englishtest", "schools", "store"].some(s => new RegExp(`(?:^|\\.)${s}\\.`).test(window.location.hostname))) {
throw new Error("Duolingo PRO: unsupported subdomain");
}
const region = new Intl.Locale(navigator.language).maximize().region;
const measurementSystem = ["US", "LR", "MM"].includes(region) ? "ussystem" : "metric";
const debug = false;
const flag01 = false;
const flag02 = false;
const flag03 = false;
const flag04 = false;
const flag05 = false; // Support chat Markdown links ([text](url))
// USAGE OR MODIFICATION OF THIS SCRIPT IMPLIES YOU AGREE TO THE TERMS AND CONDITIONS PRESENTED IN THE SCRIPT. IF YOU DO NOT AGREE, DO NOT USE OR MODIFY THIS SCRIPT.
const random16Numbers = Array.from(crypto.getRandomValues(new Uint8Array(16)), b => (b % 10)).join('');
let duplicateDetectionMarker = document.createElement("div");
duplicateDetectionMarker.setAttribute(`data-duolingo-pro-duplicate-detection-marker`, String(VERSION_NUMBER));
duplicateDetectionMarker.setAttribute(`data-duolingo-pro-duplicate-detection-priority`, String(random16Numbers));
duplicateDetectionMarker.style.display = "none";
document.body.appendChild(duplicateDetectionMarker);
function duplicateCheck() {
let allDuplicateDetectionMarkers = document.querySelectorAll('[data-duolingo-pro-duplicate-detection-marker]');
let allDuplicateDetectionPriorities = document.querySelectorAll('[data-duolingo-pro-duplicate-detection-priority]');
if (allDuplicateDetectionMarkers.length > 1) {
const markerValues = Array.from(allDuplicateDetectionMarkers).map(el => Number(el.getAttribute("data-duolingo-pro-duplicate-detection-marker")));
// 1. If any marker > VERSION_NUMBER, return
if (markerValues.some(v => v > Number(VERSION_NUMBER))) return true;
// 2. If any marker == VERSION_NUMBER, continue to priority check
if (markerValues.find(v => v === Number(VERSION_NUMBER)) !== undefined) {
const priorityValues = Array.from(allDuplicateDetectionPriorities).map(el => Number(el.getAttribute("data-duolingo-pro-duplicate-detection-priority")));
// 1: If any priority > random16Numbers, return
if (priorityValues.some(p => p > Number(random16Numbers))) return true;
return false;
}
return false;
}
}
let systemLanguage = document.cookie.split('; ').find(row => row.startsWith('lang=')).split('=')[1];
let systemText = {
en: {
1: "Switch to Legacy",
2: "Show",
3: "Connecting",
4: "Donate",
5: "Support",
6: "Settings",
7: "What's New",
8: "How much XP would you like to gain?",
9: "GET",
10: "How many Gems would you like to gain?",
12: "Would you like to redeem 3 days of Super Duolingo?",
13: "REDEEM",
14: "Terms & Conditions",
15: "See More",
16: "Back",
17: "How many lessons would you like to solve on the path?",
18: "START",
19: "How many practices would you like to solve?",
21: "How many listening practices would you like to solve? (Requires Super Duolingo)",
23: "Which and how many lessons would you like to repeat?",
25: "Please read and accept the Terms & Conditions to use Duolingo PRO 3.1.",
26: "These are the Terms & Conditions you agreed to use Duolingo PRO 3.1.",
27: "LOADING TERMS & CONDITIONS
YOU CANNOT USE THIS SOFTWARE UNTIL TERMS & CONDITIONS ARE LOADED",
28: "DECLINE",
29: "ACCEPT",
30: "Without accepting the Terms & Conditions, you cannot use Duolingo PRO 3.1.",
31: "BACK",
32: "Settings",
34: "Automatic Updates",
35: "Duolingo PRO 3.1 will automatically update itself when there's a new version available.",
37: "SAVE",
38: "Feedback",
39: "Help us make Duolingo PRO 3.1 better.",
40: "Write here as much as you can with as many details as possible.",
41: "Feedback Type: ",
42: "BUG REPORT",
43: "SUGGESTION",
44: "Add Attachment: (Optional)",
45: "UPLOAD",
47: "SEND",
48: "What's New",
51: "LEARN MORE",
52: "Welcome to",
53: "Skip the grind and jump straight to the rewards with instant XP, gem gains, streak boosts, auto-solved lessons, and more.",
54: "START",
55: "Would you like to redeem an XP Boost?",
56: "How many Streak Freezes would you like to get?",
57: "How many days would you like to increase your Streak by?",
58: "Would you like to refill your Hearts to full?",
59: "Would you like to complete all your Quests?",
60: "COMPLETE",
61: "Pick 3.1 mode for faster, instant results powered by server-side processing, or Legacy mode to keep everything running on-client.",
100: "SOLVE",
101: "SOLVE ALL",
102: "PAUSE SOLVE",
103: "Hide",
104: "Show",
105: "Switch to 3.1",
106: "Switch to Legacy",
107: "STOP",
108: "Connected",
109: "Error",
110: "SEND",
111: "SENDING",
112: "SENT",
113: "LOADING",
114: "DONE",
115: "FAILED",
116: "SAVING AND APPLYING",
200: "Under Construction",
201: "The Gems function is currently under construction. We plan to make it accessible to everyone soon.",
202: "Update Available",
203: "You are using an outdated version of Duolingo PRO.
Please update Duolingo PRO or turn on automatic updates.",
204: "Feedback Sent",
205: "Your feedback was successfully sent, and our developers will look over it. Keep in mind, we cannot respond back to your feedback.",
206: "Error Sending Feedback",
207: "Your feedback was not sent. This might be because you are using an outdated or a modified version of Duolingo PRO.",
208: "Unknown Error",
209: "Please try again later. An unknown error occurred. Number: ",
210: "hour",
211: "hours",
212: "minute",
213: "minutes",
214: "and",
215: "{hours} {hourUnit}",
216: "{minutes} {minuteUnit}",
217: "{hourPhrase} {conjunction} {minutePhrase}",
218: "XP Successfully Received",
219: "You received {amount} XP. You can request up to {remainingXP} XP before your limit resets back to {totalLimit} XP in {timeMessage}. To boost your limits, donate.",
220: "Super Duolingo Successfully Redeemed",
221: "You redeemed a 3 day Super Duolingo trial. You can request another 3 day Super Duolingo trial in {timeMessage}.",
222: "Limit Warning",
223: "You can only request up to {limitAmount} XP before your limit resets back to {totalLimitAmount} XP in {timeMessage}. To boost your limits, donate.",
224: "Limit Reached",
225: "You reached your XP limit for the next {timeMessage}. To boost your limits, donate.",
227: "You already redeemed a 3 day Super Duolingo trial. You can request another 3 day Super Duolingo trial in {timeMessage}.",
229: "REFILL",
230: "GEMS testing",
231: "Error Connecting",
232: "Duolingo PRO was unable to connect to our servers. This may be because our servers are temporarily unavailable or you are using an outdated version. Check for server status or updates.",
233: "Update Duolingo PRO",
234: "You are using an outdated version of Duolingo PRO. Please update Duolingo PRO."
},
};
let CSS1;
let HTML2;
let CSS2;
let HTML3;
let HTML4;
let HTML5;
let CSS5;
let HTML6;
let CSS6;
let HTML7;
let CSS7;
function Two() {
CSS1 = `
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Thin.woff2) format('woff2');
font-weight: 100;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Ultralight.woff2) format('woff2');
font-weight: 200;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Light.woff2) format('woff2');
font-weight: 300;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Regular.woff2) format('woff2');
font-weight: 400;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Medium.woff2) format('woff2');
font-weight: 500;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Semibold.woff2) format('woff2');
font-weight: 600;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Bold.woff2) format('woff2');
font-weight: 700;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Heavy.woff2) format('woff2');
font-weight: 800;
}
@font-face {
font-family: 'Duolingo PRO Rounded';
src: url(${serverURL}/static/fonts/V7R100DB1/Duolingo-PRO-Rounded-Black.woff2) format('woff2');
font-weight: 900;
}
:root {
--DLP-red-hex: #ff3b30;
--DLP-orange-hex: #ff9500;
--DLP-yellow-hex: #ffcc00;
--DLP-green-hex: #34c759;
--DLP-teal-hex: #00c7be;
--DLP-cyan-hex: #5ac8fa;
--DLP-blue-hex: #007aff;
--DLP-indigo-hex: #5856d6;
--DLP-purple-hex: #af52de;
--DLP-pink-hex: #ff2d55;
--DLP-red-rgb: rgb(255, 59, 48);
--DLP-orange-rgb: rgb(255, 149, 0);
--DLP-yellow-rgb: rgb(255, 204, 0);
--DLP-green-rgb: rgb(52, 199, 89);
--DLP-teal-rgb: rgb(0, 199, 190);
--DLP-cyan-rgb: rgb(90, 200, 250);
--DLP-blue-rgb: rgb(0, 122, 255);
--DLP-indigo-rgb: rgb(88, 86, 214);
--DLP-purple-rgb: rgb(175, 82, 222);
--DLP-pink-rgb: rgb(255, 45, 85);
--DLP-red: 255, 59, 48;
--DLP-orange: 255, 149, 0;
--DLP-yellow: 255, 204, 0;
--DLP-green: 52, 199, 89;
--DLP-teal: 0, 199, 190;
--DLP-cyan: 90, 200, 250;
--DLP-blue: 0, 122, 255;
--DLP-indigo: 88, 86, 214;
--DLP-purple: 175, 82, 222;
--DLP-pink: 255, 45, 85;
--DLP-corner-s: superellipse(1.32);
--DLP-corner-r-s: 4px;
--DLP-corner-r-m: 8px;
--DLP-corner-r-ml: 12px;
--DLP-corner-r-l: 16px;
--DLP-corner-r-xl: 20px;
}
@media (prefers-color-scheme: dark) {
:root {
--DLP-red-hex: #ff453a;
--DLP-orange-hex: #ff9f0a;
--DLP-yellow-hex: #ffd60a;
--DLP-green-hex: #30d158;
--DLP-teal-hex: #63e6e2;
--DLP-cyan-hex: #64d2ff;
--DLP-blue-hex: #0a84ff;
--DLP-indigo-hex: #5e5ce6;
--DLP-purple-hex: #bf5af2;
--DLP-pink-hex: #ff375f;
--DLP-red-rgb: rgb(255, 69, 58);
--DLP-orange-rgb: rgb(255, 159, 10);
--DLP-yellow-rgb: rgb(255, 214, 10);
--DLP-green-rgb: rgb(48, 209, 88);
--DLP-teal-rgb: rgb(99, 230, 226);
--DLP-cyan-rgb: rgb(100, 210, 255);
--DLP-blue-rgb: rgb(10, 132, 255);
--DLP-indigo-rgb: rgb(94, 92, 230);
--DLP-purple-rgb: rgb(191, 90, 242);
--DLP-pink-rgb: rgb(255, 55, 95);
--DLP-red: 255, 69, 58;
--DLP-orange: 255, 159, 10;
--DLP-yellow: 255, 214, 10;
--DLP-green: 48, 209, 88;
--DLP-teal: 99, 230, 226;
--DLP-cyan: 100, 210, 255;
--DLP-blue: 10, 132, 255;
--DLP-indigo: 94, 92, 230;
--DLP-purple: 191, 90, 242;
--DLP-pink: 255, 55, 95;
--DLP-background: var(--color-snow);
}
}
`;
if (CSS.supports('corner-shape', 'superellipse(1.32)')) {
CSS1 = CSS1
.replace('--DLP-corner-r-s: 4px', '--DLP-corner-r-s: 6px')
.replace('--DLP-corner-r-m: 8px', '--DLP-corner-r-m: 10px')
.replace('--DLP-corner-r-ml: 12px', '--DLP-corner-r-ml: 16px')
.replace('--DLP-corner-r-l: 16px', '--DLP-corner-r-l: 20px')
.replace('--DLP-corner-r-xl: 20px', '--DLP-corner-r-xl: 26px');
}
HTML2 = `
${systemText[systemLanguage][1]}
${systemText[systemLanguage][2]}
${systemText[systemLanguage][3]}
${systemText[systemLanguage][4]}
${systemText[systemLanguage][5]}
${systemText[systemLanguage][6]}
Boost
Duolingo
PRO 3.1
${VERSION_NAME}
${systemText[systemLanguage][8]}
${systemText[systemLanguage][9]}
${systemText[systemLanguage][10]}
${systemText[systemLanguage][9]}
Which monthly badge would you like to get?
/
${systemText[systemLanguage][9]}
${systemText[systemLanguage][12]}
${systemText[systemLanguage][13]}
${systemText[systemLanguage][55]}
${systemText[systemLanguage][13]}
${systemText[systemLanguage][56]}
${systemText[systemLanguage][9]}
${systemText[systemLanguage][57]}
${systemText[systemLanguage][9]}
${systemText[systemLanguage][58]}
${systemText[systemLanguage][229]}
${systemText[systemLanguage][59]}
${systemText[systemLanguage][60]}
Would you like to enable on-client Duolingo Max?
ENABLE IN SETTINGS
${systemText[systemLanguage][15]}
${systemText[systemLanguage][14]}
${systemText[systemLanguage][7]}
Duolingo
PRO 3.1
${VERSION_NAME}
${systemText[systemLanguage][8]}
${systemText[systemLanguage][9]}
${systemText[systemLanguage][10]}
${systemText[systemLanguage][9]}
${systemText[systemLanguage][57]}
${systemText[systemLanguage][9]}
${systemText[systemLanguage][56]}
${systemText[systemLanguage][9]}
Which monthly badge would you like to get?
/
${systemText[systemLanguage][9]}
${systemText[systemLanguage][55]}
${systemText[systemLanguage][13]}
${systemText[systemLanguage][58]}
${systemText[systemLanguage][229]}
${systemText[systemLanguage][59]}
${systemText[systemLanguage][60]}
Would you like to enable on-client Duolingo Max?
ENABLE IN SETTINGS
${systemText[systemLanguage][12]}
${systemText[systemLanguage][13]}
${systemText[systemLanguage][3]}
${systemText[systemLanguage][4]}
${systemText[systemLanguage][5]}
${systemText[systemLanguage][6]}
Boost
Duolingo
PRO LE
${VERSION_NAME}
You are using an outdated version of Duolingo PRO.
Please update Duolingo PRO or turn on automatic updates.
Duolingo PRO failed to connect. This might be happening because of an issue on our system or your device.
Try updating Duolingo PRO. If the issue persists afterwards, join our Discord Server to get support.
We are currently unable to receive new requests due to high demand. Join our Discord Server to learn more.
You can help us handle more demand by donating on Patreon while getting exclusive features and higher limits.
${systemText[systemLanguage][17]}
${systemText[systemLanguage][18]}
${systemText[systemLanguage][19]}
${systemText[systemLanguage][18]}
${systemText[systemLanguage][21]}
${systemText[systemLanguage][18]}
${systemText[systemLanguage][23]}
Unit:
Lesson:
${systemText[systemLanguage][18]}
${systemText[systemLanguage][15]}
${systemText[systemLanguage][14]}
${systemText[systemLanguage][7]}
Duolingo
PRO LE
${VERSION_NAME}
${systemText[systemLanguage][17]}
${systemText[systemLanguage][18]}
${systemText[systemLanguage][19]}
${systemText[systemLanguage][18]}
${systemText[systemLanguage][21]}
${systemText[systemLanguage][18]}
${systemText[systemLanguage][23]}
Unit:
Lesson:
${systemText[systemLanguage][18]}
Duolingo
PRO 3.1
${VERSION_NAME}
${systemText[systemLanguage][25]}
${systemText[systemLanguage][26]}
${systemText[systemLanguage][27]}
${systemText[systemLanguage][28]}
${systemText[systemLanguage][29]}
${systemText[systemLanguage][29]}
Duolingo
PRO 3.1
${VERSION_NAME}
${systemText[systemLanguage][30]}
${systemText[systemLanguage][31]}
${systemText[systemLanguage][32]}
${VERSION_NAME}
Show Solve Buttons
In lessons and practices, see the solve and solve all buttons.
Show AutoServer Button
See the AutoServer by Duolingo PRO button in your Duolingo menubar.
Random Legacy Solve Speed
Legacy will wait a random amount of seconds before solving.
Custom Random Legacy Solve Speed
Legacy will wait a random amount of seconds in between these two numbers before solving.
Help Us Make Duolingo PRO Better
Allow Duolingo PRO to collect anonymous usage data for us to improve the script.
Reduce Processing Intensive Effects
Reduce processing intensive effects by disabling confetti, starfield and other visual animations.
Free On-Client Duolingo Max
Skip the worry of running out of hearts, get free entry to legendary challenges, access to personalized practice, and learn without ads. Only works on-client.
Show Super Duolingo Trial Function
This function rarely works and is currently being deprecated. We recommend you to use Free Duolingo Max instead.
${systemText[systemLanguage][34]}
${systemText[systemLanguage][35]}
3.1 Stats
XP Gained:
Gems Gained:
Streak Gained:
Heart Refills Requested:
Streak Freezes Requested:
Double XP Boosts Requested:
Quest Completes Requested:
Legacy Mode Stats
Lessons Solved:
Questions Solved:
${systemText[systemLanguage][37]}
${systemText[systemLanguage][38]}
${VERSION_NAME}
Need Support?
Get help from our FAQ page, enhanced with AI, or join our Discord server and talk with the devs.
${systemText[systemLanguage][39]}
${systemText[systemLanguage][41]}
${systemText[systemLanguage][42]}
${systemText[systemLanguage][43]}
${systemText[systemLanguage][44]}
${systemText[systemLanguage][45]}
${systemText[systemLanguage][47]}
${systemText[systemLanguage][48]}
${systemText[systemLanguage][48]}
${VERSION_NAME}
PREVIOUS
NEXT
${systemText[systemLanguage][52]}
Duolingo
PRO 3.1
${systemText[systemLanguage][53]}
${systemText[systemLanguage][61]}
${systemText[systemLanguage][54]}
Support
${VERSION_NAME}
Response Times
It may take a few hours for a developer to respond to you. You will be notified in Duolingo PRO when there's a reply.
Send a message to start talking with a support member.
This chat was closed.
We hope to have solved your issue. If not, you can start a new chat.
Start a New Chat
Drop here to attach
${systemText[systemLanguage][32]}
${VERSION_NAME}
Duolingo Max
Skip the worry of running out of hearts, get free entry to legendary challenges, access to personalized practice, and learn without ads. Do not turn on if you already have Super Duolingo or Max.
Help Improve Duolingo PRO
Allow Duolingo PRO to automatically send anonymous bug reports and usage data to help us improve the script.
`;
indicator.addEventListener('click', () => {
indicator.remove();
scrollChatToBottom(chatBox, false, true);
});
const typingGroup = supportStack.querySelector('#DLP_Inset_Group_3');
const closedChatGroup = supportStack.querySelector('#DLP_Inset_Group_2');
if (typingGroup) {
supportStack.insertBefore(indicator, typingGroup);
} else if (closedChatGroup) {
supportStack.insertBefore(indicator, closedChatGroup);
} else {
supportStack.appendChild(indicator);
}
}
function ensureChatSpacer(chatBox) {
if (!chatBox) return;
if (chatBox.querySelector('.DLP_Chat_Spacer')) return;
const spacer = document.createElement('div');
spacer.className = 'DLP_Chat_Spacer';
chatBox.insertBefore(spacer, chatBox.firstChild);
}
function setupSupportPage() {
const container = document.getElementById("DLP_Main_Box_Divider_11_ID");
const chatBox = container.querySelector('.DLP_Chat_Box_1_ID_1');
const attachmentVisualButton = container.querySelector('#DLP_Inset_Button_1_ID');
const sendButton = container.querySelector("#DLP_Inset_Button_2_ID");
const attachmentInput = container.querySelector("#DLP_Attachment_Input_1");
const messageInput = container.querySelector("#DLP_Inset_Input_1_ID");
const activeContainer = container.querySelector('.DLP_Input_Style_1_Active');
let messageSendInProgress = false;
ensureChatSpacer(chatBox);
chatBox.addEventListener('scroll', () => {
chatPinnedToBottom = isChatAtBottom(chatBox, 12);
if (chatPinnedToBottom) {
removeNewMessageIndicator();
}
});
function resetMessageInputState() {
messageInput.value = '';
messageInput.style.height = '1.2em';
if (activeContainer) activeContainer.style.height = '48px';
messageInput.scrollTop = 0;
checkSendButton();
}
function setupCard() {
let card = document.getElementById("DLP_Main_Box_Divider_11_ID").querySelector("#DLP_Inset_Card_1");
let cardExpanded = false;
let cardAnimating = false;
let descriptionText = card.querySelectorAll(':scope > .DLP_Text_Style_1');
card.addEventListener('click', () => {
if (cardAnimating) return;
cardAnimating = true;
if (!cardExpanded) {
let cardHeight = card.offsetHeight;
let textHeight = false;
if (descriptionText.length > 0) {
textHeight = Array.from(descriptionText).map(() => "0");
descriptionText.forEach(element => {
element.style.display = 'block';
element.style.height = 'auto';
});
}
void card.offsetHeight;
let newCardHeight = card.offsetHeight;
let newTextHeight = false;
if (descriptionText.length > 0) {
newTextHeight = Array.from(descriptionText).map(element => element.offsetHeight);
}
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.height = '0px';
});
}
card.style.height = `${cardHeight}px`;
void card.offsetHeight;
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.filter = 'blur(0px)';
element.style.opacity = '1';
});
}
card.style.height = `${newCardHeight}px`;
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.height = `${newTextHeight[Array.from(descriptionText).indexOf(element)]}px`;
});
}
card.querySelector('.DLP_HStack_6').lastElementChild.style.transition = 'all 0.4s cubic-bezier(0.16, 1, 0.32, 1)';
card.querySelector('.DLP_HStack_6').lastElementChild.style.transform = 'rotate(90deg)';
setTimeout(() => {
card.style.height = 'auto';
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.height = 'auto';
});
}
cardExpanded = true;
cardAnimating = false;
}, 400);
} else {
let cardHeight = card.offsetHeight;
let textHeight = false;
if (descriptionText.length > 0) {
textHeight = Array.from(descriptionText).map(element => element.offsetHeight);
descriptionText.forEach(element => {
element.style.display = 'none';
});
}
void card.offsetHeight;
let newCardHeight = card.offsetHeight;
let newTextHeight = false;
if (descriptionText.length > 0) {
newTextHeight = Array.from(descriptionText).map(() => "0");
descriptionText.forEach(element => {
element.style.display = 'block';
element.style.height = `${textHeight[Array.from(descriptionText).indexOf(element)]}px`;
});
}
card.style.height = `${cardHeight}px`;
void card.offsetHeight;
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.filter = 'blur(4px)';
element.style.opacity = '0';
});
}
card.style.height = `${newCardHeight}px`;
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.height = '0px';
});
}
card.querySelector('.DLP_HStack_6').lastElementChild.style.transition = 'all 0.4s cubic-bezier(0.16, 1, 0.32, 1)';
card.querySelector('.DLP_HStack_6').lastElementChild.style.transform = 'rotate(0deg)';
setTimeout(() => {
card.style.height = 'auto';
if (descriptionText.length > 0) {
descriptionText.forEach(element => {
element.style.display = 'none';
});
}
cardExpanded = false;
cardAnimating = false;
}, 400);
}
});
}
setupCard();
function markTempMessageFailed(tempId) {
const tempState = pendingTempMessages.get(tempId);
if (tempState) {
tempState.sendFailed = true;
}
const tempElements = chatBox.querySelectorAll(`[data-is-temp="${tempId}"]`);
tempElements.forEach(element => {
element.style.animation = '';
element.style.color = 'rgba(var(--DLP-pink))';
});
}
function setupSendButton() {
sendButton.addEventListener('click', async () => {
if (messageSendInProgress) return;
messageSendInProgress = true;
checkSendButton();
lastTypingSent = true;
if (!storageLocal.chatKey || storageLocal.chatKey.length === 0) {
if (container?.querySelector('#DLP_Inset_Group_5')?.style.display !== 'none') container.querySelector('#DLP_Inset_Group_5').style.display = 'none';
if (chatBox?.style.display === 'none') chatBox.style.display = 'flex';
try {
let response = await fetch(apiURL + "/chats/create", {
method: "POST",
headers: {
"Authorization": `Bearer ${document.cookie.split(';').find(cookie => cookie.includes('jwt_token')).split('=')[1]}`
},
body: JSON.stringify({
"version": VERSION_FULL
})
});
let data = await response.json();
if (data?.status === false && data?.notification) {
showNotification(data.notification.icon, data.notification.head, data.notification.body, data.notification.duration);
return;
}
storageLocal.chatKey = [data.chat_key];
saveStorageLocal();
} catch (error) {
console.error("Fetch error:", error);
}
}
let formData = new FormData();
formData.append("message", messageInput.value);
formData.append("version", VERSION_FULL);
let fileUrls = [];
for (const attachment of allAttachments[currentChatId] ?? []) {
const file = attachment.file;
formData.append("files", file);
const url = URL.createObjectURL(file);
fileUrls.push(url);
}
let chatTempSendNumber = chatTempSendList.length ? chatTempSendList[chatTempSendList.length - 1] + 1 : 1;
const tempMessageId = `temp-${chatTempSendNumber}`;
let tempData = {
"accent": '#007AFF',
"author": userBioData.username,
"edited": false,
"files": fileUrls,
"message": messageInput.value,
"profile_picture": userBioData.profile_picture,
"role": "You",
"send_time": Number(Date.now()),
"message_id": tempMessageId
};
pendingTempMessages.set(chatTempSendNumber, {
...tempData,
files: [...tempData.files]
});
registerChatLookupMessage(tempData, chatTempSendNumber);
createMessage(tempData, false, chatTempSendNumber);
chatTempSendList.push(chatTempSendNumber);
scrollChatToBottom(chatBox, true);
allAttachments[currentChatId] = [];
renderAttachmentsPreview();
resetMessageInputState();
try {
let response = await fetch(apiURL + "/chats/send_message", {
method: "POST",
headers: alpha ? {
'Authorization': `Bearer ${document.cookie.split(';').find(cookie => cookie.includes('jwt_token')).split('=')[1]}`,
'X-Chat-Key': `${storageLocal.chatKey[0]}`
} : {
'X-Chat-Key': `${storageLocal.chatKey[0]}`
},
body: formData
});
let responseData = await response.json();
const isNewMessageFormat = response.ok && responseData && typeof responseData === 'object' && Object.prototype.hasOwnProperty.call(responseData, 'message_id');
if (isNewMessageFormat) {
const wasAtBottom = Math.abs(chatBox.scrollHeight - (chatBox.scrollTop + chatBox.clientHeight)) < 5;
chatBox.querySelectorAll(`[data-is-temp="${chatTempSendNumber}"]`).forEach(element => {
element.remove();
});
registerChatLookupMessage(responseData);
createMessage(responseData);
const tempIndex = chatTempSendList.indexOf(chatTempSendNumber);
if (tempIndex !== -1) {
chatTempSendList.splice(tempIndex, 1);
}
pendingTempMessages.delete(chatTempSendNumber);
if (wasAtBottom) {
scrollChatToBottom(chatBox, true);
}
} else {
if (responseData?.status === false && responseData?.notification) {
showNotification(responseData.notification.icon, responseData.notification.head, responseData.notification.body, responseData.notification.duration);
} else {
showNotification("error", "Send Failed", "We could not verify that your message was delivered. Please try again.", 8);
}
markTempMessageFailed(chatTempSendNumber);
}
} catch (error) {
console.error("Fetch error:", error);
markTempMessageFailed(chatTempSendNumber);
} finally {
messageSendInProgress = false;
checkSendButton();
}
});
}
setupSendButton();
function setupTextInput() {
const sendButton = container.querySelector("#DLP_Inset_Button_2_ID");
resetMessageInputState();
messageInput.addEventListener('input', function () {
lastTypingChat = Date.now();
lastTypingSent = false;
messageInput.style.height = '1.2em';
const lineHeight = parseInt(getComputedStyle(messageInput).lineHeight);
const maxRows = 5;
const maxHeight = lineHeight * maxRows;
const newHeight = Math.min((messageInput.scrollHeight - 32), maxHeight);
messageInput.style.height = newHeight + 'px';
if (newHeight < 20) {
activeContainer.style.height = '48px';
} else {
activeContainer.style.height = (newHeight + 32) + 'px';
}
checkSendButton();
});
messageInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (sendButton.style.pointerEvents !== 'none') {
sendButton.click();
}
}
});
}
setupTextInput();
let nextAttachmentId = 0;
let attachmentDropBoxExpanded = false;
function setupAttachmentsInput() {
const attachmentBox = container.querySelector('#DLP_Attachment_Preview_Parent');
const attachmentBoxDrop = attachmentBox.querySelector('.DLP_Attachment_Box_Drop_1');
attachmentBoxDrop.addEventListener('dragenter', event => {
event.preventDefault();
//attachmentBoxDrop.style.outline = '2px solid rgba(var(--DLP-blue), 0.20)';
attachmentBoxDrop.firstElementChild.style.opacity = '1';
});
attachmentBoxDrop.addEventListener('dragleave', event => {
event.preventDefault();
if (attachmentBoxDrop.contains(event.relatedTarget)) return;
//attachmentBoxDrop.style.outline = '2px dashed rgba(var(--DLP-blue), 0.20)';
attachmentBoxDrop.firstElementChild.style.opacity = '0.5';
});
window.addEventListener('dragover', (event) => {
event.preventDefault();
if (attachmentInput.disabled) return;
if (event.dataTransfer && event.dataTransfer.types.includes('Files')) {
if (attachmentBox.style.display === 'none') {
attachmentBox.style.display = '';
}
[...attachmentBox.children].forEach(child => {
if (child !== attachmentBoxDrop) {
child.style.display = 'none';
}
});
attachmentBoxDrop.style.display = '';
}
});
window.addEventListener('dragleave', (event) => {
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
if (attachmentBox.children.length === 1 && attachmentBox.children[0] === attachmentBoxDrop) {
attachmentBox.style.display = 'none';
}
[...attachmentBox.children].forEach(child => {
if (child !== attachmentBoxDrop) {
child.style.display = '';
}
});
attachmentBoxDrop.style.display = 'none';
//attachmentBoxDrop.style.outline = '2px dashed a(var(--DLP-blue), 0.20)';
attachmentBoxDrop.firstElementChild.style.opacity = '0.5';
}
});
window.addEventListener('drop', (event) => {
event.preventDefault();
[...attachmentBox.children].forEach(child => {
if (child !== attachmentBoxDrop) {
child.style.display = '';
}
});
attachmentBoxDrop.style.display = 'none';
//attachmentBoxDrop.style.outline = '2px dashed rgba(var(--DLP-blue), 0.20)';
attachmentBoxDrop.firstElementChild.style.opacity = '0.5';
});
attachmentBoxDrop.addEventListener('drop', (event) => {
event.preventDefault();
attachmentBoxDrop.style.display = 'none';
const selectedFiles = Array.from(event.dataTransfer.files);
triggerInputAttachments(selectedFiles);
});
attachmentVisualButton.addEventListener('click', () => {
if (storageSession.script.support.file_upload.enabled === false) {
showNotification("warning", "Feature Disabled", "This feature has been temporarily disabled.", 15);
return;
}
attachmentInput.click();
});
attachmentInput.addEventListener('change', (event) => {
const selectedFiles = Array.from(event.target.files);
triggerInputAttachments(selectedFiles);
});
}
setupAttachmentsInput();
function triggerInputAttachments(selectedFiles) {
if (!allAttachments[currentChatId]) {
allAttachments[currentChatId] = [];
}
const validFiles = [];
selectedFiles.forEach(file => {
if (file.size > storageSession.script.support.file_upload.max_size) {
showNotification("warning", "File Too Large", `${file.name} is over ${storageSession.script.support.file_upload.max_size / 1024 / 1024} MB, please choose a smaller file.`, 10);
} else {
validFiles.push(file);
}
});
const remainingSlots = storageSession.script.support.file_upload.max_files - allAttachments[currentChatId]?.length;
if (validFiles.length > remainingSlots) {
showNotification("warning", "Too Many Files", `You can only attach up to ${storageSession.script.support.file_upload.max_files} files at once.`, 10);
validFiles.length = remainingSlots;
}
validFiles.forEach(file => {
allAttachments[currentChatId]?.push({ id: String(nextAttachmentId++), file }); // wrap each in an {id, file} and append
});
updateAttachmentsInput();
renderAttachmentsPreview();
checkSendButton();
attachmentInput.value = '';
}
function updateAttachmentsInput() {
const dt = new DataTransfer();
allAttachments[currentChatId]?.forEach(a => dt.items.add(a.file));
attachmentInput.files = dt.files;
}
function removeAttachmentById(id) {
allAttachments[currentChatId] = allAttachments[currentChatId]?.filter(a => a.id !== id);
updateAttachmentsInput();
renderAttachmentsPreview();
checkSendButton();
}
function renderAttachmentsPreview() {
const attachmentBox = container.querySelector('#DLP_Attachment_Preview_Parent');
const attachmentBoxDrop = attachmentBox.querySelector('.DLP_Attachment_Box_Drop_1');
const previewWasVisible = attachmentBox.style.display !== 'none';
const wasAtBottom = Math.abs(chatBox.scrollHeight - (chatBox.scrollTop + chatBox.clientHeight)) < 5;
const currentIds = new Set(allAttachments[currentChatId]?.map(a => a.id));
// 1) remove deleted attachments from the DOM
Array.from(attachmentBox.children).forEach(child => {
const childId = child.getAttribute('data-id');
if (!currentIds.has(childId) && child !== attachmentBoxDrop) {
attachmentBox.removeChild(child);
}
});
// 2) add new attachments to the DOM
allAttachments[currentChatId]?.forEach(({ id, file }) => {
if (attachmentBox.querySelector(`[data-id="${id}"]`)) return;
const url = URL.createObjectURL(file);
const box = document.createElement('div');
box.className = 'DLP_Attachment_Box_1';
box.setAttribute('data-id', id);
box.style.position = 'relative';
let media;
if (file.type.startsWith('image/')) {
media = document.createElement('img');
media.src = url;
media.className = 'DLP_Attachment_Box_1_Content';
} else if (file.type.startsWith('video/')) {
media = document.createElement('video');
media.src = url;
media.autoplay = true;
media.muted = true;
media.loop = true;
media.className = 'DLP_Attachment_Box_1_Content';
} else {
media = document.createElement('div');
media.style.display = 'flex';
media.style.width = '100%';
media.style.height = '100%';
media.style.paddingTop = '6px';
media.style.flexDirection = 'column';
media.style.justifyContent = 'center';
media.style.alignItems = 'center';
media.style.gap = '6px';
media.style.flexShrink = '0';
mediaChild1 = document.createElement('p');
mediaChild1.className = 'DLP_Text_Style_1 DLP_NoSelect';
mediaChild1.style.fontSize = '24px';
mediaChild1.textContent = '';
media.appendChild(mediaChild1);
mediaChild2 = document.createElement('p');
mediaChild2.className = 'DLP_Text_Style_1 DLP_NoSelect';
mediaChild2.style.opacity = '0.5';
mediaChild2.textContent = 'File';
//mediaChild2.textContent = file.name;
media.appendChild(mediaChild2);
}
// Create and append delete button
const hover = document.createElement('div');
hover.className = 'DLP_Attachment_Box_1_Hover';
hover.style.display = 'none';
box.addEventListener('mouseenter', () => {
hover.style.display = '';
});
box.addEventListener('mouseleave', () => {
hover.style.display = 'none';
});
const btn = document.createElement('p');
btn.className = 'DLP_Text_Style_1 DLP_Magnetic_Hover_1 DLP_NoSelect';
if ((file.type.startsWith('image/') || file.type.startsWith('video/'))) btn.textContent = '';
else btn.textContent = '';
btn.addEventListener('click', () => removeAttachmentById(id));
hover.appendChild(btn);
box.appendChild(media);
box.appendChild(hover);
attachmentBox.appendChild(box);
if (false) {
if (file.type.startsWith('image/')) {
media.addEventListener('load', () => updateContrast(box));
if (media.complete) updateContrast(box);
} else if (file.type.startsWith('video/')) {
media.addEventListener('loadeddata', () => {
const iv = setInterval(() => {
if (!document.contains(media)) {
clearInterval(iv);
} else {
updateContrast(box);
}
}, 250);
updateContrast(box);
});
} else {
updateContrast(box);
}
}
});
// Show or hide the attachmentBox and adjust padding/navigation
if ((allAttachments[currentChatId]?.length ?? 0) === 0 && attachmentDropBoxExpanded) {
attachmentBox.style.display = 'none';
attachmentDropBoxExpanded = false;
//const nav = document.querySelector('#DLP_Main_Navigation_Box_5_ID .DLP_Col.DLP_Fill_Col.DLP_Fill_Row.DLP_Gap_8');
//nav.style.paddingBottom = `${parseFloat(getComputedStyle(nav).paddingBottom) - 104}px`;
} else if (allAttachments[currentChatId]?.length > 0 && !attachmentDropBoxExpanded) {
attachmentBox.style.display = '';
attachmentDropBoxExpanded = true;
//const nav = document.querySelector('#DLP_Main_Navigation_Box_5_ID .DLP_Col.DLP_Fill_Col.DLP_Fill_Row.DLP_Gap_8');
//nav.style.paddingBottom = `${parseFloat(getComputedStyle(nav).paddingBottom) + 104}px`;
//void container.offsetHeight;
//document.querySelector('#DLP_Main_Navigation_Box_5_ID').scrollTop += 104;
}
const previewIsVisible = attachmentBox.style.display !== 'none';
if (wasAtBottom && previewWasVisible !== previewIsVisible) {
scrollChatToBottom(chatBox, true);
}
// Disable input if there are too many files
if (allAttachments[currentChatId]?.length >= storageSession.script.support.file_upload.max_files) {
attachmentInput.disabled = true;
attachmentVisualButton.style.opacity = '0.5';
attachmentVisualButton.style.pointerEvents = 'none';
} else {
attachmentInput.disabled = false;
attachmentVisualButton.style.opacity = '';
attachmentVisualButton.style.pointerEvents = '';
}
}
function setupCreateNewChatButton() {
let theButton = container.querySelector('#DLP_Inset_Group_2').querySelector('#DLP_Inset_Button_3_ID');
theButton.addEventListener('click', async () => {
theButton.style.opacity = "0.5";
theButton.style.pointerEvents = "none";
try {
let response = await fetch(apiURL + "/chats/create", {
method: "GET",
headers: {
"Authorization": `Bearer ${document.cookie.split(';').find(cookie => cookie.includes('jwt_token')).split('=')[1]}`
}
});
let data = await response.json();
storageLocal.chatKey = [data.chat_key];
saveStorageLocal();
chatBox.innerHTML = '';
ensureChatSpacer(chatBox);
container.querySelector('#DLP_Inset_Group_1').style.display = "";
container.querySelector('#DLP_Inset_Group_2').style.display = "none";
theButton.style.opacity = "";
theButton.style.pointerEvents = "";
} catch (error) {
console.error("Fetch error:", error);
theButton.style.opacity = "";
theButton.style.pointerEvents = "";
}
});
}
setupCreateNewChatButton();
function checkSendButton() {
if (messageSendInProgress) {
sendButton.style.opacity = "0.5";
sendButton.style.pointerEvents = "none";
return;
}
if (messageInput.value.trim() !== "" || (allAttachments[currentChatId]?.length ?? 0) > 0) {
sendButton.style.opacity = "";
sendButton.style.pointerEvents = "";
} else {
sendButton.style.opacity = "0.5";
sendButton.style.pointerEvents = "none";
}
}
checkSendButton();
}
setupSupportPage();
function muteTab(value) {
HTMLAudioElement.prototype.play = function () {
if (value) {
this.muted = true;
} else {
this.muted = false;
}
return originalPlay.apply(this, arguments);
};
}
let isSolveBusy = false;
let isSolveAllBusy = false;
let solveAllRunToken = 0;
function bumpSolveAllRunToken() {
solveAllRunToken += 1;
return solveAllRunToken;
}
document.addEventListener('keydown', function (event) {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
if (event.shiftKey) {
solving();
} else {
solve();
}
}
});
let currentQuestionId = null;
let hasLoggedForCurrent = 0;
async function logOnce(flag, sol, dom) {
// flag: 1 = solved, 2 = wrong, 3 = stuck
if ((flag === 2 && hasLoggedForCurrent === 0) || (flag === 3 && hasLoggedForCurrent === 2)) {
if (alpha) {
console.log(flag);
//console.log(sol);
//console.log(dom);
console.log(sol.challengeGeneratorIdentifier.generatorId);
}
hasLoggedForCurrent++;
if (storageLocal.settings.anonymousUsageData && storageSession.script.anonymous_analytics !== false) {
if (flag === 2) showNotification("error", "Legacy Solved Incorrectly", "Legacy has detected that it solved a question incorrectly. A report has been made under ID: " + sol.challengeGeneratorIdentifier.generatorId, 10);
else if (flag === 3) showNotification("error", "Legacy is Stuck", "Legacy has detected that it is stuck on a question. A report has been made under ID: " + sol.challengeGeneratorIdentifier.generatorId, 10);
const payload = {
version: VERSION_FULL,
random: storageLocal.random16,
flag: flag,
sol: sol,
dom: dom.outerHTML
};
console.log(sol);
const response = await fetch("https://api.duolingopro.net/analytics/legacy", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
} else {
if (flag === 2) showNotification("error", "Legacy Solved Incorrectly", "Legacy has detected that it solved a question incorrectly. Turn on share anonymous usage data in settings to help us fix this bug.", 10);
else if (flag === 3) showNotification("error", "Legacy is Stuck", "Legacy has detected that it is stuck on a question. Turn on share anonymous usage data in settings to help us fix this bug.", 10);
}
}
}
function updateSolveButtonText(text) {
try {
document.getElementById("solveAllButton").innerText = text;
} catch (error) {
console.log(error);
}
}
async function solving(value) {
if (value === "start") isAutoMode = true;
else if (value === "stop") isAutoMode = false;
else isAutoMode = !isAutoMode;
const activeSolveAllRunToken = bumpSolveAllRunToken();
updateSolveButtonText(isAutoMode ? systemText[systemLanguage][102] : systemText[systemLanguage][101]);
function startSolvingLoop(runToken) {
if (solvingLoopRunning || !isAutoMode || runToken !== solveAllRunToken) return;
solvingLoopRunning = true;
let initialUrl = window.location.href;
// Fire-and-forget async loop
(async function runLoop() {
while (isAutoMode && runToken === solveAllRunToken) {
// Safety: Stop if URL changes
if (window.location.href !== initialUrl) {
isAutoMode = false;
updateSolveButtonText(isAutoMode ? systemText[systemLanguage][102] : systemText[systemLanguage][101]);
break;
}
// A. Start Timer
const startTime = Date.now();
const targetDelay = storageLocal.settings.randomSolveSpeed
? Math.floor((
storageLocal.settings.randomSolveSpeedRange[0] + (crypto.getRandomValues(new Uint32Array(1))[0] / 4294967295) *
(storageLocal.settings.randomSolveSpeedRange[1] - storageLocal.settings.randomSolveSpeedRange[0])
) * 1000)
: 400;
// B. Run Logic (Wait for it to fully finish)
await solve(true, true, runToken);
await new Promise(resolve => setTimeout(resolve, 100));
// Check if stopped while solve() was running
if (!isAutoMode || runToken !== solveAllRunToken) break;
// C. Calculate Timing
const elapsedTime = Date.now() - startTime;
const remainingTime = targetDelay - elapsedTime;
// D. Wait the remainder (if solve() was faster than solveSpeed)
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
}
// Cleanup when loop breaks
solvingLoopRunning = false;
if (isAutoMode && runToken !== solveAllRunToken) {
startSolvingLoop(solveAllRunToken);
}
})();
}
// 2. Start the Async Loop (Only if not already running)
if (isAutoMode) {
startSolvingLoop(activeSolveAllRunToken);
}
}
async function solve(check = true, skip = false, runToken = solveAllRunToken) {
if (isSolveBusy) return;
isSolveBusy = true;
syncReactLookupByContext();
try {
const practiceAgain = document.querySelector('[data-test="player-practice-again"]');
const sessionCompleteSlide = document.querySelector('[data-test="session-complete-slide"]');
const selectorsForSkip = [
'[data-test="practice-hub-ad-no-thanks-button"]',
'.vpDIE',
'[data-test="plus-no-thanks"]',
'._1N-oo._36Vd3._16r-S._1ZBYz._23KDq._1S2uf.HakPM',
'._8AMBh._2vfJy._3Qy5R._28UWu._3h0lA._1S2uf._1E9sc',
'._1Qh5D._36g4N._2YF0P._28UWu._3h0lA._1S2uf._1E9sc',
'[data-test="story-start"]',
'._3bBpU._1x5JY._1M9iF._36g4N._2YF0P.T7I0c._2EnxW.MYehf',
'._2V6ug._1ursp._7jW2t._28UWu._3h0lA._1S2uf._1E9sc', // No Thanks Legendary Button
'._1rcV8._1VYyp._1ursp._7jW2t._1gKir', // Language Score
'._2V6ug._1ursp._7jW2t._3zgLG' // Create Profile Later
];
selectorsForSkip.forEach(selector => {
const element = document.querySelector(selector);
if (element) element.click();
});
const status = storageSession.legacy.status;
const type = status ? storageSession.legacy[status]?.type : null;
let amount;
if (sessionCompleteSlide !== null && isAutoMode && storageSession.legacy.status) {
if (type === 'lesson') {
storageSession.legacy[status].amount -= 1;
saveStorageSession();
(((storageLocal.stats ??= {}).legacy ??= {})[status] ??= { lessons: 0 }).lessons++;
saveStorageLocal();
amount = status ? storageSession.legacy[status]?.amount : null;
if (amount > 0) {
if (practiceAgain !== null) {
practiceAgain.click();
return;
} else {
location.reload();
}
} else {
storageSession.legacy[status].amount = 0;
storageSession.legacy.status = false;
saveStorageSession();
window.location.href = "https://duolingo.com";
return;
}
} else if (type === 'xp') {
storageSession.legacy[status].amount -= findSubReact(document.getElementsByClassName("_1XNQX")[0]).xpGoalSessionProgress.totalXpThisSession;
saveStorageSession();
(((storageLocal.stats ??= {}).legacy ??= {})[status] ??= { lessons: 0 }).lessons++;
saveStorageLocal();
amount = status ? storageSession.legacy[status]?.amount : null;
if (amount > 0) {
if (practiceAgain !== null) {
practiceAgain.click();
return;
} else {
location.reload();
}
} else {
storageSession.legacy[status].amount = 0;
storageSession.legacy.status = false;
saveStorageSession();
window.location.href = "https://duolingo.com";
return;
}
} else if (type === 'infinity') {
(((storageLocal.stats ??= {}).legacy ??= {})[status] ??= { lessons: 0 }).lessons++;
saveStorageLocal();
if (practiceAgain !== null) {
practiceAgain.click();
return;
} else {
location.reload();
}
} else if (type === 'time') {
(((storageLocal.stats ??= {}).legacy ??= {})[status] ??= { lessons: 0 }).lessons++;
saveStorageLocal();
amount = status ? storageSession.legacy[status]?.amount : null;
if (amount > 0) {
if (practiceAgain !== null) {
practiceAgain.click();
return;
} else {
location.reload();
}
} else {
// Timer expired, stop the session
storageSession.legacy[status].amount = 0;
storageSession.legacy.status = false;
saveStorageSession();
window.location.href = "https://duolingo.com";
return;
}
}
}
window.sol = null;
try {
window.sol = findReact(document.getElementsByClassName(findReactMainElementClass)[0])?.props?.currentChallenge ?? null;
} catch (error) {
window.sol = null;
console.log(error);
//let next = document.querySelector('[data-test="player-next"]');
//if (next) {
// next.click();
//}
//return;
}
//if (!window.sol) {
// return;
//}
let challengeType;
if (window.sol) {
challengeType = determineChallengeType();
} else {
challengeType = 'error';
}
let questionKey;
if (window.sol && window.sol.id) {
questionKey = window.sol.id;
} else if (window.sol) {
// Fallback if no 'id' property: use type + prompt
questionKey = JSON.stringify({
type: window.sol.type,
prompt: window.sol.prompt || ''
});
} else {
questionKey = null;
}
if (questionKey !== currentQuestionId) {
currentQuestionId = questionKey;
hasLoggedForCurrent = 0;
}
if (challengeType === 'error') {
await Promise.race([
clickCheck(),
new Promise(resolve => setTimeout(resolve, 500))
]);
} else if (challengeType) {
if (debug) console.log("Challenge Type: " + challengeType);
let playerFooter1 = document.getElementById("session/PlayerFooter");
if ((playerFooter1 && playerFooter1.matches("._3rB4d._1VTif._2HXQ9")) || (!playerFooter1 && document.querySelector('._2i9lj'))) { // id="session/PlayerFooter", "._3rB4d._1VTif._2HXQ9" - Neutral
await Promise.race([
handleChallenge(challengeType),
new Promise(resolve => setTimeout(resolve, 2000))
]);
// await new Promise(r => requestAnimationFrame(r));
await new Promise(r => setTimeout(r, 50));
}
let skipInsteadOfCheck = false;
if (check && (playerFooter1 && playerFooter1.matches('._3rB4d._1VTif._2HXQ9')) || (!playerFooter1 && document.querySelector('._2i9lj'))) { // id="session/PlayerFooter" - Neutral
await Promise.race([
clickCheck(),
new Promise(resolve => setTimeout(resolve, 500))
]);
// await new Promise(r => requestAnimationFrame(r));
await new Promise(r => setTimeout(r, 50));
} else if (check && (playerFooter1 && !playerFooter1.matches('._3rB4d._1VTif._2HXQ9')) || ((!playerFooter1 && document.querySelector('._2i9lj')) && !document.querySelector('[data-test="stories-player-continue"]').disabled)) { // id="session/PlayerFooter" - NOT Neutral
skipInsteadOfCheck = true;
}
if (skip || skipInsteadOfCheck) {
await Promise.race([
clickNext(),
new Promise(resolve => setTimeout(resolve, 500))
]);
}
} else {
await Promise.race([
clickCheck(),
new Promise(resolve => setTimeout(resolve, 500))
]);
}
} finally {
isSolveBusy = false;
}
}
async function clickCheck() {
try {
let nextButtonNormal = document.querySelector('[data-test="player-next"]');
let storiesContinueButton = document.querySelector('[data-test="stories-player-continue"]');
let storiesDoneButton = document.querySelector('[data-test="stories-player-done"]');
let nextButtonAriaValueNormal = nextButtonNormal ? nextButtonNormal.getAttribute('aria-disabled') : null;
let nextButtonAriaValueStoriesContinue = storiesContinueButton ? storiesContinueButton.disabled : null;
let nextButton = nextButtonNormal || storiesContinueButton || storiesDoneButton;
let nextButtonAriaValue = nextButtonAriaValueNormal || nextButtonAriaValueStoriesContinue || storiesDoneButton;
if (nextButton) {
if (String(nextButtonAriaValue) === 'true') { // Case: Button is Disabled
logOnce(3, window.sol, document.querySelector('.RMEuZ._1GVfY'));
} else if (String(nextButtonAriaValue) === 'false' && (nextButton.classList.length === 7 && nextButton.matches('._1rcV8._1VYyp._1ursp._7jW2t._3DbUj._38g3s._2oGJR'))) { // Case: Button is Enabled (Click "Check")
nextButton.click();
// await new Promise(r => requestAnimationFrame(r)); // Wait one tick for the UI to update with success/fail classes
await new Promise(r => setTimeout(r, 50));
if (nextButton && nextButton.classList.contains('_2oGJR')) { // Green / Correct
logOnce(1, window.sol, document.querySelector('.RMEuZ._1GVfY'));
const status = storageSession.legacy.status;
(((storageLocal.stats ??= {}).legacy ??= {})[status] ??= { questions: 0 }).questions++;
saveStorageLocal();
} else if (nextButton && nextButton.classList.contains('_3S8jJ')) { // Red / Incorrect
logOnce(2, window.sol, document.querySelector('.RMEuZ._1GVfY'));
} else {
if (debug) console.log('The element does not have the class ._9C_ii or .NAidc or the element is not found.');
}
} else {
if (debug) console.log('The aria-disabled attribute is not set or has an unexpected value.');
nextButton.click();
}
} else {
if (debug) console.log('Element with data-test="player-next" or data-test="stories-player-continue" not found.');
}
} catch (error) {
console.error(error);
}
}
async function clickNext() {
// 1. Identify the element to watch BEFORE clicking
const challengeElement = document.querySelector('[data-test~="challenge"]');
let observer = null;
let clicked = false;
// 2. Create the promise that resolves only when the element is removed
const removalPromise = challengeElement ? new Promise((resolve) => {
// If it's already gone, resolve immediately
if (!document.body.contains(challengeElement)) return resolve();
observer = new MutationObserver(() => {
if (!document.body.contains(challengeElement)) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}) : Promise.resolve();
try {
// 3. Find and Click the button
let nextButton = document.querySelector('[data-test="player-next"]') ||
document.querySelector('[data-test="stories-player-continue"]') ||
document.querySelector('[data-test="stories-player-done"]');
if (nextButton) {
nextButton.click();
clicked = true;
} else {
if (debug) console.log('Next button not found in clickNext.');
}
} catch (error) {
console.error(error);
} finally {
// 4. Wait logic
if (clicked && challengeElement) {
await removalPromise;
} else if (observer) {
// If we didn't click or something failed, clean up the observer
observer.disconnect();
}
}
}
function getCleanButtonText(button) {
// Check if button contains ruby elements
const rubyElements = button.querySelectorAll('ruby');
if (rubyElements.length > 0) {
// Extract only the base text (not the rt annotations)
let text = '';
rubyElements.forEach(ruby => {
const baseTextElements = ruby.querySelectorAll('span[lang]:not(rt)');
baseTextElements.forEach(span => {
text += span.textContent;
});
});
return text.trim();
} else {
// Fallback for non-ruby elements
const textElement = button.querySelector('[data-test="challenge-tap-token-text"]');
return textElement ? textElement.innerText.trim() : button.innerText.trim();
}
}
function determineChallengeType() {
try {
//console.log(window.sol);
if (document.getElementsByClassName("FmlUF").length > 0) {
// Story
if (window.sol.type === "arrange") {
return "Story Arrange"
} else if (window.sol.type === "multiple-choice" || window.sol.type === "select-phrases") {
return "Story Multiple Choice"
} else if (window.sol.type === "point-to-phrase") {
return "Story Point to Phrase"
} else if (window.sol.type === "match") {
return "Story Pairs"
}
} else {
// Lesson
if (document.querySelectorAll('[data-test*="challenge-speak"]').length > 0) {
return 'Challenge Speak';
} else if (window.sol.type === 'syllableTap') {
return 'Syllable Tap';
} else if (window.sol.type === 'syllableListenTap') {
return 'Syllable Listen Tap';
} else if (window.sol.type === 'tapCompleteTable') {
return 'Tap Complete Table';
} else if (window.sol.type === 'typeCloze') {
return 'Type Cloze';
} else if (window.sol.type === 'typeClozeTable') {
return 'Type Cloze Table';
} else if (window.sol.type === 'tapClozeTable') {
return 'Tap Cloze Table';
} else if (window.sol.type === 'typeCompleteTable') {
return 'Type Complete Table';
} else if (window.sol.type === 'patternTapComplete') {
return 'Pattern Tap Complete';
} else if (window.sol.type === 'completeReverseTranslation') {
return 'Complete Reverse Translation';
} else if (document.querySelectorAll('[data-test*="challenge-name"]').length > 0 && document.querySelectorAll('[data-test="challenge-choice"]').length > 0) {
return 'Challenge Name';
} else if (window.sol.type === 'listenMatch') {
return 'Listen Match';
} else if (document.querySelectorAll('[data-test="challenge challenge-characterWrite"]').length > 0) {
if (document.querySelector('g._25Ktp')) {
return 'Character Write Drag';
} else if (document.querySelectorAll('path._1e5Zt').length > 0) {
return 'Character Write Draw';
} else {
return 'Character Write Freehand';
}
} else if (document.querySelectorAll('[data-test="challenge challenge-listenSpeak"]').length > 0) {
return 'Listen Speak';
} else if (document.querySelectorAll('[data-test="challenge-choice"]').length > 0) {
if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) {
return 'Challenge Choice with Text Input';
} else {
return 'Challenge Choice'
}
} else if (document.querySelectorAll('[data-test$="challenge-tap-token"]').length > 0) {
if (window.sol.pairs !== undefined) {
return 'Pairs';
} else if (window.sol.correctTokens !== undefined) {
return 'Tokens Run';
} else if (window.sol.correctIndices !== undefined) {
return 'Indices Run';
}
} else if (document.querySelectorAll('[data-test="challenge-tap-token-text"]').length > 0) {
return 'Fill in the Gap';
} else if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) {
return 'Challenge Text Input';
} else if (document.querySelectorAll('[data-test*="challenge-partialReverseTranslate"]').length > 0) {
return 'Partial Reverse';
} else if (document.querySelectorAll('textarea[data-test="challenge-translate-input"]').length > 0) {
return 'Challenge Translate Input';
} else if (document.querySelectorAll('[data-test="session-complete-slide"]').length > 0) {
return 'Session Complete';
} else if (document.querySelectorAll('[data-test="daily-quest-progress-slide"]').length > 0) {
return 'Daily Quest Progress';
} else if (document.querySelectorAll('[data-test="streak-slide"]').length > 0) {
return 'Streak';
} else if (document.querySelectorAll('[data-test="leaderboard-slide"]').length > 0) {
return 'Leaderboard';
} else {
return false;
}
}
} catch (error) {
console.log(error);
return 'error';
}
}
async function handleChallenge(challengeType) {
const sleep = (ms) => new Promise(r => setTimeout(r, ms)); // Helper for awaiting UI updates/animations
if (challengeType === 'Challenge Speak' || challengeType === 'Listen Match' || challengeType === 'Listen Speak') {
const buttonSkip = document.querySelector('button[data-test="player-skip"]');
buttonSkip?.click();
} else if (challengeType === 'Challenge Choice' || challengeType === 'Challenge Choice with Text Input') {
if (challengeType === 'Challenge Choice with Text Input') {
let elm = document.querySelectorAll('[data-test="challenge-text-input"]')[0];
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(elm, window.sol.correctSolutions ? window.sol.correctSolutions[0].split(/(?<=^\S+)\s/)[1] : (window.sol.displayTokens ? window.sol.displayTokens.find(t => t.isBlank).text : window.sol.prompt));
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
} else if (challengeType === 'Challenge Choice') {
document.querySelectorAll("[data-test='challenge-choice']")[window.sol.correctIndex].click();
}
} else if (challengeType === 'Pairs') {
let nl = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
window.sol.pairs?.forEach((pair) => {
for (let i = 0; i < nl.length; i++) {
if (nl[i].disabled) continue;
const buttonText = getCleanButtonText(nl[i]).toLowerCase();
try {
if (
buttonText === pair.transliteration.toLowerCase().trim() ||
buttonText === pair.character.toLowerCase().trim()
) {
nl[i].click();
}
} catch (TypeError) {
if (
buttonText === pair.learningToken.toLowerCase().trim() ||
buttonText === pair.fromToken.toLowerCase().trim()
) {
nl[i].click();
}
}
}
});
} else if (challengeType === 'Story Pairs') {
const nl = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
const textToElementMap = new Map();
for (let i = 0; i < nl.length; i++) {
const text = getCleanButtonText(nl[i]).toLowerCase();
textToElementMap.set(text, nl[i]);
}
for (const key in window.sol.dictionary) {
if (window.sol.dictionary.hasOwnProperty(key)) {
const value = window.sol.dictionary[key];
const keyPart = key.split(":")[1].toLowerCase().trim();
const normalizedValue = value.toLowerCase().trim();
const element1 = textToElementMap.get(keyPart);
const element2 = textToElementMap.get(normalizedValue);
if (element1 && !element1.disabled) element1.click();
if (element2 && !element2.disabled) element2.click();
}
}
} else if (challengeType === 'Tap Complete Table') {
const solutionRows = window.sol.displayTableTokens.slice(1);
const tableRowElements = document.querySelectorAll('tbody tr');
const wordBank = document.querySelector('div[data-test="word-bank"]');
const wordBankButtons = wordBank ? wordBank.querySelectorAll('button[data-test*="-challenge-tap-token"]') : [];
const usedWordBankIndexes = new Set();
solutionRows.forEach((solutionRow, rowIndex) => {
const answerCellData = solutionRow[1];
const correctToken = answerCellData.find(token => token.isBlank);
if (correctToken) {
const correctAnswerText = correctToken.text;
const currentRowElement = tableRowElements[rowIndex];
let buttons = currentRowElement.querySelectorAll('button[data-test*="-challenge-tap-token"]');
let clicked = false;
if (buttons.length > 0) {
for (let button of buttons) {
const buttonText = getCleanButtonText(button);
if (buttonText === correctAnswerText && !button.disabled) {
button.click();
clicked = true;
break;
}
}
}
if (!clicked && wordBankButtons.length > 0) {
for (let i = 0; i < wordBankButtons.length; i++) {
if (usedWordBankIndexes.has(i)) continue;
const button = wordBankButtons[i];
const buttonText = getCleanButtonText(button);
if (buttonText === correctAnswerText && !button.disabled) {
button.click();
usedWordBankIndexes.add(i);
break;
}
}
}
}
});
} else if (challengeType === 'Tokens Run') {
const all_tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const correct_tokens = window.sol.correctTokens;
const clicked_tokens = [];
correct_tokens.forEach(correct_token => {
const matching_elements = Array.from(all_tokens).filter(element => {
const elementText = getCleanButtonText(element);
return elementText === correct_token.trim();
});
if (matching_elements.length > 0) {
const match_index = clicked_tokens.filter(token => {
const tokenText = getCleanButtonText(token);
return tokenText === correct_token.trim();
}).length;
if (match_index < matching_elements.length) {
matching_elements[match_index].click();
clicked_tokens.push(matching_elements[match_index]);
} else {
clicked_tokens.push(matching_elements[0]);
}
}
});
} else if (challengeType === 'Indices Run' || challengeType === 'Fill in the Gap') {
if (window.sol.correctIndices) {
window.sol.correctIndices?.forEach(index => {
document.querySelectorAll('div[data-test="word-bank"] [data-test*="challenge-tap-token"]:not(span)')[index].click();
});
}
} else if (challengeType === 'Challenge Text Input') {
let elm = document.querySelectorAll('[data-test="challenge-text-input"]')[0];
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(elm, window.sol.correctSolutions ? window.sol.correctSolutions[0] : (window.sol.displayTokens ? window.sol.displayTokens.find(t => t.isBlank).text : window.sol.prompt));
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
} else if (challengeType === 'Partial Reverse') {
let elm = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector("span[contenteditable]");
let nativeInputNodeTextSetter = Object.getOwnPropertyDescriptor(Node.prototype, "textContent").set
nativeInputNodeTextSetter.call(elm, window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join()?.replaceAll(',', ''));
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
} else if (challengeType === 'Challenge Translate Input') {
const elm = document.querySelector('textarea[data-test="challenge-translate-input"]');
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeInputValueSetter.call(elm, window.sol.correctSolutions ? window.sol.correctSolutions[0] : window.sol.prompt);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
} else if (challengeType === 'Challenge Name') {
let articles = findReact(document.getElementsByClassName(findReactMainElementClass)[0]).props.currentChallenge.articles;
let correctSolutions = findReact(document.getElementsByClassName(findReactMainElementClass)[0]).props.currentChallenge.correctSolutions[0];
let matchingArticle = articles.find(article => correctSolutions.startsWith(article));
let matchingIndex = matchingArticle !== undefined ? articles.indexOf(matchingArticle) : null;
let remainingValue = correctSolutions.substring(matchingArticle.length);
let selectedElement = document.querySelector(`[data-test="challenge-choice"]:nth-child(${matchingIndex + 1})`);
if (selectedElement) {
selectedElement.click();
}
let elm = document.querySelector('[data-test="challenge-text-input"]');
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(elm, remainingValue);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
} else if (challengeType === 'Type Cloze') {
const input = document.querySelector('input[type="text"].b4jqk');
if (!input) return;
let targetToken = window.sol.displayTokens.find(t => t.damageStart !== undefined);
let correctWord = targetToken?.text || "";
let correctEnding = "";
if (typeof targetToken?.damageStart === "number") {
correctEnding = correctWord.slice(targetToken.damageStart);
}
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, correctEnding);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
} else if (challengeType === 'Type Cloze Table') {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => typeof t.damageStart === "number");
if (answerCell && tableRows[i]) {
const input = tableRows[i].querySelector('input[type="text"].b4jqk');
if (!input) return;
const correctWord = answerCell.text;
const correctEnding = correctWord.slice(answerCell.damageStart);
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, correctEnding);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}
});
} else if (challengeType === 'Tap Cloze Table') {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => typeof t.damageStart === "number");
if (!answerCell || !tableRows[i]) return;
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
const wordButtons = wordBank ? Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')) : [];
const correctWord = answerCell.text;
const correctEnding = correctWord.slice(answerCell.damageStart);
let endingMatched = "";
let used = new Set();
for (let btn of wordButtons) {
const btnText = getCleanButtonText(btn);
if (!correctEnding.startsWith(endingMatched + btnText)) continue;
btn.click();
endingMatched += btnText;
used.add(btn);
if (endingMatched === correctEnding) break;
}
});
} else if (challengeType === 'Type Complete Table') {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => t.isBlank);
if (!answerCell || !tableRows[i]) return;
const input = tableRows[i].querySelector('input[type="text"].b4jqk');
if (!input) return;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, answerCell.text);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
});
} else if (challengeType === 'Pattern Tap Complete') {
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
if (!wordBank) return;
const choices = window.sol.choices;
const correctIndex = window.sol.correctIndex ?? 0;
const correctText = choices[correctIndex];
const buttons = Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])'));
const targetButton = buttons.find(btn => {
const btnText = getCleanButtonText(btn);
return btnText === correctText;
});
if (targetButton) {
targetButton.click();
}
} else if (challengeType === 'Complete Reverse Translation') {
const blankTokens = window.sol.displayTokens.filter(t => t.isBlank);
const inputFields = document.querySelectorAll('[data-test="challenge-text-input"]');
inputFields.forEach((input, index) => {
if (blankTokens[index]) {
const answer = blankTokens[index].text;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, answer);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
});
} else if (challengeType === 'Character Write Drag') {
const strokes = window.sol.strokes;
const createEvent = (type, x, y, buttons) => new MouseEvent(type, { bubbles: true, clientX: x, clientY: y, buttons, button: 0 });
const normalize = (str) => str ? str.replace(/\s/g, '') : '';
for (let i = 0; i < strokes.length; i++) {
const targetPathData = normalize(strokes[i].path);
let path, handle;
// Poll for the correct stroke to appear
while (!path || !handle) {
const candidates = document.querySelectorAll('path._1e5Zt');
path = Array.from(candidates).find(p => normalize(p.getAttribute('d')) === targetPathData);
handle = document.querySelector('g._25Ktp');
if (!path || !handle) await sleep(10);
}
const matrix = path.getScreenCTM();
const len = path.getTotalLength();
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(len).matrixTransform(matrix);
// Execute Stroke Instantly
handle.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
// Fire all move events synchronously for maximum speed
const steps = 10;
for (let s = 1; s <= steps; s++) {
const p = path.getPointAtLength((s / steps) * len).matrixTransform(matrix);
const move = createEvent('mousemove', p.x, p.y, 1);
handle.dispatchEvent(move);
document.dispatchEvent(move);
}
// Anchor end and release
const finalMove = createEvent('mousemove', end.x, end.y, 1);
handle.dispatchEvent(finalMove);
document.dispatchEvent(finalMove);
// Tiny 5ms tick to ensure the app registers the cursor is at the end before lifting
await sleep(5);
handle.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
}
} else if (challengeType === 'Character Write Draw') {
const strokes = window.sol.strokes;
const createEvent = (type, x, y, buttons) => new MouseEvent(type, { bubbles: true, clientX: x, clientY: y, buttons, button: 0 });
const normalize = (str) => str ? str.replace(/\s/g, '') : '';
for (let i = 0; i < strokes.length; i++) {
const targetPathData = normalize(strokes[i].path);
let path, cursor;
while (!path || !cursor) {
const candidates = document.querySelectorAll('path._1e5Zt');
path = Array.from(candidates).find(p => normalize(p.getAttribute('d')) === targetPathData);
cursor = document.querySelector('g._1h31R:not(._25Ktp)');
if (!path || !cursor) await sleep(10);
}
const matrix = path.getScreenCTM();
const len = path.getTotalLength();
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(len).matrixTransform(matrix);
cursor.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
document.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
const steps = 10;
for (let s = 1; s <= steps; s++) {
const p = path.getPointAtLength((s / steps) * len).matrixTransform(matrix);
const move = createEvent('mousemove', p.x, p.y, 1);
cursor.dispatchEvent(move);
document.dispatchEvent(move);
}
const finalMove = createEvent('mousemove', end.x, end.y, 1);
cursor.dispatchEvent(finalMove);
document.dispatchEvent(finalMove);
await sleep(5);
cursor.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
}
} else if (challengeType === 'Character Write Freehand') {
const freehandStrokes = window.sol.strokes.filter(s => s.strokeDrawMode === 'FREEHAND');
const createEvent = (type, x, y, buttons) => new MouseEvent(type, { bubbles: true, clientX: x, clientY: y, buttons, button: 0 });
const normalize = (str) => str ? str.replace(/\s/g, '') : '';
for (let i = 0; i < freehandStrokes.length; i++) {
const targetPathData = normalize(freehandStrokes[i].path);
let path, svg;
while (!path || !svg) {
const candidates = document.querySelectorAll('path._22UPm');
path = Array.from(candidates).find(p => normalize(p.getAttribute('d')) === targetPathData);
svg = document.querySelector('svg.o1rqi');
if (!path || !svg) await sleep(10);
}
const matrix = path.getScreenCTM();
const len = path.getTotalLength();
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(len).matrixTransform(matrix);
svg.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
document.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
const steps = 10;
for (let s = 1; s <= steps; s++) {
const p = path.getPointAtLength((s / steps) * len).matrixTransform(matrix);
const move = createEvent('mousemove', p.x, p.y, 1);
svg.dispatchEvent(move);
document.dispatchEvent(move);
}
const finalMove = createEvent('mousemove', end.x, end.y, 1);
svg.dispatchEvent(finalMove);
document.dispatchEvent(finalMove);
await sleep(5);
svg.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
}
} else if (challengeType === 'Syllable Tap' || challengeType === 'Syllable Listen Tap') {
const correctIndices = window.sol.correctIndices;
const choicesData = window.sol.choices;
const domButtons = Array.from(document.querySelectorAll('[data-test="word-bank"] [data-test$="challenge-tap-token"]'));
correctIndices.forEach(index => {
const correctChoiceData = choicesData[index];
const correctText = correctChoiceData.text;
const matchingButton = domButtons.find(btn => getCleanButtonText(btn) === correctText);
if (matchingButton) {
matchingButton.click();
}
});
} else if (challengeType === 'Session Complete') {
} else if (challengeType === 'Story Arrange') {
let choices = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
for (let i = 0; i < window.sol.phraseOrder.length; i++) {
choices[window.sol.phraseOrder[i]].click();
}
} else if (challengeType === 'Story Multiple Choice') {
let choices = document.querySelectorAll('[data-test="stories-choice"]');
choices[window.sol.correctAnswerIndex].click();
} else if (challengeType === 'Story Point to Phrase') {
let choices = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
var correctIndex = -1;
for (let i = 0; i < window.sol.parts.length; i++) {
if (window.sol.parts[i].selectable === true) {
correctIndex += 1;
if (window.sol.correctAnswerIndex === i) {
choices[correctIndex].parentElement.click();
}
}
}
}
}
function findSubReact(dom, traverseUp = reactTraverseUp) {
if (!dom) return null;
const key = Object.keys(dom).find(key => key.startsWith("__reactProps"));
return dom?.[key]?.children?.props?.slide;
}
function findReact(dom, traverseUp = reactTraverseUp) {
if (!dom) return null;
const key = Object.keys(dom).find(key => {
return key.startsWith("__reactFiber$") // react 17+
|| key.startsWith("__reactInternalInstance$"); // react <17
});
const domFiber = dom[key];
if (domFiber == null) return null;
// react <16
if (domFiber._currentElement) {
let compFiber = domFiber._currentElement._owner;
for (let i = 0; i < traverseUp; i++) {
compFiber = compFiber._currentElement._owner;
}
return compFiber._instance;
}
// react 16+
const GetCompFiber = fiber => {
//return fiber._debugOwner; // this also works, but is __DEV__ only
let parentFiber = fiber.return;
while (typeof parentFiber.type == "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = GetCompFiber(compFiber);
}
return compFiber.stateNode;
}
window.findReact = findReact;
window.findSubReact = findSubReact;
window.ss = solving;
}
try {
if (false) {
if (storageLocal.languagePackVersion !== "00") {
if (!storageLocal.languagePack.hasOwnProperty(systemLanguage)) systemLanguage = "en";
systemText = storageLocal.languagePack;
setTimeout(() => { if (!duplicateCheck()) One(); }, 10);
} else {
systemLanguage = "en";
setTimeout(() => { if (!duplicateCheck()) One(); }, 10);
}
} else {
systemLanguage = "en";
setTimeout(() => { if (!duplicateCheck()) One(); }, 10);
}
} catch (error) {
console.log(error);
One();
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 anonymoushackerIV
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Duolingo PRO
The ultimate auto-solver and XP farming tool for Duolingo
---
## Table of Contents
- [About](#about)
- [Features](#features)
- [Getting Started](#getting-started)
- [Troubleshooting](#troubleshooting)
- [Screenshots](#screenshots)
- [Repository Activity](#repository-activity)
---
## About
**Duolingo PRO 3.1** is a powerful user-script designed to supercharge your Duolingo experience. Instantly gain XP, farm XP automatically, complete quests, climb the leaderboard, extend your streak, collect gems, and much more, all with minimal effort.
- **Latest Version:** 3.1 (Working as of April 2026)
- **Supported Platform:** Duolingo (Web)
- **Script Manager:** Tampermonkey (recommended)
- **Community:** [Join our Discord server](https://duolingopro.net/discord) for news, support, and discussions
- **Tutorials:** [Subscribe to our YouTube channel](https://duolingopro.net/youtube) for guides and latest features
---
## Features
- 🎯 Instantly earn XP
- 💎 Get free gems
- 🔥 Boost your streak
- 🤖 Automatically solve questions
- 🏆 Claim any badge you missed
- 🚀 Free XP boosts and streak freezes
- ❤️ Instantly refill hearts
- 🔄 Auto-complete lessons and practices (choose set or infinite runs)
- 🌙 Dark mode support
- 📝 Talk with support directly on the script
- 🛠️ More features coming soon (e.g., AutoServer)
---
## Getting Started
1. **Install [Tampermonkey](https://www.tampermonkey.net/)** (recommended userscript manager for Chrome/Chromium browsers).
2. **Download the latest Duolingo PRO script** from this repository.
3. **Enable Developer Mode** in Chrome (or Chromium browser):
`Settings > Extensions > Developer Mode ON`
4. **Enable Allow Userscripts** in extension details in Chrome (or Chromium browser):
`Settings > Extensions > [extension] Details > Allow Userscripts ON`
5. **Join our [Discord server](https://duolingopro.net/discord)** for updates, support, and community discussions.
6. **Watch our [YouTube tutorials](https://duolingopro.net/youtube)** for detailed setup and usage guides.
---
## Troubleshooting
If you encounter issues:
1. Ensure you are using the latest version of Duolingo PRO.
2. Use Tampermonkey; other userscript managers may not be compatible.
3. For Chromium users, confirm Developer Mode is enabled in Extensions and Allow Userscripts is enabled in Extension Details.
4. Prefer Chrome or another Chromium-based browser for best results.
5. Visit our [Discord server](https://duolingopro.net/discord) for help and bug reports.
6. Find video guides on our [YouTube channel](https://duolingopro.net/youtube).
---
## Screenshots

















---
## Repository Activity

---
*Duolingo PRO is an independent project and is not affiliated with or endorsed by Duolingo Inc.*