// ==UserScript== // @name DuoRain // @namespace http://tampermonkey.net/ // @version 5.0.0.BETA // @description Ultimate Automation Tool for Duolingo // @icon https://raw.githubusercontent.com/OracleMythix/DuoRain-BETA/main/assets/DuoRain-Icon.png // @author OracleMythix // @license MIT // @match https://*.duolingo.com/* // @match https://*.duolingo.cn/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant unsafeWindow // @connect duolingo.com // @connect stories.duolingo.com // @connect goals-api.duolingo.com // @connect ios-api-2.duolingo.com // @connect duolingo-leaderboards-prod.duolingo.com // @connect fonts.googleapis.com // @connect fonts.gstatic.com // @run-at document-start // ==/UserScript== (function() { 'use strict'; const config = { dim: { width: 900, height: 600 }, id: { app: "duorain-app", win: "duorain-win", orb: "duorain-orb" }, api: { stories: "https://stories.duolingo.com/api2/stories", users: "https://www.duolingo.com/2017-06-30/users", sessions: "https://www.duolingo.com/2017-06-30/sessions", leaderboards: "https://duolingo-leaderboards-prod.duolingo.com/leaderboards/7d9f5dd1-8423-491a-91f2-2532052038ce", shop: "https://www.duolingo.com/2023-05-23/shop-items", goals: "https://goals-api.duolingo.com" }, defaults: { delays: { xp: 100, gem: 500, streak: 100, quest: 100, league: 100 }, leagueBuffer: 60, fakeMax: false, notifPos: "bl" }, chBody: [ "assist", "characterIntro", "characterMatch", "characterPuzzle", "characterSelect", "characterTrace", "characterWrite", "completeReverseTranslation", "definition", "dialogue", "extendedMatch", "extendedListenMatch", "form", "freeResponse", "gapFill", "judge", "listen", "listenComplete", "listenMatch", "match", "name", "listenComprehension", "listenIsolation", "listenSpeak", "listenTap", "orderTapComplete", "partialListen", "partialReverseTranslate", "patternTapComplete", "radioBinary", "radioImageSelect", "radioListenMatch", "radioListenRecognize", "radioSelect", "readComprehension", "reverseAssist", "sameDifferent", "select", "selectPronunciation", "selectTranscription", "svgPuzzle", "syllableTap", "syllableListenTap", "speak", "tapCloze", "tapClozeTable", "tapComplete", "tapCompleteTable", "tapDescribe", "translate", "transliterate", "transliterationAssist", "typeCloze", "typeClozeTable", "typeComplete", "typeCompleteTable", "writeComprehension" ] }; const desc = { dash: "Overview of usage statistics, recent activity, and profile details in one place", xp: "Earn any amount of XP by looping a story", gem: "Obtain unlimited Gems by loop-claiming a reward; each loop grants 60 Gems", streak: "Restore or extend your streak by backfilling days", league: "Reach any league position you desire by automatically overtaking opponents", quest: "Instantly complete quests by injecting 2000 units of progress (Brute Force)", shop: "Acquire Duolingo store items for free", misc: "Unlock premium features and other utilities", settings: "Adjust your preferences and customize your experience" }; const assets = { logo: "https://raw.githubusercontent.com/OracleMythix/DuoRain-BETA/main/assets/DuoRain.png", titleSvg: `RainDuo`, xp: "https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg", gems: "https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg", streak: "https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg", league: "https://d35aaqx5ub95lt.cloudfront.net/vendor/ca9178510134b4b0893dbac30b6670aa.svg", quest: "https://d35aaqx5ub95lt.cloudfront.net/vendor/7ef36bae3f9d68fc763d3451b5167836.svg", shop: "https://d35aaqx5ub95lt.cloudfront.net/vendor/0e58a94dda219766d98c7796b910beee.svg", chest: "https://d35aaqx5ub95lt.cloudfront.net/images/goals/64d0bbcd8f4e6d5018502540f1e0094b.svg", misc: "https://d35aaqx5ub95lt.cloudfront.net/images/legendary/158dbe277bf83116d04692b969a27aa3.svg", icons: { dash: ``, settings: ``, close: ``, refresh: ``, arrow: ``, chevLeft: ``, chevRight: ``, manage: `` }, shopIcons: { xp: "https://d35aaqx5ub95lt.cloudfront.net/images/icons/68c1fd0f467456a4c607ecc0ac040533.svg", streak: "https://d35aaqx5ub95lt.cloudfront.net/images/icons/216ddc11afcbb98f44e53d565ccf479e.svg", heart: "https://d35aaqx5ub95lt.cloudfront.net/images/hearts/547ffcf0e6256af421ad1a32c26b8f1a.svg", gem: "https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg", outfit: "https://d35aaqx5ub95lt.cloudfront.net/vendor/0cecd302cf0bcd0f73d51768feff75fe.svg", free: "https://d35aaqx5ub95lt.cloudfront.net/images/super/11db6cd6f69cb2e3c5046b915be8e669.svg", misc: "https://d35aaqx5ub95lt.cloudfront.net/images/leagues/9fadb349c2ece257386a0e576359c867.svg" } }; let GlobalNotif; let GlobalPopup; let GlobalConfirm; const CSS = `@import url('https://fonts.googleapis.com/css2?family=Fugaz+One&display=swap');@import url('https://fonts.googleapis.com/css2?family=Google+Sans+Code:wght@400;500;600;700&display=swap');@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@700;800&display=swap');@font-face{font-family:'DuoFeather';src:url('https://d35aaqx5ub95lt.cloudfront.net/fonts/642e24bb0295f3aee4dedcd8eecd8007.woff2') format('woff2');font-weight:700}#${config.id.app}{--rain:0,198,255;--rain-hex:#00C6FF;--shadow:0 20px 60px rgba(0,0,0,0.35);--glass:blur(25px) saturate(120%);--close:rgba(255,255,255,0.2)}#${config.id.app}.dr-light{--bg:rgba(138,138,255,0.7);--sidebar:rgba(92,92,255,0.85);--input:rgba(46,46,255,0.65);--hover:rgba(46,46,255,0.4);--dropdown-bg:rgba(46,46,255,0.95);--eel:#FFFFFF;--wolf:#E0E0E0;--swan:rgba(255,255,255,0.2)}#${config.id.app}.dr-dark{--bg:rgba(15,23,42,0.85);--sidebar:rgba(30,41,59,0.9);--input:rgba(51,65,85,0.6);--hover:rgba(71,85,105,0.5);--dropdown-bg:rgba(15,23,42,0.95);--eel:#FFFFFF;--wolf:#94A3B8;--swan:rgba(255,255,255,0.1)}#${config.id.app}{position:fixed;inset:0;pointer-events:none;z-index:2147483647;font-family:'DuoFeather',sans-serif;box-sizing:border-box;user-select:none;-webkit-user-select:none}#${config.id.app} *{box-sizing:border-box}#${config.id.orb}{position:fixed;bottom:30px;left:30px;width:68px;height:68px;pointer-events:auto;cursor:pointer;border-radius:50%;box-shadow:0 10px 30px rgba(0,0,0,.3);transition:transform .3s cubic-bezier(.34,1.56,.64,1);display:flex;align-items:center;justify-content:center;background:var(--bg);border:2px solid var(--swan);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);overflow:hidden}#${config.id.orb}:hover{transform:scale(1.1)}#${config.id.orb} img{width:100%;height:100%;object-fit:contain;border-radius:50%;pointer-events:none}#${config.id.win}{position:fixed;pointer-events:auto;width:${config.dim.width}px;height:${config.dim.height}px;top:calc(50vh - ${config.dim.height/2}px);left:calc(50vw - ${config.dim.width/2}px);background:var(--bg);border:1px solid var(--swan);border-radius:28px;box-shadow:var(--shadow);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);display:flex;flex-direction:row;transition:filter .3s, opacity .3s,transform .3s cubic-bezier(.2,0,0,1);overflow:hidden}#${config.id.win}.mini{opacity:0;transform:scale(.9) translateY(30px);pointer-events:none}.sidebar{width:240px;background:var(--sidebar);border-right:1px solid var(--swan);border-radius:28px 0 0 28px;display:flex;flex-direction:column;padding:25px 16px;position:relative}.header-lock{display:flex;align-items:center;justify-content:flex-start;padding-left:6px;padding-bottom:12px;cursor:grab;gap:0}.header-lock:active{cursor:grabbing}.logo-img{width:38px;height:38px;margin-right:10px;border-radius:50%;object-fit:contain;filter:drop-shadow(0 4px 6px rgba(0,0,0,.15))}.logo-text-svg{height:45px;width:auto;margin-left:-4px;margin-top:2px;object-fit:contain;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.1))}.nav-list{flex:1;display:flex;flex-direction:column;gap:6px;position:relative}.magic-pill{position:absolute;left:0;top:0;width:4px;height:50px;background:var(--rain-hex);border-radius:0 4px 4px 0;box-shadow:0 0 15px var(--rain-hex);transition:transform .25s cubic-bezier(.3,0,.2,1)}.nav-btn{display:flex;align-items:center;gap:14px;padding:12px 20px;width:100%;text-align:left;background:0 0;border:none;border-radius:14px;color:var(--wolf);font-size:16px;font-weight:700;cursor:pointer;transition:.15s}.nav-btn:hover{background:var(--hover);color:var(--eel)}.nav-btn.active{color:var(--eel)}.nav-btn img{width:28px;height:28px;transition:transform .2s}.nav-btn svg{width:26px;height:26px;stroke:currentColor;transition:transform .2s}.nav-btn:hover img,.nav-btn:hover svg{transform:scale(1.1)}.main-view{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}.top-panel{height:75px;display:flex;align-items:center;justify-content:space-between;padding:0 30px;border-bottom:1px solid var(--swan);cursor:grab;z-index:10}.top-panel:active{cursor:grabbing}.header-title-group{display:flex;align-items:center;gap:12px;color:var(--eel)}.header-icon-box{display:flex;align-items:center;justify-content:center;width:32px;height:32px}.header-icon-box img{width:100%;height:100%;object-fit:contain}.header-icon-box svg{width:28px;height:28px;stroke:var(--eel)}.header-text-col{display:flex;flex-direction:column;justify-content:center}.title{font-size:20px;font-weight:700;color:var(--eel);line-height:1.2}.subtitle{font-size:13px;font-weight:500;color:var(--wolf);line-height:1.2}.close-btn{width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--wolf);transition:.2s;cursor:pointer}.close-btn:hover{background:var(--close);color:#FF4B4B}.close-btn svg{width:24px;height:24px}.scroller{flex:1;position:relative;padding:0;overflow-y:auto;user-select:text;-webkit-user-select:text}.scroller.no-scroll{overflow:hidden!important}.page{width:100%;padding:25px;opacity:0;transform:translateY(10px);transition:.2s ease-out;display:none}.page.active{opacity:1;transform:none;display:block;position:relative}.card{background:var(--input);border:1px solid var(--swan);border-radius:20px;padding:25px;margin-bottom:20px}.label{display:block;margin-bottom:10px;font-size:13px;font-weight:800;text-transform:uppercase;color:var(--wolf)}.input-group{display:flex;gap:10px;align-items:center}.input{flex:1;padding:14px 18px;border-radius:14px;background:var(--hover);border:2px solid var(--swan);color:var(--eel);font-weight:700;font-size:16px;outline:0;transition:.2s}.input:focus{border-color:var(--rain-hex);box-shadow:0 0 0 4px rgba(0,198,255,.15)}.input[type=number]::-webkit-inner-spin-button,.input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.btn{padding:14px 25px;border-radius:14px;background:linear-gradient(135deg,#00C6FF 0%,#0072FF 100%);color:#fff;border:none;font-size:15px;font-weight:800;text-transform:uppercase;cursor:pointer;box-shadow:0 8px 25px rgba(0,110,255,.25);transition:all .3s ease;letter-spacing:1px;white-space:nowrap}.btn:active{transform:scale(.98)}.btn:hover{filter:brightness(1.1)}.btn.disabled{filter:grayscale(1);cursor:not-allowed;transform:none}.btn.stop{background:linear-gradient(135deg, #FF4B4B 0%, #FF0000 100%);box-shadow:0 8px 25px rgba(255, 0, 0, 0.25)}.btn-got{background:linear-gradient(135deg,#2ecc71 0%,#26a65b 100%)!important;box-shadow:0 0 15px #2ecc71!important;color:#fff!important;border:none!important;pointer-events:none}.status-box{position:relative;background:rgba(0,0,0,0.4);color:#00ff9d;border-radius:14px;border:1px solid var(--swan);margin-bottom:15px;min-height:auto;overflow:hidden;display:flex;flex-direction:column}.status-content{padding:15px 20px;position:relative;z-index:2;font-family:'Google Sans Code','Consolas',monospace;font-size:14px;line-height:1.45}.progress-overlay{position:absolute;bottom:0;left:0;height:5px;width:0%;background:linear-gradient(90deg,#00C6FF,#0072FF);transition:width .3s ease;z-index:1;box-shadow:0 -2px 10px rgba(0,198,255,.5)}.scroller::-webkit-scrollbar{width:6px}.scroller::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}._profile_card{background:var(--input);border:1px solid var(--swan);border-radius:20px;padding:25px;margin-bottom:20px}._profile_header{display:flex;align-items:center;gap:15px;margin-bottom:20px}._avatar{width:56px;height:56px;background:linear-gradient(135deg,#00C6FF 0%,#0072FF 100%);border-radius:16px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:28px;box-shadow:0 8px 20px rgba(0,110,255,.25);overflow:hidden;position:relative}._profile_info{flex:1}._profile_info h2{font-size:20px;font-weight:700;color:var(--eel);margin:0 0 4px}._profile_info p{color:var(--wolf);font-size:14px;margin:0}._icon_btn{width:36px;height:36px;border:none;background:var(--hover);border:1px solid var(--swan);border-radius:10px;color:var(--wolf);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.2s}._icon_btn:hover{background:rgba(0,198,255,.2);color:#0072FF;border-color:rgba(0,198,255,.3)}._icon_btn svg{width:20px;height:20px;stroke:currentColor}._stats_row{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}._stat_item{display:flex;align-items:center;gap:12px;padding:15px;background:var(--hover);border-radius:14px;border:1px solid var(--swan)}._stat_img{width:32px;height:32px;object-fit:contain}._stat_info{display:flex;flex-direction:column}._stat_value{font-size:18px;font-weight:700;color:var(--eel)}._stat_label{font-size:12px;color:var(--wolf)}._profile_status_row{display:flex;align-items:center;gap:10px;margin-bottom:6px}._status_label{font-size:12px;font-weight:800;color:var(--wolf);min-width:55px}.toggle-switch{position:relative;width:40px;height:22px}.toggle-input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--swan);transition:.3s;border-radius:22px}.toggle-slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;bottom:3px;background-color:white;transition:.3s;border-radius:50%}.toggle-input:checked+.toggle-slider{background:linear-gradient(135deg,#00C6FF 0%,#0072FF 100%)}.toggle-input:checked+.toggle-slider:before{transform:translateX(18px)}.row-group{display:flex;gap:20px}.card.half{flex:1;margin-bottom:20px;display:flex;flex-direction:column}.big-stat-text,.big-rank{font-size:54px;font-weight:800;color:var(--eel);line-height:1;margin-top:10px;background:linear-gradient(120deg,#33F9FF 0%,#00C6FF 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-family:'Source Code Pro',monospace}.c-select{position:relative;width:100%;font-family:inherit}.c-select-trigger{padding:14px 18px;border-radius:14px;background:var(--hover);border:2px solid var(--swan);color:var(--eel);font-weight:700;font-size:16px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:.2s;user-select:none}.c-select-trigger:hover{background:var(--input)}.c-select.open .c-select-trigger{border-color:var(--rain-hex);box-shadow:0 0 0 4px rgba(0,198,255,.15)}.c-arrow{width:16px;height:16px;stroke:var(--wolf);transition:transform .3s}.c-select.open .c-arrow{transform:rotate(180deg)}.c-options{position:absolute;top:calc(100% + 8px);left:0;right:0;background:var(--dropdown-bg);border:1px solid var(--swan);border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.2);overflow:hidden;z-index:100;opacity:0;visibility:hidden;transform:translateY(-10px);transition:all .2s cubic-bezier(.2,0,.2,1);max-height:220px;overflow-y:auto;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);scrollbar-width:none;-ms-overflow-style:none}.c-options::-webkit-scrollbar{width:0;height:0;display:none}.c-select.open .c-options{opacity:1;visibility:visible;transform:translateY(0)}.c-option{padding:12px 18px;cursor:pointer;font-weight:600;color:var(--wolf);transition:background .1s;display:flex;align-items:center}.c-option:hover{background:rgba(0,198,255,.1);color:var(--eel)}.c-option.selected{background:linear-gradient(90deg,rgba(0,198,255,.1),transparent);color:#00C6FF}.search-bar{width:100%;padding:14px 18px;border-radius:14px;background:var(--hover);border:2px solid var(--swan);color:var(--eel);font-weight:700;font-size:16px;outline:0;transition:.2s;margin-bottom:20px}.search-bar:focus{border-color:var(--rain-hex);box-shadow:0 0 0 4px rgba(0,198,255,.15)}.category-header{font-size:14px;font-weight:800;text-transform:uppercase;color:var(--wolf);margin:20px 0 10px;text-align:center;position:relative}.category-header::before,.category-header::after{content:'';position:absolute;top:50%;width:30%;height:1px;background:var(--swan)}.category-header::before{left:0}.category-header::after{right:0}.shop-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));gap:12px;margin-bottom:10px}.shop-item{background:var(--hover);border:1px solid var(--swan);border-radius:16px;padding:15px;display:flex;flex-direction:column;align-items:center;text-align:center;transition:.2s;min-height:140px;justify-content:space-between}.shop-item:hover{transform:translateY(-2px);border-color:var(--rain-hex)}.item-icon{width:42px;height:42px;margin-bottom:10px;object-fit:contain}.item-name{font-size:13px;font-weight:700;color:var(--eel);line-height:1.2;margin-bottom:10px;flex:1;display:flex;align-items:center;justify-content:center}.item-btn{width:100%;padding:8px;border-radius:10px;background:var(--input);border:1px solid var(--swan);color:var(--wolf);font-size:11px;font-weight:800;cursor:pointer;transition:.2s}.item-btn:hover{background:var(--rain-hex);color:#fff;border-color:var(--rain-hex)}.item-btn.buying{pointer-events:none}.item-btn.success{background:#2ecc71;color:#fff;border-color:#2ecc71}.shop-search-row{display:flex;gap:10px;align-items:center;margin-bottom:20px}.shop-reload-btn{width:50px;height:50px;flex-shrink:0;border-radius:14px;background:var(--hover);border:2px solid var(--swan);color:var(--wolf);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.2s}.shop-reload-btn:hover{background:var(--input);color:var(--rain-hex);border-color:var(--rain-hex)}.shop-empty-state{text-align:center;padding:30px;color:var(--wolf);display:none;animation:fadeIn .3s}.shop-empty-img{width:64px;height:64px;margin-bottom:15px;opacity:.6;filter:grayscale(1);object-fit:contain}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.quest-filters{display:flex;gap:8px;overflow-x:auto;padding-bottom:5px;margin-top:10px}.quest-filter-btn{padding:8px 16px;border-radius:14px;background:var(--hover);border:2px solid var(--swan);color:var(--wolf);font-weight:700;font-size:12px;cursor:pointer;transition:.2s;white-space:nowrap}.quest-filter-btn:hover{background:var(--input)}.quest-filter-btn.active{background:var(--rain-hex);color:#fff;border-color:var(--rain-hex)}.quest-item{display:flex;align-items:center;padding:15px;background:var(--hover);border:1px solid var(--swan);border-radius:16px;margin-bottom:10px;transition:.2s}.quest-item:hover{border-color:var(--rain-hex);transform:translateY(-2px)}.quest-item.completed{border-left:4px solid #2ecc71}.quest-item.warning{border-left:4px solid #f1c40f}.quest-icon{width:48px;height:48px;object-fit:contain;margin-right:15px}.quest-info{flex:1}.quest-title{font-size:14px;font-weight:700;color:var(--eel);margin-bottom:4px}.quest-meta{font-size:11px;color:var(--wolf);margin-bottom:6px;font-family:'Google Sans Code',monospace}.quest-progress-row{display:flex;justify-content:space-between;font-size:11px;font-weight:700;color:var(--wolf);margin-bottom:4px}.quest-bar-bg{height:8px;background:var(--swan);border-radius:4px;overflow:hidden}.quest-bar-fill{height:100%;background:var(--rain-hex);width:0%;border-radius:4px;transition:width .5s}.quest-item.completed .quest-bar-fill{background:#2ecc71}.quest-actions{display:flex;gap:6px;margin-left:10px;flex-direction:column}.q-mini-btn{padding:6px 10px;border-radius:8px;background:var(--input);border:1px solid var(--swan);color:var(--wolf);font-size:10px;font-weight:800;cursor:pointer;transition:.2s;min-width:60px;text-align:center}.q-mini-btn:hover{background:var(--rain-hex);color:#fff;border-color:var(--rain-hex)}.q-mini-btn.finish{background:linear-gradient(135deg,#f1c40f 0%,#f39c12 100%);color:#fff;border:none}.q-mini-btn.finish:hover{filter:brightness(1.1)}.quest-header-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.dash-row{display:flex;gap:15px;margin-top:15px}.dash-card{flex:1;background:var(--input);border:1px solid var(--swan);border-radius:16px;padding:20px;display:flex;flex-direction:column;align-items:center;justify-content:space-between;text-align:center;position:relative;min-height:220px}.carousel-nav{position:absolute;top:50%;transform:translateY(-50%);cursor:pointer;color:var(--wolf);width:28px;height:28px;background:var(--hover);border-radius:50%;display:flex;align-items:center;justify-content:center;transition:.2s;z-index:5}.carousel-nav:hover{background:var(--swan);color:var(--eel);transform:translateY(-50%) scale(1.1)}.nav-left{left:10px}.nav-right{right:10px}.q-img{width:60px;height:60px;margin:10px 0;object-fit:contain;filter:drop-shadow(0 4px 6px rgba(0,0,0,0.1))}.q-head{font-size:14px;font-weight:800;color:var(--wolf);text-transform:uppercase}.q-text{font-size:13px;font-weight:600;color:var(--eel);margin-bottom:10px;min-height:18px}.q-btn{padding:8px 20px;border-radius:10px;background:linear-gradient(135deg,#00C6FF 0%,#0072FF 100%);color:#fff;border:none;font-size:12px;font-weight:800;cursor:pointer;transition:.2s}.q-btn:hover{filter:brightness(1.1);transform:scale(1.05)}.q-btn.done{background:none;color:#2ecc71;border:none;pointer-events:none;font-size:14px}.dr-spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:dr-spin .6s linear infinite;display:inline-block;vertical-align:middle;margin-left:5px}@keyframes dr-spin{to{transform:rotate(360deg)}}.task-item{display:flex;align-items:center;justify-content:space-between;padding:10px;background:var(--hover);border:1px solid var(--swan);border-radius:10px;margin-bottom:8px;width:100%}.task-info{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700;color:var(--eel)}.mini-stop{background:linear-gradient(135deg, #FF4B4B 0%, #FF0000 100%);color:#fff;font-size:10px;font-weight:800;border:none;padding:5px 10px;border-radius:8px;cursor:pointer;box-shadow:0 2px 5px rgba(255,0,0,0.3);transition:.2s}.mini-stop:hover{transform:scale(1.05)}.mini-stop.close{background:var(--hover);color:var(--wolf);box-shadow:none;border:1px solid var(--swan);padding:5px 8px}.mini-stop.close:hover{background:var(--close);color:#FF4B4B}.interrupted-text{color:#FF4B4B;font-size:11px;font-weight:700;margin-right:5px;display:flex;align-items:center;gap:4px}.dr-notif-container{position:fixed;z-index:2147483650;padding:20px;display:flex;gap:10px;pointer-events:none;transition:all .3s}.pos-bl{bottom:0;left:0;align-items:flex-start;flex-direction:column-reverse}.pos-br{bottom:0;right:0;align-items:flex-end;flex-direction:column-reverse}.pos-c{bottom:0;left:50%;transform:translateX(-50%);align-items:center;flex-direction:column-reverse}.dr-notification{position:relative;background:var(--dropdown-bg);border:1px solid var(--swan);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);padding:15px 25px;border-radius:24px;display:flex;align-items:center;gap:15px;box-shadow:0 15px 40px rgba(0,0,0,0.35);pointer-events:auto;min-width:300px;max-width:380px;transition:all .4s cubic-bezier(.16,1,.3,1);transform-origin:center;opacity:0;transform:scale(.9) translateY(20px);max-height:0;margin:0;overflow:hidden}.dr-notif-close{position:absolute;top:-8px;right:-8px;width:24px;height:24px;background:var(--input);border:1px solid var(--swan);border-radius:50%;color:var(--wolf);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;font-weight:700;opacity:0;transition:.2s;box-shadow:0 4px 10px rgba(0,0,0,0.2)}.dr-notification:hover .dr-notif-close{opacity:1;top:-10px;right:-10px}.dr-notif-close:hover{background:#FF4B4B;color:#fff;border-color:#FF4B4B}.dr-notif-icon{width:46px;height:46px;border-radius:50%;background:linear-gradient(135deg,#00C6FF,#0072FF);display:flex;align-items:center;justify-content:center;flex-shrink:0}.dr-notif-icon img{width:100%;height:100%;object-fit:contain;border-radius:50%}.dr-notif-content{display:flex;flex-direction:column}.dr-notif-title{color:var(--eel);font-weight:800;font-size:16px;line-height:1.2}.dr-notif-desc{color:var(--wolf);font-size:13px;font-weight:600;margin-top:3px;font-family:'Google Sans Code',monospace}.dr-notification.show{opacity:1;transform:scale(1) translateY(0);max-height:150px;margin-top:10px}.pos-bl .dr-notification.hiding{opacity:0;transform:translateX(-100%) scale(.9);max-height:0!important;margin-top:0!important;padding-top:0!important;padding-bottom:0!important;border:none!important}.pos-br .dr-notification.hiding{opacity:0;transform:translateX(100%) scale(.9);max-height:0!important;margin-top:0!important;padding-top:0!important;padding-bottom:0!important;border:none!important}.pos-c .dr-notification.hiding{opacity:0;transform:translateY(30px) scale(.8);max-height:0!important;margin-top:0!important;padding-top:0!important;padding-bottom:0!important;border:none!important}.dr-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.1);backdrop-filter:blur(15px);z-index:2147483648;display:none;align-items:center;justify-content:center;opacity:0;transition:.3s}.dr-modal-overlay.active{opacity:1;display:flex}.dr-modal{background:var(--bg);border:1px solid var(--swan);padding:30px;border-radius:24px;width:90%;max-width:400px;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.4);transform:scale(0.9);transition:.3s}.dr-modal .header-icon-box img{border-radius:50%}.dr-modal-overlay.active .dr-modal{transform:scale(1)}.dr-modal-actions{display:flex;gap:10px;margin-top:20px;justify-content:center}.dr-modal-btn-sec{background:transparent;border:2px solid var(--swan);color:var(--wolf);padding:12px 20px;border-radius:12px;font-weight:700;cursor:pointer;transition:.2s}.dr-modal-btn-sec:hover{border-color:var(--rain-hex);color:var(--eel)}[data-dr-tip]{position:relative}[data-dr-tip]:hover::after{content:attr(data-dr-tip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);padding:6px 10px;background:var(--dropdown-bg);color:var(--eel);border:1px solid var(--swan);border-radius:8px;font-size:12px;font-weight:700;white-space:nowrap;pointer-events:none;box-shadow:0 5px 15px rgba(0,0,0,0.1);margin-bottom:10px;z-index:100;backdrop-filter:blur(10px)}.ac-preview{display:flex;align-items:center;gap:10px;margin:15px 0;background:var(--hover);padding:10px;border-radius:12px;text-align:left}.ac-prev-img{width:40px;height:40px;border-radius:50%;object-fit:cover}.acc-user-card{background:var(--hover);border-radius:14px;border:1px solid var(--swan);padding:15px;display:flex;align-items:center;gap:12px;position:relative;overflow:hidden;transition:.2s}.acc-user-card:hover{border-color:var(--rain-hex)}.acc-avatar{width:42px;height:42px;border-radius:50%;object-fit:cover;flex-shrink:0}.acc-info{flex:1;display:flex;flex-direction:column}.acc-name{color:var(--eel);font-weight:700;font-size:15px}.acc-desc{color:var(--wolf);font-size:12px}.acc-manage-btn{width:100%;margin-top:10px;background:transparent;border:2px solid var(--swan);color:var(--wolf);padding:12px;border-radius:12px;font-weight:700;cursor:pointer;transition:.2s;display:flex;align-items:center;justify-content:center;gap:8px}.acc-manage-btn:hover{border-color:var(--rain-hex);color:var(--eel)}.acc-status-overlay{position:absolute;inset:0;background:var(--dropdown-bg);display:flex;align-items:center;justify-content:center;opacity:0;transition:.2s;cursor:pointer;backdrop-filter:blur(5px);border-radius:14px}.acc-user-card:hover .acc-status-overlay{opacity:1}.acc-status-text{font-weight:800;color:var(--eel);font-size:13px;text-transform:uppercase}.acc-login-btn{background:var(--rain-hex);color:#fff;border:none;padding:6px 14px;border-radius:8px;font-weight:800;font-size:12px;cursor:pointer}.acc-grid{max-height:190px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:20px;padding-right:4px}.acc-grid::-webkit-scrollbar{width:4px}.acc-grid::-webkit-scrollbar-thumb{background:var(--swan);border-radius:2px}.acc-item-row{display:flex;align-items:center;justify-content:space-between;background:var(--hover);padding:10px;border-radius:12px;border:1px solid var(--swan)}.acc-del-btn{width:28px;height:28px;border-radius:8px;background:var(--input);color:var(--wolf);border:1px solid var(--swan);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.2s}.acc-del-btn:hover{background:#FF4B4B;color:#fff;border-color:#FF4B4B}.acc-del-btn svg{width:16px;height:16px;stroke:currentColor}`; const HTML = `

