diff --git a/registry/lib/components/style/dark-mode/follow-system/index.ts b/registry/lib/components/style/dark-mode/follow-system/index.ts index c4b3634a1..7f6785d25 100644 --- a/registry/lib/components/style/dark-mode/follow-system/index.ts +++ b/registry/lib/components/style/dark-mode/follow-system/index.ts @@ -1,8 +1,8 @@ -import { ComponentMetadata } from '@/components/types' +import { defineComponentMetadata } from '@/components/define' import { LifeCycleEventTypes } from '@/core/life-cycle' import { darkExcludes } from '../dark-urls' -export const component: ComponentMetadata = { +export const component = defineComponentMetadata({ name: 'darkModeFollowSystem', displayName: '夜间模式跟随系统', entry: () => { @@ -33,4 +33,4 @@ export const component: ComponentMetadata = { > 注:在某些浏览器 (如 \`Microsoft Edge\`) 中,夜间模式仅会同步浏览器的亮 / 暗主题. `.trim(), }, -} +}) diff --git a/registry/lib/components/style/dark-mode/schedule/index.ts b/registry/lib/components/style/dark-mode/schedule/index.ts index 4da930dd7..d0dd924b6 100644 --- a/registry/lib/components/style/dark-mode/schedule/index.ts +++ b/registry/lib/components/style/dark-mode/schedule/index.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from '@/components/types' +import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '@/components/define' import { fullyLoaded } from '@/core/life-cycle' import { ComponentSettings, getComponentSettings } from '@/core/settings' import { Range } from '@/ui/range' @@ -91,7 +91,33 @@ class ScheduleTime { return result } } -const checkTime = (settings: ComponentSettings) => { + +const options = defineOptionsMetadata({ + range: { + defaultValue: { + start: '18:00', + end: '6:00', + }, + displayName: '时间段', + validator: (range: Range) => { + 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 + +const checkTime = (settings: ComponentSettings) => { const start = new ScheduleTime(settings.options.range.start) const end = new ScheduleTime(settings.options.range.end) const now = new ScheduleTime() @@ -110,7 +136,8 @@ const checkTime = (settings: ComponentSettings) => { setTimeout(() => checkTime(settings), timeout) } } -export const component: ComponentMetadata = { + +export const component = defineComponentMetadata({ name: 'darkModeSchedule', displayName: '夜间模式计划时段', description: '设置一个使用夜间模式的时间段, 进入 / 离开此时间段时, 会自动开启 / 关闭夜间模式. 结束时间小于起始时间时将视为次日, 如 `18:00` 至 `6:00` 表示晚上 18:00 到次日 6:00. 请勿和 \`夜间模式跟随系统\` 一同使用.', @@ -120,26 +147,5 @@ export const component: ComponentMetadata = { ], entry: ({ settings }) => fullyLoaded(() => checkTime(settings)), urlExclude: darkExcludes, - options: { - range: { - defaultValue: { - start: '18:00', - end: '6:00', - }, - displayName: '时间段', - validator: (range: Range) => { - 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(), - } - }, - }, - }, -} + options, +}) diff --git a/registry/lib/components/style/simplify/home/index.ts b/registry/lib/components/style/simplify/home/index.ts index 1b8ce162d..bb03bd9cc 100644 --- a/registry/lib/components/style/simplify/home/index.ts +++ b/registry/lib/components/style/simplify/home/index.ts @@ -1,12 +1,16 @@ -import { createSwitchOptions, SwitchOptions } from '@/components/switch-options' -import { ComponentMetadata } from '@/components/types' +import { + newSwitchComponentWrapper, + defineSwitchMetadata, + defineIncompleteSwitchComponentMetadata, +} from '@/components/switch-options' + import { addComponentListener, getComponentSettings } from '@/core/settings' import { sq } from '@/core/spin-query' import { addStyle } from '@/core/style' import { getCookieValue } from '@/core/utils' import { mainSiteUrls } from '@/core/utils/urls' -const switchOptions: SwitchOptions = { +const switchMetadata = defineSwitchMetadata({ name: 'simplifyOptions', dimAt: 'checked', switchProps: { @@ -43,8 +47,9 @@ const switchOptions: SwitchOptions = { displayName: '右侧分区导航(旧)', }, }, -} -const metadata: ComponentMetadata = { +}) + +const metadata = defineIncompleteSwitchComponentMetadata({ name: 'simplifyHome', displayName: '简化首页', description: { @@ -81,13 +86,17 @@ const metadata: ComponentMetadata = { () => dqa('.proxy-box > div'), elements => elements.length > 0 || isNotHome, ) - return Object.fromEntries(categoryElements.map(it => ([ - it.id.replace(/^bili_/, ''), - { - displayName: it.querySelector('header .name')?.textContent?.trim() ?? '未知分区', - defaultValue: false, - }, - ]))) + return Object.fromEntries( + categoryElements.map(it => [ + it.id.replace(/^bili_/, ''), + { + displayName: + it.querySelector('header .name')?.textContent?.trim() + ?? '未知分区', + defaultValue: false, + }, + ]), + ) } const skipIds = ['推广'] @@ -123,39 +132,47 @@ const metadata: ComponentMetadata = { } return null }) - .filter((it): it is [string, SimplifyHomeOption] => it !== null) ?? [] + .filter((it): it is [string, SimplifyHomeOption] => it !== null) + ?? [] return Object.fromEntries(entries) })() const generatedSwitches: Record = {} - Object.entries(generatedOptions).forEach(([key, { displayName, defaultValue }]) => { - const option = { - defaultValue, - displayName, - } - const optionKey = `switch-${key}` - if (options[optionKey] === undefined) { - options[optionKey] = defaultValue - } - const switchKey = `switch-${key}` - addComponentListener( - `${metadata.name}.${switchKey}`, - (value: boolean) => { - document.body.classList.toggle(`${metadata.name}-${switchKey}`, value) - }, - true, - ) - switchOptions.switches[key] = option - generatedSwitches[key] = option - }) - options.simplifyOptions.switches = generatedSwitches - const generatedStyles = Object.keys(generatedOptions).map(name => ` + Object.entries(generatedOptions).forEach( + ([key, { displayName, defaultValue }]) => { + const option = { + defaultValue, + displayName, + } + const optionKey = `switch-${key}` + if (options[optionKey] === undefined) { + options[optionKey] = defaultValue + } + const switchKey = `switch-${key}` + addComponentListener( + `${metadata.name}.${switchKey}`, + (value: boolean) => { + document.body.classList.toggle( + `${metadata.name}-${switchKey}`, + value, + ) + }, + true, + ) + 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} .storey-box .proxy-box #bili_${name} { display: none !important; } - `.trim()).join('\n') + `.trim()) + .join('\n') addStyle(generatedStyles, 'simplify-home-generated') }, -} +}) -export const component = createSwitchOptions(switchOptions)(metadata) +export const component = newSwitchComponentWrapper(switchMetadata)(metadata) diff --git a/registry/lib/components/style/simplify/live/index.ts b/registry/lib/components/style/simplify/live/index.ts index 684f4cea5..e85e40216 100644 --- a/registry/lib/components/style/simplify/live/index.ts +++ b/registry/lib/components/style/simplify/live/index.ts @@ -1,8 +1,8 @@ -import { createSwitchOptions } from '@/components/switch-options' +import { newSwitchComponentWrapper } from '@/components/switch-options' import { styledComponentEntry } from '@/components/styled-component' import { liveUrls } from '@/core/utils/urls' -export const component = createSwitchOptions({ +export const component = newSwitchComponentWrapper({ name: 'simplifyOptions', dimAt: 'checked', switchProps: { diff --git a/registry/lib/components/touch/mini-player/touch-move.ts b/registry/lib/components/touch/mini-player/touch-move.ts index bca3768b1..65c1497a8 100644 --- a/registry/lib/components/touch/mini-player/touch-move.ts +++ b/registry/lib/components/touch/mini-player/touch-move.ts @@ -67,7 +67,7 @@ export const enableTouchMove = (element: HTMLElement, options?: TouchMoveOptions // let startTouch: Touch let lastTouch: Touch 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 touchstart = (e: TouchEvent) => { if (e.touches.length < 1) { diff --git a/registry/lib/components/touch/player-gestures/swiper.ts b/registry/lib/components/touch/player-gestures/swiper.ts index c6d8f7bdf..231b50d91 100644 --- a/registry/lib/components/touch/player-gestures/swiper.ts +++ b/registry/lib/components/touch/player-gestures/swiper.ts @@ -39,7 +39,7 @@ const createPosition = (e: TouchEvent, element: HTMLElement) => { * - `cancel`: 请求取消调整 */ export class SwipeAction extends EventTarget { - minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance + minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance as number startPosition: Position = null lastAction: { type: 'brightness' | 'volume' | 'progress' diff --git a/registry/lib/components/utils/image-exporter/feed.ts b/registry/lib/components/utils/image-exporter/feed.ts index 954e56b78..e859f2ac5 100644 --- a/registry/lib/components/utils/image-exporter/feed.ts +++ b/registry/lib/components/utils/image-exporter/feed.ts @@ -6,8 +6,11 @@ import { Toast } from '@/core/toast' import { matchUrlPattern, retrieveImageUrl } from '@/core/utils' import { formatTitle } from '@/core/utils/title' import { feedsUrls } from '@/core/utils/urls' +import { Options } from '.' -export const setupFeedImageExporter: ComponentEntry = async ({ settings: { options } }) => { +export const setupFeedImageExporter: ComponentEntry = async ({ + settings: { options }, +}) => { if (!feedsUrls.some(url => matchUrlPattern(url))) { return } @@ -18,24 +21,28 @@ export const setupFeedImageExporter: ComponentEntry = async ({ settings: { optio text: '导出图片', action: async () => { const imageUrls: { url: string; extension: string }[] = [] - dqa(card.element, '.main-content .img-content, .bili-album__preview__picture__img').forEach((img: HTMLImageElement | HTMLDivElement) => { - const urlData = retrieveImageUrl(img) - if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) { - imageUrls.push(urlData) - } - }) + dqa(card.element, '.main-content .img-content, .bili-album__preview__picture__img').forEach( + (img: HTMLImageElement | HTMLDivElement) => { + const urlData = retrieveImageUrl(img) + if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) { + imageUrls.push(urlData) + } + }, + ) if (imageUrls.length === 0) { Toast.info('此条动态没有检测到任何图片.', '导出图片') return } const toast = Toast.info('下载中...', '导出图片') let downloadedCount = 0 - const imageBlobs = await Promise.all(imageUrls.map(async ({ url }) => { - const blob = await getBlob(url) - downloadedCount++ - toast.message = `下载中... (${downloadedCount}/${imageUrls.length})` - return blob - })) + const imageBlobs = await Promise.all( + imageUrls.map(async ({ url }) => { + const blob = await getBlob(url) + downloadedCount++ + toast.message = `下载中... (${downloadedCount}/${imageUrls.length})` + return blob + }), + ) const pack = new DownloadPackage() const { feedFormat } = options imageBlobs.forEach((blob, index) => { @@ -45,7 +52,10 @@ export const setupFeedImageExporter: ComponentEntry = async ({ settings: { optio originalUser: (card as RepostFeedsCard).repostUsername ?? card.username, 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() const packTitleData = { diff --git a/registry/lib/components/utils/image-exporter/index.ts b/registry/lib/components/utils/image-exporter/index.ts index 2f633822f..37900ff30 100644 --- a/registry/lib/components/utils/image-exporter/index.ts +++ b/registry/lib/components/utils/image-exporter/index.ts @@ -1,8 +1,21 @@ -import { defineComponentMetadata } from '@/components/define' +import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '@/components/define' import { matchUrlPattern } from '@/core/utils' import { columnUrls, feedsUrls } from '@/core/utils/urls' import { setupFeedImageExporter } from './feed' +const options = defineOptionsMetadata({ + columnFormat: { + defaultValue: '[title][ - n]', + displayName: '专栏图片命名格式', + }, + feedFormat: { + defaultValue: '[user][ - id][ - n]', + displayName: '动态图片命名格式', + }, +}) + +export type Options = OptionsOfMetadata + export const component = defineComponentMetadata({ name: 'imageExporter', displayName: '图片批量导出', @@ -21,14 +34,5 @@ export const component = defineComponentMetadata({ ...feedsUrls, ...columnUrls, ], - options: { - columnFormat: { - defaultValue: '[title][ - n]', - displayName: '专栏图片命名格式', - }, - feedFormat: { - defaultValue: '[user][ - id][ - n]', - displayName: '动态图片命名格式', - }, - }, + options, }) diff --git a/registry/lib/components/utils/image-resolution/index.ts b/registry/lib/components/utils/image-resolution/index.ts index ffc4867d6..436fe23bd 100644 --- a/registry/lib/components/utils/image-resolution/index.ts +++ b/registry/lib/components/utils/image-resolution/index.ts @@ -1,6 +1,16 @@ -import { defineComponentMetadata } from '@/components/define' +import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '@/components/define' import { startResolution } from './resolution' +const options = defineOptionsMetadata({ + scale: { + displayName: '缩放级别', + defaultValue: 'auto', + hidden: true, + }, +}) + +export type Options = OptionsOfMetadata + export const component = defineComponentMetadata({ name: 'imageResolution', displayName: '高分辨率图片', @@ -12,11 +22,5 @@ export const component = defineComponentMetadata({ description: { 'zh-CN': '根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)', }, - options: { - scale: { - displayName: '缩放级别', - defaultValue: 'auto', - hidden: true, - }, - }, + options, }) diff --git a/registry/lib/components/utils/image-resolution/resolution.ts b/registry/lib/components/utils/image-resolution/resolution.ts index 3d8b5a7d4..2ab9e1317 100644 --- a/registry/lib/components/utils/image-resolution/resolution.ts +++ b/registry/lib/components/utils/image-resolution/resolution.ts @@ -1,4 +1,5 @@ import { styledComponentEntry } from '@/components/styled-component' +import { Options } from '.' const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/ 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)) }) } -export const startResolution = styledComponentEntry(() => import('./fix.scss'), +export const startResolution = styledComponentEntry(() => import('./fix.scss'), async ({ settings }) => { const { allMutations } = await import('@/core/observer') const dpi = settings.options.scale === 'auto' diff --git a/registry/lib/components/utils/keymap/actions.ts b/registry/lib/components/utils/keymap/actions.ts index 6cdc53ef3..ace0242cb 100644 --- a/registry/lib/components/utils/keymap/actions.ts +++ b/registry/lib/components/utils/keymap/actions.ts @@ -1,6 +1,7 @@ import { playerAgent } from '@/components/video/player-agent' import { getComponentSettings } from '@/core/settings' import { registerAndGetData } from '@/plugins/data' +import { Options } from '.' import { KeyBindingAction, KeyBindingActionContext } from './bindings' export const clickElement = ( @@ -172,11 +173,11 @@ export const builtInActions: Record = { }, longJumpBackward: { displayName: '长倒退', - run: () => playerAgent.changeTime(-(getComponentSettings('keymap').options.longJumpSeconds)), + run: () => playerAgent.changeTime(-(getComponentSettings('keymap').options.longJumpSeconds)), }, longJumpForward: { displayName: '长前进', - run: () => playerAgent.changeTime(getComponentSettings('keymap').options.longJumpSeconds), + run: () => playerAgent.changeTime(getComponentSettings('keymap').options.longJumpSeconds), }, jumpBackward: { displayName: '倒退', diff --git a/registry/lib/components/utils/keymap/index.ts b/registry/lib/components/utils/keymap/index.ts index e9c71cab4..5f57b9896 100644 --- a/registry/lib/components/utils/keymap/index.ts +++ b/registry/lib/components/utils/keymap/index.ts @@ -1,89 +1,100 @@ import { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action' import { styledComponentEntry } from '@/components/styled-component' -import { ComponentEntry } from '@/components/types' -import { defineComponentMetadata } from '@/components/define' +import { + defineComponentMetadata, + defineOptionsMetadata, + OptionsOfMetadata, +} from '@/components/define' import { addComponentListener } from '@/core/settings' import { actions } from './actions' import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings' import { presetBase, presets } from './presets' -let config: KeyBindingConfig = null -const parseBindings = (bindings: Record) => ( - Object.entries(bindings).map(([actionName, keyString]) => { - const keys = keyString.split(' ').filter(it => it !== '') - return { - keys, - action: actions[actionName] || none, - } as KeyBinding - }) -) -const entry: ComponentEntry = styledComponentEntry(() => 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) +const options = defineOptionsMetadata({ + longJumpSeconds: { + defaultValue: 85, + displayName: '长跳跃秒数', + }, + customKeyBindings: { + defaultValue: {} as Record, + displayName: '自定义键位', + hidden: true, + }, + preset: { + defaultValue: 'Default', + displayName: '预设', + hidden: true, + }, }) +export type Options = OptionsOfMetadata +let config: KeyBindingConfig = null +const parseBindings = (bindings: Record): 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( + () => 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({ name: 'keymap', displayName: '快捷键扩展', - tags: [ - componentsTags.video, - componentsTags.utils, - ], + tags: [componentsTags.video, componentsTags.utils], // urlInclude: [ // ...videoAndBangumiUrls, // ...cheeseUrls, // ...mediaListUrls, // ], entry, - unload: () => { config && (config.enable = false) }, - reload: () => { config && (config.enable = true) }, + unload: () => { + config && (config.enable = false) + }, + reload: () => { + config && (config.enable = true) + }, description: { 'zh-CN': '为脚本的功能和 b 站的功能启用键盘快捷键支持, 快捷键列表可在`快捷键设置`中查看和配置.', }, extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default), - options: { - longJumpSeconds: { - defaultValue: 85, - displayName: '长跳跃秒数', - }, - customKeyBindings: { - defaultValue: {}, - displayName: '自定义键位', - hidden: true, - }, - preset: { - defaultValue: 'Default', - displayName: '预设', - hidden: true, - }, - }, + options, plugin: { displayName: '快捷键扩展 - 搜索支持', setup: ({ addData }) => { addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => { providers.push({ name: 'keymapSettings', - getActions: async () => [{ - name: '快捷键扩展设置', - description: 'Keymap Settings', - icon: 'mdi-keyboard-settings-outline', - action: async () => { - const { toggleKeymapSettings } = await import('./settings/vm') - toggleKeymapSettings() + getActions: async () => [ + { + name: '快捷键扩展设置', + description: 'Keymap Settings', + icon: 'mdi-keyboard-settings-outline', + action: async () => { + const { toggleKeymapSettings } = await import('./settings/vm') + toggleKeymapSettings() + }, }, - }], + ], }) }) }, diff --git a/registry/lib/components/video/player/common/speed.ts b/registry/lib/components/video/player/common/speed.ts index abd370937..f053e1573 100644 --- a/registry/lib/components/video/player/common/speed.ts +++ b/registry/lib/components/video/player/common/speed.ts @@ -1,7 +1,8 @@ import { ComponentEntry, ComponentMetadata, - ComponentOption, + OptionMetadata, + UnknownOptions, } from '@/components/types' import { CoreApis } from '@/core/core-apis' import { addComponentListener, ComponentSettings } from '@/core/settings' @@ -31,7 +32,7 @@ export type EntryContext = Parameters[0] export type OptionSubjects = O & { [K in keyof O as `${Exclude}$`]: Subject } -export class EntrySpeedComponent> +export class EntrySpeedComponent implements EntryContext { static create: < OO extends Record = unknown @@ -39,7 +40,7 @@ implements EntryContext { ComponentMetadata, 'entry' | 'reload' | 'unload' | 'options' > & { - options?: { [K in keyof OO]: ComponentOption } + options?: { [K in keyof OO]: OptionMetadata } }) => ComponentMetadata; static contextMap: Partial> = { diff --git a/registry/lib/components/video/player/extend-speed/component.ts b/registry/lib/components/video/player/extend-speed/component.ts index c8004f525..4f6c79e4d 100644 --- a/registry/lib/components/video/player/extend-speed/component.ts +++ b/registry/lib/components/video/player/extend-speed/component.ts @@ -28,7 +28,7 @@ export const ERROR_MESSAGE_DURATION = 5000 /** 计算菜单项 order */ export const calcOrder = (value: number) => ((MAX_BROWSER_SPEED_VALUE - value) * 10000).toString() -export interface Options { +export type Options = { /** 最大菜单高度 */ maxMenuHeight: boolean /** 隐藏进度条 */ diff --git a/registry/lib/components/video/player/remember-speed/component.ts b/registry/lib/components/video/player/remember-speed/component.ts index 0ff501f26..6c75b58b0 100644 --- a/registry/lib/components/video/player/remember-speed/component.ts +++ b/registry/lib/components/video/player/remember-speed/component.ts @@ -3,7 +3,7 @@ import { EntrySpeedComponent, VideoIdObject } from '../common/speed' import { NoSuchSpeedMenuItemElementError, SpeedContext } from '../common/speed/context' import { formatSpeedText } from '../common/speed/utils' -export interface Options { +export type Options = { /** 全局倍速 */ globalSpeed: number /** 固定全局倍速 */ diff --git a/registry/lib/docs/index.ts b/registry/lib/docs/index.ts index 7f5ad0dbe..9eead2c47 100644 --- a/registry/lib/docs/index.ts +++ b/registry/lib/docs/index.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from '@/components/types' +import { defineComponentMetadata } from '@/components/define' import { cdnRoots } from '@/core/cdn-types' import { branches } from '@/core/meta' import { getComponentsDoc } from './components-doc' @@ -90,7 +90,7 @@ ${getDocText(pluginsDoc.title, pluginsDoc.items)} } } } -export const doc: ComponentMetadata = { +export const doc = defineComponentMetadata({ name: 'featureDocsGenerator', displayName: '功能文档生成器', entry, @@ -99,4 +99,4 @@ export const doc: ComponentMetadata = { delete unsafeWindow.generateDocs }, tags: [componentsTags.utils], -} +}) diff --git a/registry/lib/plugins/style/custom-navbar-channel/index.ts b/registry/lib/plugins/style/custom-navbar-channel/index.ts index 7e5cfe955..edd343f71 100644 --- a/registry/lib/plugins/style/custom-navbar-channel/index.ts +++ b/registry/lib/plugins/style/custom-navbar-channel/index.ts @@ -1,3 +1,4 @@ +import { CustomNavbarOptions } from 'registry/lib/components/style/custom-navbar' import { PluginMetadata } from '@/plugins/plugin' import { getComponentSettings } from '@/core/settings' 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 name = 'channel' const isOpenInNewTab = () => { - const { options } = getComponentSettings('customNavbar') + const { options } = getComponentSettings('customNavbar') as { options: CustomNavbarOptions } if (name in options.openInNewTabOverrides) { return options.openInNewTabOverrides[name] } @@ -23,7 +24,10 @@ export const plugin: PluginMetadata = { content: () => import('./NavbarChannel.vue'), clickAction: () => { 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', + ) }, }) }) diff --git a/src/components/auto-update/checker.ts b/src/components/auto-update/checker.ts index a87ee11bc..10e968186 100644 --- a/src/components/auto-update/checker.ts +++ b/src/components/auto-update/checker.ts @@ -13,6 +13,7 @@ import { UpdateCheckItem, CheckSingleTypeUpdateConfig, } from './utils' +import { Options } from '.' export const checkUpdate = async (config: CheckUpdateConfig) => { const { @@ -140,7 +141,7 @@ export const checkAllUpdate = async (config: CheckSingleTypeUpdateConfig) => { console.groupEnd() } export const silentCheckUpdate = () => checkAllUpdate({ - maxCount: getComponentSettings(name).options.maxUpdateCount, + maxCount: getComponentSettings(name).options.maxUpdateCount, }) export const silentCheckUpdateAndReload = reload(silentCheckUpdate) diff --git a/src/components/auto-update/index.ts b/src/components/auto-update/index.ts index e120f749f..fcde49450 100644 --- a/src/components/auto-update/index.ts +++ b/src/components/auto-update/index.ts @@ -62,7 +62,7 @@ const optionsMetadata = defineOptionsMetadata({ }, }) -type Options = OptionsOfMetadata +export type Options = OptionsOfMetadata const entry: ComponentEntry = async ({ settings: { options: opt }, diff --git a/src/components/i18n/helpers.ts b/src/components/i18n/helpers.ts index 3748dae60..7bbd3f6b9 100644 --- a/src/components/i18n/helpers.ts +++ b/src/components/i18n/helpers.ts @@ -1,7 +1,7 @@ import { addData } from '@/plugins/data' import { getComponentSettings } from '@/core/settings' import { languageNameToCode } from '@/core/utils/i18n' -import { Translation, GeneralTranslation, RegexTranslation } from './types' +import { Translation, GeneralTranslation, RegexTranslation, Options as I18nOptions } from './types' /** * 在`plugin.setup`中可使用此帮助函数快速注入翻译数据 @@ -22,6 +22,6 @@ export const addI18nData = ( }) } export const getSelectedLanguage = () => { - const settings = getComponentSettings('i18n') + const settings = getComponentSettings('i18n') return languageNameToCode(settings.options.language) } diff --git a/src/components/i18n/machine-translator/translators.ts b/src/components/i18n/machine-translator/translators.ts index dafaf3e1b..e876096b8 100644 --- a/src/components/i18n/machine-translator/translators.ts +++ b/src/components/i18n/machine-translator/translators.ts @@ -4,6 +4,7 @@ import { getComponentSettings } from '@/core/settings' import { languageNameToCode } from '@/core/utils/i18n' import { registerAndGetData } from '@/plugins/data' import { formData } from '@/core/utils' +import { Options as I18nOptions } from '../types' export abstract class MachineTranslateProvider { abstract translate(text: string): Promise @@ -11,7 +12,7 @@ export abstract class MachineTranslateProvider { abstract link: string abstract defaultLanguage: string protected getTargetLanguage() { - const i18n = getComponentSettings('i18n') + const i18n = getComponentSettings('i18n') if (i18n.enabled) { return languageNameToCode(i18n.options.language) } @@ -109,7 +110,7 @@ export const [translateProviders] = registerAndGetData('i18n.machineTranslators' } as Record) export const translateProviderNames = Object.keys(translateProviders) export const getTranslator = (): MachineTranslateProvider => { - const { options: { translator } } = getComponentSettings('i18n') + const { options: { translator } } = getComponentSettings('i18n') const provider = translateProviders[translator] || translateProviders.GoogleCN return provider } diff --git a/src/components/i18n/types.ts b/src/components/i18n/types.ts index eb44b5e7e..4b99b0255 100644 --- a/src/components/i18n/types.ts +++ b/src/components/i18n/types.ts @@ -10,3 +10,7 @@ export type LanguagePack = { map?: [string, Translation][] regex?: RegexTranslation } +export type Options = { + language: string, + translator: string, +} diff --git a/src/components/styled-component.ts b/src/components/styled-component.ts index 36947ea8e..fec9c7b94 100644 --- a/src/components/styled-component.ts +++ b/src/components/styled-component.ts @@ -1,33 +1,31 @@ import { none } from '@/core/utils' -import { ComponentEntry, ComponentMetadata } from './component' +import { ComponentEntry, ComponentMetadata, UnknownOptions } from './component' /** * 创建一个自动添加指定样式的组件入口函数 * @param styleImport 动态导入样式的函数 * @param entry 组件入口函数 */ -export const styledComponentEntry = ( +export const styledComponentEntry = ( styleImport: () => Promise<{ default: string }>, - entry: ComponentEntry, -): ComponentEntry => ( - async context => { + entry: ComponentEntry, +): ComponentEntry => async context => { const { default: style } = await styleImport() const { addStyle } = await import('@/core/style') addStyle(style, context.metadata.name) return entry(context) } -) /** * 创建仅切换样式的组件`entry`, `reload`和`unload`, 展开至组件定义中即可, 也可以提供可选的组件入口函数 * @param styleImport 动态导入样式的函数 * @param entry 组件入口函数 */ -export const toggleStyle = ( +export const toggleStyle = ( name: string, styleImport: () => Promise<{ default: string }>, - entry: ComponentEntry = none, -): Pick => { + entry: ComponentEntry = none, +): Pick, 'name' | 'entry' | 'reload' | 'unload'> => { let styleElement: HTMLStyleElement = null const styleEntry = async () => { if (styleElement) { @@ -39,9 +37,7 @@ export const toggleStyle = ( } return { name, - entry: context => ( - styleEntry().then(() => entry(context)) - ), + entry: context => styleEntry().then(() => entry(context)), reload: styleEntry, unload: () => { styleElement?.remove() diff --git a/src/components/switch-options.ts b/src/components/switch-options.ts index 26d251aa2..ab03716cc 100644 --- a/src/components/switch-options.ts +++ b/src/components/switch-options.ts @@ -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 = { - [key: string]: { - displayName: string - defaultValue: boolean - } +import { getComponentSettings, addComponentListener } from '@/core/settings' +import { + ComponentEntry, + 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 = { + [key in S]: SwitchItemMetadata +} + +/** + * 可用于单独定义 SwitchItemsMetadata + */ +export const defineSwitchItemsMetadata = ( + c: SwitchItemsMetadata, +): SwitchItemsMetadata => c + +/** + * 用于配置 API 的行为。使用 {@link defineSwitchMetadata} 定义。 + */ +export interface SwitchMetadata { + /** + * 作为键名注入到组件 `options` 中 + * + * @see {@link SwitchOptions} + */ + name: N + /** 每个开关的单独配置。可用 {@link defineSwitchItemsMetadata} 单独定义 */ + switches: SwitchItemsMetadata + /** + * 是否单选。值为 `undefined` 时取默认值。 + * + * @default false + */ + radio?: undefined | boolean + /** + * 控制开关变暗的时机 + * + * `undefined`: 始终不变暗 + * `'checked'`: 当开关关闭时变暗 + * `'notChecked'`: 始终为变暗状态 + * + * @default undefined + */ + dimAt?: undefined | 'checked' | 'notChecked' + /** 配置每个开关的图标 */ switchProps?: { checkedIcon?: string notCheckedIcon?: string iconPosition?: 'left' | 'right' } } -export const createSwitchOptions = (options: SwitchOptions) => { - if (options.radio === undefined) { - options.radio = false + +/** + * 定义一个 {@link SwitchMetadata} + */ +export const defineSwitchMetadata = ( + c: SwitchMetadata, +): SwitchMetadata => c + +/** + * 注入组件的 options 的一部分。 + * + * 接口中的属性值大部分来自于 {@link SwitchMetadata} 中的定义。 + * 用户在 `SwitchMetadata` 中未定义的,为其默认值。 + * + * @see {@link SwitchOptions} + */ +export interface SwitchMetadataOption { + name: N + switches: SwitchItemsMetadata + radio: boolean + dimAt: undefined | 'checked' | 'notChecked' + switchProps?: { + checkedIcon?: string + notCheckedIcon?: string + iconPosition?: 'left' | 'right' } - const { name: optionName, switches } = options - const extendComponentOptions: ComponentOptions = {} - Object.entries(switches).forEach(([key, { displayName, defaultValue }]) => { - extendComponentOptions[`switch-${key}`] = { + /** 组件名称 */ + componentName: string + /** 组件的 Widget 显示名称 */ + optionDisplayName: string +} + +/** + * 创建一个 SwitchMetadataOption + */ +const newSwitchMetadataOption = ( + metadata: SwitchMetadata, + componentName: string, + optionDisplayName, +) => ({ + ...metadata, + radio: metadata.radio === undefined ? false : metadata.radio, + dimAt: metadata.dimAt, + componentName, + optionDisplayName, + }) + +/** + * 为类型参数 `S` 加上 'switch-' 前缀。 + */ +export type SwitchItemNames = `switch-${S}` + +/** + * API 注入的 options 中的一部分 + */ +export type SwitchItemOptions = Record, boolean> + +/** + * 包装完成后的组件元数据中的 options 类型 + * + * 由三部分组成: + * 1. 组件元数据被包装前的所有 options + * 2. 一个键值对:键为 {@link SwitchMetadata} 定义时的 `name` 属性,值为 {@link SwitchMetadataOption} 类型 + * 3. 对应 {@link SwitchItemsMetadata} 的定义内容 + * (来自于 `SwitchMetadata` 的 `switches`)。详情见该类型描述 + */ +export type SwitchOptions = O & { + N: SwitchMetadataOption +} & SwitchItemOptions + +/** + * 提取 SwitchOptions 类型 + */ +export type SwitchOptionsOfSwitchMetadata< + O extends UnknownOptions, + C extends SwitchMetadata, +> = C extends SwitchMetadata ? SwitchOptions : never + +/** + * 提取 SwitchOptions 类型 + */ +export type SwitchOptionsOfMetadata< + M extends OptionsMetadata, + C extends SwitchMetadata, +> = SwitchOptionsOfSwitchMetadata, C> + +/** + * 在原始的 OptionsMetadata 中添加了该 API 注入的 options 后形成的类型 + */ +export type SwitchOptionsMetadata< + O extends UnknownOptions, + N extends string, + S extends string, +> = OptionsMetadata> + +/** + * 向传入的 `options` 注入 API 所需的内容 + * + * 该函数会直接修改传入的 `options` 自身,并将其返回。 + */ +type SwitchOptionsMetadataExtender = ( + options: OptionsMetadata, + switchMetadataOption: SwitchMetadataOption, +) => SwitchOptionsMetadata + +/** + * 创建一个 {@link SwitchOptionsMetadataExtender} + */ +const newSwitchOptionsMetadataExtender = ( + itemsMetadata: SwitchItemsMetadata, +): SwitchOptionsMetadataExtender => { + const optionsToExtend = {} + const entries = Object.entries(itemsMetadata) + for (const [key, { displayName, defaultValue }] of entries) { + optionsToExtend[`switch-${key}`] = { defaultValue, displayName, hidden: true, } - }) - return (component: ComponentMetadata) => { - const optionDisplayName = `${component.displayName}选项` - const selfOption = { - componentName: component.name, - optionDisplayName, - } - Object.assign(options, selfOption) - extendComponentOptions[optionName] = { + } + return ( + options: OptionsMetadata, + switchMetadataOption: SwitchMetadataOption, + ) => { + optionsToExtend[switchMetadataOption.name as string] = { defaultValue: options, - displayName: optionDisplayName, + displayName: switchMetadataOption.optionDisplayName, } - component.options = { ...component.options, ...extendComponentOptions } - if (!component.widget) { - component.widget = { - component: () => import('./SwitchOptions.vue').then(m => m.default), - options, - } - } - const originalEntry = component.entry - component.entry = async (...args) => { - originalEntry?.(...args) - const { name } = component - const componentOptions = getComponentSettings(name).options - Object.keys(componentOptions).forEach(key => { - if (key.startsWith('switch-')) { - addComponentListener( - `${name}.${key}`, - (value: boolean) => { - document.body.classList.toggle(`${name}-${key}`, value) - }, - true, - ) - } - }) - } - return component + Object.assign(options, optionsToExtend) + return options as SwitchOptionsMetadata + } +} + +/** + * 创建默认 Widget + */ +const newWidget = ( + options: SwitchMetadataOption, +): Omit => ({ + component: () => import('./SwitchOptions.vue').then(m => m.default), + options, + }) + +/** + * 携带 API 注入的 `options` 的 `entry` 函数类型 + */ +export type SwitchEntry< + O extends UnknownOptions, + N extends string, + S extends string, + T = unknown, +> = ComponentEntry, T> + +/** + * 用于被 API 包装的组件原数据,类似于 `ComponentMetadata`。 + * + * 定义该类型时,请使用 {@link defineIncompleteSwitchComponentMetadata}。 + * + * 在通过该函数定义组件的 `entry` 时,可以获得被 API 注入的 options 的类型提示。 + */ +export type IncompleteSwitchComponentMetadata< + O extends UnknownOptions, + N extends string, + S extends string, +> = { + [K in keyof ComponentMetadata]: K extends 'entry' + ? SwitchEntry + : ComponentMetadata[K] +} + +/** + * 定义一个用于被该 API 包装的组件元数据, + * 类似于 {@link import("./define").defineComponentMetadata}。 + * + * 与直接定义的不同之处在于:定义 `entry` 时,可以获得被 API 注入的 options 的类型提示。 + */ +export const defineIncompleteSwitchComponentMetadata = < + O extends UnknownOptions, + N extends string, + S extends string, +>( + m: IncompleteSwitchComponentMetadata, + ): IncompleteSwitchComponentMetadata => m + +/** + * 包装原始 `entry` 函数并返回 + */ +const newSwitchEntry = ( + component: ComponentMetadata | IncompleteSwitchComponentMetadata, +): SwitchEntry => (...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> + +/** + * 用于包装组件元数据,生成开关选项。 + * + * 传入参数可以是普通的组件元数据,也可以是 {@link IncompleteSwitchComponentMetadata}。 + * 后者应使用 {@link defineIncompleteSwitchComponentMetadata} 定义; + * 在通过该函数定义组件的 `entry` 时,可以获得被 API 注入的 options 的类型提示。 + * + * 为保障组件功能的正常运行,请勿使用以 'switch-' 开头的 option。 + * + * 若传入组件未定义 Widget,则会注入一个用于控制各开关的 Widget。 + * 此 Widget 的功能与注入组件的 options 功能相同。 + * + * 被包装后的元数据,其 `entry` 函数是原函数的包装,用于实现相关功能。 + * + * 该函数会直接修改传入参数自身,并将其返回。 + * + * @param component - 被包装的组件元数据 + * @returns 包装完成的组件元数据 + */ +export type SwitchComponentWrapper = ( + component: ComponentMetadata | IncompleteSwitchComponentMetadata, +) => SwitchComponentMetadata + +/** + * 创建一个 {@link SwitchComponentWrapper} + * + * 传入参数请使用 {@link defineSwitchMetadata} 定义。 + * + * @param metadata - 相关配置元数据 + * @returns 组件包装器 + */ +export const newSwitchComponentWrapper = ( + metadata: SwitchMetadata, +): SwitchComponentWrapper => { + const extendOptions = newSwitchOptionsMetadataExtender(metadata.switches) + // 返回的 wrapper + return ( + component: ComponentMetadata | IncompleteSwitchComponentMetadata, + ) => { + 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 } } diff --git a/src/components/types.ts b/src/components/types.ts index 86bf1be91..0ea865504 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -48,8 +48,7 @@ export interface ComponentTag { type ComponentOptionValidator = (value: T, oldValue: T) => T | undefined | null -// TODO: 参考 discussion #3041。当不兼容代码替换完成后将 any 改为 unknown -export type UnknownOptions = Record +export type UnknownOptions = Record export type EmptyOptions = Record @@ -81,10 +80,6 @@ export type OptionsMetadata = { [OptionName in keyof O]: OptionMetadata } -// TODO: 参考 discussion #3041。当不兼容代码替换完成后删除 -export type ComponentOptions = OptionsMetadata -export type ComponentOption = OptionMetadata - /** 组件标签 */ export const componentsTags = { /** 视频 */ diff --git a/src/core/settings/helpers.ts b/src/core/settings/helpers.ts index ff8bd48ba..7691c92ef 100644 --- a/src/core/settings/helpers.ts +++ b/src/core/settings/helpers.ts @@ -58,12 +58,11 @@ const emptySettings: ComponentSettings = { }), } -// TODO: 参考 discussion #3041。 -// 当不兼容代码替换完成后将 R 的默认类型替换为 UnknownOptions /** * 获取已加载组件的设置 * - * 若组件未安装,则返回一个默认的 ComponentSettings 对象: + * 使用此函数,应当确保该组件已被加载。 + * 否则返回值是一个默认的 ComponentSettings 对象: * ```js * { * enabled: false, @@ -73,9 +72,9 @@ const emptySettings: ComponentSettings = { * * @param component 组件或组件名称 */ -export const getComponentSettings = ( - component: ComponentMetadata | string, -): ComponentSettings => { +export const getComponentSettings = ( + component: ComponentMetadata | string, +): ComponentSettings => { let componentMetadata: ComponentMetadata if (typeof component === 'string') { if (componentsMap[component] === undefined) {