// ==UserScript== // @name Bilibili Evolved // @version 1.8.20 // @description 强大的哔哩哔哩增强脚本: 下载视频, 音乐, 封面, 弹幕 / 自定义播放器画质, 模式, 布局 / 自定义顶栏, 删除广告, 夜间模式 / 触屏设备支持 // @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 // @grant GM_xmlhttpRequest // @connect * // @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 formatDuration = (time, fixed = 0) => { const second = (time % 60).toFixed(fixed) const minute = (Math.trunc(time / 60) % 60).toString() const hour = Math.trunc(time / 3600).toString() if (hour === '0') { return `${minute.padStart(2, '0')}:${second.padStart(2, '0')}` } return `${hour}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}` } const getDpiSourceSet = (src, baseSize, extension = 'jpg') => { const dpis = [1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4] return dpis.map(dpi => { if (typeof baseSize === 'object') { if ('width' in baseSize && 'height' in baseSize) { return `${src}@${Math.trunc(baseSize.width * dpi)}w_${Math.trunc(baseSize.height * dpi)}h.${extension} ${dpi}x` } else if ('width' in baseSize) { return `${src}@${Math.trunc(baseSize.width * dpi)}w.${extension} ${dpi}x` } else if ('height' in baseSize) { return `${src}@${Math.trunc(baseSize.height * dpi)}h.${extension} ${dpi}x` } } else { return `${src}@${Math.trunc(baseSize * dpi)}w_${Math.trunc(baseSize * dpi)}h.${extension} ${dpi}x` } }).join(",") } 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, bangumi: 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, 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: false, customNavbarShadow: true, customNavbarCompact: false, customNavbarBlur: true, customNavbarBlurOpacity: 0.7, customNavbarOrder: { ...customNavbarDefaultOrders }, customNavbarHidden: [], 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, maxDownloadLimit: '', }, searchHistory: [], seedsToCoins: true, autoSeedsToCoins: true, lastSeedsToCoinsDate: 0, autoDraw: false, keymap: false, doubleClickFullscreen: false, doubleClickFullscreenPreventSingleClick: false, 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, allNavbarFill: false, showDeadVideoTitle: 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) } static monkey (details) { return new Promise((resolve, reject) => { const fullDetails = { ...details, onload: r => resolve(r.response), onerror: r => reject(r), } if (!('method' in fullDetails)) { fullDetails.method = 'GET' } GM_xmlhttpRequest(fullDetails) }) } } // 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', 'fullTweetsTitle', ] 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 { 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)' } } const onlineData = {}; onlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/min/about.min.css"] = `.bilibili-evolved-about{height:100%;width:450px;background:#fff;color:#000;position:fixed;top:0;left:0;z-index:100000;transform:translateX(-100%);transition:.3s cubic-bezier(0,.86,.58,1);display:flex;flex-direction:column;box-shadow:4px 0 16px 0 #0000}body.dark .bilibili-evolved-about{background:#222;color:#eee}.bilibili-evolved-about.opened{transform:translateX(0);box-shadow:4px 0 16px 0 #0005}.about-header{padding:32px;display:flex;align-items:center;justify-content:flex-start}.about-header i{margin-right:8px;display:flex}.about-title{font-size:16pt}.about-content{padding:16px 36px 0;margin-bottom:36px;display:flex;flex-direction:column;overflow:auto}.about-content .name{font-size:24pt;display:none;align-items:center}.about-content .name svg{width:100%}body.dark .about-content .name.dark,body:not(.dark) .about-content .name.light{display:flex}.about-content .version{font-size:10pt;font-weight:700;opacity:.6;margin-top:6px;margin-bottom:6px;align-self:center}.about-content .love{font-size:10pt;margin-bottom:24px;align-self:center}.about-content .love a{color:inherit!important}.about-content section{font-size:10pt;margin-top:16px}.about-content section .title{display:flex;justify-content:center;text-transform:uppercase;font-weight:700;font-size:13pt;letter-spacing:3px;margin:8px 0 16px}.about-content section .supporter,.about-content section a{color:var(--theme-color)!important;margin-right:8px;display:inline-flex}.about-content section .supporter{user-select:none}.about-content section .supporter:not(:last-child)::after,.about-content section a:not(:last-child)::after{content:","}@keyframes spinner{to{transform:translate(-50%,-50%) rotate(360deg)}}.about-content section.participants .fetching{margin-right:8px;position:relative;display:inline-flex}.about-content section.participants .fetching::before{content:"Loading..."}`; onlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/min/about.min.html"] = `
v{{version}} · {{clientType}}
Made with ❤ Buy me a coffee ☕