NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name DuoGang
// @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @namespace https://openuserjs.org/users/duogang
// @version 2.1.23
// @description Duolingo automation - Auto-solve lessons, farm XP, Gems, Streaks
// @author DuoGangTeam
// @match *://*.duolingo.com/*
// @match *://*.duolingo.cn/*
// @icon data:image/png;base64,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
// @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 fonts.googleapis.com
// @connect openuserjs.org
// @connect raw.githubusercontent.com
// @run-at document-end
// @downloadURL https://openuserjs.org/src/scripts/duogang/DuoGang.user.js
// @updateURL https://openuserjs.org/meta/duogang/DuoGang.meta.js
// ==/UserScript==
(function() {
'use strict';
const fontLink = document.createElement('link');
fontLink.rel = 'stylesheet';
fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap';
document.head.appendChild(fontLink);
GM_addStyle(`
:root {
--DG-gold: 255, 179, 0;
--DG-gold-dark: 204, 143, 0;
--DG-blue: 0, 122, 255;
--DG-green: 52, 199, 89;
--DG-red: 255, 59, 48;
--DG-purple: 175, 82, 222;
--DG-orange: 255, 149, 0;
--DG-pink: 255, 45, 85;
--DG-indigo: 88, 86, 214;
--DG-teal: 0, 199, 190;
--DG-cyan: 90, 200, 250;
--DG-yellow: 255, 204, 0;
--DG-bg: rgba(255,255,255,0.92);
--DG-surface: rgba(248,248,248,0.85);
--DG-text: rgb(60, 60, 60);
--DG-text-sub: rgba(60, 60, 60, 0.55);
--DG-divider: rgba(0,0,0,0.08);
--DG-outline: rgba(0,0,0,0.08);
--DG-input-bg: rgba(255, 179, 0, 0.08);
--DG-input-outline: rgba(255, 179, 0, 0.25);
--DG-notif-bg: rgba(255,255,255,0.92);
--DG-shadow: rgba(0,0,0,0.10);
--DG-corner-s: superellipse(1.32);
--DG-corner-r-s: 4px;
--DG-corner-r-m: 8px;
--DG-corner-r-ml: 12px;
--DG-corner-r-l: 16px;
--DG-corner-r-xl: 20px;
--DG-accent: var(--DG-gold);
--DG-accent-dark: var(--DG-gold-dark);
}
@supports (corner-shape: superellipse(1.32)) {
:root {
--DG-corner-r-s: 6px;
--DG-corner-r-m: 10px;
--DG-corner-r-ml: 16px;
--DG-corner-r-l: 20px;
--DG-corner-r-xl: 26px;
}
}
#DG_Root.DG_Dark {
--DG-bg: rgba(22,22,28,0.97);
--DG-surface: rgba(30,30,38,0.95);
--DG-text: rgb(240, 240, 240);
--DG-text-sub: rgba(240, 240, 240, 0.55);
--DG-divider: rgba(255,255,255,0.08);
--DG-outline: rgba(255,255,255,0.08);
--DG-input-bg: rgba(255, 179, 0, 0.08);
--DG-input-outline: rgba(255, 179, 0, 0.28);
--DG-notif-bg: rgba(28,28,36,0.98);
--DG-shadow: rgba(0,0,0,0.45);
}
#DG_Root * {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
}
#DG_Root p, #DG_Root span, #DG_Root button, #DG_Root input, #DG_Root label, #DG_Root div {
font-family: 'Inter', 'din-round', -apple-system, BlinkMacSystemFont, sans-serif !important;
}
#DG_Root p, #DG_Root span { margin: 0; padding: 0; }
.DG_Main {
display: inline-flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
gap: 8px;
position: fixed;
right: 16px;
bottom: 16px;
z-index: 2147483647;
transition: bottom 0.8s cubic-bezier(0.16, 1, 0.32, 1), right 0.8s cubic-bezier(0.16, 1, 0.32, 1);
user-select: none;
}
.DG_Main.DG_Dragging { transition: none !important; }
@media (max-width: 699px) {
.DG_Main {
margin-bottom: 80px;
}
}
.DG_Main_Box {
display: flex;
width: 320px;
padding: 16px;
flex-direction: column;
gap: 10px;
border-radius: var(--DG-corner-r-xl);
corner-shape: var(--DG-corner-s);
outline: 2px solid var(--DG-outline);
outline-offset: -2px;
background: var(--DG-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px var(--DG-shadow);
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.32, 1),
filter 0.8s cubic-bezier(0.16, 1, 0.32, 1),
width 0.8s cubic-bezier(0.16, 1, 0.32, 1);
max-height: 80vh;
overflow-y: auto;
overflow-x: hidden;
}
.DG_HStack { display:flex; align-items:center; gap:8px; align-self:stretch; }
.DG_HStack_4 { display:flex; align-items:center; gap:4px; align-self:stretch; }
.DG_HStack_6 { display:flex; align-items:center; gap:6px; align-self:stretch; }
.DG_HStack_Between { display:flex; align-items:center; justify-content:space-between; align-self:stretch; }
.DG_HStack_Auto_Top {
display: flex;
align-items: flex-start;
justify-content: space-between;
align-self: stretch;
}
.DG_VStack { display:flex; flex-direction:column; gap:8px; align-self:stretch; }
.DG_VStack_4 { display:flex; flex-direction:column; gap:4px; align-self:stretch; }
.DG_VStack_6 { display:flex; flex-direction:column; gap:6px; align-self:stretch; }
.DG_VStack_0 { display:flex; flex-direction:column; gap:0; align-self:stretch; align-items:flex-start; }
.DG_Divider { height:1px; background:var(--DG-divider); align-self:stretch; flex-shrink:0; }
.DG_Title {
font-size: 16px;
font-weight: 600;
color: var(--DG-text);
line-height: normal;
margin: 0;
}
.DG_Title_Large {
font-size: 22px;
font-weight: 700;
color: var(--DG-text);
line-height: normal;
margin: 0;
}
.DG_Subtitle {
font-size: 15px;
font-weight: 500;
color: var(--DG-text-sub);
line-height: normal;
margin: 0;
}
.DG_Btn {
display:flex;
height: 42px;
padding: 10px 14px 10px 12px;
align-items:center;
gap: 6px;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: filter 0.4s cubic-bezier(0.16, 1, 0.32, 1),
transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
user-select: none;
-webkit-user-select: none;
flex: 1 0 0;
}
.DG_Btn:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_Btn:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Btn_Primary {
background: rgb(var(--DG-accent));
color: #1a1a1a;
outline: 2px solid rgba(0, 0, 0, 0.18);
outline-offset: -2px;
}
.DG_Btn_Blue {
background: linear-gradient(0deg, rgba(var(--DG-accent), 0.12) 0%, rgba(var(--DG-accent), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-accent), 0.22);
outline-offset: -2px;
color: rgb(var(--DG-accent-dark));
}
.DG_Btn_Green {
background: rgb(var(--DG-green));
color: #FFF;
outline: 2px solid rgba(0, 0, 0, 0.18);
outline-offset: -2px;
}
.DG_Btn_Red {
background: rgb(var(--DG-red));
color: #FFF;
outline: 2px solid rgba(0, 0, 0, 0.18);
outline-offset: -2px;
}
.DG_Btn_Ghost {
background: linear-gradient(0deg, rgba(var(--DG-accent), 0.12) 0%, rgba(var(--DG-accent), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-accent), 0.22);
outline-offset: -2px;
color: rgb(var(--DG-accent-dark));
}
.DG_Btn_Icon { flex: none; width: 42px; padding: 10px; justify-content: center; }
.DG_Input_Wrap {
display:flex;
height: 48px;
padding: 16px;
align-items:center;
gap: 6px;
flex: 1;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
outline: 2px solid var(--DG-input-outline);
outline-offset: -2px;
background: var(--DG-input-bg);
}
.DG_Input {
border: none;
outline: none;
background: none;
font-size: 16px;
font-weight: 600;
color: rgb(var(--DG-accent-dark));
text-align: right;
width: 100%;
font-family: inherit;
-moz-appearance: textfield;
}
.DG_Input::placeholder { color: rgba(var(--DG-accent-dark), 0.40); }
.DG_Input::-webkit-outer-spin-button,
.DG_Input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.DG_Input_Btn {
display:flex;
height: 48px;
padding: 12px 14px 12px 16px;
align-items:center;
gap: 6px;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
background: rgb(var(--DG-accent));
outline: 2px solid rgba(0, 0, 0, 0.16);
outline-offset: -2px;
transition: filter 0.4s cubic-bezier(0.16, 1, 0.32, 1),
transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
white-space: nowrap;
flex-shrink: 0;
}
.DG_Input_Btn:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_Input_Btn:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Input_Btn:disabled { opacity: 0.5; pointer-events: none; }
.DG_Input_Btn.Running {
background: linear-gradient(0deg, rgba(var(--DG-red), 0.12) 0%, rgba(var(--DG-red), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-red), 0.22);
color: rgb(var(--DG-red));
}
.DG_Input_Btn.Done {
background: linear-gradient(0deg, rgba(var(--DG-green), 0.12) 0%, rgba(var(--DG-green), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-green), 0.22);
color: rgb(var(--DG-green));
}
.DG_Prog_Wrap { height: 0; border-radius: 2px; background: rgba(var(--DG-accent), 0.10); overflow: hidden; transition: height 0.4s; }
.DG_Prog_Wrap.on { height: 4px; }
.DG_Prog_Fill { height: 100%; border-radius: 2px; background: linear-gradient(90deg, rgb(var(--DG-accent)), rgb(var(--DG-accent-dark))); width: 0%; transition: width 0.5s; }
.DG_Page { display: none; flex-direction: column; gap: 10px; }
.DG_Page.active { display: flex; }
.DG_Notif {
display: flex;
justify-content: center;
align-items: center;
transition: 0.8s cubic-bezier(0.16, 1, 0.32, 1);
width: 300px;
position: fixed;
left: calc(50% - (300px / 2));
z-index: 2147483646;
bottom: 16px;
border-radius: var(--DG-corner-r-l);
corner-shape: var(--DG-corner-s);
}
.DG_Notif_Box {
display: flex;
width: 300px;
padding: 16px;
flex-direction: column;
gap: 4px;
border-radius: var(--DG-corner-r-l);
corner-shape: var(--DG-corner-s);
outline: 2px solid var(--DG-outline);
outline-offset: -2px;
background: var(--DG-notif-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 8px 32px var(--DG-shadow);
transition: 0.8s cubic-bezier(0.16, 1, 0.32, 1);
filter: blur(16px);
opacity: 0;
}
.DG_Notif_Box.show {
filter: blur(0px);
opacity: 1;
}
.DG_User_Row { display: none; gap: 10px; }
.DG_Avatar {
width: 22px;
height: 22px;
border-radius: var(--DG-corner-r-l);
corner-shape: var(--DG-corner-s);
background: rgba(var(--DG-accent), 0.12);
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
outline: 2px solid rgba(0, 0, 0, 0.16);
outline-offset: -2px;
}
.DG_Stat { display: flex; align-items: center; gap: 4px; }
.DG_Stat img { width: 16px; height: 16px; }
.DG_Stat span { font-size: 15px; font-weight: 600; color: var(--DG-text-sub); }
.DG_Drag_Handle { cursor: grab; }
.DG_Drag_Handle:active { cursor: grabbing; }
.DG_Header_Btns {
display: flex;
gap: 8px;
align-self: flex-end;
}
.DG_Header_Btns .DG_Hideable {
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.32, 1),
filter 0.8s cubic-bezier(0.16, 1, 0.32, 1);
}
.DG_Header_Btns .DG_Hideable.DG_Header_Hidden {
opacity: 0;
filter: blur(8px);
pointer-events: none;
}
.DG_YT_Btn {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
padding: 0;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
border: none;
cursor: pointer;
text-decoration: none;
background: linear-gradient(0deg, rgba(var(--DG-red), 0.10) 0%, rgba(var(--DG-red), 0.10) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-red), 0.18);
outline-offset: -2px;
transition: filter 0.4s cubic-bezier(0.16, 1, 0.32, 1), transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
flex-shrink: 0;
color: rgb(var(--DG-red));
}
#DG_Root.DG_Dark .DG_YT_Btn {
background: rgba(255, 45, 85, 0.12);
color: rgb(255, 69, 95);
outline-color: rgba(255, 69, 95, 0.22);
}
.DG_YT_Btn:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_YT_Btn:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_DarkToggle {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
border: none;
cursor: pointer;
background: linear-gradient(0deg, rgba(var(--DG-accent), 0.12) 0%, rgba(var(--DG-accent), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-accent), 0.22);
outline-offset: -2px;
transition: filter 0.4s cubic-bezier(0.16, 1, 0.32, 1), transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
flex-shrink: 0;
color: rgb(var(--DG-accent-dark));
}
.DG_DarkToggle:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_DarkToggle:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Hide_Btn {
display: flex;
height: 42px;
padding: 10px 14px 10px 12px;
align-items: center;
gap: 6px;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
cursor: pointer;
background: linear-gradient(0deg, rgba(var(--DG-accent), 0.12) 0%, rgba(var(--DG-accent), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80);
backdrop-filter: blur(16px);
outline: 2px solid rgba(var(--DG-accent), 0.22);
outline-offset: -2px;
transition: filter 0.4s cubic-bezier(0.16, 1, 0.32, 1), transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
user-select: none;
-webkit-user-select: none;
flex-shrink: 0;
}
.DG_Hide_Btn:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_Hide_Btn:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Hide_Btn svg { flex-shrink: 0; }
.DG_Eye_Open { display: block; }
.DG_Eye_Closed { display: none; }
.DG_Hide_Btn_Hidden .DG_Eye_Open { display: none; }
.DG_Hide_Btn_Hidden .DG_Eye_Closed { display: block; }
.DG_Solver_Btn {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 2147483647;
display: flex;
gap: 20px;
animation: DG_SlideUp 0.3s ease-out;
}
@keyframes DG_SlideUp {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.DG_Solver_Btn button {
position: relative;
min-width: 100px;
font-size: 17px;
border: none;
border-bottom: 4px solid rgba(0,0,0,0.2);
border-radius: 16px;
padding: 13px 16px;
transition: filter .0s;
font-weight: 700;
letter-spacing: .8px;
cursor: pointer;
user-select: none;
}
.DG_Solver_Btn button:hover { filter: brightness(1.1); }
.DG_Solver_Btn button:active { border-bottom: 0; margin-bottom: 4px; top: 4px; }
.DG_Solve_One { background: rgb(var(--DG-accent)) !important; color: #1a1a1a !important; }
.DG_Solve_All { background: rgb(var(--DG-blue)) !important; border-bottom-color: #1b5fb8 !important; color: #FFF !important; }
.DG_Toggle {
position: relative;
width: 48px;
height: 30px;
flex-shrink: 0;
cursor: pointer;
border-radius: 16px;
outline: 2px solid rgba(0, 0, 0, 0.18);
outline-offset: -2px;
transition: transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
}
.DG_Toggle input { opacity: 0; width: 0; height: 0; }
.DG_Toggle_Slider { position: absolute; inset: 0; background: rgba(0,0,0,0.08); border-radius: 16px; transition: 0.4s cubic-bezier(0.16, 1, 0.32, 1); }
.DG_Toggle_Slider:before {
content: '';
position: absolute;
width: 22px;
height: 22px;
left: 4px;
top: 4px;
background: #FFF;
border-radius: 50%;
transition: 0.4s cubic-bezier(0.16, 1, 0.32, 1);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.DG_Toggle input:checked + .DG_Toggle_Slider { background: rgb(var(--DG-green)); }
.DG_Toggle input:checked + .DG_Toggle_Slider:before { transform: translateX(18px); }
.DG_Shop_Grid {
display: grid;
grid-template-columns: repeat(2,1fr);
gap: 8px;
align-self: stretch;
}
.DG_Shop_Card {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 10px;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
outline: 2px solid var(--DG-divider);
outline-offset: -2px;
background: var(--DG-surface);
transition: outline-color 0.4s, background 0.4s;
text-align: center;
}
.DG_Shop_Card:hover { outline-color: rgba(var(--DG-accent), 0.35); background: rgba(var(--DG-accent), 0.05); }
.DG_Shop_Ico { width: 36px; height: 36px; object-fit: contain; }
.DG_Shop_Name { font-size: 14px; font-weight: 600; color: var(--DG-text); line-height: 1.3; flex: 1; }
.DG_Shop_Btn {
width: 100%;
height: 30px;
border-radius: var(--DG-corner-r-s);
corner-shape: var(--DG-corner-s);
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 700;
color: #1a1a1a;
background: rgb(var(--DG-accent));
outline: 2px solid rgba(0, 0, 0, 0.16);
outline-offset: -2px;
transition: filter 0.4s, transform 0.4s, background 0.4s;
}
.DG_Shop_Btn:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_Shop_Btn:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Shop_Btn.got { background: rgba(var(--DG-green), 0.12); color: rgb(var(--DG-green)); outline-color: rgba(var(--DG-green), 0.25); pointer-events: none; }
.DG_Shop_Btn.fail { background: rgba(var(--DG-red), 0.10); color: rgb(var(--DG-red)); outline-color: rgba(var(--DG-red), 0.22); pointer-events: none; }
.DG_Scroll_Inner {
align-self: stretch;
overflow-y: auto;
max-height: 240px;
display: flex;
flex-direction: column;
gap: 6px;
padding-right: 2px;
}
.DG_Scroll_Inner::-webkit-scrollbar { width: 3px; }
.DG_Scroll_Inner::-webkit-scrollbar-thumb { background: rgba(var(--DG-accent), 0.25); border-radius: 3px; }
.DG_Acc_Card {
display: flex;
align-items: center;
gap: 10px;
align-self: stretch;
padding: 10px 12px;
border-radius: var(--DG-corner-r-m);
corner-shape: var(--DG-corner-s);
outline: 2px solid var(--DG-divider);
outline-offset: -2px;
background: var(--DG-surface);
transition: outline-color 0.4s, background 0.4s;
}
.DG_Acc_Card:hover { outline-color: rgba(var(--DG-accent), 0.35); background: rgba(var(--DG-accent), 0.05); }
.DG_Acc_Avatar {
width: 22px;
height: 22px;
border-radius: var(--DG-corner-r-l);
corner-shape: var(--DG-corner-s);
flex-shrink: 0;
background: rgba(var(--DG-accent), 0.12);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
outline: 2px solid rgba(0, 0, 0, 0.16);
outline-offset: -2px;
}
.DG_Acc_Info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.DG_Acc_Name { font-size: 15px; font-weight: 600; color: var(--DG-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.DG_Acc_Sub { font-size: 14px; font-weight: 500; color: var(--DG-text-sub); }
.DG_Acc_Sub.active { color: rgb(var(--DG-green)); display: flex; align-items: center; gap: 4px; }
.DG_Acc_Btn {
height: 28px;
padding: 0 10px;
border-radius: var(--DG-corner-r-s);
corner-shape: var(--DG-corner-s);
border: none;
cursor: pointer;
font-size: 12px;
font-weight: 700;
color: #1a1a1a;
background: rgb(var(--DG-accent));
outline: 2px solid rgba(0, 0, 0, 0.16);
outline-offset: -2px;
transition: filter 0.4s, transform 0.4s;
}
.DG_Acc_Btn:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_Acc_Btn:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Acc_Btn.del { background: rgba(var(--DG-red), 0.10); color: rgb(var(--DG-red)); outline-color: rgba(var(--DG-red), 0.20); }
.DG_Acc_Btn.del:hover { background: rgb(var(--DG-red)); color: #FFF; }
.DG_Magnetic_Hover_1 {
transition: filter 0.4s cubic-bezier(0.16, 1, 0.32, 1), transform 0.4s cubic-bezier(0.16, 1, 0.32, 1);
cursor: pointer;
}
.DG_Magnetic_Hover_1:hover { filter: brightness(0.9); transform: scale(1.04); }
.DG_Magnetic_Hover_1:active { filter: brightness(0.85); transform: scale(0.95); }
.DG_Settings_Toggle_Row {
display: flex;
justify-content: center;
align-items: center;
align-self: stretch;
gap: 8px;
}
.DG_Settings_Toggle_Info {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1 0 0;
gap: 0;
}
@keyframes DG_Rotate_360 {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes DG_Pulse_Opacity {
0% { opacity: 1; }
16.66666666% { opacity: 0.75; }
33.33333333% { opacity: 1; }
100% { opacity: 1; }
}
@keyframes DG_Outdated_Pulse {
0% { box-shadow: 0 0 0 0 rgba(255,140,0,0.55), 0 0 0 0 rgba(255,140,0,0.3); background: rgba(255,140,0,0.18); }
50% { box-shadow: 0 0 0 8px rgba(255,140,0,0.18), 0 0 0 16px rgba(255,140,0,0.07); background: rgba(255,140,0,0.32); }
100% { box-shadow: 0 0 0 0 rgba(255,140,0,0), 0 0 0 0 rgba(255,140,0,0); background: rgba(255,140,0,0.18); }
}
@keyframes DG_Outdated_Icon_Bounce {
0%, 100% { transform: translateY(0); }
30% { transform: translateY(-3px); }
60% { transform: translateY(1px); }
}
#DG_Conn_Btn.DG_Outdated_Btn {
animation: DG_Outdated_Pulse 2s ease-in-out infinite !important;
position: relative;
}
#DG_Conn_Btn.DG_Outdated_Btn::after {
content: '⬆ Click to update';
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: rgba(30,20,0,0.92);
color: rgb(255,180,40);
font-size: 12px;
font-weight: 600;
padding: 5px 10px;
border-radius: 8px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
border: 1px solid rgba(255,140,0,0.35);
z-index: 9999;
}
#DG_Conn_Btn.DG_Outdated_Btn:hover::after {
opacity: 1;
}
#DG_Conn_Btn.DG_Outdated_Btn #DG_Conn_Ico {
animation: DG_Outdated_Icon_Bounce 2s ease-in-out infinite;
}
@keyframes slideRight {
0% { transform: translateX(-150px); }
20% { transform: translateX(200px); }
100% { transform: translateX(200px); }
}
`);
const wrap = document.createElement('div');
wrap.id = 'DG_Root';
wrap.innerHTML = `
<div class="DG_Notif" id="DG_Notif">
<div class="DG_Notif_Box" id="DG_Notif_Box">
<div class="DG_HStack_4" style="align-items: center;">
<p class="DG_Title DG_Inset_Icon_1_ID"></p>
<p class="DG_Title DG_Inset_Text_1_ID" style="flex: 1 0 0;"></p>
<svg class="DG_Magnetic_Hover_1 DG_Inset_Icon_2_ID" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="cursor: pointer; flex-shrink: 0; color: var(--DG-text-sub);">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</div>
<p class="DG_Subtitle DG_Inset_Text_2_ID" style="align-self: stretch; overflow-wrap: break-word;"></p>
</div>
</div>
<div class="DG_Main" id="DG_Main">
<div class="DG_Header_Btns" id="DG_Header_Btns">
<a class="DG_YT_Btn DG_Magnetic_Hover_1 DG_Hideable" href="https://www.youtube.com/@JULSEXE_Edits/shorts?app=desktop" target="_blank" rel="noopener noreferrer" title="Follow us on YouTube">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</a>
<button class="DG_DarkToggle DG_Magnetic_Hover_1 DG_Hideable" id="DG_Dark_Btn" title="Toggle dark/light mode">
<svg id="DG_Dark_Icon_Moon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<svg id="DG_Dark_Icon_Sun" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
</button>
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1 DG_Hideable" id="DG_Settings_Btn" style="flex: none; width: 42px; justify-content: center; padding: 10px;" title="Settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</button>
<button class="DG_Hide_Btn DG_Drag_Handle" id="DG_Hide_Btn" title="Drag to move · Click to show/hide">
<svg class="DG_Eye_Open" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
</svg>
<svg class="DG_Eye_Closed" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/>
</svg>
<span class="DG_Title" id="DG_Hide_Label" style="font-size: 14px;">Hide</span>
</button>
</div>
<div class="DG_Main_Box" id="DG_Main_Box">
<!-- PAGE 1: Main -->
<div class="DG_Page active" id="DG_Page_Main">
<div class="DG_HStack">
<div class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_Conn_Btn" style="flex: 1;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" id="DG_Conn_Ico">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
<span class="DG_Title" id="DG_Conn_Txt" style="color: inherit;">Connecting</span>
</div>
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_AccMgr_Btn" style="flex: none; width: 42px; justify-content: center; padding: 10px;" title="Account Manager">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
</button>
</div>
<div class="DG_User_Row DG_HStack" id="DG_User_Row">
<div class="DG_Avatar" id="DG_Avatar">?</div>
<div style="flex:1;min-width:0;">
<p class="DG_Title" id="DG_UName" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></p>
<div class="DG_HStack" style="gap:12px;">
<div class="DG_Stat"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg"><span id="DG_UXP">0</span></div>
<div class="DG_Stat"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg"><span id="DG_UGems">0</span></div>
<div class="DG_Stat"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg"><span id="DG_UStreak">0</span></div>
</div>
</div>
</div>
<div class="DG_Divider"></div>
<!-- XP Farm -->
<div class="DG_VStack">
<div class="DG_HStack_4">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/goals/2b5a211d830a24fab92e291d50f65d1d.svg" width="18" height="18" style="flex-shrink: 0;">
<p class="DG_Title">XP to farm</p>
</div>
<div class="DG_HStack">
<div class="DG_Input_Wrap">
<input type="number" class="DG_Input" id="DG_XP_Input" placeholder="0" min="30" max="500000">
</div>
<button class="DG_Input_Btn DG_Magnetic_Hover_1" id="DG_XP_Btn" disabled>
<span>GET XP</span>
</button>
</div>
<div class="DG_Prog_Wrap" id="DG_XP_Prog"><div class="DG_Prog_Fill" id="DG_XP_Fill"></div></div>
</div>
<!-- Gems Farm -->
<div class="DG_VStack">
<div class="DG_HStack_4">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" width="18" height="18" style="flex-shrink: 0;">
<p class="DG_Title">Farm Gems</p>
</div>
<div class="DG_HStack">
<div class="DG_Input_Wrap" style="position: relative; overflow: hidden;">
<svg width="120" height="48" viewBox="0 0 120 48" fill="none" xmlns="http://www.w3.org/2000/svg" style="position: absolute; pointer-events: none; transform: translateX(-150px); animation: slideRight 4s ease-in-out forwards infinite; animation-delay: 2s;">
<path opacity="0.5" d="M72 0H96L72 48H48L72 0Z" fill="rgb(var(--DG-accent))"/>
<path opacity="0.5" d="M24 0H60L36 48H0L24 0Z" fill="rgb(var(--DG-accent))"/>
<path opacity="0.5" d="M108 0H120L96 48H84L108 0Z" fill="rgb(var(--DG-accent))"/>
</svg>
<input type="number" class="DG_Input" id="DG_Gem_Input" placeholder="0" readonly>
</div>
<button class="DG_Input_Btn DG_Magnetic_Hover_1" id="DG_Gem_Btn" disabled>
<span>RUN</span>
</button>
</div>
</div>
<!-- Streak Farm -->
<div class="DG_VStack">
<div class="DG_HStack_4">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg" width="18" height="18" style="flex-shrink: 0;">
<p class="DG_Title">Streak days to add</p>
</div>
<div class="DG_HStack">
<div class="DG_Input_Wrap">
<input type="number" class="DG_Input" id="DG_Streak_Input" placeholder="0" min="1" max="3650">
</div>
<button class="DG_Input_Btn DG_Magnetic_Hover_1" id="DG_Streak_Btn" disabled>
<span>RUN</span>
</button>
</div>
<div class="DG_Prog_Wrap" id="DG_Streak_Prog"><div class="DG_Prog_Fill" id="DG_Streak_Fill"></div></div>
</div>
<div class="DG_Divider"></div>
<!-- Extra Features -->
<div class="DG_HStack_Between">
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_Practice_Btn" style="flex:1;justify-content:center;" disabled>
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/goals/2b5a211d830a24fab92e291d50f65d1d.svg" width="15" height="15">
Practice Farm
</button>
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_Super_Btn" style="flex:1;justify-content:center;" disabled>
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/43ae4670b321ea3e5807b2a983864d18.svg" width="15" height="15">
Get Duo Max
</button>
</div>
<div class="DG_HStack_Between">
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_Shop_Btn" style="flex:1;justify-content:center;" disabled>
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/0e58a94dda219766d98c7796b910beee.svg" width="15" height="15">
Shop Items
</button>
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_League_Btn" style="flex:1;justify-content:center;" disabled>
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/ca9178510134b4b0893dbac30b6670aa.svg" width="15" height="15">
Auto League #1
</button>
</div>
<div class="DG_HStack_Between">
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_Quest_Btn" style="flex:1;justify-content:center;" disabled>
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/7ef36bae3f9d68fc763d3451b5167836.svg" width="15" height="15">
Daily Quests
</button>
<button class="DG_Btn DG_Btn_Ghost DG_Magnetic_Hover_1" id="DG_MonthlyQuest_Btn" style="flex:1;justify-content:center;" disabled>
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/streakSociety/6f7448890a01e473d141f94fea3fceb9.svg" width="15" height="15">
Monthly Quests
</button>
</div>
<div class="DG_HStack_Between">
<div class="DG_HStack_4">
<span class="DG_Subtitle">Auto Solver</span>
<label class="DG_Toggle">
<input type="checkbox" id="DG_Solver_Toggle">
<span class="DG_Toggle_Slider"></span>
</label>
</div>
<span class="DG_Subtitle">DuoGang v2.1.23</span>
</div>
</div>
<!-- PAGE 2: Settings -->
<div class="DG_Page" id="DG_Page_Settings">
<div class="DG_HStack_4 DG_Magnetic_Hover_1" id="DG_Settings_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--DG-text)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<p class="DG_Title">Back</p>
</div>
<p class="DG_Title_Large" style="align-self:stretch;">Settings</p>
<div class="DG_Divider"></div>
<div class="DG_Scroll_Inner" style="max-height: 350px; width: 100%; gap: 8px;">
<div class="DG_Settings_Toggle_Row" id="DG_Settings_Show_Solve_Buttons">
<div class="DG_Settings_Toggle_Info">
<p class="DG_Title">Show Solve Buttons</p>
<p class="DG_Subtitle">In lessons and practices, see the solve and solve all buttons.</p>
</div>
<label class="DG_Toggle">
<input type="checkbox" id="DG_Settings_Solve_Toggle">
<span class="DG_Toggle_Slider"></span>
</label>
</div>
<div class="DG_Settings_Toggle_Row" id="DG_Settings_Mute_Lessons">
<div class="DG_Settings_Toggle_Info">
<p class="DG_Title">Mute Lessons</p>
<p class="DG_Subtitle">Automatically mute lesson audio when auto-solving.</p>
</div>
<label class="DG_Toggle">
<input type="checkbox" id="DG_Settings_Mute_Toggle">
<span class="DG_Toggle_Slider"></span>
</label>
</div>
<div class="DG_Settings_Toggle_Row" id="DG_Settings_Free_Super">
<div class="DG_Settings_Toggle_Info">
<p class="DG_Title">Free Duo Max</p>
<p class="DG_Subtitle">Enable unlimited hearts and legendary access on-client. Reload required.</p>
</div>
<label class="DG_Toggle">
<input type="checkbox" id="DG_Settings_Super_Toggle">
<span class="DG_Toggle_Slider"></span>
</label>
</div>
<div class="DG_Settings_Toggle_Row" id="DG_Settings_Reduce_Effects">
<div class="DG_Settings_Toggle_Info">
<p class="DG_Title">Reduce Visual Effects</p>
<p class="DG_Subtitle">Disable confetti and other visual animations.</p>
</div>
<label class="DG_Toggle">
<input type="checkbox" id="DG_Settings_Effects_Toggle">
<span class="DG_Toggle_Slider"></span>
</label>
</div>
</div>
<button class="DG_Btn DG_Btn_Primary DG_Magnetic_Hover_1" id="DG_Settings_Save_Btn" style="width:100%; height:44px; justify-content:center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>
</svg>
SAVE & APPLY
</button>
</div>
<div class="DG_Page" id="DG_Page_Shop">
<div class="DG_HStack_4 DG_Magnetic_Hover_1" id="DG_Shop_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--DG-text)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<p class="DG_Title">Back</p>
</div>
<p class="DG_Title_Large" style="align-self:stretch;">Shop Items</p>
<div class="DG_Scroll_Inner" id="DG_Shop_Container" style="max-height:300px;">
<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">Loading shop...</p>
</div>
</div>
<div class="DG_Page" id="DG_Page_Accounts">
<div class="DG_HStack_4 DG_Magnetic_Hover_1" id="DG_AccMgr_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--DG-text)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<p class="DG_Title">Back</p>
</div>
<div class="DG_HStack_Between">
<p class="DG_Title_Large">Account Manager</p>
<button class="DG_Btn DG_Btn_Primary DG_Magnetic_Hover_1" id="DG_AccSave_Btn" disabled style="height:30px;padding:0 12px; font-size: 12px;">
SAVE CURRENT
</button>
</div>
<div class="DG_Divider"></div>
<div id="DG_AccList_Wrap" class="DG_Scroll_Inner" style="max-height:260px;width:100%;gap:6px;">
<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">No saved accounts.</p>
</div>
</div>
<div class="DG_Page" id="DG_Page_MonthlyQuests">
<div class="DG_HStack_4 DG_Magnetic_Hover_1" id="DG_MQ_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--DG-text)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<p class="DG_Title">Back</p>
</div>
<div class="DG_HStack_Between">
<p class="DG_Title_Large">Monthly Quests</p>
<button class="DG_Btn DG_Btn_Primary DG_Magnetic_Hover_1" id="DG_MQ_ClaimAll_Btn" disabled style="height:30px;padding:0 12px; font-size: 12px;">
CLAIM ALL
</button>
</div>
<div class="DG_Divider"></div>
<div id="DG_MQ_Container" class="DG_Scroll_Inner" style="max-height:300px;width:100%;">
<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">Loading quests...</p>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(wrap);
let jwt = null, sub = null, headers = null, user = null;
let running = false, task = null, hidden = false;
let isOutdated = false;
let delay = parseInt(localStorage.getItem('dg_delay') || '500', 10);
let earnedGems = 0;
let currentPage = 'main';
let solverUI = null;
let isAutoMode = false;
let solverEnabled = localStorage.getItem('dg_solver') === 'true';
let solveLoopRunning = false;
let lessonSolving = false;
let lessonsToSolve = 0;
let currentLessonCount = 0;
// PRO-inspired: track current React element class and traversal depth
const DEFAULT_REACT_CLASS = '_3yE3H';
const STORY_REACT_CLASS = '_3TJzR';
let reactClass = DEFAULT_REACT_CLASS;
let reactTraverse = 1;
// Settings state
let showSolveButtons = localStorage.getItem('dg_showSolveButtons') !== 'false';
let muteLessons = localStorage.getItem('dg_muteLessons') === 'true';
let reduceEffects = localStorage.getItem('dg_reduceEffects') === 'true';
let freeSuper = localStorage.getItem('dg_super') === 'true';
const sleep = ms => new Promise(r => setTimeout(r, ms));
const GOALS_API = 'https://goals-api.duolingo.com';
function getJwt() {
const m = document.cookie.match(/(^| )jwt_token=([^;]+)/);
return m ? m[2] : null;
}
function decodeJwt(t) {
try {
const b = t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const pad = b.padEnd(b.length + (4 - b.length % 4) % 4, '=');
return JSON.parse(decodeURIComponent(atob(pad).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')));
} catch { return null; }
}
function buildHeaders(j) {
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + j, 'User-Agent': navigator.userAgent };
}
function gm(method, url, data, hdrs) {
return new Promise((res, rej) => GM_xmlhttpRequest({
method, url, headers: hdrs || headers,
data: data ? JSON.stringify(data) : null,
onload: r => res(r), onerror: () => rej(new Error('Network')),
timeout: 15000, ontimeout: () => rej(new Error('Timeout'))
}));
}
function goalHdrs(jwt) {
return {'Content-Type':'application/json','x-requested-with':'XMLHttpRequest','accept':'application/json; charset=UTF-8','Authorization':'Bearer '+jwt};
}
function notif(msg, dur = 4) {
const notifMain = document.getElementById('DG_Notif');
const notifBox = document.getElementById('DG_Notif_Box');
const icon = notifBox.querySelector('.DG_Inset_Icon_1_ID');
const title = notifBox.querySelector('.DG_Inset_Text_1_ID');
const body = notifBox.querySelector('.DG_Inset_Text_2_ID');
icon.innerHTML = '';
icon.style.color = 'rgb(var(--DG-accent-dark))';
title.textContent = msg;
body.textContent = '';
notifBox.classList.add('show');
clearTimeout(window._dgNotifTimer);
window._dgNotifTimer = setTimeout(() => notifBox.classList.remove('show'), dur * 1000);
notifBox.querySelector('.DG_Inset_Icon_2_ID').onclick = () => {
notifBox.classList.remove('show');
clearTimeout(window._dgNotifTimer);
};
}
function setConn(state, label) {
const btn = document.getElementById('DG_Conn_Btn');
const txt = document.getElementById('DG_Conn_Txt');
const ico = document.getElementById('DG_Conn_Ico');
txt.textContent = label || state;
if (state === 'Connected') {
ico.innerHTML = '<circle cx="12" cy="12" r="10" fill="rgb(var(--DG-green))"/><path d="M7 12l3 3 7-7" stroke="#fff" stroke-width="2" stroke-linecap="round"/>';
btn.style.background = 'rgba(var(--DG-green), 0.12)';
btn.style.outline = '2px solid rgba(var(--DG-green), 0.22)';
txt.style.color = 'rgb(var(--DG-green))';
ico.style.color = 'rgb(var(--DG-green))';
document.getElementById('DG_User_Row').style.display = 'flex';
} else if (state === 'Outdated') {
btn.style.outline = '2px solid rgba(255,140,0,0.50)';
btn.style.color = 'rgb(200, 90, 0)';
txt.style.color = 'rgb(220, 100, 0)';
txt.style.fontWeight = '700';
ico.style.color = 'rgb(220, 100, 0)';
ico.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none"><circle cx="12" cy="12" r="10" fill="rgba(255,140,0,0.20)" stroke="rgb(220,100,0)" stroke-width="2"/><path d="M12 7v5" stroke="rgb(220,100,0)" stroke-width="2.5" stroke-linecap="round"/><circle cx="12" cy="16.5" r="1.2" fill="rgb(220,100,0)"/></svg>';
btn.classList.add('DG_Outdated_Btn');
btn.style.cursor = 'pointer';
btn.title = 'Click to update DuoGang';
btn.onclick = () => window.open('https://openuserjs.org/scripts/duogang/DuoGang', '_blank');
document.querySelectorAll('.DG_Input_Btn, .DG_Btn:not(#DG_Conn_Btn), .DG_Acc_Btn, .DG_Shop_Btn').forEach(el => {
el.disabled = true;
el.style.pointerEvents = 'none';
el.style.opacity = '0.35';
});
document.querySelectorAll('.DG_Input, .DG_Toggle input').forEach(el => {
el.disabled = true;
el.style.pointerEvents = 'none';
});
} else if (state === 'Error - retrying' || state === 'Not logged in' || state === 'Invalid token') {
btn.style.background = 'rgba(var(--DG-red), 0.12)';
btn.style.outline = '2px solid rgba(var(--DG-red), 0.22)';
txt.style.color = 'rgb(var(--DG-red))';
ico.style.color = 'rgb(var(--DG-red))';
ico.innerHTML = '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>';
} else {
btn.style.background = 'linear-gradient(0deg, rgba(var(--DG-accent), 0.12) 0%, rgba(var(--DG-accent), 0.12) 100%), rgba(var(--color-snow, 255, 255, 255), 0.80)';
btn.style.outline = '2px solid rgba(var(--DG-accent), 0.22)';
txt.style.color = 'rgb(var(--DG-accent-dark))';
ico.style.color = 'rgb(var(--DG-accent-dark))';
ico.innerHTML = '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>';
}
}
function setBtnState(btnId, cls, label) {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.className = cls;
const span = btn.querySelector('span');
if (span) span.textContent = label;
}
function resetBtn(btnId, label, btnClass = 'DG_Input_Btn') {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.disabled = !user;
setBtnState(btnId, btnClass, label);
const prog = document.getElementById(btnId.replace('_Btn', '_Prog'));
if (prog) setTimeout(() => prog.classList.remove('on'), 2000);
}
function setBtnProgress(btnId, pct) {
const fill = document.getElementById(btnId.replace('_Btn', '_Fill'));
if (fill) fill.style.width = pct + '%';
const btn = document.getElementById(btnId);
if (btn) {
const span = btn.querySelector('span');
if (span) span.textContent = pct + '%';
}
}
function setBtnRunning(btnId) {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.disabled = false;
setBtnState(btnId, 'DG_Input_Btn Running', 'STOP');
const prog = document.getElementById(btnId.replace('_Btn', '_Prog'));
if (prog) prog.classList.add('on');
}
function setBtnDone(btnId, label = 'DONE') {
setBtnState(btnId, 'DG_Input_Btn Done', label);
const fill = document.getElementById(btnId.replace('_Btn', '_Fill'));
if (fill) fill.style.width = '100%';
}
function switchPage(pageId) {
document.querySelectorAll('.DG_Page').forEach(p => p.classList.remove('active'));
const page = document.getElementById('DG_Page_' + pageId);
if (page) page.classList.add('active');
currentPage = pageId;
}
function initSettingsPage() {
document.getElementById('DG_Settings_Solve_Toggle').checked = showSolveButtons;
document.getElementById('DG_Settings_Mute_Toggle').checked = muteLessons;
document.getElementById('DG_Settings_Effects_Toggle').checked = reduceEffects;
document.getElementById('DG_Settings_Super_Toggle').checked = freeSuper;
document.getElementById('DG_Settings_Save_Btn').addEventListener('click', () => {
showSolveButtons = document.getElementById('DG_Settings_Solve_Toggle').checked;
muteLessons = document.getElementById('DG_Settings_Mute_Toggle').checked;
reduceEffects = document.getElementById('DG_Settings_Effects_Toggle').checked;
freeSuper = document.getElementById('DG_Settings_Super_Toggle').checked;
localStorage.setItem('dg_showSolveButtons', showSolveButtons ? 'true' : 'false');
localStorage.setItem('dg_muteLessons', muteLessons ? 'true' : 'false');
localStorage.setItem('dg_reduceEffects', reduceEffects ? 'true' : 'false');
if (freeSuper) {
localStorage.setItem('dg_super', 'true');
} else {
localStorage.setItem('dg_super', 'false');
}
notif('Settings saved! Some changes may require a page reload.');
setTimeout(() => switchPage('Main'), 800);
});
}
initSettingsPage();
function checkForUpdate() {
setTimeout(() => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://raw.githubusercontent.com/JORKLAN/autoupdate_duogang/main/version.json?_=' + Date.now(),
onload: function(res) {
try {
console.log('[DuoGang] Update check response:', res.status);
const data = JSON.parse(res.responseText);
const latest = data.version;
if (!latest) { console.log('[DuoGang] No version found in JSON'); return; }
const current = GM_info.script.version;
console.log('[DuoGang] Latest:', latest, '| Current:', current);
const toNum = v => v.split('.').map(Number).reduce((a, b, i) => a + b * Math.pow(1000, 2 - i), 0);
if (toNum(latest) > toNum(current)) {
console.log('[DuoGang] Update available!');
isOutdated = true;
setConn('Outdated', 'Outdated');
} else {
console.log('[DuoGang] Up to date.');
}
} catch (e) { console.log('[DuoGang] Update check error:', e); }
},
onerror: function(e) { console.log('[DuoGang] Update check request failed:', e); }
});
}, 500);
}
async function connect() {
setConn('Connecting');
jwt = getJwt();
if (!jwt) { setConn('Not logged in'); return; }
const dec = decodeJwt(jwt);
if (!dec) { setConn('Invalid token'); return; }
sub = dec.sub;
headers = buildHeaders(jwt);
try {
const r = await gm('GET', `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,gems,picture,streakData`);
if (r.status !== 200) throw new Error(r.status);
user = JSON.parse(r.responseText);
if (!isOutdated) setConn('Connected');
renderUser(user);
['DG_XP_Btn', 'DG_Gem_Btn', 'DG_Streak_Btn', 'DG_Practice_Btn', 'DG_Super_Btn', 'DG_Shop_Btn', 'DG_League_Btn', 'DG_Quest_Btn', 'DG_MonthlyQuest_Btn', 'DG_AccSave_Btn', 'DG_MQ_ClaimAll_Btn'].forEach(id => {
const b = document.getElementById(id);
if (b) b.disabled = false;
});
} catch (e) {
setConn('Error - retrying');
setTimeout(connect, 8000);
}
}
function renderUser(u) {
if (!u) return;
document.getElementById('DG_UName').textContent = u.username || '';
document.getElementById('DG_UXP').textContent = (u.totalXp || 0).toLocaleString();
document.getElementById('DG_UGems').textContent = (u.gems || 0).toLocaleString();
document.getElementById('DG_UStreak').textContent = (u.streak || 0).toLocaleString();
if (u.picture) {
let hq = u.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!hq.endsWith('/xlarge') && hq.includes('duolingo.com/ssr-avatars')) hq += '/xlarge';
const av = document.getElementById('DG_Avatar');
const img = document.createElement('img');
img.src = hq;
img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%;';
img.onerror = function() { av.innerHTML = '?'; };
av.innerHTML = '';
av.appendChild(img);
}
}
async function storyXP(hh) {
try {
const now = Math.floor(Date.now() / 1000), dur = Math.floor(Math.random() * 121 + 300);
const r = await gm('POST', 'https://stories.duolingo.com/api2/stories/fr-en-le-passeport/complete', {
awardXp: true, completedBonusChallenge: true,
fromLanguage: 'fr', learningLanguage: 'en',
hasXpBoost: false, illustrationFormat: 'svg',
isFeaturedStoryInPracticeHub: true, isLegendaryMode: true,
isV2Redo: false, isV2Story: false, masterVersion: true,
maxScore: 0, score: 0, happyHourBonusXp: hh,
startTime: now, endTime: now + dur
});
return r.status === 200;
} catch { return false; }
}
async function farmXP(txp) {
const MAX = 499;
let loops = Math.floor(txp / MAX), rem = txp % MAX;
if (rem > 0 && rem < 30 && loops > 0) { loops--; rem += MAX; }
const total = loops + (rem >= 30 ? 1 : 0);
let cur = 0, earned = 0;
setBtnRunning('DG_XP_Btn');
for (let i = 0; i < loops; i++) {
if (!running) break;
const ok = await storyXP(469);
if (ok) { earned += MAX; cur++; }
setBtnProgress('DG_XP_Btn', Math.floor((cur / total) * 100));
await sleep(delay);
}
if (rem >= 30 && running) {
const ok = await storyXP(Math.min(rem - 30, 469));
if (ok) { earned += rem; cur++; }
setBtnProgress('DG_XP_Btn', 100);
}
if (running) {
setBtnDone('DG_XP_Btn', 'DONE');
notif('Farmed ' + earned.toLocaleString() + ' XP!');
setTimeout(connect, 1500);
setTimeout(() => resetBtn('DG_XP_Btn', 'GET XP'), 3000);
}
}
async function getGemRewards() {
try {
const r = await gm('GET', `https://www.duolingo.com/2023-05-23/users/${sub}?fields=rewardBundles`);
if (r.status !== 200) return [];
const bundles = JSON.parse(r.responseText).rewardBundles || [];
const gems = [];
for (const bundle of bundles) {
for (const reward of bundle.rewards || []) {
if (!reward.consumed && (reward.id.includes('GEMS') || reward.currency === 'GEMS')) {
gems.push({ id: reward.id, amount: reward.amount || 0 });
}
}
}
return gems;
} catch { return []; }
}
async function exploitGemReward(rewardId) {
const body = {
consumed: true,
fromLanguage: user.fromLanguage,
learningLanguage: user.learningLanguage,
pathLevelSpecifics: {
anchorSkillId: 'f22fd38157eea63965dc39eeac3c40c1',
indexSinceAnchorSkill: 0,
treeId: '14b1a2672c1bb3b250ebaa31b86c343e',
nodeState: 'active'
}
};
try {
const r = await gm('PATCH', `https://www.duolingo.com/2023-05-23/users/${sub}/rewards/${rewardId}`, body);
return r.status === 200;
} catch { return false; }
}
async function getGemCount() {
try {
const r = await gm('GET', `https://www.duolingo.com/2023-05-23/users/${sub}?fields=gemsConfig`);
if (r.status !== 200) return null;
return JSON.parse(r.responseText).gemsConfig?.gems ?? null;
} catch { return null; }
}
async function farmGems() {
earnedGems = 0;
const gemInput = document.getElementById('DG_Gem_Input');
if (gemInput) gemInput.value = '';
setBtnRunning('DG_Gem_Btn');
const THREADS = 3;
outer: while (running && task === 'gems') {
const rewards = await getGemRewards();
if (rewards.length === 0) { await sleep(delay * 2); continue; }
let gemsBefore = await getGemCount() ?? (user?.gems ?? 0);
for (let i = 0; i < rewards.length; i += THREADS) {
if (!running || task !== 'gems') break outer;
const batch = rewards.slice(i, i + THREADS);
await Promise.all(batch.map(r => exploitGemReward(r.id)));
await sleep(200);
const now = await getGemCount();
if (now !== null) {
const gained = Math.max(0, now - gemsBefore);
earnedGems += gained;
gemsBefore = now;
if (gemInput) gemInput.value = String(earnedGems);
if (user) { user.gems = now; renderUser(user); }
}
await sleep(Math.max(100, delay));
}
await sleep(delay);
}
resetBtn('DG_Gem_Btn', 'RUN');
if (earnedGems > 0) {
notif('+' + earnedGems.toLocaleString() + ' gems gained!');
setTimeout(connect, 1500);
}
}
async function farmStreak(days) {
const CH = ["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"];
let farmStart;
try {
const s = new Date(user.streakData?.currentStreak?.startDate || Date.now());
s.setDate(s.getDate() - 1);
farmStart = s;
} catch { const n = new Date(); n.setDate(n.getDate() - 1); farmStart = n; }
setBtnRunning('DG_Streak_Btn');
for (let i = 0; i < days; i++) {
if (!running) break;
const simDay = new Date(farmStart);
simDay.setDate(simDay.getDate() - i);
const end = Math.floor(simDay.getTime() / 1000);
try {
const sr = await gm('POST', 'https://www.duolingo.com/2023-05-23/sessions', {
challengeTypes: CH, fromLanguage: user.fromLanguage, isFinalLevel: false, isV2: true,
juicy: true, learningLanguage: user.learningLanguage, smartTipsVersion: 2, type: 'GLOBAL_PRACTICE'
});
if (sr.status === 200) {
const sess = JSON.parse(sr.responseText);
await new Promise((res, rej) => GM_xmlhttpRequest({
method: 'PUT', url: `https://www.duolingo.com/2023-05-23/sessions/${sess.id}`,
headers: headers, data: JSON.stringify({
...sess, heartsLeft: 5, startTime: end - 1, endTime: end,
enableBonusPoints: false, failed: false, maxInLessonStreak: 9, shouldLearnThings: true
}),
onload: r => res(r), onerror: () => rej(), timeout: 15000, ontimeout: () => rej()
}));
}
} catch { }
setBtnProgress('DG_Streak_Btn', Math.floor(((i + 1) / days) * 100));
await sleep(delay);
}
if (running) {
setBtnDone('DG_Streak_Btn', 'DONE');
notif('Restored ' + days + ' streak days!');
setTimeout(connect, 1500);
setTimeout(() => resetBtn('DG_Streak_Btn', 'RUN'), 3000);
}
}
async function run(type, val) {
if (running) { running = false; notif('Farm stopped.'); return; }
if (!user) { notif('Please wait for connection.'); return; }
running = true; task = type;
try {
if (type === 'xp') await farmXP(val);
if (type === 'gems') await farmGems();
if (type === 'streak') await farmStreak(val);
if (type === 'league') await farmLeague();
} catch (e) { notif('Error: ' + e.message); }
if (!running) {
if (type === 'xp') resetBtn('DG_XP_Btn', 'GET XP');
if (type === 'streak') resetBtn('DG_Streak_Btn', 'RUN');
}
running = false; task = null;
}
async function farmLeague() {
const LB = 'https://duolingo-leaderboards-prod.duolingo.com/leaderboards/7d9f5dd1-8423-491a-91f2-2532052038ce';
let prog = document.getElementById('DG_League_Prog');
let fill = document.getElementById('DG_League_Fill');
if (!prog) {
const leagueSection = document.getElementById('DG_League_Btn').parentElement;
prog = document.createElement('div');
prog.className = 'DG_Prog_Wrap on';
prog.id = 'DG_League_Prog';
fill = document.createElement('div');
fill.className = 'DG_Prog_Fill';
fill.id = 'DG_League_Fill';
prog.appendChild(fill);
leagueSection.appendChild(prog);
}
if (prog) prog.classList.add('on');
if (fill) fill.style.width = '0%';
setBtnRunning('DG_League_Btn');
while (running) {
try {
const r = await gm('GET', `${LB}/users/${sub}?client_unlocked=true&_=${Date.now()}`);
if (r.status !== 200) { await sleep(3000); continue; }
const data = JSON.parse(r.responseText);
const ranks = data?.active?.cohort?.rankings || [];
const me = ranks.find(u => u.user_id == sub);
if (!me) { notif('Not in league! Join a league first.'); break; }
const rank = ranks.indexOf(me) + 1;
const top1 = ranks[0];
const gap = top1.score - me.score;
if (rank === 1 && gap <= 0) { notif('Reached Rank #1!'); break; }
if (fill) fill.style.width = Math.min(95, Math.floor((me.score / Math.max(top1.score, 1)) * 100)) + '%';
if (gap + 100 > 0) { const ok = await storyXP(469); if (!ok) await sleep(3000); }
await sleep(delay);
} catch { await sleep(5000); }
}
resetBtn('DG_League_Btn', 'RUN');
}
async function getGoalsAndProgress() {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const [schemaR, progR] = await Promise.all([
gm('GET', `${GOALS_API}/schema?ui_language=en&_=${Date.now()}`, null, goalHdrs(jwt)),
gm('GET', `${GOALS_API}/users/${sub}/progress?timezone=${tz}&ui_language=en`, null, goalHdrs(jwt))
]);
if (schemaR.status !== 200 || progR.status !== 200) return null;
return {
schema: JSON.parse(schemaR.responseText),
progress: JSON.parse(progR.responseText)
};
}
async function farmDailyQuest() {
setBtnState('DG_Quest_Btn', 'DG_Input_Btn', 'Loading...');
const data = await getGoalsAndProgress();
if (!data) { notif('Could not load quest data.'); resetBtn('DG_Quest_Btn', 'RUN'); return; }
const earned = new Set(data.progress.badges?.earned || []);
const daily = (data.schema.goals || []).filter(g => g.category && g.category.includes('DAILY'));
const metrics = new Set(daily.filter(g => !earned.has(g.badgeId) && !earned.has(g.goalId) && g.metric).map(g => g.metric));
if (!metrics.size) {
notif('All daily quests completed!');
setBtnDone('DG_Quest_Btn', 'DONE');
setTimeout(() => resetBtn('DG_Quest_Btn', 'RUN'), 3000);
return;
}
setBtnState('DG_Quest_Btn', 'DG_Input_Btn Running', 'Running...');
const updates = [...metrics].map(m => ({ metric: m, quantity: 2000 }));
updates.push({ metric: 'QUESTS', quantity: 1 });
const okR = await gm('POST', `${GOALS_API}/users/${sub}/progress/batch`, {
metric_updates: updates,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: new Date().toISOString()
}, goalHdrs(jwt));
if (okR.status === 200) {
notif('Daily quests completed!');
setBtnDone('DG_Quest_Btn', 'DONE');
setTimeout(() => resetBtn('DG_Quest_Btn', 'RUN'), 3000);
} else {
notif('Quest completion failed.');
resetBtn('DG_Quest_Btn', 'RUN');
}
}
let questState = null;
function getQuestTimestamp(goalId) {
const m = goalId.match(/^(\d{4})_(\d{2})_monthly/);
if (m) return new Date(Date.UTC(parseInt(m[1]), parseInt(m[2]) - 1, 15, 12, 0, 0)).toISOString();
return new Date().toISOString();
}
async function loadMonthlyQuests() {
const cont = document.getElementById('DG_MQ_Container');
if (!cont) return;
cont.innerHTML = '<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">Loading...</p>';
const gh = goalHdrs(jwt);
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const [schemaR, progR] = await Promise.all([
gm('GET', `${GOALS_API}/schema?ui_language=en&_=${Date.now()}`, null, gh),
gm('GET', `${GOALS_API}/users/${sub}/progress?timezone=${tz}&ui_language=en`, null, gh)
]);
if (schemaR.status !== 200 || progR.status !== 200) throw new Error('API error');
const schema = JSON.parse(schemaR.responseText);
const prog = JSON.parse(progR.responseText);
const progress = prog.goals?.progress || {};
const earned = new Set(prog.badges?.earned || []);
questState = { schema, progress, earned };
renderMonthlyQuests(schema, progress, earned, gh);
} catch (e) {
cont.innerHTML = '<p class="DG_Subtitle" style="text-align:center;color:rgb(var(--DG-red));padding:8px 0;">Failed to load quests.</p>';
}
}
function renderMonthlyQuests(schema, progress, earned, gh) {
const cont = document.getElementById('DG_MQ_Container');
if (!cont) return;
cont.innerHTML = '';
const now = new Date();
const yr = now.getFullYear().toString();
const mo = (now.getMonth() + 1).toString().padStart(2, '0');
const mReg = new RegExp(`^${yr}_${mo}_monthly`);
const monthly = (schema.goals || []).filter(g => {
if (!g.category || !g.category.includes('MONTHLY')) return false;
const m2 = g.goalId.match(/^(\d{4}_\d{2})_monthly/);
return m2 && mReg.test(g.goalId);
});
if (monthly.length === 0) {
cont.innerHTML = '<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">No monthly quests found.</p>';
return;
}
monthly.forEach(g => {
const isEarned = earned.has(g.badgeId) || earned.has(g.goalId);
let cur = 0;
const raw = progress[g.goalId];
if (typeof raw === 'number') cur = raw;
else if (raw && typeof raw === 'object') cur = raw.progress || 0;
const tgt = g.threshold || 10;
let pct = Math.min(100, (cur / tgt) * 100);
if (isEarned) { pct = 100; cur = tgt; }
const remaining = Math.max(0, tgt - cur);
const item = document.createElement('div');
item.className = 'DG_Shop_Card';
item.style.cssText = 'align-items:flex-start;text-align:left;';
item.innerHTML = `
<p class="DG_Title" style="font-size:14px;">${g.title?.uiString || g.goalId}</p>
<p class="DG_Subtitle">${isEarned ? 'COMPLETED' : cur + ' / ' + tgt + ' · ' + g.metric}</p>
<div style="width:100%;height:4px;border-radius:2px;background:rgba(var(--DG-accent),0.10);overflow:hidden;">
<div style="height:100%;background:rgb(var(--DG-accent));border-radius:2px;width:${pct}%;${isEarned ? 'background:rgb(var(--DG-green));' : ''}"></div>
</div>
${!isEarned && remaining > 0 ? `<button class="DG_Shop_Btn" data-metric="${g.metric}" data-amount="${remaining}" data-id="${g.goalId}">GET +${remaining}</button>` : ''}
${isEarned ? '<button class="DG_Shop_Btn got">DONE</button>' : ''}
`;
const btn = item.querySelector('.DG_Shop_Btn:not(.got)');
if (btn) btn.addEventListener('click', async () => {
btn.textContent = '...';
btn.disabled = true;
try {
const payload = {
metric_updates: [{ metric: btn.dataset.metric, quantity: parseInt(btn.dataset.amount) }],
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: getQuestTimestamp(btn.dataset.id)
};
const r = await gm('POST', `${GOALS_API}/users/${sub}/progress/batch`, payload, gh);
if (r.status === 200) {
btn.textContent = 'OK';
btn.classList.add('got');
notif('Quest progress injected!');
setTimeout(() => loadMonthlyQuests(), 900);
} else {
btn.textContent = 'ERR';
btn.disabled = false;
}
} catch { btn.textContent = 'ERR'; btn.disabled = false; }
});
cont.appendChild(item);
});
}
async function claimAllMonthly() {
if (!questState || !questState.schema) { notif('Open Monthly Quests first.'); return; }
const gh = goalHdrs(jwt);
const uniqueMetrics = new Set();
questState.schema.goals.forEach(g => {
if (g.category && g.category.includes('MONTHLY') && g.metric) uniqueMetrics.add(g.metric);
});
if (uniqueMetrics.size > 0) {
const updates = [...uniqueMetrics].map(m => ({ metric: m, quantity: 2000 }));
updates.push({ metric: 'QUESTS', quantity: 1 });
const payload = {
metric_updates: updates,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: new Date().toISOString()
};
try {
await gm('POST', `${GOALS_API}/users/${sub}/progress/batch`, payload, gh);
notif('All monthly quests claimed!');
setTimeout(() => loadMonthlyQuests(), 900);
} catch { notif('Failed to claim quests.'); }
}
}
async function getShopItems() {
return new Promise(r => GM_xmlhttpRequest({
method: 'GET', url: 'https://www.duolingo.com/2023-05-23/shop-items',
headers: headers,
onload: res => {
try { r(res.status === 200 ? JSON.parse(res.responseText).shopItems || [] : []); }
catch { r([]); }
},
onerror: () => r([])
}));
}
async function buyShopItem(itemId) {
const payload = { itemName: itemId, isFree: true, consumed: true, fromLanguage: user.fromLanguage, learningLanguage: user.learningLanguage };
return new Promise(r => GM_xmlhttpRequest({
method: 'POST', url: `https://www.duolingo.com/2017-06-30/users/${sub}/shop-items`,
headers: headers, data: JSON.stringify(payload),
onload: res => r(res.status === 200), onerror: () => r(false)
}));
}
function categorizeItem(item) {
const id = item.id || '';
if (id.includes('streak_freeze')) return { cat: 'Streak Freezes', icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/icons/216ddc11afcbb98f44e53d565ccf479e.svg' };
if (id.includes('xp_boost')) return { cat: 'XP Boosts', icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/icons/68c1fd0f467456a4c607ecc0ac040533.svg' };
if (id.includes('health') || id.includes('heart')) return { cat: 'Hearts', icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/hearts/547ffcf0e6256af421ad1a32c26b8f1a.svg' };
if (id.includes('gem')) return { cat: 'Gems', icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg' };
if (item.type === 'outfit') return { cat: 'Outfits', icon: 'https://d35aaqx5ub95lt.cloudfront.net/vendor/0cecd302cf0bcd0f73d51768feff75fe.svg' };
return { cat: 'Misc', icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/leagues/9fadb349c2ece257386a0e576359c867.svg' };
}
function formatItemName(id) {
return id.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
async function loadShop() {
const container = document.getElementById('DG_Shop_Container');
container.innerHTML = '<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">Loading shop...</p>';
const items = await getShopItems();
container.innerHTML = '';
const valid = items.filter(i => i.currencyType === 'XGM' && !i.id.includes('gift'));
if (!valid.length) {
container.innerHTML = '<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">No items available.</p>';
return;
}
const ORDER = ['Streak Freezes', 'XP Boosts', 'Hearts', 'Gems', 'Outfits', 'Misc'];
const grouped = {};
valid.forEach(i => {
const { cat, icon } = categorizeItem(i);
if (!grouped[cat]) grouped[cat] = [];
let name = i.name || formatItemName(i.id);
if (i.id.includes('xp_boost') && i.id.match(/\d+$/)) name += ' Mins';
grouped[cat].push({ ...i, displayName: name, icon, cat });
});
ORDER.forEach(cat => {
if (!grouped[cat]) return;
const grid = document.createElement('div');
grid.className = 'DG_Shop_Grid';
grid.innerHTML = `<p class="DG_Title" style="grid-column:1/-1;text-align:center;color:rgb(var(--DG-accent-dark));font-size:12px;text-transform:uppercase;letter-spacing:0.5px;">${cat}</p>`;
grouped[cat].forEach(item => {
const card = document.createElement('div');
card.className = 'DG_Shop_Card';
card.innerHTML = `
<img src="${item.icon}" class="DG_Shop_Ico" onerror="this.style.display='none'">
<div class="DG_Shop_Name">${item.displayName}</div>
<button class="DG_Shop_Btn" data-id="${item.id}">GET</button>
`;
const btn = card.querySelector('.DG_Shop_Btn');
btn.onclick = async () => {
btn.textContent = '...';
btn.disabled = true;
const ok = await buyShopItem(item.id);
if (ok) {
btn.textContent = 'GOT';
btn.classList.add('got');
notif('Got ' + item.displayName + '!');
} else {
btn.textContent = 'FAIL';
btn.classList.add('fail');
setTimeout(() => { btn.textContent = 'GET'; btn.classList.remove('fail'); btn.disabled = false; }, 2000);
}
};
grid.appendChild(card);
});
container.appendChild(grid);
});
}
function accGetAll() { try { return JSON.parse(localStorage.getItem('dg_accounts') || '[]'); } catch { return []; } }
function accSetAll(arr) { localStorage.setItem('dg_accounts', JSON.stringify(arr)); }
function accSaveCurrent() {
if (!user || !jwt || !sub) { notif('Please wait for connection.'); return; }
const all = accGetAll();
if (all.find(a => a.id == sub)) { notif('Already saved.'); return; }
let pic = '';
if (user.picture) {
pic = user.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!pic.endsWith('/xlarge') && pic.includes('duolingo.com/ssr-avatars')) pic += '/xlarge';
}
all.push({ id: sub, username: user.username || 'User', pic, token: jwt });
accSetAll(all);
notif('Account saved!');
renderAccounts();
}
function accRemove(id) {
const all = accGetAll().filter(a => a.id != id);
accSetAll(all);
renderAccounts();
notif('Account removed.');
}
function accLogin(id) {
const acc = accGetAll().find(a => a.id == id);
if (!acc) { notif('Account not found.'); return; }
document.cookie = `jwt_token=${acc.token}; domain=.duolingo.com; path=/; max-age=31536000`;
window.location.reload();
}
function renderAccounts() {
const wrap = document.getElementById('DG_AccList_Wrap');
if (!wrap) return;
const all = accGetAll();
if (all.length === 0) {
wrap.innerHTML = '<p class="DG_Subtitle" style="text-align:center;padding:8px 0;">No saved accounts.</p>';
return;
}
wrap.innerHTML = '';
all.forEach(acc => {
const card = document.createElement('div');
card.className = 'DG_Acc_Card';
const isCurrent = sub && acc.id == sub;
const picHtml = acc.pic
? `<img src="${acc.pic}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;" onerror="this.parentNode.innerHTML='?'">`
: '?';
card.innerHTML = `
<div class="DG_Acc_Avatar">${picHtml}</div>
<div class="DG_Acc_Info">
<p class="DG_Acc_Name">${acc.username}</p>
${isCurrent ? '<p class="DG_Acc_Sub active"><svg width="8" height="8" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="rgb(var(--DG-green))"/></svg>Active</p>' : `<p class="DG_Acc_Sub">ID: ${String(acc.id).slice(0,8)}...</p>`}
</div>
<div class="DG_HStack" style="gap:5px;flex-shrink:0;">
${!isCurrent ? `<button class="DG_Acc_Btn login" data-id="${acc.id}">LOG IN</button>` : ''}
<button class="DG_Acc_Btn del" data-id="${acc.id}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`;
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);
});
}
const solver = {
syncContext: () => {
const isStory = document.querySelector('.FmlUF') !== null
|| document.querySelector('[data-test="stories-player-continue"],[data-test="stories-player-done"],[data-test="story-start"]') !== null;
if (isStory) {
reactClass = STORY_REACT_CLASS;
reactTraverse = 0;
} else {
reactClass = DEFAULT_REACT_CLASS;
reactTraverse = 1;
}
},
findReact: (dom, traverseUp) => {
if (!dom) return null;
const key = Object.keys(dom).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
if (!key) return null;
const domFiber = dom[key];
if (!domFiber) return null;
const GetCompFiber = fiber => {
let pf = fiber.return;
while (pf && typeof pf.type === 'string') pf = pf.return;
return pf;
};
let cf = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
if (!cf) break;
cf = GetCompFiber(cf);
}
return cf?.stateNode ?? null;
},
getSolution: () => {
solver.syncContext();
let el = document.getElementsByClassName(reactClass)[0];
// Fallback chain (handles new-word lessons, error reviews, etc.)
if (!el) el = document.querySelector('._3yE3H');
if (!el) el = document.querySelector('.RMEuZ._1GVfY');
if (!el) el = document.querySelector('[data-test="challenge"]');
if (!el) el = document.querySelector('[class*="challenge"]');
if (!el) return null;
const ri = solver.findReact(el, reactTraverse);
let sol = ri?.props?.currentChallenge ?? null;
if (!sol && reactTraverse !== 0) {
const ri0 = solver.findReact(el, 0);
sol = ri0?.props?.currentChallenge ?? null;
}
if (!sol) {
const ri2 = solver.findReact(el, 2);
sol = ri2?.props?.currentChallenge ?? null;
}
return sol;
},
determineType: () => {
try {
const t = window.sol?.type;
if (document.querySelector('.FmlUF')) {
if (!t) return false;
if (t === 'arrange') return 'Story Arrange';
if (t === 'multiple-choice' || t === 'select-phrases') return 'Story Multiple Choice';
if (t === 'point-to-phrase') return 'Story Point to Phrase';
if (t === 'match') return 'Story Pairs';
return 'Story Slide';
}
if (document.querySelector('[data-test="session-complete-slide"]') ||
document.querySelector('[data-test="session-over"]')) return 'Session Complete';
if (document.querySelector('[data-test="daily-quest-progress-slide"]')) return 'Daily Quest Progress';
if (document.querySelector('[data-test="streak-slide"]')) return 'Streak Slide';
if (document.querySelector('[data-test="leaderboard-slide"]')) return 'Leaderboard Slide';
if (document.querySelector('[data-test*="challenge-speak"]') ||
document.querySelector('[data-test="challenge challenge-listenSpeak"]')) return 'Challenge Speak';
if (!t) {
if (document.querySelector('[data-test="player-next"]')) return 'Next Only';
return false;
}
if (t === 'listenMatch') return 'Listen Match';
if (t === 'syllableTap') return 'Syllable Tap';
if (t === 'syllableListenTap') return 'Syllable Listen Tap';
if (t === 'listenTap') return 'Listen Tap';
if (t === 'listen') return 'Listen Type';
if (t === 'translate') return 'Translate';
if (t === 'completeReverseTranslation') return 'Complete Reverse';
if (t === 'typeCloze') return 'Type Cloze';
if (t === 'typeClozeTable') return 'Type Cloze Table';
if (t === 'tapClozeTable') return 'Tap Cloze Table';
if (t === 'typeCompleteTable') return 'Type Complete Table';
if (t === 'tapCompleteTable') return 'Tap Complete Table';
if (t === 'patternTapComplete') return 'Pattern Tap Complete';
if (t === 'judge') return 'Judge';
if (t === 'dialogue' || t === 'characterIntro' || t === 'selectTranscription') return 'Dialogue';
if (t === 'definition' || t === 'radioBinary') return 'Select Card';
if (t === 'characterMatch' || t === 'match') {
if (document.querySelector('[data-test$="challenge-tap-token"]')) return 'Pairs';
}
if (document.querySelector('[data-test*="challenge-partialReverseTranslate"]')) return 'Partial Reverse';
if (document.querySelector('[data-test="challenge challenge-characterWrite"]')) {
if (document.querySelector('g._25Ktp')) return 'Character Write Drag';
if (document.querySelector('path._1e5Zt')) return 'Character Write Draw';
return 'Character Write Freehand';
}
if (t === 'select' || t === 'characterSelect' || t === 'form' ||
t === 'readComprehension' || t === 'listenComprehension' ||
t === 'selectPronunciation') return 'Select Card';
if (document.querySelector('[data-test*="challenge-name"]') &&
document.querySelector('[data-test="challenge-choice"]')) return 'Challenge Name';
if (document.querySelector('[data-test="challenge-choice"]')) {
if (document.querySelector('[data-test="challenge-text-input"]')) return 'Challenge Choice with Text Input';
return 'Challenge Choice';
}
if (document.querySelector('[data-test$="challenge-tap-token"]')) {
if (window.sol?.pairs !== undefined) return 'Pairs';
if (window.sol?.correctTokens !== undefined) return 'Tokens Run';
if (window.sol?.correctIndices !== undefined) return 'Indices Run';
}
if (document.querySelector('[data-test="challenge-tap-token-text"]')) return 'Fill in the Gap';
if (document.querySelector('[data-test="challenge-text-input"]')) return 'Challenge Text Input';
if (document.querySelector('textarea[data-test="challenge-translate-input"]')) return 'Challenge Translate Input';
return false;
} catch { return false; }
},
setValue: (el, val) => {
const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement : window.HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(proto.prototype, 'value').set;
setter.call(el, val);
el.dispatchEvent(new Event('input', { bubbles: true }));
},
getCleanButtonText: (button) => {
const rubyElements = button.querySelectorAll('ruby');
if (rubyElements.length > 0) {
let text = '';
rubyElements.forEach(ruby => {
ruby.querySelectorAll('span[lang]:not(rt)').forEach(span => { text += span.textContent; });
});
return text.trim();
}
const textElement = button.querySelector('[data-test="challenge-tap-token-text"]');
return textElement ? textElement.innerText.trim() : button.innerText.trim();
},
handle: async (type) => {
const sl = ms => new Promise(r => setTimeout(r, ms));
try {
switch (type) {
case 'Challenge Speak':
case 'Listen Match':
case 'Listen Speak': {
['.vpDIE','._8AMBh._2vfJy._3Qy5R._28UWu._3h0lA._1S2uf._1E9sc',
'._1Qh5D._36g4N._2YF0P._28UWu._3h0lA._1S2uf._1E9sc'].forEach(sel => {
document.querySelector(sel)?.click();
});
await sl(200);
let skipBtn = document.querySelector('button[data-test="player-skip"]');
if (!skipBtn) { await sl(500); skipBtn = document.querySelector('button[data-test="player-skip"]'); }
skipBtn?.click();
await sl(150);
break;
}
case 'Select Card': {
const idx = window.sol?.correctIndex ?? 0;
const cards = document.querySelectorAll('[data-test="challenge-choice-card"]');
if (cards.length) cards[idx]?.click();
else document.querySelectorAll('[data-test="challenge-choice"]')[idx]?.click();
break;
}
case 'Judge': {
const ci = window.sol?.correctIndices?.[0] ?? 0;
document.querySelectorAll('[data-test="challenge-judge-text"]')[ci]?.click();
break;
}
case 'Dialogue': {
const idx = window.sol?.correctIndex ?? 0;
const judges = document.querySelectorAll('[data-test="challenge-judge-text"]');
if (judges.length) judges[idx]?.click();
else document.querySelectorAll('[data-test="challenge-choice"]')[idx]?.click();
break;
}
case 'Translate': {
const { correctTokens, correctSolutions } = window.sol;
if (correctTokens?.length) {
const tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const used = [];
for (const word of correctTokens) {
for (let i = 0; i < tokens.length; i++) {
if (used.includes(i) || tokens[i].disabled) continue;
if (solver.getCleanButtonText(tokens[i]) === word.trim()) {
tokens[i].click(); used.push(i); await sl(40); break;
}
}
}
} else if (correctSolutions) {
const ta = document.querySelector('textarea[data-test="challenge-translate-input"]');
if (ta) solver.setValue(ta, correctSolutions[0]);
}
break;
}
case 'Listen Tap': {
const tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const used = [];
for (const word of (window.sol?.correctTokens || [])) {
for (let i = 0; i < tokens.length; i++) {
if (used.includes(i) || tokens[i].disabled) continue;
if (solver.getCleanButtonText(tokens[i]) === word.trim()) {
tokens[i].click(); used.push(i); await sl(40); break;
}
}
}
break;
}
case 'Listen Type': {
const ans = window.sol?.prompt || window.sol?.correctSolutions?.[0] || '';
const ta = document.querySelector('textarea[data-test="challenge-translate-input"]') ||
document.querySelector('[data-test="challenge-text-input"]');
if (ta) solver.setValue(ta, ans);
break;
}
case 'Complete Reverse': {
const blankTokens = window.sol?.displayTokens?.filter(t => t.isBlank);
const inputs = document.querySelectorAll('[data-test="challenge-text-input"]');
inputs.forEach((inp, i) => { if (blankTokens?.[i]) solver.setValue(inp, blankTokens[i].text); });
break;
}
case 'Challenge Choice':
document.querySelectorAll('[data-test="challenge-choice"]')[window.sol?.correctIndex ?? 0]?.click();
break;
case 'Challenge Choice with Text Input': {
const inp = document.querySelector('[data-test="challenge-text-input"]');
if (inp) {
const ans = window.sol?.correctSolutions
? window.sol.correctSolutions[0].split(/(?<=^\S+)\s/)[1]
: (window.sol?.displayTokens?.find(t => t.isBlank)?.text || window.sol?.prompt);
solver.setValue(inp, ans);
}
break;
}
case 'Challenge Text Input': {
const inp = document.querySelector('[data-test="challenge-text-input"]');
if (inp) solver.setValue(inp, window.sol?.correctSolutions?.[0] || window.sol?.displayTokens?.find(t => t.isBlank)?.text || window.sol?.prompt);
break;
}
case 'Challenge Translate Input': {
const ta = document.querySelector('textarea[data-test="challenge-translate-input"]');
if (ta) solver.setValue(ta, window.sol?.correctSolutions?.[0] || window.sol?.prompt);
break;
}
case 'Partial Reverse': {
const el = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector('span[contenteditable]');
if (el) {
const txt = window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join('')?.trim() || '';
const setter = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent').set;
setter.call(el, txt);
el.dispatchEvent(new Event('input', { bubbles: true }));
}
break;
}
case 'Type Cloze': {
const inp = document.querySelector('input[type="text"].b4jqk');
if (inp) {
const tok = window.sol?.displayTokens?.find(t => t.damageStart !== undefined);
if (tok) solver.setValue(inp, tok.text.slice(tok.damageStart));
}
break;
}
case 'Type Cloze Table': {
const rows = document.querySelectorAll('tbody tr');
window.sol?.displayTableTokens?.slice(1).forEach((rowToks, i) => {
const cell = rowToks[1]?.find(t => typeof t.damageStart === 'number');
if (cell && rows[i]) {
const inp = rows[i].querySelector('input[type="text"].b4jqk');
if (inp) solver.setValue(inp, cell.text.slice(cell.damageStart));
}
});
break;
}
case 'Tap Cloze Table': {
const rows = document.querySelectorAll('tbody tr');
window.sol?.displayTableTokens?.slice(1).forEach((rowToks, i) => {
const cell = rowToks[1]?.find(t => typeof t.damageStart === 'number');
if (!cell || !rows[i]) return;
const bank = document.querySelector('[data-test="word-bank"]');
const btns = bank ? Array.from(bank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')) : [];
const correct = cell.text.slice(cell.damageStart);
let matched = '';
for (const btn of btns) {
if (!correct.startsWith(matched + solver.getCleanButtonText(btn))) continue;
btn.click(); matched += solver.getCleanButtonText(btn);
if (matched === correct) break;
}
});
break;
}
case 'Type Complete Table': {
const rows = document.querySelectorAll('tbody tr');
window.sol?.displayTableTokens?.slice(1).forEach((rowToks, i) => {
const cell = rowToks[1]?.find(t => t.isBlank);
if (cell && rows[i]) {
const inp = rows[i].querySelector('input[type="text"].b4jqk');
if (inp) solver.setValue(inp, cell.text);
}
});
break;
}
case 'Pattern Tap Complete': {
const bank = document.querySelector('[data-test="word-bank"]');
if (bank) {
const idx = window.sol?.correctIndex ?? 0;
const txt = window.sol?.choices?.[idx];
const btn = Array.from(bank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')).find(b => solver.getCleanButtonText(b) === txt);
if (btn) btn.click();
}
break;
}
case 'Story Arrange': {
const choices = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
for (const idx of (window.sol?.phraseOrder || [])) {
choices[idx]?.click(); await sl(50);
}
break;
}
case 'Story Multiple Choice': {
document.querySelectorAll('[data-test="stories-choice"]')[window.sol?.correctAnswerIndex ?? 0]?.click();
break;
}
case 'Story Point to Phrase': {
const choices = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
let ci = -1;
for (let i = 0; i < (window.sol?.parts?.length || 0); i++) {
if (window.sol.parts[i].selectable) ci++;
if (window.sol.correctAnswerIndex === i) { choices[ci]?.parentElement?.click(); break; }
}
break;
}
case 'Story Pairs': {
const btns = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
const texts = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
const map = new Map();
for (let i = 0; i < btns.length; i++) map.set(texts[i]?.innerText?.toLowerCase()?.trim(), btns[i]);
for (const key in (window.sol?.dictionary || {})) {
const val = window.sol.dictionary[key];
const k = key.split(':')[1]?.toLowerCase()?.trim();
const v = val?.toLowerCase()?.trim();
map.get(k)?.click(); await sl(50);
map.get(v)?.click(); await sl(50);
}
break;
}
case 'Story Slide':
break;
case 'Challenge Name': {
const arts = window.sol?.articles;
const correct = window.sol?.correctSolutions?.[0];
if (arts && correct) {
const match = arts.find(a => correct.startsWith(a));
if (match) {
const idx = arts.indexOf(match);
const sel = document.querySelector(`[data-test="challenge-choice"]:nth-child(${idx+1})`);
if (sel) { sel.click(); await sl(50); }
const inp = document.querySelector('[data-test="challenge-text-input"]');
if (inp) solver.setValue(inp, correct.substring(match.length).trim());
}
}
break;
}
case 'Pairs': {
const btns = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
const texts = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
if (texts.length !== btns.length || !btns.length) break;
for (const pair of (window.sol?.pairs || [])) {
for (let i = 0; i < btns.length; i++) {
if (btns[i].disabled) continue;
const t = texts[i].innerText.toLowerCase().trim();
if (t === pair.transliteration?.toLowerCase()?.trim() ||
t === pair.character?.toLowerCase()?.trim() ||
t === pair.learningToken?.toLowerCase()?.trim() ||
t === pair.fromToken?.toLowerCase()?.trim()) {
btns[i].click(); await sl(50);
}
}
}
break;
}
case 'Tokens Run': {
const all = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const clicked = [];
for (const ct of (window.sol?.correctTokens || [])) {
const match = Array.from(all).filter(el => solver.getCleanButtonText(el) === ct.trim());
const idx = clicked.filter(t => solver.getCleanButtonText(t) === ct.trim()).length;
const btn = match[idx] || match[0];
if (btn && !btn.disabled) { btn.click(); clicked.push(btn); }
}
break;
}
case 'Indices Run':
case 'Fill in the Gap': {
if (!window.sol?.correctIndices) break;
const bank = document.querySelector('[data-test="word-bank"]') || document.querySelector('.eSgkc');
if (!bank) break;
const btns = Array.from(bank.querySelectorAll('button[data-test*="challenge-tap-token"]:not(span)'));
for (const idx of window.sol.correctIndices) {
if (idx>=0 && idx<btns.length && !btns[idx].disabled && btns[idx].getAttribute('aria-disabled')!=='true') {
btns[idx].click(); await sl(50);
}
}
break;
}
case 'Tap Complete Table': {
const rows = document.querySelectorAll('tbody tr');
const solRows = window.sol?.displayTableTokens?.slice(1) || [];
const bank = document.querySelector('[data-test="word-bank"]');
const bankBtns = bank ? Array.from(bank.querySelectorAll('button[data-test*="-challenge-tap-token"]')) : [];
const used = new Set();
for (let ri = 0; ri < solRows.length; ri++) {
const cell = solRows[ri][1]?.find(t => t.isBlank);
if (!cell) continue;
const txt = cell.text;
const rowBtns = rows[ri]?.querySelectorAll('button[data-test*="-challenge-tap-token"]');
let clicked = false;
if (rowBtns) {
for (const btn of rowBtns) {
const tEl = btn.querySelector('[data-test="challenge-tap-token-text"]');
if (tEl?.innerText.trim()===txt && !btn.disabled) { btn.click(); await sl(50); clicked=true; break; }
}
}
if (!clicked && bankBtns.length) {
for (let i = 0; i < bankBtns.length; i++) {
if (used.has(i)) continue;
const tEl = bankBtns[i].querySelector('[data-test="challenge-tap-token-text"]');
if (tEl?.innerText.trim()===txt && !bankBtns[i].disabled) { bankBtns[i].click(); await sl(50); used.add(i); break; }
}
}
}
break;
}
case 'Syllable Tap':
case 'Syllable Listen Tap': {
const indices = window.sol?.correctIndices || [];
const choices = window.sol?.choices || [];
const domBtns = Array.from(document.querySelectorAll('[data-test="word-bank"] [data-test$="challenge-tap-token"]'));
indices.forEach(idx => {
const txt = choices[idx]?.text;
const btn = domBtns.find(b => solver.getCleanButtonText(b) === txt);
if (btn) btn.click();
});
break;
}
case 'Character Write Drag': {
const createEvent=(t,x,y,btns)=>new MouseEvent(t,{bubbles:true,clientX:x,clientY:y,buttons:btns,button:0});
const norm=s=>s?s.replace(/\s/g,''):'';
for (const stroke of (window.sol?.strokes||[])) {
const target=norm(stroke.path);
let path,handle;
while (!path||!handle) {
const cands=document.querySelectorAll('path._1e5Zt');
path=Array.from(cands).find(p=>norm(p.getAttribute('d'))===target);
handle=document.querySelector('g._25Ktp');
if (!path||!handle) await sl(10);
}
const mat=path.getScreenCTM(),len=path.getTotalLength();
const start=path.getPointAtLength(0).matrixTransform(mat);
const end=path.getPointAtLength(len).matrixTransform(mat);
handle.dispatchEvent(createEvent('mousedown',start.x,start.y,1));
for (let s=1;s<=10;s++){const p=path.getPointAtLength((s/10)*len).matrixTransform(mat);const m=createEvent('mousemove',p.x,p.y,1);handle.dispatchEvent(m);document.dispatchEvent(m);}
const fm=createEvent('mousemove',end.x,end.y,1);handle.dispatchEvent(fm);document.dispatchEvent(fm);
await sl(5);handle.dispatchEvent(createEvent('mouseup',end.x,end.y,0));document.dispatchEvent(createEvent('mouseup',end.x,end.y,0));
}
break;
}
case 'Character Write Draw': {
const createEvent=(t,x,y,btns)=>new MouseEvent(t,{bubbles:true,clientX:x,clientY:y,buttons:btns,button:0});
const norm=s=>s?s.replace(/\s/g,''):'';
for (const stroke of (window.sol?.strokes||[])) {
const target=norm(stroke.path);
let path,cursor;
while (!path||!cursor) {
const cands=document.querySelectorAll('path._1e5Zt');
path=Array.from(cands).find(p=>norm(p.getAttribute('d'))===target);
cursor=document.querySelector('g._1h31R:not(._25Ktp)');
if (!path||!cursor) await sl(10);
}
const mat=path.getScreenCTM(),len=path.getTotalLength();
const start=path.getPointAtLength(0).matrixTransform(mat);
const end=path.getPointAtLength(len).matrixTransform(mat);
cursor.dispatchEvent(createEvent('mousedown',start.x,start.y,1));document.dispatchEvent(createEvent('mousedown',start.x,start.y,1));
for (let s=1;s<=10;s++){const p=path.getPointAtLength((s/10)*len).matrixTransform(mat);const m=createEvent('mousemove',p.x,p.y,1);cursor.dispatchEvent(m);document.dispatchEvent(m);}
const fm=createEvent('mousemove',end.x,end.y,1);cursor.dispatchEvent(fm);document.dispatchEvent(fm);
await sl(5);cursor.dispatchEvent(createEvent('mouseup',end.x,end.y,0));document.dispatchEvent(createEvent('mouseup',end.x,end.y,0));
}
break;
}
case 'Character Write Freehand': {
const createEvent=(t,x,y,btns)=>new MouseEvent(t,{bubbles:true,clientX:x,clientY:y,buttons:btns,button:0});
const norm=s=>s?s.replace(/\s/g,''):'';
const freeStrokes=(window.sol?.strokes||[]).filter(s=>s.strokeDrawMode==='FREEHAND');
for (const stroke of freeStrokes) {
const target=norm(stroke.path);
let path,svg;
while (!path||!svg) {
const cands=document.querySelectorAll('path._22UPm');
path=Array.from(cands).find(p=>norm(p.getAttribute('d'))===target);
svg=document.querySelector('svg.o1rqi');
if (!path||!svg) await sl(10);
}
const mat=path.getScreenCTM(),len=path.getTotalLength();
const start=path.getPointAtLength(0).matrixTransform(mat);
const end=path.getPointAtLength(len).matrixTransform(mat);
svg.dispatchEvent(createEvent('mousedown',start.x,start.y,1));document.dispatchEvent(createEvent('mousedown',start.x,start.y,1));
for (let s=1;s<=10;s++){const p=path.getPointAtLength((s/10)*len).matrixTransform(mat);const m=createEvent('mousemove',p.x,p.y,1);svg.dispatchEvent(m);document.dispatchEvent(m);}
const fm=createEvent('mousemove',end.x,end.y,1);svg.dispatchEvent(fm);document.dispatchEvent(fm);
await sl(5);svg.dispatchEvent(createEvent('mouseup',end.x,end.y,0));document.dispatchEvent(createEvent('mouseup',end.x,end.y,0));
}
break;
}
case 'Next Only':
case 'Session Complete':
case 'Daily Quest Progress':
case 'Streak Slide':
case 'Leaderboard Slide':
break;
}
} catch(e) { console.error('[DuoGang] Solver error:', type, e); }
},
clickNext: () => {
const nb = document.querySelector('[data-test="player-next"]') ||
document.querySelector('[data-test="stories-player-continue"]') ||
document.querySelector('[data-test="stories-player-done"]') ||
document.querySelector('[data-test="story-start"]');
if (!nb) return;
const isDisabled = nb.getAttribute('aria-disabled') === 'true' || nb.disabled;
if (!isDisabled) nb.click();
},
_busy: false,
solve: async () => {
if (solver._busy) return;
solver._busy = true;
try {
[
'[data-test="practice-hub-ad-no-thanks-button"]',
'[data-test="plus-no-thanks"]',
'[data-test="story-start"]',
'.vpDIE',
'._1N-oo._36Vd3._16r-S._1ZBYz._23KDq._1S2uf.HakPM',
'._8AMBh._2vfJy._3Qy5R._28UWu._3h0lA._1S2uf._1E9sc',
'._1Qh5D._36g4N._2YF0P._28UWu._3h0lA._1S2uf._1E9sc',
'._3bBpU._1x5JY._1M9iF._36g4N._2YF0P.T7I0c._2EnxW.MYehf',
'._2V6ug._1ursp._7jW2t._28UWu._3h0lA._1S2uf._1E9sc',
'._1rcV8._1VYyp._1ursp._7jW2t._1gKir',
'._2V6ug._1ursp._7jW2t._3zgLG'
].forEach(sel => document.querySelector(sel)?.click());
window.sol = solver.getSolution();
const ct = solver.determineType();
if (ct && ct !== 'Challenge Speak' && ct !== 'Listen Match' && ct !== 'Listen Speak') {
await Promise.race([
solver.handle(ct),
new Promise(r => setTimeout(r, 2000))
]);
await new Promise(r => setTimeout(r, 80));
} else if (ct === 'Challenge Speak' || ct === 'Listen Match' || ct === 'Listen Speak') {
await solver.handle(ct);
}
solver.clickNext();
await new Promise(r => setTimeout(r, 120));
} catch(e) {
solver.clickNext();
}
solver._busy = false;
},
createUI: () => {
if (solverUI) return;
solverUI = document.createElement('div');
solverUI.className = 'DG_Solver_Btn';
solverUI.id = 'DG_Solver_UI';
solverUI.innerHTML = `
<button class="DG_Solve_One" id="DG_Solve_One">SOLVE</button>
<button class="DG_Solve_All" id="DG_Solve_All">SOLVE ALL</button>
`;
document.body.appendChild(solverUI);
document.getElementById('DG_Solve_One').addEventListener('click', () => solver.solve());
document.getElementById('DG_Solve_All').addEventListener('click', () => solver.toggleAuto());
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
if (e.shiftKey) solver.toggleAuto();
else solver.solve();
}
});
},
removeUI: () => {
if (solverUI) { solverUI.remove(); solverUI = null; }
isAutoMode = false; solveLoopRunning = false;
},
updateAutoBtn: () => {
const btn = document.getElementById('DG_Solve_All'); if (!btn) return;
if (isAutoMode) {
btn.textContent = 'PAUSE';
btn.style.background = 'rgb(var(--DG-red))';
btn.style.borderBottomColor = '#cc0000';
} else {
btn.textContent = 'SOLVE ALL';
btn.style.background = 'rgb(var(--DG-blue))';
btn.style.borderBottomColor = '#1b5fb8';
}
},
toggleAuto: () => {
isAutoMode = !isAutoMode;
solver.updateAutoBtn();
if (isAutoMode && !solveLoopRunning) {
solveLoopRunning = true;
const initialUrl = window.location.href;
(async function loop() {
while (isAutoMode) {
if (window.location.href !== initialUrl) {
isAutoMode = false; solver.updateAutoBtn(); break;
}
const t0 = Date.now();
await solver.solve();
await new Promise(r => setTimeout(r, 80));
if (!isAutoMode) break;
const elapsed = Date.now() - t0;
const wait = Math.max(0, 380 - elapsed);
if (wait > 0) await new Promise(r => setTimeout(r, wait));
}
solveLoopRunning = false;
})();
} else if (!isAutoMode) {
solveLoopRunning = false;
}
}
};
async function farmPractice(count) {
lessonsToSolve = count; currentLessonCount = 0;
if (lessonSolving) { notif('Practice farm already running.'); return; }
lessonSolving = true;
const btn = document.getElementById('DG_Practice_Btn');
if (btn) { btn.style.background = 'rgb(var(--DG-red))'; btn.style.color = '#fff'; btn.textContent = 'STOP'; }
notif('Navigating to practice...', 3);
if (!window.location.pathname.startsWith('/practice')) { window.location.assign('/practice'); return; }
await solveCurrentLesson();
}
function isStoryLesson() {
return !!document.querySelector('.FmlUF') ||
window.location.pathname.includes('/stories') ||
!!document.querySelector('[data-test="stories-player-continue"]') ||
!!document.querySelector('[data-test="stories-player-done"]') ||
!!document.querySelector('[data-test="story-start"]');
}
function findStoryReact(dom) {
if (!dom) return null;
// Try traverseUp=0 first (story layout), then 1
for (const up of [0, 1, 2]) {
try {
const key = Object.keys(dom).find(k =>
k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
);
if (!key) continue;
const domFiber = dom[key];
if (!domFiber) continue;
const GetCompFiber = fiber => {
let p = fiber.return;
while (typeof p.type === "string") p = p.return;
return p;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < up; i++) compFiber = GetCompFiber(compFiber);
const stateNode = compFiber?.stateNode;
if (stateNode?.props?.currentChallenge || stateNode?.props?.challenge) return stateNode;
} catch {}
}
return null;
}
async function solveStoryLesson() {
let waited = 0;
while (!isStoryLesson() && waited < 10000 && lessonSolving) {
await sleep(300);
waited += 300;
}
if (!lessonSolving) return;
const startBtn = document.querySelector('[data-test="story-start"]');
if (startBtn) {
startBtn.click();
await sleep(800);
}
await new Promise(resolve => {
let ticks = 0;
const MAX_TICKS = 300;
let solving = false;
const iv = setInterval(async () => {
try {
if (!lessonSolving) { clearInterval(iv); resolve(); return; }
if (++ticks > MAX_TICKS) { clearInterval(iv); resolve(); return; }
const done = document.querySelector('[data-test="stories-player-done"]') &&
!document.querySelector('[data-test="stories-player-done"]').disabled;
const sessionOver = document.querySelector('[data-test="session-over"]') ||
document.querySelector('[data-test="session-complete-slide"]') ||
document.querySelector('[data-test="session-complete"]');
if (sessionOver) {
clearInterval(iv);
currentLessonCount++;
try {
const s = JSON.parse(sessionStorage.getItem('dg_practice') || '{}');
s.done = currentLessonCount;
sessionStorage.setItem('dg_practice', JSON.stringify(s));
} catch {}
await sleep(500);
resolve();
return;
}
if (solving) return;
const skipSels = [
'[data-test="practice-hub-ad-no-thanks-button"]',
'[data-test="plus-no-thanks"]',
'.vpDIE',
'._1N-oo._36Vd3._16r-S._1ZBYz._23KDq._1S2uf.HakPM'
];
skipSels.forEach(sel => document.querySelector(sel)?.click());
const storyEl = document.querySelector('.FmlUF') ||
document.querySelector('[data-test="stories-player-continue"]')?.closest('div') ||
document.querySelector('[class*="story"]');
let challengeEl = document.querySelector('._3yE3H') ||
document.querySelector('.RMEuZ._1GVfY') ||
document.querySelector('[data-test="challenge"]') ||
document.querySelector('.FmlUF');
if (challengeEl) {
const ri = findStoryReact(challengeEl);
window.sol = ri?.props?.currentChallenge || ri?.props?.challenge;
}
const ct = solver.determineType();
if (!ct) {
const contBtn = document.querySelector('[data-test="stories-player-continue"]');
const nextBtn = document.querySelector('[data-test="player-next"]');
if (contBtn && !contBtn.disabled) {
contBtn.click();
} else if (nextBtn && nextBtn.getAttribute('aria-disabled') !== 'true' && !nextBtn.disabled) {
nextBtn.click();
}
return;
}
solving = true;
try {
await solver.handle(ct);
} catch (e) {
console.error('Story solve error:', e);
}
await sleep(300);
const doneBtn = document.querySelector('[data-test="stories-player-done"]');
const contBtn = document.querySelector('[data-test="stories-player-continue"]');
const nextBtn = document.querySelector('[data-test="player-next"]');
if (doneBtn && !doneBtn.disabled) {
doneBtn.click();
clearInterval(iv);
currentLessonCount++;
try {
const s = JSON.parse(sessionStorage.getItem('dg_practice') || '{}');
s.done = currentLessonCount;
sessionStorage.setItem('dg_practice', JSON.stringify(s));
} catch {}
await sleep(500);
resolve();
return;
} else if (contBtn && !contBtn.disabled) {
contBtn.click();
} else if (nextBtn && nextBtn.getAttribute('aria-disabled') !== 'true' && !nextBtn.disabled) {
nextBtn.click();
}
await sleep(500);
solving = false;
} catch (e) {
console.error('Story tick error:', e);
solving = false;
}
}, 500);
setTimeout(() => { clearInterval(iv); resolve(); }, 150000);
});
}
async function solveCurrentLesson() {
await sleep(800);
if (isStoryLesson()) {
await solveStoryLesson();
} else {
let waited = 0;
while (!document.querySelector('[data-test="challenge"]') && !document.querySelector('._3yE3H') && waited < 10000 && lessonSolving) {
await sleep(500); waited += 500;
}
if (!lessonSolving) return;
await new Promise(resolve => {
let solving = false, ticks = 0;
const MAX = 240;
const clickNext = () => {
const nb = document.querySelector('[data-test="player-next"]') ||
document.querySelector('[data-test="stories-player-continue"]') ||
document.querySelector('[data-test="stories-player-done"]');
if (!nb || nb.getAttribute('aria-disabled') === 'true' || nb.disabled) return;
nb.click();
};
const iv = setInterval(async () => {
try {
if (!lessonSolving) { clearInterval(iv); resolve(); return; }
if (++ticks > MAX) { clearInterval(iv); resolve(); return; }
const done = document.querySelector('[data-test="session-over"]') ||
document.querySelector('[data-test="session-complete-slide"]') ||
document.querySelector('[data-test="session-complete"]') ||
document.querySelector('[data-test="player-practice-again"]');
if (done) {
clearInterval(iv); currentLessonCount++;
try { const s = JSON.parse(sessionStorage.getItem('dg_practice') || '{}'); s.done = currentLessonCount; sessionStorage.setItem('dg_practice', JSON.stringify(s)); } catch {}
await sleep(500); resolve(); return;
}
if (solving) return;
window.sol = solver.getSolution();
if (!window.sol) { clickNext(); return; }
const type = solver.determineType();
if (!type) { clickNext(); return; }
solving = true;
try { await solver.handle(type); } catch {}
await sleep(350); clickNext(); await sleep(600); solving = false;
} catch { solving = false; }
}, 500);
setTimeout(() => { clearInterval(iv); resolve(); }, 180000);
});
}
if (!lessonSolving) return;
if (lessonsToSolve > 0 && currentLessonCount >= lessonsToSolve) {
notif('Practice farm done! ' + currentLessonCount + ' lessons.');
stopPractice();
return;
}
notif('Done ' + currentLessonCount + (lessonsToSolve > 0 ? ' / ' + lessonsToSolve : '') + ' — loading next…', 2);
await sleep(800);
if (lessonSolving) window.location.assign('/practice');
}
function stopPractice() {
lessonSolving = false;
const btn = document.getElementById('DG_Practice_Btn');
if (btn) { btn.style.background = ''; btn.style.color = ''; btn.textContent = 'Practice Farm'; }
}
function resumePracticeIfNeeded() {
const saved = sessionStorage.getItem('dg_practice');
if (!saved) return;
try {
const { active, count, done } = JSON.parse(saved);
if (!active) return;
lessonsToSolve = count;
currentLessonCount = done;
sessionStorage.setItem('dg_practice', JSON.stringify({ active: true, count, done }));
if (window.location.pathname.startsWith('/practice')) {
lessonSolving = true;
if (user) {
const btn = document.getElementById('DG_Practice_Btn');
if (btn) { btn.style.background = 'rgb(var(--DG-red))'; btn.style.color = '#fff'; btn.textContent = 'STOP'; }
solveCurrentLesson();
} else {
const check = setInterval(() => {
if (user) {
clearInterval(check);
const btn = document.getElementById('DG_Practice_Btn');
if (btn) { btn.style.background = 'rgb(var(--DG-red))'; btn.style.color = '#fff'; btn.textContent = 'STOP'; }
solveCurrentLesson();
}
}, 500);
}
}
} catch {}
}
let solverObserver = null;
function initSolverObserver() {
if (solverObserver) solverObserver.disconnect();
const checkAndInject = () => {
const inLesson = window.location.pathname.includes('/lesson') ||
window.location.pathname.includes('/practice') ||
window.location.pathname.includes('/practice-hub') ||
window.location.pathname.includes('/stories');
const hasTarget = !!document.querySelector('[data-test="player-next"],[data-test="stories-player-continue"],[data-test="stories-player-done"],[data-test="story-start"]');
if (inLesson && solverEnabled && hasTarget && showSolveButtons) {
if (!solverUI) solver.createUI();
} else if (!inLesson || !solverEnabled) {
solver.removeUI();
}
};
solverObserver = new MutationObserver(() => { checkAndInject(); });
solverObserver.observe(document.body, { childList: true, subtree: true });
const origPush = history.pushState, origReplace = history.replaceState;
history.pushState = function(){origPush.apply(this,arguments);checkAndInject();};
history.replaceState = function(){origReplace.apply(this,arguments);checkAndInject();};
window.addEventListener('popstate', checkAndInject);
checkAndInject();
}
initSolverObserver();
function installFakeSuper() {
if (localStorage.getItem('dg_super') !== 'true') return;
const RX = /https:\/\/www\.duolingo\.com\/\d{4}-\d{2}-\d{2}\/users\/.+/;
const ITEMS = { gold_subscription: { itemName: 'gold_subscription', subscriptionInfo: { vendor: 'STRIPE', renewing: true, isFamilyPlan: true, expectedExpiration: 9999999999000 } } };
function mod(j) {
try {
const d = JSON.parse(j);
d.hasPlus = true;
if (!d.trackingProperties) d.trackingProperties = {};
d.trackingProperties.has_item_gold_subscription = true;
d.shopItems = { ...d.shopItems, ...ITEMS };
return JSON.stringify(d);
} catch { return j; }
}
const uw = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
const orig = uw.fetch;
uw.fetch = function(resource, options) {
const url = resource instanceof Request ? resource.url : resource;
const method = resource instanceof Request ? resource.method : (options?.method || 'GET');
if (method.toUpperCase() === 'GET' && RX.test(url) && !url.includes('/shop-items')) {
return orig.apply(this, arguments).then(async r => {
const text = await r.clone().text();
return new Response(mod(text), { status: r.status, statusText: r.statusText, headers: new Headers(r.headers) });
});
}
return orig.apply(this, arguments);
};
}
installFakeSuper();
const dgRoot = document.getElementById('DG_Root');
let isDark = localStorage.getItem('dg_dark') === 'true';
function applyDarkMode() {
if (isDark) {
dgRoot.classList.add('DG_Dark');
document.getElementById('DG_Dark_Icon_Moon').style.display = 'none';
document.getElementById('DG_Dark_Icon_Sun').style.display = 'block';
} else {
dgRoot.classList.remove('DG_Dark');
document.getElementById('DG_Dark_Icon_Moon').style.display = 'block';
document.getElementById('DG_Dark_Icon_Sun').style.display = 'none';
}
}
applyDarkMode();
document.getElementById('DG_Dark_Btn').addEventListener('click', () => {
isDark = !isDark;
localStorage.setItem('dg_dark', isDark ? 'true' : 'false');
applyDarkMode();
});
document.getElementById('DG_Hide_Btn').addEventListener('click', (e) => {
if (window._dgDragMoved) return;
hidden = !hidden;
const box = document.getElementById('DG_Main_Box');
const btn = document.getElementById('DG_Hide_Btn');
const label = document.getElementById('DG_Hide_Label');
const hideables = document.querySelectorAll('.DG_Hideable');
box.style.transition = 'opacity 0.8s cubic-bezier(0.16, 1, 0.32, 1), filter 0.8s cubic-bezier(0.16, 1, 0.32, 1)';
if (hidden) {
box.style.opacity = '0';
box.style.filter = 'blur(8px)';
box.style.pointerEvents = 'none';
hideables.forEach(el => el.classList.add('DG_Header_Hidden'));
label.textContent = 'Show';
btn.classList.add('DG_Hide_Btn_Hidden');
} else {
box.style.opacity = '';
box.style.filter = '';
box.style.pointerEvents = '';
hideables.forEach(el => el.classList.remove('DG_Header_Hidden'));
label.textContent = 'Hide';
btn.classList.remove('DG_Hide_Btn_Hidden');
}
setTimeout(() => { box.style.transition = ''; }, 800);
});
(function() {
const main = document.getElementById('DG_Main');
let dragging = false, startX, startY, origRight, origBottom;
window._dgDragMoved = false;
main.addEventListener('mousedown', (e) => {
const hideBtn = document.getElementById('DG_Hide_Btn');
if (!hideBtn.contains(e.target) && e.target !== hideBtn) return;
dragging = true;
window._dgDragMoved = false;
const rect = main.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
origRight = window.innerWidth - rect.right;
origBottom = window.innerHeight - rect.bottom;
main.classList.add('DG_Dragging');
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) window._dgDragMoved = true;
const newRight = Math.max(0, Math.min(window.innerWidth - 50, origRight - dx));
const newBottom = Math.max(0, Math.min(window.innerHeight - 50, origBottom - dy));
main.style.right = newRight + 'px';
main.style.bottom = newBottom + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
main.classList.remove('DG_Dragging');
localStorage.setItem('dg_pos', JSON.stringify({ right: main.style.right, bottom: main.style.bottom }));
setTimeout(() => { window._dgDragMoved = false; }, 50);
});
try {
const saved = JSON.parse(localStorage.getItem('dg_pos') || 'null');
if (saved && saved.right && saved.bottom) {
main.style.right = saved.right;
main.style.bottom = saved.bottom;
}
} catch {}
})();
document.getElementById('DG_Settings_Btn').addEventListener('click', () => switchPage('Settings'));
document.getElementById('DG_Settings_Back_Btn').addEventListener('click', () => switchPage('Main'));
const xpI = document.getElementById('DG_XP_Input'), xpB = document.getElementById('DG_XP_Btn');
xpI.addEventListener('input', () => { xpB.disabled = !user || !xpI.value || +xpI.value < 30; });
xpB.addEventListener('click', () => {
if (running && task === 'xp') { run('xp', 0); return; }
if (running) { notif('Stop current farm first.'); return; }
const v = +xpI.value;
if (v < 30) { notif('Min 30 XP'); return; }
run('xp', v);
});
xpI.addEventListener('keydown', e => { if (e.key === 'Enter' && !xpB.disabled) xpB.click(); });
document.getElementById('DG_Gem_Btn').addEventListener('click', () => {
if (running && task === 'gems') { run('gems', 0); return; }
if (running) { notif('Stop current farm first.'); return; }
run('gems', 0);
});
const stI = document.getElementById('DG_Streak_Input'), stB = document.getElementById('DG_Streak_Btn');
stI.addEventListener('input', () => { stB.disabled = !user || !stI.value || +stI.value < 1; });
stB.addEventListener('click', () => {
if (running && task === 'streak') { run('streak', 0); return; }
if (running) { notif('Stop current farm first.'); return; }
const v = +stI.value;
if (v < 1) return;
run('streak', v);
});
stI.addEventListener('keydown', e => { if (e.key === 'Enter' && !stB.disabled) stB.click(); });
document.getElementById('DG_Practice_Btn').addEventListener('click', () => {
if (lessonSolving) {
stopPractice();
sessionStorage.removeItem('dg_practice');
notif('Practice farm stopped.');
return;
}
if (running) { notif('Stop current farm first.'); return; }
if (!user) { notif('Please wait for connection.'); return; }
sessionStorage.setItem('dg_practice', JSON.stringify({ active: true, count: 0, done: 0 }));
farmPractice(0);
});
document.getElementById('DG_Super_Btn').addEventListener('click', () => {
localStorage.setItem('dg_super', localStorage.getItem('dg_super') === 'true' ? 'false' : 'true');
notif(localStorage.getItem('dg_super') === 'true' ? 'Free Super activated! Reload page.' : 'Free Super deactivated. Reload page.');
});
document.getElementById('DG_Shop_Btn').addEventListener('click', () => {
switchPage('Shop');
loadShop();
});
document.getElementById('DG_Shop_Back_Btn').addEventListener('click', () => switchPage('Main'));
document.getElementById('DG_League_Btn').addEventListener('click', () => {
if (running && task === 'league') { run('league', 0); return; }
if (running) { notif('Stop current farm first.'); return; }
run('league', 0);
});
document.getElementById('DG_Quest_Btn').addEventListener('click', async () => {
if (running) { notif('Stop current farm first.'); return; }
await farmDailyQuest();
});
document.getElementById('DG_MonthlyQuest_Btn').addEventListener('click', () => {
switchPage('MonthlyQuests');
loadMonthlyQuests();
});
document.getElementById('DG_MQ_Back_Btn').addEventListener('click', () => switchPage('Main'));
document.getElementById('DG_MQ_ClaimAll_Btn').addEventListener('click', () => claimAllMonthly());
document.getElementById('DG_AccMgr_Btn').addEventListener('click', () => {
switchPage('Accounts');
renderAccounts();
});
document.getElementById('DG_AccMgr_Back_Btn').addEventListener('click', () => switchPage('Main'));
document.getElementById('DG_AccSave_Btn').addEventListener('click', () => accSaveCurrent());
const solverT = document.getElementById('DG_Solver_Toggle');
solverT.checked = solverEnabled;
solverT.addEventListener('change', () => {
solverEnabled = solverT.checked;
localStorage.setItem('dg_solver', solverEnabled ? 'true' : 'false');
if (!solverEnabled) solver.removeUI();
});
const box = document.getElementById('DG_Main_Box');
box.style.opacity = '0';
box.style.filter = 'blur(8px)';
setTimeout(() => {
box.style.transition = 'opacity 0.8s cubic-bezier(0.16, 1, 0.32, 1), filter 0.8s cubic-bezier(0.16, 1, 0.32, 1)';
box.style.opacity = '';
box.style.filter = '';
setTimeout(() => { box.style.transition = ''; }, 800);
}, 300);
resumePracticeIfNeeded();
checkForUpdate();
setTimeout(connect, 600);
})();