// ==UserScript==
// @name DuoXJS
// @namespace https://github.com/LibreDuo/DuoXJS
// @version 1.0.1
// @description Free userscript utility for Duolingo
// @author LibreDuo
// @license MIT
// @match https://*.duolingo.com/*
// @match https://*.duolingo.cn/*
// @icon https://avatars.githubusercontent.com/u/223025697?s=200&v=4
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect duolingo.com
// @connect stories.duolingo.com
// @connect goals-api.duolingo.com
// @connect duolingo-leaderboards-prod.duolingo.com
// @connect ios-api-2.duolingo.com
// @connect raw.githubusercontent.com
// @connect avatars.githubusercontent.com
// @downloadURL https://raw.githubusercontent.com/LibreDuo/DuoXJS/main/DuoXJS.user.js
// @updateURL https://raw.githubusercontent.com/LibreDuo/DuoXJS/main/DuoXJS.meta.js
// ==/UserScript==
(function () {
"use strict";
const localSetItem = window.localStorage.setItem;
const localStorage = {
getItem: (key) => window.localStorage.getItem(key),
setItem: (key, value) => {
localSetItem.call(window.localStorage, key, value);
if (
key.startsWith("dx_") &&
typeof unsafeWindow !== "undefined" &&
unsafeWindow.localStorage
) {
try {
unsafeWindow.localStorage.setItem(key, value);
} catch (e) {}
}
},
removeItem: (key) => window.localStorage.removeItem(key),
clear: () => window.localStorage.clear(),
key: (i) => window.localStorage.key(i),
get length() {
return window.localStorage.length;
},
};
setTimeout(() => {
try {
if (typeof unsafeWindow !== "undefined" && unsafeWindow.localStorage) {
const len = window.localStorage.length;
for (let i = 0; i < len; i++) {
const key = window.localStorage.key(i);
if (key && key.startsWith("dx_")) {
try {
unsafeWindow.localStorage.setItem(
key,
window.localStorage.getItem(key),
);
} catch (e) {}
}
}
}
} catch (e) {}
}, 150);
const icons = {
avatar:
'',
info: '
',
success:
'
',
error:
'
',
warning:
'
',
chevron:
'',
hideBtn:
'',
showBtn:
'',
discordBtn:
'',
githubBtn:
'',
webBtn:
'',
modeNative:
'
',
modeSolver:
'
',
settingsBtn:
'
',
arrowRight:
'',
back: '',
hash: '',
inf: '',
shopIcons: {
streak:
"https://d35aaqx5ub95lt.cloudfront.net/images/icons/216ddc11afcbb98f44e53d565ccf479e.svg",
xp: "https://d35aaqx5ub95lt.cloudfront.net/images/icons/68c1fd0f467456a4c607ecc0ac040533.svg",
gem: "https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg",
heart:
"https://d35aaqx5ub95lt.cloudfront.net/images/hearts/7631e3ee734dd4fe7792626b59457fa4.svg",
outfit:
"https://d35aaqx5ub95lt.cloudfront.net/vendor/0cecd302cf0bcd0f73d51768feff75fe.svg",
free: "https://d35aaqx5ub95lt.cloudfront.net/images/super/11db6cd6f69cb2e3c5046b915be8e669.svg",
misc: "https://d35aaqx5ub95lt.cloudfront.net/images/legendary/158dbe277bf83116d04692b969a27aa3.svg",
},
xpIcon:
"https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg",
gemIcon:
"https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg",
streakIcon:
"https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg",
};
const dxVersion = "1.0.1";
const dxScriptVersion = "1.0.1";
const dxUpdateMetaUrl =
"https://raw.githubusercontent.com/LibreDuo/DuoXJS/main/DuoXJS.meta.js";
const dxUpdatePageUrl = "https://github.com/LibreDuo/DuoXJS";
const config = {
api: {
stories: "https://stories.duolingo.com/api2/stories",
users: "https://www.duolingo.com/2017-06-30/users",
sessions: "https://www.duolingo.com/2017-06-30/sessions",
leaderboards:
"https://duolingo-leaderboards-prod.duolingo.com/leaderboards/7d9f5dd1-8423-491a-91f2-2532052038ce",
shop: "https://www.duolingo.com/2023-05-23/shop-items",
goals: "https://goals-api.duolingo.com",
friends: "https://www.duolingo.com/2017-06-30/friends",
},
challengeTypes: [
"assist",
"characterIntro",
"characterMatch",
"characterPuzzle",
"characterSelect",
"characterTrace",
"characterWrite",
"completeReverseTranslation",
"definition",
"dialogue",
"extendedMatch",
"extendedListenMatch",
"form",
"freeResponse",
"gapFill",
"judge",
"listen",
"listenComplete",
"listenMatch",
"match",
"name",
"listenComprehension",
"listenIsolation",
"listenSpeak",
"listenTap",
"orderTapComplete",
"partialListen",
"partialReverseTranslate",
"patternTapComplete",
"radioBinary",
"radioImageSelect",
"radioListenMatch",
"radioListenRecognize",
"radioSelect",
"readComprehension",
"reverseAssist",
"sameDifferent",
"select",
"selectPronunciation",
"selectTranscription",
"svgPuzzle",
"syllableTap",
"syllableListenTap",
"speak",
"tapCloze",
"tapClozeTable",
"tapComplete",
"tapCompleteTable",
"tapDescribe",
"translate",
"transliterate",
"transliterationAssist",
"typeCloze",
"typeClozeTable",
"typeComplete",
"typeCompleteTable",
"writeComprehension",
],
};
const userAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
];
function pickUserAgent() {
let ua = localStorage.getItem("dx_user_agent");
if (!ua) {
ua = userAgents[Math.floor(Math.random() * userAgents.length)];
localStorage.setItem("dx_user_agent", ua);
}
return ua;
}
const dxUserAgent = pickUserAgent();
const DUO_LEAGUES_CDN =
"https://d35aaqx5ub95lt.cloudfront.net/images/leagues/";
const DUO_ICON_CDN = "https://d35aaqx5ub95lt.cloudfront.net/vendor/";
const leagueTierNames = [
"Bronze",
"Silver",
"Gold",
"Sapphire",
"Ruby",
"Emerald",
"Amethyst",
"Pearl",
"Obsidian",
"Diamond",
];
const leagueBadges = [
"192181672ada150becd83a74a4266ae9",
"8148b17e32d8706a82c02688f559e9ef",
"0e249b5f869b806da7406b815f4d60c6",
"3ced84eb1f0274ec0f02b24ae6e3d29b",
"74d6ab6e5b6f92e7d16a4a6664d1fafd",
"f480e032c5222395e73dac88ce3592bb",
"7f895707cd44583692d20481dcd9e0d0",
"f902954eeaa88fd2cb12f9168b4f68cb",
"57f0c6b260d33493a0cddc4ab38d6833",
"afe5c7067cd5fb7de936d3928ea7add6",
].map((h) => DUO_LEAGUES_CDN + h + ".svg");
const leagueBadgeFallback =
"https://d35aaqx5ub95lt.cloudfront.net/images/path/icons/f4b1c683214cf55f5ddea4535b983745.svg";
const leagueBadgeUrl = (tier) =>
typeof tier === "number" && tier >= 0 && leagueBadges[tier]
? leagueBadges[tier]
: leagueBadgeFallback;
const podiumMedals = [
"9e4f18c0bc42c7508d5fa5b18346af11",
"cc7b8f8582e9cfb88408ab851ec2e9bd",
"eef523c872b71178ef5acb2442d453a2",
].map((h) => DUO_LEAGUES_CDN + h + ".svg");
const yearInReviewIcons = {
top1: "https://raw.githubusercontent.com/LibreDuo/DuoXJS/refs/heads/main/assets/top_1.svg",
top3: "https://raw.githubusercontent.com/LibreDuo/DuoXJS/refs/heads/main/assets/top_3.svg",
top5: "https://raw.githubusercontent.com/LibreDuo/DuoXJS/refs/heads/main/assets/top_5.svg",
everyone:
"https://raw.githubusercontent.com/LibreDuo/DuoXJS/refs/heads/main/assets/everyone.svg",
};
const statusReactions = [
{
name: "None",
value: "NONE",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "a35f1db4398fd29e66f1abc33a0d11a2.svg",
},
{
name: "Cat",
emoji: "🐱",
value: "CAT",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "535fc27de224cc7d311dbb5de4f33be6.svg",
},
{
name: "Poop",
emoji: "💩",
value: "POOP",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "beb0df263d0f696bc7095d56b448ca78.svg",
},
{
name: "Popcorn",
emoji: "🍿",
value: "POPCORN",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "573de2bc90b2499eeb2b3738cff90133.svg",
},
{
name: "Dumpster Fire",
emoji: "🔥",
value: "DUMPSTER_FIRE",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "9fadb349c2ece257386a0e576359c867.svg",
},
{
name: "Trophy",
emoji: "🏆",
value: "TROPHY",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "22df4cb957e6cf2d7198b6e5449a342e.svg",
},
{
name: "Diamond Trophy",
emoji: "💎",
value: "TROPHY,winner",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "1795aa8b3b10d243e5d138a79bde360a.svg",
},
{
name: "Eyes",
emoji: "👀",
value: "EYES",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "a8e5c18e80054228b2c61168846ff643.svg",
},
{
name: "Flex",
emoji: "💪",
value: "FLEX",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "6b8a8db5ac7f847e7e87efe97c8b451a.svg",
},
{
name: "One Hundred",
emoji: "💯",
value: "ONE_HUNDRED",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "5642e1e72813a88e8973b551a2004c7f.svg",
},
{
name: "Popper",
emoji: "🎉",
value: "POPPER",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "2ceb401cae52712705b66a77df83ce40.svg",
},
{
name: "Sunglasses",
emoji: "😎",
value: "SUNGLASSES",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "2439bac00452e99ba7bf6a7ed0b04196.svg",
},
{
name: "Angry",
emoji: "😠",
value: "ANGRY",
cat: "Reactions",
icon: DUO_LEAGUES_CDN + "f12703218fc80de76a63e650726f742e.svg",
},
{
name: "2023 Top 1",
emoji: "👑",
value: "YEAR_IN_REVIEW,2023_top1",
cat: "2023 Events (iOS-only)",
icon: yearInReviewIcons.top1,
},
{
name: "2023 Top 3",
emoji: "⭐",
value: "YEAR_IN_REVIEW,2023_top3",
cat: "2023 Events (iOS-only)",
icon: yearInReviewIcons.top3,
},
{
name: "2023 Top 5",
emoji: "🌟",
value: "YEAR_IN_REVIEW,2023_top5",
cat: "2023 Events (iOS-only)",
icon: yearInReviewIcons.top5,
},
{
name: "2023 Everyone",
emoji: "🎊",
value: "YEAR_IN_REVIEW,2023",
cat: "2023 Events (iOS-only)",
icon: yearInReviewIcons.everyone,
},
{
name: "Chess",
emoji: "♟️",
value: "FLAG,chess",
cat: "Courses",
icon: DUO_ICON_CDN + "c8bad7c09aaf7bc29ddddc50808adb54.svg",
},
{
name: "Math",
emoji: "🔢",
value: "FLAG,math",
cat: "Courses",
icon: DUO_ICON_CDN + "395c8a6ee9783610b578b02fda405e85.svg",
},
{
name: "Music",
emoji: "🎵",
value: "FLAG,music",
cat: "Courses",
icon: DUO_ICON_CDN + "7fee27d21187165ccb88aef0234b6101.svg",
},
{
name: "Arabic",
emoji: "🇸🇦",
value: "FLAG,arabic",
cat: "Languages",
icon: DUO_ICON_CDN + "9ab6930a263c981b57f9d578ac97cae7.svg",
},
{
name: "Catalan",
emoji: "🇦🇩",
value: "FLAG,catalan",
cat: "Languages",
icon: DUO_ICON_CDN + "984fae40120b61fb684a80652e8f6a35.svg",
},
{
name: "Chinese/Cantonese",
emoji: "🇨🇳",
value: "FLAG,chinese",
cat: "Languages",
icon: DUO_ICON_CDN + "9905aa3a86fcb9e351b0b3bfaf04d8b9.svg",
},
{
name: "Czech",
emoji: "🇨🇿",
value: "FLAG,czech",
cat: "Languages",
icon: DUO_ICON_CDN + "828bf0fea457d3beaaab3d6c0bfcc975.svg",
},
{
name: "Danish",
emoji: "🇩🇰",
value: "FLAG,danish",
cat: "Languages",
icon: DUO_ICON_CDN + "6af84a7cb8e99ea8a567c2b9c55b9926.svg",
},
{
name: "Dutch",
emoji: "🇳🇱",
value: "FLAG,dutch",
cat: "Languages",
icon: DUO_ICON_CDN + "de945d789e249dcd74333a6996472ef8.svg",
},
{
name: "English",
emoji: "🇬🇧",
value: "FLAG,english",
cat: "Languages",
icon: DUO_ICON_CDN + "bbe17e16aa4a106032d8e3521eaed13e.svg",
},
{
name: "Esperanto",
emoji: "🌍",
value: "FLAG,esperanto",
cat: "Languages",
icon: DUO_ICON_CDN + "6de7e4731b2a82a6458268e1a3d67ce4.svg",
},
{
name: "Finnish",
emoji: "🇫🇮",
value: "FLAG,finnish",
cat: "Languages",
icon: DUO_ICON_CDN + "b4d0e4f6451f504e1441eb93efdbea5e.svg",
},
{
name: "French",
emoji: "🇫🇷",
value: "FLAG,french",
cat: "Languages",
icon: DUO_ICON_CDN + "482fda142ee4abd728ebf4ccce5d3307.svg",
},
{
name: "German",
emoji: "🇩🇪",
value: "FLAG,german",
cat: "Languages",
icon: DUO_ICON_CDN + "c71db846ffab7e0a74bc6971e34ad82e.svg",
},
{
name: "Greek",
emoji: "🇬🇷",
value: "FLAG,greek",
cat: "Languages",
icon: DUO_ICON_CDN + "8db373482261397a3159d3f370eed2f3.svg",
},
{
name: "Guarani",
emoji: "🇵🇾",
value: "FLAG,guarani",
cat: "Languages",
icon: DUO_ICON_CDN + "ff446507a141928d1cfd9476612d7dc0.svg",
},
{
name: "Haitian Creole",
emoji: "🇭🇹",
value: "FLAG,haitian-creole",
cat: "Languages",
icon: DUO_ICON_CDN + "8cb302b44c183c1a8ec3b90caf90d922.svg",
},
{
name: "Hawaiian",
emoji: "🏴",
value: "FLAG,hawaiian",
cat: "Languages",
icon: DUO_ICON_CDN + "312e21f793c555787d01a45e20ee8191.svg",
},
{
name: "Hebrew",
emoji: "🇮🇱",
value: "FLAG,hebrew",
cat: "Languages",
icon: DUO_ICON_CDN + "f818f545a703ddaa046ca8786e781742.svg",
},
{
name: "High Valyrian",
emoji: "🐉",
value: "FLAG,high-valyrian",
cat: "Languages",
icon: DUO_ICON_CDN + "2880099b038848abbfd11104097953ad.svg",
},
{
name: "Hindi",
emoji: "🇮🇳",
value: "FLAG,hindi",
cat: "Languages",
icon: DUO_ICON_CDN + "73837fa39dbf1bcc4c95a17a58ed0ffb.svg",
},
{
name: "Hungarian",
emoji: "🇭🇺",
value: "FLAG,hungarian",
cat: "Languages",
icon: DUO_ICON_CDN + "2ed8d0a73eab3c9cba0290e2b459684a.svg",
},
{
name: "Indonesian",
emoji: "🇮🇩",
value: "FLAG,indonesian",
cat: "Languages",
icon: DUO_ICON_CDN + "339c0413e542f19b234971d7740447e7.svg",
},
{
name: "Irish",
emoji: "🇮🇪",
value: "FLAG,irish",
cat: "Languages",
icon: DUO_ICON_CDN + "ef0bfb96037b127473bd7bcbfde1a6ed.svg",
},
{
name: "Italian",
emoji: "🇮🇹",
value: "FLAG,italian",
cat: "Languages",
icon: DUO_ICON_CDN + "635a09df9323279d39934a991edd4510.svg",
},
{
name: "Japanese",
emoji: "🇯🇵",
value: "FLAG,japanese",
cat: "Languages",
icon: DUO_ICON_CDN + "edea4fa18ff3e7d8c0282de3f102aaed.svg",
},
{
name: "Klingon",
emoji: "🖖",
value: "FLAG,klingon",
cat: "Languages",
icon: DUO_ICON_CDN + "76d654213a8282b0ebc25b4f535ee003.svg",
},
{
name: "Korean",
emoji: "🇰🇷",
value: "FLAG,korean",
cat: "Languages",
icon: DUO_ICON_CDN + "ec5835ac9f465ff3dad4b1b8725d4314.svg",
},
{
name: "Latin",
emoji: "🇻🇦",
value: "FLAG,latin",
cat: "Languages",
icon: DUO_ICON_CDN + "f7cee6cc09270371b097129faf792c2a.svg",
},
{
name: "Navajo",
emoji: "🏴",
value: "FLAG,navajo",
cat: "Languages",
icon: DUO_ICON_CDN + "bbc8ad0cfe2596d5193376ebdc3e969c.svg",
},
{
name: "Norwegian (Bokmål)",
emoji: "🇳🇴",
value: "FLAG,norwegian",
cat: "Languages",
icon: DUO_ICON_CDN + "90b37d97edc66e830dc2286279548f67.svg",
},
{
name: "Polish",
emoji: "🇵🇱",
value: "FLAG,polish",
cat: "Languages",
icon: DUO_ICON_CDN + "f095084e6ec400e631d62c3d95fefaa2.svg",
},
{
name: "Portuguese",
emoji: "🇵🇹",
value: "FLAG,portuguese",
cat: "Languages",
icon: DUO_ICON_CDN + "27d253ae1272917fc9f4a79459aacd53.svg",
},
{
name: "Romanian",
emoji: "🇷🇴",
value: "FLAG,romanian",
cat: "Languages",
icon: DUO_ICON_CDN + "357e13bb10cf86fc06552d563957e2e6.svg",
},
{
name: "Russian",
emoji: "🇷🇺",
value: "FLAG,russian",
cat: "Languages",
icon: DUO_ICON_CDN + "eadd7804652170c33814a89482f1f353.svg",
},
{
name: "Scottish Gaelic",
emoji: "🏴",
value: "FLAG,scottish-gaelic",
cat: "Languages",
icon: DUO_ICON_CDN + "09eba3135efe8fe93a4662dba813b921.svg",
},
{
name: "Spanish",
emoji: "🇪🇸",
value: "FLAG,spanish",
cat: "Languages",
icon: DUO_ICON_CDN + "59a90a2cedd48b751a8fd22014768fd7.svg",
},
{
name: "Swahili",
emoji: "🇹🇿",
value: "FLAG,swahili",
cat: "Languages",
icon: DUO_ICON_CDN + "335311988405b4354e1b6ae9037c02db.svg",
},
{
name: "Swedish",
emoji: "🇸🇪",
value: "FLAG,swedish",
cat: "Languages",
icon: DUO_ICON_CDN + "f578430c9b7ab617c107893afbb501c0.svg",
},
{
name: "Tamil",
emoji: "🇮🇳",
value: "FLAG,tamil",
cat: "Languages",
icon: DUO_ICON_CDN + "2226e2cc358e810d3ee9dbe182f7c2a9.svg",
},
{
name: "Thai",
emoji: "🇹🇭",
value: "FLAG,thai",
cat: "Languages",
icon: DUO_ICON_CDN + "d079dc0a7d111bbbf241aa79bc8ceefe.svg",
},
{
name: "Turkish",
emoji: "🇹🇷",
value: "FLAG,turkish",
cat: "Languages",
icon: DUO_ICON_CDN + "bc80a9518cd6d5af6ae14e8b22b8a1f4.svg",
},
{
name: "Ukrainian",
emoji: "🇺🇦",
value: "FLAG,ukrainian",
cat: "Languages",
icon: DUO_ICON_CDN + "7c6e12bc57527843082f7f5bb77c9862.svg",
},
{
name: "Urdu",
emoji: "🇵🇰",
value: "FLAG,urdu",
cat: "Languages",
icon: DUO_ICON_CDN + "aebc481c2b5167a564e4c9313d07278d.svg",
},
{
name: "Vietnamese",
emoji: "🇻🇳",
value: "FLAG,vietnamese",
cat: "Languages",
icon: DUO_ICON_CDN + "2b077d42185bc45d4896ed55f15c4fea.svg",
},
{
name: "Welsh",
emoji: "🏴",
value: "FLAG,welsh",
cat: "Languages",
icon: DUO_ICON_CDN + "f773f1b240623072e48843ecdf90d756.svg",
},
{
name: "Yiddish",
emoji: "🏴",
value: "FLAG,yiddish",
cat: "Languages",
icon: DUO_ICON_CDN + "55bad151fa6a8d9e2376fc9697c671c8.svg",
},
{
name: "Zulu",
emoji: "🇿🇦",
value: "FLAG,south-africa",
cat: "Languages",
icon: DUO_ICON_CDN + "112e1531d0ac198a9424bd1b0a7166e6.svg",
},
];
const mainCss = `
@font-face {
font-family: "DuoFeather";
src: url("https://d35aaqx5ub95lt.cloudfront.net/fonts/642e24bb0295f3aee4dedcd8eecd8007.woff2") format("woff2");
font-display: swap;
}
:root {
--DX-blue: 0, 122, 255;
--DX-green: 50, 215, 75;
--DX-red: 255, 69, 58;
--DX-orange: 255, 159, 10;
--dx-link-color: rgb(var(--DX-blue));
--DX-s1: 4px;
--DX-s2: 8px;
--DX-s3: 12px;
--DX-s4: 16px;
--DX-ctrl: 40px;
--DX-ctrl-lg: 48px;
--DX-r-s: 8px;
--DX-r-m: 12px;
--DX-r-l: 16px;
--DX-r-xl: 22px;
--DX-corner: 0;
--DX-ease-bounce: cubic-bezier(0.34, 1.15, 0.64, 1);
--DX-ease-out: cubic-bezier(0.25, 1, 0.5, 1);
--DX-ease: var(--DX-ease-out);
--DX-motion-fast: 150ms;
--DX-motion: 200ms;
--DX-motion-page: 400ms;
--DX-motion-spin: 1200ms;
--DX-t-title: 22px;
--DX-t-lead: 16px;
--DX-t-body: 15px;
--DX-t-label: 13px;
--DX-t-cap: 11px;
}
@supports (corner-shape: superellipse(1.4)) {
:root { --DX-corner: superellipse(1.4); }
}
.DX_Btn,
.DX_Input_Wrap,
.DX_Hash_Btn,
.DX_Select,
.DX_Select_Options,
.DX_Set_Input_Wrap,
.DX_Search,
.DX_Panel_Card,
.DX_Shop_Card,
.DX_Quest_Item,
.DX_Shop_Btn,
.DX_Quest_Get_Btn,
.DX_Profile_Block,
.DX_Modal_Box {
corner-shape: var(--DX-corner);
}
.DX_Wordmark {
display: inline-flex;
align-items: baseline;
font-family: 'DuoFeather', 'din-round';
font-size: var(--DX-t-title);
font-weight: 900;
letter-spacing: 0.2px;
line-height: 1;
}
.DX_Wordmark .dx-xjs {
background: linear-gradient(
135deg,
#F7DF1E 0%,
#FF8000 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
}
#DX_Root {
user-select: none;
-webkit-user-select: none;
}
#DX_Root * {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
.DX_Selectable {
user-select: text !important;
-webkit-user-select: text !important;
cursor: text;
}
#DX_Root p,
#DX_Root span,
#DX_Root button,
#DX_Root input,
#DX_Root label,
#DX_Root div {
font-family: 'din-round', 'DuoFeather' !important;
}
#DX_Root p,
#DX_Root span {
margin: 0;
padding: 0;
}
#DX_Root svg {
flex-shrink: 0;
}
.DX_Main {
display: inline-flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
gap: 8px;
position: fixed;
right: calc(16px + env(safe-area-inset-right, 0px));
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
z-index: 2147483646;
pointer-events: none;
transition: gap var(--DX-motion-page) var(--DX-ease);
}
.DX_Main > * {
pointer-events: auto;
}
.DX_Main.dx-panel-hidden {
gap: 0;
width: max-content;
height: auto;
}
.DX_Main.dx-panel-hidden .DX_Main_Box {
pointer-events: none !important;
}
.DX_Main_Box {
display: flex;
width: 325px;
max-width: calc(100vw - 16px);
padding: 16px;
box-sizing: border-box;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
overflow: hidden;
border-radius: var(--DX-r-xl);
corner-shape: var(--DX-corner);
position: relative;
transform-origin: var(--DX-panel-origin, center);
-webkit-transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
-webkit-transition: opacity var(--DX-motion) var(--DX-ease),
-webkit-filter var(--DX-motion) var(--DX-ease),
filter var(--DX-motion) var(--DX-ease),
-webkit-transform var(--DX-motion-page) var(--DX-ease),
transform var(--DX-motion-page) var(--DX-ease),
max-height var(--DX-motion-page) var(--DX-ease),
max-width var(--DX-motion-page) var(--DX-ease),
padding var(--DX-motion-page) var(--DX-ease);
transition: opacity var(--DX-motion) var(--DX-ease),
filter var(--DX-motion) var(--DX-ease),
transform var(--DX-motion-page) var(--DX-ease),
max-height var(--DX-motion-page) var(--DX-ease),
max-width var(--DX-motion-page) var(--DX-ease),
padding var(--DX-motion-page) var(--DX-ease);
will-change: opacity, filter, transform, max-height, max-width, padding;
}
.DX_Main_Box.dx-hidden {
opacity: 0 !important;
-webkit-filter: blur(6px) !important;
filter: blur(6px) !important;
-webkit-transform: scale3d(0.96, 0.96, 1) !important;
transform: scale3d(0.96, 0.96, 1) !important;
pointer-events: none;
}
.DX_Main_Box.dx-collapsed {
max-height: 0 !important;
max-width: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
overflow: hidden;
visibility: hidden;
}
.DX_Main_Box.dx-scroll {
overflow-y: auto;
overflow-x: hidden;
justify-content: flex-start;
}
.DX_Main_Box.dx-scroll::-webkit-scrollbar {
width: 4px;
}
.DX_Main_Box.dx-scroll::-webkit-scrollbar-track {
background: transparent;
}
.DX_Main_Box.dx-scroll::-webkit-scrollbar-thumb {
background: rgba(var(--DX-blue), 0.4);
border-radius: 4px;
}
.DX_Main_Box.dx-menu-open {
overflow: visible;
}
.DX_Main_Box.dx-scroll.dx-menu-open {
overflow-x: hidden;
overflow-y: auto;
}
.DX_Main_Box.dx-light {
--dx-panel-bg: rgba(255, 255, 255, 0.85);
background: var(--dx-panel-bg);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
outline: 2px solid rgba(229, 229, 229, 1);
outline-offset: -2px;
box-shadow: 0 18px 50px -12px rgba(17, 32, 46, 0.28), 0 2px 8px rgba(17, 32, 46, 0.06);
--dx-bg: rgba(255, 255, 255, 0.95);
--dx-text: #333;
--dx-card-bg: rgba(0, 0, 0, 0.05);
--dx-card-hover: rgba(var(--DX-blue), 0.1);
--dx-card-border: rgba(229, 229, 229, 1);
}
.DX_Main_Box.dx-dark {
--dx-panel-bg: rgba(32, 47, 54, 0.85);
background: var(--dx-panel-bg);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
outline: 2px solid rgba(55, 70, 79, 1);
outline-offset: -2px;
box-shadow: 0 20px 55px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.3);
--dx-bg: rgba(32, 47, 54, 0.95);
--dx-text: #fff;
--dx-card-bg: rgba(255, 255, 255, 0.05);
--dx-card-hover: rgba(var(--DX-blue), 0.15);
--dx-card-border: rgba(55, 70, 79, 1);
--dx-link-color: #58a6ff;
}
#duoxjs-hide-button.dx-light {
--dx-text: #333;
--dx-panel-bg: rgba(255, 255, 255, 0.85);
--dx-card-border: rgba(229, 229, 229, 1);
--dx-panel-shadow: 0 12px 32px -14px rgba(17, 32, 46, 0.28), 0 2px 8px rgba(17, 32, 46, 0.06);
}
#duoxjs-hide-button.dx-dark {
--dx-text: #fff;
--dx-panel-bg: rgba(32, 47, 54, 0.85);
--dx-card-border: rgba(55, 70, 79, 1);
--dx-panel-shadow: 0 14px 36px -14px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.3);
}
.DX_HStack_Auto {
display: flex;
align-items: center;
justify-content: space-between;
align-self: stretch;
gap: 8px;
min-width: 0;
}
.DX_HStack_8 {
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
min-width: 0;
}
.DX_HStack_4 {
display: flex;
align-items: center;
gap: 4px;
align-self: stretch;
min-width: 0;
}
.DX_VStack_8 {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
align-self: stretch;
}
.DX_VStack_4 {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
align-self: stretch;
}
.DX_NoSel {
user-select: none;
-webkit-user-select: none;
}
.DX_Divider {
align-self: stretch;
height: 1px;
background: rgba(117, 117, 117, 0.2);
flex-shrink: 0;
}
.DX_T1 {
font-size: 14px;
font-weight: 700;
line-height: 1.22;
color: var(--dx-text);
margin: 0;
min-width: 0;
}
.DX_T1:has(+ .DX_T2) {
font-weight: 800;
}
.DX_T2 {
font-size: 13px;
font-weight: 600;
line-height: 1.25;
color: var(--dx-text);
opacity: 0.6;
margin: 0;
min-width: 0;
}
.DX_Btn {
display: flex;
height: 40px;
padding: 10px 12px 10px 10px;
box-sizing: border-box;
align-items: center;
gap: 6px;
flex: 1 0 0;
min-width: 0;
border-radius: 8px;
border: none;
cursor: pointer;
user-select: none;
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
transition: filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease),
background var(--DX-motion) var(--DX-ease),
outline var(--DX-motion) var(--DX-ease),
color var(--DX-motion) var(--DX-ease);
}
.DX_Btn:focus-visible {
outline-color: var(--focus-outline, rgba(var(--DX-blue), 0.5)) !important;
}
.DX_Btn:hover {
filter: brightness(0.9);
transform: scale(1.03);
}
.DX_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Btn_Blue_Ghost,
.DX_Btn_Eel {
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
background: var(--dx-card-bg);
}
.DX_Btn_Blue_Ghost .DX_Nav_Title {
color: var(--dx-text);
}
.DX_Btn_Blue_Ghost .DX_Nav_Btn_L > svg,
.DX_Btn_Blue_Ghost .DX_Nav_Btn_L > svg [stroke],
.DX_Btn_Blue_Ghost > svg path {
stroke: var(--dx-text);
}
.DX_Btn_Icon {
flex: none !important;
width: 40px;
padding: 10px !important;
justify-content: center;
}
.DX_Nav_Btn {
align-self: stretch;
justify-content: space-between;
height: 40px;
padding: 10px 12px 10px 10px;
}
.DX_Nav_Btn_L {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 auto;
min-width: 0;
}
.DX_Nav_Btn_L > svg:first-child,
.DX_Nav_Btn_L > img:first-child {
width: 20px !important;
height: 20px !important;
flex-shrink: 0;
object-fit: contain;
}
.DX_Nav_Title {
color: rgb(var(--DX-blue));
font-size: 14px;
font-weight: 800;
line-height: 1.18;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.DX_Input_Wrap {
display: flex;
height: 44px;
padding: 12px 14px;
box-sizing: border-box;
align-items: center;
flex: 1 0 0px;
min-width: 0;
gap: 6px;
border-radius: var(--DX-r-s);
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
background: var(--dx-card-bg);
position: relative;
overflow: hidden;
transition: flex var(--DX-motion-page) var(--DX-ease),
padding var(--DX-motion-page) var(--DX-ease),
margin var(--DX-motion-page) var(--DX-ease),
opacity var(--DX-motion) var(--DX-ease),
outline-width var(--DX-motion-page) var(--DX-ease),
outline-color var(--DX-motion) var(--DX-ease),
background var(--DX-motion) var(--DX-ease);
}
.DX_Input_Wrap:focus-within {
outline-color: rgba(var(--DX-blue), 0.35);
}
.DX_Input_Wrap.dx-inf-hidden {
flex: 0 0 0px;
padding-left: 0;
padding-right: 0;
opacity: 0;
outline-width: 0px;
pointer-events: none;
margin-right: -8px;
}
.DX_Hash_Btn {
--focus-outline: var(--dx-card-border);
background: var(--dx-card-bg);
border: none;
border-radius: var(--DX-r-s);
color: var(--dx-text);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 44px;
flex: 0 0 44px;
cursor: pointer;
overflow: hidden;
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
transition: filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease),
background var(--DX-motion) var(--DX-ease),
outline-color var(--DX-motion) var(--DX-ease),
box-shadow var(--DX-motion) var(--DX-ease),
flex var(--DX-motion-page) var(--DX-ease);
}
.DX_Hash_Btn svg {
display: block;
width: 22px;
height: 22px;
}
.DX_Hash_Btn:hover {
filter: brightness(0.9);
transform: scale(1.03);
}
.DX_Hash_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Hash_Btn.dx-inf-active {
flex: 1 0 44px;
}
#DX_Root .DX_Hash_Btn:focus,
#DX_Root .DX_Hash_Btn:focus-visible,
#DX_Root .DX_Hash_Btn:active {
outline: 2px solid var(--dx-card-border) !important;
outline-offset: -2px !important;
box-shadow: none !important;
}
.DX_Hash_Lbl {
font-weight: 800;
font-size: 13px;
white-space: nowrap;
}
.DX_Input {
border: none !important;
outline: none !important;
background: none !important;
text-align: right;
font-size: 14px !important;
font-weight: 600 !important;
color: var(--dx-text) !important;
font-family: inherit !important;
width: 100%;
-webkit-appearance: none;
appearance: none;
-moz-appearance: textfield;
}
.DX_Input::placeholder {
color: var(--dx-text) !important;
opacity: 0.5;
}
.DX_Input::-webkit-outer-spin-button,
.DX_Input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.DX_Input_Btn {
display: flex;
height: 44px;
width: 66px;
padding: 12px 10px;
box-sizing: border-box;
justify-content: center;
align-items: center;
border-radius: var(--DX-r-s);
corner-shape: var(--DX-corner);
border: none;
cursor: pointer;
user-select: none;
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
background: rgb(var(--DX-blue));
white-space: nowrap;
flex-shrink: 0;
transition: background var(--DX-motion) var(--DX-ease),
outline var(--DX-motion) var(--DX-ease),
filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease);
}
.DX_Input_Btn:focus-visible {
outline-color: var(--focus-outline, rgba(0, 0, 0, 0.2)) !important;
}
.DX_Input_Btn:hover {
filter: brightness(0.9);
transform: scale(1.03);
}
.DX_Input_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Btn:disabled,
.DX_Sm_Btn:disabled,
.DX_Acc_Btn:disabled,
.DX_Quest_Get_Btn:disabled,
.DX_Shop_Btn:disabled,
.DX_Hash_Btn:disabled,
.DX_Input_Btn:disabled {
opacity: 0.38 !important;
pointer-events: none !important;
transform: scale(1) !important;
filter: none !important;
}
.DX_Btn_Label {
font-size: 14px;
font-weight: 800;
line-height: 1.1;
letter-spacing: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity var(--DX-motion) var(--DX-ease),
filter var(--DX-motion) var(--DX-ease),
color var(--DX-motion) var(--DX-ease);
}
.DX_Sm_Btn {
display: flex;
height: var(--DX-ctrl);
padding: 10px 12px;
min-width: 66px;
justify-content: center;
align-items: center;
border-radius: var(--DX-r-s);
corner-shape: var(--DX-corner);
border: none;
cursor: pointer;
user-select: none;
flex-shrink: 0;
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
background: rgb(var(--DX-blue));
white-space: nowrap;
transition: background var(--DX-motion) var(--DX-ease),
outline var(--DX-motion) var(--DX-ease),
filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease);
}
.DX_Sm_Btn:focus-visible {
outline-color: var(--focus-outline, rgba(var(--DX-blue), 0.5)) !important;
}
.DX_Sm_Btn:hover {
filter: brightness(0.9);
transform: scale(1.03);
}
.DX_Sm_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Sm_Btn_Label {
font-size: 14px;
font-weight: 800;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity var(--DX-motion) var(--DX-ease),
filter var(--DX-motion) var(--DX-ease),
color var(--DX-motion) var(--DX-ease);
}
.DX_Toggle {
position: relative;
width: 52px;
height: 28px;
border-radius: 8px;
corner-shape: var(--DX-corner);
background: var(--dx-card-border);
cursor: pointer;
user-select: none;
flex-shrink: 0;
transition: background var(--DX-motion) var(--DX-ease);
}
.DX_Toggle.on {
background: rgb(var(--DX-blue));
}
.DX_Toggle_Knob {
position: absolute;
top: 3px;
left: 3px;
width: 22px;
height: 22px;
border-radius: 6px;
corner-shape: var(--DX-corner);
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
-webkit-transition: -webkit-transform var(--DX-motion) var(--DX-ease);
transition: transform var(--DX-motion) var(--DX-ease);
}
.DX_Toggle.on .DX_Toggle_Knob {
-webkit-transform: translate3d(24px, 0, 0);
transform: translate3d(24px, 0, 0);
}
.DX_Select {
position: relative;
width: 100%;
min-width: 0;
height: 40px;
border-radius: 8px;
background: var(--dx-card-bg);
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
color: var(--dx-text);
font-weight: 700;
font-size: 13px;
cursor: pointer;
user-select: none;
transition: outline-color var(--DX-motion) var(--DX-ease),
background var(--DX-motion) var(--DX-ease);
}
.DX_Select:hover {
outline-color: rgba(var(--DX-blue), 0.35);
}
.DX_Select.open {
outline-color: rgba(var(--DX-blue), 0.35);
}
.DX_Select_Trigger {
height: 100%;
padding: 0 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
min-width: 0;
}
.DX_Select_Text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.DX_Select_Options {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: var(--dx-bg);
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
border-radius: 8px;
corner-shape: var(--DX-corner);
box-shadow: 0 10px 40px rgba(0, 0, 0, .2);
max-height: 0;
overflow-y: auto;
opacity: 0;
visibility: hidden;
-webkit-transform: translate3d(0, -8px, 0);
transform: translate3d(0, -8px, 0);
-webkit-transition: max-height var(--DX-motion) var(--DX-ease),
opacity var(--DX-motion) var(--DX-ease),
visibility var(--DX-motion) var(--DX-ease),
-webkit-transform var(--DX-motion) var(--DX-ease),
transform var(--DX-motion) var(--DX-ease);
transition: max-height var(--DX-motion) var(--DX-ease),
opacity var(--DX-motion) var(--DX-ease),
visibility var(--DX-motion) var(--DX-ease),
transform var(--DX-motion) var(--DX-ease);
z-index: 100;
backdrop-filter: blur(20px);
}
.DX_Select.open .DX_Select_Options {
max-height: 128px;
opacity: 1;
visibility: visible;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
.DX_Select_Option {
padding: 0 12px;
min-height: 40px;
display: flex;
align-items: center;
box-sizing: border-box;
color: var(--dx-text);
font-size: 13px;
font-weight: 700;
transition: background var(--DX-motion-fast) var(--DX-ease),
color var(--DX-motion-fast) var(--DX-ease);
}
.DX_Select_Option:hover {
background: var(--dx-card-hover);
color: var(--dx-text);
}
.DX_Select_Option.selected {
background: rgba(var(--DX-blue), 0.12);
color: var(--dx-link-color);
}
.DX_Select.dropup .DX_Select_Options {
top: auto;
bottom: calc(100% + 8px);
-webkit-transform: translate3d(0, 8px, 0);
transform: translate3d(0, 8px, 0);
}
.DX_Select.dropup.open .DX_Select_Options {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
.DX_Select .DX_Chevron {
-webkit-transform: rotate3d(0, 0, 1, 0deg);
transform: rotate3d(0, 0, 1, 0deg);
-webkit-transition: -webkit-transform var(--DX-motion) var(--DX-ease);
transition: transform var(--DX-motion) var(--DX-ease);
width: 16px;
height: 16px;
stroke: var(--dx-text);
}
.DX_Select.open .DX_Chevron {
-webkit-transform: rotate3d(0, 0, 1, 180deg);
transform: rotate3d(0, 0, 1, 180deg);
}
.DX_Select_Options::-webkit-scrollbar {
width: 4px;
}
.DX_Select_Options::-webkit-scrollbar-track {
margin: 8px 0;
border-radius: 12px;
}
.DX_Select_Options::-webkit-scrollbar-thumb {
background: rgba(var(--DX-blue), 0.4);
border-radius: 4px;
}
.DX_Farm_Sec {
display: flex;
flex-direction: column;
align-items: stretch;
align-self: stretch;
}
.DX_Farm_Sec > .DX_HStack_8 {
margin-top: 8px;
}
.DX_Farm_Sec > .DX_Prog_Wrap.on {
margin-top: 8px;
}
.DX_Task_Group {
display: flex;
flex-direction: column;
align-items: stretch;
align-self: stretch;
}
.DX_Task_Group > .DX_Prog_Wrap.on {
margin-top: 8px;
}
.DX_Prog_Wrap {
align-self: stretch;
height: 0;
border-radius: 3px;
background: rgba(var(--DX-blue), 0.1);
overflow: hidden;
transition: height var(--DX-motion-page) var(--DX-ease);
}
.DX_Prog_Wrap.on {
height: 4px;
}
.DX_Prog_Fill {
height: 100%;
border-radius: 3px;
background: rgb(var(--DX-blue));
width: 0%;
transition: width var(--DX-motion-page) var(--DX-ease),
background var(--DX-motion) var(--DX-ease),
box-shadow var(--DX-motion) var(--DX-ease);
box-shadow: 0 0 6px rgba(var(--DX-blue), 0.35);
}
.DX_Prog_Fill.done {
background: rgb(var(--DX-green)) !important;
box-shadow: 0 0 8px rgba(var(--DX-green), 0.45) !important;
}
.DX_Avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(var(--DX-blue), 0.1);
color: var(--dx-text);
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.DX_Stat_Ico {
width: 15px;
height: 15px;
display: block;
flex-shrink: 0;
}
.DX_Stat_Val {
font-size: 13px !important;
font-weight: 700 !important;
color: var(--dx-text) !important;
opacity: 0.8;
}
.DX_Page {
display: none;
width: 100%;
}
.DX_Page.active {
display: flex;
flex-direction: column;
gap: 8px;
align-self: stretch;
align-items: center;
}
.DX_Notif_Main {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 10px;
width: 320px;
position: fixed;
left: 50%;
top: 20px;
transform: translateX(-50%);
z-index: 2147483647;
pointer-events: none;
}
.DX_Notif_Box {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
border-radius: 16px;
corner-shape: var(--DX-corner);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
outline: 2px solid rgba(229, 229, 229, 1);
outline-offset: -2px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
opacity: 0;
transform: translate3d(0, -40px, 0) scale(0.85);
transition: transform var(--DX-motion-page) var(--DX-ease),
opacity var(--DX-motion) var(--DX-ease),
margin var(--DX-motion-page) var(--DX-ease);
pointer-events: auto;
width: 100%;
min-width: 0;
}
.DX_Notif_Title {
font-size: 14px;
font-weight: 800;
line-height: 1.2;
color: var(--dx-text);
margin: 0;
overflow-wrap: anywhere;
}
.DX_Notif_Body {
font-size: 12px;
font-weight: 600;
line-height: 1.3;
color: var(--dx-text);
opacity: 0.7;
margin: 2px 0 0 0;
overflow-wrap: anywhere;
}
.DX_Notif_Box > div:last-child {
min-width: 0;
flex: 1 0 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.DX_Notif_Main.dx-light {
--dx-text: #333;
}
.DX_Notif_Main.dx-dark {
--dx-text: #fff;
}
.DX_Notif_Main.dx-dark .DX_Notif_Box {
background: rgba(32, 47, 54, 0.85);
outline-color: rgba(55, 70, 79, 1);
}
.DX_Notif_Box.show {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1) !important;
}
.DX_Notif_Box.hide {
opacity: 0 !important;
transform: translate3d(0, -40px, 0) scale(0.85) !important;
margin-top: -60px;
z-index: -1;
}
.DX_Notif_Main[data-pos="bottom_center"] .DX_Notif_Box {
transform: translate3d(0, 40px, 0) scale(0.85);
}
.DX_Notif_Main[data-pos="bottom_center"] .DX_Notif_Box.hide {
transform: translate3d(0, 40px, 0) scale(0.85) !important;
margin-top: 0px;
margin-bottom: -60px;
z-index: -1;
}
/* Left-aligned positions (top_left, bottom_left) */
.DX_Notif_Main[data-pos$="left"] .DX_Notif_Box {
transform: translate3d(-40px, 0, 0) scale(0.85);
}
.DX_Notif_Main[data-pos$="left"] .DX_Notif_Box.hide {
transform: translate3d(-40px, 0, 0) scale(0.85) !important;
z-index: -1;
}
.DX_Notif_Main[data-pos="bottom_left"] .DX_Notif_Box.hide {
margin-top: 0px;
margin-bottom: -60px;
}
/* Right-aligned positions (top_right, bottom_right) */
.DX_Notif_Main[data-pos$="right"] .DX_Notif_Box {
transform: translate3d(40px, 0, 0) scale(0.85);
}
.DX_Notif_Main[data-pos$="right"] .DX_Notif_Box.hide {
transform: translate3d(40px, 0, 0) scale(0.85) !important;
z-index: -1;
}
.DX_Notif_Main[data-pos="bottom_right"] .DX_Notif_Box.hide {
margin-top: 0px;
margin-bottom: -60px;
}
.DX_Notif_Ico {
width: 24px;
height: 24px;
display: flex;
align-items: center;
color: var(--dx-text);
}
.DX_Notif_Ico svg {
width: 100%;
height: 100%;
}
.DX_Notif_Box.warning .DX_Notif_Ico {
color: rgb(var(--DX-orange));
}
.DX_Notif_Box.success .DX_Notif_Ico {
color: rgb(var(--DX-green));
}
.DX_Notif_Box.error .DX_Notif_Ico {
color: rgb(var(--DX-red));
}
.DX_Shop_Grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
align-self: stretch;
}
.DX_Shop_Section_Header {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.DX_Shop_Section_Line {
flex: 1;
height: 1px;
background: var(--dx-card-border);
}
.DX_Shop_Section_Title {
font-size: 11px !important;
font-weight: 800 !important;
color: var(--dx-text) !important;
text-transform: uppercase;
letter-spacing: 0;
opacity: 0.5;
}
.DX_Shop_Card {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 8px;
box-sizing: border-box;
border-radius: 12px;
outline: 1.5px solid var(--dx-card-border);
outline-offset: -1px;
background: var(--dx-card-bg);
transition: outline-color var(--DX-motion) var(--DX-ease),
background var(--DX-motion) var(--DX-ease);
text-align: center;
min-width: 0;
}
.DX_Shop_Card:hover {
outline-color: rgba(var(--DX-blue), 0.3);
background: var(--dx-card-hover);
}
.DX_Shop_Ico {
width: 36px;
height: 36px;
object-fit: contain;
flex-shrink: 0;
}
.DX_Shop_Name {
font-size: 11px;
font-weight: 700;
color: var(--dx-text);
opacity: 0.8;
line-height: 1.3;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 30px;
overflow-wrap: anywhere;
}
.DX_Shop_Btn {
width: 100%;
height: 28px;
border-radius: var(--DX-r-s);
border: none;
cursor: pointer;
font-size: 11px;
font-weight: 800;
color: #fff;
background: rgb(var(--DX-blue));
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
transition: filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease),
background var(--DX-motion) var(--DX-ease);
}
.DX_Shop_Btn:hover {
filter: brightness(0.9);
transform: scale(1.03);
}
.DX_Shop_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Shop_Btn:focus-visible {
outline-color: var(--focus-outline, rgba(0, 0, 0, 0.2)) !important;
}
.DX_Shop_Btn.loading {
--focus-outline: var(--dx-card-border);
background: var(--dx-card-bg);
color: var(--dx-text);
outline-color: var(--dx-card-border);
pointer-events: none;
}
.DX_Shop_Btn.got {
--focus-outline: rgba(var(--DX-green), 0.25);
background: rgba(var(--DX-green), 0.12);
color: rgb(var(--DX-green));
outline-color: rgba(var(--DX-green), 0.25);
pointer-events: none;
}
.DX_Btn.fail,
.DX_Sm_Btn.fail,
.DX_Input_Btn.fail,
.DX_Quest_Get_Btn.fail,
.DX_Shop_Btn.fail,
.DX_Hash_Btn.fail {
--focus-outline: rgba(var(--DX-red), 0.22) !important;
background: rgba(var(--DX-red), 0.10) !important;
color: rgb(var(--DX-red)) !important;
outline-color: rgba(var(--DX-red), 0.22) !important;
pointer-events: none !important;
}
.DX_Scroll_Inner {
align-self: stretch;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
max-height: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 4px;
}
.DX_Scroll_Inner::-webkit-scrollbar {
width: 4px;
}
.DX_Scroll_Inner::-webkit-scrollbar-track {
margin: 8px 0;
border-radius: 12px;
}
.DX_Scroll_Inner::-webkit-scrollbar-thumb {
background: rgba(var(--DX-blue), 0.4);
border-radius: 4px;
}
.DX_Search {
align-self: stretch;
height: 40px;
padding: 0 12px;
border-radius: 8px;
border: none;
-webkit-appearance: none;
appearance: none;
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
background: var(--dx-card-bg);
font-size: 14px;
font-weight: 600;
color: var(--dx-text);
transition: outline-color var(--DX-motion) var(--DX-ease);
}
.DX_Search:focus {
outline-color: rgba(var(--DX-blue), 0.35);
}
.DX_Quest_Item {
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
min-height: 56px;
padding: 8px 10px;
box-sizing: border-box;
border-radius: 8px;
outline: 1.5px solid var(--dx-card-border);
outline-offset: -1px;
background: var(--dx-card-bg);
}
.DX_Quest_Item.done {
outline-color: rgba(var(--DX-green), 0.25);
background: rgba(var(--DX-green), 0.04);
}
.DX_Acc_Card {
display: flex;
align-items: center;
gap: 10px;
align-self: stretch;
padding: 10px 12px;
box-sizing: border-box;
border-radius: 12px;
outline: 1.5px solid var(--dx-card-border);
outline-offset: -1px;
background: var(--dx-card-bg);
transition: outline-color var(--DX-motion) var(--DX-ease),
background var(--DX-motion) var(--DX-ease);
position: relative;
overflow: hidden;
}
.DX_Acc_Card:hover {
outline-color: rgba(var(--DX-blue), 0.3);
background: var(--dx-card-hover);
}
.DX_Acc_Avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
background: rgba(var(--DX-blue), 0.1);
color: var(--dx-text);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.DX_Acc_Info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.DX_Acc_Name {
font-size: 13px !important;
font-weight: 700 !important;
color: var(--dx-text) !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
}
.DX_Acc_Sub {
font-size: 11px !important;
font-weight: 600 !important;
color: var(--dx-text) !important;
opacity: 0.5;
margin: 0;
}
.DX_Acc_Sub.active {
color: rgb(var(--DX-green)) !important;
opacity: 1;
display: flex;
align-items: center;
gap: 4px;
}
.DX_Acc_Action_Row {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.DX_Acc_Btn {
height: 28px;
padding: 0 10px;
border-radius: 8px;
corner-shape: var(--DX-corner);
border: none;
cursor: pointer;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.5px;
color: #fff;
background: rgb(var(--DX-blue));
display: flex;
align-items: center;
justify-content: center;
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
transition: opacity var(--DX-motion) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease),
outline var(--DX-motion) var(--DX-ease),
filter var(--DX-motion-fast) var(--DX-ease);
}
.DX_Acc_Btn:focus-visible {
outline-color: rgba(var(--DX-blue), 0.5) !important;
}
.DX_Acc_Btn:hover {
opacity: 0.9;
transform: scale(1.03);
}
.DX_Acc_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Acc_Btn.del {
width: 28px;
padding: 0;
background: rgba(var(--DX-red), 0.15);
color: rgb(var(--DX-red));
outline: 2px solid rgba(var(--DX-red), 0.2);
}
.DX_Acc_Btn.del:hover {
background: rgb(var(--DX-red));
color: #fff;
outline: 2px solid rgba(0, 0, 0, 0.2);
}
.DX_Acc_Btn.del img {
transition: filter var(--DX-motion) var(--DX-ease);
}
.DX_Acc_Btn.del:hover img {
filter: brightness(0) invert(1);
}
.DX_Quest_Icon {
width: 36px;
height: 36px;
object-fit: contain;
flex-shrink: 0;
}
.DX_Quest_Info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.DX_Quest_Title {
font-size: 12px !important;
font-weight: 700 !important;
color: var(--dx-text) !important;
opacity: 0.9;
line-height: 1.25;
overflow-wrap: anywhere;
}
.DX_Quest_Bar_Bg {
height: 4px;
border-radius: 2px;
background: rgba(var(--DX-blue), 0.10);
overflow: hidden;
align-self: stretch;
}
.DX_Quest_Bar_Fill {
height: 100%;
background: rgb(var(--DX-blue));
border-radius: 2px;
transition: width var(--DX-motion-page) var(--DX-ease);
}
.DX_Quest_Item.done .DX_Quest_Bar_Fill {
background: rgb(var(--DX-green));
}
.DX_Quest_Get_Btn {
height: 28px;
min-width: 52px;
padding: 0 8px;
flex-shrink: 0;
border-radius: var(--DX-r-s);
border: none;
cursor: pointer;
font-size: 10px;
font-weight: 800;
white-space: nowrap;
background: rgb(var(--DX-blue));
color: #fff;
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
transition: filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease),
outline var(--DX-motion) var(--DX-ease),
background var(--DX-motion) var(--DX-ease);
}
.DX_Quest_Get_Btn:focus-visible {
outline-color: var(--focus-outline, rgba(var(--DX-blue), 0.5)) !important;
}
.DX_Quest_Get_Btn:hover {
filter: brightness(0.9);
transform: scale(1.03);
}
.DX_Quest_Get_Btn:active {
filter: brightness(0.9);
transform: scale(0.96);
}
.DX_Field_Row,
.DX_Setting_Row,
.DX_Compact_Task {
display: flex;
align-items: center;
justify-content: space-between;
align-self: stretch;
gap: 8px;
min-width: 0;
}
.DX_Stack_Section {
display: flex;
flex-direction: column;
gap: 8px;
align-self: stretch;
min-width: 0;
}
.DX_Row_Text {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1 1 auto;
min-width: 0;
}
.DX_Row_Text .DX_T1,
.DX_Row_Text .DX_T2 {
overflow-wrap: anywhere;
}
.DX_Row_Text .DX_T1 {
line-height: 1.12;
}
.DX_Row_Text .DX_T2 {
font-size: 11px;
line-height: 1.25;
}
.DX_Set_Input_Wrap {
display: flex;
align-items: center;
height: 40px;
min-width: 0;
padding: 0 12px;
box-sizing: border-box;
gap: 6px;
border-radius: 8px;
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
transition: outline-color var(--DX-motion) var(--DX-ease);
background: var(--dx-card-bg);
}
.DX_Set_Input_Wrap:focus-within {
outline-color: rgba(var(--DX-blue), 0.35);
}
.DX_Set_Input_Wrap .DX_Input {
text-align: left;
font-size: 14px !important;
}
.DX_Back_Btn {
align-self: flex-start;
width: auto;
opacity: 0.62;
cursor: pointer;
padding: 2px 0;
}
.DX_Panel_Card {
align-self: stretch;
background: var(--dx-card-bg);
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.DX_Update_Banner {
align-self: stretch;
display: none;
align-items: center;
justify-content: space-between;
gap: 8px;
box-sizing: border-box;
max-height: 0;
opacity: 0;
padding: 0 12px;
margin: 0;
overflow: hidden;
border-radius: 8px;
corner-shape: var(--DX-corner);
background: var(--dx-card-bg);
outline: 1.5px solid var(--dx-card-border);
outline-offset: -1.5px;
transition: max-height var(--DX-motion-page) var(--DX-ease),
opacity var(--DX-motion) var(--DX-ease),
padding var(--DX-motion-page) var(--DX-ease);
}
.DX_Update_Banner.on {
display: flex;
max-height: 56px;
opacity: 1;
padding: 10px 12px;
}
.DX_Quest_Get_Btn.done {
background: rgba(var(--DX-green), 0.10);
color: rgb(var(--DX-green));
pointer-events: none;
}
@media (max-width: 480px) {
.DX_Main_Box {
padding: 14px;
}
}
@media (max-width: 360px) {
.DX_Main_Box {
padding: 12px;
}
.DX_Input_Btn,
.DX_Sm_Btn {
min-width: 60px;
width: 60px;
padding-left: 8px;
padding-right: 8px;
}
}
#duoxjs-hide-button {
cursor: grab;
touch-action: none;
flex: none;
min-width: 92px;
height: 40px;
padding: 10px 12px;
justify-content: center;
align-items: center;
gap: 6px;
color: var(--dx-text);
background: var(--dx-panel-bg) !important;
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
box-shadow: var(--dx-panel-shadow);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
pointer-events: auto !important;
transition: background var(--DX-motion) var(--DX-ease),
outline var(--DX-motion) var(--DX-ease),
box-shadow var(--DX-motion) var(--DX-ease),
color var(--DX-motion) var(--DX-ease),
filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease);
}
#duoxjs-hide-button:active {
cursor: grabbing;
}
#duoxjs-hide-button .DX_Hide_Icon_Stack {
display: grid;
place-items: center;
width: 24px;
height: 18px;
flex: 0 0 24px;
}
#duoxjs-hide-button svg {
display: block !important;
grid-area: 1 / 1;
color: inherit;
fill: currentColor !important;
transition: color var(--DX-motion) var(--DX-ease),
opacity var(--DX-motion) var(--DX-ease),
filter var(--DX-motion) var(--DX-ease),
transform var(--DX-motion) var(--DX-ease);
}
#duoxjs-hide-button svg path {
fill: currentColor !important;
}
#duoxjs-hide-button #hide-icon {
opacity: 1;
transform: scale(1);
}
#duoxjs-hide-button #show-icon {
opacity: 0;
transform: scale(0.85);
}
#duoxjs-hide-button.duoxjs-show-mode #hide-icon {
opacity: 0;
transform: scale(0.85);
}
#duoxjs-hide-button.duoxjs-show-mode #show-icon {
opacity: 1;
transform: scale(1);
}
#duoxjs-hide-button.duoxjs-show-mode {
background: var(--dx-panel-bg) !important;
outline-color: var(--dx-card-border);
color: var(--dx-text);
}
#DX_Main_Content {
transition: opacity var(--DX-motion) var(--DX-ease),
filter var(--DX-motion) var(--DX-ease);
}
#DX_Main_Content.dx-disabled > *:not(#DX_User_Row):not(#DX_User_Row_Divider):not(#DX_Page_AccountManager) {
pointer-events: none;
opacity: 0.5;
filter: grayscale(1);
}
#DX_Root button {
-webkit-tap-highlight-color: transparent;
}
#DX_Root button:focus-visible {
outline-style: solid !important;
outline-width: 2px !important;
outline-color: var(--focus-outline, rgba(var(--DX-blue), 0.5)) !important;
outline-offset: -2px !important;
}
.DX_Btn:focus,
.DX_Btn:active,
.DX_Sm_Btn:focus,
.DX_Sm_Btn:active,
.DX_Acc_Btn:focus,
.DX_Acc_Btn:active,
.DX_Quest_Get_Btn:focus,
.DX_Quest_Get_Btn:active,
.DX_Shop_Btn:focus,
.DX_Shop_Btn:active,
.DX_Input_Btn:focus,
.DX_Input_Btn:active {
outline: 2px solid rgba(0, 0, 0, 0.2);
outline-offset: -2px;
}
.DX_Btn_Blue_Ghost:focus,
.DX_Btn_Blue_Ghost:active,
.DX_Btn_Eel:focus,
.DX_Btn_Eel:active,
.DX_Hash_Btn:focus,
.DX_Hash_Btn:active {
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
}
.DX_Modal_Overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 1000;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity var(--DX-motion-page) var(--DX-ease),
visibility var(--DX-motion-page) var(--DX-ease),
backdrop-filter var(--DX-motion-page) var(--DX-ease);
}
.DX_Modal_Overlay.show {
opacity: 1;
visibility: visible;
}
.DX_Modal_Box {
background: var(--dx-bg);
outline: 2px solid var(--dx-card-border);
outline-offset: -2px;
border-radius: 16px;
padding: 20px;
width: 270px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
transform: scale(0.9);
transition: transform var(--DX-motion-page) var(--DX-ease);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.DX_Modal_Overlay.show .DX_Modal_Box {
transform: scale(1);
}
.solving-btn {
position: relative;
min-width: 150px;
font-size: 17px;
border: none;
border-bottom: 4px solid #2b70c9;
border-radius: 16px;
padding: 13px 16px;
transition: filter var(--DX-motion-fast) var(--DX-ease), color var(--DX-motion-fast) var(--DX-ease);
font-weight: 700;
letter-spacing: .8px;
background: #1cb0f6;
color: #ffffff;
cursor: pointer;
}
.solve-btn {
position: relative;
min-width: 100px;
font-size: 17px;
border: none;
border-bottom: 4px solid #e0a500;
border-radius: 16px;
padding: 13px 16px;
transition: filter var(--DX-motion-fast) var(--DX-ease), color var(--DX-motion-fast) var(--DX-ease);
font-weight: 700;
letter-spacing: .8px;
background: #ffc800;
color: #ffffff;
cursor: pointer;
}
.auto-solver-btn:hover {
filter: brightness(1.15);
}
.auto-solver-btn:active {
border-bottom-width: 0px;
margin-bottom: 4px;
top: 4px;
}
/* Dark Mode Theme overrides for text color only */
html[data-duo-theme="dark"] .solving-btn,
html[data-duo-theme="dark"] .solve-btn,
html._2L9MF .solving-btn,
html._2L9MF .solve-btn {
color: #202f36;
}
._1lzAb._1Exx3 {
padding-bottom: 0px !important;
}
#DX_User_Row {
cursor: pointer;
transition: filter var(--DX-motion-fast) var(--DX-ease),
transform var(--DX-motion-fast) var(--DX-ease),
background var(--DX-motion) var(--DX-ease),
outline-color var(--DX-motion) var(--DX-ease);
}
#DX_User_Row:hover {
filter: brightness(0.9);
transform: scale(1.02);
background: var(--dx-card-hover);
outline-color: rgb(var(--DX-blue));
}
#DX_User_Row:active {
filter: brightness(0.9);
transform: scale(0.97);
}
@media (hover: none) {
.DX_Btn:hover,
.DX_Hash_Btn:hover,
.DX_Input_Btn:hover,
.DX_Sm_Btn:hover,
.DX_Shop_Btn:hover,
.DX_Acc_Btn:hover,
.DX_Quest_Get_Btn:hover,
.auto-solver-btn:hover,
#DX_User_Row:hover {
filter: none !important;
transform: none !important;
opacity: 1 !important;
}
.DX_Select:hover {
outline-color: var(--dx-card-border) !important;
}
.DX_Select_Option:hover {
background: transparent !important;
color: var(--dx-text) !important;
}
.DX_Shop_Card:hover,
.DX_Acc_Card:hover {
outline-color: var(--dx-card-border) !important;
background: var(--dx-card-bg) !important;
}
}
`;
const loadCss = `
.dx-spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(var(--DX-blue), 0.15);
border-top-color: rgb(var(--DX-blue));
border-radius: 50%;
-webkit-animation: dx-spin 0.65s linear infinite;
animation: dx-spin 0.65s linear infinite;
box-sizing: border-box;
}
@-webkit-keyframes dx-spin {
to { -webkit-transform: rotate3d(0, 0, 1, 360deg); }
}
@keyframes dx-spin {
to { transform: rotate3d(0, 0, 1, 360deg); }
}
`;
const uiHtml = `
Action Required
XP Farm is currently running. Do you want to stop it to run Auto League?
Update available
How much XP to farm?
How many Gems to farm?
How many streak days to restore?
Extra Features
Back
Extra Features
Additional utilities and statistics
Auto League
Target specific rank position
Remove Hearts
Drain hearts from this account
Shop Items
XP Summaries
Back
Settings
Configure script preferences
Loop Interval
Delay interval between loops
ms
XP Overshoot
Extra XP buffer (30–500 XP, 0 to disable)
xp
Notification Position
Set the notification pop-up position
On-Client Duolingo Max
Unlock Duolingo Max client-side
Safe Streak Farming
Prevent streak days from exceeding account age
EZ Quiz
Enable custom lessons and instant stories
EZ Quiz Question Count
Set the question count for custom lessons
Automations
Auto Solver
Back
Automations
Configure automated background tasks
Auto Join League
Automatically join a league on startup
Auto Block League
Automatically block league users on startup
Auto Reach Rank
Automatically farm to target rank on startup
Auto Keep Streak
Automatically maintain streak on startup
Auto Quest Saver
Automatically complete quests on startup
Back
About DuoXJS
Your session metrics and credits
v${dxVersion} Stats
ResetXP Gained
0
Gems Gained
0
Streak Gained
0
Since
—
Created by LibreDuo under MIT license
EULA & TOSBack
XP Summaries
Your recent XP history
Loading...
Back
Status Manager
Set your active Duolingo status emoji
Loading...
Back
Shop Items
Purchase boosts and power-ups
Loading...
Back
Quest Center
Manage and view your daily quests
Quest Operations
Loading...
Back
Account Manager
Switch and manage saved profiles
Saved Profiles
No saved accounts.
EULA & TOS
Please read and accept
Back
Social Tools
Interact with other Duolingo users
Block / Unblock
Block or unblock the specified user
Follow / Unfollow
Follow or unfollow the specified user
Send Gift
Gift an item to the specified user
Friend Streak / Quest
Start a Friends Quest or Friends Streak
Mass Follow
Follow or unfollow in bulk
Mass Block
Block or unblock your league
Privacy Status
Change privacy status
Back
Leaderboard
Your current league
Set your status
Back
Activity Feed
Recent activity from your friends
Back
Auto Solver
Configure question auto-solving settings
Show Solve Buttons
Show Solve/Solve All buttons in footer
Auto Solve Mode
Solve questions automatically on start
Random Speed
Wait a random delay before solving
Solve Speed (Fixed)
Delay if random speed is disabled
ms
Min Solve Speed
Minimum delay for random speed
sec
Max Solve Speed
Maximum delay for random speed
sec
No saved accounts.
`; return; } wrap.innerHTML = ""; all.forEach((acc) => { const card = document.createElement("div"); card.className = "DX_Acc_Card"; const isCurrentUser = userId && acc.id == userId; if (isCurrentUser) { card.style.outline = "2px solid rgba(var(--DX-blue), 0.5)"; card.style.outlineOffset = "-2px"; card.style.background = "rgba(var(--DX-blue), 0.08)"; } const avatarDiv = document.createElement("div"); avatarDiv.className = "DX_Acc_Avatar"; if (acc.pic) { const img = document.createElement("img"); img.src = acc.pic; img.style.cssText = "width:100%;height:100%;object-fit:cover;border-radius:50%;"; img.addEventListener("error", () => { avatarDiv.innerHTML = icons.avatar; }); avatarDiv.appendChild(img); } else { avatarDiv.innerHTML = icons.avatar; } card.appendChild(avatarDiv); const payload = readToken(acc.token); const isExpired = payload && payload.exp && Math.floor(Date.now() / 1000) > payload.exp; let subHtml = ""; if (isExpired || acc.status === "relogin") { subHtml = `Re-login Needed
`; } else if (acc.status === "banned") { subHtml = `Account Banned
`; } else { subHtml = `ID: ${acc.id}
`; } const infoDiv = document.createElement("div"); infoDiv.className = "DX_Acc_Info"; infoDiv.innerHTML = `${acc.username}
${subHtml} `; card.appendChild(infoDiv); const canLogin = !isCurrentUser && !isExpired && acc.status !== "relogin" && acc.status !== "banned"; const actionDiv = document.createElement("div"); actionDiv.className = "DX_Acc_Action_Row"; actionDiv.innerHTML = ` ${canLogin ? `` : ""} `; card.appendChild(actionDiv); card.querySelector(".del").addEventListener("click", (e) => { e.stopPropagation(); accRemove(acc.id); }); const loginBtn = card.querySelector(".login"); if (loginBtn) loginBtn.addEventListener("click", (e) => { e.stopPropagation(); accLogin(acc.id); }); wrap.appendChild(card); }); } async function accRefreshAll() { if (accRefreshBusy) return; accRefreshBusy = true; try { const all = accGetAll(); if (!all.length) return; let changed = false; await Promise.all( all.map(async (acc) => { try { const res = await fetchApi( "GET", `https://www.duolingo.com/2017-06-30/users/${acc.id}?fields=id,username,picture,deactivated`, null, { accept: "application/json", authorization: "Bearer " + acc.token, "content-type": "application/json", origin: "https://www.duolingo.com", "User-Agent": dxUserAgent, "x-amzn-trace-id": "User=" + acc.id, }, null, true, ); let newStatus = acc.status || "ok"; if (res.status === 200) { const fresh = safeJsonParse(res.responseText); if (fresh) { const pic = bestAvatarUrl(fresh.picture); if (fresh.username && fresh.username !== acc.username) { acc.username = fresh.username; changed = true; } if (pic !== undefined && pic !== acc.pic) { acc.pic = pic; changed = true; } newStatus = fresh.deactivated === true ? "banned" : "ok"; } else { newStatus = "ok"; } } else if (res.status === 401 || res.status === 400) { newStatus = "relogin"; } else if (res.status === 404) { newStatus = "banned"; } if (acc.status !== newStatus) { acc.status = newStatus; changed = true; } } catch { if (acc.status !== "error") { acc.status = "error"; changed = true; } } }), ); if (changed) { accSetAll(all); renderAccounts(); } } finally { accRefreshBusy = false; } } async function checkForUpdates() { if (checkForUpdatesBusy) return; checkForUpdatesBusy = true; const availableKey = "dx_update_available_version"; const now = Date.now(); try { const res = await fetchApi( "GET", dxUpdateMetaUrl + "?_=" + now, null, {}, ); if (res.status !== 200) return; const match = res.responseText.match(/@version\s+(\S+)/); if (!match) return; const remoteVersion = match[1].trim(); if (compareVersions(remoteVersion, dxScriptVersion) > 0) { localStorage.setItem(availableKey, remoteVersion); showUpdateBanner(remoteVersion); } else { localStorage.removeItem(availableKey); hideUpdateBanner(); } } catch { } finally { checkForUpdatesBusy = false; } } let notifPos = "bottom_center"; function normalizeNotifPos(pos) { const valid = [ "top_left", "top_center", "top_right", "bottom_left", "bottom_center", "bottom_right", ]; if (valid.indexOf(pos) !== -1) return pos; return "bottom_center"; } function applyNotifPos(pos) { notifPos = normalizeNotifPos(pos); layoutNotif(); } function layoutNotif() { const nMain = document.getElementById("DX_Notif_Main"); if (!nMain) return; nMain.style.width = Math.min(320, window.innerWidth - 32) + "px"; const atTop = notifPos.indexOf("top") === 0; const centered = notifPos.indexOf("center") !== -1; const leftAnchored = notifPos.indexOf("left") !== -1; if (atTop) { nMain.style.top = "20px"; nMain.style.bottom = "auto"; nMain.style.flexDirection = "column"; } else { nMain.style.top = "auto"; nMain.style.bottom = "20px"; nMain.style.flexDirection = "column-reverse"; } if (centered) { nMain.style.left = "50%"; nMain.style.right = "auto"; nMain.style.transform = "translateX(-50%)"; nMain.style.alignItems = "center"; } else if (leftAnchored) { nMain.style.left = "16px"; nMain.style.right = "auto"; nMain.style.transform = "none"; nMain.style.alignItems = "flex-start"; } else { nMain.style.left = "auto"; nMain.style.right = "16px"; nMain.style.transform = "none"; nMain.style.alignItems = "flex-end"; } nMain.setAttribute("data-pos", notifPos); } function styleBtn(id, bg, outline, color, label) { const btn = document.getElementById(id); if (!btn) { return; } const labelEl = btn.querySelector(".DX_Btn_Label") || btn.querySelector(".DX_Sm_Btn_Label"); if (!labelEl) { return; } btn.style.background = bg; btn.style.outline = `2px solid ${outline}`; btn.style.outlineOffset = "-2px"; btn.style.setProperty("--focus-outline", outline); labelEl.style.color = color; labelEl.textContent = label; } function resetBtn(id, originalLabel) { styleBtn( id, "rgb(var(--DX-blue))", "rgba(0,0,0,0.2)", "#fff", originalLabel, ); } function stopBtn(id) { styleBtn( id, "rgba(var(--DX-red),0.10)", "rgba(var(--DX-red),0.22)", "rgb(var(--DX-red))", "STOP", ); } function setProgress(identifier, percentage) { const wrapper = document.getElementById(identifier + "_Prog"); const fill = document.getElementById(identifier + "_Fill"); const pct = Number.isFinite(Number(percentage)) ? Math.max(0, Math.min(100, Number(percentage))) : 0; if (wrapper) { wrapper.classList.add("on"); } if (fill) { fill.style.width = pct + "%"; } } function clearProgress(identifier, isSuccess = false) { const wrapper = document.getElementById(identifier + "_Prog"); const fill = document.getElementById(identifier + "_Fill"); if (isSuccess && fill) { fill.classList.add("done"); } if (wrapper) { setTimeout( () => { wrapper.classList.remove("on"); setTimeout(() => { if (fill) { fill.style.width = "0%"; fill.classList.remove("done"); } }, 400); }, isSuccess ? 1500 : 800, ); } else if (fill) { fill.style.width = "0%"; fill.classList.remove("done"); } } function updateUi(status) { const btn = document.getElementById("DX_Conn_Btn"); const text = document.getElementById("DX_Conn_Txt"); const icon = document.getElementById("DX_Conn_Ico"); const content = document.getElementById("DX_Main_Content"); if (!btn || !icon || !content) { return; } const countEl = document.getElementById("DX_UAccCount"); if (countEl) { if (status === "logged_out") { const count = accGetAll().length; countEl.textContent = count === 1 ? "1 saved account" : `${count} saved accounts`; countEl.style.display = "block"; } else { countEl.style.display = "none"; } } const loaderHtml = `${parsedParagraph}
`; } return html; } function updateTermsPage( html, statusText, showDecline, acceptLabel, onAccept, ) { const pageEl = document.getElementById("DX_Page_Terms"); const mainBox = document.getElementById("DX_Main_Box"); if (!pageEl || !mainBox) return; const contentEl = document.getElementById("DX_Terms_Content"); const statusEl = document.getElementById("DX_Terms_Status"); const declineBtn = document.getElementById("DX_Terms_Decline_Btn"); const acceptBtn = document.getElementById("DX_Terms_Accept_Btn"); const easeCurve = "cubic-bezier(0.34, 1.15, 0.64, 1)"; const fadeCurve = "cubic-bezier(0.4, 0, 0.2, 1)"; pageEl.style.transition = `opacity 0.2s ${fadeCurve}, filter 0.2s ${fadeCurve}, transform 0.2s ${fadeCurve}`; pageEl.style.opacity = "0"; pageEl.style.filter = "blur(6px)"; pageEl.style.transform = "scale(0.96)"; const startHeight = mainBox.offsetHeight; setTimeout(() => { mainBox.style.transition = "none"; mainBox.style.height = startHeight + "px"; if (contentEl) contentEl.innerHTML = html; if (statusEl) statusEl.innerText = statusText; if (declineBtn) declineBtn.style.display = showDecline ? "flex" : "none"; if (acceptBtn) { acceptBtn.querySelector(".DX_Sm_Btn_Label").innerText = acceptLabel; acceptBtn.onclick = onAccept; } mainBox.style.height = "auto"; mainBox.style.maxHeight = "none"; const targetHeight = mainBox.offsetHeight; mainBox.style.height = startHeight + "px"; void mainBox.offsetHeight; mainBox.style.transition = `height 0.4s ${easeCurve}`; mainBox.style.height = targetHeight + "px"; setTimeout(() => { pageEl.style.transition = `filter 0.3s ${fadeCurve}, opacity 0.3s ${fadeCurve}, transform 0.3s ${easeCurve}`; pageEl.style.filter = "none"; pageEl.style.opacity = "1"; pageEl.style.transform = "scale(1)"; }, 50); setTimeout(() => { mainBox.style.height = "auto"; mainBox.style.transition = ""; pageEl.style.transition = ""; pageEl.style.filter = ""; pageEl.style.opacity = ""; pageEl.style.transform = ""; relayout(); }, 400); }, 200); } function loadEulaAndTos() { const contentEl = document.getElementById("DX_Terms_Content"); const statusEl = document.getElementById("DX_Terms_Status"); const declineBtn = document.getElementById("DX_Terms_Decline_Btn"); const acceptBtn = document.getElementById("DX_Terms_Accept_Btn"); if (!contentEl || !statusEl || !declineBtn || !acceptBtn) return; const termsAccepted = localStorage.getItem("dx_terms_accepted") === "true"; const statusText = termsAccepted ? "Review EULA & Terms of Service" : "Please read and accept to use DuoXJS"; const acceptLabel = termsAccepted ? "CLOSE" : "ACCEPT"; const onAccept = () => { if (!termsAccepted) { localStorage.setItem("dx_terms_accepted", "true"); connect().then(() => { accRefreshAll(); }); } changePage(1); }; updateTermsPage( 'Loading changelog...
`; } try { const res = await fetchApi( "GET", "https://raw.githubusercontent.com/LibreDuo/DuoXJS/main/CHANGELOG.md?_c=" + Date.now(), null, {}, ); if (res.status !== 200) throw new Error("Changelog not found"); const text = res.responseText || ""; const lines = text.split("\n"); const items = []; let inTargetSection = false; for (let line of lines) { line = line.trim(); if (line.startsWith("## ")) { const headingVersion = line .replace(/^##\s*/, "") .replace(/[\[\]]/g, "") .trim(); inTargetSection = compareVersions(headingVersion, dxScriptVersion) === 0; } else if ( inTargetSection && (line.startsWith("- ") || line.startsWith("* ")) ) { items.push(line.substring(2).trim()); } } const html = items.length ? `No changelog entries for this version.
`; cont.dataset.loaded = "1"; const card = document.getElementById("DX_Changelog_Card"); if (card) { if (html) { card.style.display = "block"; } else { card.style.display = "none"; } } } catch (e) { const card = document.getElementById("DX_Changelog_Card"); if (card) card.style.display = "none"; } finally { changelogBusy = false; } } async function loadXpHistory() { const cont = document.getElementById("DX_XPHistory"); if (!cont || !token || !userId || xpHistoryBusy) return; xpHistoryBusy = true; if (cont.dataset.loaded !== "1") { cont.innerHTML = `Loading...
`; } try { const d = new Date(Date.now() - 7 * 86400000); const pad = (n) => String(n).padStart(2, "0"); const startDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; const res = await fetchApi( "GET", `https://www.duolingo.com/2017-06-30/users/${userId}/xp_summaries?startDate=${startDate}&_=${Date.now()}`, ); if (res.status !== 200) throw new Error("bad status"); let summaries = ( safeJsonParse(res.responseText, {}).summaries || [] ).slice(); summaries.sort((a, b) => (b.date || 0) - (a.date || 0)); summaries = summaries.slice(0, 7).reverse(); if (!summaries.length) { cont.innerHTML = `No recent activity.
`; cont.dataset.loaded = "1"; return; } cont.innerHTML = summaries .map((s) => { const label = new Date((s.date || 0) * 1000).toLocaleDateString( undefined, { weekday: "short", month: "short", day: "numeric" }, ); return `${label}
+${(s.gainedXp || 0).toLocaleString()} XP
Failed to load XP history.
`; } } finally { xpHistoryBusy = false; } } function compactRelativeTime(diffSec) { diffSec = Math.max(0, Math.floor(diffSec)); if (diffSec < 60) return "just now"; const mins = Math.floor(diffSec / 60); if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; const weeks = Math.floor(days / 7); if (weeks < 5) return `${weeks}w ago`; const months = Math.floor(days / 30); if (months < 12) return `${months}mo ago`; return `${Math.floor(months / 12)}y ago`; } function normalizeRelativeText(value) { const raw = String(value == null ? "" : value).trim(); if (!raw) return ""; if (/^just now$/i.test(raw)) return "just now"; const match = raw.match( /^(\d+)\s*(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mos?|years?|yrs?|y)(?:\s+ago)?$/i, ); if (!match) return ""; const amount = parseInt(match[1], 10); const unit = match[2].toLowerCase(); if (unit.startsWith("sec") || unit === "s") return amount < 60 ? "just now" : `${Math.floor(amount / 60)}m ago`; if (unit.startsWith("min") || unit === "m") return `${amount}m ago`; if (unit.startsWith("h")) return `${amount}h ago`; if (unit.startsWith("d")) return `${amount}d ago`; if (unit.startsWith("w")) return `${amount}w ago`; if (unit.startsWith("mo")) return `${amount}mo ago`; if (unit.startsWith("y")) return `${amount}y ago`; return ""; } function feedTimestampMs(card) { const fields = [ card?.subtitle, card?.timestamp, card?.createdAt, card?.creationDate, card?.eventTime, card?.time, card?.date, ]; for (const value of fields) { if (value === undefined || value === null || value === "") continue; if (typeof value === "number" || /^\d+$/.test(String(value).trim())) { const num = Number(value); if (Number.isFinite(num) && num > 0) { return num < 100000000000 ? num * 1000 : num; } } const raw = String(value).trim(); const isoLike = raw.match( /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:\s*(Z|[+-]\d{2}:?\d{2}))?$/, ); if (isoLike) { const hasZone = !!isoLike[8]; const normalized = raw .replace(" ", "T") .replace(/([+-]\d{2})(\d{2})$/, "$1:$2"); const parsed = hasZone ? Date.parse(normalized) : Date.UTC( +isoLike[1], +isoLike[2] - 1, +isoLike[3], +isoLike[4], +isoLike[5], +isoLike[6], isoLike[7] ? Number((isoLike[7] + "000").slice(0, 3)) : 0, ); if (!Number.isNaN(parsed)) return parsed; } const parsed = Date.parse(raw); if (!Number.isNaN(parsed)) return parsed; } return null; } function feedTimeAgo(card) { const subtitleRelative = normalizeRelativeText(card?.subtitle); if (subtitleRelative) return subtitleRelative; const ts = feedTimestampMs(card); if (!ts) return ""; return compactRelativeTime((Date.now() - ts) / 1000); } async function getFeed() { const cont = document.getElementById("DX_Feed_Container"); if (!cont || !token || !userId || feedBusy) return; feedBusy = true; const loaded = cont.dataset.loaded === "1"; if (!loaded) cont.innerHTML = `Loading...
`; try { const res = await fetchApi( "GET", `https://www.duolingo.com/2017-06-30/friends/users/${userId}/feed/v2?uiLanguage=${uiLang()}`, ); if (res.status !== 200) throw new Error("bad status"); const cards = []; (safeJsonParse(res.responseText, {}).feed || []).forEach((section) => { (section.feedCards || []).forEach((c) => cards.push(c)); }); if (!cards.length) { cont.innerHTML = `No recent activity.
`; cont.dataset.loaded = "1"; return; } const frag = document.createDocumentFragment(); cards.slice(0, 40).forEach((c) => { const name = escapeHtml(c.displayName || "Someone"); const cardType = c.cardType || ""; let message; if (cardType === "FOLLOW") { message = `You followed ${c.displayName || "someone"}`; } else if (cardType === "FOLLOW_BACK") { message = `${c.displayName || "Someone"} followed you`; } else { message = c.body || ""; } const text = escapeHtml(message); const timeAgo = escapeHtml(feedTimeAgo(c)); const raw = c.avatarUrl || c.picture || ""; const av = raw ? (raw.indexOf("http") === 0 ? raw : "https:" + raw) + "/xlarge" : ""; const row = document.createElement("div"); row.className = "DX_HStack_4"; row.style.cssText = "align-self: stretch; padding: 8px 10px; border-radius: var(--DX-r-s); corner-shape: var(--DX-corner); gap: 8px; background: var(--dx-card-bg);"; row.innerHTML = `${name}
${text ? `${text}
` : ""}${timeAgo}
` : ""} `; frag.appendChild(row); }); cont.replaceChildren(frag); cont.dataset.loaded = "1"; } catch { if (!loaded) cont.innerHTML = `Failed to load feed.
`; } finally { feedBusy = false; } } function addStat(kind, amount) { if (!statKeys[kind] || !amount || amount <= 0) { return; } localStorage.setItem(statKeys[kind], String(readStat(kind) + amount)); showStats(); } function showUser() { if (!user) { return; } const usernameEl = document.getElementById("DX_UName"); if (usernameEl) { usernameEl.textContent = user.username || ""; } const xpEl = document.getElementById("DX_UXP"); if (xpEl) { xpEl.textContent = (user.totalXp || 0).toLocaleString(); } const gemsEl = document.getElementById("DX_UGems"); if (gemsEl) { gemsEl.textContent = (user.gems || 0).toLocaleString(); } const streakEl = document.getElementById("DX_UStreak"); if (streakEl) { streakEl.textContent = (user.streak || 0).toLocaleString(); } const userRowEl = document.getElementById("DX_User_Row"); if (userRowEl) { userRowEl.style.display = "flex"; const userRowDiv = document.getElementById("DX_User_Row_Divider"); if (userRowDiv) userRowDiv.style.display = "block"; const statsRow = document.getElementById("DX_User_Stats_Row"); if (statsRow) statsRow.style.display = "flex"; const countEl = document.getElementById("DX_UAccCount"); if (countEl) countEl.style.display = "none"; } const picUrl = bestAvatarUrl(user.picture); const avatarEl = document.getElementById("DX_Avatar"); if (avatarEl) { const currentImg = avatarEl.querySelector("img"); if (!currentImg || currentImg.src !== picUrl) { const img = document.createElement("img"); img.src = picUrl; img.style.cssText = "width: 100%; height: 100%; object-fit: cover; border-radius: 50%;"; img.addEventListener("error", () => { avatarEl.innerHTML = icons.avatar; }); avatarEl.replaceChildren(img); } } } let accountStatsTs = 0; const ACCOUNT_STATS_TTL = 30000; async function refreshStats(force = false) { if (!token || !userId || !user || refreshStatsBusy) { return; } if (!force && Date.now() - accountStatsTs < ACCOUNT_STATS_TTL) { return; } refreshStatsBusy = true; try { const res = await fetchApi( "GET", `${config.api.users}/${userId}?fields=streak,totalXp,gems,streakData,creationDate`, ); if (res.status === 200) { const data = safeJsonParse(res.responseText, {}); if (data.totalXp !== undefined) { user.totalXp = data.totalXp; } if (data.gems !== undefined) { user.gems = data.gems; } if (data.streak !== undefined) { user.streak = data.streak; } if (data.streakData !== undefined) { user.streakData = data.streakData; } if (data.creationDate !== undefined) { user.creationDate = data.creationDate; } accountStatsTs = Date.now(); showUser(); } } catch { } finally { refreshStatsBusy = false; } } function accountTimezone() { return user && user.timezone ? user.timezone : "America/New_York"; } function uiLang() { return user && user.fromLanguage ? user.fromLanguage : "en"; } function tzParts(timeZone, date) { const dtf = new Intl.DateTimeFormat("en-US", { timeZone: timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); const out = {}; dtf.formatToParts(date).forEach((part) => { if (part.type !== "literal") out[part.type] = part.value; }); return out; } function wallClockToSeconds( timeZone, year, month, day, hour, minute, second, ) { const guess = Date.UTC(year, month - 1, day, hour, minute, second); const parts = tzParts(timeZone, new Date(guess)); const localAsUtc = Date.UTC( +parts.year, +parts.month - 1, +parts.day, +parts.hour, +parts.minute, +parts.second, ); const offset = localAsUtc - guess; return Math.floor((guess - offset) / 1000); } function accountToday(timeZone) { const parts = tzParts(timeZone, new Date()); return { year: +parts.year, month: +parts.month, day: +parts.day }; } function accountNowIso() { const now = new Date(); const parts = tzParts(accountTimezone(), now); const ms = String(now.getMilliseconds()).padStart(3, "0"); return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}.${ms}Z`; } async function playStory(bonus, signal) { try { const currentSeconds = Math.floor(Date.now() / 1000); const genDuration = Math.floor(Math.random() * 121 + 300); const payload = { awardXp: true, completedBonusChallenge: true, fromLanguage: "en", learningLanguage: "fr", hasXpBoost: false, illustrationFormat: "svg", isFeaturedStoryInPracticeHub: true, isLegendaryMode: true, isV2Redo: false, isV2Story: false, masterVersion: true, maxScore: 0, score: 0, happyHourBonusXp: bonus, startTime: currentSeconds, endTime: currentSeconds + genDuration, }; const res = await fetchApi( "POST", `${config.api.stories}/fr-en-le-passeport/complete`, payload, null, signal, ); if (res.status !== 200) return false; const data = safeJsonParse(res.responseText, {}); return !!data.awardedXp; } catch { return false; } } async function farmXp(targetAmount) { const isInfinite = targetAmount === Infinity; if (!isInfinite) { const room = parseInt(localStorage.getItem("dx_xp_room")) || 0; if (room > 0) { targetAmount += Math.min(500, Math.max(30, room)); } } const maxPerReq = 499; const minPerReq = 30; let loops = isInfinite ? Infinity : Math.floor(targetAmount / maxPerReq); let remAmount = isInfinite ? 0 : targetAmount % maxPerReq; if (!isInfinite && remAmount > 0 && remAmount < minPerReq && loops > 0) { loops--; remAmount += maxPerReq; } const expectedIters = isInfinite ? Infinity : loops + (remAmount >= minPerReq ? 1 : 0); let doneIters = 0; let totalXp = 0; const sig = farmSignal("xp"); stopBtn("DX_XP_Btn"); try { for (let i = 0; i < loops; i++) { if (!farmStates.xp) { break; } const success = await playStory(469, sig); if (!farmStates.xp) break; if (success) { totalXp += maxPerReq; doneIters++; user.totalXp += maxPerReq; showUser(); } if (!isInfinite) { setProgress("DX_XP", (doneIters / expectedIters) * 100); } await waitStop(delayMs, sig); } if (!isInfinite && remAmount >= minPerReq && farmStates.xp) { const success = await playStory( Math.min(remAmount - minPerReq, 469), sig, ); if (success) { totalXp += remAmount; doneIters++; user.totalXp += remAmount; showUser(); } setProgress("DX_XP", 100); } const completed = farmStates.xp; if (totalXp > 0) { addStat("xp", totalXp); notify( "success", completed ? "XP Farm Complete" : "XP Farm Stopped", `Farmed ${totalXp} XP.`, ); refreshStats(true); } clearProgress("DX_XP", completed); } finally { farmStates.xp = false; farmCtl.xp = null; resetBtn("DX_XP_Btn", "RUN"); } } async function checkGems(signal) { try { const res = await fetchApi( "GET", `https://www.duolingo.com/2023-05-23/users/${userId}?fields=rewardBundles{rewards}`, null, null, signal, ); if (res.status !== 200) { return []; } const data = safeJsonParse(res.responseText, {}); const collected = []; for (const bundle of data.rewardBundles || []) { for (const reward of bundle.rewards || []) { if ( !reward.consumed && ((reward.id || "").startsWith("SKILL_COMPLETION-") || (reward.id || "").startsWith("SKILL_COMPLETION_BALANCED-")) ) { collected.push({ id: reward.id, amount: reward.amount || 0 }); } } } return collected; } catch { return []; } } async function claimGem(rewardId, signal) { const payload = { consumed: true, fromLanguage: user.fromLanguage, learningLanguage: user.learningLanguage, }; try { let res = await fetchApi( "PATCH", `https://www.duolingo.com/2023-05-23/users/${userId}/rewards/${rewardId}`, payload, null, signal, ); return res.status === 200; } catch { return false; } } async function farmGems(targetLoops) { const isInfinite = targetLoops === Infinity; stopBtn("DX_Gem_Btn"); let totalGained = 0; let doneLoops = 0; const sig = farmSignal("gem"); try { while (farmStates.gem && (isInfinite || doneLoops < targetLoops)) { let available = await checkGems(sig); if (!farmStates.gem) break; if (available.length === 0) { await waitStop(delayMs * 2, sig); continue; } if (farmStates.gem && (isInfinite || doneLoops < targetLoops)) { const results = await Promise.all( available.map((r) => claimGem(r.id, sig)), ); if (!farmStates.gem) break; let batchGained = 0; results.forEach((ok, idx) => { if (ok) batchGained += available[idx].amount || 0; }); if (batchGained > 0) { totalGained += batchGained; if (user) { user.gems = (user.gems || 0) + batchGained; showUser(); } } await waitStop(150, sig); doneLoops++; if (!isInfinite) { setProgress("DX_Gem", (doneLoops / targetLoops) * 100); } await waitStop(Math.max(50, delayMs - 50), sig); } await waitStop(delayMs, sig); } const completed = farmStates.gem; if (totalGained > 0) { addStat("gems", totalGained); notify( "success", completed ? "Gem Farm Complete" : "Gem Farm Stopped", `+${totalGained} gems acquired.`, ); refreshStats(true); } clearProgress("DX_Gem", completed); } finally { farmStates.gem = false; farmCtl.gem = null; resetBtn("DX_Gem_Btn", "RUN"); } } async function completePracticeSession(endSecs, signal) { try { const sPayload = { challengeTypes: config.challengeTypes, fromLanguage: user.fromLanguage, isFinalLevel: false, isV2: true, juicy: true, learningLanguage: user.learningLanguage, smartTipsVersion: 2, type: "GLOBAL_PRACTICE", }; const sRes = await fetchApi( "POST", config.api.sessions, sPayload, null, signal, ); if (sRes.status !== 200) { return false; } const sData = safeJsonParse(sRes.responseText, {}); if (!sData.id) { return false; } const fPayload = { ...sData, heartsLeft: 5, startTime: endSecs - 1, endTime: endSecs, enableBonusPoints: false, failed: false, maxInLessonStreak: 9, shouldLearnThings: true, }; const fRes = await fetchApi( "PUT", `${config.api.sessions}/${sData.id}`, fPayload, null, signal, ); if (fRes.status !== 200) { return false; } const fData = safeJsonParse(fRes.responseText, {}); if (fData.xpGain === undefined || fData.xpGain === null) { return false; } return true; } catch { return false; } } async function farmStreak(targetDays) { let isInfinite = targetDays === Infinity; stopBtn("DX_Streak_Btn"); if (localStorage.getItem("dx_safe_streak") === "true" && user) { let creationDateVal = user.creationDate; if (creationDateVal) { const creationDateObj = new Date( typeof creationDateVal === "number" ? creationDateVal * 1000 : creationDateVal, ); if (!isNaN(creationDateObj.getTime())) { const safeTz = accountTimezone(); const creationParts = tzParts(safeTz, creationDateObj); const creationDayMs = Date.UTC( +creationParts.year, +creationParts.month - 1, +creationParts.day, ); const todayParts = accountToday(safeTz); const todayDayMs = Date.UTC( todayParts.year, todayParts.month - 1, todayParts.day, ); const elapsedDays = Math.round( (todayDayMs - creationDayMs) / 86400000, ); const maxSafeStreak = elapsedDays; const currentStreak = user.streak || 0; const remainingSafeDays = Math.max(0, maxSafeStreak - currentStreak); if (remainingSafeDays <= 0) { notify( "error", "Safe Streak", "Farming blocked: your streak has already reached the maximum limit of " + maxSafeStreak + " days for your account age.", ); farmStates.streak = false; resetBtn("DX_Streak_Btn", "RUN"); return; } if (isInfinite || targetDays > remainingSafeDays) { notify( "warning", "Safe Streak", "Target days capped at " + remainingSafeDays + " to prevent exceeding account age.", ); targetDays = remainingSafeDays; isInfinite = false; } } } } const tz = accountTimezone(); let base; const dateStr = user.streakData?.currentStreak?.startDate; if (dateStr && /^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { const parts = dateStr.split("-").map(Number); base = { year: parts[0], month: parts[1], day: parts[2] }; } else { base = accountToday(tz); } let doneLoops = 0; let savedDays = 0; const sig = farmSignal("streak"); try { while (farmStates.streak && (isInfinite || doneLoops < targetDays)) { const endSecs = wallClockToSeconds( tz, base.year, base.month, base.day - 1 - doneLoops, 12, 0, 0, ); if (farmStates.streak) { const ok = await completePracticeSession(endSecs, sig); if (ok && user) { savedDays++; user.streak++; showUser(); } } doneLoops++; if (!isInfinite) { setProgress("DX_Streak", (doneLoops / targetDays) * 100); } await waitStop(delayMs, sig); } const completed = farmStates.streak; if (savedDays > 0) { addStat("streak", savedDays); notify( "success", completed ? "Streak Restored" : "Streak Farm Stopped", `Processed ${savedDays} days.`, ); refreshStats(true); } else if (completed && doneLoops > 0) { notify( "error", "Streak Farm", "No days could be saved. Please try again.", ); } clearProgress("DX_Streak", completed); } finally { farmStates.streak = false; farmCtl.streak = null; resetBtn("DX_Streak_Btn", "RUN"); } } async function keepStreak() { if (!user) return false; const endSecs = Math.floor(Date.now() / 1000); return completePracticeSession(endSecs); } async function autoKeepStreak() { if (localStorage.getItem("dx_auto_keep_streak") !== "true") return; if (!user || streakKeepBusy) return; const tz = accountTimezone(); const t = accountToday(tz); const pad = (n) => String(n).padStart(2, "0"); const todayIso = `${t.year}-${pad(t.month)}-${pad(t.day)}`; const keptKey = `dx_streak_kept_date_${userId}`; if (localStorage.getItem(keptKey) === todayIso) return; if (user.streakData?.currentStreak?.lastExtendedDate === todayIso) { localStorage.setItem(keptKey, todayIso); return; } if (localStorage.getItem("dx_safe_streak") === "true") { let creationDateVal = user.creationDate; if (creationDateVal) { const creationDateObj = new Date( typeof creationDateVal === "number" ? creationDateVal * 1000 : creationDateVal, ); if (!isNaN(creationDateObj.getTime())) { const creationParts = tzParts(tz, creationDateObj); const creationDayMs = Date.UTC( +creationParts.year, +creationParts.month - 1, +creationParts.day, ); const todayDayMs = Date.UTC(t.year, t.month - 1, t.day); const elapsedDays = Math.round( (todayDayMs - creationDayMs) / 86400000, ); const maxSafeStreak = elapsedDays; const currentStreak = user.streak || 0; if (currentStreak + 1 > maxSafeStreak) { notify( "warning", "Auto Keep Streak", "Auto streak blocked: keeping streak today would exceed the safe limit (" + maxSafeStreak + " days) for your account age.", ); return; } } } } streakKeepBusy = true; try { const ok = await keepStreak(); if (ok) { localStorage.setItem(keptKey, todayIso); refreshStats(true); notify("success", "Auto Keep Streak", "Your streak is safe for today."); } else { notify( "error", "Auto Keep Streak", "Could not save your streak today. Will keep retrying.", ); } } finally { streakKeepBusy = false; } } async function autoReachRank(knownRank) { if (localStorage.getItem("dx_auto_reach_rank") !== "true") return; if (!userId || farmStates.league) return; const target = parseInt(localStorage.getItem("dx_league_target")) || 1; const rank = knownRank === undefined ? await getLeagueRank() : knownRank; if (rank && rank > target) { notify("info", "Auto Reach Rank", `Climbing to #${target}...`); farmStates.league = true; farmCtl.league = new AbortController(); farmLeague(target); } } async function autoBlockLeague() { if (localStorage.getItem("dx_auto_block_league") !== "true") return; if (!userId || farmStates.blockmass || farmStates.unblock) return; farmStates.blockmass = true; try { const result = await blockLeagueUsers({ auto: true }); if (!result.skipped && result.ok > 0) { notify( "success", "Auto Block League", `Blocked ${result.ok} league user(s).`, ); } } finally { clearProgress("DX_Block_Mass", true); farmStates.blockmass = false; resetBtn("DX_Block_Mass_Btn", "RUN"); } } async function autoQuestSaver() { if (localStorage.getItem("dx_auto_quest_saver") !== "true") return; if (!token || !userId || questSaverBusy) return; questSaverBusy = true; try { if (!questState) return; const schemaGoals = Array.isArray(questState.schema?.goals) ? questState.schema.goals : []; if (schemaGoals.length === 0) return; const t = accountToday(accountTimezone()); const pad = (n) => String(n).padStart(2, "0"); const monthlyKey = `${t.year}_${pad(t.month)}_monthly_challenge`; const progress = questState.progress || {}; const keys = Object.keys(progress).filter((k) => k !== monthlyKey); if (keys.length === 0 || !keys.some((k) => progress[k] === 0)) return; const mSet = new Set(); schemaGoals.forEach((goal) => { if (goal.metric && goal.metric !== "QUESTS") mSet.add(goal.metric); }); if (mSet.size === 0) return; const monthPrefix = `${t.year}_${pad(t.month)}_monthly`; let monthlyThreshold = 0; schemaGoals.forEach((goal) => { if (goal.goalId && goal.goalId.startsWith(monthPrefix)) { monthlyThreshold = Math.max(monthlyThreshold, goal.threshold || 0); } }); let monthlyProgress = 0; Object.keys(progress).forEach((k) => { if (!k.startsWith(monthPrefix)) return; const rd = progress[k]; const v = typeof rd === "number" ? rd : rd && rd.progress ? rd.progress : 0; if (v > monthlyProgress) monthlyProgress = v; }); const targetDay = monthlyThreshold > 0 ? Math.min(t.day, monthlyThreshold) : t.day; const questQty = Math.max(0, targetDay - monthlyProgress); const updates = [...mSet].map((mName) => ({ metric: mName, quantity: 2000, })); if (questQty > 0) updates.push({ metric: "QUESTS", quantity: questQty }); const bPayload = { metric_updates: updates, timezone: accountTimezone(), timestamp: accountNowIso(), }; const res = await fetchApi( "POST", `${config.api.goals}/users/${userId}/progress/batch`, bPayload, setGoalHeaders(token), ); if (res.status === 200) { notify("success", "Auto Quest Saver", "Your quests have been saved."); getQuests(); } } catch { } finally { questSaverBusy = false; } } async function refreshQuestCenter() { if (!token || !userId) return; const state = await fetchQuests(false); if (state && pageId === "Quests") { showQuests(); } autoQuestSaver(); } async function joinLeague() { let wasPrivate = false; try { const pRes = await fetchApi( "GET", `https://www.duolingo.com/2023-05-23/users/${userId}/privacy-settings?fields=privacySettings`, ); if (pRes.status === 200) { const pData = safeJsonParse(pRes.responseText, {}); const soc = (pData.privacySettings || []).find( (s) => s.id === "disable_social", ); wasPrivate = soc ? soc.enabled : false; } } catch {} if (wasPrivate) { try { await fetchApi( "PATCH", `https://www.duolingo.com/2023-05-23/users/${userId}/privacy-settings?fields=privacySettings`, { DISABLE_SOCIAL: false }, ); await wait(2000); } catch {} } try { const ok = await playStory(0); if (ok && user) { user.totalXp += 30; showUser(); } } catch {} if (wasPrivate) { try { await fetchApi( "PATCH", `https://www.duolingo.com/2023-05-23/users/${userId}/privacy-settings?fields=privacySettings`, { DISABLE_SOCIAL: true }, ); } catch {} } } async function farmLeague(targetRank) { stopBtn("DX_League_Btn"); setProgress("DX_League", 10); let joinAttemptCount = 0; const sig = farmSignal("league"); try { while (farmStates.league) { try { const res = await fetchApi( "GET", `${config.api.leaderboards}/users/${userId}?client_unlocked=true&get_reactions=true&_=${Date.now()}`, null, null, sig, ); if (!farmStates.league) break; if (res.status !== 200) { await waitStop(3000, sig); continue; } const data = safeJsonParse(res.responseText, {}); leagueDataCache = data; leagueDataTs = Date.now(); applyLeagueSummary(data); const activeRanks = data?.active?.cohort?.rankings || []; const cRankings = activeRanks.find((u) => u.user_id == userId); if (!cRankings) { if (joinAttemptCount === 0) { joinAttemptCount = 1; notify( "info", "Auto League", "You are not in a league yet. Joining now...", ); await joinLeague(); await waitStop(3000, sig); continue; } else if (joinAttemptCount < 10) { joinAttemptCount++; try { const ok = await playStory(0, sig); if (ok && user) { user.totalXp += 30; showUser(); } } catch {} await waitStop(3000, sig); continue; } notify( "error", "League Error", "Could not confirm league join status after 10 checks. Please try again later.", ); break; } const cRankPos = activeRanks.indexOf(cRankings) + 1; const tUserRanking = activeRanks[targetRank - 1]; if (!tUserRanking || cRankPos <= targetRank) { if (farmStates.league) { notify("success", "Goal Reached", `Reached Rank #${targetRank}!`); } setProgress("DX_League", 100); break; } const scoreGap = tUserRanking.score - cRankings.score; const progressPct = Math.min( 95, Math.floor((cRankings.score / tUserRanking.score) * 100), ); setProgress("DX_League", progressPct); const room = parseInt(localStorage.getItem("dx_xp_room")) || 0; const overshoot = room > 0 ? Math.min(500, Math.max(30, room)) : 5; if (scoreGap + overshoot > 0) { const bonus = Math.min(469, Math.max(0, scoreGap - 30 + overshoot)); const ok = await playStory(bonus, sig); if (!farmStates.league) break; if (ok) { user.totalXp += 30 + bonus; showUser(); } else { await waitStop(2000, sig); } } await waitStop(delayMs, sig); } catch { await waitStop(3000, sig); } } const completed = farmStates.league; if (!completed) { notify("warning", "Auto League Stopped", "Rank targeting aborted."); } clearProgress("DX_League", completed); refreshStats(true); } finally { farmStates.league = false; farmCtl.league = null; resetBtn("DX_League_Btn", "RUN"); } } function showConfirmModal(onConfirm) { const modal = document.getElementById("DX_Confirm_Modal"); const btnCancel = document.getElementById("DX_Modal_Cancel"); const btnConfirm = document.getElementById("DX_Modal_Confirm"); const cleanup = () => { modal.classList.remove("show"); btnCancel.removeEventListener("click", handleCancel); btnConfirm.removeEventListener("click", handleConfirm); }; const handleCancel = () => cleanup(); const handleConfirm = () => { cleanup(); onConfirm(); }; btnCancel.addEventListener("click", handleCancel); btnConfirm.addEventListener("click", handleConfirm); modal.classList.add("show"); } let leaguePollTimer = null; let leagueJoinAttempted = false; let leagueDataCache = null; let leagueDataTs = 0; const LEAGUE_TTL = 30000; async function fetchLeagueData(force) { if (!userId || !token) return null; if (!force && leagueDataCache && Date.now() - leagueDataTs < LEAGUE_TTL) { return leagueDataCache; } try { const res = await fetchApi( "GET", `${config.api.leaderboards}/users/${userId}?client_unlocked=true&get_reactions=true&_=${Date.now()}`, ); if (res.status === 200) { const parsed = safeJsonParse(res.responseText); if (parsed) { leagueDataCache = parsed; leagueDataTs = Date.now(); } } } catch {} return leagueDataCache; } function leagueRankFromData(data) { const active = data?.active; if (active && active.cohort && Array.isArray(active.cohort.rankings)) { const myIndex = active.cohort.rankings.findIndex( (r) => r.user_id == userId, ); return myIndex !== -1 ? myIndex + 1 : 0; } return null; } function applyLeagueSummary(data) { const cohort = data?.active?.cohort; const rankings = cohort?.rankings; if (!Array.isArray(rankings)) return; const idx = rankings.findIndex((r) => String(r.user_id) === String(userId)); const tier = cohort?.tier; updateLeagueBadge(tier); updateProfileLeague(tier, idx >= 0 ? idx + 1 : null); } async function getLeagueRank() { return leagueRankFromData(await fetchLeagueData(true)); } function updateLeagueDropdown(rank) { const sel = document.getElementById("DX_League_Select"); if (!sel) return; if (sel.classList.contains("open")) return; const opts = sel.querySelector(".DX_Select_Options"); const text = sel.querySelector(".DX_Select_Text"); const chevron = sel.querySelector(".DX_Chevron"); const btn = document.getElementById("DX_League_Btn"); const lbl = document.getElementById("DX_League_Lbl"); if (btn && !farmStates.league) btn.disabled = false; if (lbl && !farmStates.league) lbl.innerText = "RUN"; if (chevron) chevron.style.display = ""; sel.style.pointerEvents = "auto"; const maxPos = 15; let optHtml = ""; for (let i = 1; i <= maxPos; i++) { optHtml += `Login required.
`; return; } const isRefresh = questState !== null; const prevScroll = cont.scrollTop; if (!isRefresh) { cont.innerHTML = `Loading...
`; } const state = await fetchQuests(force); if (state) { showQuests(); cont.scrollTop = prevScroll; if (forceBtn) { forceBtn.disabled = false; } } else { if (!isRefresh) cont.innerHTML = `Failed to load quests.
`; } } function questDisplayGoals() { if (!questState || !Array.isArray(questState.schema?.goals)) { return []; } const mGoalMap = new Map(); questState.schema.goals.forEach((goal) => { if (!goal || !goal.goalId) return; const match = goal.goalId.match(/^(\d{4}_\d{2})_monthly/); if (!match) { mGoalMap.set(goal.goalId, goal); return; } const exGoal = mGoalMap.get(match[1]); if (!exGoal) { mGoalMap.set(match[1], goal); } else if ( !exGoal.category?.includes("CHALLENGE") && goal.category?.includes("CHALLENGE") ) { mGoalMap.set(match[1], goal); } }); return [...mGoalMap.values()].reverse(); } function questRemaining(goal) { const isEarned = questState.earned.has(goal.badgeId) || questState.earned.has(goal.goalId); if (isEarned) return 0; const rData = questState.progress[goal.goalId]; let cVal = 0; if (typeof rData === "number") cVal = rData; else if (rData && rData.progress) cVal = rData.progress; return Math.max(0, (goal.threshold || 10) - cVal); } function showQuests(filterStr) { const cont = document.getElementById("DX_Quest_Container"); if (!cont) { return; } const searchEl = document.getElementById("DX_Quest_Search"); const query = ( filterStr !== undefined ? filterStr : searchEl ? searchEl.value : "" ) .trim() .toLowerCase(); cont.innerHTML = ""; const allGoals = questDisplayGoals(); if (allGoals.length === 0) { cont.innerHTML = `No active quests.
`; return; } const yearOf = (goal) => { const m = goal.goalId.match(/^(\d{4})_/); return m ? m[1] : "Daily"; }; const ddGoals = ( query ? allGoals.filter((goal) => (goal.title?.uiString || goal.goalId || "") .toLowerCase() .includes(query), ) : allGoals ).filter((goal) => yearOf(goal) !== "Daily"); if (ddGoals.length === 0) { cont.innerHTML = `No quests found.
`; return; } const sorted = ddGoals.slice().sort((a, b) => { const ya = yearOf(a), yb = yearOf(b); if (ya === yb) return 0; if (ya === "Daily") return -1; if (yb === "Daily") return 1; return Number(yb) - Number(ya); }); let lastYear = null; sorted.forEach((goal) => { const yr = yearOf(goal); if (yr !== lastYear) { lastYear = yr; const yHeader = document.createElement("div"); yHeader.className = "DX_Shop_Section_Header DX_NoSel"; yHeader.innerHTML = `${yr}`; cont.appendChild(yHeader); } const isEarned = questState.earned.has(goal.badgeId) || questState.earned.has(goal.goalId); const rData = questState.progress[goal.goalId]; let cVal = 0; if (typeof rData === "number") { cVal = rData; } else if (rData && rData.progress) { cVal = rData.progress; } const target = goal.threshold || 10; if (isEarned) { cVal = target; } const pct = Math.min(100, (cVal / target) * 100); const rem = Math.max(0, target - cVal); let qIconUrl = "https://d35aaqx5ub95lt.cloudfront.net/vendor/7ef36bae3f9d68fc763d3451b5167836.svg"; if ( goal.category?.includes("MONTHLY") && Array.isArray(questState.schema?.badges) ) { const linked = questState.schema.badges?.find( (b) => b.badgeId === goal.badgeId, ); if (linked && linked.icon?.enabled?.lightMode) { qIconUrl = linked.icon.enabled.lightMode.svg || linked.icon.enabled.lightMode.url || qIconUrl; } } const qItem = document.createElement("div"); qItem.className = `DX_Quest_Item ${isEarned ? "done" : ""}`; let bruteHtml = ""; if (!isEarned && rem > 0) { bruteHtml = ``; } qItem.innerHTML = `${escapeHtml(goal.title?.uiString || goal.goalId)}
Failed to load shop. Please try again.
`; } } catch { cont.innerHTML = `Failed to load shop. Please try again.
`; } } function showShop(filterStr) { const cont = document.getElementById("DX_Shop_Container"); if (!cont) return; cont.innerHTML = ""; const query = (filterStr || "").trim().toLowerCase(); const fPowerUps = query ? powerUpItems.filter((p) => p.name.toLowerCase().includes(query)) : powerUpItems; const fItems = query ? shopCache.filter((i) => ((i.displayName || "") + (i.id || "")).toLowerCase().includes(query), ) : shopCache; if (fPowerUps.length === 0 && fItems.length === 0) { cont.innerHTML = `No items found.
`; return; } const grid = document.createElement("div"); grid.className = "DX_Shop_Grid"; const fragment = document.createDocumentFragment(); if (fPowerUps.length > 0) { const puHeader = document.createElement("div"); puHeader.className = "DX_Shop_Section_Header DX_NoSel"; puHeader.innerHTML = ` Power-Ups `; fragment.appendChild(puHeader); fPowerUps.forEach((item) => { const card = document.createElement("div"); card.className = "DX_Shop_Card"; const ico = document.createElement("img"); ico.className = "DX_Shop_Ico DX_NoSel"; ico.src = item.ico; card.appendChild(ico); const nameDiv = document.createElement("div"); nameDiv.className = "DX_Shop_Name DX_NoSel"; nameDiv.innerText = item.name; card.appendChild(nameDiv); const btn = document.createElement("button"); btn.className = "DX_Shop_Btn"; btn.innerText = "GET"; btn.addEventListener("click", async () => { if (btn.className.includes("loading")) return; btn.className = "DX_Shop_Btn loading"; btn.innerText = "..."; const r = await applyPowerUp(item); if (r.ok) { btn.className = "DX_Shop_Btn got"; btn.innerText = "✓"; notify("success", "Shop", `Successfully applied ${item.name}.`); } else { btn.className = "DX_Shop_Btn fail"; btn.innerText = "ERR"; notify("error", "Shop", r.msg); } setTimeout(() => { btn.className = "DX_Shop_Btn"; btn.innerText = "GET"; }, 2000); }); card.appendChild(btn); fragment.appendChild(card); }); } let currentCategory = null; fItems.forEach((item) => { if (item.category !== currentCategory) { currentCategory = item.category; const secHeader = document.createElement("div"); secHeader.className = "DX_Shop_Section_Header DX_NoSel"; secHeader.innerHTML = ` ${currentCategory} `; fragment.appendChild(secHeader); } const card = document.createElement("div"); card.className = "DX_Shop_Card"; if (item.icon && preloadedShopIcons[item.icon]) { card.appendChild(preloadedShopIcons[item.icon].cloneNode(true)); } const nameDiv = document.createElement("div"); nameDiv.className = "DX_Shop_Name DX_NoSel"; nameDiv.innerText = item.displayName || "Unknown"; card.appendChild(nameDiv); const btn = document.createElement("button"); btn.className = "DX_Shop_Btn"; btn.dataset.id = item.id; btn.innerText = "GET"; btn.addEventListener("click", async () => { if (btn.className.includes("loading")) return; btn.className = "DX_Shop_Btn loading"; btn.innerText = "..."; const result = await applyPowerUp({ id: item.id, refill: /refill/i.test(item.id), }); if (result.ok) { btn.className = "DX_Shop_Btn got"; btn.innerText = "✓"; notify( "success", "Shop", `Successfully acquired ${item.displayName}.`, ); setTimeout(() => { btn.className = "DX_Shop_Btn"; btn.innerText = "GET"; }, 2000); } else { btn.className = "DX_Shop_Btn fail"; btn.innerText = "ERR"; notify("error", "Shop", `${item.displayName}: ${result.msg}`); setTimeout(() => { btn.className = "DX_Shop_Btn"; btn.innerText = "GET"; }, 2000); } }); card.appendChild(btn); fragment.appendChild(card); }); grid.appendChild(fragment); cont.appendChild(grid); } async function getPrivacy() { if (!token || !userId) { return; } try { const res = await fetchApi( "GET", `https://www.duolingo.com/2023-05-23/users/${userId}/privacy-settings?fields=privacySettings`, ); if (res.status !== 200) return; const pData = safeJsonParse(res.responseText, {}); const socSet = pData.privacySettings?.find( (s) => s.id === "disable_social", ); const isPriv = socSet ? socSet.enabled : false; const selEl = document.getElementById("DX_Privacy_Select"); if (selEl) { const tVal = isPriv ? "private" : "public"; selEl.setAttribute("data-value", tVal); selEl.querySelector(".DX_Select_Text").innerText = isPriv ? "Private" : "Public"; selEl.querySelectorAll(".DX_Select_Option").forEach((opt) => { if (opt.getAttribute("data-value") === tVal) { opt.style.color = "rgb(var(--DX-blue))"; opt.style.background = "rgba(var(--DX-blue),0.1)"; } else { opt.style.color = ""; opt.style.background = ""; } }); } } catch {} } async function setPrivacy(modePriv) { if (!token || !userId) { return; } try { const res = await fetchApi( "PATCH", `https://www.duolingo.com/2023-05-23/users/${userId}/privacy-settings?fields=privacySettings`, { DISABLE_SOCIAL: modePriv }, ); if (res.status !== 200) { throw new Error("Privacy update failed"); } notify( "success", "Privacy Updated", `Profile is now ${modePriv ? "Private" : "Public"}.`, ); } catch { notify( "error", "Privacy Settings", "Failed to update profile privacy status.", ); } } async function resolveUser(username) { try { const res = await fetchApi( "GET", `${config.api.users}?username=${encodeURIComponent(username)}`, ); if (res.status === 200) { const data = safeJsonParse(res.responseText, {}); const found = (data.users || [])[0]; return found ? found.id : null; } } catch {} return null; } async function requestEmailVerification() { try { const res = await fetchApi( "POST", `${config.api.users}/${userId}/email-verifications`, {}, ); if (res.status === 200 || res.status === 201 || res.status === 204) { notify( "success", "Verification Email", user.email ? `Sent to ${user.email}. Verify it, then reload.` : "Verification email sent. Verify it, then reload.", ); } else { notify( "error", "Verification Email", `Could not send (status ${res.status}). Verify on Duolingo directly.`, ); } } catch { notify( "error", "Verification Email", "Request failed. Verify on Duolingo directly.", ); } } async function ensureEmailVerified() { if (!user) { notify("error", "Social Tools", "Please connect your account first."); return false; } if (user.emailVerified) return true; notify( "warning", "Email Not Verified", "Click here to verify your email.", requestEmailVerification, ); return false; } async function blockTarget(username, unblock) { const label = unblock ? "Unblock" : "Block"; if (!(await ensureEmailVerified())) return; const target = await resolveUser(username); if (!target) { notify("error", label, `Could not find user "${username}".`); return; } try { const res = await fetchApi( unblock ? "DELETE" : "POST", `${config.api.friends}/users/${userId}/block/${target}`, ); if (res.status === 200 || res.status === 201) { if (unblock) autoBlockCohortKey = null; notify( "success", label, `${unblock ? "Unblocked" : "Blocked"} ${username}.`, ); } else { notify("error", label, `Failed with status ${res.status}.`); } } catch { notify("error", label, "Request failed."); } } async function followTarget(username, unfollow) { const label = unfollow ? "Unfollow" : "Follow"; if (!(await ensureEmailVerified())) return; const target = await resolveUser(username); if (!target) { notify("error", label, `Could not find user "${username}".`); return; } try { const res = await fetchApi( unfollow ? "DELETE" : "POST", `${config.api.friends}/users/${userId}/follow/${target}`, unfollow ? null : { component: "profile_header_button" }, ); if (res.status === 200 || res.status === 201) { notify( "success", label, `${unfollow ? "Unfollowed" : "Followed"} ${username}.`, ); } else { notify("error", label, `Failed with status ${res.status}.`); } } catch { notify("error", label, "Request failed."); } } async function massUnfollow() { if (farmStates.unfollow) { farmStates.unfollow = false; resetBtn("DX_Follow_Btn", "RUN"); return; } if (!(await ensureEmailVerified())) return; farmStates.unfollow = true; stopBtn("DX_Follow_Btn"); let list = []; try { try { const res = await fetchApi( "GET", `${config.api.friends}/users/${userId}/following?viewerId=${userId}`, ); if (res.status === 200) { list = safeJsonParse(res.responseText, {}).following?.users || []; } } catch {} if (list.length === 0) { notify("info", "Mass Unfollow", "You are not following anyone."); return; } let done = 0; let ok = 0; for (const entry of list) { if (!farmStates.unfollow) break; try { const res = await fetchApi( "DELETE", `${config.api.friends}/users/${userId}/follow/${entry.userId}`, ); if (res.status === 200) ok++; } catch {} done++; setProgress("DX_Follow", (done / list.length) * 100); await wait(delayMs); } const completed = farmStates.unfollow; notify( "success", completed ? "Mass Unfollow Complete" : "Mass Unfollow Stopped", `Unfollowed ${ok} user(s).`, ); clearProgress("DX_Follow", completed); } finally { farmStates.unfollow = false; resetBtn("DX_Follow_Btn", "RUN"); } } async function massUnblock() { if (farmStates.unblock) { farmStates.unblock = false; resetBtn("DX_Block_Mass_Btn", "RUN"); return; } if (!(await ensureEmailVerified())) return; farmStates.unblock = true; stopBtn("DX_Block_Mass_Btn"); let list = []; try { try { const res = await fetchApi( "GET", `${config.api.users}/${userId}?fields=blockedUserIds`, ); if (res.status === 200) { list = safeJsonParse(res.responseText, {}).blockedUserIds || []; } } catch {} if (list.length === 0) { notify("info", "Mass Unblock", "You have not blocked anyone."); return; } let done = 0; let ok = 0; for (const blockedId of list) { if (!farmStates.unblock) break; try { const res = await fetchApi( "DELETE", `${config.api.friends}/users/${userId}/block/${blockedId}`, ); if (res.status === 200) ok++; } catch {} done++; setProgress("DX_Block_Mass", (done / list.length) * 100); await wait(delayMs); } const completed = farmStates.unblock; if (ok > 0) autoBlockCohortKey = null; notify( "success", completed ? "Mass Unblock Complete" : "Mass Unblock Stopped", `Unblocked ${ok} user(s).`, ); clearProgress("DX_Block_Mass", completed); } finally { farmStates.unblock = false; resetBtn("DX_Block_Mass_Btn", "RUN"); } } async function getCohortIds(force = true) { const rankings = (await fetchLeagueData(force))?.active?.cohort?.rankings; if (Array.isArray(rankings)) { return rankings .map((r) => r.user_id) .filter((id) => id && String(id) !== String(userId)); } return null; } async function massFollow() { if (farmStates.follow) { farmStates.follow = false; resetBtn("DX_Follow_Btn", "RUN"); return; } if (!(await ensureEmailVerified())) return; farmStates.follow = true; stopBtn("DX_Follow_Btn"); try { const ids = new Set(); const cohort = await getCohortIds(); if (Array.isArray(cohort)) cohort.forEach((id) => ids.add(String(id))); try { const res = await fetchApi( "GET", `${config.api.friends}/users/${userId}/followers?viewerId=${userId}`, ); if (res.status === 200) { (safeJsonParse(res.responseText, {}).followers?.users || []).forEach( (u) => ids.add(String(u.userId)), ); } } catch {} ids.delete(String(userId)); try { const res = await fetchApi( "GET", `${config.api.friends}/users/${userId}/following?viewerId=${userId}`, ); if (res.status === 200) { (safeJsonParse(res.responseText, {}).following?.users || []).forEach( (u) => ids.delete(String(u.userId)), ); } } catch {} const list = [...ids]; if (list.length === 0) { notify("info", "Mass Follow", "No new users to follow."); return; } let done = 0; let ok = 0; for (const id of list) { if (!farmStates.follow) break; try { const res = await fetchApi( "POST", `${config.api.friends}/users/${userId}/follow/${id}`, { component: "profile_header_button" }, ); if (res.status === 200 || res.status === 201) ok++; } catch {} done++; setProgress("DX_Follow", (done / list.length) * 100); await wait(delayMs); } const completed = farmStates.follow; notify( "success", completed ? "Mass Follow Complete" : "Mass Follow Stopped", `Followed ${ok} user(s).`, ); clearProgress("DX_Follow", completed); } finally { farmStates.follow = false; resetBtn("DX_Follow_Btn", "RUN"); } } async function blockLeagueUsers(options = {}) { const isAuto = !!options.auto; const title = isAuto ? "Auto Block League" : "Mass Block"; const cohort = await getCohortIds(false); if (cohort === null) { if (!isAuto) notify("error", title, "No league cohort found. Join a league first."); return { ok: 0, total: 0, skipped: true }; } const cohortKey = cohort.map(String).sort().join(","); if (isAuto && autoBlockCohortKey === cohortKey) { return { ok: 0, total: 0, skipped: true }; } let blocked = []; try { const res = await fetchApi( "GET", `${config.api.users}/${userId}?fields=blockedUserIds`, ); if (res.status === 200) blocked = safeJsonParse(res.responseText, {}).blockedUserIds || []; } catch {} const blockedSet = new Set(blocked.map(String)); const list = cohort.filter((id) => !blockedSet.has(String(id))); if (list.length === 0) { if (!isAuto || autoBlockCohortKey !== cohortKey) { notify("info", title, "Everyone in your league is already blocked."); } autoBlockCohortKey = cohortKey; return { ok: 0, total: 0, skipped: true }; } let done = 0; let ok = 0; for (const id of list) { if (!farmStates.blockmass) break; try { const res = await fetchApi( "POST", `${config.api.friends}/users/${userId}/block/${id}`, ); if (res.status === 200 || res.status === 201) ok++; } catch {} done++; setProgress("DX_Block_Mass", (done / list.length) * 100); await wait(delayMs); } if (ok === list.length) autoBlockCohortKey = cohortKey; return { ok, total: list.length, skipped: false }; } async function massBlock() { if (farmStates.blockmass) { farmStates.blockmass = false; resetBtn("DX_Block_Mass_Btn", "RUN"); return; } if (!(await ensureEmailVerified())) return; farmStates.blockmass = true; stopBtn("DX_Block_Mass_Btn"); try { const result = await blockLeagueUsers(); const completed = farmStates.blockmass; if (!result.skipped) { notify( "success", completed ? "Mass Block Complete" : "Mass Block Stopped", `Blocked ${result.ok} user(s).`, ); } clearProgress("DX_Block_Mass", completed); } finally { farmStates.blockmass = false; resetBtn("DX_Block_Mass_Btn", "RUN"); } } const powerUpItems = [ { id: "streak_repair", name: "Streak Repair", ico: icons.shopIcons.streak }, { id: "society_streak_freeze", name: "Streak Freeze", ico: icons.shopIcons.streak, }, { id: "general_xp_boost", name: "General XP Boost", ico: icons.shopIcons.xp, }, { id: "xp_boost_stackable", name: "XP Boost (Stackable)", ico: icons.shopIcons.xp, }, { id: "xp_boost_15", name: "XP Boost x2 (15m)", ico: icons.shopIcons.xp }, { id: "xp_boost_60", name: "XP Boost x2 (60m)", ico: icons.shopIcons.xp }, { id: "xp_boost_refill", name: "XP Boost x3 (15m)", ico: icons.shopIcons.xp, refill: true, }, { id: "early_bird_xp_boost", name: "Early Bird Boost", ico: icons.shopIcons.xp, }, ]; async function applyPowerUp(item) { if (!token || !userId) return { ok: false, msg: "Login required." }; try { let res; if (item.refill) { const innerBody = { isFree: false, learningLanguage: user.learningLanguage, subscriptionFeatureGroupId: 0, xpBoostSource: "REFILL", xpBoostMinutes: 15, xpBoostMultiplier: 3, id: item.id, }; const batch = { includeHeaders: true, requests: [ { url: `/2023-05-23/users/${userId}/shop-items`, extraHeaders: {}, method: "POST", body: JSON.stringify(innerBody), }, ], }; const refillHeaders = Object.assign({}, headers, { "x-amzn-trace-id": "User=" + userId, }); res = await fetchApi( "POST", "https://ios-api-2.duolingo.com/2023-05-23/batch", batch, refillHeaders, ); } else { res = await fetchApi( "POST", `${config.api.users}/${userId}/shop-items`, { itemName: item.id, isFree: true, consumed: true, fromLanguage: user.fromLanguage, learningLanguage: user.learningLanguage, }, ); } if (res.status === 200 || res.status === 201) return { ok: true }; return { ok: false, msg: `Failed (${res.status}).` }; } catch { return { ok: false, msg: "Request failed." }; } } async function sendGift(username, itemName) { username = (username || "").trim(); const btn = document.getElementById("DX_Gift_Btn"); const lbl = btn ? btn.querySelector(".DX_Sm_Btn_Label") : null; if (!username) { notify("warning", "Send Gift", "Enter a username."); return; } if (!(await ensureEmailVerified())) return; if (btn) btn.disabled = true; if (lbl) lbl.innerText = "..."; const target = await resolveUser(username); if (!target) { notify("error", "Send Gift", `Could not find user "${username}".`); } else { try { const res = await fetchApi( "POST", `${config.api.users}/${userId}/gifts/${target}`, { itemName: itemName }, ); if (res.status === 200 || res.status === 201) { notify("success", "Send Gift", `Gift sent to ${username}.`); } else { notify("error", "Send Gift", `Failed with status ${res.status}.`); } } catch { notify("error", "Send Gift", "Request failed."); } } if (btn) btn.disabled = false; if (lbl) lbl.innerText = "RUN"; } async function forceFriend(username, mode) { username = (username || "").trim(); const isQuest = mode === "quest"; const label = isQuest ? "Friends Quest" : "Friend Streak"; const btn = document.getElementById("DX_Friend_Btn"); const lbl = btn ? btn.querySelector(".DX_Sm_Btn_Label") : null; if (!username) { notify("warning", label, "Enter a username."); return; } if (!(await ensureEmailVerified())) return; if (btn) btn.disabled = true; if (lbl) lbl.innerText = "..."; const displayName = username; const target = await resolveUser(username); if (!target) { notify("error", label, `Could not find user "${username}".`); } else { try { if (isQuest) { const chk = await fetchApi( "GET", `https://www.duolingo.com/users/${userId}/friends-quests/match`, ); let existing = null; if (chk.status === 200) { existing = safeJsonParse(chk.responseText, {}).match || null; } if (existing) { const mu = existing.matchedUser || {}; notify( "warning", label, `Already in a quest with ${mu.name || "someone"}.`, ); } else { const res = await fetchApi( "POST", `https://www.duolingo.com/2017-06-30/friends/users/${userId}/friends-quests/match`, { targetUserId: parseInt(target) }, ); let ok = false; if (res.status === 200) { ok = safeJsonParse(res.responseText, {}).success === true; } if (ok) { notify("success", label, `Quest started with ${displayName}.`); } else { notify( "error", label, `Could not start a quest with ${displayName}.`, ); } } } else { const res = await fetchApi( "POST", `https://www.duolingo.com/2017-06-30/friends/users/${userId}/matches`, { activityName: "friendsStreak", intendedMatches: [{ targetUserIds: [parseInt(target)] }], }, ); let ok = res.status === 200; let failReason = null; if (ok) { try { const d = safeJsonParse(res.responseText, {}); const fStreak = d.friendsStreak || {}; const successList = Array.isArray(fStreak.success) ? fStreak.success : []; const failList = Array.isArray(fStreak.fail) ? fStreak.fail : []; ok = successList.length > 0 && failList.length === 0; if (!ok && failList.length > 0) { failReason = failList[0].failureReason || null; } } catch { ok = false; } } if (ok) { notify("success", label, `Streak started with ${displayName}.`); } else { notify( "error", label, failReason ? `Could not start a streak with ${displayName}: ${failReason}` : `Could not start a streak with ${displayName}.`, ); } } } catch { notify("error", label, "Request failed."); } } if (btn) btn.disabled = false; if (lbl) lbl.innerText = "RUN"; } function scrollToActiveStatus() { const doScroll = () => { const cont = document.getElementById("DX_Status_Container"); if (!cont) return; const activeBtn = cont.querySelector(".DX_Shop_Btn.got"); const card = activeBtn ? activeBtn.closest(".DX_Shop_Card") : null; if (!card || !cont.clientHeight) return; const contRect = cont.getBoundingClientRect(); const cardRect = card.getBoundingClientRect(); const offset = cardRect.top - contRect.top + cont.scrollTop - cont.clientHeight / 2 + cardRect.height / 2; cont.scrollTop = Math.max(0, offset); }; requestAnimationFrame(() => requestAnimationFrame(doScroll)); setTimeout(doScroll, 450); } async function openStatusPicker() { const search = document.getElementById("DX_Status_Search"); if (search) search.value = ""; showStatuses(""); changePage("Status"); currentStatus = await getCurrentStatus(); showStatuses(""); scrollToActiveStatus(); } function updateLeagueBadge(tier) { const url = leagueBadgeUrl(tier); const name = typeof tier === "number" && leagueTierNames[tier] ? leagueTierNames[tier] + " League" : "Leaderboard"; const navIco = document.getElementById("DX_Board_Nav_Ico"); const tierIco = document.getElementById("DX_Board_Tier_Ico"); const tierName = document.getElementById("DX_Board_Tier_Name"); if (navIco) navIco.src = url; if (tierIco) tierIco.src = url; if (tierName) tierName.innerText = name; } function updateProfileLeague(tier, rank) { const wrap = document.getElementById("DX_ULeague_Wrap"); const ico = document.getElementById("DX_ULeague_Ico"); const rk = document.getElementById("DX_ULeague_Rank"); if (!wrap || !ico || !rk) return; if (typeof tier !== "number" || !rank) { wrap.style.display = "none"; return; } ico.src = leagueBadgeUrl(tier); rk.innerText = "#" + rank; wrap.style.display = "flex"; } function makeBoardSeparator(type) { const isProm = type === "prom"; const arrowUrl = isProm ? "https://d35aaqx5ub95lt.cloudfront.net/images/leagues/577cf633b59ce72791f725d0cb973061.svg" : "https://d35aaqx5ub95lt.cloudfront.net/images/leagues/248453c5e2d9de19fba7a2f4fef7f016.svg"; const textStr = isProm ? "Promotion Zone" : "Demotion Zone"; const sep = document.createElement("div"); sep.className = "DX_LB_Sep"; sep.style.cssText = "display: flex; align-items: center; gap: 8px; margin: 6px 2px; height: 14px; align-self: stretch;"; const lineCol = isProm ? "rgba(var(--DX-green), 0.35)" : "rgba(var(--DX-red), 0.35)"; const textCol = isProm ? "rgb(var(--DX-green))" : "rgb(var(--DX-red))"; sep.innerHTML = `Loading...
`; try { const data = preData || (await fetchLeagueData(false)); const cohort = data?.active?.cohort; const contest = data?.active?.contest; const rankings = cohort?.rankings; if (!Array.isArray(rankings) || rankings.length === 0) { if (!loaded) cont.innerHTML = `You are not in an active league.
`; return; } const tier = cohort?.tier; updateLeagueBadge(tier); const nProm = (contest?.ruleset?.num_promoted || [])[tier] || 0; const nDem = (contest?.ruleset?.num_demoted || [])[tier] || 0; const total = rankings.length; const statusMap = {}; statusReactions.forEach((s) => { statusMap[s.value] = s; }); const frag = document.createDocumentFragment(); const setIco = DUO_LEAGUES_CDN + "6df6337370e45c1b9a5029e78211d114.svg"; let myStatus = null; let myRank = null; rankings.forEach((r, i) => { const rank = i + 1; const isMe = String(r.user_id) === String(userId); const av = r.avatar_url ? (r.avatar_url.startsWith("http") ? r.avatar_url : "https:" + r.avatar_url) + "/xlarge" : ""; const st = r.reaction && r.reaction !== "NONE" ? statusMap[r.reaction] : null; if (isMe) { myStatus = st; myRank = rank; } let statusHtml; if (isMe) { const inner = st ? renderStatusIcon(st, 12) : `${(r.score || 0).toLocaleString()} XP
`; frag.appendChild(row); if (isMe) { const myEl = row.querySelector(".DX_Board_MyStatus"); if (myEl) myEl.addEventListener("click", openStatusPicker); } if (rank === nProm && rank < total) { frag.appendChild(makeBoardSeparator("prom")); } if (rank === total - nDem && nDem > 0 && rank > nProm) { frag.appendChild(makeBoardSeparator("dem")); } }); cont.replaceChildren(frag); cont.dataset.loaded = "1"; const barIco = document.getElementById("DX_Board_Status_Ico"); if (barIco) { if (myStatus) { barIco.innerHTML = renderStatusIcon(myStatus, 22); } else { barIco.innerHTML = `Failed to load leaderboard.
`; } } async function removeHearts(count) { const btn = document.getElementById("DX_Hearts_Btn"); if (btn) btn.disabled = true; let ok = 0; for (let i = 0; i < count; i++) { try { const res = await fetchApi( "PUT", `${config.api.users}/${userId}/remove-heart`, ); if (res.status === 200) ok++; } catch {} setProgress("DX_Hearts", ((i + 1) / count) * 100); await wait(delayMs); } notify( ok > 0 ? "success" : "error", "Remove Hearts", ok > 0 ? `Removed ${ok} heart(s).` : "Failed to remove hearts.", ); clearProgress("DX_Hearts", ok > 0); if (btn) btn.disabled = false; } function applyLocalMax() { if (localStorage.getItem("dx_local_max") !== "true") return; if (document.getElementById("DX_LocalMax_Script")) return; const script = document.createElement("script"); script.id = "DX_LocalMax_Script"; script.textContent = ` (function() { const TARGET_URL_REGEX = /https?:\\/\\/(?:[a-zA-Z0-9-]+\\.)?duolingo\\.[a-zA-Z]{2,6}(?:\\.[a-zA-Z]{2})?\\/\\d{4}-\\d{2}-\\d{2}\\/users\\/.+/; const CUSTOM_SHOP_ITEMS = { gold_subscription: { itemName: "gold_subscription", subscriptionInfo: { vendor: "STRIPE", renewing: true, expectedExpiration: Date.now() + 31536000000 } } }; const originalFetch = window.fetch; window.fetch = function (resource, options) { const url = resource instanceof Request ? resource.url : resource; if (TARGET_URL_REGEX.test(url)) { return originalFetch.apply(this, arguments).then(async function (response) { const resp = response.clone(); let raw = await resp.text(); try { let data = JSON.parse(raw); data.hasPlus = true; if (!data.trackingProperties || typeof data.trackingProperties !== 'object') data.trackingProperties = {}; data.trackingProperties.has_item_gold_subscription = true; data.shopItems = CUSTOM_SHOP_ITEMS; raw = JSON.stringify(data); } catch { } let hdrs = response.headers; try { const obj = {}; response.headers.forEach((v, k) => obj[k] = v); hdrs = obj; } catch { } return new Response(raw, { status: response.status, statusText: response.statusText, headers: hdrs }); }); } return originalFetch.apply(this, arguments); }; const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...args) { this._intercept = TARGET_URL_REGEX.test(url); this._url = url; origOpen.call(this, method, url, ...args); }; XMLHttpRequest.prototype.send = function () { if (this._intercept) { const origChange = this.onreadystatechange; const xhr = this; this.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { try { let raw = xhr.responseText; try { let data = JSON.parse(raw); data.hasPlus = true; if (!data.trackingProperties || typeof data.trackingProperties !== 'object') data.trackingProperties = {}; data.trackingProperties.has_item_gold_subscription = true; data.shopItems = CUSTOM_SHOP_ITEMS; raw = JSON.stringify(data); } catch { } Object.defineProperty(xhr, 'responseText', { writable: true, value: raw }); Object.defineProperty(xhr, 'response', { writable: true, value: raw }); } catch { } } if (origChange) origChange.apply(this, arguments); }; } origSend.apply(this, arguments); }; function remove(root = document) { const sections = root.querySelectorAll('section._3f-te'); for (let i = 0; i < sections.length; i++) { const h2 = sections[i].querySelector('h2._203-l'); if (h2 && h2.textContent.trim() === 'Manage subscription') { sections[i].remove(); break; } } } const observer = new MutationObserver(function () { remove(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); remove(); })(); `; document.documentElement.appendChild(script); } async function getCohort() { return (await fetchLeagueData(false))?.active?.cohort?.cohort_id || null; } async function getCurrentStatus() { const rankings = (await fetchLeagueData(true))?.active?.cohort?.rankings || []; const me = rankings.find((r) => r.user_id == userId); return me ? me.reaction || "NONE" : null; } async function setStatus(reaction) { if (!userId) return false; const cohort = await getCohort(); if (!cohort) { notify("error", "Leaderboard Status", "You are not in an active league."); return false; } try { const res = await fetchApi( "PATCH", `https://duolingo-leaderboards-prod.duolingo.com/reactions/${cohort}/users/${userId}`, { reaction: reaction }, ); if (res.status === 200) { notify("success", "Leaderboard Status", "Status successfully updated."); const me = leagueDataCache?.active?.cohort?.rankings?.find( (r) => String(r.user_id) === String(userId), ); if (me) me.reaction = reaction; leagueDataTs = 0; return true; } notify( "error", "Leaderboard Status", `Failed to update status (Status ${res.status}).`, ); return false; } catch { notify("error", "Leaderboard Status", "Network error: Request failed."); return false; } } function renderStatusIcon(st, size = 12) { if (!st) return ""; if (st.icon) { if (st.icon.startsWith("