// ==UserScript== // @name Bilibili Evolved (Preview) // @version 1.5.20 // @description 增强哔哩哔哩Web端体验.(预览版分支) // @author Grant Howard, Coulomb-G // @copyright 2018, Grant Howrad (https://github.com/the1812) // @license MIT // @match *://*.bilibili.com/* // @match *://*.bilibili.com // @run-at document-end // @updateURL https://github.com/the1812/Bilibili-Evolved/raw/preview/bilibili-evolved.preview.user.js // @downloadURL https://github.com/the1812/Bilibili-Evolved/raw/preview/bilibili-evolved.preview.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_addValueChangeListener // @grant GM_setClipboard // @require https://code.jquery.com/jquery-3.2.1.min.js // @require https://cdn.bootcss.com/jszip/3.1.5/jszip.min.js // @icon https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/images/logo.png // ==/UserScript== (self$ => { const $ = unsafeWindow.$ || self$; const settings = { forceWideMinWidth: "1368px", forceWide: false, darkScheduleStart: "18:00", darkScheduleEnd: "6:00", darkSchedule: false, blurSettingsPanel: false, blurVideoControl: false, toast: true, fullTweetsTitle: true, removeVideoTopMask: false, removeLiveWatermark: true, harunaScale: true, removeAds: true, hideTopSearch: false, touchVideoPlayerDoubleTapControl: false, touchVideoPlayerAnimation: false, touchNavBar: false, touchVideoPlayer: false, watchLaterRedirect: true, expandDanmakuList: true, customStyleColor: "#00A0D8", blurBackgroundOpacity: 0.382, overrideNavBar: true, showBanner: true, useDarkStyle: false, useNewStyle: true, useCache: true, cache: {} }; const fixedSettings = { guiSettings: true, viewCover: true, notifyNewVersion: true, clearCache: true, fixFullscreen: false, downloadVideo: true, latestVersionLink: "https://github.com/the1812/Bilibili-Evolved/raw/preview/bilibili-evolved.preview.user.js", currentVersion: "1.5.20" }; function loadSettings() { for (const key in settings) { settings[key] = GM_getValue(key, settings[key]); } for (const key in fixedSettings) { settings[key] = fixedSettings[key]; } } function saveSettings(newSettings) { for (const key in settings) { GM_setValue(key, newSettings[key]); } } function onSettingsChange(change) { for (const key in settings) { GM_addValueChangeListener(key, change); } } function loadResources() { const resouceData = { style: { path: "min/style.min.scss", order: 1 }, oldStyle: { path: "min/old.min.scss", order: 1 }, scrollbarStyle: { path: "min/scrollbar.min.css", order: 1 }, darkStyle: { path: "min/dark.min.scss", order: 2 }, darkStyleImportant: { path: "min/dark-important.min.scss" }, darkStyleNavBar: { path: "min/dark-navbar.min.scss" }, touchPlayerStyle: { path: "min/touch-player.min.scss", order: 3 }, navbarOverrideStyle: { path: "min/override-navbar.min.css", order: 4 }, noBannerStyle: { path: "min/no-banner.min.css", order: 5 }, removeAdsStyle: { path: "min/remove-promotions.min.css", order: 6 }, guiSettingsStyle: { path: "min/gui-settings.min.scss", order: 0 }, fullTweetsTitleStyle: { path: "min/full-tweets-title.min.css", order: 7 }, imageViewerStyle: { path: "min/image-viewer.min.scss", order: 8 }, toastStyle: { path: "min/toast.min.scss", order: 9 }, blurVideoControlStyle: { path: "min/blur-video-control.min.css", order: 10 }, forceWideStyle: { path: "min/force-wide.min.scss" }, downloadVideoStyle: { path: "min/download-video.min.scss" }, guiSettingsDom: { path: "min/gui-settings.min.html" }, imageViewerDom: { path: "min/image-viewer.min.html" }, downloadVideoDom: { path: "min/download-video.min.html" }, latestVersion: { path: "version.txt" }, guiSettings: { path: "min/gui-settings.min.js", dependencies: [ "guiSettingsDom", "guiSettingsStyle" ], displayNames: { guiSettings: "设置", blurSettingsPanel: "模糊设置面板背景" } }, useDarkStyle: { path: "min/dark-styles.min.js", dependencies: [ "darkStyle", "darkStyleImportant", "darkStyleNavBar", "scrollbarStyle" ], displayNames: { useDarkStyle: "夜间模式" } }, useNewStyle: { path: "min/new-styles.min.js", dependencies: [ "style", "oldStyle", "scrollbarStyle" ], displayNames: { useNewStyle: "样式调整", blurBackgroundOpacity: "顶栏(对横幅)不透明度" } }, overrideNavBar: { path: "min/override-navbar.min.js", dependencies: [ "navbarOverrideStyle", "noBannerStyle" ], displayNames: { overrideNavBar: "搜索栏置顶", showBanner: "显示顶部横幅" } }, touchNavBar: { path: "min/touch-navbar.min.js", displayNames: { touchNavBar: "顶栏触摸优化" } }, touchVideoPlayer: { path: "min/touch-player.min.js", dependencies: [ "touchPlayerStyle" ], displayNames: { touchVideoPlayer: "播放器触摸支持", touchVideoPlayerAnimation: "启用实验性动画效果", touchVideoPlayerDoubleTapControl: "启用双击控制" } }, expandDanmakuList: { path: "min/expand-danmaku.min.js", displayNames: { expandDanmakuList: "自动展开弹幕列表" } }, removeAds: { path: "min/remove-promotions.min.js", dependencies: [ "removeAdsStyle" ], displayNames: { removeAds: "删除广告" } }, watchLaterRedirect: { path: "min/watchlater.min.js", displayNames: { watchLaterRedirect: "稍后再看重定向" } }, hideTopSearch: { path: "min/hide-top-search.min.js", displayNames: { hideTopSearch: "隐藏搜索推荐" } }, harunaScale: { path: "min/haruna-scale.min.js", displayNames: { harunaScale: "缩放看板娘" } }, removeLiveWatermark: { path: "min/remove-watermark.min.js", displayNames: { removeLiveWatermark: "删除直播水印" } }, fullTweetsTitle: { path: "min/full-tweets-title.min.js", dependencies: [ "fullTweetsTitleStyle" ], displayNames: { fullTweetsTitle: "展开动态标题" } }, viewCover: { path: "min/view-cover.min.js", dependencies: [ "imageViewerDom", "imageViewerStyle" ], displayNames: { viewCover: "查看封面" } }, notifyNewVersion: { path: "min/notify-new-version.min.js", dependencies: [ "latestVersion" ], displayNames: { notifyNewVersion: "检查更新" } }, toast: { path: "min/toast.min.js", dependencies: [ "toastStyle" ], displayNames: { toast: "显示消息" } }, removeVideoTopMask: { path: "min/remove-top-mask.min.js", displayNames: { removeVideoTopMask: "删除视频标题层" } }, blurVideoControl: { path: "min/blur-video-control.min.js", dependencies: [ "blurVideoControlStyle" ], displayNames: { blurVideoControl: "模糊视频控制栏背景" } }, darkSchedule: { path: "min/dark-schedule.min.js", displayNames: { darkSchedule: "夜间模式计划时段", darkScheduleStart: "起始时间", darkScheduleEnd: "结束时间" } }, forceWide: { path: "min/force-wide.min.js", dependencies: [ "forceWideStyle" ], displayNames: { forceWide: "强制宽屏", forceWideMinWidth: "触发宽度" } }, clearCache: { path: "min/clear-cache.min.js", displayNames: { useCache: "启用缓存" } }, downloadVideo: { path: "min/download-video.min.js", dependencies: [ "downloadVideoDom", "downloadVideoStyle", "videoInfo" ] }, videoInfo: { path: "min/video-info.min.js" } }; Resource.root = "https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/"; Resource.all = {}; Resource.displayNames = {}; for (const [key, data] of Object.entries(resouceData)) { const resource = new Resource(data.path, data.order); resource.key = key; if (data.displayNames) { resource.displayName = data.displayNames[key]; Object.assign(Resource.displayNames, data.displayNames); } Resource.all[key] = resource; } for (const [key, data] of Object.entries(resouceData)) { if (data.dependencies) { Resource.all[key].dependencies = data.dependencies.map(name => Resource.all[name]); } } } function downloadText(url, load, error) { const xhr = new XMLHttpRequest(); xhr.open("GET", url); if (load) // callback { xhr.addEventListener("load", () => load && load(xhr.responseText)); xhr.addEventListener("error", () => error && error(xhr.responseText)); xhr.send(); } else { return new Promise((resolve, reject) => { xhr.addEventListener("load", () => resolve(xhr.responseText)); xhr.addEventListener("error", () => reject(xhr.responseText)); xhr.send(); }); } } 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"; } } // Placeholder class for Toast class Toast { constructor() { } show() { } dismiss() { } static show() { } static info() { } static success() { } static error() { } } class Observer { constructor(element, callback) { this.element = element; this.callback = callback; this.observer = null; this.options = { childList: true, subtree: true }; } 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 subtree(selector, callback) { callback(); return new Array(...document.querySelectorAll(selector)) .map(it => { return new Observer(it, callback).start(); }); } static attributes(selector, callback) { callback(); return new Array(...document.querySelectorAll(selector)) .map(it => { const observer = new Observer(it, callback); observer.options = { childList: false, subtree: false, attributes: true }; return observer.start(); }); } static all(selector, callback) { callback(); return new Array(...document.querySelectorAll(selector)) .map(it => { const observer = new Observer(it, callback); observer.options = { childList: true, subtree: true, attributes: true }; return observer.start(); }); } } class SpinQuery { constructor(query, condition, action, onFailed) { this.maxRetry = 30; this.retry = 0; this.queryInterval = 500; this.query = query; this.condition = condition; this.action = action; this.onFailed = onFailed; } start() { this.tryQuery(this.query, this.condition, this.action, this.onFailed); } tryQuery(query, condition, action, onFailed) { if (this.retry >= this.maxRetry) { if (onFailed) { onFailed(); } } else { const result = query(); if (condition(result)) { action(result); } else { this.retry++; setTimeout(() => this.tryQuery(query, condition, action, onFailed), this.queryInterval); } } } static any(query, action) { new SpinQuery(query, it => it.length > 0, action).start(); } static count(query, count, action) { new SpinQuery(query, it => it.length === count, action).start(); } } class ColorProcessor { constructor(hex) { this.hex = hex; } get rgb() { return this.hexToRgb(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"); } _hexToRgb(hex, alpha) { const isShortHand = hex.length < 6; if (isShortHand) { const shorthandRegex = this.getHexRegex(alpha, true); hex = hex.replace(shorthandRegex, function () { let result = ""; let i = 1; while (arguments[i]) { result += arguments[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._hexToRgb(hex, false); if (rgb) { rgb.a = 1; return rgb; } } return null; } hexToRgb(hex) { return this._hexToRgb(hex, false); } hexToRgba(hex) { return this._hexToRgb(hex, true); } 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, b } = this.rgbToHsb(originalRgb); const targetColor = this.hsb; const hue = targetColor.h - h; const saturate = (s - targetColor.s) / 100 + 100; const brightness = (b - targetColor.b) / 100 + 100; const filter = `hue-rotate(${hue}deg) saturate(${saturate}%) brightness(${brightness}%)`; 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(1)"; } } // [Offline build placeholder] class ResourceType { constructor(name, preprocessor) { this.name = name; this.preprocessor = preprocessor || (text => text); } static fromUrl(url) { if (url.indexOf(".scss") !== -1 || 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", style => { const color = new ColorProcessor(); const hexToRgba = text => { const replaceColor = (text, shorthand) => { const part = `([a-f\\d]${shorthand ? "" : "{2}"})`.repeat(4); return text.replace(new RegExp(`(#${part})[^a-f\\d]`, "ig"), (original, it) => { const rgba = color.hexToRgba(it); if (rgba) { return `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a})${original.slice(-1)}`; } else { return original; } }); }; return replaceColor(replaceColor(text, false), true); }; for (const key of Object.keys(settings)) { style = style .replace(new RegExp("\\$" + key, "g"), settings[key]); } return hexToRgba(style); }); } static get html() { return new ResourceType("html", html => { for (const [key, name] of Object.entries(Resource.displayNames)) { html = html.replace(new RegExp(`(<(.+)\\s*?indent="[\\d]+?"\\s*?key="${key}"\\s*?dependencies=".*?">)[^\\0]*?()`, "g"), `$1${name}$3`); } return html.replace(/([^\0]*?)<\/category>/g, `
  • $1
  • `).replace(/([^\0]*?)<\/checkbox>/g, `
  • `).replace(/([^\0]*?)<\/textbox>/g, `
  • `); }); } 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, priority) { this.url = Resource.root + url; this.dependencies = []; this.priority = priority; this.text = null; this.key = null; this.type = ResourceType.fromUrl(url); this.displayName = ""; } loadCache() { const key = this.key; if (!settings.cache || !settings.cache[key]) { return null; } else { return settings.cache[key]; } } download() { const key = this.key; return new Promise((resolve, reject) => { if (this.downloaded) { resolve(this.text); } else { Promise.all(this.dependencies.map(r => r.download())).then(() => { // +#Offline build placeholder const apply = text => { this.text = this.type.preprocessor(text); resolve(this.text); }; if (settings.useCache) { const cache = this.loadCache(key); if (cache !== null) { apply(cache); } downloadText(this.url, text => { if (cache !== text) { if (cache === null) { apply(text); } if (typeof offlineData === "undefined") { settings.cache[key] = text; saveSettings(settings); } } }, error => reject(error)); } else { downloadText(this.url, text => apply(text), error => reject(error)); } // -#Offline build placeholder }); } }); } getStyle(id) { const style = this.text; if (style === null) { console.error("Attempt to get style which is not downloaded."); } let attributes = `id='${id}'`; if (this.priority !== undefined) { attributes += ` priority='${this.priority}'`; } return ``; } getPriorStyle(root) { if (this.priority !== undefined) { let insertPosition = this.priority - 1; let formerStyle = root.find(`style[priority='${insertPosition}']`); while (insertPosition >= 0 && formerStyle.length === 0) { formerStyle = root.find(`style[priority='${insertPosition}']`); insertPosition--; } if (insertPosition < 0) { return null; } else { return formerStyle; } } else { return null; } } applyStyle(id, important) { if ($(`#${id}`).length === 0) { const element = this.getStyle(id); const root = important ? $("body") : $("head"); const priorStyle = this.getPriorStyle(root); if (priorStyle === null) { if (important) { root.after(element); } else { root.prepend(element); } } else { priorStyle.after(element); } } } } class ResourceManager { constructor() { this.data = Resource.all; this.attributes = {}; 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; } fetchByKey(key) { const resource = Resource.all[key]; if (!resource) { return null; } const promise = resource.download(); resource.dependencies .filter(it => it.type.name === "script") .forEach(it => this.fetchByKey(it.key)); return new Promise(resolve => { promise.then(text => { this.applyComponent(key, text); resolve(); }).catch(reason => { // download error console.error(`Download error, XHR status: ${reason}`); Toast.error(`无法下载组件${Resource.all[key].displayName}`, "错误"); }); }); } fetch() { return new Promise(resolve => { const promises = []; for (const key in settings) { if (settings[key] === true && key !== "toast") { const promise = this.fetchByKey(key); if (promise) { promises.push(promise); } } } this.validateCache(); Promise.all(promises).then(() => { this.applySettingsWidgets(); resolve(); }); }); } applyComponent(key, text) { const func = eval(text); if (func) { try { const attribute = func(settings, this) || {}; this.attributes[key] = attribute; } catch (error) { // execution error console.error(`Failed to apply feature "${key}": ${error}`); Toast.error(`加载组件${Resource.all[key].displayName}失败`, "错误"); } } } applySettingsWidgets() { const panel = $(".gui-settings-panel"); if (panel.length === 0) { return; } for (const info of Object.values(this.attributes) .filter(it => it.settingsWidget) .map(it => it.settingsWidget)) { if (info.after) { panel.find(info.after()).after(info.content); } else if (info.before) { panel.find(info.before()).before(info.content); } else if (info.category) { panel.find(`.widgets-container[category-name=${info.category}]`).append(info.content); } if (info.success) { info.success(); } } } 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); } applyImportantStyle(key, id) { if (id === undefined) { id = this.getDefaultStyleId(key); } Resource.all[key].applyStyle(id, true); } applyStyleFromText(text) { $("head").prepend(text); } applyImportantStyleFromText(text) { $("body").after(text); } getStyle(key, id) { return Resource.all[key].getStyle(id); } validateCache() { if (settings.cache.version !== settings.currentVersion) { settings.cache = {}; } if (settings.cache.version === undefined) { settings.cache.version = settings.currentVersion; } saveSettings(settings); } } loadResources(); loadSettings(); const resources = new ResourceManager(); if (settings.toast) { resources.fetchByKey("toast").then(() => { Toast = resources.attributes.toast.export; resources.fetch(); }); } else { resources.fetch(); } })(window.jQuery.noConflict(true));