Alert

${assets.icons.dash}
Dashboard${desc.dash}
${assets.icons.close}
👤

Loading...

...

Fetching data...

0Total XP
0Streak
0Gems
DAILY QUESTS
Checking status...
INFO
Current League Position
...
... XP
ACCOUNTS

Manage your Duolingo accounts and switch between them effortlessly.

Configuration
Enter amount of XP to farm
Status
STATUS: Idle
Target XP: none
Loops to run: 0
Loops left: 0
Estimated Time left: 0
Time taken: ...
Configuration
Enter amount of Gem loops to run
Status
STATUS: Idle
Target Gems: none
Loops to run: 0
Loops left: 0
Estimated Time left: 0
Time taken: ...
Configuration
Enter amount of Streak Days to restore/farm
Status
STATUS: Idle
Target Days: none
Days to farm: 0
Days left: 0
Estimated Time left: 0
Time taken: ...
Configuration
Select a league position to reach
# 1
Info
Current League Position
...
...
Status
STATUS: Idle
Target Position: none
Target XP: none
Loops to run: 0
Loops left: 0
Estimated Time left: 0
Time taken: ...
Controls

Loading quests...

Search

Loading shop items...

Nothing found?

Try searching for something else!

STATUS
FREE SUPER/MAX
Activate Free Max in your account indefinitely and at no cost
Free Max (Web Only)
Free Max (Web Only)
Free Super Trial (3-Days)
CUSTOMIZE NOTIFICATIONS

