// ==UserScript== // @name B站防剧透进度条 // @version 1.2.3.20200912 // @namespace laster2800 // @author Laster2800 // @description 看比赛、看番总是被进度条剧透?装上这个脚本再也不用担心这些问题了 // @icon https://www.bilibili.com/favicon.ico // @homepage https://greasyfork.org/zh-CN/scripts/411092 // @supportURL https://greasyfork.org/zh-CN/scripts/411092/feedback // @license LGPL-3.0 // @include *://www.bilibili.com/video/* // @include *://www.bilibili.com/medialist/play/watchlater // @include *://www.bilibili.com/medialist/play/watchlater/* // @include *://www.bilibili.com/bangumi/play/* // @require https://greasyfork.org/scripts/409641-api/code/API.js?version=846937 // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @connect api.bilibili.com // @downloadURL none // ==/UserScript== (function() { 'use strict' /** * 脚本内用到的枚举定义 */ const Enums = {} /** * 全局对象 * @typedef GMObject * @property {string} id 脚本标识 * @property {number} configVersion 配置版本,为最后一次执行初始化设置或功能性更新设置时脚本对应的配置版本号 * @property {number} configUpdate 当前版本对应的配置版本号,只要涉及到配置的修改都要更新;若同一天修改多次,可以追加小数来区分 * @property {GMObject_config} config 用户配置 * @property {GMObject_configMap} configMap 用户配置属性 * @property {GMObject_data} data 脚本数据 * @property {GMObject_url} url URL * @property {GMObject_regex} regex 正则表达式 * @property {GMObject_const} const 常量 * @property {GMObject_menu} menu 菜单 * @property {{[s: string]: HTMLElement}} el HTML 元素 * @property {GMObject_error} error 错误信息 */ /** * @typedef GMObject_config * @property {boolean} bangumiEnabled 番剧自动启用功能 * @property {boolean} simpleScriptControl 是否简化进度条上方的脚本控制 * @property {boolean} disableCurrentPoint 隐藏当前播放时间 * @property {boolean} disableDuration 隐藏视频时长 * @property {boolean} disablePbp 隐藏【热度】曲线 * @property {boolean} disablePreview 隐藏进度条预览 * @property {number} offsetTransformFactor 进度条极端偏移因子 * @property {number} offsetLeft 进度条偏移极左值 * @property {number} offsetRight 进度条偏移极右值 * @property {number} reservedLeft 进度条左侧预留区 * @property {number} reservedRight 进度条右侧预留区 * @property {boolean} postponeOffset 延后进度条偏移的时间点 * @property {boolean} openSettingAfterConfigUpdate 功能性更新后打开设置页面 * @property {boolean} reloadAfterSetting 设置生效后刷新页面 */ /** * @typedef {{[config: string]: GMObject_configMap_item}} GMObject_configMap */ /** * @typedef GMObject_configMap_item * @property {'checked' | 'value'} attr 对应 `DOM` 节点上的属性 * @property {boolean} [manual] 配置保存时是否需要手动处理 * @property {boolean} [needNotReload] 配置改变后是否不需要重新加载就能生效 * @property {number} [configVersion] 涉及配置更改的最后配置版本 */ /** * @callback uploaderList 不传入/传入参数时获取/修改防剧透 UP 主名单 * @param {string} [updateData] 更新数据 * @returns {string} 防剧透 UP 主名单 */ /** * @callback uploaderListSet 通过懒加载方式获取格式化的防剧透 UP 主名单 * @param {boolean} [reload] 是否重新加载数据 * @returns {Set} 防剧透 UP 主名单 */ /** * @typedef GMObject_data * @property {uploaderList} uploaderList 防剧透 UP 主名单 * @property {uploaderListSet} uploaderListSet 防剧透 UP 主名单集合 */ /** * @callback api_videoInfo * @param {string} id `aid` 或 `bvid` * @param {'aid' | 'bvid'} type `id` 类型 * @returns {string} 查询视频信息的 URL */ /** * @typedef GMObject_url * @property {api_videoInfo} api_videoInfo 视频信息 * @property {string} gm_readme 说明文档 * @property {string} gm_changelog 更新日志 * @property {string} noop 无操作 */ /** * @typedef GMObject_regex * @property {RegExp} page_videoNormalMode 匹配正常模式播放页 * @property {RegExp} page_videoWatchlaterMode 匹配稍后再看模式播放页 * @property {RegExp} page_bangumi 匹配番剧播放页 */ /** * @typedef GMObject_const * @property {number} defaultOffsetTransformFactor 进度条极端偏移因子 * @property {number} defaultOffsetLeft 结束点进度条滑块最小位置的默认值 * @property {number} defaultOffsetRight 结束点进度条滑块最大位置的默认值 * @property {number} defaultReservedLeft 进度条左侧预留区默认值 * @property {number} defaultReservedRight 进度条右侧预留区默认值 * @property {number} fadeTime UI 渐变时间(单位:ms) */ /** * @typedef GMObject_menu * @property {GMObject_menu_item} setting 设置 * @property {GMObject_menu_item} uploaderList 防剧透 UP 主名单 */ /** * @typedef GMObject_menu_item * @property {boolean} state 打开状态 * @property {HTMLElement} el 菜单元素 * @property {() => void} [openHandler] 打开菜单的回调函数 * @property {() => void} [closeHandler] 关闭菜单的回调函数 */ /** * @typedef GMObject_error * @property {string} DOM_PARSE 进度条解析错误 * @property {string} NETWORK 网络错误 */ /** * 全局对象 * @type {GMObject} */ const gm = { id: 'gm411092', configVersion: GM_getValue('configVersion'), configUpdate: 20200912, config: { bangumiEnabled: false, simpleScriptControl: false, disableCurrentPoint: true, disableDuration: true, disablePbp: true, disablePreview: false, offsetTransformFactor: null, offsetLeft: null, offsetRight: null, reservedLeft: null, reservedRight: null, postponeOffset: true, openSettingAfterConfigUpdate: true, reloadAfterSetting: true, }, configMap: { bangumiEnabled: { attr: 'checked', needNotReload: true }, simpleScriptControl: { attr: 'checked' }, disableCurrentPoint: { attr: 'checked', configVersion: 20200912 }, disableDuration: { attr: 'checked' }, disablePbp: { attr: 'checked' }, disablePreview: { attr: 'checked' }, offsetTransformFactor: { attr: 'value', manual: true, needNotReload: true, configVersion: 20200911.1 }, offsetLeft: { attr: 'value', manual: true, needNotReload: true, configVersion: 20200911 }, offsetRight: { attr: 'value', manual: true, needNotReload: true, configVersion: 20200911 }, reservedLeft: { attr: 'value', manual: true, needNotReload: true }, reservedRight: { attr: 'value', manual: true, needNotReload: true }, postponeOffset: { attr: 'checked', needNotReload: true, configVersion: 20200911 }, openSettingAfterConfigUpdate: { attr: 'checked' }, reloadAfterSetting: { attr: 'checked', needNotReload: true }, }, data: { uploaderList: null, uploaderListSet: null, }, url: { api_videoInfo: (id, type) => `https://api.bilibili.com/x/web-interface/view?${type}=${id}`, gm_readme: 'https://gitee.com/liangjiancang/userscript/blob/master/BilibiliNoSpoilProgressBar/README.md', gm_changelog: 'https://gitee.com/liangjiancang/userscript/blob/master/BilibiliNoSpoilProgressBar/changelog.md', noop: 'javascript:void(0)', }, regex: { page_videoNormalMode: /\.com\/video(?=\/|$)/, page_videoWatchlaterMode: /\.com\/medialist\/play\/watchlater(?=\/|$)/, page_bangumi: /\.com\/bangumi\/play(?=\/|$)/, }, const: { defaultOffsetTransformFactor: 0.65, defaultOffsetLeft: 40, defaultOffsetRight: 40, defaultReservedLeft: 10, defaultReservedRight: 10, fadeTime: 400, }, menu: { setting: { state: false, el: null }, uploaderList: { state: false, el: null }, }, el: { gmRoot: null, setting: null, uploaderList: null, }, error: { DOM_PARSE: `DOM解析错误。大部分情况下是由于网络加载速度不足造成的,不影响脚本工作;否则就是B站网页改版,请联系脚本作者进行修改:${GM_info.script.supportURL}`, NETWORK: `网络连接错误,出现这个问题有可能是因为网络加载速度不足或者B站后台API被改动。也不排除是脚本内部数据出错造成的,初始化脚本或清空稍后再看数据也许能解决问题。无法解决请联系脚本作者:${GM_info.script.supportURL}`, } } /* global API */ const api = new API({ id: gm.id, label: GM_info.script.name, waitTimeout: 8000, // 相关元素加载较慢,等待时间可以稍长 fadeTime: gm.const.fadeTime, }) /** * 脚本运行的抽象,脚本独立于网站、为脚本本身服务的部分 */ class Script { constructor() { /** * 通用方法 */ this.method = { /** * GM 读取流程 * * 一般情况下,读取用户配置;如果配置出错,则沿用默认值,并将默认值写入配置中 * * @param {string} gmKey 键名 * @param {*} defaultValue 默认值 * @param {boolean} [writeback=true] 配置出错时是否将默认值回写入配置中 * @returns {*} 通过校验时是配置值,不能通过校验时是默认值 */ gmValidate(gmKey, defaultValue, writeback = true) { const value = GM_getValue(gmKey) if (Enums && gmKey in Enums) { if (Enums[gmKey][value]) { return value } } else if (typeof value == typeof defaultValue) { // typeof null == 'object',对象默认值赋 null 无需额外处理 return value } if (writeback) { GM_setValue(gmKey, defaultValue) } return defaultValue }, } } /** * 初始化 */ init() { this.initGMObject() this.updateVersion() this.readConfig() } /** * 初始化全局对象 */ initGMObject() { gm.config = { ...gm.config, offsetTransformFactor: gm.const.defaultOffsetTransformFactor, offsetLeft: gm.const.defaultOffsetLeft, offsetRight: gm.const.defaultOffsetRight, reservedLeft: gm.const.defaultReservedLeft, reservedRight: gm.const.defaultReservedRight, } gm.data = { ...gm.data, uploaderList: updateData => { const _ = gm.data._ if (updateData) { GM_setValue('uploaderList', updateData) _.uploaderListSet = null return updateData } else { let uploaderList = GM_getValue('uploaderList') if (typeof uploaderList != 'string') { uploaderList = '' GM_setValue('uploaderList', uploaderList) } return uploaderList } }, uploaderListSet: reload => { const _ = gm.data._ if (!_.uploaderListSet || reload) { const set = new Set() const content = gm.data.uploaderList() if (content.startsWith('*')) { set.add('*') } else { const rows = content.split('\n') for (const row of rows) { const m = row.match(/^\d+/) if (m && m.length > 0) { set.add(m[0]) } } } _.uploaderListSet = set } return _.uploaderListSet }, _: {}, // 用于存储内部数据,不公开访问 } gm.el = { ...gm.el, gmRoot: document.body.appendChild(document.createElement('div')), } gm.el.gmRoot.id = gm.id } /** * 版本更新处理 */ updateVersion() { const _self = this // 该项与更新相关,在此处处理 gm.config.openSettingAfterConfigUpdate = _self.method.gmValidate('openSettingAfterConfigUpdate', gm.config.openSettingAfterConfigUpdate) if (gm.configVersion > 0) { if (gm.configVersion < gm.configUpdate) { if (gm.config.openSettingAfterConfigUpdate) { _self.openUserSetting(2) } // 必须按从旧到新的顺序写 // 内部不能使用 gm.cofigUpdate,必须手写更新后的配置版本号! // 1.1.0.20200911 if (gm.configVersion < 20200911) { GM_setValue('offsetLeft', 40) GM_setValue('offsetRight', 40) } // 1.2.4.20200912 if (gm.configVersion < 20200912) { GM_setValue('disableCurrentPoint', true) } } } } /** * 用户配置读取 */ readConfig() { const _self = this if (gm.configVersion > 0) { // 对配置进行校验 const cfgManual = { openSettingAfterConfigUpdate: true } // 手动处理的配置 const cfgNoWriteback = {} // 不进行回写的配置 for (const name in gm.config) { if (!cfgManual[name]) { gm.config[name] = _self.method.gmValidate(name, gm.config[name], !cfgNoWriteback[name]) } } } else { // 用户强制初始化,或者第一次安装脚本 gm.configVersion = 0 const cfgManual = {} for (const name in gm.config) { if (!cfgManual[name]) { GM_setValue(name, gm.config[name]) } } _self.openUserSetting(1) setTimeout(() => { const result = confirm(`【${GM_info.script.name}】\n\n脚本有一定的使用门槛,建议在阅读说明文档后使用。是否打开说明文档?`) if (result) { window.open(`${gm.url.gm_readme}#防剧透机制说明`) } }, 2000) } } /** * 添加脚本菜单 */ addScriptMenu() { const _self = this // 用户配置设置 GM_registerMenuCommand('用户设置', () => _self.openUserSetting()) // 防剧透 UP 主名单 GM_registerMenuCommand('防剧透UP主名单', () => _self.openUploaderList()) // 强制初始化 GM_registerMenuCommand('初始化脚本', () => _self.resetScript()) } /** * 打开用户设置 * @param {number} [type=0] 普通 `0` | 初始化 `1` | 功能性更新 `2` */ openUserSetting(type = 0) { const _self = this if (gm.el.setting) { _self.openMenuItem('setting') } else { const el = {} setTimeout(() => { initSetting() processConfigItem() processSettingItem() _self.openMenuItem('setting') }) /** * 设置页面初始化 */ const initSetting = () => { gm.el.setting = gm.el.gmRoot.appendChild(document.createElement('div')) gm.menu.setting.el = gm.el.setting gm.el.setting.className = 'gm-setting' gm.el.setting.innerHTML = `
V${GM_info.script.version} by ${GM_info.script.author}
说明
防剧透机制说明 点击查看
自动化
防剧透UP主名单 点击编辑
自动化
用户接口
用户接口
启用功能时
高级设置
防剧透参数 重置
进度条极端偏移因子 💬
进度条偏移极左值 💬
进度条偏移极右值 💬
进度条左侧预留区 💬
进度条右侧预留区 💬
用户设置
用户设置
初始化脚本
更新日志
` // 找出配置对应的元素 for (const name in gm.config) { el[name] = gm.el.setting.querySelector(`#gm-${name}`) } el.settingPage = gm.el.setting.querySelector('#gm-setting-page') el.maintitle = gm.el.setting.querySelector('#gm-maintitle') el.changelog = gm.el.setting.querySelector('#gm-changelog') switch (type) { case 1: el.settingPage.setAttribute('setting-type', 'init') el.maintitle.innerHTML += '
(初始化设置)' break case 2: el.settingPage.setAttribute('setting-type', 'updated') el.maintitle.innerHTML += '
(功能性更新设置)' for (const name in gm.configMap) { const configVersion = gm.configMap[name].configVersion if (configVersion && configVersion > gm.configVersion) { let node = el[name] while (node.nodeName != 'TD') { node = node.parentNode if (!node) { api.logger.error(gm.error.DOM_PARSE) break } } if (node && node.firstElementChild) { api.dom.addClass(node.firstElementChild, 'gm-updated') } } } break } el.save = gm.el.setting.querySelector('#gm-save') el.cancel = gm.el.setting.querySelector('#gm-cancel') el.shadow = gm.el.setting.querySelector('.gm-shadow') el.reset = gm.el.setting.querySelector('#gm-reset') el.resetParam = gm.el.setting.querySelector('#gm-resetParam') el.uploaderList = gm.el.setting.querySelector('#gm-uploaderList') // 提示信息 el.offsetTransformFactorInformation = gm.el.setting.querySelector('#gm-offsetTransformFactorInformation') api.message.advanced(el.offsetTransformFactorInformation, `
进度条极端偏移因子(范围:0.00 ~ 5.00),用于控制进度条偏移量的概率分布。更多信息请阅读说明文档。
`, '💬', { width: '36em', flagSize: '2em' }) el.offsetLeftInformation = gm.el.setting.querySelector('#gm-offsetLeftInformation') api.message.advanced(el.offsetLeftInformation, `
极限情况下进度条向左偏移的距离(百分比),该选项用于解决进度条后向剧透问题。设置为 0 可以禁止进度条左偏。更多信息请阅读说明文档。
`, '💬', { width: '36em', flagSize: '2em' }) el.offsetRightInformation = gm.el.setting.querySelector('#gm-offsetRightInformation') api.message.advanced(el.offsetRightInformation, `
极限情况下进度条向左偏移的距离(百分比),该选项用于解决进度条前向剧透问题。设置为 0 可以禁止进度条右偏。更多信息请阅读说明文档。
`, '💬', { width: '36em', flagSize: '2em' }) el.reservedLeftInformation = gm.el.setting.querySelector('#gm-reservedLeftInformation') api.message.advanced(el.reservedLeftInformation, `
进度条左侧预留区间大小(百分比)。若进度条向左偏移后导致滑块进入区间,则调整偏移量使得滑块位于区间最右侧(特别地,若播放进度比偏移量小则不偏移)。该选项是为了保证在任何情况下都能通过点击滑块左侧区域向前调整进度。更多信息请阅读说明文档。
`, '💬', { width: '36em', flagSize: '2em' }) el.reservedRightInformation = gm.el.setting.querySelector('#gm-reservedRightInformation') api.message.advanced(el.reservedRightInformation, `
进度条右侧预留区间大小(百分比)。若进度条向右偏移后导致滑块进入区间,则调整偏移量使得滑块位于区间最左侧。该选项是为了保证在任何情况下都能通过点击滑块右侧区域向后调整进度。更多信息请阅读说明文档。
`, '💬', { width: '36em', flagSize: '2em' }) el.postponeOffsetInformation = gm.el.setting.querySelector('#gm-postponeOffsetInformation') api.message.advanced(el.postponeOffsetInformation, `
默认情况下,在启用功能或改变播放进度后,对进度条的偏移不会立即应用,而是在下次进度条显示出来时应用。因为这样会被用户观察到,从而推测出偏移方向与偏移量。更多信息请阅读说明文档。
`, '💬', { width: '36em', flagSize: '2em' }) } /** * 维护与设置项相关的数据和元素 */ const processConfigItem = () => { el.offsetTransformFactor.oninput = function() { const v0 = this.value.replace(/[^\d.]/g, '') if (v0 === '') { this.value = '' } else { let value if (/^\d+\./.test(v0)) { if (!/^\d+\.\d+$/.test(v0)) { value = v0.replace(/(?<=^\d+\.\d*).*/, '') } else { value = v0 } if (parseFloat(value) >= 5) { if (value.endsWith('.')) { value = '5.' } else { value = '5.0' } } } else { value = parseFloat(v0) if (value > 5) { value = 5 } value = String(value) } if (/\.\d{3,}$/.test(value)) { value = value.replace(/(?<=\.\d{2}).*/, '') } this.value = value } } el.offsetTransformFactor.onblur = function() { let value = this.value if (value === '') { value = gm.const.defaultOffsetTransformFactor } this.value = parseFloat(value).toFixed(2) } el.offsetLeft.oninput = el.offsetRight.oninput = el.reservedLeft.oninput = el.reservedRight.oninput = function() { const v0 = this.value.replace(/[^\d]/g, '') if (v0 === '') { this.value = '' } else { let value = parseInt(v0) if (value > 100) { value = 100 } this.value = value } } el.offsetLeft.onblur = function() { if (this.value === '') { this.value = gm.const.defaultOffsetLeft } } el.offsetRight.onblur = function() { if (this.value === '') { this.value = gm.const.defaultOffsetRight } } el.reservedLeft.onblur = function() { if (this.value === '') { this.value = gm.const.defaultReservedLeft } } el.reservedRight.onblur = function() { if (this.value === '') { this.value = gm.const.defaultReservedRight } } } /** * 处理与设置页面相关的数据和元素 */ const processSettingItem = () => { const _self = this gm.menu.setting.openHandler = onOpen el.save.onclick = onSave el.cancel.onclick = () => _self.closeMenuItem('setting') el.shadow.onclick = function() { if (!this.hasAttribute('disabled')) { _self.closeMenuItem('setting') } } el.reset.onclick = () => _self.resetScript() el.resetParam.onclick = () => { el.offsetTransformFactor.value = gm.const.defaultOffsetTransformFactor el.offsetLeft.value = gm.const.defaultOffsetLeft el.offsetRight.value = gm.const.defaultOffsetRight el.reservedLeft.value = gm.const.defaultReservedLeft el.reservedRight.value = gm.const.defaultReservedRight } el.uploaderList.onclick = () => { _self.openUploaderList() } if (type > 0) { el.cancel.disabled = true el.shadow.setAttribute('disabled', '') } } let needReload = false /** * 设置保存时执行 */ const onSave = () => { // 通用处理 for (const name in gm.configMap) { const cfg = gm.configMap[name] if (!cfg.manual) { const change = saveConfig(name, cfg.attr) if (!cfg.needNotReload) { needReload = needReload || change } } } // 特殊处理 let offsetTransformFactor = parseFloat(el.offsetTransformFactor.value) let offsetLeft = parseInt(el.offsetLeft.value) let offsetRight = parseInt(el.offsetRight.value) let reservedLeft = parseInt(el.reservedLeft.value) let reservedRight = parseInt(el.reservedRight.value) if (isNaN(offsetTransformFactor)) { offsetTransformFactor = gm.const.defaultOffsetTransformFactor } if (isNaN(offsetLeft)) { offsetLeft = gm.const.defaultOffsetLeft } if (isNaN(offsetRight)) { offsetRight = gm.const.defaultOffsetRight } if (isNaN(reservedLeft)) { reservedLeft = gm.const.defaultReservedLeft } if (isNaN(reservedRight)) { reservedRight = gm.const.defaultReservedRight } if (offsetTransformFactor != gm.config.offsetTransformFactor) { gm.config.offsetTransformFactor = offsetTransformFactor GM_setValue('offsetTransformFactor', gm.config.offsetTransformFactor) } if (offsetLeft != gm.config.offsetLeft) { gm.config.offsetLeft = offsetLeft GM_setValue('offsetLeft', gm.config.offsetLeft) } if (offsetRight != gm.config.offsetRight) { gm.config.offsetRight = offsetRight GM_setValue('offsetRight', gm.config.offsetRight) } if (reservedLeft != gm.config.reservedLeft) { gm.config.reservedLeft = reservedLeft GM_setValue('reservedLeft', gm.config.reservedLeft) } if (reservedRight != gm.config.reservedRight) { gm.config.reservedRight = reservedRight GM_setValue('reservedRight', gm.config.reservedRight) } _self.closeMenuItem('setting') if (type > 0) { // 更新配置版本 gm.configVersion = gm.configUpdate GM_setValue('configVersion', gm.configVersion) // 关闭特殊状态 setTimeout(() => { el.settingPage.removeAttribute('setting-type') el.maintitle.innerText = GM_info.script.name el.cancel.disabled = false el.shadow.removeAttribute('disabled') }, gm.const.fadeTime) } if (gm.config.reloadAfterSetting && needReload) { needReload = false location.reload() } } /** * 设置打开时执行 */ const onOpen = () => { for (const name in gm.configMap) { const attr = gm.configMap[name].attr el[name][attr] = gm.config[name] } for (const name in gm.configMap) { // 需要等所有配置读取完成后再进行选项初始化 el[name].init && el[name].init() } el.settingPage.parentNode.style.display = 'block' setTimeout(() => { api.dom.setAbsoluteCenter(el.settingPage) }, 10) } /** * 保存配置 * @param {string} name 配置名称 * @param {string} attr 从对应元素的什么属性读取 * @returns {boolean} 是否有实际更新 */ const saveConfig = (name, attr) => { const elValue = el[name][attr] if (gm.config[name] != elValue) { gm.config[name] = elValue GM_setValue(name, gm.config[name]) return true } return false } } } /** * 打开防剧透 UP 主名单 */ openUploaderList() { const _self = this const el = {} if (gm.el.uploaderList) { _self.openMenuItem('uploaderList', null, true) } else { setTimeout(() => { initEditor() processItem() _self.openMenuItem('uploaderList', null, true) }) /** * 初始化防剧透 UP 主名单编辑器 */ const initEditor = () => { gm.el.uploaderList = gm.el.gmRoot.appendChild(document.createElement('div')) gm.menu.uploaderList.el = gm.el.uploaderList gm.el.uploaderList.className = 'gm-uploaderList' gm.el.uploaderList.innerHTML = `
防剧透UP主名单
当打开名单内UP主的视频时,会自动启用防剧透进度条。在下方文本框内填入UP主的UID,其中UID可在UP主的个人空间中找到。每行必须以UID开头,UID后可以用空格隔开进行注释。第一行以  *  开头时,匹配所有UP主。点击填充示例。
` el.uploaderListPage = gm.el.uploaderList.querySelector('.gm-uploaderList-page') el.uploaderList = gm.el.uploaderList.querySelector('#gm-uploaderList') el.uploaderListExample = gm.el.uploaderList.querySelector('#gm-uploader-list-example') el.save = gm.el.uploaderList.querySelector('#gm-save') el.cancel = gm.el.uploaderList.querySelector('#gm-cancel') el.shadow = gm.el.uploaderList.querySelector('.gm-shadow') } /** * 维护内部元素和数据 */ const processItem = () => { gm.menu.uploaderList.openHandler = onOpen el.uploaderListExample.onclick = () => { el.uploaderList.value = '# 非UID起始的行不会影响名单读取\n204335848 # 皇室战争电竞频道\n50329118 # 哔哩哔哩英雄联盟赛事' } el.save.onclick = onSave el.cancel.onclick = el.shadow.onclick = () => _self.closeMenuItem('uploaderList') } /** * 防剧透 UP 主名单保存时执行 */ const onSave = () => { gm.data.uploaderList(el.uploaderList.value) _self.closeMenuItem('uploaderList') } /** * 防剧透 UP 主名单打开时执行 */ const onOpen = () => { el.uploaderList.value = gm.data.uploaderList() api.dom.setAbsoluteCenter(el.uploaderListPage) } } } /** * 初始化脚本 */ resetScript() { const result = confirm(`【${GM_info.script.name}】\n\n是否要初始化脚本?`) if (result) { const keyNoReset = {} const gmKeys = GM_listValues() for (const gmKey of gmKeys) { if (!keyNoReset[gmKey]) { GM_deleteValue(gmKey) } } gm.configVersion = 0 GM_setValue('configVersion', gm.configVersion) location.reload() } } /** * 对“打开菜单项”这一操作进行处理,包括显示菜单项、设置当前菜单项的状态、关闭其他菜单项 * @param {string} name 菜单项的名称 * @param {() => void} [callback] 打开菜单项后的回调函数 * @param {boolean} [keepOthers] 打开时保留其他菜单项 */ openMenuItem(name, callback, keepOthers) { const _self = this if (!gm.menu[name].state) { for (const key in gm.menu) { /** @type {GMObject_menu_item} */ const menu = gm.menu[key] if (key == name) { menu.state = true menu.openHandler && menu.openHandler.call(menu) api.dom.fade(true, menu.el, callback) if (document.fullscreenElement) { document.exitFullscreen() } } else if (!keepOthers) { if (menu.state) { _self.closeMenuItem(key) } } } } } /** * 对“关闭菜单项”这一操作进行处理,包括隐藏菜单项、设置当前菜单项的状态 * @param {string} name 菜单项的名称 * @param {() => void} [callback] 关闭菜单项后的回调函数 */ closeMenuItem(name, callback) { /** @type {GMObject_menu_item} */ const menu = gm.menu[name] if (menu.state) { menu.state = false api.dom.fade(false, menu.el, () => { menu.closeHandler && menu.closeHandler.call(menu) callback && callback.call(menu) }) } } } /** * 页面处理的抽象,脚本围绕网站的特化部分 */ class Webpage { constructor() { this.script = new Script() /** * 播放控制 * @type {HTMLElement} */ this.control = {} /** * 进度条 * @typedef ProgressBar * @property {HTMLElement} root 进度条根元素 * @property {HTMLElement} bar 进度条主体 * @property {HTMLElement} thumb 进度条滑块 * @property {HTMLElement} track 进度条滑槽 * @property {HTMLElement} buffer 进度条缓冲显示 * @property {HTMLElement} played 进度条已播放显示 * @property {HTMLElement} preview 进度条预览 */ /** * 进度条 * @type {ProgressBar} */ this.progress = {} /** * 视频最底下的影子进度条 * @type {HTMLElement} */ this.shadowProgress = null /** * 用于模仿被隐藏的进度条滑槽 * @type {HTMLElement} */ this.fakeTrack = null /** * 用于模仿被隐藏的进度条滑槽中的已播放显示 * @type {HTMLElement} */ this.fakePlayed = null /** * 脚本控制条 * @type {HTMLElement} */ this.scriptControl = null /** * 是否开启防剧透功能 * @type {boolean} */ this.enabled = false /** * 当前 UP 主是否在防剧透名单中 */ this.uploaderEnabled = false /** * 通用方法 */ this.method = { /** * 获取 `aid` * @async * @returns {Promise} `aid` */ async getAid() { let aid try { if (unsafeWindow.aid) { aid = unsafeWindow.aid } else { aid = await api.wait.waitForConditionPassed({ condition: () => { const player = unsafeWindow.player const message = player && player.getVideoMessage && player.getVideoMessage() return message && message.aid }, }) } } catch (e) { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) } return String(aid) }, /** * 获取视频信息 * @async * @param {string} id `aid` 或 `bvid` * @param {'aid' | 'bvid'} [type='bvid'] `id` 类型 * @returns {Promise} 视频信息 */ async getVideoInfo(id, type = 'bvid') { try { const resp = await api.web.request({ method: 'GET', url: gm.url.api_videoInfo(id, type), }) return JSON.parse(resp.responseText).data } catch (e) { api.logger.error(gm.error.NETWORK) api.logger.error(e) } }, } } /** * 初始化页面内容 * @async * @throws DOM 解析错误时抛出 */ async initWebpage() { const _self = this _self.uploaderEnabled = false _self.enabled = await _self.detectEnabled() _self.control = await api.wait.waitForElementLoaded('.bilibili-player-video-control') _self.progress.root = await api.wait.waitForElementLoaded('.bilibili-player-video-progress', _self.control) _self.progress.bar = await api.wait.waitForElementLoaded('.bilibili-player-video-progress-slider', _self.progress.root) _self.progress.thumb = await api.wait.waitForElementLoaded('.bui-thumb', _self.progress.bar) _self.progress.track = await api.wait.waitForElementLoaded('.bui-bar-wrap', _self.progress.bar) _self.progress.buffer = await api.wait.waitForElementLoaded('.bui-bar-buffer', _self.progress.track) _self.progress.played = await api.wait.waitForElementLoaded('.bui-bar-normal', _self.progress.track) _self.progress.preview = await api.wait.waitForElementLoaded('.bilibili-player-video-progress-detail', _self.progress.root) _self.shadowProgress = await api.wait.waitForElementLoaded('.bilibili-player-video-progress-shadow', this.control) _self.fakeTrack = _self.progress.track.parentNode.insertBefore(_self.progress.track.cloneNode(true), _self.progress.track) // 必须在 thumb 前,否则 z 轴层次错误 _self.fakeTrack.style.visibility = 'hidden' _self.fakeTrack.querySelector('.bui-bar-buffer').style.visibility = 'hidden' _self.fakePlayed = _self.fakeTrack.querySelector('.bui-bar-normal') _self.initScriptControl() await api.wait.waitForConditionPassed({ condition: () => { const player = unsafeWindow.player return player.getCurrentTime && player.getDuration }, }) } /** * 判断当前页面时是否自动启用功能 * @async * @returns {boolean} 当前页面时是否自动启用功能 */ async detectEnabled() { const _self = this if (api.web.urlMatch(gm.regex.page_videoNormalMode) || api.web.urlMatch(gm.regex.page_videoWatchlaterMode)) { try { const ulSet = gm.data.uploaderListSet() if (ulSet.has('*')) { return true } const aid = await _self.method.getAid() const videoInfo = await _self.method.getVideoInfo(aid, 'aid') const uid = String(videoInfo.owner.mid) if (ulSet.has(uid)) { _self.uploaderEnabled = true return true } } catch (e) { api.logger.error(gm.error.NETWORK) api.logger.error(e) } } else if (api.web.urlMatch(gm.regex.page_bangumi)) { if (gm.config.bangumiEnabled) { return true } } return false } /** * 防剧透功能处理流程 */ processNoSpoil() { const _self = this setTimeout(() => { noSpoilHandler() const clzControlShow = 'video-control-show' const playerArea = document.querySelector('.bilibili-player-area') if (!playerArea._obControlShow) { playerArea._obControlShow = new MutationObserver(records => { for (const record of records) { if (record.attributeName == 'class') { const before = api.dom.containsClass({ className: record.oldValue }, clzControlShow) const current = api.dom.containsClass(playerArea, clzControlShow) if (before != current) { if (current) { if (_self.enabled) { noSpoilHandler(true) if (!playerArea._obPlayRate) { playerArea._obPlayRate = new MutationObserver(records => { for (const record of records) { if (record.attributeName == 'style') { _self.processFakePlayed() break } } }) playerArea._obPlayRate.observe(_self.progress.thumb, { attributes: true }) } } } else if (playerArea._obPlayRate) { playerArea._obPlayRate.disconnect() playerArea._obPlayRate = null } break } } } }) playerArea._obControlShow.observe(playerArea, { attributes: true, attributeOldValue: true, }) } }) if (_self.enabled) { _self.progress.preview.style.visibility = gm.config.disablePreview ? 'hidden' : 'visible' } else { _self.progress.preview.style.visibility = 'visible' } // 隐藏当前播放时间 api.wait.waitForElementLoaded('.bilibili-player-video-time-now:not(.fake)').then(currentPoint => { if (_self.enabled && gm.config.disableCurrentPoint) { if (!currentPoint._fake) { currentPoint._fake = currentPoint.parentNode.insertBefore(currentPoint.cloneNode(true), currentPoint) currentPoint._fake.innerText = '???' api.dom.addClass(currentPoint._fake, 'fake') } currentPoint.style.display = 'none' currentPoint._fake.style.display = 'unset' } else { currentPoint.style.display = 'unset' if (currentPoint._fake) { currentPoint._fake.style.display = 'none' } } }) // 隐藏视频时长 api.wait.waitForElementLoaded('.bilibili-player-video-time-total').then(duration => { if (_self.enabled && gm.config.disableDuration) { duration._innerText = duration.innerText duration.innerText = '???' } else if (duration._innerText) { duration.innerText = duration._innerText } }).catch(e => { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) }) // 隐藏高能进度条的【热度】曲线(可能存在) api.wait.waitForElementLoaded('#bilibili_pbp', _self.control).then(pbp => { const hide = _self.enabled && gm.config.disablePbp pbp.style.visibility = hide ? 'hidden' : '' }).catch(() => {}) // 隐藏 pakku 扩展引入的弹幕密度显示(可能存在) api.wait.waitForElementLoaded('canvas.pakku-fluctlight', _self.control).then(pakku => { const hide = _self.enabled && gm.config.disablePbp pakku.style.visibility = hide ? 'hidden' : '' }).catch(() => {}) /** * 防剧透处理核心流程 * @param {boolean} [noPostpone] 不延后执行 */ const noSpoilHandler = noPostpone => { try { let offset = 'offset' let playRate = 0 if (_self.enabled) { const player = unsafeWindow.player playRate = player.getCurrentTime() / player.getDuration() offset = getEndPoint() - 100 const reservedLeft = gm.config.reservedLeft const reservedRight = 100 - gm.config.reservedRight if (playRate * 100 < reservedLeft) { offset = 0 } else { const offsetRate = playRate * 100 + offset if (offsetRate < reservedLeft) { offset = reservedLeft - playRate * 100 } else if (offsetRate > reservedRight) { offset = reservedRight - playRate * 100 } } } else if (_self.progress._noSpoil) { offset = 0 } if (typeof offset == 'number') { const handler = () => { _self.progress.root.style.transform = `translateX(${offset}%)` _self.scriptControl.transform = `translateX(${-offset}%)` if (_self.enabled) { _self.fakeTrack.style.transform = `translateX(${-offset}%)` } } if (_self.enabled) { _self.progress.buffer.style.visibility = 'hidden' _self.progress.track.style.visibility = 'hidden' _self.shadowProgress.style.visibility = 'hidden' _self.fakeTrack.style.visibility = 'visible' if (noPostpone || !gm.config.postponeOffset) { handler() } else if (!_self.progress._noSpoil) { // 首次打开 _self.progress.root.style.transform = 'translateX(0)' _self.scriptControl.transform = 'translateX(0)' _self.fakeTrack.style.transform = 'translateX(0)' } _self.processFakePlayed() _self.progress._noSpoil = true } else { _self.progress.track.style.visibility = 'visible' _self.progress.buffer.style.visibility = 'visible' _self.shadowProgress.style.visibility = 'visible' _self.fakeTrack.style.visibility = 'hidden' handler() _self.progress._noSpoil = false } } if (api.web.urlMatch(gm.regex.page_videoNormalMode) || api.web.urlMatch(gm.regex.page_videoWatchlaterMode)) { if (_self.uploaderEnabled) { _self.scriptControl.uploaderEnabled.setAttribute('enabled', '') } else { _self.scriptControl.uploaderEnabled.removeAttribute('enabled') } } if (api.web.urlMatch(gm.regex.page_bangumi)) { if (gm.config.bangumiEnabled) { _self.scriptControl.bangumiEnabled.setAttribute('enabled', '') } else { _self.scriptControl.bangumiEnabled.removeAttribute('enabled') } } } catch (e) { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) } } /** * 获取偏移后进度条尾部位置 * @returns {number} 偏移后进度条尾部位置 */ const getEndPoint = () => { if (!_self.progress._noSpoil) { _self.progress._fakeRandom = Math.random() } let r = _self.progress._fakeRandom const origin = 100 // 左右分界点 const left = gm.config.offsetLeft const right = gm.config.offsetRight const factor = gm.config.offsetTransformFactor const mid = left / (left + right) // 概率中点 if (r <= mid) { // 向左偏移 r = 1 - r / mid r **= factor return origin - r * left } else { // 向右偏移 r = (r - mid) / (1 - mid) r **= factor return origin + r * right } } } /** * 初始化防剧透功能 * @param {boolean} [selfCall] 自调用 * @async */ async initNoSpoil(selfCall) { const _self = this try { await _self.initWebpage() await _self.processNoSpoil() // 加载完页面后,有时候视频会莫名其妙地重新刷新,原因不明 // 总之,先等一下看注入的内容还在,如果不再则重新初始化 // 若没有发生刷新,则不必处理 api.wait.executeAfterConditionPassed({ condition: () => { const scriptControl = document.querySelector(`.${gm.id}-scriptControl`) return !scriptControl }, callback: () => { _self.initNoSpoil() }, interval: 250, timePadding: 1000, }) } catch (e) { // 抛出异常,有可能确实是 B 站改版导致,但更多情况下,是因为网页还未加载完成导致的 // 出现这种情况,往往是因为用户一次性打开多个页面,过了非常久之后才切换过去导致的 try { if (selfCall) { throw e } else { const control = await api.wait.waitForElementLoaded('.bilibili-player-video-control') const ob = new MutationObserver((records, observer) => { observer.disconnect() _self.initNoSpoil(true) }) ob.observe(control, { childList: true, subtree: true, }) } } catch (e) { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) } } } /** * 初始化页面切换处理 * @async */ async initLocationChangeProcess() { const _self = this let currentPathname = location.pathname let currentAid = await _self.method.getAid() api.dom.createLocationchangeEvent() window.addEventListener('locationchange', function() { api.wait.waitForConditionPassed({ condition: async () => { if (location.pathname == currentPathname) { // 并非切换视频(如切分 P) return currentAid } else { const aid = await _self.method.getAid() if (aid != currentAid) { // aid 改变才能说明页面真正切换过去 currentPathname = location.pathname return aid } } }, }).then(aid => { currentAid = aid _self.initNoSpoil() if (api.web.urlMatch(gm.regex.page_videoWatchlaterMode)) { _self.initSwitchingPartProcess() } }).catch(e => { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) }) }) } /** * 初始化稍后再看模式播放页切换分 P 的处理 * @async */ async initSwitchingPartProcess() { try { const _self = this let obActiveP, obList const list = await api.wait.waitForElementLoaded('.player-auxiliary-playlist-list') try { const activeVideo = await api.wait.waitForElementLoaded('.player-auxiliary-playlist-item-active', list) const pList = await api.wait.waitForElementLoaded('.player-auxiliary-playlist-item-p-list', activeVideo) if (pList) { const activeP = await api.wait.waitForElementLoaded('.player-auxiliary-playlist-item-p-item-active', pList) obActiveP = new MutationObserver(async (records, observer) => { for (const record of records) { if (record.attributeName == 'class') { observer.disconnect() obList && obList.disconnect() const currentActive = await api.wait.waitForElementLoaded('.player-auxiliary-playlist-item-active') if (currentActive === activeVideo) { _self.initNoSpoil() _self.initSwitchingPartProcess() } break } } }) obActiveP.observe(activeP, { attributes: true }) } } catch (e) { // 只是因为 list 已经变化,导致在原 list 下找不到对应的元素而已,实际并无错误 } // 如果 list 中发生修改,则前面的监听无效,应当重新处理 obList = new MutationObserver((records, observer) => { observer.disconnect() obActiveP && obActiveP.disconnect() _self.initSwitchingPartProcess() }) obList.observe(list, { childList: true }) } catch (e) { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) } } /** * 初始化脚本控制条 */ initScriptControl() { const _self = this if (!_self.control._scriptControl) { _self.scriptControl = _self.progress.root.parentNode.appendChild(document.createElement('div')) _self.control._scriptControl = _self.scriptControl _self.scriptControl.className = `${gm.id}-scriptControl` _self.scriptControl.innerHTML = ` 防剧透 ` } _self.scriptControl.enabled = _self.scriptControl.querySelector(`#${gm.id}-enabled`) _self.scriptControl.uploaderEnabled = _self.scriptControl.querySelector(`#${gm.id}-uploaderEnabled`) _self.scriptControl.bangumiEnabled = _self.scriptControl.querySelector(`#${gm.id}-bangumiEnabled`) _self.scriptControl.setting = _self.scriptControl.querySelector(`#${gm.id}-setting`) _self.scriptControl.enabled.handler = function() { if (_self.enabled) { this.setAttribute('enabled', '') } else { this.removeAttribute('enabled') } _self.processNoSpoil() } _self.scriptControl.enabled.onclick = function() { _self.enabled = !_self.enabled this.handler() } if (this.enabled) { _self.scriptControl.enabled.handler() } if (!gm.config.simpleScriptControl) { if (api.web.urlMatch(gm.regex.page_videoNormalMode) || api.web.urlMatch(gm.regex.page_videoWatchlaterMode)) { if (!gm.data.uploaderListSet().has('*')) { // * 匹配所有 UP 主不显示该按钮 _self.scriptControl.uploaderEnabled.style.display = 'unset' _self.scriptControl.uploaderEnabled.onclick = async function() { try { const ulSet = gm.data.uploaderListSet() // 必须每次读取 const aid = await _self.method.getAid() const videoInfo = await _self.method.getVideoInfo(aid, 'aid') const uid = String(videoInfo.owner.mid) _self.uploaderEnabled = !_self.uploaderEnabled if (_self.uploaderEnabled) { this.setAttribute('enabled', '') if (!ulSet.has(uid)) { const ul = gm.data.uploaderList() gm.data.uploaderList(`${ul}\n${uid}`) } } else { this.removeAttribute('enabled') if (ulSet.has(uid)) { let ul = gm.data.uploaderList() ul = ul.replaceAll(new RegExp(`^${uid}(?=\\D|$).*\n?`, 'gm'), '') gm.data.uploaderList(ul) } } } catch (e) { api.logger.error(gm.error.NETWORK) api.logger.error(e) } } } } if (api.web.urlMatch(gm.regex.page_bangumi)) { _self.scriptControl.bangumiEnabled.style.display = 'unset' _self.scriptControl.bangumiEnabled.onclick = function() { gm.config.bangumiEnabled = !gm.config.bangumiEnabled if (gm.config.bangumiEnabled) { this.setAttribute('enabled', '') } else { this.removeAttribute('enabled') } GM_setValue('bangumiEnabled', gm.config.bangumiEnabled) } } _self.scriptControl.setting.style.display = 'unset' _self.scriptControl.setting.onclick = function() { _self.script.openUserSetting() } } } /** * 更新用于模拟已播放进度的伪已播放条 */ processFakePlayed() { try { const _self = this const player = unsafeWindow.player const currentPlayRate = player.getCurrentTime() / player.getDuration() let currentOffset const m = _self.progress.root.style.transform.match(/(?<=translateX\()[^)]+(?=\))/) if (m && m.length > 0) { currentOffset = m[0] } else { currentOffset = 0 } _self.fakePlayed.style.transform = `scaleX(${currentPlayRate + parseFloat(currentOffset) / 100})` } catch (e) { api.logger.error(gm.error.DOM_PARSE) api.logger.error(e) } } /** * 添加脚本样式 */ addStyle() { GM_addStyle(` :root { --control-item-selected-color: #00c7ff; --control-item-shadow-color: #00000050; --text-color: black; --text-bold-color: #3a3a3a; --light-text-color: white; --hint-text-color: gray; --hint-text-hightlight-color: #555555; --background-color: white; --background-hightlight-color: #ebebeb; --update-hightlight-color: #c2ffc2; --update-hightlight-hover-color: #a90000; --border-color: black; --shadow-color: #000000bf; --hightlight-color: #0075FF; --important-color: red; --warn-color: #e37100; --disabled-color: gray; --link-visited-color: #551a8b; --opacity-fade-transition: opacity ${gm.const.fadeTime}ms ease-in-out; } .${gm.id}-scriptControl { position: absolute; left: 0; bottom: 0; color: var(--light-text-color); margin-bottom: 0.8em; font-size: 13px; z-index: 10000; } .${gm.id}-scriptControl > * { cursor: pointer; border: 1px solid; border-radius: 0.4em; padding: 0.1em 0.3em; margin: 0 0.1em; background-color: var(--control-item-shadow-color); } .${gm.id}-scriptControl > *[enabled] { color: var(--control-item-selected-color); } #${gm.id} { color: var(--text-color); } #${gm.id} .gm-setting { font-size: 12px; line-height: normal; transition: var(--opacity-fade-transition); opacity: 0; display: none; position: fixed; z-index: 10000; user-select: none; } #${gm.id} .gm-setting #gm-setting-page { background-color: var(--background-color); border-radius: 10px; z-index: 65535; min-width: 42em; padding: 1em 1.4em; transition: top 100ms, left 100ms; } #${gm.id} .gm-setting #gm-maintitle * { cursor: pointer; color: var(--text-color); } #${gm.id} .gm-setting #gm-maintitle:hover * { color: var(--hightlight-color); } #${gm.id} .gm-setting .gm-items { margin: 0 0.2em; padding: 0 1.8em 0 2.2em; font-size: 1.2em; max-height: 66vh; overflow-y: auto; } #${gm.id} .gm-setting table { width: 100%; border-collapse: separate; } #${gm.id} .gm-setting td { position: relative; } #${gm.id} .gm-setting .gm-item td:first-child { vertical-align: top; padding-right: 0.6em; font-weight: bold; color: var(--text-bold-color); } #${gm.id} .gm-setting .gm-item:not(:first-child) td { padding-top: 0.5em; } #${gm.id} .gm-setting td > * { padding: 0.2em; border-radius: 0.2em; } #${gm.id} .gm-setting .gm-item:hover { color: var(--hightlight-color); } #${gm.id} .gm-setting .gm-subitem[disabled] { color: var(--disabled-color); } #${gm.id} .gm-setting .gm-subitem:hover:not([disabled]) { color: var(--hightlight-color); } #${gm.id} .gm-setting label { display: flex; align-items: center; } #${gm.id} .gm-setting input[type=checkbox] { margin-left: auto; } #${gm.id} .gm-setting input[type=text] { float: right; border-width: 0 0 1px 0; border-radius: 0; width: 2.4em; text-align: right; padding: 0 0.2em; margin: 0 -0.2em; } #${gm.id} .gm-setting select { border-width: 0 0 1px 0; cursor: pointer; margin: 0 -0.2em; } #${gm.id} .gm-setting .gm-information { margin: 0 0.2em; cursor: pointer; } #${gm.id} .gm-setting [disabled] .gm-information { cursor: not-allowed; } #${gm.id} .gm-setting .gm-warning { position: absolute; right: -1.1em; color: var(--warn-color); font-size: 1.4em; line-height: 1em; transition: var(--opacity-fade-transition); opacity: 0; display: none; cursor: pointer; } #${gm.id} .gm-uploaderList { font-size: 12px; line-height: normal; transition: var(--opacity-fade-transition); opacity: 0; display: none; position: fixed; z-index: 12000; user-select: none; } #${gm.id} .gm-uploaderList .gm-uploaderList-page { background-color: var(--background-color); border-radius: 10px; z-index: 65535; width: 36em; height: 40em; transition: top 100ms, left 100ms; } #${gm.id} .gm-uploaderList .gm-comment { margin: 0 2em; color: var(--hint-text-color); text-indent: 2em; } #${gm.id} .gm-uploaderList .gm-list-editor { margin: 1em 2em 1em 2em; } #${gm.id} .gm-uploaderList .gm-list-editor textarea { font-size: 1.3em; width: calc(100% - 2em); height: 15.2em; padding: 1em; resize: none; outline: none; } #${gm.id} .gm-bottom { margin: 1.4em 2em 1em 2em; text-align: center; } #${gm.id} .gm-bottom button { font-size: 1em; padding: 0.3em 1em; margin: 0 0.8em; cursor: pointer; background-color: var(--background-color); border: 1px solid var(--border-color); border-radius: 2px; } #${gm.id} .gm-bottom button:hover { background-color: var(--background-hightlight-color); } #${gm.id} .gm-bottom button[disabled] { cursor: not-allowed; border-color: var(--disabled-color); background-color: var(--background-color); } #${gm.id} .gm-hint-option { font-size: 0.8em; color: var(--hint-text-color); text-decoration: underline; padding: 0 0.2em; cursor: pointer; } #${gm.id} .gm-hint-option:hover { color: var(--important-color); } #${gm.id} [disabled] .gm-hint-option { color: var(--disabled-color); cursor: not-allowed; } #${gm.id} #gm-reset { position: absolute; right: 0; bottom: 0; margin: 1em 1.6em; color: var(--hint-text-color); cursor: pointer; } #${gm.id} #gm-changelog { position: absolute; right: 0; bottom: 1.8em; margin: 1em 1.6em; color: var(--hint-text-color); cursor: pointer; } #${gm.id} [setting-type=updated] #gm-changelog { font-weight: bold; color: var(--important-color); } #${gm.id} [setting-type=updated] #gm-changelog:hover { color: var(--important-color); } #${gm.id} [setting-type=updated] .gm-updated, #${gm.id} [setting-type=updated] .gm-updated input, #${gm.id} [setting-type=updated] .gm-updated select { background-color: var(--update-hightlight-color); } #${gm.id} [setting-type=updated] .gm-updated option { background-color: var(--background-color); } #${gm.id} [setting-type=updated] .gm-updated:hover { color: var(--update-hightlight-hover-color); } #${gm.id} #gm-reset:hover, #${gm.id} #gm-changelog:hover { color: var(--hint-text-hightlight-color); text-decoration: underline; } #${gm.id} .gm-title { font-size: 1.6em; margin: 1.6em 0.8em 0.8em 0.8em; text-align: center; } #${gm.id} .gm-subtitle { font-size: 0.4em; margin-top: 0.4em; } #${gm.id} .gm-shadow { background-color: var(--shadow-color); position: fixed; top: 0%; left: 0%; z-index: 10000; width: 100%; height: 100%; } #${gm.id} .gm-shadow[disabled] { cursor: auto; } #${gm.id} label { cursor: pointer; } #${gm.id} input, #${gm.id} select, #${gm.id} button { color: var(--text-color); outline: none; appearance: auto; /* 番剧播放页该项被覆盖 */ } #${gm.id} a { color: var(--hightlight-color) } #${gm.id} a:visited { color: var(--link-visited-color) } #${gm.id} [disabled], #${gm.id} [disabled] input, #${gm.id} [disabled] select { cursor: not-allowed; color: var(--disabled-color); } #${gm.id} .gm-setting .gm-items::-webkit-scrollbar, #${gm.id} .gm-uploaderList .gm-list-editor textarea::-webkit-scrollbar { width: 6px; height: 6px; background-color: var(--scrollbar-background-color); } #${gm.id} .gm-setting .gm-items::-webkit-scrollbar-thumb, #${gm.id} .gm-uploaderList .gm-list-editor textarea::-webkit-scrollbar-thumb { border-radius: 3px; background-color: var(--scrollbar-thumb-color); } #${gm.id} .gm-setting .gm-items::-webkit-scrollbar-corner, #${gm.id} .gm-uploaderList .gm-list-editor textarea::-webkit-scrollbar-corner { background-color: var(--scrollbar-background-color); } `) } } (function() { const script = new Script() const webpage = new Webpage() script.init() script.addScriptMenu() webpage.initNoSpoil() webpage.initLocationChangeProcess() if (api.web.urlMatch(gm.regex.page_videoWatchlaterMode)) { webpage.initSwitchingPartProcess() } webpage.addStyle() })() })()