Duolingo DuoFarmer

DuoFarmer to narzędzie, które pomaga farmić PD, Dni z rzędu, Klejnoty, a nawet naprawiać zamrożone Dni z rzędu na Duolingo!.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name               Duolingo DuoFarmer
// @namespace          https://duo-farmer.vercel.app
// @version            1.3.12
// @author             Lamduck
// @description        DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.
// @description:en     DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.
// @description:ar     DuoFarmer هي أداة تساعدك على تجميع نقاط الخبرة (XP) والسلاسل والجواهر أو حتى إصلاح سلسلة متجمدة على Duolingo!.
// @description:bg     DuoFarmer е инструмент, който ви помага да фармите XP, серии, скъпоценни камъни или дори да поправяте замразени серии в Duolingo!.
// @description:bn     DuoFarmer এমন একটি টুল যা আপনাকে Duolingo-তে XP, Streak, Gems ফার্ম করতে বা এমনকি জমে থাকা Streak মেরামত করতে সাহায্য করে!.
// @description:cs     DuoFarmer je nástroj, který vám pomůže farmit XP, série, drahokamy nebo dokonce opravit zamrzlou sérii na Duolingu!.
// @description:da     DuoFarmer er et værktøj, der hjælper dig med at farme XP, Streaks, Gems eller endda reparere en frossen streak på Duolingo!.
// @description:de     DuoFarmer ist ein Tool, das Ihnen hilft, XP zu farmen, Streaks zu farmen, Edelsteine zu farmen oder sogar eingefrorene Streaks auf Duolingo zu reparieren!.
// @description:el     Το DuoFarmer είναι ένα εργαλείο που σας βοηθά να φαρμάρετε XP, σερί, πετράδια ή ακόμα και να επισκευáσετε ένα παγωμένο σερί στο Duolingo!.
// @description:es     DuoFarmer es una herramienta que te ayuda a farmear XP, Rachas, Gemas ¡o incluso reparar una racha congelada en Duolingo!.
// @description:fi     DuoFarmer on työkalu, joka auttaa sinua farmaamaan XP:tä, putkia, jalokiviä tai jopa korjaamaan jäätyneen putken Duolingossa!.
// @description:fr     DuoFarmer est un outil qui vous aide à farmer de l'XP, des Séries, des Gemmes ou même à réparer une série gelée sur Duolingo !.
// @description:he     DuoFarmer הוא כלי שעוזר לך לצבור XP, רצפים, אבני חן או אפילו לתקן רצף קפוא ב-Duolingo!.
// @description:hi     DuoFarmer एक उपकरण है जो आपको Duolingo पर XP, स्ट्रीक, जेम्स फार्म करने या जमी हुई स्ट्रीक की मरम्मत करने में मदद करता है!.
// @description:hu     A DuoFarmer egy eszköz, amely segít XP-t, sorozatokat, drágaköveket farmolni, vagy akár egy befagyott sorozatot is megjavítani a Duolingón!.
// @description:id     DuoFarmer adalah alat yang membantu Anda farming XP, Streak, Permata, atau bahkan memperbaiki streak yang dibekukan di Duolingo!.
// @description:it     DuoFarmer è uno strumento che ti aiuta a farmare XP, Serie, Gemme o persino a riparare una serie congelata su Duolingo!.
// @description:ja     DuoFarmerは、DuolingoでXP、連続記録、ジェムを稼いだり、凍結された連続記録を修復したりするのに役立つツールです!.
// @description:ko     DuoFarmer는 듀오링고에서 XP, 연속 학습, 보석을 파밍하거나 얼어붙은 연속 학습을 수리하는 데 도움이 되는 도구입니다!.
// @description:ms     DuoFarmer ialah alat yang membantu anda mendapatkan XP, Streak, Permata atau bahkan membaiki streak yang beku di Duolingo!.
// @description:nl     DuoFarmer is een tool die je helpt XP te farmen, Streaks te farmen, Edelstenen te farmen of zelfs een bevroren streak op Duolingo te repareren!.
// @description:no     DuoFarmer er et verktøy som hjelper deg med å farme XP, Streaks, Gems eller til og med reparere en frossen streak på Duolingo!.
// @description:pl     DuoFarmer to narzędzie, które pomaga farmić PD, Dni z rzędu, Klejnoty, a nawet naprawiać zamrożone Dni z rzędu na Duolingo!.
// @description:pt-BR  DuoFarmer é uma ferramenta que te ajuda a farmar XP, Sequências, Gemas ou até mesmo reparar uma sequência congelada no Duolingo!.
// @description:ro     DuoFarmer este un instrument care te ajută să farmezi XP, serii, nestemate sau chiar să repari o serie înghețată pe Duolingo!.
// @description:ru     DuoFarmer — это инструмент, который помогает вам фармить опыт, серии, самоцветы и даже восстанавливать замороженные серии в Duolingo!.
// @description:sv     DuoFarmer är ett verktyg som hjälper dig att farma XP, Streaks, Ädelstenar eller till och med reparera en frusen streak på Duolingo!.
// @description:th     DuoFarmer เป็นเครื่องมือที่ช่วยให้คุณฟาร์ม XP, ฟาร์ม Streak, ฟาร์ม Gems หรือแม้แต่ซ่อมแซม Streak ที่ถูกแช่แข็งบน Duolingo!.
// @description:tr     DuoFarmer, Duolingo'da XP kasmanıza, Serileri kasmanıza, Mücevherleri kasmanıza ve hatta donmuş bir seriyi onarmanıza yardımcı olan bir araçtır!.
// @description:uk     DuoFarmer — це інструмент, який допомагає вам фармити досвід, серії, самоцвіти та навіть відновлювати заморожені серії в Duolingo!.
// @description:vi     DuoFarmer là một công cụ giúp bạn cày XP, cày Streak, cày Gems hoặc thậm chí phá băng streak bị đóng băng trên Duolingo!.
// @description:zh-CN  DuoFarmer 是一款可以帮助您在 Duolingo 上刷经验值、刷连续记录、刷宝石,甚至修复冻结的连续记录的工具!.
// @description:zh-TW  DuoFarmer 是一款可以幫助您在 Duolingo 上刷經驗值、刷連續記錄、刷寶石,甚至修復凍結的連續記錄的工具!.
// @license            CC BY-NC-SA 4.0
// @icon               https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// @match              https://*.duolingo.com/*
// @grant              GM_log
// @run-at             document-start
// ==/UserScript==