Modify the position of notifications

Bottom Left
Bottom Left
Bottom Right
Center
Loops Delay Control

Select a farm type to configure its loop delay (ms).

XP Farm
XP Farm
Gem Farm
Streak Farm
League Saver
Quests
LEAGUE SAVER CONTROL

Choose how much extra XP you want to generate as you overtake your opponents on the leaderboard.

`; class functions { constructor() { this.activeTasks = new Set(); this.taskProgress = new Map(); this.taskTimers = new Map(); this.interruptedTasks = new Map(); } installInterceptors() { const fakeMax = localStorage.getItem('dr_fake_max') === 'true'; if (!fakeMax) return; const TARGET_URL_REGEX = /https:\/\/www\.duolingo\.com\/\d{4}-\d{2}-\d{2}\/users\/.+/; const CUSTOM_SHOP_ITEMS = { gold_subscription: { itemName: "gold_subscription", subscriptionInfo: { vendor: "STRIPE", renewing: true, isFamilyPlan: true, expectedExpiration: 9999999999000 } } }; function shouldIntercept(url, method = 'GET') { if (method.toUpperCase() !== 'GET') return false; const urlStr = (typeof url === 'string') ? url : (url instanceof Request ? url.url : ''); const isMatch = TARGET_URL_REGEX.test(urlStr); if (urlStr.includes('/shop-items')) return false; return isMatch; } function modifyJson(jsonText) { try { const data = JSON.parse(jsonText); data.hasPlus = true; if (!data.trackingProperties || typeof data.trackingProperties !== 'object') { data.trackingProperties = {}; } data.trackingProperties.has_item_gold_subscription = true; data.shopItems = { ...data.shopItems, ...CUSTOM_SHOP_ITEMS }; return JSON.stringify(data); } catch (e) { return jsonText; } } const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = function (resource, options) { const url = resource instanceof Request ? resource.url : resource; const method = (resource instanceof Request) ? resource.method : (options?.method || 'GET'); if (shouldIntercept(url, method)) { return originalFetch.apply(this, arguments).then(async (response) => { const cloned = response.clone(); const jsonText = await cloned.text(); const modified = modifyJson(jsonText); const newHeaders = new Headers(response.headers); return new Response(modified, { status: response.status, statusText: response.statusText, headers: newHeaders }); }); } return originalFetch.apply(this, arguments); }; const originalXhrOpen = unsafeWindow.XMLHttpRequest.prototype.open; const originalXhrSend = unsafeWindow.XMLHttpRequest.prototype.send; unsafeWindow.XMLHttpRequest.prototype.open = function (method, url, ...args) { this._intercept = shouldIntercept(url, method); originalXhrOpen.call(this, method, url, ...args); }; unsafeWindow.XMLHttpRequest.prototype.send = function () { if (this._intercept) { const originalOnReadyStateChange = this.onreadystatechange; const xhr = this; this.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { try { const modifiedText = modifyJson(xhr.responseText); Object.defineProperty(xhr, 'responseText', { writable: true, value: modifiedText }); Object.defineProperty(xhr, 'response', { writable: true, value: modifiedText }); } catch (e) {} } if (originalOnReadyStateChange) originalOnReadyStateChange.apply(this, arguments); }; } originalXhrSend.apply(this, arguments); }; function removeManageSubscriptionSection() { const sections = document.querySelectorAll('section._3f-te'); for (const section of sections) { const h2 = section.querySelector('h2._203-l'); if (h2 && h2.textContent.trim() === 'Manage subscription') { section.remove(); break; } } } const manageSubObserver = new MutationObserver(() => { removeManageSubscriptionSection(); }); manageSubObserver.observe(document.documentElement, { childList: true, subtree: true }); } getJWT() { let match = document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)')); return match ? match[2] : null; } decJWT(token) { try { const base64Url = token.split(".")[1]; const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const jsonPayload = decodeURIComponent(atob(base64).split("").map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")); return JSON.parse(jsonPayload); } catch(e) { return null; } } formatHeads(jwt) { return { "Content-Type": "application/json", Authorization: "Bearer " + jwt, "User-Agent": navigator.userAgent }; } async getUser(sub, headers) { const res = await fetch(`https://www.duolingo.com/2023-05-23/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,level,numFollowers,numFollowing,gems,creationDate,streakData,picture,trackingProperties`, { method: "GET", headers }); if (res.ok) return await res.json(); return null; } getQuestTimestamp(goalId) { const regex = /^(\d{4})_(\d{2})_monthly/; const match = goalId.match(regex); if (match) { const year = parseInt(match[1]); const month = parseInt(match[2]) - 1; const date = new Date(Date.UTC(year, month, 15, 12, 0, 0)); return date.toISOString(); } return new Date().toISOString(); } getGoalHeaders(token) { return { "Content-Type": "application/json", "x-requested-with": "XMLHttpRequest", "accept": "application/json; charset=UTF-8", "Authorization": `Bearer ${token}` }; } async getPstatus(sub, headers) { try { const res = await fetch(`https://www.duolingo.com/2023-05-23/users/${sub}/privacy-settings?fields=privacySettings`, { method: "GET", headers }); const data = await res.json(); const social = data.privacySettings.find(s => s.id === "disable_social"); return social ? social.enabled : false; } catch (e) { return false; } } async setPstatus(sub, headers, isPrivate) { try { await fetch(`https://www.duolingo.com/2023-05-23/users/${sub}/privacy-settings?fields=privacySettings`, { method: "PATCH", headers, body: JSON.stringify({ "DISABLE_SOCIAL": isPrivate }) }); } catch (e) {} } async getLdata(userId, headers) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: `${config.api.leaderboards}/users/${userId}?client_unlocked=true&get_reactions=true&_=${Date.now()}`, headers, onload: (res) => resolve(res.status === 200 ? JSON.parse(res.responseText) : null), onerror: () => resolve(null) }); }); } async getLstatus(userId, headers) { const data = await this.getLdata(userId, headers); if (!data || !data.active || !data.active.cohort || !data.active.cohort.rankings) return null; const rankings = data.active.cohort.rankings; const myIndex = rankings.findIndex(r => r.user_id == userId); if (myIndex === -1) return null; return { rank: myIndex + 1, score: rankings[myIndex].score, rankings }; } async getShop(headers, userId) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: config.api.shop, headers, onload: (res) => { try { if (res.status === 200) resolve(JSON.parse(res.responseText).shopItems); else resolve([]); } catch (e) { resolve([]); } }, onerror: () => resolve(null) }); }); } async buyShop(headers, userId, item) { const user = await this.getUser(userId, headers); const payload = { "itemName": item.id, "isFree": true, "consumed": true, "fromLanguage": user.fromLanguage, "learningLanguage": user.learningLanguage }; return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: `https://www.duolingo.com/2017-06-30/users/${userId}/shop-items`, headers, data: JSON.stringify(payload), onload: (res) => resolve(res.status === 200), onerror: () => resolve(false) }); }); } async buyLegitSuper(headers, userId) { const user = await this.getUser(userId, headers); const payload = { "itemName": "immersive_subscription", "isFree": true, "consumed": true, "fromLanguage": user.fromLanguage, "learningLanguage": user.learningLanguage, "productId": "com.duolingo.immersive_free_trial_subscription" }; return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: `https://www.duolingo.com/2017-06-30/users/${userId}/shop-items`, headers, data: JSON.stringify(payload), onload: (res) => resolve(res.status === 200 || res.status === 201), onerror: () => resolve(false) }); }); } async getGoals(headers) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: `${config.api.goals}/schema?ui_language=en&_=${Date.now()}`, headers, onload: (res) => resolve(res.status === 200 ? JSON.parse(res.responseText) : null), onerror: () => resolve(null) }); }); } async getUserProgress(userId, headers) { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: `${config.api.goals}/users/${userId}/progress?timezone=${tz}&ui_language=en`, headers, onload: (res) => resolve(res.status === 200 ? JSON.parse(res.responseText) : null), onerror: () => resolve(null) }); }); } async updateGoal(userId, headers, metric, amount, goalId) { const payload = { "metric_updates": [{ "metric": metric, "quantity": amount }], "timezone": Intl.DateTimeFormat().resolvedOptions().timeZone, "timestamp": this.getQuestTimestamp(goalId) }; return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: `${config.api.goals}/users/${userId}/progress/batch`, headers, data: JSON.stringify(payload), onload: (res) => resolve(res.status === 200), onerror: () => resolve(false) }); }); } async bruteForceGoals(userId, headers, metrics) { const updates = metrics.map(m => ({ "metric": m, "quantity": 2000 })); updates.push({ "metric": "QUESTS", "quantity": 1 }); const payload = { "metric_updates": updates, "timezone": Intl.DateTimeFormat().resolvedOptions().timeZone, "timestamp": new Date().toISOString() }; return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: `${config.api.goals}/users/${userId}/progress/batch`, headers, data: JSON.stringify(payload), onload: (res) => resolve(res.status === 200), onerror: () => resolve(false) }); }); } formatTime(ms) { if (ms < 1000) return `${ms} ms`; const m = Math.floor(ms / 60000); const s = Math.floor((ms % 60000) / 1000); return (m > 0 ? `${m} m ` : '') + (s > 0 || m === 0 ? `${s} s` : ''); } stopTask(type) { const startTime = this.taskTimers.get(type); if (startTime) { const duration = this.formatTime(Date.now() - startTime); this.interruptedTasks.set(type, duration); this.taskTimers.delete(type); } this.activeTasks.delete(type); this.taskProgress.delete(type); this.toggleRunBtn(this.getBtnId(type), false); this.updateDashRightCard(); let name = "Unknown Task"; if(type === 'xp') name = "XP Farm"; if(type === 'gem') name = "Gem Farm"; if(type === 'streak') name = "Streak Farm"; if(type === 'league') name = "League Saver"; GlobalNotif("DuoRain Active", `${name} Stopped`); } dismissInterrupted(type) { this.interruptedTasks.delete(type); this.updateDashRightCard(); } getBtnId(type) { if(type === 'xp') return 'xp-btn-run'; if(type === 'gem') return 'gem-btn-run'; if(type === 'streak') return 'streak-btn-run'; if(type === 'league') return 'league-btn-run'; return ''; } toggleRunBtn(id, isRunning) { const btn = document.getElementById(id); if(!btn) return; if(isRunning) { btn.innerHTML = "STOP"; btn.classList.add('stop'); } else { btn.innerHTML = "RUN"; btn.classList.remove('stop', 'disabled'); } } updateDashRightCard() { const card = document.getElementById('dash-card-content'); if(!card) return; if (this.activeTasks.size === 0 && this.interruptedTasks.size === 0) { card.innerHTML = `INFO
Current League Position
...
... XP
`; document.getElementById('_refresh_profile')?.click(); return; } let html = `TASKS
`; this.activeTasks.forEach(task => { let name = "Unknown"; let prog = ""; const info = this.taskProgress.get(task); if(task === 'xp') name = "XP Farm"; if(task === 'gem') name = "Gem Farm"; if(task === 'streak') name = "Streak Farm"; if(task === 'league') name = "League Saver"; if(info) { if(task === 'league') prog = `Checking...`; else prog = `${info.current} / ${info.total}`; } html += `
${name}
${prog}
`; }); this.interruptedTasks.forEach((time, task) => { let name = "Unknown"; if(task === 'xp') name = "XP Farm"; if(task === 'gem') name = "Gem Farm"; if(task === 'streak') name = "Streak Farm"; if(task === 'league') name = "League Saver"; html += `
${name}
INTERRUPTED • ${time}
`; }); html += `
`; card.innerHTML = html; card.querySelectorAll('.mini-stop').forEach(btn => { btn.onclick = (e) => { const action = e.target.dataset.action; const task = e.target.dataset.task; if(action === 'stop') this.stopTask(task); if(action === 'dismiss') this.dismissInterrupted(task); }; }); } updateStats(type, status, target, total, left, time, pct, taken, extra = "") { const box = document.getElementById(`${type}-status-text`); const bar = document.getElementById(`${type}-prog-bar`); if(total && total !== '...') { let cur = (typeof total === 'number' && typeof left === 'number') ? (total - left) : 0; this.taskProgress.set(type, { current: cur, total: total }); this.updateDashRightCard(); } if (!box || !bar) return; const lbl = type === 'xp' ? 'Target XP' : type === 'gem' ? 'Target Gems' : type === 'streak' ? 'Target Days' : 'Target Position'; const loopsLbl = type === 'streak' ? 'Days to farm' : 'Loops to run'; const leftLbl = type === 'streak' ? 'Days left' : 'Loops left'; let h = `STATUS: ${status}
${lbl}: ${target || 'none'}
`; if (type === 'league') h += `Target XP: ${extra}
`; h += `${loopsLbl}: ${total}
${leftLbl}: ${left}
Estimated Time left: ${time}
Time taken: ${taken}`; box.innerHTML = h; bar.style.width = `${pct}%`; } updateDash(data, isPrivate) { const el = (id) => document.getElementById(id); if (el('_username')) el('_username').innerText = data.username; if (el('_user_details')) el('_user_details').innerHTML = `${data.fromLanguage} ${assets.icons.arrow} ${data.learningLanguage}`; if (el('_current_xp')) el('_current_xp').innerText = data.totalXp.toLocaleString(); if (el('_current_streak')) el('_current_streak').innerText = data.streak.toLocaleString(); if (el('_current_gems')) el('_current_gems').innerText = data.gems.toLocaleString(); if (el('_privacy_toggle') && el('_privacy_label')) { el('_privacy_toggle').checked = isPrivate; el('_privacy_label').innerText = isPrivate ? "PRIVATE" : "PUBLIC"; } if (data.picture) { let hq = data.picture.replace(/\/(medium|large|small)$/, '/xlarge'); if (!hq.endsWith('/xlarge') && hq.includes('duolingo.com/ssr-avatars')) hq += '/xlarge'; if (document.querySelector('._avatar')) { const av = document.querySelector('._avatar'); av.innerHTML = ``; } } } async runXP(amount, delayMs, uiType = 'xp') { const btnId = uiType === 'league' ? 'league-btn-run' : 'xp-btn-run'; if (this.activeTasks.has(uiType)) { this.stopTask(uiType); return; } const token = this.getJWT(); if (!token) { GlobalPopup("Please login."); return; } const sub = this.decJWT(token).sub; const headers = this.formatHeads(token); this.activeTasks.add(uiType); this.taskTimers.set(uiType, Date.now()); this.toggleRunBtn(btnId, true); this.updateDashRightCard(); GlobalNotif("DuoRain Active", "XP Farm is now running!"); const startTime = Date.now(); this.updateStats(uiType, "Initializing...", amount, "...", "...", "...", 0, "...", amount); const user = await this.getUser(sub, headers); let txp = parseInt(amount); const min = 30, max = 499; if (txp < min) txp = min; let mReq = Math.floor(txp / max); let rem = txp % max; if (rem > 0 && rem < min && mReq > 0) { mReq--; rem += max; } const total = mReq + (rem >= min ? 1 : 0); let cur = 0; const delay = delayMs || 100; const loop = async (xp, hh) => { const now = Math.floor(Date.now() / 1000); const dur = Math.floor(Math.random() * 121 + 300); const payload = { "awardXp": true, "completedBonusChallenge": true, "fromLanguage": user.fromLanguage, "learningLanguage": user.learningLanguage, "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 new Promise(r => GM_xmlhttpRequest({ method: "POST", url: `${config.api.stories}/fr-en-le-passeport/complete`, headers, data: JSON.stringify(payload), onload: res => r(res.status === 200), onerror: () => r(false) })); }; for (let i = 0; i < mReq; i++) { if (!this.activeTasks.has(uiType)) break; cur++; const lLeft = total - cur; const pct = (cur / total) * 100; this.updateStats(uiType, "RUNNING XP", (uiType === 'league' ? `XP ${amount}` : txp), total, lLeft, (lLeft * (delay / 1000)).toFixed(1) + "s", pct, "...", amount); await loop(0, 469); await new Promise(r => setTimeout(r, delay)); } if (rem >= min && this.activeTasks.has(uiType)) { cur++; this.updateStats(uiType, "FINISHING...", (uiType === 'league' ? `XP ${amount}` : txp), total, 0, "0s", 100, "...", amount); await loop(0, Math.min(Math.max(0, rem - min), 469)); } if(this.activeTasks.has(uiType)) { this.activeTasks.delete(uiType); this.taskProgress.delete(uiType); this.taskTimers.delete(uiType); this.toggleRunBtn(btnId, false); this.updateStats(uiType, "COMPLETED", (uiType === 'league' ? `XP ${amount}` : txp), total, 0, "0s", 100, this.formatTime(Date.now() - startTime), amount); this.updateDashRightCard(); GlobalNotif("DuoRain Active", `Finished farming ${txp} XP`); document.getElementById('_refresh_profile')?.click(); } } async runGem(loops, delayMs) { if (this.activeTasks.has('gem')) { this.stopTask('gem'); return; } const token = this.getJWT(); if (!token) { GlobalPopup("Please login."); return; } const sub = this.decJWT(token).sub; const headers = this.formatHeads(token); this.activeTasks.add('gem'); this.taskTimers.set('gem', Date.now()); this.toggleRunBtn('gem-btn-run', true); this.updateDashRightCard(); GlobalNotif("DuoRain Active", "Gem Farm is now running!"); const startTime = Date.now(); const total = loops * 60; this.updateStats('gem', "Initializing...", total, loops, loops, "...", 0, "..."); const user = await this.getUser(sub, headers); const rewards = ["SKILL_COMPLETION_BALANCED-…-2-GEMS", "SKILL_COMPLETION_BALANCED-…-2-GEMS"]; let cur = 0; for (let i = 0; i < loops; i++) { if (!this.activeTasks.has('gem')) break; cur++; const left = loops - cur; const pct = (cur / loops) * 100; this.updateStats('gem', "RUNNING", total, loops, left, (left * ((delayMs || 500) / 1000)).toFixed(1) + "s", pct, "..."); for (const r of rewards) { await new Promise(res => GM_xmlhttpRequest({ method: "PATCH", url: `${config.api.users}/${sub}/rewards/${r}`, headers, data: JSON.stringify({ consumed: true, fromLanguage: user.fromLanguage, learningLanguage: user.learningLanguage }), onload: () => res(), onerror: () => res() })); } await new Promise(r => setTimeout(r, delayMs || 500)); } if(this.activeTasks.has('gem')) { this.activeTasks.delete('gem'); this.taskProgress.delete('gem'); this.taskTimers.delete('gem'); this.toggleRunBtn('gem-btn-run', false); this.updateStats('gem', "COMPLETED", total, loops, 0, "0s", 100, this.formatTime(Date.now() - startTime)); this.updateDashRightCard(); GlobalNotif("DuoRain Active", `Finished farming ${total} Gems`); document.getElementById('_refresh_profile')?.click(); } } async runStreak(amount, delayMs) { if (this.activeTasks.has('streak')) { this.stopTask('streak'); return; } const token = this.getJWT(); if (!token) { GlobalPopup("Please login."); return; } const sub = this.decJWT(token).sub; const headers = this.formatHeads(token); this.activeTasks.add('streak'); this.taskTimers.set('streak', Date.now()); this.toggleRunBtn('streak-btn-run', true); this.updateDashRightCard(); GlobalNotif("DuoRain Active", "Streak Farm is now running!"); const startTime = Date.now(); this.updateStats('streak', "Initializing...", amount, amount, amount, "...", 0, "..."); const user = await this.getUser(sub, headers); let farmStart; if (!user.streakData || !user.streakData.currentStreak) { const n = new Date(); n.setDate(n.getDate() - 1); farmStart = n; } else { try { const s = new Date(user.streakData.currentStreak.startDate); s.setDate(s.getDate() - 1); farmStart = s; } catch (e) { GlobalPopup("Error parsing date."); this.activeTasks.delete('streak'); this.taskProgress.delete('streak'); this.taskTimers.delete('streak'); this.toggleRunBtn('streak-btn-run', false); this.updateDashRightCard(); return; } } let dCnt = 0; for (let i = 0; i < amount; i++) { if (!this.activeTasks.has('streak')) break; dCnt++; const left = amount - dCnt; const pct = (dCnt / amount) * 100; this.updateStats('streak', "RUNNING", amount, amount, left, (left * ((delayMs || 100) / 1000)).toFixed(1) + "s", pct, "..."); let 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: config.api.sessions, headers, data: JSON.stringify({ "challengeTypes": config.chBody, "fromLanguage": user.fromLanguage || 'en', "isFinalLevel": false, "isV2": true, "juicy": true, "learningLanguage": user.learningLanguage || 'fr', "smartTipsVersion": 2, "type": "GLOBAL_PRACTICE" }), onload: (res) => { if (res.status === 200) sess = JSON.parse(res.responseText); r(); }, onerror: () => r() })); if (sess && sess.id) { await new Promise(r => GM_xmlhttpRequest({ method: "PUT", url: `${config.api.sessions}/${sess.id}`, headers, data: JSON.stringify({ ...sess, "heartsLeft": 5, "startTime": end - 1, "endTime": end, "enableBonusPoints": false, "failed": false, "maxInLessonStreak": 9, "shouldLearnThings": true }), onload: () => r(), onerror: () => r() })); } await new Promise(r => setTimeout(r, delayMs || 100)); } if(this.activeTasks.has('streak')) { this.activeTasks.delete('streak'); this.taskProgress.delete('streak'); this.taskTimers.delete('streak'); this.toggleRunBtn('streak-btn-run', false); this.updateStats('streak', "COMPLETED", amount, amount, 0, "0s", 100, this.formatTime(Date.now() - startTime)); this.updateDashRightCard(); GlobalNotif("DuoRain Active", `Restored ${amount} Streak days`); document.getElementById('_refresh_profile')?.click(); } } async runLeague(tRank, delayMs, bufferXP) { if (this.activeTasks.has('league')) { this.stopTask('league'); return; } const token = this.getJWT(); if (!token) { GlobalPopup("Please login."); return; } const sub = this.decJWT(token).sub; const headers = this.formatHeads(token); this.activeTasks.add('league'); this.taskTimers.set('league', Date.now()); this.toggleRunBtn('league-btn-run', true); this.updateDashRightCard(); GlobalNotif("DuoRain Active", "League Saver Started"); this.updateStats('league', "Checking Rank...", `# ${tRank}`, "...", "...", "...", 0, "...", "..."); while (true) { if(!this.activeTasks.has('league')) break; const s = await this.getLstatus(sub, headers); if (!s) { GlobalPopup("Failed to fetch leaderboard."); break; } document.getElementById('league-current-rank').innerText = `# ${s.rank}`; if (s.rank <= tRank) { this.updateStats('league', "GOAL REACHED", `# ${tRank}`, 0, 0, "0s", 100, "Done!", "0"); break; } const tUser = s.rankings[tRank - 1]; if (!tUser) break; const safeBuffer = (bufferXP && bufferXP >= 10) ? bufferXP : 60; const need = (tUser.score - s.score) + safeBuffer; if (need > 0) { await this.runXP(need, delayMs, 'league'); if(!this.activeTasks.has('league')) break; } else { this.updateStats('league', "Waiting for update...", `# ${tRank}`, "...", "...", "...", 50, "...", "Checking..."); await new Promise(r => setTimeout(r, 2000)); } } if(this.activeTasks.has('league')) { this.activeTasks.delete('league'); this.taskProgress.delete('league'); this.taskTimers.delete('league'); this.toggleRunBtn('league-btn-run', false); this.updateStats('league', "COMPLETED", "League Goal Reached", 0, 0, "0s", 100, "Done!", "0"); this.updateDashRightCard(); GlobalNotif("DuoRain Active", "League Goal Reached"); document.getElementById('_refresh_profile')?.click(); } } } class DuoRain { constructor() { this.state = { mini: false, activeId: 'dash', shopItems: [], questFilter: 'MONTHLY' }; this.drag = { active: false, x: 0, y: 0 }; this.orbDrag = { active: false, x: 0, y: 0, elX: 0, elY: 0, hasMoved: false }; this.delays = this.loadDelays(); this.leagueBuffer = this.loadLeagueBuffer(); this.fx = new functions(); this.dashQuestIndex = 0; this.miscState = { type: 'fake_max' }; this.notifPos = localStorage.getItem('dr_notif_pos') || config.defaults.notifPos; } loadDelays() { const s = localStorage.getItem('dr_delays'); return s ? JSON.parse(s) : config.defaults.delays; } loadLeagueBuffer() { const s = localStorage.getItem('dr_league_buffer'); return s ? parseInt(s) : config.defaults.leagueBuffer; } saveDelays() { localStorage.setItem('dr_delays', JSON.stringify(this.delays)); } saveLeagueBuffer() { localStorage.setItem('dr_league_buffer', this.leagueBuffer); } init() { if (!document.querySelector('link[href*="Fugaz+One"]')) { const link = document.createElement('link'); link.href = 'https://fonts.googleapis.com/css2?family=Fugaz+One&display=swap'; link.rel = 'stylesheet'; document.head.appendChild(link); } GM_addStyle(CSS); const old = document.getElementById(config.id.app); if (old) old.remove(); document.body.insertAdjacentHTML('beforeend', HTML); this.cache(); this.bind(); this.manageLogo(); this.el.orb.style.transform = 'scale(0)'; this.el.delayInput.value = this.delays[this.el.delayWrapper.getAttribute('data-value')]; this.el.leagueBufferInput.value = this.leagueBuffer; this.updateMiscUI(); this.updateNotifPosUI(); GlobalNotif = (title, msg) => this.showNotif(title, msg); GlobalPopup = (msg) => this.showAlert(msg); GlobalConfirm = (title, msg) => this.showConfirm(title, msg); setTimeout(() => this.moveLine(document.querySelector('.nav-btn.active')), 200); this.loadDashData(); this.loadQuests(true); this.updateSaveCardVisibility(); this.monitorTheme(); } async saveAccount(token, dec) { const accounts = GM_getValue('dr_accounts', []); const exists = accounts.find(a => a.id === dec.sub); if(exists) return; const h = this.fx.formatHeads(token); const u = await this.fx.getUser(dec.sub, h); let pic = assets.logo; if(u && u.picture) pic = u.picture + "/large"; accounts.push({ id: dec.sub, username: u ? u.username : "User", pic: pic, token: token }); GM_setValue('dr_accounts', accounts); this.showNotif("Accounts", "Account saved successfully"); this.loadDashData(); } renderAccountsCard(currentUser, isSaved, currentSub) { const card = document.getElementById('acc-current-card'); if(!card) return; let pic = assets.logo; if(currentUser.picture) { pic = currentUser.picture.replace(/\/(medium|large|small)$/, '/xlarge'); if(!pic.endsWith('/xlarge') && pic.includes('duolingo.com/ssr-avatars')) pic += '/xlarge'; } let hoverHtml = `
Currently Logged-in
`; card.innerHTML = `
${currentUser.username} Logged In
${hoverHtml} `; } switchAccount(id) { const accounts = GM_getValue('dr_accounts', []); const target = accounts.find(a => a.id == id); if(!target) return; document.cookie = `jwt_token=${target.token}; domain=.duolingo.com; path=/; max-age=31536000`; window.location.reload(); } async manageLogo() { const url = assets.logo; const kData = 'dr_logo_data'; const kSize = 'dr_logo_size'; const setImg = (src) => { const list = [ this.el.orb.querySelector('img'), this.el.win.querySelector('.logo-img'), document.querySelector('#dr-modal img'), document.querySelector('.dr-notif-icon img') ]; assets.logo = src; list.forEach(el => { if(el) el.src = src; }); const modalImg = document.querySelector('.header-icon-box img'); if(modalImg) modalImg.src = src; }; try { const cachedData = sessionStorage.getItem(kData); const cachedSize = sessionStorage.getItem(kSize); const head = await fetch(url, { method: 'HEAD', cache: 'no-cache' }); const netSize = head.headers.get('content-length'); if (cachedData && cachedSize === netSize) { setImg(cachedData); return; } const resp = await fetch(url); const blob = await resp.blob(); const reader = new FileReader(); reader.onload = () => { const b64 = reader.result; try { sessionStorage.setItem(kData, b64); sessionStorage.setItem(kSize, netSize); } catch(e) {} setImg(b64); }; reader.readAsDataURL(blob); } catch (e) { const c = sessionStorage.getItem(kData); if(c) setImg(c); } } updateMiscUI() { const type = this.miscState.type; const btn = document.getElementById('misc-btn-get'); const sw = document.getElementById('misc-fake-switch'); const wrap = document.getElementById('misc-control-wrapper'); if(type === 'legit_trial') { sw.style.display = 'none'; btn.style.display = 'block'; } else { btn.style.display = 'none'; sw.style.display = 'block'; const key = 'dr_fake_max'; const isActive = localStorage.getItem(key) === 'true'; sw.querySelector('input').checked = isActive; } } updateNotifPosUI() { const wrap = document.getElementById('notif-pos-wrapper'); if(!wrap) return; wrap.setAttribute('data-value', this.notifPos); const map = { 'bl': "Bottom Left", 'br': "Bottom Right", 'c': "Center" }; wrap.querySelector('.c-selected-text').innerText = map[this.notifPos]; wrap.querySelectorAll('.c-option').forEach(o => { if(o.getAttribute('data-value') === this.notifPos) o.classList.add('selected'); else o.classList.remove('selected'); }); document.getElementById('dr-notif-area').className = `dr-notif-container pos-${this.notifPos}`; } monitorTheme() { const update = () => { let isDark = false; try { const bgColor = window.getComputedStyle(document.body).backgroundColor; const rgb = bgColor.match(/\d+/g); if (rgb) { const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; isDark = brightness < 128; } } catch(e) {} if (isDark) { this.el.root.classList.add('dr-dark'); this.el.root.classList.remove('dr-light'); } else { this.el.root.classList.add('dr-light'); this.el.root.classList.remove('dr-dark'); } }; update(); setInterval(update, 1000); } cache() { const $ = (s) => document.getElementById(s); this.el = { root: $(config.id.app), win: $(config.id.win), orb: $(config.id.orb), magic: document.querySelector('.magic-pill'), title: $('dr-title'), subtitle: $('dr-subtitle'), headerIcon: $('dr-header-icon'), close: $('dr-close'), btns: document.querySelectorAll('.nav-btn'), pages: document.querySelectorAll('.page'), drags: document.querySelectorAll('.drag'), xpBtn: $('xp-btn-run'), xpInput: $('xp-input'), gemBtn: $('gem-btn-run'), gemInput: $('gem-input'), streakBtn: $('streak-btn-run'), streakInput: $('streak-input'), leagueBtn: $('league-btn-run'), leagueWrapper: $('league-select-wrapper'), leagueOptions: $('league-options'), leagueRankText: $('league-current-rank'), leagueXpText: $('league-current-xp'), delayWrapper: $('set-delay-wrapper'), delayInput: $('set-delay-input'), delaySetBtn: $('set-delay-btn'), refreshProfile: $('_refresh_profile'), privacyToggle: $('_privacy_toggle'), privacyLabel: $('_privacy_label'), shopSearch: $('shop-search'), shopContainer: $('shop-container'), shopReload: $('shop-reload'), shopEmpty: $('shop-empty'), questClaim: $('quest-btn-claim'), questContainer: $('quest-container'), questFilters: document.querySelectorAll('.quest-filter-btn'), questReload: $('quest-reload'), dqHead: $('dq-head'), dqImg: $('dq-img'), dqText: $('dq-text'), dqBtn: $('dq-btn'), dqLeft: $('dq-left'), dqRight: $('dq-right'), dqRank: $('dq-rank'), dqLeagueXp: $('dq-league-xp'), dashCardContent: $('dash-card-content'), leagueBufferInput: $('set-league-buffer-input'), leagueBufferBtn: $('set-league-buffer-btn'), notifPosWrapper: $('notif-pos-wrapper'), notifPosBtn: $('notif-pos-btn'), accOpenMenu: $('acc-open-menu'), accSaveCard: $('acc-save-card'), accSaveBtn: $('acc-save-btn') }; } bind() { this.el.btns.forEach(b => b.addEventListener('click', () => this.switchTab(b))); this.el.close.addEventListener('click', () => this.toggle(true)); this.el.orb.addEventListener('mousedown', (e) => { this.orbDrag = { active: true, x: e.clientX, y: e.clientY, elX: this.el.orb.getBoundingClientRect().left, elY: this.el.orb.getBoundingClientRect().top, hasMoved: false }; this.el.orb.style.transition = 'none'; e.preventDefault(); }); this.el.drags.forEach(d => { d.addEventListener('mousedown', (e) => { this.drag.active = true; this.drag.x = e.clientX - this.el.win.offsetLeft; this.drag.y = e.clientY - this.el.win.offsetTop; e.preventDefault(); }); }); document.addEventListener('mousemove', (e) => { if (this.drag.active) { e.preventDefault(); let x = e.clientX - this.drag.x; let y = e.clientY - this.drag.y; x = Math.max(0, Math.min(x, window.innerWidth - this.el.win.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - this.el.win.offsetHeight)); this.el.win.style.left = `${x}px`; this.el.win.style.top = `${y}px`; } if (this.orbDrag.active) { e.preventDefault(); const dx = e.clientX - this.orbDrag.x; const dy = e.clientY - this.orbDrag.y; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) this.orbDrag.hasMoved = true; let nX = this.orbDrag.elX + dx; let nY = this.orbDrag.elY + dy; const maxW = window.innerWidth - 68; const maxH = window.innerHeight - 68; nX = Math.max(0, Math.min(nX, maxW)); nY = Math.max(0, Math.min(nY, maxH)); this.el.orb.style.left = nX + 'px'; this.el.orb.style.top = nY + 'px'; this.el.orb.style.bottom = 'auto'; this.el.orb.style.right = 'auto'; } }); document.addEventListener('mouseup', () => { this.drag.active = false; if (this.orbDrag.active) { this.orbDrag.active = false; this.el.orb.style.transition = 'transform .3s cubic-bezier(.34,1.56,.64,1)'; if (!this.orbDrag.hasMoved) { this.toggle(false); } } }); this.el.xpBtn.addEventListener('click', () => { if(this.fx.activeTasks.has('xp')) { this.fx.stopTask('xp'); return; } const v = this.el.xpInput.value; parseInt(v) >= 30 ? this.fx.runXP(v, this.delays.xp, 'xp') : this.showAlert("Min XP: 30"); }); this.el.gemBtn.addEventListener('click', () => { if(this.fx.activeTasks.has('gem')) { this.fx.stopTask('gem'); return; } const v = this.el.gemInput.value; parseInt(v) >= 1 ? this.fx.runGem(parseInt(v), this.delays.gem) : this.showAlert("Min loops: 1"); }); this.el.streakBtn.addEventListener('click', () => { if(this.fx.activeTasks.has('streak')) { this.fx.stopTask('streak'); return; } const v = this.el.streakInput.value; parseInt(v) >= 1 ? this.fx.runStreak(parseInt(v), this.delays.streak) : this.showAlert("Min days: 1"); }); this.el.leagueBtn.addEventListener('click', () => { if(this.fx.activeTasks.has('league')) { this.fx.stopTask('league'); return; } const v = this.el.leagueWrapper.getAttribute('data-value'); if (v) this.fx.runLeague(parseInt(v), this.delays.league, this.leagueBuffer); }); this.el.delaySetBtn.addEventListener('click', () => { const t = this.el.delayWrapper.getAttribute('data-value'); const v = parseInt(this.el.delayInput.value); if (!isNaN(v) && v >= 0) { this.delays[t] = v; this.saveDelays(); const og = this.el.delaySetBtn.innerText; this.el.delaySetBtn.innerText = "SAVED"; setTimeout(() => { this.el.delaySetBtn.innerText = og; }, 1000); } else this.showAlert("Invalid delay"); }); this.el.leagueBufferBtn.addEventListener('click', () => { const v = parseInt(this.el.leagueBufferInput.value); if(!isNaN(v) && v >= 10) { this.leagueBuffer = v; this.saveLeagueBuffer(); const og = this.el.leagueBufferBtn.innerText; this.el.leagueBufferBtn.innerText = "SAVED"; setTimeout(() => { this.el.leagueBufferBtn.innerText = og; }, 1000); } else this.showAlert("Buffer must be at least 10 XP"); }); this.el.notifPosBtn.addEventListener('click', () => { const v = this.el.notifPosWrapper.getAttribute('data-value'); this.notifPos = v; localStorage.setItem('dr_notif_pos', v); this.updateNotifPosUI(); this.showNotif("Settings", "Notification Position Saved"); const og = this.el.notifPosBtn.innerText; this.el.notifPosBtn.innerText = "SAVED"; setTimeout(() => { this.el.notifPosBtn.innerText = og; }, 1000); }); this.el.accOpenMenu.addEventListener('click', () => this.openAccountManager()); if(this.el.accSaveBtn) { this.el.accSaveBtn.addEventListener('click', async () => { const t = this.fx.getJWT(); if(t) { this.el.accSaveBtn.innerText = "SAVING..."; const dec = this.fx.decJWT(t); await this.saveAccount(t, dec); this.el.accSaveBtn.innerText = "SAVED"; this.updateSaveCardVisibility(); } }); } const miscSwitch = document.getElementById('misc-fake-switch'); if(miscSwitch) { const miscInput = miscSwitch.querySelector('input'); miscInput.addEventListener('click', async (e) => { e.preventDefault(); const isEnabled = localStorage.getItem('dr_fake_max') === 'true'; const action = isEnabled ? 'disable' : 'enable'; const confirm = await GlobalConfirm(`${action.charAt(0).toUpperCase() + action.slice(1)} Max`, `Are you sure you want to ${action} Free Max? This requires a page reload.`); if (confirm) { localStorage.setItem('dr_fake_max', (!isEnabled).toString()); localStorage.setItem('dr_fake_super', 'false'); window.location.reload(); } }); } document.getElementById('misc-btn-get').addEventListener('click', async () => { const btn = document.getElementById('misc-btn-get'); btn.innerText = "Processing..."; const t = this.fx.getJWT(); if(t) { const sub = this.fx.decJWT(t).sub; const h = this.fx.formatHeads(t); const success = await this.fx.buyLegitSuper(h, sub); if(success) { btn.innerText = "GOT"; btn.classList.add('btn-got'); setTimeout(() => { btn.innerText = "GET"; btn.classList.remove('btn-got'); }, 3000); this.showAlert("Super Trial Activated! (Refresh might be needed)"); } else { btn.innerText = "FAILED"; setTimeout(() => { btn.innerText = "GET"; }, 2000); } } else { btn.innerText = "LOGIN REQ"; } }); this.el.refreshProfile.addEventListener('click', () => { this.loadDashData(); }); this.el.privacyToggle.addEventListener('change', async (e) => { const isP = e.target.checked; this.el.privacyLabel.innerText = isP ? "PRIVATE" : "PUBLIC"; const t = this.fx.getJWT(); if (t) await this.fx.setPstatus(this.fx.decJWT(t).sub, this.fx.formatHeads(t), isP); }); this.el.shopSearch.addEventListener('input', (e) => { const t = e.target.value.toLowerCase().trim(); let total = 0; document.querySelectorAll('.shop-category-wrapper').forEach(w => { let catCount = 0; w.querySelectorAll('.shop-item').forEach(i => { const m = i.querySelector('.item-name').innerText.toLowerCase().trim().startsWith(t); i.style.display = m ? 'flex' : 'none'; if (m) catCount++; }); w.style.display = catCount > 0 ? 'block' : 'none'; total += catCount; }); this.el.shopEmpty.style.display = total === 0 ? 'block' : 'none'; }); this.el.shopReload.addEventListener('click', () => { localStorage.removeItem('dr_shop_data'); this.el.shopContainer.innerHTML = '

Reloading items...

'; this.loadShop(); }); this.el.questReload.addEventListener('click', () => this.loadQuests(false)); this.el.questClaim.addEventListener('click', () => this.finishCategory()); this.el.questFilters.forEach(b => { b.addEventListener('click', (e) => { this.el.questFilters.forEach(x => x.classList.remove('active')); e.target.classList.add('active'); this.state.questFilter = e.target.dataset.filter; this.renderQuests(); }); }); this.el.dqLeft.addEventListener('click', () => { this.dashQuestIndex = (this.dashQuestIndex - 1 + 3) % 3; this.updateDashQuestCard(); }); this.el.dqRight.addEventListener('click', () => { this.dashQuestIndex = (this.dashQuestIndex + 1) % 3; this.updateDashQuestCard(); }); this.el.dqBtn.addEventListener('click', async () => { const types = ['DAILY', 'MONTHLY', 'FRIENDS']; const target = types[this.dashQuestIndex]; const t = this.fx.getJWT(); const sub = this.fx.decJWT(t).sub; const h = this.fx.getGoalHeaders(t); this.el.dqBtn.innerText = "Processing..."; const uniqueMetrics = new Set(); this.questState.schema.goals.forEach(g => { if (g.category && g.category.includes(target)) { if (g.metric) uniqueMetrics.add(g.metric); } }); if (uniqueMetrics.size > 0) { await this.fx.bruteForceGoals(sub, h, Array.from(uniqueMetrics)); this.loadQuests(true); } else { this.el.dqBtn.innerText = "Error"; setTimeout(() => this.updateDashQuestCard(), 1000); } }); document.getElementById('dr-modal-btn').addEventListener('click', () => { document.getElementById('dr-modal-overlay').classList.remove('active'); document.getElementById('dr-modal-overlay').style.pointerEvents = "none"; this.el.win.style.filter = "none"; }); this.initDropD(); } updateSaveCardVisibility() { const t = this.fx.getJWT(); if(!t) return; const sub = this.fx.decJWT(t).sub; const accounts = GM_getValue('dr_accounts', []); const isSaved = accounts.some(a => a.id == sub); if(this.el.accSaveCard) { this.el.accSaveCard.style.display = isSaved ? 'none' : 'block'; } } openAccountManager() { document.getElementById('dr-modal-title').innerText = "Account Management"; const content = document.getElementById('dr-modal-content'); const acts = document.getElementById('dr-modal-actions'); const renderList = () => { const accounts = GM_getValue('dr_accounts', []); const currentSub = (this.fx.decJWT(this.fx.getJWT()) || {}).sub; if(accounts.length === 0) return `

