This commit is contained in:
Liwanyi 2023-02-23 16:34:46 +08:00
parent 7e52b9c382
commit ee8b660539
25 changed files with 20616 additions and 1501 deletions

4338
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,13 @@
"name": "wuhu-torn-helper",
"version": "0.8.2",
"description": "芜湖助手",
"dependencies": {},
"scripts": {
"release": "rollup -c rollup-prod.config.js && node build.js",
"watch": "rollup -c -w"
},
"devDependencies": {
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^8.5.0",
"@types/jquery": "^3.5.14",
"@types/node": "^18.0.6",
@ -16,10 +16,8 @@
"rollup": "^2.79.0",
"rollup-plugin-html-literals": "^1.1.5",
"rollup-plugin-serve": "^2.0.1",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-uglify": "^6.0.4",
"rollup-plugin-string-html": "^1.0.0",
"tslib": "^2.4.0",
"typescript": "^4.8.3",
"uglify-js": "^3.16.1"
"typescript": "^4.8.3"
}
}

View File

@ -1,20 +1,5 @@
import typescript from "@rollup/plugin-typescript";
import json from "@rollup/plugin-json";
import { string } from "rollup-plugin-string";
import { uglify } from "rollup-plugin-uglify";
import terser from "@rollup/plugin-terser";
import devConfig from "./rollup.config";
export default {
input: 'src/ts/index.ts',
output: {
file: 'dist/bundle.min.js',
format: 'iife',
},
plugins: [
typescript(),
json(),
string({
include: ["**/*.html", "**/*.css"]
}),
uglify(),
],
};
devConfig.plugins.push(terser());
export default devConfig;

View File

