steam创意工坊(单个+合集)一键复制下载命令(全集成版)

单个和合集创意工坊,一键复制下载命令,按钮样式和图标严格匹配页面元素,支持合集内单mod复制,图标用Material Symbols字体图标(file_export)

// ==UserScript==
// @name         steam创意工坊(单个+合集)一键复制下载命令(全集成版)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  单个和合集创意工坊,一键复制下载命令,按钮样式和图标严格匹配页面元素,支持合集内单mod复制,图标用Material Symbols字体图标(file_export)
// @author       Dost51552
// @match        https://steamcommunity.com/workshop/browse/*
// @match        https://steamcommunity.com/sharedfiles/filedetails/*
// @match        https://steamcommunity.com/workshop/filedetails/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=steamcommunity.com
// @require      https://unpkg.com/[email protected]/dist/jquery.min.js
// @license      MIT
// ==/UserScript==

(() => {
  "use strict";

  const SELECTED_COLOR = "#FF0000"; // 选择框选中颜色
  const COMMON_BTN_STYLE = `
    position: relative;
    height: 30px;
    padding-left: 28px;
    padding-right: 12px;
    background-repeat: no-repeat;
    display: inline-block;
    line-height: 30px;
    color: #939393;
    cursor: pointer;
    user-select: none;
    margin-left: 6px;
    border-radius: 3px;
    background-color: rgba(0, 0, 0, 0.4);
    transition: background-color 0.2s ease;
    border: none;
  `;

  const ICON_COMMON_STYLE = {
    background:
      "url('https://community.fastly.steamstatic.com/public/images/sharedfiles/ico_subscribe_tiled.png')",
    backgroundPosition: "0px 0px",
    backgroundRepeat: "no-repeat",
    height: "30px",
    width: "16px",
    position: "absolute",
    top: "0px",
    left: "8px",
    pointerEvents: "none",
  };

  // 加载Material Symbols字体(file_export图标)
  function loadMaterialSymbolsFont() {
    if (!document.getElementById("material-symbols-style")) {
      const linkEl = document.createElement("link");
      linkEl.id = "material-symbols-style";
      linkEl.rel = "stylesheet";
      linkEl.href =
        "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&icon_names=file_export";
      document.head.appendChild(linkEl);
    }
  }

  // 获取游戏ID
  function getGameId() {
    return $("div.apphub_OtherSiteInfo.responsive_hidden > a").data("appid");
  }

  // 获取页面mod id数组(合集中所有mod)
  function getModIds() {
    const ids = [...$(".collectionItemDetails > a")]
      .map(
        (el) =>
          $(el)
            .attr("href")
            ?.match(/id=(\d+)/)?.[1]
      )
      .filter(Boolean);
    if (!ids.length) {
      // 单个mod详情页也取id
      const singleId = window.location.search.match(/id=(\d+)/)?.[1];
      if (singleId) ids.push(singleId);
    }
    return ids;
  }

  // 生成下载命令文本
  function generateDownloadText(gameId, modIds) {
    return modIds.map((id) => `workshop_download_item ${gameId} ${id}`).join("\n");
  }

  // 显示按钮下方提示浮层,2秒自动消失
  function showToastBelowButton(btn, msg, isCollection = false) {
    $(".tm_custom_toast").remove();
    const rect = btn.getBoundingClientRect();
    const toast = document.createElement("div");
    toast.className = "tm_custom_toast";
    toast.innerText = msg;
    Object.assign(toast.style, {
      position: "fixed",
      left: `${rect.left}px`,
      top: `${rect.bottom + 6}px`,
      backgroundColor: isCollection ? "#97C0E3" : "#91b007",
      color: isCollection ? "#3C3D3E" : "#fff",
      padding: "6px 14px",
      borderRadius: "4px",
      fontSize: "14px",
      fontWeight: "600",
      boxShadow: "0 2px 6px rgba(0,0,0,0.25)",
      zIndex: 10000,
      userSelect: "none",
      pointerEvents: "none",
      whiteSpace: "nowrap",
      opacity: "0",
      transition: "opacity 0.3s ease",
    });
    document.body.appendChild(toast);
    requestAnimationFrame(() => (toast.style.opacity = "1"));

    const hide = () => {
      toast.style.opacity = "0";
      setTimeout(() => toast.remove(), 300);
      window.removeEventListener("scroll", hide);
      window.removeEventListener("click", hide);
    };

    const timeoutId = setTimeout(hide, 2000);
    window.addEventListener(
      "scroll",
      () => {
        clearTimeout(timeoutId);
        hide();
      },
      { once: true }
    );
    window.addEventListener(
      "click",
      () => {
        clearTimeout(timeoutId);
        hide();
      },
      { once: true }
    );
  }

  // 复制文本到剪贴板,失败时fallback提示或跳转第三方
  function copyToClipboard(text, btn, isCollection = false) {
    navigator.clipboard
      .writeText(text)
      .then(() => showToastBelowButton(btn, "下载命令已复制", isCollection))
      .catch(() => {
        const modIds = getModIds();
        if (!modIds.length) {
          prompt("复制失败,请手动复制:", text);
          return;
        }
        const fullUrl = `http://steamcommunity.com/sharedfiles/filedetails/?id=${modIds[0]}`;
        const newWin = window.open("https://steamworkshopdownloader.io/");
        if (!newWin) {
          prompt("复制失败,且弹窗被阻止,请手动复制:", text);
          return;
        }
        const intervalId = setInterval(() => {
          try {
            if (newWin.document && newWin.document.readyState === "complete") {
              clearInterval(intervalId);
              const input = newWin.document.getElementById("downloadUrlLabel");
              if (input) {
                input.value = fullUrl;
                let enterEvent;
                if (typeof KeyboardEvent === "function") {
                  enterEvent = new KeyboardEvent("keydown", {
                    key: "Enter",
                    code: "Enter",
                    keyCode: 13,
                    which: 13,
                    bubbles: true,
                    cancelable: true,
                  });
                } else {
                  enterEvent = new Event("keydown", {
                    bubbles: true,
                    cancelable: true,
                  });
                }
                input.dispatchEvent(enterEvent);
              }
            }
          } catch {
            // 跨域异常忽略
          }
        }, 300);
      });
  }

  // 鼠标悬停按钮样式切换
  function addHoverEffect(el) {
    el.addEventListener("mouseenter", () => {
      el.style.backgroundColor = "#97C0E3";
      el.style.color = "#3C3D3E";
      el.style.textDecoration = "none";
    });
    el.addEventListener("mouseleave", () => {
      el.style.backgroundColor = "rgba(0, 0, 0, 0.4)";
      el.style.color = "#939393";
    });
  }

  // --- 创建单mod详情页复制按钮 ---
  function createButtonForSingle() {
    const refBtn = document.getElementById("SubscribeItemBtn");
    if (!refBtn) return null;

    const btn = document.createElement("a");
    btn.className = refBtn.className;
    btn.style.cssText = `${refBtn.style.cssText}; display: inline-block; margin-top: 6px; user-select:none; cursor:pointer; position: relative;`;

    const iconDiv = document.createElement("div");
    iconDiv.className = "subscribeIcon";
    Object.assign(iconDiv.style, {
      position: "absolute",
      top: "0",
      left: "8px",
      height: "30px",
      width: "16px",
      filter:
        "brightness(0%) saturate(100%) invert(85%) sepia(20%) hue-rotate(180deg)",
      backgroundImage:
        "url('https://community.fastly.steamstatic.com/public/images/sharedfiles/rate_ico_up_tiled.png?v=1')",
      backgroundPosition: "0 0",
      backgroundRepeat: "no-repeat",
    });

    const spanText = document.createElement("span");
    spanText.className = "subscribeText";

    const optAdd = document.createElement("div");
    optAdd.className = "subscribeOption subscribe selected";
    optAdd.textContent = "复制";

    const optSubscribed = document.createElement("div");
    optSubscribed.className = "subscribeOption subscribed";
    optSubscribed.textContent = "已订阅";

    const optRemove = document.createElement("div");
    optRemove.className = "subscribeOption remove";
    optRemove.textContent = "取消订阅";

    spanText.append(optAdd, optSubscribed, optRemove);
    btn.append(iconDiv, spanText);

    btn.onclick = () => {
      const gameId = getGameId();
      const modIds = getModIds();
      if (!gameId || !modIds.length) {
        showToastBelowButton(btn, "未获取到游戏ID或Mod ID");
        return;
      }
      copyToClipboard(generateDownloadText(gameId, modIds), btn);
    };

    refBtn.parentNode.insertBefore(btn, refBtn.nextSibling);
    return btn;
  }

  // --- 创建合集复制按钮 ---
  function createButtonForCollection() {
    const container = document.querySelector(".subscribeCollection");
    const itemControls = document.getElementById("ItemControls");

    if (!container) {
      if (!itemControls) return null;
      const btn = document.createElement("span");
      btn.className = "general_btn subscribe";
      btn.style.cssText = COMMON_BTN_STYLE;

      const iconDiv = document.createElement("div");
      iconDiv.className = "duplicateCollectionIcon";
      Object.assign(iconDiv.style, ICON_COMMON_STYLE);
      btn.appendChild(iconDiv);

      const spanText = document.createElement("span");
      spanText.textContent = "复制命令";
      btn.appendChild(spanText);

      addHoverEffect(btn);

      btn.onclick = () => {
        const gameId = getGameId();
        const modIds = getModIds();
        if (!gameId || !modIds.length) {
          showToastBelowButton(btn, "未获取到游戏ID或Mod ID");
          return;
        }
        copyToClipboard(generateDownloadText(gameId, modIds), btn, true);
      };

      const btns = itemControls.querySelectorAll(".general_btn");
      if (btns.length)
        btns[btns.length - 1].insertAdjacentElement("afterend", btn);
      else itemControls.appendChild(btn);

      return btn;
    }

    const saveBtn = [...container.querySelectorAll(".general_btn.subscribe")].find(
      (el) => el.textContent.trim() === "保存至收藏"
    );
    if (!saveBtn) return null;

    const btn = document.createElement("span");
    btn.className = "general_btn subscribe";
    btn.style.cssText = COMMON_BTN_STYLE;

    const iconDiv = document.createElement("div");
    iconDiv.className = "duplicateCollectionIcon";
    Object.assign(iconDiv.style, ICON_COMMON_STYLE);
    btn.appendChild(iconDiv);

    const spanText = document.createElement("span");
    spanText.textContent = "复制命令";
    btn.appendChild(spanText);

    addHoverEffect(btn);

    btn.onclick = () => {
      const gameId = getGameId();
      const modIds = getModIds();
      if (!gameId || !modIds.length) {
        showToastBelowButton(btn, "未获取到游戏ID或Mod ID");
        return;
      }
      copyToClipboard(generateDownloadText(gameId, modIds), btn, true);
    };

    saveBtn.insertAdjacentElement("afterend", btn);
    return btn;
  }
  // --- 合集内单个mod复制按钮 ---
  function createCopyButtonForSingleModsInCollection() {
    loadMaterialSymbolsFont();

    $(".collectionItemDetails").each((_, container) => {
      const $container = $(container);
      const href = $container.find("> a").attr("href");
      const modId = href?.match(/id=(\d+)/)?.[1];
      if (!modId) return;

      // 获取游戏ID(合集内mod)
      const appidHref = $container.find(".workshopItemAuthorName a").attr("href");
      const appId = appidHref?.match(/appid=(\d+)/)?.[1];
      if (!appId) return;

      const subBtn = $(`#SubscribeItemBtn${modId}`);
      if (!subBtn.length || subBtn.prev(".tm_copy_single_mod_btn").length) return;

      const copyBtn = document.createElement("a");
      copyBtn.className = "general_btn subscribe tm_copy_single_mod_btn";
      Object.assign(copyBtn.style, {
        position: "relative",
        height: "30px",
        width: "31px",
        padding: "0",
        backgroundRepeat: "no-repeat",
        display: "inline-block",
        lineHeight: "30px",
        cursor: "pointer",
        color: "#939393",
        userSelect: "none",
        marginBottom: "0",
        borderRadius: "3px",
        backgroundColor: "rgba(0, 0, 0, 0.4)",
        transition: "background-color 0.2s ease",
      });

      addHoverEffect(copyBtn);

      const iconSpan = document.createElement("span");
      iconSpan.className = "material-symbols-outlined";
      iconSpan.textContent = "file_export";
      Object.assign(iconSpan.style, {
        position: "absolute",
        top: "5px",
        left: "4px",
        fontVariationSettings: "'wght' 400, 'FILL' 0, 'GRAD' 0",
        fontSize: "22px",
        color: "#DADADA",
        pointerEvents: "none",
        userSelect: "none",
      });
      copyBtn.appendChild(iconSpan);

      copyBtn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        copyToClipboard(`workshop_download_item ${appId} ${modId}`, copyBtn, true);
      });

      const parent = subBtn.parent();
      if (parent.css("position") === "static") parent.css("position", "relative");

      subBtn.before(copyBtn);
    });
  }

  // --- 浏览页每个mod添加复制按钮与选择框 ---
  function addButtonsToMods() {
    $(".workshopItem").each((_, item) => {
      const $item = $(item);

      if ($item.css("position") === "static") {
        $item.css("position", "relative");
      }

      const href = $item.find("a.item_link").attr("href");
      if (!href) return;
      const modIdMatch = href.match(/id=(\d+)/);
      if (!modIdMatch) return;
      const modId = modIdMatch[1];

      const gameId = getGameId();
      if (!gameId) return;

      if ($item.find(".tm_copy_btn").length === 0) {
        // 复制按钮 - 右下角
        const copyBtn = $("<button>")
          .addClass("tm_copy_btn subscribeIcon")
          .attr("title", "复制下载命令")
          .css({
            position: "absolute",
            bottom: "4px",
            right: "4px",
            height: "30px",
            width: "16px",
            cursor: "pointer",
            "background-color": "transparent",
            "background-image":
              "url('https://community.fastly.steamstatic.com/public/images/sharedfiles/ico_subscribe_tiled.png')",
            "background-position": "0 0",
            "background-repeat": "no-repeat",
            border: "none",
            outline: "none",
          })
          .on("click", () => {
            const cmd = generateDownloadText(gameId, [modId]);
            navigator.clipboard.writeText(cmd).then(() => {
              showToastBelowButton(copyBtn[0], "下载命令已复制");
            }).catch(() => {
              prompt("复制失败,请手动复制下面的命令:", cmd);
            });
          });

        // 选择框 - 复制按钮正上方,紧邻
        const selectCheckbox = $("<button>")
          .addClass("tm_select_checkbox")
          .attr({
            title: "选择该Mod",
            "data-selected": "false",
          })
          .css({
            position: "absolute",
            bottom: "36px", // 30 + 6
            right: "4px",
            height: "16px",
            width: "16px",
            cursor: "pointer",
            padding: 0,
            outline: "none",
            "background-color": "transparent",
            border: `2px solid #FFFFFF`,
            "border-radius": "3px",
          })
          .on("click", function () {
            const selected = $(this).attr("data-selected") === "true";
            if (selected) {
              $(this).attr("data-selected", "false");
              $(this).css("background-color", "transparent");
            } else {
              $(this).attr("data-selected", "true");
              $(this).css("background-color", SELECTED_COLOR);
            }
          });

        $item.append(copyBtn, selectCheckbox);
      }
    });
  }

  // --- 批量复制按钮添加到“按日期筛选”按钮右侧,间距5px ---
  function addBatchCopyButton() {
    const filterBtn = $("span.general_btn.createCollection");
    if (filterBtn.length === 0) return;
    if ($("#tm_batch_copy_btn").length > 0) return;

    const batchBtn = $("<button>")
      .attr("id", "tm_batch_copy_btn")
      .text("批量复制下载命令")
      .addClass("general_btn createCollection")
      .css({
        "margin-left": "5px",
        cursor: "pointer",
      })
      .on("mouseenter", function () {
        this.style.cursor = "pointer";
      })
      .on("click", () => {
        const gameId = getGameId();
        if (!gameId) {
          alert("未找到游戏ID,无法复制");
          return;
        }
        const selectedMods = [];
        $(".workshopItem .tm_select_checkbox[data-selected='true']").each((_, el) => {
          const $item = $(el).closest(".workshopItem");
          const href = $item.find("a.item_link").attr("href");
          const modIdMatch = href?.match(/id=(\d+)/);
          if (modIdMatch) selectedMods.push(modIdMatch[1]);
        });

        if (selectedMods.length === 0) {
          alert("未选择任何Mod");
          return;
        }

        const cmdText = selectedMods.map(id => `workshop_download_item ${gameId} ${id}`).join("\n");
        navigator.clipboard.writeText(cmdText).then(() => {
          alert(`已复制${selectedMods.length}个Mod的下载命令`);
        }).catch(() => {
          prompt("复制失败,请手动复制下面的命令:", cmdText);
        });
      });

    filterBtn.after(batchBtn);
  }

  // --- 入口函数 ---
  function main() {
    $(".tm_custom_toast").remove();
    $("#tm_batch_copy_btn").remove();

    const path = window.location.pathname;

    // 单个mod详情页 或 合集详情页
    if (
      path.startsWith("/workshop/filedetails") ||
      path.startsWith("/sharedfiles/filedetails")
    ) {
      createButtonForCollection();
      createCopyButtonForSingleModsInCollection();

      // 没合集按钮时,详情页加单个按钮
      if (
        !document.querySelector(".general_btn.subscribe") &&
        document.querySelector(".game_area_purchase_game #SubscribeItemBtn")
      ) {
        createButtonForSingle();
      }
    }

    // 浏览页加选择框和复制按钮及批量复制按钮
    if (path.startsWith("/workshop/browse")) {
      addButtonsToMods();
      addBatchCopyButton();
    }
  }

  $(document).ready(main);
  // 如果页面有动态加载需求,可考虑加观察者自动刷新按钮,暂不添加
})();