Greasy Fork is available in English.
Uživatelský skript pro automatické farmení XP, drahokamů a sérií v Duolingu. Funguje s Tampermonkey a Greasemonkey.
// ==UserScript==
// @name Duolingo DuoHacker Pro
// @name:en Duolingo DuoHacker — #1 Auto XP Farm, Streak & Gem Farming 2026
// @name:zh-CN Duolingo DuoHacker — 自动刷经验 宝石 连胜 农场工具
// @name:zh-TW Duolingo DuoHacker — 自動刷經驗 寶石 連勝 農場工具
// @name:ja Duolingo DuoHacker — XP・宝石・ストリーク自動ファーミングツール
// @name:es Duolingo DuoHacker — Script para farmear XP, gemas y rachas en Duolingo
// @name:es-MX Duolingo DuoHacker — Script para farmear XP, gemas y rachas en Duolingo
// @name:es-AR Duolingo DuoHacker — Script para farmear XP, gemas y rachas en Duolingo
// @name:ru Duolingo DuoHacker — Скрипт фарма XP, самоцветов и серий Duolingo
// @name:uk Duolingo DuoHacker — Скрипт фарму XP, самоцвітів та серій Duolingo
// @name:pt-BR Duolingo DuoHacker — Script para farmar XP, gemas e sequências no Duolingo
// @name:pt Duolingo DuoHacker — Script para farmar XP, gemas e sequências no Duolingo
// @name:de Duolingo DuoHacker — Skript zum Farmen von XP, Gems und Streaks
// @name:de-AT Duolingo DuoHacker — Skript zum Farmen von XP, Gems und Streaks
// @name:de-CH Duolingo DuoHacker — Skript zum Farmen von XP, Gems und Streaks
// @name:it Duolingo DuoHacker — Script per fare farming di XP, gemme e streak su Duolingo
// @name:ko Duolingo DuoHacker — XP, 보석, 스트릭 자동 파밍 스크립트
// @name:hi Duolingo DuoHacker — XP, जेम्स और स्ट्रीक फार्मिंग स्क्रिप्ट
// @name:bn Duolingo DuoHacker — XP, জেম এবং স্ট্রিক ফার্মিং স্ক্রিপ্ট
// @name:ar Duolingo DuoHacker — سكريبت زراعة XP والجواهر والسلاسل في Duolingo
// @name:fa Duolingo DuoHacker — اسکریپت فارم XP، جواهرات و استریک Duolingo
// @name:tr Duolingo DuoHacker — Duolingo XP, Gem ve Streak Farming Scripti
// @name:pl Duolingo DuoHacker — Skrypt do farmienia XP, klejnotów i passy w Duolingo
// @name:vi Duolingo DuoHacker — Script Tự Động Farm XP, Gem và Streak Duolingo
// @name:th Duolingo DuoHacker — สคริปต์ฟาร์ม XP เพชร และสตรีค Duolingo อัตโนมัติ
// @name:id Duolingo DuoHacker — Script Farm XP, Gem, dan Streak Duolingo Otomatis
// @name:fr Duolingo DuoHacker — Script de farming XP, gemmes et séries Duolingo
// @name:fr-CA Duolingo DuoHacker — Script de farming XP, gemmes et séries Duolingo
// @name:fr-BE Duolingo DuoHacker — Script de farming XP, gemmes et séries Duolingo
// @name:fr-CH Duolingo DuoHacker — Script de farming XP, gemmes et séries Duolingo
// @name:nl Duolingo DuoHacker — Script voor XP, Gems en Streaks farmen in Duolingo
// @name:nl-BE Duolingo DuoHacker — Script voor XP, Gems en Streaks farmen in Duolingo
// @name:da Duolingo DuoHacker — Script til farming af XP, gems og streaks i Duolingo
// @name:sv Duolingo DuoHacker — Skript för farming av XP, gems och streaks i Duolingo
// @name:no Duolingo DuoHacker — Skript for farming av XP, gems og streaks i Duolingo
// @name:fi Duolingo DuoHacker — Skripti XP-, gem- ja streak-farmingiin Duolingossa
// @name:cs Duolingo DuoHacker — Skript pro farmení XP, drahokamů a sérií v Duolingu
// @name:sk Duolingo DuoHacker — Skript na farmenie XP, drahokamov a sérií v Duolingu
// @name:hu Duolingo DuoHacker — Szkript XP, drágakő és sorozat farmingjához Duolingóban
// @name:ro Duolingo DuoHacker — Script pentru farmat XP, pietre prețioase și serii Duolingo
// @name:el Duolingo DuoHacker — Σκριπτ για farming XP, πετράδια και σερί Duolingo
// @name:he Duolingo DuoHacker — סקריפט לחקלאות XP, אבני חן וסדרות ב-Duolingo
// @name:ca Duolingo DuoHacker — Script per fer farming de XP, gemmes i rachas a Duolingo
// @name:gl Duolingo DuoHacker — Script para farmear XP, xemas e rachas en Duolingo
// @name:eu Duolingo DuoHacker — Duolingo XP, harri bitxi eta streak farming script-a
// @name:sq Duolingo DuoHacker — Skript për farming XP, guri i çmuar dhe seria Duolingo
// @name:hr Duolingo DuoHacker — Skripta za farmanje XP-a, dragulja i serija u Duolingu
// @name:sr Duolingo DuoHacker — Скрипта за фармање XP, драгуља и серија у Duolingu
// @name:bg Duolingo DuoHacker — Скрипт за фармене на XP, скъпоценни камъни и серии
// @name:sl Duolingo DuoHacker — Skripta za farmanje XP, draguljev in serij v Duolingu
// @name:lt Duolingo DuoHacker — Duolingo XP, brangakmenių ir serijų ūkininkavimo skriptas
// @name:lv Duolingo DuoHacker — Duolingo XP, dārgakmeņu un sēriju lauksaimniecības skripts
// @name:et Duolingo DuoHacker — Duolingo XP, vääriskivide ja seeriate farmimise skript
// @name:sw Duolingo DuoHacker — Skripti ya kulima XP, Gems na Streaks ya Duolingo
// @name:ms Duolingo DuoHacker — Skrip Ladang XP, Permata dan Streak Duolingo Automatik
// @name:fil Duolingo DuoHacker — Script para sa Awtomatikong XP, Gem at Streak Farming
// @name:tl Duolingo DuoHacker — Script para sa Awtomatikong XP, Gem at Streak Farming
// @description Duolingo userscript to farm XP, Gems and Streaks automatically. Works with Tampermonkey and Greasemonkey.
// @description:en Duolingo userscript to farm XP, Gems and Streaks automatically. Works with Tampermonkey and Greasemonkey.
// @description:zh-CN 多邻国自动脚本,支持油猴插件,自动刷经验值(XP)、宝石和连胜。兼容 Tampermonkey 和 Greasemonkey。
// @description:zh-TW 多鄰國自動腳本,支援油猴插件,自動刷經驗值(XP)、寶石和連勝。相容 Tampermonkey 和 Greasemonkey。
// @description:ja Duolingo の XP、宝石、ストリークを自動でファーミングするユーザースクリプト。Tampermonkey・Greasemonkey 対応。
// @description:es Script de usuario para farmear XP, gemas y rachas en Duolingo de forma automática. Compatible con Tampermonkey y Greasemonkey.
// @description:es-MX Script para farmear XP, gemas y rachas en Duolingo automáticamente. Compatible con Tampermonkey y Greasemonkey.
// @description:es-AR Script para farmear XP, gemas y rachas en Duolingo de forma automática. Compatible con Tampermonkey y Greasemonkey.
// @description:ru Пользовательский скрипт для автоматического фарма XP, самоцветов и серий в Duolingo. Работает с Tampermonkey и Greasemonkey.
// @description:uk Користувацький скрипт для автоматичного фарму XP, самоцвітів та серій у Duolingo. Підтримує Tampermonkey і Greasemonkey.
// @description:pt-BR Script de usuário para farmar XP, gemas e sequências no Duolingo automaticamente. Compatível com Tampermonkey e Greasemonkey.
// @description:pt Script de utilizador para farmar XP, gemas e sequências no Duolingo automaticamente. Compatível com Tampermonkey e Greasemonkey.
// @description:de Userscript zum automatischen Farmen von XP, Gems und Streaks in Duolingo. Kompatibel mit Tampermonkey und Greasemonkey.
// @description:de-AT Userscript zum automatischen Farmen von XP, Gems und Streaks in Duolingo. Kompatibel mit Tampermonkey und Greasemonkey.
// @description:de-CH Userscript zum automatischen Farmen von XP, Gems und Streaks in Duolingo. Kompatibel mit Tampermonkey und Greasemonkey.
// @description:it Script utente per fare farming automatico di XP, gemme e streak su Duolingo. Compatibile con Tampermonkey e Greasemonkey.
// @description:ko Duolingo에서 XP, 보석, 스트릭을 자동으로 파밍하는 유저스크립트. Tampermonkey 및 Greasemonkey 지원.
// @description:hi Duolingo में XP, जेम्स और स्ट्रीक्स को अपने आप फार्म करने वाली यूजरस्क्रिप्ट। Tampermonkey और Greasemonkey के साथ काम करती है।
// @description:bn Duolingo-তে XP, রত্ন এবং স্ট্রিক স্বয়ংক্রিয়ভাবে ফার্ম করার ইউজারস্ক্রিপ্ট। Tampermonkey ও Greasemonkey সমর্থিত।
// @description:ar سكريبت مستخدم لزراعة XP والجواهر والسلاسل تلقائياً في Duolingo. يعمل مع Tampermonkey وGreasemonkey.
// @description:fa اسکریپت کاربری برای فارم خودکار XP، جواهرات و استریک در Duolingo. سازگار با Tampermonkey و Greasemonkey.
// @description:tr Duolingo'da XP, gem ve streak'leri otomatik olarak farmlayan kullanıcı scripti. Tampermonkey ve Greasemonkey ile çalışır.
// @description:pl Skrypt użytkownika do automatycznego farmienia XP, klejnotów i passy w Duolingo. Działa z Tampermonkey i Greasemonkey.
// @description:vi Userscript tự động farm XP, Gem và Streak trong Duolingo. Hoạt động với Tampermonkey và Greasemonkey.
// @description:th ยูเซอร์สคริปต์สำหรับฟาร์ม XP, เพชร และสตรีคใน Duolingo อัตโนมัติ รองรับ Tampermonkey และ Greasemonkey
// @description:id Userscript untuk farming XP, Gem, dan Streak di Duolingo secara otomatis. Kompatibel dengan Tampermonkey dan Greasemonkey.
// @description:fr Script utilisateur pour farmer automatiquement les XP, gemmes et séries dans Duolingo. Compatible avec Tampermonkey et Greasemonkey.
// @description:fr-CA Script utilisateur pour farmer automatiquement les XP, gemmes et séries dans Duolingo. Compatible avec Tampermonkey et Greasemonkey.
// @description:fr-BE Script utilisateur pour farmer automatiquement les XP, gemmes et séries dans Duolingo. Compatible avec Tampermonkey et Greasemonkey.
// @description:fr-CH Script utilisateur pour farmer automatiquement les XP, gemmes et séries dans Duolingo. Compatible avec Tampermonkey et Greasemonkey.
// @description:nl Gebruikersscript om automatisch XP, gems en streaks te farmen in Duolingo. Werkt met Tampermonkey en Greasemonkey.
// @description:nl-BE Gebruikersscript om automatisch XP, gems en streaks te farmen in Duolingo. Werkt met Tampermonkey en Greasemonkey.
// @description:da Brugerscript til automatisk farming af XP, gems og streaks i Duolingo. Virker med Tampermonkey og Greasemonkey.
// @description:sv Användarskript för automatisk farming av XP, gems och streaks i Duolingo. Fungerar med Tampermonkey och Greasemonkey.
// @description:no Brukerskript for automatisk farming av XP, gems og streaks i Duolingo. Fungerer med Tampermonkey og Greasemonkey.
// @description:fi Käyttäjäskripti XP:n, jalokivien ja streakkien automaattiseen farmingiin Duolingossa. Toimii Tampermonkeyn ja Greasemonkeyn kanssa.
// @description:cs Uživatelský skript pro automatické farmení XP, drahokamů a sérií v Duolingu. Funguje s Tampermonkey a Greasemonkey.
// @description:sk Používateľský skript na automatické farmenie XP, drahokamov a sérií v Duolingu. Funguje s Tampermonkey a Greasemonkey.
// @description:hu Felhasználói szkript az XP, drágakövek és sorozatok automatikus farmingjához Duolingóban. Működik Tampermonkey-jal és Greasemonkey-jal.
// @description:ro Script de utilizator pentru farmarea automată a XP, pietrelor prețioase și seriilor în Duolingo. Compatibil cu Tampermonkey și Greasemonkey.
// @description:el Σκριπτ χρήστη για αυτόματο farming XP, πετραδιών και σερί στο Duolingo. Λειτουργεί με Tampermonkey και Greasemonkey.
// @description:he סקריפט משתמש לחקלאות אוטומטית של XP, אבני חן וסדרות ב-Duolingo. עובד עם Tampermonkey ו-Greasemonkey.
// @description:ca Script d'usuari per fer farming automàtic de XP, gemmes i rachas a Duolingo. Compatible amb Tampermonkey i Greasemonkey.
// @description:gl Script de usuario para farmear XP, xemas e rachas en Duolingo de forma automática. Compatible con Tampermonkey e Greasemonkey.
// @description:eu Duolingo-n XP, harri bitxi eta streakak automatikoki farmatzeko erabiltzaile-scripta. Tampermonkey eta Greasemonkey-rekin bateragarria.
// @description:sq Skript përdoruesi për farming automatik të XP, gurëve të çmuar dhe serive në Duolingo. Punon me Tampermonkey dhe Greasemonkey.
// @description:hr Korisnička skripta za automatsko farmanje XP-a, dragulja i serija u Duolingu. Radi s Tampermonkey i Greasemonkey.
// @description:sr Корисничка скрипта за аутоматско фармање XP-а, драгуља и серија у Duolingu. Ради са Tampermonkey и Greasemonkey.
// @description:bg Потребителски скрипт за автоматично фармене на XP, скъпоценни камъни и серии в Duolingo. Работи с Tampermonkey и Greasemonkey.
// @description:sl Uporabniška skripta za avtomatsko farmanje XP-ja, draguljev in serij v Duolingu. Deluje s Tampermonkey in Greasemonkey.
// @description:lt Naudotojo skriptas automatiniam XP, brangakmenių ir serijos ūkininkavimui Duolingo. Veikia su Tampermonkey ir Greasemonkey.
// @description:lv Lietotāja skripts automātiskai XP, dārgakmeņu un sēriju lauksaimniecībai Duolingo. Darbojas ar Tampermonkey un Greasemonkey.
// @description:et Kasutajaskript XP-i, vääriskivide ja seeriate automaatseks farmimiseks Duolingos. Töötab Tampermonkey ja Greasemonkey'ga.
// @description:sw Skripti ya mtumiaji kwa kulima XP, Gems na Streaks kiotomatiki katika Duolingo. Inafanya kazi na Tampermonkey na Greasemonkey.
// @description:ms Skrip pengguna untuk ladang XP, Permata dan Streak secara automatik dalam Duolingo. Serasi dengan Tampermonkey dan Greasemonkey.
// @description:fil Userscript para sa awtomatikong pag-farm ng XP, Gems at Streaks sa Duolingo. Gumagana sa Tampermonkey at Greasemonkey.
// @description:tl Userscript para sa awtomatikong pag-farm ng XP, Gems at Streaks sa Duolingo. Gumagana sa Tampermonkey at Greasemonkey.
// @namespace https://twisk.fun/install
// @version 2.4
// @author 2pixel
// @match https://*.duolingo.com/*
// @match https://*.duolingo.cn/*
// @icon https://github.com/FutureCLI/DuoHacker/blob/main/images/Logo_TypePNG_DuoHacker.png?raw=true
// @run-at document-start
// @grant GM_xmlhttpRequest
// @connect duome.eu
// @license MIT
// ==/UserScript==
let SOLVE_SPEED = 0.1;
// duo max
const _DH_origFetch = window.fetch.bind(window);
const _DH_origXhrOpen = XMLHttpRequest.prototype.open;
const _DH_origXhrSend = XMLHttpRequest.prototype.send;
//
const VERSION = "2.7";
let CUSTOM_DELAY = parseInt(localStorage.getItem('duohacker_custom_delay') || '500', 10);
const STORAGE_KEY = 'duohacker_accounts';
const AVATAR_KEY_PREFIX = 'duohacker_avatar_';
const normalizeAvatarUrl = (url) => {
if (!url || typeof url !== 'string') return '';
let u = url.trim();
if (!/^https?:\/\//i.test(u)) {
if (u.startsWith('//')) u = 'https:' + u;
else if (u.startsWith('/')) u = 'https://www.duolingo.com' + u;
else u = 'https:' + u;
}
u = u.replace(/\/(medium|large|small)$/, '/xlarge');
if (!u.endsWith('/xlarge') && u.includes('duolingo.com/ssr-avatars')) {
u += '/xlarge';
}
return u;
};
const getStoredAvatarUrl = (username) => {
if (!username) return '';
return normalizeAvatarUrl(localStorage.getItem(`${AVATAR_KEY_PREFIX}${username}`) || '');
};
const setStoredAvatarUrl = (username, url) => {
if (!username) return;
try {
localStorage.setItem(`${AVATAR_KEY_PREFIX}${username}`, normalizeAvatarUrl(url) || '');
} catch (e) {}
};
const getAccountAvatarUrl = (account) => {
const fromAccount = normalizeAvatarUrl(account?.picture || '');
return fromAccount || getStoredAvatarUrl(account?.username);
};
const getAccountAvatarHTML = (account, size = 20) => {
const url = getAccountAvatarUrl(account);
return url
? `<img src="${url}" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;" draggable="false">`
: `<span style="font-size: ${size}px;">👤</span>`;
};
const shopIcons = {
xp: "https://d35aaqx5ub95lt.cloudfront.net/images/icons/68c1fd0f467456a4c607ecc0ac040533.svg",
streak: "https://d35aaqx5ub95lt.cloudfront.net/images/icons/216ddc11afcbb98f44e53d565ccf479e.svg",
heart: "https://d35aaqx5ub95lt.cloudfront.net/images/hearts/547ffcf0e6256af421ad1a32c26b8f1a.svg",
gem: "https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg",
outfit: "https://d35aaqx5ub95lt.cloudfront.net/vendor/0cecd302cf0bcd0f73d51768feff75fe.svg",
free: "https://d35aaqx5ub95lt.cloudfront.net/images/super/11db6cd6f69cb2e3c5046b915be8e669.svg",
misc: "https://d35aaqx5ub95lt.cloudfront.net/images/leagues/9fadb349c2ece257386a0e576359c867.svg"
};
const DUOME_API_URL = "https://duome.eu/aggiorna.php";
const SESSION_KEY = 'duohacker_session';
var jwt, defaultHeaders, userInfo, sub;
let isAutoMode = false;
let solvingIntervalId = null;
let solverUI = null;
let isInLesson = false;
let lessonSolving = false;
window.INJECT_SOLVER_ENABLED = localStorage.getItem('duohacker_inject_solver') === 'true';
window.autoSolveEnabled = localStorage.getItem('duohacker_auto_solve') === 'true';
window.LESSON_SHORTNER_ENABLED = localStorage.getItem('duohacker_lesson_shortner') === 'true';
window.hideAnimationEnabled = localStorage.getItem('duohacker_hide_animation') === 'true';
let hideImageInterval = null;
let isRunning = false;
let currentMode = 'safe';
let hideObserver = null;
let currentTheme = 'dark';
localStorage.setItem('duofarmer_theme', 'dark');
let hiddenElements = new Map();
let hasJoined = localStorage.getItem('duofarmer_joined') === 'true';
const isMobile = /Android|iPhone|iPad|iPod|Mobile|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
let storedLiteMode = localStorage.getItem('duohacker_lite_mode');
let liteMode = storedLiteMode === null ? false : storedLiteMode === 'true';
let webhookUrl = localStorage.getItem('duohacker_webhook_url') || '';
if (isMobile) {
liteMode = true;
localStorage.setItem('duohacker_lite_mode', 'true');
} else {
if (storedLiteMode === null) {
liteMode = false;
localStorage.setItem('duohacker_lite_mode', 'false');
} else {
liteMode = storedLiteMode === 'true';
}
}
let totalEarned = {
xp: 0,
gems: 0,
streak: 0,
lessons: 0
};
let farmingStats = {
sessions: 0,
errors: 0,
startTime: null
};
let farmingInterval = null;
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
let savedAccounts = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
let duolingoMaxEnabled = localStorage.getItem('duohacker_duolingo_max') === 'true';
let sessionData = JSON.parse(localStorage.getItem(SESSION_KEY) || '{}');
let currentLessonCount = Number(sessionData.currentLessonCount ?? 0);
let lessonsToSolve = Number(sessionData.lessonsToSolve ?? 0);
let autoNameEnabled = localStorage.getItem('duohacker_auto_name') !== 'false';
let duolingoSuperEnabled = localStorage.getItem('duohacker_duolingo_super') === 'true';
let skillId = null;
const extractSkillId = (currentCourse) => {
const sections = currentCourse?.pathSectioned || [];
for (const section of sections) {
const units = section.units || [];
for (const unit of units) {
const levels = unit.levels || [];
for (const level of levels) {
const skillId = level.pathLevelMetadata?.skillId || level.pathLevelClientData?.skillId;
if (skillId) return skillId;
}
}
}
return null;
};
if (sessionData && sessionData.currentLessonCount !== undefined) {
currentLessonCount = sessionData.currentLessonCount;
lessonsToSolve = sessionData.lessonsToSolve;
window.autoSolveEnabled = sessionData.autoSolveEnabled || false;
}
const saveSessionData = () => {
sessionData = {
...sessionData,
lastActivity: new Date().toISOString(),
totalEarned,
farmingStats,
currentLessonCount,
lessonsToSolve,
autoSolveEnabled: window.autoSolveEnabled
};
localStorage.setItem(SESSION_KEY, JSON.stringify(sessionData));
};
function duomePostWho(who) {
return new Promise((resolve, reject) => {
if (!who && who !== 0) return reject(new Error("Missing who"));
const body = "who=" + encodeURIComponent(String(who).trim());
GM_xmlhttpRequest({
method: "POST",
url: DUOME_API_URL,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Accept": "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
},
data: body,
onload: (res) => {
const raw = res?.responseText ?? "";
if (!raw.trim()) return reject(new Error(`Empty response (status ${res.status})`));
try {
const json = JSON.parse(raw);
resolve({ json, raw });
} catch (e) {
reject(new Error(`Invalid JSON: ${e?.message || e}\nRAW_HEAD=${raw.slice(0, 300)}`));
}
},
onerror: () => reject(new Error("Network error calling duome.eu")),
ontimeout: () => reject(new Error("Timeout calling duome.eu")),
timeout: 15000,
});
});
}
function ensureActivityHistoryModal() {
if (document.getElementById("_activity_modal")) return;
const modal = document.createElement("div");
modal.id = "_activity_modal";
modal.className = "_modal";
modal.style.display = "none";
modal.innerHTML = `
<div class="_modal_overlay"></div>
<div class="_modal_container" style="max-width: 980px;">
<div class="_modal_header">
<h2>Activity History</h2>
<button id="_close_activity_modal" class="_close_modal_btn" title="Close">
<span style="font-size:18px;">❌</span>
</button>
</div>
<div class="_modal_content">
<div class="_settings_section">
<div class="_setting_item">
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<input id="_activity_who_input" class="_text_input" style="flex:1; min-width:220px;"
placeholder="Enter target id" />
<button id="_activity_fetch_btn" class="_setting_btn _primary" title="Fetch activity">
<span style="font-size:16px;">🔎</span> Fetch
</button>
<button id="_activity_use_me_btn" class="_setting_btn _success" title="Use current account id">
<span style="font-size:16px;">👤</span> Me
</button>
</div>
</div>
<div class="_setting_item">
<div id="_activity_summary" class="_card" style="padding:10px; border-radius:10px; border:1px solid var(--border-color); background:var(--bg-secondary);">
<div style="font-weight:700;">No data</div>
<div style="font-size:12px; color:var(--text-secondary);">Fetch to load activity history</div>
</div>
</div>
<div class="_setting_item">
<div id="_activity_list" class="_card" style="padding:10px; border-radius:10px; border:1px solid var(--border-color); background:var(--bg-secondary); max-height: 360px; overflow:auto;">
<div style="font-size:12px; color:var(--text-secondary);">Activities will show here…</div>
</div>
</div>
<div class="_setting_item">
<details>
<summary style="cursor:pointer; user-select:none;">Raw JSON</summary>
<pre id="_activity_raw" style="white-space:pre-wrap; word-break:break-word; font-size:11px; margin-top:8px;"></pre>
</details>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.querySelector("._modal_overlay").addEventListener("click", () => hideActivityModal());
document.getElementById("_close_activity_modal").addEventListener("click", () => hideActivityModal());
}
function showActivityModal() {
ensureActivityHistoryModal();
const m = document.getElementById("_activity_modal");
m.style.display = "flex";
}
function hideActivityModal() {
const m = document.getElementById("_activity_modal");
if (m) m.style.display = "none";
}
function collectEarnedXpEvents(duomeJson) {
const out = [];
const ld = duomeJson?.language_data;
if (!ld || typeof ld !== "object") return out;
for (const lang of Object.keys(ld)) {
const cal = ld?.[lang]?.calendar;
if (!Array.isArray(cal)) continue;
for (const item of cal) {
const xp = Number(item?.improvement);
const ts = Number(item?.datetime);
if (!Number.isFinite(xp) || !Number.isFinite(ts)) continue;
out.push({
xp,
ts,
lang,
event_type: item?.event_type ?? null,
skill_id: item?.skill_id ?? null,
});
}
}
out.sort((a, b) => b.ts - a.ts);
return out;
}
function renderActivityHistory(duomeJson, rawText) {
const summary = document.getElementById("_activity_summary");
const list = document.getElementById("_activity_list");
const raw = document.getElementById("_activity_raw");
const username = duomeJson?.username ?? "(unknown)";
const who = duomeJson?.id ?? "(unknown)";
const emailVerified = !!duomeJson?.emailVerified;
const totalXp = duomeJson?.totalXp ?? null;
const events = collectEarnedXpEvents(duomeJson);
summary.innerHTML = `
<div style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<div style="font-weight:800; font-size:14px;">
Username: ${escapeHtml(username)}
</div>
<div style="font-size:12px; color:var(--text-secondary);">
userId: <b>${escapeHtml(who)}</b>
</div>
<div style="font-size:12px; color:var(--text-secondary);">
Email verified: <b>${emailVerified}</b>
</div>
${totalXp != null ? `<div style="font-size:12px; color:var(--text-secondary);">Total XP: <b>${escapeHtml(totalXp)}</b></div>` : ""}
<div style="font-size:12px; color:var(--text-secondary);">
Events: <b>${events.length}</b>
</div>
</div>
`;
if (events.length === 0) {
list.innerHTML = `<div style="font-size:12px; color:var(--text-secondary);">Không thấy activity trong response.</div>`;
} else {
list.innerHTML = events.map((e) => {
const d = new Date(e.ts);
const time = isNaN(d.getTime()) ? String(e.ts) : d.toLocaleString();
return `
<div style="display:flex; justify-content:space-between; gap:12px; padding:10px 8px; border-bottom:1px solid var(--border-color);">
<div>
<div style="font-weight:800;">Earned "<span style="font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;">${escapeHtml(e.xp)}</span>" XP</div>
<div style="font-size:11px; color:var(--text-secondary);">
<span style="padding:2px 8px;border-radius:999px;border:1px solid var(--border-color);">${escapeHtml(e.lang)}</span>
${e.event_type ? `<span style="margin-left:6px;padding:2px 8px;border-radius:999px;border:1px solid var(--border-color);">${escapeHtml(e.event_type)}</span>` : ""}
${e.skill_id ? `<span style="margin-left:6px;opacity:.9;">skill_id=${escapeHtml(e.skill_id)}</span>` : ""}
</div>
</div>
<div style="font-size:11px; color:var(--text-secondary); white-space:nowrap;">${escapeHtml(time)}</div>
</div>
`;
}).join("");
}
raw.textContent = rawText || JSON.stringify(duomeJson, null, 2);
}
function escapeHtml(s) {
return String(s ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
async function fetchAndShowActivity(who) {
showActivityModal();
const summary = document.getElementById("_activity_summary");
const list = document.getElementById("_activity_list");
const raw = document.getElementById("_activity_raw");
summary.innerHTML = `<div style="font-weight:700;">Loading…</div><div style="font-size:12px; color:var(--text-secondary);">who=${escapeHtml(who)}</div>`;
list.innerHTML = `<div style="font-size:12px; color:var(--text-secondary);">Fetching duome…</div>`;
raw.textContent = "";
try {
const { json, raw: rawText } = await duomePostWho(who);
renderActivityHistory(json, rawText);
} catch (err) {
const msg = String(err?.message || err);
summary.innerHTML = `<div style="font-weight:800;">Error</div><div style="font-size:12px; color:var(--text-secondary);">${escapeHtml(msg)}</div>`;
list.innerHTML = `<div style="font-size:12px; color:var(--text-secondary);">Không load được dữ liệu.</div>`;
}
}
const sendDiscordWebhook = async (title, status, details, color = 5763719) => {
if (!webhookUrl || !webhookUrl.startsWith('http')) return;
let thumbUrl = "https://github.com/FutureCLI/DuoHacker/blob/main/images/Logo_TypePNG_DuoHacker.png?raw=true";
if (userInfo && userInfo.picture && userInfo.picture.startsWith('http') && !userInfo.picture.includes('.svg')) {
thumbUrl = userInfo.picture + '/xlarge';
}
const safeUser = (userInfo && userInfo.username) ? String(userInfo.username) : "Unknown User";
const safeStatus = String(status || "Active");
const safeTime = new Date().toLocaleTimeString();
let safeDetails = String(details || "No info");
if (safeDetails.trim().length === 0) safeDetails = "No info provided.";
const payload = {
username: "DuoHacker Webhook",
avatar_url: "https://github.com/FutureCLI/DuoHacker/blob/main/images/Logo_TypePNG_DuoHacker.png?raw=true",
embeds: [{
title: String(title),
color: color,
thumbnail: { url: thumbUrl },
fields: [
{ name: "👤 User", value: safeUser, inline: true },
{ name: "📊 Status", value: safeStatus, inline: true },
{ name: "🕒 Time", value: safeTime, inline: true },
{ name: "📝 Details", value: safeDetails, inline: false }
],
footer: {
text: "DuoHacker by 2pixel",
icon_url: "https://github.com/FutureCLI/DuoHacker/blob/main/images/Logo_TypePNG_DuoHacker.png?raw=true"
}
}]
};
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errText = await response.text();
console.error("Webhook Error:", errText);
if (title.includes("Test")) {
alert("Discord: " + errText);
}
}
} catch (e) {
console.error("Network Error:", e);
}
};
class LessonShortnerInterception {
constructor() {
this.originalFetch = window.fetch;
window.fetch = this.customFetchFunction.bind(this);
}
generateRandomHex(length) {
return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
}
customFetchFunction(resource, options) {
const url = resource instanceof Request ? resource.url : resource;
if (/\/sessions(\?.*)?$/.test(url)) {
return this.originalFetch(url, options).then(async (response) => {
const clonedResponse = response.clone();
const clonedRespText = await clonedResponse.text();
const clonedRespJson = JSON.parse(clonedRespText);
let numOfQuestions = 1;
if (clonedRespJson.type && clonedRespJson.type.startsWith('LEGENDARY')) {
numOfQuestions = 2;
}
const randomKcId = this.generateRandomHex(32);
const randomSolutionKey = this.generateRandomHex(32);
const randomGeneratorId = this.generateRandomHex(32);
const challengePayload = {
"character": { "url": "https://d2pur3iezf4d1j.cloudfront.net/images/51d3bded9ecbd8bf6e9869041c437ba9", "image": { "pdf": "https://d2pur3iezf4d1j.cloudfront.net/images/51d3bded9ecbd8bf6e9869041c437ba9", "svg": "https://d2pur3iezf4d1j.cloudfront.net/images/0f284113af41f7f7296263183701a13b" }, "gender": "MALE", "correctAnimation": "https://simg-ssl.duolingo.com/lottie/Falstaff_CORRECT_Cropped_NotBad.json", "incorrectAnimation": "https://simg-ssl.duolingo.com/lottie/Bear_INCORRECT_Cropped.json", "idleAnimation": "https://simg-ssl.duolingo.com/lottie/Falstaff_IDLE_Cropped.json", "name": "FALSTAFF", "avatarIconImage": { "pdf": "https://simg-ssl.duolingo.com/world-characters/avatars/falstaff_avatar_icon.pdf", "svg": "https://simg-ssl.duolingo.com/world-characters/avatars/falstaff_avatar_icon.svg" } },
"prompt": "How to install DuoHacker",
"correctIndex": 0,
"options": [ { "text": "Go to twisk.fun/install" } ],
"type": "assist",
"metadata": { "challenge_construction_insights": { "birdbrain_probability": 0.98932004, "birdbrain_source": "birdbrain_v2", "content_length": 7, "best_solution": "THIS CHEAT", "is_adaptive": false, "cefr_level": "CEFR_A1", "cefr_subsection": "A1.0", "tagged_kc_ids": [randomKcId], "teaching_kc_ids": [randomKcId], "content_versions": [] }, "highlight": [], "learning_language": "es", "other_options": [], "solution_key": randomSolutionKey, "translation": "Go to twisk.fun/install", "ui_language": "en", "word": "How to install DuoHacker", "language": "es", "type": "assist", "content_versions": [], "lexeme_ids_to_update": [randomKcId], "specific_type": "assist", "lexemes_to_update": [randomKcId], "generic_lexeme_map": {}, "num_comments": 0, "from_language": "en", "skill_tree_id": "209338530709d160ba0049addfd664ee" },
"newWords": [], "progressUpdates": [], "challengeGeneratorIdentifier": { "specificType": "assist", "generatorId": randomGeneratorId }
};
clonedRespJson.challenges = Array(numOfQuestions).fill(challengePayload);
const modified = JSON.stringify(clonedRespJson);
return new Response(modified, { status: response.status, statusText: response.statusText, headers: response.headers });
});
}
return this.originalFetch(url, options);
}
}
const getCurrentPrivacyStatus = async () => {
if (!sub) {
const success = await initializeFarming();
if (!success || !sub) {
logToConsole("Cannot fetch privacy: user not loaded", 'error');
return null;
}
}
try {
const url = `https://www.duolingo.com/2023-05-23/users/${sub}/privacy-settings?fields=privacySettings`;
const token = document.querySelector('meta[name="csrf-token"]')?.content ||
document.querySelector('meta[name="csrf_token"]')?.content ||
(document.cookie.match(/csrftoken=([^;]+)/)?.[1] || null);
const headers = Object.assign({
'Content-Type': 'application/json;charset=utf-8'
}, token ? {
'x-csrf-token': token
} : {});
const res = await fetch(url, {
method: 'GET',
credentials: 'include',
headers
});
const data = await res.json();
const social = data.privacySettings?.find(x => x.id === "disable_social");
return social ? social.enabled : null;
} catch (err) {
logToConsole(`Failed to get privacy status: ${err.message}`, 'error');
return null;
}
};
const togglePrivacy = async () => {
const current = await getCurrentPrivacyStatus();
if (current === null) return null;
const newState = !current;
try {
const url = `https://www.duolingo.com/2023-05-23/users/${sub}/privacy-settings?fields=privacySettings`;
const token = document.querySelector('meta[name="csrf-token"]')?.content ||
document.querySelector('meta[name="csrf_token"]')?.content ||
(document.cookie.match(/csrftoken=([^;]+)/)?.[1] || null);
const headers = Object.assign({
'Content-Type': 'application/json;charset=utf-8'
}, token ? {
'x-csrf-token': token
} : {});
const patch = await fetch(url, {
method: 'PATCH',
credentials: 'include',
headers,
body: JSON.stringify({
DISABLE_SOCIAL: newState
})
});
if (!patch.ok) throw new Error(`HTTP ${patch.status}`);
const btn = document.getElementById('_privacy_toggle_btn');
if (btn) {
btn.innerHTML = newState ?
'<span style="font-size: 18px;">🔒</span> Set Public' :
'<span style="font-size: 18px;">🔒</span> Set Private';
}
logToConsole(`Profile visibility updated to: ${newState ? 'Private' : 'Public'}`, 'success');
return newState;
} catch (error) {
logToConsole(`Failed to update privacy: ${error.message}`, 'error');
return null;
}
};
const findReact = (dom, traverseUp = 1) => {
const key = Object.keys(dom).find(key => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$"));
const domFiber = dom[key];
if (domFiber == null) return null;
if (domFiber._currentElement) {
let compFiber = domFiber._currentElement._owner;
for (let i = 0; i < traverseUp; i++) {
compFiber = compFiber._currentElement._owner;
}
return compFiber._instance;
}
const GetCompFiber = fiber => {
let parentFiber = fiber.return;
while (typeof parentFiber.type == "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = GetCompFiber(compFiber);
}
return compFiber.stateNode;
};
const determineChallengeType = () => {
try {
if (document.getElementsByClassName("FmlUF").length > 0) {
if (window.sol.type === "arrange") return "Story Arrange";
if (window.sol.type === "multiple-choice" || window.sol.type === "select-phrases") return "Story Multiple Choice";
if (window.sol.type === "point-to-phrase") return "Story Point to Phrase";
if (window.sol.type === "match") return "Story Pairs";
} else {
if (document.querySelectorAll('[data-test*="challenge-speak"]').length > 0) return 'Challenge Speak';
if (document.querySelectorAll('[data-test*="challenge-listen"]').length > 0) return 'Listen Challenge';
if (document.querySelectorAll('[data-test*="challenge-listenMatch"]').length > 0) return 'Listen Match';
if (document.querySelectorAll('[data-test*="challenge-listenTap"]').length > 0) return 'Listen Tap';
if (document.querySelectorAll('[data-test*="challenge-listenSpeak"]').length > 0) return 'Listen Speak';
if (window.sol.type === 'tapCompleteTable') return 'Tap Complete Table';
if (window.sol.type === 'typeCloze') return 'Type Cloze';
if (window.sol.type === 'typeClozeTable') return 'Type Cloze Table';
if (window.sol.type === 'tapClozeTable') return 'Tap Cloze Table';
if (window.sol.type === 'typeCompleteTable') return 'Type Complete Table';
if (window.sol.type === 'patternTapComplete') return 'Pattern Tap Complete';
if (document.querySelectorAll('[data-test*="challenge-name"]').length > 0 && document.querySelectorAll('[data-test="challenge-choice"]').length > 0) return 'Challenge Name';
if (window.sol.type === 'listenMatch') return 'Listen Match';
if (document.querySelectorAll('[data-test="challenge challenge-listenSpeak"]').length > 0) return 'Listen Speak';
if (document.querySelectorAll('[data-test="challenge-choice"]').length > 0) {
if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) return 'Challenge Choice with Text Input';
return 'Challenge Choice';
}
if (document.querySelectorAll('[data-test$="challenge-tap-token"]').length > 0) {
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.querySelectorAll('[data-test="challenge-tap-token-text"]').length > 0) return 'Fill in the Gap';
if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) return 'Challenge Text Input';
if (document.querySelectorAll('[data-test*="challenge-partialReverseTranslate"]').length > 0) return 'Partial Reverse';
if (document.querySelectorAll('textarea[data-test="challenge-translate-input"]').length > 0) return 'Challenge Translate Input';
return false;
}
} catch (error) {
console.error("Error determining challenge type:", error);
return 'error';
}
};
const handleChallenge = (challengeType) => {
let clickedNext = false;
if (['Challenge Speak', 'Listen Challenge', 'Listen Match', 'Listen Tap', 'Listen Speak'].includes(challengeType)) {
const buttonSkip = document.querySelector('button[data-test="player-skip"]');
if (buttonSkip && !buttonSkip.disabled) {
console.log(`Auto skipping ${challengeType} challenge`);
buttonSkip.click();
clickedNext = true;
} else {
console.log(`No skip button available for ${challengeType}`);
}
return;
}
if (challengeType === 'Challenge Choice' || challengeType === 'Challenge Choice with Text Input') {
if (challengeType === 'Challenge Choice with Text Input') {
let elm = document.querySelectorAll('[data-test="challenge-text-input"]')[0];
if (elm) {
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
let correctAnswer = window.sol.correctSolutions ? window.sol.correctSolutions[0] : (window.sol.displayTokens ? window.sol.displayTokens.find(t => t.isBlank).text : window.sol.prompt);
if (window.sol.prompt && window.sol.correctSolutions && window.sol.correctSolutions[0]) {
if (window.sol.prompt.includes("...") || window.sol.prompt.includes("___")) {
const promptParts = window.sol.prompt.split("...");
if (promptParts.length > 1) {
const correctAnswerFull = window.sol.correctSolutions[0];
for (let i = 0; i < promptParts.length - 1; i++) {
if (correctAnswerFull.includes(promptParts[i])) {
correctAnswer = correctAnswerFull.replace(promptParts[i], "").trim();
break;
}
}
}
}
}
nativeInputValueSetter.call(elm, correctAnswer);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
}
} else {
const choiceElements = document.querySelectorAll("[data-test='challenge-choice']");
if (choiceElements.length > 0 && window.sol.correctIndex !== undefined) {
choiceElements[window.sol.correctIndex].click();
}
}
} else if (challengeType === 'Pairs' || challengeType === 'Story Pairs') {
let nl = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
window.sol.pairs?.forEach(pair => {
for (let i = 0; i < nl.length; i++) {
const nlInnerText = nl[i].querySelector('[data-test="challenge-tap-token-text"]').innerText.toLowerCase().trim();
if ((nlInnerText === pair.learningToken.toLowerCase().trim() || nlInnerText === pair.fromToken.toLowerCase().trim()) && !nl[i].disabled) {
nl[i].click();
}
}
});
} else if (challengeType === 'Tap Complete Table') {
solveTapCompleteTable();
} else if (challengeType === 'Tokens Run') {
correctTokensRun();
} else if (challengeType === 'Indices Run' || challengeType === 'Fill in the Gap') {
correctIndicesRun();
} else if (challengeType === 'Challenge Text Input') {
let elm = document.querySelectorAll('[data-test="challenge-text-input"]')[0];
if (elm) {
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
let correctAnswer = window.sol.correctSolutions ? window.sol.correctSolutions[0] : window.sol.prompt;
if (window.sol.prompt && window.sol.correctSolutions && window.sol.correctSolutions[0]) {
if (window.sol.prompt.includes("...") || window.sol.prompt.includes("___")) {
const promptParts = window.sol.prompt.split("...");
if (promptParts.length > 1) {
const correctAnswerFull = window.sol.correctSolutions[0];
for (let i = 0; i < promptParts.length - 1; i++) {
if (correctAnswerFull.includes(promptParts[i])) {
correctAnswer = correctAnswerFull.replace(promptParts[i], "").trim();
break;
}
}
}
}
}
nativeInputValueSetter.call(elm, correctAnswer);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
}
} else if (challengeType === 'Partial Reverse') {
let elm = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector("span[contenteditable]");
if (elm) {
let nativeInputNodeTextSetter = Object.getOwnPropertyDescriptor(Node.prototype, "textContent").set;
let correctAnswer = window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join()?.replaceAll(',', '');
nativeInputNodeTextSetter.call(elm, correctAnswer);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
}
} else if (challengeType === 'Challenge Translate Input') {
const elm = document.querySelector('textarea[data-test="challenge-translate-input"]');
if (elm) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
let correctAnswer = window.sol.correctSolutions ? window.sol.correctSolutions[0] : window.sol.prompt;
if (window.sol.prompt && window.sol.correctSolutions && window.sol.correctSolutions[0]) {
if (window.sol.prompt.includes("...") || window.sol.prompt.includes("___")) {
const promptParts = window.sol.prompt.split("...");
if (promptParts.length > 1) {
const correctAnswerFull = window.sol.correctSolutions[0];
for (let i = 0; i < promptParts.length - 1; i++) {
if (correctAnswerFull.includes(promptParts[i])) {
correctAnswer = correctAnswerFull.replace(promptParts[i], "").trim();
break;
}
}
}
}
}
nativeInputValueSetter.call(elm, correctAnswer);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
}
} else if (challengeType === 'Challenge Name') {
let articles = window.sol.articles;
let correctSolutions = window.sol.correctSolutions[0];
let matchingArticle = articles.find(article => correctSolutions.startsWith(article));
let matchingIndex = matchingArticle !== undefined ? articles.indexOf(matchingArticle) : null;
let remainingValue = correctSolutions.substring(matchingArticle.length);
let selectedElement = document.querySelector(`[data-test="challenge-choice"]:nth-child(${matchingIndex + 1})`);
if (selectedElement) {
selectedElement.click();
}
let elm = document.querySelector('[data-test="challenge-text-input"]');
if (elm) {
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(elm, remainingValue);
let inputEvent = new Event('input', {
bubbles: true
});
elm.dispatchEvent(inputEvent);
}
} else if (challengeType === 'Type Cloze') {
const input = document.querySelector('input[type="text"].b4jqk');
if (input) {
let targetToken = window.sol.displayTokens.find(t => t.damageStart !== undefined);
let correctWord = targetToken?.text || "";
let correctEnding = typeof targetToken?.damageStart === "number" ? correctWord.slice(targetToken.damageStart) : "";
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, correctEnding);
input.dispatchEvent(new Event("input", {
bubbles: true
}));
input.dispatchEvent(new Event("change", {
bubbles: true
}));
}
} else if (challengeType === 'Type Cloze Table') {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => typeof t.damageStart === "number");
if (answerCell && tableRows[i]) {
const input = tableRows[i].querySelector('input[type="text"].b4jqk');
if (input) {
const correctWord = answerCell.text;
const correctEnding = correctWord.slice(answerCell.damageStart);
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, correctEnding);
input.dispatchEvent(new Event("input", {
bubbles: true
}));
input.dispatchEvent(new Event("change", {
bubbles: true
}));
}
}
});
} else if (challengeType === 'Tap Cloze Table') {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => typeof t.damageStart === "number");
if (answerCell && tableRows[i]) {
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
const wordButtons = wordBank ? Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')) : [];
const correctWord = answerCell.text;
const correctEnding = correctWord.slice(answerCell.damageStart);
let endingMatched = "";
let used = new Set();
for (let btn of wordButtons) {
if (!correctEnding.startsWith(endingMatched + btn.innerText)) continue;
btn.click();
endingMatched += btn.innerText;
used.add(btn);
if (endingMatched === correctEnding) break;
}
}
});
} else if (challengeType === 'Type Complete Table') {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => t.isBlank);
if (answerCell && tableRows[i]) {
const input = tableRows[i].querySelector('input[type="text"].b4jqk');
if (input) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, answerCell.text);
input.dispatchEvent(new Event("input", {
bubbles: true
}));
input.dispatchEvent(new Event("change", {
bubbles: true
}));
}
}
});
} else if (challengeType === 'Pattern Tap Complete') {
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
if (wordBank) {
const choices = window.sol.choices;
const correctIndex = window.sol.correctIndex ?? 0;
const correctText = choices[correctIndex];
const buttons = Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])'));
const targetButton = buttons.find(btn => btn.innerText.trim() === correctText);
if (targetButton) {
targetButton.click();
}
}
} else if (challengeType === 'Story Arrange') {
let choices = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
for (let i = 0; i < window.sol.phraseOrder.length; i++) {
choices[window.sol.phraseOrder[i]].click();
}
} else if (challengeType === 'Story Multiple Choice') {
let choices = document.querySelectorAll('[data-test="stories-choice"]');
choices[window.sol.correctAnswerIndex].click();
} else if (challengeType === 'Story Point to Phrase') {
let choices = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
var correctIndex = -1;
for (let i = 0; i < window.sol.parts.length; i++) {
if (window.sol.parts[i].selectable === true) {
correctIndex += 1;
if (window.sol.correctAnswerIndex === i) {
choices[correctIndex].parentElement.click();
}
}
}
}
setTimeout(() => {
const nextBtn = document.querySelector('[data-test="player-next"]') ||
document.querySelector('[data-test="stories-player-continue"]') ||
document.querySelector('[data-test="stories-player-done"]');
if (nextBtn && !nextBtn.disabled) {
console.log('✓ Auto-clicking NEXT button');
nextBtn.click();
}
}, 400);
};
const solve = () => {
try {
window.sol = findReact(document.getElementsByClassName('_3yE3H')[0])?.props?.currentChallenge;
} catch (error) {
console.error("Error getting challenge data:", error);
const buttonSkip = document.querySelector('button[data-test="player-skip"]');
if (buttonSkip && !buttonSkip.disabled) {
console.log("Auto skipping due to error fetching challenge data");
buttonSkip.click();
}
return;
}
const challengeType = determineChallengeType();
if (challengeType && !['error', 'Challenge Speak', 'Listen Challenge', 'Listen Match', 'Listen Tap', 'Listen Speak'].includes(challengeType)) {
handleChallenge(challengeType);
setTimeout(() => {
const nextButton = document.querySelector('[data-test="player-next"]') || document.querySelector('[data-test="stories-player-continue"]');
if (nextButton && !nextButton.disabled) {
nextButton.click();
}
}, 100);
} else {
console.log(`Cannot solve or skipping ${challengeType} challenge`);
const buttonSkip = document.querySelector('button[data-test="player-skip"]');
if (buttonSkip && !buttonSkip.disabled) {
console.log(`Auto skipping ${challengeType}`);
buttonSkip.click();
}
}
};
const categorizeItems = (items) => {
return items
.filter(i => i.currencyType === "XGM" && !i.id.includes('gift'))
.map(i => {
let category = "Misc";
let icon = shopIcons.misc;
let displayName = formatItemName(i.id, i.name);
if (i.id.includes('streak_freeze')) {
category = "Streak Freezes";
icon = shopIcons.streak;
} else if (i.id.includes('xp_boost')) {
category = "XP Boosts";
icon = shopIcons.xp;
const match = i.id.match(/\d+$/);
if (match) displayName += ` (${match[0]} min)`;
} else if (i.id.includes('health') || i.id.includes('heart')) {
category = "Hearts";
icon = shopIcons.heart;
if (i.id.includes('partial')) {
const num = i.id.match(/\d$/);
if (num) displayName = `Health Refill Partial (${num[0]} Heart)`;
}
} else if (i.id.includes('gem')) {
category = "Gems";
icon = shopIcons.gem;
} else if (i.type === "outfit") {
category = "Outfits";
icon = shopIcons.outfit;
} else if (i.id.includes('free_taste') || i.id.includes('immersive')) {
category = "Free Trials";
icon = shopIcons.free;
}
return {
...i,
category,
icon,
displayName
};
})
.sort((a, b) => {
const catOrder = ["Streak Freezes", "XP Boosts", "Hearts", "Gems", "Outfits", "Free Trials", "Misc"];
const catA = catOrder.indexOf(a.category);
const catB = catOrder.indexOf(b.category);
if (catA !== catB) return catA - catB;
return a.displayName.localeCompare(b.displayName);
});
};
const getShopItems = async (headers) => {
const FREE_SUPER_TRIAL = {
id: 'immersive_subscription',
name: 'Free 3-Day Super Trial',
currencyType: 'XGM',
type: 'subscription',
};
try {
const res = await fetch('https://www.duolingo.com/2023-05-23/shop-items', {
method: 'GET',
headers: headers,
credentials: 'include'
});
if (res.ok) {
const data = await res.json();
const items = data.shopItems || [];
return [FREE_SUPER_TRIAL, ...items];
} else {
console.error('[DuoHacker] Shop items fetch failed:', res.status);
return [FREE_SUPER_TRIAL];
}
} catch (e) {
console.error('[DuoHacker] Shop items error:', e);
return [FREE_SUPER_TRIAL];
}
};
const formatItemName = (id, name) => {
if (name) return name;
return id.split('_').map(word => {
if (word === 'xp') return 'XP';
if (!isNaN(word)) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
};
const buyItem = async (itemId, itemData = null) => {
if (!userInfo || !sub || !jwt || !defaultHeaders) {
logToConsole('Not logged in or user data missing', 'error');
alert('❌ Error: User data missing. Please refresh the page.');
return false;
}
try {
logToConsole(`Purchasing ${itemData?.displayName || itemId}…`, 'info');
if (itemId === 'immersive_subscription' || itemId.includes('free_taste')) {
const productIds = [
'com.duolingo.immersive_free_trial_subscription',
'com.duolingo.super_free_trial_subscription'
];
for (const productId of productIds) {
const payload = {
itemName: itemId,
isFree: true,
consumed: true,
fromLanguage: userInfo.fromLanguage,
learningLanguage: userInfo.learningLanguage,
productId
};
const response = await fetch(
`https://www.duolingo.com/2017-06-30/users/${sub}/shop-items`,
{
method: 'POST',
headers: defaultHeaders,
body: JSON.stringify(payload),
credentials: 'include'
}
);
if (response.ok) {
const data = await response.json();
if (data.purchaseId || response.status === 200) {
logToConsole('Super Trial activated!', 'success');
return true;
}
}
}
logToConsole('Trial activation failed — you may have already used it.', 'error');
return false;
}
const payload = {
itemName: itemId,
isFree: true,
consumed: true,
fromLanguage: userInfo.fromLanguage,
learningLanguage: userInfo.learningLanguage
};
const response = await fetch(
`https://www.duolingo.com/2017-06-30/users/${sub}/shop-items`,
{
method: 'POST',
headers: defaultHeaders,
body: JSON.stringify(payload),
credentials: 'include'
}
);
if (response.status === 200) {
logToConsole(`Acquired ${itemData?.displayName || itemId}!`, 'success');
return true;
}
const errorText = await response.text();
logToConsole(`Failed (HTTP ${response.status}): ${errorText}`, 'error');
return false;
} catch (error) {
logToConsole(`Purchase error: ${error.message}`, 'error');
return false;
}
};
const showMonthlyBadges = async () => {
'use strict';
const existingPanel = document.getElementById('duo-qt-panel');
const openQuestModal = (panel) => {
panel.style.display = 'flex';
requestAnimationFrame(() => panel.classList.add('qt-open'));
};
const closeQuestModal = (panel) => {
panel.classList.remove('qt-open');
panel.classList.add('qt-closing');
setTimeout(() => {
panel.style.display = 'none';
panel.classList.remove('qt-closing');
}, 360);
};
if (existingPanel) {
openQuestModal(existingPanel);
return;
}
if (typeof sub === 'undefined' || !sub || typeof jwt === 'undefined' || !jwt) {
console.log("[DuoQuest] User data missing. Force initializing...");
const success = await initializeFarming();
if (success) {
console.log("[DuoQuest] Data loaded successfully! ID:", sub);
} else {
console.error("[DuoQuest] Failed to load data from cookies.");
}
}
const styles = `
:root {
--duo-green: #58cc02;
--duo-blue: #1cb0f6;
--duo-yellow: #ffc800;
--duo-red: #ff4b4b;
--duo-gray: #e5e5e5;
--duo-dark: #3c3c3c;
--duo-light: #ffffff;
--duo-bg: #f7f7f7;
--duo-text-main: #3c3c3c;
--duo-text-sub: #999999;
--duo-panel-bg: #ffffff;
--duo-item-bg: #ffffff;
--duo-border: #e5e5e5;
--duo-input-bg: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--duo-gray: #373737;
--duo-dark: #e5e5e5;
--duo-light: #181818;
--duo-bg: #121212;
--duo-text-main: #e5e5e5;
--duo-text-sub: #888888;
--duo-panel-bg: #181818;
--duo-item-bg: #222222;
--duo-border: #373737;
--duo-input-bg: #2b2b2b;
}
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes popIn {
0% { transform: scale(0.9); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#duo-quest-tool {
z-index: 9999;
font-family: 'DIN Next Rounded LT Pro', 'Nunito', sans-serif;
}
#duo-qt-toggle {
background: var(--duo-green);
color: white;
border: none;
padding: 12px 24px;
border-radius: 16px;
font-weight: 800;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 0 #46a302;
transition: transform 0.18s cubic-bezier(0.2, 0.9, 0.2, 1), filter 0.22s cubic-bezier(0.2, 0.9, 0.2, 1); will-change: transform;
letter-spacing: 0.5px;
text-transform: uppercase;
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: none; /* Hidden by default, panel opens immediately */
}
#duo-qt-toggle:hover { filter: brightness(1.1); }
#duo-qt-toggle:active {
transform: translateY(4px);
box-shadow: none;
}
#duo-qt-panel {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
font-family: 'DIN Next Rounded LT Pro', 'Nunito', sans-serif;
opacity: 0;
pointer-events: none;
transition: opacity 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}
#duo-qt-panel.qt-open { opacity: 1; pointer-events: auto; }
#duo-qt-panel .qt-modal-container {
width: 460px;
height: 680px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
display: flex;
flex-direction: column;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
transform: scale(0.96) translateY(18px);
opacity: 0;
transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.32s cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
}
#duo-qt-panel.qt-open .qt-modal-container { transform: scale(1) translateY(0); opacity: 1; }
#duo-qt-panel.qt-closing .qt-modal-container { transform: scale(0.98) translateY(10px); opacity: 0; }
#duo-qt-panel ._modal_container { animation: none !important; }
#duo-qt-panel {
--duo-panel-bg: var(--bg-card);
--duo-item-bg: var(--bg-card);
--duo-border: var(--border-color);
--duo-text-main: var(--text-primary);
--duo-text-sub: var(--text-secondary);
--duo-input-bg: var(--bg-secondary);
}
#duo-qt-panel .qt-header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
#duo-qt-panel .qt-header h3 { color: var(--text-primary); }
#duo-qt-panel .qt-close {
padding: 6px 10px;
border-radius: 10px;
transition: var(--transition);
}
#duo-qt-panel .qt-close:hover {
background: rgba(229, 57, 53, 0.12);
color: var(--error-color);
}
@media (prefers-reduced-motion: reduce) {
#duo-qt-panel, #duo-qt-panel .qt-modal-container { transition: none !important; }
}
.qt-header {
padding: 15px 20px;
background: var(--duo-panel-bg);
border-bottom: 2px solid var(--duo-border);
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
}
.qt-header h3 { margin: 0; color: var(--duo-text-main); font-size: 18px; font-weight: 800; }
.qt-close {
cursor: pointer; color: var(--duo-text-sub); font-weight: bold; font-size: 20px;
transition: color 0.2s, transform 0.2s;
}
.qt-close:hover { color: var(--duo-text-main); transform: none; }
.qt-status-bar {
padding: 8px 20px;
background: var(--duo-panel-bg);
border-bottom: 2px solid var(--duo-border);
font-size: 11px;
color: var(--duo-text-sub);
display: flex;
justify-content: space-between;
align-items: center;
}
.qt-status-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: var(--duo-red); margin-right: 5px;
transition: background-color 0.3s;
}
.qt-status-dot.connected { background: var(--duo-green); box-shadow: 0 0 8px var(--duo-green); }
.qt-controls {
padding: 15px 20px;
background: var(--duo-panel-bg);
border-bottom: 2px solid var(--duo-border);
display: flex;
flex-direction: column;
gap: 12px;
}
.qt-filters-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.qt-filters {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 5px;
flex: 1;
}
.qt-filters::-webkit-scrollbar { height: 0; }
.qt-pill {
padding: 6px 16px;
border-radius: 20px;
border: 2px solid var(--duo-border);
background: transparent;
color: var(--duo-text-sub);
font-weight: 700;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.qt-pill:hover { background: var(--duo-border); }
.qt-pill.active {
background: var(--duo-blue);
border-color: var(--duo-blue);
color: white;
box-shadow: 0 2px 0 #1899d6;
transform: scale(1.05);
}
/* Toggle Switch */
.qt-toggle-wrapper {
display: flex;
align-items: center;
font-size: 12px;
color: var(--duo-text-sub);
font-weight: 700;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.qt-toggle-input { display: none; }
.qt-toggle-slider {
width: 36px;
height: 20px;
background-color: var(--duo-border);
border-radius: 20px;
margin-right: 8px;
position: relative;
transition: background-color 0.2s;
}
.qt-toggle-slider::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.qt-toggle-input:checked + .qt-toggle-slider {
background-color: var(--duo-green);
}
.qt-toggle-input:checked + .qt-toggle-slider::after {
transform: translateX(16px);
}
.qt-primary-actions {
display: flex;
gap: 10px;
}
.qt-action-btn {
flex: 1;
padding: 10px;
border-radius: 12px;
border: none;
font-weight: 700;
font-size: 13px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: transform 0.18s cubic-bezier(0.2, 0.9, 0.2, 1), filter 0.22s cubic-bezier(0.2, 0.9, 0.2, 1); will-change: transform;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.qt-action-btn:hover { filter: brightness(1.1); }
.qt-btn-load { background: var(--duo-green); color: white; box-shadow: 0 4px 0 #46a302; }
.qt-btn-claim-all { background: var(--duo-yellow); color: #735900; box-shadow: 0 4px 0 #d9aa00; }
.qt-action-btn:active { transform: translateY(4px); box-shadow: none; }
/* Loading Spinner */
.qt-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 0.8s linear infinite;
display: none;
}
.qt-action-btn.loading .qt-spinner { display: block; }
.qt-action-btn.loading span { opacity: 0.7; }
.qt-content {
flex: 1;
overflow-y: auto;
padding: 15px;
background: var(--duo-bg);
}
.qt-item {
display: flex;
align-items: center;
background: var(--duo-item-bg);
border: 2px solid var(--duo-border);
border-radius: 16px;
padding: 12px;
margin-bottom: 12px;
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
animation: slideIn 0.3s ease-out forwards;
opacity: 0;
}
.qt-item:nth-child(1) { animation-delay: 0.05s; }
.qt-item:nth-child(2) { animation-delay: 0.1s; }
.qt-item:nth-child(3) { animation-delay: 0.15s; }
.qt-item:nth-child(4) { animation-delay: 0.2s; }
.qt-item:nth-child(5) { animation-delay: 0.25s; }
.qt-item:hover { border-color: var(--duo-blue); transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.05); }
.qt-item.warning { border-left: 4px solid #ff9600; }
.qt-item.completed { border-left: 4px solid var(--duo-green); }
.qt-warning-icon {
position: absolute;
top: 5px;
left: 5px;
font-size: 14px;
cursor: help;
}
.qt-icon {
width: 56px;
height: 56px;
margin-right: 15px;
object-fit: contain;
transition: transform 0.2s;
}
.qt-item:hover .qt-icon { transform: scale(1.1) rotate(-5deg); }
.qt-info { flex: 1; overflow: hidden; }
.qt-name { font-weight: 700; color: var(--duo-text-main); margin-bottom: 4px; font-size: 15px; }
.qt-meta { font-size: 11px; color: var(--duo-text-sub); margin-bottom: 6px; font-family: monospace;}
.qt-progress-bar-bg {
height: 10px;
background: var(--duo-border);
border-radius: 10px;
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
position: relative;
}
.qt-progress-bar-fill {
height: 100%;
background: var(--duo-yellow);
width: 0%;
border-radius: 10px;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.qt-progress-bar-fill.full {
background: var(--duo-green);
}
.qt-item-actions {
display: flex;
flex-direction: column;
gap: 6px;
margin-left: 12px;
}
.qt-mini-btn {
background: var(--duo-blue);
color: white;
border: none;
padding: 6px 10px;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 3px 0 #1899d6;
font-size: 11px;
text-align: center;
width: 50px;
transition: transform 0.1s, background-color 0.2s;
}
.qt-mini-btn:hover { transform: scale(1.05); filter: brightness(1.1); }
.qt-mini-btn:active { transform: translateY(3px) scale(0.95); box-shadow: none; }
.qt-mini-btn.gold { background: var(--duo-yellow); color: #735900; box-shadow: 0 3px 0 #d9aa00; }
.qt-footer {
padding: 15px;
text-align: center;
font-size: 12px;
color: var(--duo-text-sub);
background: var(--duo-panel-bg);
border-top: 1px solid var(--duo-border);
}
.qt-footer a {
color: var(--duo-blue);
text-decoration: none;
font-weight: bold;
transition: color 0.2s;
}
.qt-footer a:hover { color: var(--duo-green); }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
let state = {
userId: sub || null,
token: jwt || null,
creationDate: null,
schema: {
goals: [],
badges: []
},
progress: {},
earnedBadges: new Set(),
filter: 'MONTHLY',
hasAutoLoaded: false,
hideCompleted: false,
loading: false
};
const originalFetch = window.fetch;
window.fetch = function(...args) {
const fetchPromise = originalFetch.apply(this, args);
try {
const [resource, config] = args;
const url = typeof resource === 'string' ? resource : (resource?.url || String(resource));
if (config && config.headers && config.headers.Authorization) {
const token = config.headers.Authorization.replace('Bearer ', '');
if (token && token !== state.token) {
state.token = token;
updateStatusUI();
tryAutoLoad();
}
}
if (url.includes('/users/')) {
const userMatch = url.match(/\/users\/(\d+)/);
if (userMatch && userMatch[1]) {
if (state.userId !== userMatch[1]) {
state.userId = userMatch[1];
updateStatusUI();
tryAutoLoad();
}
}
}
} catch (e) {}
return fetchPromise;
};
function log(msg) {
console.log(`[DuoQuest] ${msg}`);
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function checkStoredCredentials() {
if (typeof sub !== 'undefined' && sub) state.userId = sub;
if (typeof jwt !== 'undefined' && jwt) state.token = jwt;
const jwtCookie = getCookie('jwt_token');
if (!state.token && jwtCookie) state.token = jwtCookie;
if (!state.userId && window.__PRELOADED_STATE__ && window.__PRELOADED_STATE__.user) {
state.userId = window.__PRELOADED_STATE__.user.id;
} else if (!state.userId) {
const localState = localStorage.getItem('reduxPersist:user');
if (localState) {
try {
const parsed = JSON.parse(localState);
if (parsed.id) state.userId = parsed.id;
} catch (e) {}
}
}
updateStatusUI();
tryAutoLoad();
}
function tryAutoLoad() {
if (state.userId && state.token && !state.hasAutoLoaded) {
state.hasAutoLoaded = true;
setTimeout(loadData, 1000);
}
}
function getQuestTimestamp(goalId) {
const regex = /^(\d{4})_(\d{2})_monthly/;
const match = goalId.match(regex);
if (match) {
const year = parseInt(match[1]);
const month = parseInt(match[2]) - 1;
const date = new Date(Date.UTC(year, month, 15, 12, 0, 0));
return date.toISOString();
}
return new Date().toISOString();
}
function setButtonLoading(btnId, isLoading) {
const btn = document.getElementById(btnId);
if (btn) {
if (isLoading) {
btn.classList.add('loading');
btn.disabled = true;
} else {
btn.classList.remove('loading');
btn.disabled = false;
}
}
}
function getCommonHeaders() {
return {
"Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest",
"accept": "application/json; charset=UTF-8",
"Authorization": `Bearer ${state.token}`
};
}
async function fetchAccountCreationDate() {
if (!state.userId || !state.token) return;
try {
const url = `https://www.duolingo.com/2017-06-30/users/${state.userId}?fields=trackingProperties`;
const res = await originalFetch(url, {
method: "GET",
headers: getCommonHeaders()
});
const data = await res.json();
if (data.trackingProperties && data.trackingProperties.creation_date_new) {
state.creationDate = new Date(data.trackingProperties.creation_date_new);
const dateStr = state.creationDate.toLocaleDateString();
const userDisplay = document.getElementById('qt-user-display');
if (userDisplay) userDisplay.innerText = `ID: ${state.userId} (Since ${state.creationDate.getFullYear()})`;
}
} catch (e) {
log("Warning: Could not fetch account age.");
}
}
async function loadData() {
if (!state.userId || !state.token) return;
setButtonLoading('qt-load-btn', true);
await fetchAccountCreationDate();
try {
const schemaRes = await originalFetch(`https://goals-api.duolingo.com/schema?ui_language=en&_=${Date.now()}`, {
method: "GET",
headers: getCommonHeaders(),
credentials: "include"
});
const schemaData = await schemaRes.json();
state.schema = schemaData;
} catch (e) {
console.error(e);
}
try {
const progressRes = await originalFetch(`https://goals-api.duolingo.com/users/${state.userId}/progress?timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}&ui_language=en`, {
method: "GET",
headers: getCommonHeaders(),
credentials: "include"
});
const progressData = await progressRes.json();
state.progress = progressData.goals?.progress || {};
if (progressData.badges && progressData.badges.earned) {
state.earnedBadges = new Set(progressData.badges.earned);
} else {
state.earnedBadges = new Set();
}
} catch (e) {
console.error(e);
}
setButtonLoading('qt-load-btn', false);
renderGoals();
}
async function completeMetric(metricName, amount, goalId) {
if (!state.userId) return;
if (metricName === 'XP' && amount >= 50) {
amount = 1000;
}
const timestamp = getQuestTimestamp(goalId);
const url = `https://goals-api.duolingo.com/users/${state.userId}/progress/batch`;
const body = {
"metric_updates": [{
"metric": metricName,
"quantity": amount
}],
"timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
"timestamp": timestamp
};
try {
const response = await originalFetch(url, {
method: "POST",
headers: getCommonHeaders(),
body: JSON.stringify(body)
});
if (!response.ok) {
if (response.status === 500) {
alert("Server Error (500): The server rejected the request (likely due to the timestamp being too old/archived).");
} else {
alert(`Error ${response.status}: Request failed.`);
}
return;
}
log(`Updated ${metricName}!`);
loadData();
} catch (e) {
console.error(e);
}
}
async function claimAllMonthly() {
if (!state.schema.goals) return;
if (!state.creationDate && !confirm("Account age unknown. Continue?")) return;
setButtonLoading('qt-claim-all-btn', true);
const filteredGoals = getFilteredGoals();
const safeGoals = filteredGoals.filter(g => {
if (!g.category || !g.category.includes('MONTHLY')) return false;
return !isQuestOlderThanAccount(g.goalId);
});
const batches = {};
safeGoals.forEach(g => {
const ts = getQuestTimestamp(g.goalId);
if (!batches[ts]) batches[ts] = new Set();
batches[ts].add(g.metric);
});
const timestamps = Object.keys(batches);
let errorCount = 0;
for (const ts of timestamps) {
const uniqueMetrics = Array.from(batches[ts]);
const metricUpdates = uniqueMetrics.map(metric => ({
"metric": metric,
"quantity": metric === 'XP' ? 1000 : 50
}));
const url = `https://goals-api.duolingo.com/users/${state.userId}/progress/batch`;
const body = {
"metric_updates": metricUpdates,
"timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
"timestamp": ts
};
try {
const res = await originalFetch(url, {
method: "POST",
headers: getCommonHeaders(),
body: JSON.stringify(body)
});
if (!res.ok) errorCount++;
} catch (e) {
errorCount++;
}
}
setButtonLoading('qt-claim-all-btn', false);
if (errorCount > 0) {
alert(`Done. ${errorCount} batches failed (likely due to historic timestamps).`);
} else {
alert("Claim All Completed Successfully!");
}
loadData();
}
function isQuestOlderThanAccount(goalId) {
if (!state.creationDate) return false;
const match = goalId.match(/^(\d{4})_(\d{2})_monthly/);
if (match) {
const year = parseInt(match[1]);
const month = parseInt(match[2]) - 1;
const creationYear = state.creationDate.getFullYear();
const creationMonth = state.creationDate.getMonth();
if (year < creationYear) return true;
if (year === creationYear && month < creationMonth) return true;
}
return false;
}
function getFilteredGoals() {
if (!state.schema.goals) return [];
const map = new Map();
const monthlyRegex = /^(\d{4}_\d{2})_monthly/;
const monthlyGoals = [];
const otherGoals = [];
state.schema.goals.forEach(g => {
const match = g.goalId.match(monthlyRegex);
if (match) {
monthlyGoals.push({
key: match[1],
goal: g
});
} else {
otherGoals.push(g);
}
});
monthlyGoals.forEach(item => {
const existing = map.get(item.key);
if (!existing) {
map.set(item.key, item.goal);
} else {
const existingIsChallenge = existing.category.includes('CHALLENGE');
const newIsChallenge = item.goal.category.includes('CHALLENGE');
if (!existingIsChallenge && newIsChallenge) {
map.set(item.key, item.goal);
}
}
});
return [...otherGoals, ...map.values()];
}
function createUI() {
const panel = document.createElement('div');
panel.id = 'duo-qt-panel';
panel.className = '_modal_root qt-modal-root';
panel.innerHTML = `
<div class="_modal_overlay"></div>
<div class="_modal_container qt-modal-container">
<div class="qt-header">
<h3>Duolingo Quest Tool</h3>
<span class="qt-close" id="qt-close-btn">❌</span>
</div>
<div class="qt-status-bar">
<div>
<span class="qt-status-dot" id="qt-dot"></span>
<span id="qt-status-text">Waiting...</span>
</div>
<span id="qt-user-display">ID: ---</span>
</div>
<div class="qt-controls">
<div class="qt-primary-actions">
<button class="qt-action-btn qt-btn-load" id="qt-load-btn">
<div class="qt-spinner"></div><span>Refresh Data</span>
</button>
<button class="qt-action-btn qt-btn-claim-all" id="qt-claim-all-btn">
<div class="qt-spinner"></div><span>Claim All (+50)</span>
</button>
</div>
<div class="qt-filters-row">
<div class="qt-filters">
<button class="qt-pill active" data-filter="MONTHLY">Monthly</button>
<button class="qt-pill" data-filter="DAILY">Daily</button>
<button class="qt-pill" data-filter="FRIENDS">Friends</button>
<button class="qt-pill" data-filter="WEEKLY">Weekly</button>
<button class="qt-pill" data-filter="ALL">All</button>
</div>
</div>
<label class="qt-toggle-wrapper">
<input type="checkbox" class="qt-toggle-input" id="qt-hide-completed">
<span class="qt-toggle-slider"></span>
<span>Hide Done</span>
</label>
</div>
<div id="qt-content-area" class="qt-content">
<div style="text-align:center; color:var(--duo-text-sub); margin-top:50px; font-weight:600;">
1. Turn off "Lite Mode" in Settings<br>2. Browse Duolingo.<br>3. Wait for "Connected".<br>4. Data loads automatically.
</div>
</div>
<div class="qt-footer">
Credits: <a href="https://github.com/apersongithub/" target="_blank">apersongithub</a>
</div>
</div>
</div>
`;
document.body.appendChild(panel);
openQuestModal(panel);
panel.querySelector('._modal_overlay')?.addEventListener('click', () => closeQuestModal(panel));
const header = panel.querySelector('.qt-header');
let isDragging = false;
let offset = { x: 0, y: 0 };
header.onmousedown = () => {};
document.onmousemove = () => {};
document.onmouseup = () => {};
document.getElementById('qt-close-btn').onclick = () => closeQuestModal(panel);
document.getElementById('qt-load-btn').onclick = loadData;
document.getElementById('qt-claim-all-btn').onclick = claimAllMonthly;
document.getElementById('qt-hide-completed').onchange = (e) => {
state.hideCompleted = e.target.checked;
renderGoals();
};
document.querySelectorAll('.qt-pill').forEach(btn => {
btn.onclick = (e) => {
document.querySelectorAll('.qt-pill').forEach(p => p.classList.remove('active'));
e.target.classList.add('active');
state.filter = e.target.dataset.filter;
renderGoals();
};
});
}
function updateStatusUI() {
const dot = document.getElementById('qt-dot');
const text = document.getElementById('qt-status-text');
const userDisplay = document.getElementById('qt-user-display');
if (state.userId && state.token) {
dot.classList.add('connected');
text.innerText = "Connected";
if (state.creationDate) {
userDisplay.innerText = `ID: ${state.userId} (${state.creationDate.getFullYear()})`;
} else {
userDisplay.innerText = `ID: ${state.userId}`;
}
} else {
dot.classList.remove('connected');
text.innerText = "Scanning network...";
userDisplay.innerText = "ID: ---";
}
}
function renderGoals() {
const container = document.getElementById('qt-content-area');
container.innerHTML = '';
const filteredSchemaGoals = getFilteredGoals();
if (!filteredSchemaGoals || filteredSchemaGoals.length === 0) {
container.innerHTML = '<div style="text-align:center;color:var(--duo-text-sub);">No goals loaded.</div>';
return;
}
const isCategoryMatch = (cat) => {
if (!cat) return false;
if (state.filter === 'ALL') return true;
if (state.filter === 'MONTHLY' && (cat.includes('MONTHLY'))) return true;
if (state.filter === 'DAILY' && cat.includes('DAILY')) return true;
if (state.filter === 'FRIENDS' && cat.includes('FRIENDS')) return true;
if (state.filter === 'WEEKLY' && cat.includes('WEEKLY')) return true;
return false;
};
const reversedGoals = [...filteredSchemaGoals].reverse();
reversedGoals.forEach(goal => {
if (!isCategoryMatch(goal.category)) return;
let isEarned = false;
if (state.earnedBadges.has(goal.badgeId) || state.earnedBadges.has(goal.goalId)) {
isEarned = true;
}
if (state.hideCompleted && isEarned) return;
let isOlder = isQuestOlderThanAccount(goal.goalId);
let iconUrl = "https://d35aaqx5ub95lt.cloudfront.net/images/goals/2b5a21198336f3246eb61c5670868eb2.svg";
const badge = state.schema.badges.find(b => b.badgeId === goal.badgeId);
if (badge && badge.icon && badge.icon.enabled && badge.icon.enabled.lightMode) {
iconUrl = badge.icon.enabled.lightMode.svg || badge.icon.enabled.lightMode.url || iconUrl;
}
let currentProgress = 0;
let rawProgress = state.progress[goal.goalId];
if (typeof rawProgress === 'number') {
currentProgress = rawProgress;
} else if (rawProgress && typeof rawProgress === 'object') {
currentProgress = rawProgress.progress || 0;
}
const target = goal.threshold || 10;
let percentage = Math.min(100, (currentProgress / target) * 100);
const metric = goal.metric;
let progressText = `${currentProgress} / ${target}`;
let progressColor = "var(--duo-text-sub)";
if (isEarned) {
percentage = 100;
progressText = "COMPLETED";
progressColor = "var(--duo-green)";
}
const el = document.createElement('div');
el.className = 'qt-item' + (isOlder ? ' warning' : '') + (isEarned ? ' completed' : '');
el.innerHTML = `
${isOlder ? '<span class="qt-warning-icon" title="This quest is older than your account. Finishing it is risky.">⚠️</span>' : ''}
<img src="${iconUrl}" class="qt-icon" onerror="this.style.display='none'">
<div class="qt-info">
<div class="qt-name">${goal.title?.uiString || goal.goalId}</div>
<div class="qt-meta">Metric: ${metric}</div>
<div style="display:flex; justify-content:space-between; font-size:12px; font-weight:bold; color:${progressColor}; margin-bottom:2px;">
<span>${progressText}</span>
<span>${Math.round(percentage)}%</span>
</div>
<div class="qt-progress-bar-bg">
<div class="qt-progress-bar-fill ${isEarned ? 'full' : ''}" style="width: ${percentage}%"></div>
</div>
</div>
<div class="qt-item-actions">
<button class="qt-mini-btn" data-metric="${metric}" data-amt="1">+1</button>
<button class="qt-mini-btn" data-metric="${metric}" data-amt="10">+10</button>
<button class="qt-mini-btn gold" data-metric="${metric}" data-amt="50">Claim</button>
</div>
`;
const buttons = el.querySelectorAll('button');
buttons.forEach(btn => {
btn.onclick = () => {
if (isOlder && !confirm("This quest is dated BEFORE your account was created. Completing it may flag your account. Are you sure?")) return;
completeMetric(btn.dataset.metric, parseInt(btn.dataset.amt), goal.goalId);
};
});
container.appendChild(el);
});
}
setTimeout(() => {
createUI();
checkStoredCredentials();
}, 1000);
};
const checkDailyQuestStatus = async () => {
if (!sub || !jwt) {
console.log("[Quest Check] User data not available.");
return false;
}
const goalHeaders = getGoalHeaders();
if (!goalHeaders) return false;
try {
const [schemaRes, progressRes] = await Promise.all([
fetch(`${GOALS_API_URL}/schema?ui_language=en&_=${Date.now()}`, {
headers: goalHeaders
}),
fetch(`${GOALS_API_URL}/users/${sub}/progress?timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}&ui_language=en`, {
headers: goalHeaders
})
]);
if (!schemaRes.ok || !progressRes.ok) {
console.error("[Quest Check] Failed to fetch quest data.");
return false;
}
const schema = await schemaRes.json();
const progress = await progressRes.json();
const earnedQuests = new Set(progress.badges?.earned || []);
let dailyQuestsTotal = 0;
let dailyQuestsCompleted = 0;
if (!schema.goals) return false;
schema.goals.forEach(goal => {
const isDaily = goal.category?.includes('DAILY');
if (isDaily) {
dailyQuestsTotal++;
const isCompleted = earnedQuests.has(goal.badgeId) || earnedQuests.has(goal.goalId);
if (isCompleted) {
dailyQuestsCompleted++;
} else {
const goalProgress = progress.goals?.progress?.[goal.goalId];
const currentProgress = (typeof goalProgress === 'number') ? goalProgress : (goalProgress?.progress || 0);
const threshold = goal.threshold || 1;
if (currentProgress >= threshold) {
dailyQuestsCompleted++;
}
}
}
});
return dailyQuestsTotal > 0 && dailyQuestsTotal === dailyQuestsCompleted;
} catch (error) {
logToConsole(`Error checking daily quest status: ${error.message}`, 'error');
return false;
}
};
const updateDailyQuestButtonUI = async () => {
const questButton = document.querySelector('._option_btn[data-type="quest"]');
if (!questButton) return;
const existingOverlay = questButton.querySelector('._completed_overlay');
if (existingOverlay) {
existingOverlay.remove();
}
questButton.style.position = 'relative';
const areQuestsCompleted = await checkDailyQuestStatus();
if (areQuestsCompleted) {
const overlay = document.createElement('div');
overlay.className = '_completed_overlay';
overlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(20, 20, 20, 0.75); /* Lớp nền đen mờ */
backdrop-filter: blur(2px); /* Hiệu ứng làm mờ nhẹ */
color: white;
display: flex;
flex-direction: column; /* Xếp chồng các mục theo chiều dọc */
align-items: center;
justify-content: center;
border-radius: 10px; /* Bo góc khớp với nút */
text-align: center;
pointer-events: none;
z-index: 1;
padding: 5px;
box-sizing: border-box;
gap: 4px; /* Khoảng cách giữa ảnh và chữ */
animation: fadeIn 0.3s ease-out; /* Hiệu ứng xuất hiện mượt mà */
`;
overlay.innerHTML = `
<img src="https://friends.duolingo.com/kudos/assets/kudos_reaction_congrats.svg"
alt="Completed"
style="width: 38px; height: 38px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));">
<span style="font-weight: 700; font-size: 12px; line-height: 1.1;">
Daily Quests<br>Completed
</span>
`;
questButton.appendChild(overlay);
logToConsole('Daily quests are completed. Overlay updated.', 'success');
}
};
const styleSheet = document.createElement("style");
styleSheet.innerText = `
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
`;
document.head.appendChild(styleSheet);
const showItemShop = async () => {
console.log("🎁 Opening Item Shop...");
if (!userInfo || !sub || !jwt || !defaultHeaders) {
console.log("📊 User not loaded yet, initializing...");
logToConsole("Initializing user data for shop...", "info");
const success = await initializeFarming();
if (!success || !userInfo || !sub || !jwt) {
logToConsole("Failed to load user data. Please try again.", "error");
alert("Failed to load user data. Please reload the page and try again.");
return;
}
logToConsole("User data loaded successfully", "success");
}
if (!document.getElementById("_item_shop_anim_css")) {
const style = document.createElement("style");
style.id = "_item_shop_anim_css";
style.textContent = `
/* modal open/close (scoped) */
#_item_shop_modal { opacity: 0; }
#_item_shop_modal._open { opacity: 1; transition: opacity 160ms ease; }
#_item_shop_modal._closing { opacity: 0; transition: opacity 140ms ease; }
#_item_shop_modal ._modal_overlay { opacity: 0; transition: opacity 160ms ease; }
#_item_shop_modal._open ._modal_overlay { opacity: 1; }
#_item_shop_modal ._modal_container{
opacity: 0;
transform: translateY(12px) scale(.985);
transition: transform 180ms cubic-bezier(.16,1,.3,1), opacity 180ms cubic-bezier(.16,1,.3,1);
will-change: transform, opacity;
}
#_item_shop_modal._open ._modal_container{
opacity: 1;
transform: translateY(0) scale(1);
}
#_item_shop_modal._closing ._modal_container{
opacity: 0;
transform: translateY(10px) scale(.98);
}
/* card entrance (scoped) */
#_item_shop_modal ._shop_item_card{
will-change: transform, opacity;
}
#_item_shop_modal ._shop_item_card._enter{
animation: _shopFadeUp 360ms cubic-bezier(.16,1,.3,1) both;
}
@keyframes _shopFadeUp{
from{ opacity: 0; transform: translateY(10px); }
to{ opacity: 1; transform: translateY(0); }
}
/* button micro interactions (scoped) */
#_item_shop_modal ._shop_buy_btn{
transition: transform 120ms ease, filter 120ms ease;
}
#_item_shop_modal ._shop_buy_btn:hover{ filter: brightness(1.06); }
#_item_shop_modal ._shop_buy_btn:active{ transform: scale(.97); }
/* reload spin (scoped) */
#_item_shop_modal ._spin svg{ animation: _shopSpin 700ms linear 1; transform-origin: 50% 50%; }
@keyframes _shopSpin{ to{ transform: rotate(360deg); } }
/* reduced motion */
@media (prefers-reduced-motion: reduce){
#_item_shop_modal,
#_item_shop_modal ._modal_overlay,
#_item_shop_modal ._modal_container,
#_item_shop_modal ._shop_item_card._enter{
transition: none !important;
animation: none !important;
}
}
`;
document.head.appendChild(style);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const closeModal = async (modal) => {
if (!modal) return;
modal.classList.remove("_open");
modal.classList.add("_closing");
await sleep(160);
modal.remove();
};
const existingModal = document.getElementById("_item_shop_modal");
if (existingModal) existingModal.remove();
const modal = document.createElement("div");
modal.id = "_item_shop_modal";
modal.className = "_modal";
modal.style.display = "flex";
modal.innerHTML = `
<div class="_modal_overlay"></div>
<div class="_modal_container _wide">
<div class="_modal_header">
<h2>
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/0e58a94dda219766d98c7796b910beee.svg"
style="width: 32px; height: 32px; display:inline-block; vertical-align:middle; margin-right:8px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));">
Free Item Shop
</h2>
<button class="_close_modal_btn" id="_close_item_shop">
<span style="font-size: 18px;">❌</span>
</button>
</div>
<div class="_modal_content">
<div style="margin-bottom: 20px; display: flex; gap: 10px; align-items: center;">
<input type="text" id="_shop_search_input" class="_text_input"
placeholder="Search items..."
style="flex: 1; margin-bottom: 0;">
<button id="_shop_reload_btn" class="_icon_btn _primary" title="Reload Shop">
<img
src="https://uxwing.com/wp-content/themes/uxwing/download/arrow-direction/reload-icon.png"
alt="Reload"
style="width:20px;height:20px;display:block;filter: invert(1) brightness(2);"
/>
</button>
</div>
<div id="_shop_items_container" style="max-height: 500px; overflow-y: auto;">
<p style="text-align: center; color: var(--text-secondary); padding: 40px;">
Loading shop items...
</p>
</div>
<div id="_shop_empty_state" style="display: none; text-align: center; padding: 40px; color: var(--text-secondary);">
<p style="font-size: 16px; font-weight: 600;">No items found</p>
<p style="font-size: 14px; margin-top: 10px;">Try adjusting your search</p>
</div>
<p style="color: var(--text-secondary); font-size: 12px; text-align: center; margin-top: 20px;">
✨ All items are FREE! Click any item to claim it instantly.
</p>
</div>
</div>
`;
document.body.appendChild(modal);
requestAnimationFrame(() => modal.classList.add("_open"));
document.getElementById("_close_item_shop")?.addEventListener("click", () => closeModal(modal));
modal.querySelector("._modal_overlay")?.addEventListener("click", () => closeModal(modal));
const loadShopItems = async () => {
const container = document.getElementById("_shop_items_container");
const emptyState = document.getElementById("_shop_empty_state");
container.innerHTML =
'<p style="text-align: center; color: var(--text-secondary); padding: 40px;">Loading shop items...</p>';
try {
const items = await getShopItems(defaultHeaders);
if (!items || items.length === 0) {
container.innerHTML =
'<p style="text-align: center; color: var(--error-color); padding: 40px;">Failed to load shop items. Please try again.</p>';
return;
}
const categorizedItems = categorizeItems(items);
const categories = {};
categorizedItems.forEach((item) => {
if (!categories[item.category]) categories[item.category] = [];
categories[item.category].push(item);
});
let html = "";
for (const [categoryName, categoryItems] of Object.entries(categories)) {
html += `
<div class="_shop_category" data-category="${categoryName}">
<h3 style="font-size: 14px; font-weight: 800; text-transform: uppercase; color: var(--text-secondary); margin: 20px 0 10px; padding: 0 5px; position: relative; text-align: center;">
<span style="background: var(--bg-card); padding: 0 10px; position: relative; z-index: 1;">${categoryName}</span>
<span style="position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--border-color); z-index: 0;"></span>
</h3>
<div class="_shop_grid">
`;
categoryItems.forEach((item) => {
html += `
<div class="_shop_item_card" data-item-name="${item.displayName.toLowerCase()}">
<div class="_shop_item_icon">
<img src="${item.icon}" alt="${item.displayName}" style="width: 45px; height: 45px; object-fit: contain; filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1));">
</div>
<div class="_shop_item_name">${item.displayName}</div>
<button class="_shop_buy_btn" data-item-id="${item.id}" data-item-name="${item.displayName}">
Get Free
</button>
</div>
`;
});
html += `
</div>
</div>
`;
}
container.innerHTML = html;
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
if (!reduceMotion) {
const cards = container.querySelectorAll("._shop_item_card");
cards.forEach((card, idx) => {
card.style.animationDelay = `${Math.min(idx * 18, 220)}ms`;
card.classList.add("_enter");
});
} else {
container.querySelectorAll("._shop_item_card").forEach((c) => {
c.style.opacity = "1";
c.style.transform = "none";
});
}
container.querySelectorAll("._shop_buy_btn").forEach((btn) => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
const itemId = btn.dataset.itemId;
const itemName = btn.dataset.itemName;
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.textContent = "Processing...";
const success = await buyItem(itemId, { displayName: itemName });
if (success) {
btn.textContent = "Got it!";
btn.style.background = "var(--success-color)";
btn.style.color = "white";
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalHTML;
btn.style.background = "";
btn.style.color = "";
}, 3000);
} else {
btn.textContent = "❌ FAILED";
btn.style.background = "var(--error-color)";
btn.style.color = "white";
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalHTML;
btn.style.background = "";
btn.style.color = "";
}, 2000);
}
});
});
} catch (error) {
console.error("Error loading shop:", error);
container.innerHTML =
'<p style="text-align: center; color: var(--error-color); padding: 40px;">Error loading shop. Please try again.</p>';
}
};
const searchInput = document.getElementById("_shop_search_input");
searchInput?.addEventListener("input", (e) => {
const searchTerm = e.target.value.toLowerCase().trim();
const container = document.getElementById("_shop_items_container");
const emptyState = document.getElementById("_shop_empty_state");
const categories = container.querySelectorAll("._shop_category");
let totalVisible = 0;
categories.forEach((category) => {
const items = category.querySelectorAll("._shop_item_card");
let categoryVisible = 0;
items.forEach((item) => {
const itemName = item.dataset.itemName || "";
const isVisible = itemName.includes(searchTerm);
item.style.display = isVisible ? "flex" : "none";
if (isVisible) categoryVisible++;
});
category.style.display = categoryVisible > 0 ? "block" : "none";
totalVisible += categoryVisible;
});
container.style.display = totalVisible > 0 ? "block" : "none";
emptyState.style.display = totalVisible === 0 ? "block" : "none";
});
document.getElementById("_shop_reload_btn")?.addEventListener("click", (e) => {
const btn = e.currentTarget;
btn.classList.add("_spin");
loadShopItems().finally(() => setTimeout(() => btn.classList.remove("_spin"), 750));
});
await loadShopItems();
};
const autoSolver = {
findReact: (dom, traverseUp = 1) => {
const key = Object.keys(dom).find(key => {
return key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$");
});
const domFiber = dom[key];
if (!domFiber) return null;
const GetCompFiber = fiber => {
let parentFiber = fiber.return;
while (typeof parentFiber.type == "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = GetCompFiber(compFiber);
}
return compFiber.stateNode;
},
determineChallengeType: () => {
try {
const t = window.sol?.type;
if (!t) return false;
// ── skip/audio types ──────────────────────────────────────────
if (t === 'speak' || t === 'listenSpeak' ||
document.querySelector('[data-test="challenge challenge-listenSpeak"]') ||
document.querySelectorAll('[data-test*="challenge-speak"]').length > 0) return 'Challenge Speak';
if (t === 'listenMatch') return 'Listen Match';
// ── story / dialogue UI (FmlUF wrapper) ───────────────────────
if (document.querySelector('.FmlUF')) {
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';
}
// ── table types ───────────────────────────────────────────────
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';
// ── listen / type what you hear ───────────────────────────────
// listenTap: tap words from bank in order (has correctTokens + tap-token bank)
if (t === 'listenTap') return 'Listen Tap';
// listen: type the transcription into a textarea
if (t === 'listen') return 'Listen Type';
// ── translate: tap-bank or textarea ──────────────────────────
if (t === 'translate') return 'Translate';
// ── completeReverseTranslation: fill one blank in text-input ──
if (t === 'completeReverseTranslation') return 'Complete Reverse';
// ── partialReverseTranslate: contenteditable span ─────────────
if (document.querySelectorAll('[data-test*="challenge-partialReverseTranslate"]').length > 0) return 'Partial Reverse';
// ── judge: true/false style (challenge-judge-text buttons) ────
if (t === 'judge') return 'Judge';
// ── dialogue / characterIntro / selectTranscription ───────────
if (t === 'dialogue' || t === 'characterIntro' || t === 'selectTranscription') return 'Dialogue';
// ── character match ───────────────────────────────────────────
if (t === 'characterMatch' || t === 'match') {
if (document.querySelectorAll('[data-test$="challenge-tap-token"]').length > 0) return 'Pairs';
}
// ── select / form / comprehension / pronunciation → choice-card
if (t === 'select' || t === 'characterSelect' || t === 'form' ||
t === 'readComprehension' || t === 'listenComprehension' ||
t === 'selectPronunciation') {
return 'Select Card';
}
// ── challenge name (article + text-input) ─────────────────────
if (document.querySelectorAll('[data-test*="challenge-name"]').length > 0 &&
document.querySelectorAll('[data-test="challenge-choice"]').length > 0) return 'Challenge Name';
// ── generic choice (multiple choice with challenge-choice) ─────
if (document.querySelectorAll('[data-test="challenge-choice"]').length > 0) {
if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) return 'Challenge Choice with Text Input';
return 'Challenge Choice';
}
// ── tap tokens ────────────────────────────────────────────────
if (document.querySelectorAll('[data-test$="challenge-tap-token"]').length > 0) {
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.querySelectorAll('[data-test="challenge-tap-token-text"]').length > 0) return 'Fill in the Gap';
// ── text inputs ───────────────────────────────────────────────
if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) return 'Challenge Text Input';
if (document.querySelectorAll('textarea[data-test="challenge-translate-input"]').length > 0) return 'Challenge Translate Input';
return false;
} catch (error) {
return false;
}
},
setInputValue: (element, value) => {
const isTextarea = element.tagName === 'TEXTAREA';
const prototype = isTextarea ? window.HTMLTextAreaElement : window.HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(prototype.prototype, "value").set;
setter.call(element, value);
element.dispatchEvent(new Event('input', {
bubbles: true
}));
},
delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
handleChallengeName: async () => {
const articles = window.sol.articles;
const correctSolution = window.sol.correctSolutions[0];
const matchingArticle = articles.find(article => correctSolution.startsWith(article));
if (matchingArticle !== undefined) {
const matchingIndex = articles.indexOf(matchingArticle);
const remainingValue = correctSolution.substring(matchingArticle.length).trim();
const selectedElement = document.querySelector(`[data-test="challenge-choice"]:nth-child(${matchingIndex + 1})`);
if (selectedElement) {
selectedElement.click();
await autoSolver.delay(50);
}
const input = document.querySelector('[data-test="challenge-text-input"]');
if (input) autoSolver.setInputValue(input, remainingValue);
}
},
handlePairs: async () => {
const buttons = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
const texts = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
if (texts.length !== buttons.length || buttons.length === 0) return;
for (const pair of window.sol.pairs || []) {
for (let i = 0; i < buttons.length; i++) {
const button = buttons[i];
if (button.disabled) continue;
const text = texts[i].innerText.toLowerCase().trim();
const matches = text === pair.transliteration?.toLowerCase().trim() ||
text === pair.character?.toLowerCase().trim() ||
text === pair.learningToken?.toLowerCase().trim() ||
text === pair.fromToken?.toLowerCase().trim();
if (matches) {
button.click();
await autoSolver.delay(50);
}
}
}
},
handleTokensRun: async () => {
const allTokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const clickedTokens = [];
const tokensToClick = [];
for (const correctToken of window.sol.correctTokens) {
const matchingElements = Array.from(allTokens).filter(el => el.textContent.trim() === correctToken.trim());
if (matchingElements.length > 0) {
const matchIndex = clickedTokens.filter(token => token.textContent.trim() === correctToken.trim()).length;
const elementToClick = matchingElements[matchIndex] || matchingElements[0];
if (!elementToClick.disabled) {
tokensToClick.push(elementToClick);
clickedTokens.push(elementToClick);
}
}
}
tokensToClick.forEach(token => token.click());
},
handleIndicesRun: async () => {
if (!window.sol.correctIndices) return;
const wordBank = document.querySelector('div[data-test="word-bank"]') || document.querySelector('.eSgkc');
if (!wordBank) return;
const bankButtons = Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not(span)'));
for (const index of window.sol.correctIndices) {
if (index >= 0 && index < bankButtons.length) {
const button = bankButtons[index];
if (!button.disabled && button.getAttribute('aria-disabled') !== 'true') {
button.click();
await autoSolver.delay(50);
}
}
}
},
handleTapCompleteTable: async () => {
const solutionRows = window.sol.displayTableTokens.slice(1);
const tableRowElements = document.querySelectorAll('tbody tr');
const wordBank = document.querySelector('div[data-test="word-bank"]');
const wordBankButtons = wordBank ? wordBank.querySelectorAll('button[data-test*="-challenge-tap-token"]') : [];
const usedWordBankIndexes = new Set();
for (let rowIndex = 0; rowIndex < solutionRows.length; rowIndex++) {
const solutionRow = solutionRows[rowIndex];
const answerCellData = solutionRow[1];
const correctToken = answerCellData.find(token => token.isBlank);
if (correctToken) {
const correctAnswerText = correctToken.text;
const currentRowElement = tableRowElements[rowIndex];
let clicked = false;
const buttons = currentRowElement.querySelectorAll('button[data-test*="-challenge-tap-token"]');
for (const button of buttons) {
const buttonTextElm = button.querySelector('[data-test="challenge-tap-token-text"]');
if (buttonTextElm && buttonTextElm.innerText.trim() === correctAnswerText && !button.disabled) {
button.click();
await autoSolver.delay(50);
clicked = true;
break;
}
}
if (!clicked && wordBankButtons.length > 0) {
for (let i = 0; i < wordBankButtons.length; i++) {
if (usedWordBankIndexes.has(i)) continue;
const button = wordBankButtons[i];
const buttonTextElm = button.querySelector('[data-test="challenge-tap-token-text"]');
if (buttonTextElm && buttonTextElm.innerText.trim() === correctAnswerText && !button.disabled) {
button.click();
await autoSolver.delay(50);
usedWordBankIndexes.add(i);
break;
}
}
}
}
}
},
handleChallenge: async (type) => {
try {
switch (type) {
case 'Challenge Speak':
case 'Listen Match':
case 'Listen Speak':
document.querySelector('button[data-test="player-skip"]')?.click();
break;
// ── NEW: Select Card (select / characterSelect / form / comprehension / pronunciation)
case 'Select Card': {
const idx = window.sol.correctIndex ?? 0;
// try choice-card first, fallback to challenge-choice
const cards = document.querySelectorAll('[data-test="challenge-choice-card"]');
if (cards.length > 0) {
cards[idx]?.click();
} else {
document.querySelectorAll('[data-test="challenge-choice"]')[idx]?.click();
}
break;
}
// ── NEW: Judge (true/false)
case 'Judge': {
const ci = window.sol.correctIndices?.[0] ?? 0;
document.querySelectorAll('[data-test="challenge-judge-text"]')[ci]?.click();
break;
}
// ── NEW: Dialogue / characterIntro / selectTranscription
case 'Dialogue': {
const idx = window.sol.correctIndex ?? 0;
// These use judge-text buttons
const judgeItems = document.querySelectorAll('[data-test="challenge-judge-text"]');
if (judgeItems.length > 0) {
judgeItems[idx]?.click();
} else {
document.querySelectorAll('[data-test="challenge-choice"]')[idx]?.click();
}
break;
}
// ── NEW: Translate (tap word bank OR textarea)
case 'Translate': {
const { correctTokens, correctSolutions } = window.sol;
if (correctTokens && correctTokens.length > 0) {
const tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const usedIndexes = [];
for (const word of correctTokens) {
for (let i = 0; i < tokens.length; i++) {
if (usedIndexes.includes(i)) continue;
if (tokens[i].innerText.trim() === word.trim() && !tokens[i].disabled) {
tokens[i].click();
usedIndexes.push(i);
await autoSolver.delay(40);
break;
}
}
}
} else if (correctSolutions) {
const ta = document.querySelector('textarea[data-test="challenge-translate-input"]');
if (ta) autoSolver.setInputValue(ta, correctSolutions[0]);
}
break;
}
// ── NEW: Listen Tap (tap words in order from word bank)
case 'Listen Tap': {
const tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
const usedIdx = [];
for (const word of (window.sol.correctTokens || [])) {
for (let i = 0; i < tokens.length; i++) {
if (usedIdx.includes(i)) continue;
if (tokens[i].innerText.trim() === word.trim() && !tokens[i].disabled) {
tokens[i].click();
usedIdx.push(i);
await autoSolver.delay(40);
break;
}
}
}
break;
}
// ── NEW: Listen Type (type the transcription)
case 'Listen Type': {
const answer = 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) autoSolver.setInputValue(ta, answer);
break;
}
// ── NEW: Complete Reverse Translation (fill blank in text-input)
case 'Complete Reverse': {
const blank = window.sol.displayTokens?.find(t => t.isBlank);
const answer = blank?.text || window.sol.correctSolutions?.[0] || '';
const input = document.querySelector('[data-test="challenge-text-input"]');
if (input) autoSolver.setInputValue(input, answer);
break;
}
case 'Challenge Choice':
document.querySelectorAll("[data-test='challenge-choice']")[window.sol.correctIndex]?.click();
break;
case 'Challenge Choice with Text Input': {
const choiceInput = document.querySelector('[data-test="challenge-text-input"]');
if (choiceInput) {
const answer = window.sol.correctSolutions ? window.sol.correctSolutions[0].split(/(?<=^\S+)\s/)[1] : (window.sol.displayTokens ? window.sol.displayTokens.find(t => t.isBlank)?.text : window.sol.prompt);
autoSolver.setInputValue(choiceInput, answer);
}
break;
}
case 'Challenge Text Input': {
const input = document.querySelector('[data-test="challenge-text-input"]');
if (input) {
const answer = window.sol.correctSolutions?.[0] || (window.sol.displayTokens ? window.sol.displayTokens.find(t => t.isBlank)?.text : window.sol.prompt);
autoSolver.setInputValue(input, answer);
}
break;
}
case 'Challenge Translate Input': {
const textarea = document.querySelector('textarea[data-test="challenge-translate-input"]');
if (textarea) autoSolver.setInputValue(textarea, window.sol.correctSolutions?.[0] || window.sol.prompt);
break;
}
case 'Partial Reverse': {
const partialElm = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector("span[contenteditable]");
if (partialElm) {
const text = window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join('')?.trim();
const setter = Object.getOwnPropertyDescriptor(Node.prototype, "textContent").set;
setter.call(partialElm, text);
partialElm.dispatchEvent(new Event('input', {
bubbles: true
}));
}
break;
}
case 'Type Cloze': {
const clozeInput = document.querySelector('input[type="text"].b4jqk');
if (clozeInput) {
const targetToken = window.sol.displayTokens.find(t => t.damageStart !== undefined);
if (targetToken) {
const correctEnding = targetToken.text.slice(targetToken.damageStart);
autoSolver.setInputValue(clozeInput, correctEnding);
}
}
break;
}
case 'Type Cloze Table': {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => typeof t.damageStart === "number");
if (answerCell && tableRows[i]) {
const input = tableRows[i].querySelector('input[type="text"].b4jqk');
if (input) {
const correctEnding = answerCell.text.slice(answerCell.damageStart);
autoSolver.setInputValue(input, correctEnding);
}
}
});
break;
}
case 'Tap Cloze Table': {
const tapTableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => typeof t.damageStart === "number");
if (!answerCell || !tapTableRows[i]) return;
const wordBank = document.querySelector('[data-test="word-bank"]');
const wordButtons = wordBank ? Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')) : [];
const correctWord = answerCell.text;
const correctEnding = correctWord.slice(answerCell.damageStart);
let endingMatched = "";
for (let btn of wordButtons) {
if (!correctEnding.startsWith(endingMatched + btn.innerText)) continue;
btn.click();
endingMatched += btn.innerText;
if (endingMatched === correctEnding) break;
}
});
break;
}
case 'Type Complete Table': {
const completeTableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => t.isBlank);
if (!answerCell || !completeTableRows[i]) return;
const input = completeTableRows[i].querySelector('input[type="text"].b4jqk');
if (input) autoSolver.setInputValue(input, answerCell.text);
});
break;
}
case 'Pattern Tap Complete': {
const patternWordBank = document.querySelector('[data-test="word-bank"]');
if (!patternWordBank) return;
const correctIndex = window.sol.correctIndex ?? 0;
const correctText = window.sol.choices[correctIndex];
const patternButtons = Array.from(patternWordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])'));
const targetButton = patternButtons.find(btn => btn.innerText.trim() === correctText);
if (targetButton) targetButton.click();
break;
}
case 'Story Arrange': {
const arrangeChoices = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
for (let i = 0; i < window.sol.phraseOrder.length; i++) {
arrangeChoices[window.sol.phraseOrder[i]].click();
await autoSolver.delay(50);
}
break;
}
case 'Story Multiple Choice': {
const storyChoices = document.querySelectorAll('[data-test="stories-choice"]');
storyChoices[window.sol.correctAnswerIndex]?.click();
break;
}
case 'Story Point to Phrase': {
const phraseChoices = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
let phraseCorrectIndex = -1;
for (let i = 0; i < window.sol.parts.length; i++) {
if (window.sol.parts[i].selectable === true) {
phraseCorrectIndex += 1;
if (window.sol.correctAnswerIndex === i) {
phraseChoices[phraseCorrectIndex]?.parentElement.click();
break;
}
}
}
break;
}
case 'Story Pairs': {
const storyButtons = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
const storyTexts = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
const textToElementMap = new Map();
for (let i = 0; i < storyButtons.length; i++) {
const text = storyTexts[i].innerText.toLowerCase().trim();
textToElementMap.set(text, storyButtons[i]);
}
for (const key in window.sol.dictionary) {
if (window.sol.dictionary.hasOwnProperty(key)) {
const value = window.sol.dictionary[key];
const keyPart = key.split(":")[1].toLowerCase().trim();
const normalizedValue = value.toLowerCase().trim();
const element1 = textToElementMap.get(keyPart);
const element2 = textToElementMap.get(normalizedValue);
if (element1 && !element1.disabled) {
element1.click();
await autoSolver.delay(50);
}
if (element2 && !element2.disabled) {
element2.click();
await autoSolver.delay(50);
}
}
}
break;
}
case 'Challenge Name':
await autoSolver.handleChallengeName();
break;
case 'Pairs':
await autoSolver.handlePairs();
break;
case 'Tokens Run':
await autoSolver.handleTokensRun();
break;
case 'Indices Run':
case 'Fill in the Gap':
await autoSolver.handleIndicesRun();
break;
case 'Tap Complete Table':
await autoSolver.handleTapCompleteTable();
break;
}
} catch (error) {
console.error('Error handling challenge:', error);
}
},
clickNext: () => {
setTimeout(() => {
const nextBtn = document.querySelector('[data-test="player-next"]') || document.querySelector('[data-test="stories-player-continue"]') || document.querySelector('[data-test="stories-player-done"]');
if (!nextBtn) return;
const isDisabled = nextBtn.getAttribute('aria-disabled') === 'true' || nextBtn.disabled;
if (!isDisabled) {
nextBtn.click();
if (isAutoMode) {
setTimeout(() => {
if (nextBtn.classList.contains('_2oGJR')) nextBtn.click();
}, 100);
}
}
}, 100);
},
solve: async () => {
const skipSelectors = ['[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'];
skipSelectors.forEach(sel => document.querySelector(sel)?.click());
try {
let mainElement = document.querySelector('._3yE3H');
if (!mainElement) mainElement = document.querySelector('.RMEuZ._1GVfY') || document.querySelector('[data-test="challenge"]') || document.querySelector('[class*="challenge"]');
if (!mainElement) {
autoSolver.clickNext();
return;
}
const reactInstance = autoSolver.findReact(mainElement);
window.sol = reactInstance?.props?.currentChallenge;
if (!window.sol) {
autoSolver.clickNext();
return;
}
const challengeType = autoSolver.determineChallengeType();
if (challengeType) {
await autoSolver.handleChallenge(challengeType);
}
autoSolver.clickNext();
} catch (error) {
console.error('Solve error:', error);
autoSolver.clickNext();
}
},
toggleAutoMode: () => {
isAutoMode = !isAutoMode;
autoSolver.updateUI();
if (isAutoMode) {
solvingIntervalId = setInterval(autoSolver.solve, SOLVE_SPEED * 1000);
} else {
clearInterval(solvingIntervalId);
}
},
createUI: () => {
if (solverUI) return;
solverUI = document.createElement('div');
solverUI.id = 'nightware-solver-ui';
solverUI.style.cssText = `position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 999997; display: flex; gap: 12px; animation: slideUp 0.3s ease-out;`;
solverUI.innerHTML = `
<button class="nw-solver-btn" id="nw-solve-single" style="padding: 12px 24px; background: #89e219; border: none; border-bottom: 4px solid #58cc02; border-radius: 12px; color: white; font-weight: 700; font-size: 14px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">SOLVE</button>
<button class="nw-solver-btn" id="nw-solve-all" style="padding: 12px 24px; background: #ffc800; border: none; border-bottom: 4px solid #ff9600; border-radius: 12px; color: white; font-weight: 700; font-size: 14px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">SOLVE ALL</button>
`;
const style = document.createElement('style');
style.textContent = `@keyframes slideUp { from { opacity: 0; transform: translateX(-50%) translateY(20px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } .nw-solver-btn:hover { filter: brightness(1.1); transform: translateY(-2px); } .nw-solver-btn:active { border-bottom: 0px; transform: translateY(2px); }`;
document.head.appendChild(style);
document.body.appendChild(solverUI);
document.getElementById('nw-solve-single').addEventListener('click', autoSolver.solve);
document.getElementById('nw-solve-all').addEventListener('click', autoSolver.toggleAutoMode);
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
if (e.shiftKey) autoSolver.toggleAutoMode();
else autoSolver.solve();
}
});
},
removeUI: () => {
if (solverUI) {
solverUI.remove();
solverUI = null;
}
if (solvingIntervalId) {
clearInterval(solvingIntervalId);
solvingIntervalId = null;
}
isAutoMode = false;
},
updateUI: () => {
const btn = document.getElementById('nw-solve-all');
if (btn) {
btn.textContent = isAutoMode ? 'PAUSE' : 'SOLVE ALL';
btn.style.background = isAutoMode ? '#ff4b4b' : '#1cb0f6';
btn.style.borderBottomColor = isAutoMode ? '#cc0000' : '#2b70c9';
}
},
checkAndToggle: () => {
const currentIsInLesson = window.location.pathname.includes('/lesson') || window.location.pathname.includes('/practice');
if (currentIsInLesson !== isInLesson) {
isInLesson = currentIsInLesson;
if (isInLesson && INJECT_SOLVER_ENABLED) {
setTimeout(() => autoSolver.createUI(), 500);
} else {
autoSolver.removeUI();
}
}
}
};
setInterval(() => autoSolver.checkAndToggle(), 1000);
const initInterface = () => {
const containerHTML = `
<div id="_backdrop"></div>
<div id="_container" class="theme-${currentTheme}">
<div id="_header">
<div class="_header_top">
<div class="_brand">
<a href="https://twisk.fun/discord" target="_blank" rel="noopener noreferrer">
<div class="_logo_container">
<div class="_logo"
style="
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
border: 2px solid #1E88E5;
"
>
<img src=" https://github.com/FutureCLI/DuoHacker/blob/main/images/Logo_TypePNG_DuoHacker.png?raw=true"
alt="Rocket"
style="
width: 110%;
height: 110%;
object-fit: cover;
"
>
</div>
</div>
</a>
<a href="https://twisk.fun" target="_blank" rel="noopener noreferrer" style="text-decoration: none; color: inherit;">
<div class="_brand_text">
<h1>DuoHacker</h1>
<span class="_version_badge">Full</span>
</div>
</a>
</div>
<div class="_header_controls">
<button id="_gift_notification_btn" class="_control_btn _gift" style="background: linear-gradient(135deg, #ff6b9d 0%, #c44569 100%); box-shadow: 0 4px 12px rgba(255, 107, 157, 0.4);">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/legendary/158dbe277bf83116d04692b969a27aa3.svg"
style="width: 28px; height: 28px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));">
</button>
<button id="_leaderboard_btn" class="_control_btn _success" style="background: linear-gradient(135deg, #81c784 0%, #4caf50 100%); box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);">
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/ca9178510134b4b0893dbac30b6670aa.svg"
style="width: 32px; height: 32px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); object-fit: contain;">
</button>
<button id="_monthly_badges" class="_control_btn _success"
style="background: linear-gradient(135deg, #ab47bc 0%, #7b1fa2 100%); box-shadow: 0 4px 12px rgba(171, 71, 188, 0.3);">
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/7ef36bae3f9d68fc763d3451b5167836.svg"
style="width: 30px; height: 30px; object-fit: contain; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">
</button>
<button id="_item_shop_btn" class="_control_btn _success"
style="background: linear-gradient(135deg, #ffe599 0%, #f1c232 100%); box-shadow: 0 4px 12px rgba(241, 194, 50, 0.4);">
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/0e58a94dda219766d98c7796b910beee.svg"
style="width: 28px; height: 28px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));">
</button>
<button id="_accounts_btn" class="_control_btn _accounts">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/48b8884ac9d7513e65f3a2b54984c5c4.svg"
style="width: 26px; height: 26px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));">
<span class="_badge">${savedAccounts.length}</span>
</button>
<button id="_settings_btn" class="_control_btn _settings">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Windows_Settings_icon.svg/2184px-Windows_Settings_icon.svg.png"
style="width: 26px; height: 26px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));">
</button>
<button id="_minimize_btn" class="_control_btn _minimize" title="Minimize">
<span style="font-size: 18px;">➖</span>
</button>
<button id="_close_btn" class="_control_btn _close" title="Close">
<span style="font-size: 18px;">✖️</span>
</button>
</div>
</div>
</div>
<div id="_main_content" style="display:none">
<div class="_announce_bar">
<span>👍Join our community to get update annoucements!</span>
<a href="https://twisk.fun/discord" target="_blank" class="_announce_btn">Join</a>
</div>
<div class="_profile_card">
<div class="_profile_header">
<div class="_avatar">
<span style="font-size: 28px;">👤</span>
</div>
<div class="_profile_info">
<h2 id="_username">Loading...</h2>
<p id="_user_details">Fetching data...</p>
</div>
<div style="display:flex; gap:6px;">
<button id="_webhook_toggle_btn" class="_icon_btn _primary" title="Setup Discord Webhook">
<span style="font-size: 16px;">🔔</span>
</button>
<button id="_save_account_btn" class="_icon_btn _success" title="Save Current Account">
<span style="font-size: 16px;">💾</span>
</button>
<button id="_refresh_profile" class="_icon_btn _primary" title="Refresh Profile">
<span style="font-size: 16px;">🔄</span>
</button>
</div>
</div>
<div id="_webhook_panel" style="display:none; background:var(--bg-secondary); padding:10px; border-radius:10px; margin-bottom:12px; border:1px solid var(--border-color); animation: fadeIn 0.2s;">
<label style="font-size:11px; color:var(--text-secondary); display:block; margin-bottom:4px;">Discord Webhook URL</label>
<div style="display:flex; gap:6px;">
<input type="text" id="_webhook_input" class="_text_input"
style="font-size:12px; padding:6px 10px;"
placeholder="https://discord.com/api/webhooks/..."
value="${webhookUrl}">
<button id="_test_webhook_btn" class="_setting_btn _primary" style="width:auto; padding:6px 12px; font-size:12px;">Link</button>
</div>
</div>
<div class="_stats_row">
<div class="_stat_item">
<div class="_stat_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg" alt="XP Icon"></div>
<div class="_stat_info">
<span class="_stat_value" id="_current_xp">0</span>
<span class="_stat_label">Total XP</span>
</div>
</div>
<div class="_stat_item">
<div class="_stat_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg" alt="streak Icon"></div>
<div class="_stat_info">
<span class="_stat_value" id="_current_streak">0</span>
<span class="_stat_label">Streak</span>
</div>
</div>
<div class="_stat_item">
<div class="_stat_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" alt="gem Icon"></div>
<div class="_stat_info">
<span class="_stat_value" id="_current_gems">0</span>
<span class="_stat_label">Gems</span>
</div>
</div>
</div>
</div>
<div class="_mode_section">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<h3 style="margin:0;">Farming Delay</h3>
<span id="_delay_display" style="font-size:13px; font-weight:700; color:var(--primary-color); background:var(--bg-secondary); padding:3px 10px; border-radius:8px;">${CUSTOM_DELAY}ms</span>
</div>
<input type="range" id="_delay_slider" min="100" max="5000" step="100" value="${CUSTOM_DELAY}"
style="width:100%; accent-color:var(--primary-color); cursor:pointer; height:6px; border-radius:3px;">
<div style="display:flex; justify-content:space-between; font-size:11px; color:var(--text-muted); margin-top:4px;">
<span>100ms (Fast)</span><span>5000ms (Safe)</span>
</div>
</div>
<div class="_options_section">
<h3>Farming Options</h3>
<div class="_option_grid">
<button class="_option_btn" data-type="xp_10">
<div class="_option_icon">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg" alt="XP Icon">
</div>
<span>Farm XP</span>
</button>
<button class="_option_btn" data-type="gems">
<div class="_option_icon">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" alt="Gems Icon">
</div>
<span>Farm Gem</span>
</button>
<button class="_option_btn" data-type="quest">
<div class="_option_icon">
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/7ef36bae3f9d68fc763d3451b5167836.svg" alt="Quest Icon">
</div>
<span>Daily Quest</span>
</button>
<button class="_option_btn" data-type="streak_farm">
<div class="_option_icon">
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg" alt="Streak Icon">
</div>
<span>Farm Streak</span>
</button>
<button class="_option_btn" data-type="league_farm">
<div class="_option_icon">
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/ca9178510134b4b0893dbac30b6670aa.svg" alt="League Icon">
</div>
<span>Auto League</span>
</button>
<button class="_option_btn" data-type="farm_lesson">
<div class="_option_icon">
<img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/784035717e2ff1d448c0f6cc4efc89fb.svg" alt="Lesson Icon">
</div>
<span>Farm Practices</span>
</button>
</div>
</div>
<div class="_control_panel">
<button id="_start_farming" class="_start_btn">
<span class="_btn_text">Start Farming</span>
</button>
<button id="_stop_farming" class="_stop_btn" style="display:none">
<span class="_btn_text">Stop Farming</span>
</button>
</div>
<div class="_inject_section" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<div class="_setting_item" style="margin-bottom: 0;">
<div class="_toggle_container">
<label class="_toggle_label" style="font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
<span class="_toggle_icon_wrapper"><img src="https://d35aaqx5ub95lt.cloudfront.net/vendor/5187f6694476a769d4a4e28149867e3e.svg" alt="Solver Icon"></span> Inject Solver Button
</label>
<div class="_toggle_switch ${INJECT_SOLVER_ENABLED ? '_active' : ''}" id="_inject_solver_toggle">
<div class="_toggle_slider"></div>
</div>
</div>
<p class="_setting_description" style="margin-top: 5px; font-size: 13px; color: var(--text-secondary);">
Automatically show floating "SOLVE" & "SOLVE ALL" buttons when you enter a lesson.
</p>
</div>
</div>
<div class="_live_stats">
<h3>Live Statistics</h3>
<div class="_stats_grid">
<div class="_live_stat">
<div class="_live_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg" alt="XP Earned Icon"></div>
<div class="_live_data">
<span id="_earned_xp">0</span>
<small>XP Earned</small>
</div>
</div>
<div class="_live_stat">
<div class="_live_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" alt="Gems Earned Icon"></div>
<div class="_live_data">
<span id="_earned_gems">0</span>
<small>Gems Earned</small>
</div>
</div>
<div class="_live_stat">
<div class="_live_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg" alt="Streak Gained Icon"></div>
<div class="_live_data">
<span id="_earned_streak">0</span>
<small>Streak Gained</small>
</div>
</div>
<div class="_live_stat">
<div class="_live_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/48b8884ac9d7513e65f3a2b54984c5c4.svg" alt="lesson slved Icon"></div>
<div class="_live_data">
<span id="_earned_lessons">0</span>
<small>Lessons Solved</small>
</div>
</div>
<div class="_live_stat">
<div class="_live_icon"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/goals/974e284761265b0eb6c9fd85243c5c4b.svg" alt="time Icon"></div>
<div class="_live_data">
<span id="_farming_time">00:00</span>
<small>Time Elapsed</small>
</div>
</div>
</div>
</div>
<div class="_console_section">
<div class="_console_header">
<h3>Activity Log</h3>
<button id="_clear_console" class="_clear_btn">Clear</button>
</div>
<div id="_console_output" class="_console">
<div class="_log_entry _info">
<span class="_log_time">${new Date().toLocaleTimeString()}</span>
<span class="_log_msg">DuoHacker loaded</span>
</div>
</div>
</div>
</div>
<div id="_join_section" class="_join_section">
<div class="_join_content">
<img id="_join_btn"
src="https://d35aaqx5ub95lt.cloudfront.net/images/c4527dd72a1ee03a7a9999af0b01e392.svg"
style="width: 180px; height: auto; cursor: pointer; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); filter: drop-shadow(0 10px 20px rgba(0,0,0,0.15));"
onmouseover="this.style.transform='scale(1.1) rotate(-3deg)'"
onmouseout="this.style.transform='scale(1) rotate(0deg)'"
alt="Unlock Tool"
>
<h3 style="margin-top: 20px; color: var(--text-primary); font-weight: 800;">Tap to Open</h3>
<p style="color: var(--text-secondary); font-size: 13px;">Unlock DuoHacker features</p>
</div>
</div>
<div class="_footer">
<span>© DuoHacker by <a href="https://www.duolingo.com/u/561583074752767" target="_blank" style="color: #00FFFF; text-decoration: none; text-shadow: 0 0 5px #39FF14, 0 0 10px #39FF14;">2pixel</a></span>
<div class="_footer_socials">
<a href="https://twisk.fun/discord" target="_blank" title="Discord">
<img
alt="Discord"
src="data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20width%3D%2724%27%20height%3D%2724%27%20viewBox%3D%270%200%20256%20256%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%3E%3Cg%20transform%3D%27translate(0%2C24)%27%3E%3Cpath%20fill%3D%27%23fff%27%20d%3D%27M216.9%2016.5A208.1%20208.1%200%200%200%20164.7%200c-2.3%204-4.4%208.2-6.2%2012.5a192.5%20192.5%200%200%200-61%200C95.6%208.2%2093.4%204%2091.1%200A208.3%20208.3%200%200%200%2038.9%2016.5C6.6%2064.6-2%20111.4%201.8%20157.6c18.9%2014%2041%2024.8%2064.7%2031.6%205.2-7.1%209.8-14.7%2013.6-22.8-7.5-2.8-14.7-6.2-21.6-10.1%201.8-1.3%203.6-2.7%205.2-4.1%2041.7%2019.6%2086.9%2019.6%20128.1%200%201.7%201.4%203.4%202.8%205.2%204.1-6.9%204-14.1%207.3-21.6%2010.1%203.9%208.1%208.5%2015.7%2013.6%2022.8%2023.7-6.8%2045.8-17.6%2064.7-31.6%204.5-54-7.7-100.3-37.4-141.1ZM85.8%20135.3c-12.5%200-22.8-11.5-22.8-25.6%200-14%2010.1-25.6%2022.8-25.6%2012.7%200%2023%2011.5%2022.8%2025.6%200%2014.1-10.1%2025.6-22.8%2025.6Zm84.4%200c-12.5%200-22.8-11.5-22.8-25.6%200-14%2010.1-25.6%2022.8-25.6%2012.7%200%2023%2011.5%2022.8%2025.6%200%2014.1-10.1%2025.6-22.8%2025.6Z%27/%3E%3C/g%3E%3C/svg%3E"
/>
</a>
<a href="https://greasyfork.org/en/scripts/551444" target="_blank" title="Greasy Fork">
<img
alt="Greasy Fork"
src="data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20width%3D%2724%27%20height%3D%2724%27%20viewBox%3D%270%200%2024%2024%27%3E%3Crect%20x%3D%273%27%20y%3D%274%27%20width%3D%2718%27%20height%3D%2716%27%20rx%3D%272%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27/%3E%3Cpath%20d%3D%27M7%208h10M7%2012h10M7%2016h7%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27/%3E%3C/svg%3E"
>
</a>
<a href="https://twisk.fun" target="_blank" title="Website">
<img
alt="Website"
src="data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20width%3D%2724%27%20height%3D%2724%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23FFF%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Ccircle%20cx%3D%2712%27%20cy%3D%2712%27%20r%3D%2710%27/%3E%3Cline%20x1%3D%272%27%20y1%3D%2712%27%20x2%3D%2722%27%20y2%3D%2712%27/%3E%3Cpath%20d%3D%27M12%202a15.3%2015.3%200%200%201%204%2010%2015.3%2015.3%200%200%201-4%2010%2015.3%2015.3%200%200%201-4-10%2015.3%2015.3%200%200%201%204-10z%27/%3E%3C/svg%3E"
>
</a>
</div>
</div>
</div>
<div id="_accounts_modal" class="_modal" style="display:none">
<div class="_modal_overlay"></div>
<div class="_modal_container _wide">
<div class="_modal_header">
<h2>
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/48b8884ac9d7513e65f3a2b54984c5c4.svg"
style="width: 32px; height: 32px; display:inline-block; vertical-align:middle; margin-right:8px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));">
Account Manager
</h2>
<button id="_close_accounts" class="_close_modal_btn">
<span style="font-size: 18px;">❌</span>
</button>
</div>
<div class="_modal_content">
<div class="_accounts_grid" id="_accounts_list">
${savedAccounts.length === 0 ? '<div class="_empty_state"><p>No saved accounts yet. Save your current account to get started!</p></div>' : ''}
</div>
</div>
</div>
</div>
<div id="_save_account_modal" class="_modal" style="display:none">
<div class="_modal_overlay"></div>
<div class="_modal_container">
<div class="_modal_header">
<h2>Save Account</h2>
<button id="_close_save_account" class="_close_modal_btn">
<span style="font-size: 18px;">❌</span>
</button>
</div>
<div class="_modal_content">
<div class="_settings_section">
<div class="_setting_item">
<label class="_input_label">Account Nickname</label>
<input type="text" id="_account_nickname" class="_text_input" placeholder="e.g., Main Account, Alt #1, Work Account">
</div>
<div class="_setting_item">
<div class="_account_preview">
<div class="_preview_avatar" id="_preview_avatar">
<span style="font-size: 20px;">👤</span>
</div>
<div class="_preview_info">
<strong id="_preview_username">Loading...</strong>
<span id="_preview_details">...</span>
</div>
</div>
</div>
<div class="_setting_item">
<button id="_confirm_save_account" class="_setting_btn _success">
<span style="font-size: 18px;">✅</span>
Save Account
</button>
</div>
</div>
</div>
</div>
</div>
<div id="_settings_modal" class="_modal" style="display:none">
<div class="_modal_overlay"></div>
<div class="_modal_container">
<div class="_modal_header">
<h2>Settings</h2>
<button id="_close_settings" class="_close_modal_btn">
<span style="font-size: 18px;">❌</span>
</button>
</div>
<div class="_modal_content">
<!-- PERFORMANCE SECTION -->
<div class="_settings_section">
<h3>Performance</h3>
<div class="_setting_item">
<div class="_toggle_container">
<label class="_toggle_label">Lite Mode (Reduce Animations)</label>
<div class="_toggle_switch ${liteMode ? '_active' : ''}" id="_lite_mode_toggle">
<div class="_toggle_slider"></div>
</div>
</div>
<p class="_setting_description">Disable animations and visual effects for smoother performance</p>
</div>
</div>
<div class="_setting_item">
<div class="_toggle_container">
<label class="_toggle_label">Hide Animation (Images)</label>
<div class="_toggle_switch ${hideAnimationEnabled ? '_active' : ''}" id="_hide_animation_toggle">
<div class="_toggle_slider"></div>
</div>
</div>
<p class="_setting_description">Hide images to reduce RAM usage</p>
</div>
${ /* PREMIUM FEATURES SECTION ( will fix soon)
<div class="_settings_section">
<h3>Premium Features</h3>
<div class="_setting_item" style="border-bottom: 1px solid var(--border-color); padding-bottom: 16px; margin-bottom: 16px;">
<div class="_toggle_container">
<label class="_toggle_label">Enable Duolingo Max ( Beta )</label>
<div class="_toggle_switch">
<div class="_toggle_slider"></div>
</div>
</div>
<p class="_setting_description">[BETA ] Unlock Super features including unlimited hearts, no ads, and AI-powered lessons</p>
</div>
</div>
*/ '' }
<div class="_settings_section">
<h3>Streak Settings</h3>
<div class="_setting_item">
<div class="_toggle_container">
<label class="_toggle_label">Safe Streak Mode</label>
<div class="_toggle_switch" id="_safe_streak_toggle">
<div class="_toggle_slider"></div>
</div>
</div>
<p class="_setting_description">Only farm streaks within your account age. Safer but limited.</p>
</div>
</div>
<!-- PRIVACY SETTINGS SECTION -->
<div class="_settings_section">
<h3>Privacy Settings</h3>
<div class="_setting_item">
<button id="_privacy_toggle_btn" class="_setting_btn _primary">
<span style="font-size: 18px;">🔒</span>
Set Private
</button>
<p class="_setting_description">Toggle your profile visibility between public and private</p>
</div>
</div>
<!-- QUICK ACTIONS SECTION -->
<div class="_settings_section">
<h3>Quick Actions</h3>
<div class="_setting_item">
<button id="_get_jwt_btn" class="_setting_btn _primary">
<span style="font-size: 18px;">📋</span>
Copy JWT Token
</button>
</div>
<div class="_setting_item">
<button id="_logout_btn" class="_setting_btn _danger">
<span style="font-size: 18px;">🚪</span>
Log Out
</button>
</div>
</div>
<!-- MANUAL LOGIN SECTION -->
<div class="_settings_section">
<h3>Manual Login</h3>
<div class="_setting_item">
<div class="_jwt_input_group">
<input type="text" id="_jwt_input" placeholder="Paste JWT Token here">
<button id="_login_jwt_btn" class="_setting_btn _success">
<span style="font-size: 18px;">➡️</span>
Login
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="_leaderboard_modal" class="_modal" style="display:none">
<div class="_modal_overlay"></div>
<div class="_modal_container _wide">
<div class="_modal_header">
<h2>
<span style="font-size: 24px; display:inline-block;vertical-align:middle;margin-right:8px">🏆</span>
Leaderboard
</h2>
<button id="_close_leaderboard" class="_close_modal_btn">
<span style="font-size: 18px;">❌</span>
</button>
</div>
<div class="_modal_content" id="_leaderboard_content">
<!-- Leaderboard content will be injected here -->
</div>
</div>
</div>
<div id="_notification_modal" class="_modal" style="display:none">
<div class="_modal_overlay"></div>
<div class="_modal_container">
<div class="_modal_header">
<h2>
<span style="font-size: 24px; display:inline-block; vertical-align:middle; margin-right:10px; color: #ff6b9d;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
</span>
Notification
</h2>
<button id="_close_notification" class="_close_modal_btn">
<span style="font-size: 18px;">❌</span>
</button>
</div>
<div class="_modal_content" id="_notification_content">
{/* Nội dung thông báo sẽ được chèn vào đây */}
</div>
</div>
</div>
<div id="_fab_container">
<div id="_fab">
<img src=" https://github.com/FutureCLI/DuoHacker/blob/main/images/Logo_TypePNG_DuoHacker.png?raw=true" alt="Toggle Menu">
</div>
</div>
`;
const style = document.createElement("style");
style.innerHTML = `
._leaderboard_loading {
text-align: center;
padding: 50px;
color: var(--text-secondary);
font-size: 16px;
}
._leaderboard_table {
width: 100%;
border-collapse: collapse;
}
._leaderboard_row {
border-bottom: 1px solid var(--border-color);
}
._leaderboard_row:last-child {
border-bottom: none;
}
._leaderboard_row.is_self {
background: linear-gradient(90deg, rgba(93, 187, 255, 0.25) 0%, rgba(93, 187, 255, 0.15) 100%);
border-left: 4px solid var(--primary-color);
box-shadow: 0 0 0 1px var(--primary-color) inset;
font-weight: 600;
}
._leaderboard_user img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
transition: transform 0.2s;
}
._leaderboard_row.is_self ._leaderboard_user img {
border: 3px solid var(--primary-color);
box-shadow: 0 0 8px var(--primary-glow);
}
._leaderboard_cell {
padding: 12px 10px;
text-align: left;
vertical-align: middle;
}
._leaderboard_rank {
font-weight: 700;
font-size: 1.1em;
text-align: center;
width: 50px;
}
._leaderboard_rank.gold { color: #FFD700; }
._leaderboard_rank.silver { color: #C0C0C0; }
._leaderboard_rank.bronze { color: #CD7F32; }
._leaderboard_user {
display: flex;
align-items: center;
gap: 12px;
}
._leaderboard_user img {
width: 40px;
height: 40px;
border-radius: 50%;
}
._leaderboard_name {
font-weight: 600;
color: var(--text-primary);
}
._leaderboard_score {
font-weight: 700;
color: var(--primary-color);
font-size: 1.1em;
text-align: right;
}
._shop_grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
._shop_item_card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
transition: var(--transition);
cursor: pointer;
}
._shop_item_card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
border-color: var(--primary-color);
}
._shop_item_icon {
font-size: 32px;
line-height: 1;
}
._shop_item_name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
text-align: center;
line-height: 1.3;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
._shop_buy_btn {
width: 100%;
padding: 8px 12px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
._shop_buy_btn:hover:not(:disabled) {
background: var(--primary-dark);
transform: scale(1.02);
}
._shop_buy_btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
._shop_stats {
text-align: center;
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-glow);
}
@media (max-width: 768px) {
._shop_grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
}
:root {
/* Màu primary – xanh trời */
--primary-color: #5DBBFF; /* màu chính */
--primary-dark: #1B6FB8; /* màu đậm cho hover / active */
--primary-light: #A8DEFF; /* màu nhạt */
--primary-glow: rgba(93, 187, 255, 0.4); /* glow xanh */
/* State colors */
--success-color: #43A047;
--success-glow: rgba(67, 160, 71, 0.3);
--error-color: #E53935;
--error-glow: rgba(229, 57, 53, 0.3);
--warning-color: #FB8C00;
--warning-glow: rgba(251, 140, 0, 0.3);
/* Transition & shadow */
--transition: all 0.22s cubic-bezier(0.2, 0.9, 0.2, 1);
--transition-fast: all 0.08s cubic-bezier(0.4, 0, 0.2, 1);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 12px 48px rgba(0, 0, 0, 0.25);
/* Glass effect */
--glass-blur: 20px;
--glass-opacity: 0.1;
--glass-brightness: 1.1;
--glass-saturation: 1.2;
}
/* =========================
THEME DARK – tone xanh trời
========================= */
.theme-dark {
/* Nền tổng thể */
--bg-primary: linear-gradient(135deg, #050816 0%, #0b1024 40%, #12335a 100%);
--bg-secondary: rgba(15, 25, 45, 0.9);
/* Nền card / container (cái #_container đang xài var(--bg-card)) */
--bg-card: rgba(36, 52, 94, 0.95); /* xanh navy có chút sky */
/* Modal / layer đậm hơn chút */
--bg-modal: rgba(15, 22, 40, 0.98);
/* Glass background */
--bg-glass: rgba(93, 187, 255, 0.08);
/* Text */
--text-primary: #FFFFFF;
--text-secondary: #B0BEC5;
--text-muted: #78909C;
/* Border & hover */
--border-color: rgba(135, 206, 250, 0.35); /* sky border */
--border-glow: rgba(93, 187, 255, 0.4);
--hover-bg: rgba(93, 187, 255, 0.16);
/* Glass viền */
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(135, 206, 250, 0.35);
}
/* =========================
THEME LIGHT – tone xanh trời
========================= */
.theme-light {
/* Nền tổng thể */
--bg-primary: linear-gradient(135deg, #f8fbff 0%, #e9f3ff 100%);
--bg-secondary: rgba(255, 255, 255, 0.85);
/* Card / Container */
--bg-card: rgba(255, 255, 255, 0.95);
--bg-modal: rgba(255, 255, 255, 0.98);
/* Glass subtle */
--bg-glass: rgba(120, 180, 255, 0.06);
/* Text */
--text-primary: #0f1a41; /* đổi từ #1a237e → đậm nhưng không tím */
--text-secondary: #4a6572; /* cân bằng contrast */
--text-muted: #94a7b3;
/* Border & Hover */
--border-color: rgba(120, 180, 255, 0.28); /* mềm hơn */
--border-glow: rgba(120, 180, 255, 0.22);
--hover-bg: rgba(120, 180, 255, 0.10);
/* Glass border */
--glass-bg: rgba(255, 255, 255, 0.4);
--glass-border: rgba(120, 180, 255, 0.22);
/* Shadow để UI có chiều sâu */
--shadow-soft: 0 8px 25px rgba(15, 23, 42, 0.06);
--shadow-card: 0 10px 35px rgba(15, 23, 42, 0.08);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#_container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
transform-origin: center;
width: min(90vw, 920px);
max-height: 90vh;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(25px) saturate(180%);
-webkit-backdrop-filter: blur(25px) saturate(180%);
border: 1.5px solid rgba(0, 140, 255, 0.65);
box-shadow: 0 0 20px rgba(0, 140, 255, 0.25);
border-radius: 20px;
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
z-index: 9999;
display: flex;
flex-direction: column;
}
@keyframes containerAppear {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.9) translateY(20px);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1) translateY(0);
}
}
._toggle_icon_wrapper {
display: inline-block; /* Giúp icon hiển thị đúng */
width: 18px;
height: 18px;
vertical-align: middle; /* Căn giữa icon với dòng chữ */
margin-right: -2px; /* Tinh chỉnh khoảng cách một chút nếu cần */
}
._toggle_icon_wrapper img {
width: 100%;
height: 100%;
}
#_backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 9999;
animation: fadeIn 0.1s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
#_header {
background: var(--bg-secondary);
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
._header_top {
display: flex;
justify-content: space-between;
align-items: center;
}
._brand {
display: flex;
align-items: center;
gap: 12px;
}
body[data-lite-mode="true"] {
/* Tắt global transition/animation */
animation: none !important;
transition: none !important;
}
body[data-lite-mode="true"] *,
body[data-lite-mode="true"] *::before,
body[data-lite-mode="true"] *::after {
/* Tắt mọi animation & transition */
animation: none !important;
transition: none !important;
/* Giữ nguyên transform/opacity/layout */
}
/* Tắt hiệu ứng phụ không ảnh hưởng layout */
body[data-lite-mode="true"] ._fab_ring,
body[data-lite-mode="true"] ._announce_bar,
body[data-lite-mode="true"] .pulseGlow {
animation: none !important;
box-shadow: none !important;
}
/* Giữ nguyên transform cho các thành phần căn giữa */
body[data-lite-mode="true"] #_container,
body[data-lite-mode="true"] ._modal_container,
body[data-lite-mode="true"] #_fab {
/* KHÔNG GHI ĐÈ transform, opacity, position */
/* Chỉ tắt animation/transition */
animation: none !important;
transition: none !important;
}
/* Optional: tắt backdrop-filter để tăng FPS */
body[data-lite-mode="true"] #_container,
body[data-lite-mode="true"] ._modal_container,
body[data-lite-mode="true"] #_backdrop {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
._logo_container {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
._logo {
width: 100%;
height: 100%;
}
._brand_text {
display: flex;
align-items: center;
gap: 8px;
line-height: 1;
}
._brand_text h1 {
font-size: 20px;
font-weight: 700;
color: var(--primary-color);
margin: 0; /* ✅ FIX: Xóa margin mặc định */
line-height: 1.2; /* ✅ FIX: Giảm line-height */
}
._version_badge {
background: var(--primary-color);
color: white;
padding: 4px 10px; /* ✅ FIX: Tăng padding dọc */
border-radius: 10px;
font-size: 11px;
font-weight: 600;
line-height: 1; /* ✅ FIX: Loại bỏ khoảng trống dư */
display: flex; /* ✅ FIX: Căn giữa text bên trong */
align-items: center;
}
._header_controls {
display: flex;
gap: 6px;
}
._control_btn {
/* Kích thước chuẩn, đủ lớn để dễ bấm */
width: 38px;
position: relative;
height: 38px;
/* Hình dáng: Vuông bo góc (Square Rounded) */
border-radius: 10px;
/* Màu sắc: Theo Theme chính */
background: var(--primary-color);
color: #ffffff; /* Icon màu trắng */
/* Căn chỉnh Icon SVG vào giữa */
display: flex;
align-items: center;
justify-content: center;
/* Loại bỏ border thừa của mặc định */
border: none;
/* Hiệu ứng trỏ chuột */
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, filter 0.2s;
}
/* Hiệu ứng khi di chuột vào (Hover) */
._control_btn:hover {
transform: translateY(-2px); /* Nổi lên nhẹ */
box-shadow: 0 4px 12px var(--primary-glow); /* Đổ bóng màu theme */
filter: brightness(1.1); /* Sáng hơn một chút */
}
/* Đảm bảo icon SVG bên trong có kích thước hợp lý */
._control_btn svg {
width: 20px;
height: 20px;
stroke-width: 2.5; /* Làm nét đậm hơn (Bold) như bạn yêu cầu */
}
._shop_grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
._shop_item_card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
transition: var(--transition);
cursor: pointer;
}
._shop_item_card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
border-color: var(--primary-color);
}
._shop_item_icon {
font-size: 32px;
line-height: 1;
}
._shop_item_name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
text-align: center;
line-height: 1.3;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
._shop_buy_btn {
width: 100%;
padding: 8px 12px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
._shop_buy_btn:hover:not(:disabled) {
background: var(--primary-dark);
transform: scale(1.02);
}
._shop_buy_btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
._shop_stats {
text-align: center;
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-glow);
}
@media (max-width: 768px) {
._shop_grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
}
._control_btn:hover {
background: var(--hover-bg);
color: var(--primary-color);
border-color: var(--border-glow);
}
._control_btn._close:hover {
background: rgba(229, 57, 53, 0.1);
color: var(--error-color);
border-color: rgba(229, 57, 53, 0.2);
}
._control_btn._accounts,
._control_btn._settings {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: 0 2px 8px var(--primary-glow);
}
._control_btn._accounts:hover,
._control_btn._settings:hover {
background: var(--primary-dark);
box-shadow: 0 4px 12px var(--primary-glow);
}
._badge {
position: absolute;
top: -4px;
right: -4px;
background: var(--error-color);
color: white;
font-size: 10px;
font-weight: 700;
padding: 2px 5px;
border-radius: 8px;
min-width: 16px;
text-align: center;
}
#_main_content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
._profile_card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 20px;
transition: var(--transition);
}
._profile_card:hover {
box-shadow: var(--shadow-md);
border-color: var(--border-glow);
}
._profile_header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
._avatar {
width: 56px;
height: 56px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28px;
box-shadow: 0 4px 12px var(--primary-glow);
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
}
._profile_info {
flex: 1;
}
._profile_info h2 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
._profile_info p {
color: var(--text-secondary);
font-size: 13px;
}
._icon_btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 10px;
transition: var(--transition-fast);
border: 1px solid var(--border-color);
background: var(--bg-card);
color: var(--text-secondary);
}
/* Thêm hover effect nếu chưa có */
._icon_btn:hover {
background: #4A5568;
color: #E2E8F0;
}
._icon_btn._success {
background: var(--success-color);
color: white;
border-color: var(--success-color);
box-shadow: 0 2px 8px var(--success-glow);
}
._icon_btn._success:hover {
background: #2E7D32;
}
._icon_btn._primary {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: 0 2px 8px var(--primary-glow);
}
._icon_btn._primary:hover {
background: var(--primary-dark);
box-shadow: 0 4px 12px var(--primary-glow);
}
._stats_row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
._stat_item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 10px;
border: 1px solid rgba(var(--text-primary), 0.05);
transition: var(--transition);
}
._stat_item:hover {
background: var(--hover-bg);
}
._stat_icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px; /* Vẫn giữ cho các icon emoji khác nếu có */
line-height: 1;
}
._stat_icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
._stat_info {
display: flex;
flex-direction: column;
}
._stat_value {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
}
._stat_label {
font-size: 11px;
color: var(--text-secondary);
}
._mode_section h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
._mode_cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
._mode_card {
background: var(--bg-card);
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: var(--transition);
text-align: center;
}
._mode_card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
._mode_card._active {
border-color: var(--primary-color);
background: var(--hover-bg);
}
._control_btn._gift {
position: relative;
overflow: visible;
}
/* Pulse glow animation - giống các button khác */
._control_btn._gift::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(135deg, #ff6b9d 0%, #c44569 100%);
border-radius: 10px;
opacity: 0;
animation: giftGlowPulse 2s ease-in-out infinite;
z-index: -1;
}
@keyframes giftGlowPulse {
0%, 100% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.15);
}
}
/* Hover effect - giống button khác */
._control_btn._gift:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 107, 157, 0.6);
filter: brightness(1.1);
}
/* Icon rotation animation - "NEW" indicator style */
._control_btn._gift img {
animation: giftIconSpin 3s ease-in-out infinite;
}
@keyframes giftIconSpin {
0%, 90%, 100% {
transform: rotate(0deg) scale(1);
}
5% {
transform: rotate(-15deg) scale(1.1);
}
10% {
transform: rotate(15deg) scale(1.1);
}
15% {
transform: rotate(-10deg) scale(1.05);
}
20% {
transform: rotate(10deg) scale(1.05);
}
25% {
transform: rotate(0deg) scale(1);
}
}
#_notification_content {
line-height: 1.6;
color: var(--text-secondary);
}
#_notification_content ._loading_spinner {
text-align: center;
padding: 40px 0;
font-size: 16px;
color: var(--text-primary);
}
#_notification_content ._error_message {
text-align: center;
padding: 30px 15px;
background: rgba(229, 57, 53, 0.1);
border: 1px solid var(--error-color);
border-radius: 8px;
color: var(--error-color);
}
#_notification_content h1, #_notification_content h2 {
font-weight: 700;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
margin-top: 24px;
margin-bottom: 16px;
}
#_notification_content h1 { font-size: 1.5em; }
#_notification_content h2 { font-size: 1.3em; }
#_notification_content p {
margin-bottom: 16px;
}
#_notification_content a {
color: var(--primary-color);
font-weight: 600;
text-decoration: none;
}
#_notification_content a:hover {
text-decoration: underline;
}
#_notification_content ul, #_notification_content ol {
padding-left: 20px;
margin-bottom: 16px;
}
#_notification_content code {
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.9em;
border: 1px solid var(--border-color);
}
#_notification_content pre {
background: var(--bg-secondary);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
border: 1px solid var(--border-color);
}
#_notification_content pre code {
border: none;
padding: 0;
}
/* Badge "NEW" indicator (optional) */
._control_btn._gift::after {
content: 'NEW';
position: absolute;
top: -6px;
right: -6px;
background: #ff4757;
color: white;
font-size: 8px;
font-weight: 800;
padding: 2px 4px;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(255, 71, 87, 0.5);
animation: newBadgeBounce 1.5s ease-in-out infinite;
letter-spacing: 0.5px;
}
@keyframes newBadgeBounce {
0%, 100% {
transform: scale(1) translateY(0);
}
50% {
transform: scale(1.1) translateY(-2px);
}
}
._mode_icon {
width: 48px;
height: 48px;
margin: 0 auto 12px auto; /* Căn giữa chính khối icon và thêm khoảng cách dưới */
text-align: center; /* Đảm bảo nội dung bên trong được căn giữa */
}
._mode_icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
._mode_card h4 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6px;
}
._mode_card p {
color: var(--text-secondary);
font-size: 13px;
margin-bottom: 10px;
}
._mode_specs {
display: flex;
justify-content: center;
gap: 6px;
}
._spec {
background: var(--bg-secondary);
padding: 3px 6px;
border-radius: 4px;
font-size: 11px;
color: var(--text-muted);
}
._options_section h3 {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
._option_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
}
._option_btn {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px;
cursor: pointer;
transition: var(--transition);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-weight: 500;
color: var(--text-primary);
}
._option_btn:hover {
background: var(--hover-bg);
border-color: var(--primary-color);
transform: translateY(-2px);
}
._option_btn._selected {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: 0 4px 12px var(--primary-glow);
}
._option_icon {
width: 28px; /* Đặt kích thước cho icon */
height: 28px;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-fast);
}
._option_icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
._option_btn span {
font-weight: 500;
color: var(--text-primary);
}
._option_btn._selected span {
color: white;
}
._auto_solve_section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
}
._auto_solve_section h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
._control_panel {
display: flex;
justify-content: center;
gap: 12px;
}
._start_btn, ._stop_btn {
padding: 12px 32px;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 8px;
}
._start_btn {
background: linear-gradient(135deg, var(--success-color) 0%, #2E7D32 100%);
color: white;
}
._start_btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px var(--success-glow);
}
._stop_btn {
background: linear-gradient(135deg, var(--error-color) 0%, #C62828 100%);
color: white;
}
._stop_btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px var(--error-glow);
}
._live_stats h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
._stats_grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
._live_stat {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
display: flex;
align-items: center;
gap: 10px;
}
._live_icon {
width: 32px; /* Đặt kích thước cho icon */
height: 32px;
margin-right: 12px; /* Giữ khoảng cách với phần số liệu */
display: flex;
align-items: center;
justify-content: center;
font-size: 24px; /* Vẫn giữ cho các icon emoji khác nếu có */
}
._live_icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
._live_data {
display: flex;
flex-direction: column;
}
._live_data span {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
._live_data small {
font-size: 11px;
color: var(--text-secondary);
}
._console_section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
}
._console_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
._console_header h3 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
._clear_btn {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 8px;
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
transition: var(--transition);
}
._clear_btn:hover {
background: rgba(229, 57, 53, 0.1);
color: var(--error-color);
}
._console {
height: 120px;
overflow-y: auto;
padding: 12px 16px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 12px;
}
._log_entry {
display: flex;
gap: 8px;
margin-bottom: 6px;
}
._log_time {
color: var(--text-muted);
flex-shrink: 0;
}
._log_msg {
color: var(--text-secondary);
}
._log_entry._success ._log_msg {
color: var(--success-color);
}
._log_entry._error ._log_msg {
color: var(--error-color);
}
._log_entry._info ._log_msg {
color: var(--primary-color);
}
._join_section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 30px;
}
._join_content {
text-align: center;
max-width: 350px;
}
._join_icon {
width: 60px;
height: 60px;
background: var(--primary-color);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
}
._join_content h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 10px;
}
._join_content p {
color: var(--text-secondary);
margin-bottom: 20px;
}
._join_btn {
background: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: var(--transition);
}
._join_btn:hover {
background: var(--primary-dark);
}
._footer {
/* --- Bố cục Flexbox --- */
display: flex;
justify-content: space-between; /* Đẩy nội dung ra hai bên */
align-items: center; /* Căn giữa theo chiều dọc */
/* --- Kích thước & Vị trí --- */
width: 100%;
box-sizing: border-box; /* Đảm bảo padding không làm tăng kích thước */
margin-top: auto; /* <-- DÒNG QUAN TRỌNG: Đẩy footer xuống cuối */
padding: 12px 20px;
/* --- Kiểu dáng & Chữ --- */
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
font-size: 11px;
color: var(--text-muted);
}
._footer_links {
display: flex;
gap: 10px;
}
._footer_link {
display: flex;
align-items: center;
gap: 4px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 8px;
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
transition: var(--transition);
}
._footer_link:hover {
background: var(--hover-bg);
color: var(--primary-color);
}
._footer_version {
background: var(--bg-card);
padding: 2px 6px;
border-radius: 4px;
}
#_fab_container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
cursor: pointer;
}
/* 2. Style cho chính nút FAB */
#_fab {
position: relative;
z-index: 1;
width: 60px;
height: 60px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 20px var(--primary-glow);
transition: transform 0.2s ease-out;
}
/* Hiệu ứng tương tác khi hover */
#_fab:hover {
transform: scale(1.1);
}
/* 3. Style cho hình ảnh logo */
#_fab img {
width: 60px; /* Tăng nhẹ kích thước logo cho nổi bật hơn */
height: 60px;
border-radius: 50px;
position: relative;
z-index: 2; /* Đảm bảo logo luôn nằm trên */
}
/* 4. Hiệu ứng "Digital Pulse" */
#_fab::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
/* Tạo vòng viền mỏng, sắc nét */
border: 2px solid var(--primary-color);
/* Đặt z-index thấp hơn nút chính */
z-index: 0;
/* Chạy animation */
animation: digital-pulse 2.5s infinite;
/* Mặc định ẩn đi */
opacity: 0;
}
/* Dừng animation khi người dùng tương tác */
#_fab:hover::before {
animation: none;
opacity: 0;
}
/* Keyframes định nghĩa chuyển động của "Digital Pulse" */
@keyframes digital-pulse {
0% {
transform: scale(0.8);
opacity: 0;
}
40% {
opacity: 0.8; /* Hiển thị rõ vòng sáng */
}
100% {
transform: scale(1.6); /* Lan tỏa ra bên ngoài */
opacity: 0; /* Mờ dần và biến mất */
}
}
._modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
}
._modal_overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
}
._modal_container {
position: relative;
width: 90%;
max-width: 500px;
max-height: 85vh;
background: var(--bg-modal);
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
animation: modalSlideIn 0.22s cubic-bezier(0.2, 0.9, 0.2, 1);
display: flex;
flex-direction: column;
}
._modal_container._wide {
max-width: 800px;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
._modal_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
._modal_header h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
._close_modal_btn {
width: 32px;
height: 32px;
border: none;
background: var(--bg-card);
color: var(--text-secondary);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
._close_modal_btn:hover {
background: rgba(229, 57, 53, 0.1);
color: var(--error-color);
}
._modal_content {
padding: 20px;
overflow-y: auto;
flex: 1;
}
._settings_section {
margin-bottom: 20px;
}
._settings_section:last-child {
margin-bottom: 0;
}
._settings_section h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
}
._setting_item {
margin-bottom: 12px;
}
._setting_item:last-child {
margin-bottom: 0;
}
._setting_btn {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
._setting_btn:hover {
background: var(--hover-bg);
}
._setting_btn._primary {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
._setting_btn._primary:hover {
background: var(--primary-dark);
}
._setting_btn._success {
background: var(--success-color);
color: white;
border-color: var(--success-color);
}
._setting_btn._success:hover {
background: #2E7D32;
}
._setting_btn._danger {
background: var(--error-color);
color: white;
border-color: var(--error-color);
}
._setting_btn._danger:hover {
background: #C62828;
}
._jwt_input_group {
display: flex;
gap: 10px;
}
#_jwt_input, #_lesson_count_input {
flex: 1;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
transition: var(--transition);
}
#_jwt_input:focus, #_lesson_count_input:focus {
outline: none;
border-color: var(--primary-color);
}
._input_label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 6px;
}
._text_input {
width: 100%;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
transition: var(--transition);
}
._text_input:focus {
outline: none;
border-color: var(--primary-color);
}
._text_input::placeholder {
color: var(--text-muted);
}
._account_preview {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
}
._preview_avatar {
width: 40px;
height: 40px;
background: var(--primary-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
._preview_info {
display: flex;
flex-direction: column;
gap: 2px;
}
._preview_info strong {
font-size: 14px;
color: var(--text-primary);
}
._preview_info span {
font-size: 12px;
color: var(--text-secondary);
}
._accounts_grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
._empty_state {
grid-column: 1 / -1;
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
}
._empty_state p {
font-size: 14px;
}
._account_card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
transition: var(--transition);
position: relative;
cursor: pointer;
}
._account_card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--primary-color);
}
._account_card._active {
border-color: var(--success-color);
background: var(--hover-bg);
}
._account_header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
._account_avatar {
width: 40px;
height: 40px;
background: var(--primary-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
._account_info {
flex: 1;
min-width: 0;
}
._account_nickname {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
._account_username {
font-size: 12px;
color: var(--text-secondary);
overflow: hidden;
contain: layout paint;
backface-visibility: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
._account_stats {
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
}
._account_stat {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-secondary);
}
._account_actions {
display: flex;
gap: 6px;
}
._account_action_btn {
flex: 1;
padding: 8px;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
._account_action_btn._login {
background: var(--success-color);
color: white;
}
._account_action_btn._login:hover {
background: #2E7D32;
}
._account_action_btn._delete {
background: var(--error-color);
color: white;
}
._account_action_btn._delete:hover {
background: #C62828;
}
._active_badge {
position: absolute;
top: 8px;
right: 8px;
background: var(--success-color);
color: white;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
}
._superlinks_section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
._superlinks_section h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
._superlinks_input_group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
._superlinks_input {
flex: 1;
padding: 10px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-family: 'Monaco', monospace;
}
._superlinks_input:focus {
outline: none;
border-color: var(--primary-color);
}
._superlinks_check_btn {
padding: 10px 16px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-size: 13px;
}
._superlinks_check_btn:hover {
background: var(--primary-dark);
}
._superlinks_check_btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
._superlinks_result {
padding: 12px;
border-radius: 6px;
margin-top: 12px;
font-size: 14px;
font-weight: 600;
text-align: center;
display: none;
}
._superlinks_result._working {
background: rgba(67, 160, 71, 0.2);
color: #43A047;
border: 1px solid #43A047;
}
._superlinks_result._unavailable {
background: rgba(229, 57, 53, 0.2);
color: #E53935;
border: 1px solid #E53935;
}
._superlinks_result._loading {
background: rgba(30, 136, 229, 0.2);
color: #1E88E5;
border: 1px solid #1E88E5;
}
._toggle_container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
._toggle_label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
._toggle_switch {
position: relative;
width: 50px;
height: 26px;
background-color: var(--border-color);
border-radius: 13px;
cursor: pointer;
transition: var(--transition);
}
._toggle_switch._active {
background-color: var(--primary-color);
}
._toggle_slider {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
transition: var(--transition);
}
._toggle_switch._active ._toggle_slider {
transform: translateX(24px);
}
._setting_description {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
@media (max-width: 768px) {
#_container {
width: 95vw;
max-height: 95vh;
}
._stats_row, ._mode_cards, ._option_grid, ._stats_grid {
grid-template-columns: 1fr;
}
._control_panel {
flex-direction: column;
}
._start_btn, ._stop_btn {
width: 100%;
}
._footer {
flex-direction: column;
gap: 8px;
}
._footer_links {
width: 100%;
justify-content: center;
}
._footer_socials a{
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
}
._footer_socials img{
width: 24px;
height: 24px;
display: block;
object-fit: contain;
}
._jwt_input_group {
flex-direction: column;
}
._accounts_grid {
grid-template-columns: 1fr;
}
._modal_container._wide {
max-width: 95%;
}
}
`;
document.head.appendChild(style);
style.innerHTML += `
/* Reduce dark overlay opacity */
._modal_overlay {
background: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(3px) !important;
}
/* Make modal box less transparent & text brighter */
._modal_container {
background: rgba(30, 30, 30, 0.98) !important;
color: #fff !important;
}
/* Improve input visibility */
._text_input, #_jwt_input, #_lesson_count_input {
background: #2c2c2c !important;
color: #fff !important;
border: 1px solid #444 !important;
}
/* Buttons inside settings/login modals */
._setting_btn {
background: #1e88e5 !important;
color: #fff !important;
border-color: #1565c0 !important;
}
._setting_btn:hover {
background: #1565c0 !important;
}
/* Make account card text readable */
._account_card {
background: rgba(40, 40, 40, 0.95) !important;
color: #fff !important;
}
._announce_bar {
background: linear-gradient(90deg, #1E88E5 0%, #64B5F6 100%);
padding: 12px 16px;
margin-bottom: 20px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
color: white;
font-weight: 700;
font-size: 14px;
box-shadow: 0 0 18px rgba(30, 136, 229, 0.45);
animation: pulseGlowSoft 3.5s ease-in-out infinite; /* chậm hơn, nhẹ hơn */
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
._announce_btn {
background: white;
color: #1565c0;
border: none;
padding: 6px 16px;
border-radius: 20px;
font-weight: 800;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
white-space: nowrap;
margin-left: 10px;
text-decoration: none;
display: inline-block;
}
._announce_btn:hover {
box-shadow:
0 0 16px rgba(0, 170, 255, 0.9),
0 0 30px rgba(0, 170, 255, 0.5);
transform: translateY(-1px);
}
@keyframes pulseGlow {
0% { box-shadow: 0 0 12px rgba(30,136,229,0.40); }
50% { box-shadow: 0 0 22px rgba(30,136,229,0.60); }
100% { box-shadow: 0 0 12px rgba(30,136,229,0.40); }
}
`;
const container = document.createElement("div");
container.innerHTML = containerHTML;
document.body.appendChild(container);
if (liteMode) {
document.body.setAttribute('data-lite-mode', 'true');
} else {
document.body.removeAttribute('data-lite-mode');
}
};
const logToConsole = (message, type = 'info') => {
const console = document.getElementById('_console_output');
if (!console) return;
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = `_log_entry _${type}`;
entry.innerHTML = `
<span class="_log_time">${timestamp}</span>
<span class="_log_msg">${message}</span>
`;
console.appendChild(entry);
console.scrollTop = console.scrollHeight;
while (console.children.length > 50) {
console.removeChild(console.firstChild);
}
};
const LEADERBOARDS_URL = "https://duolingo-leaderboards-prod.duolingo.com/leaderboards/7d9f5dd1-8423-491a-91f2-2532052038ce";
const showLeaderboard = async () => {
const modal = document.getElementById('_leaderboard_modal');
const content = document.getElementById('_leaderboard_content');
if (!modal || !content) return;
modal.style.display = 'flex';
content.innerHTML = '<div class="_leaderboard_loading">⏳ Initializing & Loading Leaderboard...</div>';
if (!sub || !jwt || !defaultHeaders) {
logToConsole('Leaderboard: User data not found, attempting to initialize...', 'info');
const success = await initializeFarming();
if (!success) {
logToConsole('Leaderboard: Initialization failed. User might not be logged in.', 'error');
content.innerHTML = '<div class="_leaderboard_loading">❌ Initialization failed. Please make sure you are logged in to Duolingo and refresh the page.</div>';
return;
}
logToConsole('Leaderboard: Initialization successful.', 'success');
}
try {
content.innerHTML = '<div class="_leaderboard_loading">⏳ Fetching leaderboard data...</div>';
const res = await fetch(`${LEADERBOARDS_URL}/users/${sub}?client_unlocked=true&_=${Date.now()}`, {
headers: defaultHeaders
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`HTTP ${res.status}: ${errorText}`);
}
const data = await res.json();
renderLeaderboard(data);
} catch (error) {
console.error("Failed to fetch leaderboard:", error);
content.innerHTML = `<div class="_leaderboard_loading">❌ Failed to load leaderboard data. Please check the console for details.</div>`;
}
};
const renderLeaderboard = (data) => {
const content = document.getElementById('_leaderboard_content');
const rankings = data?.active?.cohort?.rankings || [];
if (rankings.length === 0) {
content.innerHTML = '<div class="_leaderboard_loading">No leaderboard data found.</div>';
return;
}
const tableRowsHTML = rankings.map((user, index) => {
const rank = index + 1;
const isSelf = user.user_id == sub;
let rankIcon = `<span class="_leaderboard_rank">${rank}</span>`;
if (rank === 1) rankIcon = `<span class="_leaderboard_rank gold">🥇</span>`;
if (rank === 2) rankIcon = `<span class="_leaderboard_rank silver">🥈</span>`;
if (rank === 3) rankIcon = `<span class="_leaderboard_rank bronze">🥉</span>`;
const avatarUrl = isSelf && userInfo?.picture
? userInfo.picture.replace(/\/(medium|large|small)$/, '/xlarge')
: 'https://d35aaqx5ub95lt.cloudfront.net/vendor/0cecd302cf0bcd0f73d51768feff75fe.svg';
const finalAvatarUrl = avatarUrl.includes('duolingo.com/ssr-avatars') && !avatarUrl.endsWith('/xxlarge')
? avatarUrl + '/xlarge'
: avatarUrl;
return `
<tr class="_leaderboard_row ${isSelf ? 'is_self' : ''}">
<td class="_leaderboard_cell">${rankIcon}</td>
<td class="_leaderboard_cell">
<div class="_leaderboard_user">
<img src="${finalAvatarUrl}" alt="${user.display_name}">
<span class="_leaderboard_name">${user.display_name} ${isSelf ? '(You)' : ''}</span>
</div>
</td>
<td class="_leaderboard_cell _leaderboard_score">${user.score.toLocaleString()} XP</td>
</tr>
`;
}).join('');
content.innerHTML = `
<table class="_leaderboard_table">
<tbody>
${tableRowsHTML}
</tbody>
</table>
`;
};
const updateEarnedStats = () => {
const elements = {
xp: document.getElementById('_earned_xp'),
gems: document.getElementById('_earned_gems'),
streak: document.getElementById('_earned_streak'),
lessons: document.getElementById('_earned_lessons')
};
const sessionXP = parseInt(sessionStorage.getItem('dh_session_xp_earned') || '0', 10);
if (elements.xp) elements.xp.textContent = sessionXP.toLocaleString();
if (elements.gems) elements.gems.textContent = totalEarned.gems.toLocaleString();
if (elements.streak) elements.streak.textContent = totalEarned.streak;
if (elements.lessons) elements.lessons.textContent = totalEarned.lessons.toLocaleString();
};
const farmXP110Once = async () => {
try {
if (!skillId) {
skillId = extractSkillId(userInfo?.currentCourse || {});
if (!skillId) {
logToConsole('No skill ID available. Cannot farm XP.', 'error');
return false;
}
}
const sessionRes = await fetch('https://www.duolingo.com/2017-06-30/sessions', {
method: 'POST',
headers: defaultHeaders,
body: JSON.stringify({
challengeTypes: [],
fromLanguage: userInfo.fromLanguage,
learningLanguage: userInfo.learningLanguage,
type: 'UNIT_TEST',
skillIds: [skillId]
})
});
if (!sessionRes.ok) {
logToConsole(`Session failed (${sessionRes.status})`, 'error');
return false;
}
const sessionData = await sessionRes.json();
const startTime = Math.floor(Date.now() / 1000);
const updateRes = await fetch(`https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`, {
method: 'PUT',
headers: defaultHeaders,
body: JSON.stringify({
id: sessionData.id,
metadata: sessionData.metadata,
type: 'UNIT_TEST',
fromLanguage: userInfo.fromLanguage,
learningLanguage: userInfo.learningLanguage,
challenges: [],
adaptiveChallenges: [],
sessionExperimentRecord: [],
experiments_with_treatment_contexts: [],
adaptiveInterleavedChallenges: [],
sessionStartExperiments: [],
trackingProperties: [],
ttsAnnotations: [],
heartsLeft: 0,
startTime: startTime,
enableBonusPoints: true,
endTime: startTime + 60,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true,
hasBoost: true,
happyHourBonusXp: 10,
pathLevelSpecifics: { unitIndex: 0 }
})
});
if (updateRes.ok) {
const data = await updateRes.json();
const earned = data?.awardedXp || data?.xpGain || 110;
totalEarned.xp += earned;
const currentSessionXP = parseInt(sessionStorage.getItem('dh_session_xp_earned') || '0', 10);
sessionStorage.setItem('dh_session_xp_earned', String(currentSessionXP + earned));
updateEarnedStats();
saveSessionData();
logToConsole(`Earned ${earned} XP`, 'success');
return true;
} else {
logToConsole(`Session update failed (${updateRes.status})`, 'error');
return false;
}
} catch (error) {
logToConsole(`XP farm error: ${error.message}`, 'error');
return false;
}
};
const farmXP110 = async (delayMs) => {
if (!skillId) {
skillId = extractSkillId(userInfo?.currentCourse || {});
if (!skillId) {
logToConsole('Skill ID not found. Navigate to a course and click Refresh Profile.', 'error');
stopFarming();
return;
}
}
let consecutiveErrors = 0;
const MAX_CONSECUTIVE_ERRORS = 5;
while (isRunning) {
const success = await farmXP110Once();
if (success) {
consecutiveErrors = 0;
if (totalEarned.xp % 500 < 110 && sendDiscordWebhook) {
sendDiscordWebhook('XP Progress', 'Active', `Total XP: ${totalEarned.xp}`, 16761035);
}
await delay(delayMs);
} else {
consecutiveErrors++;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
logToConsole(`Too many errors. Stopping.`, 'error');
stopFarming();
break;
}
await delay(delayMs * 2);
}
}
};
const updateFarmingTime = () => {
if (!farmingStats.startTime) return;
const elapsed = Date.now() - farmingStats.startTime;
const minutes = Math.floor(elapsed / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
const timeElement = document.getElementById('_farming_time');
if (timeElement) {
timeElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
};
const setInterfaceVisible = (visible) => {
const container = document.getElementById("_container");
const backdrop = document.getElementById("_backdrop");
if (container && backdrop) {
container.style.display = visible ? "flex" : "none";
backdrop.style.display = visible ? "block" : "none";
}
};
const isInterfaceVisible = () => {
const container = document.getElementById("_container");
return container && container.style.display !== "none";
};
const toggleInterface = () => {
setInterfaceVisible(!isInterfaceVisible());
};
const applyTheme = (theme) => {
currentTheme = theme;
localStorage.setItem('duofarmer_theme', theme);
const container = document.getElementById("_container");
if (container) {
container.className = container.className.replace(/theme-\w+/, `theme-${theme}`);
}
const themeToggle = document.getElementById('_theme_toggle');
if (themeToggle) {
themeToggle.innerHTML = `<span style="font-size: 18px;">${theme === 'dark' ? '☀️' : '🌙'}</span>`;
}
};
const saveAccount = (nickname) => {
if (!jwt || !userInfo) {
logToConsole('Cannot save account: not logged in', 'error');
return false;
}
let avatarPicture = normalizeAvatarUrl(userInfo.picture);
const account = {
id: Date.now().toString(),
nickname: nickname || userInfo.username,
username: userInfo.username,
jwt: jwt,
fromLanguage: userInfo.fromLanguage,
learningLanguage: userInfo.learningLanguage,
streak: userInfo.streak,
gems: userInfo.gems,
totalXp: userInfo.totalXp,
picture: avatarPicture,
savedAt: new Date().toISOString()
};
setStoredAvatarUrl(account.username, avatarPicture);
const existingIndex = savedAccounts.findIndex(acc => acc.username === account.username);
if (existingIndex !== -1) {
savedAccounts[existingIndex] = account;
logToConsole(`Updated account: ${nickname}`, 'success');
} else {
savedAccounts.push(account);
logToConsole(`Saved new account: ${nickname}`, 'success');
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedAccounts));
updateAccountsBadge();
return true;
};
const customMarkdownParser = (markdownText) => {
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
let text = markdownText;
text = text.replace(/```([^\n]*)?\n(.*?)```/gs, (match, language, code) => {
const escapedCode = escapeHtml(code);
return `<pre><code>${escapedCode.trim()}</code></pre>`;
});
let html = escapeHtml(text);
html = html
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^\* (.*$)/gim, '<li>$1</li>')
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/^---$/gim, '<hr>');
html = html.replace(/((?:<li>.*?<\/li>\s*)+)/g, '<ul>\n$1</ul>\n');
html = html.split('\n').map(line => line.trim()).filter(line => line.length > 0)
.map(paragraph => {
if (!paragraph.match(/<\/?(h[1-3]|ul|li|pre|hr|p)/)) {
return `<p>${paragraph}</p>`;
}
return paragraph;
}).join('\n');
return html.replace(/<p><pre>/g, '<pre>').replace(/<\/pre><\/p>/g, '</pre>');
};
const showGiftNotification = async () => {
const NOTIFICATION_URL = 'https://raw.githubusercontent.com/helloticc/DuoHacker/refs/heads/main/markdown.txt';
const modal = document.getElementById('_notification_modal');
const contentDiv = document.getElementById('_notification_content');
if (!modal || !contentDiv) {
console.error('Lỗi: Không tìm thấy các thành phần của modal thông báo.');
return;
}
modal.style.display = 'flex';
contentDiv.innerHTML = '<div class="_loading_spinner">⏳ Loading...</div>';
try {
const response = await fetch(NOTIFICATION_URL, { cache: "no-store" });
if (!response.ok) {
throw new Error(`Lỗi mạng: ${response.status} ${response.statusText}`);
}
const markdownText = await response.text();
const cleanHtml = customMarkdownParser(markdownText);
contentDiv.innerHTML = cleanHtml;
} catch (error) {
console.error('Error load:', error);
contentDiv.innerHTML = `<div class="_error_message">
<strong>Can't load notification.</strong>
<p>Check URL or check your internet connection.</p>
</div>`;
}
};
const hideImages = () => {
hideAnimationEnabled = true;
localStorage.setItem('duohacker_hide_animation', 'true');
const toggle = document.getElementById('_hide_animation_toggle');
if (toggle) toggle.classList.add('_active');
if (hideObserver) return;
const protectSelectors = ['#_container', '._modal', '#_fab', '#_update_overlay', '#_backdrop', '._fab_ring'];
const shouldIgnore = (el) => {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false;
return protectSelectors.some(sel => el.closest?.(sel));
};
const hideEl = (el) => {
if (shouldIgnore(el)) return;
if (el.style.display === 'none') return;
el.dataset.dhOrigDisplay = el.style.display || '';
el.dataset.dhOrigVisibility = el.style.visibility || '';
el.dataset.dhOrigPe = el.style.pointerEvents || '';
if (el.style.backgroundImage) {
el.dataset.dhOrigBg = el.style.backgroundImage;
}
el.style.display = 'none';
el.style.visibility = 'hidden';
el.style.pointerEvents = 'none';
if (el.style.backgroundImage) el.style.backgroundImage = 'none';
};
const processNode = (node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches?.('img, svg, [role="img"]')) hideEl(node);
const imgs = node.querySelectorAll?.('img, svg, [role="img"]') || [];
imgs.forEach(hideEl);
const all = [node, ...(node.querySelectorAll?.('*') || [])];
all.forEach(el => {
if (shouldIgnore(el)) return;
const bg = getComputedStyle(el).backgroundImage;
if (bg && bg !== 'none' && bg.includes('url(')) {
if (!el.dataset.dhOrigBg) el.dataset.dhOrigBg = el.style.backgroundImage || bg;
el.style.backgroundImage = 'none';
}
});
};
document.querySelectorAll('img, svg, [role="img"]').forEach(hideEl);
document.querySelectorAll('body *').forEach(el => {
if (shouldIgnore(el)) return;
const bg = getComputedStyle(el).backgroundImage;
if (bg && bg !== 'none' && bg.includes('url(')) {
if (!el.dataset.dhOrigBg) el.dataset.dhOrigBg = el.style.backgroundImage || bg;
el.style.backgroundImage = 'none';
}
});
hideObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') {
m.addedNodes.forEach(processNode);
}
}
});
hideObserver.observe(document.body, {
childList: true,
subtree: true
});
logToConsole('Hide Animation enabled – using MutationObserver', 'success');
};
const farmLeague = async () => {
logToConsole('Starting Auto League (Target: Rank 1)', 'info');
const delayMs = CUSTOM_DELAY;
const LB_URL = "https://duolingo-leaderboards-prod.duolingo.com/leaderboards/7d9f5dd1-8423-491a-91f2-2532052038ce";
while (isRunning) {
try {
const res = await fetch(`${LB_URL}/users/${sub}?client_unlocked=true&_=${Date.now()}`, {
headers: defaultHeaders
});
if (!res.ok) {
logToConsole('Failed to fetch leaderboard. Retrying...', 'warning');
await delay(2000);
continue;
}
const data = await res.json();
const rankings = data?.active?.cohort?.rankings || [];
const myData = rankings.find(u => u.user_id == sub);
if (!myData) {
logToConsole('Leaderboard data not found (Are you in a league?)', 'error');
stopFarming();
break;
}
const currentRank = rankings.indexOf(myData) + 1;
if (currentRank === 1) {
const top2 = rankings[1];
if (top2) {
const gap = myData.score - top2.score;
if (gap > 1000) {
logToConsole(`Top 1 Secured! (Gap: ${gap} XP). Stopping.`, 'success');
stopFarming();
break;
} else {
logToConsole(`Currently Top 1. Widening gap... (Gap: ${gap} XP)`, 'info');
}
} else {
logToConsole(`You are alone in Top 1!`, 'success');
stopFarming();
break;
}
} else {
const top1 = rankings[0];
const gap = top1.score - myData.score;
logToConsole(`Rank: ${currentRank} | Behind Top 1: ${gap} XP | Farming...`, 'info');
}
const farmRes = await farmXpOnce();
if (farmRes.ok) {
const d = await farmRes.json();
const earned = d.awardedXp || 0;
totalEarned.xp += earned;
updateEarnedStats();
saveSessionData();
} else {
logToConsole('XP Farm failed, retrying...', 'warning');
}
await delay(delayMs);
} catch (error) {
logToConsole(`League Error: ${error.message}`, 'error');
await delay(5000);
}
}
};
const showImages = () => {
hideAnimationEnabled = false;
localStorage.setItem('duohacker_hide_animation', 'false');
const toggle = document.getElementById('_hide_animation_toggle');
if (toggle) toggle.classList.remove('_active');
if (hideObserver) {
hideObserver.disconnect();
hideObserver = null;
}
const allHidden = document.querySelectorAll('[data-dhOrigDisplay], [data-dh-orig-display]');
allHidden.forEach(el => {
if (el.dataset.dhOrigDisplay !== undefined) el.style.display = el.dataset.dhOrigDisplay;
if (el.dataset.dhOrigVisibility !== undefined) el.style.visibility = el.dataset.dhOrigVisibility;
if (el.dataset.dhOrigPe !== undefined) el.style.pointerEvents = el.dataset.dhOrigPe;
if (el.dataset.dhOrigBg !== undefined) el.style.backgroundImage = el.dataset.dhOrigBg;
delete el.dataset.dhOrigDisplay;
delete el.dataset.dhOrigVisibility;
delete el.dataset.dhOrigPe;
delete el.dataset.dhOrigBg;
});
logToConsole('Hide Animation disabled – UI and images restored', 'info');
};
const solveTapCompleteTable = () => {
const tableRows = document.querySelectorAll('tbody tr');
window.sol.displayTableTokens.slice(1).forEach((rowTokens, i) => {
const answerCell = rowTokens[1]?.find(t => t.isBlank);
if (answerCell && tableRows[i]) {
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
const wordButtons = wordBank ? Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')) : [];
let answerBuilt = "";
for (let btn of wordButtons) {
if (!answerCell.text.startsWith(answerBuilt + btn.innerText)) continue;
btn.click();
answerBuilt += btn.innerText;
if (answerBuilt === answerCell.text) break;
}
}
});
};
const correctTokensRun = () => {
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
if (!wordBank) return;
const buttons = Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])'));
const correctTokens = window.sol.correctTokens || [];
for (let token of correctTokens) {
const btn = buttons.find(b => b.innerText.toLowerCase().trim() === token.toLowerCase().trim());
if (btn) {
btn.click();
}
}
};
const correctIndicesRun = () => {
const wordBank = document.querySelector('[data-test="word-bank"], .eSgkc');
if (!wordBank) return;
const buttons = Array.from(wordBank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])'));
const correctIndices = window.sol.correctIndices || [];
for (let i of correctIndices) {
if (buttons[i]) {
buttons[i].click();
}
}
};
if (typeof checkForAutoSolve === 'undefined') {
const checkForAutoSolve = () => {
if (window.location.pathname.includes('/lesson') && autoSolveEnabled) {
logToConsole('Auto-solve mode: Detected lesson page, starting to solve', 'info');
if (!lessonSolving) {
startLessonSolving();
}
}
};
}
const deleteAccount = (accountId) => {
savedAccounts = savedAccounts.filter(acc => acc.id !== accountId);
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedAccounts));
updateAccountsBadge();
renderAccountsList();
logToConsole('Account deleted', 'info');
};
const loginWithAccount = (account) => {
document.cookie = `jwt_token=${account.jwt}; path=/; domain=.duolingo.com`;
logToConsole(`Logging in as ${account.username}...`, 'info');
setTimeout(() => {
window.location.reload();
}, 1000);
};
const updateAccountsBadge = () => {
const badge = document.querySelector('._control_btn._accounts ._badge');
if (badge) {
badge.textContent = savedAccounts.length;
}
};
const renderAccountsList = () => {
const accountsList = document.getElementById('_accounts_list');
if (!accountsList) return;
if (savedAccounts.length === 0) {
accountsList.innerHTML = '<div class="_empty_state"><p>No saved accounts yet. Save your current account to get started!</p></div>';
return;
}
const currentUsername = userInfo?.username;
accountsList.innerHTML = savedAccounts.map(account => {
const isActive = account.username === currentUsername;
return `
<div class="_account_card ${isActive ? '_active' : ''}" data-id="${account.id}">
${isActive ? '<div class="_active_badge">ACTIVE</div>' : ''}
<div class="_account_header">
<div class="_account_avatar">
${getAccountAvatarHTML(account)}
</div>
<div class="_account_info">
<div class="_account_nickname">${account.nickname}</div>
<div class="_account_username">@${account.username}</div>
</div>
</div>
<div class="_account_stats">
<div class="_account_stat">⚡ ${account.totalXp?.toLocaleString() || 0}</div>
<div class="_account_stat">🔥 ${account.streak || 0}</div>
<div class="_account_stat">💎 ${account.gems || 0}</div>
</div>
<div class="_account_actions">
${!isActive ? `<button class="_account_action_btn _login" data-action="login">
<span style="font-size: 14px;">➡️</span>
Login
</button>` : '<div style="flex:1"></div>'}
<button class="_account_action_btn _delete" data-action="delete">
<span style="font-size: 14px;">🗑️</span>
</button>
</div>
</div>
`;
}).join('');
accountsList.querySelectorAll('._account_card').forEach(card => {
const accountId = card.dataset.id;
const account = savedAccounts.find(acc => acc.id === accountId);
card.querySelector('[data-action="login"]')?.addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Switch to account: ${account.nickname}?`)) {
loginWithAccount(account);
}
});
card.querySelector('[data-action="delete"]')?.addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Delete account: ${account.nickname}?`)) {
deleteAccount(accountId);
}
});
});
};
const addEventListeners = () => {
document.getElementById('_delay_slider')?.addEventListener('input', (e) => {
CUSTOM_DELAY = parseInt(e.target.value, 10);
localStorage.setItem('duohacker_custom_delay', CUSTOM_DELAY.toString());
const display = document.getElementById('_delay_display');
if (display) display.textContent = `${CUSTOM_DELAY}ms`;
});
document.getElementById('_safe_streak_toggle')?.addEventListener('click', () => {
const toggle = document.getElementById('_safe_streak_toggle');
const current = localStorage.getItem('duohacker_safe_streak') === 'true';
const newVal = !current;
localStorage.setItem('duohacker_safe_streak', newVal.toString());
if (newVal) {
toggle.classList.add('_active');
logToConsole('Safe Streak Mode enabled', 'success');
} else {
toggle.classList.remove('_active');
logToConsole('Safe Streak Mode disabled', 'warning');
}
});
document.getElementById('_test_webhook_btn')?.addEventListener('click', async () => {
const btn = document.getElementById('_test_webhook_btn');
if (!webhookUrl) {
alert("Please enter a webhook URL first!");
return;
}
btn.innerText = "Sending...";
btn.disabled = true;
await sendDiscordWebhook(
"🔔 Test Notification",
"Success",
"Your webhook is linked to DuoHacker successfully!",
3447003
);
btn.innerText = "Linked!";
setTimeout(() => {
btn.innerText = "Link";
btn.disabled = false;
}, 2000);
});
document.getElementById('_webhook_input')?.addEventListener('blur', (e) => {
webhookUrl = e.target.value.trim();
localStorage.setItem('duohacker_webhook_url', webhookUrl);
if(webhookUrl) logToConsole('Webhook URL saved', 'success');
});
document.getElementById('_webhook_toggle_btn')?.addEventListener('click', () => {
const panel = document.getElementById('_webhook_panel');
if (panel.style.display === 'none') {
panel.style.display = 'block';
} else {
panel.style.display = 'none';
}
});
document.getElementById('_gift_notification_btn')?.addEventListener('click', showGiftNotification);
document.getElementById('_close_notification')?.addEventListener('click', () => {
document.getElementById('_notification_modal').style.display = 'none';
});
document.getElementById('_notification_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_notification_modal').style.display = 'none';
}
});
document.getElementById('_leaderboard_btn')?.addEventListener('click', showLeaderboard);
document.getElementById('_close_leaderboard')?.addEventListener('click', () => {
document.getElementById('_leaderboard_modal').style.display = 'none';
});
document.getElementById('_leaderboard_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_leaderboard_modal').style.display = 'none';
}
});
document.getElementById('_inject_solver_toggle')?.addEventListener('click', () => {
const toggle = document.getElementById('_inject_solver_toggle');
INJECT_SOLVER_ENABLED = !INJECT_SOLVER_ENABLED;
localStorage.setItem('duohacker_inject_solver', INJECT_SOLVER_ENABLED.toString());
if (INJECT_SOLVER_ENABLED) {
toggle.classList.add('_active');
logToConsole('Auto Solver Enabled - Enter a lesson to see buttons', 'success');
autoSolver.checkAndToggle();
} else {
toggle.classList.remove('_active');
logToConsole('Auto Solver Disabled', 'info');
autoSolver.removeUI();
}
});
document.getElementById('_item_shop_btn')?.addEventListener('click', showItemShop);
document.getElementById('_monthly_badges')?.addEventListener('click', showMonthlyBadges);
document.getElementById('_fab')?.addEventListener('click', toggleInterface);
document.getElementById('_minimize_btn')?.addEventListener('click', () => {
setInterfaceVisible(false);
});
document.getElementById('_close_btn')?.addEventListener('click', () => {
if (isRunning) {
if (confirm('Farming is active. Are you sure you want to close?')) {
stopFarming();
setInterfaceVisible(false);
}
} else {
setInterfaceVisible(false);
}
});
document.getElementById('_hide_animation_toggle')?.addEventListener('click', (e) => {
e.stopPropagation();
const toggle = document.getElementById('_hide_animation_toggle');
if (hideAnimationEnabled) {
showImages();
toggle.classList.remove('_active');
} else {
hideImages();
toggle.classList.add('_active');
}
});
document.getElementById('_theme_toggle')?.addEventListener('click', () => {
applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
});
document.getElementById('_accounts_btn')?.addEventListener('click', () => {
renderAccountsList();
document.getElementById('_accounts_modal').style.display = 'flex';
});
document.getElementById('_close_accounts')?.addEventListener('click', () => {
document.getElementById('_accounts_modal').style.display = 'none';
});
document.getElementById('_accounts_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_accounts_modal').style.display = 'none';
}
});
document.getElementById('_settings_btn')?.addEventListener('click', async () => {
document.getElementById('_settings_modal').style.display = 'flex';
const btn = document.getElementById('_privacy_toggle_btn');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span style="font-size: 18px;">⏳</span> Loading...';
const isPrivate = await getCurrentPrivacyStatus();
if (isPrivate === true) {
btn.innerHTML = '<span style="font-size: 18px;">🔒</span> Set Public';
} else if (isPrivate === false) {
btn.innerHTML = '<span style="font-size: 18px;">🔒</span> Set Private';
} else {
btn.innerHTML = '<span style="font-size: 18px;">🔒</span> Set Private';
logToConsole("Could not load privacy status – defaulting to Set Private", 'warning');
}
btn.disabled = false;
}
});
document.getElementById('_close_settings')?.addEventListener('click', () => {
document.getElementById('_settings_modal').style.display = 'none';
});
document.getElementById('_settings_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_settings_modal').style.display = 'none';
}
});
document.getElementById('_lite_mode_toggle')?.addEventListener('click', () => {
const toggle = document.getElementById('_lite_mode_toggle');
liteMode = !liteMode;
localStorage.setItem('duohacker_lite_mode', liteMode.toString());
if (liteMode) {
document.body.setAttribute('data-lite-mode', 'true');
logToConsole('Lite Mode enabled – animations reduced', 'info');
toggle.classList.add('_active');
} else {
document.body.removeAttribute('data-lite-mode');
logToConsole('Lite Mode disabled – full animations restored', 'info');
toggle.classList.remove('_active');
}
});
document.getElementById('_auto_name_toggle')?.addEventListener('click', () => {
const toggle = document.getElementById('_auto_name_toggle');
autoNameEnabled = !autoNameEnabled;
localStorage.setItem('duohacker_auto_name', autoNameEnabled.toString());
if (autoNameEnabled) {
document.body.setAttribute('data-auto-name', 'true');
logToConsole('Auto-Name enabled – name will be changed when farming', 'success');
toggle.classList.add('_active');
} else {
document.body.removeAttribute('data-auto-name');
logToConsole('Auto-Name disabled – your name will not be changed', 'info');
toggle.classList.remove('_active');
}
});
document.getElementById('_privacy_toggle_btn')?.addEventListener('click', async () => {
const newState = await togglePrivacy();
if (newState !== null) {
const privacyBtn = document.getElementById('_privacy_toggle_btn');
if (privacyBtn) {
privacyBtn.innerHTML = newState ?
'<span style="font-size: 18px;">🔒</span> Set Public' :
'<span style="font-size: 18px;">🔒</span> Set Private';
}
}
});
document.getElementById('_duolingo_max_toggle')?.addEventListener('click', () => {
const toggle = document.getElementById('_duolingo_max_toggle');
duolingoMaxEnabled = !duolingoMaxEnabled;
localStorage.setItem('duohacker_duolingo_max', duolingoMaxEnabled.toString());
if (duolingoMaxEnabled) {
toggle.classList.add('_active');
logToConsole('Duolingo Max enabled — reload the page to apply', 'success');
if (confirm('Duolingo Max enabled! Reload now to apply?')) window.location.reload();
} else {
toggle.classList.remove('_active');
if (window.disableDuolingoMax) window.disableDuolingoMax();
logToConsole('Duolingo Max disabled — reload the page to apply', 'info');
if (confirm('Duolingo Max disabled! Reload now to apply?')) window.location.reload();
}
});
document.getElementById('_save_account_btn')?.addEventListener('click', () => {
if (!userInfo) {
logToConsole('Please wait for user data to load', 'error');
return;
}
document.getElementById('_preview_username').textContent = userInfo.username;
document.getElementById('_preview_details').textContent = `${userInfo.fromLanguage} → ${userInfo.learningLanguage}`;
document.getElementById('_account_nickname').value = userInfo.username;
const previewAvatar = document.getElementById('_preview_avatar');
if (previewAvatar) {
const previewUrl = normalizeAvatarUrl(userInfo.picture);
previewAvatar.innerHTML = previewUrl
? `<img src="${previewUrl}" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;" draggable="false">`
: '<span style="font-size: 20px;">👤</span>';
setStoredAvatarUrl(userInfo.username, previewUrl);
}
document.getElementById('_save_account_modal').style.display = 'flex';
});
document.getElementById('_close_save_account')?.addEventListener('click', () => {
document.getElementById('_save_account_modal').style.display = 'none';
});
document.getElementById('_save_account_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_save_account_modal').style.display = 'none';
}
});
document.getElementById('_confirm_save_account')?.addEventListener('click', () => {
const nickname = document.getElementById('_account_nickname').value.trim();
if (!nickname) {
alert('Please enter a nickname for this account');
return;
}
if (saveAccount(nickname)) {
document.getElementById('_save_account_modal').style.display = 'none';
alert(`Account saved as: ${nickname}`);
}
});
document.getElementById('_get_jwt_btn')?.addEventListener('click', () => {
const token = getJwtToken();
if (token) {
navigator.clipboard.writeText(token);
logToConsole('JWT Token copied to clipboard', 'success');
alert('JWT Token copied to clipboard!');
} else {
logToConsole('JWT Token not found', 'error');
alert('JWT Token not found! Please make sure you are logged in to Duolingo.');
}
});
document.getElementById('_logout_btn')?.addEventListener('click', () => {
if (confirm('Are you sure you want to log out?')) {
window.location.href = 'https://www.duolingo.com/logout';
}
});
document.getElementById('_login_jwt_btn')?.addEventListener('click', () => {
const jwtInput = document.getElementById('_jwt_input');
const token = jwtInput.value.trim();
if (token) {
document.cookie = `jwt_token=${token}; path=/; domain=.duolingo.com`;
logToConsole('JWT Token updated, refreshing page...', 'success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
logToConsole('Please enter a valid JWT Token', 'error');
alert('Please enter a valid JWT Token');
}
});
document.getElementById('_join_btn')?.addEventListener('click', () => {
localStorage.setItem('duofarmer_joined', 'true');
hasJoined = true;
document.getElementById('_join_section').style.display = 'none';
document.getElementById('_main_content').style.display = 'flex';
initializeFarming();
});
document.querySelectorAll('._option_btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('._option_btn').forEach(b => b.classList.remove('_selected'));
btn.classList.add('_selected');
});
});
document.getElementById('_start_farming')?.addEventListener('click', startFarming);
document.getElementById('_stop_farming')?.addEventListener('click', stopFarming);
document.getElementById('_refresh_profile')?.addEventListener('click', async () => {
const btn = document.getElementById('_refresh_profile');
btn.style.animation = 'spin 1s linear';
await refreshUserData();
btn.style.animation = '';
});
document.addEventListener("click", (e) => {
const t = e.target;
if (!t) return;
if (t.id === "_activity_fetch_btn") {
const who = document.getElementById("_activity_who_input")?.value?.trim();
if (who) fetchAndShowActivity(who);
}
if (t.id === "_activity_use_me_btn") {
const me = _getMyUserId() || "";
const input = document.getElementById("_activity_who_input");
if (input) input.value = String(me || "");
if (me) fetchAndShowActivity(me);
}
});
document.addEventListener("keydown", (e) => {
if (e.key !== "Enter") return;
const active = document.activeElement;
if (active && active.id === "_activity_who_input") {
const who = active.value.trim();
if (who) fetchAndShowActivity(who);
}
});
document.getElementById('_clear_console')?.addEventListener('click', () => {
const console = document.getElementById('_console_output');
if (console) {
console.innerHTML = '';
logToConsole('Console cleared', 'info');
}
});
document.getElementById('_free_super_btn')?.addEventListener('click', () => {
document.getElementById('_super_modal').style.display = 'flex';
});
document.getElementById('_close_super_modal')?.addEventListener('click', () => {
document.getElementById('_super_modal').style.display = 'none';
});
document.getElementById('_super_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_super_modal').style.display = 'none';
}
});
document.getElementById('_get_super_link_btn')?.addEventListener('click', async () => {
const btn = document.getElementById('_get_super_link_btn');
const errorDiv = document.getElementById('_super_error');
const resultDiv = document.getElementById('_super_link_display');
const linkAnchor = document.getElementById('_super_link_anchor');
btn.disabled = true;
btn.textContent = '⏳ Fetching...';
errorDiv.style.display = 'none';
resultDiv.style.display = 'none';
try {
const res = await fetch('https://raw.githubusercontent.com/pillowslua/DuoHacker/refs/heads/main/public/super.txt');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
const links = text
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
if (links.length === 0) {
throw new Error('No links found in file');
}
const selectedLink = links[Math.floor(Math.random() * links.length)];
linkAnchor.href = selectedLink;
linkAnchor.target = '_blank';
linkAnchor.textContent = selectedLink;
resultDiv.style.display = 'block';
console.log(`Fetched ${links.length} links, selected: ${selectedLink}`);
} catch (err) {
errorDiv.textContent = `❌ Error: ${err.message}`;
errorDiv.style.display = 'block';
console.error('Super link fetch error:', err);
} finally {
btn.disabled = false;
btn.textContent = '�Get Free Super Link';
}
});
document.getElementById('_go_to_link_btn')?.addEventListener('click', () => {
let url = document.getElementById('_super_link_anchor').textContent?.trim();
if (!url) {
alert('No link available');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
console.log('Opening:', url);
window.open(url, '_blank');
});
document.getElementById('_close_result_btn')?.addEventListener('click', () => {
document.getElementById('_super_modal').style.display = 'none';
});
const checkBtn = document.getElementById('_superlinks_check_btn');
const input = document.getElementById('_superlinks_input');
if (checkBtn && input) {
checkBtn.addEventListener('click', () => {
if (input.value.trim()) {
checkSuperlink(input.value);
} else {
alert('Please enter a superlink or ID');
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
checkSuperlink(input.value);
}
});
}
};
const checkSuperlink = async (input) => {
const resultDiv = document.getElementById('_superlinks_result');
const checkBtn = document.getElementById('_superlinks_check_btn');
resultDiv.style.display = 'block';
resultDiv.className = '_superlinks_result _loading';
resultDiv.textContent = '⏳ Checking...';
checkBtn.disabled = true;
try {
let id = input.trim();
if (id.includes('invite.duolingo.com')) {
id = id.split('/family-plan/')[1];
}
if (id.includes('https://') || id.includes('http://')) {
id = id.split('/').pop();
}
if (!id) {
throw new Error('Invalid link or ID format');
}
const url = `https://www.duolingo.com/2023-05-23/family-plan/invite/${id}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (response.status === 200) {
const data = await response.json();
if (data.isValid) {
resultDiv.className = '_superlinks_result _working';
resultDiv.innerHTML = `✅ <strong>Working</strong><br><small>${id}</small>`;
logToConsole(`Superlink ${id} is WORKING`, 'success');
} else {
resultDiv.className = '_superlinks_result _unavailable';
resultDiv.innerHTML = `❌ <strong>Unavailable</strong><br><small>Invalid link</small>`;
logToConsole(`Superlink ${id} is UNAVAILABLE`, 'error');
}
} else {
resultDiv.className = '_superlinks_result _unavailable';
resultDiv.innerHTML = `❌ <strong>Unavailable</strong><br><small>HTTP ${response.status}</small>`;
logToConsole(`Superlink check failed: ${response.status}`, 'error');
}
} catch (error) {
resultDiv.className = '_superlinks_result _unavailable';
resultDiv.innerHTML = `❌ <strong>Unavailable</strong><br><small>${error.message}</small>`;
logToConsole(`Superlink check error: ${error.message}`, 'error');
} finally {
checkBtn.disabled = false;
}
};
const initSuperlinksChecker = () => {
const checkBtn = document.getElementById('_superlinks_check_btn');
const input = document.getElementById('_superlinks_input');
if (checkBtn && input) {
checkBtn.addEventListener('click', () => {
if (input.value.trim()) {
checkSuperlink(input.value);
} else {
alert('Please enter a superlink or ID');
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
checkSuperlink(input.value);
}
});
}
};
/**
* Shows a Duolingo-style notification banner when the course is not Vi→En.
* Returns true if the course is valid (Vi→En), false otherwise.
* Only relevant for the 499 XP farm (farmXpOnce) which hits the
* en-{fromLanguage}-the-passport story endpoint.
*/
const checkViEnCourse = () => {
const from = (userInfo?.fromLanguage || '').toLowerCase();
const to = (userInfo?.learningLanguage || '').toLowerCase();
if (from === 'vi' && to === 'en') return true;
const bannerId = '_viEn_warning_banner';
if (document.getElementById(bannerId)) return false; // already visible
const banner = document.createElement('div');
banner.id = bannerId;
banner.style.cssText = [
'position:fixed',
'top:0',
'left:0',
'width:100%',
'z-index:99999',
'background:linear-gradient(135deg,#ff6b35 0%,#f7c948 100%)',
'color:#fff',
'font-family:"Nintendo of America",sans-serif,Arial,sans-serif',
'padding:0',
'display:flex',
'flex-direction:column',
'align-items:center',
'box-shadow:0 4px 14px rgba(0,0,0,.35)',
'animation:_viEn_slideDown .35s cubic-bezier(.4,0,.2,1) both'
].join(';');
banner.innerHTML = `
<style>
@keyframes _viEn_slideDown {
from { transform:translateY(-110%); opacity:0; }
to { transform:translateY(0); opacity:1; }
}
@keyframes _viEn_slideUp {
from { transform:translateY(0); opacity:1; }
to { transform:translateY(-110%); opacity:0; }
}
#${bannerId} .duo-icon { font-size:32px; margin-top:14px; }
#${bannerId} .duo-title {
font-size:18px; font-weight:800; margin:8px 0 4px;
text-shadow:0 1px 3px rgba(0,0,0,.25);
}
#${bannerId} .duo-body {
font-size:14px; font-weight:600; opacity:.95;
max-width:520px; text-align:center; line-height:1.45;
margin:0 16px;
}
#${bannerId} .duo-course-tag {
display:inline-block; background:rgba(255,255,255,.22);
border-radius:6px; padding:2px 10px; margin:0 3px;
font-weight:700; letter-spacing:.3px;
}
#${bannerId} .duo-close {
position:absolute; top:10px; right:14px;
background:none; border:none; color:#fff;
font-size:22px; cursor:pointer; line-height:1;
opacity:.75; transition:opacity .15s;
}
#${bannerId} .duo-close:hover { opacity:1; }
</style>
<div class="duo-icon">🦉</div>
<div class="duo-title">Oops! Wrong course detected</div>
<div class="duo-body">
Your current course is
<span class="duo-course-tag">${(userInfo?.fromLanguage || '?').toUpperCase()} → ${(userInfo?.learningLanguage || '?').toUpperCase()}</span>.
Please set your learning course to
<span class="duo-course-tag">Vi → En</span>
on <strong>duolingo.com</strong> before using Farm XP.
</div>
<button class="duo-close" id="${bannerId}_close">✕</button>
`;
document.body.appendChild(banner);
const autoClose = setTimeout(() => {
banner.style.animation = `_viEn_slideUp .3s cubic-bezier(.4,0,.2,1) forwards`;
setTimeout(() => banner.remove(), 320);
}, 8000);
document.getElementById(`${bannerId}_close`).addEventListener('click', () => {
clearTimeout(autoClose);
banner.style.animation = `_viEn_slideUp .3s cubic-bezier(.4,0,.2,1) forwards`;
setTimeout(() => banner.remove(), 320);
});
logToConsole('Course is not Vi→En. Please change your course on duolingo.com.', 'error');
return false;
};
const startFarming = async () => {
if (isRunning) {
logToConsole('Farming is already running', 'warning');
return;
}
if (!userInfo || !sub || !jwt || !defaultHeaders) {
logToConsole('Initializing user data...', 'info');
const success = await initializeFarming();
if (!success) {
logToConsole('Failed to initialize. Please refresh and try again.', 'error');
return;
}
}
const selectedOption = document.querySelector('._option_btn._selected');
if (!selectedOption) {
logToConsole('Please select a farming option', 'error');
return;
}
const type = selectedOption.dataset.type;
const delayMs = CUSTOM_DELAY;
// Vi→En course guard — only the farmXpOnce endpoint requires Vi→En.
// xp_10 (110 XP) uses a generic skill session and works with any course.
if (type === 'xp') {
if (!checkViEnCourse()) return;
}
if (type === 'xp_10') {
if (!skillId) {
skillId = extractSkillId(userInfo?.currentCourse || {});
}
if (!skillId) {
const continueAnyway = confirm(
'⚠️ Skill ID not found!\n\n' +
'XP farming requires a skill ID from your current course.\n\n' +
'Options:\n' +
'1. Click "Refresh Profile" and try again\n' +
'2. Navigate to a course page and refresh\n\n' +
'Continue anyway? (May fail)'
);
if (!continueAnyway) {
return;
}
}
}
if (type === 'farm_lesson') {
const input = prompt('How many lessons do you want to farm?\n(Enter 0 for unlimited)');
if (input === null) return;
const count = parseInt(input, 10);
if (isNaN(count) || count < 0) {
logToConsole('Invalid lesson count entered', 'error');
return;
}
currentLessonCount = 0;
lessonsToSolve = count;
// Persist across page reloads so farming resumes after navigation
sessionStorage.setItem('dh_lesson_farming', JSON.stringify({
active: true,
lessonsToSolve: count,
currentLessonCount: 0
}));
saveSessionData();
await startLessonSolving();
return;
}
isRunning = true;
farmingStats.startTime = Date.now();
const startBtn = document.getElementById('_start_farming');
const stopBtn = document.getElementById('_stop_farming');
if (startBtn) startBtn.style.display = 'none';
if (stopBtn) stopBtn.style.display = 'block';
logToConsole(`Started ${type} farming`, 'success');
const timer = setInterval(updateFarmingTime, 1000);
try {
switch (type) {
case 'xp_10':
await farmXP110(delayMs);
break;
case 'gems':
await farmGems(delayMs);
break;
case 'quest':
await runAutoCompleteQuests();
break;
case 'streak_farm':
await farmStreak();
break;
case 'league_farm':
await farmLeague();
break;
}
} catch (error) {
logToConsole(`Farming error: ${error.message}`, 'error');
} finally {
clearInterval(timer);
if (isRunning) {
stopFarming();
}
}
};
const GOALS_API_URL = "https://goals-api.duolingo.com";
const getGoalHeaders = () => {
if (!jwt) return null;
return {
...defaultHeaders,
"Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest",
"accept": "application/json; charset=UTF-8",
"Authorization": `Bearer ${jwt}`
};
};
const getQuestSchema = async (headers) => {
try {
const res = await fetch(`${GOALS_API_URL}/schema?ui_language=en&_=${Date.now()}`, {
headers
});
if (res.ok) return await res.json();
} catch (e) {
logToConsole(`Error fetching quest schema: ${e.message}`, 'error');
}
return null;
};
const getUserQuestProgress = async (userId, headers) => {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const res = await fetch(`${GOALS_API_URL}/users/${userId}/progress?timezone=${tz}&ui_language=en`, {
headers
});
if (res.ok) return await res.json();
} catch (e) {
logToConsole(`Error fetching user progress: ${e.message}`, 'error');
}
return null;
};
const bruteForceQuests = async (userId, headers, metrics) => {
const updates = metrics.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 {
const res = await fetch(`${GOALS_API_URL}/users/${userId}/progress/batch`, {
method: "POST",
headers,
body: JSON.stringify(payload)
});
return res.ok;
} catch (e) {
logToConsole(`Error brute forcing quests: ${e.message}`, 'error');
return false;
}
};
const runAutoCompleteQuests = async () => {
logToConsole('Starting Auto Quest...', 'info');
isRunning = true;
document.getElementById('_start_farming').style.display = 'none';
document.getElementById('_stop_farming').style.display = 'block';
const goalHeaders = getGoalHeaders();
if (!sub || !goalHeaders) {
logToConsole('User data not loaded. Please wait and try again.', 'error');
stopFarming();
return;
}
const schema = await getQuestSchema(goalHeaders);
const progress = await getUserQuestProgress(sub, goalHeaders);
if (!schema || !progress) {
logToConsole('Failed to load quest data.', 'error');
stopFarming();
return;
}
const earnedQuests = new Set(progress.badges?.earned || []);
const dailyQuestMetrics = new Set();
schema.goals.forEach(goal => {
const isDaily = goal.category?.includes('DAILY');
const isCompleted = earnedQuests.has(goal.badgeId) || earnedQuests.has(goal.goalId);
if (isDaily && !isCompleted && goal.metric) {
dailyQuestMetrics.add(goal.metric);
}
});
if (dailyQuestMetrics.size === 0) {
logToConsole('All daily quests are already completed!', 'success');
stopFarming();
return;
}
logToConsole(`Found ${dailyQuestMetrics.size} daily quests to complete...`, 'info');
const success = await bruteForceQuests(sub, goalHeaders, Array.from(dailyQuestMetrics));
if (success) {
logToConsole('Daily quests completed successfully!', 'success');
} else {
logToConsole('Failed to complete daily quests.', 'error');
}
await refreshUserData();
stopFarming();
};
const stopFarming = () => {
if (!isRunning && !lessonSolving) {
logToConsole('Farming is not running', 'warning');
return;
}
isRunning = false;
lessonSolving = false;
sessionStorage.removeItem('dh_lesson_farming');
if (farmingInterval) {
clearInterval(farmingInterval);
farmingInterval = null;
}
const startBtn = document.getElementById('_start_farming');
const stopBtn = document.getElementById('_stop_farming');
if (startBtn) {
startBtn.style.display = 'block';
startBtn.disabled = false;
}
if (stopBtn) {
stopBtn.style.display = 'none';
stopBtn.disabled = false;
}
logToConsole('Farming stopped', 'success');
saveSessionData();
// Small delay to ensure flags are reset
setTimeout(() => {
isRunning = false;
lessonSolving = false;
}, 100);
};
const startLessonSolving = async () => {
if (lessonSolving) {
logToConsole('Lesson solving already running', 'warning');
return;
}
// ✅ SET UI STATE FIRST
isRunning = true;
lessonSolving = true;
farmingStats.startTime = Date.now();
const startBtn = document.getElementById('_start_farming');
const stopBtn = document.getElementById('_stop_farming');
if (startBtn) startBtn.style.display = 'none';
if (stopBtn) stopBtn.style.display = 'block';
logToConsole('Starting Farm Practice (P-5xp)', 'success');
let sessionData = JSON.parse(localStorage.getItem('duohacker_session') || '{}');
sessionData.autoSolveEnabled = true;
localStorage.setItem('duohacker_session', JSON.stringify(sessionData));
try {
// Navigate to practice
if (!window.location.pathname.startsWith('/practice')) {
logToConsole('Navigating to practice...', 'info');
window.location.assign('/practice');
return;
}
// Wait for challenge to load
let waited = 0;
while (!document.querySelector('._3yE3H') && !document.querySelector('[data-test="challenge"]') && waited < 10000 && lessonSolving && isRunning) {
await delay(500);
waited += 500;
}
if (!isRunning || !lessonSolving) {
logToConsole('Stopped before solving', 'info');
return;
}
await solveCurrentLesson();
if (!isRunning || !lessonSolving) {
logToConsole('Stopped during solving', 'info');
return;
}
if (lessonsToSolve > 0 && currentLessonCount >= lessonsToSolve) {
logToConsole(`Completed ${currentLessonCount}/${lessonsToSolve} practices!`, 'success');
stopFarming();
return;
}
logToConsole('Loading next practice...', 'info');
if (window.location.pathname.startsWith('/practice')) {
await delay(800);
startLessonSolving();
} else {
window.location.assign('/practice');
}
} catch (error) {
console.error("Farming error:", error);
logToConsole(`Error: ${error.message}`, 'error');
stopFarming();
}
};
const solveCurrentLesson = () => {
return new Promise((resolve) => {
let lastChallengeId = null;
let solving = false;
let ticks = 0;
const MAX_TICKS = 240;
let iv = null;
const clickNext = () => {
const nextBtn = document.querySelector('[data-test="player-next"]') ||
document.querySelector('[data-test="stories-player-continue"]') ||
document.querySelector('[data-test="stories-player-done"]');
if (!nextBtn) return;
const isDisabled = nextBtn.getAttribute('aria-disabled') === 'true' || nextBtn.disabled;
if (!isDisabled) {
nextBtn.click();
setTimeout(() => {
if (!nextBtn.disabled) nextBtn.click();
}, 5);
}
};
const tick = async () => {
try {
if (!isRunning || !lessonSolving) {
if (iv) clearInterval(iv);
logToConsole('Lesson solving stopped', 'warning');
resolve();
return;
}
ticks++;
if (ticks > MAX_TICKS) {
if (iv) clearInterval(iv);
logToConsole('Lesson timeout (exceeded max ticks)', 'warning');
resolve();
return;
}
const completeSlide = document.querySelector('[data-test="session-over"]') ||
document.querySelector('[data-test="session-complete-slide"]') ||
document.querySelector('[data-test="challenge-complete"]') ||
document.querySelector('[data-test="session-complete"]');
if (completeSlide) {
if (iv) clearInterval(iv);
await delay(500);
const xpBefore = userInfo?.totalXp || 0;
await refreshUserData();
const xpAfter = userInfo?.totalXp || 0;
const xpGained = Math.max(0, xpAfter - xpBefore);
saveLessonXP(xpGained);
totalEarned.xp += xpGained;
logToConsole(`Lesson completed! Gained ${xpGained} XP`, 'success');
totalEarned.lessons++;
currentLessonCount++;
updateEarnedStats();
resolve();
return;
}
if (solving) return;
let el = document.querySelector('._3yE3H') ||
document.querySelector('[data-test="challenge"]') ||
document.querySelector('[class*="challenge"]');
if (!el) {
clickNext();
return;
}
const reactInst = autoSolver.findReact(el);
window.sol = reactInst?.props?.currentChallenge;
if (!window.sol) {
clickNext();
return;
}
const challengeId = `${window.sol.type}:${window.sol.id || JSON.stringify(window.sol.correctIndex ?? window.sol.correctTokens ?? window.sol.correctSolutions?.[0] ?? '')}`;
if (challengeId === lastChallengeId) {
clickNext();
return;
}
const type = autoSolver.determineChallengeType();
if (!type) {
logToConsole(`Unknown challenge, skipping...`, 'warning');
clickNext();
return;
}
solving = true;
lastChallengeId = challengeId;
try {
await autoSolver.handleChallenge(type);
} catch (e) {
logToConsole(`Solve error: ${e.message}`, 'error');
}
await autoSolver.delay(350);
clickNext();
await autoSolver.delay(600);
solving = false;
} catch (err) {
logToConsole(`Tick error: ${err.message}`, 'error');
solving = false;
}
};
iv = setInterval(tick, 500);
const timeoutId = setTimeout(() => {
if (iv) clearInterval(iv);
logToConsole('Lesson timeout', 'warning');
resolve();
}, 180000);
});
};
const saveLessonXP = (xpGained) => {
try {
const currentSessionXP = parseInt(sessionStorage.getItem('dh_session_xp_earned') || '0', 10);
const newTotal = currentSessionXP + xpGained;
sessionStorage.setItem('dh_session_xp_earned', String(newTotal));
console.log(`[XP Track] +${xpGained} XP | Total: ${newTotal}`);
} catch (error) {
console.error('Error saving lesson XP:', error);
}
};
const farmXP = async (delayMs) => {
while (isRunning) {
try {
const response = await farmXpOnce();
if (response.ok) {
const data = await response.json();
const earned = data?.awardedXp || 0;
totalEarned.xp += earned;
updateEarnedStats();
saveSessionData();
logToConsole(`Earned ${earned} XP`, 'success');
if (totalEarned.xp % 100 <= earned) {
sendDiscordWebhook("⚡ XP Farmed", "Active", `Gained **${earned} XP**\nTotal Session: ${totalEarned.xp} XP`, 16761035);
}
}
await delay(delayMs);
} catch (error) {
logToConsole(`XP farming error: ${error.message}`, 'error');
await delay(delayMs * 2);
}
}
};
const farmGems = async (delayMs) => {
while (isRunning) {
try {
const response = await farmGemOnce();
if (response.ok) {
totalEarned.gems += 30;
updateEarnedStats();
saveSessionData();
logToConsole('Earned 30 gems', 'success');
sendDiscordWebhook("💎 Gems Farmed", "Active", `Gained **30 Gems**\nTotal Session: ${totalEarned.gems} Gems`, 3066993);
}
await delay(delayMs);
} catch (error) {
logToConsole(`Gem farming error: ${error.message}`, 'error');
await delay(delayMs * 2);
}
}
};
const farmStreak = async () => {
if (!userInfo || !sub || !jwt || !defaultHeaders) {
logToConsole('Initializing user data for streak farming...', 'info');
const success = await initializeFarming();
if (!success || !userInfo) {
logToConsole('Failed to load user data. Please try again.', 'error');
stopFarming();
return;
}
}
const isSafeMode = localStorage.getItem('duohacker_safe_streak') === 'true';
logToConsole(isSafeMode ? 'Safe streak mode ON' : 'Normal streak mode', isSafeMode ? 'success' : 'warning');
if (isSafeMode) {
await farmStreakSafe();
} else {
await farmStreakNormal();
}
};
const farmStreakSafe = async () => {
logToConsole('Starting SAFE streak farming (limited to account age)...', 'info');
if (!userInfo) {
logToConsole('User data not loaded. Attempting to initialize...', 'error');
const success = await initializeFarming();
if (!success || !userInfo) {
logToConsole('Failed to initialize user data. Please refresh and try again.', 'error');
stopFarming();
return;
}
}
if (!userInfo.streakData) {
logToConsole('Streak data not available. Please refresh and try again.', 'error');
stopFarming();
return;
}
let creationDate;
try {
if (typeof userInfo.creationDate === 'number') {
creationDate = new Date(userInfo.creationDate);
} else if (typeof userInfo.creationDate === 'string') {
creationDate = new Date(userInfo.creationDate);
} else {
throw new Error('Invalid creationDate format');
}
if (isNaN(creationDate.getTime())) {
throw new Error('Invalid date object');
}
} catch (error) {
logToConsole(`Error parsing creation date: ${error.message}`, 'error');
logToConsole(`Raw creationDate: ${JSON.stringify(userInfo.creationDate)}`, 'error');
stopFarming();
return;
}
const now = new Date();
const accountAgeMs = now.getTime() - creationDate.getTime();
const accountAgeDays = Math.floor(accountAgeMs / (1000 * 60 * 60 * 24));
if (accountAgeDays > 5500 || accountAgeDays < 0) {
logToConsole(`Invalid account age: ${accountAgeDays} days`, 'error');
logToConsole(`Creation: ${creationDate.toISOString()}`, 'error');
logToConsole(`Current: ${now.toISOString()}`, 'error');
logToConsole('This seems wrong. Please report this issue.', 'error');
stopFarming();
return;
}
const creationTimestamp = Math.floor(creationDate.getTime() / 1000);
const currentTime = Math.floor(now.getTime() / 1000);
const maxSafeStreak = accountAgeDays;
logToConsole(`📅 Account created: ${creationDate.toLocaleDateString()}`, 'info');
logToConsole(`📊 Account age: ${accountAgeDays} days (~${Math.floor(accountAgeDays/365)} years)`, 'info');
logToConsole(`Max safe streak: ${maxSafeStreak} days`, 'info');
logToConsole(`🔥 Current streak: ${userInfo.streak} days`, 'info');
if (userInfo.streak >= maxSafeStreak) {
logToConsole(`You already have maximum safe streak (${userInfo.streak}/${maxSafeStreak} days)!`, 'success');
stopFarming();
return;
}
isRunning = true;
farmingStats.startTime = Date.now();
document.getElementById('_start_farming')?.style?.display && (document.getElementById('_start_farming').style.display = 'none');
document.getElementById('_stop_farming')?.style?.display && (document.getElementById('_stop_farming').style.display = 'block');
const streaksToFarm = maxSafeStreak - userInfo.streak;
logToConsole(`Will farm ${streaksToFarm} streaks to reach safe limit...`, 'success');
if (!confirm(`This will farm ${streaksToFarm} streaks safely.\n\nAccount age: ${accountAgeDays} days\nCurrent streak: ${userInfo.streak}\nTarget streak: ${maxSafeStreak}\n\nContinue?`)) {
logToConsole('Streak farming cancelled by user', 'info');
stopFarming();
return;
}
let farmTimestamp = creationTimestamp;
const endTimestamp = currentTime;
let farmedCount = 0;
while (isRunning && farmTimestamp <= endTimestamp && farmedCount < streaksToFarm) {
try {
await farmSessionOnce(farmTimestamp, farmTimestamp + 60);
farmTimestamp += 86400;
farmedCount++;
totalEarned.streak++;
userInfo.streak++;
updateUserInfo();
updateEarnedStats();
saveSessionData();
const progress = Math.round((farmedCount / streaksToFarm) * 100);
logToConsole(`📈 Progress: ${farmedCount}/${streaksToFarm} (${progress}%) | Streak: ${userInfo.streak}/${maxSafeStreak}`, 'success');
if (farmedCount % 25 === 0 && sendDiscordWebhook) {
sendDiscordWebhook("🔥 Streak Progress", "Safe Mode",
`Farmed: **${farmedCount}/${streaksToFarm}** streaks\nCurrent: **${userInfo.streak} days**\nTarget: **${maxSafeStreak} days**`, 15158332);
}
await delay(CUSTOM_DELAY);
} catch (error) {
logToConsole(`Error farming streak: ${error.message}`, 'error');
await delay(CUSTOM_DELAY * 2);
}
}
if (farmedCount >= streaksToFarm || userInfo.streak >= maxSafeStreak) {
logToConsole(`SAFE STREAK FARMING COMPLETE!`, 'success');
logToConsole(`Final streak: ${userInfo.streak}/${maxSafeStreak} days`, 'success');
if (sendDiscordWebhook) {
sendDiscordWebhook(" Safe Streak Farm Complete", "Success",
`Maximum safe streak achieved!\n\n**Final Streak:** ${userInfo.streak} days\n**Account Age:** ${accountAgeDays} days\n**Farmed:** ${farmedCount} streaks`, 3066993);
}
} else if (!isRunning) {
logToConsole(` Streak farming stopped by user`, 'info');
logToConsole(`Farmed ${farmedCount}/${streaksToFarm} streaks`, 'info');
}
stopFarming();
};
const farmStreakNormal = async () => {
logToConsole('WARNING: Normal streak farming has higher ban risk!', 'warning');
const hasStreak = !!userInfo.streakData?.currentStreak;
const startStreakDate = hasStreak ? userInfo.streakData.currentStreak.startDate : new Date();
const startFarmStreakTimestamp = Math.floor(new Date(startStreakDate).getTime() / 1000);
let currentTimestamp = hasStreak ? startFarmStreakTimestamp - 86400 : startFarmStreakTimestamp;
const delayMs = CUSTOM_DELAY;
while (isRunning) {
try {
await farmSessionOnce(currentTimestamp, currentTimestamp + 60);
currentTimestamp -= 86400;
totalEarned.streak++;
userInfo.streak++;
updateUserInfo();
updateEarnedStats();
saveSessionData();
logToConsole(`Streak increased to ${userInfo.streak}`, 'success');
if (userInfo.streak % 10 === 0) {
sendDiscordWebhook(
"🔥 Streak Extended",
"Success",
`Current Streak: **${userInfo.streak} Days**`,
15158332
);
}
await delay(delayMs);
} catch (error) {
logToConsole(`Streak farming error: ${error.message}`, 'error');
await delay(delayMs * 2);
}
}
};
const getQuestTimestamp = (goalId) => {
const regex = /^(\d{4})_(\d{2})_monthly/;
const match = goalId.match(regex);
if (match) {
const year = parseInt(match[1]);
const month = parseInt(match[2]) - 1;
const date = new Date(Date.UTC(year, month, 15, 12, 0, 0));
return date.toISOString();
}
return new Date().toISOString();
};
const isQuestOlderThanAccount = (goalId) => {
if (!userInfo?.creationDate) return false;
const match = goalId.match(/^(\d{4})_(\d{2})_monthly/);
if (match) {
const year = parseInt(match[1]);
const month = parseInt(match[2]) - 1;
const creationDate = new Date(userInfo.creationDate);
const creationYear = creationDate.getFullYear();
const creationMonth = creationDate.getMonth();
if (year < creationYear) return true;
if (year === creationYear && month < creationMonth) return true;
}
return false;
};
const getJwtToken = () => {
let match = document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'));
if (match) {
return match[2];
}
return null;
};
const decodeJwtToken = (token) => {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
.join("")
);
return JSON.parse(jsonPayload);
};
const formatHeaders = (jwt) => ({
"Content-Type": "application/json",
Authorization: "Bearer " + jwt,
"User-Agent": navigator.userAgent,
});
const getUserInfo = async (sub) => {
const userInfoUrl = `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,level,numFollowers,numFollowing,gems,creationDate,streakData,picture,hasPlus,trackingProperties,currentCourse{pathSectioned{units{levels{pathLevelMetadata{skillId}}}}}`;
const response = await fetch(userInfoUrl, {
method: "GET",
headers: defaultHeaders,
});
const data = await response.json();
if (data.trackingProperties && data.trackingProperties.creation_date_new) {
data.creationDate = data.trackingProperties.creation_date_new;
console.log('Using trackingProperties.creation_date_new:', data.creationDate);
} else if (typeof data.creationDate === 'number') {
data.creationDate = new Date(data.creationDate).toISOString();
console.log('⚠️ Converted numeric creationDate to ISO:', data.creationDate);
}
return data;
};
const sendRequestWithDefaultHeaders = async ({
url,
payload,
headers = {},
method = "GET"
}) => {
const mergedHeaders = {
...defaultHeaders,
...headers
};
return await fetch(url, {
method,
headers: mergedHeaders,
body: payload ? JSON.stringify(payload) : undefined,
});
};
const farmGemOnce = async () => {
const idReward = "SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
const patchUrl = `https://www.duolingo.com/2023-05-23/users/${sub}/rewards/${idReward}`;
const patchData = {
consumed: true,
learningLanguage: userInfo.learningLanguage,
fromLanguage: userInfo.fromLanguage,
};
return await sendRequestWithDefaultHeaders({
url: patchUrl,
payload: patchData,
method: "PATCH",
});
};
const farmSessionOnce = async (startTime, endTime) => {
const sessionPayload = {
challengeTypes: [
"assist", "characterIntro", "characterMatch", "characterPuzzle", "characterSelect",
"characterTrace", "characterWrite", "completeReverseTranslation", "definition",
"dialogue", "extendedMatch", "extendedListenMatch", "form", "freeResponse",
"gapFill", "judge", "listen", "listenComplete", "listenMatch", "match", "name",
"listenComprehension", "listenIsolation", "listenSpeak", "listenTap",
"orderTapComplete", "partialListen", "partialReverseTranslate", "patternTapComplete",
"radioBinary", "radioImageSelect", "radioListenMatch", "radioListenRecognize",
"radioSelect", "readComprehension", "reverseAssist", "sameDifferent", "select",
"selectPronunciation", "selectTranscription", "svgPuzzle", "syllableTap",
"syllableListenTap", "speak", "tapCloze", "tapClozeTable", "tapComplete",
"tapCompleteTable", "tapDescribe", "translate", "transliterate",
"transliterationAssist", "typeCloze", "typeClozeTable", "typeComplete",
"typeCompleteTable", "writeComprehension",
],
fromLanguage: userInfo.fromLanguage,
isFinalLevel: false,
isV2: true,
juicy: true,
learningLanguage: userInfo.learningLanguage,
smartTipsVersion: 2,
type: "GLOBAL_PRACTICE",
};
const sessionRes = await sendRequestWithDefaultHeaders({
url: "https://www.duolingo.com/2023-05-23/sessions",
payload: sessionPayload,
method: "POST",
});
const streakSessionData = await sessionRes.json();
const updateSessionPayload = {
...streakSessionData,
heartsLeft: 0,
startTime: startTime,
enableBonusPoints: false,
endTime: endTime,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true,
};
const updateRes = await sendRequestWithDefaultHeaders({
url: `https://www.duolingo.com/2023-05-23/sessions/${streakSessionData.id}`,
payload: updateSessionPayload,
method: "PUT",
});
return await updateRes.json();
};
const updateUserInfo = () => {
if (!userInfo) return;
const elements = {
username: document.getElementById('_username'),
user_details: document.getElementById('_user_details'),
currentStreak: document.getElementById('_current_streak'),
currentGems: document.getElementById('_current_gems'),
currentXp: document.getElementById('_current_xp')
};
if (elements.username) elements.username.textContent = userInfo.username;
if (elements.user_details) {
elements.user_details.textContent = `${userInfo.fromLanguage} → ${userInfo.learningLanguage}`;
}
if (elements.currentStreak) elements.currentStreak.textContent = userInfo.streak?.toLocaleString() || '0';
if (elements.currentGems) elements.currentGems.textContent = userInfo.gems?.toLocaleString() || '0';
if (elements.currentXp) elements.currentXp.textContent = userInfo.totalXp?.toLocaleString() || '0';
updateAvatarDisplay();
};
const updateAvatarDisplay = () => {
const mainAvatarEl = document.querySelector('._avatar');
if (mainAvatarEl) {
if (userInfo && userInfo.picture) {
let hqUrl = userInfo.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!hqUrl.endsWith('/xlarge') && hqUrl.includes('duolingo.com/ssr-avatars')) {
hqUrl += '/xlarge';
}
mainAvatarEl.innerHTML = `<img src="${hqUrl}" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;" draggable="false">`;
setStoredAvatarUrl(userInfo?.username, hqUrl);
} else {
mainAvatarEl.innerHTML = '<span style="font-size: 28px;">👤</span>';
}
}
};
const refreshUserData = async () => {
if (!sub || !defaultHeaders) return;
try {
logToConsole('Refreshing user data...', 'info');
userInfo = await getUserInfo(sub);
skillId = extractSkillId(userInfo.currentCourse);
if (skillId) {
logToConsole(`Skill ID detected: ${skillId}`, 'success');
logToConsole('XP 110 farming is now available!', 'success');
} else {
logToConsole('No skill ID found. XP 110 may not work.', 'warning');
logToConsole('Navigate to a course page and try refreshing again.', 'info');
}
updateUserInfo();
updateAvatarDisplay();
updateDailyQuestButtonUI();
logToConsole('User data refreshed', 'success');
} catch (error) {
logToConsole(`Failed to refresh: ${error.message}`, 'error');
}
};
const initializeFarming = async () => {
try {
jwt = getJwtToken();
if (!jwt) {
logToConsole('Please login to Duolingo and reload', 'error');
return false;
}
defaultHeaders = formatHeaders(jwt);
const decodedJwt = decodeJwtToken(jwt);
sub = decodedJwt.sub;
userInfo = await getUserInfo(sub);
if (userInfo && userInfo.username) {
updateUserInfo();
return true;
}
} catch (error) {
logToConsole(`Init error: ${error.message}`, 'error');
return false;
}
};
const updateStyle = document.createElement('style');
updateStyle.innerHTML = `
#_update_overlay {
animation: fadeInUpdate 0.5s ease-out;
}
@keyframes fadeInUpdate {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
#_update_btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
#_update_btn:active {
transform: translateY(0);
}
`;
document.head.appendChild(updateStyle);
function _getMyUserId() {
try {
if (typeof state !== "undefined" && state?.userId) return state.userId;
} catch {}
try {
if (typeof sub !== "undefined" && sub) return sub;
} catch {}
try {
const pre = window?.__PRELOADED_STATE__?.user?.id;
if (pre) return pre;
} catch {}
try {
const raw = localStorage.getItem("reduxPersist:user");
if (raw) {
const p = JSON.parse(raw);
if (p?.id) return p.id;
}
} catch {}
return null;
}
(async () => {
const sessionData = JSON.parse(localStorage.getItem('duohacker_session') || '{}');
if (sessionData.autoSolveEnabled) {
const path = window.location.pathname;
if (path.startsWith('/practice') || path.startsWith('/lesson')) {
logToConsole('Resuming high-speed farming session...', 'success');
setTimeout(() => {
startLessonSolving();
}, 2000);
}
}
try {
// removed
await new Promise(resolve => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', resolve, { once: true });
} else {
resolve();
}
});
initInterface();
setInterfaceVisible(false);
applyTheme(currentTheme);
initSuperlinksChecker();
addEventListeners();
updateDailyQuestButtonUI();
updateAccountsBadge();
document.getElementById('_join_section').style.display = 'flex';
document.getElementById('_main_content').style.display = 'none';
if (hideAnimationEnabled) {
setTimeout(() => {
hideImages();
}, 500);
}
setInterval(() => { if (typeof checkForLessonPage === 'function') checkForLessonPage(); }, 2000);
logToConsole('DuoHacker ready', 'success');
} catch (error) {
console.error('Init failed:', error);
}
})();