Greasy Fork is available in English.

斗鱼全民星推荐自动领取pro

原版《斗鱼全民星推荐自动领取》的增强版(应该增强了……)在保留核心功能的基础上,引入了可视化管理面板

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name             斗鱼全民星推荐自动领取pro
// @namespace        http://tampermonkey.net/
// @version          2.0.9
// @author           ienone&Truthss
// @description      原版《斗鱼全民星推荐自动领取》的增强版(应该增强了……)在保留核心功能的基础上,引入了可视化管理面板
// @license          MIT
// @match            *://www.douyu.com/*
// @connect          list-www.douyu.com
// @grant            GM_addStyle
// @grant            GM_cookie
// @grant            GM_deleteValue
// @grant            GM_getValue
// @grant            GM_listValues
// @grant            GM_log
// @grant            GM_notification
// @grant            GM_openInTab
// @grant            GM_setValue
// @grant            GM_xmlhttpRequest
// @grant            window.close
// @run-at           document-idle
// @noframes
// @original-author  ysl-ovo (https://greasyfork.org/zh-CN/users/1453821-ysl-ovo)
// ==/UserScript==

(function () {
  'use strict';

  const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):document.head.appendChild(document.createElement("style")).append(t);})(e));};

  const CONFIG = {



SCRIPT_PREFIX: "[全民星推荐助手]",
CONTROL_ROOM_ID: "6657",
TEMP_CONTROL_ROOM_RID: "6979222",

POPUP_WAIT_TIMEOUT: 2e4,
PANEL_WAIT_TIMEOUT: 1e4,
ELEMENT_WAIT_TIMEOUT: 3e4,
RED_ENVELOPE_LOAD_TIMEOUT: 15e3,
MIN_DELAY: 1e3,
MAX_DELAY: 2500,
CLOSE_TAB_DELAY: 1500,
INITIAL_SCRIPT_DELAY: 3e3,
UNRESPONSIVE_TIMEOUT: 15 * 60 * 1e3,
SWITCHING_CLEANUP_TIMEOUT: 3e4,
HEALTHCHECK_INTERVAL: 1e4,
DISCONNECTED_GRACE_PERIOD: 1e4,
STATS_UPDATE_INTERVAL: 4e3,

DRAGGABLE_BUTTON_ID: "douyu-qmx-starter-button",
BUTTON_POS_STORAGE_KEY: "douyu_qmx_button_position",
MODAL_DISPLAY_MODE: "floating",

API_URL: "https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/redbag/square/list",
COIN_LIST_URL: "https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/coin/record/list",
API_RETRY_COUNT: 3,
API_RETRY_DELAY: 5e3,

MAX_WORKER_TABS: 24,
DAILY_LIMIT_ACTION: "CONTINUE_DORMANT",
AUTO_PAUSE_ENABLED: true,
AUTO_PAUSE_DELAY_AFTER_ACTION: 5e3,
CALIBRATION_MODE_ENABLED: false,
SHOW_STATS_IN_PANEL: false,

ENABLE_DANMU_PRO: false,
STATE_STORAGE_KEY: "douyu_qmx_dashboard_state",
DAILY_LIMIT_REACHED_KEY: "douyu_qmx_daily_limit_reached",
STATS_INFO_STORAGE_KEY: "douyu_qmx_stats",

DEFAULT_THEME: "dark",
    INJECT_TARGET_RETRIES: 10,
INJECT_TARGET_INTERVAL: 500,
API_ROOM_FETCH_COUNT: 10,
UI_FEEDBACK_DELAY: 2e3,
DRAG_BUTTON_DEFAULT_PADDING: 20,
CONVERT_LEGACY_POSITION: true,

SELECTORS: {
redEnvelopeContainer: 'div.activeItem__d6uUm, div[class*="activeItem"]',
clickableContainer: 'div.container__0Xsh2, div[class*="container__"]',
countdownTimer: 'div.boxContent__N0d-3, div[class*="boxContent"]',
statusHeadline: 'div.boxHeadline__GP-am, div[class*="boxHeadline"]',
boxIcon: 'div.boxIcon__H-44m, div[class*="boxIcon"]',
popupModal: 'div.LiveNewAnchorSupportT-pop--inner, div[class*="pop--inner"]',
singleBag: 'div.LiveNewAnchorSupportT-singleBag, div[class*="singleBag"]',
openButton: 'div.LiveNewAnchorSupportT-singleBag--btn, div[class*="singleBag--btn"]',
closeButton: 'div.LiveNewAnchorSupportT-pop--close, div[class*="pop--close"]',
criticalElement: "#js-player-video",
      pauseButton: '#js-player-controlbar [class*="left-"] i:nth-child(1), [class*="icon-pause"], .icon-c8be96',
rewardSuccessIndicator: '[class*="singleBagOpened"]',
limitReachedPopup: "div.dy-Message-custom-content.dy-Message-info",
      rankListContainer: "#layout-Player-aside > div.layout-Player-asideMainTop > div.layout-Player-rank",
      anchorName: 'h3.anchorName__6NXv9, h3[class*="anchorName"], div.Title-anchorName > h2.Title-anchorNameH2',
prizeContainer: 'div.LiveNewAnchorSupportT-singleBag--awards, div[class*="singleBag--awards"]',
      prizeItem: 'div.LiveNewAnchorSupportT-singleBag--prize, div[class*="singleBag--prize"]',
      prizeImage: "img",
      prizeCount: "span"
    },






DB_NAME: "DouyuDanmukuPro",
    DB_VERSION: 2,
    DB_STORE_NAME: "danmuku_templates",
SETTINGS_KEY_PREFIX: "dda_",
CSS_CLASSES: {
      POPUP: "dda-popup",
      POPUP_SHOW: "show",
      POPUP_CONTENT: "dda-popup-content",
      POPUP_ITEM: "dda-popup-item",
      POPUP_ITEM_ACTIVE: "dda-popup-item-active",
      POPUP_ITEM_TEXT: "dda-popup-item-text",
      POPUP_EMPTY: "dda-popup-empty",
      EMPTY_MESSAGE: "dda-empty-message"
    },
KEYBOARD: {
      ENTER: "Enter",
      ESCAPE: "Escape",
      ARROW_UP: "ArrowUp",
      ARROW_DOWN: "ArrowDown",
      ARROW_LEFT: "ArrowLeft",
      ARROW_RIGHT: "ArrowRight",
      TAB: "Tab",
      BACKSPACE: "Backspace"
    },
API: {
      BASE_URL: "https://api.example.com",
      TIMEOUT: 5e3,
      RETRY_ATTEMPTS: 3
    },
DEBUG: false,
LOG_LEVEL: "info",


minSearchLength: 1,
maxSuggestions: 10,
debounceDelay: 300,

sortBy: "relevance",
autoImportMaxPages: 5,
autoImportPageSize: 50,
autoImportSortByPopularity: true,

enterSelectionModeKey: "ArrowUp",
exitSelectionModeKey: "ArrowDown",
expandCandidatesKey: "ArrowUp",
navigationLeftKey: "ArrowLeft",
navigationRightKey: "ArrowRight",
selectKey: "Enter",
cancelKey: "Escape",

popupShowDelay: 100,
popupHideDelay: 200,
animationDuration: 200,

maxPopupHeight: 300,
itemHeight: 40,
maxCandidateWidth: 200,

capsule: {
      maxWidth: 150,
height: 24,
padding: 16,
margin: 16,
totalHeight: 40,
fontSize: 12,
itemsPerRow: 4,
singleRowMaxItems: 8,

preview: {
        enabled: true,
showDelay: 500,
hideDelay: 100,
maxWidth: 300,
animationDuration: 200,
keyboardShowDelay: 150,
verticalOffset: 8,
horizontalOffset: 0,
preferredPosition: "top"
}
    },
enableAutoComplete: true,
enableKeyboardShortcuts: true,
enableSelectionMode: true,
enableSound: false,

enableSync: false,
syncInterval: 3e5,

maxCacheSize: 1e3,
cacheExpireTime: 864e5
};
  const SettingsManager = {
    STORAGE_KEY: "douyu_qmx_user_settings",
get() {
      const userSettings = GM_getValue(this.STORAGE_KEY, {});
      const themeSetting = GM_getValue(
        "douyu_qmx_theme",
        CONFIG.DEFAULT_THEME
      );
      return Object.assign({}, CONFIG, userSettings, { THEME: themeSetting });
    },
save(settingsToSave) {
      if (Object.hasOwn(settingsToSave, "THEME")) {
        const theme = settingsToSave.THEME;
        GM_setValue("douyu_qmx_theme", theme);
        delete settingsToSave.THEME;
      }
      delete settingsToSave.OPEN_TAB_DELAY;
      GM_setValue(this.STORAGE_KEY, settingsToSave);
    },
update(newSettings) {
      Object.assign(SETTINGS, newSettings);
      const currentStored = GM_getValue(this.STORAGE_KEY, {});
      const mergedToSave = Object.assign({}, currentStored, newSettings);
      this.save(mergedToSave);
      window.dispatchEvent(new CustomEvent("qmx-settings-update", { detail: newSettings }));
    },
reset() {
      GM_deleteValue(this.STORAGE_KEY);
      GM_deleteValue("douyu_qmx_theme");
    }
  };
  const SETTINGS = SettingsManager.get();
  SETTINGS.THEME = GM_getValue("douyu_qmx_theme", SETTINGS.DEFAULT_THEME);
  const STATE = {
    isSwitchingRoom: false,
    lastActionTime: 0
  };
  const Utils = {
log(message) {
      const logMsg = `${SETTINGS.SCRIPT_PREFIX} ${message}`;
      try {
        GM_log(logMsg);
      } catch (e) {
        console.log(e);
        console.log(logMsg);
      }
    },
sleep(ms) {
      return new Promise((resolve) => setTimeout(resolve, ms));
    },
getRandomDelay(min = SETTINGS.MIN_DELAY, max = SETTINGS.MAX_DELAY) {
      return Math.floor(Math.random() * (max - min + 1)) + min;
    },
getCurrentRoomId() {
      const url = window.location.href;
      let match = url.match(/douyu\.com\/(?:beta\/)?(\d+)/);
      if (match && match[1]) {
        return match[1];
      }
      match = url.match(/rid=(\d+)/);
      if (match && match[1]) {
        return match[1];
      }
      return null;
    },
formatTime(totalSeconds) {
      const minutes = Math.floor(totalSeconds / 60);
      const seconds = Math.floor(totalSeconds % 60);
      const paddedMinutes = String(minutes).padStart(2, "0");
      const paddedSeconds = String(seconds).padStart(2, "0");
      return `${paddedMinutes}:${paddedSeconds}`;
    },
getBeijingTime() {
      const now = new Date();
      const utcMillis = now.getTime();
      const beijingMillis = utcMillis + 8 * 60 * 60 * 1e3;
      return new Date(beijingMillis);
    },
formatDateAsBeijing(date) {
      const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1e3);
      const year = beijingDate.getUTCFullYear();
      const month = String(beijingDate.getUTCMonth() + 1).padStart(2, "0");
      const day = String(beijingDate.getUTCDate()).padStart(2, "0");
      return `${year}-${month}-${day}`;
    },
lockChecker: function(lockKey, callback, ...args) {
      if (GM_getValue(lockKey, false)) {
        setTimeout(() => callback(...args), 50);
        return false;
      }
      return true;
    },
setLocalValueWithLock: function(lockKey, storageKey, value, nickname) {
      try {
        GM_setValue(lockKey, true);
        GM_setValue(storageKey, value);
      } catch (e) {
        Utils.log(`[${nickname}-写] 严重错误:GM_setValue 写入失败! 错误信息: ${e.message}`);
      } finally {
        GM_setValue(lockKey, false);
      }
    },
debounce(func, delay) {
      let timeoutId;
      return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
      };
    },
throttle(func, delay) {
      let lastCall = 0;
      return function(...args) {
        const now = Date.now();
        if (now - lastCall >= delay) {
          lastCall = now;
          return func.apply(this, args);
        }
      };
    },
isInLiveRoom() {
      const roomId = this.getCurrentRoomId();
      return roomId !== null && document.querySelector("[data-v-5aa519d2]");
    },
getElementPosition(element) {
      const rect = element.getBoundingClientRect();
      return {
        x: rect.left + window.scrollX,
        y: rect.top + window.scrollY,
        width: rect.width,
        height: rect.height
      };
    },
safeExecute(func, context = "unknown") {
      try {
        return func();
      } catch (error) {
        this.log(`执行函数时出错 [${context}]: ${error.message}`, "error");
        return null;
      }
    },
generateId(prefix = "dda") {
      return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    },
deepClone(obj) {
      if (obj === null || typeof obj !== "object") {
        return obj;
      }
      if (obj instanceof Date) {
        return new Date(obj.getTime());
      }
      if (obj instanceof Array) {
        return obj.map((item) => this.deepClone(item));
      }
      if (typeof obj === "object") {
        const cloned = {};
        for (const key in obj) {
          if (Object.hasOwn(obj, key)) {
            cloned[key] = this.deepClone(obj[key]);
          }
        }
        return cloned;
      }
    },
