Greasy Fork

Xbox Cloud Gaming 游戏信息汉化

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

目前为 2024-10-27 提交的版本。查看 最新版本

// ==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.org/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.org/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);
    });

})();