Greasy Fork is available in English.

Bilibili Evolved

增强哔哩哔哩Web端体验: 下载视频, 音乐, 封面, 弹幕; 自定义播放器的画质, 模式, 布局; 自定义顶栏, 删除广告, 使用夜间模式; 以及增加对触屏设备的支持等.

2019/07/07時点のページです。最新版はこちら。

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Bilibili Evolved
// @version      1.8.8
// @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
// @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
// @namespace https://greasyfork.org/users/221184
// ==/UserScript==
// if (typeof GM_addValueChangeListener === "undefined")
// {
//     GM_addValueChangeListener = function () { };
// }
function logError(message)
{
    if (settings.toastInternalError)
    {
        Toast.error(typeof message === "object" && "stack" in message
            ? message.stack
            : message, "错误");
    }
    console.error(message);
}
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();
}
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 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,
  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',
  hideCategory: false,
  foldComment: true,
  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", () => 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 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 downloadText(url, load, error) // The old method for compatibility
{
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);

    if (load !== undefined) // callback
    {
        xhr.addEventListener("load", () => load && load(xhr.responseText));
        xhr.addEventListener("error", () => error && error(xhr.status));
        xhr.send();
    }
    else
    {
        return new Promise((resolve, reject) =>
        {
            xhr.addEventListener("load", () => resolve(xhr.responseText));
            xhr.addEventListener("error", () => reject(xhr.status));
            xhr.send();
        });
    }
};
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 `<style ${attributes}>${style}</style>`;
        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: '删除广告'
    }
  },
  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': '批量下载'
    }
  },
  downloadDanmaku: {
    path: 'download-danmaku.min.js',
    dependencies: [
      'title',
      'videoInfo',
      'danmakuConverter'
    ],
    displayNames: {
      'downloadDanmaku': '下载弹幕'
    }
  },
  danmakuConverter: {
    path: 'danmaku-converter.min.js'
  },
  videoInfo: {
    path: 'video-info.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', '1.25', '1.5', '2.0'],
    }
  },
}
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 = `无法下载组件<span>${Resource.all[key].displayName}</span>`
      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 */`<div class="loading"></div>正在初始化脚本`, '初始化')
      }
    }
    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 = `加载组件<span>${Resource.all[key].displayName}</span>失败`
        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', `<li data-name="${itemHtml}">${itemHtml}</li>`)
        })
        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()));
            }
        },
    });
    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);
                },
                deleteProperty: function (target, property)
                {
                    return true;
                },
                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);
}