No saved accounts.

`; return accounts.map(acc => { const isActive = acc.id == currentSub; const actionBtn = isActive ? `ACTIVE` : ``; return `
${acc.username}
${actionBtn}
`; }).join(''); }; const updateContent = () => { content.innerHTML = `
${renderList()}
`; const list = document.getElementById('acc-modal-list'); list.querySelectorAll('.acc-login-btn').forEach(b => { b.onclick = () => this.switchAccount(b.dataset.id); }); list.querySelectorAll('.acc-del-btn').forEach(b => { b.onclick = async () => { const c = await GlobalConfirm("Remove Account", "Are you sure you want to remove this account?"); if(c) { const accounts = GM_getValue('dr_accounts', []); const n = accounts.filter(a => a.id != b.dataset.id); GM_setValue('dr_accounts', n); updateContent(); this.loadDashData(); this.updateSaveCardVisibility(); } } }); }; updateContent(); acts.innerHTML = ``; const ov = document.getElementById('dr-modal-overlay'); ov.style.pointerEvents = "auto"; ov.classList.add('active'); this.el.win.style.filter = "blur(5px)"; document.getElementById('dr-modal-close').onclick = () => { ov.classList.remove('active'); ov.style.pointerEvents = "none"; this.el.win.style.filter = "none"; }; } initDropD() { document.querySelectorAll('.c-select-trigger').forEach(t => { t.addEventListener('click', (e) => { e.stopPropagation(); const p = t.parentElement; document.querySelectorAll('.c-select').forEach(s => { if (s !== p) s.classList.remove('open'); }); p.classList.toggle('open'); }); }); document.addEventListener('click', (e) => { if (e.target.closest('.c-option')) { e.stopPropagation(); const o = e.target.closest('.c-option'); const w = o.closest('.c-select'); const v = o.getAttribute('data-value'); w.querySelector('.c-selected-text').innerText = o.innerText; w.setAttribute('data-value', v); w.querySelectorAll('.c-option').forEach(op => op.classList.remove('selected')); o.classList.add('selected'); w.classList.remove('open'); if (w.id === 'set-delay-wrapper') this.el.delayInput.value = this.delays[v]; if (w.id === 'misc-super-select-wrapper') { this.miscState.type = v; const descEl = document.getElementById('misc-desc'); if(v === 'fake_max') descEl.innerText = "Activate Free Max in your account indefinitely and at no cost"; else descEl.innerText = "Activate Free 3-Days Super trial"; this.updateMiscUI(); } } else { document.querySelectorAll('.c-select').forEach(s => s.classList.remove('open')); } }); } showNotif(title, msg) { const area = document.getElementById('dr-notif-area'); const el = document.createElement('div'); el.className = 'dr-notification'; el.innerHTML = `
×
${title} ${msg}
`; const closeBtn = el.querySelector('.dr-notif-close'); const remove = () => { if(el.classList.contains('hiding')) return; el.classList.remove('show'); el.classList.add('hiding'); setTimeout(() => el.remove(), 400); }; closeBtn.onclick = remove; area.prepend(el); requestAnimationFrame(() => el.classList.add('show')); setTimeout(remove, 5000); } showAlert(msg) { document.getElementById('dr-modal-title').innerText = "Alert"; document.getElementById('dr-modal-content').innerHTML = `

