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
|
// 创建收藏夹,获取新收藏夹id
|
||||||
let favId = 0
|
let favId = 0
|
||||||
while (true) {
|
for (;;) {
|
||||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
|
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@ -43,7 +43,7 @@ const importSeries = async (sid, uid, csrf) => {
|
|||||||
for (let i = 0; i < seriesVideos.length; i++) {
|
for (let i = 0; i < seriesVideos.length; i++) {
|
||||||
// 做个延迟,防止太快而遭服务器拒绝
|
// 做个延迟,防止太快而遭服务器拒绝
|
||||||
await delay(500)
|
await delay(500)
|
||||||
while (true) {
|
for (;;) {
|
||||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
|
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@ -72,7 +72,7 @@ const importSeries = async (sid, uid, csrf) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const importCollection = async (sid, csrf) => {
|
const importCollection = async (sid, csrf) => {
|
||||||
while (true) {
|
for (;;) {
|
||||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
|
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@ -444,6 +444,7 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
|
|||||||
setTimeout(() => this.forceUpdateStyle(value))
|
setTimeout(() => this.forceUpdateStyle(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected readonly filterNativeSpeed =
|
protected readonly filterNativeSpeed =
|
||||||
() =>
|
() =>
|
||||||
({ subscribe, next }: PublishContext<number>) => {
|
({ 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 I18n } from './i18n'
|
||||||
import { component as AutoUpdate } from './auto-update'
|
import { component as AutoUpdate } from './auto-update'
|
||||||
import { component as NotifyNewVersion } from './notify-new-version'
|
import { component as NotifyNewVersion } from './notify-new-version'
|
||||||
|
import { component as Bisector } from './bisector'
|
||||||
|
|
||||||
export const getBuiltInComponents = (): ComponentMetadata[] => [
|
export const getBuiltInComponents = (): ComponentMetadata[] => [
|
||||||
SettingsPanel,
|
SettingsPanel,
|
||||||
@ -11,6 +12,7 @@ export const getBuiltInComponents = (): ComponentMetadata[] => [
|
|||||||
I18n,
|
I18n,
|
||||||
AutoUpdate,
|
AutoUpdate,
|
||||||
NotifyNewVersion,
|
NotifyNewVersion,
|
||||||
|
Bisector,
|
||||||
]
|
]
|
||||||
|
|
||||||
export const isBuiltInComponent = (name: string) =>
|
export const isBuiltInComponent = (name: string) =>
|
||||||
|
|||||||
@ -12,11 +12,15 @@ export class Translator {
|
|||||||
static map: Map<string, any>
|
static map: Map<string, any>
|
||||||
static regex: [RegExp, string][]
|
static regex: [RegExp, string][]
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected getValue = (node: Node) => node.nodeValue
|
protected getValue = (node: Node) => node.nodeValue
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected setValue = (node: Node, value: string) => {
|
protected setValue = (node: Node, value: string) => {
|
||||||
node.nodeValue = value
|
node.nodeValue = value
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected getElement = (node: Node) => node as Element
|
protected getElement = (node: Node) => node as Element
|
||||||
translate(node: Node) {
|
translate(node: Node) {
|
||||||
let value = this.getValue(node)
|
let value = this.getValue(node)
|
||||||
@ -99,17 +103,23 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class TextNodeTranslator extends Translator {
|
export class TextNodeTranslator extends Translator {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
|
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getElement = (node: Node) => node.parentElement
|
getElement = (node: Node) => node.parentElement
|
||||||
}
|
}
|
||||||
export class TitleTranslator extends Translator {
|
export class TitleTranslator extends Translator {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getValue = (node: Node) => (node as Element).getAttribute('title')
|
getValue = (node: Node) => (node as Element).getAttribute('title')
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
setValue = (node: Node, value: string) => {
|
setValue = (node: Node, value: string) => {
|
||||||
;(node as Element).setAttribute('title', value)
|
;(node as Element).setAttribute('title', value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class PlaceholderTranslator extends Translator {
|
export class PlaceholderTranslator extends Translator {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
setValue = (node: Node, value: string) => {
|
setValue = (node: Node, value: string) => {
|
||||||
;(node as Element).setAttribute('placeholder', value)
|
;(node as Element).setAttribute('placeholder', value)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,3 +216,6 @@ export interface ComponentMetadata<O extends UnknownOptions = UnknownOptions>
|
|||||||
|
|
||||||
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
|
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
|
||||||
export type UserComponentMetadata = Omit<ComponentMetadata, keyof FunctionalMetadata>
|
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 { componentToSettings } from '@/core/settings'
|
||||||
import { isBuiltInComponent } from './built-in-components'
|
import { isBuiltInComponent } from './built-in-components'
|
||||||
import { ComponentMetadata, componentsMap } from './component'
|
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
|
const { displayName } = userComponent.metadata
|
||||||
return `已${enabled ? '开启' : '关闭'}组件'${displayName}', 可能需要刷新后才能生效`
|
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> {
|
export interface DialogInstance extends Required<DialogInputs> {
|
||||||
open: boolean
|
open: boolean
|
||||||
close: Promise<void>
|
close(): Promise<void>
|
||||||
closeListeners: (() => void)[]
|
closeListeners: (() => void)[]
|
||||||
}
|
}
|
||||||
export const showDialog = (inputs: DialogInputs) => {
|
export const showDialog = (inputs: DialogInputs) => {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
export class LoadFeatureCodeError extends Error {}
|
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` 中的键值对
|
// 需要被注入到 `sandbox` 中的键值对
|
||||||
const injection = new Map([
|
const injection = new Map([
|
||||||
// 加固,防止逃逸
|
// 加固,防止逃逸
|
||||||
@ -55,8 +58,9 @@ const getSandbox = lodash.once((): CodeSandbox => {
|
|||||||
return [exported, returned]
|
return [exported, returned]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
let staticCodeSandbox: CodeSandbox | undefined
|
||||||
/**
|
/**
|
||||||
* 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据
|
* 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据
|
||||||
*
|
*
|
||||||
@ -69,14 +73,15 @@ const getSandbox = lodash.once((): CodeSandbox => {
|
|||||||
*
|
*
|
||||||
* 代码默认以非严格模式执行,启用需自行添加 `use strict`。(从本项目中打包的 feature 自带严格模式)
|
* 代码默认以非严格模式执行,启用需自行添加 `use strict`。(从本项目中打包的 feature 自带严格模式)
|
||||||
*
|
*
|
||||||
* 全局对象为脚本管理器提供的 `window`,支持访问 `unsafeWindow`。
|
* 代码执行时的全局对象为脚本管理器提供的 `window`。代码中支持访问 `unsafeWindow`。
|
||||||
*
|
*
|
||||||
* @param code - 被执行的代码
|
* @param code - 被执行的代码
|
||||||
* @returns 导出的元数据(不检测正确性)
|
* @returns 导出的元数据(不检测是否为正确的 feature)
|
||||||
* @throws {@link LoadFeatureCodeError}
|
* @throws {@link LoadFeatureCodeError}
|
||||||
* 代码包含语法错误或代码执行时产生了异常
|
* 代码包含语法错误或代码执行时产生了异常
|
||||||
*/
|
*/
|
||||||
export const loadFeatureCode = (code: string): unknown => {
|
export const loadFeatureCode = (code: string): unknown => {
|
||||||
const [exported, returned] = getSandbox().run(code)
|
staticCodeSandbox || (staticCodeSandbox = createCodeSandbox())
|
||||||
|
const [exported, returned] = staticCodeSandbox.run(code)
|
||||||
return exported || returned
|
return exported || returned
|
||||||
}
|
}
|
||||||
|
|||||||
@ -407,6 +407,7 @@ export class DoubleClickEvent {
|
|||||||
singleClickHandler: (e: MouseEvent) => void = none
|
singleClickHandler: (e: MouseEvent) => void = none
|
||||||
|
|
||||||
private clickedOnce = false
|
private clickedOnce = false
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
private readonly stopPropagationHandler = (e: MouseEvent) => {
|
private readonly stopPropagationHandler = (e: MouseEvent) => {
|
||||||
e.stopImmediatePropagation()
|
e.stopImmediatePropagation()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user