Greasy Fork

Greasy Fork is available in English.

Xbox Cloud Gaming 游戏信息汉化

汉化信息并使游戏搜索支持中文

当前为 2024-10-27 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                 Xbox Cloud Gaming 游戏信息汉化
// @namespace            https://b1ue.me
// @description          汉化信息并使游戏搜索支持中文
// @version              1.0.5
// @author               b1ue
// @license              MIT
// @match                https://www.xbox.com/*/*play*
// @run-at               document-start
// @grant                GM_getResourceText
// @grant                GM.xmlHttpRequest
// @grant                GM_registerMenuCommand
// @grant                GM_getValue
// @grant                GM_setValue
// @grant                GM_notification
// @grant                unsafeWindow
// @connect              update.greasyfork.org
// @resource game_titles https://update.greasyfork.icu/scripts/493376/xbt-title.js
// ==/UserScript==

(function() {
    'use strict';
    const Nconfig = {
        localizeGameInfo: true,			//游戏信息汉化
        alwaysShowTitle: 2,				//移动端保持标题显示
        fullScreenlandscape: true,		//全屏时强制横屏
    };
    Object.keys(Nconfig).forEach(key => {
        let _val = GM_getValue(key);
        try { _val = JSON.parse(_val) ;} catch (e) {}
        if(_val != null) Nconfig[key] = _val;
    });

    let game_titles = {};
    (async () => {
        const timestamp = () => Math.floor(new Date().getTime() / 1000);
        let resText = GM_getValue('game_titles');
        const game_titles_gettime = GM_getValue("game_titles_gettime") || 0;
        if(!resText || timestamp() - game_titles_gettime > 7200){
        	try {
				const r = await GM.xmlHttpRequest({url: "https://update.greasyfork.icu/scripts/493376/xbt-title.js", nocache:true});
				if(r.status == 200) resText = r.responseText;
			} catch (e) {}
            if(!resText) resText = GM_getResourceText("game_titles");
            if(resText){
                GM_setValue("game_titles", resText);
                GM_setValue("game_titles_gettime", timestamp());
            }
        }
        game_titles = JSON.parse(resText);
    })();

    let allFullLanguages = [];
    let browserFirstLanguage = "zh-CN";
    navigator.languages.forEach(language => {
        const reg = /^[a-z]{2}-[A-Z]{2}$/;
        const isFullLanguage = reg.test(language);
        if (isFullLanguage) allFullLanguages.push(language);
    });
    if (allFullLanguages.length > 0) {
        browserFirstLanguage = allFullLanguages[0];
    }

    const oWindow = self.unsafeWindow || window;

    document.addEventListener("fullscreenchange", function (e) {
        if (document.fullscreenElement) {
            try {
                Nconfig.fullScreenlandscape && screen?.orientation?.lock("landscape");
            } catch (e) {}
        }
    });

    const originFetch = oWindow.fetch;
    oWindow.fetch = async (...arg) => {
        let arg0 = arg[0];
        let url = "";
        let isRequest = false;
        switch (typeof arg0) {
            case "object":
                url = arg0.url;
                isRequest = true;
                break;
            case "string":
                url = arg0;
                break;
            default:
                break;
        }

        if(Nconfig.localizeGameInfo != 1) return originFetch(...arg);

        if (url.includes('/v3/products')) {
            let ourl = new URL(url);
            let json = await arg0.json();
            let body = JSON.stringify(json);
            ourl.searchParams.set("language",browserFirstLanguage);
            let nurl = ourl.toString();
            arg[0] = new Request(nurl, {
                method: arg0.method,
                headers: arg0.headers,
                body: body,
            });

            let res = originFetch(...arg).then(response => {
                response.json = () => response.clone().json().then(json => {
                    for(let gId in json.Products){
                        let title_zh = "";
                        if(gId in game_titles && (title_zh = game_titles[gId][0])) json.Products[gId].ProductTitle = title_zh;
                    }
                    return Promise.resolve(json);
                });
                return response;
            });
            return res;
        } else if (url.includes('/search/v2')) {
            let ourl = new URL(url);
            let json = await arg0.json();
            let body = JSON.stringify(json);
            ourl.searchParams.set("language",browserFirstLanguage);
            let nurl = ourl.toString();
            arg[0] = new Request(nurl, {
                method: arg0.method,
                headers: arg0.headers,
                body: body,
            });

            const query = json.Query;
            const Scope = json.Scope;
            if(query && Scope === 'EDGEWATER'){
                let new_SearchResults = []
                for(let gId in game_titles){
                    if(game_titles[gId][0].includes(query) || game_titles[gId][1].includes(query)){
                        new_SearchResults.push(gId);
                    }
                }

                let res = originFetch(...arg).then(response => {
                    response.json = () => response.clone().json().then(async json => {
                        new_SearchResults = new_SearchResults.filter(gId => !(gId in json.SearchResults));
                        if(new_SearchResults.length > 0){
                            const response = await originFetch(`https://catalog.gamepass.com/v3/products?market=${ourl.searchParams.get("market")}&language=${browserFirstLanguage}&hydration=${ourl.searchParams.get("hydration")}`, {
                                method: 'POST',
                                headers: arg0.headers,
                                body: JSON.stringify({
                                    Products: new_SearchResults,
                                }),
                            });
                            const data = await response.json();
                            for(let gId in data.Products){
                                json.Products[gId] = data.Products[gId];
                            }
                        }
                        for(let gId in json.Products){
                            let title_zh = "";
                            if(gId in game_titles && (title_zh = game_titles[gId][0])) json.Products[gId].ProductTitle = title_zh;
                        }
                        json.SearchResults = json.SearchResults.concat(new_SearchResults);
                        return Promise.resolve(json);
                    });
                    return response;
                });
                return res;
            }
        } else if (url.includes('/v4/api/selection')) {
            let res = originFetch(...arg).then(response => {
                response.json = () => response.clone().json().then(json => {
                    let items_array = json?.batchrsp?.items;
                    if(items_array){
                        items_array.forEach( _item => {
                            const item = JSON.parse(_item?.item);
                            const title = item?.ad?.items?.[0]?.title;
                            const actionLink = item?.ad?.items?.[0]?.actionLink;
                            const gId = /msgamepass:\/\/details\?id=([A-Z0-9]+)/.exec(actionLink)?.[1]
                            if(title && gId){
                                let title_zh = "";
                                if(gId in game_titles && (title_zh = game_titles[gId][0])){
                                    item.ad.items[0].title = title_zh;
                                    _item.item = JSON.stringify(item);
                                }
                            }
                        });
                    }
                    return Promise.resolve(json);
                });
                return response;
            });
            return res;
        }
        return originFetch(...arg);
    }

    function toggleTitleVisible(){
        let action = 0;
        switch(Nconfig.alwaysShowTitle){
            case 0:
            default:
                action = 1;
                break
            case 1:
                action = 2;
                break
            case 2:
                action = ('ontouchstart' in window || navigator.msMaxTouchPoints > 0)?2:1;
                break
        }
        if(action == 1){
            document.querySelector('style#showTitle')?.remove();
        }else if(action == 2){
            if(document.querySelector('style#showTitle')) return;
            const nCss = `
[class^="GameCard-module__gameTitleInnerWrapper___"] {
	max-height: 100%!important;
	visibility: visible!important;
}
[class^="GameCard-module__children___"] {
	visibility: hidden!important;
}`
            const xfextraStyle = document.createElement('style');
            xfextraStyle.id = 'showTitle';
            xfextraStyle.innerHTML = nCss;
            const docxf = document.head || document.documentElement;
            docxf.appendChild(xfextraStyle);
        }
    }

    let __PRELOADED_STATE__;
    Object.defineProperty(oWindow, '__PRELOADED_STATE__', {
        configurable: true,
        get: () => {
            return __PRELOADED_STATE__;
        },
        set: state => {
            try {
                state.appContext.marketInfo.locale = browserFirstLanguage;
            } catch (e) {}
            __PRELOADED_STATE__ = state;
        }
    });

    const NATIVE_Object_defineProperty = Object.defineProperty;
    Object.defineProperty = (obj, prop, descriptor) => {
        if(obj === oWindow && prop === '__PRELOADED_STATE__'){
            if(descriptor && descriptor?.hasOwnProperty('set')){
                const NATIVE_descriptor_set = descriptor.set;
                descriptor.set = (state, ...arg) => {
                    try {
                        state.appContext.marketInfo.locale = browserFirstLanguage;
                    } catch (e) {}
                    descriptor.set = NATIVE_descriptor_set;
                    Object.defineProperty = NATIVE_Object_defineProperty;
                    return NATIVE_descriptor_set(state, ...arg);
                };
            }
        }
        return NATIVE_Object_defineProperty(obj, prop, descriptor);
    };

    let updateMenu = (param) => {
        (param <= 1) && GM_registerMenuCommand(`${GM_getValue('localizeGameInfo',Nconfig.localizeGameInfo)?'✅':'❌'} 游戏信息汉化`, (event) => {
            GM_setValue("localizeGameInfo",!GM_getValue('localizeGameInfo',Nconfig.localizeGameInfo));
            GM_notification({title: '设置变更', text: '修改将在刷新后生效!', tag:'notify', timeout: 2000});
            updateMenu(1);
        },{id:'localizeGameInfo_id', autoClose:false});
        if(param <= 2){
            let inx = Nconfig.alwaysShowTitle; (inx>2 || inx<0) && (inx=0);
            GM_registerMenuCommand(`${['❌','1️⃣','2️⃣'][inx]}保持标题显示[${['关闭','开启','仅移动设备'][inx]}] - 点击切换`, (event) => {
                Nconfig.alwaysShowTitle = ++inx>2?0:inx;
                GM_setValue("alwaysShowTitle", Nconfig.alwaysShowTitle);
                toggleTitleVisible();
                updateMenu(2);
            },{id:'alwaysShowTitle_id', autoClose:false});
        }
        (param <= 3) && GM_registerMenuCommand(`${Nconfig.fullScreenlandscape?'✅':'❌'} 全屏时强制横屏`, (event) => {
            Nconfig.fullScreenlandscape = !Nconfig.fullScreenlandscape;
            GM_setValue("fullScreenlandscape",Nconfig.fullScreenlandscape);
            updateMenu(3);
        },{id:'fullScreenlandscape_id', autoClose:false});
    }

    updateMenu(0);

    document.addEventListener("DOMContentLoaded", (event) => {
        setTimeout(() => {toggleTitleVisible()},100);
    });

})();