export class ResourceManager { constructor () { this.data = Resource.all this.skippedImport = [] this.attributes = {} this.styleManager = new StyleManager(this) const styleMethods = Object.getOwnPropertyNames(StyleManager.prototype).filter(it => it !== 'constructor') for (const key of styleMethods) { this[key] = function (...params) { this.styleManager[key](...params) } } this.setupColors() } setupColors () { this.color = new ColorProcessor(settings.customStyleColor) settings.foreground = this.color.foreground settings.blueImageFilter = this.color.blueImageFilter settings.pinkImageFilter = this.color.pinkImageFilter settings.brightness = this.color.brightness settings.filterInvert = this.color.filterInvert const hexToRgba = input => this.color.rgbToString(this.color.hexToRgba(input)) let styles = [] styles.push('--theme-color:' + settings.customStyleColor) for (let opacity = 10; opacity <= 90; opacity += 10) { const color = this.color.hexToRgba(settings.customStyleColor) color.a = opacity / 100 styles.push(`--theme-color-${opacity}:` + this.color.rgbToString(color)) } styles.push('--foreground-color:' + settings.foreground) styles.push('--foreground-color-b:' + hexToRgba(settings.foreground + 'b')) styles.push('--foreground-color-d:' + hexToRgba(settings.foreground + 'd')) styles.push('--blue-image-filter:' + settings.blueImageFilter) styles.push('--pink-image-filter:' + settings.pinkImageFilter) styles.push('--brightness:' + settings.brightness) styles.push('--invert-filter:' + settings.filterInvert) styles.push('--blur-background-opacity:' + settings.blurBackgroundOpacity) // styles.push("--custom-control-background-opacity:" + settings.customControlBackgroundOpacity); this.applyStyleFromText(`html{${styles.join(';')}}`, 'bilibili-evolved-variables') } resolveComponentName (componentName) { const keyword = '/' + componentName.replace('./', '').replace('../', '') + '.min.js' for (const [name, value] of Object.entries(Resource.all)) { if (value.url.endsWith(keyword)) { return name } } return componentName } resolveComponent (componentName) { const resource = Resource.all[this.resolveComponentName(componentName)] if (!resource) { this.skippedImport.push(componentName) } return resource } importAsync (componentName) { return new Promise(resolve => { const resource = this.resolveComponent(componentName) if (!resource) { resolve(unsafeWindow.bilibiliEvolved) } if (!Object.keys(this.attributes).includes(resource.key)) { if (resource.type.name === 'html' || resource.type.name === 'style') { resource.download().then(() => resolve(this.import(componentName))) } else { this.fetchByKey(resource.key).then(() => resolve(this.import(componentName))) } } else { resolve(this.import(componentName)) } }) } import (componentName) { const resource = this.resolveComponent(componentName) if (!resource) { return unsafeWindow.bilibiliEvolved } if (resource.type.name === 'html' || resource.type.name === 'style') { if (!resource.downloaded) { console.error(`Import failed: component "${componentName}" is not loaded.`) return null } return resource.text } else { const attribute = this.attributes[this.resolveComponentName(componentName)] if (attribute === undefined) { console.error(`Import failed: component "${componentName}" is not loaded.`) return null } return attribute.export } } async fetchByKey (key) { const resource = Resource.all[key] if (!resource) { return null } const text = await resource.download().catch(reason => { console.error(`Download error, XHR status: ${reason}`) let toastMessage = `无法下载组件${Resource.all[key].displayName}` if (settings.toastInternalError) { toastMessage += '\n' + reason } Toast.error(toastMessage, '错误') }) await Promise.all(resource.dependencies .filter(it => it.type.name === 'style') .map(it => this.styleManager.fetchStyleByKey(it.key))) await Promise.all(resource.dependencies .filter(it => it.type.name === 'script') .map(it => this.fetchByKey(it.key))) this.applyComponent(key, text) } async fetch () { const isCacheValid = this.validateCache() let loadingToast = null if (settings.toast === true) { await this.fetchByKey('toast') unsafeWindow.bilibiliEvolved.Toast = Toast = this.attributes.toast.export.Toast || this.attributes.toast.export if (!isCacheValid && settings.useCache) { loadingToast = Toast.info(/* html */`
正在初始化脚本`, '初始化') } } const promises = [] for (const key in settings) { if (settings[key] === true && key !== 'toast') { const promise = this.fetchByKey(key) if (promise) { promises.push(promise) } } } await Promise.all(promises) saveSettings(settings) if (loadingToast) { loadingToast.dismiss() } this.applyReloadables() // reloadables run sync // await this.applyDropdownOptions(); this.applyWidgets() // No need to wait the widgets } applyReloadables () { const checkAttribute = (key, attributes) => { if (attributes.reload && attributes.unload) { addSettingsListener(key, newValue => { if (newValue === true) { attributes.reload() } else { attributes.unload() } }) } } for (const key of Resource.reloadables) { const attributes = this.attributes[key] if (attributes === undefined) { const fetchListener = async newValue => { if (newValue === true) { await this.styleManager.fetchStyleByKey(key) await this.fetchByKey(key) removeSettingsListener(key, fetchListener) checkAttribute(key, this.attributes[key]) } } addSettingsListener(key, fetchListener) } else { checkAttribute(key, attributes) } } } applyComponent (key, text) { const func = eval(text) if (func) { try { const attribute = func(settings, this) || {} this.attributes[key] = attribute } catch (error) { console.error(`Failed to apply feature "${key}": ${error}`) let toastMessage = `加载组件${Resource.all[key].displayName}失败` if (settings.toastInternalError) { toastMessage += '\n' + error } Toast.error(toastMessage, '错误') } } } async applyWidget (info) { let condition = true if (typeof info.condition === 'function') { condition = info.condition() if (typeof condition === 'object' && 'then' in condition) { condition = await condition.catch(() => { return false }) } } if (condition === true) { if (info.content) { document.querySelector('.widgets-container').insertAdjacentHTML('beforeend', info.content) } if (info.success) { info.success() } } } async applyWidgets () { await Promise.all(Object.values(this.attributes) .filter(it => it.widget) .map(it => this.applyWidget(it.widget)) ) } async applyDropdownOptions () { async function applyDropdownOption (info) { if (Array.isArray(info)) { await Promise.all(info.map(applyDropdownOption)) } else { const dropdownInput = dq(`.gui-settings-dropdown input[key=${info.key}]`) dropdownInput.value = settings[info.key] dropdownInput.setAttribute('data-name', settings[info.key]) const dropdown = dropdownInput.parentElement const list = dropdown.querySelector('ul') const input = dropdown.querySelector('input') info.items.forEach(itemHtml => { list.insertAdjacentHTML('beforeend', `