Steam Workshop Link Extractor

Extract, copy, or selectively pick Workshop item links from any collection or single item page. Circle toggles let you choose specific items — blue = selected, red = not.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Steam Workshop Link Extractor
// @namespace    SteamWorkshopLinkExtractor
// @version      1
// @description  Extract, copy, or selectively pick Workshop item links from any collection or single item page. Circle toggles let you choose specific items — blue = selected, red = not.
// @author       KhelMho
// @license      MIT
// @match        https://steamcommunity.com/sharedfiles/filedetails*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // --- CONFIGURATION ---
    const outputFilename = 'steam_links.txt';
    // ---------------------

    const CHECKBOX_CLASS = 'ws_extractor_cb';

    /**
     * Returns the ID of the current page's item (if any), so we can exclude it.
     */
    function getSelfId() {
        const el = document.querySelector('input[name="id"]');
        return el ? el.value : null;
    }

    /**
     * Injects a checkbox on the right side of every .collectionItem card.
     * Safe to call multiple times — skips cards that already have one.
     */
    function injectCheckboxes() {
        document.querySelectorAll('.collectionChildren .collectionItem').forEach(item => {
            if (item.querySelector('.' + CHECKBOX_CLASS)) return; // already injected

            const rawId = item.id || '';
            const id    = rawId.replace('sharedfile_', '');
            if (!id) return;

            const subscribeBtn = item.querySelector('a.general_btn.subscribe');
            if (!subscribeBtn) return;

            const cb = document.createElement('div');
            cb.className  = CHECKBOX_CLASS;
            cb.dataset.id = id;
            cb.dataset.selected = 'false';
            Object.assign(cb.style, {
                display:         'inline-block',
                width:           '18px',
                height:          '18px',
                borderRadius:    '50%',
                backgroundColor: '#c0392b',
                border:          '2px solid rgba(0,0,0,0.4)',
                cursor:          'pointer',
                verticalAlign:   'middle',
                marginLeft:      '6px',
                flexShrink:      '0',
                boxShadow:       'inset 0 1px 3px rgba(0,0,0,0.4)',
                transition:      'background-color 0.15s'
            });

            cb.addEventListener('click', e => {
                e.preventDefault();
                e.stopPropagation();
                const selected = cb.dataset.selected === 'true';
                cb.dataset.selected = (!selected).toString();
                cb.style.backgroundColor = !selected ? '#1a9fff' : '#c0392b';
                document.dispatchEvent(new Event('change_ws'));
            });

            subscribeBtn.insertAdjacentElement('afterend', cb);
        });
    }

    /**
     * Returns all IDs from selected (blue) circle dots.
     */
    function getCheckedIds() {
        return [...document.querySelectorAll('.' + CHECKBOX_CLASS)]
            .filter(cb => cb.dataset.selected === 'true')
            .map(cb => cb.dataset.id)
            .filter(Boolean);
    }

    /**
     * Returns all IDs from every circle dot on the page.
     */
    function detectLinks() {
        const found = new Set();
        document.querySelectorAll('.' + CHECKBOX_CLASS).forEach(dot => {
            if (dot.dataset.id) found.add(`https://steamcommunity.com/sharedfiles/filedetails/?id=${dot.dataset.id}`);
        });
        return [...found];
    }

    /**
     * Returns the single page link (for single-item pages).
     */
    function getSingleLink() {
        const id = getSelfId();
        return id ? `https://steamcommunity.com/sharedfiles/filedetails/?id=${id}` : null;
    }

    /**
     * Downloads an array of links as a .txt file, one URL per line.
     */
    function downloadLinks(links, filename) {
        const blob = new Blob([links.join('\n')], { type: 'text/plain;charset=utf-8' });
        const url  = URL.createObjectURL(blob);
        const a    = document.createElement('a');
        a.href          = url;
        a.download      = filename || outputFilename;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        URL.revokeObjectURL(url);
        document.body.removeChild(a);
    }

    /**
     * Copies text to clipboard and briefly changes button label.
     */
    function copyToClipboard(text, btn, originalLabel) {
        navigator.clipboard.writeText(text).then(() => {
            btn.textContent = '✓ Copied!';
            setTimeout(() => { btn.textContent = originalLabel; }, 2000);
        });
    }

    /**
     * Builds and injects the floating button panel.
     */
    function createPanel() {
        const isCollectionPage = !!document.querySelector('.collectionChildren');

        const panel = document.createElement('div');
        Object.assign(panel.style, {
            position:        'fixed',
            bottom:          '24px',
            right:           '24px',
            zIndex:          '99999',
            backgroundColor: '#1b2838',
            border:          '1px solid #4c6b8a',
            padding:         '8px',
            borderRadius:    '3px',
            display:         'flex',
            flexDirection:   'column',
            gap:             '4px',
            minWidth:        '195px',
            fontFamily:      '"Motiva Sans", sans-serif',
            boxShadow:       '0 0 8px rgba(0,0,0,0.8)'
        });

        // Title
        const title = document.createElement('div');
        Object.assign(title.style, {
            color:         '#c6d4df',
            fontSize:      '11px',
            fontWeight:    '600',
            textAlign:     'center',
            letterSpacing: '0.5px',
            paddingBottom: '4px',
            borderBottom:  '1px solid #2a475e',
            marginBottom:  '2px',
            textTransform: 'uppercase'
        });
        title.textContent = 'Steam Workshop Link Extractor';
        panel.appendChild(title);

        // Helper to create a styled Steam button
        function makeBtn(label, type) {
            const btn = document.createElement('button');
            const isGreen = type === 'green';
            const isGrey  = type === 'grey';
            Object.assign(btn.style, {
                padding:      '7px 12px',
                fontSize:     '11px',
                fontWeight:   '400',
                color:        isGrey ? '#b8c4d0' : '#d2e885',
                background:   isGreen
                    ? 'linear-gradient(to bottom, #a4d007 5%, #536904 95%)'
                    : isGrey
                        ? 'linear-gradient(to bottom, #4d6a82 5%, #2f4a5c 95%)'
                        : 'linear-gradient(to bottom, #5c7e9e 5%, #3a5a73 95%)',
                border:       '1px solid #000',
                borderRadius: '2px',
                cursor:       'pointer',
                width:        '100%',
                textAlign:    'center',
                lineHeight:   '1',
                textShadow:   '1px 1px 0px rgba(0,0,0,0.4)',
                boxShadow:    'inset 0 1px 0 rgba(255,255,255,0.1)',
                transition:   'filter 0.1s'
            });
            btn.textContent = label;
            btn.addEventListener('mouseover', () => { if (!btn.disabled) btn.style.filter = 'brightness(1.15)'; });
            btn.addEventListener('mouseout',  () => { btn.style.filter = 'none'; });
            return btn;
        }

        // ── SINGLE ITEM PAGE ──
        if (!isCollectionPage) {
            const link = getSingleLink();

            const extractBtn = makeBtn('Extract Link', 'blue');
            extractBtn.addEventListener('click', () => {
                if (!link) return;
                downloadLinks([link], outputFilename);
                extractBtn.textContent = '✓ Downloaded';
                setTimeout(() => { extractBtn.textContent = 'Extract Link'; }, 2000);
            });
            panel.appendChild(extractBtn);

            const copyBtn = makeBtn('Copy Link', 'grey');
            copyBtn.addEventListener('click', () => {
                if (!link) return;
                copyToClipboard(link, copyBtn, 'Copy Link');
            });
            panel.appendChild(copyBtn);

        // ── COLLECTION PAGE ──
        } else {
            // Count based on subscribeIcon divs — exact visible items
            const getCount = () => document.querySelectorAll('.collectionChildren .collectionItem .subscribeIcon').length;

            const extractAllBtn = makeBtn(`Extract All Links (${getCount()})`, 'blue');
            extractAllBtn.addEventListener('click', () => {
                const links = detectLinks();
                if (!links.length) return;
                downloadLinks(links, outputFilename);
                extractAllBtn.textContent = `✓ Downloaded (${links.length})`;
                setTimeout(() => { extractAllBtn.textContent = `Extract All Links (${getCount()})`; }, 2000);
            });
            panel.appendChild(extractAllBtn);

            const extractSelBtn = makeBtn('Extract Selected Links (0)', 'green');
            extractSelBtn.style.opacity = '0.4';
            extractSelBtn.disabled      = true;
            extractSelBtn.addEventListener('click', () => {
                const ids = getCheckedIds();
                if (!ids.length) return;
                const links = ids.map(id => `https://steamcommunity.com/sharedfiles/filedetails/?id=${id}`);
                downloadLinks(links, 'steam_links_selected.txt');
                extractSelBtn.textContent = `✓ Downloaded (${ids.length})`;
                setTimeout(() => refreshSelBtn(), 2000);
            });
            panel.appendChild(extractSelBtn);

            const copySelBtn = makeBtn('Copy Selected Links (0)', 'grey');
            copySelBtn.style.opacity = '0.4';
            copySelBtn.disabled      = true;
            copySelBtn.addEventListener('click', () => {
                const ids = getCheckedIds();
                if (!ids.length) return;
                const text = ids.map(id => `https://steamcommunity.com/sharedfiles/filedetails/?id=${id}`).join('\n');
                copyToClipboard(text, copySelBtn, `Copy Selected Links (${ids.length})`);
            });
            panel.appendChild(copySelBtn);

            let allSelected = false;
            const selectAllBtn = makeBtn('Select All', 'grey');
            selectAllBtn.addEventListener('click', () => {
                allSelected = !allSelected;
                document.querySelectorAll('.' + CHECKBOX_CLASS).forEach(cb => {
                    cb.dataset.selected      = allSelected.toString();
                    cb.style.backgroundColor = allSelected ? '#1a9fff' : '#c0392b';
                });
                selectAllBtn.textContent = allSelected ? 'Deselect All' : 'Select All';
                refreshSelBtn();
            });
            panel.appendChild(selectAllBtn);

            function refreshSelBtn() {
                const ids = getCheckedIds();
                if (ids.length > 0) {
                    extractSelBtn.textContent = `Extract Selected Links (${ids.length})`;
                    extractSelBtn.style.opacity = '1';
                    extractSelBtn.disabled      = false;
                    copySelBtn.textContent      = `Copy Selected Links (${ids.length})`;
                    copySelBtn.style.opacity    = '1';
                    copySelBtn.disabled         = false;
                } else {
                    extractSelBtn.textContent   = 'Extract Selected Links (0)';
                    extractSelBtn.style.opacity = '0.4';
                    extractSelBtn.disabled      = true;
                    copySelBtn.textContent      = 'Copy Selected Links (0)';
                    copySelBtn.style.opacity    = '0.4';
                    copySelBtn.disabled         = true;
                }
                const all  = document.querySelectorAll('.' + CHECKBOX_CLASS).length;
                allSelected = ids.length === all && all > 0;
                selectAllBtn.textContent = allSelected ? 'Deselect All' : 'Select All';
                // Also refresh extract all count
                extractAllBtn.textContent = `Extract All Links (${getCount()})`;
            }

            document.addEventListener('change_ws', refreshSelBtn);
        }

        document.body.appendChild(panel);

        // Auto-rescan when DOM changes
        const observer = new MutationObserver(() => {
            injectCheckboxes();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Init
    function init() {
        injectCheckboxes();
        createPanel();
    }

    if (document.body) {
        init();
    } else {
        document.addEventListener('DOMContentLoaded', init);
    }

})();