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 { let logger = Container.factory(Logger); let start = new Timer(); const engine = this.getScriptEngine(); logger.info('跨域获取数据开始, 脚本引擎: ' + engine); return new Promise((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} */ public static elementReady(selectors: string, content: Document = document, timeout: number = 30000): Promise { 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 { return CommonUtils.elementReady(selectors, content, timeout); } public querySelector(selectors: string, content: Document = document, timeout: number = 30000): Promise { 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 { 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 { 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} */ public sleep(ms: number): Promise { 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] = (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 }): Partial { return m1[key] ? m2[m1[key]] : m2[key]; } } export const CommonUtilsKey = Symbol('CommonUtilsKey') as InjectionKey;