Kimochi Gaming Tools

提供对 Kimochi Gaming 的界面汉化、搜索标签汉化辅助以及一些小功能

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Kimochi Gaming Tools
// @namespace    http://tampermonkey.net/
// @version      2.0.1
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kimochi.info
// @description  提供对 Kimochi Gaming 的界面汉化、搜索标签汉化辅助以及一些小功能
// @author       RONUKUIGO
// @match        https://kimochi.info/*
// @grant        none
// @run-at       document-start
// @require      https://update.greasyfork.org/scripts/582521/1850372/Kimochi%20Gaming%20Dictionary.js
// ==/UserScript==

(function () {
    'use strict';

    let DictLib = null;
    let TranslationRules = [];

    // ================== 1. 运行时匹配规则引擎 ==================
    const RuleEngine = {
        lastUrl: '',
        activeRules: [],
        updateActiveRules() {
            const currentUrl = window.location.href;
            if (currentUrl !== this.lastUrl) {
                this.lastUrl = currentUrl;
                this.activeRules = TranslationRules.filter(rule =>
                    rule.urls.some(regex => regex.test(currentUrl))
                );
            }
        },
        translate(originalText, element) {
            this.updateActiveRules();
            let text = originalText;
            for (let rule of this.activeRules) {
                if (!rule.dict) continue;

                // 检查元素白名单限制
                if (rule.selectors && rule.selectors.length > 0) {
                    const selectorStr = rule.selectors.join(',');
                    if (!element || !element.closest(selectorStr)) {
                        continue;
                    }
                }

                // 精准匹配
                if (rule.dict[text]) return rule.dict[text];

                // 长词部分包含替换 (长度限制>3防止短词误伤)
                for (let key in rule.dict) {
                    if (text.includes(key) && key.length > 3) {
                        text = text.replace(key, rule.dict[key]);
                    }
                }
            }
            return text;
        }
    };

    // ================== 2. 界面汉化功能 ==================

    // 动态匹配替换规则
    function dynamicTranslate(text) {
        const monthsMap = {
            jan: '1月', janua: '1月', feb: '2月', febru: '2月',
            mar: '3月', march: '3月', apr: '4月', april: '4月',
            may: '5月', jun: '6月', june: '6月', jul: '7月', july: '7月',
            aug: '8月', augus: '8月', sep: '9月', sept: '9月', septe: '9月',
            oct: '10月', octob: '10月', nov: '11月', novem: '11月', dec: '12月', decem: '12月'
        };

        const monthsRegexStr = "\\b(Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sept?(?:ember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\\b";

        function getMonthChinese(monthStr) {
            let key = monthStr.toLowerCase().replace('.', '').substring(0, 4);
            if (monthsMap[key]) return monthsMap[key];
            key = key.substring(0, 3);
            return monthsMap[key] || (monthStr + '月');
        }

        // 处理带有年份的日期
        let dateWithYearRegex = new RegExp(monthsRegexStr + '(?:\\.)?\\s+(\\d{1,2}),?\\s+(\\d{4})', 'i');
        if (dateWithYearRegex.test(text)) {
            text = text.replace(dateWithYearRegex, (match, m, d, y) => {
                return `${y}年${getMonthChinese(m)}${parseInt(d)}日`;
            });
        }

        // 处理不带年份的日期
        let dateRegex = new RegExp('(?:[A-Za-z]{3,10},\\s+)?' + monthsRegexStr + '(?:\\.)?\\s+(\\d{1,2})\\b', 'i');
        if (dateRegex.test(text)) {
            text = text.replace(dateRegex, (match, m, d) => {
                if (match.toLowerCase().includes('view') || match.toLowerCase().includes('game')) return match;
                return `${getMonthChinese(m)}${parseInt(d)}日`;
            });
        }

        // 浏览量、游戏数量转换
        text = text.replace(/(•\s*)([\d.]+K?)\s+VIEWS?\s+TODAY/i, '$1今日 $2 浏览量');
        text = text.replace(/(\d+)\s+games?/i, '$1 个游戏');

        // 累计浏览量转换
        let viewsRegex = /(\d+(?:\.\d+)?K?)\s+views?/i;
        if (viewsRegex.test(text) && !text.includes('今日')) {
            text = text.replace(viewsRegex, '$1 次浏览');
        }

        return text;
    }

    // 获取并缓存页面中需要排除的黑名单文本(H1和H3)
    function getHeaderTexts() {
        const selector = [
            'h1.text-4xl.md\\:text-5xl.lg\\:text-6xl.font-headline.font-black.tracking-tighter.uppercase.leading-\\[0\\.95\\].text-on-surface',
            'h3.font-headline.font-bold.text-sm.md\\:text-base.leading-snug.line-clamp-2.text-on-surface.group-hover\\:text-primary.transition-colors'
        ].join(',');

        return new Set(
            Array.from(document.querySelectorAll(selector))
                .map(el => el.textContent.trim())
                .filter(text => text.length > 0)
        );
    }

    // 替换节点文本的核心函数
    function translateNode(node, blacklistedTexts) {
        if (!node) return;

        // 核心过滤逻辑 (包含新增 of searctags)
        const skipSelector = [
            'h1.text-4xl.md\\:text-5xl.lg\\:text-6xl.font-headline.font-black.tracking-tighter.uppercase.leading-\\[0\\.95\\].text-on-surface',
            'h3.font-headline.font-bold.text-sm.md\\:text-base.leading-snug.line-clamp-2.text-on-surface.group-hover\\:text-primary.transition-colors',
            '.searctags',            // 跳过标签候选词的英文部分
            '.search-suggest-panel'  // 跳过整个标签面板
        ].join(',');

        const currentElement = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;

        if (currentElement) {
            // 规则 1:匹配跳过列表自身或父级
            if (currentElement.matches(skipSelector) || (currentElement.parentElement && currentElement.parentElement.matches(skipSelector))) {
                return;
            }

            // 规则 2:P 标签内容去重跳过
            const pElement = currentElement.closest('p');
            if (pElement && blacklistedTexts.has(pElement.textContent.trim())) {
                return;
            }
        }

        // 1. 处理纯文本节点
        if (node.nodeType === Node.TEXT_NODE) {
            let text = node.nodeValue.trim();
            if (!text) return;

            let translated = RuleEngine.translate(text, node.parentElement);
            if (translated !== node.nodeValue) {
                node.nodeValue = translated;
            } else {
                let dynamicText = dynamicTranslate(node.nodeValue);
                if (dynamicText !== node.nodeValue) {
                    node.nodeValue = dynamicText;
                }
            }
        }
        // 2. 处理带有文本属性的元素节点
        else if (node.nodeType === Node.ELEMENT_NODE) {
            if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'].includes(node.nodeName)) return;

            if (node.placeholder) {
                let trimmedPlaceholder = node.placeholder.trim();
                let translatedPlaceholder = RuleEngine.translate(trimmedPlaceholder, node);
                if (translatedPlaceholder !== trimmedPlaceholder) {
                    node.placeholder = translatedPlaceholder;
                }
            }
            for (let child of node.childNodes) {
                translateNode(child, blacklistedTexts);
            }
        }
    }

    // 全局扫描翻译入口
    function translatePage() {
        const blacklistedTexts = getHeaderTexts(); // 每次大范围翻译前获取一次,节省算力
        translateNode(document.body, blacklistedTexts);
    }


    // ================== 3. 标签搜索建议功能 ==================

    // 核心样式更新
    const style = document.createElement('style');
    style.innerHTML = `
        .custom-target-p {
            display: inline-flex !important;
            align-items: center !important;
            flex-wrap: nowrap !important;
            width: 100% !important;
        }

        /* 标签标题防折叠的样式保护 */
        div[data-tax="post_tag"] > div:first-child {
            white-space: nowrap !important;
            flex-shrink: 0 !important;
        }

        .search-suggest-panel {
            display: inline-flex;
            flex-direction: row;
            align-items: center;
            background-color: #252020;
            border-left: 1px solid #d195ff;
            border-right: 1px solid #d195ff;
            border-top: none;
            border-bottom: none;
            align-self: stretch !important;
            box-sizing: border-box !important;
            margin-left: 12px;
            width: max-content;
            flex-shrink: 1;
            min-width: 0;
            overflow-x: auto;
            overflow-y: hidden;
            z-index: 99999;
            padding: 0 4px;
            box-shadow: 0px 2px 6px rgba(0,0,0,0.4);
            color: #e0d0cc;
            font-family: sans-serif;
            -ms-overflow-style: none;
            scrollbar-width: none;
        }
        .search-suggest-panel::-webkit-scrollbar {
            display: none;
        }
        .search-suggest-item {
            padding: 0 8px;
            height: 100%;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 5px;
            white-space: nowrap;
            border-radius: 4px;
            font-size: 13px;
            transition: background 0.2s;
        }
        .search-suggest-item:hover {
            background-color: #3d302f;
            color: #ffb4ab;
        }
        /* 独立的 searctags 英文标签容器样式 */
        .search-suggest-item .searctags {
            font-size: 0.8em;
            color: #a08c8a;
            background: #1e1b1b;
            padding: 1px 4px;
            border-radius: 3px;
        }
    `;
    document.head.appendChild(style);

    function initDropdownLogic(inputEl, targetContainer) {
        if (inputEl.dataset.suggestBound) return;
        inputEl.dataset.suggestBound = "true";

        const targetP = targetContainer.querySelector('div > div:nth-of-type(2) > p');
        if (!targetP) return;

        targetP.classList.add('custom-target-p');

        const suggestPanel = document.createElement('span');
        suggestPanel.className = 'search-suggest-panel';
        suggestPanel.style.display = 'none';
        targetP.appendChild(suggestPanel);

        inputEl.addEventListener('input', function () {
            const query = this.value.trim().toLowerCase();

            if (!query) {
                suggestPanel.style.display = 'none';
                return;
            }

            // 使用专用的 DictLib.dict.tags 进行搜索
            const dictTags = (DictLib && DictLib.dict && DictLib.dict.tags) || {};
            const matches = Object.entries(dictTags).filter(([eng, chn]) => {
                return chn.toLowerCase().includes(query) || eng.toLowerCase().includes(query);
            });

            if (matches.length > 0) {
                suggestPanel.style.display = 'inline-flex';
                suggestPanel.innerHTML = '';

                matches.forEach(([eng, chn]) => {
                    const item = document.createElement('div');
                    item.className = 'search-suggest-item';
                    item.innerHTML = `<span>${chn}</span><span class="searctags">${eng}</span>`;

                    item.addEventListener('mousedown', function (e) {
                        e.preventDefault();
                        e.stopPropagation();

                        inputEl.value = eng;
                        suggestPanel.style.display = 'none';
                        inputEl.focus();

                        inputEl.dispatchEvent(new Event('input', { bubbles: true }));
                        inputEl.dispatchEvent(new Event('change', { bubbles: true }));
                    });
                    suggestPanel.appendChild(item);
                });
            } else {
                suggestPanel.style.display = 'none';
            }
        });

        document.addEventListener('click', function (e) {
            if (e.target !== inputEl && !suggestPanel.contains(e.target)) {
                suggestPanel.style.display = 'none';
            }
        });
    }

    function findAndBindInput() {
        const targetContainers = document.querySelectorAll('div[data-tax="post_tag"]');
        targetContainers.forEach(container => {
            const input = container.querySelector('input[type="text"], input[type="search"], input:not([type])');
            if (input) {
                initDropdownLogic(input, container);
            }
        });
    }


    // ================== 4. 自动下载与全局初始化 ==================

    // 跳过下载页等待并直接跳转
    function checkDownload() {
        const downloadBtn = document.getElementById('downloadBtn');
        if (downloadBtn && downloadBtn.href) {
            const targetUrl = downloadBtn.href.trim();
            if (targetUrl.startsWith('http') && targetUrl !== window.location.href) {
                // 常见国外云盘特征正则
                const isTargetCloud = /drive\.google|mediafire|mega\.nz|mega\.co\.nz|dropbox|pixeldrain|gofile|onedrive|1drv\.ms|yadi\.sk|yandex|workupload|qiwi|krakenfiles|terabox|rapidgator|ddownload/i.test(targetUrl);
                const isDownloadPage = window.location.pathname.includes('/download');

                if (isTargetCloud || isDownloadPage) {
                    console.log('已捕获目标链接,立即执行无等待跳转:', targetUrl);
                    window.location.replace(targetUrl); // 使用 replace 避免历史记录混乱,且跳过跳转页面等待
                }
            }
        }
    }

    // 统一使用一个 MutationObserver 处理动态元素及按钮 href 变更(优化性能与跳过跳转)
    const observer = new MutationObserver((mutations) => {
        let hasNewNodes = false;
        let hrefUpdated = false;

        for (let mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                hasNewNodes = true;
            }
            if (mutation.type === 'attributes' && mutation.attributeName === 'href' && mutation.target.id === 'downloadBtn') {
                hrefUpdated = true;
            }
        }

        // 只有在真的添加了新节点时才执行遍历和翻译
        if (hasNewNodes) {
            const blacklistedTexts = getHeaderTexts();
            for (let mutation of mutations) {
                if (mutation.addedNodes.length > 0) {
                    for (let node of mutation.addedNodes) {
                        translateNode(node, blacklistedTexts);
                    }
                }
            }
            findAndBindInput(); // 重新检查并绑定输入框
        }

        if (hasNewNodes || hrefUpdated) {
            checkDownload(); // 立即触发下载跳转检查
        }
    });

    // 延时与兜底的初始化函数
    function init() {
        DictLib = window.GameTranslationLib;
        if (!DictLib || !DictLib.dict) {
            // 延时重试,防止由于脚本加载顺序造成的竞态问题导致失效
            setTimeout(init, 10);
            return;
        }

        const dict = DictLib.dict;
        // 如果外置字典为老版本(仅 ui 与 tags),则自动兜底将 ui 作为全局公共字典 common
        const commonDict = dict.common || dict.ui || {};
        const tagsDict = dict.tags || {};

        TranslationRules = [
            // 1. 标签专属汉化 (白名单限定,优先级最高)
            {
                name: "标签专属汉化",
                urls: [/.*/],
                selectors: ['.searctags', '.search-suggest-panel', 'div[data-tax="post_tag"]', 'a[href*="/tag/"]'],
                dict: tagsDict
            },
            // 2. 各页面特定作用域汉化 (按专有度降序排序,未匹配到的专属分区将自动使用 common 兜底)
            {
                name: "主页专属汉化",
                urls: [/^https?:\/\/kimochi\.info\/?$/, /^https?:\/\/kimochi\.info\/\?/],
                selectors: [],
                dict: dict.home || commonDict
            },
            {
                name: "趋势页专属汉化",
                urls: [/\/trending/],
                selectors: [],
                dict: dict.trending || commonDict
            },
            {
                name: "探索页专属汉化",
                urls: [/\/explore$/],
                selectors: [],
                dict: dict.explore || commonDict
            },
            {
                name: "子页专属汉化",
                urls: [/\/browse/, /\/explore\//],
                selectors: [],
                dict: dict.browse || commonDict
            },
            {
                name: "下载页专属汉化",
                urls: [/\/download/],
                selectors: [],
                dict: dict.download || commonDict
            },
            {
                name: "搜索页专属汉化",
                urls: [/\/search/, /\/query/],
                selectors: [],
                dict: dict.search || commonDict
            },
            {
                name: "详情页专属汉化",
                urls: [/\/game\//],
                selectors: [],
                dict: dict.detail || commonDict
            },
            // 3. 全局共享兜底词库 (UI/类别/盘/语言)
            {
                name: "全局基础UI",
                urls: [/.*/],
                selectors: [],
                dict: commonDict
            },
            {
                name: "类别汉化",
                urls: [/.*/],
                selectors: [],
                dict: dict.categories || commonDict
            },
            {
                name: "盘类型汉化",
                urls: [/.*/],
                selectors: [],
                dict: dict.drives || commonDict
            },
            {
                name: "语言汉化",
                urls: [/.*/],
                selectors: [],
                dict: dict.languages || commonDict
            }
        ];

        RuleEngine.updateActiveRules();

        const runAll = () => {
            translatePage();
            findAndBindInput();
            checkDownload();
        };

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                runAll();
                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                    attributes: true,
                    attributeFilter: ['href']
                });
            });
        } else {
            runAll();
            if (document.body) {
                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                    attributes: true,
                    attributeFilter: ['href']
                });
            }
        }

        window.addEventListener('load', runAll);
    }

    init();

})();