visualstudio marketplace toolkit

visualstudio marketplace toolkit by Theo·Chan

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         visualstudio marketplace toolkit
// @namespace    http://tampermonkey.net/
// @version      0.02
// @description  visualstudio marketplace toolkit by Theo·Chan
// @author       Theo·Chan
// @match        *://marketplace.visualstudio.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license      AGPL-3.0-or-later
// ==/UserScript==

(function () {
    'use strict';

    let vsMarketDownloader = (function () {
        return {
            interval: null,
            // 存储所有绑定的事件回调,方便后续解绑
            eventHandlers: [],

            copyCurl: function(spanEl){
                if (!(spanEl instanceof HTMLSpanElement)) return;

                const curlValue = spanEl.dataset.curl;
                const iconEl = spanEl;
                const tipEl = spanEl.parentElement.querySelector('.curl-tip');

                if (!curlValue || !tipEl || !iconEl) return;

                navigator.clipboard.writeText(curlValue)
                    .then(() => {
                        tipEl.style.display = '';
                        iconEl.style.display = 'none';

                        setTimeout(() => {
                            tipEl.style.display = 'none';
                            iconEl.style.display = '';
                        }, 5000);
                    })
                    .catch((err) => {
                        console.error('复制失败:', err);
                        setTimeout(() => {
                            tipEl.style.display = 'none';
                            iconEl.style.display = 'inline-block';
                        }, 2000);
                    });
            },

            extractPkgName: function(){
                const urlObj = new URL(window.location.href);
                let itemName = urlObj.searchParams.get('itemName');
                return itemName == null ? '' : itemName;
            },

            addDownloadBtn: function () {
                const histories = document.querySelectorAll('tr.version-history-container-row');
                if (histories.length === 0) return;

                // 清除定时器(避免重复执行)
                if (vsMarketDownloader.interval) {
                    clearInterval(vsMarketDownloader.interval);
                    vsMarketDownloader.interval = null;
                }

                if (window.location.host.toUpperCase() !== 'MARKETPLACE.VISUALSTUDIO.COM') return false;

                const pkgName = vsMarketDownloader.extractPkgName();
                if (!pkgName) return; // 无 itemName 时直接返回,避免 split 报错

                const [author, id] = pkgName.split('.');
                if (!author || !id) return; // 兼容 pkgName 格式异常的情况

                // 遍历历史版本(注意:原代码 i 从 1 开始,跳过第一个?需确认逻辑)
                for (let i = 1; i < histories.length; i++) {
                    const historyRow = histories[i];
                    const version = historyRow.firstChild.textContent.trim(); // 去除空格,避免版本号异常
                    const href = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/${author}/vsextensions/${id}/${version}/vspackage`;

                    // 创建下载按钮
                    const a = document.createElement('a');
                    a.className = 'bowtie-icon bowtie-install';
                    a.style.marginLeft = '1rem';
                    a.href = href;
                    historyRow.firstChild.appendChild(a);

                    // 创建 curl 复制相关元素
                    const curlSpan = document.createElement('span');
                    curlSpan.style.marginLeft = '.25rem';

                    const curlIcon = document.createElement('span');
                    curlIcon.className = 'curl-icon bowtie-icon bowtie-copy-to-clipboard';
                    curlIcon.style.cssText = 'color: #1e90ff; cursor:pointer; padding: 0px 1px;';
                    curlIcon.title = '复制为 curl 命令';
                    curlIcon.dataset.curl = `curl -o ${id}-${version}@${author}.vsix ${href}`;

                    // 存储事件回调,方便后续解绑
                    const clickHandler = (e) => {
                        e.stopPropagation(); // 阻止事件冒泡(可选)
                        vsMarketDownloader.copyCurl(curlIcon);
                    };
                    curlIcon.addEventListener('click', clickHandler);
                    vsMarketDownloader.eventHandlers.push({ element: curlIcon, handler: clickHandler });

                    const curlTip = document.createElement('span');
                    curlTip.className = 'curl-tip bowtie-icon bowtie-check';
                    curlTip.style.cssText = 'color: rgb(16, 124, 16); border: 2px solid rgb(16, 124, 16); border-radius: 2px; padding: 0px 1px; display: none;';
                    curlTip.title = '已复制到剪切板';

                    curlSpan.appendChild(curlIcon);
                    curlSpan.appendChild(curlTip);
                    historyRow.firstChild.appendChild(curlSpan);
                }
                return true;
            },

            // 清理资源:解绑事件 + 清除定时器
            cleanup: function() {
                // 解绑所有事件监听
                vsMarketDownloader.eventHandlers.forEach(({ element, handler }) => {
                    element.removeEventListener('click', handler);
                });
                vsMarketDownloader.eventHandlers = [];

                // 清除定时器
                if (vsMarketDownloader.interval) {
                    clearInterval(vsMarketDownloader.interval);
                    vsMarketDownloader.interval = null;
                }
            }
        };
    })();

    // 启动定时器(1.2秒轮询添加按钮)
    vsMarketDownloader.interval = setInterval(() => {
        vsMarketDownloader.addDownloadBtn();
    }, 1200);

    // 页面卸载时清理资源(避免内存泄漏)
    window.addEventListener('beforeunload', () => {
        vsMarketDownloader.cleanup();
    });
})();