Kimochi Gaming Tools

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

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();