um-98t-list-preview

[Refactored] v2.4.0 - 支持JSON批量导入与持续添加,引入avIDs V2结构(ID/评分/时间),集成JavDB已阅高亮与同步。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         um-98t-list-preview
// @description  [Refactored] v2.4.0 - 支持JSON批量导入与持续添加,引入avIDs V2结构(ID/评分/时间),集成JavDB已阅高亮与同步。
// @version      2.5.0
// @icon         https://www.google.com/s2/favicons?sz=64&domain=www.sehuatang.net
// @author       UnforgetMemory
// @namespace    https://www.sehuatang.net/
// @match        https://www.sehuatang.net/forum*
// @match        https://www.sehuatang.org/forum*
// @match        https://www.sehuatang.net/forum.php?mod=forumdisplay&fid=103&page=*
// @match        https://www.sehuatang.org/forum.php?mod=forumdisplay&fid=103&page=*
// @match        https://javdb.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://cdn.jsdelivr.net/npm/[email protected]/i18next.min.js
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_setClipboard
// @license      GNU GPLv3
// ==/UserScript==

(function () {
  "use strict";

  // --- CONFIGURATION ---
  const CONFIG = {
    AVID_REGEX: /[a-zA-Z]{2,6}[-\s]?\d{2,5}/gi,
    JAVDB_HOST: "javdb.com",
    HTTP_HEADERS: {
      "User-Agent": window.navigator.userAgent,
      Accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
      Cookie: document.cookie,
    },
    LOCAL_STORAGE_KEYS: {
      VIEWED_AVIDS: "68905cf391b2428572e6446042ab1029",
      VIEWED_US_TITLES: "abba24c58fc69bf0955bddc7a0eadee1",
      HIDE_VIEWED_MODE: "780fbed5c332f7f96ca73e19e94a9749",
      JIANGUOYUN: "97f6483755d45ad927caf3108b61be91",
      LOCALE: "ee2757153264e82a1c8f64db8ddcb3e2",
    },
    JIANGUOYUN: {
      AVIDS_FILENAME: "95551967d3da7c5af36b141f630683c4",
      US_FILENAME: "53648e7622cfa657f1f6de856efd67c9",
    },
    LOCALES: { enUS: "en-US", zhCN: "zh-CN", zhHK: "zh-HK", zhTW: "zh-TW" },
  };

  // --- GLOBAL STATE ---
  const STATE = {
    totalTasks: 0,
    finishedTasks: 0,
    processedIds: new Set(),
    filteredCount: 0,
    isJavDB: location.hostname.includes("javdb"),
  };

  // --- I18N INITIALIZATION ---
  i18next.init({
    lng: GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.LOCALE, CONFIG.LOCALES.enUS),
    fallbackLng: CONFIG.LOCALES.enUS,
    resources: {
      "en-US": {
        translation: {
          Language: "🕮 Language",
          "Hide Viewed": "Hide Viewed",
          "Manual Add": "📝 Manual Add",
          "Check Viewed Status": "🔍 Check Viewed Status",
          Jianguoyun: "☁️ Jianguoyun",
          "Jianguoyun Config": "☁️ Jianguoyun Config",
          "Upload To Jianguoyun": "↑ Upload To ☁️ Jianguoyun",
          "Download from Jianguoyun": "↓ Download from ☁️ Jianguoyun",
          "Local and Jianguoyun merge": "🔄 Local and ☁️ Jianguoyun merge",
          "DAV URL": "☁️  DAV URL",
          Account: "👤 Account",
          Password: "🔑 Password",
          Save: "Save",
          Close: "Close",
          "Save Successful!": "Save Successful!",
          "Upload successful!": "Upload successful!",
          "Sync successful!": "Sync successful!",
          "Download successful!": "Download successful!",
          "Check Config!": "Check Config!",
          "Update Bad": "Update Bad",
          "Bad Download": "Bad Download Data",
          "Viewed Total": "Viewed Total",
          click_all_magnet: "Copy All Magnet Links",
          copy_btn_loading: "Loading... ({{loaded}}/{{total}})",
          copy_btn_ready: "⚡ Copy Magnets ({{count}})",
          copy_done: "Copied {{count}} magnet links!",
          no_new_magnets: "No new magnet links found.",
          header_info: "Hidden: {{hidden}} | History: {{total}}",
          javdb_detected: "JavDB detected: {{count}} viewed items dimmed.",
          panel_title: "Add Record",
          panel_id_placeholder: "ID or JSON [{id,rating,time}...]",
          panel_rating_label: "Rating (0-10):",
          added_msg: "Added {{count}} items.",
          invalid_json: "Invalid JSON format",
          check_viewed_title: "Check Viewed Status",
          check_viewed_id_placeholder: "Enter AV ID",
          check_viewed_check_btn: "Check",
          check_viewed_status: "Status",
          check_viewed_not_viewed: "Not Viewed",
          check_viewed_viewed_on: "Viewed on",
          check_viewed_rating: "Rating",
        },
      },
      "zh-CN": {
        translation: {
          Language: "🕮 语言(简体)",
          "Hide Viewed": "隐藏已阅",
          "Manual Add": "📝 手动添加记录",
          "Check Viewed Status": "🔍 查询已阅状态",
          Jianguoyun: "☁️ 坚果云",
          "Jianguoyun Config": "☁️ 坚果云配置",
          "Upload To Jianguoyun": "↑ 上传至 ☁️ 坚果云",
          "Download from Jianguoyun": "从 ☁️ 坚果云 ↓ 下载",
          "Local and Jianguoyun merge": "🔄 双端同步 ☁️ 坚果云 ",
          "DAV URL": "☁️  DAV URL",
          Account: "👤 账号",
          Password: "🔑 密码",
          Save: "保存",
          Close: "关闭",
          "Save Successful!": "保存成功!",
          "Upload successful!": "上传成功!",
          "Sync successful!": "同步成功!",
          "Download successful!": "下载完成!",
          "Check Config!": "检查配置!",
          "Update Bad": "更新失败",
          "Bad Download": "下载数据出错",
          "Viewed Total": "浏览量",
          click_all_magnet: "复制所有磁力",
          copy_btn_loading: "加载中... ({{loaded}}/{{total}})",
          copy_btn_ready: "⚡ 一键复制磁力 ({{count}})",
          copy_done: "已复制 {{count}} 个磁力链接!",
          no_new_magnets: "未发现新的磁力链接",
          header_info: "本页隐藏: {{hidden}} | 历史总阅: {{total}}",
          javdb_detected: "JavDB检测: 已淡化 {{count}} 个已阅条目。",
          panel_title: "添加浏览记录",
          panel_id_placeholder: "输入番号 或 JSON数组",
          panel_rating_label: "评分 (0-10):",
          added_msg: "已添加 {{count}} 条记录",
          invalid_json: "无效的 JSON 格式",
          check_viewed_title: "查询已阅状态",
          check_viewed_id_placeholder: "输入 AV 番号",
          check_viewed_check_btn: "查询",
          check_viewed_status: "状态",
          check_viewed_not_viewed: "未阅",
          check_viewed_viewed_on: "已阅于",
          check_viewed_rating: "评分",
        },
      },
    },
  });

  // --- STYLES ---
  GM_addStyle(`
    :root { --bg-color: #121212; --card-bg-color: #1e1e1e; --text-color: #e0e0e0; --accent-color: #03dac6; --border-color: #333333; }
    /* Sehuatang Styles */
    #filtered-info-bar { background-color: var(--card-bg-color); color: #a0a0a0; padding: 10px 25px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 1000; }
    .action-btn { background-color: var(--accent-color); color: #000; border: none; padding: 6px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; }
    .action-btn:disabled { background-color: #3a3a3a; color: #888; }
    #modern-preview-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 25px; padding: 25px; }
    .preview-card { background-color: var(--card-bg-color); border-radius: 12px; border: 1px solid var(--border-color); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.4); transition: transform 0.3s ease; }
    .preview-card:hover { transform: translateY(-8px); }
    .preview-card.viewed { opacity: 0.6; }
    .preview-card.viewed:hover { opacity: 1; }
    .card-image-container { aspect-ratio: 16 / 10; background-color: #2a2a2a; overflow: hidden; position: relative; }
    .card-image-container img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; }
    .preview-card:hover .card-image-container img { transform: scale(1.05); }
    .card-content { padding: 15px; display: flex; flex-direction: column; flex-grow: 1; }
    .card-title a { color: var(--text-color); text-decoration: none; font-size: 1.1rem; font-weight: 600; }
    .card-links { margin-top: auto; display: flex; justify-content: flex-end; gap: 15px; }
    .card-links a { font-size: 1.5rem; padding: 6px 12px; border-radius: 8px; text-decoration: none; }
    .magnet-link { background-color: #443b17; color: #ffc107; }
    .torrent-link { background-color: #1c3a1e; color: #4caf50; }

    /* JavDB Specific Styles */
    body.javdb-enhanced .item.viewed {
        opacity: 0.3 !important;
        transition: opacity 0.3s ease-in-out;
        filter: grayscale(80%);
    }
    body.javdb-enhanced .item.viewed:hover {
        opacity: 1 !important;
        filter: grayscale(0%);
    }
    body.javdb-enhanced .item.viewed::after {
        content: "👁";
        position: absolute;
        top: 5px;
        right: 5px;
        font-size: 1.2rem;
        background: rgba(0,0,0,0.5);
        border-radius: 50%;
        padding: 2px;
        color: #03dac6;
        pointer-events: none;
        z-index: 10;
    }

    /* Manual Add Panel Styles */
    #um-manual-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; justify-content: center; align-items: center; }
    #um-manual-panel { background: var(--card-bg-color); padding: 25px; border-radius: 12px; border: 1px solid var(--border-color); width: 400px; display: flex; flex-direction: column; gap: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
    #um-manual-panel h3 { margin: 0; color: var(--accent-color); text-align: center; }
    .um-input-group { display: flex; flex-direction: column; gap: 5px; }
    .um-input-group label { font-size: 0.9rem; color: #aaa; }
    .um-input { background: #2a2a2a; border: 1px solid #444; color: white; padding: 10px; border-radius: 6px; outline: none; }
    .um-input:focus { border-color: var(--accent-color); }
    .um-btn-row { display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px; }
    .um-btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-weight: bold; }
    .um-btn-primary { background: var(--accent-color); color: #000; }
    .um-btn-secondary { background: #444; color: #ccc; }

    /* Check Viewed Panel Styles */
    #um-check-viewed-panel { position: fixed; top: 50px; left: 50%; transform: translateX(-50%); background: var(--card-bg-color); padding: 20px; border-radius: 12px; border: 1px solid var(--border-color); width: 350px; display: flex; flex-direction: column; gap: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 10001; cursor: move; }
    #um-check-viewed-panel h3 { margin: 0; color: var(--accent-color); text-align: center; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); }
    #um-check-viewed-panel .um-input-group { flex-direction: row; align-items: center; }
    #um-check-viewed-panel .um-input { flex-grow: 1; }
    #um-check-viewed-panel .um-btn-primary { margin-left: 10px; }
    #um-check-viewed-result { margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border-color); }
    .um-result-row { display: flex; justify-content: space-between; padding: 5px 0; }
    .um-result-label { font-weight: bold; color: #aaa; }
    .um-result-value { color: var(--text-color); }
    .um-result-value.not-viewed { color: #f44336; }
    .um-result-value.viewed { color: #4caf50; }
    #um-check-viewed-close { position: absolute; top: 10px; right: 10px; background: none; border: none; color: #aaa; font-size: 20px; cursor: pointer; }
  `);

  // --- STORAGE HELPERS ---
  const gmFetch = (details) =>
    new Promise((resolve, reject) => {
      details.onload = resolve;
      details.onerror = reject;
      details.ontimeout = reject;
      GM_xmlhttpRequest(details);
    });
  const getJsonValue = (key, defaultValue = "[]") =>
    JSON.parse(GM_getValue(key, defaultValue));
  const setJsonValue = (key, value) => GM_setValue(key, JSON.stringify(value));
  const showToast = (title, icon = "success") =>
    Swal.fire({
      toast: true,
      position: "top-end",
      showConfirmButton: false,
      timer: 3000,
      title,
      icon,
    });

  // --- TIME UTILS ---
  const normalizeTime = (inputTime) => {
    if (!inputTime) return new Date().toISOString();
    try {
      const d = new Date(inputTime);
      if (isNaN(d.getTime())) {
        console.warn("Invalid time format:", inputTime, "Using current time.");
        return new Date().toISOString();
      }
      return d.toISOString();
    } catch (e) {
      return new Date().toISOString();
    }
  };

  /**
   * Data Structure V2:
   * Array of Objects: { id: string, rating: number (0-10), updatedAt: string (ISO UTC) }
   */
  const viewedAVIDs = {
    // 获取所有数据,包含自动迁移逻辑
    getAll: () => {
      let data = getJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS);

      // Migration Logic: Convert Array<string> to Array<Object>
      if (
        Array.isArray(data) &&
        data.length > 0 &&
        typeof data[0] === "string"
      ) {
        const now = new Date().toISOString();
        data = data.map((id) => ({
          id: id.toUpperCase(),
          rating: 0,
          updatedAt: now,
        }));
        setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS, data);
        console.log("Migration to avIDsV2 completed.");
      }
      return data;
    },
    // 检查是否存在
    has: (id) => {
      if (!id) return false;
      const cleanId = id.toUpperCase();
      return viewedAVIDs.getAll().some((item) => item.id === cleanId);
    },
    // 批量添加 (Batch Add) - 核心修改
    batchAdd: (items) => {
      if (!Array.isArray(items) || items.length === 0) return 0;

      const currentList = viewedAVIDs.getAll();
      const map = new Map();
      currentList.forEach((item) => map.set(item.id, item));

      let addedCount = 0;

      items.forEach((newItem) => {
        if (!newItem.id) return;

        const cleanId = newItem.id.toUpperCase().trim();
        const cleanRating = parseInt(newItem.rating);
        const finalRating = isNaN(cleanRating) ? 0 : cleanRating;
        const finalTime = normalizeTime(newItem.time);

        const existing = map.get(cleanId);

        if (existing) {
          existing.updatedAt = finalTime;
          if (finalRating > 0) existing.rating = finalRating;
        } else {
          map.set(cleanId, {
            id: cleanId,
            rating: finalRating,
            updatedAt: finalTime,
          });
        }
        addedCount++;
      });

      setJsonValue(
        CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS,
        Array.from(map.values())
      );
      return addedCount;
    },
    // 单个添加 (保留兼容性,底层调用batch)
    add: (id, rating = 0) => {
      viewedAVIDs.batchAdd([{ id, rating, time: new Date().toISOString() }]);
    },
    // 重置 (通常用于覆盖)
    reset: (data) => setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS, data),
    // 合并 (处理 V1 字符串数组和 V2 对象数组的混合情况)
    merge: (newData) => {
      const normalized = newData.map((item) => {
        if (typeof item === "string") {
          return { id: item, rating: 0 };
        }
        return item;
      });
      viewedAVIDs.batchAdd(normalized);
    },
  };

  const viewedUSTitles = {
    list: () => getJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_US_TITLES),
    has: (title) => viewedUSTitles.list().includes(title),
    add: (title) => {
      const c = viewedUSTitles.list();
      if (!c.includes(title))
        setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_US_TITLES, [...c, title]);
    },
    reset: (titles) =>
      setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_US_TITLES, titles),
    merge: (newTitles) =>
      viewedUSTitles.reset(_.union(viewedUSTitles.list(), newTitles)),
  };

  // --- UI COMPONENTS ---
  function createManualPanel() {
    if (document.getElementById("um-manual-overlay")) return;

    const overlay = document.createElement("div");
    overlay.id = "um-manual-overlay";

    const panel = document.createElement("div");
    panel.id = "um-manual-panel";

    const title = document.createElement("h3");
    title.innerText = i18next.t("panel_title");
    panel.appendChild(title);

    const idGroup = document.createElement("div");
    idGroup.className = "um-input-group";
    const idInput = document.createElement("input");
    idInput.type = "text";
    idInput.className = "um-input";
    idInput.placeholder = i18next.t("panel_id_placeholder");
    idInput.onkeydown = (e) => {
      if (e.key === "Enter") saveBtn.click();
      if (e.key === "Escape") overlay.remove();
    };
    idGroup.appendChild(idInput);
    panel.appendChild(idGroup);

    const rateGroup = document.createElement("div");
    rateGroup.className = "um-input-group";
    const rateLabel = document.createElement("label");
    rateLabel.innerText = i18next.t("panel_rating_label");
    rateGroup.appendChild(rateLabel);

    const rateSelect = document.createElement("select");
    rateSelect.className = "um-input";
    for (let i = 0; i <= 10; i++) {
      const opt = document.createElement("option");
      opt.value = i;
      opt.innerText = i;
      if (i === 5) opt.selected = true;
      rateSelect.appendChild(opt);
    }
    rateGroup.appendChild(rateSelect);
    panel.appendChild(rateGroup);

    const btnRow = document.createElement("div");
    btnRow.className = "um-btn-row";

    const closeBtn = document.createElement("button");
    closeBtn.className = "um-btn um-btn-secondary";
    closeBtn.innerText = i18next.t("Close");
    closeBtn.onclick = () => overlay.remove();

    const saveBtn = document.createElement("button");
    saveBtn.className = "um-btn um-btn-primary";
    saveBtn.innerText = i18next.t("Save");
    saveBtn.onclick = () => {
      const val = idInput.value.trim();
      if (!val) return;

      let processedCount = 0;
      let isJson = false;

      if (val.startsWith("[") && val.endsWith("]")) {
        try {
          const parsed = JSON.parse(val);
          if (Array.isArray(parsed)) {
            isJson = true;
            processedCount = viewedAVIDs.batchAdd(parsed);
          }
        } catch (e) {
          showToast(i18next.t("invalid_json"), "error");
          return;
        }
      }

      if (!isJson) {
        viewedAVIDs.add(val, parseInt(rateSelect.value));
        processedCount = 1;
      }

      if (processedCount > 0) {
        showToast(i18next.t("added_msg", { count: processedCount }));
        idInput.value = "";
        idInput.focus();

        if (STATE.isJavDB) handleJavDB();
        else updateSehuatangHeader();
      }
    };

    btnRow.appendChild(closeBtn);
    btnRow.appendChild(saveBtn);
    panel.appendChild(btnRow);

    overlay.appendChild(panel);
    document.body.appendChild(overlay);

    setTimeout(() => idInput.focus(), 100);

    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) overlay.remove();
    });
  }

  function createCheckViewedPanel() {
    if (document.getElementById("um-check-viewed-panel")) return;

    const panel = document.createElement("div");
    panel.id = "um-check-viewed-panel";

    const title = document.createElement("h3");
    title.innerText = i18next.t("check_viewed_title");
    panel.appendChild(title);

    const closeBtn = document.createElement("button");
    closeBtn.id = "um-check-viewed-close";
    closeBtn.innerHTML = "&times;";
    closeBtn.onclick = () => panel.remove();
    panel.appendChild(closeBtn);

    const inputGroup = document.createElement("div");
    inputGroup.className = "um-input-group";
    const idInput = document.createElement("input");
    idInput.type = "text";
    idInput.className = "um-input";
    idInput.placeholder = i18next.t("check_viewed_id_placeholder");
    const checkBtn = document.createElement("button");
    checkBtn.className = "um-btn um-btn-primary";
    checkBtn.innerText = i18next.t("check_viewed_check_btn");
    inputGroup.appendChild(idInput);
    inputGroup.appendChild(checkBtn);
    panel.appendChild(inputGroup);

    const resultDiv = document.createElement("div");
    resultDiv.id = "um-check-viewed-result";
    panel.appendChild(resultDiv);

    const checkAvid = () => {
      const avid = idInput.value.trim().toUpperCase();
      if (!avid) return;

      const allViewed = viewedAVIDs.getAll();
      const viewedItem = allViewed.find((item) => item.id === avid);

      resultDiv.innerHTML = ""; // Clear previous results

      const statusRow = document.createElement("div");
      statusRow.className = "um-result-row";
      statusRow.innerHTML = `<span class="um-result-label">${i18next.t(
        "check_viewed_status"
      )}</span>`;
      const statusValue = document.createElement("span");
      statusValue.className = "um-result-value";

      if (viewedItem) {
        statusValue.innerText = i18next.t("check_viewed_viewed_on");
        statusValue.classList.add("viewed");
        statusRow.appendChild(statusValue);
        resultDiv.appendChild(statusRow);

        const dateRow = document.createElement("div");
        dateRow.className = "um-result-row";
        const formattedDate = new Date(
          viewedItem.updatedAt
        ).toLocaleDateString();
        dateRow.innerHTML = `<span class="um-result-label"></span><span class="um-result-value">${formattedDate}</span>`;
        resultDiv.appendChild(dateRow);

        const ratingRow = document.createElement("div");
        ratingRow.className = "um-result-row";
        ratingRow.innerHTML = `<span class="um-result-label">${i18next.t(
          "check_viewed_rating"
        )}</span><span class="um-result-value">${
          viewedItem.rating
        } / 10</span>`;
        resultDiv.appendChild(ratingRow);
      } else {
        statusValue.innerText = i18next.t("check_viewed_not_viewed");
        statusValue.classList.add("not-viewed");
        statusRow.appendChild(statusValue);
        resultDiv.appendChild(statusRow);
      }
    };

    checkBtn.onclick = checkAvid;
    idInput.onkeydown = (e) => {
      if (e.key === "Enter") checkAvid();
    };

    document.body.appendChild(panel);
    idInput.focus();

    // Make the panel draggable
    let isDragging = false;
    let offsetX, offsetY;

    panel.addEventListener("mousedown", (e) => {
      isDragging = true;
      offsetX = e.clientX - panel.getBoundingClientRect().left;
      offsetY = e.clientY - panel.getBoundingClientRect().top;
      panel.style.userSelect = "none";
    });

    document.addEventListener("mousemove", (e) => {
      if (isDragging) {
        panel.style.left = `${e.clientX - offsetX}px`;
        panel.style.top = `${e.clientY - offsetY}px`;
        panel.style.transform = "none"; // Disable transform when dragging
      }
    });

    document.addEventListener("mouseup", () => {
      isDragging = false;
      panel.style.userSelect = "";
    });
  }

  // --- JIANGUOYUN SYNC CLASS ---
  class JianguoyunClient {
    constructor() {
      const config = getJsonValue(CONFIG.LOCAL_STORAGE_KEYS.JIANGUOYUN, "{}");
      this.davURL = config.url;
      this.auth =
        config.account && config.password
          ? `Basic ${btoa(`${config.account}:${config.password}`)}`
          : null;
    }
    isValid() {
      return !!(this.davURL && this.auth);
    }
    async request(method, fileName, data = null) {
      if (!this.isValid()) return Promise.reject("Bad Jianguoyun Config!");
      const url = `${this.davURL}/${fileName}.json`.replace(
        /(?<!:)\/{2,}/g,
        "/"
      );
      return await gmFetch({
        method,
        url,
        data,
        headers: { Authorization: this.auth },
        timeout: 5000,
      });
    }
    async download(fileName) {
      return this.request("GET", fileName);
    }
    async upload(fileName, data) {
      return this.request("PUT", fileName, JSON.stringify(data));
    }
  }

  // --- SEHUATANG LOGIC ---
  function updateSehuatangHeader() {
    const btn = document.getElementById("header-copy-btn");
    const infoText = document.getElementById("header-info-text");
    if (btn) {
      if (STATE.totalTasks > 0 && STATE.finishedTasks >= STATE.totalTasks) {
        const magnets = document.querySelectorAll(
          ".preview-card:not(.viewed) .magnet-link"
        ).length;
        btn.disabled = false;
        btn.textContent = i18next.t("copy_btn_ready", { count: magnets });
      } else {
        btn.disabled = true;
        btn.textContent = i18next.t("copy_btn_loading", {
          loaded: STATE.finishedTasks,
          total: STATE.totalTasks,
        });
      }
    }
    if (infoText) {
      infoText.textContent = i18next.t("header_info", {
        hidden: STATE.filteredCount,
        total: viewedAVIDs.getAll().length,
      });
    }
  }

  async function createSehuatangCard(info, container) {
    const isViewed = info.avId
      ? viewedAVIDs.has(info.avId)
      : viewedUSTitles.has(info.fullTitle);
    if (GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE) && isViewed)
      return;

    STATE.totalTasks++;
    updateSehuatangHeader();

    const card = document.createElement("div");
    card.className = "preview-card";
    if (info.avId) card.dataset.avid = info.avId;
    card.dataset.fullTitle = info.fullTitle;
    if (isViewed) card.classList.add("viewed");

    card.innerHTML = `
        <div class="card-image-container"></div>
        <div class="card-content">
            <h3 class="card-title"><a href="${info.url}" target="_blank">${info.fullTitle}</a></h3>
            <p class="card-meta">${info.releaseDate}</p>
            <div class="card-links"></div>
        </div>`;
    container.appendChild(card);

    const markAsViewed = () => {
      if (info.avId) viewedAVIDs.add(info.avId, 0); // Default 0 for auto-add
      else viewedUSTitles.add(info.fullTitle);
      card.classList.add("viewed");
      updateSehuatangHeader();
    };

    try {
      const res = await gmFetch({
        method: "GET",
        url: info.url,
        headers: CONFIG.HTTP_HEADERS,
      });
      const doc = new DOMParser().parseFromString(
        res.responseText,
        "text/html"
      );
      const imgFile = doc
        .querySelector("ignore_js_op > img")
        ?.getAttribute("zoomfile");
      const magnetLink = doc.querySelector(
        ".blockcode > div > ol > li"
      )?.innerText;

      const imgContainer = card.querySelector(".card-image-container");
      if (imgFile) {
        const img = document.createElement("img");
        img.src = imgFile;
        img.onclick = () => {
          window.open(info.url, "_blank");
          markAsViewed();
        };
        imgContainer.appendChild(img);
      } else {
        imgContainer.innerText = "No Image";
        imgContainer.style.color = "#666";
        imgContainer.style.display = "flex";
        imgContainer.style.alignItems = "center";
        imgContainer.style.justifyContent = "center";
      }

      if (magnetLink) {
        const a = document.createElement("a");
        a.href = magnetLink;
        a.className = "magnet-link";
        a.textContent = "⚡";
        a.title = "Copy Magnet";
        a.onclick = markAsViewed;
        card.querySelector(".card-links").appendChild(a);
      }
    } catch (e) {
      console.error(e);
    } finally {
      STATE.finishedTasks++;
      updateSehuatangHeader();
    }
  }

  function handleSehuatang() {
    const threadListTable = document.getElementById("threadlisttableid");
    if (!threadListTable) return;
    threadListTable.style.display = "none";

    // Header Construction
    const infoBar = document.createElement("div");
    infoBar.id = "filtered-info-bar";
    const infoText = document.createElement("div");
    infoText.id = "header-info-text";
    infoText.className = "info-text";
    infoBar.appendChild(infoText);
    const actionsDiv = document.createElement("div");
    const copyBtn = document.createElement("button");
    copyBtn.id = "header-copy-btn";
    copyBtn.className = "action-btn";
    copyBtn.onclick = () => {
      const links = Array.from(
        document.querySelectorAll(".preview-card:not(.viewed) .magnet-link")
      );
      if (links.length) {
        GM_setClipboard(links.map((l) => l.href).join("\r\n"));
        links.forEach((l) => {
          const card = l.closest(".preview-card");
          if (card.dataset.avid) viewedAVIDs.add(card.dataset.avid, 0);
          card.classList.add("viewed");
        });
        showToast(i18next.t("copy_done", { count: links.length }));
        updateSehuatangHeader();
      } else {
        showToast(i18next.t("no_new_magnets"), "info");
      }
    };
    actionsDiv.appendChild(copyBtn);
    infoBar.appendChild(actionsDiv);
    const container = document.createElement("div");
    container.id = "modern-preview-container";
    document.body.prepend(container);
    document.body.prepend(infoBar);

    // Initial Count
    if (GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE)) {
      threadListTable
        .querySelectorAll('tbody[id^="normalthread_"]')
        .forEach((el) => {
          const title = el.querySelector("th a.s.xst")?.innerText.trim();
          const avid = title?.match(CONFIG.AVID_REGEX)?.[0]?.toUpperCase();
          if (
            (avid && viewedAVIDs.has(avid)) ||
            (title && viewedUSTitles.has(title))
          )
            STATE.filteredCount++;
        });
    }
    updateSehuatangHeader();

    // Process Loop
    const run = () => {
      threadListTable
        .querySelectorAll('tbody[id^="normalthread_"]:not([data-processed])')
        .forEach((el) => {
          el.dataset.processed = "true";
          const linkEl = el.querySelector("th a.s.xst");
          if (!linkEl) return;
          const title = linkEl.innerText.trim();
          const avIdMatch = title.match(CONFIG.AVID_REGEX);
          const info = {
            url: linkEl.href,
            fullTitle: title,
            avId: avIdMatch ? avIdMatch[0].toUpperCase() : null,
            releaseDate: el.querySelector("td.by em span")?.innerText || "N/A",
          };
          createSehuatangCard(info, container);
        });
    };
    _.debounce(run, 300)();
    new MutationObserver(_.debounce(run, 500)).observe(threadListTable, {
      childList: true,
      subtree: true,
    });
  }

  // --- JAVDB LOGIC ---
  function handleJavDB() {
    console.log("JavDB Enhanced Mode Activated");
    document.body.classList.add("javdb-enhanced");

    const processItem = (item) => {
      if (item.dataset.processed) return;

      const titleStrong = item.querySelector(".video-title strong");
      if (!titleStrong) return;

      const avid = titleStrong.textContent.trim().toUpperCase();
      if (!avid) return;

      item.dataset.processed = "true";
      item.dataset.avid = avid;

      if (viewedAVIDs.has(avid)) {
        item.classList.add("viewed");
      }

      item.addEventListener("click", (e) => {
        viewedAVIDs.add(avid, 0);
        item.classList.add("viewed");
      });
    };

    const run = () => {
      const items = document.querySelectorAll(".item, .grid-item");
      items.forEach(processItem);
    };

    run();

    const observer = new MutationObserver((mutations) => {
      let shouldRun = false;
      mutations.forEach((m) => {
        if (m.addedNodes.length > 0) shouldRun = true;
      });
      if (shouldRun) run();
    });

    const container = document.querySelector(".movie-list") || document.body;
    observer.observe(container, { childList: true, subtree: true });
  }

  // --- MENU & MAIN ---
  function setupMenu() {
    GM_registerMenuCommand(i18next.t("Manual Add"), createManualPanel);
    GM_registerMenuCommand(
      i18next.t("Check Viewed Status"),
      createCheckViewedPanel
    );

    GM_registerMenuCommand(i18next.t("Language"), () => {
      // Toggle logic or prompt not fully implemented in snippet
    });
    GM_registerMenuCommand(
      `${
        GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE) ? "✅" : "❌"
      } ${i18next.t("Hide Viewed")}`,
      () => {
        GM_setValue(
          CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE,
          !GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE)
        );
        location.reload();
      }
    );
    GM_registerMenuCommand(
      `${i18next.t("Viewed Total")}: ${viewedAVIDs.getAll().length}`,
      () => {
        showToast(
          `${i18next.t("Viewed Total")}: ${viewedAVIDs.getAll().length}`,
          "info"
        );
      }
    );

    // Jianguoyun commands
    const jgyCmds = [
      {
        name: "Jianguoyun Config",
        action: async () => {
          // Implementation omitted
        },
      },
      {
        name: "Upload To Jianguoyun",
        action: async () => {
          const jgy = new JianguoyunClient();
          if (!jgy.isValid())
            return showToast(i18next.t("Check Config!"), "error");
          try {
            await jgy.upload(
              CONFIG.JIANGUOYUN.AVIDS_FILENAME,
              viewedAVIDs.getAll()
            );
            await jgy.upload(
              CONFIG.JIANGUOYUN.US_FILENAME,
              viewedUSTitles.list()
            );
            showToast(i18next.t("Upload successful!"));
          } catch (e) {
            showToast(i18next.t("Update Bad"), "error");
          }
        },
      },
      {
        name: "Download from Jianguoyun",
        action: async () => {
          const jgy = new JianguoyunClient();
          if (!jgy.isValid())
            return showToast(i18next.t("Check Config!"), "error");
          try {
            const res = await jgy.download(CONFIG.JIANGUOYUN.AVIDS_FILENAME);
            if (res.status === 200)
              viewedAVIDs.merge(JSON.parse(res.responseText));
            showToast(i18next.t("Download successful!"));
            setTimeout(() => location.reload(), 1000);
          } catch (e) {
            showToast(i18next.t("Bad Download"), "error");
          }
        },
      },
      {
        name: "Local and Jianguoyun merge",
        action: async () => {
          const jgy = new JianguoyunClient();
          if (!jgy.isValid())
            return showToast(i18next.t("Check Config!"), "error");
          try {
            const r1 = await jgy.download(CONFIG.JIANGUOYUN.AVIDS_FILENAME);
            const c1 = r1.status === 200 ? JSON.parse(r1.responseText) : [];
            viewedAVIDs.merge(c1);
            await jgy.upload(
              CONFIG.JIANGUOYUN.AVIDS_FILENAME,
              viewedAVIDs.getAll()
            );
            showToast(i18next.t("Sync successful!"));
            setTimeout(() => location.reload(), 1000);
          } catch (e) {
            showToast(i18next.t("Update Bad"), "error");
          }
        },
      },
    ];
    jgyCmds.forEach((c) => GM_registerMenuCommand(i18next.t(c.name), c.action));
  }

  // --- ENTRY POINT ---
  setupMenu();
  if (STATE.isJavDB) {
    handleJavDB();
  } else {
    handleSehuatang();
  }
})();