Steam提取游戏语言和标签

提取Steam游戏的热门标签和语言信息,并提供一键复制功能

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Steam提取游戏语言和标签
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  提取Steam游戏的热门标签和语言信息,并提供一键复制功能
// @author       WhiteLycoris and DeepSeek, thanks lucianjp
// @match        *://store.steampowered.com/app/*
// @grant        GM_setClipboard
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let extractedTags = null;
    let extractedLanguages = null;
    let extractedLanguagesDetailed = null;
    let infoPanelCreated = false;
    let languagesExtracted = false;

    // 使用更早的加载时机,不等待页面完全加载
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startExtraction);
    } else {
        startExtraction();
    }

    function startExtraction() {
        // 立即开始尝试提取
        tryExtractInfo();

        // 同时设置观察器,以防内容延迟加载
        setupObservers();

        // 设置超时检查,确保最终能提取到信息
        setTimeout(finalAttempt, 5000);
    }

    function tryExtractInfo() {
        // 尝试提取标签
        if (!extractedTags) {
            extractedTags = extractAllTags();
        }

        // 尝试提取语言(包括隐藏的语言)
        if (!languagesExtracted) {
            const languageData = extractAllLanguagesDetailed();
            if (languageData) {
                extractedLanguages = languageData.simple;
                extractedLanguagesDetailed = languageData.detailed;
                languagesExtracted = true;
            }
        }

        // 如果已经提取到信息且面板尚未创建,则创建面板
        if ((extractedTags || extractedLanguages) && !infoPanelCreated) {
            createInfoPanel(extractedTags, extractedLanguages, extractedLanguagesDetailed);
            infoPanelCreated = true;
        }
    }

    function setupObservers() {
        // 观察整个文档的变化,以便在相关区域加载时立即提取
        const observer = new MutationObserver(function(mutations) {
            let shouldUpdate = false;

            mutations.forEach(function(mutation) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(function(node) {
                        if (node.nodeType === 1) { // Element node
                            // 检查是否添加了标签或语言相关的元素
                            if (node.querySelector && (
                                node.querySelector('#glanceCtnResponsiveRight .glance_tags.popular_tags') ||
                                node.querySelector('#languageTable table.game_language_options') ||
                                node.id === 'glanceCtnResponsiveRight' ||
                                node.id === 'languageTable'
                            )) {
                                shouldUpdate = true;
                            }
                        }
                    });
                }
            });

            if (shouldUpdate) {
                tryExtractInfo();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 特别观察标签和语言区域
        observeSpecificAreas();
    }

    function observeSpecificAreas() {
        // 尝试找到标签容器并观察其变化
        const tagsContainer = document.querySelector('#glanceCtnResponsiveRight');
        if (tagsContainer) {
            const tagsObserver = new MutationObserver(tryExtractInfo);
            tagsObserver.observe(tagsContainer, {
                childList: true,
                subtree: true,
                characterData: true
            });
        }

        // 尝试找到语言容器并观察其变化
        const langContainer = document.querySelector('.block.responsive_apppage_details_right');
        if (langContainer) {
            const langObserver = new MutationObserver(tryExtractInfo);
            langObserver.observe(langContainer, {
                childList: true,
                subtree: true,
                characterData: true
            });
        }
    }

    function finalAttempt() {
        // 最终尝试提取信息
        if (!infoPanelCreated) {
            tryExtractInfo();
        }
    }

    function extractAllTags() {
        const tagsContainer = document.querySelector('#glanceCtnResponsiveRight .glance_tags.popular_tags');
        if (!tagsContainer) return null;

        const tags = [];
        const tagElements = tagsContainer.querySelectorAll('a.app_tag');

        tagElements.forEach(tag => {
            // 提取所有标签,包括隐藏的,但排除"+"按钮
            if (tag.textContent.trim() && !tag.classList.contains('add_button')) {
                let tagText = tag.textContent.trim();

                // 格式化标签文本
                tagText = formatTagText(tagText);

                if (tagText && !tags.includes(tagText)) {
                    tags.push(tagText);
                }
            }
        });

        return tags.length > 0 ? tags.join(', ') : null;
    }

    function formatTagText(tagText) {
        // 将减号替换为点号
        tagText = tagText.replace(/-/g, '.');

        // 将空格替换为点号
        tagText = tagText.replace(/\s+/g, '.');

        // 将大写字母转换为小写
        tagText = tagText.toLowerCase();

        return tagText;
    }

    function extractAllLanguagesDetailed() {
        // 尝试多种选择器,因为Steam的HTML结构可能有变化
        const languageTables = [
            document.querySelector('#languageTable table.game_language_options'),
            document.querySelector('.game_language_options'),
            document.querySelector('.block.responsive_apppage_details_right table')
        ];

        let languageTable = null;
        for (let table of languageTables) {
            if (table) {
                languageTable = table;
                break;
            }
        }

        if (!languageTable) {
            console.log('未找到语言表格');
            return null;
        }

        // 一次性获取所有行
        const languageRows = languageTable.rows;
        const rowCount = languageRows.length;

        if (rowCount <= 1) return null; // 只有表头行或没有行

        const languages = [];
        // 详细语言支持信息 - 合并Interface和Subtitles
        const detailedLanguages = {
            interfaceSubtitles: [],
            fullAudio: []
        };

        // 跳过表头行,从索引1开始
        for (let i = 1; i < rowCount; i++) {
            const row = languageRows[i];
            // 使用cells属性而不是querySelector
            const cells = row.cells;
            if (cells.length === 0) continue;

            // 第一列是语言名称
            const languageName = cells[0].textContent.trim();
            if (!languageName) continue;

            // 直接收集所有语言,不再检查display状态
            if (!languages.includes(languageName)) {
                languages.push(languageName);
            }

            // 检查支持类型 - 使用更高效的方法
            // 第二列是界面支持,第四列是字幕支持 - 合并两者
            const hasInterface = cells[1] && cells[1].textContent.includes('✔');
            const hasSubtitles = cells[3] && cells[3].textContent.includes('✔');

            if ((hasInterface || hasSubtitles) && !detailedLanguages.interfaceSubtitles.includes(languageName)) {
                detailedLanguages.interfaceSubtitles.push(languageName);
            }

            // 第三列是全音频支持
            if (cells[2] && cells[2].textContent.includes('✔')) {
                if (!detailedLanguages.fullAudio.includes(languageName)) {
                    detailedLanguages.fullAudio.push(languageName);
                }
            }
        }

        console.log(`提取到 ${languages.length} 种语言:`, languages);
        console.log('详细语言支持信息:', detailedLanguages);

        // 格式化详细语言信息
        let detailedText = "";
        if (detailedLanguages.interfaceSubtitles.length > 0) {
            detailedText += `Interface and Subtitles: ${detailedLanguages.interfaceSubtitles.join(', ')}\n`;
        }
        if (detailedLanguages.fullAudio.length > 0) {
            detailedText += `Full Audio: ${detailedLanguages.fullAudio.join(', ')}`;
        }

        return {
            simple: languages.length > 0 ? `Languages: ${languages.join(', ')}` : null,
            detailed: detailedText
        };
    }

    function createInfoPanel(tags, languages, languagesDetailed) {
        // 如果面板已经存在,先移除
        const existingPanel = document.getElementById('steam-info-extractor-panel');
        if (existingPanel) {
            existingPanel.remove();
        }

        // 创建信息面板容器
        const panel = document.createElement('div');
        panel.id = 'steam-info-extractor-panel';
        /*
        CSS参数说明(此处是示例区,下方才是代码区)
        position: fixed; - 固定定位,不随页面滚动而移动
        top: 65px; - 距离页面顶部65像素
        right: 35px; - 距离页面右侧35像素
        background: #1b2838; - 背景颜色(深蓝色)
        border: 2px solid #67c1f5; - 边框(2像素宽,浅蓝色)
        border-radius: 8px; - 边框圆角8像素
        padding: 10px; - 内边距10像素
        color: white; - 文字颜色白色
        font-family: Arial, sans-serif; - 字体
        font-size: 12px; - 字体大小12像素
        z-index: 9999; - 层级,确保显示在最前面
        max-width: 300px; - 最大宽度300像素
        max-height: 300px; - 面板整体最大高度300像素
        overflow-y: auto; - 当内容超过最大高度时显示垂直滚动条
        box-shadow: 0 4px 12px rgba(0,0,0,0.5); - 阴影效果
        */
        panel.style.cssText = `
            position: fixed;
            top: 65px;
            right: 35px;
            background: #1b2838;
            border: 2px solid #67c1f5;
            border-radius: 8px;
            padding: 10px;
            color: white;
            font-family: Arial, sans-serif;
            font-size: 12px;
            z-index: 9999;
            max-width: 300px;
            max-height: 300px;
            overflow-y: auto;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
        `;

        // 创建标题
        const title = document.createElement('div');
        title.textContent = 'Steam提取游戏语言和标签';
        title.style.cssText = `
            font-weight: bold;
            margin-bottom: 10px;
            color: #67c1f5;
            border-bottom: 1px solid #67c1f5;
            padding-bottom: 5px;
        `;
        panel.appendChild(title);

        // 创建信息显示区域
        const infoContent = document.createElement('div');
        infoContent.style.cssText = `
            margin-bottom: 10px;
            line-height: 1.4;
        `;

        // 先添加语言信息和复制按钮
        if (languagesDetailed) {
            const langSection = document.createElement('div');
            langSection.style.marginBottom = '15px';

            const langHeader = document.createElement('div');
            langHeader.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;';

            const langTitle = document.createElement('strong');
            langTitle.textContent = '支持语言:';
            langHeader.appendChild(langTitle);

            const copyLangBtn = createCopyButton('复制语言', languagesDetailed);
            langHeader.appendChild(copyLangBtn);

            langSection.appendChild(langHeader);

            const langText = document.createElement('div');
            langText.textContent = languagesDetailed;
            langText.style.cssText = 'background: rgba(103, 193, 245, 0.1); padding: 8px; border-radius: 4px; font-size: 13px; white-space: pre-line;';
            langSection.appendChild(langText);

            infoContent.appendChild(langSection);
        }

        // 再添加标签信息和复制按钮
        if (tags) {
            const tagsSection = document.createElement('div');

            const tagsHeader = document.createElement('div');
            tagsHeader.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;';

            const tagsTitle = document.createElement('strong');
            tagsTitle.textContent = '热门标签:';
            tagsHeader.appendChild(tagsTitle);

            const copyTagsBtn = createCopyButton('复制标签', tags);
            tagsHeader.appendChild(copyTagsBtn);

            tagsSection.appendChild(tagsHeader);

            const tagsText = document.createElement('div');
            tagsText.textContent = tags;
            tagsText.style.cssText = 'background: rgba(103, 193, 245, 0.1); padding: 8px; border-radius: 4px; font-size: 13px;';
            tagsSection.appendChild(tagsText);

            infoContent.appendChild(tagsSection);
        }

        panel.appendChild(infoContent);

        // 创建关闭按钮
        const closeButton = document.createElement('button');
        closeButton.textContent = '×';
        closeButton.style.cssText = `
            position: absolute;
            top: 5px;
            right: 8px;
            background: none;
            border: none;
            color: #67c1f5;
            font-size: 18px;
            cursor: pointer;
            padding: 0;
            width: 20px;
            height: 20px;
        `;

        closeButton.addEventListener('click', function() {
            document.body.removeChild(panel);
            infoPanelCreated = false;
        });

        panel.appendChild(closeButton);

        // 添加到页面
        document.body.appendChild(panel);
    }

    function createCopyButton(text, content) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.cssText = `
            background: rgba(103, 193, 245, 0.2);
            color: #67c1f5;
            border: 1px solid #67c1f5;
            padding: 4px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            transition: all 0.3s ease;
        `;

        button.addEventListener('mouseover', function() {
            this.style.background = 'rgba(103, 193, 245, 0.4)';
        });

        button.addEventListener('mouseout', function() {
            this.style.background = 'rgba(103, 193, 245, 0.2)';
        });

        button.addEventListener('click', function() {
            GM_setClipboard(content, 'text');
            showCopyFeedback(this, '✓ 已复制!');
        });

        return button;
    }

    function showCopyFeedback(button, feedbackText) {
        const originalText = button.textContent;
        const originalBackground = button.style.background;

        button.textContent = feedbackText;
        button.style.background = '#5cb85c';
        button.style.borderColor = '#5cb85c';
        button.style.color = 'white';

        setTimeout(() => {
            button.textContent = originalText;
            button.style.background = originalBackground;
            button.style.borderColor = '#67c1f5';
            button.style.color = '#67c1f5';
        }, 2000);
    }
})();