${msg}

`; document.getElementById('dr-modal-actions').innerHTML = ``; const ov = document.getElementById('dr-modal-overlay'); ov.style.pointerEvents = "auto"; ov.classList.add('active'); this.el.win.style.filter = "blur(5px)"; document.getElementById('dr-modal-btn').onclick = () => { ov.classList.remove('active'); ov.style.pointerEvents = "none"; this.el.win.style.filter = "none"; }; } async showConfirm(title, msg) { return new Promise((resolve) => { document.getElementById('dr-modal-title').innerText = title; document.getElementById('dr-modal-content').innerHTML = `

${msg}

`; const acts = document.getElementById('dr-modal-actions'); acts.innerHTML = ` `; const ov = document.getElementById('dr-modal-overlay'); ov.style.pointerEvents = "auto"; ov.classList.add('active'); this.el.win.style.filter = "blur(5px)"; const close = (res) => { ov.classList.remove('active'); ov.style.pointerEvents = "none"; this.el.win.style.filter = "none"; resolve(res); }; document.getElementById('dr-confirm-yes').onclick = () => close(true); document.getElementById('dr-confirm-no').onclick = () => close(false); }); } async loadDashData() { const t = this.fx.getJWT(); if (t) { const sub = this.fx.decJWT(t).sub; const h = this.fx.formatHeads(t); const u = await this.fx.getUser(sub, h); const p = await this.fx.getPstatus(sub, h); if (u) { this.fx.updateDash(u, p); const accounts = GM_getValue('dr_accounts', []); const isSaved = accounts.some(a => a.id === sub); this.renderAccountsCard(u, isSaved, sub); } const l = await this.fx.getLstatus(sub, h); const elRank = document.getElementById('dq-rank'); const elXP = document.getElementById('dq-league-xp'); if (l) { this.el.leagueRankText.innerText = `# ${l.rank}`; this.el.leagueXpText.innerText = `${l.score.toLocaleString()} XP`; if(elRank && this.fx.activeTasks.size === 0) elRank.innerText = `# ${l.rank}`; if(elXP && this.fx.activeTasks.size === 0) elXP.innerText = `${l.score.toLocaleString()} XP`; this.popLeagueSelect(l.rank); } else { if(elRank && this.fx.activeTasks.size === 0) elRank.innerText = "--"; if(elXP && this.fx.activeTasks.size === 0) elXP.innerText = "No League Data"; } this.updateSaveCardVisibility(); } } popLeagueSelect(rank) { const c = this.el.leagueOptions; c.innerHTML = ""; if (rank <= 1) { c.innerHTML = `
Max Rank Reached
`; this.el.leagueWrapper.querySelector('.c-selected-text').innerText = "Max"; this.el.leagueWrapper.setAttribute('data-value', ""); return; } for (let i = 1; i < rank; i++) { const d = document.createElement('div'); d.className = 'c-option'; if (i === 1) d.classList.add('selected'); d.setAttribute('data-value', i); d.innerText = `# ${i}`; c.appendChild(d); } this.el.leagueWrapper.querySelector('.c-selected-text').innerText = "# 1"; this.el.leagueWrapper.setAttribute('data-value', "1"); } formatItem(id) { return id.split('_').map(w => { if (w === 'xp') return 'XP'; if (!isNaN(w)) return w; return w.charAt(0).toUpperCase() + w.slice(1); }).join(' '); } defineShop(items) { const valid = items.filter(i => i.currencyType === "XGM" && !i.id.includes('gift')); const p = valid.map(i => { let name = i.name || this.formatItem(i.id); let cat = "Misc"; let icon = assets.shopIcons.misc; if (i.id.includes('streak_freeze')) { cat = "Streak Freezes"; icon = assets.shopIcons.streak; } else if (i.id.includes('xp_boost')) { cat = "XP Boosts"; icon = assets.shopIcons.xp; if (i.id.match(/\d+$/)) name += " Mins"; } else if (i.id.includes('health') || i.id.includes('heart')) { cat = "Hearts"; icon = assets.shopIcons.heart; if (i.id.includes('partial')) { const n = i.id.match(/\d$/); if (n) name = `Health Refill Partial (${n[0]} Heart)`; } } else if (i.id.includes('gem')) { cat = "Gems"; icon = assets.shopIcons.gem; } else if (i.type === "outfit") { cat = "Outfits"; icon = assets.shopIcons.outfit; } else if (i.id.includes('free_taste')) { cat = "Free Taste"; icon = assets.shopIcons.free; } return { ...i, displayName: name, category: cat, icon }; }); const ord = ["Streak Freezes", "XP Boosts", "Hearts", "Gems", "Outfits", "Free Taste", "Misc"]; return p.sort((a, b) => { const ca = ord.indexOf(a.category); const cb = ord.indexOf(b.category); return ca !== cb ? ca - cb : a.displayName.localeCompare(b.displayName); }); } renderShop(items) { this.el.shopContainer.innerHTML = ""; const grp = {}; items.forEach(i => { if (!grp[i.category]) grp[i.category] = []; grp[i.category].push(i); }); for (const c in grp) { const w = document.createElement('div'); w.className = 'shop-category-wrapper'; const h = document.createElement('div'); h.className = 'category-header'; h.innerText = c; w.appendChild(h); const g = document.createElement('div'); g.className = 'shop-grid'; grp[c].forEach(i => { const card = document.createElement('div'); card.className = 'shop-item'; card.innerHTML = `
${i.displayName}
`; g.appendChild(card); }); w.appendChild(g); this.el.shopContainer.appendChild(w); } this.el.shopContainer.querySelectorAll('.item-btn').forEach(b => { b.addEventListener('click', async () => { const id = b.getAttribute('data-id'); const item = items.find(i => i.id === id); if (!item) return; b.classList.add('buying'); b.innerText = "0%"; const t = this.fx.getJWT(); if (t) { const h = this.fx.formatHeads(t); const sub = this.fx.decJWT(t).sub; setTimeout(() => { b.innerText = "50%"; }, 300); const ok = await this.fx.buyShop(h, sub, item); b.innerText = "100%"; setTimeout(() => { if (ok) { b.innerText = "GOT"; b.classList.add('btn-got'); this.showNotif("Shop", `Acquired ${item.displayName}`); setTimeout(() => { b.innerText = "GET"; b.classList.remove('btn-got'); b.classList.remove('buying'); }, 3000); } else { b.innerText = "FAILED"; b.classList.remove('buying'); setTimeout(() => { b.innerText = "GET"; }, 2000); this.showAlert("Failed to buy item."); } }, 300); } }); }); } async loadShop() { const cached = localStorage.getItem('dr_shop_data'); if (cached) { try { const items = JSON.parse(cached); if (items && items.length > 0) { this.renderShop(this.defineShop(items)); return; } } catch(e) { localStorage.removeItem('dr_shop_data'); } } const t = this.fx.getJWT(); if (t) { const h = this.fx.formatHeads(t); const s = this.fx.decJWT(t).sub; const items = await this.fx.getShop(h, s); if(items && items.length > 0) localStorage.setItem('dr_shop_data', JSON.stringify(items)); this.renderShop(this.defineShop(items)); } } async loadQuests(silent = false) { const t = this.fx.getJWT(); if (!t) return; const sub = this.fx.decJWT(t).sub; const headers = this.fx.getGoalHeaders(t); if (!silent) { this.el.questContainer.innerHTML = `

