mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge branch 'preview-features' of https://github.com/the1812/Bilibili-Evolved into preview-features
This commit is contained in:
commit
65439a9fe6
@ -19,7 +19,7 @@ const importSeries = async (sid, uid, csrf) => {
|
||||
|
||||
// 创建收藏夹,获取新收藏夹id
|
||||
let favId = 0
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@ -43,7 +43,7 @@ const importSeries = async (sid, uid, csrf) => {
|
||||
for (let i = 0; i < seriesVideos.length; i++) {
|
||||
// 做个延迟,防止太快而遭服务器拒绝
|
||||
await delay(500)
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@ -72,7 +72,7 @@ const importSeries = async (sid, uid, csrf) => {
|
||||
}
|
||||
|
||||
const importCollection = async (sid, csrf) => {
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
|
||||
@ -444,6 +444,7 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
|
||||
setTimeout(() => this.forceUpdateStyle(value))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected readonly filterNativeSpeed =
|
||||
() =>
|
||||
({ subscribe, next }: PublishContext<number>) => {
|
||||
|
||||
69
src/components/bisector/DialogContent.vue
Normal file
69
src/components/bisector/DialogContent.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="bisector-dialog-content-container">
|
||||
<div class="bisector-dialog-content-text">
|
||||
组件二等分进行中,预计剩余
|
||||
<b style="color: var(--theme-color)">{{ rouge }} </b>轮次,请确认当前情况:
|
||||
</div>
|
||||
<div class="bisector-dialog-button-group">
|
||||
<VButton @click="onGood">正常</VButton>
|
||||
<VButton @click="onBad">不正常</VButton>
|
||||
<VButton type="primary" @click="onAbort">终止</VButton>
|
||||
</div>
|
||||
<div class="bisector-dialog-content-text weak">
|
||||
*可以点击×号关闭对话框,刷新页面后,会再次询问
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { VButton } from '@/ui'
|
||||
|
||||
export default Vue.extend({
|
||||
components: { VButton },
|
||||
props: {
|
||||
rouge: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
onAbort: {
|
||||
type: Function,
|
||||
default: none,
|
||||
},
|
||||
onGood: {
|
||||
type: Function,
|
||||
default: none,
|
||||
},
|
||||
onBad: {
|
||||
type: Function,
|
||||
default: none,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bisector-dialog-content-container {
|
||||
padding: 0 16px;
|
||||
|
||||
.bisector-dialog-content-text {
|
||||
&.weak {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.bisector-dialog-content-text,
|
||||
.bisector-dialog-button-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bisector-dialog-button-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
.be-button:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
src/components/bisector/DialogTitle.vue
Normal file
43
src/components/bisector/DialogTitle.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
组件二等分
|
||||
<div
|
||||
class="peek"
|
||||
title="透视"
|
||||
style="margin-left: auto"
|
||||
@mouseover="peek = true"
|
||||
@mouseout="peek = false"
|
||||
>
|
||||
<VIcon icon="eye" :size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { VIcon } from '@/ui'
|
||||
|
||||
export default Vue.extend({
|
||||
components: { VIcon },
|
||||
data() {
|
||||
return { peek: false }
|
||||
},
|
||||
watch: {
|
||||
peek(value) {
|
||||
this.$el.closest('.be-dialog').style.opacity = value ? '0.1' : '1'
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'common';
|
||||
|
||||
div {
|
||||
@include h-center(8px);
|
||||
}
|
||||
|
||||
.peek {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
54
src/components/bisector/ResultToastContent.vue
Normal file
54
src/components/bisector/ResultToastContent.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>可能出问题的组件是:{{ displayName }}({{ name }})</div>
|
||||
<div>
|
||||
<span class="link" @click="restore">恢复原状({{ countdown }} 秒)</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
countdown: 30,
|
||||
userComponent: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayName() {
|
||||
return this.userComponent?.metadata?.displayName
|
||||
},
|
||||
name() {
|
||||
return this.userComponent?.metadata?.name
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
countdown(value) {
|
||||
if (value === 0) {
|
||||
this.restore()
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.interval = setInterval(() => {
|
||||
if (this.countdown > 0) {
|
||||
this.countdown--
|
||||
} else {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
}, 1e3)
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.interval)
|
||||
},
|
||||
methods: {
|
||||
restore() {
|
||||
clearInterval(this.interval)
|
||||
this.$emit('restore')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
198
src/components/bisector/api.ts
Normal file
198
src/components/bisector/api.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { DialogInstance, showDialog } from '@/core/dialog'
|
||||
import type { Settings } from '@/core/settings/types'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { getRandomId, mountVueComponent, delay } from '@/core/utils'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import type { RecordValue } from '../types'
|
||||
import type { BisectNext, BisectReturn } from './bisect'
|
||||
import { bisect } from './bisect'
|
||||
import { BisectorOptions } from './options'
|
||||
import ResultToastContent from './ResultToastContent.vue'
|
||||
|
||||
type UserComponent = RecordValue<Settings['userComponents']>
|
||||
|
||||
let bisectorOptions: BisectorOptions
|
||||
let scopedConsole: ReturnType<typeof useScopedConsole>
|
||||
let bisectorGenerator: Generator<BisectNext<UserComponent>, BisectReturn<UserComponent>>
|
||||
let groupedComponents: Awaited<ReturnType<typeof classifyComponents>>
|
||||
let dialog: DialogInstance
|
||||
|
||||
export const setOptions = (options: BisectorOptions) => {
|
||||
if (!bisectorOptions) {
|
||||
bisectorOptions = options
|
||||
} else {
|
||||
Object.assign(bisectorOptions, options)
|
||||
}
|
||||
}
|
||||
|
||||
export const setConsole = (console: ReturnType<typeof useScopedConsole>) => {
|
||||
scopedConsole = console
|
||||
}
|
||||
|
||||
const classifyComponents = async () => {
|
||||
const { settings } = await import('@/core/settings')
|
||||
|
||||
const { userComponents } = settings
|
||||
const configurableUserComponents = lodash.pickBy(
|
||||
userComponents,
|
||||
v => v.metadata.configurable || true,
|
||||
)
|
||||
const { targetComponents, keepDisabledComponents, keepEnabledComponents } = lodash.transform(
|
||||
configurableUserComponents,
|
||||
(result, v, k) => {
|
||||
if (bisectorOptions.keepEnabledComponents?.includes(k)) {
|
||||
result.keepEnabledComponents.push(v)
|
||||
return
|
||||
}
|
||||
if (bisectorOptions.keepDisabledComponents?.includes(k)) {
|
||||
result.keepDisabledComponents.push(v)
|
||||
return
|
||||
}
|
||||
result.targetComponents.push(v)
|
||||
},
|
||||
{
|
||||
targetComponents: [] as UserComponent[],
|
||||
keepDisabledComponents: [] as UserComponent[],
|
||||
keepEnabledComponents: [] as UserComponent[],
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
userComponents,
|
||||
configurableUserComponents,
|
||||
targetComponents,
|
||||
keepDisabledComponents,
|
||||
keepEnabledComponents,
|
||||
}
|
||||
}
|
||||
|
||||
const setComponentsEnabled = (components: UserComponent[], enabled: boolean) =>
|
||||
components.forEach(component => (component.settings.enabled = enabled))
|
||||
|
||||
const getComponentNames = (components: UserComponent[]) =>
|
||||
components
|
||||
.map(component => `${component.metadata.displayName}(${component.metadata.name})`)
|
||||
.join(', ') || '无'
|
||||
|
||||
export const isRecover = () => !lodash.isEmpty(bisectorOptions.bisectInitialState)
|
||||
|
||||
export const stop = async () => {
|
||||
scopedConsole?.log('stop - 准备停止组件二等分')
|
||||
dialog?.close()
|
||||
const { configurableUserComponents } = await classifyComponents()
|
||||
const unmatchedComponents: UserComponent[] = []
|
||||
for (const [componentName, componentSettings] of Object.entries(configurableUserComponents)) {
|
||||
const originalStatus = bisectorOptions.originalComponentEnableState?.[componentName]
|
||||
if (originalStatus == null) {
|
||||
unmatchedComponents.push(componentSettings)
|
||||
continue
|
||||
}
|
||||
componentSettings.settings.enabled = originalStatus
|
||||
}
|
||||
if (unmatchedComponents.length) {
|
||||
const msg = `部分组件未能还原状态:${getComponentNames(unmatchedComponents)}`
|
||||
scopedConsole?.warn(`stop - ${msg}`)
|
||||
Toast.error(msg, '组件二等分')
|
||||
await delay(3e3)
|
||||
}
|
||||
scopedConsole?.log('stop - 清理状态')
|
||||
bisectorGenerator = null
|
||||
bisectorOptions.bisectInitialState = {}
|
||||
bisectorOptions.originalComponentEnableState = {}
|
||||
scopedConsole?.log('stop - 重载页面')
|
||||
location.reload()
|
||||
}
|
||||
|
||||
export const next = async (seeingBad?: boolean, autoReload?: boolean) => {
|
||||
dialog?.close()
|
||||
scopedConsole?.log(
|
||||
`next - 当前工作状态:${
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
seeingBad == null ? '未知' : seeingBad ? '异常' : '正常'
|
||||
}`,
|
||||
)
|
||||
const { done, value } = bisectorGenerator.next(seeingBad)
|
||||
if (done) {
|
||||
const { low, high } = value
|
||||
bisectorOptions.bisectInitialState = { low, high }
|
||||
const elementId = `bisector-result-toast-content-${getRandomId()}`
|
||||
Toast.info(/* html */ `<div id="${elementId}"></div>`, '组件二等分结果')
|
||||
await delay()
|
||||
const vm = mountVueComponent<{ userComponent: UserComponent }>(
|
||||
ResultToastContent,
|
||||
`#${elementId}`,
|
||||
)
|
||||
vm.userComponent = value.target
|
||||
vm.$on('restore', () => {
|
||||
stop()
|
||||
})
|
||||
} else {
|
||||
const { slice, low, high } = value as BisectNext<UserComponent>
|
||||
const needEnabled = slice
|
||||
const needDisabled = lodash.difference(groupedComponents.targetComponents, slice)
|
||||
bisectorOptions.bisectInitialState = { low, high }
|
||||
scopedConsole?.log(`next - 关闭组件:${getComponentNames(needDisabled)}`)
|
||||
scopedConsole?.log(`next - 开启组件:${getComponentNames(needEnabled)}`)
|
||||
setComponentsEnabled(needDisabled, false)
|
||||
setComponentsEnabled(needEnabled, true)
|
||||
if (autoReload) {
|
||||
scopedConsole?.log('next - 重载页面')
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
return { done, value }
|
||||
}
|
||||
|
||||
export const recover = async () => {
|
||||
scopedConsole?.log('recover - 准备恢复组件二等分')
|
||||
groupedComponents = await classifyComponents()
|
||||
const { targetComponents, keepDisabledComponents, keepEnabledComponents } = groupedComponents
|
||||
setComponentsEnabled(keepEnabledComponents, true)
|
||||
setComponentsEnabled(keepDisabledComponents, false)
|
||||
scopedConsole?.log(`recover - 保持关闭组件:${getComponentNames(keepDisabledComponents)}`)
|
||||
scopedConsole?.log(`recover - 保持开启组件:${getComponentNames(keepEnabledComponents)}`)
|
||||
scopedConsole?.log(`recover - 全部目标组件:${getComponentNames(targetComponents)}`)
|
||||
bisectorGenerator = bisect(targetComponents, bisectorOptions.bisectInitialState)
|
||||
const { done, value } = await next()
|
||||
if (!done) {
|
||||
const { rouge } = value as BisectNext<UserComponent>
|
||||
dialog = showDialog({
|
||||
title: () => import('./DialogTitle.vue'),
|
||||
content: () => import('./DialogContent.vue'),
|
||||
contentProps: {
|
||||
rouge,
|
||||
onGood: () => next(false, true),
|
||||
onBad: () => next(true, true),
|
||||
onAbort: () => stop(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const start = async () => {
|
||||
if (isRecover()) {
|
||||
await recover()
|
||||
return
|
||||
}
|
||||
scopedConsole?.log('start - 准备开始组件二等分')
|
||||
groupedComponents = await classifyComponents()
|
||||
const {
|
||||
configurableUserComponents,
|
||||
targetComponents,
|
||||
keepDisabledComponents,
|
||||
keepEnabledComponents,
|
||||
} = groupedComponents
|
||||
scopedConsole?.log('start - 保存组件初始启用状态')
|
||||
bisectorOptions.originalComponentEnableState = lodash.mapValues(
|
||||
configurableUserComponents,
|
||||
v => v.settings.enabled,
|
||||
)
|
||||
setComponentsEnabled(targetComponents, true)
|
||||
setComponentsEnabled(keepEnabledComponents, true)
|
||||
setComponentsEnabled(keepDisabledComponents, false)
|
||||
scopedConsole?.log(`start - 保持关闭组件:${getComponentNames(keepDisabledComponents)}`)
|
||||
scopedConsole?.log(`start - 保持开启组件:${getComponentNames(keepEnabledComponents)}`)
|
||||
scopedConsole?.log(`start - 启用全部目标组件:${getComponentNames(targetComponents)}`)
|
||||
bisectorGenerator = bisect(targetComponents, bisectorOptions.bisectInitialState)
|
||||
await next(undefined, true)
|
||||
}
|
||||
92
src/components/bisector/bisect.ts
Normal file
92
src/components/bisector/bisect.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
export interface BisectNext<O> {
|
||||
low: number
|
||||
high: number
|
||||
mid: number
|
||||
slice: O[]
|
||||
rouge: number
|
||||
}
|
||||
|
||||
export interface BisectReturn<O> extends BisectNext<O> {
|
||||
target: O
|
||||
}
|
||||
|
||||
export interface InitialState {
|
||||
low?: number
|
||||
high?: number
|
||||
}
|
||||
|
||||
export function* bisectLeft<O>(
|
||||
data: readonly O[],
|
||||
initialState?: InitialState,
|
||||
): Generator<BisectNext<O>, BisectReturn<O>> {
|
||||
let low = initialState?.low ?? 0
|
||||
let high = initialState?.high ?? data.length
|
||||
let mid: number
|
||||
|
||||
while (low + 1 < high) {
|
||||
mid = (low + high) >>> 1
|
||||
|
||||
const seeingBad = yield {
|
||||
low,
|
||||
high,
|
||||
mid,
|
||||
slice: data.slice(low, mid),
|
||||
rouge: Math.trunc(Math.log2(high - low)),
|
||||
}
|
||||
|
||||
if (seeingBad) {
|
||||
high = mid
|
||||
} else {
|
||||
low = mid
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
low,
|
||||
high,
|
||||
mid,
|
||||
slice: data.slice(low, mid),
|
||||
rouge: Math.trunc(Math.log2(high - low)),
|
||||
target: data[low],
|
||||
}
|
||||
}
|
||||
|
||||
export function* bisectRight<O>(
|
||||
data: readonly O[],
|
||||
initialState?: InitialState,
|
||||
): Generator<BisectNext<O>, BisectReturn<O>> {
|
||||
let low = initialState?.low ?? 0
|
||||
let high = initialState?.high ?? data.length
|
||||
let mid = (low + high) >>> 1
|
||||
|
||||
while (low + 1 < high) {
|
||||
mid = (low + high) >>> 1
|
||||
|
||||
const seeingBad = yield {
|
||||
low,
|
||||
high,
|
||||
mid,
|
||||
slice: data.slice(mid, high),
|
||||
rouge: Math.trunc(Math.log2(high - low)),
|
||||
}
|
||||
|
||||
if (seeingBad) {
|
||||
low = mid
|
||||
} else {
|
||||
high = mid
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
low,
|
||||
high,
|
||||
mid,
|
||||
slice: data.slice(low, mid),
|
||||
rouge: Math.trunc(Math.log2(high - low)),
|
||||
target: data[low],
|
||||
}
|
||||
}
|
||||
|
||||
export const bisect = bisectLeft
|
||||
45
src/components/bisector/index.ts
Normal file
45
src/components/bisector/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { bisectorOptionsMetadata } from './options'
|
||||
import { LifeCycleEventTypes } from '@/core/life-cycle'
|
||||
import { componentsTags } from '@/components/types'
|
||||
import * as bisector from './api'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import type { LaunchBarActionProvider } from '../launch-bar/launch-bar-action'
|
||||
|
||||
export const component = defineComponentMetadata({
|
||||
name: 'bisector',
|
||||
displayName: '组件二等分',
|
||||
tags: [componentsTags.general, componentsTags.utils],
|
||||
hidden: true,
|
||||
configurable: false,
|
||||
entry: async ({ settings: { options } }) => {
|
||||
bisector.setOptions(options)
|
||||
bisector.setConsole(useScopedConsole('组件二等分'))
|
||||
unsafeWindow.addEventListener(LifeCycleEventTypes.ComponentsLoaded, () => {
|
||||
if (bisector.isRecover()) {
|
||||
bisector.recover()
|
||||
}
|
||||
})
|
||||
},
|
||||
options: bisectorOptionsMetadata,
|
||||
plugin: {
|
||||
displayName: '组件二等分 - 功能扩展',
|
||||
setup: ({ addData }) => {
|
||||
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
|
||||
providers.push({
|
||||
name: 'bisector-start',
|
||||
getActions: async () => [
|
||||
{
|
||||
name: '开始 / 继续组件二等分',
|
||||
description: 'Start/Continue component bisection',
|
||||
icon: 'mdi-view-split-horizontal',
|
||||
action: async () => {
|
||||
await bisector.start()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
31
src/components/bisector/options.ts
Normal file
31
src/components/bisector/options.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import type { InitialState } from './bisect'
|
||||
|
||||
export const bisectorOptionsMetadata = defineOptionsMetadata({
|
||||
// 原始的组件启用状态
|
||||
originalComponentEnableState: {
|
||||
defaultValue: {} as Record<string, boolean>,
|
||||
hidden: true,
|
||||
},
|
||||
// 保持禁用状态的组件内部名称数组
|
||||
keepDisabledComponents: {
|
||||
defaultValue: [] as string[],
|
||||
hidden: true,
|
||||
},
|
||||
// 保持启用状态的组件内部名称数组
|
||||
keepEnabledComponents: {
|
||||
defaultValue: [] as string[],
|
||||
hidden: true,
|
||||
},
|
||||
// bisect 生成器的初始状态
|
||||
bisectInitialState: {
|
||||
defaultValue: {} as Partial<InitialState>,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
|
||||
export const getBisectorOptions = () =>
|
||||
getComponentSettings<OptionsOfMetadata<typeof bisectorOptionsMetadata>>('bisector').options
|
||||
|
||||
export type BisectorOptions = ReturnType<typeof getBisectorOptions>
|
||||
@ -4,6 +4,7 @@ import { component as LaunchBar } from './launch-bar'
|
||||
import { component as I18n } from './i18n'
|
||||
import { component as AutoUpdate } from './auto-update'
|
||||
import { component as NotifyNewVersion } from './notify-new-version'
|
||||
import { component as Bisector } from './bisector'
|
||||
|
||||
export const getBuiltInComponents = (): ComponentMetadata[] => [
|
||||
SettingsPanel,
|
||||
@ -11,6 +12,7 @@ export const getBuiltInComponents = (): ComponentMetadata[] => [
|
||||
I18n,
|
||||
AutoUpdate,
|
||||
NotifyNewVersion,
|
||||
Bisector,
|
||||
]
|
||||
|
||||
export const isBuiltInComponent = (name: string) =>
|
||||
|
||||
@ -12,11 +12,15 @@ export class Translator {
|
||||
static map: Map<string, any>
|
||||
static regex: [RegExp, string][]
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected getValue = (node: Node) => node.nodeValue
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected setValue = (node: Node, value: string) => {
|
||||
node.nodeValue = value
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected getElement = (node: Node) => node as Element
|
||||
translate(node: Node) {
|
||||
let value = this.getValue(node)
|
||||
@ -99,17 +103,23 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
export class TextNodeTranslator extends Translator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getElement = (node: Node) => node.parentElement
|
||||
}
|
||||
export class TitleTranslator extends Translator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getValue = (node: Node) => (node as Element).getAttribute('title')
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setValue = (node: Node, value: string) => {
|
||||
;(node as Element).setAttribute('title', value)
|
||||
}
|
||||
}
|
||||
export class PlaceholderTranslator extends Translator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setValue = (node: Node, value: string) => {
|
||||
;(node as Element).setAttribute('placeholder', value)
|
||||
}
|
||||
|
||||
@ -216,3 +216,6 @@ export interface ComponentMetadata<O extends UnknownOptions = UnknownOptions>
|
||||
|
||||
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
|
||||
export type UserComponentMetadata = Omit<ComponentMetadata, keyof FunctionalMetadata>
|
||||
|
||||
/** 推断 Record 的 Value 类型 */
|
||||
export type RecordValue<R> = R extends Record<any, infer V> ? V : never
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { componentToSettings } from '@/core/settings'
|
||||
import { isBuiltInComponent } from './built-in-components'
|
||||
import { ComponentMetadata, componentsMap } from './component'
|
||||
import * as bisector from './bisector/api'
|
||||
import { BisectorOptions } from './bisector/options'
|
||||
|
||||
/**
|
||||
* 安装自定义组件
|
||||
@ -132,3 +134,16 @@ export const toggleComponent = async (nameOrDisplayName: string) => {
|
||||
const { displayName } = userComponent.metadata
|
||||
return `已${enabled ? '开启' : '关闭'}组件'${displayName}', 可能需要刷新后才能生效`
|
||||
}
|
||||
|
||||
/**
|
||||
* 二等分自定义组件的开关状态
|
||||
*
|
||||
* @param options 二等分选项
|
||||
* @returns
|
||||
*/
|
||||
export const bisectComponent = async (options?: BisectorOptions) => {
|
||||
if (options) {
|
||||
bisector.setOptions(options)
|
||||
}
|
||||
return bisector
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ export interface DialogInputs {
|
||||
}
|
||||
export interface DialogInstance extends Required<DialogInputs> {
|
||||
open: boolean
|
||||
close: Promise<void>
|
||||
close(): Promise<void>
|
||||
closeListeners: (() => void)[]
|
||||
}
|
||||
export const showDialog = (inputs: DialogInputs) => {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
export class LoadFeatureCodeError extends Error {}
|
||||
|
||||
interface CodeSandbox {
|
||||
/**
|
||||
* feature 代码运行沙箱
|
||||
*/
|
||||
type CodeSandbox = {
|
||||
/**
|
||||
* 在沙箱中执行代码
|
||||
*
|
||||
@ -15,11 +18,11 @@ interface CodeSandbox {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代码运行沙箱
|
||||
* 创建 feature 代码的运行沙箱
|
||||
*
|
||||
* @returns 一个函数:接受代码,返回。
|
||||
* @returns 一个 `CodeSandbox`。
|
||||
*/
|
||||
const getSandbox = lodash.once((): CodeSandbox => {
|
||||
const createCodeSandbox = (): CodeSandbox => {
|
||||
// 需要被注入到 `sandbox` 中的键值对
|
||||
const injection = new Map([
|
||||
// 加固,防止逃逸
|
||||
@ -55,8 +58,9 @@ const getSandbox = lodash.once((): CodeSandbox => {
|
||||
return [exported, returned]
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let staticCodeSandbox: CodeSandbox | undefined
|
||||
/**
|
||||
* 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据
|
||||
*
|
||||
@ -69,14 +73,15 @@ const getSandbox = lodash.once((): CodeSandbox => {
|
||||
*
|
||||
* 代码默认以非严格模式执行,启用需自行添加 `use strict`。(从本项目中打包的 feature 自带严格模式)
|
||||
*
|
||||
* 全局对象为脚本管理器提供的 `window`,支持访问 `unsafeWindow`。
|
||||
* 代码执行时的全局对象为脚本管理器提供的 `window`。代码中支持访问 `unsafeWindow`。
|
||||
*
|
||||
* @param code - 被执行的代码
|
||||
* @returns 导出的元数据(不检测正确性)
|
||||
* @returns 导出的元数据(不检测是否为正确的 feature)
|
||||
* @throws {@link LoadFeatureCodeError}
|
||||
* 代码包含语法错误或代码执行时产生了异常
|
||||
*/
|
||||
export const loadFeatureCode = (code: string): unknown => {
|
||||
const [exported, returned] = getSandbox().run(code)
|
||||
staticCodeSandbox || (staticCodeSandbox = createCodeSandbox())
|
||||
const [exported, returned] = staticCodeSandbox.run(code)
|
||||
return exported || returned
|
||||
}
|
||||
|
||||
@ -407,6 +407,7 @@ export class DoubleClickEvent {
|
||||
singleClickHandler: (e: MouseEvent) => void = none
|
||||
|
||||
private clickedOnce = false
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private readonly stopPropagationHandler = (e: MouseEvent) => {
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user