Merge branch 'preview-features' of https://github.com/the1812/Bilibili-Evolved into preview-features

This commit is contained in:
the1812 2023-03-04 10:25:42 +08:00
commit 65439a9fe6
16 changed files with 581 additions and 12 deletions

View File

@ -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',

View File

@ -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>) => {

View 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>

View 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>

View 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>

View 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)
}

View 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

View 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()
},
},
],
})
})
},
},
})

View 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>

View File

@ -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) =>

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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) => {

View File

@ -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
}

View File

@ -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()
}