Refreshing quest data...

`; } const user = await this.fx.getUser(sub, this.fx.formatHeads(t)); this.questState = { creationDate: user.trackingProperties?.creation_date_new ? new Date(user.trackingProperties.creation_date_new) : null }; const schema = await this.fx.getGoals(headers); const progress = await this.fx.getUserProgress(sub, headers); if (schema && progress) { this.questState.schema = schema; this.questState.progress = progress.goals?.progress || {}; this.questState.earned = new Set(progress.badges?.earned || []); this.renderQuests(); this.updateDashQuestCard(); } else { if (!silent) this.el.questContainer.innerHTML = `

Error loading quests.

`; } } isQuestOlder(goalId) { if (!this.questState.creationDate) return false; const m = goalId.match(/^(\d{4})_(\d{2})_monthly/); if (m) { const y = parseInt(m[1]), mo = parseInt(m[2]) - 1; const cY = this.questState.creationDate.getFullYear(), cM = this.questState.creationDate.getMonth(); if (y < cY || (y === cY && mo < cM)) return true; } return false; } updateMainButton() { const filter = this.state.questFilter; let label = "FINISH ALL"; if (filter === 'MONTHLY') label = "FINISH MONTHLY"; else if (filter === 'DAILY') label = "FINISH DAILY"; else if (filter === 'FRIENDS') label = "FINISH FRIENDS"; else if (filter === 'WEEKLY') label = "FINISH WEEKLY"; this.el.questClaim.innerText = label; } renderQuests() { if (!this.questState || !this.questState.schema) return; const c = this.el.questContainer; c.innerHTML = ""; this.updateMainButton(); const map = new Map(); const monthlyGoals = [], otherGoals = []; const monthlyRegex = /^(\d{4}_\d{2})_monthly/; this.questState.schema.goals.forEach(g => { const match = g.goalId.match(monthlyRegex); if (match) { monthlyGoals.push({ key: match[1], goal: g }); } else { otherGoals.push(g); } }); monthlyGoals.forEach(item => { const existing = map.get(item.key); if (!existing) { map.set(item.key, item.goal); } else { const existingIsChallenge = existing.category.includes('CHALLENGE'); const newIsChallenge = item.goal.category.includes('CHALLENGE'); if (!existingIsChallenge && newIsChallenge) { map.set(item.key, item.goal); } } }); const allGoals = [...otherGoals, ...map.values()].reverse(); allGoals.forEach(g => { if (!g.category) return; if (this.state.questFilter !== 'ALL' && !g.category.includes(this.state.questFilter)) return; const isEarned = this.questState.earned.has(g.badgeId) || this.questState.earned.has(g.goalId); const isOld = this.isQuestOlder(g.goalId); let cur = 0; const raw = this.questState.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; } let remaining = Math.max(0, tgt - cur); const item = document.createElement('div'); item.className = `quest-item ${isEarned ? 'completed' : ''} ${isOld ? 'warning' : ''}`; let icon = assets.chest; if (g.category.includes("MONTHLY")) { const b = this.questState.schema.badges.find(x => x.badgeId === g.badgeId); if (b && b.icon?.enabled?.lightMode) icon = b.icon.enabled.lightMode.svg || b.icon.enabled.lightMode.url || icon; } let actionBtn = ''; if (!isEarned && remaining > 0) { actionBtn = ``; } item.innerHTML = `
${g.title?.uiString || g.goalId} ${isOld ? '⚠️' : ''}
${g.metric} • ${isEarned ? 'COMPLETED' : `${cur} / ${tgt}`}
${actionBtn}
`; item.querySelectorAll('.q-mini-btn').forEach(btn => { btn.onclick = async () => { if (isOld) { const confirm = await GlobalConfirm("Risk Warning", "This quest is old. Continuing might be risky. Proceed?"); if(!confirm) return; } btn.innerText = "..."; const t = this.fx.getJWT(); const sub = this.fx.decJWT(t).sub; const h = this.fx.getGoalHeaders(t); await this.fx.updateGoal(sub, h, btn.dataset.m, parseInt(btn.dataset.a), btn.dataset.id); btn.innerText = "OK"; this.showNotif("Quest", "Progress injected successfully"); setTimeout(() => this.loadQuests(true), 800); }; }); c.appendChild(item); }); } updateDashQuestCard() { if (!this.questState || !this.questState.schema) { this.el.dqText.innerText = "Data missing..."; return; } const types = ['DAILY', 'MONTHLY', 'FRIENDS']; const labels = ['Daily Quests', 'Monthly Quest', 'Friends Quest']; const currentType = types[this.dashQuestIndex]; let total = 0; let completed = 0; let icon = assets.chest; const currentDate = new Date(); const currentYearStr = currentDate.getFullYear().toString(); const currentMonthStr = (currentDate.getMonth() + 1).toString().padStart(2, '0'); const monthlyRegex = new RegExp(`^${currentYearStr}_${currentMonthStr}_monthly`); if (currentType === 'MONTHLY') { this.questState.schema.goals.forEach(g => { if (g.category && g.category.includes('MONTHLY')) { if (g.goalId.match(monthlyRegex)) { total++; const b = this.questState.schema.badges.find(x => x.badgeId === g.badgeId); if (b && b.icon?.enabled?.lightMode) icon = b.icon.enabled.lightMode.svg || b.icon.enabled.lightMode.url; const isEarned = this.questState.earned.has(g.badgeId) || this.questState.earned.has(g.goalId); const raw = this.questState.progress[g.goalId]; const cur = (typeof raw === 'number') ? raw : (raw?.progress || 0); const tgt = g.threshold || 1000; if (isEarned || cur >= tgt) completed++; } } }); } else { this.questState.schema.goals.forEach(g => { if (g.category && g.category.includes(currentType)) { total++; const isEarned = this.questState.earned.has(g.badgeId) || this.questState.earned.has(g.goalId); const raw = this.questState.progress[g.goalId]; const cur = (typeof raw === 'number') ? raw : (raw?.progress || 0); const tgt = g.threshold || 1; if (isEarned || cur >= tgt) completed++; } }); } const isDone = total > 0 && total === completed; this.el.dqHead.innerText = labels[this.dashQuestIndex]; this.el.dqImg.src = icon; if (total === 0) { this.el.dqText.innerText = `No active ${labels[this.dashQuestIndex].toLowerCase()}`; this.el.dqBtn.classList.add('done'); this.el.dqBtn.innerText = "NONE"; } else if (isDone) { this.el.dqText.innerText = `All ${labels[this.dashQuestIndex].toLowerCase()} have been completed!`; this.el.dqBtn.classList.add('done'); this.el.dqBtn.innerText = "COMPLETED"; } else { this.el.dqText.innerText = `Complete ${labels[this.dashQuestIndex].toLowerCase()}?`; this.el.dqBtn.classList.remove('done'); this.el.dqBtn.innerText = "FINISH"; } } async finishCategory() { if (!this.questState || !this.questState.schema) return; const activeFilter = this.state.questFilter; const confirm = await GlobalConfirm("Mass Completion", `Force complete quests in ${activeFilter} category?`); if (!confirm) return; this.el.questClaim.innerText = "PROCESSING..."; this.el.questClaim.classList.add('disabled'); const t = this.fx.getJWT(); const sub = this.fx.decJWT(t).sub; const h = this.fx.getGoalHeaders(t); const uniqueMetrics = new Set(); this.questState.schema.goals.forEach(g => { if (g.category && (activeFilter === 'ALL' || g.category.includes(activeFilter))) { if (g.metric) uniqueMetrics.add(g.metric); } }); if (uniqueMetrics.size > 0) { await this.fx.bruteForceGoals(sub, h, Array.from(uniqueMetrics)); this.showNotif("Mass Quest", "Finished processing quests"); } else { this.showAlert("No metrics found for this category."); } this.el.questClaim.innerText = "DONE"; setTimeout(() => { this.updateMainButton(); this.el.questClaim.classList.remove('disabled'); this.loadQuests(true); }, 1000); } switchTab(b) { const id = b.dataset.id; if (this.state.activeId === id) return; this.state.activeId = id; this.el.title.innerText = b.querySelector('span').innerText; this.el.subtitle.innerText = desc[id] || ""; const ic = b.querySelector('svg') || b.querySelector('img'); if (ic) { this.el.headerIcon.innerHTML = ''; this.el.headerIcon.appendChild(ic.cloneNode(true)); } this.el.btns.forEach(x => x.classList.remove('active')); b.classList.add('active'); this.moveLine(b); this.el.pages.forEach(p => p.classList.remove('active')); document.getElementById(`pg-${id}`).classList.add('active'); if (id === 'league') { this.loadDashData(); document.querySelector('.scroller').classList.add('no-scroll'); } else { document.querySelector('.scroller').classList.remove('no-scroll'); } if (id === 'shop') this.loadShop(); if (id === 'quest') this.loadQuests(true); } moveLine(b) { if (b) this.el.magic.style.transform = `translateY(${b.getBoundingClientRect().top - b.parentElement.getBoundingClientRect().top}px)`; } toggle(mini) { if (mini) { this.el.win.classList.add('mini'); setTimeout(() => { this.el.win.style.display = 'none'; this.el.orb.style.display = 'flex'; setTimeout(() => { this.el.orb.style.transform = 'scale(1)'; }, 50); }, 300); } else { this.el.orb.style.transform = 'scale(0) rotate(180deg)'; this.el.win.style.display = 'flex'; void this.el.win.offsetWidth; setTimeout(() => this.el.win.classList.remove('mini'), 100); } } } const preLoader = new functions(); preLoader.installInterceptors(); if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', () => new DuoRain().init()); else new DuoRain().init(); })();