visualstudio marketplace toolkit

visualstudio marketplace toolkit by Theo·Chan

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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