getElementWithRetry: async function(selector, parentNode = document, retries = 5, interval = 1e3) {
      let element = parentNode.querySelector(selector);
      if (element) {
        return element;
      }
      for (let i = 0; i < retries; i++) {
        await Utils.sleep(interval);
        element = parentNode.querySelector(selector);
        if (element) {
          return element;
        }
      }
      throw new Error(`无法找到元素: ${selector},已重试 ${retries} 次`);
    }
  };
  function initHackTimer(workerScript) {
    try {
      var blob = new Blob([
        "                var fakeIdToId = {};                onmessage = function (event) {                    var data = event.data,                        name = data.name,                        fakeId = data.fakeId,                        time;                    if(data.hasOwnProperty('time')) {                        time = data.time;                    }                    switch (name) {                        case 'setInterval':                            fakeIdToId[fakeId] = setInterval(function () {                                postMessage({fakeId: fakeId});                            }, time);                            break;                        case 'clearInterval':                            if (fakeIdToId.hasOwnProperty (fakeId)) {                                clearInterval(fakeIdToId[fakeId]);                                delete fakeIdToId[fakeId];                            }                            break;                        case 'setTimeout':                            fakeIdToId[fakeId] = setTimeout(function () {                                postMessage({fakeId: fakeId});                                if (fakeIdToId.hasOwnProperty (fakeId)) {                                    delete fakeIdToId[fakeId];                                }                            }, time);                            break;                        case 'clearTimeout':                            if (fakeIdToId.hasOwnProperty (fakeId)) {                                clearTimeout(fakeIdToId[fakeId]);                                delete fakeIdToId[fakeId];                            }                            break;                    }                }                "
      ]);
      workerScript = window.URL.createObjectURL(blob);
    } catch (error) {
      Utils.log(error);
    }
    var worker, fakeIdToCallback = {}, lastFakeId = 0, maxFakeId = 2147483647, logPrefix = "HackTimer.js by turuslan: ";
    if (typeof Worker !== "undefined") {
      let getFakeId = function() {
        do {
          if (lastFakeId == maxFakeId) {
            lastFakeId = 0;
          } else {
            lastFakeId++;
          }
        } while (Object.hasOwn(fakeIdToCallback, lastFakeId));
        return lastFakeId;
      };
      try {
        worker = new Worker(workerScript);
        window.setInterval = function(callback, time) {
          var fakeId = getFakeId();
          fakeIdToCallback[fakeId] = {
            callback,
            parameters: Array.prototype.slice.call(arguments, 2)
          };
          worker.postMessage({
            name: "setInterval",
            fakeId,
            time
          });
          return fakeId;
        };
        window.clearInterval = function(fakeId) {
          if (Object.hasOwn(fakeIdToCallback, fakeId)) {
            delete fakeIdToCallback[fakeId];
            worker.postMessage({
              name: "clearInterval",
              fakeId
            });
          }
        };
        window.setTimeout = function(callback, time) {
          var fakeId = getFakeId();
          fakeIdToCallback[fakeId] = {
            callback,
            parameters: Array.prototype.slice.call(arguments, 2),
            isTimeout: true
          };
          worker.postMessage({
            name: "setTimeout",
            fakeId,
            time
          });
          return fakeId;
        };
        window.clearTimeout = function(fakeId) {
          if (Object.hasOwn(fakeIdToCallback, fakeId)) {
            delete fakeIdToCallback[fakeId];
            worker.postMessage({
              name: "clearTimeout",
              fakeId
            });
          }
        };
        worker.onmessage = function(event) {
          var data = event.data, fakeId = data.fakeId, request, parameters, callback;
          if (Object.hasOwn(fakeIdToCallback, fakeId)) {
            request = fakeIdToCallback[fakeId];
            callback = request.callback;
            parameters = request.parameters;
            if (Object.hasOwn(request, "isTimeout") && request.isTimeout) {
              delete fakeIdToCallback[fakeId];
            }
          }
          if (typeof callback === "string") {
            try {
              callback = new Function(callback);
            } catch (error) {
              console.log(logPrefix + "Error parsing callback code string: ", error);
            }
          }
          if (typeof callback === "function") {
            callback.apply(window, parameters);
          }
        };
        worker.onerror = function(event) {
          console.log(event);
        };
        console.log(logPrefix + "Initialisation succeeded");
      } catch (error) {
        console.log(logPrefix + "Initialisation failed");
        console.error(error);
      }
    } else {
      console.log(logPrefix + "Initialisation failed - HTML5 Web Worker is not supported");
    }
  }
  const ControlPanelRefactoredCss = ':root{color-scheme:light dark;--motion-easing: cubic-bezier(.4, 0, .2, 1);--status-color-waiting: #4CAF50;--status-color-claiming: #2196F3;--status-color-switching: #FFC107;--status-color-error: #F44336;--status-color-opening: #9C27B0;--status-color-dormant: #757575;--status-color-unresponsive: #FFA000;--status-color-disconnected: #BDBDBD;--status-color-stalled: #9af39dff}body[data-theme=dark]{--md-sys-color-primary: #D0BCFF;--md-sys-color-on-primary: #381E72;--md-sys-color-primary-container: #4F378B;--md-sys-color-on-primary-container: #EADDFF;--md-sys-color-surface-container: #211F26;--md-sys-color-on-surface: #E6E1E5;--md-sys-color-on-surface-variant: #CAC4D0;--md-sys-color-outline: #938F99;--md-sys-color-surface-bright: #36343B;--md-sys-color-tertiary: #EFB8C8;--md-sys-color-scrim: #000000;--surface-container-highest: #3D3B42}body[data-theme=light]{--md-sys-color-primary: #6750A4;--md-sys-color-on-primary: #FFFFFF;--md-sys-color-primary-container: #EADDFF;--md-sys-color-on-primary-container: #21005D;--md-sys-color-surface-container: #F3EDF7;--md-sys-color-surface-bright: #FEF7FF;--md-sys-color-on-surface: #1C1B1F;--md-sys-color-on-surface-variant: #49454F;--md-sys-color-outline: #79747E;--md-sys-color-tertiary: #7D5260;--md-sys-color-scrim: #000000;--surface-container-highest: #E6E0E9}.qmx-hidden{display:none!important}.qmx-modal-open-scroll-lock{overflow:hidden!important}.is-dragging{transition:none!important}.qmx-flex-center{display:flex;align-items:center;justify-content:center}.qmx-flex-between{display:flex;align-items:center;justify-content:space-between}.qmx-flex-column{display:flex;flex-direction:column}.qmx-modal-base{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);z-index:10001;background-color:var(--md-sys-color-surface-bright);color:var(--md-sys-color-on-surface);border-radius:28px;box-shadow:0 12px 32px #00000080;display:flex;flex-direction:column;opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s,transform .3s}.qmx-modal-base.visible{opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1)}.qmx-backdrop{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:var(--md-sys-color-scrim);z-index:9998;opacity:0;visibility:hidden;transition:opacity .3s ease}.qmx-backdrop.visible{opacity:.5;visibility:visible}.qmx-btn{padding:10px 16px;border:1px solid var(--md-sys-color-outline);background-color:transparent;color:var(--md-sys-color-primary);border-radius:20px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,transform .2s,box-shadow .2s;-webkit-user-select:none;user-select:none}.qmx-btn:hover{background-color:#d0bcff1a;transform:translateY(-2px);box-shadow:0 2px 4px #0000001a}.qmx-btn:active{transform:translateY(0) scale(.98);box-shadow:none}.qmx-btn:disabled{opacity:.5;cursor:not-allowed}.qmx-btn--primary{background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);border:none}.qmx-btn--primary:hover{background-color:#c2b3ff;box-shadow:0 4px 8px #0003}.qmx-btn--danger{border-color:#f44336;color:#f44336}.qmx-btn--danger:hover{background-color:#f443361a}.qmx-btn--icon{width:36px;height:36px;padding:0;border-radius:50%;background-color:#d0bcff26;border:none;color:var(--md-sys-color-primary)}.qmx-btn--icon:hover{background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);transform:scale(1.05) rotate(180deg)}.qmx-styled-list{list-style:none;padding-left:0}.qmx-styled-list li{position:relative;padding-left:20px;margin-bottom:8px}.qmx-styled-list li:before{content:"◆";position:absolute;left:0;top:2px;color:var(--md-sys-color-primary);font-size:12px}.qmx-scrollbar::-webkit-scrollbar{width:10px}.qmx-scrollbar::-webkit-scrollbar-track{background:var(--md-sys-color-surface-bright);border-radius:10px}.qmx-scrollbar::-webkit-scrollbar-thumb{background-color:var(--md-sys-color-primary);border-radius:10px;border:2px solid var(--md-sys-color-surface-bright)}.qmx-scrollbar::-webkit-scrollbar-thumb:hover{background-color:#e0d1ff}.qmx-input{background-color:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline);color:var(--md-sys-color-on-surface);border-radius:8px;padding:12px;width:100%;box-sizing:border-box;transition:box-shadow .2s,border-color .2s}.qmx-input:hover{border-color:var(--md-sys-color-primary)}.qmx-input:focus{outline:none;border-color:var(--md-sys-color-primary);box-shadow:0 0 0 2px #d0bcff4d}.qmx-input[type=number]::-webkit-inner-spin-button,.qmx-input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.qmx-input[type=number]{margin-left:5px;margin-bottom:9px;-moz-appearance:textfield;appearance:textfield}.qmx-fieldset-unit{position:relative;padding:0;margin:0;border:1px solid var(--md-sys-color-outline);border-radius:8px;background-color:var(--md-sys-color-surface-container);transition:border-color .2s,box-shadow .2s;width:100%;box-sizing:border-box}.qmx-fieldset-unit:hover{border-color:var(--md-sys-color-primary)}.qmx-fieldset-unit:focus-within{border-color:var(--md-sys-color-primary);box-shadow:0 0 0 2px #d0bcff4d}.qmx-fieldset-unit input[type=number]{border:none;background:none;outline:none;box-shadow:none;color:var(--md-sys-color-on-surface);padding:3px 10px 4px;width:100%;box-sizing:border-box}.qmx-fieldset-unit legend{padding:0 6px;font-size:12px;color:var(--md-sys-color-on-surface-variant);margin-left:auto;margin-right:12px;text-align:right;pointer-events:none}.qmx-toggle{position:relative;display:inline-block;width:52px;height:30px}.qmx-toggle input{opacity:0;width:0;height:0}.qmx-toggle .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline);border-radius:30px;transition:background-color .3s,border-color .3s}.qmx-toggle .slider:before{position:absolute;content:"";height:22px;width:22px;left:3px;bottom:3px;background-color:var(--md-sys-color-on-surface-variant);border-radius:50%;box-shadow:0 1px 3px #0003;transition:all .3s cubic-bezier(.175,.885,.32,1.275)}.qmx-toggle input:checked+.slider{background-color:var(--md-sys-color-primary);border-color:var(--md-sys-color-primary)}.qmx-toggle input:checked+.slider:before{background-color:var(--md-sys-color-on-primary);transform:translate(22px)}.qmx-toggle:hover .slider{border-color:var(--md-sys-color-primary)}.qmx-select{position:relative;width:100%}.qmx-select-styled{position:relative;padding:10px 30px 10px 12px;background-color:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline);border-radius:8px;cursor:pointer;transition:all .2s;-webkit-user-select:none;user-select:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:inset 0 2px 4px #00000014}.qmx-select-styled:after{content:"";position:absolute;top:50%;right:12px;transform:translateY(-50%);width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid var(--md-sys-color-on-surface-variant);transition:transform .3s ease}.qmx-select:hover .qmx-select-styled{border-color:var(--md-sys-color-primary)}.qmx-select.active .qmx-select-styled{border-color:var(--md-sys-color-primary);box-shadow:inset 0 3px 6px #0000001a,0 0 0 2px #d0bcff4d}.qmx-select.active .qmx-select-styled:after{transform:translateY(-50%) rotate(180deg)}.qmx-select-options{position:absolute;top:105%;left:0;right:0;z-index:10;background-color:var(--md-sys-color-surface-bright);border:1px solid var(--md-sys-color-outline);border-radius:8px;max-height:0;overflow:hidden;opacity:0;transform:translateY(-10px);transition:all .3s ease;padding:4px 0}.qmx-select.active .qmx-select-options{max-height:200px;opacity:1;transform:translateY(0)}.qmx-select-options div{padding:10px 12px;cursor:pointer;transition:background-color .2s}.qmx-select-options div:hover{background-color:#d0bcff1a}.qmx-select-options div.selected{background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);font-weight:500}.qmx-range-slider-wrapper{display:flex;flex-direction:column;gap:8px}.qmx-range-slider-container{position:relative;height:24px;display:flex;align-items:center}.qmx-range-slider-container input[type=range]{position:absolute;width:100%;height:4px;-webkit-appearance:none;appearance:none;background:none;pointer-events:none;margin:0}.qmx-range-slider-container input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:20px;height:20px;background-color:var(--md-sys-color-primary);border-radius:50%;cursor:grab;border:none;box-shadow:0 1px 3px #0000004d;transition:transform .2s}.qmx-range-slider-container input[type=range]::-webkit-slider-thumb:active{cursor:grabbing;transform:scale(1.1)}.qmx-range-slider-container input[type=range]::-moz-range-thumb{pointer-events:auto;width:20px;height:20px;background-color:var(--md-sys-color-primary);border-radius:50%;cursor:grab;border:none;box-shadow:0 1px 3px #0000004d;transition:transform .2s}.qmx-range-slider-container input[type=range]::-moz-range-thumb:active{cursor:grabbing;transform:scale(1.1)}.qmx-range-slider-track-container{position:absolute;width:100%;height:4px;background-color:var(--md-sys-color-surface-container);border-radius:2px}.qmx-range-slider-progress{position:absolute;height:100%;background-color:var(--md-sys-color-primary);border-radius:2px}.qmx-range-slider-values{font-size:14px;color:var(--md-sys-color-primary);text-align:center;font-weight:500}.qmx-tooltip-icon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background-color:var(--md-sys-color-outline);color:var(--md-sys-color-surface-container);font-size:12px;font-weight:700;cursor:help;-webkit-user-select:none;user-select:none}#qmx-global-tooltip{position:fixed;background-color:var(--surface-container-highest);color:var(--md-sys-color-on-surface);padding:8px 12px;border-radius:8px;box-shadow:0 4px 12px #0003;font-size:12px;font-weight:400;line-height:1.5;z-index:10002;max-width:250px;opacity:0;visibility:hidden;transform:translateY(-5px);transition:opacity .2s ease,transform .2s ease,visibility .2s;pointer-events:none}#qmx-global-tooltip.visible{opacity:1;visibility:visible;transform:translateY(0)}.theme-switch{position:relative;display:block;width:60px;height:34px;cursor:pointer;transition:none}.theme-switch input{opacity:0;width:0;height:0}.theme-switch-wrapper{align-self:center}.slider-track{position:absolute;top:0;left:0;width:34px;height:34px;background-color:var(--surface-container-highest);border-radius:17px;box-shadow:inset 2px 2px 4px #0003,inset -2px -2px 4px #ffffff0d;transition:width .3s ease,left .3s ease,border-radius .3s ease,box-shadow .3s ease}.theme-switch:hover .slider-track{width:60px}.theme-switch input:checked+.slider-track{left:26px}.theme-switch:hover input:checked+.slider-track{left:0}.slider-dot{position:absolute;height:26px;width:26px;left:4px;top:4px;background-color:var(--md-sys-color-primary);border-radius:50%;display:flex;align-items:center;justify-content:center;overflow:hidden;box-shadow:0 4px 8px #0000004d;transition:transform .3s cubic-bezier(.4,0,.2,1),background-color .3s ease,box-shadow .3s ease}.theme-switch input:checked~.slider-dot{transform:translate(26px);background-color:var(--primary-container)}.slider-dot .icon{position:absolute;width:20px;height:20px;color:var(--md-sys-color-on-primary);transition:opacity .3s ease,transform .3s cubic-bezier(.4,0,.2,1)}.sun{opacity:1;transform:translateY(0) rotate(0)}.moon{opacity:0;transform:translateY(20px) rotate(-45deg)}.theme-switch input:checked~.slider-dot .sun{opacity:0;transform:translateY(-20px) rotate(45deg)}.theme-switch input:checked~.slider-dot .moon{opacity:1;transform:translateY(0) rotate(0);color:var(--md-sys-color-on-surface)}#douyu-qmx-starter-button{position:fixed;top:0;left:0;z-index:10000;background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);border:none;width:56px;height:56px;border-radius:16px;cursor:grab;box-shadow:0 4px 8px #0000004d;display:flex;align-items:center;justify-content:center;transform:translate3d(var(--tx, 0px),var(--ty, 0px),0) scale(1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .3s cubic-bezier(.4,0,.2,1);will-change:transform,opacity}#douyu-qmx-starter-button .icon{font-size:28px}#douyu-qmx-starter-button.hidden{opacity:0;transform:translate3d(var(--tx, 0px),var(--ty, 0px),0) scale(.5);pointer-events:none}#qmx-modal-container{background-color:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface);display:flex;flex-direction:column}#qmx-modal-container.mode-floating,#qmx-modal-container.mode-centered{position:fixed;z-index:9999;width:335px;max-width:90vw;max-height:80vh;border-radius:28px;box-shadow:0 8px 24px #0006;opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s,transform .2s ease-out;will-change:transform,opacity}#qmx-modal-container.visible{opacity:1;visibility:visible}#qmx-modal-container.mode-floating{top:0;left:0;transform:translate3d(var(--tx, 0px),var(--ty, 0px),0)}#qmx-modal-container.mode-floating .qmx-modal-header{cursor:move}#qmx-modal-container.mode-centered{top:50%;left:50%;transform:translate(-50%,-50%)}#qmx-modal-container.mode-inject-rank-list{position:relative;width:100%;flex:1;min-height:0;box-shadow:none;border-radius:0;transform:none!important}.qmx-modal-header{position:relative;padding:10px 20px 4px;font-size:20px;font-weight:400;color:var(--md-sys-color-on-surface);-webkit-user-select:none;user-select:none;display:flex;align-items:center;justify-content:space-between}.qmx-modal-close-icon{width:36px;height:36px;background-color:#d0bcff26;border:none;border-radius:50%;cursor:pointer;transition:background-color .2s,transform .2s;position:relative;flex-shrink:0}.qmx-modal-close-icon:hover{background-color:var(--md-sys-color-primary);transform:scale(1.05) rotate(180deg)}.qmx-modal-close-icon:before,.qmx-modal-close-icon:after{content:"";position:absolute;top:50%;left:50%;width:16px;height:2px;background-color:var(--md-sys-color-primary);transition:background-color .2s ease-in-out}.qmx-modal-close-icon:hover:before,.qmx-modal-close-icon:hover:after{background-color:var(--md-sys-color-on-primary)}.qmx-modal-close-icon:before{transform:translate(-50%,-50%) rotate(45deg)}.qmx-modal-close-icon:after{transform:translate(-50%,-50%) rotate(-45deg)}.qmx-modal-content{padding:0 24px;flex:1;min-height:0;display:flex;flex-direction:column}.qmx-modal-content h3{flex-shrink:0;font-size:16px;font-weight:500;color:var(--md-sys-color-on-surface-variant);margin:0 0 8px}.qmx-stats-header{display:flex;align-items:center;justify-content:space-between;cursor:pointer;padding:4px 0;-webkit-user-select:none;user-select:none;transition:background-color .2s;border-radius:8px}.qmx-stats-header:hover{background-color:#ffffff0d}.qmx-stats-header h3{font-size:16px;font-weight:500;color:var(--md-sys-color-on-surface-variant);margin:8px 0}.qmx-stats-arrow{font-size:12px;color:var(--md-sys-color-on-surface-variant);transition:transform .3s ease}.qmx-stats-header.expanded .qmx-stats-arrow{transform:rotate(180deg)}.qmx-stats-container{position:relative;overflow:hidden;padding:0}.qmx-stats-toggle{position:relative;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .3s cubic-bezier(.25,.46,.45,.94);margin:4px 24px;border-radius:10px}.qmx-stats-indicator{font-size:15px;color:var(--md-sys-color-on-surface-variant);transition:all .3s cubic-bezier(.25,.46,.45,.94);position:absolute;z-index:2}.qmx-stats-label{font-size:12px;color:var(--md-sys-color-on-surface-variant);opacity:0;transform:scale(.95);transition:all .3s cubic-bezier(.25,.46,.45,.94);position:absolute;z-index:1;white-space:nowrap}.qmx-stats-refresh{opacity:0;font-size:15px;transition:all .3s cubic-bezier(.25,.46,.45,.94);position:relative;background:transparent;border:none;color:var(--md-sys-color-on-surface-variant);padding:4px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer}.qmx-stats-refresh:hover{background-color:#ffffff1a;color:var(--md-sys-color-primary)}.qmx-stats-refresh svg{width:20px;height:20px}.qmx-stats-refresh.rotating{animation:rotate360 1s linear}@keyframes rotate360{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.qmx-stats-switcher{opacity:0;font-size:20px;transition:all .3s cubic-bezier(.25,.46,.45,.94);margin:8px 8px 12px}.qmx-stats-toggle:hover{background-color:#ffffff0d;margin:4px 24px 21px}.qmx-stats-toggle:hover .qmx-stats-indicator{transform:translateY(85%)}.qmx-stats-toggle:hover .qmx-stats-label{opacity:1;transform:scale(1)}.qmx-stats-toggle.expanded{height:28px;padding:0 12px;margin:6px 20px}.qmx-stats-toggle.expanded .qmx-stats-indicator{opacity:1;transform:rotate(180deg) scale(1.05);position:relative;margin:9px}.qmx-stats-toggle.expanded .qmx-stats-indicator.transitioning{opacity:0}.qmx-stats-toggle.expanded .qmx-stats-label{opacity:1;transform:scale(1.05);font-size:12px;font-weight:500;position:relative;transition:all .3s cubic-bezier(.25,.46,.45,.94);color:var(--md-sys-color-on-surface)}.qmx-stats-toggle.expanded .qmx-stats-label.transitioning{opacity:0}.qmx-stats-toggle.expanded .qmx-stats-refresh{opacity:1;cursor:pointer;transform:scale(1.05);margin:8px}.qmx-stats-toggle.expanded .qmx-stats-refresh.disabled{opacity:0;transform:translate(100%)}.qmx-stats-toggle.expanded .qmx-stats-switcher{cursor:pointer;opacity:1;position:absolute}.qmx-stats-toggle.expanded #qmx-stats-left{left:60px;transform:translate(-55px)}.qmx-stats-toggle.expanded #qmx-stats-right{right:60px;transform:translate(55px)}.qmx-stats-toggle.expanded .qmx-stats-switcher.disabled{cursor:not-allowed;color:#666}.qmx-stats-content{max-height:0;opacity:0;overflow:hidden;transition:max-height .3s cubic-bezier(.25,.46,.45,.94),opacity .2s cubic-bezier(.25,.46,.45,.94) .1s,padding .3s cubic-bezier(.25,.46,.45,.94);padding:0 24px}.qmx-stats-content.expanded{max-height:120px;opacity:1;padding:8px 24px 16px}.qmx-modal-stats{display:flex;gap:1px}.qmx-modal-stats-child{background-color:var(--md-sys-color-surface-bright);border-radius:12px;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;gap:8px;transition:background-color .2s,transform .3s ease,opacity .3s ease;width:88px;float:left;margin-left:2%}.qmx-stat-info-avg,.qmx-stat-info-total,.qmx-stat-info-receivedCount{display:flex;flex-direction:column;flex-grow:1;gap:4px;font-size:14px;overflow:hidden}.qmx-stat-header{display:flex;align-items:baseline;justify-content:center}.qmx-stat-nickname{font-weight:500;color:var(--md-sys-color-on-surface);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto!important}.qmx-stat-details{opacity:1;display:flex;align-items:center;font-size:13px;color:var(--md-sys-color-on-surface-variant);transition:all .3s cubic-bezier(.25,.46,.45,.94);justify-content:center;flex-wrap:wrap}.qmx-stat-details.transitioning{opacity:0}.qmx-stat-stats{font-weight:500}#qmx-tab-list{overflow-y:auto;flex-grow:1;padding-right:4px;margin-right:-4px}.qmx-tab-list-item{background-color:var(--md-sys-color-surface-bright);border-radius:16px;padding:8px 16px 8px 18px;margin-bottom:8px;display:flex;align-items:center;gap:12px;transition:background-color .2s,transform .3s ease,opacity .3s ease;position:relative;overflow:hidden}.qmx-tab-list-item:hover{background-color:var(--surface-container-highest)}.qmx-item-enter{opacity:0;transform:translate(20px)}.qmx-item-enter-active{opacity:1;transform:translate(0)}.qmx-item-exit-active{position:absolute;opacity:0;transform:scale(.8);transition:all .3s ease;z-index:-1;pointer-events:none}.qmx-tab-status-dot{position:absolute;left:0;top:50%;transform:translateY(-50%);width:2px;height:28px;border-radius:0 4px 4px 0;transition:all .3s cubic-bezier(.25,.46,.45,.94);flex-shrink:0}.qmx-tab-list-item:hover .qmx-tab-status-dot{height:32px;width:3px}.qmx-tab-info{display:flex;flex-direction:column;flex-grow:1;gap:2px;font-size:14px;overflow:hidden;min-width:0}.qmx-tab-header{display:flex;align-items:center;justify-content:flex-start;height:auto;overflow:visible;margin-bottom:2px}.qmx-tab-identity{position:relative;display:inline-flex;align-items:center;gap:0;padding:2px 4px;border-radius:999px;border:1px solid var(--md-sys-color-on-surface-variant);background-color:var(--md-sys-color-surface-bright);color:var(--md-sys-color-on-surface);font-size:13px;font-weight:500;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease,border-color .2s ease,padding-left .2s ease;overflow:visible}.qmx-tab-identity:hover{padding-left:24px;border-color:var(--md-sys-color-primary);box-shadow:0 6px 16px #00000040}.qmx-tab-identity.copied{border-color:var(--status-color-waiting);box-shadow:0 0 0 2px #4caf5033}.qmx-tab-identity-icon{position:absolute;left:8px;top:50%;transform:translateY(-50%) translate(-100%);width:14px;height:14px;color:var(--md-sys-color-primary);opacity:0;transition:opacity .2s ease,transform .2s ease;pointer-events:none;z-index:10}.qmx-tab-identity:hover .qmx-tab-identity-icon{opacity:1;transform:translateY(-50%) translate(0);pointer-events:auto;cursor:pointer}.qmx-tab-identity-text{display:inline-flex;flex-direction:column;position:relative;overflow:hidden;pointer-events:none;min-width:0}.qmx-tab-identity-text span{transition:transform .25s ease,opacity .2s ease;white-space:nowrap;text-align:left}.qmx-tab-identity[data-state=nickname] .identity-roomid,.qmx-tab-identity[data-state=room] .identity-nickname{transform:translateY(-100%);opacity:0;position:absolute;left:0;top:0}.qmx-tab-identity[data-state=nickname] .identity-nickname,.qmx-tab-identity[data-state=room] .identity-roomid{position:relative;transform:translateY(0);opacity:1}.qmx-tab-details{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--md-sys-color-on-surface-variant);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.qmx-tab-prizes{display:flex;align-items:center;gap:4px;margin-left:auto;padding:4px 8px;background-color:var(--md-sys-color-surface-container, rgba(0, 0, 0, .05));flex-shrink:0}.qmx-tab-prizes.single-prize{flex-direction:row;border-radius:100px}.qmx-tab-prizes.multi-prizes{flex-direction:column;border-radius:12px;min-width:70px;padding:6px 10px}.qmx-tab-prize-item{display:inline-flex;align-items:center;gap:4px;background:transparent;padding:0;border:none;line-height:1;color:var(--md-sys-color-on-surface);font-weight:500;font-size:11px}.qmx-tab-prize-item:hover{opacity:.8}.qmx-tab-prize-item svg{display:block;width:12px;height:12px;flex-shrink:0}.qmx-tab-prize-text{font-weight:600;white-space:nowrap;color:inherit;font-size:11px;display:flex;align-items:center}.qmx-tab-close-btn{flex-shrink:0;background-color:#d0bcff26;border:none;color:var(--md-sys-color-primary);cursor:pointer;padding:0;transition:background-color .2s,transform .2s,color .2s;display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%}.qmx-tab-close-btn svg{width:14px;height:14px;stroke:currentColor;stroke-width:3}.qmx-tab-close-btn:hover{color:var(--md-sys-color-on-primary);background-color:var(--md-sys-color-primary);transform:scale(1.1) rotate(90deg)}.qmx-modal-footer{padding:16px 24px;display:flex;gap:8px}.qmx-modal-btn:hover{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);transform:translateY(-2px);box-shadow:0 2px 4px #0000001a}.qmx-modal-btn.primary:hover{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);box-shadow:0 4px 8px #0003}.qmx-modal-btn.danger{border-color:var(--status-color-error);color:var(--status-color-error)}.qmx-modal-btn.danger:hover{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.qmx-tab-header.show-id .qmx-tab-nickname{pointer-events:none!important}.qmx-tab-header.show-id .qmx-tab-room-id{pointer-events:auto!important}#qmx-settings-modal{width:500px;max-width:95vw}.qmx-settings-header{padding:12px 24px;border-bottom:1px solid var(--md-sys-color-outline);flex-shrink:0}.qmx-settings-tabs{display:flex;gap:8px}.qmx-settings-tabs .tab-link{padding:8px 16px;border:none;background:none;color:var(--md-sys-color-on-surface-variant);cursor:pointer;border-radius:8px;transition:background-color .2s,color .2s;font-size:14px}.qmx-settings-tabs .tab-link:hover{background-color:#ffffff0d}.qmx-settings-tabs .tab-link.active{background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);font-weight:500}.qmx-settings-content{padding:16px 24px;flex-grow:1;overflow-y:auto;overflow-x:hidden;max-height:60vh;scrollbar-gutter:stable}.qmx-settings-content .tab-content{display:none}.qmx-settings-content .tab-content.active{display:block}.qmx-settings-footer{padding:16px 24px;display:flex;justify-content:flex-end;gap:10px;border-top:1px solid var(--md-sys-color-outline);flex-shrink:0}.qmx-settings-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:24px;align-items:start}.qmx-settings-item{display:flex;flex-direction:column;justify-content:center;gap:8px}.qmx-settings-item label{font-size:14px;font-weight:500;display:flex;align-items:center;gap:6px}.qmx-settings-item small{font-size:12px;color:var(--md-sys-color-on-surface-variant);opacity:.8}.qmx-settings-warning{padding:12px;background-color:#f4433633;border:1px solid #F44336;color:#efb8c8;border-radius:8px;grid-column:1 / -1}#tab-about{line-height:1.7;font-size:14px}#tab-about h4{color:var(--md-sys-color-primary);font-size:16px;font-weight:500;margin-top:20px;margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid var(--md-sys-color-outline)}#tab-about h4:first-child{margin-top:0}#tab-about p{margin-bottom:10px;color:var(--md-sys-color-on-surface-variant)}#tab-about .version-tag{display:inline-block;background-color:var(--md-sys-color-tertiary);color:var(--md-sys-color-on-primary);padding:2px 8px;border-radius:12px;font-size:13px;font-weight:500;margin-left:8px}#tab-about a{color:var(--md-sys-color-tertiary);text-decoration:none;font-weight:500;transition:color .2s}#tab-about a:hover{color:#ffd6e1;text-decoration:underline}#qmx-notice-modal{width:450px;max-width:90vw}#qmx-notice-modal .qmx-modal-content{padding:16px 24px}#qmx-notice-modal .qmx-modal-content p{margin-bottom:12px;line-height:1.6;font-size:15px;color:var(--md-sys-color-on-surface-variant)}#qmx-notice-modal .qmx-modal-content ul{margin:12px 0;padding-left:20px}#qmx-notice-modal .qmx-modal-content li{margin-bottom:10px;position:relative;font-size:15px;line-height:1.6}#qmx-notice-modal .qmx-modal-content li:before{content:"◆";position:absolute;left:-18px;color:var(--md-sys-color-primary);font-size:12px}#qmx-notice-modal h3{font-size:20px;font-weight:500;margin:0}#qmx-notice-modal h4{color:var(--md-sys-color-primary);font-size:16px;font-weight:500;margin-top:16px;margin-bottom:8px;padding-bottom:5px;border-bottom:1px solid var(--md-sys-color-outline)}#qmx-notice-modal .qmx-warning-text{background-color:#ffc1071a;border-left:4px solid #FFC107;padding:12px 16px;margin:16px 0;border-radius:4px;font-size:15px;line-height:1.6}#qmx-notice-modal .qmx-warning-text strong{color:#ff8f00}#qmx-notice-modal a{color:var(--md-sys-color-tertiary);text-decoration:none;font-weight:500;transition:color .2s}#qmx-notice-modal a:hover{color:#ffd6e1;text-decoration:underline}#qmx-modal-backdrop,#qmx-notice-backdrop{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:var(--md-sys-color-scrim);z-index:9998;opacity:0;visibility:hidden;transition:opacity .3s ease}#qmx-modal-backdrop.visible,#qmx-notice-backdrop.visible{opacity:.5;visibility:visible}#qmx-settings-modal,#qmx-notice-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);z-index:10001;background-color:var(--md-sys-color-surface-bright);color:var(--md-sys-color-on-surface);border-radius:28px;box-shadow:0 12px 32px #00000080;display:flex;flex-direction:column;opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s,transform .3s}#qmx-settings-modal.visible,#qmx-notice-modal.visible{opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1)}.qmx-modal-btn{flex-grow:1;padding:10px 16px;border:1px solid var(--md-sys-color-outline);background-color:transparent;color:var(--md-sys-color-primary);border-radius:20px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,transform .2s,box-shadow .2s;-webkit-user-select:none;user-select:none}.qmx-modal-btn:hover{background-color:#d0bcff1a;transform:translateY(-2px);box-shadow:0 2px 4px #0000001a}.qmx-modal-btn:active{transform:translateY(0) scale(.98);box-shadow:none}.qmx-modal-btn:disabled{opacity:.5;cursor:not-allowed}.qmx-modal-btn.primary{background-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);border:none}.qmx-modal-btn.primary:hover{background-color:#c2b3ff;box-shadow:0 4px 8px #0003}.qmx-modal-btn.danger{border-color:#f44336;color:#f44336}.qmx-modal-btn.danger:hover{background-color:#f443361a}.qmx-modal-content::-webkit-scrollbar,.qmx-settings-content::-webkit-scrollbar{width:10px}.qmx-modal-content::-webkit-scrollbar-track,.qmx-settings-content::-webkit-scrollbar-track{background:var(--md-sys-color-surface-bright);border-radius:10px}.qmx-modal-content::-webkit-scrollbar-thumb,.qmx-settings-content::-webkit-scrollbar-thumb{background-color:var(--md-sys-color-primary);border-radius:10px;border:2px solid var(--md-sys-color-surface-bright)}.qmx-modal-content::-webkit-scrollbar-thumb:hover,.qmx-settings-content::-webkit-scrollbar-thumb:hover{background-color:#e0d1ff}';
  importCSS(ControlPanelRefactoredCss);
  const statsPanelTemplate = `
    <div class="qmx-stats-container">
        <div class="qmx-stats-toggle" id="qmx-stats-toggle">
            <button id="qmx-stats-left" class="qmx-stats-switcher"><</button>
            <span class="qmx-stats-indicator">▼</span>
            <span class="qmx-stats-label">今日统计</span>
            <button class="qmx-stats-refresh">
                <svg xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 0 24 24" width="18px" fill="currentColor">
                    <path d="M0 0h24v24H0V0z" fill="none"/>
                    <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
                </svg>
            </button>
            <button id="qmx-stats-right" class="qmx-stats-switcher">></button>
        </div>
        <div class="qmx-stats-content" id="qmx-stats-content">
            <div class="qmx-modal-stats" id="qmx-stats-panel"></div>
        </div>
    </div>
`;
  const mainPanelTemplate = (maxTabs) => `
    <div class="qmx-modal-header">
        <span>控制中心</span>
        <button id="qmx-modal-close-btn" class="qmx-modal-close-icon" title="关闭"></button>
    </div>
    ${statsPanelTemplate}
    <div class="qmx-modal-content">
        <h3>监控面板 (<span id="qmx-active-tabs-count">0</span>/${maxTabs})</h3>
        <div id="qmx-tab-list"></div>
    </div>
    <div class="qmx-modal-footer">
        <button id="qmx-modal-settings-btn" class="qmx-modal-btn">设置</button>
        <button id="qmx-modal-close-all-btn" class="qmx-modal-btn danger">关闭所有</button>
        <button id="qmx-modal-open-btn" class="qmx-modal-btn primary">打开新房间</button>
    </div>
`;
  const createUnitInput = (id, label, settingsMeta) => {
    const meta = settingsMeta[id];
    return `
                <div class="qmx-settings-item">
                    <label for="${id}">
                        ${label}
                        <span class="qmx-tooltip-icon" data-tooltip-key="${id.replace("setting-", "")}">?</span>
                    </label>
                    <fieldset class="qmx-fieldset-unit">
                        <legend>${meta.unit}</legend>
                        <input type="number" class="qmx-input" id="${id}" value="${meta.value}">
                    </fieldset>
                </div>
            `;
  };
  const settingsPanelTemplate = (SETTINGS2) => {
    const settingsMeta = {
      "setting-initial-script-delay": { value: SETTINGS2.INITIAL_SCRIPT_DELAY / 1e3, unit: "秒" },
      "setting-auto-pause-delay": { value: SETTINGS2.AUTO_PAUSE_DELAY_AFTER_ACTION / 1e3, unit: "秒" },
      "setting-unresponsive-timeout": { value: SETTINGS2.UNRESPONSIVE_TIMEOUT / 6e4, unit: "分钟" },
      "setting-red-envelope-timeout": { value: SETTINGS2.RED_ENVELOPE_LOAD_TIMEOUT / 1e3, unit: "秒" },
      "setting-popup-wait-timeout": { value: SETTINGS2.POPUP_WAIT_TIMEOUT / 1e3, unit: "秒" },
      "setting-worker-loading-timeout": { value: SETTINGS2.ELEMENT_WAIT_TIMEOUT / 1e3, unit: "秒" },
      "setting-close-tab-delay": { value: SETTINGS2.CLOSE_TAB_DELAY / 1e3, unit: "秒" },
      "setting-api-retry-delay": { value: SETTINGS2.API_RETRY_DELAY / 1e3, unit: "秒" },
      "setting-switching-cleanup-timeout": { value: SETTINGS2.SWITCHING_CLEANUP_TIMEOUT / 1e3, unit: "秒" },
      "setting-healthcheck-interval": { value: SETTINGS2.HEALTHCHECK_INTERVAL / 1e3, unit: "秒" },
      "setting-disconnected-grace-period": { value: SETTINGS2.DISCONNECTED_GRACE_PERIOD / 1e3, unit: "秒" },
      "setting-stats-update-interval": { value: SETTINGS2.STATS_UPDATE_INTERVAL / 1e3, unit: "秒" }
    };
    return `
        <div class="qmx-settings-header">
            <div class="qmx-settings-tabs">
                <button class="tab-link active" data-tab="basic">基本设置</button>
                <button class="tab-link" data-tab="perf">性能与延迟</button>
                <button class="tab-link" data-tab="advanced">高级设置</button>
                ${""}
                <button class="tab-link" data-tab="about">关于</button>
                <!-- 主题模式切换开关 -->
                <div class="qmx-settings-item">
                    <div class="theme-switch-wrapper">
                        <label class="theme-switch">
                            <input type="checkbox" id="setting-theme-mode" ${SETTINGS2.THEME === "dark" ? "checked" : ""}>

                            <!-- 1. 背景轨道:只负责展开和收缩的动画 -->
                            <span class="slider-track"></span>

                            <!-- 2. 滑块圆点:只负责左右移动和图标切换 -->
                            <span class="slider-dot">
                                <span class="icon sun">
                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
                                        <circle cx="12" cy="12" r="5"></circle>
                                        <line x1="12" y1="1" x2="12" y2="3"></line>
                                        <line x1="12" y1="21" x2="12" y2="23"></line>
                                        <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
                                        <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
                                        <line x1="1" y1="12" x2="3" y2="12"></line>
                                        <line x1="21" y1="12" x2="23" y2="12"></line>
                                        <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
                                        <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
                                    </svg>
                                </span>
                                <span class="icon moon">
                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-3.51 1.713-6.636 4.398-8.552a.75.75 0 01.818.162z" clip-rule="evenodd"></path></svg>
                                </span>
                            </span>
                        </label>
                    </div>
                </div>
            </div>
        </div>
        <div class="qmx-settings-content">
            <!-- ==================== Tab 1: 基本设置 ==================== -->
            <div id="tab-basic" class="tab-content active">
                <div class="qmx-settings-grid">
                    <div class="qmx-settings-item">
                        <label for="setting-control-room-id">控制室房间号 <span class="qmx-tooltip-icon" data-tooltip-key="control-room">?</span></label>
                        <input type="number" class="qmx-input" id="setting-control-room-id" value="${SETTINGS2.CONTROL_ROOM_ID}">
                    </div>
                    <!-- 新增:第二房间号设置 -->
                    <div class="qmx-settings-item">
                        <label for="setting-temp-control-room-id">第二房间号(RID) <span class="qmx-tooltip-icon" data-tooltip-key="temp-control-room">?</span></label>
                        <input type="number" class="qmx-input" id="setting-temp-control-room-id" value="${SETTINGS2.TEMP_CONTROL_ROOM_RID}">
                    </div>
                    <div class="qmx-settings-item">
                        <label>自动暂停后台视频 <span class="qmx-tooltip-icon" data-tooltip-key="auto-pause">?</span></label>
                        <label class="qmx-toggle">
                            <input type="checkbox" id="setting-auto-pause" ${SETTINGS2.AUTO_PAUSE_ENABLED ? "checked" : ""}>
                            <span class="slider"></span>
                        </label>
                    </div>
                    <div class="qmx-settings-item">
                        <label>展示数据统计 <span class="qmx-tooltip-icon" data-tooltip-key="stats-info">?</span></label>
                        <label class="qmx-toggle">
                            <input type="checkbox" id="setting-stats-info" ${SETTINGS2.SHOW_STATS_IN_PANEL ? "checked" : ""}>
                            <span class="slider"></span>
                        </label>
                    </div>
                    <div class="qmx-settings-item">
                        <label>启用校准模式 <span class="qmx-tooltip-icon" data-tooltip-key="calibration-mode">?</span></label>
                        <label class="qmx-toggle">
                            <input type="checkbox" id="setting-calibration-mode" ${SETTINGS2.CALIBRATION_MODE_ENABLED ? "checked" : ""}>
                            <span class="slider"></span>
                        </label>
                    </div>
                    ${""}
                    <div class="qmx-settings-item">
                        <label>达到上限后的行为</label>
                        <div class="qmx-select" data-target-id="setting-daily-limit-action">
                            <div class="qmx-select-styled"></div>
                            <div class="qmx-select-options"></div>
                            <select id="setting-daily-limit-action" style="display: none;">
                                <option value="STOP_ALL" ${SETTINGS2.DAILY_LIMIT_ACTION === "STOP_ALL" ? "selected" : ""}>直接关停所有任务</option>
                                <option value="CONTINUE_DORMANT" ${SETTINGS2.DAILY_LIMIT_ACTION === "CONTINUE_DORMANT" ? "selected" : ""}>进入休眠模式,等待刷新</option>
                            </select>
                        </div>
                    </div>
                    <div class="qmx-settings-item">
                        <label>控制中心显示模式</label>
                        <div class="qmx-select" data-target-id="setting-modal-mode">
                            <div class="qmx-select-styled"></div>
                            <div class="qmx-select-options"></div>
                            <select id="setting-modal-mode" style="display: none;">
                                <option value="floating" ${SETTINGS2.MODAL_DISPLAY_MODE === "floating" ? "selected" : ""}>浮动窗口</option>
                                <option value="centered" ${SETTINGS2.MODAL_DISPLAY_MODE === "centered" ? "selected" : ""}>屏幕居中</option>
                                <option value="inject-rank-list" ${SETTINGS2.MODAL_DISPLAY_MODE === "inject-rank-list" ? "selected" : ""}>替换排行榜显示</option>
                            </select>
                        </div>
                    </div>
                </div>
            </div>

            <!-- ==================== Tab 2: 性能与延迟 ==================== -->
            <div id="tab-perf" class="tab-content">
                <div class="qmx-settings-grid">
                    ${createUnitInput("setting-initial-script-delay", "脚本初始启动延迟", settingsMeta)}
                    ${createUnitInput("setting-auto-pause-delay", "领取后暂停延迟", settingsMeta)}
                    ${createUnitInput("setting-unresponsive-timeout", "工作页失联超时", settingsMeta)}
                    ${createUnitInput("setting-red-envelope-timeout", "红包活动加载超时", settingsMeta)}
                    ${createUnitInput("setting-popup-wait-timeout", "红包弹窗等待超时", settingsMeta)}
                    ${createUnitInput("setting-worker-loading-timeout", "播放器加载超时", settingsMeta)}
                    ${createUnitInput("setting-close-tab-delay", "关闭标签页延迟", settingsMeta)}
                    ${createUnitInput("setting-switching-cleanup-timeout", "切换中状态兜底超时", settingsMeta)}
                    ${createUnitInput("setting-healthcheck-interval", "哨兵健康检查间隔", settingsMeta)}
                    ${createUnitInput("setting-disconnected-grace-period", "断开连接清理延迟", settingsMeta)}
                    ${createUnitInput("setting-api-retry-delay", "API重试延迟", settingsMeta)}
                    ${createUnitInput("setting-stats-update-interval", "统计信息更新间隔", settingsMeta)}
                    
                    <div class="qmx-settings-item" style="grid-column: 1 / -1;">
                        <label>模拟操作延迟范围 (秒) <span class="qmx-tooltip-icon" data-tooltip-key="range-delay">?</span></label>
                        <div class="qmx-range-slider-wrapper">
                            <div class="qmx-range-slider-container">
                                <div class="qmx-range-slider-track-container"><div class="qmx-range-slider-progress"></div></div>
                                <input type="range" id="setting-min-delay" min="0.1" max="5" step="0.1" value="${SETTINGS2.MIN_DELAY / 1e3}">
                                <input type="range" id="setting-max-delay" min="0.1" max="5" step="0.1" value="${SETTINGS2.MAX_DELAY / 1e3}">
                            </div>
                            <div class="qmx-range-slider-values"></div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- ==================== Tab 3: 高级设置 ==================== -->
            <div id="tab-advanced" class="tab-content">
                <div class="qmx-settings-grid">
                    <div class="qmx-settings-item">
                        <label for="setting-max-tabs">最大工作标签页数量 <span class="qmx-tooltip-icon" data-tooltip-key="max-worker-tabs">?</span></label>
                        <input type="number" class="qmx-input" id="setting-max-tabs" value="${SETTINGS2.MAX_WORKER_TABS}">
                    </div>
                    <div class="qmx-settings-item">
                        <label for="setting-api-fetch-count">单次API获取房间数 <span class="qmx-tooltip-icon" data-tooltip-key="api-room-fetch-count">?</span></label>
                        <input type="number" class="qmx-input" id="setting-api-fetch-count" value="${SETTINGS2.API_ROOM_FETCH_COUNT}">
                    </div>
                    <div class="qmx-settings-item">
                        <label for="setting-api-retry-count">API请求重试次数 <span class="qmx-tooltip-icon" data-tooltip-key="api-retry-count">?</span></label>
                        <input type="number" class="qmx-input" id="setting-api-retry-count" value="${SETTINGS2.API_RETRY_COUNT}">
                    </div>

                    

                    <!-- 新增:添加两个空的占位符,使网格平衡为 2x3 -->
                    <div class="qmx-settings-item"></div>
                    <div class="qmx-settings-item"></div>
                </div>
            </div>

            <!-- ==================== Tab 4: 弹幕助手 ==================== -->
            ${""}
            <!-- ==================== Tab 5: 关于 ==================== -->
            <div id="tab-about" class="tab-content">
                <!-- 调试工具 - 仅在开发时启用
                <h4>调试工具 <span style="color: #ff6b6b;">⚠️ 仅供测试使用</span></h4>
                <div class="qmx-settings-grid">
                    <div class="qmx-settings-item">
                        <label>模拟达到每日上限</label>
                        <button id="test-daily-limit-btn" class="qmx-modal-btn" style="background-color: #ff6b6b; color: white;">
                            设置为已达上限
                        </button>
                        <small style="color: #888; display: block; margin-top: 5px;">
                            点击后将模拟达到每日红包上限,触发休眠模式(如果启用)
                        </small>
                    </div>
                    <div class="qmx-settings-item">
                        <label>重置每日上限状态</label>
                        <button id="reset-daily-limit-btn" class="qmx-modal-btn">
                            重置上限状态
                        </button>
                        <small style="color: #888; display: block; margin-top: 5px;">
                            清除上限标记,恢复正常运行模式
                        </small>
                    </div>
                </div>
                -->
                
                <h4>关于脚本 <span class="version-tag">v2.0.9</span></h4>
                <h4>致谢</h4>
                <ul class="qmx-styled-list">
                    <li>本脚本基于<a href="https://greasyfork.org/zh-CN/users/1453821-ysl-ovo" target="_blank" rel="noopener noreferrer">ysl-ovo</a>的插件<a href="https://greasyfork.org/zh-CN/scripts/532514-%E6%96%97%E9%B1%BC%E5%85%A8%E6%B0%91%E6%98%9F%E6%8E%A8%E8%8D%90%E8%87%AA%E5%8A%A8%E9%A2%86%E5%8F%96" target="_blank" rel="noopener noreferrer">《斗鱼全民星推荐自动领取》</a>
                        进行一些功能改进(也许)与界面美化,同样遵循MIT许可证开源。感谢原作者的分享</li>
                    <li>兼容斗鱼新版UI的相关功能与项目重构主要由<a href="https://github.com/Truthss" target="_blank" rel="noopener noreferrer">@Truthss</a> 贡献,非常感谢!</li>
                </ul>
                <h4>⚠️ 重要提示</h4>
                <ul class="qmx-styled-list">
                    <li><strong>斗鱼Ex插件冲突</strong>:如需使用本脚本抢红包,请暂时关闭斗鱼Ex插件,否则红包会消失。</li>
                    <li><strong>浏览器DNS设置</strong>:请在浏览器设置中搜索"DNS",将"使用安全的DNS"选项关闭,否则红包也会消失。</li>
                    <li><strong>新版UI适配</strong>:控制面板"替换排行榜"模式暂不可用,请使用"浮动窗口"或"屏幕居中"模式。</li>
                    <li>启用统计功能需要把"油猴管理面板->设置->安全->允许脚本访问 Cookie"改为ALL!!</li>
                    <li>每天大概1000左右金币到上限。</li>
                </ul>
                <h4>脚本更新日志 (v2.0.9)</h4>
                <ul class="qmx-styled-list">
                    <li>【新增】全新统计面板:支持查看今日及最近7天的红包数量与金币总数。</li>
                    <li>【新增】奖励显示:控制面板现在可以直接显示每个红包的具体奖励信息(金币/荧光棒)。</li>
                    <li>【优化】设置体验升级:修改设置选项后不再需要刷新页面即可生效。</li>
                    <li>【优化】界面与布局:调整了控制面板样式,位置随窗口大小自动调整。</li>
                    <li>【修复】适配斗鱼新版直播间界面(部分功能受限)。</li>
                    <li>【修复】修复了重新打开控制页面时会残留已关闭的直播间信息的问题。</li>
                </ul>
                <h4>源码与社区</h4>
                <ul class="qmx-styled-list">
                    <li>可以在 <a href="https://github.com/ienone/douyu-qmx-pro/" target="_blank" rel="noopener noreferrer">GitHub</a> 查看本脚本源码</li>
                    <li>发现BUG或有功能建议,欢迎提交 <a href="https://github.com/ienone/douyu-qmx-pro/issues" target="_blank" rel="noopener noreferrer">Issue</a>(不过大概率不会修……)</li>
                    <li>如果你有能力进行改进,非常欢迎提交 <a href="https://github.com/ienone/douyu-qmx-pro/pulls" target="_blank" rel="noopener noreferrer">Pull Request</a>!</li>
                </ul>
            </div>
        </div>
        <div class="qmx-settings-footer">
            <button id="qmx-settings-cancel-btn" class="qmx-modal-btn">取消</button>
            <button id="qmx-settings-reset-btn" class="qmx-modal-btn danger">恢复默认</button>
            <button id="qmx-settings-save-btn" class="qmx-modal-btn primary">保存</button>
        </div>
        `;
  };
  const ThemeManager = {
applyTheme(theme) {
      document.body.setAttribute("data-theme", theme);
      SETTINGS.THEME = theme;
      GM_setValue("douyu_qmx_theme", theme);
    }
  };
  const GlobalState = {
get() {
      let state = GM_getValue(SETTINGS.STATE_STORAGE_KEY, { tabs: {} });
      if (!state || typeof state !== "object") {
        state = { tabs: {} };
      }
      return state;
    },
set(state) {
      const lockKey = "douyu_qmx_state_lock";
      if (!Utils.lockChecker(lockKey, () => this.set(), state)) {
        return;
      }
      Utils.setLocalValueWithLock(lockKey, SETTINGS.STATE_STORAGE_KEY, state, "更新全局状态");
    },
updateWorker(roomId, status, statusText, options = {}) {
      if (!roomId) return;
      const state = this.get();
      const oldTabData = state.tabs[roomId] || {};
      if (status === "DISCONNECTED" && oldTabData.status === "SWITCHING") {
        Utils.log(`[状态管理] 检测到正在切换的标签页已断开连接,判定为成功关闭,立即清理。`);
        this.removeWorker(roomId);
        return;
      }
      if (Object.keys(state.tabs).length === 0 && status === "SWITCHING") {
        Utils.log(`[状态管理] 检测到全局状态已清空,忽略残留的SWITCHING状态更新 (房间: ${roomId})`);
        return;
      }
      const updates = {
        status,
        statusText,
        lastUpdateTime: Date.now(),
        ...options
      };
      const newTabData = { ...oldTabData, ...updates };
      for (const key in newTabData) {
        if (newTabData[key] === null) {
          delete newTabData[key];
        }
      }
      state.tabs[roomId] = newTabData;
      this.set(state);
    },
removeWorker(roomId) {
      if (!roomId) return;
      const state = this.get();
      delete state.tabs[roomId];
      this.set(state);
    },
setDailyLimit(reached) {
      GM_setValue(SETTINGS.DAILY_LIMIT_REACHED_KEY, { reached, timestamp: Date.now() });
    },
getDailyLimit() {
      return GM_getValue(SETTINGS.DAILY_LIMIT_REACHED_KEY);
    }
  };
  var _GM_cookie = (() => typeof GM_cookie != "undefined" ? GM_cookie : void 0)();
  var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  const DouyuAPI = {
getRooms(count, rid, retries = SETTINGS.API_RETRY_COUNT) {
      return new Promise((resolve, reject) => {
        const attempt = (remainingTries) => {
          Utils.log(`开始调用 API 获取房间列表... (剩余重试次数: ${remainingTries})`);
          _GM_xmlhttpRequest({
            method: "GET",
            url: `${SETTINGS.API_URL}?rid=${rid}`,
            headers: {
              Referer: "https://www.douyu.com/",
              "User-Agent": navigator.userAgent
            },
            responseType: "json",
            timeout: 1e4,
            onload: (response) => {
              if (response.status === 200 && response.response?.error === 0 && Array.isArray(response.response.data?.redBagList)) {
                const rooms = response.response.data.redBagList.map((item) => item.rid).filter(Boolean).slice(0, count * 2).map((rid2) => `https://www.douyu.com/${rid2}`);
                Utils.log(`API 成功返回 ${rooms.length} 个房间URL。`);
                resolve(rooms);
              } else {
                const errorMsg = `API 数据格式错误或失败: ${response.response?.msg || "未知错误"}`;
                Utils.log(errorMsg);
                if (remainingTries > 0) retry(remainingTries - 1, errorMsg);
                else reject(new Error(errorMsg));
              }
            },
            onerror: (error) => {
              const errorMsg = `API 请求网络错误: ${error.statusText || "未知"}`;
              Utils.log(errorMsg);
              if (remainingTries > 0) retry(remainingTries - 1, errorMsg);
              else reject(new Error(errorMsg));
            },
            ontimeout: () => {
              const errorMsg = "API 请求超时";
              Utils.log(errorMsg);
              if (remainingTries > 0) retry(remainingTries - 1, errorMsg);
              else reject(new Error(errorMsg));
            }
          });
        };
        const retry = (remainingTries, reason) => {
          Utils.log(`${reason},将在 ${SETTINGS.API_RETRY_DELAY / 1e3} 秒后重试...`);
          setTimeout(() => attempt(remainingTries), SETTINGS.API_RETRY_DELAY);
        };
        attempt(retries);
      });
    },
getCookie: function(cookieName) {
      return new Promise((resolve, reject) => {
        _GM_cookie.list({ name: cookieName }, function(cookies, error) {
          if (error) {
            Utils.log(error);
            reject(error);
          } else if (cookies && cookies.length > 0) {
            resolve(cookies[0]);
          } else {
            resolve(null);
          }
        });
      });
    },
getCoinRecord: function(current, count, rid, retries = SETTINGS.API_RETRY_COUNT) {
      return new Promise((resolve, reject) => {
        this.getCookie("acf_auth").then((acfCookie) => {
          if (!acfCookie) {
            Utils.log("获取cookie错误");
            reject(new Error("获取cookie错误"));
            return;
          }
          const fullUrl = `${SETTINGS.COIN_LIST_URL}?current=${current}&pageSize=${count}&rid=${rid}`;
          const attempt = (remainingTries) => {
            Utils.log(
              `开始调用 API 获取金币历史列表... (剩余重试次数: ${remainingTries})`
            );
            _GM_xmlhttpRequest({
              method: "GET",
              url: fullUrl,
              headers: {
                Referer: "https://www.douyu.com/",
                "User-Agent": navigator.userAgent
              },
              cookie: acfCookie["value"],
              responseType: "json",
              timeout: 1e4,
              onload: (response) => {
                if (response.status === 200 && response.response?.error === 0 && Array.isArray(response.response.data.list)) {
                  const coinListData = response.response.data.list.filter(
                    (item) => item.opDirection === 1 && item.remark.includes("红包")
                  );
                  Utils.log(`API 成功返回 ${coinListData.length} 个红包记录。`);
                  resolve(coinListData);
                } else {
                  const errorMsg = `API 数据格式错误或失败: ${response.response?.msg || "未知错误"}`;
                  Utils.log(errorMsg);
                  if (remainingTries > 0) retry(remainingTries - 1, errorMsg);
                  else reject(new Error(errorMsg));
                }
              },
              onerror: (error) => {
                const errorMsg = `API 请求网络错误: ${error.statusText || "未知"}`;
                Utils.log(errorMsg);
                if (remainingTries > 0) retry(remainingTries - 1, errorMsg);
                else reject(new Error(errorMsg));
              },
              ontimeout: () => {
                const errorMsg = "API 请求超时";
                Utils.log(errorMsg);
                if (remainingTries > 0) retry(remainingTries - 1, errorMsg);
                else reject(new Error(errorMsg));
              }
            });
          };
          const retry = (remainingTries, reason) => {
            Utils.log(`${reason},将在 ${SETTINGS.API_RETRY_DELAY / 1e3} 秒后重试...`);
            setTimeout(() => attempt(remainingTries), SETTINGS.API_RETRY_DELAY);
          };
          attempt(retries);
        }).catch((error) => {
          Utils.log(error);
          reject(error);
        });
      });
    }
  };
  let isGlobalClickListenerAdded = false;
  function activateCustomSelects(parentElement) {
    parentElement.querySelectorAll(".qmx-select").forEach((wrapper) => {
      const nativeSelect = wrapper.querySelector("select");
      const styledSelect = wrapper.querySelector(".qmx-select-styled");
      const optionsList = wrapper.querySelector(".qmx-select-options");
      styledSelect.textContent = nativeSelect.options[nativeSelect.selectedIndex].text;
      optionsList.innerHTML = "";
      for (const option of nativeSelect.options) {
        const optionDiv = document.createElement("div");
        optionDiv.textContent = option.text;
        optionDiv.dataset.value = option.value;
        if (option.selected) {
          optionDiv.classList.add("selected");
        }
        optionsList.appendChild(optionDiv);
      }
      styledSelect.addEventListener("click", (e) => {
        e.stopPropagation();
        document.querySelectorAll(".qmx-select.active").forEach((el) => {
          if (el !== wrapper) {
            el.classList.remove("active");
          }
        });
        wrapper.classList.toggle("active");
      });
      optionsList.querySelectorAll("div").forEach((optionDiv) => {
        optionDiv.addEventListener("click", () => {
          styledSelect.textContent = optionDiv.textContent;
          nativeSelect.value = optionDiv.dataset.value;
          optionsList.querySelector(".selected")?.classList.remove("selected");
          optionDiv.classList.add("selected");
          wrapper.classList.remove("active");
        });
      });
    });
    if (!isGlobalClickListenerAdded) {
      document.addEventListener("click", () => {
        document.querySelectorAll(".qmx-select.active").forEach((el) => {
          el.classList.remove("active");
        });
      });
      isGlobalClickListenerAdded = true;
    }
  }
  function activateRangeSlider(parentElement) {
    const wrapper = parentElement.querySelector(".qmx-range-slider-wrapper");
    if (!wrapper) {
      return;
    }
    const minSlider = wrapper.querySelector("#setting-min-delay");
    const maxSlider = wrapper.querySelector("#setting-max-delay");
    const sliderValues = wrapper.querySelector(".qmx-range-slider-values");
    const progress = wrapper.querySelector(".qmx-range-slider-progress");
    if (!minSlider || !maxSlider || !sliderValues || !progress) {
      console.error("范围滑块组件缺少必要的子元素 (min/max slider, values, progress)。");
      return;
    }
    function updateSliderView() {
      if (parseFloat(minSlider.value) > parseFloat(maxSlider.value)) {
        maxSlider.value = minSlider.value;
      }
      sliderValues.textContent = `${minSlider.value} s - ${maxSlider.value} s`;
      const minPercent = (minSlider.value - minSlider.min) / (minSlider.max - minSlider.min) * 100;
      const maxPercent = (maxSlider.value - maxSlider.min) / (maxSlider.max - minSlider.min) * 100;
      progress.style.left = `${minPercent}%`;
      progress.style.width = `${maxPercent - minPercent}%`;
    }
    minSlider.addEventListener("input", updateSliderView);
    maxSlider.addEventListener("input", updateSliderView);
    updateSliderView();
  }
  let tooltipElement = null;
  function _ensureTooltipElement() {
    if (!tooltipElement) {
      tooltipElement = document.createElement("div");
      tooltipElement.id = "qmx-global-tooltip";
      document.body.appendChild(tooltipElement);
    }
  }
  function activateToolTips(parentElement, tooltipData) {
    if (!parentElement || typeof tooltipData !== "object") {
      console.warn("[Tooltip] 调用失败:必须提供 parentElement 和 tooltipData。");
      return;
    }
    _ensureTooltipElement();
    parentElement.addEventListener("mouseover", (e) => {
      const trigger = e.target.closest(".qmx-tooltip-icon");
      if (!trigger) return;
      const key = trigger.dataset.tooltipKey;
      const text = tooltipData[key];
      if (text) {
        tooltipElement.textContent = text;
        const triggerRect = trigger.getBoundingClientRect();
        const left = triggerRect.left + triggerRect.width / 2;
        const top = triggerRect.top;
        tooltipElement.style.left = `${left}px`;
        tooltipElement.style.top = `${top}px`;
        tooltipElement.style.transform = `translate(-50%, calc(-100% - 8px))`;
        tooltipElement.classList.add("visible");
      }
    });
    parentElement.addEventListener("mouseout", (e) => {
      const trigger = e.target.closest(".qmx-tooltip-icon");
      if (trigger) {
        tooltipElement.classList.remove("visible");
      }
    });
  }
  const SettingsPanel = {

RELOAD_REQUIRED_KEYS: [
],
show() {
      const modal = document.getElementById("qmx-settings-modal");
      const allTooltips = {
        "control-room": "只有在此房间号的直播间中才能看到插件面板,看准了再改!(修改后不会立即刷新,下次进入该房间生效)",
        "temp-control-room": "备用的控制室房间号(真实RID),用于兼容特殊活动页或Topic页面。",
        "auto-pause": "自动暂停非控制直播间的视频播放,大幅降低资源占用。",
        "initial-script-delay": "页面加载后等待多久再运行脚本,可适当增加以确保页面完全加载。",
        "auto-pause-delay": "领取红包后等待多久再次尝试暂停视频。",
        "unresponsive-timeout": '工作页多久未汇报任何状态后,在面板上标记为"无响应"。',
        "red-envelope-timeout": "进入直播间后,最长等待多久来寻找红包活动,超时后将切换房间。(默认15秒)",
        "popup-wait-timeout": "点击红包后,等待领取弹窗出现的最长时间。注意:系统会在发现新红包时提前提取奖励信息。",
        "worker-loading-timeout": "新开的直播间卡在加载状态多久还没显示播放组件,被判定为加载失败或缓慢。",
        "range-delay": "脚本在每次点击等操作前后随机等待的时间范围,模拟真人行为。",
        "close-tab-delay": "旧页面在打开新页面后,等待多久再关闭自己,确保新页面已接管。",
        "switching-cleanup-timeout": "处于“切换中”状态的标签页,超过此时间后将被强行清理,避免残留。",
        "max-worker-tabs": "同时运行的直播间数量上限。",
        "api-room-fetch-count": "每次从API获取的房间数。增加可提高找到新房间的几率。",
        "api-retry-count": "获取房间列表失败时的重试次数。",
        "api-retry-delay": "API请求失败后,等待多久再重试。",
        "healthcheck-interval": "哨兵检查后台UI的频率。值越小,UI节流越快,但会增加资源占用。",
        "disconnected-grace-period": "刷新或关闭的标签页,在被彻底清理前等待重连的宽限时间。",
        "calibration-mode": "启用校准模式可提高倒计时精准度。注意:启用此项前请先关闭DouyuEx的 阻止P2P上传 功能",
        "stats-info": '此功能需要把"油猴管理面板->设置->安全->允许脚本访问 Cookie"改为ALL!! 在控制面板中显示统计信息标签页,记录每日领取的红包数量和金币总额。',
        "stats-update-interval": "统计信息面板中数据更新的频率,值越小更新越及时,但会增加API使用次数。",
        "danmupro-mode": "启用斗鱼弹幕助手功能,可以在弹幕输入框中使用自动弹幕推荐等功能。"
      };
      modal.innerHTML = settingsPanelTemplate(SETTINGS);
      activateToolTips(modal, allTooltips);
      activateCustomSelects(modal);
      activateRangeSlider(modal);
      this.bindPanelEvents(modal);
      document.getElementById("qmx-modal-backdrop").classList.add("visible");
      modal.classList.add("visible");
      document.body.classList.add("qmx-modal-open-scroll-lock");
      this.updateSaveButtonState();
    },
hide() {
      const modal = document.getElementById("qmx-settings-modal");
      modal.classList.remove("visible");
      document.body.classList.remove("qmx-modal-open-scroll-lock");
      if (SETTINGS.MODAL_DISPLAY_MODE !== "centered" || !document.getElementById("qmx-modal-container").classList.contains("visible")) {
        document.getElementById("qmx-modal-backdrop").classList.remove("visible");
      }
    },
getSettingsFromUI() {
      return {
CONTROL_ROOM_ID: document.getElementById("setting-control-room-id").value,
        TEMP_CONTROL_ROOM_RID: document.getElementById("setting-temp-control-room-id").value,
        AUTO_PAUSE_ENABLED: document.getElementById("setting-auto-pause").checked,
...{},
        DAILY_LIMIT_ACTION: document.getElementById("setting-daily-limit-action").value,
        MODAL_DISPLAY_MODE: document.getElementById("setting-modal-mode").value,
        SHOW_STATS_IN_PANEL: document.getElementById("setting-stats-info").checked,
        THEME: document.getElementById("setting-theme-mode").checked ? "dark" : "light",

INITIAL_SCRIPT_DELAY: parseFloat(document.getElementById("setting-initial-script-delay").value) * 1e3,
        AUTO_PAUSE_DELAY_AFTER_ACTION: parseFloat(document.getElementById("setting-auto-pause-delay").value) * 1e3,
        SWITCHING_CLEANUP_TIMEOUT: parseFloat(document.getElementById("setting-switching-cleanup-timeout").value) * 1e3,
        UNRESPONSIVE_TIMEOUT: parseInt(document.getElementById("setting-unresponsive-timeout").value, 10) * 6e4,
        RED_ENVELOPE_LOAD_TIMEOUT: parseFloat(document.getElementById("setting-red-envelope-timeout").value) * 1e3,
        POPUP_WAIT_TIMEOUT: parseFloat(document.getElementById("setting-popup-wait-timeout").value) * 1e3,
        CALIBRATION_MODE_ENABLED: document.getElementById("setting-calibration-mode").checked,
        ELEMENT_WAIT_TIMEOUT: parseFloat(document.getElementById("setting-worker-loading-timeout").value) * 1e3,
        MIN_DELAY: parseFloat(document.getElementById("setting-min-delay").value) * 1e3,
        MAX_DELAY: parseFloat(document.getElementById("setting-max-delay").value) * 1e3,
        CLOSE_TAB_DELAY: parseFloat(document.getElementById("setting-close-tab-delay").value) * 1e3,
        HEALTHCHECK_INTERVAL: parseFloat(document.getElementById("setting-healthcheck-interval").value) * 1e3,
        DISCONNECTED_GRACE_PERIOD: parseFloat(document.getElementById("setting-disconnected-grace-period").value) * 1e3,
        STATS_UPDATE_INTERVAL: parseFloat(document.getElementById("setting-stats-update-interval").value) * 1e3,
MAX_WORKER_TABS: parseInt(document.getElementById("setting-max-tabs").value, 10),
        API_ROOM_FETCH_COUNT: parseInt(document.getElementById("setting-api-fetch-count").value, 10),
        API_RETRY_COUNT: parseInt(document.getElementById("setting-api-retry-count").value, 10),
        API_RETRY_DELAY: parseFloat(document.getElementById("setting-api-retry-delay").value) * 1e3
      };
    },
updateSaveButtonState() {
      const newSettings = this.getSettingsFromUI();
      let needReload = false;
      for (const key of Object.keys(newSettings)) {
        if (SETTINGS[key] !== newSettings[key]) {
          if (this.RELOAD_REQUIRED_KEYS.includes(key)) {
            needReload = true;
            break;
          }
        }
      }
      const saveBtn = document.getElementById("qmx-settings-save-btn");
      if (saveBtn) {
        if (saveBtn.textContent.includes("已保存")) return { newSettings, needReload };
        if (needReload) {
          saveBtn.textContent = "保存并刷新";
        } else {
          saveBtn.textContent = "保存";
        }
      }
      return { newSettings, needReload };
    },
save() {
      const { newSettings, needReload } = this.updateSaveButtonState();
      const existingUserSettings = GM_getValue(SettingsManager.STORAGE_KEY, {});
      const finalSettingsToSave = Object.assign(existingUserSettings, newSettings);
      delete finalSettingsToSave.OPEN_TAB_DELAY;
      SettingsManager.save(finalSettingsToSave);
      if (needReload) {
        window.location.reload();
      } else {
        SettingsManager.update(newSettings);
        const saveBtn = document.getElementById("qmx-settings-save-btn");
        if (saveBtn) {
          const originalText = saveBtn.textContent;
          saveBtn.textContent = "已保存~";
          saveBtn.style.backgroundColor = "var(--status-color-success, #4CAF50)";
          setTimeout(() => {
            saveBtn.textContent = originalText;
            saveBtn.style.backgroundColor = "";
            this.hide();
            this.updateSaveButtonState();
          }, 600);
        } else {
          this.hide();
        }
      }
    },
bindPanelEvents(modal) {
      modal.querySelector("#qmx-settings-cancel-btn").onclick = () => this.hide();
      modal.querySelector("#qmx-settings-save-btn").onclick = () => this.save();
      modal.querySelector("#qmx-settings-reset-btn").onclick = () => {
        if (confirm("确定要恢复所有默认设置吗?此操作会刷新页面。")) {
          SettingsManager.reset();
          window.location.reload();
        }
      };
      const inputs = modal.querySelectorAll("input, select");
      inputs.forEach((input) => {
        input.addEventListener("change", () => this.updateSaveButtonState());
        input.addEventListener("input", () => this.updateSaveButtonState());
      });
      const customOptions = modal.querySelectorAll(".qmx-select-options div");
      customOptions.forEach((opt) => {
        opt.addEventListener("click", () => {
          setTimeout(() => this.updateSaveButtonState(), 10);
        });
      });
      modal.querySelectorAll(".tab-link").forEach((button) => {
        button.onclick = (e) => {
          const tabId = e.target.dataset.tab;
          modal.querySelector(".tab-link.active")?.classList.remove("active");
          modal.querySelector(".tab-content.active")?.classList.remove("active");
          e.target.classList.add("active");
          modal.querySelector(`#tab-${tabId}`).classList.add("active");
        };
      });
      const themeToggle = modal.querySelector("#setting-theme-mode");
      if (themeToggle) {
        themeToggle.addEventListener("change", (e) => {
          const newTheme = e.target.checked ? "dark" : "light";
          ThemeManager.applyTheme(newTheme);
          this.updateSaveButtonState();
        });
      }
    }

};
  const FirstTimeNotice = {
showCalibrationNotice() {
      const NOTICE_SHOWN_KEY = "douyu_qmx_calibration_notice_shown";
      const hasShownNotice = GM_getValue(NOTICE_SHOWN_KEY, false);
      if (!hasShownNotice) {
        const noticeHTML = `
                <div class="qmx-modal-header">
                    <h3>⚠️ 重要更新提示</h3>
                    <button id="qmx-notice-close-btn" class="qmx-modal-close-icon" title="关闭"></button>
                </div>
                <div class="qmx-modal-content">
                    <h4 style="color: var(--accent-color, #ff6b6b); margin-top: 0;">斗鱼网页UI更新说明</h4>
                    <p>斗鱼已更新直播间界面,脚本正在适配中。目前基本功能可用,但请注意:</p>
                    <ul style="margin: 10px 0; padding-left: 20px;">
                        <li><strong>控制面板"替换排行榜"模式暂不可用</strong>,请使用"浮动窗口"或"屏幕居中"模式</li>
                        <li><strong>刚开始打开的几个工作标签页</strong>可能需要手动切换激活一下才能正常加载</li>
                        <li><strong>弹幕助手功能</strong>正在适配中,暂时可能无法使用</li>
                    </ul>
                    
                    <h4 style="color: var(--accent-color, #ff6b6b);">⚠️使用前必读</h4>
                    <ul style="margin: 10px 0; padding-left: 20px;">
                        <li><strong>斗鱼Ex插件冲突</strong>:如需使用本脚本抢红包,请暂时关闭斗鱼Ex插件,否则红包会消失。后续会尝试沟通解决此问题</li>
                        <li><strong>浏览器DNS设置</strong>:请在浏览器设置中搜索"DNS",将"使用安全的DNS"选项关闭,否则红包也会消失</li>
                    </ul>
                    
                    <h4 style="color: var(--status-color-success, #4CAF50);">✨ 新增功能</h4>
                    <p>控制面板现在会显示每个红包的具体奖励信息🎁</p>
                    <p>启用统计功能需要把"油猴管理面板->设置->安全->允许脚本访问 Cookie"改为ALL!!</p>
                    
                    <h4 style="margin-bottom: 5px;">⭐️点点star吧~</h4>
                    <p style="margin-top: 5px;">项目地址:<a href="https://github.com/ienone/douyu-qmx-pro" target="_blank" rel="noopener noreferrer" style="color: var(--accent-color, #ff6b6b);">douyu-qmx-pro</a>,觉得好用请给个star🌟~~</p>
                </div>
                <div class="qmx-modal-footer">
                    <button id="qmx-notice-settings-btn" class="qmx-modal-btn">前往设置</button>
                    <button id="qmx-notice-ok-btn" class="qmx-modal-btn primary">我知道了</button>
                </div>
            `;
        const noticeContainer = document.createElement("div");
        noticeContainer.id = "qmx-notice-modal";
        noticeContainer.className = "visible mode-centered";
        noticeContainer.innerHTML = noticeHTML;
        const backdrop = document.createElement("div");
        backdrop.id = "qmx-notice-backdrop";
        backdrop.className = "visible";
        document.body.appendChild(backdrop);
        document.body.appendChild(noticeContainer);
        const closeNotice = () => {
          noticeContainer.classList.remove("visible");
          backdrop.classList.remove("visible");
          setTimeout(() => {
            noticeContainer.remove();
            backdrop.remove();
          }, 300);
          GM_setValue(NOTICE_SHOWN_KEY, true);
        };
        document.getElementById("qmx-notice-close-btn").onclick = closeNotice;
        document.getElementById("qmx-notice-ok-btn").onclick = closeNotice;
        document.getElementById("qmx-notice-settings-btn").onclick = () => {
          closeNotice();
          SettingsPanel.show();
        };
      }
    }
  };
  const typedSettings = SETTINGS;
  const globalValue = {
    currentDatePage: Utils.formatDateAsBeijing( new Date()),
updateIntervalID: void 0,
    statElements: new Map()
  };
  const StatsInfo = {
    init: async function() {
      let stats;
      try {
        stats = await Utils.getElementWithRetry(".qmx-stats-content");
      } catch (error) {
        Utils.log(`[数据统计] 初始化失败,错误: ${error}`);
        return;
      }
      const statsConfigs = [
        ["receivedCount", "已领个数"],
        ["total", "总金币"],
        ["avg", "平均每个"]
      ];
      for (const [name, nickname] of statsConfigs) {
        const element = this.initRender(name, nickname);
        stats.appendChild(element);
        try {
          const details = await Utils.getElementWithRetry(".qmx-stat-details", element);
          if (details) {
            globalValue.statElements.set(
              name,
              details
            );
          }
        } catch (error) {
          Utils.log(`[数据统计] 缓存元素获取失败: ${error}`);
        }
      }
      GM_setValue("douyu_qmx_stats_lock", false);
      this.ensureTodayDataExists();
      this.updateTodayData();
      await this.getCoinListUpdate();
      this.removeExpiredData();
      this.bindEvents();
      this.updateInterval();
      setInterval(() => {
        this.updateDataForDailyReset();
      }, 60 * 1e3);
    },
updateInterval: function() {
      if (globalValue.updateIntervalID) {
        clearInterval(globalValue.updateIntervalID);
        globalValue.updateIntervalID = void 0;
      }
      globalValue.updateIntervalID = setInterval(() => {
        this.checkUpdate();
      }, typedSettings.STATS_UPDATE_INTERVAL);
    },
destroy: function() {
      if (globalValue.updateIntervalID) {
        clearInterval(globalValue.updateIntervalID);
        globalValue.updateIntervalID = void 0;
      }
      globalValue.statElements.clear();
    },
ensureTodayDataExists: function() {
      const today = Utils.formatDateAsBeijing( new Date());
      let allData = GM_getValue(typedSettings.STATS_INFO_STORAGE_KEY, null);
      if (!allData || typeof allData !== "object") {
        allData = {};
      }
      if (!allData[today]) {
        allData[today] = {
          receivedCount: 0,
          avg: 0,
          total: 0
        };
        GM_setValue(typedSettings.STATS_INFO_STORAGE_KEY, allData);
      }
      return { allData, todayData: allData[today], today };
    },
bindEvents: function() {
      try {
        this.bindRefreshEvent();
        this.bindSwitcherLeft();
        this.bindSwitcherRight();
      } catch (e) {
        Utils.log(`[数据统计] 绑定事件异常: ${e}`);
        setTimeout(() => {
          this.bindEvents();
        }, 500);
      }
    },
bindRefreshEvent: function() {
      const refreshButton = document.querySelector(".qmx-stats-refresh");
      const today = Utils.formatDateAsBeijing( new Date());
      if (!refreshButton) {
        throw new Error("无法找到刷新按钮元素");
      }
      if (globalValue.currentDatePage !== today) {
        refreshButton.classList.add("disabled");
        refreshButton.onclick = null;
        return;
      }
      setTimeout(() => {
        refreshButton.classList.remove("disabled");
      }, 300);
      refreshButton.onclick = async (e) => {
        e.stopPropagation();
        void refreshButton.offsetWidth;
        refreshButton.classList.add("rotating");
        setTimeout(() => {
          refreshButton.classList.remove("rotating");
        }, 1e3);
        await this.getCoinListUpdate();
      };
    },
bindSwitcher: function(direction) {
      const { allData, today } = this.ensureTodayDataExists();
      globalValue.currentDatePage = globalValue.currentDatePage ?? today;
      const dateList = Object.keys(allData);
      const currentIndex = dateList.indexOf(globalValue.currentDatePage);
      const statsLable = document.querySelector(".qmx-stats-label");
      const indecator = document.querySelector(".qmx-stats-indicator");
      const button = document.querySelector(`#qmx-stats-${direction}`);
      if (!statsLable || !indecator || !button) {
        Utils.log("[数据统计] 切换按钮绑定失败,正在重试");
        setTimeout(() => {
          this.bindSwitcher(direction);
        }, 500);
        return;
      }
      const shouldDisableButton = direction === "left" ? dateList.length <= 1 || currentIndex - 1 < 0 : dateList.length <= 1 || currentIndex + 1 >= dateList.length;
      if (shouldDisableButton) {
        button.classList.add("disabled");
        button.onclick = null;
        return;
      }
      button.classList.remove("disabled");
      button.onclick = (e) => {
        e.stopPropagation();
        const newIndex = direction === "left" ? currentIndex - 1 : currentIndex + 1;
        if (newIndex >= 0 && newIndex < dateList.length) {
          globalValue.currentDatePage = dateList[newIndex];
          this.refreshUI(allData[globalValue.currentDatePage]);
          this.bindEvents();
        }
        this.itemTransiton(indecator);
        if (globalValue.currentDatePage !== today) {
          this.contentTransition(statsLable, globalValue.currentDatePage);
        } else {
          this.contentTransition(statsLable, "今日统计");
        }
        if (globalValue.currentDatePage === today) {
          this.updateInterval();
          this.getCoinListUpdate();
        } else {
          clearInterval(globalValue.updateIntervalID);
          globalValue.updateIntervalID = void 0;
        }
      };
    },
bindSwitcherLeft: function() {
      this.bindSwitcher("left");
    },
bindSwitcherRight: function() {
      this.bindSwitcher("right");
    },
contentTransition: function(element, newText, duration = 300) {
      element.classList.add("transitioning");
      setTimeout(() => {
        element.textContent = newText;
        element.classList.remove("transitioning");
      }, duration);
    },
itemTransiton: function(element, duration = 300) {
      element.classList.add("transitioning");
      setTimeout(() => {
        element.classList.remove("transitioning");
      }, duration);
    },
initRender: function(name, nickname) {
      const newItem = document.createElement("div");
      newItem.className = "qmx-modal-stats-child";
      const className = "qmx-stat-info-" + name;
      newItem.innerHTML = `
                <div class=${className}>
                    <div class="qmx-stat-header">
                        <span class="qmx-stat-nickname">${nickname}</span>
                    </div>
                    <div class="qmx-stat-details">
                        <span class="qmx-stat-item">0</span>
                    </div>
                </div>
            `;
      return newItem;
    },
updateTodayData: function() {
      const { allData, todayData } = this.ensureTodayDataExists();
      if (!todayData) return;
      todayData.avg = todayData.receivedCount ? parseFloat((todayData.total / todayData.receivedCount).toFixed(2)) : 0;
      if (!Utils.lockChecker("douyu_qmx_stats_lock", this.updateTodayData.bind(this))) return;
      Utils.setLocalValueWithLock(
        "douyu_qmx_stats_lock",
        typedSettings.STATS_INFO_STORAGE_KEY,
        allData,
        "更新今日统计数据"
      );
      this.refreshUI(todayData);
    },
set: function(name, value) {
      const { allData, todayData } = this.ensureTodayDataExists();
      if (!todayData) return;
      todayData[name] = value;
      if (!Utils.lockChecker("douyu_qmx_stats_lock", this.set.bind(this), name, value)) return;
      Utils.setLocalValueWithLock(
        "douyu_qmx_stats_lock",
        typedSettings.STATS_INFO_STORAGE_KEY,
        allData,
        "更新统计数据"
      );
      this.refreshUI(todayData);
    },
getCoinListUpdate: async function() {
      const currentRoomId = Utils.getCurrentRoomId();
      if (!currentRoomId) {
        Utils.log("[统计] 无法获取当前房间ID,跳过金币记录更新。");
        return;
      }
      const coinList = await DouyuAPI.getCoinRecord(1, 100, currentRoomId, 3);
      const startOfToday = new Date();
      startOfToday.setHours(0, 0, 0, 0);
      const filteredData = coinList.filter(
        (item) => item.createTime > startOfToday.getTime() / 1e3
      );
      const totalCoin = filteredData.reduce((sum, item) => sum + item.balanceDiff, 0);
      const updateList = [
        ["receivedCount", filteredData.length],
        ["total", totalCoin]
      ];
      updateList.forEach(([name, value]) => {
        this.set(name, value);
      });
      this.updateTodayData();
    },
refreshUI: function(todayData) {
      for (const key in todayData) {
        try {
          const typedKey = key;
          const element = globalValue.statElements.get(typedKey);
          if (!element) continue;
          this.contentTransition(element, todayData[typedKey].toString());
        } catch (e) {
          Utils.log(`[StatsInfo] UI刷新异常: ${e}`);
          continue;
        }
      }
    },
removeExpiredData: function() {
      const allData = this.ensureTodayDataExists().allData;
      const newAllData = Object.keys(allData).filter((dateString) => {
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        const date = new Date(dateString);
        const diff = today.getTime() - date.getTime();
        const dayDiff = diff / (1e3 * 60 * 60 * 24);
        return dayDiff <= 6;
      }).reduce((obj, key) => {
        return Object.assign(obj, { [key]: allData[key] });
      }, {});
      GM_setValue(typedSettings.STATS_INFO_STORAGE_KEY, newAllData);
      Utils.log("[数据统计]:已清理过期数据");
    },
updateDataForDailyReset: function() {
      const allData = GM_getValue(
        typedSettings.STATS_INFO_STORAGE_KEY,
        null
      );
      if (!allData || typeof allData !== "object") {
        this.ensureTodayDataExists();
        this.updateTodayData();
        this.removeExpiredData();
        return;
      }
      const lastDate = Object.keys(allData).at(-1);
      const nowDate = Utils.formatDateAsBeijing( new Date());
      if (lastDate !== nowDate) {
        this.updateTodayData();
        this.removeExpiredData();
      }
    },
checkUpdate: function() {
      const state = GlobalState.get();
      const tabList = document.getElementById("qmx-tab-list");
      if (!tabList) return;
      const tabIds = Object.keys(state.tabs);
      tabIds.forEach((roomId) => {
        const tabData = state.tabs[roomId];
        const currentStatusText = tabData.statusText;
        if (typedSettings.SHOW_STATS_IN_PANEL) {
          if (currentStatusText.includes("领取到")) {
            this.getCoinListUpdate();
          }
        }
      });
    }
  };
  const ICONS = {
    GOLD: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#FFD700" stroke="#FFA000" stroke-width="2"/><text x="50%" y="50%" text-anchor="middle" dy=".35em" font-size="14" fill="#B8860B" font-weight="bold" font-family="Arial">¥</text></svg>`,
    STARLIGHT: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="#FF69B4" stroke="#FF1493" stroke-width="2"/></svg>`
  };
  const ControlPage = {
injectionTarget: null,
isPanelInjected: false,
commandChannel: null,
    modalContainer: null,

init() {
      Utils.log("当前是控制页面,开始设置UI...");
      this.commandChannel = new BroadcastChannel("douyu_qmx_commands");
      ThemeManager.applyTheme(SETTINGS.THEME);
      this.clearClosedTabs();
      this.createHTML();
      const qmxModalHeader = document.querySelector(".qmx-modal-header");
      if (SETTINGS.SHOW_STATS_IN_PANEL) {
        if (qmxModalHeader) {
          qmxModalHeader.style.padding = "10px 20px 0px 20px";
        }
        StatsInfo.init();
      } else {
        const statsContent = document.querySelector(".qmx-stats-container");
        if (statsContent && qmxModalHeader) {
          statsContent.remove();
          qmxModalHeader.style.padding = "10px 20px 4px 20px";
        }
      }
      this.applyModalMode();
      this.bindEvents();
      window.addEventListener("qmx-settings-update", (e) => {
        this.handleSettingsUpdate(e.detail);
      });
      setInterval(() => {
        this.renderDashboard();
        this.cleanupAndMonitorWorkers();
        this.checkInjectionState();
      }, 1e3);
      FirstTimeNotice.showCalibrationNotice();
      window.addEventListener("beforeunload", () => {
        if (this.commandChannel) {
          this.commandChannel.close();
        }
      });
      window.addEventListener("resize", () => {
        this.correctButtonPosition();
        this.correctModalPosition();
      });
    },
checkInjectionState() {
      if (SETTINGS.MODAL_DISPLAY_MODE === "inject-rank-list" && this.isPanelInjected) {
        if (this.modalContainer && !this.modalContainer.isConnected) {
          Utils.log("[监控] 检测到面板脱离DOM (可能是页面重绘),正在重新注入...");
          this.isPanelInjected = false;
          this.applyModalMode();
        }
      }
    },
async handleSettingsUpdate(newSettings) {
      Utils.log("[ControlPage] 检测到设置更新,正在应用...");
      if (newSettings.MODAL_DISPLAY_MODE) {
        this.applyModalMode();
        this.correctModalPosition();
      }
      if (typeof newSettings.SHOW_STATS_IN_PANEL !== "undefined") {
        this.toggleStatsPanel(newSettings.SHOW_STATS_IN_PANEL);
      }
      if (newSettings.STATS_UPDATE_INTERVAL && SETTINGS.SHOW_STATS_IN_PANEL) {
        StatsInfo.updateInterval();
      }
    },
toggleStatsPanel(show) {
      const qmxModalHeader = document.querySelector(".qmx-modal-header");
      let statsContent = document.querySelector(".qmx-stats-container");
      if (show) {
        if (!statsContent) {
          const tempDiv = document.createElement("div");
          tempDiv.innerHTML = statsPanelTemplate;
          statsContent = tempDiv.firstElementChild;
          qmxModalHeader.after(statsContent);
          const statsToggle = document.getElementById("qmx-stats-toggle");
          const statsContentEl = document.getElementById("qmx-stats-content");
          if (statsToggle && statsContentEl) {
            statsToggle.addEventListener("click", () => {
              const isExpanded = statsToggle.classList.contains("expanded");
              if (isExpanded) {
                statsToggle.classList.remove("expanded");
                statsContentEl.classList.remove("expanded");
              } else {
                statsToggle.classList.add("expanded");
                statsContentEl.classList.add("expanded");
              }
            });
          }
        }
        if (qmxModalHeader) {
          qmxModalHeader.style.padding = "10px 20px 0px 20px";
        }
        StatsInfo.init();
      } else {
        if (statsContent) {
          statsContent.remove();
        }
        if (qmxModalHeader) {
          qmxModalHeader.style.padding = "10px 20px 4px 20px";
        }
        StatsInfo.destroy();
      }
    },
    createHTML() {
      Utils.log("创建UI的HTML结构...");
      const modalBackdrop = document.createElement("div");
      modalBackdrop.id = "qmx-modal-backdrop";
      const modalContainer = document.createElement("div");
      modalContainer.id = "qmx-modal-container";
      this.modalContainer = modalContainer;
      modalContainer.innerHTML = mainPanelTemplate(SETTINGS.MAX_WORKER_TABS);
      document.body.appendChild(modalBackdrop);
      document.body.appendChild(modalContainer);
      const mainButton = document.createElement("button");
      mainButton.id = SETTINGS.DRAGGABLE_BUTTON_ID;
      mainButton.innerHTML = `<span class="icon">🎁</span>`;
      document.body.appendChild(mainButton);
      const settingsModal = document.createElement("div");
      settingsModal.id = "qmx-settings-modal";
      document.body.appendChild(settingsModal);
      const globalTooltip = document.createElement("div");
      globalTooltip.id = "qmx-global-tooltip";
      document.body.appendChild(globalTooltip);
    },
cleanupAndMonitorWorkers() {
      const state = GlobalState.get();
      let stateModified = false;
      for (const roomId in state.tabs) {
        const tab = state.tabs[roomId];
        const timeSinceLastUpdate = Date.now() - tab.lastUpdateTime;
        if (tab.status === "DISCONNECTED" && timeSinceLastUpdate > SETTINGS.DISCONNECTED_GRACE_PERIOD) {
          Utils.log(
            `[监控] 任务 ${roomId} (已断开) 超过 ${SETTINGS.DISCONNECTED_GRACE_PERIOD / 1e3} 秒未重连,执行清理。`
          );
          delete state.tabs[roomId];
          stateModified = true;
          continue;
        }
        if (tab.status === "SWITCHING" && timeSinceLastUpdate > SETTINGS.SWITCHING_CLEANUP_TIMEOUT) {
          Utils.log(`[监控] 任务 ${roomId} (切换中) 已超时,判定为已关闭,执行清理。`);
          delete state.tabs[roomId];
          stateModified = true;
          continue;
        }
        if (timeSinceLastUpdate > SETTINGS.UNRESPONSIVE_TIMEOUT && tab.status !== "UNRESPONSIVE") {
          Utils.log(`[监控] 任务 ${roomId} 已失联超过 ${SETTINGS.UNRESPONSIVE_TIMEOUT / 6e4} 分钟,标记为无响应。`);
          tab.status = "UNRESPONSIVE";
          tab.statusText = "心跳失联,请点击激活或关闭此标签页";
          stateModified = true;
        }
      }
      if (stateModified) {
        GlobalState.set(state);
      }
    },
bindEvents() {
      Utils.log("为UI元素绑定事件...");
      const mainButton = document.getElementById(SETTINGS.DRAGGABLE_BUTTON_ID);
      const modalContainer = document.getElementById("qmx-modal-container");
      const modalBackdrop = document.getElementById("qmx-modal-backdrop");
      const statsToggle = document.getElementById("qmx-stats-toggle");
      const statsContent = document.getElementById("qmx-stats-content");
      if (statsToggle && statsContent) {
        statsToggle.addEventListener("click", () => {
          const isExpanded = statsToggle.classList.contains("expanded");
          if (isExpanded) {
            statsToggle.classList.remove("expanded");
            statsContent.classList.remove("expanded");
          } else {
            statsToggle.classList.add("expanded");
            statsContent.classList.add("expanded");
          }
        });
      }
      this.setupDrag(mainButton, SETTINGS.BUTTON_POS_STORAGE_KEY, () => this.showPanel());
      const modalHeader = modalContainer.querySelector(".qmx-modal-header");
      this.setupDrag(modalContainer, "douyu_qmx_modal_position", null, modalHeader);
      document.getElementById("qmx-modal-close-btn").onclick = () => this.hidePanel();
      document.addEventListener("keydown", (e) => {
        if (e.key === "Escape" && modalContainer.classList.contains("visible")) {
          this.hidePanel();
        }
      });
      if (SETTINGS.MODAL_DISPLAY_MODE !== "inject-rank-list") {
        modalBackdrop.onclick = () => this.hidePanel();
      }
      document.getElementById("qmx-modal-open-btn").onclick = () => this.openOneNewTab();
      document.getElementById("qmx-modal-settings-btn").onclick = () => SettingsPanel.show();
      document.getElementById("qmx-modal-close-all-btn").onclick = async () => {
        if (confirm("确定要关闭所有工作标签页吗?")) {
          Utils.log("用户请求关闭所有标签页。");
          Utils.log("通过 BroadcastChannel 发出 CLOSE_ALL 指令...");
          this.commandChannel.postMessage({ action: "CLOSE_ALL", target: "*" });
          await new Promise((resolve) => setTimeout(resolve, 500));
          Utils.log("强制清空全局状态中的标签页列表...");
          let state = GlobalState.get();
          if (Object.keys(state.tabs).length > 0) {
            Utils.log(`清理前还有 ${Object.keys(state.tabs).length} 个标签页残留`);
            state.tabs = {};
            GlobalState.set(state);
          }
          this.renderDashboard();
          setTimeout(() => {
            state = GlobalState.get();
            if (Object.keys(state.tabs).length > 0) {
              Utils.log("检测到残留标签页,执行二次清理...");
              state.tabs = {};
              GlobalState.set(state);
              this.renderDashboard();
            }
          }, 1e3);
        }
      };
      document.getElementById("qmx-tab-list").addEventListener("click", (e) => {
        const closeButton = e.target.closest(".qmx-tab-close-btn");
        if (!closeButton) return;
        const roomItem = e.target.closest("[data-room-id]");
        const roomId = roomItem?.dataset.roomId;
        if (!roomId) return;
        Utils.log(`[控制中心] 用户请求关闭房间: ${roomId}。`);
        const state = GlobalState.get();
        delete state.tabs[roomId];
        GlobalState.set(state);
        Utils.log(`通过 BroadcastChannel 向 ${roomId} 发出 CLOSE 指令...`);
        this.commandChannel.postMessage({ action: "CLOSE", target: roomId });
        roomItem.style.opacity = "0";
        roomItem.style.transform = "scale(0.8)";
        roomItem.style.transition = "all 0.3s ease";
        setTimeout(() => roomItem.remove(), 300);
      });
    },
renderDashboard() {
      const state = GlobalState.get();
      const tabList = document.getElementById("qmx-tab-list");
      if (!tabList) return;
      const tabIds = Object.keys(state.tabs);
      document.getElementById("qmx-active-tabs-count").textContent = tabIds.length;
      const statusDisplayMap = {
        OPENING: "加载中",
        WAITING: "等待中",
        CLAIMING: "领取中",
        SWITCHING: "切换中",
        DORMANT: "休眠中",
        ERROR: "出错了",
        UNRESPONSIVE: "无响应",
        DISCONNECTED: "已断开",
        STALLED: "UI节流"
      };
      const existingRoomIds = new Set(
        Array.from(tabList.children).map((node) => node.dataset.roomId).filter(Boolean)
      );
      tabIds.forEach((roomId) => {
        const tabData = state.tabs[roomId];
        let existingItem = tabList.querySelector(`[data-room-id="${roomId}"]`);
        let currentStatusText = tabData.statusText;
        if (tabData.status === "WAITING" && tabData.countdown?.endTime && (!currentStatusText || currentStatusText.startsWith("倒计时") || currentStatusText === "寻找任务中...")) {
          const remainingSeconds = (tabData.countdown.endTime - Date.now()) / 1e3;
          if (remainingSeconds > 0) {
            currentStatusText = `倒计时 ${Utils.formatTime(remainingSeconds)}`;
          } else {
            currentStatusText = "等待开抢...";
          }
        }
        if (existingItem) {
          const nicknameEl = existingItem.querySelector(".identity-nickname") || existingItem.querySelector(".qmx-tab-nickname");
          const statusNameEl = existingItem.querySelector(".qmx-tab-status-name");
          const statusTextEl = existingItem.querySelector(".qmx-tab-status-text");
          const dotEl = existingItem.querySelector(".qmx-tab-status-dot");
          if (tabData.nickname && nicknameEl && nicknameEl.textContent !== tabData.nickname) {
            nicknameEl.textContent = tabData.nickname;
          }
          const newStatusName = `[${statusDisplayMap[tabData.status] || tabData.status}]`;
          if (statusNameEl.textContent !== newStatusName) {
            statusNameEl.textContent = newStatusName;
            dotEl.style.backgroundColor = `var(--status-color-${tabData.status.toLowerCase()}, #9E9E9E)`;
          }
          if (statusTextEl.textContent !== currentStatusText) {
            statusTextEl.textContent = currentStatusText;
          }
          let prizesContainer = existingItem.querySelector(".qmx-tab-prizes");
          const newPrizesHtml = this.generatePrizesHTML(tabData.prizes);
          if (newPrizesHtml) {
            if (!prizesContainer) {
              const closeBtn = existingItem.querySelector(".qmx-tab-close-btn");
              if (closeBtn) {
                closeBtn.insertAdjacentHTML("beforebegin", newPrizesHtml);
              } else {
                existingItem.insertAdjacentHTML("beforeend", newPrizesHtml);
              }
            } else {
              const tempDiv = document.createElement("div");
              tempDiv.innerHTML = newPrizesHtml;
              const newContainer = tempDiv.firstElementChild;
              const oldLayoutClass = prizesContainer.classList.contains("multi-prizes") ? "multi-prizes" : "single-prize";
              const newLayoutClass = newContainer.classList.contains("multi-prizes") ? "multi-prizes" : "single-prize";
              const oldText = prizesContainer.textContent.trim();
              const newText = newContainer.textContent.trim();
              if (oldLayoutClass !== newLayoutClass || oldText !== newText) {
                prizesContainer.outerHTML = newPrizesHtml;
              }
            }
          } else if (prizesContainer) {
            prizesContainer.remove();
          }
        } else {
          const newItem = this.createTaskItem(roomId, tabData, statusDisplayMap, currentStatusText);
          tabList.appendChild(newItem);
          requestAnimationFrame(() => {
            newItem.classList.add("qmx-item-enter-active");
            setTimeout(() => newItem.classList.remove("qmx-item-enter"), 300);
          });
        }
      });
      existingRoomIds.forEach((roomId) => {
        if (!state.tabs[roomId]) {
          const itemToRemove = tabList.querySelector(`[data-room-id="${roomId}"]`);
          if (itemToRemove && !itemToRemove.classList.contains("qmx-item-exit-active")) {
            Utils.log(`[Render] 房间 ${roomId}: 在最新状态中已消失,执行移除。`);
            itemToRemove.classList.add("qmx-item-exit-active");
            setTimeout(() => itemToRemove.remove(), 300);
          }
        }
      });
      const emptyMsg = tabList.querySelector(".qmx-empty-list-msg");
      if (tabIds.length === 0) {
        if (!emptyMsg) {
          tabList.innerHTML = '<div class="qmx-tab-list-item qmx-empty-list-msg">没有正在运行的任务</div>';
        }
      } else if (emptyMsg) {
        emptyMsg.remove();
      }
      this.renderLimitStatus();
    },