@ -1,7 +1,6 @@
import typescript from "@rollup/plugin-typescript";
import json from "@rollup/plugin-json";
// import template from "rollup-plugin-html-literals";
import { string } from "rollup-plugin-string";
import html from "rollup-plugin-string-html";
export default {
input: 'src/ts/index.ts',
@ -10,15 +9,22 @@ export default {
format: 'iife',
},
plugins: [
// template({
// include: '*.html',
// failOnError: true
// }),
typescript(),
json(),
string({
include: ["**/*.html", "**/*.css"]
html({
include: ["**/*.html", "**/*.css"],
minifier: {
includeAutoGeneratedTags: true,
removeAttributeQuotes: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortClassName: true,
useShortDoctype: true,
collapseWhitespace: true,
minifyCSS: true,
}
}),
// uglify(),
],
};

View File

@ -13,7 +13,7 @@ import ZhongIcon from "./ZhongIcon";
import Alert from "./utils/Alert";
import FetchEventCallback from "./action/FetchEventCallback";
import globVars from "../globVars";
import Translate from "./action/Translate";
import TranslateNew from "./action/TranslateNew";
/**
*
@ -66,9 +66,9 @@ export class Common extends WuhuBase {
}
// fetch方法处理
FetchEventCallback.getInstance();
globVars.responseHandlers.push(FetchEventCallback.getInstance().responseHandler);
// fetch方法处理-翻译
globVars.responseHandlers.push(Translate.responseHandler);
globVars.responseHandlers.push(TranslateNew.getInstance().responseHandler);
// 存钱相关
depoHelper();

View File

@ -4,28 +4,25 @@ export default class Log {
public static info(...o): void {
Log.counter.info++;
let time = this.getTime();
let flag = '[WH] IFO';
let flag = '%c WH %cIFO%c' + this.getTime() + '%c';
if (this.debug()) {
console.log(flag, time, ...o);
console.log(flag, 'background:grey;color:white;', '', 'color:grey;', '', ...o);
}
this.saveLogs(flag, time, ...o);
this.saveLogs(flag, ...o);
}
public static error(...o): void {
Log.counter.error++;
let time = this.getTime();
let flag = '[WH] ERR';
console.error(flag, time, ...o);
this.saveLogs(flag, time, ...o);
let flag = '%c WH %cERR%c' + this.getTime() + '%c';
console.error(flag, 'background:grey;color:white;', 'background:red;color:white;', 'color:grey;', '', ...o);
this.saveLogs(flag, ...o);
}
public static warn(...o): void {
Log.counter.warning++;
let time = this.getTime();
let flag = '[WH] WRN';
(this.debug()) && (console.warn(flag, time, ...o));
this.saveLogs(flag, time, ...o);
let flag = '%c WH %cWRN%c' + this.getTime() + '%c';
console.warn(flag, 'background:grey;color:white;', 'background:#ff9800;color:white;', 'color:grey;', '', ...o);
this.saveLogs(flag, ...o);
}
public static debug(): boolean {
@ -47,7 +44,7 @@ export default class Log {
let minutes = ('0' + d.getMinutes()).slice(-2);
let seconds = ('0' + d.getSeconds()).slice(-2);
let ms = ('00' + d.getMilliseconds()).slice(-3);
return `[${ year }-${ month }-${ date } ${ hours }:${ minutes }:${ seconds }.${ ms }]`;
return `${ year }-${ month }-${ date } ${ hours }:${ minutes }:${ seconds }.${ ms }`;
}
public static getLogs() {
@ -56,7 +53,7 @@ export default class Log {
private static saveLogs(...o) {
o.forEach(item => {
if (typeof item === 'string') this.logs += item;
if (typeof item === 'string') this.logs += item.replaceAll('%c', '');
else if (item !== null && item !== undefined) {
let json = '{}';
let name = Object.getPrototypeOf(item).constructor.name;
@ -79,4 +76,4 @@ export default class Log {
warning: Log.counter.warning,
}
}
}
}

View File

@ -5,7 +5,6 @@ import WuhuBase from "./WuhuBase";
import CommonUtils from "./utils/CommonUtils";
import Log from "./Log";
import WuhuConfig from "./WuhuConfig";
import Alert from "./utils/Alert";
import SHOP_BEER_STATIC_ITEM_HTML from "../static/html/buyBeer/shop_beer_static_item.html";
import ADD_BEER_HEAD_HTML from "../static/html/buyBeer/add_beer_head.html";
import QUICK_CRIMES_HTML from "../static/html/quick_crimes.html";
@ -98,16 +97,7 @@ export default class UrlPattern extends WuhuBase {
});
// 监听啤酒购买
let buyBeerResultMonitor = (url, body, opt) => {
if (url.includes('shops.php') && opt.method === 'POST') {
let req = opt.requestBody;
if (req && req.includes('step=buyShopItem') && req.includes('ID=180') && body.json && body.json['success']) {
new Alert('检测到已成功购买啤酒');
BuyBeerHelper.getInstance().skip_today();
}
}
};
globVars.responseHandlers.push(buyBeerResultMonitor);
globVars.responseHandlers.push(BuyBeerHelper.getInstance().responseHandler);
}
// 快速crime TODO 重构、与翻译解藕

View File

@ -33,8 +33,8 @@ export default class WuhuBase extends Provider {
if (condition) throw '芜湖';
}
public getClassName() {
return this.className;
}
// public getClassName() {
// return this.className;
// }
}

View File

@ -7,8 +7,9 @@ import MathUtils from "../utils/MathUtils";
import NOTIFY_HTML from "../../static/html/buyBeer/notify.html";
import CommonUtils from "../utils/CommonUtils";
import Popup from "../utils/Popup";
import ResponseInject from "../../interface/ResponseInject";
export default class BuyBeerHelper extends WuhuBase implements BeerMonitorLoop {
export default class BuyBeerHelper extends WuhuBase implements BeerMonitorLoop, ResponseInject {
className = 'BuyBeerHelper';
private isNotifying = false;
@ -119,6 +120,16 @@ export default class BuyBeerHelper extends WuhuBase implements BeerMonitorLoop {
});
popup.getElement().appendChild(confirm);
}
public responseHandler(url: string, body: { json: unknown; text: string; isModified: boolean }, opt: { method: "GET" | "POST"; requestBody: string }) {
if (url.includes('shops.php') && opt?.method === 'POST') {
let req = opt.requestBody;
if (req && req.includes('step=buyShopItem') && req.includes('ID=180') && body.json && body.json['success']) {
new Alert('检测到已成功购买啤酒');
BuyBeerHelper.getInstance().skip_today();
}
}
}
}
export interface BeerMonitorLoop {

View File

View File

@ -1,26 +1,21 @@
import WuhuBase from "../WuhuBase";
import { MiniProfile } from "../../interface/responseType/MiniProfile";
import CommonUtils from "../utils/CommonUtils";
import globVars from "../../globVars";
import WuhuConfig from "../WuhuConfig";
import Provider from "../provider/Provider";
import ResponseInject from "../../interface/ResponseInject";
/**
* fetch
*/
export default class FetchEventCallback extends WuhuBase {
export default class FetchEventCallback extends Provider implements ResponseInject {
className = "FetchEventCallback";
constructor() {
super();
globVars.responseHandlers.push((url, response) => this.handler(response, url))
}
/**
* fetch
* @param response
* @param url
* @param response
*/
public handler(response, url: string) {
public responseHandler(url: string, response) {
// mini profile 中添加上次动作
if (url.includes('profiles.php?step=getUserNameContextMenu') && WuhuConfig.get('ShowMiniProfLastAct')) {
window.setTimeout(async () => {

View File

@ -4,11 +4,18 @@ import WuhuConfig from "../WuhuConfig";
import CommonUtils from "../utils/CommonUtils";
import TornStyleBlock from "../utils/TornStyleBlock";
import TornStyleSwitch from "../utils/TornStyleSwitch";
import FetchUtils from "../utils/FetchUtils";
import ResponseInject from "../../interface/ResponseInject";
import globVars from "../../globVars";
import IUserProfileData from "../../interface/IUserProfileData";
export default class ProfileHelper extends WuhuBase {
export default class ProfileHelper extends WuhuBase implements ResponseInject {
className = 'ProfileHelper';
private readonly block;
// 曾用名已检测过标记
private task = true;
constructor() {
super();
CommonUtils.addStyle('body.wh-hide_profile_img .profile-image a.profile-image-wrapper .img-wrap img{display:none;}');
@ -21,27 +28,33 @@ export default class ProfileHelper extends WuhuBase {
Log.info('[ProfileHelper] 隐藏头像');
document.body.classList.toggle('wh-hide_profile_img');
}
let block = new TornStyleBlock('芜湖助手').insert2Dom();
this.block = new TornStyleBlock('芜湖助手').insert2Dom();
// 隐藏头像
let hideImgSwitch = new TornStyleSwitch('隐藏头像', WuhuConfig.get('HideProfileImg'));
block.append(hideImgSwitch.getBase());
this.block.append(hideImgSwitch.getBase());
hideImgSwitch.getInput().addEventListener('change', () => {
document.body.classList.toggle('wh-hide_profile_img');
WuhuConfig.set('HideProfileImg', hideImgSwitch.getInput().checked, true);
});
// 曾用名
let nameHistoryNode;
if (WuhuConfig.get('ShowNameHistory')) {
nameHistoryNode = document.createElement('p');
nameHistoryNode.innerHTML = '曾用名:';
block.append(nameHistoryNode);
FetchUtils.getInstance().getProfile(id).then((res) => {
if (res.userInformation.previousAliases.length > 0) {
res.userInformation.previousAliases.forEach(item => nameHistoryNode.innerHTML += item + ' ');
} else {
nameHistoryNode.innerHTML += '暂无';
}
});
globVars.responseHandlers.push((url, body) => this.responseHandler(url, body));
}
}
}
responseHandler(url: string, body: { json: unknown; text: string; isModified: boolean }) {
if (url.includes('profiles.php?step=getProfileData') && this.task) {
// 曾用名
let nameHistoryNode;
nameHistoryNode = document.createElement('p');
nameHistoryNode.innerHTML = '曾用名:';
this.block.append(nameHistoryNode);
let resp = body.json as IUserProfileData;
if (resp.userInformation.previousAliases.length > 0) {
resp.userInformation.previousAliases.forEach(item => nameHistoryNode.innerHTML += item + ' ');
} else {
nameHistoryNode.innerHTML += '暂无';
}
this.task = false;
}
}
}

View File

@ -1,20 +1,7 @@
import WuhuBase from "../WuhuBase";
import {
chatDict,
eventsDict,
headerDict,
itemNameDict,
itemPageDict,
propertyDict,
sidebarDict
} from "../../dictionary/translation";
import { chatDict, eventsDict, headerDict, propertyDict, sidebarDict } from "../../dictionary/translation";
import Log from "../Log";
import Timer from "../utils/Timer";
import { Button, MiniProfile } from "../../interface/responseType/MiniProfile";
import WuhuConfig from "../WuhuConfig";
import Sidebar from "../../interface/responseType/Sidebar";
import CommonUtils from "../utils/CommonUtils";
import InventoryItemInfo from "../../interface/responseType/InventoryItemInfo";
export default class Translate extends WuhuBase {
className = 'Translate';
@ -303,369 +290,4 @@ export default class Translate extends WuhuBase {
observer.observe(document.body, opt);
}).observe(document.body, opt);
}
/**
* fetch xhr
* @param url
* @param body
* @param opt
*/
public static responseHandler(url: string, body: { json: unknown, text: string, isModified: boolean }, opt: { method: 'GET' | 'POST', requestBody: string }): void {
if (!WuhuConfig.get('transNew')) return;
// TODO 字典抽取
let map = {
iconMap: {
'Online': { title: "在线" },
'Level 100': { title: "100 级" },
'Jail': {
title: "坐牢", description: {
map: {
'Being questioned for suspicious online activity.': '因可疑的网上活动而被盘问。',
'Suspect of a presidential assassination': '刺杀总统的嫌疑人',
}
}
},
'Federal jail': {
title: "联邦监狱(FJ)", description: {
map: {
'Account marked for deletion': '账号标记删除',
}
}
},
'Idle': { title: "暂离" },
'Offline': { title: "离线" },
'Enby': { title: "中性" },
'Male': { title: "男性" },
'Female': { title: "女性" },
'Donator': { title: "DP捐助者" },
'Subscriber': { title: "蓝星订阅者" },
'Traveling': { title: "旅行中" },
'Hospital': {
title: "已入院",
description: {
map: {
'Fell from a two story building while on a hitman mission': '在执行刺杀任务时从二楼摔下',
'Overdosed on Xanax': '吃 Xan 后 OD',
'Mauled by a guard dog': '被看门狗咬',
},
replace: [/(Hospitalized|Mugged|Attacked|Defeated) by (someone|<a.+\/a>)(.+Early discharge available.+)?/, '被 $2 $1$3'],
attachedMap: [
{
'Hospitalized': '强制住院',
'Mugged': '抢劫',
'Attacked': '攻击',
'Defeated': '击败',
},
{ 'someone': '某人' },
{ '<br><i>Early discharge available</i>': '<br><i>提前出院(ED)可用</i>' },
],
}
},
'Bounty': {
title: "被悬赏",
description: { replace: [/On this person's head for (\$[,0-9]+)( : .+)?/, '$1 悬赏此人$2'] }
},
'Married': {
title: "已婚", description: { replace: [/To/, '和'] }
},
'Company': {
title: "公司",
description: {
replace: [/([a-zA-Z ]+)( of )(.+) \(([aA-zZ ]+)\)$/, "【$3】$4的$1"],
attachedMap: [
{
'Director': '老板',
'Salesperson': '销售员',
},
{}, {},
{
'Private Security Firm': '安保公司 (PSF)',
'Lingerie Store': '内衣店 (LS)',
'Adult Novelties': '成人用品店 (AN)',
}
]
}
},
'Job': {
title: "系统公司",
description: {
replace: [/([a-zA-Z ]+) (in|at) (.+)$/, "$3的$1"],
attachedMap: [
{
'Manager': '经理',
},
{},
{
'a Grocery Store': '杂货店',
}
]
}
},
'Faction': {
title: "帮派",
description: { replace: [/([aA-zZ ]+)( of )(.+)/, "[$3] 帮派的 [$1]"] }
},
'Bazaar': {
title: "摊位",
description: {
map: {
'This person has items in their bazaar for sale':
'此人在摊位上有物品出售'
}
}
},
},
buttonMap: {
message: {
'Add $0 to your enemy list': '添加 $0 到敌人列表',
'Add $0 to your friend list': '添加 $0 到好友列表',
'You are currently traveling': '你在天上',
'Initiate a chat with $0': '与 $0 私聊',
'You are not in Torn': '你不在城内',
"View $0's personal statistics": '查看 $0 的个人统计数据',
"Place a bounty on $0": '对 $0 发起悬赏',
"Report $0 to staff": '向工作人员举报 $0',
"Send $0 a message": '发邮件给 $0',
"View $0's display case": '查看 $0 的展柜',
"$0 is currently in hospital": '$0 正在住院',
"$0 has not been online in the last 6 hours": '$0 超 6 小时未在线',
"Give some money to $0": '给 $0 一些钱',
"$0's bazaar is closed": '$0 的摊位已关闭',
"View $0's bazaar": '查看 $0 的摊位',
"Initiate a trade with $0": '与 $0 交易',
"$0 has no items in their bazaar": '$0 的摊位是空的',
"$0 is currently in jail": '$0 目前在坐牢',
"Pay $0's bail": '支付 $0 的保释金',
"Bust $0 out of jail": '把 $0 从监狱里踢出来',
"$0 is currently in federal jail": '$0 目前在联邦监狱(FJ)',
"NPC's cannot be bountied": 'NPC 不能被悬赏',
"$0 is hiding out abroad": '$0 正在海外躲藏',
"$0 has no items in their display cabinet": '$0 的展柜是空的',
"You do not have enough energy to attack $0": '你没有足够的能量攻击 $0',
"You have not met this person recently or they have blocked you": '最近你没有遇见此人,或已被屏蔽',
},
},
destinationMap: {
'Mexico': '墨西哥',
'Cayman Islands': '开曼群岛',
'Canada': '加拿大',
'Hawaii': '夏威夷',
'United Kingdom': '英国',
'Argentina': '阿根廷',
'Switzerland': '瑞士',
'Japan': '日本',
'China': '中国',
'UAE': '阿联酋(UAE)',
'South Africa': '南非',
},
barMap: {
'Chain': { name: '连击' },
'Energy': { name: '能量' },
'Happy': { name: '快乐' },
'Life': { name: '血量' },
'Nerve': { name: '犯罪' },
},
areaMap: {
calendar: { name: '日历', shortName: '日历' },
traveling: { name: '飞行中', shortName: '飞行' },
casino: { name: '赌场' },
city: { name: '城市' },
crimes: { name: '犯罪' },
education: { name: '教育', shortName: '教育' },
forums: { name: '论坛' },
gym: { name: '健身房' },
hall_of_fame: { name: '名人堂', shortName: '排名' },
home: { name: '主页', shortName: '主页' },
hospital: { name: '医院' },
items: { name: '物品' },
jail: { name: '监狱' },
job: { name: '工作', shortName: '工作' },
missions: { name: '任务' },
my_faction: { name: '帮派', shortName: '帮派' },
newspaper: { name: '报纸', shortName: '报纸' },
properties: { name: '住宅', shortName: '住宅' },
recruit_citizens: { name: '招募玩家', shortName: '招募' },
},
accountMap: {
awards: { name: '奖章' },
events: { name: '通知' },
messages: { name: '邮件' }
}
};
try {
// Mini Profile
if (url.includes('profiles.php?step=getUserNameContextMenu')) {
let jsonObj: MiniProfile = body.json as MiniProfile;
Log.info('翻译mini profile返回内容');
// 状态图标
jsonObj.icons.forEach(icon => {
let iconMap = map.iconMap;
let oriTitle = icon.title;
if (iconMap[oriTitle]) {
icon.title = iconMap[oriTitle].title;
let desc = iconMap[oriTitle].description;
let oriDesc = icon.description;
if (icon.description && desc) {
if (desc.map && desc.map[oriDesc]) {
icon.description = desc.map[oriDesc];
} else if (desc.replace) {
icon.description = oriDesc.replace(new RegExp(desc.replace[0]), desc.replace[1]);
if (desc.attachedMap) {
desc.attachedMap.forEach((item, index) => {
let factor = oriDesc.replace(new RegExp(desc.replace[0]), '$' + (index + 1));
Log.info({ factor });
let cn = item[factor];
cn && (icon.description = icon.description.replace(factor, cn));
});
}
}
}
}
});
// 离线转钱警告
if (jsonObj.user.sendMoneyWarning) {
let daysMatch = jsonObj.user.sendMoneyWarning.match(/[0-9]+/);
if (daysMatch.length !== 0)
jsonObj.user.sendMoneyWarning = `警告:此人已离线 ${ daysMatch[0] }`;
}
// 按钮
let buttons = jsonObj.profileButtons.buttons;
let buttonKeyList = Object.keys(buttons);
let username = jsonObj.user.playerName;
let msgMap = map.buttonMap.message;
buttonKeyList.forEach(buttonKey => {
if (buttons[buttonKey].state === 'hidden') return;
let button: Button = buttons[buttonKey];
let oriMsg = button.message.replace(username, '$0');
if (msgMap[oriMsg]) {
button.message = msgMap[oriMsg].replace('$0', username);
}
});
// TODO 称号
// 用户状态
let status = jsonObj.userStatus.status.type;
switch (status) {
case 'traveling-to': {
let origin = jsonObj.userStatus.status.to.simpleName;
let cn = map.destinationMap[origin]
cn && (jsonObj.userStatus.status.to.simpleName = cn);
break;
}
case 'traveling-from': {
let origin = jsonObj.userStatus.status.from.simpleName;
let cn = map.destinationMap[origin]
cn && (jsonObj.userStatus.status.from.simpleName = cn);
break;
}
case 'abroad': {
let origin = jsonObj.userStatus.status.in.simpleName;
let cn = map.destinationMap[origin]
cn && (jsonObj.userStatus.status.in.simpleName = cn);
break;
}
case 'jail': {
let origin = jsonObj.userStatus.status.description;
let cn = map.iconMap.Jail.description.map[origin];
cn && (jsonObj.userStatus.status.description = cn);
break;
}
}
body.isModified = true;
Log.info({ 'localized': jsonObj });
}
// TODO 边栏
else if (url.includes('sidebarAjaxAction.php?q=sync')) {
let response = body.json as Sidebar;
type target = { [k: string]: { name: string, shortName?: string } };
let nameMapReplace = (target: target, _map: target) => {
Object.keys(target).forEach(key => {
if (target[key] && _map[key]) {
target[key].name = _map[key].name;
if (target[key].shortName && _map[key].shortName) {
target[key].shortName = _map[key].shortName;
}
}
});
};
nameMapReplace(response.areas, map.areaMap);
nameMapReplace(response.account, map.accountMap);
body.isModified = true;
}
// 物品详情
else if (url.includes('inventory.php') && opt?.method === 'POST' &&
typeof opt?.requestBody === 'string' && opt?.requestBody.includes('step=info')) {
let resp = body.json as InventoryItemInfo;
// TODO 维护通用物品数据(对应名称、描述、类型)缓存
let map: { [k: string]: Partial<InventoryItemInfo> } = {
'Glass of Beer': {
itemName: '一杯啤酒',
itemInfo: '[译]Only savages drink beer straight out of the bottle. This glass of beer is obtained fresh from the keg, and provides the same level of drunken joy as you\'d get from a regular bottle of suds. Provides a moderate nerve increase when consumed.',
itemInfoContent: "\n" +
" <div class='m-bottom10'>\n" +
" <span class=\"bold\">一杯啤酒</span> 是酒类物品\n" +
" </div>\n" +
" Only savages drink beer straight out of the bottle. This glass of beer is obtained fresh from the keg, and provides the same level of drunken joy as you'd get from a regular bottle of suds. Provides a moderate nerve increase when consumed.\n" +
" <div class=\"t-green bold item-effect m-top10\">效果: 犯罪 + 2增幅CD + 1h。</div>",
},
};
let idMap = { 816: 'Glass of Beer' };
let itemInfo = CommonUtils.getInstance().getItemByIdOrName(resp.itemName, idMap, map);
if (itemInfo) {
body.isModified = true;
resp.itemInfo = itemInfo.itemInfo;
resp.itemName = itemInfo.itemName;
resp.itemInfoContent = itemInfo.itemInfoContent;
}
// TODO 老字典
let itemName = itemNameDict[resp.itemName];
if (itemName) {
body.isModified = true;
resp.itemInfoContent = resp.itemInfoContent
.replace('The ', '')
.replace(resp.itemName, `${ itemName }(${ resp.itemName })`);
}
}
// 物品列表
else if ((url.includes('item.php') || url.includes('inventory.php')) && opt?.method === 'POST' &&
typeof opt?.requestBody === 'string' && (opt?.requestBody.includes('step=getCategoryList') || opt?.requestBody.includes('step=getList'))) {
let resp = body.json as { html: string };
if (resp.html) {
let tmp = document.createElement('div');
tmp.innerHTML = resp.html;
Log.info(tmp);
tmp.childNodes.forEach(li => {
if (li.nodeType === 1) {
let elem = li as Element;
// 物品名
let name = elem.querySelector('.name-wrap .name');
let nameZh = itemNameDict[name.innerText.trim()];
if (nameZh) {
name.innerText = `${ name.innerText } ${ nameZh }`;
}
// 操作按钮
let actions = elem.querySelectorAll('.icon-h');
actions.forEach(action => {
let attrTitle = action.getAttribute('title');
// TODO
let zh = itemPageDict[attrTitle];
if (zh) {
action.setAttribute('title', zh);
}
});
}
});
resp.html = tmp.innerHTML;
body.isModified = true;
}
}
// TODO 物品列表json版
else if (url.includes('inventory.php') && opt?.method === 'POST' &&
typeof opt?.requestBody === 'string' && opt?.requestBody.includes('step=getList')) {
}
} catch (e) {
Log.error('responseHandler', e.stack || e.message);
}
}
}

View File

@ -0,0 +1,382 @@
import ResponseInject from "../../interface/ResponseInject";
import WuhuConfig from "../WuhuConfig";
import { Button, MiniProfile } from "../../interface/responseType/MiniProfile";
import Log from "../Log";
import Sidebar from "../../interface/responseType/Sidebar";
import InventoryItemInfo from "../../interface/responseType/InventoryItemInfo";
import CommonUtils from "../utils/CommonUtils";
import { itemNameDict, itemPageDict } from "../../dictionary/translation";
import Provider from "../provider/Provider";
/**
*
*/
export default class TranslateNew extends Provider implements ResponseInject {
className = 'TranslateNew';
/**
* fetch xhr
* @param url
* @param body
* @param opt
*/
public responseHandler(url: string, body: { json: unknown, text: string, isModified: boolean }, opt: { method: 'GET' | 'POST', requestBody: string }): void {
if (!WuhuConfig.get('transNew')) return;
// TODO 字典抽取
let map = {
iconMap: {
'Online': { title: "在线" },
'Level 100': { title: "100 级" },
'Jail': {
title: "坐牢", description: {
map: {
'Being questioned for suspicious online activity.': '因可疑的网上活动而被盘问。',
'Suspect of a presidential assassination': '刺杀总统的嫌疑人',
}
}
},
'Federal jail': {
title: "联邦监狱(FJ)", description: {
map: {
'Account marked for deletion': '账号标记删除',
}
}
},
'Idle': { title: "暂离" },
'Offline': { title: "离线" },
'Enby': { title: "中性" },
'Male': { title: "男性" },
'Female': { title: "女性" },
'Donator': { title: "DP捐助者" },
'Subscriber': { title: "蓝星订阅者" },
'Traveling': { title: "旅行中" },
'Hospital': {
title: "已入院",
description: {
map: {
'Fell from a two story building while on a hitman mission': '在执行刺杀任务时从二楼摔下',
'Overdosed on Xanax': '吃 Xan 后 OD',
'Mauled by a guard dog': '被看门狗咬',
},
replace: [/(Hospitalized|Mugged|Attacked|Defeated) by (someone|<a.+\/a>)(.+Early discharge available.+)?/, '被 $2 $1$3'],
attachedMap: [
{
'Hospitalized': '强制住院',
'Mugged': '抢劫',
'Attacked': '攻击',
'Defeated': '击败',
},
{ 'someone': '某人' },
{ '<br><i>Early discharge available</i>': '<br><i>提前出院(ED)可用</i>' },
],
}
},
'Bounty': {
title: "被悬赏",
description: { replace: [/On this person's head for (\$[,0-9]+)( : .+)?/, '$1 悬赏此人$2'] }
},
'Married': {
title: "已婚", description: { replace: [/To/, '和'] }
},
'Company': {
title: "公司",
description: {
replace: [/([a-zA-Z ]+)( of )(.+) \(([aA-zZ ]+)\)$/, "【$3】$4的$1"],
attachedMap: [
{
'Director': '老板',
'Salesperson': '销售员',
},
{}, {},
{
'Private Security Firm': '安保公司 (PSF)',
'Lingerie Store': '内衣店 (LS)',
'Adult Novelties': '成人用品店 (AN)',
}
]
}
},
'Job': {
title: "系统公司",
description: {
replace: [/([a-zA-Z ]+) (in|at) (.+)$/, "$3的$1"],
attachedMap: [
{
'Manager': '经理',
},
{},
{
'a Grocery Store': '杂货店',
}
]
}
},
'Faction': {
title: "帮派",
description: { replace: [/([aA-zZ ]+)( of )(.+)/, "[$3] 帮派的 [$1]"] }
},
'Bazaar': {
title: "摊位",
description: {
map: {
'This person has items in their bazaar for sale':
'此人在摊位上有物品出售'
}
}
},
},
buttonMap: {
message: {
'Add $0 to your enemy list': '添加 $0 到敌人列表',
'Add $0 to your friend list': '添加 $0 到好友列表',
'You are currently traveling': '你在天上',
'Initiate a chat with $0': '与 $0 私聊',
'You are not in Torn': '你不在城内',
"View $0's personal statistics": '查看 $0 的个人统计数据',
"Place a bounty on $0": '对 $0 发起悬赏',
"Report $0 to staff": '向工作人员举报 $0',
"Send $0 a message": '发邮件给 $0',
"View $0's display case": '查看 $0 的展柜',
"$0 is currently in hospital": '$0 正在住院',
"$0 has not been online in the last 6 hours": '$0 超 6 小时未在线',
"Give some money to $0": '给 $0 一些钱',
"$0's bazaar is closed": '$0 的摊位已关闭',
"View $0's bazaar": '查看 $0 的摊位',
"Initiate a trade with $0": '与 $0 交易',
"$0 has no items in their bazaar": '$0 的摊位是空的',
"$0 is currently in jail": '$0 目前在坐牢',
"Pay $0's bail": '支付 $0 的保释金',
"Bust $0 out of jail": '把 $0 从监狱里踢出来',
"$0 is currently in federal jail": '$0 目前在联邦监狱(FJ)',
"NPC's cannot be bountied": 'NPC 不能被悬赏',
"$0 is hiding out abroad": '$0 正在海外躲藏',
"$0 has no items in their display cabinet": '$0 的展柜是空的',
"You do not have enough energy to attack $0": '你没有足够的能量攻击 $0',
"You have not met this person recently or they have blocked you": '最近你没有遇见此人,或已被屏蔽',
},
},
destinationMap: {
'Mexico': '墨西哥',
'Cayman Islands': '开曼群岛',
'Canada': '加拿大',
'Hawaii': '夏威夷',
'United Kingdom': '英国',
'Argentina': '阿根廷',
'Switzerland': '瑞士',
'Japan': '日本',
'China': '中国',
'UAE': '阿联酋(UAE)',
'South Africa': '南非',
},
barMap: {
'Chain': { name: '连击' },
'Energy': { name: '能量' },
'Happy': { name: '快乐' },
'Life': { name: '血量' },
'Nerve': { name: '犯罪' },
},
areaMap: {
calendar: { name: '日历', shortName: '日历' },
traveling: { name: '飞行中', shortName: '飞行' },
casino: { name: '赌场' },
city: { name: '城市' },
crimes: { name: '犯罪' },
education: { name: '教育', shortName: '教育' },
forums: { name: '论坛' },
gym: { name: '健身房' },
hall_of_fame: { name: '名人堂', shortName: '排名' },
home: { name: '主页', shortName: '主页' },
hospital: { name: '医院' },
items: { name: '物品' },
jail: { name: '监狱' },
job: { name: '工作', shortName: '工作' },
missions: { name: '任务' },
my_faction: { name: '帮派', shortName: '帮派' },
newspaper: { name: '报纸', shortName: '报纸' },
properties: { name: '住宅', shortName: '住宅' },
recruit_citizens: { name: '招募玩家', shortName: '招募' },
},
accountMap: {
awards: { name: '奖章' },
events: { name: '通知' },
messages: { name: '邮件' }
}
};
try {
// Mini Profile
if (url.includes('profiles.php?step=getUserNameContextMenu')) {
let jsonObj: MiniProfile = body.json as MiniProfile;
Log.info('翻译mini profile返回内容');
// 状态图标
jsonObj.icons.forEach(icon => {
let iconMap = map.iconMap;
let oriTitle = icon.title;
if (iconMap[oriTitle]) {
icon.title = iconMap[oriTitle].title;
let desc = iconMap[oriTitle].description;
let oriDesc = icon.description;
if (icon.description && desc) {
if (desc.map && desc.map[oriDesc]) {
icon.description = desc.map[oriDesc];
} else if (desc.replace) {
icon.description = oriDesc.replace(new RegExp(desc.replace[0]), desc.replace[1]);
if (desc.attachedMap) {
desc.attachedMap.forEach((item, index) => {
let factor = oriDesc.replace(new RegExp(desc.replace[0]), '$' + (index + 1));
Log.info({ factor });
let cn = item[factor];
cn && (icon.description = icon.description.replace(factor, cn));
});
}
}
}
}
});
// 离线转钱警告
if (jsonObj.user.sendMoneyWarning) {
let daysMatch = jsonObj.user.sendMoneyWarning.match(/[0-9]+/);
if (daysMatch.length !== 0)
jsonObj.user.sendMoneyWarning = `警告:此人已离线 ${ daysMatch[0] }`;
}
// 按钮
let buttons = jsonObj.profileButtons.buttons;
let buttonKeyList = Object.keys(buttons);
let username = jsonObj.user.playerName;
let msgMap = map.buttonMap.message;
buttonKeyList.forEach(buttonKey => {
if (buttons[buttonKey].state === 'hidden') return;
let button: Button = buttons[buttonKey];
let oriMsg = button.message.replace(username, '$0');
if (msgMap[oriMsg]) {
button.message = msgMap[oriMsg].replace('$0', username);
}
});
// TODO 称号
// 用户状态
let status = jsonObj.userStatus.status.type;
switch (status) {
case 'traveling-to': {
let origin = jsonObj.userStatus.status.to.simpleName;
let cn = map.destinationMap[origin]
cn && (jsonObj.userStatus.status.to.simpleName = cn);
break;
}
case 'traveling-from': {
let origin = jsonObj.userStatus.status.from.simpleName;
let cn = map.destinationMap[origin]
cn && (jsonObj.userStatus.status.from.simpleName = cn);
break;
}
case 'abroad': {
let origin = jsonObj.userStatus.status.in.simpleName;
let cn = map.destinationMap[origin]
cn && (jsonObj.userStatus.status.in.simpleName = cn);
break;
}
case 'jail': {
let origin = jsonObj.userStatus.status.description;
let cn = map.iconMap.Jail.description.map[origin];
cn && (jsonObj.userStatus.status.description = cn);
break;
}
}
body.isModified = true;
Log.info({ 'localized': jsonObj });
}
// TODO 边栏
else if (url.includes('sidebarAjaxAction.php?q=sync')) {
let response = body.json as Sidebar;
type target = { [k: string]: { name: string, shortName?: string } };
let nameMapReplace = (target: target, _map: target) => {
Object.keys(target).forEach(key => {
if (target[key] && _map[key]) {
target[key].name = _map[key].name;
if (target[key].shortName && _map[key].shortName) {
target[key].shortName = _map[key].shortName;
}
}
});
};
nameMapReplace(response.areas, map.areaMap);
nameMapReplace(response.account, map.accountMap);
body.isModified = true;
}
// 物品详情
else if ((url.includes('inventory.php?step=info')) || (url.includes('inventory.php') && opt?.method === 'POST' &&
typeof opt?.requestBody === 'string' && opt?.requestBody.includes('step=info'))) {
Log.info('responseHandler');
let resp = body.json as InventoryItemInfo;
// TODO 维护通用物品数据(对应名称、描述、类型)缓存
let map: { [k: string]: Partial<InventoryItemInfo> } = {
'Glass of Beer': {
itemName: '一杯啤酒',
itemInfo: '[译]Only savages drink beer straight out of the bottle. This glass of beer is obtained fresh from the keg, and provides the same level of drunken joy as you\'d get from a regular bottle of suds. Provides a moderate nerve increase when consumed.',
itemInfoContent: "\n" +
" <div class='m-bottom10'>\n" +
" <span class=\"bold\">一杯啤酒</span> 是酒类物品\n" +
" </div>\n" +
" Only savages drink beer straight out of the bottle. This glass of beer is obtained fresh from the keg, and provides the same level of drunken joy as you'd get from a regular bottle of suds. Provides a moderate nerve increase when consumed.\n" +
" <div class=\"t-green bold item-effect m-top10\">效果: 犯罪 + 2增幅CD + 1h。</div>",
},
};
let idMap = { 816: 'Glass of Beer' };
let itemInfo = CommonUtils.getInstance().getItemByIdOrName(resp.itemName, idMap, map);
if (itemInfo) {
body.isModified = true;
resp.itemInfo = itemInfo.itemInfo;
resp.itemName = itemInfo.itemName;
resp.itemInfoContent = itemInfo.itemInfoContent;
}
// TODO 老字典
let itemName = itemNameDict[resp.itemName];
if (itemName) {
body.isModified = true;
resp.itemInfoContent = resp.itemInfoContent
.replace('The ', '')
.replace(resp.itemName, `${ itemName }(${ resp.itemName })`);
}
}
// 物品列表
else if ((url.includes('item.php') || url.includes('inventory.php')) && opt?.method === 'POST' &&
typeof opt?.requestBody === 'string' && (opt?.requestBody.includes('step=getCategoryList') || opt?.requestBody.includes('step=getList'))) {
let resp = body.json as { html: string };
if (resp.html) {
let tmp = document.createElement('div');
tmp.innerHTML = resp.html;
Log.info(tmp);
tmp.childNodes.forEach(li => {
if (li.nodeType === 1) {
let elem = li as Element;
// 物品名
let name = elem.querySelector('.name-wrap .name');
let nameZh = itemNameDict[name.innerText.trim()];
if (nameZh) {
name.innerText = `${ name.innerText } ${ nameZh }`;
}
// 操作按钮
let actions = elem.querySelectorAll('.icon-h');
actions.forEach(action => {
let attrTitle = action.getAttribute('title');
// TODO
let zh = itemPageDict[attrTitle];
if (zh) {
action.setAttribute('title', zh);
}
});
}
});
resp.html = tmp.innerHTML;
body.isModified = true;
}
}
// TODO 物品列表json版
else if (url.includes('inventory.php') && opt?.method === 'POST' &&
typeof opt?.requestBody === 'string' && opt?.requestBody.includes('step=getList')) {
}
} catch (e) {
Log.error('responseHandler', e.stack || e.message);
}
}
}

View File

@ -0,0 +1,21 @@
import WuhuBase from "../WuhuBase";
import Popup from "../utils/Popup";
import Elem from "../provider/Elem";
/**
*
*/
export default class ItemValueQueryHandler extends WuhuBase {
className = "ItemValueQueryHandler";
constructor() {
super();
}
public show() {
let pop = new Popup('', '快速查价');
pop.getElement().append(
new Elem('div').html('<p>test</p>').class('wh-test').el()
)
}
}

View File

@ -0,0 +1,26 @@
export default class Elem {
private readonly elem: HTMLElement;
constructor(tagName) {
this.elem = document.createElement(tagName);
}
public html(htmlString): Elem {
this.elem.innerHTML = htmlString;
return this;
}
public id(id): Elem {
this.elem.id = id;
return this;
}
public class(className): Elem {
this.elem.classList.add(className);
return this;
}
public el(): HTMLElement {
return this.elem;
}
}

View File

@ -30,4 +30,8 @@ export default class Provider {
pool: Provider.pool,
}
}
public getClassName() {
return this.className;
}
}

View File

@ -0,0 +1,116 @@
import WuhuBase from "../WuhuBase";
import InventoryItemInfo from "../../interface/responseType/InventoryItemInfo";
import FetchUtils from "./FetchUtils";
import CommonUtils from "./CommonUtils";
import Log from "../Log";
export default class ItemHelper extends WuhuBase {
className = "ItemHelper";
itemValueMap: { [k: string]: Partial<InventoryItemInfo> } = {
'Glass of Beer': {
itemName: '一杯啤酒',
itemInfo: '[译]Only savages drink beer straight out of the bottle. This glass of beer is obtained fresh from the keg, and provides the same level of drunken joy as you\'d get from a regular bottle of suds. Provides a moderate nerve increase when consumed.',
itemInfoContent: "\n" +
" <div class='m-bottom10'>\n" +
" <span class=\"bold\">一杯啤酒</span> 是酒类物品\n" +
" </div>\n" +
" Only savages drink beer straight out of the bottle. This glass of beer is obtained fresh from the keg, and provides the same level of drunken joy as you'd get from a regular bottle of suds. Provides a moderate nerve increase when consumed.\n" +
" <div class=\"t-green bold item-effect m-top10\">效果: 犯罪 + 2增幅CD + 1h。</div>",
},
};
itemPriceMap = {
205: { name: 'Vicodin', price: 1300 },
};
// 缓存过期时间 分钟
private readonly priceTimeout = 720;
// TODO 定时
constructor() {
super();
}
public async getItemData(idOrName: string): Promise<Partial<InventoryItemInfo>> {
return await (await FetchUtils.getInstance().ajaxFetch({
url: `/inventory.php?step=info&itemID=${ idOrName }&armouryID=0&asObject=true`,
method: 'GET',
referrer: 'bazaar.php',
})).json();
}
/**
* -
* @param idOrName
*/
public getItemPrice(idOrName) {
}
/**
* -API
* @param idOrName
*/
public getItemPriceApi(idOrName) {
}
/**
*
*/
public async getItemValue(idOrName) {
return (await this.getItemData(idOrName)).itemValue.replaceAll(/[,$]/, '');
}
public getLocalPriceData(): { priceData: PriceData, promise: Promise<PriceData> } {
// 获取本地缓存
let localStore = localStorage.getItem('WHItemPrice');
let res = { priceData: null, promise: null };
let data: PriceData = null;
// 无缓存
if (!localStore) {
res.promise = this.fetchPriceData();
return res;
}
// 解析缓存
try {
data = JSON.parse(localStore);
} catch (e) {
Log.error(e.stack || e.message || e);
throw new Error('JSON解析错误');
}
// 缓存超时
if (Date.now() - data.timestamp > this.priceTimeout * 60000) {
res.priceData = null;
res.promise = this.fetchPriceData();
} else {
res.priceData = data;
res.promise = null;
}
return res;
}
private async fetchPriceData(): Promise<PriceData> {
let res = null;
// 获取在线价格
try {
res = JSON.parse(await CommonUtils.COFetch('https://jjins.github.io/item_price_raw.json'))
} catch (err) {
Log.error(err.stack || err.message || err);
throw new Error('获取在线价格时出错');
}
// 更新缓存
let localData: PriceData = {
timestamp: Date.now(),
data: res,
};
localStorage.setItem('WHItemPrice', JSON.stringify(localData));
return res;
}
}
interface PriceData {
timestamp: number
data: {
[k: number]: {
name: string
price: number
}
}
}

View File

@ -2,11 +2,19 @@ import WuhuBase from "../WuhuBase";
import POPUP_HTML from "../../static/html/popup.html";
import Log from "../Log";
/**
*
*/
export default class Popup extends WuhuBase {
className = 'Popup';
private readonly container: HTMLElement = null;
private readonly node: HTMLElement = null;
/**
*
* @param innerHTML
* @param title
*/
constructor(innerHTML: string, title: string = '芜湖助手') {
super();
if (Popup.glob.popup_node) {

View File

@ -1,13 +1,22 @@
import toThousands from "../utils/toThousands";
import CommonUtils from "../../class/utils/CommonUtils";
import Log from "../../class/Log";
import CITY_FINDER_CSS from "../../static/css/city_finder.css";
import TornStyleBlock from "../../class/utils/TornStyleBlock";
import ItemHelper from "../../class/utils/ItemHelper";
/**
* 2023-02-22
* @param _base
*/
export default function cityFinder(_base: TornStyleBlock): void {
CommonUtils.addStyle(CITY_FINDER_CSS);
// 物品名与价格
let items = null;
let items: {
[k: number]: {
name: string
price: number
}
} = null;
// const base = document.createElement('div');
// base.id = 'wh-city-finder';
// const container = document.createElement('div');
@ -24,12 +33,13 @@ export default function cityFinder(_base: TornStyleBlock): void {
_base.append(header, info);
document.body.classList.add('wh-city-finds');
CommonUtils.COFetch('https://jjins.github.io/item_price_raw.json')
.then(r => items = JSON.parse(r))
.catch(err => {
Log.error(err);
items = undefined
});
// CommonUtils.COFetch('https://jjins.github.io/item_price_raw.json')
// .then(r => items = JSON.parse(r))
// .catch(err => {
// Log.error(err);
// items = undefined
// });
let itemHelper = ItemHelper.getInstance();
CommonUtils.elementReady('div.leaflet-marker-pane').then(elem => {
// document.querySelector('.content-wrapper').prepend(base);
@ -73,29 +83,40 @@ export default function cityFinder(_base: TornStyleBlock): void {
header.innerHTML = `捡垃圾助手 - ${ founds.length } 个物品,总价值 $${ toThousands(total) }`;
// _base.setTitle(`捡垃圾助手 - ${ founds.length } 个物品,总价值 $${ toThousands(total) }`);
};
// 未取到数据时添加循环来调用函数
if (items === null) {
// 15s超时
let timeout = 30;
const interval = window.setInterval(() => {
timeout--;
if (items !== null) {
displayNamePrice();
window.clearInterval(interval);
}
if (0 === timeout) {
Log.info('获取物品名称与价格信息超时')
window.clearInterval(interval)
}
}, 500);
}
// 无法跨域获取数据时
else if (items === undefined) {
info.innerHTML += '(当前平台暂不支持查询价格)';
}
// 调用函数
else {
// // 未取到数据时添加循环来调用函数
// if (items === null) {
// // 15s超时
// let timeout = 30;
// const interval = window.setInterval(() => {
// timeout--;
// if (items !== null) {
// displayNamePrice();
// window.clearInterval(interval);
// }
// if (0 === timeout) {
// Log.info('获取物品名称与价格信息超时')
// window.clearInterval(interval)
// }
// }, 500);
// }
// // 无法跨域获取数据时
// else if (items === undefined) {
// info.innerHTML += '(当前平台暂不支持查询价格)';
// }
// // 调用函数
// else {
// displayNamePrice();
// }
let priceData = itemHelper.getLocalPriceData();
if (priceData.priceData) {
items = priceData.priceData.data;
displayNamePrice();
} else {
window.setTimeout(async () => {
items = (await priceData.promise).data;
displayNamePrice();
}, 0);
}
})
}
}

View File

@ -0,0 +1,22 @@
/**
*
*/
export default interface ResponseInject {
/**
* @param url url
* @param body
* @param opt
*/
responseHandler(
url: string,
body: {
json: unknown,
text: string,
isModified: boolean
},
opt: {
method: 'GET' | 'POST',
requestBody: string
}
): void
}

View File

@ -2,7 +2,7 @@
*
*/
export default interface InventoryItemInfo {
itemID: 816
itemID: number
itemInfo: string
// html 格式
itemInfoContent: string
@ -10,4 +10,5 @@ export default interface InventoryItemInfo {
// html 格式
itemRareTitle: string
itemType: "Item"
itemValue: string
}

View File

@ -4,6 +4,9 @@ import CommonUtils from "../class/utils/CommonUtils";
import Popup from "../class/utils/Popup";
import CompanyHelper from "../class/action/CompanyHelper";
import globVars from "../globVars";
import FetchUtils from "../class/utils/FetchUtils";
import ItemValueQueryHandler from "../class/handler/ItemValueQueryHandler";
import createApp from "vue";
export default class Test extends WuhuBase {
className = 'Test';
@ -12,10 +15,17 @@ export default class Test extends WuhuBase {
let popup = new Popup(CommonUtils.getInstance().getTravelStage().toString());
popup.getElement()['__POOL__'] = Test.getPool();
Log.info({ NET: globVars.WH_NET_LOG });
FetchUtils.getInstance().ajaxFetch({
url: '/inventory.php?step=info&itemID=205&armouryID=0&asObject=true',
method: 'GET',
referrer: 'bazaar.php',
});
ItemValueQueryHandler.getInstance().show();
// this.case1()
// this.case2()
// this.case3().then();
createApp()
}
private case1() {

16538
src/ts/test/vue.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,6 @@
"removeComments": true,
"sourceMap": false,
"resolveJsonModule": true,
// "strict": true,
"jsx": "react-native"
"strict": false
}
}
}