// ==UserScript== // @name Bilibili Evolved // @version 1.8.14 // @description 增强哔哩哔哩Web端体验: 下载视频, 音乐, 封面, 弹幕; 自定义播放器的画质, 模式, 布局; 自定义顶栏, 删除广告, 使用夜间模式; 以及增加对触屏设备的支持等. // @author Grant Howard, Coulomb-G // @copyright 2019, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G) // @license MIT // @match *://*.bilibili.com/* // @match *://*.bilibili.com // @run-at document-start // @updateURL https://github.com/the1812/Bilibili-Evolved/raw/master/bilibili-evolved.user.js // @downloadURL https://github.com/the1812/Bilibili-Evolved/raw/master/bilibili-evolved.user.js // @supportURL https://github.com/the1812/Bilibili-Evolved/issues // @homepage https://github.com/the1812/Bilibili-Evolved // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @grant GM_info // @require https://code.jquery.com/jquery-3.4.0.min.js // @require https://cdn.bootcss.com/jszip/3.1.5/jszip.min.js // @require https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js // @icon https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/images/logo-small.png // @icon64 https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/images/logo.png // ==/UserScript== function logError (error) { let finalMessage = error if (typeof error === 'object' && 'stack' in error) { if (settings.toastInternalError) { finalMessage = `${error.message}\n${error.stack}` } else { finalMessage = error.message } } Toast.error(finalMessage, '错误') console.error(error) } function raiseEvent (element, eventName) { const event = document.createEvent('HTMLEvents') event.initEvent(eventName, true, true) element.dispatchEvent(event) } async function loadLazyPanel (selector) { await SpinQuery.unsafeJquery() const panel = await SpinQuery.any(() => unsafeWindow.$(selector)) if (!panel) { throw new Error(`Panel not found: ${selector}`) } panel.mouseover().mouseout() } async function loadDanmakuSettingsPanel () { const style = document.createElement('style') style.innerText = `.bilibili-player-video-danmaku-setting-wrap { display: none !important; }` document.body.insertAdjacentElement('beforeend', style) await loadLazyPanel('.bilibili-player-video-danmaku-setting') setTimeout(() => style.remove(), 300) } function contentLoaded (callback) { if (/complete|interactive|loaded/.test(document.readyState)) { callback() } else { document.addEventListener('DOMContentLoaded', () => callback()) } } function fullyLoaded (callback) { if (document.readyState === 'complete') { callback() } else { unsafeWindow.addEventListener('load', () => callback()) } } function fixed (number, precision = 1) { const str = number.toString() const index = str.indexOf('.') if (index !== -1) { if (str.length - index > precision + 1) { return str.substring(0, index + precision + 1) } else { return str } } else { return str + '.0' } } function isEmbeddedPlayer () { return location.host === 'player.bilibili.com' || document.URL.startsWith('https://www.bilibili.com/html/player.html') } function isIframe () { return document.body && unsafeWindow.parent.window !== unsafeWindow } const languageNameToCode = { '日本語': 'ja-JP', 'English': 'en-US', 'Deutsch': 'de-DE' } const languageCodeToName = { 'ja-JP': '日本語', 'en-US': 'English', 'de-DE': 'Deutsch' } function getI18nKey () { return settings.i18n ? languageNameToCode[settings.i18nLanguage] : 'zh-CN' } const dq = (selector) => document.querySelector(selector) const dqa = (selector) => [...document.querySelectorAll(selector)] const UserAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0` const EmptyImageUrl = 'data:image/svg+xml;utf-8,' const ascendingSort = (itemProp) => { return (a, b) => itemProp(a) - itemProp(b) } const descendingSort = (itemProp) => { return (a, b) => itemProp(b) - itemProp(a) } const formatFileSize = (bytes, fixed = 1) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] let number = bytes let unitIndex = 0 while (number >= 1024) { number /= 1024 unitIndex++ } return `${Math.round(number * (10 ** fixed)) / (10 ** fixed)}${units[unitIndex]}` } const customNavbarDefaultOrders = { blank1: 0, logo: 1, category: 2, rankingLink: 3, drawingLink: 4, musicLink: 5, gamesIframe: 6, livesIframe: 7, shopLink: 8, mangaLink: 9, blank2: 10, search: 11, userInfo: 12, messages: 13, activities: 14, bangumiLink: 15, watchlaterList: 16, favoritesList: 17, historyList: 18, upload: 19, blank3: 20, } const settings = { useDarkStyle: false, compactLayout: false, // showBanner: true, hideBanner: false, expandDanmakuList: true, expandDescription: true, watchLaterRedirect: true, touchNavBar: false, touchVideoPlayer: false, customControlBackgroundOpacity: 0.64, customControlBackground: true, darkScheduleStart: '18:00', darkScheduleEnd: '6:00', darkSchedule: false, blurVideoControl: false, toast: true, fullTweetsTitle: true, fullPageTitle: false, removeVideoTopMask: false, removeLiveWatermark: true, harunaScale: true, removeAds: true, showBlockedAdsTip: false, hideTopSearch: false, touchVideoPlayerDoubleTapControl: false, customStyleColor: '#00A0D8', preserveRank: true, blurBackgroundOpacity: 0.382, useDefaultPlayerMode: false, applyPlayerModeOnPlay: true, defaultPlayerMode: '常规', useDefaultVideoQuality: false, defaultVideoQuality: '自动', useDefaultDanmakuSettings: false, enableDanmaku: true, rememberDanmakuSettings: false, danmakuSettings: { subtitlesPreserve: false, smartMask: false, }, defaultPlayerLayout: '新版', defaultBangumiLayout: '旧版', useDefaultPlayerLayout: false, skipChargeList: false, comboLike: false, autoLightOff: false, useCache: true, autoContinue: false, allowJumpContinue: false, autoPlay: false, showDeadVideoTitle: false, deadVideoTitleProvider: '稍后再看', useBiliplusRedirect: false, biliplusRedirect: false, framePlayback: true, useCommentStyle: true, imageResolution: false, imageResolutionScale: 'auto', toastInternalError: false, i18n: false, i18nLanguage: '日本語', playerFocus: false, playerFocusOffset: -10, oldTweets: false, simplifyLiveroom: false, simplifyLiveroomSettings: { vip: true, fansMedal: true, title: true, userLevel: true, guard: true, systemMessage: true, welcomeMessage: true, giftMessage: true, guardPurchase: true, popup: false, skin: false, }, customNavbar: true, customNavbarFill: true, allNavbarFill: true, customNavbarShadow: true, customNavbarCompact: false, customNavbarBlur: false, customNavbarBlurOpacity: 0.7, customNavbarOrder: { ...customNavbarDefaultOrders }, customNavbarHidden: ['bangumiLink'], customNavbarBoundsPadding: 5, playerShadow: false, narrowDanmaku: true, favoritesRedirect: true, outerWatchlater: true, hideOldEntry: true, videoScreenshot: false, hideBangumiReviews: false, filenameFormat: '[title][ - ep]', sideBarOffset: 0, noLiveAutoplay: false, hideHomeLive: false, noMiniVideoAutoplay: false, useDefaultVideoSpeed: false, defaultVideoSpeed: '1.0', hideCategory: false, foldComment: true, downloadVideoDefaultDanmaku: '无', aria2RpcOption: { secretKey: '', dir: '', host: '127.0.0.1', port: '6800', method: 'get', skipByDefault: false, }, searchHistory: [], seedsToCoins: true, autoSeedsToCoins: true, lastSeedsToCoinsDate: 0, cache: {}, } const fixedSettings = { guiSettings: true, viewCover: true, notifyNewVersion: true, clearCache: true, downloadVideo: true, downloadDanmaku: true, downloadAudio: true, playerLayout: true, medalHelper: true, about: true, forceWide: false, useNewStyle: false, overrideNavBar: false, touchVideoPlayerAnimation: false, latestVersionLink: 'https://github.com/the1812/Bilibili-Evolved/raw/master/bilibili-evolved.user.js', currentVersion: GM_info.script.version, } const settingsChangeHandlers = {} function addSettingsListener (key, handler, initCall) { if (!settingsChangeHandlers[key]) { settingsChangeHandlers[key] = [handler] } else { settingsChangeHandlers[key].push(handler) } if (initCall) { const value = settings[key] handler(value, value) } } function removeSettingsListener (key, handler) { const handlers = settingsChangeHandlers[key] if (!handlers) { return } handlers.splice(handlers.indexOf(handler), 1) } function loadSettings () { for (const key in fixedSettings) { settings[key] = fixedSettings[key] GM_setValue(key, fixedSettings[key]) } if (Object.keys(languageCodeToName).includes(navigator.language)) { settings.i18n = true settings.i18nLanguage = languageCodeToName[navigator.language] } for (const key in settings) { let value = GM_getValue(key) if (value === undefined) { value = settings[key] GM_setValue(key, settings[key]) } else if (settings[key] !== undefined && value.constructor === Object) { value = Object.assign(settings[key], value) } Object.defineProperty(settings, key, { get () { return value }, set (newValue) { value = newValue GM_setValue(key, newValue) const handlers = settingsChangeHandlers[key] if (handlers) { if (key === 'useDarkStyle') { setTimeout(() => handlers.forEach(h => h(newValue, value)), 200) } else { handlers.forEach(h => h(newValue, value)) } } const input = document.querySelector(`input[key=${key}]`) if (input !== null) { if (input.type === 'checkbox') { input.checked = newValue } else if (input.type === 'text' && !input.parentElement.classList.contains('gui-settings-dropdown')) { input.value = newValue } } } }) } } function saveSettings (newSettings) { } function onSettingsChange () { console.warn('此功能已弃用.') } class Ajax { static send (xhr, body, text = true) { return new Promise((resolve, reject) => { xhr.addEventListener('load', () => { // if (xhr.status.toString().match(/^[45]/)) { // reject(xhr.status) // } else { resolve(text ? xhr.responseText : xhr.response) // } }) xhr.addEventListener('error', () => reject(xhr.status)) xhr.send(body) }) } static getBlob (url) { const xhr = new XMLHttpRequest() xhr.responseType = 'blob' xhr.open('GET', url) return this.send(xhr, undefined, false) } static getBlobWithCredentials (url) { const xhr = new XMLHttpRequest() xhr.responseType = 'blob' xhr.open('GET', url) xhr.withCredentials = true return this.send(xhr, undefined, false) } static async getJson (url) { return JSON.parse(await this.getText(url)) } static async getJsonWithCredentials (url) { return JSON.parse(await this.getTextWithCredentials(url)) } static getText (url) { const xhr = new XMLHttpRequest() xhr.open('GET', url) return this.send(xhr) } static getTextWithCredentials (url) { const xhr = new XMLHttpRequest() xhr.open('GET', url) xhr.withCredentials = true return this.send(xhr) } static postText (url, body) { const xhr = new XMLHttpRequest() xhr.open('POST', url) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') return this.send(xhr, body) } static postTextWithCredentials (url, body) { const xhr = new XMLHttpRequest() xhr.open('POST', url) xhr.withCredentials = true xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') return this.send(xhr, body) } static postJson (url, json) { const xhr = new XMLHttpRequest() xhr.open('POST', url) xhr.setRequestHeader('Content-Type', 'application/json') return this.send(xhr, JSON.stringify(json), false) } static postJsonWithCredentials (url, json) { const xhr = new XMLHttpRequest() xhr.open('POST', url) xhr.withCredentials = true xhr.setRequestHeader('Content-Type', 'application/json') return this.send(xhr, JSON.stringify(json), false) } static getHandlers (name) { name = name.toLowerCase() let handlers = Ajax[name] if (handlers === undefined) { handlers = Ajax[name] = [] } return handlers } static addEventListener (type, handler) { const handlers = Ajax.getHandlers(type) handlers.push(handler) } static removeEventListener (type, handler) { const handlers = Ajax.getHandlers(type) handlers.splice(handlers.indexOf(handler), 1) } } // https://github.com/the1812/Bilibili-Evolved/issues/84 function setupAjaxHook () { const original = { open: XMLHttpRequest.prototype.open, send: XMLHttpRequest.prototype.send } const fireHandlers = (name, thisArg, ...args) => Ajax.getHandlers(name).forEach(it => it.call(thisArg, ...args)) const hook = (name, thisArgs, ...args) => { fireHandlers('before' + name, thisArgs, ...args) const returnValue = original[name].call(thisArgs, ...args) fireHandlers('after' + name, thisArgs, ...args) return returnValue } const hookOnEvent = (name, thisArg) => { if (thisArg[name]) { const originalHandler = thisArg[name] thisArg[name] = (...args) => { fireHandlers('before' + name, thisArg, ...args) originalHandler.apply(thisArg, args) fireHandlers('after' + name, thisArg, ...args) } } else { thisArg[name] = (...args) => { fireHandlers('before' + name, thisArg, ...args) fireHandlers('after' + name, thisArg, ...args) } } } XMLHttpRequest.prototype.open = function (...args) { return hook('open', this, ...args) } XMLHttpRequest.prototype.send = function (...args) { hookOnEvent('onreadystatechange', this) hookOnEvent('onload', this) return hook('send', this, ...args) } } function loadResources () { Resource.root = 'https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/' Resource.all = {} Resource.displayNames = {} Resource.reloadables = [ 'useDarkStyle', 'hideBanner', 'customNavbar', 'playerShadow', 'narrowDanmaku', 'compactLayout', 'useCommentStyle', 'removeVideoTopMask', 'hideOldEntry', 'hideBangumiReviews', 'videoScreenshot', 'blurVideoControl', 'customControlBackground', 'harunaScale', 'removeLiveWatermark', 'framePlayback', 'hideCategory', ] for (const [key, data] of Object.entries(Resource.manifest)) { const resource = new Resource(data.path, { styles: data.styles, alwaysPreview: data.alwaysPreview }) resource.key = key resource.dropdown = data.dropdown if (data.displayNames) { resource.displayName = data.displayNames[key] Object.assign(Resource.displayNames, data.displayNames) } if (data.style) { const styleKey = key + 'Style' const style = Resource.all[styleKey] = new Resource(data.path.replace('.js', '.css'), { alwaysPreview: data.alwaysPreview }) style.key = styleKey switch (data.style) { case 'instant': { resource.styles.push(styleKey) break } case true: { resource.dependencies.push(style) break } case 'important': { resource.styles.push({ key: styleKey, important: true }) break } default: { if (typeof data.style === 'object') { resource.styles.push(Object.assign({ key: styleKey }, data.style)) } break } } } if (data.html === true) { const htmlKey = key + 'Html' const html = Resource.all[htmlKey] = new Resource(data.path.replace('.js', '.html'), { alwaysPreview: data.alwaysPreview }) html.key = htmlKey resource.dependencies.push(html) } Resource.all[key] = resource } for (const [key, data] of Object.entries(Resource.manifest)) { if (data.dependencies) { Resource.all[key].dependencies.push(...data.dependencies.map(name => Resource.all[name])) } } } // Placeholder class for Toast class Toast { constructor() { } show() { } dismiss() { } static show() { } static info() { } static success() { } static error() { } } class DoubleClickEvent { constructor(handler, singleClickHandler = null) { this.handler = handler; this.singleClickHandler = singleClickHandler; this.elements = []; this.clickedOnce = false; this.doubleClickHandler = e => { if (!this.clickedOnce) { this.clickedOnce = true; setTimeout(() => { if (this.clickedOnce) { this.clickedOnce = false; this.singleClickHandler && this.singleClickHandler(e); } }, 200); } else { this.clickedOnce = false; this.handler && this.handler(e); } }; } bind(element) { if (this.elements.indexOf(element) === -1) { this.elements.push(element); element.addEventListener("click", this.doubleClickHandler); } } unbind(element) { const index = this.elements.indexOf(element); if (index === -1) { return; } this.elements.splice(index, 1); element.removeEventListener("click", this.doubleClickHandler); } } let cidHooked = false const videoChangeCallbacks = [] class Observer { constructor (element, callback) { this.element = element this.callback = callback this.observer = null this.options = undefined } start () { if (this.element) { this.observer = new MutationObserver(this.callback) this.observer.observe(this.element, this.options) } return this } stop () { this.observer && this.observer.disconnect() return this } static observe (selector, callback, options) { callback([]) let elements = selector if (typeof selector === 'string') { elements = [...document.querySelectorAll(selector)] } else if (!Array.isArray(selector)) { elements = [selector] } return elements.map( it => { const observer = new Observer(it, callback) observer.options = options return observer.start() }) } static childList (selector, callback) { return Observer.observe(selector, callback, { childList: true, subtree: false, attributes: false }) } static childListSubtree (selector, callback) { return Observer.observe(selector, callback, { childList: true, subtree: true, attributes: false }) } static attributes (selector, callback) { return Observer.observe(selector, callback, { childList: false, subtree: false, attributes: true }) } static attributesSubtree (selector, callback) { return Observer.observe(selector, callback, { childList: false, subtree: true, attributes: true }) } static all (selector, callback) { return Observer.observe(selector, callback, { childList: true, subtree: true, attributes: true }) } static async videoChange (callback) { const cid = await SpinQuery.select(() => unsafeWindow.cid) if (cid === null) { return } if (!cidHooked) { let hookedCid = cid Object.defineProperty(unsafeWindow, 'cid', { get () { return hookedCid }, set (newId) { hookedCid = newId if (!Array.isArray(newId)) { videoChangeCallbacks.forEach(it => it()) } } }) cidHooked = true } // callback(); const videoContainer = await SpinQuery.select('#bofqi video') if (videoContainer) { Observer.childList(videoContainer, callback) } else { callback() } videoChangeCallbacks.push(callback) } } class SpinQuery { constructor (query, condition, action, failed) { this.maxRetry = 15 this.retry = 0 this.queryInterval = 1000 this.query = query this.condition = condition this.action = action this.failed = failed } start () { this.tryQuery(this.query, this.condition, this.action, this.failed) } tryQuery (query, condition, action, failed) { if (this.retry < this.maxRetry) { const result = query() if (condition(result)) { action(result) } else { if (document.hasFocus()) { this.retry++ } setTimeout(() => this.tryQuery(query, condition, action, failed), this.queryInterval) } } else { typeof failed === 'function' && failed() } } static condition (query, condition, action, failed) { if (action !== undefined) { new SpinQuery(query, condition, action, failed).start() } else { return new Promise((resolve) => { new SpinQuery(query, condition, it => resolve(it), () => resolve(null)).start() }) } } static select (query, action, failed) { if (typeof query === 'string') { const selector = query query = () => document.querySelector(selector) } return SpinQuery.condition(query, it => it !== null && it !== undefined, action, failed) } static any (query, action, failed) { if (typeof query === 'string') { const selector = query query = () => $(selector) } return SpinQuery.condition(query, it => it.length > 0, action, failed) } static count (query, count, action, failed) { if (typeof query === 'string') { const selector = query query = () => document.querySelectorAll(selector) } return SpinQuery.condition(query, it => it.length === count, action, failed) } static unsafeJquery (action, failed) { return SpinQuery.condition(() => unsafeWindow.$, jquery => jquery !== undefined, action, failed) } } class ColorProcessor { constructor(hex) { this.hex = hex; } get rgb() { return this.hexToRgb(this.hex); } get rgba() { return this.hexToRgba(this.hex); } getHexRegex(alpha, shorthand) { const repeat = shorthand ? "" : "{2}"; const part = `([a-f\\d]${repeat})`; const count = alpha ? 4 : 3; const pattern = `#?${part.repeat(count)}`; return new RegExp(pattern, "ig"); } hexToRgbOrRgba(hex, alpha) { const isShortHand = hex.length < 6; if (isShortHand) { const shorthandRegex = this.getHexRegex(alpha, true); hex = hex.replace(shorthandRegex, function (...args) { let result = ""; let i = 1; while (args[i]) { result += args[i].repeat(2); i++; } return result; }); } const regex = this.getHexRegex(alpha, false); const regexResult = regex.exec(hex); if (regexResult) { const color = { r: parseInt(regexResult[1], 16), g: parseInt(regexResult[2], 16), b: parseInt(regexResult[3], 16), }; if (regexResult[4]) { color.a = parseInt(regexResult[4], 16) / 255; } return color; } else if (alpha) { const rgb = this.hexToRgbOrRgba(hex, false); if (rgb) { rgb.a = 1; return rgb; } } return null; } hexToRgb(hex) { return this.hexToRgbOrRgba(hex, false); } hexToRgba(hex) { return this.hexToRgbOrRgba(hex, true); } rgbToString(color) { if (color.a) { return `rgba(${color.r},${color.g},${color.b},${color.a})`; } return `rgb(${color.r},${color.g},${color.b})`; } rgbToHsb(rgb) { const { r, g, b, } = rgb; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; const s = Math.round((max === 0 ? 0 : delta / max) * 100); const v = Math.round(max / 255 * 100); let h; if (delta === 0) { h = 0; } else if (r === max) { h = (g - b) / delta % 6; } else if (g === max) { h = (b - r) / delta + 2; } else if (b === max) { h = (r - g) / delta + 4; } h = Math.round(h * 60); if (h < 0) { h += 360; } return { h: h, s: s, b: v, }; } get hsb() { return this.rgbToHsb(this.rgb); } get grey() { const color = this.rgb; return 1 - (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255; } get foreground() { const color = this.rgb; if (color && this.grey < 0.35) { return "#000"; } return "#fff"; } makeImageFilter(originalRgb) { const { h, s, } = this.rgbToHsb(originalRgb); const targetColor = this.hsb; const hue = targetColor.h - h; const saturate = ((targetColor.s - s) / 100 + 1) * 100; // const brightness = ((targetColor.b - b) / 100 + 1) * 100; const filter = `hue-rotate(${hue}deg) saturate(${saturate}%)`; return filter; } get blueImageFilter() { const blueColor = { r: 0, g: 160, b: 213, }; return this.makeImageFilter(blueColor); } get pinkImageFilter() { const pinkColor = { r: 251, g: 113, b: 152, }; return this.makeImageFilter(pinkColor); } get brightness() { return `${this.foreground === "#000" ? "100" : "0"}%`; } get filterInvert() { return this.foreground === "#000" ? "invert(0)" : "invert(1)"; } } // [Offline build placeholder] class ResourceType { constructor(name, preprocessor) { this.name = name; this.preprocessor = preprocessor || (text => text); } static fromUrl(url) { if (url.indexOf(".css") !== -1) { return this.style; } else if (url.indexOf(".html") !== -1 || url.indexOf(".htm") !== -1) { return this.html; } else if (url.indexOf(".js") !== -1) { return this.script; } else if (url.indexOf(".txt") !== -1) { return this.text; } else { return this.unknown; } } static get style() { return new ResourceType("style"); } static get html() { return new ResourceType("html"); } static get script() { return new ResourceType("script"); } static get text() { return new ResourceType("text"); } static get unknown() { return new ResourceType("unknown"); } } class Resource { get downloaded() { return this.text !== null; } constructor(url, { styles = [], alwaysPreview = false } = {}) { this.rawUrl = Resource.root + "min/" + url; this.dependencies = []; // this.priority = priority; this.styles = styles; this.text = null; this.key = null; this.alwaysPreview = alwaysPreview; this.type = ResourceType.fromUrl(url); this.displayName = ""; } get url() { if (typeof offlineData === "undefined" && this.alwaysPreview) { return this.rawUrl.replace("/master/", "/preview/"); } return this.rawUrl; } flatMapPolyfill() { if (Array.prototype.flatMap === undefined) { const flatMap = function (mapFunc) { return this .map(mapFunc) .reduce((acc, it) => acc.concat(it), []); }; return flatMap; } else { return Array.prototype.flatMap; } } loadCache() { const key = this.key; if (!settings.cache || !settings.cache[key]) { return null; } else { return settings.cache[key]; } } async download() { const key = this.key; return new Promise((resolve, reject) => { if (this.downloaded) { resolve(this.text); } else { const flattenStyles = this.flatMapPolyfill() .bind(this.styles)(it => typeof it === "object" ? it.key : it); Promise.all(this.dependencies .concat(flattenStyles.map(it => Resource.all[it])) .map(r => r.download()) ) .then(() => { // +#Offline build placeholder if (settings.useCache) { const cache = this.loadCache(key); if (cache !== null) { this.text = cache; resolve(cache); } Ajax.getText(this.url).then(text => { this.text = this.type.preprocessor(text); if (text === null) { reject("download failed"); } if (cache !== this.text) { if (cache === null) { resolve(this.text); } if (typeof offlineData === "undefined") { settings.cache = Object.assign(settings.cache, { [key]: this.text }); saveSettings(settings); } } }).catch(error => reject(error)); } else { Ajax.getText(this.url) .then(text => { this.text = this.type.preprocessor(text); resolve(this.text); }) .catch(error => reject(error)); } // -#Offline build placeholder }); } }); } getStyle(id) { const style = this.text; if (style === null) { logError("Attempt to get style which is not downloaded."); } // let attributes = `id='${id}'`; // if (this.priority !== undefined) // { // attributes += ` priority='${this.priority}'`; // } // return ``; const styleElement = document.createElement("style"); styleElement.id = id; styleElement.innerText = style; return styleElement; } getPriorStyle() { if (this.priority !== undefined) { let insertPosition = this.priority - 1; let formerStyle = $(`style[priority='${insertPosition}']`); while (insertPosition >= 0 && formerStyle.length === 0) { formerStyle = $(`style[priority='${insertPosition}']`); insertPosition--; } if (insertPosition < 0) { return null; } else { return formerStyle; } } else { return null; } } applyStyle(id, important) { if (!document.querySelector(`#${id}`)) { const style = this.getStyle(id); // const priorStyle = this.getPriorStyle(); // if (priorStyle === null) // { // if (important) // { // $("html").append(element); // } // else // { // $("head").prepend(element); // } // } // else // { // priorStyle.after(element); // } if (important) { document.body.insertAdjacentElement("beforeend", style); } else { document.head.insertAdjacentElement("afterbegin", style); } } } } Resource.manifest = { style: { path: 'style.min.css' }, oldStyle: { path: 'old.min.css' }, scrollbarStyle: { path: 'scrollbar.min.css' }, darkStyle: { path: 'dark.min.css', alwaysPreview: true }, darkStyleImportant: { path: 'dark-important.min.css', alwaysPreview: true }, darkStyleNavBar: { path: 'dark-navbar.min.css', alwaysPreview: true }, touchPlayerStyle: { path: 'touch-player.min.css' }, navbarOverrideStyle: { path: 'override-navbar.min.css' }, noBannerStyle: { path: 'no-banner.min.css' }, imageViewerStyle: { path: 'image-viewer.min.css' }, imageViewerHtml: { path: 'image-viewer.min.html' }, iconsStyle: { path: 'icons.min.css' }, settingsSideBar: { path: 'settings-side-bar.min.js' }, textValidate: { path: 'text-validate.min.js' }, themeColors: { path: 'theme-colors.min.js' }, settingsTooltipStyle: { path: 'settings-tooltip.min.css' }, settingsTooltipJapanese: { path: 'settings-tooltip.ja-JP.min.js' }, settingsTooltipChinese: { path: 'settings-tooltip.zh-CN.min.js' }, settingsTooltipEnglish: { path: 'settings-tooltip.en-US.min.js' }, settingsTooltip: { path: 'settings-tooltip.loader.min.js', dependencies: [ 'settingsTooltipStyle' ] }, settingsSearch: { path: 'settings-search.min.js' }, guiSettings: { path: 'gui-settings.min.js', html: true, style: 'instant', dependencies: [ 'textValidate', 'settingsSideBar', 'themeColors', 'settingsTooltip', 'settingsSearch' ], styles: [ { key: 'iconsStyle', important: true } ], displayNames: { guiSettings: '设置', blurSettingsPanel: '模糊设置面板背景', clearCache: '清除缓存', settingsTooltip: '设置项帮助', settingsSearch: '搜索设置', sideBarOffset: '侧栏垂直偏移量' } }, useDarkStyle: { path: 'dark-styles.min.js', alwaysPreview: true, styles: [ 'darkStyle', 'scrollbarStyle', { key: 'darkStyleNavBar', important: true, condition () { return !settings.useNewStyle && ($('#banner_link').length === 0 || $('#banner_link').length > 0 && settings.overrideNavBar && !settings.showBanner) } }, { key: 'darkStyleImportant', important: true, condition: () => true } ], displayNames: { useDarkStyle: '夜间模式' } }, tweetsStyle: { path: 'tweets.min.css' }, useNewStyle: { path: 'new-styles.min.js', dependencies: [ 'style', 'oldStyle' ], styles: [ 'tweetsStyle', { key: 'scrollbarStyle', condition: () => document.URL !== `https://h.bilibili.com/` } ], displayNames: { useNewStyle: '样式调整', blurBackgroundOpacity: '顶栏(对横幅)透明度' } }, hideBanner: { path: 'hide-banner.min.js', style: true, displayNames: { hideBanner: '隐藏顶部横幅' } }, touchNavBar: { path: 'touch-navbar.min.js', displayNames: { touchNavBar: '顶栏触摸优化' } }, touchVideoPlayer: { path: 'touch-player.min.js', styles: [ 'touchPlayerStyle' ], displayNames: { touchVideoPlayer: '播放器触摸支持', touchVideoPlayerAnimation: '启用实验性动画效果', touchVideoPlayerDoubleTapControl: '启用双击控制' } }, expandDanmakuList: { path: 'expand-danmaku.min.js', displayNames: { expandDanmakuList: '自动展开弹幕列表' } }, removeAds: { path: 'remove-promotions.min.js', style: 'instant', displayNames: { removeAds: '删除广告', showBlockedAdsTip: '显示占位文本', } }, watchLaterRedirect: { path: 'watchlater.min.js', displayNames: { watchLaterRedirect: '稍后再看重定向' } }, hideTopSearch: { path: 'hide-top-search.min.js', displayNames: { hideTopSearch: '隐藏搜索推荐' } }, harunaScale: { path: 'haruna-scale.min.js', displayNames: { harunaScale: '缩放直播看板娘' } }, removeLiveWatermark: { path: 'remove-watermark.min.js', displayNames: { removeLiveWatermark: '删除直播水印' } }, fullTweetsTitle: { path: 'full-tweets-title.min.js', style: 'instant', displayNames: { fullTweetsTitle: '展开动态标题' } }, fullPageTitle: { path: 'full-page-title.min.js', style: 'instant', displayNames: { fullPageTitle: '展开选集标题' } }, viewCover: { path: 'view-cover.min.js', dependencies: [ 'imageViewerHtml', 'videoInfo', 'title' ], styles: [ 'imageViewerStyle' ], displayNames: { viewCover: '查看封面' } }, notifyNewVersion: { path: 'notify-new-version.min.js', displayNames: { notifyNewVersion: '检查更新' } }, toast: { path: 'toast.min.js', style: 'instant', displayNames: { toast: '显示消息', toastInternalError: '显示内部错误消息' } }, removeVideoTopMask: { path: 'remove-top-mask.min.js', displayNames: { removeVideoTopMask: '删除视频标题层' } }, blurVideoControl: { path: 'blur-video-control.min.js', style: 'instant', displayNames: { blurVideoControl: '模糊视频控制栏背景' } }, darkSchedule: { path: 'dark-schedule.min.js', displayNames: { darkSchedule: '夜间模式计划时段', darkScheduleStart: '起始时间', darkScheduleEnd: '结束时间' } }, clearCache: { path: 'clear-cache.min.js', displayNames: { useCache: '启用缓存' } }, downloadVideo: { path: 'download-video.min.js', html: true, style: 'instant', dependencies: ['title'], displayNames: { 'downloadVideo': '下载视频', 'batchDownload': '批量下载', 'aria2Rpc': 'aria2 RPC', } }, downloadDanmaku: { path: 'download-danmaku.min.js', dependencies: [ 'title', 'videoInfo', 'danmakuConverter' ], displayNames: { 'downloadDanmaku': '下载弹幕' } }, danmakuConverter: { path: 'danmaku-converter.min.js' }, videoInfo: { path: 'video-info.min.js' }, videoStory: { path: 'video-story.min.js' }, about: { path: 'about.min.js', html: true, style: 'important', displayNames: { 'about': '关于' } }, customControlBackground: { path: 'custom-control-background.min.js', style: { key: 'customControlBackgroundStyle', condition: () => settings.customControlBackgroundOpacity > 0 }, displayNames: { customControlBackground: '控制栏着色', customControlBackgroundOpacity: '不透明度' } }, useDefaultPlayerMode: { path: 'default-player-mode.min.js', displayNames: { useDefaultPlayerMode: '使用默认播放器模式', defaultPlayerMode: '默认播放器模式', autoLightOff: '播放时自动关灯', applyPlayerModeOnPlay: '播放时应用模式' }, dropdown: { key: 'defaultPlayerMode', items: ['常规', '宽屏', '网页全屏', '全屏'] } }, useDefaultVideoQuality: { path: 'default-video-quality.min.js', displayNames: { useDefaultVideoQuality: '使用默认视频画质', defaultVideoQuality: '画质设定' }, dropdown: { key: 'defaultVideoQuality', items: ['1080P60', '1080P+', '1080P', '720P60', '720P', '480P', '360P', '自动'] } }, comboLike: { path: 'combo-like.min.js', displayNames: { comboLike: '素质三连触摸支持' } }, autoContinue: { path: 'auto-continue.min.js', displayNames: { autoContinue: '自动从历史记录点播放', allowJumpContinue: '允许跨集跳转' } }, expandDescription: { path: 'expand-description.min.js', style: 'instant', displayNames: { expandDescription: '自动展开视频简介' } }, defaultDanmakuSettingsStyle: { path: 'default-danmaku-settings.min.css' }, useDefaultDanmakuSettings: { path: 'default-danmaku-settings.min.js', styles: [ { key: 'defaultDanmakuSettingsStyle', condition: () => settings.rememberDanmakuSettings } ], displayNames: { useDefaultDanmakuSettings: '使用默认弹幕设置', enableDanmaku: '开启弹幕', rememberDanmakuSettings: '记住弹幕设置' } }, skipChargeList: { path: 'skip-charge-list.min.js', style: 'instant', displayNames: { skipChargeList: '跳过充电鸣谢' } }, playerLayout: { path: 'default-player-layout.min.js', displayNames: { useDefaultPlayerLayout: '指定播放器布局', defaultPlayerLayout: '视频区布局', defaultBangumiLayout: '番剧区布局' }, dropdown: [ { key: 'defaultPlayerLayout', items: ['旧版', '新版'] }, { key: 'defaultBangumiLayout', items: ['旧版', '新版'] } ] }, compactLayout: { path: 'compact-layout.min.js', style: true, displayNames: { compactLayout: '首页使用紧凑布局' } }, medalHelper: { path: 'medal-helper.min.js', html: true, style: 'instant', displayNames: { medalHelper: '直播勋章快速更换' } }, showDeadVideoTitle: { path: 'show-dead-video-title.min.js', displayNames: { showDeadVideoTitle: '显示失效视频信息', useBiliplusRedirect: '失效视频重定向', deadVideoTitleProvider: '信息来源', }, dropdown: { key: 'deadVideoTitleProvider', items: ['稍后再看'], }, }, autoPlay: { path: 'auto-play.min.js', displayNames: { autoPlay: '自动播放视频' } }, useCommentStyle: { path: 'comment.min.js', style: 'important', displayNames: { useCommentStyle: '简化评论区' } }, title: { path: 'title.min.js', displayNames: { filenameFormat: '文件命名格式' } }, imageResolution: { path: 'image-resolution.min.js', displayNames: { imageResolution: '高分辨率图片' } }, biliplusRedirect: { path: 'biliplus-redirect.min.js', displayNames: { biliplusRedirect: 'BiliPlus跳转支持' } }, framePlayback: { path: 'frame-playback.min.js', style: 'instant', html: true, displayNames: { framePlayback: '启用逐帧调整' } }, downloadAudio: { path: 'download-audio.min.js', displayNames: { downloadAudio: '下载音频' } }, i18nEnglish: { path: 'i18n.en-US.min.js', alwaysPreview: true }, i18nJapanese: { path: 'i18n.ja-JP.min.js', alwaysPreview: true }, i18nTraditionalChinese: { path: 'i18n.zh-TW.min.js', alwaysPreview: true }, i18nGerman: { path: 'i18n.de-DE.min.js', alwaysPreview: true }, i18n: { path: 'i18n.min.js', alwaysPreview: true, style: 'important', displayNames: { i18n: '界面翻译', i18nLanguage: '语言', i18nEnglish: '英语翻译模块', i18nJapanese: '日语翻译模块', i18nGerman: '德语翻译模块', i18nTraditionalChinese: '繁体翻译模块' }, dropdown: { key: 'i18nLanguage', // items: Object.keys(languageCodeMap), items: [`日本語`, `English`] } }, playerFocus: { path: 'player-focus.min.js', displayNames: { playerFocus: '自动定位到播放器', playerFocusOffset: '定位偏移量' } }, simplifyLiveroom: { path: 'simplify-liveroom.min.js', style: 'important', displayNames: { simplifyLiveroom: '简化直播间' } }, oldTweets: { path: 'old-tweets.min.js', displayNames: { oldTweets: '旧版动态跳转支持' } }, customNavbar: { path: 'custom-navbar.min.js', style: 'instant', html: true, displayNames: { customNavbar: '使用自定义顶栏', customNavbarFill: '主题色填充', customNavbarShadow: '投影', customNavbarCompact: '紧凑布局', customNavbarBlur: '背景模糊', customNavbarBlurOpacity: '模糊层不透明度', allNavbarFill: '填充其他顶栏' } }, favoritesRedirect: { path: 'favorites-redirect.min.js', displayNames: { favoritesRedirect: '收藏夹视频重定向' } }, outerWatchlater: { path: 'outer-watchlater.min.js', style: 'important', displayNames: { outerWatchlater: '外置稍后再看' } }, playerShadow: { path: 'player-shadow.min.js', displayNames: { playerShadow: '播放器投影' } }, narrowDanmaku: { path: 'narrow-danmaku.min.js', displayNames: { narrowDanmaku: '强制保留弹幕栏' } }, hideOldEntry: { path: 'hide-old-entry.min.js', displayNames: { hideOldEntry: '隐藏返回旧版' } }, batchDownload: { path: 'batch-download.min.js' }, slip: { path: 'slip.min.js', displayNames: { slip: 'Slip.js' } }, debounce: { path: 'debounce.min.js', displayNames: { slip: 'debounce.js' } }, videoScreenshot: { path: 'screenshot.min.js', style: true, displayNames: { videoScreenshot: '启用视频截图' }, dependencies: [ 'title' ] }, hideBangumiReviews: { path: 'hide-bangumi-reviews.min.js', displayNames: { hideBangumiReviews: '隐藏番剧点评' } }, noLiveAutoplay: { path: 'no-live-autoplay.min.js', displayNames: { noLiveAutoplay: '禁止直播首页自动播放', hideHomeLive: '隐藏首页推荐直播', } }, noMiniVideoAutoplay: { path: 'no-mini-video-autoplay.min.js', displayNames: { noMiniVideoAutoplay: '禁止小视频自动播放', } }, hideCategory: { path: 'hide-category.min.js', displayNames: { hideCategory: '隐藏分区栏', }, }, foldComment: { path: 'fold-comment.min.js', style: true, displayNames: { foldComment: '快速收起动态评论区', }, }, useDefaultVideoSpeed: { path: 'default-video-speed.min.js', displayNames: { useDefaultVideoSpeed: '使用默认播放速度', defaultVideoSpeed: '默认播放速度', }, dropdown: { key: 'defaultVideoSpeed', items: ['0.5', '0.75', '1.0', '1.25', '1.5', '2.0'], } }, aria2Rpc: { path: 'aria2-rpc.min.js', }, seedsToCoins: { path: 'seeds-to-coins.min.js', displayNames: { seedsToCoins: '瓜子换硬币', autoSeedsToCoins: '自动运行', }, }, } const resourceManifest = Resource.manifest class StyleManager { constructor(resources) { this.resources = resources; } getDefaultStyleId(key) { return key.replace(/([a-z][A-Z])/g, g => `${g[0]}-${g[1].toLowerCase()}`); } applyStyle(key, id) { if (id === undefined) { id = this.getDefaultStyleId(key); } Resource.all[key].applyStyle(id, false); } removeStyle(key) { const style = document.querySelector(`#${this.getDefaultStyleId(key)}`); style && style.remove(); } applyImportantStyle(key, id) { if (id === undefined) { id = this.getDefaultStyleId(key); } Resource.all[key].applyStyle(id, true); } applyStyleFromText(text, id) { if (!id) { document.head.insertAdjacentHTML("afterbegin", text); } else { const style = document.createElement("style"); style.id = id; style.innerText = text; document.head.insertAdjacentElement("afterbegin", style); } } applyImportantStyleFromText(text, id) { if (!id) { document.body.insertAdjacentHTML("beforeend", text); } else { const style = document.createElement("style"); style.id = id; style.innerText = text; document.body.insertAdjacentElement("beforeend", style); } } getStyle(key, id) { return Resource.all[key].getStyle(id); } fetchStyleByKey(key) { if (settings[key] !== true) { return; } Resource.all[key].styles .filter(it => it.condition !== undefined ? it.condition() : true) .forEach(it => { const important = typeof it === "object" ? it.important : false; const key = typeof it === "object" ? it.key : it; Resource.all[key].download().then(() => { if (important) { contentLoaded(() => this.applyImportantStyle(key)); } else { this.applyStyle(key); } }); }); } prefetchStyles() { for (const key in Resource.all) { if (typeof offlineData !== "undefined" || settings.useCache && settings.cache[key]) { this.fetchStyleByKey(key); } } } } class ResourceManager { constructor () { this.data = Resource.all this.skippedImport = [] this.attributes = {} this.styleManager = new StyleManager(this) const styleMethods = Object.getOwnPropertyNames(StyleManager.prototype).filter(it => it !== 'constructor') for (const key of styleMethods) { this[key] = function (...params) { this.styleManager[key](...params) } } this.setupColors() } setupColors () { this.color = new ColorProcessor(settings.customStyleColor) settings.foreground = this.color.foreground settings.blueImageFilter = this.color.blueImageFilter settings.pinkImageFilter = this.color.pinkImageFilter settings.brightness = this.color.brightness settings.filterInvert = this.color.filterInvert const hexToRgba = input => this.color.rgbToString(this.color.hexToRgba(input)) let styles = [] styles.push('--theme-color:' + settings.customStyleColor) for (let opacity = 10; opacity <= 90; opacity += 10) { const color = this.color.hexToRgba(settings.customStyleColor) color.a = opacity / 100 styles.push(`--theme-color-${opacity}:` + this.color.rgbToString(color)) } styles.push('--foreground-color:' + settings.foreground) styles.push('--foreground-color-b:' + hexToRgba(settings.foreground + 'b')) styles.push('--foreground-color-d:' + hexToRgba(settings.foreground + 'd')) styles.push('--blue-image-filter:' + settings.blueImageFilter) styles.push('--pink-image-filter:' + settings.pinkImageFilter) styles.push('--brightness:' + settings.brightness) styles.push('--invert-filter:' + settings.filterInvert) styles.push('--blur-background-opacity:' + settings.blurBackgroundOpacity) // styles.push("--custom-control-background-opacity:" + settings.customControlBackgroundOpacity); this.applyStyleFromText(`html{${styles.join(';')}}`, 'bilibili-evolved-variables') } resolveComponentName (componentName) { const keyword = '/' + componentName.replace('./', '').replace('../', '') + '.min.js' for (const [name, value] of Object.entries(Resource.all)) { if (value.url.endsWith(keyword)) { return name } } return componentName } resolveComponent (componentName) { const resource = Resource.all[this.resolveComponentName(componentName)] if (!resource) { this.skippedImport.push(componentName) } return resource } importAsync (componentName) { return new Promise(resolve => { const resource = this.resolveComponent(componentName) if (!resource) { resolve(unsafeWindow.bilibiliEvolved) } if (!Object.keys(this.attributes).includes(resource.key)) { if (resource.type.name === 'html' || resource.type.name === 'style') { resource.download().then(() => resolve(this.import(componentName))) } else { this.fetchByKey(resource.key).then(() => resolve(this.import(componentName))) } } else { resolve(this.import(componentName)) } }) } import (componentName) { const resource = this.resolveComponent(componentName) if (!resource) { return unsafeWindow.bilibiliEvolved } if (resource.type.name === 'html' || resource.type.name === 'style') { if (!resource.downloaded) { console.error(`Import failed: component "${componentName}" is not loaded.`) return null } return resource.text } else { const attribute = this.attributes[this.resolveComponentName(componentName)] if (attribute === undefined) { console.error(`Import failed: component "${componentName}" is not loaded.`) return null } return attribute.export } } async fetchByKey (key) { const resource = Resource.all[key] if (!resource) { return null } const text = await resource.download().catch(reason => { console.error(`Download error, XHR status: ${reason}`) let toastMessage = `无法下载组件${Resource.all[key].displayName}` if (settings.toastInternalError) { toastMessage += '\n' + reason } Toast.error(toastMessage, '错误') }) await Promise.all(resource.dependencies .filter(it => it.type.name === 'style') .map(it => this.styleManager.fetchStyleByKey(it.key))) await Promise.all(resource.dependencies .filter(it => it.type.name === 'script') .map(it => this.fetchByKey(it.key))) this.applyComponent(key, text) } async fetch () { const isCacheValid = this.validateCache() let loadingToast = null if (settings.toast === true) { await this.fetchByKey('toast') unsafeWindow.bilibiliEvolved.Toast = Toast = this.attributes.toast.export.Toast || this.attributes.toast.export if (!isCacheValid && settings.useCache) { loadingToast = Toast.info(/* html */`
正在初始化脚本`, '初始化') } } const promises = [] for (const key in settings) { if (settings[key] === true && key !== 'toast') { const promise = this.fetchByKey(key) if (promise) { promises.push(promise) } } } await Promise.all(promises) saveSettings(settings) if (loadingToast) { loadingToast.dismiss() } this.applyReloadables() // reloadables run sync // await this.applyDropdownOptions(); this.applyWidgets() // No need to wait the widgets } applyReloadables () { const checkAttribute = (key, attributes) => { if (attributes.reload && attributes.unload) { addSettingsListener(key, newValue => { if (newValue === true) { attributes.reload() } else { attributes.unload() } }) } } for (const key of Resource.reloadables) { const attributes = this.attributes[key] if (attributes === undefined) { const fetchListener = async newValue => { if (newValue === true) { await this.styleManager.fetchStyleByKey(key) await this.fetchByKey(key) removeSettingsListener(key, fetchListener) checkAttribute(key, this.attributes[key]) } } addSettingsListener(key, fetchListener) } else { checkAttribute(key, attributes) } } } applyComponent (key, text) { const func = eval(text) if (func) { try { const attribute = func(settings, this) || {} this.attributes[key] = attribute } catch (error) { console.error(`Failed to apply feature "${key}": ${error}`) let toastMessage = `加载组件${Resource.all[key].displayName}失败` if (settings.toastInternalError) { toastMessage += '\n' + error } Toast.error(toastMessage, '错误') } } } async applyWidget (info) { let condition = true if (typeof info.condition === 'function') { condition = info.condition() if (typeof condition === 'object' && 'then' in condition) { condition = await condition.catch(() => { return false }) } } if (condition === true) { if (info.content) { document.querySelector('.widgets-container').insertAdjacentHTML('beforeend', info.content) } if (info.success) { info.success() } } } async applyWidgets () { await Promise.all(Object.values(this.attributes) .filter(it => it.widget) .map(it => this.applyWidget(it.widget)) ) } async applyDropdownOptions () { async function applyDropdownOption (info) { if (Array.isArray(info)) { await Promise.all(info.map(applyDropdownOption)) } else { const dropdownInput = dq(`.gui-settings-dropdown input[key=${info.key}]`) dropdownInput.value = settings[info.key] dropdownInput.setAttribute('data-name', settings[info.key]) const dropdown = dropdownInput.parentElement const list = dropdown.querySelector('ul') const input = dropdown.querySelector('input') info.items.forEach(itemHtml => { list.insertAdjacentHTML('beforeend', `
  • ${itemHtml}
  • `) }) list.querySelectorAll('li').forEach(li => li.addEventListener('click', () => { input.value = li.innerText input.setAttribute('data-name', li.getAttribute('data-name')) settings[info.key] = li.getAttribute('data-name') })) } } const manifests = Object.values(Resource.manifest).filter(it => it.dropdown).map(it => it.dropdown) Object.values(Resource.all) // .concat(Object.values(this.attributes)) .filter(it => it.dropdown) .map(it => it.dropdown) .forEach(it => { if (!manifests.some(m => m.key === it.key)) { manifests.push(it) } }) await Promise.all(manifests.map(it => applyDropdownOption(it))) } toggleStyle (content, id) { if (id === undefined) { // content is resource name this.styleManager.applyStyle(content) return { reload: () => this.styleManager.applyStyle(content), unload: () => this.styleManager.removeStyle(content) } } else { // content is style text this.styleManager.applyStyleFromText(content, id) return { reload: () => this.styleManager.applyStyleFromText(content, id), unload: () => document.getElementById(id).remove() } } } validateCache () { if (typeof offlineData !== 'undefined') { // offline version always has cache return true } if (Object.getOwnPropertyNames(settings.cache).length === 0) { // has no cache return false } if (settings.cache.version === undefined) { // Has newly downloaded cache settings.cache = Object.assign(settings.cache, { version: settings.currentVersion }) // settings.cache.version = settings.currentVersion; saveSettings(settings) return true } if (settings.cache.version !== settings.currentVersion) { // Has old version cache settings.cache = {} saveSettings(settings) return false } return true // Has cache } } try { Vue.config.productionTip = false Vue.config.devtools = false setupAjaxHook() const events = {} for (const name of ['init', 'styleLoaded', 'scriptLoaded']) { events[name] = { completed: false, subscribers: [], complete () { this.completed = true this.subscribers.forEach(it => it()) } } } if (unsafeWindow.bilibiliEvolved === undefined) { unsafeWindow.bilibiliEvolved = { addons: [] } } Object.assign(unsafeWindow.bilibiliEvolved, { subscribe (type, callback) { const event = events[type] if (callback) { if (event && !event.completed) { event.subscribers.push(callback) } else { callback() } } else { return new Promise((resolve) => this.subscribe(type, () => resolve())) } } }) contentLoaded(() => { document.body.classList.add('round-corner') }) loadResources() loadSettings() const resources = new ResourceManager() events.init.complete() resources.styleManager.prefetchStyles() events.styleLoaded.complete() Object.assign(unsafeWindow.bilibiliEvolved, { SpinQuery, Toast, Observer, DoubleClickEvent, ColorProcessor, StyleManager, ResourceManager, Resource, ResourceType, Ajax, resourceManifest, loadSettings, saveSettings, onSettingsChange, logError, raiseEvent, loadLazyPanel, contentLoaded, fixed, settings, settingsChangeHandlers, addSettingsListener, removeSettingsListener, isEmbeddedPlayer, isIframe, resources, theWorld: waitTime => { if (waitTime > 0) { setTimeout(() => { debugger }, waitTime) } else { debugger } }, monkeyInfo: GM_info, monkeyApis: { getValue: GM_getValue, setValue: GM_setValue, setClipboard: GM_setClipboard, addValueChangeListener: () => console.warn('此功能已弃用.') } }) const applyScripts = () => resources.fetch() .then(() => { events.scriptLoaded.complete() const addons = new Proxy(unsafeWindow.bilibiliEvolved.addons || [], { apply: function (target, thisArg, argumentsList) { return thisArg[target].apply(this, argumentsList) }, set: function (target, property, value) { if (target[property] === undefined) { resources.applyWidget(value) } target[property] = value return true } }) addons.forEach(it => resources.applyWidget(it)) Object.assign(unsafeWindow.bilibiliEvolved, { addons }) }) .catch(error => logError(error)) contentLoaded(applyScripts) } catch (error) { logError(error) }