renderLimitStatus() {
      let limitState = GlobalState.getDailyLimit();
      let limitMessageEl = document.getElementById("qmx-limit-message");
      const openBtn = document.getElementById("qmx-modal-open-btn");
      if (limitState?.reached && Utils.formatDateAsBeijing(new Date(limitState.timestamp)) !== Utils.formatDateAsBeijing( new Date())) {
        Utils.log("[控制中心] 新的一天,重置每日上限旗标。");
        GlobalState.setDailyLimit(false);
        limitState = null;
      }
      if (limitState?.reached) {
        if (!limitMessageEl) {
          limitMessageEl = document.createElement("div");
          limitMessageEl.id = "qmx-limit-message";
          limitMessageEl.style.cssText = "padding: 10px 24px; background-color: var(--status-color-error); color: white; font-weight: 500; text-align: center;";
          const header = document.querySelector(".qmx-modal-header");
          header.parentNode.insertBefore(limitMessageEl, header.nextSibling);
          document.querySelector(".qmx-modal-header").after(limitMessageEl);
        }
        if (SETTINGS.DAILY_LIMIT_ACTION === "CONTINUE_DORMANT") {
          limitMessageEl.textContent = "今日已达上限。任务休眠中,可新增标签页为明日准备。";
          openBtn.disabled = false;
          openBtn.textContent = "新增休眠标签页";
        } else {
          limitMessageEl.textContent = "今日已达上限。任务已全部停止。";
          openBtn.disabled = true;
          openBtn.textContent = "今日已达上限";
        }
      } else {
        if (limitMessageEl) limitMessageEl.remove();
        openBtn.disabled = false;
        openBtn.textContent = "打开新房间";
      }
    },
