visualstudio marketplace toolkit

visualstudio marketplace toolkit by Theo·Chan

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

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

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

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

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

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

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

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

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

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

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

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

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