um-98t-list-preview

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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();
  }
})();