async openOneNewTab() {
      const openBtn = document.getElementById("qmx-modal-open-btn");
      if (openBtn.disabled) return;
      const state = GlobalState.get();
      const openedCount = Object.keys(state.tabs).length;
      if (openedCount >= SETTINGS.MAX_WORKER_TABS) {
        Utils.log(`已达到最大标签页数量 (${SETTINGS.MAX_WORKER_TABS})。`);
        return;
      }
      openBtn.disabled = true;
      openBtn.textContent = "正在查找...";
      try {
        const openedRoomIds = new Set(Object.keys(state.tabs));
        const apiRoomUrls = await DouyuAPI.getRooms(SETTINGS.API_ROOM_FETCH_COUNT, SETTINGS.CONTROL_ROOM_ID);
        const newUrl = apiRoomUrls.find((url) => {
          const rid = url.match(/\/(\d+)/)?.[1];
          return rid && !openedRoomIds.has(rid);
        });
        if (newUrl) {
          const newRoomId = newUrl.match(/\/(\d+)/)[1];
          const pendingWorkers = GM_getValue("qmx_pending_workers", []);
          pendingWorkers.push(newRoomId);
          GM_setValue("qmx_pending_workers", pendingWorkers);
          Utils.log(`已将房间 ${newRoomId} 加入待处理列表。`);
          GlobalState.updateWorker(newRoomId, "OPENING", "正在打开...");
          if (window.location.href.includes("/beta") || localStorage.getItem("newWebLive") !== "A") {
            localStorage.setItem("newWebLive", "A");
          }
          GM_openInTab(newUrl, { active: false, setParent: true });
          Utils.log(`打开指令已发送: ${newUrl}`);
        } else {
          Utils.log("未能找到新的、未打开的房间。");
          openBtn.textContent = "无新房间";
          await Utils.sleep(SETTINGS.UI_FEEDBACK_DELAY);
        }
      } catch (error) {
        Utils.log(`查找或打开房间时出错: ${error.message}`);
        openBtn.textContent = "查找出错";
        await Utils.sleep(SETTINGS.UI_FEEDBACK_DELAY);
      } finally {
        openBtn.disabled = false;
      }
    },
