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