Merge pull request #3715 from timongh/tools-to-define-api

define API 收尾工作完成
This commit is contained in:
Grant Howard 2022-10-07 22:07:32 +08:00 committed by GitHub
commit df95d9dfe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 595 additions and 259 deletions

View File

@ -1,8 +1,8 @@
import { ComponentMetadata } from '@/components/types' import { defineComponentMetadata } from '@/components/define'
import { LifeCycleEventTypes } from '@/core/life-cycle' import { LifeCycleEventTypes } from '@/core/life-cycle'
import { darkExcludes } from '../dark-urls' import { darkExcludes } from '../dark-urls'
export const component: ComponentMetadata = { export const component = defineComponentMetadata({
name: 'darkModeFollowSystem', name: 'darkModeFollowSystem',
displayName: '夜间模式跟随系统', displayName: '夜间模式跟随系统',
entry: () => { entry: () => {
@ -33,4 +33,4 @@ export const component: ComponentMetadata = {
> ( \`Microsoft Edge\`) 中,夜间模式仅会同步浏览器的亮 / 暗主题. > ( \`Microsoft Edge\`) 中,夜间模式仅会同步浏览器的亮 / 暗主题.
`.trim(), `.trim(),
}, },
} })

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types' import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
import { fullyLoaded } from '@/core/life-cycle' import { fullyLoaded } from '@/core/life-cycle'
import { ComponentSettings, getComponentSettings } from '@/core/settings' import { ComponentSettings, getComponentSettings } from '@/core/settings'
import { Range } from '@/ui/range' import { Range } from '@/ui/range'
@ -91,7 +91,33 @@ class ScheduleTime {
return result return result
} }
} }
const checkTime = (settings: ComponentSettings) => {
const options = defineOptionsMetadata({
range: {
defaultValue: {
start: '18:00',
end: '6:00',
},
displayName: '时间段',
validator: (range: Range<string>) => {
const { start, end } = range
const regex = /^(\d{1,2}):(\d{1,2})$/
if (!regex.test(start) || !regex.test(end)) {
return null
}
const startTime = new ScheduleTime(range.start)
const endTime = new ScheduleTime(range.end)
return {
start: startTime.toString(),
end: endTime.toString(),
}
},
},
})
type Options = OptionsOfMetadata<typeof options>
const checkTime = (settings: ComponentSettings<Options>) => {
const start = new ScheduleTime(settings.options.range.start) const start = new ScheduleTime(settings.options.range.start)
const end = new ScheduleTime(settings.options.range.end) const end = new ScheduleTime(settings.options.range.end)
const now = new ScheduleTime() const now = new ScheduleTime()
@ -110,7 +136,8 @@ const checkTime = (settings: ComponentSettings) => {
setTimeout(() => checkTime(settings), timeout) setTimeout(() => checkTime(settings), timeout)
} }
} }
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'darkModeSchedule', name: 'darkModeSchedule',
displayName: '夜间模式计划时段', displayName: '夜间模式计划时段',
description: '设置一个使用夜间模式的时间段, 进入 / 离开此时间段时, 会自动开启 / 关闭夜间模式. 结束时间小于起始时间时将视为次日, 如 `18:00` 至 `6:00` 表示晚上 18:00 到次日 6:00. 请勿和 \`夜间模式跟随系统\` 一同使用.', description: '设置一个使用夜间模式的时间段, 进入 / 离开此时间段时, 会自动开启 / 关闭夜间模式. 结束时间小于起始时间时将视为次日, 如 `18:00` 至 `6:00` 表示晚上 18:00 到次日 6:00. 请勿和 \`夜间模式跟随系统\` 一同使用.',
@ -120,26 +147,5 @@ export const component: ComponentMetadata = {
], ],
entry: ({ settings }) => fullyLoaded(() => checkTime(settings)), entry: ({ settings }) => fullyLoaded(() => checkTime(settings)),
urlExclude: darkExcludes, urlExclude: darkExcludes,
options: { options,
range: { })
defaultValue: {
start: '18:00',
end: '6:00',
},
displayName: '时间段',
validator: (range: Range<string>) => {
const { start, end } = range
const regex = /^(\d{1,2}):(\d{1,2})$/
if (!regex.test(start) || !regex.test(end)) {
return null
}
const startTime = new ScheduleTime(range.start)
const endTime = new ScheduleTime(range.end)
return {
start: startTime.toString(),
end: endTime.toString(),
}
},
},
},
}

View File

@ -1,12 +1,16 @@
import { createSwitchOptions, SwitchOptions } from '@/components/switch-options' import {
import { ComponentMetadata } from '@/components/types' newSwitchComponentWrapper,
defineSwitchMetadata,
defineIncompleteSwitchComponentMetadata,
} from '@/components/switch-options'
import { addComponentListener, getComponentSettings } from '@/core/settings' import { addComponentListener, getComponentSettings } from '@/core/settings'
import { sq } from '@/core/spin-query' import { sq } from '@/core/spin-query'
import { addStyle } from '@/core/style' import { addStyle } from '@/core/style'
import { getCookieValue } from '@/core/utils' import { getCookieValue } from '@/core/utils'
import { mainSiteUrls } from '@/core/utils/urls' import { mainSiteUrls } from '@/core/utils/urls'
const switchOptions: SwitchOptions = { const switchMetadata = defineSwitchMetadata({
name: 'simplifyOptions', name: 'simplifyOptions',
dimAt: 'checked', dimAt: 'checked',
switchProps: { switchProps: {
@ -43,8 +47,9 @@ const switchOptions: SwitchOptions = {
displayName: '右侧分区导航(旧)', displayName: '右侧分区导航(旧)',
}, },
}, },
} })
const metadata: ComponentMetadata = {
const metadata = defineIncompleteSwitchComponentMetadata({
name: 'simplifyHome', name: 'simplifyHome',
displayName: '简化首页', displayName: '简化首页',
description: { description: {
@ -81,13 +86,17 @@ const metadata: ComponentMetadata = {
() => dqa('.proxy-box > div'), () => dqa('.proxy-box > div'),
elements => elements.length > 0 || isNotHome, elements => elements.length > 0 || isNotHome,
) )
return Object.fromEntries(categoryElements.map(it => ([ return Object.fromEntries(
it.id.replace(/^bili_/, ''), categoryElements.map(it => [
{ it.id.replace(/^bili_/, ''),
displayName: it.querySelector('header .name')?.textContent?.trim() ?? '未知分区', {
defaultValue: false, displayName:
}, it.querySelector('header .name')?.textContent?.trim()
]))) ?? '未知分区',
defaultValue: false,
},
]),
)
} }
const skipIds = ['推广'] const skipIds = ['推广']
@ -123,39 +132,47 @@ const metadata: ComponentMetadata = {
} }
return null return null
}) })
.filter((it): it is [string, SimplifyHomeOption] => it !== null) ?? [] .filter((it): it is [string, SimplifyHomeOption] => it !== null)
?? []
return Object.fromEntries(entries) return Object.fromEntries(entries)
})() })()
const generatedSwitches: Record<string, unknown> = {} const generatedSwitches: Record<string, unknown> = {}
Object.entries(generatedOptions).forEach(([key, { displayName, defaultValue }]) => { Object.entries(generatedOptions).forEach(
const option = { ([key, { displayName, defaultValue }]) => {
defaultValue, const option = {
displayName, defaultValue,
} displayName,
const optionKey = `switch-${key}` }
if (options[optionKey] === undefined) { const optionKey = `switch-${key}`
options[optionKey] = defaultValue if (options[optionKey] === undefined) {
} options[optionKey] = defaultValue
const switchKey = `switch-${key}` }
addComponentListener( const switchKey = `switch-${key}`
`${metadata.name}.${switchKey}`, addComponentListener(
(value: boolean) => { `${metadata.name}.${switchKey}`,
document.body.classList.toggle(`${metadata.name}-${switchKey}`, value) (value: boolean) => {
}, document.body.classList.toggle(
true, `${metadata.name}-${switchKey}`,
) value,
switchOptions.switches[key] = option )
generatedSwitches[key] = option },
}) true,
options.simplifyOptions.switches = generatedSwitches )
const generatedStyles = Object.keys(generatedOptions).map(name => ` switchMetadata.switches[key] = option
generatedSwitches[key] = option
},
);
(options.simplifyOptions as any).switches = generatedSwitches
const generatedStyles = Object.keys(generatedOptions)
.map(name => `
body.simplifyHome-switch-${name} .bili-layout .bili-grid[data-area="${name}"], body.simplifyHome-switch-${name} .bili-layout .bili-grid[data-area="${name}"],
body.simplifyHome-switch-${name} .storey-box .proxy-box #bili_${name} { body.simplifyHome-switch-${name} .storey-box .proxy-box #bili_${name} {
display: none !important; display: none !important;
} }
`.trim()).join('\n') `.trim())
.join('\n')
addStyle(generatedStyles, 'simplify-home-generated') addStyle(generatedStyles, 'simplify-home-generated')
}, },
} })
export const component = createSwitchOptions(switchOptions)(metadata) export const component = newSwitchComponentWrapper(switchMetadata)(metadata)

View File

@ -1,8 +1,8 @@
import { createSwitchOptions } from '@/components/switch-options' import { newSwitchComponentWrapper } from '@/components/switch-options'
import { styledComponentEntry } from '@/components/styled-component' import { styledComponentEntry } from '@/components/styled-component'
import { liveUrls } from '@/core/utils/urls' import { liveUrls } from '@/core/utils/urls'
export const component = createSwitchOptions({ export const component = newSwitchComponentWrapper({
name: 'simplifyOptions', name: 'simplifyOptions',
dimAt: 'checked', dimAt: 'checked',
switchProps: { switchProps: {

View File

@ -67,7 +67,7 @@ export const enableTouchMove = (element: HTMLElement, options?: TouchMoveOptions
// let startTouch: Touch // let startTouch: Touch
let lastTouch: Touch let lastTouch: Touch
let move: boolean let move: boolean
const minMoveDistance = lodash.get(options, 'minMoveDistance', getComponentSettings('touchMiniPlayer').options.touchMoveDistance) const minMoveDistance = lodash.get(options, 'minMoveDistance', getComponentSettings('touchMiniPlayer').options.touchMoveDistance) as number
// const scroll = lodash.get(options, 'scroll', false) // const scroll = lodash.get(options, 'scroll', false)
const touchstart = (e: TouchEvent) => { const touchstart = (e: TouchEvent) => {
if (e.touches.length < 1) { if (e.touches.length < 1) {

View File

@ -39,7 +39,7 @@ const createPosition = (e: TouchEvent, element: HTMLElement) => {
* - `cancel`: * - `cancel`:
*/ */
export class SwipeAction extends EventTarget { export class SwipeAction extends EventTarget {
minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance as number
startPosition: Position = null startPosition: Position = null
lastAction: { lastAction: {
type: 'brightness' | 'volume' | 'progress' type: 'brightness' | 'volume' | 'progress'

View File

@ -6,8 +6,11 @@ import { Toast } from '@/core/toast'
import { matchUrlPattern, retrieveImageUrl } from '@/core/utils' import { matchUrlPattern, retrieveImageUrl } from '@/core/utils'
import { formatTitle } from '@/core/utils/title' import { formatTitle } from '@/core/utils/title'
import { feedsUrls } from '@/core/utils/urls' import { feedsUrls } from '@/core/utils/urls'
import { Options } from '.'
export const setupFeedImageExporter: ComponentEntry = async ({ settings: { options } }) => { export const setupFeedImageExporter: ComponentEntry<Options> = async ({
settings: { options },
}) => {
if (!feedsUrls.some(url => matchUrlPattern(url))) { if (!feedsUrls.some(url => matchUrlPattern(url))) {
return return
} }
@ -18,24 +21,28 @@ export const setupFeedImageExporter: ComponentEntry = async ({ settings: { optio
text: '导出图片', text: '导出图片',
action: async () => { action: async () => {
const imageUrls: { url: string; extension: string }[] = [] const imageUrls: { url: string; extension: string }[] = []
dqa(card.element, '.main-content .img-content, .bili-album__preview__picture__img').forEach((img: HTMLImageElement | HTMLDivElement) => { dqa(card.element, '.main-content .img-content, .bili-album__preview__picture__img').forEach(
const urlData = retrieveImageUrl(img) (img: HTMLImageElement | HTMLDivElement) => {
if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) { const urlData = retrieveImageUrl(img)
imageUrls.push(urlData) if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) {
} imageUrls.push(urlData)
}) }
},
)
if (imageUrls.length === 0) { if (imageUrls.length === 0) {
Toast.info('此条动态没有检测到任何图片.', '导出图片') Toast.info('此条动态没有检测到任何图片.', '导出图片')
return return
} }
const toast = Toast.info('下载中...', '导出图片') const toast = Toast.info('下载中...', '导出图片')
let downloadedCount = 0 let downloadedCount = 0
const imageBlobs = await Promise.all(imageUrls.map(async ({ url }) => { const imageBlobs = await Promise.all(
const blob = await getBlob(url) imageUrls.map(async ({ url }) => {
downloadedCount++ const blob = await getBlob(url)
toast.message = `下载中... (${downloadedCount}/${imageUrls.length})` downloadedCount++
return blob toast.message = `下载中... (${downloadedCount}/${imageUrls.length})`
})) return blob
}),
)
const pack = new DownloadPackage() const pack = new DownloadPackage()
const { feedFormat } = options const { feedFormat } = options
imageBlobs.forEach((blob, index) => { imageBlobs.forEach((blob, index) => {
@ -45,7 +52,10 @@ export const setupFeedImageExporter: ComponentEntry = async ({ settings: { optio
originalUser: (card as RepostFeedsCard).repostUsername ?? card.username, originalUser: (card as RepostFeedsCard).repostUsername ?? card.username,
n: (index + 1).toString(), n: (index + 1).toString(),
} }
pack.add(`${formatTitle(feedFormat, false, titleData)}${imageUrls[index].extension}`, blob) pack.add(
`${formatTitle(feedFormat, false, titleData)}${imageUrls[index].extension}`,
blob,
)
}) })
toast.close() toast.close()
const packTitleData = { const packTitleData = {

View File

@ -1,8 +1,21 @@
import { defineComponentMetadata } from '@/components/define' import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
import { matchUrlPattern } from '@/core/utils' import { matchUrlPattern } from '@/core/utils'
import { columnUrls, feedsUrls } from '@/core/utils/urls' import { columnUrls, feedsUrls } from '@/core/utils/urls'
import { setupFeedImageExporter } from './feed' import { setupFeedImageExporter } from './feed'
const options = defineOptionsMetadata({
columnFormat: {
defaultValue: '[title][ - n]',
displayName: '专栏图片命名格式',
},
feedFormat: {
defaultValue: '[user][ - id][ - n]',
displayName: '动态图片命名格式',
},
})
export type Options = OptionsOfMetadata<typeof options>
export const component = defineComponentMetadata({ export const component = defineComponentMetadata({
name: 'imageExporter', name: 'imageExporter',
displayName: '图片批量导出', displayName: '图片批量导出',
@ -21,14 +34,5 @@ export const component = defineComponentMetadata({
...feedsUrls, ...feedsUrls,
...columnUrls, ...columnUrls,
], ],
options: { options,
columnFormat: {
defaultValue: '[title][ - n]',
displayName: '专栏图片命名格式',
},
feedFormat: {
defaultValue: '[user][ - id][ - n]',
displayName: '动态图片命名格式',
},
},
}) })

View File

@ -1,6 +1,16 @@
import { defineComponentMetadata } from '@/components/define' import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
import { startResolution } from './resolution' import { startResolution } from './resolution'
const options = defineOptionsMetadata({
scale: {
displayName: '缩放级别',
defaultValue: 'auto',
hidden: true,
},
})
export type Options = OptionsOfMetadata<typeof options>
export const component = defineComponentMetadata({ export const component = defineComponentMetadata({
name: 'imageResolution', name: 'imageResolution',
displayName: '高分辨率图片', displayName: '高分辨率图片',
@ -12,11 +22,5 @@ export const component = defineComponentMetadata({
description: { description: {
'zh-CN': '根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)', 'zh-CN': '根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)',
}, },
options: { options,
scale: {
displayName: '缩放级别',
defaultValue: 'auto',
hidden: true,
},
},
}) })

View File

@ -1,4 +1,5 @@
import { styledComponentEntry } from '@/components/styled-component' import { styledComponentEntry } from '@/components/styled-component'
import { Options } from '.'
const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/ const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/
const excludeSelectors = [ const excludeSelectors = [
@ -59,7 +60,7 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
replaceSource(e => e.style.backgroundImage, (e, v) => (e.style.backgroundImage = v)) replaceSource(e => e.style.backgroundImage, (e, v) => (e.style.backgroundImage = v))
}) })
} }
export const startResolution = styledComponentEntry(() => import('./fix.scss'), export const startResolution = styledComponentEntry<Options>(() => import('./fix.scss'),
async ({ settings }) => { async ({ settings }) => {
const { allMutations } = await import('@/core/observer') const { allMutations } = await import('@/core/observer')
const dpi = settings.options.scale === 'auto' const dpi = settings.options.scale === 'auto'

View File

@ -1,6 +1,7 @@
import { playerAgent } from '@/components/video/player-agent' import { playerAgent } from '@/components/video/player-agent'
import { getComponentSettings } from '@/core/settings' import { getComponentSettings } from '@/core/settings'
import { registerAndGetData } from '@/plugins/data' import { registerAndGetData } from '@/plugins/data'
import { Options } from '.'
import { KeyBindingAction, KeyBindingActionContext } from './bindings' import { KeyBindingAction, KeyBindingActionContext } from './bindings'
export const clickElement = ( export const clickElement = (
@ -172,11 +173,11 @@ export const builtInActions: Record<string, KeyBindingAction> = {
}, },
longJumpBackward: { longJumpBackward: {
displayName: '长倒退', displayName: '长倒退',
run: () => playerAgent.changeTime(-(getComponentSettings('keymap').options.longJumpSeconds)), run: () => playerAgent.changeTime(-(getComponentSettings<Options>('keymap').options.longJumpSeconds)),
}, },
longJumpForward: { longJumpForward: {
displayName: '长前进', displayName: '长前进',
run: () => playerAgent.changeTime(getComponentSettings('keymap').options.longJumpSeconds), run: () => playerAgent.changeTime(getComponentSettings<Options>('keymap').options.longJumpSeconds),
}, },
jumpBackward: { jumpBackward: {
displayName: '倒退', displayName: '倒退',

View File

@ -1,89 +1,100 @@
import { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action' import { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import { styledComponentEntry } from '@/components/styled-component' import { styledComponentEntry } from '@/components/styled-component'
import { ComponentEntry } from '@/components/types' import {
import { defineComponentMetadata } from '@/components/define' defineComponentMetadata,
defineOptionsMetadata,
OptionsOfMetadata,
} from '@/components/define'
import { addComponentListener } from '@/core/settings' import { addComponentListener } from '@/core/settings'
import { actions } from './actions' import { actions } from './actions'
import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings' import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings'
import { presetBase, presets } from './presets' import { presetBase, presets } from './presets'
let config: KeyBindingConfig = null const options = defineOptionsMetadata({
const parseBindings = (bindings: Record<string, string>) => ( longJumpSeconds: {
Object.entries(bindings).map(([actionName, keyString]) => { defaultValue: 85,
const keys = keyString.split(' ').filter(it => it !== '') displayName: '长跳跃秒数',
return { },
keys, customKeyBindings: {
action: actions[actionName] || none, defaultValue: {} as Record<string, string>,
} as KeyBinding displayName: '自定义键位',
}) hidden: true,
) },
const entry: ComponentEntry = styledComponentEntry(() => import('./playback-tip.scss'), async ({ settings }) => { preset: {
const update = () => { defaultValue: 'Default',
const presetName = settings.options.preset displayName: '预设',
const preset = presets[presetName] || {} hidden: true,
const bindings = parseBindings( },
{ ...presetBase, ...preset, ...settings.options.customKeyBindings },
)
if (config) {
config.bindings = bindings
} else {
config = loadKeyBindings(bindings)
}
}
addComponentListener('keymap.preset', update, true)
addComponentListener('keymap.customKeyBindings', update)
}) })
export type Options = OptionsOfMetadata<typeof options>
let config: KeyBindingConfig = null
const parseBindings = (bindings: Record<string, string>): KeyBinding[] => {
const parseBinding = (actionName: string, keyString: string) => {
const keys = keyString.split(' ').filter(it => it !== '')
return { keys, action: actions[actionName] }
}
return Object.entries(bindings).map(([n, k]) => parseBinding(n, k))
}
const entry = styledComponentEntry<Options>(
() => import('./playback-tip.scss'),
async ({ settings }) => {
const update = () => {
const presetName = settings.options.preset
const preset = presets[presetName] || {}
const bindings = parseBindings({
...presetBase,
...preset,
...settings.options.customKeyBindings,
})
if (config) {
config.bindings = bindings
} else {
config = loadKeyBindings(bindings)
}
}
addComponentListener('keymap.preset', update, true)
addComponentListener('keymap.customKeyBindings', update)
},
)
export const component = defineComponentMetadata({ export const component = defineComponentMetadata({
name: 'keymap', name: 'keymap',
displayName: '快捷键扩展', displayName: '快捷键扩展',
tags: [ tags: [componentsTags.video, componentsTags.utils],
componentsTags.video,
componentsTags.utils,
],
// urlInclude: [ // urlInclude: [
// ...videoAndBangumiUrls, // ...videoAndBangumiUrls,
// ...cheeseUrls, // ...cheeseUrls,
// ...mediaListUrls, // ...mediaListUrls,
// ], // ],
entry, entry,
unload: () => { config && (config.enable = false) }, unload: () => {
reload: () => { config && (config.enable = true) }, config && (config.enable = false)
},
reload: () => {
config && (config.enable = true)
},
description: { description: {
'zh-CN': '为脚本的功能和 b 站的功能启用键盘快捷键支持, 快捷键列表可在`快捷键设置`中查看和配置.', 'zh-CN': '为脚本的功能和 b 站的功能启用键盘快捷键支持, 快捷键列表可在`快捷键设置`中查看和配置.',
}, },
extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default), extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default),
options: { options,
longJumpSeconds: {
defaultValue: 85,
displayName: '长跳跃秒数',
},
customKeyBindings: {
defaultValue: {},
displayName: '自定义键位',
hidden: true,
},
preset: {
defaultValue: 'Default',
displayName: '预设',
hidden: true,
},
},
plugin: { plugin: {
displayName: '快捷键扩展 - 搜索支持', displayName: '快捷键扩展 - 搜索支持',
setup: ({ addData }) => { setup: ({ addData }) => {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => { addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({ providers.push({
name: 'keymapSettings', name: 'keymapSettings',
getActions: async () => [{ getActions: async () => [
name: '快捷键扩展设置', {
description: 'Keymap Settings', name: '快捷键扩展设置',
icon: 'mdi-keyboard-settings-outline', description: 'Keymap Settings',
action: async () => { icon: 'mdi-keyboard-settings-outline',
const { toggleKeymapSettings } = await import('./settings/vm') action: async () => {
toggleKeymapSettings() const { toggleKeymapSettings } = await import('./settings/vm')
toggleKeymapSettings()
},
}, },
}], ],
}) })
}) })
}, },

View File

@ -1,7 +1,8 @@
import { import {
ComponentEntry, ComponentEntry,
ComponentMetadata, ComponentMetadata,
ComponentOption, OptionMetadata,
UnknownOptions,
} from '@/components/types' } from '@/components/types'
import { CoreApis } from '@/core/core-apis' import { CoreApis } from '@/core/core-apis'
import { addComponentListener, ComponentSettings } from '@/core/settings' import { addComponentListener, ComponentSettings } from '@/core/settings'
@ -31,7 +32,7 @@ export type EntryContext = Parameters<ComponentEntry>[0]
export type OptionSubjects<O> = O & { [K in keyof O as `${Exclude<K, symbol>}$`]: Subject<O[K]> } export type OptionSubjects<O> = O & { [K in keyof O as `${Exclude<K, symbol>}$`]: Subject<O[K]> }
export class EntrySpeedComponent<O = Record<string, unknown>> export class EntrySpeedComponent<O extends UnknownOptions = UnknownOptions>
implements EntryContext { implements EntryContext {
static create: < static create: <
OO extends Record<string, any> = unknown OO extends Record<string, any> = unknown
@ -39,7 +40,7 @@ implements EntryContext {
ComponentMetadata, ComponentMetadata,
'entry' | 'reload' | 'unload' | 'options' 'entry' | 'reload' | 'unload' | 'options'
> & { > & {
options?: { [K in keyof OO]: ComponentOption } options?: { [K in keyof OO]: OptionMetadata }
}) => ComponentMetadata; }) => ComponentMetadata;
static contextMap: Partial<Record<keyof EntrySpeedComponent, keyof SpeedContext | string>> = { static contextMap: Partial<Record<keyof EntrySpeedComponent, keyof SpeedContext | string>> = {

View File

@ -28,7 +28,7 @@ export const ERROR_MESSAGE_DURATION = 5000
/** 计算菜单项 order */ /** 计算菜单项 order */
export const calcOrder = (value: number) => ((MAX_BROWSER_SPEED_VALUE - value) * 10000).toString() export const calcOrder = (value: number) => ((MAX_BROWSER_SPEED_VALUE - value) * 10000).toString()
export interface Options { export type Options = {
/** 最大菜单高度 */ /** 最大菜单高度 */
maxMenuHeight: boolean maxMenuHeight: boolean
/** 隐藏进度条 */ /** 隐藏进度条 */

View File

@ -3,7 +3,7 @@ import { EntrySpeedComponent, VideoIdObject } from '../common/speed'
import { NoSuchSpeedMenuItemElementError, SpeedContext } from '../common/speed/context' import { NoSuchSpeedMenuItemElementError, SpeedContext } from '../common/speed/context'
import { formatSpeedText } from '../common/speed/utils' import { formatSpeedText } from '../common/speed/utils'
export interface Options { export type Options = {
/** 全局倍速 */ /** 全局倍速 */
globalSpeed: number globalSpeed: number
/** 固定全局倍速 */ /** 固定全局倍速 */

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types' import { defineComponentMetadata } from '@/components/define'
import { cdnRoots } from '@/core/cdn-types' import { cdnRoots } from '@/core/cdn-types'
import { branches } from '@/core/meta' import { branches } from '@/core/meta'
import { getComponentsDoc } from './components-doc' import { getComponentsDoc } from './components-doc'
@ -90,7 +90,7 @@ ${getDocText(pluginsDoc.title, pluginsDoc.items)}
} }
} }
} }
export const doc: ComponentMetadata = { export const doc = defineComponentMetadata({
name: 'featureDocsGenerator', name: 'featureDocsGenerator',
displayName: '功能文档生成器', displayName: '功能文档生成器',
entry, entry,
@ -99,4 +99,4 @@ export const doc: ComponentMetadata = {
delete unsafeWindow.generateDocs delete unsafeWindow.generateDocs
}, },
tags: [componentsTags.utils], tags: [componentsTags.utils],
} })

View File

@ -1,3 +1,4 @@
import { CustomNavbarOptions } from 'registry/lib/components/style/custom-navbar'
import { PluginMetadata } from '@/plugins/plugin' import { PluginMetadata } from '@/plugins/plugin'
import { getComponentSettings } from '@/core/settings' import { getComponentSettings } from '@/core/settings'
import type { CustomNavbarItemInit } from '../../../components/style/custom-navbar/custom-navbar-item' import type { CustomNavbarItemInit } from '../../../components/style/custom-navbar/custom-navbar-item'
@ -11,7 +12,7 @@ export const plugin: PluginMetadata = {
const defaultLink = 'https://www.bilibili.com/v/channel/' const defaultLink = 'https://www.bilibili.com/v/channel/'
const name = 'channel' const name = 'channel'
const isOpenInNewTab = () => { const isOpenInNewTab = () => {
const { options } = getComponentSettings('customNavbar') const { options } = getComponentSettings('customNavbar') as { options: CustomNavbarOptions }
if (name in options.openInNewTabOverrides) { if (name in options.openInNewTabOverrides) {
return options.openInNewTabOverrides[name] return options.openInNewTabOverrides[name]
} }
@ -23,7 +24,10 @@ export const plugin: PluginMetadata = {
content: () => import('./NavbarChannel.vue'), content: () => import('./NavbarChannel.vue'),
clickAction: () => { clickAction: () => {
const channelId = dq('.navbar-channel[data-channel-id]').getAttribute('data-channel-id') const channelId = dq('.navbar-channel[data-channel-id]').getAttribute('data-channel-id')
window.open(channelId ? `${defaultLink}${channelId}` : defaultLink, isOpenInNewTab() ? '_blank' : '_self') window.open(
channelId ? `${defaultLink}${channelId}` : defaultLink,
isOpenInNewTab() ? '_blank' : '_self',
)
}, },
}) })
}) })

View File

@ -13,6 +13,7 @@ import {
UpdateCheckItem, UpdateCheckItem,
CheckSingleTypeUpdateConfig, CheckSingleTypeUpdateConfig,
} from './utils' } from './utils'
import { Options } from '.'
export const checkUpdate = async (config: CheckUpdateConfig) => { export const checkUpdate = async (config: CheckUpdateConfig) => {
const { const {
@ -140,7 +141,7 @@ export const checkAllUpdate = async (config: CheckSingleTypeUpdateConfig) => {
console.groupEnd() console.groupEnd()
} }
export const silentCheckUpdate = () => checkAllUpdate({ export const silentCheckUpdate = () => checkAllUpdate({
maxCount: getComponentSettings(name).options.maxUpdateCount, maxCount: getComponentSettings<Options>(name).options.maxUpdateCount,
}) })
export const silentCheckUpdateAndReload = reload(silentCheckUpdate) export const silentCheckUpdateAndReload = reload(silentCheckUpdate)

View File

@ -62,7 +62,7 @@ const optionsMetadata = defineOptionsMetadata({
}, },
}) })
type Options = OptionsOfMetadata<typeof optionsMetadata> export type Options = OptionsOfMetadata<typeof optionsMetadata>
const entry: ComponentEntry<Options> = async ({ const entry: ComponentEntry<Options> = async ({
settings: { options: opt }, settings: { options: opt },

View File

@ -1,7 +1,7 @@
import { addData } from '@/plugins/data' import { addData } from '@/plugins/data'
import { getComponentSettings } from '@/core/settings' import { getComponentSettings } from '@/core/settings'
import { languageNameToCode } from '@/core/utils/i18n' import { languageNameToCode } from '@/core/utils/i18n'
import { Translation, GeneralTranslation, RegexTranslation } from './types' import { Translation, GeneralTranslation, RegexTranslation, Options as I18nOptions } from './types'
/** /**
* `plugin.setup`使 * `plugin.setup`使
@ -22,6 +22,6 @@ export const addI18nData = (
}) })
} }
export const getSelectedLanguage = () => { export const getSelectedLanguage = () => {
const settings = getComponentSettings('i18n') const settings = getComponentSettings<I18nOptions>('i18n')
return languageNameToCode(settings.options.language) return languageNameToCode(settings.options.language)
} }

View File

@ -4,6 +4,7 @@ import { getComponentSettings } from '@/core/settings'
import { languageNameToCode } from '@/core/utils/i18n' import { languageNameToCode } from '@/core/utils/i18n'
import { registerAndGetData } from '@/plugins/data' import { registerAndGetData } from '@/plugins/data'
import { formData } from '@/core/utils' import { formData } from '@/core/utils'
import { Options as I18nOptions } from '../types'
export abstract class MachineTranslateProvider { export abstract class MachineTranslateProvider {
abstract translate(text: string): Promise<string> abstract translate(text: string): Promise<string>
@ -11,7 +12,7 @@ export abstract class MachineTranslateProvider {
abstract link: string abstract link: string
abstract defaultLanguage: string abstract defaultLanguage: string
protected getTargetLanguage() { protected getTargetLanguage() {
const i18n = getComponentSettings('i18n') const i18n = getComponentSettings<I18nOptions>('i18n')
if (i18n.enabled) { if (i18n.enabled) {
return languageNameToCode(i18n.options.language) return languageNameToCode(i18n.options.language)
} }
@ -109,7 +110,7 @@ export const [translateProviders] = registerAndGetData('i18n.machineTranslators'
} as Record<string, MachineTranslateProvider>) } as Record<string, MachineTranslateProvider>)
export const translateProviderNames = Object.keys(translateProviders) export const translateProviderNames = Object.keys(translateProviders)
export const getTranslator = (): MachineTranslateProvider => { export const getTranslator = (): MachineTranslateProvider => {
const { options: { translator } } = getComponentSettings('i18n') const { options: { translator } } = getComponentSettings<I18nOptions>('i18n')
const provider = translateProviders[translator] || translateProviders.GoogleCN const provider = translateProviders[translator] || translateProviders.GoogleCN
return provider return provider
} }

View File

@ -10,3 +10,7 @@ export type LanguagePack = {
map?: [string, Translation][] map?: [string, Translation][]
regex?: RegexTranslation regex?: RegexTranslation
} }
export type Options = {
language: string,
translator: string,
}

View File

@ -1,33 +1,31 @@
import { none } from '@/core/utils' import { none } from '@/core/utils'
import { ComponentEntry, ComponentMetadata } from './component' import { ComponentEntry, ComponentMetadata, UnknownOptions } from './component'
/** /**
* *
* @param styleImport * @param styleImport
* @param entry * @param entry
*/ */
export const styledComponentEntry = ( export const styledComponentEntry = <O extends UnknownOptions>(
styleImport: () => Promise<{ default: string }>, styleImport: () => Promise<{ default: string }>,
entry: ComponentEntry, entry: ComponentEntry<O>,
): ComponentEntry => ( ): ComponentEntry<O> => async context => {
async context => {
const { default: style } = await styleImport() const { default: style } = await styleImport()
const { addStyle } = await import('@/core/style') const { addStyle } = await import('@/core/style')
addStyle(style, context.metadata.name) addStyle(style, context.metadata.name)
return entry(context) return entry(context)
} }
)
/** /**
* `entry`, `reload``unload`, , * `entry`, `reload``unload`, ,
* @param styleImport * @param styleImport
* @param entry * @param entry
*/ */
export const toggleStyle = ( export const toggleStyle = <O extends UnknownOptions>(
name: string, name: string,
styleImport: () => Promise<{ default: string }>, styleImport: () => Promise<{ default: string }>,
entry: ComponentEntry = none, entry: ComponentEntry<O> = none,
): Pick<ComponentMetadata, 'name' | 'entry' | 'reload' | 'unload'> => { ): Pick<ComponentMetadata<O>, 'name' | 'entry' | 'reload' | 'unload'> => {
let styleElement: HTMLStyleElement = null let styleElement: HTMLStyleElement = null
const styleEntry = async () => { const styleEntry = async () => {
if (styleElement) { if (styleElement) {
@ -39,9 +37,7 @@ export const toggleStyle = (
} }
return { return {
name, name,
entry: context => ( entry: context => styleEntry().then(() => entry(context)),
styleEntry().then(() => entry(context))
),
reload: styleEntry, reload: styleEntry,
unload: () => { unload: () => {
styleElement?.remove() styleElement?.remove()

View File

@ -1,71 +1,352 @@
import { getComponentSettings, addComponentListener } from '@/core/settings' /**
import { ComponentMetadata, ComponentOptions } from './component' * Switch Options API
* @module src/components/switch-options
*
* ComponentMetadata
* Widget Widget
*
* API {@link newSwitchComponentWrapper}
*/
type Switches = { import { getComponentSettings, addComponentListener } from '@/core/settings'
[key: string]: { import {
displayName: string ComponentEntry,
defaultValue: boolean ComponentMetadata,
} OptionsMetadata,
OptionsOfMetadata,
UnknownOptions,
} from './component'
import { Widget } from './widget'
/**
*
*/
export type SwitchItemMetadata = {
/** 开关的显示名称 */
displayName: string
/** 开关的默认开启状态 */
defaultValue: boolean
} }
export interface SwitchOptions {
name: string /**
switches: Switches *
radio?: boolean *
dimAt?: 'checked' | 'notChecked' * option
* option 'switch-' `boolean`
*/
export type SwitchItemsMetadata<S extends string> = {
[key in S]: SwitchItemMetadata
}
/**
* SwitchItemsMetadata
*/
export const defineSwitchItemsMetadata = <S extends string>(
c: SwitchItemsMetadata<S>,
): SwitchItemsMetadata<S> => c
/**
* API 使 {@link defineSwitchMetadata}
*/
export interface SwitchMetadata<N extends string, S extends string> {
/**
* `options`
*
* @see {@link SwitchOptions}
*/
name: N
/** 每个开关的单独配置。可用 {@link defineSwitchItemsMetadata} 单独定义 */
switches: SwitchItemsMetadata<S>
/**
* `undefined`
*
* @default false
*/
radio?: undefined | boolean
/**
*
*
* `undefined`:
* `'checked'`:
* `'notChecked'`:
*
* @default undefined
*/
dimAt?: undefined | 'checked' | 'notChecked'
/** 配置每个开关的图标 */
switchProps?: { switchProps?: {
checkedIcon?: string checkedIcon?: string
notCheckedIcon?: string notCheckedIcon?: string
iconPosition?: 'left' | 'right' iconPosition?: 'left' | 'right'
} }
} }
export const createSwitchOptions = (options: SwitchOptions) => {
if (options.radio === undefined) { /**
options.radio = false * {@link SwitchMetadata}
*/
export const defineSwitchMetadata = <N extends string, S extends string>(
c: SwitchMetadata<N, S>,
): SwitchMetadata<N, S> => c
/**
* options
*
* {@link SwitchMetadata}
* `SwitchMetadata`
*
* @see {@link SwitchOptions}
*/
export interface SwitchMetadataOption<N extends string, S extends string> {
name: N
switches: SwitchItemsMetadata<S>
radio: boolean
dimAt: undefined | 'checked' | 'notChecked'
switchProps?: {
checkedIcon?: string
notCheckedIcon?: string
iconPosition?: 'left' | 'right'
} }
const { name: optionName, switches } = options /** 组件名称 */
const extendComponentOptions: ComponentOptions = {} componentName: string
Object.entries(switches).forEach(([key, { displayName, defaultValue }]) => { /** 组件的 Widget 显示名称 */
extendComponentOptions[`switch-${key}`] = { optionDisplayName: string
}
/**
* SwitchMetadataOption
*/
const newSwitchMetadataOption = <N extends string, S extends string>(
metadata: SwitchMetadata<N, S>,
componentName: string,
optionDisplayName,
) => ({
...metadata,
radio: metadata.radio === undefined ? false : metadata.radio,
dimAt: metadata.dimAt,
componentName,
optionDisplayName,
})
/**
* `S` 'switch-'
*/
export type SwitchItemNames<S extends string> = `switch-${S}`
/**
* API options
*/
export type SwitchItemOptions<S extends string> = Record<SwitchItemNames<S>, boolean>
/**
* options
*
*
* 1. options
* 2. {@link SwitchMetadata} `name` {@link SwitchMetadataOption}
* 3. {@link SwitchItemsMetadata}
* `SwitchMetadata` `switches`
*/
export type SwitchOptions<O extends UnknownOptions, N extends string, S extends string> = O & {
N: SwitchMetadataOption<N, S>
} & SwitchItemOptions<S>
/**
* SwitchOptions
*/
export type SwitchOptionsOfSwitchMetadata<
O extends UnknownOptions,
C extends SwitchMetadata<string, string>,
> = C extends SwitchMetadata<infer N, infer S> ? SwitchOptions<O, N, S> : never
/**
* SwitchOptions
*/
export type SwitchOptionsOfMetadata<
M extends OptionsMetadata,
C extends SwitchMetadata<string, string>,
> = SwitchOptionsOfSwitchMetadata<OptionsOfMetadata<M>, C>
/**
* OptionsMetadata API options
*/
export type SwitchOptionsMetadata<
O extends UnknownOptions,
N extends string,
S extends string,
> = OptionsMetadata<SwitchOptions<O, N, S>>
/**
* `options` API
*
* `options`
*/
type SwitchOptionsMetadataExtender<S extends string> = <O extends UnknownOptions, N extends string>(
options: OptionsMetadata<O>,
switchMetadataOption: SwitchMetadataOption<N, S>,
) => SwitchOptionsMetadata<O, N, S>
/**
* {@link SwitchOptionsMetadataExtender}
*/
const newSwitchOptionsMetadataExtender = <S extends string>(
itemsMetadata: SwitchItemsMetadata<S>,
): SwitchOptionsMetadataExtender<S> => {
const optionsToExtend = {}
const entries = Object.entries<SwitchItemMetadata>(itemsMetadata)
for (const [key, { displayName, defaultValue }] of entries) {
optionsToExtend[`switch-${key}`] = {
defaultValue, defaultValue,
displayName, displayName,
hidden: true, hidden: true,
} }
}) }
return (component: ComponentMetadata) => { return <O extends UnknownOptions, N extends string>(
const optionDisplayName = `${component.displayName}选项` options: OptionsMetadata<O>,
const selfOption = { switchMetadataOption: SwitchMetadataOption<N, S>,
componentName: component.name, ) => {
optionDisplayName, optionsToExtend[switchMetadataOption.name as string] = {
}
Object.assign(options, selfOption)
extendComponentOptions[optionName] = {
defaultValue: options, defaultValue: options,
displayName: optionDisplayName, displayName: switchMetadataOption.optionDisplayName,
} }
component.options = { ...component.options, ...extendComponentOptions } Object.assign(options, optionsToExtend)
if (!component.widget) { return options as SwitchOptionsMetadata<O, N, S>
component.widget = { }
component: () => import('./SwitchOptions.vue').then(m => m.default), }
options,
} /**
} * Widget
const originalEntry = component.entry */
component.entry = async (...args) => { const newWidget = <N extends string, S extends string>(
originalEntry?.(...args) options: SwitchMetadataOption<N, S>,
const { name } = component ): Omit<Widget, 'name'> => ({
const componentOptions = getComponentSettings(name).options component: () => import('./SwitchOptions.vue').then(m => m.default),
Object.keys(componentOptions).forEach(key => { options,
if (key.startsWith('switch-')) { })
addComponentListener(
`${name}.${key}`, /**
(value: boolean) => { * API `options` `entry`
document.body.classList.toggle(`${name}-${key}`, value) */
}, export type SwitchEntry<
true, O extends UnknownOptions,
) N extends string,
} S extends string,
}) T = unknown,
} > = ComponentEntry<SwitchOptions<O, N, S>, T>
return component
/**
* API `ComponentMetadata`
*
* 使 {@link defineIncompleteSwitchComponentMetadata}
*
* `entry` API options
*/
export type IncompleteSwitchComponentMetadata<
O extends UnknownOptions,
N extends string,
S extends string,
> = {
[K in keyof ComponentMetadata<O>]: K extends 'entry'
? SwitchEntry<O, N, S>
: ComponentMetadata<O>[K]
}
/**
* API
* {@link import("./define").defineComponentMetadata}
*
* `entry` API options
*/
export const defineIncompleteSwitchComponentMetadata = <
O extends UnknownOptions,
N extends string,
S extends string,
>(
m: IncompleteSwitchComponentMetadata<O, N, S>,
): IncompleteSwitchComponentMetadata<O, N, S> => m
/**
* `entry`
*/
const newSwitchEntry = <O extends UnknownOptions, N extends string, S extends string>(
component: ComponentMetadata<O> | IncompleteSwitchComponentMetadata<O, N, S>,
): SwitchEntry<O, N, S> => (...args) => {
const result = component.entry(...args)
const componentOptions = getComponentSettings(component.name).options
Object.keys(componentOptions).forEach(key => {
if (key.startsWith('switch-')) {
addComponentListener(
`${component.name}.${key}`,
(value: boolean) => {
document.body.classList.toggle(`${component.name}-${key}`, value)
},
true,
)
}
})
return result
}
/**
* {@link ComponentMetadata}
*/
export type SwitchComponentMetadata<
O extends UnknownOptions,
N extends string,
S extends string,
> = ComponentMetadata<SwitchOptions<O, N, S>>
/**
*
*
* {@link IncompleteSwitchComponentMetadata}
* 使 {@link defineIncompleteSwitchComponentMetadata}
* `entry` API options
*
* 使 'switch-' option
*
* Widget Widget
* Widget options
*
* `entry`
*
*
*
* @param component -
* @returns
*/
export type SwitchComponentWrapper<N extends string, S extends string> = <O extends UnknownOptions>(
component: ComponentMetadata<O> | IncompleteSwitchComponentMetadata<O, N, S>,
) => SwitchComponentMetadata<O, N, S>
/**
* {@link SwitchComponentWrapper}
*
* 使 {@link defineSwitchMetadata}
*
* @param metadata -
* @returns
*/
export const newSwitchComponentWrapper = <N extends string, S extends string>(
metadata: SwitchMetadata<N, S>,
): SwitchComponentWrapper<N, S> => {
const extendOptions = newSwitchOptionsMetadataExtender(metadata.switches)
// 返回的 wrapper
return <O extends UnknownOptions>(
component: ComponentMetadata<O> | IncompleteSwitchComponentMetadata<O, N, S>,
) => {
const switchMetadataOption = newSwitchMetadataOption(
metadata,
component.name,
`${component.displayName}选项`,
)
// 若没有 Widget则注入一个
if (!component.widget) {
component.widget = newWidget(switchMetadataOption)
}
// 扩展组件 options
extendOptions(component.options, switchMetadataOption)
// 包装 entry 函数
component.entry = newSwitchEntry(component)
return component as SwitchComponentMetadata<O, N, S>
} }
} }

View File

@ -48,8 +48,7 @@ export interface ComponentTag {
type ComponentOptionValidator<T> = (value: T, oldValue: T) => T | undefined | null type ComponentOptionValidator<T> = (value: T, oldValue: T) => T | undefined | null
// TODO: 参考 discussion #3041。当不兼容代码替换完成后将 any 改为 unknown export type UnknownOptions = Record<string, unknown>
export type UnknownOptions = Record<string, any>
export type EmptyOptions = Record<string, never> export type EmptyOptions = Record<string, never>
@ -81,10 +80,6 @@ export type OptionsMetadata<O extends UnknownOptions = UnknownOptions> = {
[OptionName in keyof O]: OptionMetadata<O[OptionName]> [OptionName in keyof O]: OptionMetadata<O[OptionName]>
} }
// TODO: 参考 discussion #3041。当不兼容代码替换完成后删除
export type ComponentOptions = OptionsMetadata
export type ComponentOption = OptionMetadata
/** 组件标签 */ /** 组件标签 */
export const componentsTags = { export const componentsTags = {
/** 视频 */ /** 视频 */

View File

@ -58,12 +58,11 @@ const emptySettings: ComponentSettings = {
}), }),
} }
// TODO: 参考 discussion #3041。
// 当不兼容代码替换完成后将 R 的默认类型替换为 UnknownOptions
/** /**
* *
* *
* ComponentSettings * 使
* ComponentSettings
* ```js * ```js
* { * {
* enabled: false, * enabled: false,
@ -73,9 +72,9 @@ const emptySettings: ComponentSettings = {
* *
* @param component * @param component
*/ */
export const getComponentSettings = <R extends UnknownOptions = UnknownOptions>( export const getComponentSettings = <O extends UnknownOptions>(
component: ComponentMetadata | string, component: ComponentMetadata<O> | string,
): ComponentSettings<R> => { ): ComponentSettings<O> => {
let componentMetadata: ComponentMetadata let componentMetadata: ComponentMetadata
if (typeof component === 'string') { if (typeof component === 'string') {
if (componentsMap[component] === undefined) { if (componentsMap[component] === undefined) {