setupDrag(element, storageKey, onClick, handle = element) {
      let isMouseDown = false;
      let hasDragged = false;
      let startX, startY, initialX, initialY;
      const clickThreshold = 5;
      const setPosition = (x, y) => {
        element.style.setProperty("--tx", `${x}px`);
        element.style.setProperty("--ty", `${y}px`);
      };
      const savedPos = GM_getValue(storageKey);
      let currentRatio = null;
      if (savedPos) {
        if (typeof savedPos.ratioX === "number" && typeof savedPos.ratioY === "number") {
          currentRatio = savedPos;
        } else if (SETTINGS.CONVERT_LEGACY_POSITION && typeof savedPos.x === "number" && typeof savedPos.y === "number") {
          Utils.log(`[位置迁移] 发现旧的像素位置,正在转换为比例位置...`);
          const movableWidth = window.innerWidth - element.offsetWidth;
          const movableHeight = window.innerHeight - element.offsetHeight;
          currentRatio = {
            ratioX: Math.max(0, Math.min(1, savedPos.x / movableWidth)),
            ratioY: Math.max(0, Math.min(1, savedPos.y / movableHeight))
          };
          GM_setValue(storageKey, currentRatio);
        }
      }
      if (currentRatio) {
        const newX = currentRatio.ratioX * (window.innerWidth - element.offsetWidth);
        const newY = currentRatio.ratioY * (window.innerHeight - element.offsetHeight);
        setPosition(newX, newY);
      } else {
        if (element.id === SETTINGS.DRAGGABLE_BUTTON_ID) {
          const padding = SETTINGS.DRAG_BUTTON_DEFAULT_PADDING;
          const defaultX = window.innerWidth - element.offsetWidth - padding;
          const defaultY = padding;
          setPosition(defaultX, defaultY);
        } else {
          const defaultX = (window.innerWidth - element.offsetWidth) / 2;
          const defaultY = (window.innerHeight - element.offsetHeight) / 2;
          setPosition(defaultX, defaultY);
        }
      }
      const onMouseDown = (e) => {
        if (e.button !== 0) return;
        isMouseDown = true;
        hasDragged = false;
        const rect = element.getBoundingClientRect();
        startX = e.clientX;
        startY = e.clientY;
        initialX = rect.left;
        initialY = rect.top;
        element.classList.add("is-dragging");
        handle.style.cursor = "grabbing";
        document.addEventListener("mousemove", onMouseMove);
        document.addEventListener("mouseup", onMouseUp, { once: true });
      };
      const onMouseMove = (e) => {
        if (!isMouseDown) return;
        e.preventDefault();
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        if (!hasDragged && Math.sqrt(dx * dx + dy * dy) > clickThreshold) {
          hasDragged = true;
        }
        let newX = initialX + dx;
        let newY = initialY + dy;
        const maxX = window.innerWidth - element.offsetWidth;
        const maxY = window.innerHeight - element.offsetHeight;
        newX = Math.max(0, Math.min(newX, maxX));
        newY = Math.max(0, Math.min(newY, maxY));
        setPosition(newX, newY);
      };
      const onMouseUp = () => {
        isMouseDown = false;
        document.removeEventListener("mousemove", onMouseMove);
        element.classList.remove("is-dragging");
        handle.style.cursor = "grab";
        if (hasDragged) {
          const finalRect = element.getBoundingClientRect();
          const movableWidth = window.innerWidth - element.offsetWidth;
          const movableHeight = window.innerHeight - element.offsetHeight;
          const ratioX = movableWidth > 0 ? Math.max(0, Math.min(1, finalRect.left / movableWidth)) : 0;
          const ratioY = movableHeight > 0 ? Math.max(0, Math.min(1, finalRect.top / movableHeight)) : 0;
          GM_setValue(storageKey, { ratioX, ratioY });
        } else if (onClick && typeof onClick === "function") {
          onClick();
        }
      };
      handle.addEventListener("mousedown", onMouseDown);
    },
