The #1 Duolingo hack - Farm XP, Gems, Streaks and unlock Duolingo Max for free.
// ==UserScript==
// @name Duolingo DuoHacker
// @name:vi Duolingo DuoHacker
// @name:zh-CN 多邻国 DuoHacker
// @name:zh-TW 多鄰國 DuoHacker
// @name:ja Duolingo DuoHacker
// @name:ko 듀오링고 DuoHacker
// @name:fr Duolingo DuoHacker
// @name:de Duolingo DuoHacker
// @name:es Duolingo DuoHacker
// @name:pt-BR Duolingo DuoHacker
// @name:pt-PT Duolingo DuoHacker
// @name:ru Дуолинго DuoHacker
// @name:ar Duolingo DuoHacker دوهاكر
// @name:tr Duolingo DuoHacker
// @name:id Duolingo DuoHacker
// @name:th Duolingo DuoHacker
// @name:pl Duolingo DuoHacker
// @name:nl Duolingo DuoHacker
// @name:it Duolingo DuoHacker
// @name:sv Duolingo DuoHacker
// @name:da Duolingo DuoHacker
// @name:fi Duolingo DuoHacker
// @name:nb Duolingo DuoHacker
// @name:cs Duolingo DuoHacker
// @name:hu Duolingo DuoHacker
// @name:ro Duolingo DuoHacker
// @name:uk Дуолінго DuoHacker
// @name:hi Duolingo DuoHacker
// @name:bn Duolingo DuoHacker
// @name:fa Duolingo DuoHacker دوهاکر
// @name:he Duolingo DuoHacker
// @name:ms Duolingo DuoHacker
// @name:fil Duolingo DuoHacker
// @name:el Duolingo DuoHacker
// @name:hr Duolingo DuoHacker
// @name:sk Duolingo DuoHacker
// @name:bg Дуолинго DuoHacker
// @name:sr Дуолинго DuoHacker
// @name:lt Duolingo DuoHacker
// @name:lv Duolingo DuoHacker
// @name:et Duolingo DuoHacker
// @name:sl Duolingo DuoHacker
// @name:ca Duolingo DuoHacker
// @name:af Duolingo DuoHacker
// @name:sw Duolingo DuoHacker
// @name:zu Duolingo DuoHacker
// @name:mn Duolingo DuoHacker
// @name:my Duolingo DuoHacker
// @name:km Duolingo DuoHacker
// @name:lo Duolingo DuoHacker
// @name:ur Duolingo DuoHacker
// @namespace https://github.com/not2pixel/DuoHacker
// @version 2026.06.15
// @description The #1 Duolingo hack - Farm XP, Gems, Streaks and unlock Duolingo Max for free.
// @description:vi Công cụ hack Duolingo #1 - Farm XP, Gems, Streaks và mở khóa Duolingo Max miễn phí.
// @description:zh-CN 最强 Duolingo 辅助工具 - 自动刷 XP、宝石、连胜,免费解锁 Duolingo Max。
// @description:zh-TW 最強 Duolingo 輔助工具 - 自動刷 XP、寶石、連勝,免費解鎖 Duolingo Max。
// @description:ja Duolingo最強ハックツール - XP・ジェム・連続記録を自動取得、Duolingo Maxを無料解放。
// @description:ko 최고의 Duolingo 핵 툴 - XP, 젬, 스트릭 자동 획득 및 Duolingo Max 무료 해제.
// @description:fr Le hack Duolingo #1 - Farmez XP, Gemmes, Séries et débloquez Duolingo Max gratuitement.
// @description:de Der #1 Duolingo-Hack - XP, Gems, Serien farmen und Duolingo Max kostenlos freischalten.
// @description:es El hack #1 de Duolingo - Farmea XP, Gemas, Rachas y desbloquea Duolingo Max gratis.
// @description:pt-BR O hack #1 do Duolingo - Farme XP, Gemas, Sequências e desbloqueie o Duolingo Max de graça.
// @description:pt-PT O hack #1 do Duolingo - Faça farm de XP, Gemas, Sequências e desbloqueie o Duolingo Max gratuitamente.
// @description:ru Лучший хак для Duolingo - Фармите XP, Гемы, Серии и разблокируйте Duolingo Max бесплатно.
// @description:ar أفضل هاك لـ Duolingo - اجمع XP والجواهر والسلاسل وافتح Duolingo Max مجاناً.
// @description:tr Duolingo için #1 hack - XP, Gem, Seri farm'la ve Duolingo Max'ı ücretsiz aç.
// @description:id Hack Duolingo #1 - Farm XP, Gems, Streak dan buka kunci Duolingo Max gratis.
// @description:th แฮก Duolingo อันดับ 1 - ฟาร์ม XP, เพชร, สตรีคและปลดล็อก Duolingo Max ฟรี.
// @description:pl Najlepszy hack na Duolingo - Farmuj XP, Klejnoty, Serie i odblokuj Duolingo Max za darmo.
// @description:nl De #1 Duolingo-hack - Farm XP, Edelstenen, Reeksen en ontgrendel Duolingo Max gratis.
// @description:it Il miglior hack per Duolingo - Fai farm di XP, Gemme, Streak e sblocca Duolingo Max gratis.
// @description:sv Bästa Duolingo-hacket - Farma XP, Ädelstenar, Streaks och lås upp Duolingo Max gratis.
// @description:da Den bedste Duolingo-hack - Farm XP, Ædelstene, Striber og lås Duolingo Max op gratis.
// @description:fi Paras Duolingo-hakki - Farmmaa XP, Jalokivet, Putket ja avaa Duolingo Max ilmaiseksi.
// @description:nb Den beste Duolingo-hacken - Farm XP, Edelstener, Rekker og lås opp Duolingo Max gratis.
// @description:cs Nejlepší hack na Duolingo - Farmujte XP, Drahokamy, Série a odemkněte Duolingo Max zdarma.
// @description:hu A legjobb Duolingo-hack - Farmolj XP-t, Drágaköveket, Sorozatokat és oldd fel a Duolingo Maxot ingyen.
// @description:ro Cel mai bun hack pentru Duolingo - Farmează XP, Pietre, Serii și deblochează Duolingo Max gratuit.
// @description:uk Найкращий хак для Duolingo - Фармте XP, Самоцвіти, Серії та розблокуйте Duolingo Max безкоштовно.
// @description:hi Duolingo का #1 हैक - XP, Gems, Streaks फार्म करें और Duolingo Max मुफ्त में अनलॉक करें।
// @description:bn সেরা Duolingo হ্যাক - XP, Gems, Streaks ফার্ম করুন এবং বিনামূল্যে Duolingo Max আনলক করুন।
// @description:fa بهترین هک Duolingo - XP، جواهر و رکورد را فارم کنید و Duolingo Max را رایگان باز کنید.
// @description:he ההאק הטוב ביותר ל-Duolingo - צבור XP, אבנים יקרות, רצפים ופתח את Duolingo Max בחינם.
// @description:ms Hack Duolingo terbaik - Farm XP, Permata, Streak dan buka kunci Duolingo Max secara percuma.
// @description:fil Ang pinakamahusay na Duolingo hack - Mag-farm ng XP, Gems, Streaks at i-unlock ang Duolingo Max nang libre.
// @description:el Το καλύτερο hack για το Duolingo - Κάντε farm XP, Πετράδια, Σειρές και ξεκλειδώστε το Duolingo Max δωρεάν.
// @description:hr Najbolji Duolingo hack - Farmajte XP, Dragulje, Nizove i otključajte Duolingo Max besplatno.
// @description:sk Najlepší hack na Duolingo - Farmujte XP, Drahokamy, Série a odomknite Duolingo Max zadarmo.
// @description:bg Най-добрият хак за Duolingo - Фармете XP, Скъпоценни камъни, Серии и отключете Duolingo Max безплатно.
// @description:sr Najbolji Duolingo hak - Farmujte XP, Dragulje, Serije i otključajte Duolingo Max besplatno.
// @description:lt Geriausias Duolingo įsilaužimas - Farminkite XP, Brangakmenius, Serijas ir atrakinkite Duolingo Max nemokamai.
// @description:lv Labākais Duolingo hack - Farmējiet XP, Dārgakmeņus, Sērijas un atbloķējiet Duolingo Max bez maksas.
// @description:et Parim Duolingo häkk - Farmige XP, Kalliskive, Seeriad ja avage Duolingo Max tasuta.
// @description:sl Najboljši Duolingo hack - Farmajte XP, Dragulji, Serije in odklenite Duolingo Max brezplačno.
// @description:ca El millor hack per a Duolingo - Fes farm de XP, Gemmes, Ratxes i desbloqueja Duolingo Max gratis.
// @description:af Die beste Duolingo-hack - Boer XP, Edelstene, Reekse en ontsluit Duolingo Max gratis.
// @description:sw Hack bora ya Duolingo - Fanya farm ya XP, Vito, Mifululizo na fungua Duolingo Max bila malipo.
// @description:zu I-hack engcono kakhulu ye-Duolingo - Fama i-XP, Amagugu, Izindawo futhi vula i-Duolingo Max mahhala.
// @description:mn Duolingo-н хамгийн шилдэг хак - XP, Эрдэнийн чулуу, Цуваа фарм хийж Duolingo Max-ийг үнэгүй нээ.
// @description:my Duolingo hack အကောင်းဆုံး - XP, Gems, Streaks farm လုပ်ပြီး Duolingo Max ကို အခမဲ့ ဖွင့်ပါ။
// @description:km ហេគ Duolingo ល្អបំផុត - ដាំ XP, Gems, Streaks និងដោះសោ Duolingo Max ដោយឥតគិតថ្លៃ។
// @description:lo ແຮັກ Duolingo ອັນດັບໜຶ່ງ - ຟາມ XP, ເພັດ, ສາຍຕໍ່ເນື່ອງ ແລະ ປົດລັອກ Duolingo Max ຟຣີ.
// @description:ur Duolingo کا بہترین ہیک - XP، Gems، Streaks فارم کریں اور Duolingo Max مفت میں انلاک کریں۔
// @author DuoHacker
// @match https://*.duolingo.com/*
// @match https://*.duolingo.cn/*
// @icon https://github.com/not2pixel/DuoHacker/blob/main/images/DuoHacker_Logo_NoBG_PNG.png?raw=true
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect duolingo.com
// @connect stories.duolingo.com
// @connect goals-api.duolingo.com
// @connect duolingo-leaderboards-prod.duolingo.com
// @connect raw.githubusercontent.com
// @connect avatars.githubusercontent.com
// @connect greasyfork.org
// @connect assets.duohacker.io.vn
// @connect font.duohacker.io.vn
// @compatible chrome Tested on Chrome 120+ with Tampermonkey
// @compatible firefox Tested on Firefox 120+ with Tampermonkey / Violentmonkey
// @compatible edge Tested on Edge 120+ with Tampermonkey
// @compatible opera Supported via Tampermonkey / Violentmonkey
// @compatible safari Supported via Userscripts app
// @compatible brave Supported via Tampermonkey
// @homepageURL https://duohacker.io.vn
// @supportURL https://duohacker.io.vn/discord
// @copyright 2026, DuoHacker (https://github.com/not2pixel)
// @license BY-NC-ND 4.0
// ==/UserScript==
(function() {
'use strict';
// ── i18n ──────────────────────────────────────────────────────────
var _I18N_KEY = 'dh2_lang';
var _lang = localStorage.getItem(_I18N_KEY) || 'vi';
var _isOutdated = false;
var _currentConnState = null;
var _LANGS = {
en: {
// ── Language selector ──
lang_select_label: 'Language',
lang_en: 'English',
lang_vi: 'Tiếng Việt',
// ── Top bar ──
hide: 'Hide',
show: 'Show',
switch_v1: 'Switch to V1',
switch_v2: 'Switch to V2',
// ── Connection status ──
connecting: 'Connecting',
connected: 'Connected',
error: 'Error',
outdated: 'Outdated',
// ── Page 1 (main) ──
donate: 'Donate',
xp_question: 'How much XP would you like to gain?',
gems_run_label: 'Click "RUN" to Farm Gems',
streak_question: 'How many Streak days to restore?',
extra_features: 'Extra Features',
// ── Page 2 (extra features) ──
back: 'Back',
farm_practice: 'Farm Practice',
farm_practice_sub: '0 = unlimited practice sessions',
shop_items: 'Shop Items',
auto_league: 'Auto League #1',
auto_league_sub: 'Farm XP until rank #1',
auto_daily_quest: 'Auto Daily Quest',
auto_daily_sub: 'Complete all daily quests',
claim_monthly: 'Claim Monthly Quest',
claim_monthly_sub: 'View & claim monthly quests',
free_super: 'Free Super Duolingo',
free_super_sub: 'Activate Super Duolingo for free',
// ── Page 4 (settings) ──
loop_delay: 'Loop delay (ms)',
free_duo_max: 'Free Duolingo Max',
free_duo_max_sub: 'Client-side only, reload to apply',
hide_profile: 'Hide Profile',
auto_solver: 'Auto Solver',
auto_solver_sub: 'Show Auto Solver buttons during lessons',
hide_animation: 'Hide Animation',
hide_animation_sub: 'Hide Duolingo images & animations',
lesson_shortener: 'Lesson Shortener',
lesson_shortener_sub: 'Replace lessons with 1 instant question',
stories_shortener: 'Story Skip Challenges',
stories_shortener_sub: 'Skip all challenges in stories',
view_credits: 'View Credits',
// ── Page 3 (shop) ──
search_placeholder: 'Search items...',
loading_shop: 'Loading shop...',
no_items_found: 'No items found.',
no_items_available: 'No items available.',
// ── Page 5 (account manager) ──
account_manager: 'Account Manager',
no_saved_accounts: 'No saved accounts.',
// ── Page 6 (monthly quests) ──
monthly_quests: 'Monthly Quests',
loading_quests: 'Loading quests...',
no_monthly_quests: 'No monthly quests found.',
quest_failed: 'Failed to load quests.',
// ── Page 7 (credits) ──
credits: 'Credits',
// ── Page V1 ──
xp_farming: 'XP Farming',
farm_gems: 'Farm Gems',
streak_farming: 'Streak Farming',
activate_super_q: 'Would you like to activate Free Super Duolingo?',
settings: 'Settings',
// ── Not connected ──
not_connected: 'Not connected.',
// ── Buttons (short labels) ──
btn_get: 'GET',
btn_run: 'RUN',
btn_stop: 'STOP',
btn_save: 'SAVE',
btn_saved: 'SAVED ✓',
btn_claim: 'CLAIM',
btn_activate: 'ACTIVATE',
btn_done: 'DONE ✓',
btn_save_current: 'SAVE CURRENT',
btn_solve_all: 'SOLVE ALL',
btn_pause: 'PAUSE',
btn_loading: 'Loading...',
btn_running: 'Running...',
btn_got: 'GOT ✓',
btn_failed: 'FAILED',
// ── Hide Profile status ──
profile_private: 'Profile is private',
profile_public: 'Profile is public',
status_unavailable: 'Unavailable',
status_saving: 'Saving\u2026',
status_failed_retry: 'Failed \u2014 try again',
status_not_connected: 'Not connected',
status_loading: 'Loading\u2026',
},
vi: {
// ── Language selector ──
lang_select_label: 'Ngôn ngữ',
lang_en: 'English',
lang_vi: 'Tiếng Việt',
// ── Top bar ──
hide: 'Ẩn',
show: 'Hiện',
switch_v1: 'Đổi thành V1',
switch_v2: 'Đổi thành V2',
// ── Connection status ──
connecting: 'Đang kết nối',
connected: 'Đã kết nối',
error: 'Lỗi',
outdated: 'Phiên bản cũ',
// ── Page 1 (main) ──
donate: 'Quyên góp',
xp_question: 'Số XP bạn muốn farm',
gems_run_label: 'Nhấn vào "CHẠY" để farm Gem',
streak_question: 'Số Streak bạn muốn khôi phục',
extra_features: 'Tính năng khác',
// ── Page 2 (extra features) ──
back: 'Quay lại',
farm_practice: 'Cày bài ôn tập',
farm_practice_sub: '0 = vô hạn luồng cày',
shop_items: 'Shop vật phẩm',
auto_league: 'Tự động cày bảng xếp hạng',
auto_league_sub: 'Cày XP đến khi đạt #1',
auto_daily_quest: 'Hoàn thành nhiệm vụ hằng ngày',
auto_daily_sub: 'Hoàn thành tất cả nhiệm vụ hằng ngày',
claim_monthly: 'Hack huy hiệu tháng',
claim_monthly_sub: 'Xem & nhận huy hiệu tháng',
free_super: 'Duolingo Super miễn phí',
free_super_sub: 'Kích hoạt Duolingo Super khoảng 3 ngày miễn phí',
// ── Page 4 (settings) ──
loop_delay: 'Độ trễ vòng',
free_duo_max: 'Duolingo Max miễn phí',
free_duo_max_sub: 'Chỉ hoạt động ở Web, làm mới trang để áp dụng',
hide_profile: 'Ẩn profile',
auto_solver: 'Tự động giải',
auto_solver_sub: 'Tự động giải bài trong lesson',
hide_animation: 'Ẩn hiệu ứng',
hide_animation_sub: 'Ẩn hình ảnh và hiệu ứng của Duolingo',
lesson_shortener: 'Rút gọn bài học',
lesson_shortener_sub: 'Thay thế các bài học bằng 1 câu hỏi',
stories_shortener: 'Skip toàn bộ câu hỏi Stories',
stories_shortener_sub: 'Bỏ qua tất cả câu hỏi trong stories',
view_credits: 'Xem credit',
// ── Page 3 (shop) ──
search_placeholder: 'Tìm kiếm vật phẩm...',
loading_shop: 'Đang tải shop...',
no_items_found: 'Không tìm thấy vật phẩm nào.',
no_items_available: 'Không có vật phẩm nào.',
// ── Page 5 (account manager) ──
account_manager: 'Quản lý tài khoản',
no_saved_accounts: 'Chưa có tài khoản nào được lưu.',
// ── Page 6 (monthly quests) ──
monthly_quests: 'Nhiệm vụ hằng tháng',
loading_quests: 'Đang tải nhiệm vụ...',
no_monthly_quests: 'Không tìm thấy nhiệm vụ tháng này.',
quest_failed: 'Tải nhiệm vụ thất bại.',
// ── Page 7 (credits) ──
credits: 'Credits',
// ── Page V1 ──
xp_farming: 'Cày XP',
farm_gems: 'Cày Gem',
streak_farming: 'Cày Streak',
activate_super_q: 'Bạn có muốn kích hoạt Super Duolingo miễn phí không?',
settings: 'Cài đặt',
// ── Not connected ──
not_connected: 'Chưa kết nối.',
// ── Buttons (short labels) ──
btn_get: 'LẤY',
btn_run: 'CHẠY',
btn_stop: 'DỪNG',
btn_save: 'LƯU',
btn_saved: 'ĐÃ LƯU ✓',
btn_claim: 'NHẬN',
btn_activate: 'KÍCH HOẠT',
btn_done: 'XONG ✓',
btn_save_current: 'LƯU HIỆN TẠI',
btn_solve_all: 'GIẢI HẾT',
btn_pause: 'TẠM DỪNG',
btn_loading: 'Đang tải...',
btn_running: 'Đang chạy...',
btn_got: 'ĐÃ LẤY ✓',
btn_failed: 'THẤT BẠI',
// ── Hide Profile status ──
profile_private: 'Hồ sơ đã ẩn',
profile_public: 'Hồ sơ công khai',
status_unavailable: 'Không khả dụng',
status_saving: 'Đang lưu…',
status_failed_retry: 'Lỗi — thử lại',
status_not_connected: 'Chưa kết nối',
status_loading: 'Đang tải…',
}
};
function _t(key) {
const d = _LANGS[_lang] || _LANGS.en;
const v = d[key];
// fallback to EN if VI value empty
return (v !== undefined && v !== '') ? v : (_LANGS.en[key] || key);
}
function _setLang(l) {
if (l !== 'vi' && l !== 'en') return;
_lang = l;
localStorage.setItem(_I18N_KEY, l);
_applyLang();
}
function _applyLang() {
// Static text nodes mapped by element id → translation key
const _ID_MAP = {
'DH_Hide_Txt': () => document.getElementById('DH_Ico_Hidden')?.style.display === 'none' ? 'hide' : 'show',
'DH_Conn_Txt': () => _currentConnState === 'connected' && _isOutdated ? 'outdated' : (_currentConnState || 'connecting'),
// Page 1
'DH_Donate_Btn_Lbl': 'donate',
'DH_XP_Question': 'xp_question',
'DH_Gem_Run_Label': 'gems_run_label',
'DH_Streak_Question': 'streak_question',
'DH_ExtraFeatures_Lbl': 'extra_features',
// Page 2
'DH_Back_Txt': 'back',
'DH_Practice_Title': 'farm_practice',
'DH_Practice_Sub': 'farm_practice_sub',
'DH_Shop_Lbl': 'shop_items',
'DH_League_Title': 'auto_league',
'DH_League_Sub': 'auto_league_sub',
'DH_Quest_Title': 'auto_daily_quest',
'DH_Quest_Sub': 'auto_daily_sub',
'DH_Monthly_Title': 'claim_monthly',
'DH_Monthly_Sub': 'claim_monthly_sub',
'DH_FreeSuper_Title': 'free_super',
'DH_FreeSuper_Sub': 'free_super_sub',
// Page 4
'DH_Settings_Back_Txt': 'back',
'DH_LoopDelay_Lbl': 'loop_delay',
'DH_FreeDuoMax_Lbl': 'free_duo_max',
'DH_FreeDuoMax_Sub': 'free_duo_max_sub',
'DH_HideProfile_Lbl': 'hide_profile',
'DH_AutoSolver_Lbl': 'auto_solver',
'DH_AutoSolver_Sub': 'auto_solver_sub',
'DH_HideAnim_Lbl': 'hide_animation',
'DH_HideAnim_Sub': 'hide_animation_sub',
'DH_LessonShortener_Lbl': 'lesson_shortener',
'DH_LessonShortener_Sub': 'lesson_shortener_sub',
'DH_StoriesShortener_Lbl': 'stories_shortener',
'DH_StoriesShortener_Sub': 'stories_shortener_sub',
'DH_Credits_Btn': 'view_credits',
// Page 9 (license) / Page 10 (changelog)
'DH_License_Back_Txt': 'back',
'DH_Changelog_Back_Txt': 'back',
// Page 3 shop back (now has id)
'DH_Shop_Back_Txt': 'back',
// Account / Monthly Quest back + static labels
'DH_AccMgr_Back_Txt': 'back',
'DH_AccMgr_Title': 'account_manager',
'DH_MQ_Back_Txt': 'back',
'DH_MQ_Title': 'monthly_quests',
// Page 7
'DH_Credits_Back_Txt': 'back',
'DH_Credits_Title': 'credits',
// Page V1
'DH_V1_XP_Title': 'xp_farming',
'DH_V1_Gem_Title': 'farm_gems',
'DH_V1_Streak_Title': 'streak_farming',
'DH_V1_Super_Q': 'activate_super_q',
'DH_V1_Settings_Lbl': 'settings',
'DH_V1_Settings_Back_Txt': 'back',
// Switch buttons
'DH_SwitchV1_Lbl': 'switch_v1',
'DH_SwitchV2_Lbl': 'switch_v2',
// Main action buttons
'DH_XP_Lbl': 'btn_get',
'DH_Gem_Lbl': 'btn_run',
'DH_Streak_Lbl': 'btn_run',
'DH_Practice_Lbl': 'btn_run',
'DH_League_Lbl': 'btn_run',
'DH_Quest_Lbl': 'btn_run',
'DH_MonthlyQuest_Claim_Lbl': 'btn_get',
'DH_Super_Activate_Lbl': 'btn_activate',
'DH_Delay_Lbl': 'btn_save',
// Account / Monthly Quest
'DH_AccSave_Lbl': 'btn_save_current',
'DH_MQ_ClaimAll_Lbl': 'btn_claim',
// V1 buttons
'DH_V1_XP_Lbl': 'btn_run',
'DH_V1_Gem_Lbl': 'btn_run',
'DH_V1_Streak_Lbl': 'btn_run',
'DH_V1_Super_Activate_Lbl': 'btn_activate',
// Static initial state text
'DH_AccNoSaved_Txt': 'no_saved_accounts',
'DH_ShopLoading_Txt': 'loading_shop',
};
for (const [id, key] of Object.entries(_ID_MAP)) {
const el = document.getElementById(id);
if (!el) continue;
if (typeof key === 'function') {
el.textContent = _t(key());
} else if (key) {
el.textContent = _t(key);
}
}
// Re-apply connection status text so it updates on lang switch
// try/catch because _currentConnState is declared later (let TDZ on first _applyLang call)
// Placeholder attributes
const ph = document.getElementById('DH_Shop_Search');
if (ph) ph.placeholder = _t('search_placeholder');
// Lang selector button label
const lb = document.getElementById('DH_LangSelector_Lbl');
if (lb) {
lb.innerHTML = _lang === 'vi' ?
`
<svg
width="20"
height="14"
viewBox="0 0 60 40"
style="border-radius:2px; vertical-align:middle; flex-shrink:0;"
aria-hidden="true"
>
<rect width="60" height="40" fill="#DA251D"></rect>
<polygon
points="
30,9
32.59,16.44
40.46,16.60
34.18,21.36
36.47,28.90
30,24.40
23.53,28.90
25.82,21.36
19.54,16.60
27.41,16.44
"
fill="#FFCD00"
></polygon>
</svg>
VI
` :
`
<svg
width="20"
height="14"
viewBox="0 0 60 40"
style="border-radius:2px; vertical-align:middle; flex-shrink:0;"
aria-hidden="true"
>
<rect width="60" height="40" fill="#012169"></rect>
<line
x1="0"
y1="0"
x2="60"
y2="40"
stroke="#fff"
stroke-width="6.5"
></line>
<line
x1="60"
y1="0"
x2="0"
y2="40"
stroke="#fff"
stroke-width="6.5"
></line>
<line
x1="0"
y1="0"
x2="60"
y2="40"
stroke="#C8102E"
stroke-width="4"
></line>
<line
x1="60"
y1="0"
x2="0"
y2="40"
stroke="#C8102E"
stroke-width="4"
></line>
<rect x="0" y="15" width="60" height="10" fill="#fff"></rect>
<rect x="25" y="0" width="10" height="40" fill="#fff"></rect>
<rect
x="0"
y="16.5"
width="60"
height="7"
fill="#C8102E"
></rect>
<rect
x="26.5"
y="0"
width="7"
height="40"
fill="#C8102E"
></rect>
</svg>
EN
`;
}
// Dropdown option labels
const opEn = document.getElementById('DH_LangOpt_en');
const opVi = document.getElementById('DH_LangOpt_vi');
if (opEn) opEn.innerHTML = '<svg width="20" height="14" viewBox="0 0 60 40" style="border-radius:2px;vertical-align:middle;flex-shrink:0;" aria-hidden="true"><rect width="60" height="40" fill="#012169"/><line x1="0" y1="0" x2="60" y2="40" stroke="#fff" stroke-width="6.5"/><line x1="60" y1="0" x2="0" y2="40" stroke="#fff" stroke-width="6.5"/><line x1="0" y1="0" x2="60" y2="40" stroke="#C8102E" stroke-width="4"/><line x1="60" y1="0" x2="0" y2="40" stroke="#C8102E" stroke-width="4"/><rect x="0" y="15" width="60" height="10" fill="#fff"/><rect x="25" y="0" width="10" height="40" fill="#fff"/><rect x="0" y="16.5" width="60" height="7" fill="#C8102E"/><rect x="26.5" y="0" width="7" height="40" fill="#C8102E"/></svg> ' + _t('lang_en');
if (opVi) opVi.innerHTML = '<svg width="20" height="14" viewBox="0 0 60 40" style="border-radius:2px;display:block;flex-shrink:0;" aria-hidden="true"><rect width="60" height="40" fill="#DA251D"></rect><polygon points="30,9 32.59,16.44 40.46,16.60 34.18,21.36 36.47,28.90 30,24.40 23.53,28.90 25.82,21.36 19.54,16.60 27.41,16.44" fill="#FFCD00"></polygon></svg><span>' + _t('lang_vi') + '</span>';
}
// ── End i18n ──────────────────────────────────────────────────────
// ── Unified fetch hook (Lesson Shortener + Stories Shortener + Free Max) ──
//
// ROOT CAUSE OF PREVIOUS CONFLICT:
// _installFakeSuper() injected a <script> into page context that replaced
// window.fetch AFTER _lsInstallHook() already wrapped unsafeWindow.fetch.
// Result: the page-context fetch (used by React/Duolingo) was the FakeSuper
// hook, which called the *original* fetch — completely bypassing the LS/SS
// hook that lived only in the sandbox layer.
//
// FIX: One single <script> injection handles ALL three features together.
// The sandbox LS hook still runs for the /2023-05-23/sessions POST (which
// goes through GM_xmlhttpRequest anyway), but the page-context hook covers
// the GET intercepts that React actually uses.
//
const _LS_KEY = 'dh2_lessonShortener';
const _SS_KEY = 'dh2_storiesShortener';
// ── Page-context injection: FakeSuper + Stories Shortener GET intercept ──
// Runs immediately at script load, before React captures fetch references.
(function _installPageContextHook() {
const lsKey = _LS_KEY; // closure — will be stringified below
const ssKey = _SS_KEY;
const superKey = 'dh2_super';
const script = document.createElement('script');
script.textContent = `(function() {
var LS_KEY = ${JSON.stringify(_LS_KEY)};
var SS_KEY = ${JSON.stringify(_SS_KEY)};
var SUPER_KEY = 'dh2_super';
var USER_RX = /https?:\\/\\/(?:[a-zA-Z0-9-]+\\.)?duolingo\\.[a-zA-Z]{2,6}(?:\\.[a-zA-Z]{2})?\\/\\d{4}-\\d{2}-\\d{2}\\/users\\/.+/;
var STORY_RX = /https?:\\/\\/stories\\.duolingo\\.com\\/api2\\/stories\\//;
var SESSION_PATH = '/2023-05-23/sessions';
var SUPER_ITEMS = {
gold_subscription: {
itemName: 'gold_subscription',
subscriptionInfo: {
vendor: 'STRIPE',
renewing: true,
isFamilyPlan: true,
expectedExpiration: 9999999999000
}
}
};
var STORY_CHALLENGES = new Set(['MULTIPLE_CHOICE','ARRANGE','MATCH','SELECT_PHRASE','POINT_TO_PHRASE','CHALLENGE_PROMPT']);
function modSuper(j) {
try {
var d = JSON.parse(j);
d.hasPlus = true;
if (!d.trackingProperties || typeof d.trackingProperties !== 'object') d.trackingProperties = {};
d.trackingProperties.has_item_gold_subscription = true;
d.shopItems = Object.assign({}, d.shopItems, SUPER_ITEMS);
return JSON.stringify(d);
} catch(e) { return j; }
}
function modStory(j) {
try {
var d = JSON.parse(j);
if (Array.isArray(d.elements)) {
d.elements = d.elements.filter(function(el) {
return !STORY_CHALLENGES.has(el.type);
});
}
return JSON.stringify(d);
} catch(e) { return j; }
}
function makeResponse(text, orig) {
var hdrs = {};
try { orig.headers.forEach(function(v,k){ hdrs[k]=v; }); } catch(e) {}
hdrs['content-type'] = 'application/json';
return new Response(text, {
status: orig.status,
statusText: orig.statusText,
headers: hdrs
});
}
// --- fetch hook ---
var origFetch = window.fetch;
window.__dhOrigFetch = origFetch;
window.fetch = function dhFetch(resource, options) {
var url = resource instanceof Request ? resource.url : String(resource || '');
var method = resource instanceof Request ? resource.method : ((options && options.method) || 'GET');
method = method.toUpperCase();
var wantsSuper = localStorage.getItem(SUPER_KEY) === 'true';
var wantsSS = localStorage.getItem(SS_KEY) === 'true';
var wantsLS = localStorage.getItem(LS_KEY) === 'true';
// Super: intercept GET /users/... responses
if (method === 'GET' && wantsSuper && USER_RX.test(url) && url.indexOf('/shop-items') === -1) {
return origFetch.apply(this, arguments).then(function(r) {
return r.clone().text().then(function(text) {
return makeResponse(modSuper(text), r);
});
});
}
// Stories Shortener: intercept GET stories API
if (method === 'GET' && wantsSS && STORY_RX.test(url)) {
return origFetch.apply(this, arguments).then(function(r) {
return r.clone().text().then(function(text) {
return makeResponse(modStory(text), r);
});
});
}
// Lesson Shortener: intercept POST /sessions (page-context path)
// Note: the sandbox hook below also handles this for direct GM calls.
// This covers any React-initiated session creation.
if (method === 'POST' && wantsLS && url.indexOf(SESSION_PATH) !== -1) {
return origFetch.apply(this, arguments).then(function(r) {
return r.clone().json().then(function(data) {
var n = (data.type && data.type.indexOf('LEGENDARY') === 0) ? 2 : 1;
var ll = (window._dhUser && window._dhUser.learningLanguage) || data.learningLanguage || 'en';
var fl = (window._dhUser && window._dhUser.fromLanguage) || data.fromLanguage || 'en';
var lsChallenge = {
character: {
url: 'https://d2pur3iezf4d1j.cloudfront.net/images/51d3bded9ecbd8bf6e9869041c437ba9',
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'
},
prompt: 'Thanks for using our tool 🥰',
correctIndex: 0,
options: [{ text: 'Our website : https://duohacker.io.vn' }],
type: 'assist',
id: 'duohacker',
newWords: [],
progressUpdates: [],
canBeSkipped: false,
metadata: {
learning_language: ll,
ui_language: fl,
from_language: fl,
type: 'assist',
specific_type: 'assist',
other_options: []
}
};
data.challenges = Array.from({ length: n }, function() { return lsChallenge; });
data.adaptiveChallenges = [];
data.adaptiveInterleavedChallenges = { challenges: [], speakOrListenReplacementIndices: [] };
return new Response(JSON.stringify(data), {
status: r.status,
statusText: r.statusText,
headers: { 'Content-Type': 'application/json' }
});
}).catch(function() { return r; });
});
}
return origFetch.apply(this, arguments);
};
window.fetch._isDH = true;
// --- XHR hook (fallback) ---
var origOpen = XMLHttpRequest.prototype.open;
var origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
var m = (method || '').toUpperCase();
this._dh_isUserGet = m === 'GET' && USER_RX.test(url) && url.indexOf('/shop-items') === -1;
this._dh_isStoryGet = m === 'GET' && STORY_RX.test(url);
origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
var xhr = this;
if (xhr._dh_isUserGet || xhr._dh_isStoryGet) {
var origChange = xhr.onreadystatechange;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
try {
var raw = xhr.responseText;
if (xhr._dh_isUserGet && localStorage.getItem(SUPER_KEY) === 'true') raw = modSuper(raw);
if (xhr._dh_isStoryGet && localStorage.getItem(SS_KEY) === 'true') raw = modStory(raw);
Object.defineProperty(xhr, 'responseText', { writable: true, configurable: true, value: raw });
Object.defineProperty(xhr, 'response', { writable: true, configurable: true, value: raw });
} catch(e) {}
}
if (origChange) origChange.apply(this, arguments);
};
}
origSend.apply(this, arguments);
};
})();`;
document.documentElement.appendChild(script);
script.remove();
})();
// ── Sandbox-layer hook: handles Lesson Shortener POST /sessions ────────
// (GM_xmlhttpRequest / direct fetch calls from userscript sandbox)
const _lsOriginalFetch = unsafeWindow.fetch;
let _lsRealFetch = _lsOriginalFetch;
function _lsInstallHook() {
const cur = unsafeWindow.fetch;
// Don't re-wrap our own hook; also skip if page-context already injected
if (!cur || cur._isDHLS) return;
_lsRealFetch = cur;
unsafeWindow.__dhRealFetch = cur;
const hooked = async function(resource, options) {
const url = resource instanceof Request ? resource.url : String(resource);
const parsedUrl = new URL(url, location.origin);
const method =
options?.method ||
(resource instanceof Request ? resource.method : 'GET');
const isCreateSession =
parsedUrl.pathname === '/2023-05-23/sessions' &&
method.toUpperCase() === 'POST';
if (isCreateSession && localStorage.getItem(_LS_KEY) === 'true') {
try {
const res = await _lsRealFetch.call(this, resource, options);
const data = await res.clone().json();
const n = data.type?.startsWith('LEGENDARY') ? 2 : 1;
const ll = unsafeWindow._dhUser?.learningLanguage || data.learningLanguage || 'en';
const fl = unsafeWindow._dhUser?.fromLanguage || data.fromLanguage || 'en';
const lsChallenge = {
character: {
url: 'https://d2pur3iezf4d1j.cloudfront.net/images/51d3bded9ecbd8bf6e9869041c437ba9',
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'
},
prompt: 'Thanks for using our tool 🥰',
correctIndex: 0,
options: [{
text: 'Our website : https://duohacker.io.vn'
}],
type: 'assist',
id: 'duohacker',
newWords: [],
progressUpdates: [],
canBeSkipped: false,
metadata: {
learning_language: ll,
ui_language: fl,
from_language: fl,
type: 'assist',
specific_type: 'assist',
other_options: []
}
};
data.challenges = Array.from({
length: n
}, () => lsChallenge);
data.adaptiveChallenges = [];
data.adaptiveInterleavedChallenges = {
challenges: [],
speakOrListenReplacementIndices: []
};
return new Response(JSON.stringify(data), {
status: res.status,
statusText: res.statusText,
headers: {
'Content-Type': 'application/json'
}
});
} catch (e) {}
}
// Stories Shortener — sandbox layer (fallback, page-context hook is primary)
const isStory =
parsedUrl.hostname === 'stories.duolingo.com' &&
parsedUrl.pathname.startsWith('/api2/stories/') &&
method.toUpperCase() === 'GET';
if (isStory && localStorage.getItem(_SS_KEY) === 'true') {
try {
const _storyFetch = unsafeWindow.__dhRealFetch || _lsRealFetch;
const res = await _storyFetch.call(this, resource, options);
const data = await res.clone().json();
if (Array.isArray(data.elements)) {
const CHALLENGES = new Set(['MULTIPLE_CHOICE', 'ARRANGE', 'MATCH', 'SELECT_PHRASE', 'POINT_TO_PHRASE', 'CHALLENGE_PROMPT']);
data.elements = data.elements.filter(el => !CHALLENGES.has(el.type));
}
return new Response(JSON.stringify(data), {
status: res.status,
statusText: res.statusText,
headers: {
'Content-Type': 'application/json'
}
});
} catch (e) {}
}
return (unsafeWindow.__dhRealFetch || _lsRealFetch).call(this, resource, options);
};
hooked._isDHLS = true;
unsafeWindow.fetch = hooked;
}
// Install hook immediately (fetch exists at document-end)
_lsInstallHook();
// Periodic re-check: Duolingo may replace fetch during Legendary sessions
setInterval(() => {
if (unsafeWindow.fetch && !unsafeWindow.fetch._isDHLS) _lsInstallHook();
}, 2000);
let _lsHref = location.href;
new MutationObserver(() => {
if (location.href === _lsHref) return;
_lsHref = location.href;
if (unsafeWindow.fetch && !unsafeWindow.fetch._isDHLS) _lsInstallHook();
}).observe(document.documentElement, {
subtree: true,
childList: true
});
// ──────────────────────────────────────────────────────────────────
GM_addStyle(`
@font-face {
font-family: "SF Pro Rounded";
src: url("https://font.duohacker.io.vn/SF-Pro-Rounded-Regular.otf") format("opentype");
font-weight: 400; font-display: swap;
}
@font-face {
font-family: "SF Pro Rounded";
src: url("https://font.duohacker.io.vn/SF-Pro-Rounded-Semibold.otf") format("opentype");
font-weight: 600; font-display: swap;
}
@font-face {
font-family: "SF Pro Rounded";
src: url("https://font.duohacker.io.vn/SF-Pro-Rounded-Bold.otf") format("opentype");
font-weight: 700; font-display: swap;
}
@font-face {
font-family: "SF Pro Rounded";
src: url("https://font.duohacker.io.vn/SF-Pro-Rounded-Heavy.otf") format("opentype");
font-weight: 800; font-display: swap;
}
@font-face {
font-family: "SF Pro Rounded";
src: url("https://font.duohacker.io.vn/SF-Pro-Rounded-Black.otf") format("opentype");
font-weight: 900; font-display: swap;
}
:root {
--DH-blue: 0, 122, 255;
--DH-green: 52, 199, 89;
--DH-red: 255, 59, 48;
--DH-orange: 255, 149, 0;
/* Corner system — giống PRO, nâng lên khi browser support superellipse */
--DH-corner-s: superellipse(1.32);
--DH-corner-r-s: 6px;
--DH-corner-r-m: 8px;
--DH-corner-r-ml: 12px;
--DH-corner-r-l: 16px;
--DH-corner-r-xl: 20px;
}
@media (prefers-color-scheme: dark) {
:root {
--DH-blue: 10, 132, 255;
--DH-green: 48, 209, 88;
--DH-red: 255, 69, 58;
}
}
#DH_Root * { box-sizing: border-box; }
#DH_Root p, #DH_Root span, #DH_Root button, #DH_Root input, #DH_Root label, #DH_Root div {
font-family: 'SF Pro Rounded', 'din-round', -apple-system, sans-serif !important;
}
#DH_Root p, #DH_Root span { margin: 0; padding: 0; }
#DH_Root svg { flex-shrink: 0; }
.DH_Main {
display: inline-flex; flex-direction: column;
justify-content: flex-end; align-items: flex-end;
gap: 8px; position: fixed; right: 16px; bottom: 16px;
z-index: 2147483647;
}
@media (max-width: 699px) {
.DH_Main { margin-bottom: 80px; }
}
.DH_Main_Box {
display: flex; width: 312px; padding: 16px;
box-sizing: border-box;
flex-direction: column; justify-content: center; align-items: center; gap: 8px;
overflow: hidden; border-radius: 20px;
outline: 2px solid rgb(var(--color-eel, 117,117,117), 0.10); outline-offset: -2px;
background: rgb(var(--color-snow), 0.90);
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.DH_HStack_Auto { display:flex; align-items:center; justify-content:space-between; align-self:stretch; }
.DH_HStack_8 { display:flex; align-items:center; gap:8px; align-self:stretch; }
.DH_HStack_4 { display:flex; align-items:center; gap:4px; align-self:stretch; }
.DH_VStack_8 { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:8px; align-self:stretch; }
.DH_VStack_4 { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:4px; align-self:stretch; }
.DH_NoSel { user-select:none; -webkit-user-select:none; }
.DH_Divider { align-self:stretch; height:1px; background:rgb(var(--color-eel,117,117,117), 0.10); flex-shrink:0; }
.DH_T1 { font-size:15px; font-weight:700; line-height:normal; color:rgb(var(--color-wolf,60,60,67), 0.80); margin:0; }
.DH_T2 { font-size:13px; font-weight:600; line-height:normal; color:rgb(var(--color-wolf,60,60,67), 0.50); margin:0; }
.DH_Btn {
display:flex; height:40px; padding:10px 12px 10px 10px; box-sizing:border-box;
align-items:center; gap:6px; flex:1 0 0;
border-radius:8px; border:none; cursor:pointer;
user-select:none; -webkit-user-select:none;
transition: filter 0.4s cubic-bezier(0.16,1,0.32,1), transform 0.4s cubic-bezier(0.16,1,0.32,1);
}
.DH_Btn:hover { filter:brightness(0.88); transform:scale(1.04); }
.DH_Btn:active { filter:brightness(0.82); transform:scale(0.96); }
.DH_Btn_Blue_Ghost {
outline:2px solid rgba(var(--DH-blue),0.20); outline-offset:-2px;
background:linear-gradient(0deg,rgba(var(--DH-blue),0.10),rgba(var(--DH-blue),0.10)),rgb(var(--color-snow),0.80);
backdrop-filter:blur(16px);
}
.DH_Btn_Eel {
outline:2px solid rgb(var(--color-eel,117,117,117),0.20); outline-offset:-2px;
background:rgb(var(--color-eel,117,117,117),0.10);
}
.DH_Btn_Icon { flex:none!important; width:40px; padding:10px!important; justify-content:center; }
.DH_Input_Wrap {
display:flex; height:48px; padding:16px; box-sizing:border-box;
align-items:center; flex:1 0 0; gap:6px;
border-radius:8px;
outline:2px solid rgba(var(--DH-blue),0.20); outline-offset:-2px;
background:rgba(var(--DH-blue),0.10);
position:relative; overflow:hidden;
}
.DH_Input {
border:none!important; outline:none!important; background:none!important;
text-align:right; font-size:16px!important; font-weight:600!important;
color:rgb(var(--DH-blue))!important; font-family:inherit!important; width:100%;
-moz-appearance:textfield;
}
.DH_Input::placeholder { color:rgba(var(--DH-blue),0.50)!important; }
.DH_Input::-webkit-outer-spin-button,
.DH_Input::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
.DH_Input_Btn {
display:flex; height:48px; padding:12px 14px; box-sizing:border-box;
justify-content:center; align-items:center;
border-radius:8px; border:none; cursor:pointer;
user-select:none; -webkit-user-select:none;
outline:2px solid rgba(0,0,0,0.20); outline-offset:-2px;
background:rgb(var(--DH-blue));
white-space:nowrap; flex-shrink:0;
transition:
width 0.8s cubic-bezier(0.77,0,0.18,1),
background 0.8s cubic-bezier(0.16,1,0.32,1),
outline 0.8s cubic-bezier(0.16,1,0.32,1),
filter 0.4s cubic-bezier(0.16,1,0.32,1),
transform 0.4s cubic-bezier(0.16,1,0.32,1);
}
.DH_Input_Btn:hover { filter:brightness(0.88); transform:scale(1.04); }
.DH_Input_Btn:active { filter:brightness(0.82); transform:scale(0.96); }
.DH_Input_Btn:disabled { opacity:0.38; pointer-events:none; }
.DH_Btn_Label {
font-size:15px; font-weight:800; letter-spacing:0.2px;
transition:opacity 0.4s, filter 0.4s, color 0.4s;
}
.DH_Sm_Btn {
display:flex; height:38px; padding:0 16px;
justify-content:center; align-items:center;
border-radius:8px; border:none; cursor:pointer;
user-select:none; flex-shrink:0;
outline:2px solid rgba(0,0,0,0.20); outline-offset:-2px;
background:rgb(var(--DH-blue));
white-space:nowrap;
transition:
width 0.8s cubic-bezier(0.77,0,0.18,1),
background 0.8s cubic-bezier(0.16,1,0.32,1),
outline 0.8s cubic-bezier(0.16,1,0.32,1),
filter 0.4s cubic-bezier(0.16,1,0.32,1),
transform 0.4s cubic-bezier(0.16,1,0.32,1);
}
.DH_Sm_Btn:hover { filter:brightness(0.88); transform:scale(1.04); }
.DH_Sm_Btn:active { filter:brightness(0.82); transform:scale(0.96); }
.DH_Sm_Btn:disabled { opacity:0.38; pointer-events:none; }
.DH_Sm_Btn_Label { font-size:13px; font-weight:800; transition:opacity 0.4s, filter 0.4s, color 0.4s; }
.DH_Prog_Wrap {
align-self:stretch; height:0; border-radius:3px;
background:rgba(var(--DH-blue),0.10); overflow:hidden;
transition:height 0.4s cubic-bezier(0.16,1,0.32,1);
}
.DH_Prog_Wrap.on { height:4px; }
.DH_Prog_Fill {
height:100%; border-radius:3px; background:rgb(var(--DH-blue));
width:0%; transition:width 0.5s cubic-bezier(0.16,1,0.32,1);
box-shadow:0 0 6px rgba(var(--DH-blue),0.35);
}
.DH_Avatar {
width:32px; height:32px; border-radius:50%;
background:rgba(var(--DH-blue),0.10);
overflow:hidden; flex-shrink:0;
display:flex; align-items:center; justify-content:center; font-size:16px;
}
.DH_Stat_Ico { width:15px; height:15px; display:block; flex-shrink:0; }
.DH_Stat_Val { font-size:13px!important; font-weight:700!important; color:rgb(var(--color-wolf,60,60,67),0.60)!important; }
.DH_Toggle { position:relative; width:44px; height:26px; flex-shrink:0; cursor:pointer; }
.DH_Toggle input { opacity:0; width:0; height:0; }
.DH_Toggle_Slider {
position:absolute; cursor:pointer; inset:0;
background:rgb(var(--color-eel,117,117,117),0.22); border-radius:26px; transition:background 0.3s;
}
.DH_Toggle_Slider:before {
content:''; position:absolute; width:20px; height:20px; left:3px; bottom:3px;
background:#fff; border-radius:50%; transition:transform 0.3s; box-shadow:0 2px 4px rgba(0,0,0,0.2);
}
.DH_Toggle input:checked + .DH_Toggle_Slider { background:rgb(var(--DH-blue)); }
.DH_Toggle input:checked + .DH_Toggle_Slider:before { transform:translateX(18px); }
.DH_Shimmer {
position:absolute; pointer-events:none; top:0; left:0;
transform:translateX(-150px);
animation:DH_Shimmer 5s ease-in-out infinite 1.5s;
}
@keyframes DH_Shimmer {
0% { transform:translateX(-150px); }
18% { transform:translateX(340px); }
100%{ transform:translateX(340px); }
}
@keyframes DH_Spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
.DH_Spin_Ico { animation:DH_Spin 1.5s linear infinite; display:inline-block; }
.DH_Page { display:none; }
.DH_Page.active { display:flex; flex-direction:column; gap:8px; align-self:stretch; align-items:center; }
.DH_Notif_Main {
display:flex; justify-content:center; align-items:center;
width:300px; position:fixed; left:calc(50% - 150px);
z-index:2147483647; bottom:16px; border-radius:16px;
transition:0.8s cubic-bezier(0.16,1,0.32,1); pointer-events:none;
}
.DH_Notif_Box {
display:flex; width:300px; padding:14px 16px;
flex-direction:column; gap:3px;
border-radius:16px;
outline:2px solid rgb(var(--color-eel,117,117,117),0.10); outline-offset:-2px;
background:rgb(var(--color-snow),0.90);
backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
box-shadow:0 8px 32px rgba(0,0,0,0.12);
transition:0.8s cubic-bezier(0.16,1,0.32,1); filter:blur(16px); opacity:0;
pointer-events:auto;
}
.DH_Notif_Box.show { filter:blur(0px); opacity:1; }
.DH_Shop_Grid { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; align-self:stretch; }
.DH_Shop_Card {
display:flex; flex-direction:column; align-items:center;
gap:6px; padding:12px 8px; border-radius:12px;
outline:1.5px solid rgb(var(--color-eel,117,117,117),0.13); outline-offset:-1px;
background:rgb(var(--color-eel,117,117,117),0.05);
transition:outline-color 0.2s, background 0.2s; text-align:center;
}
.DH_Shop_Card:hover { outline-color:rgba(var(--DH-blue),0.30); background:rgba(var(--DH-blue),0.04); }
.DH_Shop_Ico { width:36px; height:36px; object-fit:contain; }
.DH_Shop_Name { font-size:11px; font-weight:700; color:rgb(var(--color-wolf,60,60,67),0.75); line-height:1.3; flex:1; }
.DH_Shop_Btn {
width:100%; height:28px; border-radius:7px; border:none; cursor:pointer;
font-size:11px; font-weight:800; color:#fff;
background:rgb(var(--DH-blue));
outline:2px solid rgba(0,0,0,0.20); outline-offset:-2px;
transition:filter 0.4s cubic-bezier(0.16,1,0.32,1), transform 0.4s cubic-bezier(0.16,1,0.32,1), background 0.4s;
}
.DH_Shop_Btn:hover { filter:brightness(0.88); transform:scale(1.03); }
.DH_Shop_Btn:active { transform:scale(0.97); }
.DH_Shop_Btn.loading { background:rgb(var(--color-eel,117,117,117),0.12); color:rgb(var(--color-eel,117,117,117),0.50); outline-color:rgb(var(--color-eel,117,117,117),0.18); pointer-events:none; }
.DH_Shop_Btn.got { background:rgba(var(--DH-green),0.12); color:rgb(var(--DH-green)); outline-color:rgba(var(--DH-green),0.25); pointer-events:none; }
.DH_Shop_Btn.fail { background:rgba(var(--DH-red),0.10); color:rgb(var(--DH-red)); outline-color:rgba(var(--DH-red),0.22); pointer-events:none; }
.DH_Cat_Header {
align-self:stretch; font-size:11px; font-weight:800; text-transform:uppercase;
letter-spacing:0.6px; color:rgb(var(--color-wolf,60,60,67),0.40);
padding:4px 0 2px; text-align:center;
border-bottom:1px solid rgb(var(--color-eel,117,117,117),0.10); margin-bottom:2px;
}
.DH_Scroll_Inner {
align-self:stretch; overflow-y:auto;
max-height:220px; display:flex; flex-direction:column; gap:6px; padding-right:2px;
}
.DH_Scroll_Inner::-webkit-scrollbar { width:3px; }
.DH_Scroll_Inner::-webkit-scrollbar-thumb { background:rgba(var(--DH-blue),0.2); border-radius:3px; }
.DH_Scroll_Inner { scrollbar-width:thin; scrollbar-color:rgba(var(--DH-blue),0.2) transparent; }
.DH_Credit_Card { display:flex; flex-direction:column; gap:6px; padding:10px 12px; border-radius:12px; background:rgba(var(--DH-blue),0.06); outline:1.5px solid rgba(var(--DH-blue),0.12); outline-offset:-1.5px; align-self:stretch; }
.DH_Credit_Card_Header { display:flex; align-items:center; gap:8px; }
.DH_Credit_Thumb { width:28px; height:28px; border-radius:8px; object-fit:cover; flex-shrink:0; background:rgba(var(--DH-blue),0.1); }
.DH_Credit_Script { font-size:13px; font-weight:800; color:rgb(var(--color-wolf,60,60,67)); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.DH_Credit_Author { font-size:11px; color:rgba(var(--color-wolf,60,60,67),0.55); }
.DH_Credit_Task { font-size:11px; color:rgba(var(--color-wolf,60,60,67),0.7); line-height:1.4; }
.DH_Credit_Link { font-size:11px; font-weight:700; color:rgb(var(--DH-blue)); text-decoration:none; }
.DH_Credit_Link:hover { opacity:0.75; }
.DH_Search {
align-self:stretch; height:40px; padding:0 14px;
border-radius:8px; border:none;
outline:2px solid rgb(var(--color-eel,117,117,117),0.15); outline-offset:-2px;
background:rgb(var(--color-eel,117,117,117),0.06);
font-size:14px; font-weight:600;
color:rgb(var(--color-wolf,60,60,67),0.80); transition:outline-color 0.2s;
}
.DH_Search:focus { outline-color:rgba(var(--DH-blue),0.35); }
.DH_Search::placeholder { color:rgb(var(--color-wolf,60,60,67),0.35); }
.DH_Btn_Ico {
width:28px; height:28px; border-radius:7px; flex-shrink:0;
background:rgba(var(--DH-blue),0.12);
display:flex; align-items:center; justify-content:center;
}
.DH_Acc_Card {
display:flex; align-items:center; gap:10px; align-self:stretch;
padding:10px 12px; border-radius:12px;
outline:1.5px solid rgb(var(--color-eel,117,117,117),0.13); outline-offset:-1px;
background:rgb(var(--color-eel,117,117,117),0.05);
transition:outline-color 0.2s, background 0.2s;
position:relative; overflow:hidden;
}
.DH_Acc_Card:hover { outline-color:rgba(var(--DH-blue),0.30); background:rgba(var(--DH-blue),0.04); }
.DH_Acc_Avatar {
width:36px; height:36px; border-radius:50%; flex-shrink:0;
background:rgba(var(--DH-blue),0.10); overflow:hidden;
display:flex; align-items:center; justify-content:center; font-size:16px;
}
.DH_Acc_Info { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.DH_Acc_Name { font-size:13px!important; font-weight:700!important; color:rgb(var(--color-wolf,60,60,67),0.85)!important; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.DH_Acc_Sub { font-size:11px!important; font-weight:600!important; color:rgb(var(--color-wolf,60,60,67),0.45)!important; }
.DH_Acc_Sub.active { color:rgb(var(--DH-green))!important; display:flex; align-items:center; gap:4px; }
.DH_Acc_Action_Row { display:flex; gap:5px; flex-shrink:0; }
.DH_Acc_Btn {
height:26px; padding:0 8px; border-radius:7px; border:none; cursor:pointer;
font-size:10px; font-weight:800; color:#fff;
background:rgb(var(--DH-blue));
outline:2px solid rgba(0,0,0,0.15); outline-offset:-2px;
transition:filter 0.3s,transform 0.3s;
}
.DH_Acc_Btn:hover { filter:brightness(0.88); transform:scale(1.06); }
.DH_Acc_Btn:active { transform:scale(0.95); }
.DH_Acc_Btn.del { background:rgba(var(--DH-red),0.10); color:rgb(var(--DH-red)); outline-color:rgba(var(--DH-red),0.20); }
.DH_Acc_Btn.del:hover { background:rgb(var(--DH-red)); color:#fff; }
.DH_Quest_Item {
display:flex; align-items:center; gap:10px; align-self:stretch;
padding:10px 12px; border-radius:12px;
outline:1.5px solid rgb(var(--color-eel,117,117,117),0.13); outline-offset:-1px;
background:rgb(var(--color-eel,117,117,117),0.05);
transition:outline-color 0.2s, background 0.2s;
}
.DH_Quest_Item.done { outline-color:rgba(var(--DH-green),0.25); background:rgba(var(--DH-green),0.04); }
.DH_Quest_Icon { width:40px; height:40px; object-fit:contain; flex-shrink:0; }
.DH_Quest_Info { flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
.DH_Quest_Title { font-size:12px!important; font-weight:700!important; color:rgb(var(--color-wolf,60,60,67),0.85)!important; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.DH_Quest_Meta { font-size:10px!important; font-weight:600!important; color:rgb(var(--color-wolf,60,60,67),0.45)!important; }
.DH_Quest_Bar_Bg { height:4px; border-radius:2px; background:rgba(var(--DH-blue),0.10); overflow:hidden; align-self:stretch; }
.DH_Quest_Bar_Fill { height:100%; background:rgb(var(--DH-blue)); border-radius:2px; transition:width 0.5s; }
.DH_Quest_Item.done .DH_Quest_Bar_Fill { background:rgb(var(--DH-green)); }
.DH_Quest_Get_Btn {
height:26px; padding:0 8px; flex-shrink:0; border-radius:7px; border:none; cursor:pointer;
font-size:10px; font-weight:800; white-space:nowrap;
background:rgb(var(--DH-blue)); color:#fff;
outline:2px solid rgba(0,0,0,0.15); outline-offset:-2px;
transition:filter 0.3s,transform 0.3s;
}
.DH_Quest_Get_Btn:hover { filter:brightness(0.88); transform:scale(1.06); }
.DH_Quest_Get_Btn:active { transform:scale(0.95); }
.DH_Quest_Get_Btn.done { background:rgba(var(--DH-green),0.10); color:rgb(var(--DH-green)); outline-color:rgba(var(--DH-green),0.20); pointer-events:none; }
#DH_AccSettings_Btn { transform-origin: right center; }
#DH_AccSettings_Btn:hover { filter:brightness(0.85); transform:scale(1.1); }
#DH_AccSettings_Btn:active { transform:scale(0.92); }
/* PAGE 9: License */
#DH_Page_9 { flex:1; min-height:0; }
.DH_License_Scroll {
flex:1;
min-height:0;
overflow-y:auto;
overflow-x:hidden;
padding:14px 16px;
border-radius:12px;
outline:2px solid rgba(var(--color-eel,117,117,117),0.10);
outline-offset:-2px;
background:rgba(var(--color-eel,117,117,117),0.04);
align-self:stretch;
max-height:320px;
}
.DH_License_Scroll::-webkit-scrollbar { width:3px; }
.DH_License_Scroll::-webkit-scrollbar-thumb { background:rgba(var(--color-eel,117,117,117),0.20); border-radius:2px; }
#DH_License_Text {
font-size:11.5px;
font-weight:600;
line-height:1.65;
color:rgb(var(--color-wolf,60,60,67),0.75);
white-space:pre-wrap;
margin:0;
word-break:break-word;
}
@media (max-width:699px) {
.DH_License_Scroll { max-height:calc(100svh - 240px); }
.DH_Main { margin-bottom:80px; }
}
.DH_LangSelector {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 2147483647;
}
.DH_LangSelector_Btn {
height: 38px;
padding: 0 14px;
display: flex;
align-items: center;
gap: 7px;
border: none;
border-radius: 10px;
cursor: pointer;
background: rgb(var(--color-snow), 0.92);
outline: 2px solid rgba(var(--DH-blue), 0.20);
outline-offset: -2px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
color: rgb(var(--DH-blue));
font-size: 13px;
font-weight: 800;
transition: filter 0.2s, transform 0.2s;
}
.DH_LangSelector_Btn:hover {
filter: brightness(0.92);
transform: scale(1.03);
}
.DH_LangSelector_Arrow {
transition: transform 0.2s;
}
.DH_LangSelector.open .DH_LangSelector_Arrow {
transform: rotate(180deg);
}
.DH_LangMenu {
position: absolute;
top: calc(100% + 7px);
left: 50%;
transform: translateX(-50%) translateY(-5px);
min-width: 150px;
padding: 6px;
display: flex;
flex-direction: column;
gap: 3px;
border-radius: 12px;
background: rgb(var(--color-snow), 0.96);
outline: 2px solid rgb(var(--color-eel, 117, 117, 117), 0.12);
outline-offset: -2px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
}
.DH_LangSelector.open .DH_LangMenu {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
.DH_LangOption {
width: 100%;
height: 36px;
padding: 0 11px;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 8px !important;
text-align: left;
white-space: nowrap;
line-height: 1;
background: transparent;
color: rgb(var(--color-wolf, 60, 60, 67), 0.82);
font-size: 13px;
font-weight: 700;
}
.DH_LangOption > svg {
display: block !important;
width: 20px;
height: 14px;
flex: 0 0 20px;
}
.DH_LangOption > span {
display: inline-block !important;
line-height: 14px;
}
.DH_LangOption:hover {
background: rgba(var(--DH-blue), 0.10);
color: rgb(var(--DH-blue));
}
`);
// Mount UI only after DOM is ready
function _dhMountUI() {
if (!document.body) {
setTimeout(_dhMountUI, 10);
return;
}
const _wrap = document.createElement('div');
_wrap.id = 'DH_Root';
_wrap.innerHTML = `
<div class="DH_LangSelector" id="DH_LangSelector">
<button
type="button"
class="DH_LangSelector_Btn DH_NoSel"
id="DH_LangSelector_Btn"
>
<span
id="DH_LangSelector_Lbl"
style="display:inline-flex;align-items:center;gap:5px;"
>
<svg
width="20"
height="14"
viewBox="0 0 60 40"
style="border-radius:2px;flex-shrink:0;"
aria-hidden="true"
focusable="false"
>
<rect
width="60"
height="40"
fill="#DA251D"
></rect>
<polygon
points="30,9 32.59,16.44 40.46,16.60 34.18,21.36 36.47,28.90 30,24.40 23.53,28.90 25.82,21.36 19.54,16.60 27.41,16.44"
fill="#FFCD00"
></polygon>
</svg>
<span>VI</span>
</span>
<svg
class="DH_LangSelector_Arrow"
width="10"
height="6"
viewBox="0 0 10 6"
fill="none"
aria-hidden="true"
>
<path
d="M1 1L5 5L9 1"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
</button>
<div class="DH_LangMenu" id="DH_LangMenu">
<button
type="button"
class="DH_LangOption DH_NoSel"
id="DH_LangOpt_vi"
data-lang="vi"
>
Tiếng Việt
</button>
<button
type="button"
class="DH_LangOption DH_NoSel"
id="DH_LangOpt_en"
data-lang="en"
>
English
</button>
</div>
</div>
<div class="DH_Notif_Main" id="DH_Notif_Main">
<div class="DH_Notif_Box" id="DH_Notif_Box">
<div class="DH_HStack_4" style="align-items:center;">
<p class="DH_T1 DH_NoSel" id="DH_Notif_Icon" style="font-size:18px;flex-shrink:0;"></p>
<p class="DH_T1 DH_NoSel" id="DH_Notif_Title" style="flex:1 0 0;"></p>
</div>
<p class="DH_T2 DH_NoSel" id="DH_Notif_Body" style="align-self:stretch;overflow-wrap:break-word;"></p>
</div>
</div>
<div class="DH_Main" id="DH_Main">
<div class="DH_HStack_8" style="align-self:flex-end;">
<div class="DH_Btn DH_Btn_Blue_Ghost DH_NoSel" id="DH_SwitchV1_Btn"
style="flex:none; display:none; order:-1;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 1l4 4-4 4" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11V9a4 4 0 0 1 4-4h14" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 23l-4-4 4-4" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 13v2a4 4 0 0 1-4 4H3" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<p class="DH_T1 DH_NoSel" id="DH_SwitchV1_Lbl" style="color:rgb(var(--DH-blue));font-size:13px;">Switch to V1</p>
</div>
<div class="DH_Btn DH_Btn_Blue_Ghost DH_NoSel" id="DH_SwitchV2_Btn"
style="flex:none; display:none; order:-1;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 1l4 4-4 4" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11V9a4 4 0 0 1 4-4h14" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 23l-4-4 4-4" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 13v2a4 4 0 0 1-4 4H3" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<p class="DH_T1 DH_NoSel" id="DH_SwitchV2_Lbl" style="color:rgb(var(--DH-blue));font-size:13px;">Switch to V2</p>
</div>
<div class="DH_Btn DH_NoSel" id="DH_Hide_Btn"
style="flex:none; outline:2px solid rgba(0,0,0,0.20); outline-offset:-2px; background:rgb(var(--DH-blue)); backdrop-filter:blur(16px);">
<svg id="DH_Ico_Visible" width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 0C5 0 1.73 2.5 0 6c1.73 3.5 5 6 9 6s7.27-2.5 9-6c-1.73-3.5-5-6-9-6zm0 10a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6.4a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8z" fill="#FFF"/>
<path d="M1 1l16 10" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<svg id="DH_Ico_Hidden" width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:none;">
<path d="M9 0C5 0 1.73 2.5 0 6c1.73 3.5 5 6 9 6s7.27-2.5 9-6c-1.73-3.5-5-6-9-6zm0 10a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6.4a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8z" fill="rgb(var(--DH-blue))"/>
</svg>
<p class="DH_T1 DH_NoSel" id="DH_Hide_Txt" style="color:#fff;">Hide</p>
</div>
</div>
<div class="DH_Main_Box" id="DH_Main_Box">
<div class="DH_Page active" id="DH_Page_1">
<!-- Row 1: Connection btn + Settings icon -->
<div class="DH_HStack_8">
<div class="DH_Btn DH_Btn_Eel DH_NoSel" id="DH_Conn_Btn" style="padding:10px 0 10px 10px; transition:background 0.8s cubic-bezier(0.16,1,0.32,1), outline 0.8s cubic-bezier(0.16,1,0.32,1), filter 0.4s cubic-bezier(0.16,1,0.32,1), transform 0.4s cubic-bezier(0.16,1,0.32,1);">
<span id="DH_Conn_Ico" style="display:flex;align-items:center;justify-content:center;flex-shrink:0;"><svg class="DH_Spin_Ico" width="16" height="16" viewBox="0 0 297 297" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M294.853 178.121c-1.814-1.671-4.404-2.203-6.729-1.382l-25.97 9.168c17.558-70.878-24.569-143.07-94.69-161.569-34.475-9.094-70.423-4.215-101.225 13.737C35.438 56.028 13.475 84.901 4.397 119.377c-8.083 30.698-4.951 63.329 8.819 91.883 13.616 28.234 36.767 50.846 65.186 63.669 3.256 1.47 6.737 2.203 10.214 2.203 3.647 0 7.293-.807 10.672-2.418 6.59-3.141 11.435-8.99 13.293-16.047 3.086-11.721-2.756-23.89-13.893-28.937-37.335-16.923-56.844-58.027-46.387-97.737 5.7-21.65 19.508-39.794 38.878-51.089 19.372-11.295 41.963-14.378 63.611-8.675 44.273 11.659 70.99 56.813 60.113 101.108l-17.584-20.48c-1.612-1.878-4.135-2.705-6.544-2.152-2.412.555-4.318 2.401-4.948 4.794l-11.478 43.588c-.566 2.153-.02 4.447 1.457 6.112l40.428 45.603c1.785 2.012 4.604 2.752 7.144 1.882l57.644-19.78c2.106-.723 3.711-2.45 4.279-4.603l11.479-43.587c.635-2.412-.106-4.949-1.92-6.62z" fill="currentColor"/></svg></span>
<p class="DH_T1 DH_NoSel" id="DH_Conn_Txt" style="color:rgb(var(--color-eel,117,117,117),0.70);">Connecting</p>
</div>
<div class="DH_Btn DH_Btn_Icon DH_NoSel" id="DH_TopSettings_Btn" style="outline:2px solid rgba(var(--DH-blue),0.20);outline-offset:-2px;background:linear-gradient(0deg,rgba(var(--DH-blue),0.10),rgba(var(--DH-blue),0.10)),rgb(var(--color-snow),0.80);backdrop-filter:blur(16px);" title="Settings">
<svg width="18" height="18" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.1,11c-3.9,0-7,3.1-7,7s3.1,7,7,7c3.9,0,7-3.1,7-7S22,11,18.1,11z M18.1,23c-2.8,0-5-2.2-5-5s2.2-5,5-5c2.8,0,5,2.2,5,5S20.9,23,18.1,23z" fill="rgb(var(--DH-blue))" stroke="rgb(var(--DH-blue))" stroke-width="1.2" stroke-linejoin="round" paint-order="stroke fill"/>
<path d="M32.8,14.7L30,13.8l-0.6-1.5l1.4-2.6c0.3-0.6,0.2-1.4-0.3-1.9l-2.4-2.4c-0.5-0.5-1.3-0.6-1.9-0.3l-2.6,1.4l-1.5-0.6l-0.9-2.8C21,2.5,20.4,2,19.7,2h-3.4c-0.7,0-1.3,0.5-1.4,1.2L14,6c-0.6,0.1-1.1,0.3-1.6,0.6L9.8,5.2C9.2,4.9,8.4,5,7.9,5.5L5.5,7.9C5,8.4,4.9,9.2,5.2,9.8l1.3,2.5c-0.2,0.5-0.4,1.1-0.6,1.6l-2.8,0.9C2.5,15,2,15.6,2,16.3v3.4c0,0.7,0.5,1.3,1.2,1.5L6,22.1l0.6,1.5l-1.4,2.6c-0.3,0.6-0.2,1.4,0.3,1.9l2.4,2.4c0.5,0.5,1.3,0.6,1.9,0.3l2.6-1.4l1.5,0.6l0.9,2.9c0.2,0.6,0.8,1.1,1.5,1.1h3.4c0.7,0,1.3-0.5,1.5-1.1l0.9-2.9l1.5-0.6l2.6,1.4c0.6,0.3,1.4,0.2,1.9-0.3l2.4-2.4c0.5-0.5,0.6-1.3,0.3-1.9l-1.4-2.6l0.6-1.5l2.9-0.9c0.6-0.2,1.1-0.8,1.1-1.5v-3.4C34,15.6,33.5,14.9,32.8,14.7z M32,19.4l-3.6,1.1L28.3,21c-0.3,0.7-0.6,1.4-0.9,2.1l-0.3,0.5l1.8,3.3l-2,2l-3.3-1.8l-0.5,0.3c-0.7,0.4-1.4,0.7-2.1,0.9l-0.5,0.1L19.4,32h-2.8l-1.1-3.6L15,28.3c-0.7-0.3-1.4-0.6-2.1-0.9l-0.5-0.3l-3.3,1.8l-2-2l1.8-3.3l-0.3-0.5c-0.4-0.7-0.7-1.4-0.9-2.1l-0.1-0.5L4,19.4v-2.8l3.4-1l0.2-0.5c0.2-0.8,0.5-1.5,0.9-2.2l0.3-0.5L7.1,9.1l2-2l3.2,1.8l0.5-0.3c0.7-0.4,1.4-0.7,2.2-0.9l0.5-0.2L16.6,4h2.8l1.1,3.5L21,7.7c0.7,0.2,1.4,0.5,2.1,0.9l0.5,0.3l3.3-1.8l2,2l-1.8,3.3l0.3,0.5c0.4,0.7,0.7,1.4,0.9,2.1l0.1,0.5l3.6,1.1V19.4z" fill="rgb(var(--DH-blue))" stroke="rgb(var(--DH-blue))" stroke-width="1.2" stroke-linejoin="round" paint-order="stroke fill"/>
</svg>
</div>
</div>
<!-- Row 2: Donate + YouTube + Discord + GitHub -->
<div class="DH_HStack_8">
<div class="DH_Btn DH_NoSel" id="DH_Donate_Btn" style="padding:10px 0 10px 10px;outline:2px solid rgba(0,0,0,0.20);outline-offset:-2px;background:url(https://duohacker.io.vn/wallpaper.png) lightgray 50% / cover no-repeat;">
<svg width="19" height="17" viewBox="0 0 24 22" fill="#FFF" xmlns="http://www.w3.org/2000/svg">
<path d="M12 21.593c-.425-.394-8.993-7.755-8.993-12.419C3.007 4.984 6.077 2 9.535 2c1.997 0 3.714.862 4.465 2.099C14.75 2.862 16.467 2 18.465 2 21.922 2 25 4.984 25 9.174c0 4.664-8.571 12.025-8.993 12.419L12 21.593z" transform="translate(-1 -1)"/>
</svg>
<p class="DH_T1 DH_NoSel" id="DH_Donate_Btn_Lbl" style="color:#FFF;font-size:15px;font-weight:700;">Donate</p>
</div>
<div class="DH_Btn DH_Btn_Icon DH_NoSel" id="DH_YouTube_Btn" style="background:#FF0000;outline:2px solid rgba(0,0,0,.18);outline-offset:-2px;">
<svg width="18" height="13" viewBox="0 0 22 16" fill="#FFF"><path fill-rule="evenodd" clip-rule="evenodd" d="M19.2043 1.0885C20.1084 1.33051 20.8189 2.041 21.0609 2.9451C21.4982 4.58216 21.5 7.99976 21.5 7.99976C21.5 7.99976 21.5 11.4174 21.0609 13.0544C20.8189 13.9585 20.1084 14.669 19.2043 14.911C17.5673 15.3501 11 15.3501 11 15.3501C11 15.3501 4.43274 15.3501 2.79568 14.911C1.89159 14.669 1.1811 13.9585 0.939084 13.0544C0.5 11.4174 0.5 7.99976 0.5 7.99976C0.5 7.99976 0.5 4.58216 0.939084 2.9451C1.1811 2.041 1.89159 1.33051 2.79568 1.0885C4.43274 0.649414 11 0.649414 11 0.649414C11 0.649414 17.5673 0.649414 19.2043 1.0885ZM14.3541 8.00005L8.89834 11.1497V4.85038L14.3541 8.00005Z"/></svg>
</div>
<div class="DH_Btn DH_Btn_Icon DH_NoSel" id="DH_Discord_Btn" style="background:rgb(88,101,242);outline:2px solid rgba(0,0,0,.18);outline-offset:-2px;">
<svg width="18" height="14" viewBox="0 0 22 16" fill="#FFF"><path d="M18.289 1.34C16.9296.714 15.4761.259 13.9565 0c-.1866.332-.4046.779-.5549 1.134-1.6154-.239-3.2159-.239-4.8016 0C8.4497.779 8.2267.332 8.0384 0 6.5172.259 5.062.716 3.7027 1.343.9608 5.421.2175 9.398.5892 13.318c1.8185 1.337 3.5809 2.149 5.3136 2.68.4278-.579.8093-1.195 1.138-1.845-.6259-.234-1.2255-.523-1.7921-.858.1503-.11.2973-.225.4393-.307 3.4554 1.591 7.2098 1.591 10.624 0 .1437.118.2907.233.4393.342-.6262.337-1.2274.626-1.8534.86.3287.648.7086 1.265 1.138 1.845 1.7343-.531 3.4983-1.343 5.3168-2.681.4361-4.545-.7449-8.484-3.121-11.978ZM7.5115 10.908c-1.0373 0-1.8879-.954-1.8879-2.114 0-1.161.8325-2.115 1.8879-2.115 1.0555 0 1.9061.954 1.8879 2.115.0016 1.16-.8325 2.114-1.8879 2.114Zm6.9769 0c-1.0373 0-1.8879-.954-1.8879-2.114 0-1.161.8324-2.115 1.8879-2.115 1.0554 0 1.9061.954 1.8879 2.115 0 1.16-.8325 2.114-1.8879 2.114Z"/></svg>
</div>
<div class="DH_Btn DH_Btn_Icon DH_NoSel" id="DH_GitHub_Btn" style="background:#24292e;outline:2px solid rgba(255,255,255,.18);outline-offset:-2px;">
<svg width="18" height="18" viewBox="0 0 22 22" fill="#FFF"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.009.5C5.198.5.5 5.313.5 11.266c0 4.759 3.01 8.788 7.186 10.214.522.107.713-.232.713-.517 0-.25-.017-1.105-.017-1.997-2.923.642-3.532-1.283-3.532-1.283-.47-1.248-1.166-1.568-1.166-1.568-.957-.659.07-.659.07-.659 1.062.071 1.619 1.105 1.619 1.105.94 1.64 2.453 1.176 3.062.891.087-.695.366-1.176.661-1.444-2.332-.25-4.785-1.176-4.785-5.312 0-1.176.418-2.139 1.08-2.887-.106-.267-.461-1.373.105-2.852 0 0 .888-.285 2.899 1.09a9.847 9.847 0 0 1 2.636-.356c.888 0 1.793.125 2.628.356 2.01-1.375 2.898-1.09 2.898-1.09.566 1.479.21 2.585.105 2.852.662.748 1.08 1.711 1.08 2.887 0 4.136-2.453 5.045-4.803 5.312.383.338.714.98.714 2.004 0 1.444-.018 2.606-.018 2.963 0 .285.192.624.714.48C18.49 20.054 21.5 16.025 21.5 11.266 21.517 5.313 16.802.5 11.009.5Z"/></svg>
</div>
</div>
<div class="DH_HStack_8" id="DH_User_Row" style="display:none;gap:10px;">
<div class="DH_Avatar" id="DH_Avatar">👤</div>
<div class="DH_VStack_4" style="flex:1 0 0;min-width:0;align-items:flex-start;">
<p class="DH_T1 DH_NoSel" id="DH_UName" style="font-size:14px;align-self:stretch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></p>
<div class="DH_HStack_4" style="gap:10px;">
<div class="DH_HStack_4" style="gap:3px;">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg">
<span class="DH_Stat_Val DH_NoSel" id="DH_UXP">0</span>
</div>
<div class="DH_HStack_4" style="gap:3px;">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg">
<span class="DH_Stat_Val DH_NoSel" id="DH_UGems">0</span>
</div>
<div class="DH_HStack_4" style="gap:3px;">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg">
<span class="DH_Stat_Val DH_NoSel" id="DH_UStreak">0</span>
</div>
</div>
</div>
<svg id="DH_AccSettings_Btn" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" title="Account Manager"
style="cursor:pointer;flex-shrink:0;transition:filter 0.3s,transform 0.3s;transform-origin:center;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z" fill="rgb(var(--DH-blue))"/>
</svg>
</div>
<div class="DH_Divider"></div>
<div class="DH_VStack_8">
<p class="DH_T1 DH_NoSel" id="DH_XP_Question" style="align-self:stretch;">How much XP would you like to gain?</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<p class="DH_T1 DH_NoSel" style="color:rgba(var(--DH-blue),0.38);font-size:13px;flex-shrink:0;">#</p>
<input type="number" class="DH_Input DH_NoSel" id="DH_XP_Input" placeholder="0" min="30" max="500000">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_XP_Btn" disabled>
<span class="DH_Btn_Label" id="DH_XP_Lbl" style="color:#fff;">GET</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_XP_Prog"><div class="DH_Prog_Fill" id="DH_XP_Fill"></div></div>
</div>
<!-- Gems Farm -->
<div class="DH_VStack_8" id="DH_Gem_Section">
<p class="DH_T1 DH_NoSel" id="DH_Gem_Run_Label" style="align-self:stretch;">Click "RUN" to Farm Gems</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<svg class="DH_Shimmer" width="120" height="48" viewBox="0 0 120 48" fill="none">
<path opacity="0.4" d="M72 0H96L72 48H48L72 0Z" fill="rgb(var(--DH-blue))"/>
<path opacity="0.4" d="M24 0H60L36 48H0L24 0Z" fill="rgb(var(--DH-blue))"/>
<path opacity="0.4" d="M108 0H120L96 48H84L108 0Z" fill="rgb(var(--DH-blue))"/>
</svg>
<p class="DH_T1 DH_NoSel" style="color:rgba(var(--DH-blue),0.38);font-size:14px;flex-shrink:0;">#</p>
<input type="number" class="DH_Input DH_NoSel" id="DH_Gem_Input" placeholder="0" readonly style="pointer-events:none;">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_Gem_Btn" disabled>
<span class="DH_Btn_Label" id="DH_Gem_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
</div>
<div class="DH_VStack_8">
<p class="DH_T1 DH_NoSel" id="DH_Streak_Question" style="align-self:stretch;">How many Streak days to restore?</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<p class="DH_T1 DH_NoSel" style="color:rgba(var(--DH-blue),0.38);font-size:14px;flex-shrink:0;">#</p>
<input type="number" class="DH_Input DH_NoSel" id="DH_Streak_Input" placeholder="0" min="1" max="3650">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_Streak_Btn" disabled>
<span class="DH_Btn_Label" id="DH_Streak_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_Streak_Prog"><div class="DH_Prog_Fill" id="DH_Streak_Fill"></div></div>
</div>
<div class="DH_Divider"></div>
<div class="DH_Btn DH_Btn_Blue_Ghost DH_NoSel" id="DH_Settings_Btn" style="align-self:stretch; justify-content:space-between; padding:10px 12px;">
<p class="DH_T1 DH_NoSel" id="DH_ExtraFeatures_Lbl" style="color:rgb(var(--DH-blue));">Extra Features</p>
<svg width="8" height="13" viewBox="0 0 8 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1l6 5.5L1 12" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="DH_HStack_Auto">
<p class="DH_T2 DH_NoSel" style="color:rgba(var(--DH-blue),0.45);">duohacker.io.vn</p>
<p class="DH_T2 DH_NoSel" style="color:rgba(var(--DH-blue),0.45);">v2026.06.15</p>
</div>
</div>
<div class="DH_Page" id="DH_Page_2">
<div class="DH_HStack_4 DH_NoSel" id="DH_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_Back_Txt">Back</p>
</div>
<div class="DH_HStack_Auto" style="align-self:stretch;">
<div style="display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;">
<p class="DH_T1 DH_NoSel" id="DH_Practice_Title">Farm Practice</p>
<p class="DH_T2 DH_NoSel" id="DH_Practice_Sub" style="font-size:11px;">0 = unlimited practice sessions</p>
</div>
<button class="DH_Sm_Btn DH_NoSel" id="DH_Practice_Btn" disabled>
<span class="DH_Sm_Btn_Label" id="DH_Practice_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<!-- Practice input inline below button -->
<div style="display:flex;align-items:center;gap:8px;align-self:stretch;">
<div class="DH_Input_Wrap" style="flex:1;">
<p class="DH_T1 DH_NoSel" style="color:rgba(var(--DH-blue),0.38);font-size:14px;flex-shrink:0;">#</p>
<input type="number" class="DH_Input DH_NoSel" id="DH_Practice_Input" placeholder="0" min="0" max="9999">
</div>
</div>
<div class="DH_Divider"></div>
<!-- Shop Items nav -->
<div class="DH_Btn DH_Btn_Blue_Ghost DH_NoSel"
id="DH_Shop_Btn"
style="align-self:stretch;justify-content:space-between;padding:10px 12px;">
<div style="display:flex;align-items:center;">
<p class="DH_T1 DH_NoSel"
id="DH_Shop_Lbl"
style="color:rgb(var(--DH-blue));">
Shop Items
</p>
</div>
<svg width="8" height="13" viewBox="0 0 8 13" fill="none">
<path d="M1 1l6 5.5L1 12"
stroke="rgb(var(--DH-blue))"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</div>
<!-- Auto League -->
<div class="DH_HStack_Auto" style="align-self:stretch;">
<div style="display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;">
<p class="DH_T1 DH_NoSel" id="DH_League_Title">Auto League #1</p>
<p class="DH_T2 DH_NoSel" id="DH_League_Sub" style="font-size:11px;">Farm XP until rank #1</p>
</div>
<button class="DH_Sm_Btn DH_NoSel" id="DH_League_Btn" disabled>
<span class="DH_Sm_Btn_Label" id="DH_League_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_League_Prog" style="align-self:stretch;"><div class="DH_Prog_Fill" id="DH_League_Fill"></div></div>
<!-- Auto Daily Quest - SIMPLIFIED -->
<div class="DH_HStack_Auto" style="align-self:stretch;">
<div style="display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;">
<p class="DH_T1 DH_NoSel" id="DH_Quest_Title">Auto Daily Quest</p>
<p class="DH_T2 DH_NoSel" id="DH_Quest_Sub" style="font-size:11px;">Complete all daily quests</p>
</div>
<button class="DH_Sm_Btn DH_NoSel" id="DH_Quest_Btn" disabled>
<span class="DH_Sm_Btn_Label" id="DH_Quest_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<!-- Monthly Quest -->
<div class="DH_HStack_Auto" style="align-self:stretch;cursor:pointer;" id="DH_MonthlyQuest_Nav_Btn">
<div style="display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;">
<p class="DH_T1 DH_NoSel" id="DH_Monthly_Title">Claim Monthly Quest</p>
<p class="DH_T2 DH_NoSel" id="DH_Monthly_Sub" style="font-size:11px;">View & claim monthly quests</p>
</div>
<button class="DH_Sm_Btn DH_NoSel" id="DH_MonthlyQuest_Claim_Btn" disabled>
<span class="DH_Sm_Btn_Label" id="DH_MonthlyQuest_Claim_Lbl" style="color:#fff;">GET</span>
</button>
</div>
<!-- Activate Free Super Duolingo -->
<div class="DH_HStack_Auto" style="align-self:stretch;">
<div style="display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;">
<p class="DH_T1 DH_NoSel" id="DH_FreeSuper_Title">Free Super Duolingo</p>
<p class="DH_T2 DH_NoSel" id="DH_FreeSuper_Sub" style="font-size:11px;">Activate Super Duolingo for free</p>
</div>
<button class="DH_Sm_Btn DH_NoSel" id="DH_Super_Activate_Btn" disabled>
<span class="DH_Sm_Btn_Label" id="DH_Super_Activate_Lbl" style="color:#fff;">ACTIVATE</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_Super_Prog" style="align-self:stretch;"><div class="DH_Prog_Fill" id="DH_Super_Fill"></div></div>
<div class="DH_Divider"></div>
</div>
<div class="DH_Page" id="DH_Page_4">
<div class="DH_HStack_4 DH_NoSel" id="DH_Settings_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_Settings_Back_Txt">Back</p>
</div>
<div style="width:100%;gap:8px;display:flex;flex-direction:column;align-items:flex-start;overflow-y:auto;max-height:460px;padding-right:2px;" class="DH_Scroll_Inner">
<!-- Loop Delay -->
<div class="DH_VStack_8" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_LoopDelay_Lbl" style="align-self:stretch;">Loop delay (ms)</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<p class="DH_T1 DH_NoSel" style="color:rgba(var(--DH-blue),0.50);font-size:13px;flex-shrink:0;">ms</p>
<input type="number" class="DH_Input DH_NoSel" id="DH_Delay_Input" placeholder="500" min="0" max="10000">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_Delay_Btn">
<span class="DH_Btn_Label" id="DH_Delay_Lbl" style="color:#fff;">SAVE</span>
</button>
</div>
</div>
<div class="DH_Divider"></div>
<!-- Free Duolingo Max toggle -->
<div class="DH_HStack_Auto" style="align-self:stretch;padding:4px 0;">
<div style="display:flex;flex-direction:column;gap:2px;">
<p class="DH_T1 DH_NoSel" id="DH_FreeDuoMax_Lbl">Free Duolingo Max</p>
<p class="DH_T2 DH_NoSel" id="DH_FreeDuoMax_Sub" style="font-size:11px;">Client-side only, reload to apply</p>
</div>
<label class="DH_Toggle">
<input type="checkbox" id="DH_Super_Toggle">
<span class="DH_Toggle_Slider"></span>
</label>
</div>
<!-- Hide Profile toggle -->
<div class="DH_HStack_Auto" style="align-self:stretch;padding:4px 0;">
<div style="display:flex;flex-direction:column;gap:2px;">
<p class="DH_T1 DH_NoSel" id="DH_HideProfile_Lbl">Hide Profile</p>
<p class="DH_T2 DH_NoSel" id="DH_HideProfile_Status" style="font-size:11px;">Loading…</p>
</div>
<label class="DH_Toggle">
<input type="checkbox" id="DH_HideProfile_Toggle" disabled>
<span class="DH_Toggle_Slider"></span>
</label>
</div>
<!-- Inject Solver toggle -->
<div class="DH_HStack_Auto" style="align-self:stretch;padding:4px 0;">
<div style="display:flex;flex-direction:column;gap:2px;">
<p class="DH_T1 DH_NoSel" id="DH_AutoSolver_Lbl">Auto Solver</p>
<p class="DH_T2 DH_NoSel" id="DH_AutoSolver_Sub" style="font-size:11px;">Show Auto Solver buttons during lessons</p>
</div>
<label class="DH_Toggle">
<input type="checkbox" id="DH_Solver_Toggle">
<span class="DH_Toggle_Slider"></span>
</label>
</div>
<!-- Hide Animation toggle -->
<div class="DH_HStack_Auto" style="align-self:stretch;padding:4px 0;">
<div style="display:flex;flex-direction:column;gap:2px;">
<p class="DH_T1 DH_NoSel" id="DH_HideAnim_Lbl">Hide Animation</p>
<p class="DH_T2 DH_NoSel" id="DH_HideAnim_Sub" style="font-size:11px;">Hide Duolingo images & animations</p>
</div>
<label class="DH_Toggle">
<input type="checkbox" id="DH_HideAnim_Toggle">
<span class="DH_Toggle_Slider"></span>
</label>
</div>
<!-- Lesson Shortener toggle -->
<div class="DH_HStack_Auto" style="align-self:stretch;padding:4px 0;">
<div style="display:flex;flex-direction:column;gap:2px;">
<p class="DH_T1 DH_NoSel" id="DH_LessonShortener_Lbl">Lesson Shortener</p>
<p class="DH_T2 DH_NoSel" id="DH_LessonShortener_Sub" style="font-size:11px;">Replace lessons with 1 instant question</p>
</div>
<label class="DH_Toggle">
<input type="checkbox" id="DH_LS_Toggle">
<span class="DH_Toggle_Slider"></span>
</label>
</div>
<!-- Stories Shortener toggle -->
<div class="DH_HStack_Auto" style="align-self:stretch;padding:4px 0;">
<div style="display:flex;flex-direction:column;gap:2px;">
<p class="DH_T1 DH_NoSel" id="DH_StoriesShortener_Lbl">Stories Shortener</p>
<p class="DH_T2 DH_NoSel" id="DH_StoriesShortener_Sub" style="font-size:11px;">Skip all challenges in stories</p>
</div>
<label class="DH_Toggle">
<input type="checkbox" id="DH_SS_Toggle">
<span class="DH_Toggle_Slider"></span>
</label>
</div>
<div class="DH_Divider"></div>
<div class="DH_HStack_Auto" style="align-self:stretch;padding:2px 0;">
<p class="DH_T2 DH_NoSel" id="DH_License_Open_Btn" style="color:rgba(var(--DH-blue),0.45);cursor:pointer;text-decoration:underline;text-underline-offset:2px;">DuoHacker V2</p>
<p class="DH_T2 DH_NoSel" id="DH_Credits_Btn" style="color:rgb(var(--DH-blue));cursor:pointer;font-weight:700;text-decoration:underline;text-underline-offset:2px;">View Credits</p>
</div>
</div>
</div>
<!-- PAGE 9: License -->
<div class="DH_Page" id="DH_Page_9" style="flex:1;min-height:0;">
<div class="DH_HStack_4 DH_NoSel" id="DH_License_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_License_Back_Txt">Back</p>
</div>
<div class="DH_HStack_Auto DH_NoSel" style="align-self:stretch;">
<p class="DH_T1" style="font-size:15px;">BY-NC-ND 4.0 License</p>
<p class="DH_T2" style="font-size:11px;color:rgba(var(--DH-blue),0.50);">DuoHacker</p>
</div>
<div class="DH_License_Scroll">
<p id="DH_License_Text" class="DH_NoSel">Loading…</p>
</div>
</div>
<!-- PAGE 3: Shop -->
<div class="DH_Page" id="DH_Page_3">
<div class="DH_HStack_4 DH_NoSel" id="DH_Shop_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_Shop_Back_Txt">Back</p>
</div>
<input type="text" class="DH_Search DH_NoSel" id="DH_Shop_Search" placeholder="Search items...">
<!-- Shop scrollable container -->
<div class="DH_Scroll_Inner" id="DH_Shop_Container" style="max-height:300px;">
<p class="DH_T2 DH_NoSel" id="DH_ShopLoading_Txt" style="text-align:center;padding:8px 0;">Loading shop...</p>
</div>
</div>
<!-- PAGE 5: Account Manager -->
<div class="DH_Page" id="DH_Page_5">
<div class="DH_HStack_4 DH_NoSel" id="DH_AccMgr_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_AccMgr_Back_Txt">Back</p>
</div>
<div class="DH_HStack_Auto" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_AccMgr_Title" style="font-size:14px;font-weight:800;">Account Manager</p>
<button class="DH_Sm_Btn DH_NoSel" id="DH_AccSave_Btn" disabled style="height:30px;padding:0 12px;">
<span class="DH_Sm_Btn_Label" id="DH_AccSave_Lbl" style="color:#fff;font-size:11px;">SAVE CURRENT</span>
</button>
</div>
<div class="DH_Divider"></div>
<div id="DH_AccList_Wrap" class="DH_Scroll_Inner" style="max-height:260px;width:100%;gap:6px;">
<p class="DH_T2 DH_NoSel" id="DH_AccNoSaved_Txt" style="text-align:center;padding:8px 0;">No saved accounts.</p>
</div>
</div>
<!-- PAGE 7: Credits -->
<div class="DH_Page" id="DH_Page_7">
<div class="DH_HStack_4 DH_NoSel" id="DH_Credits_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_Credits_Back_Txt">Back</p>
</div>
<p class="DH_T1 DH_NoSel" id="DH_Credits_Title" style="font-size:14px;font-weight:800;align-self:stretch;">Credits</p>
<div class="DH_Divider"></div>
<div id="DH_Credits_Container" class="DH_Scroll_Inner" style="max-height:280px;width:100%;gap:10px;">
</div>
</div>
<!-- PAGE 6: Monthly Quests -->
<div class="DH_Page" id="DH_Page_6">
<div class="DH_HStack_4 DH_NoSel" id="DH_MQ_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_MQ_Back_Txt">Back</p>
</div>
<div class="DH_HStack_Auto" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_MQ_Title" style="font-size:14px;font-weight:800;">Monthly Quests</p>
<button class="DH_Sm_Btn DH_NoSel" id="DH_MQ_ClaimAll_Btn" disabled style="height:30px;padding:0 12px;">
<span class="DH_Sm_Btn_Label" id="DH_MQ_ClaimAll_Lbl" style="color:#fff;font-size:11px;">CLAIM</span>
</button>
</div>
<div class="DH_Divider"></div>
<div id="DH_MQ_Container" class="DH_Scroll_Inner" style="max-height:300px;width:100%;">
<p class="DH_T2 DH_NoSel" style="text-align:center;padding:8px 0;">Loading quests...</p>
</div>
</div>
<!-- PAGE V1: V1 Mode — simple farm with live counters, no extra features -->
<div class="DH_Page" id="DH_Page_V1">
<div class="DH_Divider"></div>
<!-- User row (reused from V2 data) -->
<div class="DH_HStack_8" id="DH_V1_User_Row" style="display:none;gap:10px;">
<div class="DH_Avatar" id="DH_V1_Avatar">👤</div>
<div class="DH_VStack_4" style="flex:1 0 0;min-width:0;align-items:flex-start;">
<p class="DH_T1 DH_NoSel" id="DH_V1_UName" style="font-size:14px;align-self:stretch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></p>
<div class="DH_HStack_4" style="gap:10px;">
<div class="DH_HStack_4" style="gap:3px;">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg">
<span class="DH_Stat_Val DH_NoSel" id="DH_V1_UXP">0</span>
</div>
<div class="DH_HStack_4" style="gap:3px;">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg">
<span class="DH_Stat_Val DH_NoSel" id="DH_V1_UGems">0</span>
</div>
<div class="DH_HStack_4" style="gap:3px;">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg">
<span class="DH_Stat_Val DH_NoSel" id="DH_V1_UStreak">0</span>
</div>
</div>
</div>
</div>
<div class="DH_Divider" id="DH_V1_User_Divider" style="display:none;"></div>
<!-- XP Farm -->
<div class="DH_VStack_8" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_V1_XP_Title" style="align-self:stretch;">XP Farming</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg" style="flex-shrink:0;">
<input type="number" class="DH_Input DH_NoSel" id="DH_V1_XP_Input" placeholder="0" readonly style="pointer-events:none;">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_V1_XP_Btn" disabled>
<span class="DH_Btn_Label" id="DH_V1_XP_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_V1_XP_Prog"><div class="DH_Prog_Fill" id="DH_V1_XP_Fill"></div></div>
</div>
<!-- Gems Farm -->
<div class="DH_VStack_8" id="DH_V1_Gem_Section" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_V1_Gem_Title" style="align-self:stretch;">Farm Gems</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" style="flex-shrink:0;">
<input type="number" class="DH_Input DH_NoSel" id="DH_V1_Gem_Input" placeholder="0" readonly style="pointer-events:none;">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_V1_Gem_Btn" disabled>
<span class="DH_Btn_Label" id="DH_V1_Gem_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_V1_Gem_Prog"><div class="DH_Prog_Fill" id="DH_V1_Gem_Fill"></div></div>
</div>
<!-- Streak Farm -->
<div class="DH_VStack_8" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_V1_Streak_Title" style="align-self:stretch;">Streak Farming</p>
<div class="DH_HStack_8">
<div class="DH_Input_Wrap">
<img class="DH_Stat_Ico" src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg" style="flex-shrink:0;">
<input type="number" class="DH_Input DH_NoSel" id="DH_V1_Streak_Input" placeholder="0" readonly style="pointer-events:none;">
</div>
<button class="DH_Input_Btn DH_NoSel" id="DH_V1_Streak_Btn" disabled>
<span class="DH_Btn_Label" id="DH_V1_Streak_Lbl" style="color:#fff;">RUN</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_V1_Streak_Prog"><div class="DH_Prog_Fill" id="DH_V1_Streak_Fill"></div></div>
</div>
<!-- Activate Free Super Duolingo V1 -->
<div class="DH_VStack_8" style="align-self:stretch;">
<p class="DH_T1 DH_NoSel" id="DH_V1_Super_Q" style="align-self:stretch;">Would you like to activate Free Super Duolingo?</p>
<div class="DH_HStack_8">
<button class="DH_Input_Btn DH_NoSel" id="DH_V1_Super_Activate_Btn" style="flex:1 0 0;" disabled>
<span class="DH_Btn_Label" id="DH_V1_Super_Activate_Lbl" style="color:#fff;">ACTIVATE</span>
</button>
</div>
<div class="DH_Prog_Wrap" id="DH_V1_Super_Prog"><div class="DH_Prog_Fill" id="DH_V1_Super_Fill"></div></div>
</div>
<div class="DH_Divider"></div>
<!-- Settings (same as V2 settings page) -->
<div class="DH_Btn DH_Btn_Blue_Ghost DH_NoSel" id="DH_V1_Settings_Btn" style="align-self:stretch; justify-content:space-between; padding:10px 12px;">
<div style="display:flex; align-items:center; gap:8px;">
<div class="DH_Btn_Ico">
<svg width="16" height="16" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.1,11c-3.9,0-7,3.1-7,7s3.1,7,7,7c3.9,0,7-3.1,7-7S22,11,18.1,11z M18.1,23c-2.8,0-5-2.2-5-5s2.2-5,5-5c2.8,0,5,2.2,5,5S20.9,23,18.1,23z" fill="rgb(var(--DH-blue))" stroke="rgb(var(--DH-blue))" stroke-width="1.2" stroke-linejoin="round" paint-order="stroke fill"/>
<path d="M32.8,14.7L30,13.8l-0.6-1.5l1.4-2.6c0.3-0.6,0.2-1.4-0.3-1.9l-2.4-2.4c-0.5-0.5-1.3-0.6-1.9-0.3l-2.6,1.4l-1.5-0.6l-0.9-2.8C21,2.5,20.4,2,19.7,2h-3.4c-0.7,0-1.3,0.5-1.4,1.2L14,6c-0.6,0.1-1.1,0.3-1.6,0.6L9.8,5.2C9.2,4.9,8.4,5,7.9,5.5L5.5,7.9C5,8.4,4.9,9.2,5.2,9.8l1.3,2.5c-0.2,0.5-0.4,1.1-0.6,1.6l-2.8,0.9C2.5,15,2,15.6,2,16.3v3.4c0,0.7,0.5,1.3,1.2,1.5L6,22.1l0.6,1.5l-1.4,2.6c-0.3,0.6-0.2,1.4,0.3,1.9l2.4,2.4c0.5,0.5,1.3,0.6,1.9,0.3l2.6-1.4l1.5,0.6l0.9,2.9c0.2,0.6,0.8,1.1,1.5,1.1h3.4c0.7,0,1.3-0.5,1.5-1.1l0.9-2.9l1.5-0.6l2.6,1.4c0.6,0.3,1.4,0.2,1.9-0.3l2.4-2.4c0.5-0.5,0.6-1.3,0.3-1.9l-1.4-2.6l0.6-1.5l2.9-0.9c0.6-0.2,1.1-0.8,1.1-1.5v-3.4C34,15.6,33.5,14.9,32.8,14.7z M32,19.4l-3.6,1.1L28.3,21c-0.3,0.7-0.6,1.4-0.9,2.1l-0.3,0.5l1.8,3.3l-2,2l-3.3-1.8l-0.5,0.3c-0.7,0.4-1.4,0.7-2.1,0.9l-0.5,0.1L19.4,32h-2.8l-1.1-3.6L15,28.3c-0.7-0.3-1.4-0.6-2.1-0.9l-0.5-0.3l-3.3,1.8l-2-2l1.8-3.3l-0.3-0.5c-0.4-0.7-0.7-1.4-0.9-2.1l-0.1-0.5L4,19.4v-2.8l3.4-1l0.2-0.5c0.2-0.8,0.5-1.5,0.9-2.2l0.3-0.5L7.1,9.1l2-2l3.2,1.8l0.5-0.3c0.7-0.4,1.4-0.7,2.2-0.9l0.5-0.2L16.6,4h2.8l1.1,3.5L21,7.7c0.7,0.2,1.4,0.5,2.1,0.9l0.5,0.3l3.3-1.8l2,2l-1.8,3.3l0.3,0.5c0.4,0.7,0.7,1.4,0.9,2.1l0.1,0.5l3.6,1.1V19.4z" fill="rgb(var(--DH-blue))" stroke="rgb(var(--DH-blue))" stroke-width="1.2" stroke-linejoin="round" paint-order="stroke fill"/>
</svg>
</div>
<p class="DH_T1 DH_NoSel" id="DH_V1_Settings_Lbl" style="color:rgb(var(--DH-blue));">Settings</p>
</div>
<svg width="8" height="13" viewBox="0 0 8 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1l6 5.5L1 12" stroke="rgb(var(--DH-blue))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="DH_HStack_Auto">
<p class="DH_T2 DH_NoSel" style="color:rgba(var(--DH-blue),0.45);">duohacker.io.vn</p>
<p class="DH_T2 DH_NoSel" style="color:rgba(var(--DH-blue),0.45);">V1 Mode</p>
</div>
</div>
<!-- PAGE 10: Changelog -->
<div class="DH_Page" id="DH_Page_10">
<div class="DH_HStack_4 DH_NoSel" id="DH_Changelog_Back_Btn" style="align-self:flex-start;cursor:pointer;opacity:0.55;">
<svg width="8" height="14" viewBox="0 0 9 16" fill="none"><path d="M8 1L2 8l6 7" stroke="rgb(var(--color-wolf,60,60,67))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p class="DH_T1" id="DH_Changelog_Back_Txt">Back</p>
</div>
<div style="width:100%;display:flex;flex-direction:column;gap:12px;overflow-y:auto;max-height:360px;padding-right:2px;" class="DH_Scroll_Inner" id="DH_Changelog_List">
<!-- populated by JS -->
</div>
</div>
</div>
</div>
`;
document.body.appendChild(_wrap);
const _langSelector = document.getElementById('DH_LangSelector');
const _langSelectorBtn = document.getElementById('DH_LangSelector_Btn');
_langSelectorBtn.addEventListener('click', event => {
event.stopPropagation();
_langSelector.classList.toggle('open');
});
document.querySelectorAll('.DH_LangOption').forEach(option => {
option.addEventListener('click', event => {
event.stopPropagation();
const lang = option.dataset.lang;
if (lang !== 'vi' && lang !== 'en') return;
_setLang(lang);
_langSelector.classList.remove('open');
});
});
document.addEventListener('click', event => {
if (!_langSelector.contains(event.target)) {
_langSelector.classList.remove('open');
}
});
_applyLang();
let _jwt = null,
_sub = null,
_hdrs = null,
_user = null,
_privacy = null;
let _v1Mode = false;
let _v1Running = false,
_v1Task = null;
const _v1Earned = {
xp: 0,
gems: 0,
streak: 0
};
function _v1UpdateDisplay() {
requestAnimationFrame(() => {
const xi = document.getElementById('DH_V1_XP_Input');
const gi = document.getElementById('DH_V1_Gem_Input');
const si = document.getElementById('DH_V1_Streak_Input');
if (xi) xi.value = _v1Earned.xp > 0 ? String(_v1Earned.xp) : '';
if (gi) gi.value = _v1Earned.gems > 0 ? String(_v1Earned.gems) : '';
if (si) si.value = _v1Earned.streak > 0 ? String(_v1Earned.streak) : '';
});
}
function _v1SyncUser() {
if (!_user) return;
const row = document.getElementById('DH_V1_User_Row');
const div = document.getElementById('DH_V1_User_Divider');
if (row) row.style.display = 'flex';
if (div) div.style.display = '';
document.getElementById('DH_V1_UName').textContent = _user.username || '';
document.getElementById('DH_V1_UXP').textContent = (_user.totalXp || 0).toLocaleString();
document.getElementById('DH_V1_UGems').textContent = (_user.gems || 0).toLocaleString();
document.getElementById('DH_V1_UStreak').textContent = (_user.streak || 0).toLocaleString();
if (_user.picture) {
let hq = _user.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!hq.endsWith('/xlarge') && hq.includes('duolingo.com/ssr-avatars')) hq += '/xlarge';
const av = document.getElementById('DH_V1_Avatar');
const img = document.createElement('img');
img.src = hq;
img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;';
img.onerror = function() {
av.innerHTML = '👤';
};
av.innerHTML = '';
av.appendChild(img);
}
}
let _remoteVersion = '';
let _running = false,
_task = null,
_hidden = false;
let _delay = parseInt(localStorage.getItem('dh2_delay') || '500', 10);
let _shopItems = [];
const _sleep = ms => new Promise(r => setTimeout(r, ms));
const GOALS_API = 'https://goals-api.duolingo.com';
let _pageHistory = [1];
let _questState = null;
let _solverUI = null;
let _isAutoMode = false;
let _solvingIntervalId = null;
let _isInLesson = false;
const _SOLVE_SPEED = 0.1;
let _INJECT_SOLVER_ENABLED = localStorage.getItem('duohacker_inject_solver') === 'true';
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;
},
findStoryChallenge: (dom) => {
// Walk fiber tree upward looking for story challenge props (type in uppercase like MULTIPLE_CHOICE)
const key = Object.keys(dom || {}).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
if (!key) return null;
let fiber = dom[key];
let depth = 0;
while (fiber && depth < 60) {
const props = fiber.memoizedProps || fiber.pendingProps;
if (props) {
// Story challenge props have type in UPPER_CASE
if (props.currentElement?.type && props.currentElement.type === props.currentElement.type.toUpperCase()) return props.currentElement;
if (props.element?.type && props.element.type === props.element.type.toUpperCase()) return props.element;
if (props.challenge?.type && props.challenge.type === props.challenge.type.toUpperCase()) return props.challenge;
if (props.currentChallenge?.type) return props.currentChallenge;
}
fiber = fiber.return;
depth++;
}
return null;
},
determineChallengeType: () => {
try {
const t = window.sol?.type;
if (!t) return false;
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';
const isStory = location.hostname.includes('stories.duolingo.com') ||
location.pathname.includes('/stories') ||
!!document.querySelector('.FmlUF') ||
!!document.querySelector('[data-test="stories-choice"]') ||
!!document.querySelector('[data-test="story-start"]') ||
!!document.querySelector('[data-test="stories-player-continue"]') ||
!!document.querySelector('[data-test="stories-player-done"]');
const tl = t?.toLowerCase();
if (isStory) {
if (tl === 'arrange') return 'Story Arrange';
if (tl === 'multiple-choice' || tl === 'multiple_choice' || tl === 'select-phrases') return 'Story Multiple Choice';
if (tl === 'point-to-phrase' || tl === 'point_to_phrase') return 'Story Point to Phrase';
if (tl === 'match') return 'Story Pairs';
if (tl === 'gap-fill' || tl === 'gap_fill') return 'Story Gap Fill';
}
// Fallback: story-exclusive types
if (tl === 'arrange') return 'Story Arrange';
if (tl === 'multiple-choice' || tl === 'multiple_choice') return 'Story Multiple Choice';
if (tl === 'point-to-phrase' || tl === 'point_to_phrase') return 'Story Point to Phrase';
if (tl === 'gap-fill' || tl === 'gap_fill') return 'Story Gap Fill';
if (t === 'typeCloze') return 'Type Cloze';
if (t === 'typeClozeTable') return 'Type Cloze Table';
if (t === 'tapClozeTable') return 'Tap Cloze Table';
if (t === 'typeCompleteTable') return 'Type Complete Table';
if (t === 'tapCompleteTable') return 'Tap Complete Table';
if (t === 'patternTapComplete') return 'Pattern Tap Complete';
if (t === 'syllableTap') return 'Syllable Tap';
if (t === 'syllableListenTap') return 'Syllable Listen Tap';
if (t === 'listenTap') return 'Listen Tap';
if (t === 'listen') return 'Listen Type';
if (t === 'translate') return 'Translate';
if (t === 'completeReverseTranslation') return 'Complete Reverse';
if (document.querySelectorAll('[data-test*="challenge-partialReverseTranslate"]').length > 0) return 'Partial Reverse';
if (document.querySelectorAll('[data-test="challenge challenge-characterWrite"]').length > 0) {
if (document.querySelector('g._25Ktp')) return 'Character Write Drag';
if (document.querySelectorAll('path._1e5Zt').length > 0) return 'Character Write Draw';
return 'Character Write Freehand';
}
if (t === 'judge') return 'Judge';
if (t === 'dialogue' || t === 'characterIntro' || t === 'selectTranscription') return 'Dialogue';
if (t === 'characterMatch' || t === 'match') {
if (document.querySelectorAll('[data-test$="challenge-tap-token"]').length > 0) return 'Pairs';
}
if (t === 'select' || t === 'characterSelect' || t === 'form' ||
t === 'readComprehension' || t === 'listenComprehension' ||
t === 'selectPronunciation') {
return 'Select Card';
}
if (document.querySelectorAll('[data-test*="challenge-name"]').length > 0 &&
document.querySelectorAll('[data-test="challenge-choice"]').length > 0) return 'Challenge Name';
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 (t === 'orderTapComplete') return 'Order Tap Complete';
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('textarea[data-test="challenge-translate-input"]').length > 0) return 'Challenge Translate Input';
if (document.querySelectorAll('[data-test="daily-quest-progress-slide"]').length > 0) return 'Daily Quest Progress';
if (document.querySelectorAll('[data-test="streak-slide"]').length > 0) return 'Streak Slide';
if (document.querySelectorAll('[data-test="leaderboard-slide"]').length > 0) return 'Leaderboard Slide';
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 'Order Tap Complete': {
const blanks = window.sol.displayTokens?.filter(t => t.isBlank);
if (!blanks || blanks.length === 0) break;
const choicesData = window.sol.choices || [];
const usedChoiceIdx = new Set();
for (const blank of blanks) {
// Find the choices index for this blank (skip already-used ones)
let choiceIdx = -1;
for (let i = 0; i < choicesData.length; i++) {
if (usedChoiceIdx.has(i)) continue;
const choiceText = typeof choicesData[i] === 'object' ? choicesData[i].text : choicesData[i];
if (choiceText?.trim() === blank.text?.trim()) {
choiceIdx = i;
break;
}
}
if (choiceIdx === -1) continue;
usedChoiceIdx.add(choiceIdx);
// Click the DOM button at that index
const buttons = document.querySelectorAll('[data-test="word-bank"] [data-test*="challenge-tap-token"]:not(span)');
if (buttons[choiceIdx]) {
buttons[choiceIdx].click();
await _autoSolver.delay(50);
}
}
break;
}
case 'Challenge Speak':
case 'Listen Match':
case 'Listen Speak':
document.querySelector('button[data-test="player-skip"]')?.click();
break;
case 'Select Card': {
const idx = window.sol.correctIndex ?? 0;
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;
}
case 'Judge': {
const ci = window.sol.correctIndices?.[0] ?? 0;
document.querySelectorAll('[data-test="challenge-judge-text"]')[ci]?.click();
break;
}
case 'Dialogue': {
const idx = window.sol.correctIndex ?? 0;
const 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;
}
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;
}
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;
}
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;
}
case 'Complete Reverse': {
const blankTokens = window.sol.displayTokens?.filter(t => t.isBlank);
const inputFields = document.querySelectorAll('[data-test="challenge-text-input"]');
if (blankTokens && blankTokens.length > 1 && inputFields.length > 1) {
// Multi-blank support
inputFields.forEach((input, index) => {
if (blankTokens[index]) {
_autoSolver.setInputValue(input, blankTokens[index].text);
}
});
} else {
// Single blank (fallback)
const answer = blankTokens?.[0]?.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"]');
const parts = window.sol.transcriptParts || window.sol.parts || [];
let phraseCorrectIndex = -1;
for (let i = 0; i < parts.length; i++) {
if (parts[i].selectable === true) {
phraseCorrectIndex += 1;
if (window.sol.correctAnswerIndex === i) {
phraseChoices[phraseCorrectIndex]?.parentElement.click();
break;
}
}
}
break;
}
case 'Story Gap Fill': {
// correctIndices = [idx1, idx2, ...] — index into choices[], click in order
const correctIndices = window.sol.correctIndices || [];
const choicesList = window.sol.choices || [];
for (const idx of correctIndices) {
const targetText = choicesList[idx];
if (targetText == null) continue;
// Re-query each time to skip already-used (disabled) buttons
const gapBtns = document.querySelectorAll('[data-test="stories-choice"], [data-test*="challenge-tap-token"]:not(span):not([disabled]):not([aria-disabled="true"])');
for (const btn of gapBtns) {
if (btn.innerText.trim() === targetText.trim() && !btn.disabled && btn.getAttribute('aria-disabled') !== 'true') {
btn.click();
await _autoSolver.delay(50);
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;
case 'Syllable Tap':
case 'Syllable Listen Tap': {
const correctIndices = window.sol.correctIndices;
const choicesData = window.sol.choices;
const domButtons = Array.from(document.querySelectorAll('[data-test="word-bank"] [data-test$="challenge-tap-token"]'));
correctIndices.forEach(index => {
const correctText = choicesData[index].text;
const matchingButton = domButtons.find(btn => btn.innerText.trim() === correctText);
if (matchingButton) matchingButton.click();
});
break;
}
case 'Character Write Drag': {
const sleep = ms => new Promise(r => setTimeout(r, ms));
const createEvent = (type, x, y, buttons) => new MouseEvent(type, {
bubbles: true,
clientX: x,
clientY: y,
buttons,
button: 0
});
const normalize = str => str ? str.replace(/\s/g, '') : '';
const strokes = window.sol.strokes;
for (let i = 0; i < strokes.length; i++) {
const targetPathData = normalize(strokes[i].path);
let path, handle;
while (!path || !handle) {
const candidates = document.querySelectorAll('path._1e5Zt');
path = Array.from(candidates).find(p => normalize(p.getAttribute('d')) === targetPathData);
handle = document.querySelector('g._25Ktp');
if (!path || !handle) await sleep(10);
}
const matrix = path.getScreenCTM();
const len = path.getTotalLength();
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(len).matrixTransform(matrix);
handle.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
const steps = 10;
for (let s = 1; s <= steps; s++) {
const p = path.getPointAtLength((s / steps) * len).matrixTransform(matrix);
const move = createEvent('mousemove', p.x, p.y, 1);
handle.dispatchEvent(move);
document.dispatchEvent(move);
}
const finalMove = createEvent('mousemove', end.x, end.y, 1);
handle.dispatchEvent(finalMove);
document.dispatchEvent(finalMove);
await sleep(5);
handle.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
}
break;
}
case 'Character Write Draw': {
const sleep = ms => new Promise(r => setTimeout(r, ms));
const createEvent = (type, x, y, buttons) => new MouseEvent(type, {
bubbles: true,
clientX: x,
clientY: y,
buttons,
button: 0
});
const normalize = str => str ? str.replace(/\s/g, '') : '';
const strokes = window.sol.strokes;
for (let i = 0; i < strokes.length; i++) {
const targetPathData = normalize(strokes[i].path);
let path, cursor;
while (!path || !cursor) {
const candidates = document.querySelectorAll('path._1e5Zt');
path = Array.from(candidates).find(p => normalize(p.getAttribute('d')) === targetPathData);
cursor = document.querySelector('g._1h31R:not(._25Ktp)');
if (!path || !cursor) await sleep(10);
}
const matrix = path.getScreenCTM();
const len = path.getTotalLength();
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(len).matrixTransform(matrix);
cursor.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
document.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
const steps = 10;
for (let s = 1; s <= steps; s++) {
const p = path.getPointAtLength((s / steps) * len).matrixTransform(matrix);
const move = createEvent('mousemove', p.x, p.y, 1);
cursor.dispatchEvent(move);
document.dispatchEvent(move);
}
const finalMove = createEvent('mousemove', end.x, end.y, 1);
cursor.dispatchEvent(finalMove);
document.dispatchEvent(finalMove);
await sleep(5);
cursor.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
}
break;
}
case 'Character Write Freehand': {
const sleep = ms => new Promise(r => setTimeout(r, ms));
const createEvent = (type, x, y, buttons) => new MouseEvent(type, {
bubbles: true,
clientX: x,
clientY: y,
buttons,
button: 0
});
const normalize = str => str ? str.replace(/\s/g, '') : '';
const freehandStrokes = window.sol.strokes.filter(s => s.strokeDrawMode === 'FREEHAND');
for (let i = 0; i < freehandStrokes.length; i++) {
const targetPathData = normalize(freehandStrokes[i].path);
let path, svg;
while (!path || !svg) {
const candidates = document.querySelectorAll('path._22UPm');
path = Array.from(candidates).find(p => normalize(p.getAttribute('d')) === targetPathData);
svg = document.querySelector('svg.o1rqi');
if (!path || !svg) await sleep(10);
}
const matrix = path.getScreenCTM();
const len = path.getTotalLength();
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(len).matrixTransform(matrix);
svg.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
document.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
const steps = 10;
for (let s = 1; s <= steps; s++) {
const p = path.getPointAtLength((s / steps) * len).matrixTransform(matrix);
const move = createEvent('mousemove', p.x, p.y, 1);
svg.dispatchEvent(move);
document.dispatchEvent(move);
}
const finalMove = createEvent('mousemove', end.x, end.y, 1);
svg.dispatchEvent(finalMove);
document.dispatchEvent(finalMove);
await sleep(5);
svg.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
}
break;
}
case 'Daily Quest Progress':
case 'Streak Slide':
case 'Leaderboard Slide':
document.querySelector('[data-test="player-next"]')?.click();
break;
}
} catch (error) {
}
},
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();
},
solve: async () => {
if (_autoSolver._isBusy) return;
_autoSolver._isBusy = true;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const skipSelectors = [
'[data-test="practice-hub-ad-no-thanks-button"]',
'[data-test="plus-no-thanks"]',
'[data-test="story-start"]',
'.vpDIE', // "Đọc câu này sau" / "Tôi không thể nói lúc này"
'._1N-oo._36Vd3._16r-S._1ZBYz._23KDq._1S2uf.HakPM',
'._8AMBh._2vfJy._3Qy5R._28UWu._3h0lA._1S2uf._1E9sc', // Cant speak modal button variant 1
'._1Qh5D._36g4N._2YF0P._28UWu._3h0lA._1S2uf._1E9sc', // Cant speak modal button variant 2
'._3bBpU._1x5JY._1M9iF._36g4N._2YF0P.T7I0c._2EnxW.MYehf',
'._2V6ug._1ursp._7jW2t._28UWu._3h0lA._1S2uf._1E9sc', // No Thanks Legendary
'._1rcV8._1VYyp._1ursp._7jW2t._1gKir', // Language Score
'._2V6ug._1ursp._7jW2t._3zgLG', // Create Profile Later
];
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"]');
// Stories elements
if (!mainElement) mainElement = document.querySelector('[data-test="stories-choice"]')?.closest('[class]') || document.querySelector('[data-test="challenge-tap-token-text"]')?.closest('[class]') || document.querySelector('.FmlUF');
if (!mainElement) {
_autoSolver.clickNext();
_autoSolver._isBusy = false;
return;
}
const reactInstance = _autoSolver.findReact(mainElement);
window.sol = reactInstance?.props?.currentChallenge;
if (!window.sol) {
_autoSolver.clickNext();
_autoSolver._isBusy = false;
return;
}
const challengeType = _autoSolver.determineChallengeType();
if (challengeType && challengeType !== 'Challenge Speak' && challengeType !== 'Listen Match' && challengeType !== 'Listen Speak') {
await Promise.race([
_autoSolver.handleChallenge(challengeType),
new Promise(r => setTimeout(r, 2000))
]);
await sleep(80);
} else if (challengeType === 'Challenge Speak' || challengeType === 'Listen Match' || challengeType === 'Listen Speak') {
// Dismiss "Đọc câu này sau" / "Tôi không thể nói lúc này" modal trước
const speakModalSelectors = ['.vpDIE', '._8AMBh._2vfJy._3Qy5R._28UWu._3h0lA._1S2uf._1E9sc', '._1Qh5D._36g4N._2YF0P._28UWu._3h0lA._1S2uf._1E9sc'];
for (const sel of speakModalSelectors) {
const btn = document.querySelector(sel);
if (btn) {
btn.click();
await sleep(300);
break;
}
}
// Chờ player-skip xuất hiện rồi click
let skipBtn = document.querySelector('button[data-test="player-skip"]');
if (!skipBtn) {
await sleep(500);
skipBtn = document.querySelector('button[data-test="player-skip"]');
}
skipBtn?.click();
await sleep(150);
}
_autoSolver.clickNext();
await sleep(120);
} catch (error) {
_autoSolver.clickNext();
}
_autoSolver._isBusy = false;
},
_isBusy: false,
_solveLoopRunning: false,
toggleAutoMode: () => {
_isAutoMode = !_isAutoMode;
_autoSolver.updateUI();
if (_isAutoMode && !_autoSolver._solveLoopRunning) {
_autoSolver._solveLoopRunning = true;
const initialUrl = window.location.href;
(async function loop() {
while (_isAutoMode) {
if (window.location.href !== initialUrl) {
_isAutoMode = false;
_autoSolver.updateUI();
break;
}
const t0 = Date.now();
await _autoSolver.solve();
await new Promise(r => setTimeout(r, 100));
if (!_isAutoMode) break;
const elapsed = Date.now() - t0;
const wait = Math.max(0, 400 - elapsed);
if (wait > 0) await new Promise(r => setTimeout(r, wait));
}
_autoSolver._solveLoopRunning = false;
})();
} else if (!_isAutoMode) {
_autoSolver._solveLoopRunning = false;
}
},
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: 2147483647; display: flex; gap: 12px; animation: slideUp 0.3s ease-out; pointer-events: auto;`;
_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); pointer-events: auto;">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); pointer-events: auto;">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);
let _lessonSolving = false;
let _currentLessonCount = 0;
let _lessonsToSolve = 0;
function _getJwt() {
const m = document.cookie.match(/(^| )jwt_token=([^;]+)/);
return m ? m[2] : null;
}
function _decodeJwt(t) {
try {
const b = t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const pad = b.padEnd(b.length + (4 - b.length % 4) % 4, '=');
return JSON.parse(decodeURIComponent(atob(pad).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')));
} catch {
return null;
}
}
function _buildHdrs(jwt) {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + jwt,
'User-Agent': navigator.userAgent
};
}
function _goalHdrs(jwt) {
return {
'Content-Type': 'application/json',
'x-requested-with': 'XMLHttpRequest',
'accept': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + jwt
};
}
function _gm(method, url, data, hdrs) {
return new Promise((res, rej) => GM_xmlhttpRequest({
method,
url,
headers: hdrs || _hdrs,
data: data ? JSON.stringify(data) : null,
onload: r => res(r),
onerror: () => rej(new Error('Network')),
timeout: 15000,
ontimeout: () => rej(new Error('Timeout'))
}));
}
function _setBtnState(btnId, cfg, labelText) {
const btn = document.getElementById(btnId);
if (!btn) return;
const lbl = btn.querySelector('.DH_Btn_Label') || btn.querySelector('.DH_Sm_Btn_Label');
if (!lbl) return;
const prevTxt = lbl.textContent;
lbl.textContent = labelText;
const newW = btn.offsetWidth;
lbl.textContent = prevTxt;
btn.style.width = btn.offsetWidth + 'px';
requestAnimationFrame(() => {
lbl.style.opacity = '0';
lbl.style.filter = 'blur(4px)';
btn.style.width = newW + 'px';
btn.style.background = cfg.bg;
btn.style.outline = `solid 2px ${cfg.outline}`;
btn.style.outlineOffset = '-2px';
});
setTimeout(() => {
lbl.style.transition = '0s';
lbl.style.color = cfg.tc;
void lbl.offsetWidth;
lbl.style.transition = '0.4s';
lbl.textContent = labelText;
requestAnimationFrame(() => {
lbl.style.opacity = '1';
lbl.style.filter = 'blur(0)';
});
setTimeout(() => {
btn.style.width = '';
}, 400);
}, 400);
}
const _C_BLUE = {
bg: 'rgb(var(--DH-blue))',
outline: 'rgba(0,0,0,0.18)',
tc: '#fff'
};
const _C_GREEN = {
bg: 'rgba(var(--DH-green),0.10)',
outline: 'rgba(var(--DH-green),0.22)',
tc: 'rgb(var(--DH-green))'
};
const _C_RED = {
bg: 'rgba(var(--DH-red),0.10)',
outline: 'rgba(var(--DH-red),0.22)',
tc: 'rgb(var(--DH-red))'
};
const _C_GRAY = {
bg: 'rgb(var(--color-eel,117,117,117),0.10)',
outline: 'rgb(var(--color-eel,117,117,117),0.20)',
tc: 'rgb(var(--color-eel,117,117,117),0.60)'
};
function _resetBtn(btnId, label) {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.disabled = false;
_setBtnState(btnId, _C_BLUE, label);
const prog = document.getElementById(btnId.replace('_Btn', '_Prog'));
if (prog) setTimeout(() => prog.classList.remove('on'), 2000);
}
function _setBtnProgress(btnId, pct) {
const btn = document.getElementById(btnId);
if (!btn) return;
const lbl = btn.querySelector('.DH_Btn_Label') || btn.querySelector('.DH_Sm_Btn_Label');
if (lbl) lbl.textContent = pct + '%';
const fill = document.getElementById(btnId.replace('_Btn', '_Fill'));
if (fill) fill.style.width = pct + '%';
}
function _setBtnRunning(btnId) {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.disabled = false;
_setBtnState(btnId, _C_RED, '0%');
const prog = document.getElementById(btnId.replace('_Btn', '_Prog'));
if (prog) prog.classList.add('on');
}
function _setBtnDone(btnId, label) {
const btn = document.getElementById(btnId);
if (!btn) return;
_setBtnState(btnId, _C_GREEN, label || _t('btn_done'));
const fill = document.getElementById(btnId.replace('_Btn', '_Fill'));
if (fill) fill.style.width = '100%';
}
const _GF_SCRIPT_URL = 'https://greasyfork.org/en/scripts/561041-duolingo-duohacker';
const _CURRENT_VER = '2026.06.15';
/* ── Changelog Popup ── */
const _CHANGELOG = [{
version: '2026.06.15',
changes: [
'Improved Gem Farming & Streak Farming',
'Updated donation link',
'Added Lesson Shortener',
'Added Skip Stories Challange',
'Added Languages Switcher',
]
}, ];
function _setConn(state, label) {
_currentConnState = state;
if (state === 'connected' && _isOutdated) {
state = 'outdated';
label = `v${_remoteVersion} available — click to update`;
}
const btn = document.getElementById('DH_Conn_Btn');
const ico = document.getElementById('DH_Conn_Ico');
const txt = document.getElementById('DH_Conn_Txt');
if (!btn || !ico || !txt) return;
ico.classList.remove('DH_Spin_Ico');
if (state === 'outdated') {
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
const nb = document.getElementById('DH_Conn_Btn');
const ni = document.getElementById('DH_Conn_Ico');
const nt = document.getElementById('DH_Conn_Txt');
nb.style.background = `linear-gradient(0deg,rgba(var(--DH-orange),0.10),rgba(var(--DH-orange),0.10)),rgb(var(--color-snow),0.90)`;
nb.style.outline = `2px solid rgba(var(--DH-orange),0.30)`;
nb.style.outlineOffset = '-2px';
nb.style.cursor = 'pointer';
nb.style.display = 'flex';
nb.style.alignItems = 'center';
nt.textContent = _t('outdated');
nt.style.color = `rgb(var(--DH-orange))`;
ni.textContent = '';
ni.style.display = 'flex';
ni.style.alignItems = 'center';
ni.style.justifyContent = 'center';
ni.style.color = `rgb(var(--DH-orange))`;
ni.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0;display:block;"><path fill="currentColor" d="M21.171 15.398l-5.912-9.854c-.776-1.293-1.963-2.033-3.259-2.033s-2.483.74-3.259 2.031l-5.912 9.856c-.786 1.309-.872 2.705-.235 3.83.636 1.126 1.878 1.772 3.406 1.772h12c1.528 0 2.77-.646 3.406-1.771.637-1.125.551-2.521-.235-3.831zm-9.171 2.151c-.854 0-1.55-.695-1.55-1.549 0-.855.695-1.551 1.55-1.551s1.55.696 1.55 1.551c0 .854-.696 1.549-1.55 1.549zm1.633-7.424c-.011.031-1.401 3.468-1.401 3.468-.038.094-.13.156-.231.156s-.193-.062-.231-.156l-1.391-3.438c-.09-.233-.129-.443-.129-.655 0-.965.785-1.75 1.75-1.75s1.75.785 1.75 1.75c0 .212-.039.422-.117.625z"/></svg>`;
nb.title = label;
nb.onclick = () => window.open(_GF_SCRIPT_URL, '_blank');
document.getElementById('DH_User_Row').style.display = 'flex';
return;
}
const _SVG_CONNECTING = `<svg class="DH_Spin_Ico" width="16" height="16" viewBox="0 0 297 297" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M294.853 178.121c-1.814-1.671-4.404-2.203-6.729-1.382l-25.97 9.168c17.558-70.878-24.569-143.07-94.69-161.569-34.475-9.094-70.423-4.215-101.225 13.737C35.438 56.028 13.475 84.901 4.397 119.377c-8.083 30.698-4.951 63.329 8.819 91.883 13.616 28.234 36.767 50.846 65.186 63.669 3.256 1.47 6.737 2.203 10.214 2.203 3.647 0 7.293-.807 10.672-2.418 6.59-3.141 11.435-8.99 13.293-16.047 3.086-11.721-2.756-23.89-13.893-28.937-37.335-16.923-56.844-58.027-46.387-97.737 5.7-21.65 19.508-39.794 38.878-51.089 19.372-11.295 41.963-14.378 63.611-8.675 44.273 11.659 70.99 56.813 60.113 101.108l-17.584-20.48c-1.612-1.878-4.135-2.705-6.544-2.152-2.412.555-4.318 2.401-4.948 4.794l-11.478 43.588c-.566 2.153-.02 4.447 1.457 6.112l40.428 45.603c1.785 2.012 4.604 2.752 7.144 1.882l57.644-19.78c2.106-.723 3.711-2.45 4.279-4.603l11.479-43.587c.635-2.412-.106-4.949-1.92-6.62z" fill="currentColor"/></svg>`;
const _SVG_CONNECTED = `<svg width="16" height="16" viewBox="0 0 921 921" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M648.5 794.6c-23.5 44.5-51.2 79.5-82.1 104.2-6.8 5.5-13.7 10.3-20.8 14.7 98.5-18.4 186-68.1 251.7-138.4-6.1-3.7-12.5-7.3-19.1-10.8-29.8-15.8-63.1-29.1-99.1-39.6-8.9 24.8-19.1 48.2-30.6 69.9zm-170.5-134.6c62.4 1.2 122.7 8.8 178.3 22.3C674.9 620.5 685.4 550.8 686.8 478H478V660zm0 242.5c22.8-3.5 45.1-13.9 66.6-31 27.2-21.7 51.7-53.1 73-93.3 10.3-19.4 19.5-40.4 27.6-62.6C593.1 703.2 536.6 696.2 478 695v207.5zM231 229.6c-37.8-11.2-73-25.2-104.5-41.9-9-4.8-17.7-9.7-26-14.9C40.8 247.4 3.8 340.9 0 443h199.2C200.7 367.2 211.6 294.5 231 229.6zm589.5-56.8c-8.3 5.1-16.9 10.1-26 14.9-31.5 16.7-66.7 30.7-104.5 41.9 19.4 64.9 30.3 137.6 31.8 213.4H921C917.2 340.9 880.2 247.4 820.5 172.8zM376.4 871.5c21.4 17.1 43.8 27.5 66.6 31V695c-58.6 1.2-115.1 8.2-167.2 20.6 8.1 22.2 17.3 43.1 27.6 62.6 21.3 40.2 45.9 71.6 73 93.3zM443 660V478H234.2c1.5 72.8 12 142.5 30.5 204.3C320.3 668.9 380.6 661.2 443 660zm-208.8-217H443V261c-62.4-1.2-122.7-8.8-178.3-22.3C246.2 300.5 235.6 370.2 234.2 443zm487.6 35c-1.5 75.8-12.4 148.5-31.8 213.4 37.8 11.2 73 25.2 104.5 41.9 9 4.8 17.7 9.7 26 14.9C880.2 673.6 917.2 580 921 478H721.8zM478 443h208.8c-1.5-72.8-12-142.5-30.5-204.3C600.7 252.2 540.4 259.8 478 261v182zm0-424.5V226c58.6-1.2 115.1-8.2 167.2-20.6-8.1-22.2-17.3-43.1-27.6-62.6-21.3-40.2-45.8-71.6-73-93.3C523.1 32.4 500.8 22 478 18.5zM123.7 145.9c6.1 3.7 12.5 7.3 19.1 10.8 29.8 15.8 63.1 29.1 99.1 39.6 8.9-24.8 19.1-48.2 30.6-69.9 23.5-44.5 51.2-79.5 82.1-104.2 6.8-5.5 13.7-10.3 20.8-14.7C276.9 25.9 189.4 75.6 123.7 145.9zM443 18.5c-22.8 3.5-45.1 13.9-66.6 31-27.2 21.7-51.7 53.1-73 93.3-10.3 19.4-19.5 40.4-27.6 62.6C327.9 217.8 384.4 224.8 443 226V18.5zm-316.6 714.8c31.5-16.7 66.7-30.7 104.5-41.9C211.5 626.5 200.6 553.8 199.1 478H0c3.8 102.1 40.8 195.6 100.5 270.2 8.2-5.1 16.8-10.1 25.9-14.9zm-2.7 41.8C189.4 845.1 276.9 894.8 375.4 913.2c-7-4.3-13.9-9.2-20.8-14.7C323.7 874.1 296 839 272.5 794.6c-11.5-21.7-21.7-45.1-30.6-69.9-35.9 10.6-69.2 23.9-99.1 39.6-6.6 3.5-13 7.1-19.1 10.8zm525.1-651.7c11.5 21.7 21.7 45.1 30.6 69.9 35.9-10.6 69.2-23.9 99.1-39.6 6.6-3.5 13-7.1 19.1-10.8C731.6 75.6 644.1 25.9 545.6 7.5c7 4.3 13.9 9.2 20.8 14.7 30.9 24.7 58.6 59.7 82.1 104.2z"/></svg>`;
const _SVG_ERROR = `<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M21.171 15.398l-5.912-9.854c-.776-1.293-1.963-2.033-3.259-2.033s-2.483.74-3.259 2.031l-5.912 9.856c-.786 1.309-.872 2.705-.235 3.83.636 1.126 1.878 1.772 3.406 1.772h12c1.528 0 2.77-.646 3.406-1.771.637-1.125.551-2.521-.235-3.831zm-9.171 2.151c-.854 0-1.55-.695-1.55-1.549 0-.855.695-1.551 1.55-1.551s1.55.696 1.55 1.551c0 .854-.696 1.549-1.55 1.549zm1.633-7.424c-.011.031-1.401 3.468-1.401 3.468-.038.094-.13.156-.231.156s-.193-.062-.231-.156l-1.391-3.438c-.09-.233-.129-.443-.129-.655 0-.965.785-1.75 1.75-1.75s1.75.785 1.75 1.75c0 .212-.039.422-.117.625z"/></svg>`;
const S = {
connecting: {
bg: `rgb(var(--color-eel,117,117,117),0.10)`,
outline: `rgb(var(--color-eel,117,117,117),0.20)`,
tc: `rgb(var(--color-eel,117,117,117),0.70)`,
t: _t('connecting'),
svg: _SVG_CONNECTING
},
connected: {
bg: `linear-gradient(0deg,rgba(var(--DH-green),0.10),rgba(var(--DH-green),0.10)),rgb(var(--color-snow),0.90)`,
outline: `rgba(var(--DH-green),0.22)`,
tc: `rgb(var(--DH-green))`,
t: _t('connected'),
svg: _SVG_CONNECTED
},
error: {
bg: `rgba(var(--DH-red),0.08)`,
outline: `rgba(var(--DH-red),0.20)`,
tc: `rgb(var(--DH-red))`,
t: label || _t('error'),
svg: _SVG_ERROR
},
} [state];
btn.style.background = S.bg;
btn.style.outline = `2px solid ${S.outline}`;
btn.style.outlineOffset = '-2px';
txt.textContent = S.t;
txt.style.color = S.tc;
ico.innerHTML = S.svg;
ico.style.color = S.tc;
ico.style.display = 'flex';
ico.style.alignItems = 'center';
ico.style.justifyContent = 'center';
if (state === 'connected') document.getElementById('DH_User_Row').style.display = 'flex';
}
async function _checkVersionOnLoad() {
try {
const r = await new Promise((res, rej) => GM_xmlhttpRequest({
method: 'GET',
url: `https://greasyfork.org/scripts/561041.json`,
headers: {
'Accept': 'application/json'
},
onload: r => res(r),
onerror: () => rej(),
timeout: 5000
}));
if (r.status !== 200) return;
const data = JSON.parse(r.responseText);
const remoteVer = (data.version || '').trim();
const cmp = (v1, v2) => {
const a = v1.split('.').map(Number);
const b = v2.split('.').map(Number);
for (let i = 0; i < Math.max(a.length, b.length); i++) {
if ((a[i] || 0) > (b[i] || 0)) return 1;
if ((a[i] || 0) < (b[i] || 0)) return -1;
}
return 0;
};
if (cmp(remoteVer, _CURRENT_VER) !== 0) {
_isOutdated = true;
_remoteVersion = remoteVer;
const txt = document.getElementById('DH_Conn_Txt');
if (_currentConnState === 'connected') {
_setConn('outdated', `v${remoteVer} available`);
}
}
} catch (e) {}
}
setTimeout(_checkVersionOnLoad, 2000);
let _animBusy = false;
function _doHide(val) {
if (_animBusy) return;
_animBusy = true;
_hidden = val;
const main = document.getElementById('DH_Main');
const box = document.getElementById('DH_Main_Box');
const h = box.offsetHeight;
const icoVisible = document.getElementById('DH_Ico_Visible');
const icoHidden = document.getElementById('DH_Ico_Hidden');
const hideTxt = document.getElementById('DH_Hide_Txt');
const hideBtn = document.getElementById('DH_Hide_Btn');
main.style.transition = '0.8s cubic-bezier(0.16,1,0.32,1)';
box.style.transition = '0.8s cubic-bezier(0.16,1,0.32,1)';
const switchV1Btn = document.getElementById('DH_SwitchV1_Btn');
const switchV2Btn = document.getElementById('DH_SwitchV2_Btn');
if (val) {
if (switchV1Btn) switchV1Btn.style.display = 'none';
if (switchV2Btn) switchV2Btn.style.display = 'none';
hideBtn.style.background = `linear-gradient(0deg,rgba(var(--DH-blue),0.10),rgba(var(--DH-blue),0.10)),rgb(var(--color-snow),0.80)`;
hideBtn.style.outline = `2px solid rgba(var(--DH-blue),0.20)`;
if (icoVisible) icoVisible.style.display = 'none';
if (icoHidden) {
icoHidden.style.display = '';
icoHidden.querySelector('path').setAttribute('fill', 'rgb(var(--DH-blue))');
}
hideTxt.textContent = _t('show');
hideTxt.style.color = 'rgb(var(--DH-blue))';
main.style.bottom = `-${h-8}px`;
box.style.filter = 'blur(8px)';
box.style.opacity = '0';
} else {
if (switchV1Btn) switchV1Btn.style.display = (!_v1Mode) ? '' : 'none';
if (switchV2Btn) switchV2Btn.style.display = (_v1Mode) ? '' : 'none';
hideBtn.style.background = `rgb(var(--DH-blue))`;
hideBtn.style.outline = `2px solid rgba(0,0,0,0.20)`;
if (icoHidden) icoHidden.style.display = 'none';
if (icoVisible) {
icoVisible.style.display = '';
}
hideTxt.textContent = _t('hide');
hideTxt.style.color = '#fff';
main.style.bottom = '16px';
box.style.filter = '';
box.style.opacity = '';
}
setTimeout(() => {
main.style.transition = '';
box.style.transition = '';
_animBusy = false;
}, 800);
}
let _curPage = 1,
_pageBusy = false;
function _goPage(to) {
if (_pageBusy || _curPage === to) return;
_pageBusy = true;
const box = document.getElementById('DH_Main_Box');
const fromEl = document.getElementById(`DH_Page_${_curPage}`);
const toEl = document.getElementById(`DH_Page_${to}`);
if (!fromEl || !toEl) {
_pageBusy = false;
return;
}
const oldH = box.offsetHeight;
fromEl.style.display = 'none';
toEl.classList.add('active');
toEl.style.opacity = '0';
const newH = box.offsetHeight;
toEl.classList.remove('active');
toEl.style.opacity = '';
fromEl.style.display = '';
box.style.height = oldH + 'px';
box.style.transition = 'height 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)';
fromEl.style.transition = 'opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1), filter 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1)';
fromEl.style.opacity = '0';
fromEl.style.filter = 'blur(6px)';
fromEl.style.transform = 'scale(0.96)';
requestAnimationFrame(() => {
box.style.height = newH + 'px';
});
setTimeout(() => {
fromEl.classList.remove('active');
fromEl.style.cssText = '';
toEl.classList.add('active');
toEl.style.opacity = '0';
toEl.style.filter = 'blur(6px)';
toEl.style.transform = 'scale(1.04)';
void toEl.offsetWidth;
toEl.style.transition = 'opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)';
toEl.style.opacity = '1';
toEl.style.filter = 'blur(0)';
toEl.style.transform = 'scale(1)';
_pageHistory.push(to);
_curPage = to;
if (to === 2) {
const lgB = document.getElementById('DH_League_Btn');
const qB = document.getElementById('DH_Quest_Btn');
if (_user && lgB) lgB.disabled = false;
if (_user && qB) qB.disabled = false;
}
if (to === 4) {
const delI = document.getElementById('DH_Delay_Input');
if (delI) delI.value = _delay;
}
// Đã preload rồi nên không cần load lại khi chuyển page
setTimeout(() => {
box.style.height = '';
box.style.transition = '';
toEl.style.cssText = '';
_pageBusy = false;
}, 400);
}, 380);
}
function _goBack() {
if (_pageHistory.length > 1) _pageHistory.pop();
const prev = _pageHistory[_pageHistory.length - 1] || 1;
_pageHistory.pop();
_goPage(prev);
}
let _nTimer;
function _notif(icon, title, body, dur = 5) {
const box = document.getElementById('DH_Notif_Box');
document.getElementById('DH_Notif_Icon').textContent = icon;
document.getElementById('DH_Notif_Title').textContent = title;
document.getElementById('DH_Notif_Body').textContent = body;
box.classList.add('show');
clearTimeout(_nTimer);
_nTimer = setTimeout(() => box.classList.remove('show'), dur * 1000);
}
async function _connect() {
_setConn('connecting');
_jwt = _getJwt();
if (!_jwt) {
_setConn('error', 'Not logged in');
return;
}
const dec = _decodeJwt(_jwt);
if (!dec) {
_setConn('error', 'Invalid token');
return;
}
_sub = dec.sub;
_hdrs = _buildHdrs(_jwt);
try {
const r = await _gm('GET', `https://www.duolingo.com/2017-06-30/users/${_sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,gems,picture,streakData`);
if (r.status !== 200) throw new Error(r.status);
_user = JSON.parse(r.responseText);
unsafeWindow._dhUser = _user;
_setConn('connected');
_renderUser(_user);
_getPrivacy().then(v => {
_privacy = v;
_applyHideProfileToggle();
});
_v1FetchSkillId();
['DH_XP_Btn', 'DH_Gem_Btn', 'DH_Streak_Btn', 'DH_League_Btn', 'DH_Quest_Btn', 'DH_Practice_Btn', 'DH_V1_XP_Btn', 'DH_V1_Gem_Btn', 'DH_V1_Streak_Btn', 'DH_Super_Activate_Btn', 'DH_V1_Super_Activate_Btn'].forEach(id => {
const b = document.getElementById(id);
if (b) b.disabled = false;
});
const saveBtn = document.getElementById('DH_AccSave_Btn');
if (saveBtn) saveBtn.disabled = false;
const mqClaimNav = document.getElementById('DH_MonthlyQuest_Claim_Btn');
if (mqClaimNav) mqClaimNav.disabled = false;
// Preload tất cả dữ liệu ngay sau khi kết nối thành công
setTimeout(() => {
_loadShop(); // Load shop items
_renderAccounts(); // Load account manager
_loadMonthlyQuests(); // Load monthly quests
_loadLicense(); // Load license
}, 500);
} catch (e) {
_setConn('error', 'Failed — retrying');
setTimeout(_connect, 8000);
}
}
function _renderUser(u) {
if (!u) return;
document.getElementById('DH_UName').textContent = u.username || '';
_v1SyncUser();
document.getElementById('DH_UXP').textContent = (u.totalXp || 0).toLocaleString();
document.getElementById('DH_UGems').textContent = (u.gems || 0).toLocaleString();
document.getElementById('DH_UStreak').textContent = (u.streak || 0).toLocaleString();
if (u.picture) {
let hq = u.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!hq.endsWith('/xlarge') && hq.includes('duolingo.com/ssr-avatars')) hq += '/xlarge';
const av = document.getElementById('DH_Avatar');
const avImg = document.createElement('img');
avImg.src = hq;
avImg.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;';
avImg.draggable = false;
avImg.onerror = function() {
av.innerHTML = '👤';
};
av.innerHTML = '';
av.appendChild(avImg);
}
}
async function _farmXP(txp) {
const MIN = 30,
MAX = 499;
let loops = Math.floor(txp / MAX),
rem = txp % MAX;
if (rem > 0 && rem < MIN && loops > 0) {
loops--;
rem += MAX;
}
const total = loops + (rem >= MIN ? 1 : 0);
let cur = 0,
earned = 0;
_setBtnRunning('DH_XP_Btn');
for (let i = 0; i < loops; i++) {
if (!_running) break;
const ok = await _storyXP(469);
if (ok) {
earned += MAX;
cur++;
}
_setBtnProgress('DH_XP_Btn', Math.floor((cur / total) * 100));
await _sleep(_delay);
}
if (rem >= MIN && _running) {
const ok = await _storyXP(Math.min(rem - MIN, 469));
if (ok) {
earned += rem;
cur++;
}
_setBtnProgress('DH_XP_Btn', 100);
}
if (_running) {
_setBtnDone('DH_XP_Btn', _t('btn_done'));
_notif('✅', 'XP Farm Done!', `Farmed ${earned} XP in ${cur} loops.`);
setTimeout(_connect, 1500);
setTimeout(() => _resetBtn('DH_XP_Btn', _t('btn_get')), 3000);
}
}
async function _storyXP(hh) {
try {
const now = Math.floor(Date.now() / 1000),
dur = Math.floor(Math.random() * 121 + 300);
const r = await _gm('POST', 'https://stories.duolingo.com/api2/stories/fr-en-le-passeport/complete', {
awardXp: true,
completedBonusChallenge: true,
fromLanguage: 'fr',
learningLanguage: 'en',
hasXpBoost: false,
illustrationFormat: 'svg',
isFeaturedStoryInPracticeHub: true,
isLegendaryMode: true,
isV2Redo: false,
isV2Story: false,
masterVersion: true,
maxScore: 0,
score: 0,
happyHourBonusXp: hh,
startTime: now,
endTime: now + dur
});
return r.status === 200;
} catch {
return false;
}
}
// ── Slug probe — used only by V1 Mode infinite farm (fallback detection) ──
let _workingSlug = null,
_workingSlugFrom = null,
_workingSlugLearn = null;
let _probingSlugPromise = null;
const _SLUG_CANDIDATES = () => [
['vi-en-le-passeport', 'vi', 'en'],
['fr-en-le-passeport', 'fr', 'en'],
['en-fr-le-passeport', 'en', 'fr'],
['es-en-le-passeport', 'es', 'en'],
['de-en-le-passeport', 'de', 'en'],
['pt-en-le-passeport', 'pt', 'en'],
['it-en-le-passeport', 'it', 'en'],
];
async function _probeSlug() {
if (_workingSlug) return _workingSlug;
if (_probingSlugPromise) return _probingSlugPromise;
_probingSlugPromise = (async () => {
const now = Math.floor(Date.now() / 1000);
const tryCandidate = ([slug, from, learn]) => _gm('POST', `https://stories.duolingo.com/api2/stories/${slug}/complete`, {
awardXp: false,
completedBonusChallenge: false,
fromLanguage: from,
learningLanguage: learn,
hasXpBoost: false,
illustrationFormat: 'svg',
isFeaturedStoryInPracticeHub: true,
isLegendaryMode: true,
isV2Redo: false,
isV2Story: false,
masterVersion: true,
maxScore: 0,
score: 0,
happyHourBonusXp: 0,
startTime: now,
endTime: now + 300
}).then(r => {
if (r.status === 200 || r.status === 429) return [slug, from, learn];
return null;
}).catch(() => null);
const winner = await new Promise(resolve => {
let settled = false;
let pending = _SLUG_CANDIDATES().length;
_SLUG_CANDIDATES().forEach(c => {
tryCandidate(c).then(result => {
pending--;
if (result && !settled) {
settled = true;
resolve(result);
} else if (pending === 0 && !settled) resolve(null);
});
});
});
if (winner) {
_workingSlug = winner[0];
_workingSlugFrom = winner[1];
_workingSlugLearn = winner[2];
}
_probingSlugPromise = null;
return winner ? winner[0] : null;
})();
return _probingSlugPromise;
}
// ── Gem Farm Helpers (ported from GemHelper) ──────────────────────────────
async function _fetchGemRewards() {
try {
const response = await fetch(`https://www.duolingo.com/2023-05-23/users/${_sub}?fields=rewardBundles`, {
headers: {
'authorization': `Bearer ${_jwt}`,
'content-type': 'application/json'
}
});
if (!response.ok) return [];
const data = await response.json();
const bundles = data.rewardBundles || [];
const gemRewards = [];
for (const bundle of bundles) {
for (const reward of bundle.rewards || []) {
if (!reward.consumed && (reward.id.includes('GEMS') || reward.currency === 'GEMS')) {
gemRewards.push({
id: reward.id,
amount: reward.amount || 0
});
}
}
}
return gemRewards;
} catch (e) {
return [];
}
}
async function _exploitGemReward(rewardId) {
const body = {
consumed: true,
fromLanguage: _user.fromLanguage,
learningLanguage: _user.learningLanguage,
pathLevelSpecifics: {
anchorSkillId: 'f22fd38157eea63965dc39eeac3c40c1',
indexSinceAnchorSkill: 0,
treeId: '14b1a2672c1bb3b250ebaa31b86c343e',
nodeState: 'active'
}
};
try {
// Use native fetch like Gem Helper instead of _gm wrapper
const response = await fetch(`https://www.duolingo.com/2023-05-23/users/${_sub}/rewards/${rewardId}`, {
method: 'PATCH',
headers: {
'authorization': `Bearer ${_jwt}`,
'content-type': 'application/json',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'x-amzn-trace-id': `User=${_sub}`,
'x-requested-with': 'XMLHttpRequest',
'referer': 'https://www.duolingo.com/learn',
'origin': 'https://www.duolingo.com'
},
body: JSON.stringify(body)
});
return response.ok;
} catch (e) {
return false;
}
}
async function _getGemCount() {
try {
const response = await fetch(`https://www.duolingo.com/2023-05-23/users/${_sub}?fields=gemsConfig`, {
headers: {
'authorization': `Bearer ${_jwt}`,
'content-type': 'application/json'
}
});
if (!response.ok) return null;
const data = await response.json();
return data.gemsConfig?.gems ?? null;
} catch (e) {
return null;
}
}
const THREADS = 8;
async function _farmGems() {
_setBtnState('DH_Gem_Btn', _C_RED, _t('btn_stop'));
const btn = document.getElementById('DH_Gem_Btn');
if (btn) btn.disabled = false;
const gemInput = document.getElementById('DH_Gem_Input');
if (gemInput) gemInput.value = '';
let totalGained = 0;
outer:
while (_running && _task === 'gem') {
const rewards = await _fetchGemRewards();
if (rewards.length === 0) {
await _sleep(Math.max(200, _delay));
continue;
}
const gemsBefore = await _getGemCount() ?? (_user?.gems ?? 0);
// Fire all rewards in parallel batches — no inter-batch delay
for (let i = 0; i < rewards.length; i += THREADS) {
if (!_running || _task !== 'gem') break outer;
const batch = rewards.slice(i, i + THREADS);
await Promise.all(batch.map(r => _exploitGemReward(r.id)));
}
// Single gem count check after full pass — avoids extra API calls
const now = await _getGemCount();
if (now !== null) {
const gained = Math.max(0, now - gemsBefore);
totalGained += gained;
if (gemInput) gemInput.value = String(totalGained);
if (_user) {
_user.gems = now;
_renderUser(_user);
}
}
// Short cooldown before next pass
await _sleep(Math.max(200, _delay / 2));
}
// Always reset button after farm ends (stopped or done)
_setBtnState('DH_Gem_Btn', _C_BLUE, _t('btn_run'));
if (btn) btn.disabled = !_user;
if (totalGained > 0) {
_notif('✅', 'Gem Farm Done!', `+${totalGained} gems gained.`);
setTimeout(_connect, 1500);
}
}
async function _farmStreak(days) {
const CH = ["assist", "characterIntro", "characterMatch", "characterPuzzle", "characterSelect", "characterTrace", "characterWrite", "completeReverseTranslation", "definition", "dialogue", "extendedMatch", "extendedListenMatch", "form", "freeResponse", "gapFill", "judge", "listen", "listenComplete", "listenMatch", "match", "name", "listenComprehension", "listenIsolation", "listenSpeak", "listenTap", "orderTapComplete", "partialListen", "partialReverseTranslate", "patternTapComplete", "radioBinary", "radioImageSelect", "radioListenMatch", "radioListenRecognize", "radioSelect", "readComprehension", "reverseAssist", "sameDifferent", "select", "selectPronunciation", "selectTranscription", "svgPuzzle", "syllableTap", "syllableListenTap", "speak", "tapCloze", "tapClozeTable", "tapComplete", "tapCompleteTable", "tapDescribe", "translate", "transliterate", "transliterationAssist", "typeCloze", "typeClozeTable", "typeComplete", "typeCompleteTable", "writeComprehension"];
let farmStart;
try {
const s = new Date(_user.streakData?.currentStreak?.startDate || Date.now());
s.setDate(s.getDate() - 1);
farmStart = s;
} catch {
const n = new Date();
n.setDate(n.getDate() - 1);
farmStart = n;
}
_setBtnRunning('DH_Streak_Btn');
for (let i = 0; i < days; i++) {
if (!_running) break;
_setBtnProgress('DH_Streak_Btn', Math.floor((i / days) * 100));
const simDay = new Date(farmStart);
simDay.setDate(simDay.getDate() - i);
const end = Math.floor(simDay.getTime() / 1000);
let sess = null;
await new Promise(r => GM_xmlhttpRequest({
method: 'POST',
url: 'https://www.duolingo.com/2017-06-30/sessions',
headers: _hdrs,
data: JSON.stringify({
challengeTypes: CH,
fromLanguage: _user.fromLanguage,
isFinalLevel: false,
isV2: true,
juicy: true,
learningLanguage: _user.learningLanguage,
smartTipsVersion: 2,
type: 'GLOBAL_PRACTICE'
}),
onload: res => { if (res.status === 200) sess = JSON.parse(res.responseText); r(); },
onerror: () => r(),
timeout: 15000,
ontimeout: () => r()
}));
if (sess?.id) {
await new Promise(r => GM_xmlhttpRequest({
method: 'PUT',
url: `https://www.duolingo.com/2017-06-30/sessions/${sess.id}`,
headers: _hdrs,
data: JSON.stringify({
...sess,
heartsLeft: 5,
startTime: end - 1,
endTime: end,
enableBonusPoints: false,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true
}),
onload: () => r(),
onerror: () => r(),
timeout: 15000,
ontimeout: () => r()
}));
}
}
if (_running) {
_setBtnProgress('DH_Streak_Btn', 100);
_setBtnDone('DH_Streak_Btn', _t('btn_done'));
_notif('🔥', 'Streak Farm Done!', `Restored ${days} streak days.`);
setTimeout(_connect, 1500);
setTimeout(() => _resetBtn('DH_Streak_Btn', _t('btn_run')), 3000);
}
}
async function _farmPractice(count) {
_lessonsToSolve = count;
_currentLessonCount = 0;
if (_lessonSolving) {
_notif('⚠️', 'Busy', 'Practice farm already running.');
return;
}
_lessonSolving = true;
const btn = document.getElementById('DH_Practice_Btn');
_setBtnState('DH_Practice_Btn', _C_RED, _t('btn_stop'));
_notif('📚', 'Farm Practice', 'Navigating to practice...', 3);
if (!window.location.pathname.startsWith('/practice')) {
window.location.assign('/practice');
return;
}
await _solveCurrentLesson();
}
async function _solveCurrentLesson() {
let waited = 0;
while (!document.querySelector('[data-test="challenge"]') && !document.querySelector('._3yE3H') && waited < 10000 && _lessonSolving) {
await _sleep(500);
waited += 500;
}
if (!_lessonSolving) return;
await new Promise(resolve => {
let lastId = null,
solving = false,
ticks = 0;
const MAX = 240;
const clickNext = () => {
const nb = document.querySelector('[data-test="player-next"]') || document.querySelector('[data-test="stories-player-continue"]') || document.querySelector('[data-test="stories-player-done"]');
if (!nb || nb.getAttribute('aria-disabled') === 'true' || nb.disabled) return;
nb.click();
setTimeout(() => {
if (!nb.disabled) nb.click();
}, 5);
};
const iv = setInterval(async () => {
try {
if (!_lessonSolving) {
clearInterval(iv);
resolve();
return;
}
if (++ticks > MAX) {
clearInterval(iv);
resolve();
return;
}
const done = document.querySelector('[data-test="session-over"]') || document.querySelector('[data-test="session-complete-slide"]') || document.querySelector('[data-test="session-complete"]');
if (done) {
clearInterval(iv);
_currentLessonCount++;
try {
const s = JSON.parse(sessionStorage.getItem('dh2_practice') || '{}');
s.done = _currentLessonCount;
sessionStorage.setItem('dh2_practice', JSON.stringify(s));
} catch {}
await _sleep(500);
resolve();
return;
}
if (solving) return;
let el = document.querySelector('._3yE3H') || document.querySelector('[data-test="challenge"]') || document.querySelector('[class*="challenge"]');
// Stories elements
if (!el) el = document.querySelector('[data-test="stories-choice"]')?.closest('[class]') || document.querySelector('[data-test="challenge-tap-token-text"]')?.closest('[class]') || document.querySelector('.FmlUF');
if (!el) {
clickNext();
return;
}
const ri = _autoSolver.findReact(el);
window.sol = ri?.props?.currentChallenge;
if (!window.sol) {
clickNext();
return;
}
const cid = `${window.sol.type}:${window.sol.id||JSON.stringify(window.sol.correctIndex??window.sol.correctTokens??window.sol.correctSolutions?.[0]??'')}`;
if (cid === lastId) {
clickNext();
return;
}
const type = _autoSolver.determineChallengeType();
if (!type) {
clickNext();
return;
}
solving = true;
lastId = cid;
try {
await _autoSolver.handleChallenge(type);
} catch {}
await _sleep(350);
clickNext();
await _sleep(600);
solving = false;
} catch {
solving = false;
}
}, 500);
setTimeout(() => {
clearInterval(iv);
resolve();
}, 180000);
});
if (!_lessonSolving) return;
if (_lessonsToSolve > 0 && _currentLessonCount >= _lessonsToSolve) {
_notif('✅', 'Farm Practice Done!', `Completed ${_currentLessonCount} practice(s).`);
_stopPractice();
return;
}
_notif('📚', 'Farm Practice', `Done ${_currentLessonCount}${_lessonsToSolve>0?' / '+_lessonsToSolve:''} — loading next...`, 2);
await _sleep(800);
if (_lessonSolving) window.location.assign('/practice');
}
function _stopPractice() {
_lessonSolving = false;
_setBtnState('DH_Practice_Btn', _C_BLUE, _t('btn_run'));
const btn = document.getElementById('DH_Practice_Btn');
if (btn) btn.disabled = !_user;
}
function _resumePracticeIfNeeded() {
const saved = sessionStorage.getItem('dh2_practice');
if (!saved) return;
try {
const {
active,
count,
done
} = JSON.parse(saved);
if (!active) return;
_lessonsToSolve = count;
_currentLessonCount = done;
sessionStorage.setItem('dh2_practice', JSON.stringify({
active: true,
count,
done
}));
if (window.location.pathname.startsWith('/practice')) {
_lessonSolving = true;
if (_user) {
_setBtnState('DH_Practice_Btn', _C_RED, _t('btn_stop'));
_solveCurrentLesson();
} else {
const orig = _setConn.bind(null);
const check = setInterval(() => {
if (_user) {
clearInterval(check);
_setBtnState('DH_Practice_Btn', _C_RED, _t('btn_stop'));
_solveCurrentLesson();
}
}, 500);
}
}
} catch {}
}
async function _farmLeague() {
const LB = 'https://duolingo-leaderboards-prod.duolingo.com/leaderboards/7d9f5dd1-8423-491a-91f2-2532052038ce';
const prog = document.getElementById('DH_League_Prog');
const fill = document.getElementById('DH_League_Fill');
if (prog) prog.classList.add('on');
_setBtnState('DH_League_Btn', _C_RED, _t('btn_stop'));
while (_running) {
try {
const r = await _gm('GET', `${LB}/users/${_sub}?client_unlocked=true&_=${Date.now()}`);
if (r.status !== 200) {
await _sleep(3000);
continue;
}
const data = JSON.parse(r.responseText);
const ranks = data?.active?.cohort?.rankings || [];
const me = ranks.find(u => u.user_id == _sub);
if (!me) {
_notif('⚠️', 'Not in league!', 'Join a league first.');
break;
}
const rank = ranks.indexOf(me) + 1;
const top1 = ranks[0];
const gap = top1.score - me.score;
if (rank === 1 && gap <= 0) {
_notif('🏆', 'League #1!', 'You reached Rank #1!');
break;
}
if (fill) fill.style.width = Math.min(95, Math.floor((me.score / Math.max(top1.score, 1)) * 100)) + '%';
if (gap + 100 > 0) {
const ok = await _storyXP(469);
if (!ok) await _sleep(3000);
}
await _sleep(_delay);
} catch {
await _sleep(5000);
}
}
_resetLeague();
}
function _getQuestTimestamp(goalId) {
const m = goalId.match(/^(\d{4})_(\d{2})_monthly/);
if (m) {
return new Date(Date.UTC(parseInt(m[1]), parseInt(m[2]) - 1, 15, 12, 0, 0)).toISOString();
}
return new Date().toISOString();
}
async function _getGoals() {
return new Promise(r => GM_xmlhttpRequest({
method: 'GET',
url: `${GOALS_API}/schema?ui_language=en&_=${Date.now()}`,
headers: _goalHdrs(_jwt),
onload: res => r(res.status === 200 ? JSON.parse(res.responseText) : null),
onerror: () => r(null)
}));
}
async function _getProgress() {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
return new Promise(r => GM_xmlhttpRequest({
method: 'GET',
url: `${GOALS_API}/users/${_sub}/progress?timezone=${encodeURIComponent(tz)}&ui_language=en`,
headers: _goalHdrs(_jwt),
onload: res => r(res.status === 200 ? JSON.parse(res.responseText) : null),
onerror: () => r(null)
}));
}
async function _bruteForceGoals(metrics) {
const updates = metrics.map(m => ({
metric: m,
quantity: 2000
}));
updates.push({
metric: 'QUESTS',
quantity: 1
});
return new Promise(r => GM_xmlhttpRequest({
method: 'POST',
url: `${GOALS_API}/users/${_sub}/progress/batch`,
headers: _goalHdrs(_jwt),
data: JSON.stringify({
metric_updates: updates,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: new Date().toISOString()
}),
onload: res => r(res.status === 200),
onerror: () => r(false)
}));
}
async function _updateGoal(metric, amount, goalId) {
return new Promise(r => GM_xmlhttpRequest({
method: 'POST',
url: `${GOALS_API}/users/${_sub}/progress/batch`,
headers: _goalHdrs(_jwt),
data: JSON.stringify({
metric_updates: [{
metric,
quantity: amount
}],
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: _getQuestTimestamp(goalId)
}),
onload: res => r(res.status === 200),
onerror: () => r(false)
}));
}
async function _farmDailyQuest() {
_setBtnState('DH_Quest_Btn', _C_GRAY, _t('btn_loading'));
const [schema, progress] = await Promise.all([_getGoals(), _getProgress()]);
if (!schema || !progress) {
_notif('❌', 'Error', 'Could not load quest data.');
_resetBtn('DH_Quest_Btn', _t('btn_run'));
return;
}
const earned = new Set(progress.badges?.earned || []);
const daily = (schema.goals || []).filter(g => g.category && g.category.includes('DAILY'));
const metrics = new Set(daily.filter(g => !earned.has(g.badgeId) && !earned.has(g.goalId) && g.metric).map(g => g.metric));
if (!metrics.size) {
_notif('✅', 'All Done!', 'All daily quests completed.');
_setBtnDone('DH_Quest_Btn', _t('btn_done'));
setTimeout(() => _resetBtn('DH_Quest_Btn', _t('btn_run')), 3000);
return;
}
_setBtnState('DH_Quest_Btn', _C_RED, _t('btn_running'));
const ok = await _bruteForceGoals(Array.from(metrics));
if (ok) {
_notif('✅', 'Daily Quests Done!', `Completed ${metrics.size} metric(s).`);
_setBtnDone('DH_Quest_Btn', _t('btn_done'));
setTimeout(() => _resetBtn('DH_Quest_Btn', _t('btn_run')), 3000);
} else {
_notif('❌', 'Error', 'Quest completion failed.');
_resetBtn('DH_Quest_Btn', _t('btn_run'));
}
}
function _formatItem(id) {
return id.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
function _categorizeItem(item) {
const id = item.id || '';
if (id.includes('streak_freeze')) return {
cat: 'Streak Freezes',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/icons/216ddc11afcbb98f44e53d565ccf479e.svg'
};
if (id.includes('xp_boost')) return {
cat: 'XP Boosts',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/icons/68c1fd0f467456a4c607ecc0ac040533.svg'
};
if (id.includes('health') || id.includes('heart')) return {
cat: 'Hearts',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/hearts/547ffcf0e6256af421ad1a32c26b8f1a.svg'
};
if (id.includes('gem')) return {
cat: 'Gems',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg'
};
if (item.type === 'outfit') return {
cat: 'Outfits',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/vendor/0cecd302cf0bcd0f73d51768feff75fe.svg'
};
if (id.includes('free_taste')) return {
cat: 'Free Taste',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/super/11db6cd6f69cb2e3c5046b915be8e669.svg'
};
return {
cat: 'Misc',
icon: 'https://d35aaqx5ub95lt.cloudfront.net/images/leagues/9fadb349c2ece257386a0e576359c867.svg'
};
}
async function _getShopItems() {
return new Promise(r => GM_xmlhttpRequest({
method: 'GET',
url: 'https://www.duolingo.com/2023-05-23/shop-items',
headers: _hdrs,
onload: res => {
try {
if (res.status === 200) r(JSON.parse(res.responseText).shopItems || []);
else r([]);
} catch {
r([]);
}
},
onerror: () => r([])
}));
}
async function _buyShopItem(itemId) {
const payload = {
itemName: itemId,
isFree: true,
consumed: true,
fromLanguage: _user.fromLanguage,
learningLanguage: _user.learningLanguage
};
return new Promise(r => GM_xmlhttpRequest({
method: 'POST',
url: `https://www.duolingo.com/2017-06-30/users/${_sub}/shop-items`,
headers: _hdrs,
data: JSON.stringify(payload),
onload: res => r(res.status === 200),
onerror: () => r(false)
}));
}
let _allShopItems = [];
function _renderShop(items, filter = '') {
const container = document.getElementById('DH_Shop_Container');
container.innerHTML = '';
const valid = items.filter(i => i.currencyType === 'XGM' && !i.id.includes('gift'));
const f = filter.trim().toLowerCase();
const filtered = f ? valid.filter(i => (i.name || _formatItem(i.id)).toLowerCase().includes(f)) : valid;
if (!filtered.length) {
const p = document.createElement('p');
p.className = 'DH_T2 DH_NoSel';
p.style.cssText = 'text-align:center;padding:8px 0;';
p.textContent = f ?
_t('no_items_found') :
_t('no_items_available');
container.appendChild(p);
return;
}
const ORDER = ['Streak Freezes', 'XP Boosts', 'Hearts', 'Gems', 'Outfits', 'Free Taste', 'Misc'];
const grouped = {};
filtered.forEach(i => {
const {
cat,
icon
} = _categorizeItem(i);
if (!grouped[cat]) grouped[cat] = [];
let name = i.name || _formatItem(i.id);
if (i.id.includes('xp_boost') && i.id.match(/\d+$/)) name += ' Mins';
grouped[cat].push({
...i,
displayName: name,
icon,
cat
});
});
ORDER.forEach(cat => {
if (!grouped[cat]) return;
const header = document.createElement('div');
header.className = 'DH_Cat_Header DH_NoSel';
header.textContent = cat;
container.appendChild(header);
const grid = document.createElement('div');
grid.className = 'DH_Shop_Grid';
grouped[cat].forEach(item => {
const card = document.createElement('div');
card.className = 'DH_Shop_Card';
card.innerHTML = `
<img src="${item.icon}" class="DH_Shop_Ico">
<div class="DH_Shop_Name DH_NoSel">${item.displayName}</div>
<button class="DH_Shop_Btn" data-id="${item.id}">GET</button>`;
const ico = card.querySelector('.DH_Shop_Ico');
if (ico) ico.onerror = function() {
this.style.display = 'none';
const fb = document.createElement('div');
fb.style.cssText = 'width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:22px;';
fb.textContent = '🎁';
this.parentNode.insertBefore(fb, this);
};
const btn = card.querySelector('.DH_Shop_Btn');
btn.onclick = async () => {
btn.className = 'DH_Shop_Btn loading';
btn.textContent = '...';
setTimeout(() => {
if (btn.textContent === '...') btn.textContent = '50%';
}, 300);
const ok = await _buyShopItem(item.id);
btn.textContent = '100%';
setTimeout(() => {
if (ok) {
btn.className = 'DH_Shop_Btn got';
btn.textContent = _t('btn_got');
_notif('🛒', 'Shop', 'Got ' + item.displayName + '!');
setTimeout(() => {
btn.className = 'DH_Shop_Btn';
btn.textContent = _t('btn_get');
}, 3000);
} else {
btn.className = 'DH_Shop_Btn fail';
btn.textContent = _t('btn_failed');
setTimeout(() => {
btn.className = 'DH_Shop_Btn';
btn.textContent = _t('btn_get');
}, 2000);
_notif('❌', 'Shop', 'Failed to get item.');
}
}, 300);
};
grid.appendChild(card);
});
container.appendChild(grid);
});
}
async function _loadShop() {
const container = document.getElementById('DH_Shop_Container');
const cached = localStorage.getItem('dh2_shop');
if (cached) {
try {
const items = JSON.parse(cached);
if (items && items.length) {
_allShopItems = items;
_renderShop(items);
return;
}
} catch {
localStorage.removeItem('dh2_shop');
}
}
container.innerHTML = `
<p class="DH_T2 DH_NoSel"
style="text-align:center;padding:8px 0;">
${_t('loading_shop')}
</p>
`;
const items = await _getShopItems();
if (items.length) localStorage.setItem('dh2_shop', JSON.stringify(items));
_allShopItems = items;
_renderShop(items);
}
// _installFakeSuper() has been merged into the unified page-context hook
// at the top of the script (together with Lesson Shortener & Stories Shortener).
// No separate call needed here.
function _resetLeague() {
const btn = document.getElementById('DH_League_Btn');
if (!btn) return;
btn.disabled = false;
_setBtnState('DH_League_Btn', _C_BLUE, _t('btn_run'));
const fill = document.getElementById('DH_League_Fill');
if (fill) fill.style.width = '0%';
const prog = document.getElementById('DH_League_Prog');
if (prog) setTimeout(() => prog.classList.remove('on'), 2000);
}
function _v1UpdateDisplayNow() {
requestAnimationFrame(() => {
const xi = document.getElementById('DH_V1_XP_Input');
const gi = document.getElementById('DH_V1_Gem_Input');
const si = document.getElementById('DH_V1_Streak_Input');
if (xi) {
xi.value = _v1Earned.xp > 0 ? String(_v1Earned.xp) : '';
}
if (gi) {
gi.value = _v1Earned.gems > 0 ? String(_v1Earned.gems) : '';
}
if (si) {
si.value = _v1Earned.streak > 0 ? String(_v1Earned.streak) : '';
}
});
}
function _v1SetBtnState(btnId, cfg, label) {
const btn = document.getElementById(btnId);
if (!btn) return;
const lbl = btn.querySelector('.DH_Btn_Label');
if (!lbl) return;
btn.style.background = cfg.bg;
btn.style.outline = `2px solid ${cfg.outline}`;
btn.style.outlineOffset = '-2px';
lbl.style.color = cfg.tc;
lbl.textContent = label;
}
function _v1SetProg(id, pct) {
const prog = document.getElementById(id + '_Prog');
const fill = document.getElementById(id + '_Fill');
if (prog && !prog.classList.contains('on')) prog.classList.add('on');
if (fill) fill.style.width = Math.min(100, Math.max(1, pct)) + '%';
}
function _v1ClearProg(id) {
const prog = document.getElementById(id + '_Prog');
const fill = document.getElementById(id + '_Fill');
setTimeout(() => {
if (prog) prog.classList.remove('on');
}, 2000);
if (fill) fill.style.width = '0%';
}
let _v1SkillId = null;
async function _v1FetchSkillId() {
if (_v1SkillId) return _v1SkillId;
try {
const r = await _gm('GET',
`https://www.duolingo.com/2017-06-30/users/${_sub}?fields=currentCourse{pathSectioned{units{levels{pathLevelMetadata{skillId},pathLevelClientData{skillId}}}}}`
);
if (r.status !== 200) return null;
const d = JSON.parse(r.responseText);
const sections = d.currentCourse?.pathSectioned || [];
for (const sec of sections)
for (const unit of (sec.units || []))
for (const lvl of (unit.levels || [])) {
const sid = lvl.pathLevelMetadata?.skillId || lvl.pathLevelClientData?.skillId;
if (sid) {
_v1SkillId = sid;
return sid;
}
}
} catch {}
return null;
}
async function _v1XP110Once() {
const sid = await _v1FetchSkillId();
if (!sid) return false;
try {
const sr = await _gm('POST', 'https://www.duolingo.com/2017-06-30/sessions', {
challengeTypes: [],
fromLanguage: _user.fromLanguage,
learningLanguage: _user.learningLanguage,
type: 'UNIT_TEST',
skillIds: [sid]
});
if (!sr || sr.status !== 200) return false;
const sess = JSON.parse(sr.responseText);
const now = Math.floor(Date.now() / 1000);
const ur = await _gm('PUT', `https://www.duolingo.com/2017-06-30/sessions/${sess.id}`, {
id: sess.id,
metadata: sess.metadata,
type: 'UNIT_TEST',
fromLanguage: _user.fromLanguage,
learningLanguage: _user.learningLanguage,
challenges: [],
adaptiveChallenges: [],
sessionExperimentRecord: [],
experiments_with_treatment_contexts: [],
adaptiveInterleavedChallenges: [],
sessionStartExperiments: [],
trackingProperties: [],
ttsAnnotations: [],
heartsLeft: 0,
startTime: now,
enableBonusPoints: true,
endTime: now + 60,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true,
hasBoost: true,
happyHourBonusXp: 10,
pathLevelSpecifics: {
unitIndex: 0
}
});
if (ur && ur.status === 200) {
const d = JSON.parse(ur.responseText);
return d?.awardedXp || d?.xpGain || 110;
}
} catch {}
return false;
}
async function _v1FarmXP() {
_v1Earned.xp = 0;
_v1UpdateDisplayNow();
_v1SetBtnState('DH_V1_XP_Btn', _C_RED, _t('btn_stop'));
_v1SetProg('DH_V1_XP', 1);
// Always start with story API (499 XP), silently fallback to global API (110 XP) on failure
let use499 = true;
let cons429 = 0;
const MAX_429 = 2;
let fallbackErrors = 0;
const MAX_FALLBACK = 5;
let loopPct = 0;
let fallbackLoops = 0; // count loops in fallback mode, re-probe every 10
// Slug is fixed (fr-en-le-passeport), no need to probe before starting
_workingSlug = 'fr-en-le-passeport';
_workingSlugFrom = 'fr';
_workingSlugLearn = 'en';
_v1FetchSkillId();
while (_v1Running && _v1Task === 'xp') {
if (use499) {
let status = 0;
try {
const now = Math.floor(Date.now() / 1000);
const dur = Math.floor(Math.random() * 121 + 300);
const r = await _gm('POST', `https://stories.duolingo.com/api2/stories/${_workingSlug}/complete`, {
awardXp: true,
completedBonusChallenge: true,
fromLanguage: _workingSlugFrom,
learningLanguage: _workingSlugLearn,
hasXpBoost: false,
illustrationFormat: 'svg',
isFeaturedStoryInPracticeHub: true,
isLegendaryMode: true,
isV2Redo: false,
isV2Story: false,
masterVersion: true,
maxScore: 0,
score: 0,
happyHourBonusXp: 469,
startTime: now,
endTime: now + dur
});
status = r.status;
} catch {}
if (status === 200) {
cons429 = 0;
fallbackErrors = 0;
_v1Earned.xp += 499;
_v1UpdateDisplayNow();
loopPct = (loopPct + 2) % 99 + 1;
_v1SetProg('DH_V1_XP', loopPct);
} else if (status === 429) {
cons429++;
if (cons429 >= MAX_429) {
use499 = false;
fallbackLoops = 0;
}
await _sleep(_delay * 2);
continue;
} else {
use499 = false;
fallbackLoops = 0;
continue;
}
} else {
// Global API — DuoFarmer-style UNIT_TEST session (110 XP)
// Every 10 fallback loops, try story API again
if (fallbackLoops > 0 && fallbackLoops % 10 === 0) {
use499 = true;
cons429 = 0;
}
fallbackLoops++;
const earned = await _v1XP110Once();
if (earned) {
fallbackErrors = 0;
_v1Earned.xp += earned;
_v1UpdateDisplayNow();
loopPct = (loopPct + 1) % 99 + 1;
_v1SetProg('DH_V1_XP', loopPct);
} else {
fallbackErrors++;
if (fallbackErrors >= MAX_FALLBACK) {
_notif('❌', 'V1 XP', 'Too many errors, stopping.');
break;
}
await _sleep(_delay * 3);
continue;
}
}
await _sleep(_delay);
}
_v1ClearProg('DH_V1_XP');
_v1SetBtnState('DH_V1_XP_Btn', _C_BLUE, _t('btn_run'));
document.getElementById('DH_V1_XP_Btn').disabled = !_user;
_v1Running = false;
_v1Task = null;
if (_v1Earned.xp > 0) {
_notif('✅', 'XP Farm Done!', `Farmed ${_v1Earned.xp.toLocaleString()} XP.`);
setTimeout(_connect, 1500);
}
}
async function _v1FarmGems() {
_v1Earned.gems = 0;
_v1UpdateDisplayNow();
_v1SetBtnState('DH_V1_Gem_Btn', _C_RED, _t('btn_stop'));
_v1SetProg('DH_V1_Gem', 1);
let loopPct = 0;
outer:
while (_v1Running && _v1Task === 'gems') {
const rewards = await _fetchGemRewards();
if (rewards.length === 0) {
_notif('⚠️', 'Gem Farm', 'No rewards available. Retrying…', 3);
await _sleep(_delay * 2);
continue;
}
const gemsBefore = await _getGemCount() ?? (_user?.gems ?? 0);
// Fire all rewards in parallel batches — no inter-batch delay
for (let i = 0; i < rewards.length; i += THREADS) {
if (!_v1Running || _v1Task !== 'gems') break outer;
const batch = rewards.slice(i, i + THREADS);
await Promise.all(batch.map(r => _exploitGemReward(r.id)));
}
// Single gem count check after full pass
const now = await _getGemCount();
if (now !== null) {
const cycle = Math.max(0, now - gemsBefore);
_v1Earned.gems += cycle;
_v1UpdateDisplayNow();
loopPct = (loopPct + 1) % 99 + 1;
_v1SetProg('DH_V1_Gem', loopPct);
if (_user) {
_user.gems = now;
_v1SyncUser();
}
}
// Short cooldown before next pass
await _sleep(Math.max(200, _delay / 2));
}
_v1ClearProg('DH_V1_Gem');
_v1SetBtnState('DH_V1_Gem_Btn', _C_BLUE, _t('btn_run'));
document.getElementById('DH_V1_Gem_Btn').disabled = !_user;
_v1Running = false;
_v1Task = null;
if (_v1Earned.gems > 0) {
_notif('✅', 'Gem Farm Done!', `+${_v1Earned.gems.toLocaleString()} gems gained.`);
setTimeout(_connect, 1500);
}
}
async function _v1FarmStreak() {
_v1Earned.streak = 0;
_v1UpdateDisplayNow();
_v1SetBtnState('DH_V1_Streak_Btn', _C_RED, _t('btn_stop'));
_v1SetProg('DH_V1_Streak', 1);
const CH = ["assist", "characterIntro", "characterMatch", "characterPuzzle", "characterSelect", "characterTrace", "characterWrite", "completeReverseTranslation", "definition", "dialogue", "extendedMatch", "extendedListenMatch", "form", "freeResponse", "gapFill", "judge", "listen", "listenComplete", "listenMatch", "match", "name", "listenComprehension", "listenIsolation", "listenSpeak", "listenTap", "orderTapComplete", "partialListen", "partialReverseTranslate", "patternTapComplete", "radioBinary", "radioImageSelect", "radioListenMatch", "radioListenRecognize", "radioSelect", "readComprehension", "reverseAssist", "sameDifferent", "select", "selectPronunciation", "selectTranscription", "svgPuzzle", "syllableTap", "syllableListenTap", "speak", "tapCloze", "tapClozeTable", "tapComplete", "tapCompleteTable", "tapDescribe", "translate", "transliterate", "transliterationAssist", "typeCloze", "typeClozeTable", "typeComplete", "typeCompleteTable", "writeComprehension"];
let farmStart;
try {
const s = new Date(_user.streakData?.currentStreak?.startDate || Date.now());
s.setDate(s.getDate() - 1);
farmStart = s;
} catch {
farmStart = new Date();
farmStart.setDate(farmStart.getDate() - 1);
}
let dayIdx = 0;
while (_v1Running && _v1Task === 'streak') {
const simDay = new Date(farmStart);
simDay.setDate(simDay.getDate() - dayIdx);
const end = Math.floor(simDay.getTime() / 1000);
let sess = null;
await new Promise(r => GM_xmlhttpRequest({
method: 'POST',
url: 'https://www.duolingo.com/2017-06-30/sessions',
headers: _hdrs,
data: JSON.stringify({
challengeTypes: CH,
fromLanguage: _user.fromLanguage,
isFinalLevel: false,
isV2: true,
juicy: true,
learningLanguage: _user.learningLanguage,
smartTipsVersion: 2,
type: 'GLOBAL_PRACTICE'
}),
onload: res => { if (res.status === 200) sess = JSON.parse(res.responseText); r(); },
onerror: () => r(),
timeout: 15000,
ontimeout: () => r()
}));
if (sess?.id) {
await new Promise(r => GM_xmlhttpRequest({
method: 'PUT',
url: `https://www.duolingo.com/2017-06-30/sessions/${sess.id}`,
headers: _hdrs,
data: JSON.stringify({
...sess,
heartsLeft: 5,
startTime: end - 1,
endTime: end,
enableBonusPoints: false,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true
}),
onload: () => r(),
onerror: () => r(),
timeout: 15000,
ontimeout: () => r()
}));
_v1Earned.streak++;
_v1UpdateDisplayNow();
}
const loopPct = (dayIdx % 9) * 11 + 1;
_v1SetProg('DH_V1_Streak', loopPct);
dayIdx++;
}
_v1ClearProg('DH_V1_Streak');
_v1SetBtnState('DH_V1_Streak_Btn', _C_BLUE, _t('btn_run'));
document.getElementById('DH_V1_Streak_Btn').disabled = !_user;
_v1Running = false;
_v1Task = null;
if (_v1Earned.streak > 0) {
_notif('🔥', 'Streak Farm Done!', `Farmed ${_v1Earned.streak} streak days.`);
setTimeout(_connect, 1500);
}
}
function _v1RunToggle(task) {
if (_v1Running && _v1Task === task) {
_v1Running = false;
_v1Task = null;
return;
}
if (_v1Running) {
_notif('⚠️', 'Busy', 'Stop current V1 farm first.');
return;
}
if (!_user) {
_notif('⚠️', 'Not connected', 'Please wait.');
return;
}
_v1Running = true;
_v1Task = task;
if (task === 'xp') _v1FarmXP();
if (task === 'gems') _v1FarmGems();
if (task === 'streak') _v1FarmStreak();
}
async function _run(type, val) {
if (_running) {
_running = false;
_notif('⏹️', 'Stopped', 'Farm stopped.');
return;
}
if (!_user) {
_notif('⚠️', 'Not connected', 'Please wait.');
return;
}
_running = true;
_task = type;
try {
if (type === 'xp') await _farmXP(val);
if (type === 'gem') await _farmGems();
if (type === 'streak') await _farmStreak(val);
if (type === 'league') await _farmLeague();
} catch (e) {
_notif('❌', 'Error', e.message);
}
// If stopped mid-farm, reset XP/Streak buttons (Gem and League reset themselves)
if (!_running) {
if (type === 'xp') _resetBtn('DH_XP_Btn', _t('btn_get'));
if (type === 'streak') _resetBtn('DH_Streak_Btn', _t('btn_run'));
}
_running = false;
_task = null;
}
function _accGetAll() {
try {
return JSON.parse(localStorage.getItem('dh2_accounts') || '[]');
} catch {
return [];
}
}
function _accSetAll(arr) {
localStorage.setItem('dh2_accounts', JSON.stringify(arr));
}
function _accSaveCurrent() {
if (!_user || !_jwt || !_sub) {
_notif('⚠️', 'Not connected', 'Please wait for connection.');
return;
}
const all = _accGetAll();
if (all.find(a => a.id == _sub)) {
_notif('ℹ️', 'Already saved', 'This account is already in the list.');
return;
}
let pic = '';
if (_user.picture) {
pic = _user.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!pic.endsWith('/xlarge') && pic.includes('duolingo.com/ssr-avatars')) pic += '/xlarge';
}
all.push({
id: _sub,
username: _user.username || 'User',
pic,
token: _jwt
});
_accSetAll(all);
_notif('✅', 'Account Saved', `Saved account: ${_user.username}`);
_renderAccounts();
}
function _accRemove(id) {
const all = _accGetAll().filter(a => a.id != id);
_accSetAll(all);
_renderAccounts();
_notif('🗑️', 'Removed', 'Account removed from list.');
}
function _accLogin(id) {
const acc = _accGetAll().find(a => a.id == id);
if (!acc) {
_notif('⚠️', 'Not found', 'Account not found.');
return;
}
document.cookie = `jwt_token=${acc.token}; domain=.duolingo.com; path=/; max-age=31536000`;
window.location.reload();
}
function _renderAccounts() {
const wrap = document.getElementById('DH_AccList_Wrap');
if (!wrap) return;
const all = _accGetAll();
const saveBtn = document.getElementById('DH_AccSave_Btn');
if (saveBtn) saveBtn.disabled = !_user;
if (all.length === 0) {
wrap.innerHTML = `
<p class="DH_T2 DH_NoSel"
style="text-align:center;padding:8px 0;">
${_t('no_saved_accounts')}
</p>
`;
return;
}
wrap.innerHTML = '';
all.forEach(acc => {
const card = document.createElement('div');
card.className = 'DH_Acc_Card';
const isCurrentUser = _sub && acc.id == _sub;
const picHtml = acc.pic ?
`<img src="${acc.pic}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;" onerror="this.parentNode.innerHTML='👤'">` :
'👤';
card.innerHTML = `
<div class="DH_Acc_Avatar">${picHtml}</div>
<div class="DH_Acc_Info">
<p class="DH_Acc_Name DH_NoSel">${acc.username}</p>
${isCurrentUser
? `<p class="DH_Acc_Sub active DH_NoSel"><svg width="8" height="8" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="rgb(var(--DH-green))"><animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/></circle></svg>Active</p>`
: `<p class="DH_Acc_Sub DH_NoSel">ID: ${String(acc.id).slice(0,8)}…</p>`
}
</div>
<div class="DH_Acc_Action_Row">
${!isCurrentUser?`<button class="DH_Acc_Btn login" data-id="${acc.id}">LOG IN</button>`:''}
<button class="DH_Acc_Btn del" data-id="${acc.id}">✕</button>
</div>
`;
card.querySelector('.del').addEventListener('click', e => {
e.stopPropagation();
_accRemove(acc.id);
});
const loginBtn = card.querySelector('.login');
if (loginBtn) loginBtn.addEventListener('click', e => {
e.stopPropagation();
_accLogin(acc.id);
});
wrap.appendChild(card);
});
}
async function _accRefreshAll() {
const all = _accGetAll();
if (!all.length) return;
let changed = false;
await Promise.all(all.map(async (acc) => {
try {
const r = await _gm(
'GET',
`https://www.duolingo.com/2017-06-30/users/${acc.id}?fields=id,username,picture`,
null,
{ 'Authorization': 'Bearer ' + acc.token, 'Content-Type': 'application/json' }
);
if (r.status !== 200) return;
const fresh = JSON.parse(r.responseText);
let pic = '';
if (fresh.picture) {
pic = fresh.picture.replace(/\/(medium|large|small)$/, '/xlarge');
if (!pic.endsWith('/xlarge') && pic.includes('duolingo.com/ssr-avatars')) pic += '/xlarge';
}
if (fresh.username && fresh.username !== acc.username) {
acc.username = fresh.username;
changed = true;
}
if (pic && pic !== acc.pic) {
acc.pic = pic;
changed = true;
}
} catch { /* token hết hạn hoặc lỗi mạng — bỏ qua */ }
}));
if (changed) {
_accSetAll(all);
_renderAccounts();
}
}
function _goalHdrsLocal(jwt) {
return {
'Content-Type': 'application/json',
'x-requested-with': 'XMLHttpRequest',
'accept': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + jwt
};
}
function _mqGm(method, url, data, hdrs) {
return new Promise((res, rej) => GM_xmlhttpRequest({
method,
url,
headers: hdrs,
data: data ? JSON.stringify(data) : null,
onload: r => res(r),
onerror: () => rej(new Error('Network')),
timeout: 15000,
ontimeout: () => rej(new Error('Timeout'))
}));
}
function _mqGetTimestamp(goalId) {
const m = goalId.match(/^(\d{4})_(\d{2})_monthly/);
if (m) {
const d = new Date(Date.UTC(parseInt(m[1]), parseInt(m[2]) - 1, 15, 12, 0, 0));
return d.toISOString();
}
return new Date().toISOString();
}
async function _loadMonthlyQuests() {
const cont = document.getElementById('DH_MQ_Container');
if (!cont) return;
const claimBtn = document.getElementById('DH_MQ_ClaimAll_Btn');
if (!_jwt || !_sub) {
cont.innerHTML = `<p class="DH_T2 DH_NoSel" style="text-align:center;padding:8px 0;">${_t('not_connected')}</p>`;
return;
}
cont.innerHTML = `<p class="DH_T2 DH_NoSel" style="text-align:center;padding:8px 0;">⟳ ${_t('loading_quests')}</p>`;
const gh = _goalHdrsLocal(_jwt);
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const [schemaR, progR] = await Promise.all([
_mqGm('GET', `${GOALS_API}/schema?ui_language=en&_=${Date.now()}`, null, gh),
_mqGm('GET', `${GOALS_API}/users/${_sub}/progress?timezone=${tz}&ui_language=en`, null, gh)
]);
if (schemaR.status !== 200 || progR.status !== 200) throw new Error('API error');
const schema = JSON.parse(schemaR.responseText);
const prog = JSON.parse(progR.responseText);
const progress = prog.goals?.progress || {};
const earned = new Set(prog.badges?.earned || []);
_questState = {
schema,
progress,
earned
};
_renderMonthlyQuests(schema, progress, earned, gh);
if (claimBtn) {
claimBtn.disabled = false;
}
} catch (e) {
cont.innerHTML = `<p class="DH_T2 DH_NoSel" style="text-align:center;color:rgb(var(--DH-red));padding:8px 0;">${_t('quest_failed')}</p>`;
}
}
function _renderMonthlyQuests(schema, progress, earned, gh) {
const cont = document.getElementById('DH_MQ_Container');
if (!cont) return;
cont.innerHTML = '';
const now = new Date();
const yr = now.getFullYear().toString();
const mo = (now.getMonth() + 1).toString().padStart(2, '0');
const mReg = new RegExp(`^${yr}_${mo}_monthly`);
const map = new Map();
schema.goals.forEach(g => {
const m2 = g.goalId.match(/^(\d{4}_\d{2})_monthly/);
if (!m2) return;
const key = m2[1];
const existing = map.get(key);
if (!existing) {
map.set(key, g);
} else {
const existIsChallenge = existing.category?.includes('CHALLENGE');
const newIsChallenge = g.category?.includes('CHALLENGE');
if (!existIsChallenge && newIsChallenge) map.set(key, g);
}
});
const goals = [...map.values()].filter(g => g.goalId.match(mReg) || true).reverse();
const monthly = goals.filter(g => g.category && g.category.includes('MONTHLY'));
if (monthly.length === 0) {
cont.innerHTML = `<p class="DH_T2 DH_NoSel" style="text-align:center;padding:8px 0;">${_t('no_monthly_quests')}</p>`;
return;
}
monthly.forEach(g => {
const isEarned = earned.has(g.badgeId) || earned.has(g.goalId);
let cur = 0;
const raw = progress[g.goalId];
if (typeof raw === 'number') cur = raw;
else if (raw && typeof raw === 'object') cur = raw.progress || 0;
const tgt = g.threshold || 10;
let pct = Math.min(100, (cur / tgt) * 100);
if (isEarned) {
pct = 100;
cur = tgt;
}
const remaining = Math.max(0, tgt - cur);
let icon = 'https://d35aaqx5ub95lt.cloudfront.net/images/achievement/aca5f82d97f5e67c1acb1ea05a0e6d1a.svg';
const badge = schema.badges?.find(x => x.badgeId === g.badgeId);
if (badge && badge.icon?.enabled?.lightMode) {
icon = badge.icon.enabled.lightMode.svg || badge.icon.enabled.lightMode.url || icon;
}
const item = document.createElement('div');
item.className = `DH_Quest_Item${isEarned?' done':''}`;
item.innerHTML = `
<img src="${icon}" class="DH_Quest_Icon" onerror="this.src='https://d35aaqx5ub95lt.cloudfront.net/images/achievement/aca5f82d97f5e67c1acb1ea05a0e6d1a.svg'">
<div class="DH_Quest_Info">
<p class="DH_Quest_Title DH_NoSel">${g.title?.uiString||g.goalId}</p>
<p class="DH_Quest_Meta DH_NoSel">${isEarned?'COMPLETED':''+cur+' / '+tgt+' · '+g.metric}</p>
<div class="DH_Quest_Bar_Bg"><div class="DH_Quest_Bar_Fill" style="width:${pct}%"></div></div>
</div>
${!isEarned&&remaining>0?`<button class="DH_Quest_Get_Btn" data-metric="${g.metric}" data-amount="${remaining}" data-id="${g.goalId}">GET +${remaining}</button>`:''}
${isEarned?`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0;"><circle cx="12" cy="12" r="10" fill="rgba(var(--DH-green),0.15)" stroke="rgb(var(--DH-green))" stroke-width="1.5"/><path d="M7.5 12.5l3 3 6-6" stroke="rgb(var(--DH-green))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`:''}
`;
const btn = item.querySelector('.DH_Quest_Get_Btn');
if (btn) btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = '…';
try {
const payload = {
metric_updates: [{
metric: btn.dataset.metric,
quantity: parseInt(btn.dataset.amount)
}],
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: _mqGetTimestamp(btn.dataset.id)
};
const r = await _mqGm('POST', `${GOALS_API}/users/${_sub}/progress/batch`, payload, gh);
if (r.status === 200) {
btn.textContent = '✓';
btn.classList.add('done');
_notif('✅', 'Quest Done!', 'Progress injected successfully.');
setTimeout(() => _loadMonthlyQuests(), 900);
} else {
btn.textContent = 'ERR';
btn.disabled = false;
}
} catch {
btn.textContent = 'ERR';
btn.disabled = false;
}
});
cont.appendChild(item);
});
}
async function _claimAllMonthly() {
if (!_questState || !_questState.schema) {
_notif('⚠️', 'Not loaded', 'Open Monthly Quests first.');
return;
}
const claimBtn = document.getElementById('DH_MQ_ClaimAll_Btn');
if (claimBtn) {
claimBtn.disabled = true;
const lbl = claimBtn.querySelector('.DH_Sm_Btn_Label');
if (lbl) lbl.textContent = '…';
}
const gh = _goalHdrsLocal(_jwt);
const uniqueMetrics = new Set();
_questState.schema.goals.forEach(g => {
if (g.category && g.category.includes('MONTHLY') && g.metric) uniqueMetrics.add(g.metric);
});
if (uniqueMetrics.size > 0) {
const updates = [...uniqueMetrics].map(m => ({
metric: m,
quantity: 2000
}));
updates.push({
metric: 'QUESTS',
quantity: 1
});
const payload = {
metric_updates: updates,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: new Date().toISOString()
};
try {
await _mqGm('POST', `${GOALS_API}/users/${_sub}/progress/batch`, payload, gh);
_notif('✅', 'Monthly Quests', 'All monthly quests claimed!');
setTimeout(() => _loadMonthlyQuests(), 900);
} catch {
_notif('❌', 'Error', 'Failed to claim quests.');
}
} else {
_notif('ℹ️', 'Nothing to do', 'No monthly metrics found.');
}
if (claimBtn) {
claimBtn.disabled = false;
const lbl = claimBtn.querySelector('.DH_Sm_Btn_Label');
if (lbl) lbl.textContent = _t('btn_claim');
}
}
document.getElementById('DH_Hide_Btn').addEventListener('click', () => _doHide(!_hidden));
document.getElementById('DH_SwitchV1_Btn').addEventListener('click', () => {
_v1Mode = true;
document.getElementById('DH_SwitchV1_Btn').style.display = 'none';
document.getElementById('DH_SwitchV2_Btn').style.display = '';
_v1SyncUser();
_applyLang();
_goPage('V1');
});
document.getElementById('DH_SwitchV2_Btn').addEventListener('click', () => {
_v1Mode = false;
if (_v1Running) {
_v1Running = false;
_v1Task = null;
}
document.getElementById('DH_SwitchV2_Btn').style.display = 'none';
document.getElementById('DH_SwitchV1_Btn').style.display = '';
_goPage(1);
});
document.getElementById('DH_V1_XP_Btn').addEventListener('click', () => _v1RunToggle('xp'));
document.getElementById('DH_V1_Gem_Btn').addEventListener('click', () => _v1RunToggle('gems'));
document.getElementById('DH_V1_Streak_Btn').addEventListener('click', () => _v1RunToggle('streak'));
// ── Free Super Duolingo ──
function _getSuperJWT() {
const match = document.cookie.split('; ').find(r => r.startsWith('jwt_token='));
return match ? match.split('=')[1] : null;
}
async function _activateFreeSuper(btnId, lblId, progId, fillId) {
const jwt = _getSuperJWT();
if (!jwt) {
_notif('⚠️', 'Not logged in', 'Could not find your Duolingo token. Please log in first.', 5);
return;
}
const btn = document.getElementById(btnId);
const lbl = document.getElementById(lblId);
const prog = document.getElementById(progId);
const fill = document.getElementById(fillId);
btn.disabled = true;
lbl.textContent = '...';
prog.classList.add('on');
fill.style.width = '30%';
const _resetBtn = () => {
lbl.textContent = _t('btn_activate');
btn.disabled = false;
};
const _endProg = () => {
fill.style.width = '100%';
setTimeout(() => {
prog.classList.remove('on');
fill.style.width = '0%';
}, 800);
};
const _failProg = () => {
fill.style.width = '0%';
prog.classList.remove('on');
};
if (!_user || !_sub || !_hdrs) {
_failProg();
_resetBtn();
_notif('⚠️', 'Not connected', 'Please wait for connection and try again.', 5);
return;
}
fill.style.width = '60%';
try {
const payload = {
itemName: 'immersive_subscription',
isFree: true,
consumed: true,
fromLanguage: _user.fromLanguage,
learningLanguage: _user.learningLanguage,
productId: 'com.duolingo.immersive_free_trial_subscription'
};
const r = await _gm('POST', `https://www.duolingo.com/2017-06-30/users/${_sub}/shop-items`, payload);
if (r.status === 200 || r.status === 201) {
_endProg();
_resetBtn();
_notif('✅', 'Super Activated!', 'Free Super Duolingo activated!', 7);
} else {
_failProg();
_resetBtn();
_notif('❌', 'Failed', 'Activation failed. You may already have Super.', 6);
}
} catch (_) {
_failProg();
_resetBtn();
_notif('❌', 'Network error', 'Could not reach Duolingo API.', 5);
}
}
// Enable Super buttons after user connects (jwt available from cookie)
// V2 Extra Features (page 2)
const superBtn = document.getElementById('DH_Super_Activate_Btn');
superBtn.disabled = false;
superBtn.addEventListener('click', () => _activateFreeSuper('DH_Super_Activate_Btn', 'DH_Super_Activate_Lbl', 'DH_Super_Prog', 'DH_Super_Fill'));
// V1
const superV1Btn = document.getElementById('DH_V1_Super_Activate_Btn');
superV1Btn.disabled = false;
superV1Btn.addEventListener('click', () => _activateFreeSuper('DH_V1_Super_Activate_Btn', 'DH_V1_Super_Activate_Lbl', 'DH_V1_Super_Prog', 'DH_V1_Super_Fill'));
document.getElementById('DH_V1_Settings_Btn').addEventListener('click', () => {
_goPage(4);
_initHideProfileToggle();
});
document.getElementById('DH_Discord_Btn').addEventListener('click', () => window.open('https://duohacker.io.vn/discord', '_blank'));
document.getElementById('DH_GitHub_Btn').addEventListener('click', () => window.open('https://duohacker.io.vn/github', '_blank'));
document.getElementById('DH_YouTube_Btn').addEventListener('click', () => window.open('https://duohacker.io.vn/youtube', '_blank'));
document.getElementById('DH_TopSettings_Btn').addEventListener('click', () => {
_goPage(4);
_initHideProfileToggle();
});
document.getElementById('DH_Donate_Btn').addEventListener('click', () => {
window.open('https://duohacker.io.vn/donation#choose', '_blank');
});
async function _getPrivacy() {
if (!_sub || !_hdrs) return null;
try {
const r = await _gm('GET', `https://www.duolingo.com/2023-05-23/users/${_sub}/privacy-settings?fields=privacySettings`);
if (r.status !== 200) return null;
const data = JSON.parse(r.responseText);
const social = data.privacySettings?.find(x => x.id === 'disable_social');
return social ? social.enabled : null;
} catch (e) {
return null;
}
}
async function _setPrivacy(hide) {
if (!_sub || !_hdrs) return false;
try {
const r = await _gm('PATCH', `https://www.duolingo.com/2023-05-23/users/${_sub}/privacy-settings?fields=privacySettings`, {
DISABLE_SOCIAL: hide
});
return r.status === 200 || r.status === 204;
} catch (e) {
return false;
}
}
function _applyHideProfileToggle() {
const tog = document.getElementById('DH_HideProfile_Toggle');
const lbl = document.getElementById('DH_HideProfile_Status');
if (!tog || !lbl) return;
if (_privacy === null) {
lbl.textContent = _t('status_unavailable');
tog.disabled = true;
return;
}
if (!tog.dataset.dhBound) {
tog.dataset.dhBound = '1';
tog.addEventListener('change', async function() {
tog.disabled = true;
lbl.textContent = _t('status_saving');
const ok = await _setPrivacy(tog.checked);
if (ok) {
_privacy = tog.checked;
lbl.textContent = tog.checked ? _t('profile_private') : _t('profile_public');
} else {
tog.checked = !tog.checked;
lbl.textContent = _t('status_failed_retry');
}
tog.disabled = false;
});
}
tog.checked = _privacy;
tog.disabled = false;
lbl.textContent = _privacy ? _t('profile_private') : _t('profile_public');
}
function _initHideProfileToggle() {
const tog = document.getElementById('DH_HideProfile_Toggle');
const lbl = document.getElementById('DH_HideProfile_Status');
if (!tog || !lbl) return;
if (!_sub || !_hdrs) {
lbl.textContent = _t('status_not_connected');
tog.disabled = true;
return;
}
if (_privacy !== null) {
_applyHideProfileToggle();
return;
}
lbl.textContent = _t('status_loading');
tog.disabled = true;
_getPrivacy().then(v => {
_privacy = v;
_applyHideProfileToggle();
});
}
document.getElementById('DH_Settings_Btn').addEventListener('click', () => {
_goPage(2);
_initHideProfileToggle();
});
document.getElementById('DH_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_Shop_Btn').addEventListener('click', () => _goPage(3));
document.getElementById('DH_Shop_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_Settings_Back_Btn').addEventListener('click', () => {
_initHideProfileToggle();
_goBack();
});
document.getElementById('DH_AccSettings_Btn').addEventListener('click', () => {
_goPage(5);
_accRefreshAll().then(() => _renderAccounts());
});
document.getElementById('DH_AccMgr_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_AccSave_Btn').addEventListener('click', () => _accSaveCurrent());
document.getElementById('DH_MonthlyQuest_Nav_Btn').addEventListener('click', e => {
if (!e.target.closest('#DH_MonthlyQuest_Claim_Btn')) _goPage(6);
});
document.getElementById('DH_MonthlyQuest_Claim_Btn').addEventListener('click', e => {
e.stopPropagation();
_goPage(6);
});
document.getElementById('DH_MQ_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_Credits_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_Credits_Btn').addEventListener('click', () => {
const container = document.getElementById('DH_Credits_Container');
container.innerHTML = '';
CREDITS.forEach(c => {
const card = document.createElement('div');
card.className = 'DH_Credit_Card';
const header = document.createElement('div');
header.className = 'DH_Credit_Card_Header';
const img = document.createElement('img');
img.className = 'DH_Credit_Thumb';
img.src = c.thumbnail;
img.alt = '';
img.onerror = function() {
this.style.display = 'none';
};
const info = document.createElement('div');
info.style.cssText = 'display:flex;flex-direction:column;gap:1px;min-width:0;';
const name = document.createElement('p');
name.className = 'DH_Credit_Script DH_NoSel';
name.textContent = c.script;
const author = document.createElement('p');
author.className = 'DH_Credit_Author DH_NoSel';
author.textContent = 'by ' + c.author;
info.appendChild(name);
info.appendChild(author);
header.appendChild(img);
header.appendChild(info);
const task = document.createElement('p');
task.className = 'DH_Credit_Task DH_NoSel';
task.textContent = c.task;
const link = document.createElement('a');
link.className = 'DH_Credit_Link';
link.href = c.url;
link.target = '_blank';
link.rel = 'noopener';
link.textContent = 'View Script \u2197';
card.appendChild(header);
card.appendChild(task);
card.appendChild(link);
container.appendChild(card);
});
_goPage(7);
});
document.getElementById('DH_MQ_ClaimAll_Btn').addEventListener('click', () => _claimAllMonthly());
const xpI = document.getElementById('DH_XP_Input'),
xpB = document.getElementById('DH_XP_Btn');
xpI.addEventListener('input', () => {
xpB.disabled = !_user || !xpI.value || +xpI.value < 30;
});
xpB.addEventListener('click', () => {
if (_running && _task === 'xp') {
_run('xp', 0);
return;
}
if (_running) {
_notif('⚠️', 'Busy', 'Stop current farm first.');
return;
}
const v = +xpI.value;
if (v < 30) {
_notif('⚠️', 'Min 30 XP', 'Enter at least 30 XP.');
return;
}
_run('xp', v);
});
xpI.addEventListener('keydown', e => {
if (e.key === 'Enter' && !xpB.disabled) xpB.click();
});
const gmB = document.getElementById('DH_Gem_Btn');
gmB.addEventListener('click', () => {
if (_running && _task === 'gem') {
_run('gem', 0);
return;
}
if (_running) {
_notif('⚠️', 'Busy', 'Stop current farm first.');
return;
}
_run('gem', 0);
});
const stI = document.getElementById('DH_Streak_Input'),
stB = document.getElementById('DH_Streak_Btn');
stI.addEventListener('input', () => {
stB.disabled = !_user || !stI.value || +stI.value < 1;
});
stB.addEventListener('click', () => {
if (_running && _task === 'streak') {
_run('streak', 0);
return;
}
if (_running) {
_notif('⚠️', 'Busy', 'Stop current farm first.');
return;
}
const v = +stI.value;
if (v < 1) return;
_run('streak', v);
});
stI.addEventListener('keydown', e => {
if (e.key === 'Enter' && !stB.disabled) stB.click();
});
const prI = document.getElementById('DH_Practice_Input'),
prB = document.getElementById('DH_Practice_Btn');
prI.addEventListener('input', () => {
prB.disabled = !_user;
});
prB.addEventListener('click', () => {
if (_lessonSolving) {
_stopPractice();
sessionStorage.removeItem('dh2_practice');
_notif('⏹️', 'Stopped', 'Practice farm stopped.');
return;
}
if (_running) {
_notif('⚠️', 'Busy', 'Stop current farm first.');
return;
}
if (!_user) {
_notif('⚠️', 'Not connected', 'Please wait.');
return;
}
const v = parseInt(prI.value) || 0;
sessionStorage.setItem('dh2_practice', JSON.stringify({
active: true,
count: v,
done: 0
}));
_farmPractice(v);
});
document.getElementById('DH_League_Btn').addEventListener('click', () => {
if (_running && _task === 'league') {
_run('league', 0);
return;
}
if (_running) {
_notif('⚠️', 'Busy', 'Stop current farm first.');
return;
}
if (!confirm('⚠️ Warning\n\nOverusing this feature may result in your account being permanently banned from the leaderboard.\n\nDo you wish to continue?')) return;
_run('league', 0);
});
document.getElementById('DH_Quest_Btn').addEventListener('click', async () => {
if (_running) {
_notif('⚠️', 'Busy', 'Stop current farm first.');
return;
}
await _farmDailyQuest();
});
const delI = document.getElementById('DH_Delay_Input'),
delB = document.getElementById('DH_Delay_Btn');
delB.addEventListener('click', () => {
const v = parseInt(delI.value);
if (!isNaN(v) && v >= 0) {
_delay = v;
localStorage.setItem('dh2_delay', v);
_setBtnState('DH_Delay_Btn', _C_GREEN, _t('btn_saved'));
setTimeout(() => _setBtnState('DH_Delay_Btn', _C_BLUE, _t('btn_save')), 1500);
}
});
const supT = document.getElementById('DH_Super_Toggle');
supT.checked = localStorage.getItem('dh2_super') === 'true';
supT.addEventListener('change', () => {
localStorage.setItem('dh2_super', supT.checked ? 'true' : 'false');
_notif('ℹ️', 'Reload required', 'Refresh page to apply.', 4);
});
const solverT = document.getElementById('DH_Solver_Toggle');
solverT.checked = localStorage.getItem('duohacker_inject_solver') === 'true';
_INJECT_SOLVER_ENABLED = solverT.checked;
solverT.addEventListener('change', () => {
_INJECT_SOLVER_ENABLED = solverT.checked;
localStorage.setItem('duohacker_inject_solver', solverT.checked ? 'true' : 'false');
if (!_INJECT_SOLVER_ENABLED) {
_autoSolver.removeUI();
} else {
const inLesson = window.location.pathname.includes('/lesson') || window.location.pathname.includes('/practice');
if (inLesson) setTimeout(() => _autoSolver.createUI(), 300);
}
});
document.getElementById('DH_Shop_Search').addEventListener('input', e => {
_renderShop(_allShopItems, e.target.value);
});
let _hideAnimObserver = null;
const _HIDE_ANIM_STYLE_ID = 'DH_HideAnim_Style';
const _HIDE_ANIM_KEY = 'duohacker_hide_animation';
const _HIDE_PROTECT = ['#DH_Root', '#DH_Root *'];
function _applyHideAnim() {
if (document.getElementById(_HIDE_ANIM_STYLE_ID)) return;
const s = document.createElement('style');
s.id = _HIDE_ANIM_STYLE_ID;
s.textContent = `
body img:not(#DH_Root img),
body svg:not(#DH_Root svg),
body [role="img"]:not(#DH_Root [role="img"]),
body canvas:not(#DH_Root canvas),
body video:not(#DH_Root video),
body lottie-player:not(#DH_Root lottie-player) {
visibility:hidden!important;
animation:none!important;
transition:none!important;
}
body * {
animation-play-state:paused!important;
transition:none!important;
}
#DH_Root {
visibility:visible!important;
}
#DH_Root * {
animation-play-state:running!important;
visibility:visible!important;
transition:unset!important;
}`;
document.head.appendChild(s);
}
function _removeHideAnim() {
document.getElementById(_HIDE_ANIM_STYLE_ID)?.remove();
}
const _hideAnimT = document.getElementById('DH_HideAnim_Toggle');
_hideAnimT.checked = localStorage.getItem(_HIDE_ANIM_KEY) === 'true';
if (_hideAnimT.checked) _applyHideAnim();
_hideAnimT.addEventListener('change', () => {
localStorage.setItem(_HIDE_ANIM_KEY, _hideAnimT.checked ? 'true' : 'false');
_hideAnimT.checked ? _applyHideAnim() : _removeHideAnim();
});
const CREDITS = [{
script: 'DuoHacker V1',
url: 'https://github.com/not2pixel/DuoHacker/tree/main/v1',
thumbnail: 'https://raw.githubusercontent.com/not2pixel/DuoHacker/refs/heads/main/images/DuoHacker_Logo_NoBG_PNG.png',
author: 'not2pixel',
task: 'Original script - The main cores are being used in V2'
},
{
script: 'Duolingo PRO',
url: 'https://github.com/anonymoushackerIV/Duolingo-PRO',
thumbnail: 'https://www.duolingopro.net/static/favicons/duo/128/light/primary.png',
author: 'anonymoushackerIV',
task: 'The V2 UI was inspired by this script'
}
];
const main = document.getElementById('DH_Main');
const box = document.getElementById('DH_Main_Box');
main.style.bottom = `-${box.offsetHeight-8}px`;
box.style.opacity = '0';
box.style.filter = 'blur(8px)';
_doHide(false);
setTimeout(() => {
main.style.transition = '0.8s cubic-bezier(0.16,1,0.32,1)';
box.style.transition = '0.8s cubic-bezier(0.16,1,0.32,1)';
main.style.bottom = '16px';
box.style.opacity = '';
box.style.filter = '';
setTimeout(() => {
main.style.transition = '';
box.style.transition = '';
}, 800);
}, 600);
let _licenseLoaded = false;
function _loadLicense() {
if (_licenseLoaded) return;
const txt = document.getElementById('DH_License_Text');
if (!txt) return;
GM_xmlhttpRequest({
method: 'GET',
url: 'https://raw.githubusercontent.com/not2pixel/DuoHacker/refs/heads/main/LICENSE',
onload: r => {
if (!document.getElementById('DH_License_Text')) return;
document.getElementById('DH_License_Text').textContent =
r.status === 200 ? r.responseText : 'Could not load license. Please check your connection.';
_licenseLoaded = true;
},
onerror: () => {
const t = document.getElementById('DH_License_Text');
if (t) t.textContent = 'Could not load license. Please check your connection.';
}
});
}
document.getElementById('DH_License_Open_Btn').addEventListener('click', () => _goPage(9));
document.getElementById('DH_License_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_Changelog_Back_Btn').addEventListener('click', () => _goBack());
document.getElementById('DH_Conn_Btn').addEventListener('click', () => {
_renderChangelog();
_goPage(10);
});
function _renderChangelog() {
const list = document.getElementById('DH_Changelog_List');
if (!list || list.dataset.rendered) return;
list.dataset.rendered = '1';
_CHANGELOG.forEach((entry, i) => {
const isLatest = i === 0;
const block = document.createElement('div');
block.style.cssText = 'display:flex;flex-direction:column;gap:8px;align-self:stretch;';
const vRow = document.createElement('div');
vRow.style.cssText = 'display:flex;align-items:center;gap:8px;';
vRow.innerHTML = `
<p class="DH_T1 DH_NoSel" style="font-size:15px;font-weight:800;color:rgb(var(--color-eel,88,88,88));">v${entry.version}</p>
${isLatest ? `<span style="background:rgb(var(--DH-blue));color:#fff;font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;">CURRENT</span>` : ''}
`;
const changeList = document.createElement('div');
changeList.style.cssText = 'display:flex;flex-direction:column;gap:6px;';
entry.changes.forEach(c => {
const item = document.createElement('p');
item.className = 'DH_T2 DH_NoSel';
item.style.cssText = `
font-size:13px;font-weight:600;line-height:1.5;
background:rgba(var(--color-eel,88,88,88),0.06);
border-radius:10px;padding:9px 12px;
color:rgb(var(--color-eel,88,88,88));
`;
item.textContent = c;
changeList.appendChild(item);
});
block.appendChild(vRow);
block.appendChild(changeList);
if (i < _CHANGELOG.length - 1) {
const div = document.createElement('div');
div.className = 'DH_Divider';
block.appendChild(div);
}
list.appendChild(block);
});
}
// ── Lesson Shortener toggle ────────────────────────────────────────
const _lsTog = document.getElementById('DH_LS_Toggle');
_lsTog.checked = localStorage.getItem(_LS_KEY) === 'true';
_lsTog.addEventListener('change', () => {
localStorage.setItem(_LS_KEY, _lsTog.checked ? 'true' : 'false');
});
// ── Stories Shortener toggle ───────────────────────────────────────
const _ssTog = document.getElementById('DH_SS_Toggle');
_ssTog.checked = localStorage.getItem(_SS_KEY) === 'true';
_ssTog.addEventListener('change', () => {
localStorage.setItem(_SS_KEY, _ssTog.checked ? 'true' : 'false');
});
// ──────────────────────────────────────────────────────────────────
_connect();
setTimeout(() => {
if (!_v1Mode) {
const sb = document.getElementById('DH_SwitchV1_Btn');
if (sb && !_hidden) sb.style.display = '';
}
}, 700);
_resumePracticeIfNeeded();
}
_dhMountUI();
})();