456 lines
18 KiB
TypeScript
456 lines
18 KiB
TypeScript
import UserScriptEngine from "../../enum/UserScriptEngine";
|
||
import Device from "../../enum/Device";
|
||
import LOADING_IMG_HTML from "../../../static/html/loading_img.html";
|
||
import Timer from "./Timer";
|
||
import FetchUtils from "./FetchUtils";
|
||
import TornStyleSwitch from "./TornStyleSwitch";
|
||
import { MenuItemConfig } from "../ZhongIcon";
|
||
import TRAVEL_STATE from "../../enum/TravelState";
|
||
import InventoryItemInfo from "../../interface/responseType/InventoryItemInfo";
|
||
import { Injectable } from "../../container/Injectable";
|
||
import ClassName from "../../container/ClassName";
|
||
import LocalConfigWrapper from "../LocalConfigWrapper";
|
||
import Logger from "../Logger";
|
||
import Global from "../Global";
|
||
import { Container } from "../../container/Container";
|
||
import TornPDAUtils from "./TornPDAUtils";
|
||
import { InjectionKey } from "vue";
|
||
|
||
@Injectable()
|
||
@ClassName('CommonUtils')
|
||
export default class CommonUtils {
|
||
|
||
constructor(
|
||
private readonly localConfigWrapper: LocalConfigWrapper,
|
||
private readonly fetchUtils: FetchUtils,
|
||
private readonly logger: Logger,
|
||
private readonly global: Global,
|
||
private readonly tornPDAUtils: TornPDAUtils,
|
||
) {
|
||
}
|
||
|
||
/**
|
||
* @deprecated
|
||
*/
|
||
static getScriptEngine() {
|
||
let glob = Container.factory(Global);
|
||
let tornPDAUtils = Container.factory(TornPDAUtils);
|
||
return glob.GM_xmlhttpRequest ? UserScriptEngine.GM : tornPDAUtils.isPDA()
|
||
? UserScriptEngine.PDA : UserScriptEngine.RAW;
|
||
}
|
||
|
||
static COFetch(url: URL | string, method: 'get' | 'post' | string = 'get', body: any = null): Promise<string> {
|
||
let logger = Container.factory(Logger);
|
||
let start = new Timer();
|
||
const engine = this.getScriptEngine();
|
||
logger.info('跨域获取数据开始, 脚本引擎: ' + engine);
|
||
return new Promise<string>((resolve, reject) => {
|
||
switch (engine) {
|
||
case UserScriptEngine.RAW: {
|
||
logger.error(`跨域请求错误:${ UserScriptEngine.RAW }环境下无法进行跨域请求`);
|
||
reject(`错误:${ UserScriptEngine.RAW }环境下无法进行跨域请求`);
|
||
break;
|
||
}
|
||
case UserScriptEngine.PDA: {
|
||
const { PDA_httpGet, PDA_httpPost } = window;
|
||
// get
|
||
if (method === 'get') {
|
||
if (typeof PDA_httpGet !== 'function') {
|
||
logger.error('COFetch网络错误:PDA版本不支持');
|
||
reject('COFetch网络错误:PDA版本不支持');
|
||
}
|
||
PDA_httpGet(url)
|
||
.then(res => {
|
||
logger.info('跨域获取数据成功, 耗时' + start.getTimeMs());
|
||
resolve(res.responseText);
|
||
})
|
||
.catch(e => {
|
||
logger.error('COFetch网络错误', e);
|
||
reject(`COFetch网络错误 ${ e }`);
|
||
})
|
||
}
|
||
// post
|
||
else {
|
||
if (typeof PDA_httpPost !== 'function') {
|
||
logger.error('COFetch网络错误:PDA版本不支持');
|
||
reject('COFetch网络错误:PDA版本不支持');
|
||
}
|
||
PDA_httpPost(url, { 'content-type': 'application/json' }, body)
|
||
.then(res => resolve(res.responseText))
|
||
.catch(e => {
|
||
logger.error('COFetch网络错误', e);
|
||
reject(`COFetch网络错误 ${ e }`);
|
||
});
|
||
}
|
||
break;
|
||
}
|
||
case UserScriptEngine.GM: {
|
||
let { GM_xmlhttpRequest } = Container.factory(Global);
|
||
if (typeof GM_xmlhttpRequest !== 'function') {
|
||
logger.error('COFetch网络错误:用户脚本扩展API错误');
|
||
reject('错误:用户脚本扩展API错误');
|
||
}
|
||
GM_xmlhttpRequest({
|
||
method: method,
|
||
url: url,
|
||
data: method === 'get' ? null : body,
|
||
headers: method === 'get' ? null : { 'content-type': 'application/json' },
|
||
onload: res => {
|
||
logger.info('跨域获取数据成功,耗时' + start.getTimeMs());
|
||
resolve(res.response);
|
||
},
|
||
onerror: res => reject(`连接错误 ${ JSON.stringify(res) }`),
|
||
ontimeout: res => reject(`连接超时 ${ JSON.stringify(res) }`),
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
getScriptEngine() {
|
||
// let glob = Container.factory(Global);
|
||
// let tornPDAUtils = Container.factory(TornPDAUtils);
|
||
return this.global.GM_xmlhttpRequest ? UserScriptEngine.GM : this.tornPDAUtils.isPDA()
|
||
? UserScriptEngine.PDA : UserScriptEngine.RAW;
|
||
}
|
||
|
||
// /**
|
||
// * 返回玩家信息的对象 { playername: string, userID: number }
|
||
// * @return {PlayerInfo} rs
|
||
// */
|
||
// static getPlayerInfo(): PlayerInfo {
|
||
// const node = document.querySelector('script[uid]');
|
||
// if (node) {
|
||
// return {
|
||
// playername: node.getAttribute('name'),
|
||
// userID: node.getAttribute('uid') as unknown as number,
|
||
// }
|
||
// } else {
|
||
// new Alert('严重错误:芜湖助手无法获取用户数据,已退出');
|
||
// throw '芜湖助手无法获取用户数据';
|
||
// }
|
||
// }
|
||
|
||
// 用户设备类型 对应PC MOBILE TABLET
|
||
public static getDeviceType(): Device {
|
||
return window.innerWidth >= 1000
|
||
? Device.PC : window.innerWidth <= 600 ? Device.MOBILE : Device.TABLET;
|
||
}
|
||
|
||
public static getYaoCD(): string {
|
||
if (document.querySelector("#icon49-sidebar")) { // 0-10min
|
||
return '<10分'
|
||
} else if (document.querySelector("#icon50-sidebar")) { // 10min-1h
|
||
return '<1时'
|
||
} else if (document.querySelector("#icon51-sidebar")) { // 1h-2h
|
||
return '1~2时'
|
||
} else if (document.querySelector("#icon52-sidebar")) { // 2h-5h
|
||
return '2~5时'
|
||
} else if (document.querySelector("#icon53-sidebar")) { // 5h+
|
||
return '>5时'
|
||
} else {
|
||
return '无效'
|
||
}
|
||
}
|
||
|
||
// public static ajaxFetch(opt: AjaxFetchOption) {
|
||
// let { url, referrer = '/', method, body = null } = opt;
|
||
// let req_params: RequestInit = {
|
||
// headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||
// referrer,
|
||
// method,
|
||
// };
|
||
// if (method === 'POST') {
|
||
// req_params.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
||
// req_params.body = body;
|
||
// }
|
||
// return window.fetch(url, req_params);
|
||
// }
|
||
|
||
/**
|
||
* 通过 mutation.observe 方法异步返回元素
|
||
* @param {String} selectors - CSS规则的HTML元素选择器
|
||
* @param {Document} content - 上下文
|
||
* @param {number} timeout - 超时毫秒数
|
||
* @returns {Promise<HTMLElement|null>}
|
||
*/
|
||
public static elementReady(selectors: string, content: Document = document, timeout: number = 30000): Promise<HTMLElement> {
|
||
const logger = Container.factory(Logger);
|
||
logger.info('等待元素:' + selectors);
|
||
let timer = new Timer();
|
||
return new Promise((resolve, reject) => {
|
||
let el = content.querySelector(selectors) as HTMLElement;
|
||
if (el) {
|
||
logger.info('已获取元素, 耗时' + timer.getTimeMs(), el);
|
||
resolve(el);
|
||
return;
|
||
}
|
||
let observer = new MutationObserver((_, observer) => {
|
||
content.querySelectorAll(selectors).forEach((element) => {
|
||
logger.info({ innerHTML: element.innerHTML, element });
|
||
observer.disconnect();
|
||
logger.info('已获取元素, 耗时' + timer.getTimeMs(), element);
|
||
resolve(element as HTMLElement);
|
||
});
|
||
});
|
||
window.setTimeout(() => {
|
||
observer.disconnect();
|
||
logger.error(`等待元素超时! [${ selectors }]\n${ content.documentElement.tagName }, 耗时` + timer.getTimeMs());
|
||
reject(`等待元素超时! [${ selectors }]\n${ content.documentElement.tagName }, 耗时` + timer.getTimeMs());
|
||
}, timeout);
|
||
observer.observe(content.documentElement, { childList: true, subtree: true });
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 通过 mutation.observe 方法异步返回元素
|
||
* @param selectors
|
||
* @param content
|
||
* @param timeout
|
||
*/
|
||
public static querySelector(selectors: string, content: Document = document, timeout: number = 30000): Promise<HTMLElement> {
|
||
return CommonUtils.elementReady(selectors, content, timeout);
|
||
}
|
||
|
||
public querySelector(selectors: string, content: Document = document, timeout: number = 30000): Promise<HTMLElement> {
|
||
return CommonUtils.elementReady(selectors, content, timeout);
|
||
}
|
||
|
||
public static addStyle(rules: string): void {
|
||
const logger = Container.factory(Logger);
|
||
let element = document.querySelector('style#wh-trans-gStyle');
|
||
if (element) {
|
||
element.innerHTML += rules;
|
||
} else {
|
||
element = document.createElement("style");
|
||
element.id = 'wh-trans-gStyle';
|
||
element.innerHTML = rules;
|
||
document.head.appendChild(element);
|
||
}
|
||
logger.info('CSS规则已添加', element);
|
||
}
|
||
|
||
public styleInject(rules: string): void {
|
||
const element = document.createElement("style");
|
||
element.setAttribute('type', 'text/css');
|
||
element.innerHTML = rules;
|
||
document.head.appendChild(element);
|
||
}
|
||
|
||
public static loading_gif_html(): string {
|
||
return LOADING_IMG_HTML;
|
||
}
|
||
|
||
/**
|
||
* 播放音频
|
||
* @param {string} url 播放的音频URL
|
||
* @returns {undefined}
|
||
*/
|
||
public audioPlay(url: string = 'https://www.torn.com/js/chat/sounds/Warble_1.mp3'): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const audio = new Audio(url);
|
||
audio.addEventListener("canplaythrough", () => {
|
||
audio.play()
|
||
.catch(err => {
|
||
this.logger.error('播放音频出错', err.message, err.stack);
|
||
reject();
|
||
})
|
||
.then(
|
||
() => resolve()
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 与给定日期对比,现在是否是新的一天
|
||
* @param target
|
||
* @param offsetHours 比对时间的偏移小时数
|
||
*/
|
||
public isNewDay(target: number | Date, offsetHours: number = 0): boolean {
|
||
let tar: Date = typeof target === "number" ? new Date(target) : target;
|
||
let today = new Date();
|
||
let utcNewDay = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
|
||
utcNewDay.setHours(utcNewDay.getHours() + offsetHours);
|
||
return utcNewDay > tar;
|
||
}
|
||
|
||
public jQueryReady(): Promise<null> {
|
||
this.logger.info('等待jQuery加载中...');
|
||
this.fetchUtils.fetchText('/js/script/lib/jquery-1.8.2.js?v=f9128651g')
|
||
.then(res => window.eval(res));
|
||
let intervalId = window.setInterval(() => {
|
||
this.logger.info('仍在等待jQuery加载中...');
|
||
}, 1000);
|
||
return new Promise(async resolve => {
|
||
while (true) {
|
||
if (typeof window.$ === 'function') break;
|
||
await this.sleep(100);
|
||
}
|
||
window.clearInterval(intervalId);
|
||
this.logger.info('jQuery已加载');
|
||
resolve(null);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 等待毫秒数
|
||
* @param {Number} ms 毫秒
|
||
* @returns {Promise<void>}
|
||
*/
|
||
public sleep(ms: number): Promise<null> {
|
||
let time = Math.max(ms, 10);
|
||
return new Promise(resolve => setTimeout(() => resolve(null), time));
|
||
}
|
||
|
||
public elemGenerator(setting: MenuItemConfig, root_node: Element): HTMLElement {
|
||
let { tip, domType } = setting;
|
||
let new_node = null;
|
||
switch (domType) {
|
||
case 'checkbox': {
|
||
new_node = document.createElement('div');
|
||
let { domId, dictName, domText, changeEv } = setting;
|
||
|
||
let switcher = new TornStyleSwitch(domText);
|
||
let _input = switcher.getInput();
|
||
switcher.getBase().id = domId;
|
||
(tip) && (switcher.getBase().setAttribute('title', tip));
|
||
_input.checked = this.localConfigWrapper.config[dictName];
|
||
_input.onchange = e => {
|
||
this.localConfigWrapper.config[dictName] = _input.checked;
|
||
if (changeEv) changeEv(e);
|
||
};
|
||
new_node.appendChild(switcher.getBase());
|
||
break;
|
||
}
|
||
case 'button': {
|
||
new_node = document.createElement('div');
|
||
let { domId, domText, isTornBtn, clickFunc } = setting;
|
||
let btn = document.createElement('button');
|
||
(tip) && (btn.setAttribute('title', tip));
|
||
btn.id = domId;
|
||
btn.innerHTML = domText;
|
||
if (isTornBtn) btn.classList.add('torn-btn');
|
||
btn.addEventListener('click', clickFunc);
|
||
new_node.appendChild(btn);
|
||
break;
|
||
}
|
||
case 'select': {
|
||
new_node = document.createElement('div');
|
||
let { domSelectOpt, dictName, domId, domText } = setting;
|
||
let label = document.createElement('label');
|
||
(tip) && (label.setAttribute('title', tip));
|
||
let text = document.createTextNode(domText);
|
||
let select = document.createElement('select');
|
||
select.id = domId;
|
||
domSelectOpt.forEach((opt, i) => {
|
||
let { domVal, domText } = opt;
|
||
let option = document.createElement('option');
|
||
option.value = domVal;
|
||
option.innerHTML = domText;
|
||
option.selected = i === this.localConfigWrapper.config[dictName];
|
||
option.innerHTML = domText;
|
||
select.appendChild(option);
|
||
});
|
||
select.onchange = e => this.localConfigWrapper.config[dictName] = (<HTMLSelectElement>e.target).selectedIndex;
|
||
label.appendChild(text);
|
||
label.appendChild(select);
|
||
new_node.appendChild(label);
|
||
break;
|
||
}
|
||
case 'plain': {
|
||
let tag = setting.tagName || 'div';
|
||
new_node = document.createElement(tag);
|
||
if (setting.domId) new_node.id = setting.domId;
|
||
new_node.innerHTML += setting['domHTML'];
|
||
break;
|
||
}
|
||
}
|
||
// 移动节点
|
||
return root_node.appendChild(new_node);
|
||
}
|
||
|
||
public exportTextFile(filename, content) {
|
||
const tmp = document.createElement('a');
|
||
tmp.href = URL.createObjectURL(new Blob(content, { type: 'text/plain', endings: 'transparent' }));
|
||
tmp.download = filename;
|
||
tmp.click();
|
||
tmp.remove();
|
||
URL.revokeObjectURL(tmp.href);
|
||
}
|
||
|
||
public isValidUid(id: string | number): boolean {
|
||
if (typeof id === 'string') {
|
||
return /^[0-9]{1,7}$/.test(id);
|
||
} else {
|
||
return /^[0-9]{1,7}$/.test(id.toString());
|
||
}
|
||
}
|
||
|
||
public getTravelStage(): TRAVEL_STATE {
|
||
let global = Container.factory(Global);
|
||
if (global.bodyAttrs["data-abroad"] === 'false') {
|
||
return TRAVEL_STATE.IN_TORN;
|
||
}
|
||
// 海外落地:abroad
|
||
else if (global.bodyAttrs["data-traveling"] === 'false') {
|
||
return TRAVEL_STATE.ABROAD;
|
||
}
|
||
// 飞行中:traveling+abroad
|
||
else {
|
||
return TRAVEL_STATE.FLYING;
|
||
}
|
||
}
|
||
|
||
public matchOne(src: string, reg: string | RegExp): string {
|
||
let regExp = typeof reg === 'string' ? new RegExp(reg) : reg,
|
||
ret = null;
|
||
let match = src.match(regExp);
|
||
if (match.length > 0) ret = match[0];
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* 把秒数字格式化为
|
||
*
|
||
* `日 时 分 秒`
|
||
* @param s
|
||
*/
|
||
public secondsFormat(s: number): string {
|
||
let gap = '日 时 分 秒'.split(' ');
|
||
let last = s;
|
||
let formatDate = [];
|
||
let days = last / 86400 | 0;
|
||
formatDate.push(days);
|
||
last -= days * 86400;
|
||
let hours = last / 3600 | 0;
|
||
formatDate.push(hours);
|
||
last -= hours * 3600;
|
||
let mins = last / 60 | 0;
|
||
formatDate.push(mins);
|
||
last -= mins * 60;
|
||
formatDate.push(last);
|
||
let ret = '';
|
||
formatDate.forEach((num, i) => {
|
||
if (num > 0) {
|
||
let twoDig = i === 0 ? num : ('0' + num).slice(-2);
|
||
ret += twoDig + gap[i];
|
||
}
|
||
});
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @param key id或物品名
|
||
* @param m1 id->name map
|
||
* @param m2 name->item map
|
||
*/
|
||
public getItemByIdOrName(key: string, m1, m2: {
|
||
[k: string]: Partial<InventoryItemInfo>
|
||
}): Partial<InventoryItemInfo> {
|
||
return m1[key] ? m2[m1[key]] : m2[key];
|
||
}
|
||
}
|
||
|
||
export const CommonUtilsKey = Symbol('CommonUtilsKey') as InjectionKey<CommonUtils>;
|