showPanel() {
      const mainButton = document.getElementById(SETTINGS.DRAGGABLE_BUTTON_ID);
      const modalContainer = document.getElementById("qmx-modal-container");
      mainButton.classList.add("hidden");
      if (this.isPanelInjected) {
        this.injectionTarget.classList.add("qmx-hidden");
        modalContainer.classList.remove("qmx-hidden");
      } else {
        modalContainer.classList.add("visible");
        if (SETTINGS.MODAL_DISPLAY_MODE === "centered") {
          document.getElementById("qmx-modal-backdrop").classList.add("visible");
        }
      }
      Utils.log("控制面板已显示。");
    },
hidePanel() {
      const mainButton = document.getElementById(SETTINGS.DRAGGABLE_BUTTON_ID);
      const modalContainer = document.getElementById("qmx-modal-container");
      mainButton.classList.remove("hidden");
      if (this.isPanelInjected) {
        modalContainer.classList.add("qmx-hidden");
        if (this.injectionTarget) {
          this.injectionTarget.classList.remove("qmx-hidden");
        }
      } else {
        modalContainer.classList.remove("visible");
        if (SETTINGS.MODAL_DISPLAY_MODE === "centered") {
          document.getElementById("qmx-modal-backdrop").classList.remove("visible");
        }
      }
      Utils.log("控制面板已隐藏。");
    },
