Greasy Fork is available in English.

B站防剧透进度条@Deprecated

看比赛、看番总是被进度条剧透?装上这个脚本再也不用担心这些问题了

Nainštalovať tento skript?
Autor skriptu navrhuje

Tiež sa vám môže páčiť B站稍后再看功能增强.

Nainštalovať tento skript
  1. // ==UserScript==
  2. // @name B站防剧透进度条@Deprecated
  3. // @version 2.5.12@Deprecated.20220706
  4. // @namespace laster2800
  5. // @author Laster2800
  6. // @description 看比赛、看番总是被进度条剧透?装上这个脚本再也不用担心这些问题了
  7. // @icon https://www.bilibili.com/favicon.ico
  8. // @homepageURL https://greasyfork.org/zh-CN/scripts/411092
  9. // @supportURL https://greasyfork.org/zh-CN/scripts/411092/feedback
  10. // @license LGPL-3.0
  11. // @noframes
  12. // @include *://www.bilibili.com/video/*
  13. // @include *://www.bilibili.com/medialist/play/watchlater
  14. // @include *://www.bilibili.com/medialist/play/watchlater/*
  15. // @include *://www.bilibili.com/medialist/play/ml*
  16. // @include *://www.bilibili.com/bangumi/play/*
  17. // @require https://greasyfork.org/scripts/409641-userscriptapi/code/UserscriptAPI.js?version=974252
  18. // @require https://greasyfork.org/scripts/431998-userscriptapidom/code/UserscriptAPIDom.js?version=1005139
  19. // @require https://greasyfork.org/scripts/432000-userscriptapimessage/code/UserscriptAPIMessage.js?version=1055883
  20. // @require https://greasyfork.org/scripts/432002-userscriptapiwait/code/UserscriptAPIWait.js?version=1035042
  21. // @require https://greasyfork.org/scripts/432003-userscriptapiweb/code/UserscriptAPIWeb.js?version=977807
  22. // @require https://greasyfork.org/scripts/432807-inputnumber/code/InputNumber.js?version=973690
  23. // @grant GM_registerMenuCommand
  24. // @grant GM_xmlhttpRequest
  25. // @grant GM_setValue
  26. // @grant GM_getValue
  27. // @grant GM_deleteValue
  28. // @grant GM_listValues
  29. // @connect api.bilibili.com
  30. // @compatible edge 版本不小于 85
  31. // @compatible chrome 版本不小于 85
  32. // @compatible firefox 版本不小于 90
  33. // ==/UserScript==
  34.  
  35. (function() {
  36. 'use strict'
  37.  
  38. if (GM_info.scriptHandler !== 'Tampermonkey') {
  39. const { script } = GM_info
  40. script.author ??= 'Laster2800'
  41. script.homepage ??= 'https://greasyfork.org/zh-CN/scripts/411092'
  42. script.supportURL ??= 'https://greasyfork.org/zh-CN/scripts/411092/feedback'
  43. }
  44.  
  45. /**
  46. * 脚本内用到的枚举定义
  47. */
  48. const Enums = {}
  49.  
  50. /**
  51. * 全局对象
  52. * @typedef GMObject
  53. * @property {string} id 脚本标识
  54. * @property {number} configVersion 配置版本,为最后一次执行初始化设置或功能性更新设置时脚本对应的配置版本号
  55. * @property {number} configUpdate 当前版本对应的配置版本号,只要涉及到配置的修改都要更新;若同一天修改多次,可以追加小数来区分
  56. * @property {GMObject_config} config 用户配置
  57. * @property {GMObject_configMap} configMap 用户配置属性
  58. * @property {GMObject_infoMap} infoMap 信息属性
  59. * @property {GMObject_data} data 脚本数据
  60. * @property {GMObject_url} url URL
  61. * @property {GMObject_regex} regex 正则表达式
  62. * @property {{[c: string]: *}} const 常量
  63. * @property {GMObject_panel} panel 面板
  64. * @property {{[s: string]: HTMLElement}} el HTML 元素
  65. */
  66. /**
  67. * @typedef GMObject_config
  68. * @property {boolean} bangumiEnabled 番剧自动启用功能
  69. * @property {boolean} simpleScriptControl 是否简化进度条上方的脚本控制
  70. * @property {boolean} disableCurrentPoint 隐藏当前播放时间
  71. * @property {boolean} disableDuration 隐藏视频时长
  72. * @property {boolean} disablePreview 隐藏进度条预览
  73. * @property {boolean} disablePartInformation 隐藏分P信息
  74. * @property {boolean} disableSegmentInformation 隐藏分段信息
  75. * @property {number} offsetTransformFactor 进度条极端偏移因子
  76. * @property {number} offsetLeft 进度条偏移极左值
  77. * @property {number} offsetRight 进度条偏移极右值
  78. * @property {number} reservedLeft 进度条左侧预留区
  79. * @property {number} reservedRight 进度条右侧预留区
  80. * @property {boolean} postponeOffset 延后进度条偏移的时间点
  81. * @property {boolean} reloadAfterSetting 设置生效后刷新页面
  82. */
  83. /**
  84. * @typedef {{[config: string]: GMObject_configMap_item}} GMObject_configMap
  85. */
  86. /**
  87. * @typedef GMObject_configMap_item
  88. * @property {*} default 默认值
  89. * @property {'string' | 'boolean' | 'int' | 'float'} [type] 数据类型
  90. * @property {'checked' | 'value'} attr 对应 `DOM` 元素上的属性
  91. * @property {boolean} [manual] 配置保存时是否需要手动处理
  92. * @property {boolean} [needNotReload] 配置改变后是否不需要重新加载就能生效
  93. * @property {number} [min] 最小值
  94. * @property {number} [max] 最大值
  95. * @property {number} [configVersion] 涉及配置更改的最后配置版本
  96. */
  97. /**
  98. * @typedef {{[info: string]: GMObject_infoMap_item}} GMObject_infoMap
  99. */
  100. /**
  101. * @typedef GMObject_infoMap_item
  102. * @property {number} [configVersion] 涉及信息更改的最后配置版本
  103. */
  104. /**
  105. * @callback uploaderList 不传入/传入参数时获取/修改防剧透UP主名单
  106. * @param {string} [updateData] 更新数据
  107. * @returns {string} 防剧透UP主名单
  108. */
  109. /**
  110. * @callback uploaderListSet 通过懒加载方式获取格式化的防剧透UP主名单
  111. * @param {boolean} [reload] 是否重新加载数据
  112. * @returns {Set<String>} 防剧透UP主名单
  113. */
  114. /**
  115. * @typedef GMObject_data
  116. * @property {uploaderList} uploaderList 防剧透UP主名单
  117. * @property {uploaderListSet} uploaderListSet 防剧透UP主名单集合
  118. */
  119. /**
  120. * @callback api_videoInfo
  121. * @param {string} id `aid` 或 `bvid`
  122. * @param {'aid' | 'bvid'} type `id` 类型
  123. * @returns {string} 查询视频信息的 URL
  124. */
  125. /**
  126. * @typedef GMObject_url
  127. * @property {api_videoInfo} api_videoInfo 视频信息
  128. * @property {string} gm_readme 说明文档
  129. * @property {string} gm_changelog 更新日志
  130. */
  131. /**
  132. * @typedef GMObject_regex
  133. * @property {RegExp} page_videoNormalMode 匹配常规播放页
  134. * @property {RegExp} page_videoWatchlaterMode 匹配稍后再看播放页
  135. * @property {RegExp} page_bangumi 匹配番剧播放页
  136. */
  137. /**
  138. * @typedef GMObject_panel
  139. * @property {GMObject_panel_item} setting 设置
  140. */
  141. /**
  142. * @typedef GMObject_panel_item
  143. * @property {0 | 1 | 2 | 3 | -1} state 打开状态(关闭 | 开启中 | 打开 | 关闭中 | 错误)
  144. * @property {0 | 1 | 2} wait 等待阻塞状态(无等待阻塞 | 等待开启 | 等待关闭)
  145. * @property {HTMLElement} el 面板元素
  146. * @property {() => (void | Promise<void>)} [openHandler] 打开面板的回调函数
  147. * @property {() => (void | Promise<void>)} [closeHandler] 关闭面板的回调函数
  148. * @property {() => void} [openedHandler] 彻底打开面板后的回调函数
  149. * @property {() => void} [closedHandler] 彻底关闭面板后的回调函数
  150. */
  151. /**
  152. * 全局对象
  153. * @type {GMObject}
  154. */
  155. const gm = {
  156. id: 'gm411092',
  157. configVersion: GM_getValue('configVersion'),
  158. configUpdate: 20210806,
  159. config: {},
  160. configMap: {
  161. bangumiEnabled: { default: false, attr: 'checked', needNotReload: true },
  162. simpleScriptControl: { default: false, attr: 'checked' },
  163. disableCurrentPoint: { default: true, attr: 'checked', configVersion: 20200912 },
  164. disableDuration: { default: true, attr: 'checked' },
  165. disablePreview: { default: false, attr: 'checked' },
  166. disablePartInformation: { default: true, attr: 'checked', configVersion: 20210302 },
  167. disableSegmentInformation: { default: true, attr: 'checked', configVersion: 20210806 },
  168. offsetTransformFactor: { default: 0.6, type: 'float', attr: 'value', needNotReload: true, max: 5.0, configVersion: 20210722 },
  169. offsetLeft: { default: 60, type: 'int', attr: 'value', needNotReload: true, configVersion: 20210722 },
  170. offsetRight: { default: 60, type: 'int', attr: 'value', needNotReload: true, configVersion: 20210722 },
  171. reservedLeft: { default: 10, type: 'int', attr: 'value', needNotReload: true, configVersion: 20210722 },
  172. reservedRight: { default: 15, type: 'int', attr: 'value', needNotReload: true, configVersion: 20210722 },
  173. postponeOffset: { default: true, attr: 'checked', needNotReload: true, configVersion: 20200911 },
  174. reloadAfterSetting: { default: true, attr: 'checked', needNotReload: true },
  175. },
  176. infoMap: {
  177. help: {},
  178. uploaderList: {},
  179. resetParam: {},
  180. },
  181. data: {
  182. uploaderList: null,
  183. uploaderListSet: null,
  184. },
  185. url: {
  186. api_videoInfo: (id, type) => `https://api.bilibili.com/x/web-interface/view?${type}=${id}`,
  187. gm_readme: 'https://gitee.com/liangjiancang/userscript/blob/master/script/@Deprecated/BilibiliNoSpoilProgressBar/README.md',
  188. gm_changelog: 'https://gitee.com/liangjiancang/userscript/blob/master/script/@Deprecated/BilibiliNoSpoilProgressBar/changelog.md',
  189. },
  190. regex: {
  191. page_videoNormalMode: /\.com\/video([#/?]|$)/,
  192. page_videoWatchlaterMode: /\.com\/medialist\/play\/(watchlater|ml\d+)([#/?]|$)/,
  193. page_bangumi: /\.com\/bangumi\/play([#/?]|$)/,
  194. },
  195. const: {
  196. fadeTime: 400,
  197. },
  198. panel: {
  199. setting: { state: 0, wait: 0, el: null },
  200. },
  201. el: {
  202. gmRoot: null,
  203. setting: null,
  204. },
  205. }
  206.  
  207. /* global UserscriptAPI */
  208. const api = new UserscriptAPI({
  209. id: gm.id,
  210. label: GM_info.script.name,
  211. fadeTime: gm.const.fadeTime,
  212. wait: { element: { timeout: 15000 } },
  213. })
  214.  
  215. /** @type {Script} */
  216. let script = null
  217. /** @type {Webpage} */
  218. let webpage = null
  219.  
  220. /**
  221. * 脚本运行的抽象,为脚本本身服务的核心功能
  222. */
  223. class Script {
  224. #data = {}
  225.  
  226. /** 通用方法 */
  227. method = {
  228. /**
  229. * GM 读取流程
  230. *
  231. * 一般情况下,读取用户配置;如果配置出错,则沿用默认值,并将默认值写入配置中
  232. * @param {string} gmKey 键名
  233. * @param {*} defaultValue 默认值
  234. * @param {boolean} [writeback=true] 配置出错时是否将默认值回写入配置中
  235. * @returns {*} 通过校验时是配置值,不能通过校验时是默认值
  236. */
  237. getConfig(gmKey, defaultValue, writeback = true) {
  238. let invalid = false
  239. let value = GM_getValue(gmKey)
  240. if (Enums && gmKey in Enums) {
  241. if (!Object.values(Enums[gmKey]).includes(value)) {
  242. invalid = true
  243. }
  244. } else if (typeof value === typeof defaultValue) { // 对象默认赋 null 无需额外处理
  245. const { type } = gm.configMap[gmKey]
  246. if (type === 'int' || type === 'float') {
  247. invalid = gm.configMap[gmKey].min > value || gm.configMap[gmKey].max < value
  248. }
  249. } else {
  250. invalid = true
  251. }
  252. if (invalid) {
  253. value = defaultValue
  254. writeback && GM_setValue(gmKey, value)
  255. }
  256. return value
  257. },
  258.  
  259. /**
  260. * 重置脚本
  261. */
  262. reset() {
  263. const gmKeys = GM_listValues()
  264. for (const gmKey of gmKeys) {
  265. GM_deleteValue(gmKey)
  266. }
  267. },
  268. }
  269.  
  270. /**
  271. * 初始化
  272. */
  273. init() {
  274. try {
  275. this.initGMObject()
  276. this.updateVersion()
  277. this.readConfig()
  278. } catch (e) {
  279. api.logger.error(e)
  280. api.message.confirm('初始化错误!是否彻底清空内部数据以重置脚本?').then(result => {
  281. if (result) {
  282. this.method.reset()
  283. location.reload()
  284. }
  285. })
  286. }
  287. }
  288.  
  289. /**
  290. * 初始化全局对象
  291. */
  292. initGMObject() {
  293. gm.data = {
  294. ...gm.data,
  295. uploaderList: updateData => {
  296. if (typeof updateData === 'string') {
  297. // 注意多行模式「\n」位置为「line$\n^line」,且「\n」是空白符,被视为在下一行「行首」
  298. updateData = updateData.replace(/\s+$/gm, '') // 除空行及行尾空白符(有效的换行符被「^」隔断而得以保留),除下面的特殊情况
  299. .replace(/^\n/, '') // 移除为作为「\s*$」且有后续的首行的换行符,此时该换行符被视为在第二行「行首」
  300. GM_setValue('uploaderList', updateData)
  301. this.#data.uploaderListSet = undefined
  302. return updateData
  303. } else {
  304. let uploaderList = GM_getValue('uploaderList')
  305. if (typeof uploaderList !== 'string') {
  306. uploaderList = ''
  307. GM_setValue('uploaderList', uploaderList)
  308. }
  309. return uploaderList
  310. }
  311. },
  312. uploaderListSet: reload => {
  313. const $data = this.#data
  314. if (!$data.uploaderListSet || reload) {
  315. const set = new Set()
  316. const content = gm.data.uploaderList()
  317. if (content.startsWith('*')) {
  318. set.add('*')
  319. } else {
  320. const rows = content.split('\n')
  321. for (const row of rows) {
  322. const m = /^\d+/.exec(row)
  323. if (m) {
  324. set.add(m[0])
  325. }
  326. }
  327. }
  328. $data.uploaderListSet = set
  329. }
  330. return $data.uploaderListSet
  331. },
  332. }
  333.  
  334. gm.el.gmRoot = document.createElement('div')
  335. gm.el.gmRoot.id = gm.id
  336. api.wait.executeAfterElementLoaded({ // body 已存在时无异步
  337. selector: 'body',
  338. callback: body => body.append(gm.el.gmRoot),
  339. })
  340. }
  341.  
  342. /**
  343. * 版本更新处理
  344. */
  345. updateVersion() {
  346. if (gm.configVersion >= 20210627) { // 1.5.5.20210627
  347. if (gm.configVersion < gm.configUpdate) {
  348. // 必须按从旧到新的顺序写
  349. // 内部不能使用 gm.configUpdate,必须手写更新后的配置版本号!
  350.  
  351. // 2.0.0.20210806
  352. if (gm.configVersion < 20210806) {
  353. GM_deleteValue('disablePbp')
  354. }
  355.  
  356. // 功能性更新后更新此处配置版本,通过时跳过功能性更新设置,否则转至 readConfig() 中处理
  357. if (gm.configVersion >= 20210806) {
  358. gm.configVersion = gm.configUpdate
  359. GM_setValue('configVersion', gm.configVersion)
  360. }
  361. }
  362. } else {
  363. this.method.reset()
  364. gm.configVersion = null
  365. }
  366. }
  367.  
  368. /**
  369. * 用户配置读取
  370. */
  371. readConfig() {
  372. if (gm.configVersion > 0) {
  373. for (const [name, item] of Object.entries(gm.configMap)) {
  374. gm.config[name] = this.method.getConfig(name, item.default)
  375. }
  376. if (gm.configVersion !== gm.configUpdate) {
  377. this.openUserSetting(2)
  378. }
  379. } else {
  380. // 用户强制初始化,或第一次安装脚本,或版本过旧
  381. gm.configVersion = 0
  382. for (const [name, item] of Object.entries(gm.configMap)) {
  383. gm.config[name] = item.default
  384. GM_setValue(name, item.default)
  385. }
  386. this.openUserSetting(1)
  387.  
  388. setTimeout(async () => {
  389. const result = await api.message.confirm('脚本有一定使用门槛,如果不理解防剧透机制效果将会剧减,这种情况下用户甚至完全不明白脚本在「干什么」,建议在阅读说明后使用。是否立即打开防剧透机制说明?')
  390. if (result) {
  391. window.open(`${gm.url.gm_readme}#防剧透机制说明`)
  392. }
  393. }, 2000)
  394. }
  395. }
  396.  
  397. /**
  398. * 添加脚本菜单
  399. */
  400. addScriptMenu() {
  401. // 用户配置设置
  402. GM_registerMenuCommand('用户设置', () => this.openUserSetting())
  403. // 防剧透UP主名单
  404. GM_registerMenuCommand('防剧透UP主名单', () => this.openUploaderList())
  405. // 强制初始化
  406. GM_registerMenuCommand('初始化脚本', () => this.resetScript())
  407. }
  408.  
  409. /**
  410. * 打开用户设置
  411. * @param {number} [type=0] 常规 `0` | 初始化 `1` | 功能性更新 `2`
  412. */
  413. openUserSetting(type = 0) {
  414. if (gm.el.setting) {
  415. this.openPanelItem('setting')
  416. } else {
  417. /** @type {{[n: string]: HTMLElement}} */
  418. const el = {}
  419. setTimeout(() => {
  420. initSetting()
  421. processSettingItem()
  422. this.openPanelItem('setting')
  423. })
  424.  
  425. /**
  426. * 设置页初始化
  427. */
  428. const initSetting = () => {
  429. gm.el.setting = gm.el.gmRoot.appendChild(document.createElement('div'))
  430. gm.panel.setting.el = gm.el.setting
  431. gm.el.setting.className = 'gm-setting gm-modal-container'
  432.  
  433. const getItemHTML = (label, ...items) => {
  434. let html = `<div class="gm-item-container"><div class="gm-item-label">${label}</div><div class="gm-item-content">`
  435. for (const item of items) {
  436. html += `<div class="${item.className ? `${item.className}` : 'gm-item'}"${item.desc ? ` title="${item.desc}"` : ''}>${item.html}</div>`
  437. }
  438. html += '</div></div>'
  439. return html
  440. }
  441. let itemsHTML = ''
  442. itemsHTML += getItemHTML('说明', {
  443. desc: '查看脚本防剧透机制的实现原理。',
  444. html: `<div>
  445. <span>防剧透机制说明</span>
  446. <a id="gm-help" class="gm-info" href="${gm.url.gm_readme}#防剧透机制说明" target="_blank"">查看</a>
  447. </div>`,
  448. })
  449. itemsHTML += getItemHTML('自动化', {
  450. desc: '加入防剧透名单UP主的视频,会在打开视自动开启防剧透进度条。',
  451. html: `<div>
  452. <span>防剧透UP主名单</span>
  453. <span id="gm-uploaderList" class="gm-info">编辑</span>
  454. </div>`,
  455. })
  456. itemsHTML += getItemHTML('自动化', {
  457. desc: '番剧是否自动打开防剧透进度条?',
  458. html: `<label>
  459. <span>番剧自动启用防剧透进度条</span>
  460. <input id="gm-bangumiEnabled" type="checkbox">
  461. </label>`,
  462. })
  463. itemsHTML += getItemHTML('用户接口', {
  464. desc: '是否简化进度条上方的脚本控制?',
  465. html: `<label>
  466. <span>简化进度条上方的脚本控制</span>
  467. <input id="gm-simpleScriptControl" type="checkbox">
  468. </label>`,
  469. })
  470. itemsHTML += getItemHTML('用户接口', {
  471. desc: '这些功能可能会造成剧透,根据需要在防剧透进度条中进行隐藏。',
  472. html: `<div>
  473. <span>启用功能时</span>
  474. </div>`,
  475. }, {
  476. desc: '是否在防剧透进度条中隐藏当前播放时间?该功能可能会造成剧透。',
  477. html: `<label>
  478. <span>隐藏当前播放时间</span>
  479. <input id="gm-disableCurrentPoint" type="checkbox">
  480. </label>`,
  481. }, {
  482. desc: '是否在防剧透进度条中隐藏视频时长?该功能可能会造成剧透。',
  483. html: `<label>
  484. <span>隐藏视频时长</span>
  485. <input id="gm-disableDuration" type="checkbox">
  486. </label>`,
  487. }, {
  488. desc: '是否在防剧透进度条中隐藏进度条预览?该功能可能会造成剧透。',
  489. html: `<label>
  490. <span>隐藏进度条预览</span>
  491. <input id="gm-disablePreview" type="checkbox">
  492. </label>`,
  493. }, {
  494. desc: '是否隐藏视频分P信息?它们可能会造成剧透。该功能对番剧无效。',
  495. html: `<label>
  496. <span>隐藏分P信息</span>
  497. <input id="gm-disablePartInformation" type="checkbox">
  498. </label>`,
  499. }, {
  500. desc: '是否隐藏视频分段信息?它们可能会造成剧透。',
  501. html: `<label>
  502. <span>隐藏分段信息</span>
  503. <input id="gm-disableSegmentInformation" type="checkbox">
  504. </label>`,
  505. })
  506. itemsHTML += getItemHTML('高级设置', {
  507. desc: '防剧透参数设置,请务必在理解参数作用的前提下修改!',
  508. html: `<div>
  509. <span>防剧透参数</span>
  510. <span id="gm-resetParam" class="gm-info" title="重置防剧透参数。">重置</span>
  511. </div>`,
  512. }, {
  513. desc: '进度条极端偏移因子设置。',
  514. html: `<div>
  515. <span>进度条极端偏移因子</span>
  516. <span id="gm-offsetTransformFactorInformation" class="gm-information" title="">💬</span>
  517. <input is="laster2800-input-number" id="gm-offsetTransformFactor" value="${gm.configMap.offsetTransformFactor.default}" max="${gm.configMap.offsetTransformFactor.max}" digits="1">
  518. </div>`,
  519. }, {
  520. desc: '进度条偏移极左值设置。',
  521. html: `<div>
  522. <span>进度条偏移极左值</span>
  523. <span id="gm-offsetLeftInformation" class="gm-information" title="">💬</span>
  524. <input is="laster2800-input-number" id="gm-offsetLeft" value="${gm.configMap.offsetLeft.default}" max="100">
  525. </div>`,
  526. }, {
  527. desc: '进度条偏移极右值设置。',
  528. html: `<div>
  529. <span>进度条偏移极右值</span>
  530. <span id="gm-offsetRightInformation" class="gm-information" title="">💬</span>
  531. <input is="laster2800-input-number" id="gm-offsetRight" value="${gm.configMap.offsetRight.default}" max="100">
  532. </div>`,
  533. }, {
  534. desc: '进度条左侧预留区设置。',
  535. html: `<div>
  536. <span>进度条左侧预留区</span>
  537. <span id="gm-reservedLeftInformation" class="gm-information" title="">💬</span>
  538. <input is="laster2800-input-number" id="gm-reservedLeft" value="${gm.configMap.reservedLeft.default}" max="100">
  539. </div>`,
  540. }, {
  541. desc: '进度条右侧预留区设置。',
  542. html: `<div>
  543. <span>进度条右侧预留区</span>
  544. <span id="gm-reservedRightInformation" class="gm-information" title="">💬</span>
  545. <input is="laster2800-input-number" id="gm-reservedRight" value="${gm.configMap.reservedRight.default}" max="100">
  546. </div>`,
  547. }, {
  548. desc: '是否延后进度条偏移的时间点,使得在启用功能或改变播放进度后立即进行进度条偏移?',
  549. html: `<label>
  550. <span>延后进度条偏移的时间点</span>
  551. <span id="gm-postponeOffsetInformation" class="gm-information" title="">💬</span>
  552. <input id="gm-postponeOffset" type="checkbox">
  553. </label>`,
  554. })
  555. itemsHTML += getItemHTML('用户设置', {
  556. desc: '如果更改的配置需要重新加载才能生效,那么在设置完成后重新加载页面。',
  557. html: `<label>
  558. <span>必要时在设置完成后重新加载页面</span>
  559. <input id="gm-reloadAfterSetting" type="checkbox">
  560. </label>`,
  561. })
  562.  
  563. gm.el.setting.innerHTML = `
  564. <div class="gm-setting-page gm-modal">
  565. <div class="gm-title">
  566. <a class="gm-maintitle" title="${GM_info.script.homepage}" href="${GM_info.script.homepage}" target="_blank">
  567. <span>${GM_info.script.name}</span>
  568. </a>
  569. <div class="gm-subtitle">V${GM_info.script.version} by ${GM_info.script.author}</div>
  570. </div>
  571. <div class="gm-items">${itemsHTML}</div>
  572. <div class="gm-bottom">
  573. <button class="gm-save">保存</button>
  574. <button class="gm-cancel">取消</button>
  575. </div>
  576. <div class="gm-reset" title="重置脚本设置及内部数据(防剧透UP主名单除外),也许能解决脚本运行错误的问题。无法解决请联系脚本作者:${GM_info.script.supportURL}">初始化脚本</div>
  577. <a class="gm-changelog" title="显示更新日志" href="${gm.url.gm_changelog}" target="_blank">更新日志</a>
  578. </div>
  579. <div class="gm-shadow"></div>
  580. `
  581.  
  582. // 找出配置对应的元素
  583. for (const name of Object.keys({ ...gm.configMap, ...gm.infoMap })) {
  584. el[name] = gm.el.setting.querySelector(`#gm-${name}`)
  585. }
  586.  
  587. el.settingPage = gm.el.setting.querySelector('.gm-setting-page')
  588. el.maintitle = gm.el.setting.querySelector('.gm-maintitle')
  589. el.changelog = gm.el.setting.querySelector('.gm-changelog')
  590. switch (type) {
  591. case 1:
  592. el.settingPage.dataset.type = 'init'
  593. el.maintitle.innerHTML += '<br><span style="font-size:0.8em">(初始化设置)</span>'
  594. break
  595. case 2:
  596. el.settingPage.dataset.type = 'updated'
  597. el.maintitle.innerHTML += '<br><span style="font-size:0.8em">(功能性更新设置)</span>'
  598. for (const [name, item] of Object.entries({ ...gm.configMap, ...gm.infoMap })) {
  599. if (item.configVersion > gm.configVersion) {
  600. const updated = api.dom.findAncestor(el[name], el => el.classList.contains('gm-item'))
  601. updated?.classList.add('gm-updated')
  602. }
  603. }
  604. break
  605. default:
  606. break
  607. }
  608. el.save = gm.el.setting.querySelector('.gm-save')
  609. el.cancel = gm.el.setting.querySelector('.gm-cancel')
  610. el.shadow = gm.el.setting.querySelector('.gm-shadow')
  611. el.reset = gm.el.setting.querySelector('.gm-reset')
  612.  
  613. // 提示信息
  614. el.offsetTransformFactorInformation = gm.el.setting.querySelector('#gm-offsetTransformFactorInformation')
  615. api.message.hoverInfo(el.offsetTransformFactorInformation, `
  616. <style>
  617. .${gm.id}-infobox ul > li {
  618. list-style: disc;
  619. margin-left: 1em;
  620. }
  621. </style>
  622. <div style="line-height:1.6em">
  623. <div>进度条极端偏移因子(范围:0.00 ~ 5.00),用于控制进度条偏移量的概率分布。更多信息请阅读说明文档。</div>
  624. <ul>
  625. <li>因子的值越小,则出现极限偏移的概率越高。最小可取值为 <b>0</b>,此时偏移值必定为极左值或极右值。</li>
  626. <li>因子的值越大,则出现极限偏移的概率越低,偏移值趋向于 0。无理论上限,但实际取值达到 3 效果就已经非常明显,限制最大值为 5。</li>
  627. <li>因子取值为 <b>1</b> 时,偏移量的概率会在整个区间平滑分布。</li>
  628. </ul>
  629. </div>
  630. `, null, { width: '36em', flagSize: '2em', position: { top: '80%' } })
  631. el.offsetLeftInformation = gm.el.setting.querySelector('#gm-offsetLeftInformation')
  632. api.message.hoverInfo(el.offsetLeftInformation, `
  633. <div style="line-height:1.6em">
  634. 极限情况下进度条向左偏移的距离(百分比),该选项用于解决进度条后向剧透问题。设置为 <b>0</b> 可以禁止进度条左偏。更多信息请阅读说明文档。
  635. </div>
  636. `, null, { width: '36em', flagSize: '2em' })
  637. el.offsetRightInformation = gm.el.setting.querySelector('#gm-offsetRightInformation')
  638. api.message.hoverInfo(el.offsetRightInformation, `
  639. <div style="line-height:1.6em">
  640. 极限情况下进度条向右偏移的距离(百分比),该选项用于解决进度条前向剧透问题。设置为 <b>0</b> 可以禁止进度条右偏。更多信息请阅读说明文档。
  641. </div>
  642. `, null, { width: '36em', flagSize: '2em' })
  643. el.reservedLeftInformation = gm.el.setting.querySelector('#gm-reservedLeftInformation')
  644. api.message.hoverInfo(el.reservedLeftInformation, `
  645. <div style="line-height:1.6em">
  646. 进度条左侧预留区间大小(百分比)。若进度条向左偏移后导致滑块进入区间,则调整偏移量使得滑块位于区间最右侧(特别地,若播放进度比偏移量小则不偏移)。该选项是为了保证在任何情况下都能通过点击滑块左侧区域向前调整进度。更多信息请阅读说明文档。
  647. </div>
  648. `, null, { width: '36em', flagSize: '2em' })
  649. el.reservedRightInformation = gm.el.setting.querySelector('#gm-reservedRightInformation')
  650. api.message.hoverInfo(el.reservedRightInformation, `
  651. <div style="line-height:1.6em">
  652. 进度条右侧预留区间大小(百分比)。若进度条向右偏移后导致滑块进入区间,则调整偏移量使得滑块位于区间最左侧。该选项是为了保证在任何情况下都能通过点击滑块右侧区域向后调整进度。更多信息请阅读说明文档。
  653. </div>
  654. `, null, { width: '36em', flagSize: '2em' })
  655. el.postponeOffsetInformation = gm.el.setting.querySelector('#gm-postponeOffsetInformation')
  656. api.message.hoverInfo(el.postponeOffsetInformation, `
  657. <div style="line-height:1.6em">
  658. 在启用功能或改变播放进度后,不要立即对进度条进行偏移,而是在下次进度条显示出来时偏移。这样可以避免用户观察到处理过程,从而防止用户推测出偏移方向与偏移量。更多信息请阅读说明文档。
  659. </div>
  660. `, null, { width: '36em', flagSize: '2em' })
  661. }
  662.  
  663. /**
  664. * 处理与设置页相关的数据和元素
  665. */
  666. const processSettingItem = () => {
  667. gm.panel.setting.openHandler = onOpen
  668. gm.el.setting.fadeInDisplay = 'flex'
  669. el.save.addEventListener('click', onSave)
  670. el.cancel.addEventListener('click', () => this.closePanelItem('setting'))
  671. el.shadow.addEventListener('click', () => {
  672. if (!el.shadow.hasAttribute('disabled')) {
  673. this.closePanelItem('setting')
  674. }
  675. })
  676. el.reset.addEventListener('click', () => this.resetScript())
  677. el.resetParam.addEventListener('click', () => {
  678. el.offsetTransformFactor.value = gm.configMap.offsetTransformFactor.default
  679. el.offsetLeft.value = gm.configMap.offsetLeft.default
  680. el.offsetRight.value = gm.configMap.offsetRight.default
  681. el.reservedLeft.value = gm.configMap.reservedLeft.default
  682. el.reservedRight.value = gm.configMap.reservedRight.default
  683. el.postponeOffset.checked = gm.configMap.postponeOffset.default
  684. })
  685. el.uploaderList.addEventListener('click', () => this.openUploaderList())
  686. if (type > 0) {
  687. el.cancel.disabled = true
  688. el.shadow.setAttribute('disabled', '')
  689. }
  690. }
  691.  
  692. let needReload = false
  693. /**
  694. * 设置保存时执行
  695. */
  696. const onSave = () => {
  697. // 通用处理
  698. for (const [name, item] of Object.entries(gm.configMap)) {
  699. if (!item.manual) {
  700. const change = saveConfig(name, item.attr)
  701. if (!item.needNotReload) {
  702. needReload ||= change
  703. }
  704. }
  705. }
  706.  
  707. this.closePanelItem('setting')
  708. if (type > 0) {
  709. // 更新配置版本
  710. gm.configVersion = gm.configUpdate
  711. GM_setValue('configVersion', gm.configVersion)
  712. // 关闭特殊状态
  713. setTimeout(() => {
  714. delete el.settingPage.dataset.type
  715. el.maintitle.textContent = GM_info.script.name
  716. el.cancel.disabled = false
  717. el.shadow.removeAttribute('disabled')
  718. }, gm.const.fadeTime)
  719. }
  720.  
  721. if (gm.config.reloadAfterSetting && needReload) {
  722. needReload = false
  723. location.reload()
  724. }
  725. }
  726.  
  727. /**
  728. * 设置打开时执行
  729. */
  730. const onOpen = () => {
  731. for (const [name, item] of Object.entries(gm.configMap)) {
  732. const { attr } = item
  733. el[name][attr] = gm.config[name]
  734. }
  735. for (const name of Object.keys(gm.configMap)) {
  736. // 需要等所有配置读取完成后再进行选项初始化
  737. el[name].init?.()
  738. }
  739. }
  740.  
  741. /**
  742. * 保存配置
  743. * @param {string} name 配置名称
  744. * @param {string} attr 从对应元素的什么属性读取
  745. * @returns {boolean} 是否有实际更新
  746. */
  747. const saveConfig = (name, attr) => {
  748. let val = el[name][attr]
  749. const { type } = gm.configMap[name]
  750. if (type === 'int' || type === 'float') {
  751. if (typeof val !== 'number') {
  752. val = type === 'int' ? Number.parseInt(val) : Number.parseFloat(val)
  753. }
  754. if (Number.isNaN(val)) {
  755. val = gm.configMap[name].default
  756. }
  757. }
  758. if (gm.config[name] !== val) {
  759. gm.config[name] = val
  760. GM_setValue(name, gm.config[name])
  761. return true
  762. }
  763. return false
  764. }
  765. }
  766. }
  767.  
  768. /**
  769. * 打开防剧透UP主名单
  770. */
  771. openUploaderList() {
  772. const dialog = api.message.dialog(`
  773. <div style="color:var(--${gm.id}-hint-text-color);font-size:0.8em;text-indent:2em;line-height:1.6em">
  774. 当打开名单内UP主的视频时,会自动启用防剧透进度条。在下方文本框内填入UP主的 UID,其中 UID 可在UP主的个人空间中找到。每行必须以 UID 开头,UID 后可以用空格隔开进行注释。<b>第一行以&nbsp;&nbsp;*&nbsp;&nbsp;开头</b>时,匹配所有UP主。<span id="gm-uploader-list-example" class="gm-info">点击填充示例。</span>
  775. </div>
  776. `, {
  777. html: true,
  778. title: '防剧透UP主名单',
  779. boxInput: true,
  780. buttons: ['保存', '取消'],
  781. width: '28em',
  782. })
  783. const [list, save, cancel] = dialog.interactives
  784. const example = dialog.querySelector('#gm-uploader-list-example')
  785.  
  786. list.style.height = '15em'
  787. list.value = gm.data.uploaderList()
  788. save.addEventListener('click', () => {
  789. gm.data.uploaderList(list.value)
  790. api.message.info('防剧透UP主名单保存成功')
  791. dialog.close()
  792. })
  793. cancel.addEventListener('click', () => dialog.close())
  794. example.addEventListener('click', () => {
  795. list.value = '# 非 UID 起始的行不会影响名单读取\n204335848 # 皇室战争电竞频道\n50329118 # 哔哩哔哩英雄联盟赛事'
  796. })
  797. dialog.open()
  798. }
  799.  
  800. /**
  801. * 初始化脚本
  802. */
  803. async resetScript() {
  804. const result = await api.message.confirm('是否要初始化脚本?本操作不会重置「防剧透UP主名单」。')
  805. if (result) {
  806. const keyNoReset = { uploaderList: true }
  807. const gmKeys = GM_listValues()
  808. for (const gmKey of gmKeys) {
  809. if (!keyNoReset[gmKey]) {
  810. GM_deleteValue(gmKey)
  811. }
  812. }
  813. gm.configVersion = 0
  814. GM_setValue('configVersion', gm.configVersion)
  815. location.reload()
  816. }
  817. }
  818.  
  819. /**
  820. * 打开面板项
  821. * @param {string} name 面板项名称
  822. * @param {(panel: GMObject_panel_item) => void} [callback] 打开面板项后的回调函数
  823. * @param {boolean} [keepOthers] 打开时保留其他面板项
  824. * @returns {Promise<boolean>} 操作是否成功
  825. */
  826. async openPanelItem(name, callback, keepOthers) {
  827. let success = false
  828. /** @type {GMObject_panel_item} */
  829. const panel = gm.panel[name]
  830. if (panel.wait > 0) return false
  831. try {
  832. try {
  833. if (panel.state === 1) {
  834. panel.wait = 1
  835. await api.wait.waitForConditionPassed({
  836. condition: () => panel.state === 2,
  837. timeout: 1500 + (panel.el.fadeInTime ?? gm.const.fadeTime),
  838. })
  839. return true
  840. } else if (panel.state === 3) {
  841. panel.wait = 1
  842. await api.wait.waitForConditionPassed({
  843. condition: () => panel.state === 0,
  844. timeout: 1500 + (panel.el.fadeOutTime ?? gm.const.fadeTime),
  845. })
  846. }
  847. } catch (e) {
  848. panel.state = -1
  849. api.logger.error(e)
  850. } finally {
  851. panel.wait = 0
  852. }
  853. if (panel.state === 0 || panel.state === -1) {
  854. panel.state = 1
  855. if (!keepOthers) {
  856. for (const [key, curr] of Object.entries(gm.panel)) {
  857. if (key === name || curr.state === 0) continue
  858. this.closePanelItem(key)
  859. }
  860. }
  861. await panel.openHandler?.()
  862. await new Promise(resolve => {
  863. api.dom.fade(true, panel.el, () => {
  864. resolve()
  865. panel.openedHandler?.()
  866. callback?.(panel)
  867. })
  868. })
  869. panel.state = 2
  870. success = true
  871. }
  872. if (success && document.fullscreenElement) {
  873. document.exitFullscreen()
  874. }
  875. } catch (e) {
  876. panel.state = -1
  877. api.logger.error(e)
  878. }
  879. return success
  880. }
  881.  
  882. /**
  883. * 关闭面板项
  884. * @param {string} name 面板项名称
  885. * @param {(panel: GMObject_panel_item) => void} [callback] 关闭面板项后的回调函数
  886. * @returns {Promise<boolean>} 操作是否成功
  887. */
  888. async closePanelItem(name, callback) {
  889. /** @type {GMObject_panel_item} */
  890. const panel = gm.panel[name]
  891. if (panel.wait > 0) return
  892. try {
  893. try {
  894. if (panel.state === 1) {
  895. panel.wait = 2
  896. await api.wait.waitForConditionPassed({
  897. condition: () => panel.state === 2,
  898. timeout: 1500 + (panel.el.fadeInTime ?? gm.const.fadeTime),
  899. })
  900. } else if (panel.state === 3) {
  901. panel.wait = 2
  902. await api.wait.waitForConditionPassed({
  903. condition: () => panel.state === 0,
  904. timeout: 1500 + (panel.el.fadeOutTime ?? gm.const.fadeTime),
  905. })
  906. return true
  907. }
  908. } catch (e) {
  909. panel.state = -1
  910. api.logger.error(e)
  911. } finally {
  912. panel.wait = 0
  913. }
  914. if (panel.state === 2 || panel.state === -1) {
  915. panel.state = 3
  916. await panel.closeHandler?.()
  917. await new Promise(resolve => {
  918. api.dom.fade(false, panel.el, () => {
  919. resolve()
  920. panel.closedHandler?.()
  921. callback?.(panel)
  922. })
  923. })
  924. panel.state = 0
  925. return true
  926. }
  927. } catch (e) {
  928. panel.state = -1
  929. api.logger.error(e)
  930. }
  931. return false
  932. }
  933. }
  934.  
  935. /**
  936. * 页面处理的抽象,脚本围绕网站的特化部分
  937. */
  938. class Webpage {
  939. /**
  940. * 播放控制
  941. * @type {HTMLElement}
  942. */
  943. control = null
  944. /**
  945. * 播放控制面板
  946. * @type {HTMLElement}
  947. */
  948. controlPanel = null
  949. /**
  950. * 进度条
  951. * @typedef ProgressBar
  952. * @property {HTMLElement} root 进度条根元素
  953. * @property {HTMLElement} thumb 进度条滑块
  954. * @property {HTMLElement} preview 进度条预览
  955. * @property {HTMLElement[]} dispEl 进度条中应该被隐藏的可视部分
  956. */
  957. /**
  958. * 进度条
  959. * @type {ProgressBar}
  960. */
  961. progress = {}
  962. /**
  963. * 伪进度条
  964. * @typedef FakeProgressBar
  965. * @property {HTMLElement} root 伪进度条根元素
  966. * @property {HTMLElement} track 伪进度条滑槽
  967. * @property {HTMLElement} played 伪进度条已播放部分
  968. */
  969. /**
  970. * 伪进度条
  971. * @type {FakeProgressBar}
  972. */
  973. fakeProgress = {}
  974.  
  975. /**
  976. * 脚本控制条
  977. * @type {HTMLElement}
  978. */
  979. scriptControl = null
  980.  
  981. /**
  982. * 是否开启防剧透功能
  983. * @type {boolean}
  984. */
  985. enabled = false
  986. /**
  987. * 当前UP主是否在防剧透名单中
  988. */
  989. uploaderEnabled = false
  990.  
  991. /** 通用方法 */
  992. method = {
  993. /** @type {Webpage} */
  994. obj: null,
  995.  
  996. /**
  997. * 判断播放器是否为 V3
  998. * @returns {boolean} 播放器是否为 V3
  999. */
  1000. isV3Player() {
  1001. return Boolean(document.querySelector('.bpx-player-video-area'))
  1002. },
  1003.  
  1004. /**
  1005. * 判断播放器是否启用分段进度条
  1006. * @returns {boolean} 播放器是否启用分段进度条
  1007. */
  1008. isSegmentedProgress() {
  1009. return Boolean(document.querySelector('.bilibili-player-video-btn-viewpointlist'))
  1010. },
  1011.  
  1012. /**
  1013. * 从 URL 获取视频 ID
  1014. * @param {string} [url=location.pathname] 提取视频 ID 的源字符串
  1015. * @returns {{id: string, type: 'aid' | 'bvid'}} `{id, type}`
  1016. */
  1017. getVid(url = location.pathname) {
  1018. let m = null
  1019. if ((m = /\/bv([\da-z]+)([#/?]|$)/i.exec(url))) {
  1020. return { id: 'BV' + m[1], type: 'bvid' }
  1021. } else if ((m = /\/(av)?(\d+)([#/?]|$)/i.exec(url))) { // 兼容 URL 中 BV 号被第三方修改为 AV 号的情况
  1022. return { id: m[2], type: 'aid' }
  1023. }
  1024. return null
  1025. },
  1026.  
  1027. /**
  1028. * 获取视频信息
  1029. * @param {string} id `aid` 或 `bvid`
  1030. * @param {'aid' | 'bvid'} [type='bvid'] `id` 类型
  1031. * @returns {Promise<JSON>} 视频信息
  1032. */
  1033. async getVideoInfo(id, type = 'bvid') {
  1034. const resp = await api.web.request({
  1035. url: gm.url.api_videoInfo(id, type),
  1036. }, { check: r => r.code === 0 })
  1037. return resp.data
  1038. },
  1039.  
  1040. /**
  1041. * 获取当前播放时间
  1042. * @returns {number} 当前播放时间(单位:秒)
  1043. */
  1044. getCurrentTime() {
  1045. const el = this.obj.control.querySelector('.bilibili-player-video-time-now, .squirtle-video-time-now')
  1046. return this.getTimeFromElement(el)
  1047. },
  1048.  
  1049. /**
  1050. * 获取视频时长
  1051. * @returns {number} 视频时长(单位:秒)
  1052. */
  1053. getDuration() {
  1054. const el = this.obj.control.querySelector('.bilibili-player-video-time-total, .squirtle-video-time-total')
  1055. return this.getTimeFromElement(el)
  1056. },
  1057.  
  1058. /**
  1059. * 从元素中提取时间
  1060. * @param {HTMLElement} el 元素
  1061. * @returns {number} 时间(单位:秒)
  1062. */
  1063. getTimeFromElement(el) {
  1064. let result = 0
  1065. const factors = [24 * 3600, 3600, 60, 1]
  1066. const parts = el.textContent.split(':')
  1067. while (parts.length > 0) {
  1068. result += parts.pop() * factors.pop()
  1069. }
  1070. return result
  1071. },
  1072. }
  1073.  
  1074. constructor() {
  1075. this.method.obj = this
  1076. }
  1077.  
  1078. /**
  1079. * 初始化页面内容
  1080. */
  1081. async initWebpage() {
  1082. const selector = {
  1083. control: '.bilibili-player-video-control, .squirtle-controller',
  1084. controlPanel: '.bilibili-player-video-control-bottom, .squirtle-controller-wrap',
  1085. progressRoot: '.bilibili-player-video-progress, .squirtle-progress-wrap',
  1086. }
  1087. this.control = await api.wait.$(selector.control)
  1088. this.controlPanel = await api.wait.$(selector.controlPanel, this.control)
  1089. this.progress.root = await api.wait.$(selector.progressRoot, this.control)
  1090. this.initScriptControl()
  1091. }
  1092.  
  1093. /**
  1094. * 初始化进度条
  1095. */
  1096. async initProgress() {
  1097. const segmented = this.method.isSegmentedProgress() // 目前还没出现 V3 的分段进度条
  1098. const selector = {
  1099. thumb: segmented
  1100. ? '.bilibili-player-video-segmentation-progress-slider .bui-thumb'
  1101. : '.bilibili-player-video-progress .bui-thumb, .squirtle-progress-dot',
  1102. preview: '.bilibili-player-video-progress-detail, .squirtle-progress-detail',
  1103. }
  1104. if (this.method.isV3Player()) {
  1105. selector.dispEl = [
  1106. '.squirtle-progress-totalline', // 进度条背景
  1107. '.squirtle-progress-timeline', // 已播放条
  1108. '.squirtle-progress-buffer', // 缓冲条
  1109. ]
  1110. } else {
  1111. if (segmented) {
  1112. selector.dispEl = [
  1113. '/* <select-all> */.bilibili-player-video-segmentation-progress-slider .bui-bar-wrap.bui-segmented', // 各分段可视部分
  1114. '.bilibili-player-video-progress-shadow.segmented', // 影子进度条
  1115. ]
  1116. } else {
  1117. selector.dispEl = [
  1118. '.bilibili-player-video-progress .bui-bar-wrap, .bilibili-player-video-progress .bui-schedule-wrap', // 进度条可视部分
  1119. '.bilibili-player-video-progress-shadow', // 影子进度条
  1120. ]
  1121. }
  1122. }
  1123.  
  1124. this.progress.thumb = await api.wait.$(selector.thumb, this.control)
  1125. this.progress.preview = await api.wait.$(selector.preview, this.control)
  1126. this.progress.dispEl = []
  1127. for (const elSelector of selector.dispEl) {
  1128. if (elSelector.includes('<select-all>')) {
  1129. await api.wait.$(elSelector, this.control)
  1130. for (const el of this.control.querySelectorAll(elSelector)) {
  1131. this.progress.dispEl.push(el)
  1132. }
  1133. } else {
  1134. this.progress.dispEl.push(await api.wait.$(elSelector, this.control))
  1135. }
  1136. }
  1137.  
  1138. if (!this.control.contains(this.fakeProgress.root)) {
  1139. this.fakeProgress.root = this.progress.root.insertAdjacentElement('beforebegin', document.createElement('div'))
  1140. this.fakeProgress.root.id = `${gm.id}-fake-progress`
  1141. if (this.method.isV3Player()) {
  1142. this.fakeProgress.root.dataset.mode = 'v3'
  1143. } else if (this.control.querySelector('.bilibili-player-video-progress .bui-schedule-wrap')) {
  1144. this.fakeProgress.root.dataset.mode = 'v2-type2'
  1145. }
  1146. this.fakeProgress.root.innerHTML = `
  1147. <div class='fake-track'></div>
  1148. <div class='fake-played'></div>
  1149. `
  1150. this.fakeProgress.track = this.fakeProgress.root.children[0]
  1151. this.fakeProgress.played = this.fakeProgress.root.children[1]
  1152. }
  1153.  
  1154. if (!this.progress.thumb._replaceDetect) {
  1155. // 有些播放页面,自动跳转到上次播放进度时,thumb 被会被替换成新的
  1156. // 似乎最多只会变一次,暂时就只处理一次
  1157. api.wait.executeAfterElementLoaded({
  1158. selector: selector.thumb,
  1159. base: this.progress.root,
  1160. exclude: [this.progress.thumb],
  1161. onTimeout: null,
  1162. callback: thumb => {
  1163. this.progress.thumb = thumb
  1164. },
  1165. })
  1166. this.progress.thumb._replaceDetect = true
  1167. }
  1168. }
  1169.  
  1170. /**
  1171. * 判断当前页面时是否自动启用功能
  1172. * @returns {Promise<boolean>} 当前页面时是否自动启用功能
  1173. */
  1174. async detectEnabled() {
  1175. if (api.base.urlMatch([gm.regex.page_videoNormalMode, gm.regex.page_videoWatchlaterMode])) {
  1176. try {
  1177. const ulSet = gm.data.uploaderListSet()
  1178. if (ulSet.has('*')) {
  1179. return true
  1180. }
  1181. const vid = this.method.getVid()
  1182. const videoInfo = await this.method.getVideoInfo(vid.id, vid.type)
  1183. const uid = String(videoInfo.owner.mid)
  1184. if (ulSet.has(uid)) {
  1185. this.uploaderEnabled = true
  1186. return true
  1187. }
  1188. } catch (e) {
  1189. api.logger.error(e)
  1190. }
  1191. } else if (api.base.urlMatch(gm.regex.page_bangumi) && gm.config.bangumiEnabled) {
  1192. return true
  1193. }
  1194. return false
  1195. }
  1196.  
  1197. /**
  1198. * 隐藏必要元素(相关设置修改后需刷新页面)
  1199. */
  1200. hideElementStatic() {
  1201. // 隐藏进度条预览
  1202. if (this.enabled) {
  1203. this.progress.preview.style.visibility = gm.config.disablePreview ? 'hidden' : 'visible'
  1204. } else {
  1205. this.progress.preview.style.visibility = 'visible'
  1206. }
  1207.  
  1208. // 隐藏当前播放时间
  1209. api.wait.$('.bilibili-player-video-time-now:not(.fake), .squirtle-video-time-now:not(.fake)').then(currentPoint => {
  1210. if (this.enabled && gm.config.disableCurrentPoint) {
  1211. if (!currentPoint._fake) {
  1212. currentPoint._fake = currentPoint.insertAdjacentElement('afterend', currentPoint.cloneNode(true))
  1213. currentPoint._fake.textContent = '???'
  1214. currentPoint._fake.classList.add('fake')
  1215. }
  1216. currentPoint.style.display = 'none'
  1217. currentPoint._fake.style.display = 'unset'
  1218. } else {
  1219. currentPoint.style.display = 'unset'
  1220. if (currentPoint._fake) {
  1221. currentPoint._fake.style.display = 'none'
  1222. }
  1223. }
  1224. })
  1225. // 隐藏视频预览上的当前播放时间(鼠标移至进度条上显示)
  1226. api.wait.$('.bilibili-player-video-progress-detail-time, .squirtle-progress-time').then(currentPoint => {
  1227. if (this.enabled && gm.config.disableCurrentPoint) {
  1228. currentPoint.style.visibility = 'hidden'
  1229. } else {
  1230. currentPoint.style.visibility = 'visible'
  1231. }
  1232. })
  1233.  
  1234. // 隐藏视频时长
  1235. api.wait.$('.bilibili-player-video-time-total:not(.fake), .squirtle-video-time-total:not(.fake)').then(duration => {
  1236. if (this.enabled && gm.config.disableDuration) {
  1237. if (!duration._fake) {
  1238. duration._fake = duration.insertAdjacentElement('afterend', duration.cloneNode(true))
  1239. duration._fake.textContent = '???'
  1240. duration._fake.classList.add('fake')
  1241. }
  1242. duration.style.display = 'none'
  1243. duration._fake.style.display = 'unset'
  1244. } else {
  1245. duration.style.display = 'unset'
  1246. if (duration._fake) {
  1247. duration._fake.style.display = 'none'
  1248. }
  1249. }
  1250. })
  1251. // 隐藏进度条自动跳转提示(可能存在)
  1252. api.wait.$('.bilibili-player-video-toast-wrp, .bpx-player-toast-wrap', document, true).then(tip => {
  1253. if (this.enabled) {
  1254. tip.style.display = 'none'
  1255. } else {
  1256. tip.style.display = 'unset'
  1257. }
  1258. }).catch(() => {})
  1259.  
  1260. // 隐藏高能进度条的「热度」曲线(可能存在)
  1261. api.wait.$('#bilibili_pbp', this.control, true).then(pbp => {
  1262. pbp.style.visibility = this.enabled ? 'hidden' : ''
  1263. }).catch(() => {})
  1264.  
  1265. // 隐藏 pakku 扩展引入的弹幕密度显示(可能存在)
  1266. api.wait.$('.pakku-fluctlight', this.control, true).then(pakku => {
  1267. pakku.style.visibility = this.enabled ? 'hidden' : ''
  1268. }).catch(() => {})
  1269.  
  1270. // 隐藏分P信息(番剧没有必要隐藏)
  1271. if (gm.config.disablePartInformation && !api.base.urlMatch(gm.regex.page_bangumi)) {
  1272. // 全屏播放时的分P选择(即使没有分P也存在)
  1273. if (this.enabled) {
  1274. api.wait.$('.bilibili-player-video-btn-menu').then(menu => {
  1275. for (const [idx, item] of menu.querySelectorAll('.bilibili-player-video-btn-menu-list').entries()) {
  1276. item.textContent = `P${idx + 1}`
  1277. }
  1278. })
  1279. }
  1280. // 全屏播放时显示的分P标题
  1281. api.wait.$('.bilibili-player-video-top-title').then(el => {
  1282. el.style.visibility = this.enabled ? 'hidden' : 'visible'
  1283. })
  1284. // 播放页右侧分P选择(可能存在)
  1285. if (api.base.urlMatch(gm.regex.page_videoNormalMode)) {
  1286. api.wait.$('#multi_page', document, true).then(multiPage => {
  1287. for (const el of multiPage.querySelectorAll('.clickitem .part, .clickitem .duration')) {
  1288. el.style.visibility = this.enabled ? 'hidden' : 'visible'
  1289. }
  1290. if (this.enabled) {
  1291. for (const el of multiPage.querySelectorAll('[title]')) {
  1292. el.title = '' // 隐藏提示信息
  1293. }
  1294. }
  1295. }).catch(() => {})
  1296. } else if (api.base.urlMatch(gm.regex.page_videoWatchlaterMode)) {
  1297. api.wait.$('.player-auxiliary-playlist-list').then(list => {
  1298. const exec = () => {
  1299. if (this.enabled) {
  1300. for (const item of list.querySelectorAll('.player-auxiliary-playlist-item-p-item')) {
  1301. const m = /^(p\d+)\D/i.exec(item.textContent)
  1302. if (m) {
  1303. item.textContent = m[1]
  1304. }
  1305. }
  1306. }
  1307. }
  1308. exec()
  1309. if (!list._obHidePart) { // 如果 list 中发生修改,则重新处理
  1310. list._obHidePart = new MutationObserver(exec)
  1311. list._obHidePart.observe(list, { childList: true })
  1312. }
  1313. })
  1314. }
  1315. }
  1316.  
  1317. // 隐藏分段信息
  1318. if (gm.config.disableSegmentInformation && this.method.isSegmentedProgress()) {
  1319. if (!this.method.isV3Player()) {
  1320. // 分段按钮
  1321. api.wait.$('.bilibili-player-video-btn-viewpointlist', this.control).then(btn => {
  1322. btn.style.visibility = this.enabled ? 'hidden' : ''
  1323. })
  1324. // 分段列表
  1325. api.wait.$('.player-auxiliary-collapse-viewpointlist').then(list => {
  1326. list.style.display = 'none' // 一律隐藏即可,用户要看就再点一次分段按钮
  1327. })
  1328. // 进度条预览上的分段标题(必定存在)
  1329. api.wait.$('.bilibili-player-video-progress-detail-content').then(content => {
  1330. content.style.display = this.enabled ? 'none' : ''
  1331. })
  1332. }
  1333. }
  1334. }
  1335.  
  1336. /**
  1337. * 防剧透功能处理流程
  1338. */
  1339. async processNoSpoil() {
  1340. const _self = this
  1341. if (unsafeWindow.player) {
  1342. await api.wait.waitForConditionPassed({
  1343. condition: () => unsafeWindow.player.isInitialized(),
  1344. })
  1345. }
  1346. await this.initProgress()
  1347. this.hideElementStatic()
  1348. processControlShow()
  1349. core()
  1350. if (this.enabled) {
  1351. this.scriptControl.enabled.setAttribute('enabled', '')
  1352. } else {
  1353. this.scriptControl.enabled.removeAttribute('enabled')
  1354. }
  1355.  
  1356. /**
  1357. * 处理视频控制的显隐
  1358. */
  1359. function processControlShow() {
  1360. if (!_self.enabled) return
  1361.  
  1362. const addObserver = target => {
  1363. if (!target._obPlayRate) {
  1364. target._obPlayRate = new MutationObserver(api.base.throttle(() => {
  1365. _self.processFakePlayed()
  1366. }, 500))
  1367. target._obPlayRate.observe(_self.progress.thumb, { attributeFilter: ['style'] })
  1368. }
  1369. }
  1370. if (_self.method.isV3Player()) {
  1371. const panel = _self.controlPanel
  1372. if (!_self.controlPanel._obControlShow) {
  1373. // 切换视频控制显隐时,添加或删除 ob 以控制伪进度条
  1374. panel._obControlShow = new MutationObserver(() => {
  1375. if (panel.style.display !== 'none') {
  1376. if (_self.enabled) {
  1377. _self.fakeProgress.root.style.visibility = 'visible'
  1378. core(true)
  1379. addObserver(panel)
  1380. }
  1381. } else {
  1382. if (_self.enabled) {
  1383. _self.fakeProgress.root.style.visibility = ''
  1384. }
  1385. if (panel._obPlayRate) {
  1386. panel._obPlayRate.disconnect()
  1387. panel._obPlayRate = null
  1388. }
  1389. }
  1390. })
  1391. panel._obControlShow.observe(panel, { attributeFilter: ['style'] })
  1392. }
  1393. if (panel.style.display !== 'none') {
  1394. addObserver(panel)
  1395. }
  1396. } else {
  1397. const clzControlShow = 'video-control-show'
  1398. const playerArea = document.querySelector('.bilibili-player-area')
  1399. if (!playerArea._obControlShow) {
  1400. // 切换视频控制显隐时,添加或删除 ob 以控制伪进度条
  1401. playerArea._obControlShow = new MutationObserver(records => {
  1402. if (records[0].oldValue === playerArea.className) return // 不能去,有个东西一直在原地修改 class……
  1403. const before = new RegExp(String.raw`(^|\s)${clzControlShow}(\s|$)`).test(records[0].oldValue)
  1404. const current = playerArea.classList.contains(clzControlShow)
  1405. if (before !== current) {
  1406. if (current) {
  1407. if (_self.enabled) {
  1408. core(true)
  1409. addObserver(playerArea)
  1410. }
  1411. } else if (playerArea._obPlayRate) {
  1412. playerArea._obPlayRate.disconnect()
  1413. playerArea._obPlayRate = null
  1414. }
  1415. }
  1416. })
  1417. playerArea._obControlShow.observe(playerArea, {
  1418. attributeFilter: ['class'],
  1419. attributeOldValue: true,
  1420. })
  1421. }
  1422. if (playerArea.classList.contains(clzControlShow)) {
  1423. addObserver(playerArea)
  1424. }
  1425. }
  1426. }
  1427.  
  1428. /**
  1429. * 防剧透处理核心流程
  1430. * @param {boolean} [noPostpone] 不延后执行
  1431. */
  1432. function core(noPostpone) {
  1433. let offset = 'offset'
  1434. let playRate = 0
  1435. if (_self.enabled) {
  1436. playRate = _self.method.getCurrentTime() / _self.method.getDuration()
  1437. offset = getEndPoint() - 100
  1438. const { reservedLeft } = gm.config
  1439. const reservedRight = 100 - gm.config.reservedRight
  1440. if (playRate * 100 < reservedLeft) {
  1441. offset = 0
  1442. } else {
  1443. const offsetRate = playRate * 100 + offset
  1444. if (offsetRate < reservedLeft) {
  1445. offset = reservedLeft - playRate * 100
  1446. } else if (offsetRate > reservedRight) {
  1447. offset = reservedRight - playRate * 100
  1448. }
  1449. }
  1450. } else if (_self.progress._noSpoil) {
  1451. offset = 0
  1452. }
  1453.  
  1454. if (typeof offset === 'number') {
  1455. const handler = () => {
  1456. _self.progress.root._offset = offset
  1457. _self.progress.root.style.transform = `translateX(${offset}%)`
  1458. }
  1459.  
  1460. if (_self.enabled) {
  1461. for (const el of _self.progress.dispEl) {
  1462. el.style.visibility = 'hidden'
  1463. }
  1464. if (_self.method.isV3Player()) {
  1465. _self.progress.thumb.parentElement.style.backgroundColor = 'unset'
  1466. }
  1467. _self.fakeProgress.root.style.visibility = 'visible'
  1468.  
  1469. if (noPostpone || !gm.config.postponeOffset) {
  1470. handler()
  1471. } else if (!_self.progress._noSpoil) { // 首次打开
  1472. _self.progress.root._offset = 0
  1473. _self.progress.root.style.transform = 'translateX(0)'
  1474. _self.fakeProgress.played.style.transform = 'scaleX(0)'
  1475. }
  1476. _self.processFakePlayed()
  1477.  
  1478. _self.progress._noSpoil = true
  1479. } else {
  1480. for (const el of _self.progress.dispEl) {
  1481. el.style.visibility = ''
  1482. }
  1483. if (_self.method.isV3Player()) {
  1484. _self.progress.thumb.parentElement.style.backgroundColor = ''
  1485. }
  1486. _self.fakeProgress.root.style.visibility = ''
  1487. handler()
  1488.  
  1489. _self.progress._noSpoil = false
  1490. }
  1491. }
  1492.  
  1493. if (api.base.urlMatch([gm.regex.page_videoNormalMode, gm.regex.page_videoWatchlaterMode])) {
  1494. if (_self.uploaderEnabled) {
  1495. _self.scriptControl.uploaderEnabled.setAttribute('enabled', '')
  1496. } else {
  1497. _self.scriptControl.uploaderEnabled.removeAttribute('enabled')
  1498. }
  1499. }
  1500. if (api.base.urlMatch(gm.regex.page_bangumi)) {
  1501. if (gm.config.bangumiEnabled) {
  1502. _self.scriptControl.bangumiEnabled.setAttribute('enabled', '')
  1503. } else {
  1504. _self.scriptControl.bangumiEnabled.removeAttribute('enabled')
  1505. }
  1506. }
  1507. }
  1508.  
  1509. /**
  1510. * 获取偏移后进度条尾部位置
  1511. * @returns {number} 偏移后进度条尾部位置
  1512. */
  1513. function getEndPoint() {
  1514. if (!_self.progress._noSpoil) {
  1515. _self.progress._fakeRandom = Math.random()
  1516. }
  1517. let r = _self.progress._fakeRandom
  1518. const origin = 100 // 左右分界点
  1519. const left = gm.config.offsetLeft
  1520. const right = gm.config.offsetRight
  1521. const factor = gm.config.offsetTransformFactor
  1522. const mid = left / (left + right) // 概率中点
  1523. if (r <= mid) { // 向左偏移
  1524. r = 1 - r / mid
  1525. r **= factor
  1526. return origin - r * left
  1527. } else { // 向右偏移
  1528. r = (r - mid) / (1 - mid)
  1529. r **= factor
  1530. return origin + r * right
  1531. }
  1532. }
  1533. }
  1534.  
  1535. /**
  1536. * 初始化防剧透功能
  1537. */
  1538. async initNoSpoil() {
  1539. this.uploaderEnabled = false
  1540. this.enabled = await this.detectEnabled()
  1541. await this.initWebpage()
  1542. if (this.enabled) {
  1543. await this.processNoSpoil()
  1544. }
  1545. }
  1546.  
  1547. /**
  1548. * 切换分P、页面内切换视频、播放器刷新等各种情况下,重新初始化防剧透流程
  1549. */
  1550. initSwitch() {
  1551. if (this.method.isV3Player()) {
  1552. // V3 会使用原来的大部分组件,刷一下 static 就行
  1553. window.addEventListener('urlchange', e => {
  1554. if (location.pathname !== e.detail.prev.pathname) {
  1555. // 其实只有 pbp 需要重刷,但是 pbp 来得很晚且不好检测,而且影响也不是很大,稍微延迟一下得了
  1556. setTimeout(() => this.hideElementStatic(), 5000)
  1557. }
  1558. })
  1559. } else {
  1560. // V2 在这些情况下会自动刷新
  1561. if (unsafeWindow.player) {
  1562. unsafeWindow.player.addEventListener('video_destroy', async () => {
  1563. await this.initNoSpoil()
  1564. this.initSwitch()
  1565. })
  1566. } else {
  1567. api.wait.executeAfterElementLoaded({
  1568. selector: '.bilibili-player-video-control',
  1569. exclude: [this.control],
  1570. repeat: true,
  1571. throttleWait: 2000,
  1572. timeout: 0,
  1573. callback: () => this.initNoSpoil(),
  1574. })
  1575. }
  1576. }
  1577. }
  1578.  
  1579. /**
  1580. * 初始化脚本控制条
  1581. */
  1582. initScriptControl() {
  1583. if (!this.controlPanel.contains(this.scriptControl)) {
  1584. this.scriptControl = this.controlPanel.appendChild(document.createElement('div'))
  1585. this.control._scriptControl = this.scriptControl
  1586. this.scriptControl.className = `${gm.id}-scriptControl`
  1587. if (this.method.isV3Player()) {
  1588. this.scriptControl.dataset.mode = 'v3'
  1589. }
  1590. this.scriptControl.innerHTML = `
  1591. <span id="${gm.id}-enabled">防剧透</span>
  1592. <span id="${gm.id}-uploaderEnabled" style="display:none">将UP主加入防剧透名单</span>
  1593. <span id="${gm.id}-bangumiEnabled" style="display:none">番剧自动启用防剧透</span>
  1594. <span id="${gm.id}-setting" style="display:none">设置</span>
  1595. `
  1596. this.scriptControl.enabled = this.scriptControl.querySelector(`#${gm.id}-enabled`)
  1597. this.scriptControl.uploaderEnabled = this.scriptControl.querySelector(`#${gm.id}-uploaderEnabled`)
  1598. this.scriptControl.bangumiEnabled = this.scriptControl.querySelector(`#${gm.id}-bangumiEnabled`)
  1599. this.scriptControl.setting = this.scriptControl.querySelector(`#${gm.id}-setting`)
  1600.  
  1601. this.scriptControl.enabled.addEventListener('click', () => {
  1602. this.enabled = !this.enabled
  1603. this.processNoSpoil()
  1604. })
  1605.  
  1606. if (!gm.config.simpleScriptControl) {
  1607. if (api.base.urlMatch([gm.regex.page_videoNormalMode, gm.regex.page_videoWatchlaterMode])) {
  1608. if (!gm.data.uploaderListSet().has('*')) { // * 匹配所有UP主不显示该按钮
  1609. this.scriptControl.uploaderEnabled.style.display = 'unset'
  1610. this.scriptControl.uploaderEnabled.addEventListener('click', async () => {
  1611. const target = this.scriptControl.uploaderEnabled
  1612. const ulSet = gm.data.uploaderListSet() // 必须每次读取
  1613. const vid = this.method.getVid()
  1614. const videoInfo = await this.method.getVideoInfo(vid.id, vid.type)
  1615. const uid = String(videoInfo.owner.mid)
  1616.  
  1617. this.uploaderEnabled = !this.uploaderEnabled
  1618. if (this.uploaderEnabled) {
  1619. target.setAttribute('enabled', '')
  1620. if (!ulSet.has(uid)) {
  1621. const ul = gm.data.uploaderList()
  1622. gm.data.uploaderList(`${ul}\n${uid} # ${videoInfo.owner.name}`)
  1623. }
  1624. } else {
  1625. target.removeAttribute('enabled')
  1626. if (ulSet.has(uid)) {
  1627. let ul = gm.data.uploaderList()
  1628. ul = ul.replace(new RegExp(String.raw`^${uid}(?=\D|$).*\n?`, 'gm'), '')
  1629. gm.data.uploaderList(ul)
  1630. }
  1631. }
  1632. })
  1633. }
  1634. }
  1635.  
  1636. if (api.base.urlMatch(gm.regex.page_bangumi)) {
  1637. this.scriptControl.bangumiEnabled.style.display = 'unset'
  1638. this.scriptControl.bangumiEnabled.addEventListener('click', () => {
  1639. const target = this.scriptControl.bangumiEnabled
  1640. gm.config.bangumiEnabled = !gm.config.bangumiEnabled
  1641. if (gm.config.bangumiEnabled) {
  1642. target.setAttribute('enabled', '')
  1643. } else {
  1644. target.removeAttribute('enabled')
  1645. }
  1646. GM_setValue('bangumiEnabled', gm.config.bangumiEnabled)
  1647. })
  1648. }
  1649.  
  1650. this.scriptControl.setting.style.display = 'unset'
  1651. this.scriptControl.setting.addEventListener('click', () => script.openUserSetting())
  1652. }
  1653.  
  1654. api.dom.fade(true, this.scriptControl)
  1655. }
  1656.  
  1657. if (!this.progress.root._scriptControlListeners) {
  1658. // 临时将 z-index 调至底层,不要影响信息的显示
  1659. // 不通过样式直接将 z-index 设为最底层,是因为会被 pbp 遮盖导致点击不了
  1660. // 问题的关键在于,B站已经给进度条和 pbp 内所有元素都设定好 z-index,只能用这种奇技淫巧来解决
  1661. this.progress.root.addEventListener('mouseenter', () => {
  1662. this.scriptControl.style.zIndex = '-1'
  1663. })
  1664. this.progress.root.addEventListener('mouseleave', () => {
  1665. this.scriptControl.style.zIndex = ''
  1666. })
  1667. this.progress.root._scriptControlListeners = true
  1668. }
  1669. }
  1670.  
  1671. /**
  1672. * 更新用于模拟已播放进度的伪已播放条
  1673. */
  1674. processFakePlayed() {
  1675. if (!this.enabled) return
  1676. const playRate = this.method.getCurrentTime() / this.method.getDuration()
  1677. let offset = this.progress.root._offset ?? 0
  1678. // 若处于播放进度小于左侧预留区的特殊情况,不要进行处理
  1679. // 注意,一旦离开这种特殊状态,就再也不可能进入该特殊状态了,因为这样反而会暴露信息
  1680. if (offset !== 0) {
  1681. let reservedZone = false
  1682. const offsetPlayRate = offset + playRate * 100
  1683. const { reservedLeft } = gm.config
  1684. const reservedRight = 100 - gm.config.reservedRight
  1685. // 当实际播放进度小于左侧保留区时,不作特殊处理,因为这样反而会暴露信息
  1686. if (offsetPlayRate < reservedLeft) {
  1687. offset += reservedLeft - offsetPlayRate
  1688. reservedZone = true
  1689. } else if (offsetPlayRate > reservedRight) {
  1690. offset -= offsetPlayRate - reservedRight
  1691. reservedZone = true
  1692. }
  1693. if (reservedZone) {
  1694. this.progress.root._offset = offset
  1695. this.progress.root.style.transform = `translateX(${offset}%)`
  1696. }
  1697. }
  1698. this.fakeProgress.played.style.transform = `scaleX(${playRate + offset / 100})`
  1699. }
  1700.  
  1701. /**
  1702. * 添加脚本样式
  1703. */
  1704. addStyle() {
  1705. api.base.addStyle(`
  1706. :root {
  1707. --${gm.id}-progress-track-color: hsla(0, 0%, 100%, .3);
  1708. --${gm.id}-progress-played-color: rgba(35, 173, 229, 1);
  1709. --${gm.id}-control-item-selected-color: #00c7ff;
  1710. --${gm.id}-control-item-shadow-color: #00000080;
  1711. --${gm.id}-text-color: black;
  1712. --${gm.id}-text-bold-color: #3a3a3a;
  1713. --${gm.id}-light-text-color: white;
  1714. --${gm.id}-hint-text-color: gray;
  1715. --${gm.id}-hint-text-hightlight-color: #555555;
  1716. --${gm.id}-background-color: white;
  1717. --${gm.id}-background-hightlight-color: #ebebeb;
  1718. --${gm.id}-update-hightlight-color: #4cff9c;
  1719. --${gm.id}-update-hightlight-hover-color: red;
  1720. --${gm.id}-border-color: black;
  1721. --${gm.id}-shadow-color: #000000bf;
  1722. --${gm.id}-hightlight-color: #0075FF;
  1723. --${gm.id}-important-color: red;
  1724. --${gm.id}-disabled-color: gray;
  1725. --${gm.id}-opacity-fade-transition: opacity ${gm.const.fadeTime}ms ease-in-out;
  1726. --${gm.id}-scrollbar-background-color: transparent;
  1727. --${gm.id}-scrollbar-thumb-color: #0000002b;
  1728. }
  1729.  
  1730. .${gm.id}-scriptControl {
  1731. position: absolute;
  1732. left: 0;
  1733. bottom: 100%;
  1734. color: var(--${gm.id}-light-text-color);
  1735. margin-bottom: 0.3em;
  1736. font-size: 13px;
  1737. z-index: 1; /* 需保证不被 pbp 等元素遮盖 */
  1738. display: flex;
  1739. opacity: 0;
  1740. transition: opacity ${gm.const.fadeTime}ms ease-in-out;
  1741. }
  1742. .mode-fullscreen .${gm.id}-scriptControl,
  1743. .mode-webfullscreen .${gm.id}-scriptControl {
  1744. margin-bottom: 1em;
  1745. }
  1746. .${gm.id}-scriptControl[data-mode=v3] {
  1747. left: 1em;
  1748. margin-bottom: 0.2em;
  1749. }
  1750.  
  1751. .${gm.id}-scriptControl > * {
  1752. cursor: pointer;
  1753. border-radius: 4px;
  1754. padding: 0.3em;
  1755. margin: 0 0.12em;
  1756. background-color: var(--${gm.id}-control-item-shadow-color);
  1757. line-height: 1em;
  1758. opacity: 0.7;
  1759. transition: opacity ease-in-out ${gm.const.fadeTime}ms;
  1760. }
  1761. .${gm.id}-scriptControl > *:hover {
  1762. opacity: 1;
  1763. }
  1764. .${gm.id}-scriptControl > *[enabled] {
  1765. color: var(--${gm.id}-control-item-selected-color);
  1766. }
  1767.  
  1768. #${gm.id} {
  1769. color: var(--${gm.id}-text-color);
  1770. }
  1771. #${gm.id} * {
  1772. box-sizing: content-box;
  1773. }
  1774.  
  1775. #${gm.id} .gm-modal-container {
  1776. display: none;
  1777. position: fixed;
  1778. justify-content: center;
  1779. align-items: center;
  1780. top: 0;
  1781. left: 0;
  1782. width: 100%;
  1783. height: 100%;
  1784. z-index: 1000000;
  1785. font-size: 12px;
  1786. line-height: normal;
  1787. user-select: none;
  1788. opacity: 0;
  1789. transition: var(--${gm.id}-opacity-fade-transition);
  1790. }
  1791.  
  1792. #${gm.id} .gm-modal {
  1793. position: relative;
  1794. background-color: var(--${gm.id}-background-color);
  1795. border-radius: 10px;
  1796. z-index: 1;
  1797. }
  1798.  
  1799. #${gm.id} .gm-setting .gm-setting-page {
  1800. min-width: 42em;
  1801. max-width: 84em;
  1802. padding: 1em 1.4em;
  1803. }
  1804.  
  1805. #${gm.id} .gm-setting .gm-maintitle {
  1806. cursor: pointer;
  1807. color: var(--${gm.id}-text-color);
  1808. }
  1809. #${gm.id} .gm-setting .gm-maintitle:hover {
  1810. color: var(--${gm.id}-hightlight-color);
  1811. }
  1812.  
  1813. #${gm.id} .gm-setting .gm-items {
  1814. position: relative;
  1815. display: flex;
  1816. flex-direction: column;
  1817. gap: 0.2em;
  1818. margin: 0 0.2em;
  1819. padding: 0 1.8em 0 2.2em;
  1820. font-size: 1.2em;
  1821. max-height: 66vh;
  1822. overflow-y: auto;
  1823. }
  1824. #${gm.id} .gm-setting .gm-item-container {
  1825. display: flex;
  1826. gap: 1em;
  1827. }
  1828. #${gm.id} .gm-setting .gm-item-label {
  1829. flex: none;
  1830. font-weight: bold;
  1831. color: var(--${gm.id}-text-bold-color);
  1832. width: 4em;
  1833. margin-top: 0.2em;
  1834. }
  1835. #${gm.id} .gm-setting .gm-item-content {
  1836. display: flex;
  1837. flex-direction: column;
  1838. width: 100%;
  1839. }
  1840. #${gm.id} .gm-setting .gm-item {
  1841. padding: 0.2em;
  1842. border-radius: 2px;
  1843. }
  1844. #${gm.id} .gm-setting .gm-item > * {
  1845. display: flex;
  1846. align-items: center;
  1847. }
  1848. #${gm.id} .gm-setting .gm-item:hover {
  1849. color: var(--${gm.id}-hightlight-color);
  1850. }
  1851.  
  1852. #${gm.id} .gm-setting input[type=checkbox] {
  1853. margin-left: auto;
  1854. }
  1855. #${gm.id} .gm-setting input[is=laster2800-input-number] {
  1856. border-width: 0 0 1px 0;
  1857. width: 2.4em;
  1858. text-align: right;
  1859. padding: 0 0.2em;
  1860. margin-left: auto;
  1861. }
  1862.  
  1863. #${gm.id} .gm-setting .gm-information {
  1864. margin: 0 0.4em;
  1865. cursor: pointer;
  1866. }
  1867.  
  1868. #${gm.id} .gm-bottom {
  1869. margin: 1.4em 2em 1em 2em;
  1870. text-align: center;
  1871. }
  1872.  
  1873. #${gm.id} .gm-bottom button {
  1874. font-size: 1em;
  1875. padding: 0.3em 1em;
  1876. margin: 0 0.8em;
  1877. cursor: pointer;
  1878. background-color: var(--${gm.id}-background-color);
  1879. border: 1px solid var(--${gm.id}-border-color);
  1880. border-radius: 2px;
  1881. }
  1882. #${gm.id} .gm-bottom button:hover {
  1883. background-color: var(--${gm.id}-background-hightlight-color);
  1884. }
  1885. #${gm.id} .gm-bottom button[disabled] {
  1886. border-color: var(--${gm.id}-disabled-color);
  1887. background-color: var(--${gm.id}-background-color);
  1888. }
  1889.  
  1890. #${gm.id} .gm-info,
  1891. .${gm.id}-dialog .gm-info {
  1892. font-size: 0.8em;
  1893. color: var(--${gm.id}-hint-text-color);
  1894. text-decoration: underline;
  1895. padding: 0 0.2em;
  1896. cursor: pointer;
  1897. }
  1898. #${gm.id} .gm-info:hover,
  1899. .${gm.id}-dialog .gm-info:hover {
  1900. color: var(--${gm.id}-important-color);
  1901. }
  1902.  
  1903. #${gm.id} .gm-reset {
  1904. position: absolute;
  1905. right: 0;
  1906. bottom: 0;
  1907. margin: 1em 1.6em;
  1908. color: var(--${gm.id}-hint-text-color);
  1909. cursor: pointer;
  1910. }
  1911.  
  1912. #${gm.id} .gm-changelog {
  1913. position: absolute;
  1914. right: 0;
  1915. bottom: 1.8em;
  1916. margin: 1em 1.6em;
  1917. color: var(--${gm.id}-hint-text-color);
  1918. cursor: pointer;
  1919. }
  1920. #${gm.id} [data-type=updated] .gm-changelog {
  1921. font-weight: bold;
  1922. color: var(--${gm.id}-update-hightlight-hover-color);
  1923. }
  1924. #${gm.id} [data-type=updated] .gm-changelog:hover {
  1925. color: var(--${gm.id}-update-hightlight-hover-color);
  1926. }
  1927. #${gm.id} [data-type=updated] .gm-updated,
  1928. #${gm.id} [data-type=updated] .gm-updated input,
  1929. #${gm.id} [data-type=updated] .gm-updated select {
  1930. background-color: var(--${gm.id}-update-hightlight-color);
  1931. }
  1932. #${gm.id} [data-type=updated] .gm-updated option {
  1933. background-color: var(--${gm.id}-background-color);
  1934. }
  1935. #${gm.id} [data-type=updated] .gm-item.gm-updated:hover {
  1936. color: var(--${gm.id}-update-hightlight-hover-color);
  1937. font-weight: bold;
  1938. }
  1939.  
  1940. #${gm.id} .gm-reset:hover,
  1941. #${gm.id} .gm-changelog:hover {
  1942. color: var(--${gm.id}-hint-text-hightlight-color);
  1943. text-decoration: underline;
  1944. }
  1945.  
  1946. #${gm.id} .gm-title {
  1947. font-size: 1.6em;
  1948. margin: 1.6em 0.8em 0.8em 0.8em;
  1949. text-align: center;
  1950. }
  1951.  
  1952. #${gm.id} .gm-subtitle {
  1953. font-size: 0.4em;
  1954. margin-top: 0.4em;
  1955. }
  1956.  
  1957. #${gm.id} .gm-shadow {
  1958. background-color: var(--${gm.id}-shadow-color);
  1959. position: fixed;
  1960. top: 0%;
  1961. left: 0%;
  1962. width: 100%;
  1963. height: 100%;
  1964. }
  1965. #${gm.id} .gm-shadow[disabled] {
  1966. cursor: unset !important;
  1967. }
  1968.  
  1969. #${gm.id} label {
  1970. cursor: pointer;
  1971. }
  1972.  
  1973. #${gm.id} input,
  1974. #${gm.id} select,
  1975. #${gm.id} button {
  1976. color: var(--${gm.id}-text-color);
  1977. outline: none;
  1978. border-radius: 0;
  1979. appearance: auto; /* 番剧播放页该项被覆盖 */
  1980. }
  1981.  
  1982. #${gm.id} [disabled],
  1983. #${gm.id} [disabled] * {
  1984. cursor: not-allowed !important;
  1985. color: var(--${gm.id}-disabled-color) !important;
  1986. }
  1987.  
  1988. #${gm.id} .gm-setting .gm-items::-webkit-scrollbar {
  1989. width: 6px;
  1990. height: 6px;
  1991. background-color: var(--${gm.id}-scrollbar-background-color);
  1992. }
  1993. #${gm.id} .gm-setting .gm-items::-webkit-scrollbar-thumb {
  1994. border-radius: 3px;
  1995. background-color: var(--${gm.id}-scrollbar-thumb-color);
  1996. }
  1997. #${gm.id} .gm-setting .gm-items::-webkit-scrollbar-corner {
  1998. background-color: var(--${gm.id}-scrollbar-background-color);
  1999. }
  2000.  
  2001. #${gm.id}-fake-progress {
  2002. position: absolute;
  2003. top: 42%;
  2004. left: 0;
  2005. height: 2px;
  2006. width: 100%;
  2007. cursor: pointer;
  2008. visibility: hidden;
  2009. }
  2010. #${gm.id}-fake-progress[data-mode="v2-type2"] {
  2011. top: 64%;
  2012. }
  2013. #${gm.id}-fake-progress[data-mode=v3] {
  2014. top: 13%;
  2015. left: 1.5%;
  2016. height: 4px;
  2017. width: 97%;
  2018. }
  2019. [data-screen=full] #${gm.id}-fake-progress[data-mode=v3],
  2020. [data-screen=web] #${gm.id}-fake-progress[data-mode=v3],
  2021. [data-screen=wide] #${gm.id}-fake-progress[data-mode=v3] {
  2022. top: 8%;
  2023. left: 0.8%;
  2024. width: 98.4%;
  2025. }
  2026. #${gm.id}-fake-progress > * {
  2027. position: absolute;
  2028. top: 0;
  2029. left: 0;
  2030. height: 100%;
  2031. width: 100%
  2032. }
  2033. #${gm.id}-fake-progress .fake-track {
  2034. background-color: var(--${gm.id}-progress-track-color);
  2035. }
  2036. #${gm.id}-fake-progress .fake-played {
  2037. background-color: var(--${gm.id}-progress-played-color);
  2038. transform-origin: left;
  2039. transform: scaleX(0);
  2040. }
  2041.  
  2042. /* 隐藏番剧中的进度条自动跳转提示(该提示出现太快,常规方式处理不及,这里先用样式覆盖一下) */
  2043. .bpx-player-toast-wrap {
  2044. display: none;
  2045. }
  2046. `)
  2047. }
  2048. }
  2049.  
  2050. document.readyState !== 'complete' ? window.addEventListener('load', main) : main()
  2051.  
  2052. function main() {
  2053. script = new Script()
  2054. webpage = new Webpage()
  2055.  
  2056. script.init()
  2057. script.addScriptMenu()
  2058. webpage.addStyle()
  2059. api.base.initUrlchangeEvent()
  2060.  
  2061. api.wait.waitForConditionPassed({
  2062. condition: () => webpage.method.getVid() !== null,
  2063. interval: 500,
  2064. }).then(() => webpage.initNoSpoil())
  2065. .then(() => webpage.initSwitch())
  2066. }
  2067. })()