(function () {
  'use strict';

  const SETTINGS_KEY = "duofarmerSettings";
  const TARGET_URL_REGEX = /https?:\/\/(?:[a-zA-Z0-9-]+\.)?duolingo\.[a-zA-Z]{2,6}(?:\.[a-zA-Z]{2})?\/\d{4}-\d{2}-\d{2}\/users\/.+/;
  const CUSTOM_SHOP_ITEMS = {
    gold_subscription: {
      itemName: "gold_subscription",
      subscriptionInfo: {
        vendor: "STRIPE",
        renewing: true,
        isFamilyPlan: true,
        expectedExpiration: 9999999999e3
      }
    }
  };
  function isMaxEnabled() {
    try {
      const saved = localStorage.getItem(SETTINGS_KEY);
      if (saved) return JSON.parse(saved).enableMaxPatch || false;
    } catch (e) {
    }
    return false;
  }
  function shouldIntercept(url, method = "GET") {
    if (!isMaxEnabled()) return false;
    if (method.toUpperCase() !== "GET") return false;
    if (url.includes("/shop-items")) return false;
    return TARGET_URL_REGEX.test(url);
  }
  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;
    }
  }
  function removeManageSubscriptionSection(root = document) {
    const sections = root.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;
      }
    }
  }
  function initPatcher() {
    if (typeof window === "undefined") return;
    const originalFetch = window.fetch;
    window.fetch = function(resource, options) {
      const url = resource instanceof Request ? resource.url : resource;
      const method = resource instanceof Request ? resource.method : (options == null ? void 0 : 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);
          let hdrs = response.headers;
          try {
            const obj = {};
            response.headers.forEach((v, k) => obj[k] = v);
            hdrs = obj;
          } catch {
          }
          return new Response(modified, {
            status: response.status,
            statusText: response.statusText,
            headers: hdrs
          });
        }).catch((err) => {
          throw err;
        });
      }
      return originalFetch.apply(this, arguments);
    };
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    const originalXhrSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function(method, url, ...args) {
      this._method = method;
      this._url = url;
      originalXhrOpen.call(this, method, url, ...args);
    };
    XMLHttpRequest.prototype.send = function() {
      if (shouldIntercept(this._url, this._method)) {
        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);
    };
    const manageSubObserver = new MutationObserver(() => {
      if (isMaxEnabled()) removeManageSubscriptionSection();
    });
    manageSubObserver.observe(document.documentElement, { childList: true, subtree: true });
  }
  const templateRaw = `<div id="overlay"></div>
<div id="container">
  <div id="header">
    <div id="header-left">
      <img src="https://img.icons8.com/?size=48&id=mPfeGOwngGmN&format=png&color=000000">
      <span class="label">Duofarmer</span>
    </div>
    <button id="settings-btn">⚙️</button>
  </div>
  <div id="body">
    <table id="table-main" class="table">
      <thead>
        <tr>
          <th>Username</th>
          <th>From</th>
          <th>Learning</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td id="username">duofarmer</td>
          <td id="from">any</td>
          <td id="learn">any</td>
        </tr>
      </tbody>
    </table>
    <table id="table-progress" class="table">
      <thead>
        <tr>
          <th>Streak</th>
          <th>Gem</th>
          <th>XP</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td id="streak">0</td>
          <td id="gem">0</td>
          <td id="xp">0</td>
        </tr>
      </tbody>
    </table>
    <div id="action-row">
      <select id="select-option">
        <!-- <option value="option1">Option 1</option> -->
        <!-- <option value="option2">Option 2</option> -->
      </select>
      <button id="start-btn">Start</button>
      <button id="stop-btn" hidden>Stop</button>
    </div>
    <div id="notify">Getting user info, please wait until it's done.<br /> If it takes too long, please refresh the
      page.</div>
  </div>
  <div id="footer">
    <span class="label">If info is wrong, reload the page!</span>
  </div>
</div>
<div id="settings-container">
  <div id="settings-menu" class="modal-content">
    <div class="modal-header">
      <span class="label">Settings</span>
      <button id="settings-close" class="modal-close">✕</button>
    </div>
    <div class="modal-body">
      <div class="settings-group">
        <h3>Duolingo Max</h3>
        <div class="setting-item">
          <span>Enable Duolingo Max Patch
            <br>
            <i class="muted">(Bypass subscription check to get Duolingo Max!, thanks <a href="https://github.com/apersongithub/Duolingo-Unlimited-Hearts" target="_blank">apersongithub</a>)</i>
          </span>
          <input type="checkbox" id="enable-max-patch">
        </div>
      </div>
      <div class="settings-group">
        <h3>General</h3>
        <div class="setting-item">
          <span>Auto open UI onload</span>
          <input type="checkbox" id="auto-open-ui">
        </div>
        <div class="setting-item">
          <span>Auto start farming onload</span>
          <input type="checkbox" id="auto-start">
        </div>
        <div class="setting-item">
          <span>Default farming option</span>
          <select id="default-option">
            <!-- option auto -->
          </select>
        </div>
        <div class="setting-item">
          <span>Hide username</span>
          <input type="checkbox" id="hide-username">
        </div>
        <div class="setting-item">
          <span>Keep screen on</span>
          <input type="checkbox" id="keep-screen-on">
        </div>
      </div>
      <div class="settings-group">
        <h3>Performance</h3>
        <div class="setting-item">
          <span>Delay time (100ms - 10000ms):
            <br>
            <i class="muted">(Lower delay = faster = high limit rate ban)</i>
          </span>
          <input type="number" id="delay-time" min="100" max="10000" value="500">
        </div>
        <div class="setting-item">
          <span>Retry time (100ms - 10000ms):
            <br>
          </span>
          <input type="number" id="retry-time" min="100" max="10000" value="1000">
        </div>
        <div class="setting-item">
          <span>Auto stop after (min) (set 0 for unlimited)</span>
          <input type="number" id="auto-stop-time" min="0" max="60" value="0">
        </div>
      </div>
      <div class="settings-group">
        <h3>Advanced</h3>
        <div class="setting-item">
          <span>Get ur JWT token</span>
          <button id="get-jwt-token" class="setting-btn">Get Token</button>
        </div>
        <div class="setting-item">
          <span>Set account to public</span>
          <button id="set-account-public" class="setting-btn">Set Public</button>
        </div>
        <div class="setting-item">
          <span>Set account to private</span>
          <button id="set-account-private" class="setting-btn">Set Private</button>
        </div>
        <div class="setting-item">
          <span>Quick logout</span>
          <button id="quick-logout" class="setting-btn">Logout</button>
        </div>
        <div class="setting-item">
          <span>Reset setting</span>
          <button id="reset-setting" class="setting-btn">Reset</button>
        </div>
      </div>
      <div class="settings-group">
        <h3>Others</h3>
        <div class="setting-item">
          <span>Blank page (best performance)</span>
          <a href="https://www.duolingo.com/errors/0">Here</a>
        </div>
        <div class="setting-item">
          <span>Duolingo homepage</span>
          <a href="https://www.duolingo.com/">Here</a>
        </div>
        <div class="setting-item">
          <span>Greasyfork</span>
          <a href="https://greasyfork.org/vi/scripts/528621-duofarmer" target="_blank">Here</a>
        </div>
        <div class="setting-item">
          <span>Telegram</span>
          <a href="https://t.me/duofarmer" target="_blank">Here</a>
        </div>
        <div class="setting-item">
          <span>Duofarmer Homepage</span>
          <a href="https://duo-farmer.vercel.app" target="_blank">Here</a>
        </div>
      </div>

      <div class="settings-group">
        <h3>Donate me (please im unemployed 😭)</h3>
        <div class="setting-item">
          <span>Donate via PayPal</span>
          <a href="https://duo-farmer.vercel.app/donate/paypal" target="_blank">Here</a>
        </div>
        <div class="setting-item">
          <span>Donate via Momo ( Vietnam )</span>
          <a href="https://t.me/duofarmer" target="_blank">Direct message me</a>
        </div>
      </div>

      <div class="settings-group">
        <h3>Feature Guide</h3>
        <div class="setting-item">
          <code id="feature-guide">
            <p><b>- Enable Duolingo Max Patch:</b> Bypass subscription check to get Duolingo Max!, thanks <a href="https://github.com/apersongithub/Duolingo-Unlimited-Hearts" target="_blank">apersongithub</a>.</p>
            <p><b>- Auto start farming onload:</b> Start farming default selected option automatically when the page loads. <br></p>
            <p><b>- Repair streak:</b> Fills missing streak days from account creation date to now, it's also break all the frozen streak.<br></p>
            <p><b>- Blank page (best performance):</b> Duolingo's error page with minimal load. It will have 100% power for farming 😎.<br></p>
            <p><b>- Public/Private:</b> Toggle account visibility. Private = hidden from leaderboards. (Recommended to use private) <br></p>
          </code>
        </div>
      </div>

      <div class="settings-group">
        <h3>All userinfo (json)</h3>
        <div class="setting-item">
          <code id="user-info-display">
              Loading...
            </code>
        </div>
      </div>

    </div>
    <div class="modal-footer">
      <span></span>
      <button id="save-settings" class="save-btn">Save</button>
    </div>
  </div>
</div>
<div id="floating-btn">🐸</div>`;
  const cssText = "#action-row{width:90%;display:flex;justify-content:space-between;align-items:center;margin:8px 0;gap:8px}#blank-page-link{margin-bottom:8px;color:#fce6ff;font-weight:700;font-style:italic}#body{min-height:40vh;max-height:100%;min-width:0;background:#282828;display:flex;align-items:center;justify-content:center;width:100%;overflow-y:auto;flex:1;flex-direction:column}#body .label{font-size:1.2em}#body h3{margin:0;color:#fff;font-size:1.1em;font-weight:700;letter-spacing:1px}#container{width:90vw;max-width:800px;min-height:40vh;max-height:90vh;background:#222;color:#fff;border-radius:10px;box-shadow:0 2px 12px #0008;font-family:sans-serif;font-size:.9em;display:flex;flex-direction:column;align-items:center;justify-content:center;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;box-sizing:border-box}#floating-btn{position:fixed;bottom:10%;right:2%;width:40px;height:40px;background:#35bd00;border-radius:50%;box-shadow:0 2px 8px #0000004d;z-index:10000;cursor:pointer;display:flex;align-items:center;justify-content:center}#footer{height:30px;background:#222;display:flex;align-items:center;justify-content:space-evenly;border-bottom-left-radius:10px;border-bottom-right-radius:10px;width:100%}#footer a,#footer span{text-decoration:none;color:#00aeff;font-size:1em;font-weight:700;font-style:italic}#header{height:60px;background:#333;display:flex;align-items:center;justify-content:space-between;border-top-left-radius:10px;border-top-right-radius:10px;width:100%;position:relative}#header .label{font-size:1.4em;font-weight:600;color:#fff}#header-left{display:flex;align-items:center}#header-left img{width:32px;height:32px;margin-right:8px;vertical-align:middle}#notify{width:90%;max-width:90%;min-height:10vh;margin:8px 0;padding:8px 12px;border-radius:6px;background:#333;color:#c8ff00;font-size:1em;word-wrap:break-word}#overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#000c;z-index:9998;pointer-events:all}#select-option{width:90%;max-width:90%;margin-right:8px;padding:8px 12px;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;outline:none}#settings-btn{position:absolute;right:20px;background:none;border:none;color:#fff;font-size:20px;cursor:pointer;padding:5px;border-radius:3px}#settings-btn:hover{background:#555}#settings-container{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10000;display:flex;align-items:center;justify-content:center;background:#000c}#start-btn,#stop-btn{width:auto;margin-left:0;padding:8px 18px;border-radius:6px;border:none;background:#229100;color:#fff;font-size:1em;font-weight:700;cursor:pointer;box-shadow:0 2px 8px #0003}#stop-btn{background:#af0303}.modal-body{min-height:25vh;max-height:100%;min-width:0;background:#282828;display:flex;align-items:center;justify-content:flex-start;width:100%;overflow-y:auto;flex:1;flex-direction:column;padding:20px}.modal-close{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;padding:5px;border-radius:3px}.modal-close:hover{background:#555}.modal-content{width:90vw;max-width:800px;min-height:25vh;max-height:70vh;background:#222;color:#fff;border-radius:10px;box-shadow:0 2px 12px #0008;font-family:sans-serif;font-size:.9em;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;box-sizing:border-box}.modal-footer{height:60px;background:#333;display:flex;align-items:center;justify-content:flex-end;padding:5px 20px;border-top:1px solid #444;border-bottom-left-radius:10px;border-bottom-right-radius:10px;width:100%}.modal-header{height:60px;background:#333;display:flex;align-items:center;justify-content:space-between;padding:0 20px;border-top-left-radius:10px;border-top-right-radius:10px;width:100%}.modal-header .label{font-weight:700}.save-btn{padding:8px 10px;border-radius:6px;border:none;background:#229100;color:#fff;font-weight:bolder;cursor:pointer}.setting-item{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px;padding:10px;background:#333;border-radius:5px}.setting-item a{color:#4caf50;font-style:italic;text-decoration:none;margin-left:auto;font-size:.9em}.setting-item a:hover{color:#66bb6a;text-decoration:underline}.setting-item input:not([type=checkbox]){width:120px;padding:8px 12px;margin-left:auto;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;outline:none}.setting-item input[type=checkbox]{width:18px;height:18px;margin-left:auto;cursor:pointer;accent-color:#229100}.setting-item input[type=number]{width:120px;padding:8px 12px;margin-left:auto;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;text-align:center;outline:none}.setting-item input[type=number]:focus{border-color:#229100}.setting-item select{width:120px;padding:8px 12px;margin-left:auto;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;outline:none;cursor:pointer}.setting-item span{flex:1;margin-right:10px}.setting-item .setting-btn{padding:6px 12px;margin-left:auto;background:#555;border:1px solid #666;border-radius:4px;color:#fff;font-size:.9em;cursor:pointer}.settings-group{width:100%;margin-bottom:30px}.settings-group h3{margin:0 0 15px;color:#fff;font-size:16px;border-bottom:1px solid #444;padding-bottom:5px}.blur{filter:blur(4px)}code{background:#333;margin:10px 0;padding:8px 12px;border-left:#229100 3px solid;font-family:monospace;line-height:1.5em;display:block;border-radius:2px}.disable-btn{background:#52454560!important;cursor:not-allowed!important}.disabled{background:#26202060!important;color:#888!important;cursor:not-allowed!important;pointer-events:none!important}.hidden{display:none!important}.label{font-size:1em}.muted{color:#555!important;font-size:smaller!important}.table{width:100%;background:#232323;color:#fff;border-radius:8px;padding:8px 12px;text-align:center;table-layout:fixed}.table th,.table td{padding:9px 12px;text-align:center;border-bottom:1px solid #444;width:1%}.table tbody tr:last-child td{border-bottom:none}";
  const log = (message) => {
    if (typeof GM_log !== "undefined") {
      GM_log(message);
    } else {
      console.log("[DuoFarmer]", message);
    }
  };
  const logError = (error, context = "") => {
    const message = (error == null ? void 0 : error.message) || (error == null ? void 0 : error.toString()) || "Unknown error";
    const fullMessage = context ? `[${context}] ${message}` : message;
    log(fullMessage);
  };
  const delay = (ms) => {
    return new Promise((resolve) => setTimeout(resolve, ms));
  };
  const toTimestamp = (dateStr) => {
    return Math.floor(new Date(dateStr).getTime() / 1e3);
  };
  const getCurrentUnixTimestamp = () => {
    return Math.floor(Date.now() / 1e3);
  };
  const daysBetween = (startTimestamp, endTimestamp) => {
    return Math.floor((endTimestamp - startTimestamp) / (60 * 60 * 24));
  };
  const getJwtToken = () => {
    const cookies = document.cookie.split(";");
    for (let i = 0; i < cookies.length; i++) {
      const cookie = cookies[i].trim();
      if (cookie.startsWith("jwt_token=")) {
        return cookie.substring("jwt_token=".length);
      }
    }
    return null;
  };
  const decodeJwtToken = (token) => {
    const base64Url = token.split(".")[1];
    const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
    const jsonPayload = decodeURIComponent(
      atob(base64).split("").map(function(c) {
        return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
      }).join("")
    );
    return JSON.parse(jsonPayload);
  };
  const formatHeaders = (jwtToken) => {
    return {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${jwtToken}`,
      "User-Agent": navigator.userAgent,
      "Accept-Encoding": "gzip, deflate, br, zstd"
    };
  };
  const extractSkillId = (currentCourse) => {
    var _a, _b;
    const sections = (currentCourse == null ? void 0 : currentCourse.pathSectioned) || [];
    for (const section of sections) {
      const units = section.units || [];
      for (const unit of units) {
        const levels = unit.levels || [];
        for (const level of levels) {
          const skillId = ((_a = level.pathLevelMetadata) == null ? void 0 : _a.skillId) || ((_b = level.pathLevelClientData) == null ? void 0 : _b.skillId);
          if (skillId) return skillId;
        }
      }
    }
    return null;
  };
  const waitForBody = () => {
    return new Promise((resolve) => {
      if (document.body) {
        resolve();
      } else {
        const observer = new MutationObserver(() => {
          if (document.body) {
            observer.disconnect();
            resolve();
          }
        });
        observer.observe(document.documentElement, { childList: true });
      }
    });
  };
  class ApiService {
    constructor(jwt, defaultHeaders, userInfo, sub) {
      this.jwt = jwt;
      this.defaultHeaders = defaultHeaders;
      this.userInfo = userInfo;
      this.sub = sub;
    }
    static async getUserInfo(userSub, headers) {
      const userInfoUrl = `https://www.duolingo.com/2017-06-30/users/${userSub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,level,numFollowers,numFollowing,gems,creationDate,streakData,privacySettings,currentCourse{pathSectioned{units{levels{pathLevelMetadata{skillId}}}}}`;
      const response = await fetch(userInfoUrl, { method: "GET", headers });
      return await response.json();
    }
    async sendRequest({ url, payload, headers, method = "PUT" }) {
      try {
        const res = await fetch(url, {
          method,
          headers,
          body: payload ? JSON.stringify(payload) : void 0
        });
        return res;
      } catch (error) {
        return error;
      }
    }
    async setPrivacyStatus(privacyStatus) {
      const patchUrl = `https://www.duolingo.com/2017-06-30/users/${this.sub}/privacy-settings?fields=privacySettings`;
      const patchBody = {
        "DISABLE_SOCIAL": privacyStatus
      };
      return await this.sendRequest({ url: patchUrl, payload: patchBody, headers: this.defaultHeaders, method: "PATCH" });
    }
    async farmGemOnce() {
      const idReward = "SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
      const patchUrl = `https://www.duolingo.com/2017-06-30/users/${this.sub}/rewards/${idReward}`;
      const patchBody = {
        consumed: true,
        learningLanguage: this.userInfo.learningLanguage,
        fromLanguage: this.userInfo.fromLanguage
      };
      return await this.sendRequest({ url: patchUrl, payload: patchBody, headers: this.defaultHeaders, method: "PATCH" });
    }
    async farmSessionOnce(config = {}) {
      const startTime = config.startTime || getCurrentUnixTimestamp();
      const endTime = config.endTime || startTime + 60;
      const sessionPayload = {
        challengeTypes: [],
        fromLanguage: this.userInfo.fromLanguage,
        learningLanguage: this.userInfo.learningLanguage,
        // isFinalLevel: false,
        // isV2: true,
        // juicy: true,
        // smartTipsVersion: 2,
        type: "GLOBAL_PRACTICE",
        ...config.sessionPayload || {}
      };
      const sessionRes = await this.sendRequest({ url: "https://www.duolingo.com/2017-06-30/sessions", payload: sessionPayload, headers: this.defaultHeaders, method: "POST" });
      const sessionData = await sessionRes.json();
      const updateSessionPayload = {
        // ...sessionData,
        id: sessionData.id,
        metadata: sessionData.metadata,
        type: sessionData.type,
        fromLanguage: this.userInfo.fromLanguage,
        learningLanguage: this.userInfo.learningLanguage,
        challenges: [],
        // empty for fast response
        adaptiveChallenges: [],
        // empty for fast response
        sessionExperimentRecord: [],
        experiments_with_treatment_contexts: [],
        adaptiveInterleavedChallenges: [],
        adaptiveChallenges: [],
        sessionStartExperiments: [],
        trackingProperties: [],
        ttsAnnotations: [],
        heartsLeft: 0,
        startTime,
        enableBonusPoints: false,
        endTime,
        failed: false,
        maxInLessonStreak: 9,
        shouldLearnThings: true,
        ...config.updateSessionPayload || {}
      };
      const updateRes = await this.sendRequest({ url: `https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`, payload: updateSessionPayload, headers: this.defaultHeaders, method: "PUT" });
      return updateRes;
    }
  }
  class SettingsManager {
    constructor(shadowRoot, apiService = null) {
      this.shadowRoot = shadowRoot;
      this.apiService = apiService;
      this.DEFAULT_SETTINGS = {
        autoOpenUI: false,
        autoStart: false,
        defaultOption: 1,
        // index of option in OPTIONS array (0-based)
        hideUsername: false,
        keepScreenOn: false,
        delayTime: 500,
        retryTime: 1e3,
        autoStopTime: 0,
        enableMaxPatch: false
      };
      this.settings = this.loadSettings();
    }
    loadSettings() {
      try {
        const saved = localStorage.getItem("duofarmerSettings");
        if (saved) {
          return { ...this.DEFAULT_SETTINGS, ...JSON.parse(saved) };
        }
        return { ...this.DEFAULT_SETTINGS };
      } catch (error) {
        return { ...this.DEFAULT_SETTINGS };
      }
    }
    saveSettings(settings) {
      this.settings = settings;
      localStorage.setItem("duofarmerSettings", JSON.stringify(settings));
    }
    getSettings() {
      return { ...this.settings };
    }
    loadSettingsToUI() {
      const elements = this.getElements();
      const mappings = [
        { key: "autoOpenUI", element: elements.autoOpenUI, setter: (el, val) => el.checked = val },
        { key: "autoStart", element: elements.autoStart, setter: (el, val) => el.checked = val },
        { key: "defaultOption", element: elements.defaultOption, setter: (el, val) => el.value = val.toString() },
        { key: "hideUsername", element: elements.hideUsername, setter: (el, val) => el.checked = val },
        { key: "keepScreenOn", element: elements.keepScreenOn, setter: (el, val) => el.checked = val },
        { key: "delayTime", element: elements.delayTime, setter: (el, val) => el.value = val },
        { key: "retryTime", element: elements.retryTime, setter: (el, val) => el.value = val },
        { key: "autoStopTime", element: elements.autoStopTime, setter: (el, val) => el.value = val },
        { key: "enableMaxPatch", element: elements.enableMaxPatch, setter: (el, val) => el.checked = val }
      ];
      mappings.forEach(({ key, element, setter }) => {
        if (element && this.settings[key] !== void 0) {
          setter(element, this.settings[key]);
        }
      });
    }
    saveSettingsFromUI() {
      const elements = this.getElements();
      const getters = {
        autoOpenUI: () => {
          var _a;
          return ((_a = elements.autoOpenUI) == null ? void 0 : _a.checked) || false;
        },
        autoStart: () => {
          var _a;
          return ((_a = elements.autoStart) == null ? void 0 : _a.checked) || false;
        },
        defaultOption: () => {
          var _a;
          return parseInt((_a = elements.defaultOption) == null ? void 0 : _a.value) || 1;
        },
        hideUsername: () => {
          var _a;
          return ((_a = elements.hideUsername) == null ? void 0 : _a.checked) || false;
        },
        keepScreenOn: () => {
          var _a;
          return ((_a = elements.keepScreenOn) == null ? void 0 : _a.checked) || false;
        },
        delayTime: () => {
          var _a;
          return Math.max(100, Math.min(1e4, parseInt((_a = elements.delayTime) == null ? void 0 : _a.value) || 500));
        },
        retryTime: () => {
          var _a;
          return Math.max(100, Math.min(1e4, parseInt((_a = elements.retryTime) == null ? void 0 : _a.value) || 1e3));
        },
        autoStopTime: () => {
          var _a;
          return parseInt((_a = elements.autoStopTime) == null ? void 0 : _a.value) || 0;
        },
        enableMaxPatch: () => {
          var _a;
          return ((_a = elements.enableMaxPatch) == null ? void 0 : _a.checked) || false;
        }
      };
      const settings = Object.fromEntries(
        Object.entries(getters).map(([key, getter]) => [key, getter()])
      );
      this.saveSettings(settings);
      return settings;
    }
    getElements() {
      return {
        autoOpenUI: this.shadowRoot.getElementById("auto-open-ui"),
        autoStart: this.shadowRoot.getElementById("auto-start"),
        defaultOption: this.shadowRoot.getElementById("default-option"),
        hideUsername: this.shadowRoot.getElementById("hide-username"),
        keepScreenOn: this.shadowRoot.getElementById("keep-screen-on"),
        delayTime: this.shadowRoot.getElementById("delay-time"),
        retryTime: this.shadowRoot.getElementById("retry-time"),
        autoStopTime: this.shadowRoot.getElementById("auto-stop-time"),
        saveSettings: this.shadowRoot.getElementById("save-settings"),
        quickLogout: this.shadowRoot.getElementById("quick-logout"),
        getJwtToken: this.shadowRoot.getElementById("get-jwt-token"),
        resetSetting: this.shadowRoot.getElementById("reset-setting"),
        settingsContainer: this.shadowRoot.getElementById("settings-container"),
        setAccountPublic: this.shadowRoot.getElementById("set-account-public"),
        setAccountPrivate: this.shadowRoot.getElementById("set-account-private"),
        enableMaxPatch: this.shadowRoot.getElementById("enable-max-patch")
      };
    }
    addEventListeners() {
      const elements = this.getElements();
      elements.saveSettings.addEventListener("click", () => {
        this.saveSettingsFromUI();
        alert("Settings saved successfully, reload the page to apply changes!");
        confirm("Reload now?") && location.reload();
      });
      elements.quickLogout.addEventListener("click", () => {
        if (confirm("Are you sure you want to logout?")) {
          window.location.href = "https://www.duolingo.com/logout";
        }
      });
      elements.getJwtToken.addEventListener("click", () => {
        const token = getJwtToken();
        if (token) {
          confirm(`Your JWT Token:

${token}

Copy to clipboard?`) && navigator.clipboard.writeText(token);
        }
      });
      elements.resetSetting.addEventListener("click", () => {
        if (confirm("Reset all settings to default? This cannot be undone.")) {
          localStorage.removeItem("duofarmerSettings");
          this.settings = { ...this.DEFAULT_SETTINGS };
          this.loadSettingsToUI();
          alert("All settings reset successfully! Reload to apply changes.");
        }
      });
      elements.setAccountPublic.addEventListener("click", async () => {
        if (confirm("Are you sure you want to set your account to public?")) {
          try {
            await this.apiService.setPrivacyStatus(false);
            alert("Account set to public successfully! Reload the page to see changes.");
          } catch (error) {
            alert("Failed to set account to public: " + error.message);
          }
        }
      });
      elements.setAccountPrivate.addEventListener("click", async () => {
        if (confirm("Are you sure you want to set your account to private?")) {
          try {
            await this.apiService.setPrivacyStatus(true);
            alert("Account set to private successfully! Reload the page to see changes.");
          } catch (error) {
            alert("Failed to set account to private: " + error.message);
          }
        }
      });
    }
    addEventSettings(container) {
      const elements = this.getElements();
      const settingsBtn = this.shadowRoot.getElementById("settings-btn");
      const settingsContainer = elements.settingsContainer;
      const settingsClose = this.shadowRoot.getElementById("settings-close");
      const toggleModal2 = (modalElement, mainElement) => ({
        show: () => {
          mainElement.style.display = "none";
          modalElement.style.display = "flex";
        },
        hide: () => {
          modalElement.style.display = "none";
          mainElement.style.display = "flex";
        }
      });
      const settingsModal = toggleModal2(settingsContainer, container);
      settingsBtn.addEventListener("click", settingsModal.show);
      settingsClose.addEventListener("click", settingsModal.hide);
    }
    loadDefaultFarmingOption(optionsArray) {
      const select = this.shadowRoot.getElementById("select-option");
      const optionIndex = this.settings.defaultOption;
      select.selectedIndex = optionIndex;
    }
    populateDefaultOptionSelect(optionsArray) {
      const select = this.shadowRoot.getElementById("default-option");
      select.innerHTML = "";
      optionsArray.forEach((opt, index) => {
        const option = document.createElement("option");
        option.value = index.toString();
        option.textContent = opt.label;
        if (opt.disabled) option.disabled = true;
        select.appendChild(option);
      });
    }
  }
  const safeCall = (callback, ...args) => callback == null ? void 0 : callback(...args);
  const handleFarmingError = (error, context, callbacks) => {
    const message = (error == null ? void 0 : error.status) ? `Error ${error.status}! Please report in telegram group!` : `Error in ${context}: ${(error == null ? void 0 : error.message) || error}`;
    safeCall(callbacks.onError, message);
    return message;
  };
  const FARMING_STRATEGIES = {
    gem: (apiService, config, callbacks) => new GemFarming(apiService, config, callbacks),
    xp: (apiService, config, callbacks) => new XpFarming(apiService, config, callbacks),
    streak: (apiService, config, callbacks) => new StreakFarming(apiService, config, callbacks)
  };
  class GemFarming {
    constructor(apiService, config, callbacks) {
      this.apiService = apiService;
      this.config = config;
      this.callbacks = callbacks;
      this.gemFarmed = 30;
    }
    async start(userInfo) {
      while (this.config.isRunning) {
        try {
          await this.apiService.farmGemOnce(userInfo);
          safeCall(this.callbacks.onUpdate, "gem", this.gemFarmed);
          await this.callbacks.delay(this.config.delayTime);
        } catch (error) {
          handleFarmingError(error, "gemFarming", this.callbacks);
          await this.callbacks.delay(this.config.retryTime);
        }
      }
    }
  }
  class XpFarming {
    constructor(apiService, config, callbacks) {
      this.apiService = apiService;
      this.config = config;
      this.callbacks = callbacks;
    }
    async start(value, amount, config = {}, userInfo) {
      while (this.config.isRunning) {
        try {
          const response = await this.apiService.farmSessionOnce(config);
          if (response.status > 400) {
            safeCall(this.callbacks.onError, `Something went wrong! Please try again later.`);
            await this.callbacks.delay(this.config.retryTime);
            continue;
          }
          const responseData = await response.json();
          const xpFarmed = (responseData == null ? void 0 : responseData.awardedXp) || (responseData == null ? void 0 : responseData.xpGain) || 0;
          safeCall(this.callbacks.onUpdate, "xp", xpFarmed);
          await this.callbacks.delay(this.config.delayTime);
        } catch (error) {
          handleFarmingError(error, "xpFarming", this.callbacks);
          await this.callbacks.delay(this.config.retryTime);
        }
      }
    }
  }
  class StreakFarming {
    constructor(apiService, config, callbacks) {
      this.apiService = apiService;
      this.config = config;
      this.callbacks = callbacks;
      this.SECONDS_PER_DAY = 86400;
      this.SESSION_DURATION_SECONDS = 60;
    }
    async start(value = "farm", userInfo) {
      const method = value === "repair" ? this.repair.bind(this) : this.farm.bind(this);
      await method(userInfo);
    }
    async farm(userInfo) {
      const hasStreak = !!userInfo.streakData.currentStreak;
      const startStreakDate = hasStreak ? userInfo.streakData.currentStreak.startDate : /* @__PURE__ */ new Date();
      const startFarmStreakTimestamp = toTimestamp(startStreakDate);
      let currentTimestamp = hasStreak ? startFarmStreakTimestamp - this.SECONDS_PER_DAY : startFarmStreakTimestamp;
      while (this.config.isRunning) {
        try {
          const sessionRes = await this.apiService.farmSessionOnce({
            startTime: currentTimestamp,
            endTime: currentTimestamp + this.SESSION_DURATION_SECONDS
          });
          if (sessionRes) {
            currentTimestamp -= this.SECONDS_PER_DAY;
            safeCall(this.callbacks.onUpdate, "streak", 1);
            await this.callbacks.delay(this.config.delayTime);
          } else {
            safeCall(this.callbacks.onError, "Failed to farm streak session, I'm trying again...");
            await this.callbacks.delay(this.config.retryTime);
            continue;
          }
        } catch (error) {
          handleFarmingError(error, "farmStreak", this.callbacks);
          await this.callbacks.delay(this.config.retryTime);
          continue;
        }
      }
    }
    validateRepair(userInfo) {
      const creationDate = userInfo.creationDate;
      const currentStreak = userInfo.streak || 0;
      const currentTime = getCurrentUnixTimestamp();
      const daysSinceCreation = daysBetween(creationDate, currentTime);
      const maxPossibleStreak = daysSinceCreation + 1;
      const missingStreaks = maxPossibleStreak - currentStreak;
      if (currentStreak >= maxPossibleStreak) {
        return {
          valid: false,
          message: `Current streak (${currentStreak}) is greater than or equal to maximum possible streak (${maxPossibleStreak}). No repair needed.`
        };
      }
      if (missingStreaks <= 0) {
        return {
          valid: false,
          message: "No missing streaks to repair."
        };
      }
      return {
        valid: true,
        missingStreaks,
        endTimestamp: creationDate,
        maxPossibleStreak
      };
    }
    async repair(userInfo) {
      const validation = this.validateRepair(userInfo);
      if (!validation.valid) {
        safeCall(this.callbacks.onNotify, validation.message);
        safeCall(this.callbacks.onStop);
        return;
      }
      const { missingStreaks, maxPossibleStreak, endTimestamp } = validation;
      if (!confirm(`This feature will repair ${missingStreaks} missing streaks, so your streak will be ${maxPossibleStreak} days. Are you sure you want to continue?`)) {
        const message = `Streak repair cancelled.`;
        safeCall(this.callbacks.onNotify, message);
        safeCall(this.callbacks.onStop);
        return;
      }
      safeCall(this.callbacks.onNotify, `Repairing ${missingStreaks} missing streaks...`);
      const hasStreak = !!userInfo.streakData.currentStreak;
      const startStreakDate = hasStreak ? userInfo.streakData.currentStreak.startDate : /* @__PURE__ */ new Date();
      const startFarmStreakTimestamp = toTimestamp(startStreakDate);
      let repairTimestamp = hasStreak ? startFarmStreakTimestamp - this.SECONDS_PER_DAY : startFarmStreakTimestamp;
      let repairedCount = 0;
      while (this.config.isRunning && repairTimestamp >= endTimestamp && repairedCount < missingStreaks) {
        try {
          const sessionRes = await this.apiService.farmSessionOnce({
            startTime: repairTimestamp,
            endTime: repairTimestamp + this.SESSION_DURATION_SECONDS
          });
          if (sessionRes) {
            repairTimestamp -= this.SECONDS_PER_DAY;
            safeCall(this.callbacks.onUpdate, "streak", 1);
            repairedCount += 1;
            await this.callbacks.delay(this.config.delayTime);
          } else {
            safeCall(this.callbacks.onError, "Failed to repair streak session, I'm trying again...");
            await this.callbacks.delay(this.config.retryTime);
            continue;
          }
        } catch (error) {
          handleFarmingError(error, "repairStreak", this.callbacks);
          await this.callbacks.delay(this.config.retryTime);
          continue;
        }
      }
      if (repairedCount >= missingStreaks || repairTimestamp < endTimestamp) {
        const message = `Streak repair completed. Repaired ${repairedCount} day(s).`;
        safeCall(this.callbacks.onNotify, message);
        safeCall(this.callbacks.onStop);
      }
    }
  }
  class FarmingController {
    constructor(apiService, config, callbacks) {
      this.apiService = apiService;
      this.config = config;
      this.callbacks = callbacks;
      this.isRunning = false;
      this.autoStopTimerId = null;
      this.currentFarming = null;
    }
    getIsRunning() {
      return this.isRunning;
    }
    setIsRunning(running) {
      this.isRunning = running;
      if (!running && this.autoStopTimerId) {
        clearTimeout(this.autoStopTimerId);
        this.autoStopTimerId = null;
      }
    }
    startAutoStopTimer(autoStopTimeMinutes) {
      if (autoStopTimeMinutes > 0) {
        this.autoStopTimerId = setTimeout(() => {
          const message = `Auto-stopped by setting (stop after ${autoStopTimeMinutes} minutes).`;
          safeCall(this.callbacks.onNotify, message);
          safeCall(this.callbacks.onAlert, message);
          this.stop();
        }, autoStopTimeMinutes * 60 * 1e3);
      }
    }
    async start(option, userInfo) {
      if (this.isRunning) {
        return;
      }
      this.setIsRunning(true);
      this.startAutoStopTimer(this.config.autoStopTime);
      const { type, value, amount, config } = option;
      try {
        const strategy = FARMING_STRATEGIES[type];
        if (!strategy) {
          throw new Error(`Unknown farming type: ${type}`);
        }
        this.currentFarming = strategy(this.apiService, this.config, this.callbacks);
        if (type === "xp") {
          await this.currentFarming.start(value, amount, config, userInfo);
        } else if (type === "streak") {
          await this.currentFarming.start(value, userInfo);
        } else {
          await this.currentFarming.start(userInfo);
        }
      } catch (error) {
        handleFarmingError(error, "FarmingController.start", this.callbacks);
      } finally {
        this.setIsRunning(false);
      }
    }
    stop() {
      this.setIsRunning(false);
      this.currentFarming = null;
    }
  }
  class UserManager {
    constructor(callbacks) {
      this.userInfo = null;
      this.callbacks = callbacks;
    }
    setUserInfo(userInfo) {
      this.userInfo = userInfo;
      if (this.callbacks.onUserInfoUpdate) {
        this.callbacks.onUserInfoUpdate(this.userInfo);
      }
    }
    getUserInfo() {
      return this.userInfo;
    }
    updateFarmResult(type, farmedAmount) {
      if (!this.userInfo) {
        return;
      }
      switch (type) {
        case "gem":
          this.userInfo = { ...this.userInfo, gems: this.userInfo.gems + farmedAmount };
          if (this.callbacks.onNotify) {
            this.callbacks.onNotify(`You got ${farmedAmount} gem!!!`);
          }
          break;
        case "xp":
          this.userInfo = { ...this.userInfo, totalXp: this.userInfo.totalXp + farmedAmount };
          if (this.callbacks.onNotify) {
            this.callbacks.onNotify(`You got ${farmedAmount} XP!!!`);
          }
          break;
        case "streak":
          this.userInfo = { ...this.userInfo, streak: this.userInfo.streak + farmedAmount };
          if (this.callbacks.onNotify) {
            this.callbacks.onNotify(`You got ${farmedAmount} streak! (maybe some xp too, idk)`);
          }
          break;
      }
      if (this.callbacks.onUserInfoUpdate) {
        this.callbacks.onUserInfoUpdate(this.userInfo);
      }
    }
  }
  function generateFarmOptions(userInfo) {
    const skillId = extractSkillId(userInfo.currentCourse || {});
    return [
      { type: "separator", label: "⟡ GEM FARMING ⟡", value: "", disabled: true },
      { type: "gem", label: "Gem 30", value: "fixed", amount: 30 },
      { type: "separator", label: "⟡ XP FARMING ⟡", value: "", disabled: true },
      { type: "xp", label: "XP 10", value: "xp", amount: 10, config: {} },
      { type: "xp", label: "XP 20", value: "xp", amount: 20, config: { updateSessionPayload: { hasBoost: true } } },
      { type: "xp", label: "XP 40", value: "xp", amount: 40, config: { updateSessionPayload: { hasBoost: true, type: "TARGET_PRACTICE" } } },
      { type: "xp", label: "XP 50", value: "xp", amount: 50, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true, happyHourBonusXp: 10, type: "TARGET_PRACTICE" } } },
      { type: "xp", label: "XP 110", value: "xp", amount: 110, config: { sessionPayload: { type: "UNIT_TEST", skillIds: skillId ? [skillId] : [] }, updateSessionPayload: { type: "UNIT_TEST", hasBoost: true, happyHourBonusXp: 10, pathLevelSpecifics: { unitIndex: 0 } } }, disabled: !skillId },
      { type: "separator", label: "⟡ STREAK FARMING ⟡", value: "", disabled: true },
      { type: "streak", label: "Unlimited Streak", value: "farm" },
      { type: "streak", label: "Repair Streak", value: "repair" }
    ];
  }
  function getElements(shadowRoot) {
    return {
      startBtn: shadowRoot.getElementById("start-btn"),
      stopBtn: shadowRoot.getElementById("stop-btn"),
      select: shadowRoot.getElementById("select-option"),
      floatingBtn: shadowRoot.getElementById("floating-btn"),
      container: shadowRoot.getElementById("container"),
      overlay: shadowRoot.getElementById("overlay"),
      notify: shadowRoot.getElementById("notify"),
      username: shadowRoot.getElementById("username"),
      from: shadowRoot.getElementById("from"),
      learn: shadowRoot.getElementById("learn"),
      streak: shadowRoot.getElementById("streak"),
      gem: shadowRoot.getElementById("gem"),
      xp: shadowRoot.getElementById("xp"),
      settingsBtn: shadowRoot.getElementById("settings-btn"),
      settingsContainer: shadowRoot.getElementById("settings-container"),
      settingsClose: shadowRoot.getElementById("settings-close"),
      userInfoDisplay: shadowRoot.getElementById("user-info-display"),
      setAccountPublic: shadowRoot.getElementById("set-account-public"),
      setAccountPrivate: shadowRoot.getElementById("set-account-private")
    };
  }
  function showElement(element) {
    if (element) element.style.display = "flex";
  }
  function hideElement(element) {
    if (element) element.style.display = "none";
  }
  function toggleModal(modalElement, mainElement) {
    return {
      show: () => {
        hideElement(mainElement);
        showElement(modalElement);
      },
      hide: () => {
        hideElement(modalElement);
        showElement(mainElement);
      }
    };
  }
  class UIController {
    constructor(templateRaw2, cssText2) {
      this.templateRaw = templateRaw2;
      this.cssText = cssText2;
      this.shadowRoot = null;
      this.container = null;
    }
    init() {
      this.container = document.createElement("div");
      this.shadowRoot = this.container.attachShadow({ mode: "open" });
      const style = document.createElement("style");
      style.textContent = this.cssText;
      this.shadowRoot.appendChild(style);
      const content = document.createElement("div");
      content.innerHTML = this.templateRaw;
      this.shadowRoot.appendChild(content);
      document.body.appendChild(this.container);
      const settingsContainer = this.shadowRoot.getElementById("settings-container");
      if (settingsContainer) {
        settingsContainer.style.display = "none";
      }
      const requiredElements = [
        "start-btn",
        "stop-btn",
        "select-option",
        "floating-btn",
        "container",
        "overlay",
        "notify"
      ];
      for (const id of requiredElements) {
        if (!this.shadowRoot.getElementById(id)) {
          throw new Error(`Required UI element '${id}' not found in template. Template may be corrupted.`);
        }
      }
      return this.shadowRoot;
    }
    getShadowRoot() {
      return this.shadowRoot;
    }
    setVisible(visible) {
      const elements = getElements(this.shadowRoot);
      if (visible) {
        showElement(elements.container);
        showElement(elements.overlay);
      } else {
        hideElement(elements.container);
        hideElement(elements.overlay);
      }
    }
    isVisible() {
      const elements = getElements(this.shadowRoot);
      return elements.container.style.display !== "none" && elements.container.style.display !== "";
    }
    toggle() {
      this.setVisible(!this.isVisible());
    }
  }
  class UIState {
    constructor(shadowRoot) {
      this.shadowRoot = shadowRoot;
      this.isRunning = false;
      this.autoStopTimerId = null;
    }
    setRunning(running) {
      this.isRunning = running;
      const elements = getElements(this.shadowRoot);
      if (running) {
        elements.startBtn.hidden = true;
        elements.stopBtn.hidden = false;
        elements.stopBtn.disabled = true;
        elements.stopBtn.className = "disable-btn";
        elements.select.disabled = true;
      } else {
        elements.stopBtn.hidden = true;
        elements.startBtn.hidden = false;
        elements.startBtn.disabled = true;
        elements.startBtn.className = "disable-btn";
        elements.select.disabled = false;
        if (this.autoStopTimerId) {
          clearTimeout(this.autoStopTimerId);
          this.autoStopTimerId = null;
        }
      }
      setTimeout(() => {
        const elements2 = getElements(this.shadowRoot);
        elements2.startBtn.className = "";
        elements2.startBtn.disabled = false;
        elements2.stopBtn.className = "";
        elements2.stopBtn.disabled = false;
      }, 3e3);
    }
    getIsRunning() {
      return this.isRunning;
    }
    disableAllControls() {
      const elements = getElements(this.shadowRoot);
      elements.startBtn.disabled = true;
      elements.startBtn.className = "disable-btn";
      elements.stopBtn.disabled = true;
      elements.select.disabled = true;
    }
  }
  const updatePrivacyButtons = (elements, isPrivate) => {
    elements.setAccountPublic.style.display = isPrivate ? "none" : "flex";
    elements.setAccountPrivate.style.display = isPrivate ? "flex" : "none";
  };
  const extractOptionData = (selected) => ({
    type: selected.getAttribute("data-type"),
    amount: Number(selected.getAttribute("data-amount")),
    value: selected.value,
    label: selected.textContent,
    config: selected.getAttribute("data-config") ? JSON.parse(selected.getAttribute("data-config")) : {}
  });
  class UIHandlers {
    constructor(shadowRoot, farmingController, userManager, settingsManager, uiController, uiState) {
      this.shadowRoot = shadowRoot;
      this.farmingController = farmingController;
      this.userManager = userManager;
      this.settingsManager = settingsManager;
      this.uiController = uiController;
      this.uiState = uiState;
    }
    setupEventListeners() {
      this.addEventStartBtn();
      this.addEventStopBtn();
      this.addEventFloatingBtn();
      this.addEventSettings();
      this.settingsManager.addEventListeners();
    }
    addEventStartBtn() {
      const elements = getElements(this.shadowRoot);
      elements.startBtn.addEventListener("click", async () => {
        this.uiState.setRunning(true);
        const selected = elements.select.options[elements.select.selectedIndex];
        const optionData = extractOptionData(selected);
        const userInfo = this.userManager.getUserInfo();
        this.farmingController.start(optionData, userInfo).catch((error) => {
          this.updateNotify(`Farming error: ${(error == null ? void 0 : error.message) || error}`);
          this.uiState.setRunning(false);
        });
      });
    }
    addEventStopBtn() {
      const elements = getElements(this.shadowRoot);
      elements.stopBtn.addEventListener("click", () => {
        this.farmingController.stop();
        this.uiState.setRunning(false);
      });
    }
    addEventFloatingBtn() {
      const elements = getElements(this.shadowRoot);
      elements.floatingBtn.addEventListener("click", () => {
        if (this.uiState.getIsRunning()) {
          if (confirm("Duofarmer is farming. Do you want to stop and hide UI?")) {
            this.farmingController.stop();
            this.uiState.setRunning(false);
            this.uiController.setVisible(false);
          }
          return;
        }
        this.uiController.toggle();
      });
    }
    addEventSettings() {
      const elements = getElements(this.shadowRoot);
      const settingsModal = toggleModal(elements.settingsContainer, elements.container);
      elements.settingsBtn.addEventListener("click", settingsModal.show);
      elements.settingsClose.addEventListener("click", settingsModal.hide);
    }
    updateNotify(message) {
      const elements = getElements(this.shadowRoot);
      const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
      elements.notify.innerText = `[${now}] ` + message;
      log(`[${now}] ${message}`);
    }
    updateUserInfo(userInfo, skillId, sub) {
      var _a;
      if (!userInfo) return;
      const elements = getElements(this.shadowRoot);
      elements.username.innerText = userInfo.username;
      elements.from.innerText = userInfo.fromLanguage;
      elements.learn.innerText = userInfo.learningLanguage;
      elements.streak.innerText = userInfo.streak;
      elements.gem.innerText = userInfo.gems;
      elements.xp.innerText = userInfo.totalXp;
      const isPrivate = (_a = userInfo.privacySettings) == null ? void 0 : _a.some(
        (setting) => ["DISABLE_FRIENDS_QUESTS", "DISABLE_LEADERBOARDS"].includes(setting)
      );
      updatePrivacyButtons(elements, isPrivate);
      elements.userInfoDisplay.innerText = JSON.stringify({
        id: userInfo.id,
        username: userInfo.username,
        fromLanguage: userInfo.fromLanguage,
        learningLanguage: userInfo.learningLanguage,
        streak: userInfo.streak,
        gems: userInfo.gems,
        totalXp: userInfo.totalXp,
        creationDate: userInfo.creationDate,
        skillId,
        jwt: "hidden - use get jwt button to view",
        sub,
        privacySettings: userInfo.privacySettings,
        streakData: userInfo.streakData
      }, null, 2);
    }
    populateOptions(farmOptions) {
      const select = this.shadowRoot.getElementById("select-option");
      select.innerHTML = "";
      farmOptions.forEach((opt) => {
        const option = document.createElement("option");
        option.value = opt.value;
        option.textContent = opt.label;
        option.setAttribute("data-type", opt.type);
        if (opt.amount != null) option.setAttribute("data-amount", String(opt.amount));
        if (opt.config) option.setAttribute("data-config", JSON.stringify(opt.config));
        if (opt.disabled) option.disabled = true;
        select.appendChild(option);
      });
    }
    loadSavedSettings(settings) {
      const elements = getElements(this.shadowRoot);
      if (settings.autoOpenUI) {
        this.uiController.setVisible(true);
      }
      if (settings.autoStart) {
        this.uiController.setVisible(true);
        elements.startBtn.click();
      }
      if (settings.hideUsername) {
        elements.username.classList.add("blur");
      }
      if (settings.keepScreenOn && "wakeLock" in navigator) {
        navigator.wakeLock.request("screen").then((wakeLock) => {
          log("Screen wake lock active");
        });
      }
    }
  }
  initPatcher();
  function setupCallbacks(userManager, farmingController, uiHandlers, skillId, sub) {
    userManager.callbacks.onUserInfoUpdate = (userInfo) => {
      uiHandlers.updateUserInfo(userInfo, skillId, sub);
    };
    userManager.callbacks.onNotify = (message) => {
      uiHandlers.updateNotify(message);
    };
    farmingController.callbacks.onError = (message) => {
      uiHandlers.updateNotify(message);
    };
    farmingController.callbacks.onNotify = (message) => {
      uiHandlers.updateNotify(message);
    };
  }
  (async () => {
    await waitForBody();
    try {
      const uiController = new UIController(templateRaw, cssText);
      const shadowRoot = uiController.init();
      uiController.setVisible(false);
      const uiState = new UIState(shadowRoot);
      const userManager = new UserManager({
        onUserInfoUpdate: (userInfo2) => {
        }
      });
      const jwt = getJwtToken();
      if (!jwt) {
        uiState.disableAllControls();
        log("Please login to Duolingo and reload!");
        return;
      }
      const defaultHeaders = formatHeaders(jwt);
      const decodedJwt = decodeJwtToken(jwt);
      const sub = decodedJwt.sub;
      const userInfo = await ApiService.getUserInfo(sub, defaultHeaders);
      if (!userInfo || !userInfo.id) {
        uiState.disableAllControls();
        log("Failed to get user info. Please reload!");
        return;
      }
      userInfo.sub = sub;
      userManager.setUserInfo(userInfo);
      const apiService = new ApiService(jwt, defaultHeaders, userInfo, sub);
      const settingsManager = new SettingsManager(shadowRoot, apiService);
      const savedSettings = settingsManager.getSettings();
      const skillId = extractSkillId(userInfo.currentCourse || {});
      const farmOptions = generateFarmOptions(userInfo);
      let farmingController;
      const farmingConfig = {
        get isRunning() {
          return farmingController ? farmingController.getIsRunning() : false;
        },
        get delayTime() {
          return savedSettings.delayTime;
        },
        get retryTime() {
          return savedSettings.retryTime;
        },
        get autoStopTime() {
          return savedSettings.autoStopTime;
        }
      };
      farmingController = new FarmingController(
        apiService,
        farmingConfig,
        {
          delay,
          onUpdate: (type, amount) => userManager.updateFarmResult(type, amount),
          onError: () => {
          },
          // Will be set up after handlers are created
          onNotify: () => {
          },
          // Will be set up after handlers are created
          onStop: () => uiState.setRunning(false),
          onAlert: (message) => alert(message)
        }
      );
      const uiHandlers = new UIHandlers(
        shadowRoot,
        farmingController,
        userManager,
        settingsManager,
        uiController,
        uiState
      );
      setupCallbacks(userManager, farmingController, uiHandlers, skillId, sub);
      uiHandlers.populateOptions(farmOptions);
      settingsManager.populateDefaultOptionSelect(farmOptions);
      settingsManager.loadDefaultFarmingOption(farmOptions);
      settingsManager.loadSettingsToUI();
      uiHandlers.updateUserInfo(userInfo, skillId, sub);
      uiHandlers.setupEventListeners();
      uiHandlers.loadSavedSettings(savedSettings);
      uiHandlers.updateNotify('Duofarmer ready! For safety, I suggest that you use 2nd accounts.\nRecommended to use "Blank page" for best performance (check in setting)');
    } catch (err) {
      logError(err, "Duofarmer init error!");
    }
  })();

})();