createTaskItem(roomId, tabData, statusMap, statusText) {
      const newItem = document.createElement("div");
      newItem.className = "qmx-tab-list-item qmx-item-enter";
      newItem.dataset.roomId = roomId;
      const statusColor = `var(--status-color-${tabData.status.toLowerCase()}, #9E9E9E)`;
      const nickname = tabData.nickname || "加载中...";
      const statusName = statusMap[tabData.status] || tabData.status;
      const prizesHtml = this.generatePrizesHTML(tabData.prizes);
      newItem.innerHTML = `
                <div class="qmx-tab-status-dot" style="background-color: ${statusColor};"></div>
                <div class="qmx-tab-info">
                    <div class="qmx-tab-header">
                        <button class="qmx-tab-identity" type="button" data-state="nickname" title="点击切换或复制">
                            <span class="qmx-tab-identity-icon" aria-hidden="true">
                                <svg viewBox="0 0 24 24" role="img" focusable="false">
                                    <path d="M8 7h3v2H8v9H6V9H3V7h3V4h2v3zm7 0h6v2h-6v9h-2V7h2z" fill="currentColor"></path>
                                </svg>
                            </span>
                            <span class="qmx-tab-identity-text">
                                <span class="identity-nickname">${nickname}</span>
                                <span class="identity-roomid">${roomId}</span>
                            </span>
                        </button>
                    </div>
                    <div class="qmx-tab-details">
                        <span class="qmx-tab-status-name">[${statusName}]</span>
                        <span class="qmx-tab-status-text">${statusText}</span>
                    </div>
                </div>
                ${prizesHtml}
                <button class="qmx-tab-close-btn" title="关闭该标签页">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </button>
            `;
      const identityBtn = newItem.querySelector(".qmx-tab-identity");
      const nicknameSpan = identityBtn.querySelector(".identity-nickname");
      const roomIdSpan = identityBtn.querySelector(".identity-roomid");
      const iconSpan = identityBtn.querySelector(".qmx-tab-identity-icon");
      const setIdentityState = (state) => {
        identityBtn.dataset.state = state;
      };
      const copyIdentityValue = async (state) => {
        const value = state === "room" ? roomIdSpan.textContent.trim() : nicknameSpan.textContent.trim();
        const label = state === "room" ? "房间号" : "房间名";
        try {
          await navigator.clipboard.writeText(value);
          identityBtn.classList.add("copied");
          setTimeout(() => identityBtn.classList.remove("copied"), 300);
          Utils.log(`[房间号切换] 已复制${label}: ${value}`);
        } catch (err) {
          Utils.log(`[房间号切换] 复制失败: ${err.message}`);
        }
      };
      iconSpan.addEventListener("click", async (e) => {
        e.stopPropagation();
        const currentState = identityBtn.dataset.state === "room" ? "room" : "nickname";
        await copyIdentityValue(currentState);
      });
      identityBtn.addEventListener("click", (e) => {
        if (e.target.closest(".qmx-tab-identity-icon")) {
          return;
        }
        e.stopPropagation();
        const currentState = identityBtn.dataset.state === "room" ? "room" : "nickname";
        const nextState = currentState === "nickname" ? "room" : "nickname";
        setIdentityState(nextState);
      });
      identityBtn.addEventListener(
        "mouseenter",
        () => Utils.log(`[房间号切换] 鼠标悬停在房间胶囊: ${roomId}`),
        { once: true }
      );
      return newItem;
    },
generatePrizesHTML(prizes) {
      if (!prizes || !Array.isArray(prizes) || prizes.length === 0) return "";
      const layoutClass = prizes.length > 1 ? "multi-prizes" : "single-prize";
      return `<div class="qmx-tab-prizes ${layoutClass}">` + prizes.map((p, index) => {
        let icon = ICONS.GOLD;
        if (prizes.length === 2 && index === 1) {
          icon = ICONS.STARLIGHT;
        }
        return `<div class="qmx-tab-prize-item" title="${p.name || p.text}">
                    ${icon}
                    <span class="qmx-tab-prize-text">${p.text}</span>
                </div>`;
      }).join("") + `</div>`;
    },
applyModalMode() {
      const modalContainer = this.modalContainer || document.getElementById("qmx-modal-container");
      if (!modalContainer) return;
      const mode = SETTINGS.MODAL_DISPLAY_MODE;
      const mainButton = document.getElementById(SETTINGS.DRAGGABLE_BUTTON_ID);
      Utils.log(`尝试应用模态框模式: ${mode}`);
      if (this.isPanelInjected && mode !== "inject-rank-list") {
        if (this.injectionTarget) {
          this.injectionTarget.classList.remove("qmx-hidden");
        }
        document.body.appendChild(modalContainer);
        this.isPanelInjected = false;
        this.injectionTarget = null;
        modalContainer.classList.remove("mode-inject-rank-list", "qmx-hidden");
        if (mainButton && mainButton.classList.contains("hidden")) {
          modalContainer.classList.add("visible");
        } else {
          modalContainer.classList.remove("visible");
        }
      }
      if (mode === "inject-rank-list") {
        const waitForTarget = (retries = SETTINGS.INJECT_TARGET_RETRIES, interval = SETTINGS.INJECT_TARGET_INTERVAL) => {
          const target = document.querySelector(SETTINGS.SELECTORS.rankListContainer);
          if (target) {
            if (this.isPanelInjected && this.injectionTarget === target && modalContainer.parentNode === target.parentNode) {
              return;
            }
            Utils.log("执行注入逻辑...");
            if (this.injectionTarget && this.injectionTarget !== target) {
              this.injectionTarget.classList.remove("qmx-hidden");
            }
            this.injectionTarget = target;
            this.isPanelInjected = true;
            target.parentNode.insertBefore(modalContainer, target.nextSibling);
            modalContainer.classList.add("mode-inject-rank-list");
            modalContainer.classList.remove("mode-centered", "mode-floating");
            if (mainButton && mainButton.classList.contains("hidden")) {
              modalContainer.classList.remove("qmx-hidden");
              this.injectionTarget.classList.add("qmx-hidden");
            } else {
              modalContainer.classList.add("qmx-hidden");
              this.injectionTarget.classList.remove("qmx-hidden");
            }
            modalContainer.classList.remove("visible");
          } else if (retries > 0) {
            setTimeout(() => waitForTarget(retries - 1, interval), interval);
          } else {
            Utils.log(`[注入失败] 未找到目标元素 "${SETTINGS.SELECTORS.rankListContainer}"。`);
            Utils.log("[降级] 自动切换到 'floating' 备用模式。");
            SETTINGS.MODAL_DISPLAY_MODE = "floating";
            this.applyModalMode();
          }
        };
        waitForTarget();
        return;
      }
      this.isPanelInjected = false;
      modalContainer.classList.remove("mode-inject-rank-list", "qmx-hidden");
      modalContainer.classList.remove("mode-centered", "mode-floating");
      modalContainer.classList.add(`mode-${mode}`);
    },
correctPosition(elementId, storageKey) {
      const element = document.getElementById(elementId);
      if (!element) return;
      const savedPos = GM_getValue(storageKey);
      if (savedPos && typeof savedPos.ratioX === "number" && typeof savedPos.ratioY === "number") {
        const newX = savedPos.ratioX * (window.innerWidth - element.offsetWidth);
        const newY = savedPos.ratioY * (window.innerHeight - element.offsetHeight);
        element.style.setProperty("--tx", `${newX}px`);
        element.style.setProperty("--ty", `${newY}px`);
      }
    },
correctButtonPosition() {
      this.correctPosition(SETTINGS.DRAGGABLE_BUTTON_ID, SETTINGS.BUTTON_POS_STORAGE_KEY);
    },
correctModalPosition() {
      if (SETTINGS.MODAL_DISPLAY_MODE !== "floating" || this.isPanelInjected) {
        return;
      }
      this.correctPosition("qmx-modal-container", "douyu_qmx_modal_position");
    },
