// ==UserScript==
// @name Xbox Cloud Gaming 游戏信息汉化
// @namespace https://b1ue.me
// @description 汉化信息并使游戏搜索支持中文
// @version 1.0.8
// @author b1ue
// @license MIT
// @match https://www.xbox.com/*/*play*
// @run-at document-start
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
const Nconfig = {
localizeGameInfo: true, //游戏信息汉化
alwaysShowTitle: 2, //移动端保持标题显示
fullScreenlandscape: false, //全屏时强制横屏
};
const BXCG = {
getValue: (key, defaultValue) => {
if(typeof GM_getValue === 'function') return GM_getValue(key, defaultValue);
let _val = localStorage.getItem('BXCG_' + key) ?? JSON.stringify(defaultValue);
try { _val = JSON.parse(_val) ;} catch (e) {}
return _val;
},
setValue: (key, value) => {
if(typeof GM_setValue === 'function') return GM_setValue(key, value);
return localStorage.setItem('BXCG_' + key, JSON.stringify(value));
},
getTitleList: async() => {
return await fetch("https://update.greasyfork.org/scripts/493376/xbt-title.js", {cache: "reload"}).then(res => res.text()).catch(() => null);
}
};
const oWindow = self.unsafeWindow || window;
Object.keys(Nconfig).forEach(key => {
let _val = BXCG.getValue(key);
if(_val != null) Nconfig[key] = _val;
});
let game_titles = {};
(async () => {
const timestamp = () => Math.floor(new Date().getTime() / 1000);
let resText = BXCG.getValue('game_titles');
const game_titles_gettime = BXCG.getValue("game_titles_gettime", 0);
if(!resText || timestamp() - game_titles_gettime > 7200){
resText = await BXCG.getTitleList();
if(resText){
BXCG.setValue("game_titles", resText);
BXCG.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];
}
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) 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('/sigls/v2')) {
let ourl = new URL(url);
ourl.searchParams.set("language",browserFirstLanguage);
let nurl = ourl.toString();
arg[0] = new Request(nurl, {
method: arg0.method,
headers: arg0.headers,
});
} 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__;
const handle_state = (state) => {
if(Nconfig.localizeGameInfo){
try {
state.appContext.marketInfo.locale = browserFirstLanguage;
for(let gId in state.xcloud.products.data){
let title_zh = "";
if(gId in game_titles && (title_zh = game_titles[gId][0])) state.xcloud.products.data[gId].data.title = title_zh;
}
for(let i in state.xcloud.hero.heroData.data){
const gId = state.xcloud.hero.heroData.data[i].productID;
let title_zh = "";
if(gId in game_titles && (title_zh = game_titles[gId][0])) state.xcloud.hero.heroData.data[i].title = title_zh;
state.xcloud.hero.heroData.data[i].subtitle = '';
}
} catch (e) {}
}
__PRELOADED_STATE__ = state;
};
Object.defineProperty(oWindow, '__PRELOADED_STATE__', {
configurable: true,
get: () => {
return __PRELOADED_STATE__;
},
set: state => {
handle_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) => {
handle_state(state);
descriptor.set = NATIVE_descriptor_set;
Object.defineProperty = NATIVE_Object_defineProperty;
return NATIVE_descriptor_set(state, ...arg);
};
}
}
return NATIVE_Object_defineProperty(obj, prop, descriptor);
};
if(typeof GM_registerMenuCommand === 'function'){
let updateMenu = (param) => {
(param <= 1) && GM_registerMenuCommand(`${BXCG.getValue('localizeGameInfo',Nconfig.localizeGameInfo)?'✅':'❌'} 游戏信息汉化`, (event) => {
BXCG.setValue("localizeGameInfo",!BXCG.getValue('localizeGameInfo',Nconfig.localizeGameInfo));
if(typeof GM_notification === 'function') 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;
BXCG.setValue("alwaysShowTitle", Nconfig.alwaysShowTitle);
toggleTitleVisible();
updateMenu(2);
},{id:'alwaysShowTitle_id', autoClose:false});
}
(param <= 3) && GM_registerMenuCommand(`${Nconfig.fullScreenlandscape?'✅':'❌'} 全屏时强制横屏`, (event) => {
Nconfig.fullScreenlandscape = !Nconfig.fullScreenlandscape;
BXCG.setValue("fullScreenlandscape",Nconfig.fullScreenlandscape);
updateMenu(3);
},{id:'fullScreenlandscape_id', autoClose:false});
}
updateMenu(0);
}
document.addEventListener("DOMContentLoaded", (event) => {
setTimeout(() => {toggleTitleVisible()},100);
});
})();