UserscriptAPI

My API for userscripts.

Від 21.08.2020. Дивіться остання версія.

Цей скрипт не слід встановлювати безпосередньо. Це - бібліотека для інших скриптів для включення в мета директиву // @require https://update.greatest.deepsurf.us/scripts/409641/839930/UserscriptAPI.js

  1. /**
  2. * API
  3. * @author Laster2800
  4. */
  5. class API {
  6. /**
  7. * @param {Object} [options] 选项
  8. * @param {string} [options.id='_0'] 标识符
  9. * @param {number} [options.fadeTime=400] UI 渐变时间(单位:ms)
  10. */
  11. constructor(options) {
  12. const defaultOptions = {
  13. id: '_0',
  14. fadeTime: 400,
  15. }
  16. this.options = {
  17. ...defaultOptions,
  18. ...options,
  19. }
  20.  
  21. if (window[`_api_${this.options.id}`]) {
  22. return window[`_api_${this.options.id}`]
  23. }
  24. window[`_api_${this.options.id}`] = this
  25.  
  26. const api = this
  27. /** DOM 相关 */
  28. this.dom = {
  29. /**
  30. * 创建 locationchange 事件
  31. * @see {@link https://stackoverflow.com/a/52809105 How to detect if URL has changed after hash in JavaScript}
  32. */
  33. createLocationchangeEvent() {
  34. if (!unsafeWindow._createLocationchangeEvent) {
  35. history.pushState = (f => function pushState() {
  36. const ret = f.apply(this, arguments)
  37. window.dispatchEvent(new Event('pushstate'))
  38. window.dispatchEvent(new Event('locationchange'))
  39. return ret
  40. })(history.pushState)
  41. history.replaceState = (f => function replaceState() {
  42. const ret = f.apply(this, arguments)
  43. window.dispatchEvent(new Event('replacestate'))
  44. window.dispatchEvent(new Event('locationchange'))
  45. return ret
  46. })(history.replaceState)
  47. window.addEventListener('popstate', () => {
  48. window.dispatchEvent(new Event('locationchange'))
  49. })
  50. unsafeWindow._createLocationchangeEvent = true
  51. }
  52. },
  53.  
  54. /**
  55. * 将一个元素绝对居中
  56. *
  57. * 要求该元素此时可见且尺寸为确定值(一般要求为块状元素)。运行后会在 `target` 上附加 `_absoluteCenter` 方法,若该方法已存在,则无视 `config` 直接执行 `target._absoluteCenter()`。
  58. * @param {HTMLElement} target 目标元素
  59. * @param {Object} [config] 配置
  60. * @param {string} [config.position='fixed'] 定位方式
  61. * @param {string} [config.top='50%'] `style.top`
  62. * @param {string} [config.left='50%'] `style.left`
  63. */
  64. setAbsoluteCenter(target, config) {
  65. if (!target._absoluteCenter) {
  66. const defaultConfig = {
  67. position: 'fixed',
  68. top: '50%',
  69. left: '50%',
  70. }
  71. config = { ...defaultConfig, ...config }
  72. target._absoluteCenter = () => {
  73. const style = getComputedStyle(target)
  74. const top = (parseFloat(style.height) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)) / 2
  75. const left = (parseFloat(style.width) + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)) / 2
  76. target.style.top = `calc(${config.top} - ${top}px)`
  77. target.style.left = `calc(${config.left} - ${left}px)`
  78. target.style.position = config.position
  79. }
  80.  
  81. // 实现一个简单的 debounce 来响应 resize 事件
  82. let tid
  83. window.addEventListener('resize', function() {
  84. if (target._absoluteCenter) {
  85. if (tid) {
  86. clearTimeout(tid)
  87. tid = null
  88. }
  89. tid = setTimeout(() => {
  90. target._absoluteCenter()
  91. }, 500)
  92. }
  93. })
  94. }
  95. target._absoluteCenter()
  96. },
  97.  
  98. /**
  99. * 处理 HTML 元素的渐显和渐隐
  100. * @param {boolean} inOut 渐显/渐隐
  101. * @param {HTMLElement} target HTML 元素
  102. * @param {() => void} [callback] 处理完成的回调函数
  103. */
  104. fade(inOut, target, callback) {
  105. // fadeId 等同于当前时间戳,其意义在于保证对于同一元素,后执行的操作必将覆盖前的操作
  106. const fadeId = new Date().getTime()
  107. target._fadeId = fadeId
  108. if (inOut) { // 渐显
  109. // 只有 display 可视情况下修改 opacity 才会触发 transition
  110. if (getComputedStyle(target).display == 'none') {
  111. target.style.display = 'unset'
  112. }
  113. setTimeout(() => {
  114. let success = false
  115. if (target._fadeId <= fadeId) {
  116. target.style.opacity = '1'
  117. success = true
  118. }
  119. callback && callback(success)
  120. }, 10) // 此处的 10ms 是为了保证修改 display 后在浏览器上真正生效,按 HTML5 定义,浏览器需保证 display 在修改 4ms 后保证生效,但实际上大部分浏览器貌似做不到,等个 10ms 再修改 opacity
  121. } else { // 渐隐
  122. target.style.opacity = '0'
  123. setTimeout(() => {
  124. let success = false
  125. if (target._fadeId <= fadeId) {
  126. target.style.display = 'none'
  127. success = true
  128. }
  129. callback && callback(success)
  130. }, api.options.fadeTime)
  131. }
  132. },
  133.  
  134. /**
  135. * 为 HTML 元素添加 `class`
  136. * @param {HTMLElement} el 目标元素
  137. * @param {string} className `class`
  138. */
  139. addClass(el, className) {
  140. if (el instanceof HTMLElement) {
  141. if (!el.className) {
  142. el.className = className
  143. } else {
  144. const clz = el.className.split(' ')
  145. if (clz.indexOf(className) < 0) {
  146. clz.push(className)
  147. el.className = clz.join(' ')
  148. }
  149. }
  150. }
  151. },
  152.  
  153. /**
  154. * 为 HTML 元素移除 `class`
  155. * @param {HTMLElement} el 目标元素
  156. * @param {string} [className] `class`,未指定时移除所有 `class`
  157. */
  158. removeClass(el, className) {
  159. if (el instanceof HTMLElement) {
  160. if (typeof className == 'string') {
  161. if (el.className == className) {
  162. el.className = ''
  163. } else {
  164. let clz = el.className.split(' ')
  165. clz = clz.reduce((prev, current) => {
  166. if (current != className) {
  167. prev.push(current)
  168. }
  169. return prev
  170. }, [])
  171. el.className = clz.join(' ')
  172. }
  173. } else {
  174. el.className = ''
  175. }
  176. }
  177. },
  178.  
  179. /**
  180. * 判断 HTML 元素类名中是否含有 `class`
  181. * @param {HTMLElement} el 目标元素
  182. * @param {string | string[]} className `class`,支持同时判断多个
  183. * @param {boolean} [and] 同时判断多个 `class` 时,默认采取 `OR` 逻辑,是否采用 `AND` 逻辑
  184. * @returns {boolean} 是否含有 `class`
  185. */
  186. containsClass(el, className, and = false) {
  187. if (el instanceof HTMLElement) {
  188. if (el.className == className) {
  189. return true
  190. } else {
  191. const clz = el.className.split(' ')
  192. if (className instanceof Array) {
  193. if (and) {
  194. for (const c of className) {
  195. if (clz.indexOf(c) < 0) {
  196. return false
  197. }
  198. }
  199. return true
  200. } else {
  201. for (const c of className) {
  202. if (clz.indexOf(c) >= 0) {
  203. return true
  204. }
  205. }
  206. return false
  207. }
  208. } else {
  209. return clz.indexOf(className) >= 0
  210. }
  211. }
  212. }
  213. },
  214. }
  215. /** 信息通知相关 */
  216. this.message = {
  217. /**
  218. * 创建信息
  219. * @param {string} msg 信息
  220. * @param {Object} [config] 设置
  221. * @param {boolean} [config.autoClose=true] 是否自动关闭信息,配合 `config.ms` 使用
  222. * @param {number} [config.ms=gm.const.messageTime] 显示时间(单位:ms,不含渐显/渐隐时间)
  223. * @param {boolean} [config.html=false] 是否将 `msg` 理解为 HTML
  224. * @param {string} [config.width] 信息框的宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  225. * @param {{top: string, left: string}} [config.position] 信息框的位置,不设置该项时,相当于设置为 `{ top: '70%', left: '50%' }`
  226. * @return {HTMLElement} 信息框元素
  227. */
  228. create(msg, config) {
  229. const defaultConfig = {
  230. autoClose: true,
  231. ms: 1200,
  232. html: false,
  233. width: null,
  234. position: {
  235. top: '70%',
  236. left: '50%',
  237. },
  238. }
  239. config = { ...defaultConfig, ...config }
  240.  
  241. const msgbox = document.body.appendChild(document.createElement('div'))
  242. msgbox.className = `${api.options.id}-msgbox`
  243. if (config.width) {
  244. msgbox.style.minWidth = 'auto' // 为什么一个是 auto 一个是 none?真是神奇的设计
  245. msgbox.style.maxWidth = 'none'
  246. msgbox.style.width = config.width
  247. }
  248.  
  249. msgbox.style.display = 'block'
  250. setTimeout(() => {
  251. api.dom.setAbsoluteCenter(msgbox, config.position)
  252. }, 10)
  253.  
  254. if (config.html) {
  255. msgbox.innerHTML = msg
  256. } else {
  257. msgbox.innerText = msg
  258. }
  259. api.dom.fade(true, msgbox, () => {
  260. if (config.autoClose) {
  261. setTimeout(() => {
  262. this.close(msgbox)
  263. }, config.ms)
  264. }
  265. })
  266. return msgbox
  267. },
  268.  
  269. /**
  270. * 关闭信息
  271. * @param {HTMLElement} msgbox 信息框元素
  272. */
  273. close(msgbox) {
  274. if (msgbox) {
  275. api.dom.fade(false, msgbox, () => {
  276. msgbox && msgbox.remove()
  277. })
  278. }
  279. },
  280.  
  281. /**
  282. * 创建高级信息
  283. * @param {HTMLElement} el 启动元素
  284. * @param {string} msg 信息
  285. * @param {string} flag 标志信息
  286. * @param {Object} [config] 设置
  287. * @param {string} [config.flagSize='1.8em'] 标志大小
  288. * @param {string} [config.width] 信息框的宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  289. * @param {{top: string, left: string}} [config.position] 信息框的位置,不设置该项时,相当于设置为 `{ top: gm.const.messageTop, left: gm.const.messageLeft }`
  290. * @param {() => boolean} [config.disabled] 是否处于禁用状态
  291. */
  292. advanced(el, msg, flag, config) {
  293. const defaultConfig = {
  294. flagSize: '1.8em',
  295. // 不能把数据列出,否则解构的时候会出问题
  296. }
  297. config = { ...defaultConfig, ...config }
  298.  
  299. const _self = this
  300. el.show = false
  301. el.onmouseenter = function() {
  302. if (config.disabled && config.disabled()) {
  303. return
  304. }
  305.  
  306. const htmlMsg = `
  307. <table class="gm-advanced-table"><tr>
  308. <td style="font-size:${config.flagSize};line-height:${config.flagSize}">${flag}</td>
  309. <td>${msg}</td>
  310. </tr></table>
  311. `
  312. this.msgbox = _self.create(htmlMsg, { ...config, html: true, autoClose: false })
  313.  
  314. // 可能信息框刚好生成覆盖在 el 上,需要做一个处理
  315. this.msgbox.onmouseenter = function() {
  316. this.mouseOver = true
  317. }
  318. // 从信息框出来也会关闭信息框,防止覆盖的情况下无法关闭
  319. this.msgbox.onmouseleave = function() {
  320. _self.close(this)
  321. }
  322. }
  323. el.onmouseleave = function() {
  324. setTimeout(() => {
  325. if (this.msgbox && !this.msgbox.mouseOver) {
  326. this.msgbox.onmouseleave = null
  327. _self.close(this.msgbox)
  328. }
  329. })
  330. }
  331. },
  332. }
  333. /** 用于等待元素加载/条件达成再执行操作 */
  334. this.wait = {
  335. /**
  336. * 在条件满足后执行操作
  337. *
  338. * 当条件满足后,如果不存在终止条件,那么直接执行 `callback(result)`。
  339. *
  340. * 当条件满足后,如果存在终止条件,且 `stopTimeout` 大于 0,则还会在接下来的 `stopTimeout` 时间内判断是否满足终止条件,称为终止条件的二次判断。
  341. * 如果在此期间,终止条件通过,则表示依然不满足条件,故执行 `onStop()` 而非 `callback(result)`。
  342. * 如果在此期间,终止条件一直失败,则顺利通过检测,执行 `callback(result)`。
  343. *
  344. * @param {Object} options 选项
  345. * @param {() => *} options.condition 条件,当 `condition()` 返回的 `result` 为真值时满足条件
  346. * @param {(result) => void} [options.callback] 当满足条件时执行 `callback(result)`
  347. * @param {number} [options.interval=100] 检测时间间隔(单位:ms)
  348. * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms)
  349. * @param {() => void} [options.onTimeout] 检测超时时执行 `onTimeout()`
  350. * @param {() => *} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  351. * @param {() => void} [options.onStop] 终止条件达成时执行 `onStop()`(包括终止条件的二次判断达成)
  352. * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
  353. * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
  354. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  355. */
  356. executeAfterConditionPassed(options) {
  357. const defaultOptions = {
  358. callback: result => api.logger.info(result),
  359. interval: 100,
  360. timeout: 5000,
  361. onTimeout: null,
  362. stopCondition: null,
  363. onStop: null,
  364. stopInterval: 50,
  365. stopTimeout: 0,
  366. timePadding: 0,
  367. }
  368. options = {
  369. ...defaultOptions,
  370. ...options,
  371. }
  372.  
  373. let tid
  374. let cnt = 0
  375. const maxCnt = (options.timeout - options.timePadding) / options.interval
  376. const task = async () => {
  377. const result = await options.condition()
  378. const stopResult = options.stopCondition && await options.stopCondition()
  379. if (stopResult) {
  380. clearInterval(tid)
  381. options.onStop && options.onStop.call(options)
  382. } else if (++cnt > maxCnt) {
  383. clearInterval(tid)
  384. options.onTimeout && options.onTimeout.call(options)
  385. } else if (result) {
  386. clearInterval(tid)
  387. if (options.stopCondition && options.stopTimeout > 0) {
  388. this.executeAfterConditionPassed({
  389. condition: options.stopCondition,
  390. callback: options.onStop,
  391. interval: options.stopInterval,
  392. timeout: options.stopTimeout,
  393. onTimeout: () => options.callback.call(options, result)
  394. })
  395. } else {
  396. options.callback.call(options, result)
  397. }
  398. }
  399. }
  400. setTimeout(() => {
  401. tid = setInterval(task, options.interval)
  402. task()
  403. }, options.timePadding)
  404. },
  405.  
  406. /**
  407. * 在元素加载完成后执行操作
  408. *
  409. * 当条件满足后,如果不存在终止条件,那么直接执行 `callback(element)`。
  410. *
  411. * 当条件满足后,如果存在终止条件,且 `stopTimeout` 大于 `0`,则还会在接下来的 `stopTimeout` 时间内判断是否满足终止条件,称为终止条件的二次判断。
  412. * 如果在此期间,终止条件通过,则表示依然不满足条件,故执行 `onStop()` 而非 `callback(element)`。
  413. * 如果在此期间,终止条件一直失败,则顺利通过检测,执行 `callback(element)`。
  414. *
  415. * @param {Object} options 选项
  416. * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
  417. * @param {HTMLElement} [options.base=document] 基元素
  418. * @param {(element: HTMLElement) => void} [options.callback] 当 `element` 加载成功时执行 `callback(element)`
  419. * @param {number} [options.interval=100] 检测时间间隔(单位:ms)
  420. * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms)
  421. * @param {() => void} [options.onTimeout] 检测超时时执行 `onTimeout()`
  422. * @param {string | (() => *)} [options.stopCondition] 终止条件。若为函数,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测;若为字符串,则作为元素选择器指定终止元素 `stopElement`,若该元素加载成功则终止检测
  423. * @param {() => void} [options.onStop] 终止条件达成时执行 `onStop()`(包括终止条件的二次判断达成)
  424. * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
  425. * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
  426. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  427. */
  428. executeAfterElementLoaded(options) {
  429. const defaultOptions = {
  430. base: document,
  431. callback: el => api.logger.info(el),
  432. interval: 100,
  433. timeout: 5000,
  434. onTimeout: null,
  435. stopCondition: null,
  436. onStop: null,
  437. stopInterval: 50,
  438. stopTimeout: 0,
  439. timePadding: 0,
  440. }
  441. options = {
  442. ...defaultOptions,
  443. ...options,
  444. }
  445. this.executeAfterConditionPassed({
  446. ...options,
  447. condition: () => options.base.querySelector(options.selector),
  448. stopCondition: () => {
  449. if (options.stopCondition) {
  450. if (options.stopCondition) {
  451. return options.stopCondition()
  452. } else if (typeof options.stopCondition == 'string') {
  453. return document.querySelector(options.stopCondition)
  454. }
  455. }
  456. },
  457. })
  458. },
  459.  
  460. /**
  461. * 等待条件满足
  462. *
  463. * 执行细节类似于 {@link executeAfterConditionPassed}。在原来执行 `callback(result)` 的地方执行 `resolve(result)`,被终止或超时执行 `reject()`。
  464. * @async
  465. * @see executeAfterConditionPassed
  466. * @param {Object} options 选项
  467. * @param {() => *} options.condition 条件,当 `condition()` 返回的 `result` 为真值时满足条件
  468. * @param {number} [options.interval=100] 检测时间间隔(单位:ms)
  469. * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms)
  470. * @param {() => *} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  471. * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
  472. * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
  473. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  474. * @returns {Promise} `result`
  475. * @throws 当等待超时或者被终止时抛出
  476. */
  477. async waitForConditionPassed(options) {
  478. return new Promise((resolve, reject) => {
  479. this.executeAfterConditionPassed({
  480. ...options,
  481. callback: result => resolve(result),
  482. onTimeout: function() {
  483. reject(['TIMEOUT', 'waitForConditionPassed', this])
  484. },
  485. onStop: function() {
  486. reject(['STOP', 'waitForConditionPassed', this])
  487. },
  488. })
  489. })
  490. },
  491.  
  492. /**
  493. * 等待元素加载
  494. *
  495. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  496. * @async
  497. * @see executeAfterElementLoaded
  498. * @param {string} selector 该选择器指定要等待加载的元素 `element`
  499. * @param {HTMLElement} [base=document] 基元素
  500. * @returns {Promise<HTMLElement>} `element`
  501. * @throws 当等待超时或者被终止时抛出
  502. */
  503. /**
  504. * 等待元素加载
  505. *
  506. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  507. * @async
  508. * @see executeAfterElementLoaded
  509. * @param {Object} options 选项
  510. * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
  511. * @param {HTMLElement} [options.base=document] 基元素
  512. * @param {number} [options.interval=100] 检测时间间隔(单位:ms)
  513. * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms)
  514. * @param {string | (() => *)} [options.stopCondition] 终止条件。若为函数,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测;若为字符串,则作为元素选择器指定终止元素 `stopElement`,若该元素加载成功则终止检测
  515. * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
  516. * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
  517. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  518. * @returns {Promise<HTMLElement>} `element`
  519. * @throws 当等待超时或者被终止时抛出
  520. */
  521. async waitForElementLoaded() {
  522. let options
  523. if (arguments.length > 0) {
  524. if (typeof arguments[0] == 'string') {
  525. options = { selector: arguments[0] }
  526. if (arguments[1]) {
  527. options.base = arguments[1]
  528. }
  529. } else {
  530. options = arguments[0]
  531. }
  532. }
  533. return new Promise((resolve, reject) => {
  534. this.executeAfterElementLoaded({
  535. ...options,
  536. callback: element => resolve(element),
  537. onTimeout: function() {
  538. reject(['TIMEOUT', 'waitForElementLoaded', this])
  539. },
  540. onStop: function() {
  541. reject(['STOP', 'waitForElementLoaded', this])
  542. },
  543. })
  544. })
  545. },
  546. }
  547. /** 网络相关 */
  548. this.web = {
  549. /** @typedef {Object} GM_xmlhttpRequest_details */
  550. /** @typedef {Object} GM_xmlhttpRequest_response */
  551. /**
  552. * 发起网络请求
  553. * @async
  554. * @param {GM_xmlhttpRequest_details} details 定义及细节同 {@link GM_xmlhttpRequest} 的 `details`
  555. * @returns {Promise<GM_xmlhttpRequest_response>} 响应对象
  556. * @throws 当请求发生错误或者超时时抛出
  557. * @see {@link https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest GM_xmlhttpRequest}
  558. */
  559. async request(details) {
  560. if (details) {
  561. return new Promise((resolve, reject) => {
  562. const throwHandler = function(msg) {
  563. api.logger.error('NETWORK REQUEST ERROR')
  564. reject(msg)
  565. }
  566. details.onerror = details.onerror || (() => throwHandler(['ERROR', 'request', details]))
  567. details.ontimeout = details.ontimeout || (() => throwHandler(['TIMEOUT', 'request', details]))
  568. details.onload = details.onload || (response => resolve(response))
  569. GM_xmlhttpRequest(details)
  570. })
  571. }
  572. },
  573.  
  574. /**
  575. * 判断当前 URL 是否匹配
  576. * @param {RegExp} reg 用于判断是否匹配的正则表达式
  577. * @returns {boolean} 是否匹配
  578. */
  579. urlMatch(reg) {
  580. return reg.test(location.href)
  581. },
  582. }
  583. /**
  584. * 日志
  585. */
  586. this.logger = {
  587. /**
  588. * 打印格式化日志
  589. * @param {*} message 日志信息
  590. * @param {string} label 日志标签
  591. * @param {boolean} [error] 是否错误信息
  592. */
  593. log(message, label, error) {
  594. const css = `
  595. background-color: black;
  596. color: white;
  597. border-radius: 2px;
  598. padding: 2px;
  599. margin-right: 2px;
  600. `
  601. const output = console[error ? 'error' : 'log']
  602. const type = typeof message == 'string' ? '%s' : '%o'
  603. output(`%c${label}%c${type}`, css, '', message)
  604. },
  605.  
  606. /**
  607. * 打印日志
  608. * @param {*} message 日志信息
  609. */
  610. info(message) {
  611. this.log(message, GM_info.script.name)
  612. },
  613.  
  614. /**
  615. * 打印错误日志
  616. * @param {*} message 错误日志信息
  617. */
  618. error(message) {
  619. this.log(message, GM_info.script.name, true)
  620. },
  621. }
  622.  
  623. GM_addStyle(`
  624. :root {
  625. --light-text-color: white;
  626. --shadow-color: #000000bf;
  627. }
  628.  
  629. .${api.options.id}-msgbox {
  630. z-index: 65535;
  631. background-color: var(--shadow-color);
  632. font-size: 16px;
  633. max-width: 24em;
  634. min-width: 2em;
  635. color: var(--light-text-color);
  636. padding: 0.5em 1em;
  637. border-radius: 0.6em;
  638. opacity: 0;
  639. transition: opacity ${api.options.fadeTime}ms ease-in-out;
  640. user-select: none;
  641. }
  642.  
  643. .${api.options.id}-msgbox .gm-advanced-table td {
  644. vertical-align: middle;
  645. }
  646. .${api.options.id}-msgbox .gm-advanced-table td:first-child {
  647. padding-right: 0.6em;
  648. }
  649. `)
  650. }
  651. }