clearClosedTabs() {
      const state = GlobalState.get();
      if (state.tabs && Object.keys(state.tabs).length > 0) {
        Utils.log("检测到残留的标签页状态,正在清空...");
        state.tabs = {};
        GlobalState.set(state);
        Utils.log("已清空残留的标签页状态");
      }
    }
  };
  const DOM = {
async findElement(selector, timeout = SETTINGS.PANEL_WAIT_TIMEOUT, parent = document, validator = null) {
      const startTime = Date.now();
      while (Date.now() - startTime < timeout) {
        const elements = parent.querySelectorAll(selector);
        for (const element of elements) {
          if (window.getComputedStyle(element).display === "none") {
            continue;
          }
          if (validator && !validator(element)) {
            continue;
          }
          return element;
        }
        await Utils.sleep(300);
      }
      Utils.log(`查找元素超时: ${selector}`);
      return null;
    },
async safeClick(element, description) {
      if (!element) {
        return false;
      }
      try {
        if (window.getComputedStyle(element).display === "none") {
          return false;
        }
        await Utils.sleep(Utils.getRandomDelay(SETTINGS.MIN_DELAY / 2, SETTINGS.MAX_DELAY / 2));
        element.click();
        await Utils.sleep(Utils.getRandomDelay());
        return true;
      } catch (error) {
        Utils.log(`[点击异常] ${description} 时发生错误: ${error.message}`);
        return false;
      }
    },
async checkForLimitPopup() {
      const limitPopup = await this.findElement(SETTINGS.SELECTORS.limitReachedPopup, 3e3);
      if (limitPopup && limitPopup.textContent.includes("已达上限")) {
        Utils.log("捕获到“已达上限”弹窗!");
        return true;
      }
      return false;
    }
  };
  const WorkerPage = {
healthCheckTimeoutId: null,
    currentTaskEndTime: null,
    lastHealthCheckTime: null,
    lastPageCountdown: null,
    stallLevel: 0,
remainingTimeMap: new Map(),
consecutiveStallCount: 0,
    previousDeviation: 0,

async init() {
      Utils.log("混合模式工作单元初始化...");
      const roomId = Utils.getCurrentRoomId();
      if (!roomId) {
        Utils.log("无法识别当前房间ID,脚本停止。");
        return;
      }
      GlobalState.updateWorker(roomId, "OPENING", "页面加载中...", { countdown: null, nickname: null });
      await Utils.sleep(1e3);
      this.startCommandListener(roomId);
      window.addEventListener("beforeunload", () => {
        GlobalState.updateWorker(Utils.getCurrentRoomId(), "DISCONNECTED", "连接已断开...");
        if (this.pauseSentinelInterval) {
          clearInterval(this.pauseSentinelInterval);
        }
      });
      Utils.log("正在等待页面关键元素 (#js-player-video) 加载...");
      const criticalElement = await DOM.findElement(SETTINGS.SELECTORS.criticalElement, SETTINGS.ELEMENT_WAIT_TIMEOUT);
      if (!criticalElement) {
        Utils.log("页面关键元素加载超时,此标签页可能无法正常工作,即将关闭。");
        await this.selfClose(roomId);
        return;
      }
      Utils.log("页面关键元素已加载。");
      Utils.log("开始检测 UI 版本 和红包活动...");
      if (window.location.href.includes("/beta")) {
        GlobalState.updateWorker(roomId, "OPENING", "切换旧版UI...");
        localStorage.setItem("newWebLive", "A");
        window.location.href = window.location.href.replace("/beta", "");
      }
      Utils.log("确认进入稳定工作状态,执行身份核销。");
      const pendingWorkers = GM_getValue("qmx_pending_workers", []);
      const myIndex = pendingWorkers.indexOf(roomId);
      if (myIndex > -1) {
        pendingWorkers.splice(myIndex, 1);
        GM_setValue("qmx_pending_workers", pendingWorkers);
        Utils.log(`房间 ${roomId} 已从待处理列表中移除。`);
      }
      const anchorNameElement = document.querySelector(SETTINGS.SELECTORS.anchorName);
      const nickname = anchorNameElement ? anchorNameElement.textContent.trim() : `房间${roomId}`;
      GlobalState.updateWorker(roomId, "WAITING", "寻找任务中...", { nickname, countdown: null });
      const limitState = GlobalState.getDailyLimit();
      if (limitState?.reached) {
        Utils.log("初始化检查:检测到全局上限旗标。");
        if (SETTINGS.DAILY_LIMIT_ACTION === "CONTINUE_DORMANT") {
          await this.enterDormantMode();
        } else {
          await this.selfClose(roomId);
        }
        return;
      }
      this.findAndExecuteNextTask(roomId);
      if (SETTINGS.AUTO_PAUSE_ENABLED) {
        this.pauseSentinelInterval = setInterval(() => this.autoPauseVideo(), 8e3);
      }
    },
    async findAndExecuteNextTask(roomId) {
      if (this.healthCheckTimeoutId) {
        clearTimeout(this.healthCheckTimeoutId);
        this.healthCheckTimeoutId = null;
      }
      this.stallLevel = 0;
      const limitState = GlobalState.getDailyLimit();
      if (limitState?.reached) {
        Utils.log(`[上限检查] 房间 ${roomId} 检测到已达每日上限。`);
        if (SETTINGS.DAILY_LIMIT_ACTION === "CONTINUE_DORMANT") {
          await this.enterDormantMode();
        } else {
          await this.selfClose(roomId);
        }
        return;
      }
      if (SETTINGS.AUTO_PAUSE_ENABLED) this.autoPauseVideo();
      Utils.log(`[调试] 开始在房间 ${roomId} 寻找红包...`);
      const outerContainers = document.querySelectorAll(SETTINGS.SELECTORS.redEnvelopeContainer);
      let redEnvelopeDiv = null;
      let statusText = "";
      let countdownText = "";
      let foundValidContainer = false;
      for (let i = 0; i < outerContainers.length; i++) {
        const outer = outerContainers[i];
        const hasBoxIcon = outer.querySelector(SETTINGS.SELECTORS.boxIcon);
        if (!hasBoxIcon) {
          continue;
        }
        foundValidContainer = true;
        const headlineElem = outer.querySelector(SETTINGS.SELECTORS.statusHeadline);
        const headline = headlineElem ? headlineElem.textContent.trim() : "";
        const contentElem = outer.querySelector(SETTINGS.SELECTORS.countdownTimer);
        const content = contentElem ? contentElem.textContent.trim() : "";
        Utils.log(`[调试] 容器 #${i} 标题: "${headline}" | 内容: "${content}"`);
        if (headline.includes("倒计时") && content.includes(":")) {
          redEnvelopeDiv = outer;
          statusText = headline;
          countdownText = content;
          break;
        } else if (headline.includes("可领取") || headline.includes("立即")) {
          redEnvelopeDiv = outer;
          statusText = headline;
          countdownText = "";
          break;
        }
        if (!redEnvelopeDiv) {
          redEnvelopeDiv = outer;
          statusText = headline;
          countdownText = content;
        }
      }
      if (!foundValidContainer) {
        Utils.log(`[调试] 初次查找未发现红包容器,等待 ${SETTINGS.RED_ENVELOPE_LOAD_TIMEOUT / 1e3} 秒后重新检查...`);
        const waitedDiv = await this.waitForRedEnvelopeContainer(SETTINGS.RED_ENVELOPE_LOAD_TIMEOUT);
        if (!waitedDiv) {
          Utils.log("[判断] 超时未找到红包容器,判定活动已结束。");
          GlobalState.updateWorker(roomId, "SWITCHING", "活动已结束, 切换中", { countdown: null });
          await this.switchRoom();
          return;
        }
        foundValidContainer = true;
        const headlineElem = waitedDiv.querySelector(SETTINGS.SELECTORS.statusHeadline);
        const contentElem = waitedDiv.querySelector(SETTINGS.SELECTORS.countdownTimer);
        statusText = headlineElem ? headlineElem.textContent.trim() : "";
        countdownText = contentElem ? contentElem.textContent.trim() : "";
        redEnvelopeDiv = waitedDiv;
      }
      if (countdownText.includes(":")) {
        const timeMatch = countdownText.match(/(\d+):(\d+)/);
        if (timeMatch) {
          const minutes = parseInt(timeMatch[1]);
          const seconds = parseInt(timeMatch[2]);
          const remainingSeconds = minutes * 60 + seconds;
          const currentCount = this.remainingTimeMap.get(remainingSeconds) || 0;
          this.remainingTimeMap.set(remainingSeconds, currentCount + 1);
          if (Array.from(this.remainingTimeMap.values()).some((value) => value > 3)) {
            GlobalState.updateWorker(roomId, "SWITCHING", "倒计时卡死, 切换中", { countdown: null });
            await this.switchRoom();
            return;
          }
          this.currentTaskEndTime = Date.now() + remainingSeconds * 1e3;
          Utils.log(`[任务] 识别到倒计时: ${timeMatch[0]}`);
          const prizes = await this.extractPrizeInfo(redEnvelopeDiv);
          GlobalState.updateWorker(roomId, "WAITING", `倒计时 ${timeMatch[0]}`, {
            countdown: { endTime: this.currentTaskEndTime },
            prizes
          });
          const wakeUpDelay = Math.max(0, remainingSeconds * 1e3 - 1500);
          setTimeout(() => this.claimAndRecheck(roomId), wakeUpDelay);
          this.startHealthChecks(roomId, redEnvelopeDiv);
        } else {
          Utils.log(`[错误] 无法解析时间: "${countdownText}"`);
          setTimeout(() => this.findAndExecuteNextTask(roomId), 5e3);
        }
      } else if (/可领取|立即/.test(statusText)) {
        Utils.log(`[任务] 检测到可领取状态: ${statusText}`);
        GlobalState.updateWorker(roomId, "CLAIMING", "立即领取中...");
        await this.claimAndRecheck(roomId);
      } else {
        Utils.log(`[警告] 红包容器存在但状态无法识别 - 标题: "${statusText}" | 内容: "${countdownText}"`);
        if (statusText.includes("已领完") || statusText.includes("已结束") || statusText.includes("已抢完")) {
          Utils.log("[判断] 红包已领完或活动已结束。");
          GlobalState.updateWorker(roomId, "SWITCHING", "红包已领完, 切换中", { countdown: null });
          await this.switchRoom();
        } else {
          GlobalState.updateWorker(roomId, "WAITING", `状态未知, 重试中...`, { countdown: null });
          setTimeout(() => this.findAndExecuteNextTask(roomId), 5e3);
        }
      }
    },
async extractPrizeInfo(redEnvelopeDiv) {
      try {
        Utils.log("[奖励提取] 开始提取奖励信息...");
        const clickableContainer = redEnvelopeDiv.querySelector(SETTINGS.SELECTORS.clickableContainer);
        if (!clickableContainer) {
          Utils.log("[奖励提取] 未找到可点击容器");
          return null;
        }
        if (!await DOM.safeClick(clickableContainer, "红包容器(提取奖励)")) {
          Utils.log("[奖励提取] 点击失败");
          return null;
        }
        const popup = await DOM.findElement(SETTINGS.SELECTORS.popupModal, 3e3);
        if (!popup) {
          Utils.log("[奖励提取] 弹窗未出现");
          return null;
        }
        await Utils.sleep(500);
        const prizes = [];
        const prizeContainer = popup.querySelector(SETTINGS.SELECTORS.prizeContainer);
        if (prizeContainer) {
          const prizeItems = prizeContainer.querySelectorAll(SETTINGS.SELECTORS.prizeItem);
          Utils.log(`[奖励提取] 找到 ${prizeItems.length} 个奖励项容器`);
          for (const item of prizeItems) {
            const imgElement = item.querySelector(SETTINGS.SELECTORS.prizeImage);
            const countElement = item.querySelector(SETTINGS.SELECTORS.prizeCount);
            if (imgElement && countElement && countElement.textContent.trim()) {
              const prizeData = {
                img: imgElement.src,
                text: countElement.textContent.trim(),
                name: imgElement.getAttribute("alt") || ""
              };
              Utils.log(`[奖励提取] ✓ 提取到奖励: ${prizeData.text}, 名称: ${prizeData.name}`);
              prizes.push(prizeData);
            } else {
              Utils.log(`[奖励提取] ✗ 跳过不完整项 - 图片: ${!!imgElement}, 数量: ${countElement ? `"${countElement.textContent.trim()}"` : "null"}`);
            }
          }
        } else {
          Utils.log("[奖励提取] 未找到奖励容器");
        }
        Utils.log(`[奖励提取] 成功提取 ${prizes.length} 个有效奖励`);
        const closeBtn = popup.querySelector(SETTINGS.SELECTORS.closeButton);
        if (closeBtn) {
          await DOM.safeClick(closeBtn, "关闭按钮(提取奖励)");
          await Utils.sleep(300);
        }
        return prizes.length > 0 ? prizes : null;
      } catch (error) {
        Utils.log(`[奖励提取] 提取失败: ${error.message}`);
        try {
          const closeBtn = document.querySelector(SETTINGS.SELECTORS.closeButton);
          if (closeBtn) {
            await DOM.safeClick(closeBtn, "关闭按钮(异常)");
          }
        } catch {
        }
        return null;
      }
    },
startHealthChecks(roomId, redEnvelopeDiv) {
      const CHECK_INTERVAL = SETTINGS.HEALTHCHECK_INTERVAL;
      const STALL_THRESHOLD = 4;
      const check = () => {
        const contentElem = redEnvelopeDiv.querySelector(SETTINGS.SELECTORS.countdownTimer);
        const headlineElem = redEnvelopeDiv.querySelector(SETTINGS.SELECTORS.statusHeadline);
        const currentPageContent = contentElem ? contentElem.textContent.trim() : "";
        const currentPageHeadline = headlineElem ? headlineElem.textContent.trim() : "";
        if (!currentPageHeadline.includes("倒计时") || !currentPageContent.includes(":")) {
          Utils.log("[哨兵] 检测到状态变化,停止监控。");
          return;
        }
        const scriptRemainingSeconds = (this.currentTaskEndTime - Date.now()) / 1e3;
        const timeMatch = currentPageContent.match(/(\d+):(\d+)/);
        if (!timeMatch) {
          Utils.log("[哨兵] 无法解析UI倒计时,跳过本次检查。");
          return;
        }
        const pMin = parseInt(timeMatch[1]);
        const pSec = parseInt(timeMatch[2]);
        const pageRemainingSeconds = pMin * 60 + pSec;
        const deviation = Math.abs(scriptRemainingSeconds - pageRemainingSeconds);
        const currentFormattedTime = Utils.formatTime(scriptRemainingSeconds);
        const pageFormattedTime = Utils.formatTime(pageRemainingSeconds);
        Utils.log(
          `[哨兵] 脚本倒计时: ${currentFormattedTime} | UI显示: ${pageFormattedTime} | 差值: ${deviation.toFixed(2)}秒`
        );
        Utils.log(`校准模式开启状态为 ${SETTINGS.CALIBRATION_MODE_ENABLED ? "开启" : "关闭"}`);
        if (SETTINGS.CALIBRATION_MODE_ENABLED) {
          if (deviation <= STALL_THRESHOLD) {
            const difference = scriptRemainingSeconds - pageRemainingSeconds;
            this.currentTaskEndTime = Date.now() + pageRemainingSeconds * 1e3;
            if (deviation > 0.1) {
              const direction = difference > 0 ? "慢" : "快";
              const calibrationMessage = `${direction}${deviation.toFixed(1)}秒, 已校准`;
              Utils.log(`[校准] ${calibrationMessage}。新倒计时: ${pageFormattedTime}`);
              GlobalState.updateWorker(roomId, "WAITING", calibrationMessage, {
                countdown: { endTime: this.currentTaskEndTime }
              });
              setTimeout(() => {
                if (this.currentTaskEndTime > Date.now()) {
                  GlobalState.updateWorker(roomId, "WAITING", `倒计时...`, {
                    countdown: { endTime: this.currentTaskEndTime }
                  });
                }
              }, 2500);
            } else {
              GlobalState.updateWorker(roomId, "WAITING", `倒计时...`, {
                countdown: { endTime: this.currentTaskEndTime }
              });
            }
            this.consecutiveStallCount = 0;
            this.previousDeviation = 0;
            this.stallLevel = 0;
          } else {
            const deviationIncreasing = deviation > this.previousDeviation;
            this.previousDeviation = deviation;
            if (deviationIncreasing) {
              this.consecutiveStallCount++;
              Utils.log(`[警告] 检测到UI卡顿第 ${this.consecutiveStallCount} 次,差值: ${deviation.toFixed(2)}秒`);
            } else {
              this.consecutiveStallCount = Math.max(0, this.consecutiveStallCount - 1);
            }
            if (this.consecutiveStallCount >= 3) {
              Utils.log(`[严重] 连续检测到卡顿且差值增大,判定为卡死状态。`);
              GlobalState.updateWorker(roomId, "SWITCHING", "倒计时卡死, 切换中", { countdown: null });
              clearTimeout(this.healthCheckTimeoutId);
              this.switchRoom();
              return;
            }
            this.stallLevel = 1;
            GlobalState.updateWorker(roomId, "ERROR", `UI卡顿 (${deviation.toFixed(1)}秒)`, {
              countdown: { endTime: this.currentTaskEndTime }
            });
          }
        } else {
          if (deviation > STALL_THRESHOLD) {
            if (this.stallLevel === 0) {
              Utils.log(`[哨兵] 检测到UI节流。脚本精确倒计时: ${currentFormattedTime} | UI显示: ${pageFormattedTime}`);
            }
            this.stallLevel = 1;
            GlobalState.updateWorker(roomId, "STALLED", `UI节流中...`, {
              countdown: { endTime: this.currentTaskEndTime }
            });
          } else {
            if (this.stallLevel > 0) {
              Utils.log("[哨兵] UI已从节流中恢复。");
              this.stallLevel = 0;
            }
            GlobalState.updateWorker(roomId, "WAITING", `倒计时 ${currentFormattedTime}`, {
              countdown: { endTime: this.currentTaskEndTime }
            });
          }
        }
        if (scriptRemainingSeconds > CHECK_INTERVAL / 1e3 + 1) {
          this.healthCheckTimeoutId = setTimeout(check, CHECK_INTERVAL);
        }
      };
      this.healthCheckTimeoutId = setTimeout(check, CHECK_INTERVAL);
    },
async claimAndRecheck(roomId) {
      if (this.healthCheckTimeoutId) {
        clearTimeout(this.healthCheckTimeoutId);
        this.healthCheckTimeoutId = null;
      }
      Utils.log(`[领取] 房间 ${roomId} 准备触发红包弹窗...`);
      GlobalState.updateWorker(roomId, "CLAIMING", "尝试打开红包...", { countdown: null });
      const outerContainers = document.querySelectorAll(SETTINGS.SELECTORS.redEnvelopeContainer);
      let targetBtn = null;
      for (const outer of outerContainers) {
        const hasBoxIcon = outer.querySelector(SETTINGS.SELECTORS.boxIcon);
        if (!hasBoxIcon) {
          continue;
        }
        const innerContainer = outer.querySelector(SETTINGS.SELECTORS.clickableContainer);
        if (!innerContainer) {
          continue;
        }
        const headlineElem = outer.querySelector(SETTINGS.SELECTORS.statusHeadline);
        const contentElem = outer.querySelector(SETTINGS.SELECTORS.countdownTimer);
        const headline = headlineElem ? headlineElem.textContent.trim() : "";
        const content = contentElem ? contentElem.textContent.trim() : "";
        if (headline.includes("倒计时") && content.includes(":") || headline.includes("可领取") || headline.includes("立即")) {
          targetBtn = innerContainer;
          Utils.log(`[领取] 锁定点击目标 - 标题: "${headline}" | 内容: "${content}"`);
          break;
        }
      }
      if (!targetBtn) {
        Utils.log("[领取] 未能锁定有效的点击目标,尝试兜底查找...");
        const outerDiv = await DOM.findElement(SETTINGS.SELECTORS.redEnvelopeContainer, 3e3);
        if (outerDiv) {
          targetBtn = outerDiv.querySelector(SETTINGS.SELECTORS.clickableContainer) || outerDiv;
        }
      }
      if (!await DOM.safeClick(targetBtn, "红包内层容器")) {
        Utils.log("[领取] 点击失败,重新寻找任务。");
        await Utils.sleep(2e3);
        this.findAndExecuteNextTask(roomId);
        return;
      }
      const popup = await DOM.findElement(SETTINGS.SELECTORS.popupModal, SETTINGS.POPUP_WAIT_TIMEOUT);
      if (!popup) {
        Utils.log("等待红包弹窗超时,重新寻找任务。");
        await Utils.sleep(2e3);
        this.findAndExecuteNextTask(roomId);
        return;
      }
      const singleBag = popup.querySelector(SETTINGS.SELECTORS.singleBag);
      const openBtn = singleBag ? singleBag.querySelector(SETTINGS.SELECTORS.openButton) : null;
      if (openBtn) {
        const btnText = openBtn.textContent.trim();
        Utils.log(`[领取] 找到打开按钮,文本: "${btnText}"`);
        if (/(\d+)秒后/.test(btnText)) {
          const waitMatch = btnText.match(/(\d+)秒后/);
          if (waitMatch) {
            const waitSeconds = parseInt(waitMatch[1]);
            Utils.log(`[领取] 按钮显示还需等待 ${waitSeconds} 秒,等待中...`);
            GlobalState.updateWorker(roomId, "CLAIMING", `等待 ${waitSeconds} 秒...`, { countdown: null });
            await Utils.sleep((waitSeconds + 1) * 1e3);
          }
        }
      }
      const clickTarget = openBtn || singleBag || popup;
      const targetName = openBtn ? "红包打开按钮" : singleBag ? "红包主体" : "红包弹窗";
      if (await DOM.safeClick(clickTarget, targetName)) {
        if (await DOM.checkForLimitPopup()) {
          GlobalState.setDailyLimit(true);
          Utils.log("检测到每日上限!");
          if (SETTINGS.DAILY_LIMIT_ACTION === "CONTINUE_DORMANT") {
            await this.enterDormantMode();
          } else {
            await this.selfClose(roomId);
          }
          return;
        }
        await Utils.sleep(1500);
        const successIndicator = await DOM.findElement(SETTINGS.SELECTORS.rewardSuccessIndicator, 3e3, popup);
        const reward = successIndicator ? "领取成功 " : "空包或失败";
        Utils.log(`领取操作完成,结果: ${reward}`);
        GlobalState.updateWorker(roomId, "WAITING", `领取到: ${reward}`, { countdown: null });
        const closeBtn = document.querySelector(SETTINGS.SELECTORS.closeButton);
        await DOM.safeClick(closeBtn, "关闭按钮");
      } else {
        Utils.log("[领取] 点击打开操作失败。");
      }
      STATE.lastActionTime = Date.now();
      Utils.log("操作完成,2秒后在本房间内寻找下一个任务...");
      await Utils.sleep(2e3);
      this.findAndExecuteNextTask(roomId);
    },
async autoPauseVideo() {
      if (STATE.isSwitchingRoom || Date.now() - STATE.lastActionTime < SETTINGS.AUTO_PAUSE_DELAY_AFTER_ACTION) {
        return;
      }
      let pauseBtn = document.querySelector(SETTINGS.SELECTORS.pauseButton);
      if (!pauseBtn) {
        const player = document.querySelector("#js-player-video");
        if (player) {
          player.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
          await Utils.sleep(500);
          pauseBtn = document.querySelector(SETTINGS.SELECTORS.pauseButton);
        }
      }
      if (pauseBtn) {
        const isPauseIcon = pauseBtn.innerHTML.includes("M9.5 7");
        if (isPauseIcon) {
          if (await DOM.safeClick(pauseBtn, "暂停按钮")) {
            Utils.log("视频已通过脚本暂停。");
          }
        }
      }
    },
async switchRoom() {
      if (this.healthCheckTimeoutId) {
        clearTimeout(this.healthCheckTimeoutId);
        this.healthCheckTimeoutId = null;
      }
      if (STATE.isSwitchingRoom) return;
      STATE.isSwitchingRoom = true;
      Utils.log("开始执行切换房间流程...");
      const currentRoomId = Utils.getCurrentRoomId();
      GlobalState.updateWorker(currentRoomId, "SWITCHING", "查找新房间...");
      try {
        const apiRoomUrls = await DouyuAPI.getRooms(SETTINGS.API_ROOM_FETCH_COUNT, currentRoomId);
        const currentState = GlobalState.get();
        const openedRoomIds = new Set(Object.keys(currentState.tabs));
        const nextUrl = apiRoomUrls.find((url) => {
          const rid = url.match(/\/(\d+)/)?.[1];
          return rid && !openedRoomIds.has(rid);
        });
        if (nextUrl) {
          Utils.log(`确定下一个房间链接: ${nextUrl}`);
          const nextRoomId = nextUrl.match(/\/(\d+)/)[1];
          const pendingWorkers = GM_getValue("qmx_pending_workers", []);
          pendingWorkers.push(nextRoomId);
          GM_setValue("qmx_pending_workers", pendingWorkers);
          Utils.log(`已将房间 ${nextRoomId} 加入待处理列表。`);
          if (window.location.href.includes("/beta") || localStorage.getItem("newWebLive") !== "A") {
            localStorage.setItem("newWebLive", "A");
          }
          GM_openInTab(nextUrl, { active: false, setParent: true });
          await Utils.sleep(SETTINGS.CLOSE_TAB_DELAY);
          await this.selfClose(currentRoomId);
        } else {
          Utils.log("API未能返回任何新的、未打开的房间,将关闭当前页。");
          await this.selfClose(currentRoomId);
        }
      } catch (error) {
        Utils.log(`切换房间时发生严重错误: ${error.message}`);
        await this.selfClose(currentRoomId);
      }
    },
async enterDormantMode() {
      const roomId = Utils.getCurrentRoomId();
      Utils.log(`[上限处理] 房间 ${roomId} 进入休眠模式。`);
      GlobalState.updateWorker(roomId, "DORMANT", "休眠中 (等待北京时间0点)", { countdown: null });
      const now = Utils.getBeijingTime();
      const tomorrow = new Date(now.getTime());
      tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
      tomorrow.setUTCHours(0, 0, 30, 0);
      const msUntilMidnight = tomorrow.getTime() - now.getTime();
      Utils.log(`将在 ${Math.round(msUntilMidnight / 1e3 / 60)} 分钟后自动刷新页面 (基于北京时间)。`);
      setTimeout(() => window.location.reload(), msUntilMidnight);
    },
async selfClose(roomId, fromCloseAll = false) {
      Utils.log(`本单元任务结束 (房间: ${roomId}),尝试更新状态并关闭。`);
      if (this.pauseSentinelInterval) {
        clearInterval(this.pauseSentinelInterval);
      }
      if (fromCloseAll) {
        Utils.log(`[关闭所有] 跳过状态更新,直接关闭标签页 (房间: ${roomId})`);
        GlobalState.removeWorker(roomId);
        await Utils.sleep(100);
        this.closeTab();
        return;
      }
      GlobalState.updateWorker(roomId, "SWITCHING", "任务结束,关闭中...");
      await Utils.sleep(100);
      GlobalState.removeWorker(roomId);
      await Utils.sleep(300);
      this.closeTab();
    },
closeTab() {
      try {
        window.close();
      } catch (e) {
        window.location.replace("about:blank");
        Utils.log(`关闭失败,故障为: ${e.message}`);
      }
    },

startCommandListener(roomId) {
      this.commandChannel = new BroadcastChannel("douyu_qmx_commands");
      Utils.log(`工作页 ${roomId} 已连接到指令广播频道。`);
      this.commandChannel.onmessage = (event) => {
        const { action, target } = event.data;
        if (target === roomId || target === "*") {
          Utils.log(`接收到广播指令: ${action} for target ${target}`);
          if (action === "CLOSE") {
            this.selfClose(roomId, false);
          } else if (action === "CLOSE_ALL") {
            this.selfClose(roomId, true);
          }
        }
      };
      window.addEventListener("beforeunload", () => {
        if (this.commandChannel) {
          this.commandChannel.close();
        }
      });
    },
async waitForRedEnvelopeContainer(timeout) {
      const startTime = Date.now();
      while (Date.now() - startTime < timeout) {
        const containers = document.querySelectorAll(SETTINGS.SELECTORS.redEnvelopeContainer);
        for (const container of containers) {
          const hasBoxIcon = container.querySelector(SETTINGS.SELECTORS.boxIcon);
          if (hasBoxIcon) {
            Utils.log(`[等待] 找到红包容器,耗时 ${Date.now() - startTime}ms`);
            return container;
          }
        }
        await Utils.sleep(300);
      }
      Utils.log(`[等待] 等待红包容器超时(${timeout}ms)`);
      return null;
    }
  };
  (function() {
    function main() {
      initHackTimer("HackTimerWorker.js");
      const currentUrl = window.location.href;
      const controlIds = [SETTINGS.CONTROL_ROOM_ID, SETTINGS.TEMP_CONTROL_ROOM_RID].filter(Boolean);
      Utils.log(`控制页识别ID列表: ${controlIds.join(", ")}`);
      const isControlRoom = controlIds.some(
        (id) => currentUrl.includes(`/${id}`) || currentUrl.includes(`/topic/`) && currentUrl.includes(`rid=${id}`)
      );
      if (isControlRoom) {
        ControlPage.init();
        return;
      }
      const roomId = Utils.getCurrentRoomId();
      if (roomId && (currentUrl.match(/douyu\.com\/(?:beta\/)?(\d+)/) || currentUrl.match(/douyu\.com\/(?:beta\/)?topic\/.*rid=(\d+)/))) {
        const globalTabs = GlobalState.get().tabs;
        const pendingWorkers = GM_getValue("qmx_pending_workers", []);
        if (Object.hasOwn(globalTabs, roomId) || pendingWorkers.includes(roomId)) {
          Utils.log(`[身份验证] 房间 ${roomId} 身份合法,授权初始化。`);
          const pendingIndex = pendingWorkers.indexOf(roomId);
          if (Object.hasOwn(globalTabs, roomId) && pendingIndex > -1) {
            pendingWorkers.splice(pendingIndex, 1);
            GM_setValue("qmx_pending_workers", pendingWorkers);
          }
          WorkerPage.init();
        } else {
          Utils.log(`[身份验证] 房间 ${roomId} 未在全局状态或待处理列表中,脚本不活动。`);
        }
      } else {
        Utils.log("当前页面非控制页或工作页,脚本不活动。");
      }
    }
    Utils.log(`脚本将在 ${SETTINGS.INITIAL_SCRIPT_DELAY / 1e3} 秒后开始初始化...`);
    setTimeout(main, SETTINGS.INITIAL_SCRIPT_DELAY);
  })();

})();