您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Parse SteamDB Related Apps and DLC into structured format for SLSsteam.
// ==UserScript== // @name SteamDB App Parser // @namespace ViolentMonkey // @version 1.1 // @description Parse SteamDB Related Apps and DLC into structured format for SLSsteam. // @author Tasteless Void // @match https://steamdb.info/app/* // @grant none // @license MIT // ==/UserScript== (() => { 'use strict'; // === Configuration === const IGNORE_TYPES = ['Demo', 'Soundtrack', 'Beta']; // === Helper Functions === const getMainAppId = () => location.pathname.match(/\/app\/(\d+)\//)?.[1] || null; const extractAppIdFromRow = (row) => { const attr = row.getAttribute('data-appid'); if (attr) return attr; for (const cell of row.querySelectorAll('td')) { const text = cell.textContent.trim(); if (/^\d{4,}$/.test(text)) return text; const link = cell.querySelector('a[href*="/app/"]'); const match = link?.href.match(/\/app\/(\d+)\//); if (match) return match[1]; } return null; }; const parseAppTable = (selector, mainAppId, ignoreTypes = []) => { const results = []; const rows = document.querySelectorAll(`${selector} tbody tr`); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length < 2) continue; const type = cells[1]?.textContent.trim(); const name = cells[2]?.textContent.trim() || type; const appId = extractAppIdFromRow(row); if (!appId || appId === mainAppId || ignoreTypes.includes(type)) continue; results.push({ appId, appName: name.replace(/"/g, '\\"'), type }); } return results; }; const generateYamlOutput = (mainAppId, additionalApps, dlcEntries) => { const dlcMap = new Map(); for (const { appId, appName } of dlcEntries) { dlcMap.set(appId, appName); } const lines = []; if (additionalApps.length) { lines.push('AdditionalApps:'); for (const { appId, appName } of [...additionalApps].sort((a, b) => a.appId - b.appId)) { lines.push(` - ${appId}\t# ${appName}`); } } if (dlcMap.size) { lines.push('\nDlcData:'); lines.push(` ${mainAppId}:`); for (const [appId, appName] of [...dlcMap.entries()].sort((a, b) => a[0] - b[0])) { lines.push(` ${appId}: "${appName}"`); } } return lines.join('\n'); }; const showOutputModal = (output) => { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; `; const box = document.createElement('div'); box.style.cssText = ` background: #fff; padding: 20px; border-radius: 8px; max-width: 80%; max-height: 80%; overflow: auto; `; const textarea = document.createElement('textarea'); textarea.readOnly = true; textarea.value = output; textarea.style.cssText = ` width: 100%; height: 400px; font-family: monospace; font-size: 12px; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; `; const buttonRow = document.createElement('div'); buttonRow.style.textAlign = 'center'; const makeButton = (label, color, callback) => { const btn = document.createElement('button'); btn.textContent = label; btn.style.cssText = ` margin: 0 5px; padding: 10px 20px; background: ${color}; color: white; border: none; border-radius: 4px; cursor: pointer; `; btn.onclick = callback; return btn; }; buttonRow.appendChild(makeButton('Copy to Clipboard', '#007bff', () => { textarea.select(); document.execCommand('copy'); alert('Copied to clipboard!'); })); buttonRow.appendChild(makeButton('Close', '#6c757d', () => overlay.remove())); box.appendChild(textarea); box.appendChild(buttonRow); overlay.appendChild(box); document.body.appendChild(overlay); overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; }; const ensureLinkedTabLoaded = () => { const tab = document.getElementById('tab-linked'); if (!tab || tab.classList.contains('active')) return Promise.resolve(); tab.click(); return new Promise((resolve) => { const waitUntilLoaded = () => { const isReady = document.querySelector('#linked table tbody tr'); if (isReady) resolve(); else setTimeout(waitUntilLoaded, 300); }; waitUntilLoaded(); }); }; const createParseButton = () => { if (document.getElementById('steamdb-parse-button')) return; const btn = document.createElement('button'); btn.id = 'steamdb-parse-button'; btn.textContent = 'Parse SteamDB Data'; btn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #28a745; color: white; border: none; padding: 12px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); z-index: 1000; `; btn.onclick = async () => { const mainAppId = getMainAppId(); if (!mainAppId) return; await ensureLinkedTabLoaded(); const linkedApps = parseAppTable('#linked', mainAppId, IGNORE_TYPES); const dlcApps = parseAppTable('#dlc', mainAppId); const additionalApps = linkedApps; const dlcCombined = [...linkedApps.filter(e => e.type === 'DLC'), ...dlcApps]; const output = generateYamlOutput(mainAppId, additionalApps, dlcCombined); if (!output) { alert('Could not parse any app data.'); return; } showOutputModal(output); }; document.body.appendChild(btn); }; const waitForTabs = () => { const hasTab = document.getElementById('tab-linked') || document.getElementById('tab-dlc'); if (hasTab) createParseButton(); else setTimeout(waitForTabs, 1000); }; const observeNavigationChanges = () => { let previousPath = location.pathname; const observer = new MutationObserver(() => { if (location.pathname !== previousPath) { previousPath = location.pathname; setTimeout(waitForTabs, 1000); } }); observer.observe(document.body, { childList: true, subtree: true }); }; // === Bootstrapping === if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { waitForTabs(); observeNavigationChanges(); }); } else { waitForTabs(); observeNavigationChanges(); } })();