Duolingo DuoFarmer

DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.

Asenna tämä skripti?
Author's suggested script

Saatat myös pitää

Asenna tämä skripti
// ==UserScript==
// @name         Duolingo DuoFarmer
// @namespace    https://duo-farmer.vercel.app
// @version      1.3.7
// @author       Lamduck
// @description  DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.
// @license      none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// @match        https://*.duolingo.com/*
// @grant        GM_log
// ==/UserScript==

(function () {
  'use strict';

  const templateRaw = `<div id="overlay"></div>
<div id="container">
  <div id="header">
    <span class="label">Duofarmer</span>
    <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">u gay 💔</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>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 disabled">
        <h3>Interface (coming soon)</h3>
        <div class="setting-item">
          <span>Dark mode</span>
          <input type="checkbox" id="dark-mode">
        </div>
        <div class="setting-item">
          <span>Compact UI</span>
          <input type="checkbox" id="compact-ui">
        </div>
        <div class="setting-item">
          <span>Show progress bar</span>
          <input type="checkbox" id="show-progress">
        </div>
        <div class="setting-item">
          <span>Font size</span>
          <select id="font-size">
            <option value="small">Small</option>
            <option value="medium">Medium</option>
            <option value="large">Large</option>
          </select>
        </div>
        <div class="setting-item">
          <span>Reset theme</span>
          <button id="reset-theme" class="setting-btn">Reset</button>
        </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 class="setting-item">
          <span>All user info:
            <br>
            <code id="user-info-display">
              Loading...
            </code>
          </span>
        </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 = "#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}#header{height:60px;background:#333;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border-top-right-radius:10px;width:100%;position:relative}#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}#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}#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%}.label{font-size:1em}#header .label{font-size:1.5em;font-style:italic;font-weight:700;color:#fac8ff}#body .label{font-size:1.2em}.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}#body h3{margin:0;color:#fff;font-size:1.1em;font-weight:700;letter-spacing:1px}#action-row{width:90%;display:flex;justify-content:space-between;align-items:center;margin:8px 0;gap:8px}#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}#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}.disable-btn{background:#52454560!important;cursor:not-allowed!important}#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}#blank-page-link{margin-bottom:8px;color:#fce6ff;font-weight:700;font-style:italic}#footer a,#footer span{text-decoration:none;color:#00aeff;font-size:1em;font-weight:700;font-style:italic}#overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#000c;z-index:9998;pointer-events:all}#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}#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}.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-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}.modal-close{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;padding:5px;border-radius:3px}.modal-close:hover{background:#555}.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-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%}.save-btn{padding:8px 10px;border-radius:6px;border:none;background:#229100;color:#fff;font-weight:bolder;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}.setting-item{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px;padding:10px;background:#333;border-radius:5px}.setting-item span{flex:1;margin-right:10px}.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 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 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 .setting-btn{padding:6px 12px;margin-left:auto;background:#555;border:1px solid #666;border-radius:4px;color:#fff;font-size:.9em;cursor:pointer}.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}.blur{filter:blur(4px)}.disabled{background:#26202060!important;color:#888!important;cursor:not-allowed!important;pointer-events:none!important}.hidden{display:none!important}.muted{color:#555!important;font-size:smaller!important}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}";
  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 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
    };
  };
  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 skillId2 = ((_a = level.pathLevelMetadata) == null ? void 0 : _a.skillId) || ((_b = level.pathLevelClientData) == null ? void 0 : _b.skillId);
          if (skillId2) return skillId2;
        }
      }
    }
    return null;
  };
  class ApiService {
    constructor(jwt2, defaultHeaders2, userInfo2, sub2) {
      this.jwt = jwt2;
      this.defaultHeaders = defaultHeaders2;
      this.userInfo = userInfo2;
      this.sub = sub2;
    }
    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 farmStoryOnce(config = {}) {
      const startTime = getCurrentUnixTimestamp();
      const fromLanguage = this.userInfo.fromLanguage;
      const completeUrl = `https://stories.duolingo.com/api2/stories/en-${fromLanguage}-the-passport/complete`;
      const storyPayload = {
        awardXp: true,
        isFeaturedStoryInPracticeHub: false,
        completedBonusChallenge: true,
        mode: "READ",
        isV2Redo: false,
        isV2Story: false,
        isLegendaryMode: true,
        masterVersion: false,
        maxScore: 0,
        numHintsUsed: 0,
        score: 0,
        startTime,
        fromLanguage,
        learningLanguage: this.userInfo.learningLanguage,
        hasXpBoost: false,
        // happyHourBonusXp: 449,
        ...config.storyPayload || {}
      };
      return await this.sendRequest({ url: completeUrl, payload: storyPayload, headers: this.defaultHeaders, method: "POST" });
    }
    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(shadowRoot2, apiService2 = null) {
      this.shadowRoot = shadowRoot2;
      this.apiService = apiService2;
      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,
        darkMode: false,
        compactUI: false,
        showProgress: false,
        fontSize: "medium"
      };
      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();
      if (elements.autoOpenUI) elements.autoOpenUI.checked = this.settings.autoOpenUI;
      if (elements.autoStart) elements.autoStart.checked = this.settings.autoStart;
      if (elements.defaultOption) elements.defaultOption.value = this.settings.defaultOption.toString();
      if (elements.hideUsername) elements.hideUsername.checked = this.settings.hideUsername;
      if (elements.keepScreenOn) elements.keepScreenOn.checked = this.settings.keepScreenOn;
      if (elements.delayTime) elements.delayTime.value = this.settings.delayTime;
      if (elements.retryTime) elements.retryTime.value = this.settings.retryTime;
      if (elements.autoStopTime) elements.autoStopTime.value = this.settings.autoStopTime;
      if (elements.darkMode) elements.darkMode.checked = this.settings.darkMode;
      if (elements.compactUI) elements.compactUI.checked = this.settings.compactUI;
      if (elements.showProgress) elements.showProgress.checked = this.settings.showProgress;
      if (elements.fontSize) elements.fontSize.value = this.settings.fontSize;
    }
    saveSettingsFromUI() {
      var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
      const elements = this.getElements();
      const settings = {
        autoOpenUI: ((_a = elements.autoOpenUI) == null ? void 0 : _a.checked) || false,
        autoStart: ((_b = elements.autoStart) == null ? void 0 : _b.checked) || false,
        defaultOption: parseInt((_c = elements.defaultOption) == null ? void 0 : _c.value) || 1,
        // index in OPTIONS array
        hideUsername: ((_d = elements.hideUsername) == null ? void 0 : _d.checked) || false,
        keepScreenOn: ((_e = elements.keepScreenOn) == null ? void 0 : _e.checked) || false,
        delayTime: Math.max(100, Math.min(1e4, parseInt((_f = elements.delayTime) == null ? void 0 : _f.value) || 500)),
        retryTime: Math.max(100, Math.min(1e4, parseInt((_g = elements.retryTime) == null ? void 0 : _g.value) || 1e3)),
        autoStopTime: parseInt((_h = elements.autoStopTime) == null ? void 0 : _h.value) || 0,
        darkMode: ((_i = elements.darkMode) == null ? void 0 : _i.checked) || false,
        compactUI: ((_j = elements.compactUI) == null ? void 0 : _j.checked) || false,
        showProgress: ((_k = elements.showProgress) == null ? void 0 : _k.checked) || false,
        fontSize: ((_l = elements.fontSize) == null ? void 0 : _l.value) || "medium"
      };
      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"),
        darkMode: this.shadowRoot.getElementById("dark-mode"),
        compactUI: this.shadowRoot.getElementById("compact-ui"),
        showProgress: this.shadowRoot.getElementById("show-progress"),
        fontSize: this.shadowRoot.getElementById("font-size"),
        saveSettings: this.shadowRoot.getElementById("save-settings"),
        quickLogout: this.shadowRoot.getElementById("quick-logout"),
        resetTheme: this.shadowRoot.getElementById("reset-theme"),
        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")
      };
    }
    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.resetTheme.addEventListener("click", () => {
      });
      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 toggleModal = (modalElement, mainElement) => ({
        show: () => {
          mainElement.style.display = "none";
          modalElement.style.display = "flex";
        },
        hide: () => {
          modalElement.style.display = "none";
          mainElement.style.display = "flex";
        }
      });
      const settingsModal = toggleModal(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);
      });
    }
  }
  let runtimeSettings = {
    delayTime: 500,
    retryTime: 1e3,
    autoStopTime: 0
  };
  let jwt = null;
  let defaultHeaders = null;
  let userInfo = null;
  let sub = null;
  let skillId = null;
  let isRunning = false;
  let shadowRoot = null;
  let apiService = null;
  let settingsManager = null;
  let farmOptions = [];
  let autoStopTimerId = null;
  const getElements = () => {
    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")
    };
  };
  const setRunningState = (running) => {
    isRunning = running;
    const { startBtn, stopBtn, select } = getElements();
    if (running) {
      startBtn.hidden = true;
      stopBtn.hidden = false;
      stopBtn.disabled = true;
      stopBtn.className = "disable-btn";
      select.disabled = true;
    } else {
      stopBtn.hidden = true;
      startBtn.hidden = false;
      startBtn.disabled = true;
      startBtn.className = "disable-btn";
      select.disabled = false;
      if (autoStopTimerId) {
        clearTimeout(autoStopTimerId);
        autoStopTimerId = null;
      }
    }
    setTimeout(() => {
      const { startBtn: btn, stopBtn: stop } = getElements();
      btn.className = "";
      btn.disabled = false;
      stop.className = "";
      stop.disabled = false;
    }, 3e3);
  };
  const disableAllControls = (notifyMessage = null) => {
    const { startBtn, stopBtn, select } = getElements();
    startBtn.disabled = true;
    startBtn.className = "disable-btn";
    stopBtn.disabled = true;
    select.disabled = true;
    if (notifyMessage) {
      updateNotify(notifyMessage);
    }
  };
  const initInterface = () => {
    const container = document.createElement("div");
    shadowRoot = container.attachShadow({ mode: "open" });
    const style = document.createElement("style");
    style.textContent = cssText;
    shadowRoot.appendChild(style);
    const content = document.createElement("div");
    content.innerHTML = templateRaw;
    shadowRoot.appendChild(content);
    document.body.appendChild(container);
    const settingsContainer = 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 (!shadowRoot.getElementById(id)) {
        throw new Error(`Required UI element '${id}' not found in template. Template may be corrupted.`);
      }
    }
  };
  const showElement = (element) => {
    if (element) element.style.display = "flex";
  };
  const hideElement = (element) => {
    if (element) element.style.display = "none";
  };
  const setInterfaceVisible = (visible) => {
    const { container, overlay } = getElements();
    if (visible) {
      showElement(container);
      showElement(overlay);
    } else {
      hideElement(container);
      hideElement(overlay);
    }
  };
  const addEventFloatingBtn = () => {
    const { floatingBtn } = getElements();
    floatingBtn.addEventListener("click", () => {
      if (isRunning) {
        if (confirm("Duofarmer is farming. Do you want to stop and hide UI?")) {
          setRunningState(false);
          setInterfaceVisible(false);
        }
        return;
      }
      toggleInterface();
    });
  };
  const addEventStartBtn = () => {
    const { startBtn, select } = getElements();
    startBtn.addEventListener("click", async () => {
      setRunningState(true);
      if (runtimeSettings.autoStopTime > 0) {
        autoStopTimerId = setTimeout(() => {
          alert(`Auto-stopped by setting (stop after ${runtimeSettings.autoStopTime} minutes).`);
          updateNotify(`Auto-stopped by setting (stop after ${runtimeSettings.autoStopTime} minutes).`);
          setRunningState(false);
        }, runtimeSettings.autoStopTime * 60 * 1e3);
      }
      const selected = select.options[select.selectedIndex];
      const optionData = {
        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")) : {}
      };
      await farmSelectedOption(optionData);
    });
  };
  const addEventStopBtn = () => {
    const { stopBtn } = getElements();
    stopBtn.addEventListener("click", () => {
      setRunningState(false);
    });
  };
  const isInterfaceVisible = () => {
    const { container } = getElements();
    return container.style.display !== "none" && container.style.display !== "";
  };
  const toggleInterface = () => {
    setInterfaceVisible(!isInterfaceVisible());
  };
  const addEventListeners = () => {
    addEventStartBtn();
    addEventStopBtn();
    const { container } = getElements();
    settingsManager.addEventSettings(container);
    settingsManager.addEventListeners();
  };
  const populateOptions = () => {
    const select = 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);
    });
  };
  const updateNotify = (message) => {
    const { notify } = getElements();
    const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
    notify.innerText = `[${now}] ` + message;
    log(`[${now}] ${message}`);
  };
  const updateUserInfo = () => {
    const elements = getElements();
    if (userInfo) {
      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;
      hideElement(userInfo.privacySettings && (userInfo.privacySettings.includes("DISABLE_FRIENDS_QUESTS") || userInfo.privacySettings.includes("DISABLE_LEADERBOARDS")) ? elements.setAccountPrivate : elements.setAccountPublic);
      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);
    }
  };
  const updateFarmResult = (type, farmedAmount) => {
    switch (type) {
      case "gem":
        userInfo = { ...userInfo, gems: userInfo.gems + farmedAmount };
        updateNotify(`You got ${farmedAmount} gem!!!`);
        break;
      case "xp":
        userInfo = { ...userInfo, totalXp: userInfo.totalXp + farmedAmount };
        updateNotify(`You got ${farmedAmount} XP!!!`);
        break;
      case "streak":
        userInfo = { ...userInfo, streak: userInfo.streak + farmedAmount };
        updateNotify(`You got ${farmedAmount} streak! (maybe some xp too, idk)`);
        break;
    }
    updateUserInfo();
  };
  const gemFarmingLoop = async () => {
    const gemFarmed = 30;
    while (isRunning) {
      try {
        await apiService.farmGemOnce(userInfo);
        updateFarmResult("gem", gemFarmed);
        await delay(runtimeSettings.delayTime);
      } catch (error) {
        updateNotify(`Error ${error.status}! Please report in telegram group!`);
        await delay(runtimeSettings.retryTime);
      }
    }
  };
  const xpFarmingLoop = async (value, amount, config = {}) => {
    while (isRunning) {
      try {
        let response;
        if (value === "session") {
          response = await apiService.farmSessionOnce(config);
        } else if (value === "story") {
          response = await apiService.farmStoryOnce(config);
        }
        if (response.status > 400) {
          updateNotify(`Something went wrong! Pls try other farming methods.
If you are using story method, u should try with English course!`);
          await delay(runtimeSettings.retryTime);
          continue;
        }
        const responseData = await response.json();
        const xpFarmed = (responseData == null ? void 0 : responseData.awardedXp) || (responseData == null ? void 0 : responseData.xpGain) || 0;
        updateFarmResult("xp", xpFarmed);
        await delay(runtimeSettings.delayTime);
      } catch (error) {
        updateNotify(`Error ${error.status}! Please report in telegram group!`);
        await delay(runtimeSettings.retryTime);
      }
    }
  };
  const streakFarmingLoop = async () => {
    const hasStreak = !!userInfo.streakData.currentStreak;
    const startStreakDate = hasStreak ? userInfo.streakData.currentStreak.startDate : /* @__PURE__ */ new Date();
    const startFarmStreakTimestamp = toTimestamp(startStreakDate);
    let currentTimestamp = hasStreak ? startFarmStreakTimestamp - 86400 : startFarmStreakTimestamp;
    while (isRunning) {
      try {
        const sessionRes = await apiService.farmSessionOnce({ startTime: currentTimestamp, endTime: currentTimestamp + 60 });
        if (sessionRes) {
          currentTimestamp -= 86400;
          updateFarmResult("streak", 1);
          await delay(runtimeSettings.delayTime);
        } else {
          updateNotify("Failed to farm streak session, I'm trying again...");
          await delay(runtimeSettings.retryTime);
          continue;
        }
      } catch (error) {
        updateNotify(`Error in farmStreak: ${(error == null ? void 0 : error.message) || error}`);
        await delay(runtimeSettings.retryTime);
        continue;
      }
    }
  };
  const farmSelectedOption = async (option) => {
    const { type, value, amount, config } = option;
    switch (type) {
      case "gem":
        gemFarmingLoop();
        break;
      case "xp":
        xpFarmingLoop(value, amount, config);
        break;
      case "streak":
        streakFarmingLoop();
        break;
    }
  };
  const loadSavedSettings = (settings) => {
    runtimeSettings = { ...runtimeSettings, ...settings };
    const elements = getElements();
    if (settings.autoOpenUI) {
      setInterfaceVisible(true);
    }
    if (settings.autoStart) {
      setInterfaceVisible(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");
      });
    }
    if (settings.darkMode) ;
    if (settings.compactUI) ;
    if (settings.showProgress) ;
    if (settings.fontSize) ;
  };
  const initVariables = async () => {
    jwt = getJwtToken();
    if (!jwt) {
      disableAllControls("Please login to Duolingo and reload!");
      return;
    }
    defaultHeaders = formatHeaders(jwt);
    const decodedJwt = decodeJwtToken(jwt);
    sub = decodedJwt.sub;
    userInfo = await ApiService.getUserInfo(sub, defaultHeaders);
    apiService = new ApiService(jwt, defaultHeaders, userInfo, sub);
    settingsManager = new SettingsManager(shadowRoot, apiService);
    skillId = extractSkillId(userInfo.currentCourse || {});
    farmOptions = [
      { type: "separator", label: "⟡ GEM FARMING ⟡", value: "", disabled: true },
      { type: "gem", label: "Gem 30", value: "fixed", amount: 30 },
      { type: "separator", label: "⟡ XP SESSION FARMING ⟡", value: "", disabled: true },
      { type: "separator", label: "(slow, safe, any language)", value: "", disabled: true },
      { type: "xp", label: "XP 10", value: "session", amount: 10, config: {} },
      // { type: 'xp', label: 'XP 13', value: 'session', amount: 13, config: { updateSessionPayload: { enableBonusPoints: true } } },
      { type: "xp", label: "XP 20", value: "session", amount: 20, config: { updateSessionPayload: { hasBoost: true } } },
      // { type: 'xp', label: 'XP 26', value: 'session', amount: 26, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true } } },
      // { type: 'xp', label: 'XP 36', value: 'session', amount: 36, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true, happyHourBonusXp: 10 } } },
      { type: "xp", label: "XP 40", value: "session", amount: 40, config: { updateSessionPayload: { hasBoost: true, type: "TARGET_PRACTICE" } } },
      { type: "xp", label: "XP 50", value: "session", amount: 50, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true, happyHourBonusXp: 10, type: "TARGET_PRACTICE" } } },
      { type: "xp", label: "XP 110", value: "session", amount: 110, config: { sessionPayload: { type: "UNIT_TEST", skillIds: skillId ? [skillId] : [] }, updateSessionPayload: { type: "UNIT_TEST", hasBoost: true, happyHourBonusXp: 10, pathLevelSpecifics: { unitIndex: 0 } } }, disabled: !skillId },
      // {
      // 	type: 'xp', label: 'TEST', value: 'session', amount: 0, config: {
      // 		sessionPayload: { type: 'UNIT_TEST', skillIds: skillId ? [skillId] : [] },
      // 		updateSessionPayload: {
      // 			hasBoost: true,
      // 			happyHourBonusXp: 10,
      // 			pathLevelSpecifics: {
      // 				unitIndex: 0,
      // 			}
      // 		}
      // 	},
      // 	disabled: !skillId
      // },
      { type: "separator", label: "⟡ XP STORY FARMING ⟡", value: "", disabled: true },
      { type: "separator", label: "(fast, unsafe, English only) ", value: "", disabled: true },
      { type: "xp", label: "XP 50", value: "story", amount: 50, config: {} },
      // { type: 'xp', label: 'XP 90 ', value: 'story', amount: 90, config: { storyPayload: { hasXpBoost: true } } },
      { type: "xp", label: "XP 100 ", value: "story", amount: 100, config: { storyPayload: { happyHourBonusXp: 50 } } },
      { type: "xp", label: "XP 200 ", value: "story", amount: 200, config: { storyPayload: { happyHourBonusXp: 150 } } },
      { type: "xp", label: "XP 300 ", value: "story", amount: 300, config: { storyPayload: { happyHourBonusXp: 250 } } },
      { type: "xp", label: "XP 400 ", value: "story", amount: 400, config: { storyPayload: { happyHourBonusXp: 350 } } },
      { type: "xp", label: "XP 499 ", value: "story", amount: 499, config: { storyPayload: { happyHourBonusXp: 449 } } },
      { type: "separator", label: "⟡ STREAK FARMING ⟡", value: "", disabled: true },
      { type: "streak", label: "Streak farm (test)", value: "farm" }
    ];
  };
  const initSettings = () => {
    settingsManager.populateDefaultOptionSelect(farmOptions);
    settingsManager.loadDefaultFarmingOption(farmOptions);
    settingsManager.loadSettingsToUI();
  };
  (async () => {
    try {
      initInterface();
      setInterfaceVisible(false);
      addEventFloatingBtn();
      await initVariables();
      populateOptions();
      initSettings();
      updateUserInfo();
      addEventListeners();
      loadSavedSettings(settingsManager.getSettings());
      updateNotify('Duofarmer ready! For safety, I suggest that you use 2nd accounts.\nLimited or no use of "Story Farming"!');
    } catch (err) {
      logError(err, "Duofarmer init error!");
    }
  })();

})();