mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Format src folder
This commit is contained in:
parent
dce37e217f
commit
92314cef6f
@ -12,7 +12,10 @@ export const compatibilityPatch = () => {
|
||||
'https://live.bilibili.com/blackboard/dropdown-menu.html',
|
||||
'https://www.bilibili.com/page-proxy/game-nav.html',
|
||||
]
|
||||
document.documentElement.classList.toggle('iframe', isIframe() && transparentFrames.some(matchUrlPattern))
|
||||
document.documentElement.classList.toggle(
|
||||
'iframe',
|
||||
isIframe() && transparentFrames.some(matchUrlPattern),
|
||||
)
|
||||
})
|
||||
fullyLoaded(() => {
|
||||
select('meta[name=spm_prefix]').then(spm => {
|
||||
|
||||
@ -77,10 +77,9 @@ export const init = async () => {
|
||||
const { getGeneralSettings } = await import('@/core/settings/helpers')
|
||||
const { devMode } = getGeneralSettings()
|
||||
if (devMode) {
|
||||
const {
|
||||
promiseLoadTime,
|
||||
promiseResolveTime,
|
||||
} = await import('@/core/performance/promise-trace')
|
||||
const { promiseLoadTime, promiseResolveTime } = await import(
|
||||
'@/core/performance/promise-trace'
|
||||
)
|
||||
const { logStats } = await import('@/core/performance/stats')
|
||||
logStats('init block', promiseLoadTime)
|
||||
logStats('init resolve', promiseResolveTime)
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="switch-options" :class="{ 'small-size': smallSize, 'grid': !popupMode }">
|
||||
<div class="switch-options" :class="{ 'small-size': smallSize, grid: !popupMode }">
|
||||
<template v-if="popupMode">
|
||||
<VButton
|
||||
ref="button"
|
||||
@click="popupOpen = !popupOpen"
|
||||
>
|
||||
<VButton ref="button" @click="popupOpen = !popupOpen">
|
||||
<VIcon
|
||||
class="switch-icon"
|
||||
icon="mdi-checkbox-marked-circle-outline"
|
||||
@ -51,9 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
VPopup, VButton, VIcon, CheckBox, RadioButton,
|
||||
} from '@/ui'
|
||||
import { VPopup, VButton, VIcon, CheckBox, RadioButton } from '@/ui'
|
||||
import { getComponentSettings } from '../core/settings'
|
||||
|
||||
export default Vue.extend({
|
||||
@ -102,7 +97,10 @@ export default Vue.extend({
|
||||
element.style.setProperty('--columns', columns.toString())
|
||||
},
|
||||
isDim(name: string) {
|
||||
return (this.componentOptions[`switch-${name}`] && this.options.dimAt === 'checked') || this.options.dimAt === 'notChecked'
|
||||
return (
|
||||
(this.componentOptions[`switch-${name}`] && this.options.dimAt === 'checked') ||
|
||||
this.options.dimAt === 'notChecked'
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -121,7 +119,7 @@ export default Vue.extend({
|
||||
transform: scale(0.9);
|
||||
}
|
||||
.dim {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&-grid {
|
||||
font-size: 12px;
|
||||
|
||||
@ -1,22 +1,12 @@
|
||||
<template>
|
||||
<VButton
|
||||
:disabled="disabled"
|
||||
class="check-all-updates"
|
||||
@click="checkUpdates()"
|
||||
>
|
||||
<VIcon
|
||||
:size="16"
|
||||
icon="mdi-cloud-sync-outline"
|
||||
/>
|
||||
<VButton :disabled="disabled" class="check-all-updates" @click="checkUpdates()">
|
||||
<VIcon :size="16" icon="mdi-cloud-sync-outline" />
|
||||
立即检查所有更新
|
||||
</VButton>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Toast } from '@/core/toast'
|
||||
import {
|
||||
VButton,
|
||||
VIcon,
|
||||
} from '@/ui'
|
||||
import { VButton, VIcon } from '@/ui'
|
||||
import { forceCheckUpdateAndReload } from './checker'
|
||||
|
||||
export default Vue.extend({
|
||||
|
||||
@ -38,17 +38,14 @@ export const checkUpdate = async (config: CheckUpdateConfig) => {
|
||||
return filterNames.includes(itemName)
|
||||
}
|
||||
let updatedCount = 0
|
||||
const updateItems = Object.entries(items)
|
||||
.filter(([itemName, item]) => shouldUpdate(itemName) && Boolean(item.url))
|
||||
const updateItems = Object.entries(items).filter(
|
||||
([itemName, item]) => shouldUpdate(itemName) && Boolean(item.url),
|
||||
)
|
||||
const results = await Promise.allSettled(
|
||||
updateItems.map(async ([itemName, item]) => {
|
||||
const { url, lastUpdateCheck, alwaysUpdate } = item
|
||||
const isDebugItem = alwaysUpdate && devMode
|
||||
if (
|
||||
!isDebugItem
|
||||
&& now - lastUpdateCheck <= options.minimumDuration
|
||||
&& !force
|
||||
) {
|
||||
if (!isDebugItem && now - lastUpdateCheck <= options.minimumDuration && !force) {
|
||||
return `[${itemName}] 未超过更新间隔期, 已跳过`
|
||||
}
|
||||
if (updatedCount > maxCount && !force) {
|
||||
@ -69,9 +66,7 @@ export const checkUpdate = async (config: CheckUpdateConfig) => {
|
||||
if (!isFeatureAcceptable(code)) {
|
||||
return `[${itemName}] 版本不匹配, 取消更新`
|
||||
}
|
||||
const { installFeatureFromCode } = await import(
|
||||
'@/core/install-feature'
|
||||
)
|
||||
const { installFeatureFromCode } = await import('@/core/install-feature')
|
||||
const { message } = await installFeatureFromCode(code, url)
|
||||
item.lastUpdateCheck = Number(new Date())
|
||||
updatedCount++
|
||||
@ -114,24 +109,25 @@ export const checkStylesUpdate: CheckSingleTypeUpdate = async config => {
|
||||
})
|
||||
}
|
||||
|
||||
const reload = <T extends any[]> (method: (...args: T) => Promise<any>) => async (...args: T) => {
|
||||
await method(...args)
|
||||
window.location.reload()
|
||||
}
|
||||
const checkByName = (method: CheckSingleTypeUpdate) => reload(
|
||||
async (...itemNames: string[]) => {
|
||||
const reload =
|
||||
<T extends any[]>(method: (...args: T) => Promise<any>) =>
|
||||
async (...args: T) => {
|
||||
await method(...args)
|
||||
window.location.reload()
|
||||
}
|
||||
const checkByName = (method: CheckSingleTypeUpdate) =>
|
||||
reload(async (...itemNames: string[]) => {
|
||||
await method({ filterNames: itemNames, force: true })
|
||||
},
|
||||
) as (...itemNames: string[]) => Promise<void>
|
||||
}) as (...itemNames: string[]) => Promise<void>
|
||||
|
||||
export const checkAllUpdate = async (config: CheckSingleTypeUpdateConfig) => {
|
||||
const { options } = getComponentSettings(name)
|
||||
const console = useScopedConsole('检查所有更新')
|
||||
console.log('开始检查更新')
|
||||
const updateMessages = [
|
||||
await checkComponentsUpdate(config) || '暂无组件更新',
|
||||
await checkPluginsUpdate(config) || '暂无插件更新',
|
||||
await checkStylesUpdate(config) || '暂无样式更新',
|
||||
(await checkComponentsUpdate(config)) || '暂无组件更新',
|
||||
(await checkPluginsUpdate(config)) || '暂无插件更新',
|
||||
(await checkStylesUpdate(config)) || '暂无样式更新',
|
||||
]
|
||||
options.lastUpdateCheck = Number(new Date())
|
||||
options.lastInstalledVersion = meta.version
|
||||
@ -139,14 +135,16 @@ export const checkAllUpdate = async (config: CheckSingleTypeUpdateConfig) => {
|
||||
updateMessages.forEach(message => console.log(message))
|
||||
console.groupEnd()
|
||||
}
|
||||
export const silentCheckUpdate = () => checkAllUpdate({
|
||||
maxCount: getComponentSettings(name).options.maxUpdateCount,
|
||||
})
|
||||
export const silentCheckUpdate = () =>
|
||||
checkAllUpdate({
|
||||
maxCount: getComponentSettings(name).options.maxUpdateCount,
|
||||
})
|
||||
export const silentCheckUpdateAndReload = reload(silentCheckUpdate)
|
||||
|
||||
export const forceCheckUpdate = () => checkAllUpdate({
|
||||
force: true,
|
||||
})
|
||||
export const forceCheckUpdate = () =>
|
||||
checkAllUpdate({
|
||||
force: true,
|
||||
})
|
||||
export const forceCheckUpdateAndReload = reload(forceCheckUpdate)
|
||||
|
||||
export const checkComponentsByName = checkByName(checkComponentsUpdate)
|
||||
@ -156,9 +154,11 @@ export const checkLastFeature = async () => {
|
||||
const { options } = getComponentSettings(name)
|
||||
const items = Object.values(options.urls)
|
||||
.flatMap(it => Object.entries(it))
|
||||
.map(([key, record]: [string, UpdateCheckItem]) => (
|
||||
{ key, time: record.lastUpdateCheck, item: record }
|
||||
))
|
||||
.map(([key, record]: [string, UpdateCheckItem]) => ({
|
||||
key,
|
||||
time: record.lastUpdateCheck,
|
||||
item: record,
|
||||
}))
|
||||
.sort(descendingSort(it => it.time))
|
||||
const [firstItem] = items
|
||||
if (!firstItem) {
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import { ComponentEntry, componentsTags } from '@/components/types'
|
||||
import { meta } from '@/core/meta'
|
||||
import {
|
||||
getComponentSettings,
|
||||
getGeneralSettings,
|
||||
isUserComponent,
|
||||
} from '@/core/settings'
|
||||
import { getComponentSettings, getGeneralSettings, isUserComponent } from '@/core/settings'
|
||||
import { isIframe } from '@/core/utils'
|
||||
import { Version } from '@/core/version'
|
||||
import {
|
||||
@ -64,9 +60,7 @@ const optionsMetadata = defineOptionsMetadata({
|
||||
|
||||
type Options = OptionsOfMetadata<typeof optionsMetadata>
|
||||
|
||||
const entry: ComponentEntry<Options> = async ({
|
||||
settings: { options: opt },
|
||||
}) => {
|
||||
const entry: ComponentEntry<Options> = async ({ settings: { options: opt } }) => {
|
||||
if (isIframe()) {
|
||||
return checkerMethods
|
||||
}
|
||||
@ -112,9 +106,7 @@ export const component = defineComponentMetadata({
|
||||
after: (_, url: string, metadata: { name: string }) => {
|
||||
// console.log('hook', `user${lodash.startCase(type)}.add`, metadata.name, url)
|
||||
const { options } = getComponentSettings('autoUpdate')
|
||||
const existingItem = options.urls[type][
|
||||
metadata.name
|
||||
] as UpdateCheckItem
|
||||
const existingItem = options.urls[type][metadata.name] as UpdateCheckItem
|
||||
if (!existingItem) {
|
||||
options.urls[type][metadata.name] = {
|
||||
url,
|
||||
@ -140,36 +132,33 @@ export const component = defineComponentMetadata({
|
||||
},
|
||||
})
|
||||
})
|
||||
addData(
|
||||
'settingsPanel.componentActions',
|
||||
(actions: ComponentAction[]) => {
|
||||
const { options } = getComponentSettings<AutoUpdateOptions>('autoUpdate')
|
||||
actions.push(metadata => {
|
||||
const item = options.urls.components[metadata.name]
|
||||
if (!item) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
name: 'checkUpdate',
|
||||
displayName: '检查更新',
|
||||
icon: isLocalItem(item.url)
|
||||
? 'mdi-file-download-outline'
|
||||
: 'mdi-cloud-download-outline',
|
||||
visible: isUserComponent(metadata),
|
||||
title: item.url,
|
||||
action: async () => {
|
||||
const { Toast } = await import('@/core/toast')
|
||||
const toast = Toast.info('检查更新中...', '检查更新')
|
||||
toast.message = await checkComponentsUpdate({
|
||||
filterNames: [metadata.name],
|
||||
force: true,
|
||||
})
|
||||
toast.duration = 3000
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
addData('settingsPanel.componentActions', (actions: ComponentAction[]) => {
|
||||
const { options } = getComponentSettings<AutoUpdateOptions>('autoUpdate')
|
||||
actions.push(metadata => {
|
||||
const item = options.urls.components[metadata.name]
|
||||
if (!item) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
name: 'checkUpdate',
|
||||
displayName: '检查更新',
|
||||
icon: isLocalItem(item.url)
|
||||
? 'mdi-file-download-outline'
|
||||
: 'mdi-cloud-download-outline',
|
||||
visible: isUserComponent(metadata),
|
||||
title: item.url,
|
||||
action: async () => {
|
||||
const { Toast } = await import('@/core/toast')
|
||||
const toast = Toast.info('检查更新中...', '检查更新')
|
||||
toast.message = await checkComponentsUpdate({
|
||||
filterNames: [metadata.name],
|
||||
force: true,
|
||||
})
|
||||
toast.duration = 3000
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const icon = 'mdi-cloud-sync-outline'
|
||||
addData('launchBar.actions', (actions: LaunchBarActionProvider[]) => {
|
||||
@ -193,12 +182,14 @@ export const component = defineComponentMetadata({
|
||||
addData('settingsPanel.searchBarActions', (actions: SearchBarAction[]) => {
|
||||
actions.unshift({
|
||||
key: 'updateFeatures',
|
||||
title: ({ selectedComponents }) => (selectedComponents.length > 0 ? '更新所选组件' : '检查所有更新'),
|
||||
title: ({ selectedComponents }) =>
|
||||
selectedComponents.length > 0 ? '更新所选组件' : '检查所有更新',
|
||||
icon: 'mdi-cloud-download-outline',
|
||||
run: async context => {
|
||||
const confirmMessage = context.selectedComponents.length > 0
|
||||
? `确定要更新所选的 ${context.selectedComponents.length} 个组件吗?`
|
||||
: '确定要检查所有更新吗?'
|
||||
const confirmMessage =
|
||||
context.selectedComponents.length > 0
|
||||
? `确定要更新所选的 ${context.selectedComponents.length} 个组件吗?`
|
||||
: '确定要检查所有更新吗?'
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return
|
||||
}
|
||||
@ -237,10 +228,7 @@ export const component = defineComponentMetadata({
|
||||
description: 'Check Last Update',
|
||||
action: async () => {
|
||||
const { Toast } = await import('@/core/toast')
|
||||
const toast = Toast.info(
|
||||
'正在检查更新...',
|
||||
'检查最近更新的功能',
|
||||
)
|
||||
const toast = Toast.info('正在检查更新...', '检查最近更新的功能')
|
||||
await checkLastFeature()
|
||||
toast.close()
|
||||
},
|
||||
|
||||
@ -21,8 +21,7 @@ export interface CheckUpdateConfig extends CheckSingleTypeUpdateConfig {
|
||||
// installer: (code: string) => Promise<{ message: string }>
|
||||
}
|
||||
export const isLocalItem = (url: string) => localhost.test(url)
|
||||
export const defaultExistPredicate = (itemName: string) => (
|
||||
settings.userComponents[itemName] !== undefined
|
||||
|| settings.userPlugins[itemName] !== undefined
|
||||
|| settings.userStyles[itemName] !== undefined
|
||||
)
|
||||
export const defaultExistPredicate = (itemName: string) =>
|
||||
settings.userComponents[itemName] !== undefined ||
|
||||
settings.userPlugins[itemName] !== undefined ||
|
||||
settings.userStyles[itemName] !== undefined
|
||||
|
||||
@ -13,6 +13,5 @@ export const getBuiltInComponents = (): ComponentMetadata[] => [
|
||||
NotifyNewVersion,
|
||||
]
|
||||
|
||||
export const isBuiltInComponent = (name: string) => (
|
||||
export const isBuiltInComponent = (name: string) =>
|
||||
getBuiltInComponents().some(c => c.name === name)
|
||||
)
|
||||
|
||||
@ -27,10 +27,7 @@ const loadI18n = async (component: ComponentMetadata) => {
|
||||
}
|
||||
const { addI18nData } = await import('@/components/i18n/helpers')
|
||||
for (const [language, data] of Object.entries(component.i18n)) {
|
||||
const {
|
||||
map = [],
|
||||
regex = [],
|
||||
} = (typeof data === 'function' ? (await data()) : data)
|
||||
const { map = [], regex = [] } = typeof data === 'function' ? await data() : data
|
||||
addI18nData(language, map, regex)
|
||||
}
|
||||
}
|
||||
@ -49,10 +46,7 @@ const loadWidget = async (component: ComponentMetadata) => {
|
||||
if (widgets.find(w => w.name === widget.name)) {
|
||||
return
|
||||
}
|
||||
const {
|
||||
urlInclude,
|
||||
urlExclude,
|
||||
} = widget
|
||||
const { urlInclude, urlExclude } = widget
|
||||
if (component.urlInclude) {
|
||||
if (urlInclude) {
|
||||
urlInclude.push(...component.urlInclude)
|
||||
@ -100,7 +94,7 @@ export const loadComponent = async (component: ComponentMetadata) => {
|
||||
metadata: component,
|
||||
coreApis,
|
||||
})
|
||||
loadedComponents[component.name] = data as any || {}
|
||||
loadedComponents[component.name] = (data as any) || {}
|
||||
}
|
||||
if (component.reload && component.unload) {
|
||||
addComponentListener(component.name, async (enable: boolean) => {
|
||||
@ -147,10 +141,9 @@ export const loadComponent = async (component: ComponentMetadata) => {
|
||||
/** 加载所有用户组件的定义 (不运行) */
|
||||
export const loadAllUserComponents = async () => {
|
||||
const { settings } = await import('@/core/settings')
|
||||
const {
|
||||
loadFeaturesFromCodes,
|
||||
FeatureKind,
|
||||
} = await import('@/core/external-input/load-features-from-codes')
|
||||
const { loadFeaturesFromCodes, FeatureKind } = await import(
|
||||
'@/core/external-input/load-features-from-codes'
|
||||
)
|
||||
const loadUserComponent = (component: ComponentMetadata) => {
|
||||
components.push(component)
|
||||
componentsMap[component.name] = component
|
||||
@ -166,19 +159,20 @@ export const loadAllUserComponents = async () => {
|
||||
export const loadAllComponents = async () => {
|
||||
const generalSettings = getGeneralSettings()
|
||||
const { loadAllPlugins } = await import('@/plugins/plugin')
|
||||
const loadComponents = () => loadAllPlugins(components)
|
||||
.then(() => Promise.allSettled(components.map(loadI18n)))
|
||||
.then(() => Promise.allSettled(components.map(loadComponent))).then(async () => {
|
||||
if (generalSettings.devMode) {
|
||||
const {
|
||||
componentLoadTime,
|
||||
componentResolveTime,
|
||||
} = await import('@/core/performance/component-trace')
|
||||
const { logStats } = await import('@/core/performance/stats')
|
||||
logStats('components block', componentLoadTime)
|
||||
logStats('components resolve', componentResolveTime)
|
||||
}
|
||||
})
|
||||
const loadComponents = () =>
|
||||
loadAllPlugins(components)
|
||||
.then(() => Promise.allSettled(components.map(loadI18n)))
|
||||
.then(() => Promise.allSettled(components.map(loadComponent)))
|
||||
.then(async () => {
|
||||
if (generalSettings.devMode) {
|
||||
const { componentLoadTime, componentResolveTime } = await import(
|
||||
'@/core/performance/component-trace'
|
||||
)
|
||||
const { logStats } = await import('@/core/performance/stats')
|
||||
logStats('components block', componentLoadTime)
|
||||
logStats('components resolve', componentResolveTime)
|
||||
}
|
||||
})
|
||||
return new Promise(resolve => {
|
||||
if (generalSettings.scriptLoadingMode === LoadingMode.Delay) {
|
||||
// requestIdleCallback(() => loadComponents())
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
import {
|
||||
ComponentMetadata, EmptyOptions,
|
||||
ComponentMetadata,
|
||||
EmptyOptions,
|
||||
OptionMetadata,
|
||||
OptionsMetadata,
|
||||
UnknownOptions,
|
||||
} from './types'
|
||||
|
||||
/** 从 OptionsMetadata 中获取 Options(即 OptionsMetadata 的类型参数) */
|
||||
export type OptionsOfMetadata<M extends OptionsMetadata> = (
|
||||
M extends OptionsMetadata<infer O> ? O : never
|
||||
)
|
||||
export type OptionsOfMetadata<M extends OptionsMetadata> = M extends OptionsMetadata<infer O>
|
||||
? O
|
||||
: never
|
||||
|
||||
/** 定义单个 OptionMetadata */
|
||||
export const defineOptionMetadata = <T>(
|
||||
m: OptionMetadata<T>,
|
||||
): OptionMetadata<T> => m
|
||||
export const defineOptionMetadata = <T>(m: OptionMetadata<T>): OptionMetadata<T> => m
|
||||
|
||||
/** 单独定义 OptionsMetadata */
|
||||
export const defineOptionsMetadata = <
|
||||
O extends UnknownOptions
|
||||
>(m: OptionsMetadata<O>): OptionsMetadata<O> => m
|
||||
export const defineOptionsMetadata = <O extends UnknownOptions>(
|
||||
m: OptionsMetadata<O>,
|
||||
): OptionsMetadata<O> => m
|
||||
|
||||
/** 定义组件 */
|
||||
export const defineComponentMetadata = <
|
||||
O extends UnknownOptions = EmptyOptions
|
||||
>(m: ComponentMetadata<O>): ComponentMetadata<O> => m
|
||||
export const defineComponentMetadata = <O extends UnknownOptions = EmptyOptions>(
|
||||
m: ComponentMetadata<O>,
|
||||
): ComponentMetadata<O> => m
|
||||
|
||||
@ -44,9 +44,8 @@ export const getDescriptionMarkdown = async (item: ItemWithDescription) => {
|
||||
* 同 `getDescriptionMarkdown`, 将最后的 Markdown 转为 HTML string
|
||||
* @param item 功能
|
||||
*/
|
||||
export const getDescriptionHTML = async (item: ItemWithDescription) => marked(
|
||||
await getDescriptionMarkdown(item),
|
||||
)
|
||||
export const getDescriptionHTML = async (item: ItemWithDescription) =>
|
||||
marked(await getDescriptionMarkdown(item))
|
||||
/**
|
||||
* 同 `getDescriptionMarkdown`, 将最后的 Markdown 转为纯文本 (innerText)
|
||||
* @param item 功能
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<a
|
||||
class="bangumi-card"
|
||||
:class="{ new: isNew }"
|
||||
target="_blank"
|
||||
:href="data.url"
|
||||
>
|
||||
<a class="bangumi-card" :class="{ new: isNew }" target="_blank" :href="data.url">
|
||||
<div class="ep-cover-container">
|
||||
<DpiImage class="ep-cover" :size="{width: 100}" :src="data.epCoverUrl"></DpiImage>
|
||||
<DpiImage class="ep-cover" :size="{ width: 100 }" :src="data.epCoverUrl"></DpiImage>
|
||||
</div>
|
||||
<h1 class="ep-title" :title="data.epTitle">{{ data.epTitle }}</h1>
|
||||
<div class="up" :title="data.title">
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<a
|
||||
class="column-card"
|
||||
target="_blank"
|
||||
:href="`https://www.bilibili.com/read/cv${data.cvID}`"
|
||||
>
|
||||
<a class="column-card" target="_blank" :href="`https://www.bilibili.com/read/cv${data.cvID}`">
|
||||
<div class="covers">
|
||||
<DpiImage
|
||||
v-for="cover of data.covers"
|
||||
@ -13,11 +9,7 @@
|
||||
:src="cover"
|
||||
></DpiImage>
|
||||
</div>
|
||||
<a
|
||||
class="up"
|
||||
target="_blank"
|
||||
:href="`https://space.bilibili.com/${data.upID}`"
|
||||
>
|
||||
<a class="up" target="_blank" :href="`https://space.bilibili.com/${data.upID}`">
|
||||
<DpiImage class="face" :size="24" :src="data.upFaceUrl"></DpiImage>
|
||||
<div class="name">{{ data.upName }}</div>
|
||||
</a>
|
||||
@ -47,7 +39,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "common";
|
||||
@import 'common';
|
||||
.column-card {
|
||||
width: 356px;
|
||||
display: flex;
|
||||
|
||||
@ -6,18 +6,10 @@
|
||||
:title="upName"
|
||||
target="_blank"
|
||||
>
|
||||
<DpiImage
|
||||
v-if="upFaceUrl"
|
||||
:size="24"
|
||||
class="be-up-info-cover"
|
||||
:src="upFaceUrl"
|
||||
/>
|
||||
<DpiImage v-if="upFaceUrl" :size="24" class="be-up-info-cover" :src="upFaceUrl" />
|
||||
<div v-else class="be-up-info-cover-fallback">
|
||||
<slot name="fallback-icon">
|
||||
<VIcon
|
||||
icon="up-outline"
|
||||
:size="18"
|
||||
/>
|
||||
<VIcon icon="up-outline" :size="18" />
|
||||
</slot>
|
||||
</div>
|
||||
<div class="be-up-info-name">
|
||||
@ -62,7 +54,7 @@ export default Vue.extend({
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.be-up-info {
|
||||
&:not(.fallback) {
|
||||
@ -88,7 +80,7 @@ export default Vue.extend({
|
||||
}
|
||||
&-name {
|
||||
@include single-line();
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
&:hover .be-up-info-name {
|
||||
color: var(--theme-color) !important;
|
||||
|
||||
@ -10,11 +10,7 @@
|
||||
:class="{ vertical: orientation === 'vertical', 'no-stats': !showStats }"
|
||||
>
|
||||
<div class="cover-container">
|
||||
<DpiImage
|
||||
class="cover"
|
||||
:src="coverUrl"
|
||||
:size="{ height: 120, width: 196 }"
|
||||
></DpiImage>
|
||||
<DpiImage class="cover" :src="coverUrl" :size="{ height: 120, width: 196 }"></DpiImage>
|
||||
<div v-if="isNew" class="new">NEW</div>
|
||||
<template v-if="pubTime && pubTimeText">
|
||||
<div class="publish-time-summary">
|
||||
@ -30,10 +26,7 @@
|
||||
class="watchlater"
|
||||
@click.stop.prevent="toggleWatchlater(aid)"
|
||||
>
|
||||
<VIcon
|
||||
:size="15"
|
||||
:icon="watchlater ? 'mdi-check-circle' : 'mdi-clock-outline'"
|
||||
></VIcon>
|
||||
<VIcon :size="15" :icon="watchlater ? 'mdi-check-circle' : 'mdi-clock-outline'"></VIcon>
|
||||
{{ watchlater ? '已添加' : '稍后再看' }}
|
||||
</div>
|
||||
</div>
|
||||
@ -76,12 +69,7 @@
|
||||
:title="up.name"
|
||||
:href="up.id ? 'https://space.bilibili.com/' + up.id : null"
|
||||
>
|
||||
<DpiImage
|
||||
v-if="up.faceUrl"
|
||||
class="face"
|
||||
:src="up.faceUrl"
|
||||
:size="24"
|
||||
/>
|
||||
<DpiImage v-if="up.faceUrl" class="face" :src="up.faceUrl" :size="24" />
|
||||
<VIcon v-else icon="up" />
|
||||
</a>
|
||||
</div>
|
||||
@ -137,10 +125,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
DpiImage,
|
||||
VIcon,
|
||||
} from '@/ui'
|
||||
import { DpiImage, VIcon } from '@/ui'
|
||||
import { getUID } from '@/core/utils'
|
||||
import { watchlaterList, toggleWatchlater } from '@/components/video/watchlater'
|
||||
|
||||
@ -340,7 +325,7 @@ export default {
|
||||
overflow: hidden;
|
||||
.cover {
|
||||
transition: 0.1s cubic-bezier(0.39, 0.58, 0.57, 1);
|
||||
-webkit-transform:rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@ -16,25 +16,25 @@ export * from './manager'
|
||||
*/
|
||||
export const groupVideoFeeds = (cards: VideoCard[]) => {
|
||||
const groups = lodash.groupBy(cards, c => c.aid)
|
||||
const cardToCooperationItem = (card: VideoCard) => (
|
||||
{
|
||||
id: card.upID,
|
||||
name: card.upName,
|
||||
faceUrl: card.upFaceUrl,
|
||||
}
|
||||
)
|
||||
const results = Object.values(groups).map(groupCards => {
|
||||
if (groupCards.length === 1) {
|
||||
return groupCards[0]
|
||||
}
|
||||
const [firstCard, ...restCards] = groupCards
|
||||
firstCard.cooperation = [
|
||||
cardToCooperationItem(firstCard),
|
||||
...restCards.map(cardToCooperationItem),
|
||||
]
|
||||
console.log([...firstCard.cooperation])
|
||||
return firstCard
|
||||
}).sort(descendingStringSort(it => it.id))
|
||||
const cardToCooperationItem = (card: VideoCard) => ({
|
||||
id: card.upID,
|
||||
name: card.upName,
|
||||
faceUrl: card.upFaceUrl,
|
||||
})
|
||||
const results = Object.values(groups)
|
||||
.map(groupCards => {
|
||||
if (groupCards.length === 1) {
|
||||
return groupCards[0]
|
||||
}
|
||||
const [firstCard, ...restCards] = groupCards
|
||||
firstCard.cooperation = [
|
||||
cardToCooperationItem(firstCard),
|
||||
...restCards.map(cardToCooperationItem),
|
||||
]
|
||||
console.log([...firstCard.cooperation])
|
||||
return firstCard
|
||||
})
|
||||
.sort(descendingStringSort(it => it.id))
|
||||
return results
|
||||
}
|
||||
/** 判断动态卡片是否包含预约功能.
|
||||
@ -44,20 +44,21 @@ export const groupVideoFeeds = (cards: VideoCard[]) => {
|
||||
export const isPreOrderedVideo = (card: any) => lodash.get(card, 'extra.is_reserve_recall', 0) === 1
|
||||
|
||||
export interface FeedsContentFilter {
|
||||
filter: <T> (items: T[]) => T[]
|
||||
filter: <T>(items: T[]) => T[]
|
||||
}
|
||||
const contentFiltersKey = 'feeds.contentFilters'
|
||||
registerData(contentFiltersKey, [] as FeedsContentFilter[])
|
||||
/** 对动态内容进行过滤 */
|
||||
export const applyContentFilter = <T> (items: T[]) => {
|
||||
export const applyContentFilter = <T>(items: T[]) => {
|
||||
const [contentFilters] = getData(contentFiltersKey) as [FeedsContentFilter[]]
|
||||
const result = contentFilters.reduce((acc, it) => (acc = it.filter(acc)), items)
|
||||
return result
|
||||
}
|
||||
/** 对异步获取动态内容的函数进行包装, 将返回值套用 `applyContentFilter` */
|
||||
export const withContentFilter = <Args extends any[], Item> (
|
||||
func: (...args: Args) => Promise<Item[]>,
|
||||
) => (...args: Args) => func(...args).then(items => applyContentFilter(items))
|
||||
export const withContentFilter =
|
||||
<Args extends any[], Item>(func: (...args: Args) => Promise<Item[]>) =>
|
||||
(...args: Args) =>
|
||||
func(...args).then(items => applyContentFilter(items))
|
||||
|
||||
/**
|
||||
* 获取动态 API 地址
|
||||
@ -80,9 +81,8 @@ export const getFeedsUrl = (type: FeedsCardType | string, afterID?: string | num
|
||||
* @param type 动态类型, 或传入类型ID列表返回最新动态
|
||||
* @param afterID 返回指定ID之前的动态历史, 省略则返回最新的动态
|
||||
*/
|
||||
export const getFeeds = async (type: FeedsCardType | string, afterID?: string | number) => (
|
||||
export const getFeeds = async (type: FeedsCardType | string, afterID?: string | number) =>
|
||||
getJsonWithCredentials(getFeedsUrl(type, afterID))
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取视频或番剧动态
|
||||
@ -94,22 +94,22 @@ export const getVideoFeeds = withContentFilter(
|
||||
if (!getUID()) {
|
||||
return []
|
||||
}
|
||||
const json = await getJsonWithCredentials(getFeedsUrl(type === 'video' ? feedsCardTypes.video : feedsCardTypes.bangumi, afterID))
|
||||
const json = await getJsonWithCredentials(
|
||||
getFeedsUrl(type === 'video' ? feedsCardTypes.video : feedsCardTypes.bangumi, afterID),
|
||||
)
|
||||
if (json.code !== 0) {
|
||||
throw new Error(json.message)
|
||||
}
|
||||
const dataCards = json.data.cards as any[]
|
||||
const dataCardsWithoutPreOrder = dataCards.filter(it => !isPreOrderedVideo(JSON.parse(it.card)))
|
||||
if (type === 'video') {
|
||||
return groupVideoFeeds(dataCards.map(
|
||||
(c: any): VideoCard => {
|
||||
return groupVideoFeeds(
|
||||
dataCards.map((c: any): VideoCard => {
|
||||
const card = JSON.parse(c.card)
|
||||
const topics = lodash.get(c, 'display.topic_info.topic_details', []).map(
|
||||
(it: any) => ({
|
||||
id: it.topic_id,
|
||||
name: it.topic_name,
|
||||
}),
|
||||
)
|
||||
const topics = lodash.get(c, 'display.topic_info.topic_details', []).map((it: any) => ({
|
||||
id: it.topic_id,
|
||||
name: it.topic_name,
|
||||
}))
|
||||
return {
|
||||
id: c.desc.dynamic_id_str,
|
||||
aid: card.aid,
|
||||
@ -131,33 +131,32 @@ export const getVideoFeeds = withContentFilter(
|
||||
danmakuCount: formatCount(card.stat.danmaku),
|
||||
watchlater: watchlaterList.includes(card.aid),
|
||||
}
|
||||
},
|
||||
))
|
||||
} if (type === 'bangumi') {
|
||||
return dataCardsWithoutPreOrder.map(
|
||||
(c: any): VideoCard => {
|
||||
const card = JSON.parse(c.card)
|
||||
return {
|
||||
id: c.desc.dynamic_id_str,
|
||||
aid: card.aid,
|
||||
bvid: c.desc.bvid || card.bvid,
|
||||
epID: card.episode_id,
|
||||
title: card.new_desc,
|
||||
upName: card.apiSeasonInfo.title,
|
||||
upFaceUrl: card.apiSeasonInfo.cover,
|
||||
coverUrl: card.cover,
|
||||
description: '',
|
||||
timestamp: c.timestamp,
|
||||
time: new Date(c.timestamp * 1000),
|
||||
like: formatCount(c.desc.like),
|
||||
durationText: '',
|
||||
playCount: formatCount(card.play_count),
|
||||
danmakuCount: formatCount(card.bullet_count),
|
||||
watchlater: false,
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (type === 'bangumi') {
|
||||
return dataCardsWithoutPreOrder.map((c: any): VideoCard => {
|
||||
const card = JSON.parse(c.card)
|
||||
return {
|
||||
id: c.desc.dynamic_id_str,
|
||||
aid: card.aid,
|
||||
bvid: c.desc.bvid || card.bvid,
|
||||
epID: card.episode_id,
|
||||
title: card.new_desc,
|
||||
upName: card.apiSeasonInfo.title,
|
||||
upFaceUrl: card.apiSeasonInfo.cover,
|
||||
coverUrl: card.cover,
|
||||
description: '',
|
||||
timestamp: c.timestamp,
|
||||
time: new Date(c.timestamp * 1000),
|
||||
like: formatCount(c.desc.like),
|
||||
durationText: '',
|
||||
playCount: formatCount(card.play_count),
|
||||
danmakuCount: formatCount(card.bullet_count),
|
||||
watchlater: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
return []
|
||||
},
|
||||
)
|
||||
@ -167,11 +166,14 @@ export const getVideoFeeds = withContentFilter(
|
||||
* @param card 动态卡片
|
||||
* @param config 菜单项配置
|
||||
*/
|
||||
export const addMenuItem = (card: FeedsCard, config: {
|
||||
className: string
|
||||
text: string
|
||||
action: (e: MouseEvent) => void
|
||||
}) => {
|
||||
export const addMenuItem = (
|
||||
card: FeedsCard,
|
||||
config: {
|
||||
className: string
|
||||
text: string
|
||||
action: (e: MouseEvent) => void
|
||||
},
|
||||
) => {
|
||||
const morePanel = dq(card.element, '.more-panel, .bili-dyn-more__menu') as HTMLElement
|
||||
const isV2 = morePanel.classList.contains('bili-dyn-more__menu')
|
||||
const { className, text, action } = config
|
||||
@ -191,7 +193,15 @@ export const addMenuItem = (card: FeedsCard, config: {
|
||||
menuItem.classList.add('child-button', 'c-pointer', className)
|
||||
}
|
||||
menuItem.textContent = text
|
||||
const vueScopeAttributes = [...new Set([...morePanel.children].map((element: HTMLElement) => element.getAttributeNames().filter(it => it.startsWith('data-v-'))).flat())]
|
||||
const vueScopeAttributes = [
|
||||
...new Set(
|
||||
[...morePanel.children]
|
||||
.map((element: HTMLElement) =>
|
||||
element.getAttributeNames().filter(it => it.startsWith('data-v-')),
|
||||
)
|
||||
.flat(),
|
||||
),
|
||||
]
|
||||
vueScopeAttributes.forEach(attr => menuItem.setAttribute(attr, ''))
|
||||
menuItem.addEventListener('click', e => {
|
||||
action(e)
|
||||
|
||||
@ -23,11 +23,9 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
adaptors.push(
|
||||
{
|
||||
name: 'live',
|
||||
match: [
|
||||
...liveUrls,
|
||||
],
|
||||
match: [...liveUrls],
|
||||
watchCardsList: async manager => {
|
||||
const feedsContainer = await select('.room-feed') as HTMLElement
|
||||
const feedsContainer = (await select('.room-feed')) as HTMLElement
|
||||
if (!feedsContainer) {
|
||||
return false
|
||||
}
|
||||
@ -35,9 +33,9 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
let cardListObserver: MutationObserver | null = null
|
||||
childList(feedsContainer, async () => {
|
||||
if (dq('.room-feed-content')) {
|
||||
const cardsList = await select('.room-feed-content .content') as HTMLElement
|
||||
cardListObserver?.disconnect();
|
||||
[cardListObserver] = manager.updateCards(cardsList)
|
||||
const cardsList = (await select('.room-feed-content .content')) as HTMLElement
|
||||
cardListObserver?.disconnect()
|
||||
;[cardListObserver] = manager.updateCards(cardsList)
|
||||
} else {
|
||||
cardListObserver?.disconnect()
|
||||
cardListObserver = null
|
||||
@ -49,11 +47,9 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
match: [
|
||||
'https://space.bilibili.com/',
|
||||
],
|
||||
match: ['https://space.bilibili.com/'],
|
||||
watchCardsList: async manager => {
|
||||
const container = await select('.s-space') as HTMLDivElement
|
||||
const container = (await select('.s-space')) as HTMLDivElement
|
||||
if (!container) {
|
||||
return false
|
||||
}
|
||||
@ -75,11 +71,13 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
if (vm.observer) {
|
||||
return vm.observer
|
||||
}
|
||||
const newListPromise = select('.feed-card .content, .bili-dyn-list__items') as Promise<HTMLElement>
|
||||
const newListPromise = select(
|
||||
'.feed-card .content, .bili-dyn-list__items',
|
||||
) as Promise<HTMLElement>
|
||||
vm.observer = (async () => {
|
||||
// const newList = await vm.listElement as HTMLElement
|
||||
const newList = await newListPromise
|
||||
if (newList !== await vm.listElement) {
|
||||
if (newList !== (await vm.listElement)) {
|
||||
if (vm.listElement) {
|
||||
await stop()
|
||||
}
|
||||
@ -104,20 +102,18 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
},
|
||||
{
|
||||
name: 'topic',
|
||||
match: [
|
||||
'https://t.bilibili.com/topic',
|
||||
],
|
||||
match: ['https://t.bilibili.com/topic'],
|
||||
watchCardsList: async manager => {
|
||||
const feedsContainer = await select('.page-container') as HTMLElement
|
||||
const feedsContainer = (await select('.page-container')) as HTMLElement
|
||||
if (!feedsContainer) {
|
||||
return false
|
||||
}
|
||||
let cardListObserver: MutationObserver | null = null
|
||||
childList(feedsContainer, async () => {
|
||||
if (dq('.page-container .feed')) {
|
||||
const cardsList = await select('.feed .feed-topic') as HTMLElement
|
||||
cardListObserver?.disconnect();
|
||||
[cardListObserver] = manager.updateCards(cardsList)
|
||||
const cardsList = (await select('.feed .feed-topic')) as HTMLElement
|
||||
cardListObserver?.disconnect()
|
||||
;[cardListObserver] = manager.updateCards(cardsList)
|
||||
} else {
|
||||
cardListObserver?.disconnect()
|
||||
cardListObserver = null
|
||||
@ -129,11 +125,11 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
match: [
|
||||
'https://t.bilibili.com/',
|
||||
],
|
||||
match: ['https://t.bilibili.com/'],
|
||||
watchCardsList: async manager => {
|
||||
const list = await select('.feed-card .content, .detail-content .detail-card, #app > .content > .card, .bili-dyn-list__items') as HTMLElement
|
||||
const list = (await select(
|
||||
'.feed-card .content, .detail-content .detail-card, #app > .content > .card, .bili-dyn-list__items',
|
||||
)) as HTMLElement
|
||||
if (!list) {
|
||||
return false
|
||||
}
|
||||
@ -146,8 +142,8 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
|
||||
return
|
||||
}
|
||||
cardListObserver?.disconnect()
|
||||
manager.cards = [];
|
||||
[cardListObserver] = manager.updateCards(changedList)
|
||||
manager.cards = []
|
||||
;[cardListObserver] = manager.updateCards(changedList)
|
||||
})
|
||||
} else {
|
||||
manager.updateCards(list)
|
||||
|
||||
@ -5,17 +5,18 @@ import { ListAdaptorKey, FeedsCardsListAdaptor } from './adaptor'
|
||||
|
||||
export const feedsCardCallbacks: Required<FeedsCardCallback>[] = []
|
||||
|
||||
export const getVueData = (el: any) => (
|
||||
export const getVueData = (el: any) =>
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
el.__vue__ ?? el.parentElement.__vue__ ?? el.children[0].__vue__
|
||||
)
|
||||
|
||||
export const createNodeValidator = (className: string) => (node: Node): node is HTMLElement => {
|
||||
const notNull = Boolean(node)
|
||||
const notDetached = node && node.parentNode
|
||||
const matchClassName = (node instanceof HTMLElement) && node.matches(className)
|
||||
return notNull && notDetached && matchClassName
|
||||
}
|
||||
export const createNodeValidator =
|
||||
(className: string) =>
|
||||
(node: Node): node is HTMLElement => {
|
||||
const notNull = Boolean(node)
|
||||
const notDetached = node && node.parentNode
|
||||
const matchClassName = node instanceof HTMLElement && node.matches(className)
|
||||
return notNull && notDetached && matchClassName
|
||||
}
|
||||
|
||||
/** 动态卡片管理器支持的自定义事件 */
|
||||
export enum FeedsCardsManagerEventType {
|
||||
@ -51,7 +52,9 @@ export abstract class FeedsCardsManager extends EventTarget {
|
||||
dispatchCardEvent(type: FeedsCardsManagerEventType, card: FeedsCard) {
|
||||
const event = new CustomEvent(type, { detail: card })
|
||||
this.dispatchEvent(event)
|
||||
feedsCardCallbacks.forEach(c => c[type === FeedsCardsManagerEventType.AddCard ? 'added' : 'removed'](card))
|
||||
feedsCardCallbacks.forEach(c =>
|
||||
c[type === FeedsCardsManagerEventType.AddCard ? 'added' : 'removed'](card),
|
||||
)
|
||||
}
|
||||
/** 对当前页面开始监测 */
|
||||
async startWatching() {
|
||||
|
||||
@ -10,10 +10,7 @@ export const isV2Feeds = () => {
|
||||
if (!hasCookieValue) {
|
||||
return false
|
||||
}
|
||||
return [
|
||||
't.bilibili.com',
|
||||
'space.bilibili.com',
|
||||
].some(host => location.host === host)
|
||||
return ['t.bilibili.com', 'space.bilibili.com'].some(host => location.host === host)
|
||||
}
|
||||
export const feedsCardsManager = (() => {
|
||||
const isV2 = isV2Feeds()
|
||||
|
||||
@ -40,11 +40,11 @@ const getFeedsCardType = (element: HTMLElement) => {
|
||||
*/
|
||||
const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
const getSimpleText = async (selector: string) => {
|
||||
const subElement = await sq(
|
||||
const subElement = (await sq(
|
||||
() => element.querySelector(selector),
|
||||
it => it !== null || element.parentNode === null,
|
||||
{ queryInterval: 100 },
|
||||
) as HTMLElement
|
||||
)) as HTMLElement
|
||||
if (element.parentNode === null) {
|
||||
// console.log('skip detached node:', element)
|
||||
return ''
|
||||
@ -67,7 +67,11 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
}
|
||||
const originalCard = JSON.parse(vueData.card.origin)
|
||||
const originalText: string = vueData.originCardData.pureText
|
||||
const originalDescription: string = lodash.get(originalCard, 'item.description', lodash.get(originalCard, 'desc', ''))
|
||||
const originalDescription: string = lodash.get(
|
||||
originalCard,
|
||||
'item.description',
|
||||
lodash.get(originalCard, 'desc', ''),
|
||||
)
|
||||
const originalTitle: string = originalCard.title
|
||||
return {
|
||||
originalText,
|
||||
@ -96,17 +100,13 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
if (type === feedsCardTypes.repost) {
|
||||
const currentText = vueData.card.item.content
|
||||
const repostData = getRepostData(vueData)
|
||||
return [
|
||||
currentText,
|
||||
...Object.values(repostData).filter(it => it !== ''),
|
||||
].filter(it => Boolean(it)).join('\n')
|
||||
return [currentText, ...Object.values(repostData).filter(it => it !== '')]
|
||||
.filter(it => Boolean(it))
|
||||
.join('\n')
|
||||
}
|
||||
const currentText = vueData.originCardData.pureText
|
||||
const currentTitle = vueData.originCardData.title
|
||||
return [
|
||||
currentText,
|
||||
currentTitle,
|
||||
].filter(it => Boolean(it)).join('\n')
|
||||
return [currentText, currentTitle].filter(it => Boolean(it)).join('\n')
|
||||
}
|
||||
const getNumber = async (selector: string) => {
|
||||
const result = parseInt(await getSimpleText(selector))
|
||||
@ -124,7 +124,9 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
likes: await getNumber('.button-bar .single-button:nth-child(3) .text-offset'),
|
||||
element,
|
||||
type: getFeedsCardType(element),
|
||||
get presented() { return element.parentNode !== null },
|
||||
get presented() {
|
||||
return element.parentNode !== null
|
||||
},
|
||||
async getText() {
|
||||
return getComplexText(this.type)
|
||||
},
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { childList } from '@/core/observer'
|
||||
import { descendingStringSort } from '@/core/utils/sort'
|
||||
import { pascalCase } from '@/core/utils'
|
||||
import { createNodeValidator, FeedsCardsManager, FeedsCardsManagerEventType, getVueData } from './base'
|
||||
import {
|
||||
createNodeValidator,
|
||||
FeedsCardsManager,
|
||||
FeedsCardsManagerEventType,
|
||||
getVueData,
|
||||
} from './base'
|
||||
import { FeedsCard, FeedsCardType, feedsCardTypes, isRepostType } from '../types'
|
||||
|
||||
/** b 站的动态卡片 type 标记 -> FeedsCard.type */
|
||||
@ -16,10 +21,13 @@ const feedsCardTypeMap = {
|
||||
DynamicTypeLiveRcmd: feedsCardTypes.liveRecord,
|
||||
}
|
||||
|
||||
const combineText = (...texts: string[]) => texts.filter(it => Boolean(it)).join('\n').trim()
|
||||
const getType = (rawType: string) => (
|
||||
const combineText = (...texts: string[]) =>
|
||||
texts
|
||||
.filter(it => Boolean(it))
|
||||
.join('\n')
|
||||
.trim()
|
||||
const getType = (rawType: string) =>
|
||||
feedsCardTypeMap[pascalCase(rawType)] ?? feedsCardTypeMap.DynamicTypeWord
|
||||
)
|
||||
const getText = (dynamicModule: any, cardType: FeedsCardType) => {
|
||||
const { desc: mainDesc, major } = dynamicModule
|
||||
const mainText = mainDesc?.text ?? ''
|
||||
@ -55,7 +63,9 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
text: '',
|
||||
type: cardType,
|
||||
element,
|
||||
get presented() { return document.body.contains(element) },
|
||||
get presented() {
|
||||
return document.body.contains(element)
|
||||
},
|
||||
async getText() {
|
||||
return getText(modules.module_dynamic, cardType)
|
||||
},
|
||||
@ -63,9 +73,7 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
if (isRepostType(card)) {
|
||||
const currentUsername = card.username
|
||||
const {
|
||||
module_author: {
|
||||
name: repostUsername,
|
||||
},
|
||||
module_author: { name: repostUsername },
|
||||
module_dynamic: repostDynamicModule,
|
||||
} = vueData.data.orig.modules
|
||||
card.repostUsername = repostUsername
|
||||
@ -73,10 +81,8 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
if (repostUsername === currentUsername) {
|
||||
element.setAttribute('data-self-repost', 'true')
|
||||
}
|
||||
card.getText = async () => combineText(
|
||||
getText(modules.module_dynamic, cardType),
|
||||
getText(repostDynamicModule, cardType),
|
||||
)
|
||||
card.getText = async () =>
|
||||
combineText(getText(modules.module_dynamic, cardType), getText(repostDynamicModule, cardType))
|
||||
}
|
||||
card.text = await card.getText()
|
||||
return card
|
||||
|
||||
@ -79,9 +79,8 @@ export const feedsCardTypes = {
|
||||
} as FeedsCardType,
|
||||
}
|
||||
/** 是否是转发类型的卡片, 额外能够读取被转发动态的信息 */
|
||||
export const isRepostType = (card: FeedsCard): card is RepostFeedsCard => (
|
||||
export const isRepostType = (card: FeedsCard): card is RepostFeedsCard =>
|
||||
card.type === feedsCardTypes.repost
|
||||
)
|
||||
/** 番剧类型列表 (用于API请求) */
|
||||
export const bangumiTypeList = '512,4097,4098,4099,4100,4101'
|
||||
/** 顶栏动态类型列表 (用于API请求) */
|
||||
|
||||
@ -9,7 +9,7 @@ export const disableProfilePopup = async () => {
|
||||
if (document.URL.replace(window.location.search, '') !== 'https://t.bilibili.com/') {
|
||||
return
|
||||
}
|
||||
const list = await select('.live-up-list, .bili-dyn-live-users__body') as HTMLElement
|
||||
const list = (await select('.live-up-list, .bili-dyn-live-users__body')) as HTMLElement
|
||||
if (list === null) {
|
||||
return
|
||||
}
|
||||
@ -17,11 +17,15 @@ export const disableProfilePopup = async () => {
|
||||
if (eventAttached) {
|
||||
return
|
||||
}
|
||||
list.addEventListener('mouseenter', e => {
|
||||
if (counter > 0) {
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
}, { capture: true })
|
||||
list.addEventListener(
|
||||
'mouseenter',
|
||||
e => {
|
||||
if (counter > 0) {
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
},
|
||||
{ capture: true },
|
||||
)
|
||||
eventAttached = true
|
||||
}
|
||||
/** 取消一次 {@link disableProfilePopup} 的效果, 可以用来配合其他地方的生命周期 */
|
||||
|
||||
@ -24,7 +24,9 @@ export const setLatestID = (id: string) => {
|
||||
if (compareID(id, currentID) < 0) {
|
||||
return
|
||||
}
|
||||
document.cookie = `bp_t_offset_${getUID()}=${id};path=/;domain=.bilibili.com;max-age=${60 * 60 * 24 * 30}`
|
||||
document.cookie = `bp_t_offset_${getUID()}=${id};path=/;domain=.bilibili.com;max-age=${
|
||||
60 * 60 * 24 * 30
|
||||
}`
|
||||
}
|
||||
export const isNewID = (id: string) => compareID(id, getLatestID()) > 0
|
||||
export const updateLatestID = (cards: { id: string }[]) => {
|
||||
@ -32,7 +34,9 @@ export const updateLatestID = (cards: { id: string }[]) => {
|
||||
setLatestID(id)
|
||||
}
|
||||
export const getNotifyCount = async (typeList?: string): Promise<number> => {
|
||||
const api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_num?rsp_type=1&uid=${getUID()}&update_num_dy_id=${getLatestID()}&type_list=${typeList || navbarFeedsTypeList}`
|
||||
const api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_num?rsp_type=1&uid=${getUID()}&update_num_dy_id=${getLatestID()}&type_list=${
|
||||
typeList || navbarFeedsTypeList
|
||||
}`
|
||||
const json = await getJsonWithCredentials(api)
|
||||
if (json.code !== 0) {
|
||||
return 0
|
||||
|
||||
@ -14,7 +14,9 @@ export class Translator {
|
||||
|
||||
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
||||
protected getValue = (node: Node) => node.nodeValue
|
||||
protected setValue = (node: Node, value: string) => { node.nodeValue = value }
|
||||
protected setValue = (node: Node, value: string) => {
|
||||
node.nodeValue = value
|
||||
}
|
||||
protected getElement = (node: Node) => node as Element
|
||||
translate(node: Node) {
|
||||
let value = this.getValue(node)
|
||||
@ -89,7 +91,7 @@ export class Translator {
|
||||
for (const { selector, text } of selectors) {
|
||||
const element = document.querySelector(selector)
|
||||
if (element) {
|
||||
[...element.childNodes]
|
||||
;[...element.childNodes]
|
||||
.filter(it => it.nodeType === Node.TEXT_NODE)
|
||||
.forEach(it => (it.nodeValue = text))
|
||||
}
|
||||
@ -103,24 +105,20 @@ export class TextNodeTranslator extends Translator {
|
||||
export class TitleTranslator extends Translator {
|
||||
getValue = (node: Node) => (node as Element).getAttribute('title')
|
||||
setValue = (node: Node, value: string) => {
|
||||
(node as Element).setAttribute('title', value)
|
||||
;(node as Element).setAttribute('title', value)
|
||||
}
|
||||
}
|
||||
export class PlaceholderTranslator extends Translator {
|
||||
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
||||
setValue = (node: Node, value: string) => {
|
||||
(node as Element).setAttribute('placeholder', value)
|
||||
;(node as Element).setAttribute('placeholder', value)
|
||||
}
|
||||
}
|
||||
|
||||
Translator.textNode = new TextNodeTranslator()
|
||||
Translator.title = new TitleTranslator()
|
||||
Translator.placeholder = new PlaceholderTranslator()
|
||||
Translator.sensitiveTranslators = [
|
||||
Translator.textNode,
|
||||
Translator.title,
|
||||
Translator.placeholder,
|
||||
]
|
||||
Translator.sensitiveTranslators = [Translator.textNode, Translator.title, Translator.placeholder]
|
||||
|
||||
export const startTranslate: ComponentEntry = async () => {
|
||||
const { getSelectedLanguage } = await import('./helpers')
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { defaultLanguageCode, languageCodeToName } from '@/core/utils/i18n'
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { componentsTags } from '../types'
|
||||
import {
|
||||
translateProviderNames,
|
||||
translateProviders,
|
||||
} from './machine-translator/translators'
|
||||
import { translateProviderNames, translateProviders } from './machine-translator/translators'
|
||||
import { startTranslate } from './dom-translator'
|
||||
|
||||
export const component = defineComponentMetadata({
|
||||
@ -12,11 +9,7 @@ export const component = defineComponentMetadata({
|
||||
displayName: '多语言',
|
||||
configurable: false,
|
||||
entry: startTranslate,
|
||||
tags: [
|
||||
componentsTags.utils,
|
||||
componentsTags.experimental,
|
||||
componentsTags.general,
|
||||
],
|
||||
tags: [componentsTags.utils, componentsTags.experimental, componentsTags.general],
|
||||
description: {
|
||||
'zh-CN':
|
||||
'安装其他语言包可以更换界面语言, 机器翻译选择可以设定其他一些功能如`动态翻译`, `评论翻译`使用的翻译器. 机器翻译的选择不影响界面语言.',
|
||||
|
||||
@ -4,9 +4,7 @@
|
||||
<VIcon :size="14" icon="mdi-earth" />翻译
|
||||
</div>
|
||||
<div v-if="translated" class="translated">
|
||||
<a :href="activeTranslator && activeTranslator.link" target="_blank">
|
||||
翻译自
|
||||
</a>
|
||||
<a :href="activeTranslator && activeTranslator.link" target="_blank"> 翻译自 </a>
|
||||
<VDropdown
|
||||
:items="Object.values(translateProviders)"
|
||||
:value="activeTranslator"
|
||||
@ -85,7 +83,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.bb-comment .translate-container,
|
||||
.card-content .translate-container {
|
||||
|
||||
@ -109,7 +109,9 @@ export const [translateProviders] = registerAndGetData('i18n.machineTranslators'
|
||||
} as Record<string, MachineTranslateProvider>)
|
||||
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
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ export default Vue.extend({
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.suggest-item {
|
||||
outline: none !important;
|
||||
|
||||
@ -49,7 +49,7 @@ export default Vue.extend({
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.global-launch-bar-container {
|
||||
$barHeight: 50px;
|
||||
@ -65,7 +65,7 @@ export default Vue.extend({
|
||||
border: 1px solid #8882;
|
||||
font-size: 16px;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity .2s ease-out;
|
||||
transition: opacity 0.2s ease-out;
|
||||
|
||||
.launch-bar {
|
||||
flex: 1;
|
||||
@ -73,7 +73,7 @@ export default Vue.extend({
|
||||
--color: #eee;
|
||||
}
|
||||
.launch-bar-suggest-list {
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
top: calc(100% + 8px);
|
||||
max-height: calc(80vh - 16px - #{$barHeight});
|
||||
@include no-scrollbar();
|
||||
|
||||
@ -31,7 +31,10 @@
|
||||
@previous-item="previousItem($event, index)"
|
||||
@next-item="nextItem($event, index)"
|
||||
@delete-item="onDeleteItem($event, index)"
|
||||
@action="(index === actions.length - 1) && onClearHistory(); onAction(a)"
|
||||
@action="
|
||||
index === actions.length - 1 && onClearHistory()
|
||||
onAction(a)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isHistory" class="launch-bar-action-list">
|
||||
@ -60,11 +63,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js'
|
||||
import {
|
||||
VIcon,
|
||||
VLoading,
|
||||
VEmpty,
|
||||
} from '@/ui'
|
||||
import { VIcon, VLoading, VEmpty } from '@/ui'
|
||||
import { registerAndGetData } from '@/plugins/data'
|
||||
import { select } from '@/core/spin-query'
|
||||
import { matchUrlPattern } from '@/core/utils'
|
||||
@ -75,18 +74,19 @@ import {
|
||||
LaunchBarAction,
|
||||
} from './launch-bar-action'
|
||||
import { searchProvider, search } from './search-provider'
|
||||
import {
|
||||
historyProvider,
|
||||
} from './history-provider'
|
||||
import { historyProvider } from './history-provider'
|
||||
|
||||
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
|
||||
searchProvider,
|
||||
historyProvider,
|
||||
]) as [LaunchBarActionProvider[]]
|
||||
const generateKeys = (provider: LaunchBarActionProvider, actions: LaunchBarAction[]): ({
|
||||
const generateKeys = (
|
||||
provider: LaunchBarActionProvider,
|
||||
actions: LaunchBarAction[],
|
||||
): ({
|
||||
key: string
|
||||
provider: LaunchBarActionProvider
|
||||
} & LaunchBarAction)[] => (
|
||||
} & LaunchBarAction)[] =>
|
||||
actions.map(a => {
|
||||
const key = `${provider.name}.${a.name}`
|
||||
return {
|
||||
@ -95,13 +95,14 @@ const generateKeys = (provider: LaunchBarActionProvider, actions: LaunchBarActio
|
||||
provider,
|
||||
}
|
||||
})
|
||||
)
|
||||
async function getOnlineActions() {
|
||||
const onlineActions = (await Promise.all(
|
||||
actionProviders.map(async provider => (
|
||||
generateKeys(provider, await provider.getActions(this.keyword))
|
||||
)),
|
||||
)).flat()
|
||||
const onlineActions = (
|
||||
await Promise.all(
|
||||
actionProviders.map(async provider =>
|
||||
generateKeys(provider, await provider.getActions(this.keyword)),
|
||||
),
|
||||
)
|
||||
).flat()
|
||||
if (this.isHistory) {
|
||||
return
|
||||
}
|
||||
@ -217,13 +218,13 @@ export default Vue.extend({
|
||||
if (index === 0) {
|
||||
this.focus()
|
||||
} else {
|
||||
((e.currentTarget as HTMLElement).previousElementSibling as HTMLElement).focus()
|
||||
;((e.currentTarget as HTMLElement).previousElementSibling as HTMLElement).focus()
|
||||
}
|
||||
},
|
||||
nextItem(e: KeyboardEvent, index: number) {
|
||||
const lastItemIndex = this.actions.length - 1
|
||||
if (index !== lastItemIndex) {
|
||||
((e.currentTarget as HTMLElement).nextElementSibling as HTMLElement).focus()
|
||||
;((e.currentTarget as HTMLElement).nextElementSibling as HTMLElement).focus()
|
||||
} else {
|
||||
this.focus()
|
||||
}
|
||||
@ -248,7 +249,7 @@ export default Vue.extend({
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
.launch-bar {
|
||||
--color: black;
|
||||
color: var(--color);
|
||||
|
||||
@ -16,15 +16,28 @@ export const getHistoryItems = (key = SearchHistoryKey) => {
|
||||
export const clearHistoryItems = (key = SearchHistoryKey) => localStorage.setItem(key, '[]')
|
||||
export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
|
||||
console.log('add', keyword)
|
||||
localStorage.setItem(key, JSON.stringify(
|
||||
lodash.sortBy(lodash.uniqBy([{
|
||||
value: keyword,
|
||||
isHistory: 1,
|
||||
timestamp: Number(new Date()),
|
||||
}, ...getHistoryItems()], h => h.value), h => h.timestamp)
|
||||
.reverse()
|
||||
.slice(0, SearchHistoryMaxItems),
|
||||
))
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify(
|
||||
lodash
|
||||
.sortBy(
|
||||
lodash.uniqBy(
|
||||
[
|
||||
{
|
||||
value: keyword,
|
||||
isHistory: 1,
|
||||
timestamp: Number(new Date()),
|
||||
},
|
||||
...getHistoryItems(),
|
||||
],
|
||||
h => h.value,
|
||||
),
|
||||
h => h.timestamp,
|
||||
)
|
||||
.reverse()
|
||||
.slice(0, SearchHistoryMaxItems),
|
||||
),
|
||||
)
|
||||
}
|
||||
export const deleteHistoryItem = (keyword: string, key = SearchHistoryKey) => {
|
||||
const items = getHistoryItems()
|
||||
@ -48,18 +61,21 @@ export const historyProvider: LaunchBarActionProvider = {
|
||||
clearHistoryItems()
|
||||
},
|
||||
}
|
||||
const items = getHistoryItems().map(it => ({
|
||||
name: it.value,
|
||||
icon: 'mdi-history',
|
||||
// description: `在 ${formatDate(new Date(it.timestamp))} 搜索过`,
|
||||
explicitSelect: true,
|
||||
action: () => {
|
||||
search(it.value)
|
||||
},
|
||||
deleteAction: () => {
|
||||
deleteHistoryItem(it.value)
|
||||
},
|
||||
} as LaunchBarAction))
|
||||
const items = getHistoryItems().map(
|
||||
it =>
|
||||
({
|
||||
name: it.value,
|
||||
icon: 'mdi-history',
|
||||
// description: `在 ${formatDate(new Date(it.timestamp))} 搜索过`,
|
||||
explicitSelect: true,
|
||||
action: () => {
|
||||
search(it.value)
|
||||
},
|
||||
deleteAction: () => {
|
||||
deleteHistoryItem(it.value)
|
||||
},
|
||||
} as LaunchBarAction),
|
||||
)
|
||||
if (items.length > 0) {
|
||||
items.push(clearAction)
|
||||
}
|
||||
|
||||
@ -19,43 +19,49 @@ export const searchProvider: LaunchBarActionProvider = {
|
||||
getActions: async input => {
|
||||
const api = `https://s.search.bilibili.com/main/suggest?func=suggest&suggest_type=accurate&sub_type=tag&main_ver=v1&highlight=&userid=${getUID()}&bangumi_acc_num=1&special_acc_num=1&topic_acc_num=1&upuser_acc_num=3&tag_num=10&special_num=10&bangumi_num=10&upuser_num=3&term=${input}`
|
||||
const json = await getJson(api)
|
||||
const results: LaunchBarAction[] = [{
|
||||
name: input,
|
||||
icon: 'search',
|
||||
content: async () => Vue.extend({
|
||||
render: h => {
|
||||
const content = h('div', {
|
||||
domProps: {
|
||||
innerHTML: /* html */`<em class="suggest-highlight">${input}</em>`,
|
||||
const results: LaunchBarAction[] = [
|
||||
{
|
||||
name: input,
|
||||
icon: 'search',
|
||||
content: async () =>
|
||||
Vue.extend({
|
||||
render: h => {
|
||||
const content = h('div', {
|
||||
domProps: {
|
||||
innerHTML: /* html */ `<em class="suggest-highlight">${input}</em>`,
|
||||
},
|
||||
})
|
||||
return content
|
||||
},
|
||||
})
|
||||
return content
|
||||
},
|
||||
}),
|
||||
action: () => search(input),
|
||||
}]
|
||||
}),
|
||||
action: () => search(input),
|
||||
},
|
||||
]
|
||||
if (json.code !== 0) {
|
||||
return results
|
||||
}
|
||||
const suggests: { value: string, name: string }[] = lodash.get(json, 'result.tag')
|
||||
const suggests: { value: string; name: string }[] = lodash.get(json, 'result.tag')
|
||||
if (!suggests) {
|
||||
return results
|
||||
}
|
||||
results.push(...suggests.map(result => ({
|
||||
name: result.value,
|
||||
icon: 'search',
|
||||
content: async () => Vue.extend({
|
||||
render: h => {
|
||||
const content = h('div', {
|
||||
domProps: {
|
||||
innerHTML: result.name.replace(/suggest_high_light/g, 'suggest-highlight'),
|
||||
results.push(
|
||||
...suggests.map(result => ({
|
||||
name: result.value,
|
||||
icon: 'search',
|
||||
content: async () =>
|
||||
Vue.extend({
|
||||
render: h => {
|
||||
const content = h('div', {
|
||||
domProps: {
|
||||
innerHTML: result.name.replace(/suggest_high_light/g, 'suggest-highlight'),
|
||||
},
|
||||
})
|
||||
return content
|
||||
},
|
||||
})
|
||||
return content
|
||||
},
|
||||
}),
|
||||
action: () => search(result.value),
|
||||
})))
|
||||
}),
|
||||
action: () => search(result.value),
|
||||
})),
|
||||
)
|
||||
return lodash.uniqBy(results, it => it.name)
|
||||
},
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ class SocketBufferHelper {
|
||||
private static writeInt(buffer: number[], start: number, length: number, value: number) {
|
||||
let i = 0
|
||||
while (i < length) {
|
||||
buffer[start + i] = value / (256 ** (length - i - 1))
|
||||
buffer[start + i] = value / 256 ** (length - i - 1)
|
||||
i++
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ class SocketBufferHelper {
|
||||
const packetLen = 16 + data.byteLength
|
||||
const header = [0, 0, 0, 0, 0, 16, 0, 1, 0, 0, 0, liveOperationCodes[operationCode], 0, 0, 0, 1]
|
||||
SocketBufferHelper.writeInt(header, 0, 4, packetLen)
|
||||
return (new Uint8Array(header.concat(...data))).buffer
|
||||
return new Uint8Array(header.concat(...data)).buffer
|
||||
}
|
||||
decode(blob: Blob) {
|
||||
const decodeBuffer = async (buffer: Uint8Array) => {
|
||||
@ -59,7 +59,7 @@ class SocketBufferHelper {
|
||||
}
|
||||
const results = [result]
|
||||
if (result.packetLength < buffer.length) {
|
||||
results.push(...await decodeBuffer(buffer.slice(result.packetLength)))
|
||||
results.push(...(await decodeBuffer(buffer.slice(result.packetLength))))
|
||||
}
|
||||
if (result.operation === liveOperationCodes.message) {
|
||||
const bodyBuffer = buffer.slice(result.headerLength, result.packetLength)
|
||||
@ -101,15 +101,24 @@ class LiveTimeExtractor {
|
||||
resolve(this.startTime)
|
||||
return
|
||||
}
|
||||
const timeElement = dq('.bilibili-live-player-video-controller-duration-btn span') as HTMLElement
|
||||
const timeElement = dq(
|
||||
'.bilibili-live-player-video-controller-duration-btn span',
|
||||
) as HTMLElement
|
||||
const [observer] = childList(timeElement, records => {
|
||||
const isTimeChanged = records.length > 0
|
||||
&& records.some(r => r.addedNodes.length > 0
|
||||
&& [...r.addedNodes].every(it => it.nodeType === Node.TEXT_NODE))
|
||||
const isTimeChanged =
|
||||
records.length > 0 &&
|
||||
records.some(
|
||||
r =>
|
||||
r.addedNodes.length > 0 &&
|
||||
[...r.addedNodes].every(it => it.nodeType === Node.TEXT_NODE),
|
||||
)
|
||||
if (isTimeChanged) {
|
||||
observer.disconnect()
|
||||
const time = records[0].addedNodes[0].textContent as string
|
||||
const [seconds, minutes, hours = 0] = time.split(':').reverse().map(lodash.unary(parseInt))
|
||||
const [seconds, minutes, hours = 0] = time
|
||||
.split(':')
|
||||
.reverse()
|
||||
.map(lodash.unary(parseInt))
|
||||
const now = Number(new Date())
|
||||
this.startTime = now - hours * 1000 * 3600 - minutes * 60 * 1000 - seconds * 1000
|
||||
resolve(this.startTime)
|
||||
@ -186,10 +195,12 @@ export class LiveSocket extends EventTarget {
|
||||
if (!this.stopRequested && this.autoRetry) {
|
||||
console.log(`Live Socket: unexpected disconnect, retry in ${this.retryInterval}ms`)
|
||||
const index = this.servers.indexOf(this.selectedServer)
|
||||
if (index < this.servers.length - 1) { // 尝试下一个服务器
|
||||
if (index < this.servers.length - 1) {
|
||||
// 尝试下一个服务器
|
||||
this.selectedServer = this.servers[index + 1]
|
||||
} else { // 所有服务器用尽, 从头再来
|
||||
[this.selectedServer] = this.servers
|
||||
} else {
|
||||
// 所有服务器用尽, 从头再来
|
||||
;[this.selectedServer] = this.servers
|
||||
}
|
||||
console.log('Live Socket: server changed to', this.selectedServer)
|
||||
setTimeout(() => this.start(), this.retryInterval)
|
||||
@ -197,28 +208,34 @@ export class LiveSocket extends EventTarget {
|
||||
}
|
||||
/** 启动WebSocket */
|
||||
async start() {
|
||||
const roomConfig = await getJson(`https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=${this.roomID}&platform=pc&player=web`)
|
||||
const roomConfig = await getJson(
|
||||
`https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=${this.roomID}&platform=pc&player=web`,
|
||||
)
|
||||
const hostServers: { host: string }[] = lodash.get(roomConfig, 'data.host_server_list', [])
|
||||
// let server = 'broadcastlv.chat.bilibili.com'
|
||||
// if (hostServers.length > 0) {
|
||||
// server = hostServers[0].host
|
||||
// }
|
||||
this.servers = [...new Set([...this.servers, ...hostServers.map(it => it.host)])]
|
||||
if (this.selectedServer === '') { // 首次启动
|
||||
[this.selectedServer] = this.servers
|
||||
if (this.selectedServer === '') {
|
||||
// 首次启动
|
||||
;[this.selectedServer] = this.servers
|
||||
console.log('Initial server:', this.selectedServer)
|
||||
}
|
||||
|
||||
if (this.webSocket
|
||||
&& ([WebSocket.CONNECTING, WebSocket.OPEN].includes(this.webSocket.readyState))
|
||||
if (
|
||||
this.webSocket &&
|
||||
[WebSocket.CONNECTING, WebSocket.OPEN].includes(this.webSocket.readyState)
|
||||
) {
|
||||
this.stop()
|
||||
}
|
||||
this.webSocket = new WebSocket(`wss://${this.selectedServer}/sub`)
|
||||
this.stopRequested = false
|
||||
this.dispatchEvent(new CustomEvent('start', {
|
||||
detail: this.webSocket,
|
||||
}))
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('start', {
|
||||
detail: this.webSocket,
|
||||
}),
|
||||
)
|
||||
this.webSocket.addEventListener('open', () => {
|
||||
const enterRoomData = {
|
||||
roomid: this.roomID,
|
||||
@ -230,15 +247,19 @@ export class LiveSocket extends EventTarget {
|
||||
key: lodash.get(roomConfig, 'data.token'),
|
||||
}
|
||||
this.webSocket.send(this.bufferHelper.encode(JSON.stringify(enterRoomData), 'enterRoom'))
|
||||
this.dispatchEvent(new CustomEvent('open', {
|
||||
detail: enterRoomData,
|
||||
}))
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('open', {
|
||||
detail: enterRoomData,
|
||||
}),
|
||||
)
|
||||
})
|
||||
this.webSocket.addEventListener('message', async e => {
|
||||
const [data] = await this.bufferHelper.decode(e.data)
|
||||
this.dispatchEvent(new CustomEvent('message', {
|
||||
detail: data,
|
||||
}))
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('message', {
|
||||
detail: data,
|
||||
}),
|
||||
)
|
||||
switch (data.operation) {
|
||||
case liveOperationCodes.enterRoomResponse: {
|
||||
if (this.heartBeatTimer) {
|
||||
@ -253,9 +274,11 @@ export class LiveSocket extends EventTarget {
|
||||
if (!data.heartBeatResponse) {
|
||||
break
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('heartBeatResponse', {
|
||||
detail: data.heartBeatResponse.count,
|
||||
}))
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('heartBeatResponse', {
|
||||
detail: data.heartBeatResponse.count,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case liveOperationCodes.message: {
|
||||
@ -281,14 +304,17 @@ export class LiveSocket extends EventTarget {
|
||||
return this.sendTime - this.startTime
|
||||
},
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('danmaku', {
|
||||
detail: danmaku,
|
||||
}))
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('danmaku', {
|
||||
detail: danmaku,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
default: break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
this.webSocket.addEventListener('close', e => {
|
||||
|
||||
@ -45,10 +45,7 @@ export const component = defineComponentMetadata({
|
||||
options.lastUpdateCheck = Number(new Date())
|
||||
const versionMatch = scriptText.match(/^\/\/ @version\s*([\d.]+)$/m)
|
||||
if (!versionMatch?.[1]) {
|
||||
console.warn(
|
||||
'[新版本提示] 未能检测出脚本版本, scriptText.length =',
|
||||
scriptText.length,
|
||||
)
|
||||
console.warn('[新版本提示] 未能检测出脚本版本, scriptText.length =', scriptText.length)
|
||||
return
|
||||
}
|
||||
const latestVersion = new Version(versionMatch[1])
|
||||
|
||||
@ -26,8 +26,8 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import "markdown";
|
||||
@import 'common';
|
||||
@import 'markdown';
|
||||
|
||||
.component-description {
|
||||
word-break: break-all;
|
||||
|
||||
@ -21,9 +21,7 @@
|
||||
v-if="(componentData.options && generatedOptions.length > 0) || componentData.extraOptions"
|
||||
>
|
||||
<div class="component-detail-options">
|
||||
<div class="component-detail-options-title">
|
||||
选项
|
||||
</div>
|
||||
<div class="component-detail-options-title">选项</div>
|
||||
<div v-for="[name, option] of generatedOptions" :key="name" class="generated-option">
|
||||
<ComponentOption
|
||||
:name="name"
|
||||
@ -49,14 +47,10 @@
|
||||
<div class="component-detail-grow"></div>
|
||||
<div class="component-detail-internal-data">
|
||||
<div v-if="componentData.commitHash" class="component-detail-internal-data-row">
|
||||
<div class="internal-name">
|
||||
Commit: {{ componentData.commitHash.substring(0, 9) }}
|
||||
</div>
|
||||
<div class="internal-name">Commit: {{ componentData.commitHash.substring(0, 9) }}</div>
|
||||
</div>
|
||||
<div class="component-detail-internal-data-row">
|
||||
<div class="internal-name">
|
||||
内部名称: {{ componentData.name }}
|
||||
</div>
|
||||
<div class="internal-name">内部名称: {{ componentData.name }}</div>
|
||||
<MiniToast
|
||||
v-if="componentData.configurable !== false && componentActions.length > 0"
|
||||
placement="bottom"
|
||||
@ -68,10 +62,7 @@
|
||||
</div>
|
||||
<template #toast>
|
||||
<div class="extra-actions-list">
|
||||
<div
|
||||
v-for="a of componentActions"
|
||||
:key="a.name"
|
||||
>
|
||||
<div v-for="a of componentActions" :key="a.name">
|
||||
<component
|
||||
:is="a.component"
|
||||
v-if="a.component"
|
||||
@ -97,12 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
VButton,
|
||||
VIcon,
|
||||
SwitchBox,
|
||||
MiniToast,
|
||||
} from '@/ui'
|
||||
import { VButton, VIcon, SwitchBox, MiniToast } from '@/ui'
|
||||
import { visible } from '@/core/observer'
|
||||
import { ComponentOptions } from '../component'
|
||||
import ComponentDescription from './ComponentDescription.vue'
|
||||
|
||||
@ -11,11 +11,7 @@
|
||||
:placeholder="value.toString()"
|
||||
@change="type === 'text' ? valueChange($event) : numberChange($event)"
|
||||
></TextBox>
|
||||
<SwitchBox
|
||||
v-if="type === 'boolean'"
|
||||
:checked="value"
|
||||
@change="valueChange($event)"
|
||||
></SwitchBox>
|
||||
<SwitchBox v-if="type === 'boolean'" :checked="value" @change="valueChange($event)"></SwitchBox>
|
||||
<ColorPicker
|
||||
v-if="type === 'color'"
|
||||
:compact="true"
|
||||
@ -29,11 +25,7 @@
|
||||
:range="value"
|
||||
@change="valueChange($event)"
|
||||
></RangeInput>
|
||||
<ImagePicker
|
||||
v-if="type === 'image'"
|
||||
:image="value"
|
||||
@change="valueChange($event)"
|
||||
></ImagePicker>
|
||||
<ImagePicker v-if="type === 'image'" :image="value" @change="valueChange($event)"></ImagePicker>
|
||||
<VDropdown
|
||||
v-if="type === 'dropdown'"
|
||||
:value="value"
|
||||
@ -57,22 +49,12 @@
|
||||
:value="value"
|
||||
@change="debounceValueChange($event)"
|
||||
></VSlider>
|
||||
<div v-if="type === 'unknown'" class="unknown-option-type">
|
||||
未知的选项类型
|
||||
</div>
|
||||
<div v-if="type === 'unknown'" class="unknown-option-type">未知的选项类型</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
TextBox,
|
||||
SwitchBox,
|
||||
ColorPicker,
|
||||
RangeInput,
|
||||
VDropdown,
|
||||
ImagePicker,
|
||||
VSlider,
|
||||
} from '@/ui'
|
||||
import { TextBox, SwitchBox, ColorPicker, RangeInput, VDropdown, ImagePicker, VSlider } from '@/ui'
|
||||
import { getComponentSettings, ComponentSettings } from '@/core/settings'
|
||||
import { ComponentOption } from '../component'
|
||||
import { getDropdownItems } from './dropdown'
|
||||
|
||||
@ -6,10 +6,7 @@
|
||||
<div class="display-name">
|
||||
{{ componentData.displayName }}
|
||||
</div>
|
||||
<SwitchBox
|
||||
v-if="componentData.configurable !== false"
|
||||
v-model="settings.enabled"
|
||||
/>
|
||||
<SwitchBox v-if="componentData.configurable !== false" v-model="settings.enabled" />
|
||||
<VIcon v-else icon="right-arrow" class="details-arrow" :size="18" />
|
||||
</div>
|
||||
</template>
|
||||
@ -74,18 +71,15 @@ export default Vue.extend({
|
||||
if (typeof description === 'string') {
|
||||
return description
|
||||
}
|
||||
return (
|
||||
description[getSelectedLanguage()]
|
||||
|| description['zh-CN']
|
||||
)
|
||||
return description[getSelectedLanguage()] || description['zh-CN']
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import "markdown";
|
||||
@import 'common';
|
||||
@import 'markdown';
|
||||
|
||||
.component-settings {
|
||||
display: flex;
|
||||
@ -98,13 +92,13 @@ export default Vue.extend({
|
||||
min-height: 36px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
user-select: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-out;
|
||||
transition: opacity 0.2s ease-out;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10px;
|
||||
@ -113,8 +107,16 @@ export default Vue.extend({
|
||||
pointer-events: none;
|
||||
$color: #8882;
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom, #0000, #0000 10px, $color 10px,
|
||||
$color 30px, #0000 30px, #0000 38px, $color 38px, $color 50px, #0000 50px
|
||||
to bottom,
|
||||
#0000,
|
||||
#0000 10px,
|
||||
$color 10px,
|
||||
$color 30px,
|
||||
#0000 30px,
|
||||
#0000 38px,
|
||||
$color 38px,
|
||||
$color 50px,
|
||||
#0000 50px
|
||||
);
|
||||
}
|
||||
&.virtual {
|
||||
|
||||
@ -21,11 +21,7 @@
|
||||
<VIcon :size="20" :icon="t.icon" :style="{ color: t.color }" />
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<div
|
||||
v-for="t of subPages"
|
||||
:key="t.name"
|
||||
class="component-tags-item"
|
||||
>
|
||||
<div v-for="t of subPages" :key="t.name" class="component-tags-item">
|
||||
<VIcon :size="20" :icon="t.icon" :style="{ color: 'inherit' }" />
|
||||
</div>
|
||||
</div>
|
||||
@ -41,9 +37,7 @@
|
||||
<div class="tag-name">
|
||||
{{ t.displayName }}
|
||||
</div>
|
||||
<div class="tag-count">
|
||||
({{ t.count }})
|
||||
</div>
|
||||
<div class="tag-count">({{ t.count }})</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<div
|
||||
@ -123,7 +117,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
.settings-panel-content .sidebar > * {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@ -134,7 +128,7 @@ export default Vue.extend({
|
||||
.settings-panel-sub-page {
|
||||
font-size: 13px;
|
||||
top: 12px;
|
||||
transition: .3s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
transition: 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
transform: translateX(calc(-12.5% * var(--direction)));
|
||||
min-width: 372px;
|
||||
padding: 12px;
|
||||
@ -197,7 +191,7 @@ export default Vue.extend({
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
pointer-events: none;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #8882;
|
||||
|
||||
@ -44,10 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
VPopup,
|
||||
VIcon,
|
||||
} from '@/ui'
|
||||
import { VPopup, VIcon } from '@/ui'
|
||||
import { externalApis } from '@/core/core-apis'
|
||||
|
||||
export default {
|
||||
@ -74,9 +71,7 @@ export default {
|
||||
trigger: HTMLElement
|
||||
}) {
|
||||
if (
|
||||
dqa('.be-settings-extra-options').some(
|
||||
c => c === data.target || c.contains(data.target),
|
||||
)
|
||||
dqa('.be-settings-extra-options').some(c => c === data.target || c.contains(data.target))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@ -96,7 +91,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
.be-settings {
|
||||
body.player-mode-blackmask & {
|
||||
visibility: hidden;
|
||||
@ -138,7 +133,7 @@ export default {
|
||||
margin-bottom: $size;
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
width: 140%;
|
||||
height: 140%;
|
||||
position: absolute;
|
||||
@ -191,7 +186,7 @@ export default {
|
||||
transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
opacity 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
&.close {
|
||||
transform: translateZ(0)translateY(-50%) translateX(calc(-48% * var(--direction)));
|
||||
transform: translateZ(0) translateY(-50%) translateX(calc(-48% * var(--direction)));
|
||||
}
|
||||
&.open {
|
||||
transform: translateZ(0) translateY(-50%) translateX(0);
|
||||
@ -201,7 +196,7 @@ export default {
|
||||
.bilibili-player-dm-tip-wrap {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
@import "./dock/center";
|
||||
@import "./dock/left";
|
||||
@import "./dock/right";
|
||||
@import './dock/center';
|
||||
@import './dock/left';
|
||||
@import './dock/right';
|
||||
</style>
|
||||
|
||||
@ -23,30 +23,18 @@
|
||||
<div ref="mainContainer" class="main">
|
||||
<div ref="componentList" class="component-list">
|
||||
<div class="settings-panel-search-bar">
|
||||
<TextBox
|
||||
v-model="searchKeyword"
|
||||
class="settings-panel-search"
|
||||
placeholder="搜索"
|
||||
/>
|
||||
<TextBox v-model="searchKeyword" class="settings-panel-search" placeholder="搜索" />
|
||||
<VButton
|
||||
v-for="action of searchBarActions"
|
||||
:key="action.key"
|
||||
type="transparent"
|
||||
icon
|
||||
:title="
|
||||
typeof action.title === 'function'
|
||||
? action.title(searchBarContext)
|
||||
: action.title
|
||||
"
|
||||
:disabled="
|
||||
action.disabled ? action.disabled(searchBarContext) : false
|
||||
typeof action.title === 'function' ? action.title(searchBarContext) : action.title
|
||||
"
|
||||
:disabled="action.disabled ? action.disabled(searchBarContext) : false"
|
||||
>
|
||||
<VIcon
|
||||
:icon="action.icon"
|
||||
:size="18"
|
||||
@click="action.run(searchBarContext)"
|
||||
/>
|
||||
<VIcon :icon="action.icon" :size="18" @click="action.run(searchBarContext)" />
|
||||
</VButton>
|
||||
</div>
|
||||
<div
|
||||
@ -85,19 +73,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
VIcon,
|
||||
TextBox,
|
||||
VPopup,
|
||||
VEmpty,
|
||||
VButton,
|
||||
} from '@/ui'
|
||||
import { VIcon, TextBox, VPopup, VEmpty, VButton } from '@/ui'
|
||||
import { getHook } from '@/plugins/hook'
|
||||
import { deleteValue } from '@/core/utils'
|
||||
import ComponentSettings from './ComponentSettings.vue'
|
||||
import {
|
||||
ComponentMetadata, ComponentTag, components,
|
||||
} from '../component'
|
||||
import { ComponentMetadata, ComponentTag, components } from '../component'
|
||||
import ComponentDetail from './ComponentDetail.vue'
|
||||
import ComponentTags from './ComponentTags.vue'
|
||||
import { getDescriptionText } from '../description'
|
||||
@ -132,23 +112,31 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isComponentSelected() {
|
||||
return (name: string) => (
|
||||
return (name: string) =>
|
||||
this.selectedComponents.some((c: ComponentMetadata) => c.name === name)
|
||||
)
|
||||
},
|
||||
tags() {
|
||||
const renderedComponents = this.renderedComponents as ComponentMetadata[]
|
||||
let tags = [] as (ComponentTag & { count: number })[]
|
||||
renderedComponents.forEach(it => it.tags.forEach(t => {
|
||||
tags.push({ count: 0, ...t })
|
||||
}))
|
||||
renderedComponents.forEach(it =>
|
||||
it.tags.forEach(t => {
|
||||
tags.push({ count: 0, ...t })
|
||||
}),
|
||||
)
|
||||
const counts = lodash.countBy(tags, (t: ComponentTag) => t.name)
|
||||
tags = lodash.uniqBy(tags, t => t.name)
|
||||
tags.forEach(t => (t.count = counts[t.name]))
|
||||
return tags
|
||||
},
|
||||
searchBarContext(): SearchBarActionContext {
|
||||
return lodash.pick(this, 'components', 'selectedComponent', 'selectedComponents', 'searchKeyword', 'searchFilter')
|
||||
return lodash.pick(
|
||||
this,
|
||||
'components',
|
||||
'selectedComponent',
|
||||
'selectedComponents',
|
||||
'searchKeyword',
|
||||
'searchFilter',
|
||||
)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -168,9 +156,9 @@ export default {
|
||||
components() {
|
||||
this.updateRenderedComponents()
|
||||
this.$refs.componentTags.refreshTags()
|
||||
if (!this.components.some((c: ComponentMetadata) => (
|
||||
c.name === this.selectedComponent?.name
|
||||
))) {
|
||||
if (
|
||||
!this.components.some((c: ComponentMetadata) => c.name === this.selectedComponent?.name)
|
||||
) {
|
||||
this.selectedComponent = null
|
||||
}
|
||||
},
|
||||
@ -191,7 +179,7 @@ export default {
|
||||
let endIdx = list.findIndex(c => c.name === name)
|
||||
if (startIdx > endIdx) {
|
||||
// if start index is greater than end index, swap them
|
||||
[startIdx, endIdx] = [endIdx, startIdx]
|
||||
;[startIdx, endIdx] = [endIdx, startIdx]
|
||||
}
|
||||
this.selectedComponents = list.slice(startIdx, endIdx + 1)
|
||||
return
|
||||
@ -228,12 +216,19 @@ export default {
|
||||
return {}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
await Promise.all(components.map(async c => [c.name, ([
|
||||
c.name,
|
||||
c.displayName,
|
||||
c.tags.map(t => `${t.name}\n${t.displayName}`).join('\n'),
|
||||
await getDescriptionText(c),
|
||||
]).join('\n').toLowerCase()])),
|
||||
await Promise.all(
|
||||
components.map(async c => [
|
||||
c.name,
|
||||
[
|
||||
c.name,
|
||||
c.displayName,
|
||||
c.tags.map(t => `${t.name}\n${t.displayName}`).join('\n'),
|
||||
await getDescriptionText(c),
|
||||
]
|
||||
.join('\n')
|
||||
.toLowerCase(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
})()
|
||||
const internalFiltered = components.filter(c => {
|
||||
@ -397,8 +392,7 @@ export default {
|
||||
left: calc(100% - 12px);
|
||||
height: calc(100% - 22px);
|
||||
z-index: -1;
|
||||
transform: translateZ(0) translateY(-50%)
|
||||
translateX(calc(-48% * var(--direction)));
|
||||
transform: translateZ(0) translateY(-50%) translateX(calc(-48% * var(--direction)));
|
||||
transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
opacity 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
padding-left: 12px;
|
||||
|
||||
@ -42,7 +42,7 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStyle(tag: { color: string}, index: number) {
|
||||
getStyle(tag: { color: string }, index: number) {
|
||||
const strokeDashoffset = (index / this.tags.length) * this.circumference
|
||||
return {
|
||||
strokeDashoffset,
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div class="widgets-panel">
|
||||
<div class="widgets-panel-header">
|
||||
<VIcon icon="widgets"></VIcon>功能
|
||||
</div>
|
||||
<div class="widgets-panel-header"><VIcon icon="widgets"></VIcon>功能</div>
|
||||
<!-- <div class="widgets-loading" v-if="loading">加载中...</div> -->
|
||||
<VEmpty v-if="!loading && widgets.length === 0" class="widgets-empty"></VEmpty>
|
||||
<div class="widget-items">
|
||||
@ -20,10 +18,7 @@
|
||||
<script lang="ts">
|
||||
import { Widget } from '@/components/widget'
|
||||
import { deleteValue, matchUrlPattern } from '@/core/utils'
|
||||
import {
|
||||
VIcon,
|
||||
VEmpty,
|
||||
} from '@/ui'
|
||||
import { VIcon, VEmpty } from '@/ui'
|
||||
import { registerAndGetData } from '../../plugins/data'
|
||||
import { WidgetsPlugin } from '.'
|
||||
|
||||
@ -37,10 +32,7 @@ const widgetFilter = async (w: Widget) => {
|
||||
}
|
||||
if (w.condition) {
|
||||
const result = w.condition()
|
||||
if (
|
||||
result === true
|
||||
|| (result instanceof Promise && (await result) === true)
|
||||
) {
|
||||
if (result === true || (result instanceof Promise && (await result) === true)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -83,7 +75,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
.widgets-panel {
|
||||
max-height: var(--panel-height);
|
||||
min-height: 80px;
|
||||
@ -126,7 +118,7 @@ export default Vue.extend({
|
||||
align-items: flex-start;
|
||||
.widget-item {
|
||||
font-size: 14px;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
display: flex;
|
||||
&-enter,
|
||||
&-leave-to {
|
||||
|
||||
@ -18,8 +18,9 @@ export interface ComponentVueAction {
|
||||
name: string
|
||||
component: Executable<VueModule>
|
||||
}
|
||||
export type ComponentAction = (metadata: ComponentMetadata)
|
||||
=> ComponentConfigAction | ComponentVueAction
|
||||
export type ComponentAction = (
|
||||
metadata: ComponentMetadata,
|
||||
) => ComponentConfigAction | ComponentVueAction
|
||||
|
||||
const builtInActions: ComponentAction[] = [
|
||||
metadata => ({
|
||||
@ -35,4 +36,7 @@ const builtInActions: ComponentAction[] = [
|
||||
},
|
||||
}),
|
||||
]
|
||||
export const [componentActions] = registerAndGetData('settingsPanel.componentActions', builtInActions)
|
||||
export const [componentActions] = registerAndGetData(
|
||||
'settingsPanel.componentActions',
|
||||
builtInActions,
|
||||
)
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
export const getDropdownItems = <T> (enumClass: T) => {
|
||||
export const getDropdownItems = <T>(enumClass: T) => {
|
||||
if (Array.isArray(enumClass)) {
|
||||
return enumClass
|
||||
}
|
||||
const dropdownItems = Object.entries(enumClass).filter(([key]) => {
|
||||
const charCode = key.charCodeAt(0)
|
||||
// '0': 48; '9': 57
|
||||
if (charCode >= 48 && charCode <= 57) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}).map(([, value]) => value)
|
||||
const dropdownItems = Object.entries(enumClass)
|
||||
.filter(([key]) => {
|
||||
const charCode = key.charCodeAt(0)
|
||||
// '0': 48; '9': 57
|
||||
if (charCode >= 48 && charCode <= 57) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map(([, value]) => value)
|
||||
// console.log(dropdownItems)
|
||||
return dropdownItems
|
||||
}
|
||||
|
||||
@ -6,15 +6,17 @@ export const provideActions = () => {
|
||||
const onlineRegistryActionName = 'onlineRegistry'
|
||||
providers.push({
|
||||
name: onlineRegistryActionName,
|
||||
getActions: async () => [{
|
||||
name: '切换在线仓库',
|
||||
description: 'Toggle Online Registry',
|
||||
icon: 'mdi-web',
|
||||
action: async () => {
|
||||
const { togglePopup } = await import('./sub-pages/online-registry/vm')
|
||||
togglePopup()
|
||||
getActions: async () => [
|
||||
{
|
||||
name: '切换在线仓库',
|
||||
description: 'Toggle Online Registry',
|
||||
icon: 'mdi-web',
|
||||
action: async () => {
|
||||
const { togglePopup } = await import('./sub-pages/online-registry/vm')
|
||||
togglePopup()
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,11 +5,7 @@ import { CdnTypes } from '@/core/cdn-types'
|
||||
import { addComponentListener } from '@/core/settings'
|
||||
import { DownloadPackageEmitMode } from '@/core/download-mode'
|
||||
import { ComponentEntry, componentsTags } from '../types'
|
||||
import {
|
||||
defineComponentMetadata,
|
||||
defineOptionsMetadata,
|
||||
OptionsOfMetadata,
|
||||
} from '../define'
|
||||
import { defineComponentMetadata, defineOptionsMetadata, OptionsOfMetadata } from '../define'
|
||||
import { provideActions } from './external-actions'
|
||||
|
||||
export const WidgetsPlugin = 'widgets'
|
||||
|
||||
@ -14,25 +14,26 @@ export const componentSettingsMixin = Vue.extend({
|
||||
}
|
||||
},
|
||||
})
|
||||
export const virtualScrollMixin = (containerSelector: string) => Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
virtual: false,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const { dq } = await import('@/core/utils')
|
||||
const { visibleInside } = await import('@/core/observer')
|
||||
const element = this.$el as HTMLElement
|
||||
const container = dq(containerSelector) as HTMLElement
|
||||
if (!container) {
|
||||
console.warn('virtual container not found, virtual scroll will be disabled!')
|
||||
return
|
||||
}
|
||||
visibleInside(element, container, '150% 0px', records => {
|
||||
records.forEach(record => {
|
||||
this.virtual = !record.isIntersecting
|
||||
export const virtualScrollMixin = (containerSelector: string) =>
|
||||
Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
virtual: false,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const { dq } = await import('@/core/utils')
|
||||
const { visibleInside } = await import('@/core/observer')
|
||||
const element = this.$el as HTMLElement
|
||||
const container = dq(containerSelector) as HTMLElement
|
||||
if (!container) {
|
||||
console.warn('virtual container not found, virtual scroll will be disabled!')
|
||||
return
|
||||
}
|
||||
visibleInside(element, container, '150% 0px', records => {
|
||||
records.forEach(record => {
|
||||
this.virtual = !record.isIntersecting
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@ -39,4 +39,6 @@ const builtInActions: SearchBarAction[] = [
|
||||
},
|
||||
},
|
||||
]
|
||||
export const [searchBarActions] = registerAndGetData('settingsPanel.searchBarActions', [...builtInActions])
|
||||
export const [searchBarActions] = registerAndGetData('settingsPanel.searchBarActions', [
|
||||
...builtInActions,
|
||||
])
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
<div class="be-about-page">
|
||||
<div class="be-about-page-header">
|
||||
<VIcon icon="mdi-information-outline" />
|
||||
<div class="title-text">
|
||||
关于
|
||||
</div>
|
||||
<div class="title-text">关于</div>
|
||||
</div>
|
||||
<div class="be-about-page-content">
|
||||
<div class="script-meta-info">
|
||||
@ -22,25 +20,41 @@
|
||||
</div> -->
|
||||
</div>
|
||||
<div v-if="feedbackSupported" class="script-links">
|
||||
<a target="_blank" href="https://github.com/the1812/Bilibili-Evolved" class="homepage script-link">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/the1812/Bilibili-Evolved"
|
||||
class="homepage script-link"
|
||||
>
|
||||
<VButton>
|
||||
<VIcon icon="mdi-home-outline" :size="20" />
|
||||
主页
|
||||
</VButton>
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/the1812/Bilibili-Evolved/issues" class="feedback script-link">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/the1812/Bilibili-Evolved/issues"
|
||||
class="feedback script-link"
|
||||
>
|
||||
<VButton>
|
||||
<VIcon icon="mdi-message-text-outline" :size="18" />
|
||||
反馈
|
||||
</VButton>
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/the1812/Bilibili-Evolved/releases" class="releases script-link">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/the1812/Bilibili-Evolved/releases"
|
||||
class="releases script-link"
|
||||
>
|
||||
<VButton>
|
||||
<VIcon icon="mdi-update" :size="20" />
|
||||
更新日志
|
||||
</VButton>
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/the1812/Bilibili-Evolved/blob/preview/doc/donate.md" class="donate script-link">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/the1812/Bilibili-Evolved/blob/preview/doc/donate.md"
|
||||
class="donate script-link"
|
||||
>
|
||||
<VButton>
|
||||
<VIcon icon="mdi-heart-outline" :size="18" />
|
||||
捐赠
|
||||
@ -66,10 +80,7 @@
|
||||
<script lang="ts">
|
||||
import { meta } from '@/core/meta'
|
||||
import { formatDateTime } from '@/core/utils/formatters'
|
||||
import {
|
||||
VButton,
|
||||
VIcon,
|
||||
} from '@/ui'
|
||||
import { VButton, VIcon } from '@/ui'
|
||||
import { AboutPageAction, aboutPageActions } from './about-page'
|
||||
|
||||
const feedbackSupported = (() => {
|
||||
@ -110,7 +121,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.be-about-page {
|
||||
flex: 1;
|
||||
@ -139,7 +150,7 @@ export default Vue.extend({
|
||||
color: var(--theme-color);
|
||||
}
|
||||
&-description {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,10 @@ const config: ManagePanelConfig<ComponentMetadata> = {
|
||||
// if (item.hidden) {
|
||||
// return false
|
||||
// }
|
||||
if (search && !`${item.name}\n${item.displayName}`.toLowerCase().includes(search.toLowerCase())) {
|
||||
if (
|
||||
search &&
|
||||
!`${item.name}\n${item.displayName}`.toLowerCase().includes(search.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (excludeBuiltIn && !isUserComponent(item)) {
|
||||
@ -42,20 +45,18 @@ const config: ManagePanelConfig<ComponentMetadata> = {
|
||||
return message
|
||||
},
|
||||
}
|
||||
const getItemConfig = (item: ComponentMetadata): ManageItem<ComponentMetadata> => (
|
||||
{
|
||||
key: 'userComponents',
|
||||
item,
|
||||
isUserItem: isUserComponent(item),
|
||||
getSettings: it => getComponentSettings(it),
|
||||
onItemRemove: async it => {
|
||||
const { before, after } = getHook('userComponents.remove', it)
|
||||
await before()
|
||||
uninstallComponent(it.name)
|
||||
await after()
|
||||
},
|
||||
}
|
||||
)
|
||||
const getItemConfig = (item: ComponentMetadata): ManageItem<ComponentMetadata> => ({
|
||||
key: 'userComponents',
|
||||
item,
|
||||
isUserItem: isUserComponent(item),
|
||||
getSettings: it => getComponentSettings(it),
|
||||
onItemRemove: async it => {
|
||||
const { before, after } = getHook('userComponents.remove', it)
|
||||
await before()
|
||||
uninstallComponent(it.name)
|
||||
await after()
|
||||
},
|
||||
})
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
ManagePanel,
|
||||
|
||||
@ -10,9 +10,7 @@
|
||||
<script lang="ts">
|
||||
import { isUserPlugin } from '@/core/settings'
|
||||
import { getHook } from '@/plugins/hook'
|
||||
import {
|
||||
installPlugin, PluginMetadata, plugins, uninstallPlugin,
|
||||
} from '@/plugins/plugin'
|
||||
import { installPlugin, PluginMetadata, plugins, uninstallPlugin } from '@/plugins/plugin'
|
||||
import { ManageItem, ManagePanelConfig } from './manage-panel/manage-panel'
|
||||
import ManagePanel from './manage-panel/ManagePanel.vue'
|
||||
import UserItem from './manage-panel/UserItem.vue'
|
||||
@ -21,10 +19,14 @@ const config: ManagePanelConfig<PluginMetadata> = {
|
||||
key: 'userPlugins',
|
||||
icon: 'mdi-puzzle-outline',
|
||||
title: '插件',
|
||||
description: '可以在此处管理插件, 插件能够增强现有组件的功能. 内置插件包括脚本本体包含的插件和组件自带的插件, 组件自带的插件会自动随组件卸载而卸载.',
|
||||
description:
|
||||
'可以在此处管理插件, 插件能够增强现有组件的功能. 内置插件包括脚本本体包含的插件和组件自带的插件, 组件自带的插件会自动随组件卸载而卸载.',
|
||||
list: plugins,
|
||||
listFilter: (item, search, excludeBuiltIn) => {
|
||||
if (search && !`${item.name}\n${item.displayName}`.toLowerCase().includes(search.toLowerCase())) {
|
||||
if (
|
||||
search &&
|
||||
!`${item.name}\n${item.displayName}`.toLowerCase().includes(search.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (excludeBuiltIn && !isUserPlugin(item.name)) {
|
||||
@ -40,19 +42,17 @@ const config: ManagePanelConfig<PluginMetadata> = {
|
||||
return message
|
||||
},
|
||||
}
|
||||
const getItemConfig = (item: PluginMetadata): ManageItem<PluginMetadata> => (
|
||||
{
|
||||
key: 'userPlugins',
|
||||
item,
|
||||
isUserItem: isUserPlugin(item.name),
|
||||
onItemRemove: async it => {
|
||||
const { before, after } = getHook('userPlugins.remove', it)
|
||||
await before()
|
||||
uninstallPlugin(it.name)
|
||||
await after()
|
||||
},
|
||||
}
|
||||
)
|
||||
const getItemConfig = (item: PluginMetadata): ManageItem<PluginMetadata> => ({
|
||||
key: 'userPlugins',
|
||||
item,
|
||||
isUserItem: isUserPlugin(item.name),
|
||||
onItemRemove: async it => {
|
||||
const { before, after } = getHook('userPlugins.remove', it)
|
||||
await before()
|
||||
uninstallPlugin(it.name)
|
||||
await after()
|
||||
},
|
||||
})
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
ManagePanel,
|
||||
|
||||
@ -9,9 +9,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { getHook } from '@/plugins/hook'
|
||||
import {
|
||||
UserStyle, installStyle, uninstallStyle, styles,
|
||||
} from '@/plugins/style'
|
||||
import { UserStyle, installStyle, uninstallStyle, styles } from '@/plugins/style'
|
||||
import { ManageItem, ManagePanelConfig } from './manage-panel/manage-panel'
|
||||
import ManagePanel from './manage-panel/ManagePanel.vue'
|
||||
import UserItem from './manage-panel/UserItem.vue'
|
||||
@ -21,10 +19,14 @@ const config: ManagePanelConfig<StyleType> = {
|
||||
key: 'userStyles',
|
||||
icon: 'mdi-tune',
|
||||
title: '样式',
|
||||
description: '可以在此处管理自定义样式, 自定义样式能简单修改界面元素以满足您的需求, 对于更复杂的样式, 推荐使用 Stylus 浏览器插件来管理.',
|
||||
description:
|
||||
'可以在此处管理自定义样式, 自定义样式能简单修改界面元素以满足您的需求, 对于更复杂的样式, 推荐使用 Stylus 浏览器插件来管理.',
|
||||
list: styles,
|
||||
listFilter: (item, search) => {
|
||||
if (search && !`${item.name}\n${item.displayName}`.toLowerCase().includes(search.toLowerCase())) {
|
||||
if (
|
||||
search &&
|
||||
!`${item.name}\n${item.displayName}`.toLowerCase().includes(search.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -37,19 +39,17 @@ const config: ManagePanelConfig<StyleType> = {
|
||||
return message
|
||||
},
|
||||
}
|
||||
const getItemConfig = (item: StyleType): ManageItem<StyleType> => (
|
||||
{
|
||||
key: 'userStyles',
|
||||
item,
|
||||
isUserItem: true,
|
||||
onItemRemove: async it => {
|
||||
const { before, after } = getHook('userStyles.remove', it)
|
||||
await before()
|
||||
uninstallStyle(it.name)
|
||||
await after()
|
||||
},
|
||||
}
|
||||
)
|
||||
const getItemConfig = (item: StyleType): ManageItem<StyleType> => ({
|
||||
key: 'userStyles',
|
||||
item,
|
||||
isUserItem: true,
|
||||
onItemRemove: async it => {
|
||||
const { before, after } = getHook('userStyles.remove', it)
|
||||
await before()
|
||||
uninstallStyle(it.name)
|
||||
await after()
|
||||
},
|
||||
})
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
ManagePanel,
|
||||
|
||||
@ -19,9 +19,7 @@
|
||||
</div>
|
||||
<div v-if="config.description" class="sub-page-row separator"></div>
|
||||
<div class="sub-page-row add-item-row">
|
||||
<div class="title-text">
|
||||
添加{{ config.title }}:
|
||||
</div>
|
||||
<div class="title-text">添加{{ config.title }}:</div>
|
||||
<div class="item-actions">
|
||||
<VButton ref="batchAddButton" @click="showBatchAddPopup()">
|
||||
<VIcon :size="18" icon="mdi-download-multiple" />
|
||||
@ -73,9 +71,7 @@
|
||||
</div>
|
||||
<div class="sub-page-row separator"></div>
|
||||
<div class="sub-page-row">
|
||||
<div class="title-text">
|
||||
已安装的{{ config.title }}:
|
||||
</div>
|
||||
<div class="title-text">已安装的{{ config.title }}:</div>
|
||||
<div class="exclude-built-in">
|
||||
隐藏内置{{ config.title }}
|
||||
<SwitchBox v-model="excludeBuiltIn" />
|
||||
@ -86,10 +82,7 @@
|
||||
</div>
|
||||
<div v-if="loaded" class="manage-item-list">
|
||||
<VEmpty v-if="debouncedList.length === 0" key="empty" />
|
||||
<ManageItem
|
||||
v-for="item of debouncedList"
|
||||
:key="item.name"
|
||||
>
|
||||
<ManageItem v-for="item of debouncedList" :key="item.name">
|
||||
<slot name="item" :item="item">
|
||||
{{ item.displayName }}
|
||||
</slot>
|
||||
@ -103,16 +96,7 @@ import { pickFile } from '@/core/file-picker'
|
||||
import { Toast, ToastType } from '@/core/toast'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { JSZipLibrary } from '@/core/runtime-library'
|
||||
import {
|
||||
VIcon,
|
||||
VButton,
|
||||
TextBox,
|
||||
VEmpty,
|
||||
VLoading,
|
||||
VPopup,
|
||||
TextArea,
|
||||
SwitchBox,
|
||||
} from '@/ui'
|
||||
import { VIcon, VButton, TextBox, VEmpty, VLoading, VPopup, TextArea, SwitchBox } from '@/ui'
|
||||
import ManageItem from './ManageItem.vue'
|
||||
import OnlineRegistryButton from '../online-registry/OnlineRegistryButton.vue'
|
||||
|
||||
@ -148,9 +132,9 @@ export default Vue.extend({
|
||||
},
|
||||
computed: {
|
||||
filteredList() {
|
||||
return this.config.list.filter(it => this.config.listFilter(
|
||||
it, this.search, this.excludeBuiltIn,
|
||||
))
|
||||
return this.config.list.filter(it =>
|
||||
this.config.listFilter(it, this.search, this.excludeBuiltIn),
|
||||
)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -220,27 +204,32 @@ export default Vue.extend({
|
||||
if (!this.batchUrl) {
|
||||
return
|
||||
}
|
||||
const urls = (this.batchUrl as string).split('\n')
|
||||
const urls = (this.batchUrl as string)
|
||||
.split('\n')
|
||||
.map(it => it.trim())
|
||||
.filter(it => it !== '')
|
||||
const toast = Toast.info(`获取中... (0/${urls.length})`, '批量添加')
|
||||
let completed = 0
|
||||
const results = await Promise.allSettled(urls.map(async url => {
|
||||
const { message } = await installFeature(url)
|
||||
completed++
|
||||
toast.message = `获取中... (${completed}/${urls.length})`
|
||||
return message
|
||||
}))
|
||||
const results = await Promise.allSettled(
|
||||
urls.map(async url => {
|
||||
const { message } = await installFeature(url)
|
||||
completed++
|
||||
toast.message = `获取中... (${completed}/${urls.length})`
|
||||
return message
|
||||
}),
|
||||
)
|
||||
const successCount = results.filter(it => it.status === 'fulfilled').length
|
||||
const failCount = results.filter(it => it.status === 'rejected').length
|
||||
toast.message = `安装完成, 成功 ${successCount} 个, 失败 ${failCount} 个.`
|
||||
const resultsText = results.map((r, index) => {
|
||||
const suffix = urls[index]
|
||||
if (r.status === 'fulfilled') {
|
||||
return `${r.value} ${suffix}`
|
||||
}
|
||||
return `${r.reason} ${suffix}`
|
||||
}).join('\n')
|
||||
const resultsText = results
|
||||
.map((r, index) => {
|
||||
const suffix = urls[index]
|
||||
if (r.status === 'fulfilled') {
|
||||
return `${r.value} ${suffix}`
|
||||
}
|
||||
return `${r.reason} ${suffix}`
|
||||
})
|
||||
.join('\n')
|
||||
console.log(resultsText)
|
||||
this.batchUrl = ''
|
||||
},
|
||||
@ -248,7 +237,7 @@ export default Vue.extend({
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.manage-panel {
|
||||
height: calc(var(--panel-height) - 52px - 48px);
|
||||
@ -291,7 +280,7 @@ export default Vue.extend({
|
||||
text-align: center;
|
||||
}
|
||||
.description-text {
|
||||
opacity: .75;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.add-item-row {
|
||||
position: relative;
|
||||
@ -299,7 +288,7 @@ export default Vue.extend({
|
||||
.batch-add-popup {
|
||||
top: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
|
||||
@ -13,23 +13,11 @@
|
||||
class="user-item-remove"
|
||||
@dblclick="removeItem()"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-trash-can-outline"
|
||||
:size="18"
|
||||
/>
|
||||
<div
|
||||
ref="removeConfirmTemplate"
|
||||
class="user-item-remove-confirm"
|
||||
>
|
||||
<VIcon icon="mdi-trash-can-outline" :size="18" />
|
||||
<div ref="removeConfirmTemplate" class="user-item-remove-confirm">
|
||||
确定要卸载 {{ config.item.displayName }} 吗?
|
||||
<VButton
|
||||
type="primary"
|
||||
@click="removeItem()"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-trash-can-outline"
|
||||
:size="16"
|
||||
/>
|
||||
<VButton type="primary" @click="removeItem()">
|
||||
<VIcon icon="mdi-trash-can-outline" :size="16" />
|
||||
确定
|
||||
</VButton>
|
||||
</div>
|
||||
@ -76,7 +64,7 @@ export default Vue.extend({
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
@import 'common';
|
||||
|
||||
.manage-panel .user-item {
|
||||
display: grid;
|
||||
@ -90,13 +78,13 @@ export default Vue.extend({
|
||||
}
|
||||
.user-item-name {
|
||||
grid-area: name;
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
font-size: 11px;
|
||||
}
|
||||
.user-item-line {
|
||||
grid-area: line;
|
||||
justify-self: stretch;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
@ -133,20 +121,20 @@ export default Vue.extend({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.1;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: #E54E4E;
|
||||
color: #e54e4e;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.user-item-remove:not(:hover) {
|
||||
opacity: .75;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.user-item-line {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export interface ManageItem<
|
||||
ItemType extends {
|
||||
displayName: string
|
||||
name: string
|
||||
}
|
||||
},
|
||||
> {
|
||||
/** 唯一名称 */
|
||||
key: string
|
||||
|
||||
@ -65,14 +65,7 @@ import { cdnRoots } from '@/core/cdn-types'
|
||||
import { meta } from '@/core/meta'
|
||||
import { getGeneralSettings } from '@/core/settings'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import {
|
||||
VIcon,
|
||||
VDropdown,
|
||||
TextBox,
|
||||
VPopup,
|
||||
VLoading,
|
||||
VEmpty,
|
||||
} from '@/ui'
|
||||
import { VIcon, VDropdown, TextBox, VPopup, VLoading, VEmpty } from '@/ui'
|
||||
import RegistryItem from './RegistryItem.vue'
|
||||
import { registryBranches } from './third-party'
|
||||
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
<template>
|
||||
<VButton
|
||||
@mouseover="initPopup()"
|
||||
@click="togglePopup()"
|
||||
>
|
||||
<VButton @mouseover="initPopup()" @click="togglePopup()">
|
||||
<slot />
|
||||
</VButton>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
VButton,
|
||||
} from '@/ui'
|
||||
import { VButton } from '@/ui'
|
||||
import { initPopup, togglePopup } from './vm'
|
||||
|
||||
export default Vue.extend({
|
||||
|
||||
@ -86,7 +86,8 @@ const typeMappings = {
|
||||
pack: {
|
||||
icon: 'mdi-package-variant-closed',
|
||||
badge: '合集包',
|
||||
getUrl: (pack: PackItem, branch: string) => pack.items.map(it => getFeatureUrl(it, branch)).join('\n'),
|
||||
getUrl: (pack: PackItem, branch: string) =>
|
||||
pack.items.map(it => getFeatureUrl(it, branch)).join('\n'),
|
||||
isInstalled: (pack: PackItem) => pack.items.every(isFeatureInstalled),
|
||||
},
|
||||
}
|
||||
@ -103,9 +104,7 @@ export default Vue.extend({
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const {
|
||||
icon, badge, getUrl, isInstalled,
|
||||
} = typeMappings[this.item.type]
|
||||
const { icon, badge, getUrl, isInstalled } = typeMappings[this.item.type]
|
||||
return {
|
||||
icon,
|
||||
badge,
|
||||
@ -138,9 +137,7 @@ export default Vue.extend({
|
||||
.filter(it => it !== '')
|
||||
try {
|
||||
this.installing = true
|
||||
await Promise.all(
|
||||
urls.map(async url => installFeature(url)),
|
||||
)
|
||||
await Promise.all(urls.map(async url => installFeature(url)))
|
||||
this.checkInstalled()
|
||||
if (this.item.type === 'pack') {
|
||||
this.$emit('refresh')
|
||||
@ -162,9 +159,9 @@ export default Vue.extend({
|
||||
min-height: 39px;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
content: '';
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-out;
|
||||
transition: opacity 0.2s ease-out;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { registerAndGetData } from '@/plugins/data'
|
||||
|
||||
const defaultRegistryBranches = ['master', 'preview']
|
||||
export const [registryBranches] = registerAndGetData('settingsPanel.registryBranches', [...defaultRegistryBranches])
|
||||
export const [registryBranches] = registerAndGetData('settingsPanel.registryBranches', [
|
||||
...defaultRegistryBranches,
|
||||
])
|
||||
|
||||
@ -12,33 +12,38 @@ export interface SettingsTag extends ComponentTag {
|
||||
export type SettingsTagFunction = (context: SettingsTagFilterContext) => SettingsTag | SettingsTag[]
|
||||
export type TagFilter = SettingsTag | SettingsTagFunction
|
||||
const builtInTagFilters: TagFilter[] = [
|
||||
({ renderedComponents }) => ( // 全部组件
|
||||
{
|
||||
name: 'all',
|
||||
displayName: '全部',
|
||||
color: 'inherit',
|
||||
icon: 'mdi-shape-outline',
|
||||
order: 0,
|
||||
count: renderedComponents.length,
|
||||
filter: c => c,
|
||||
}
|
||||
),
|
||||
({ renderedComponents }) => { // 按组件标签分类
|
||||
(
|
||||
{ renderedComponents }, // 全部组件
|
||||
) => ({
|
||||
name: 'all',
|
||||
displayName: '全部',
|
||||
color: 'inherit',
|
||||
icon: 'mdi-shape-outline',
|
||||
order: 0,
|
||||
count: renderedComponents.length,
|
||||
filter: c => c,
|
||||
}),
|
||||
({ renderedComponents }) => {
|
||||
// 按组件标签分类
|
||||
const tags: SettingsTag[] = []
|
||||
renderedComponents.forEach(it => it.tags.forEach(t => {
|
||||
tags.push({
|
||||
count: 0,
|
||||
...t,
|
||||
filter: components => components.filter(c => {
|
||||
if (t.name === 'all') {
|
||||
return true
|
||||
}
|
||||
return c.tags.some(tag => tag.name === t.name)
|
||||
}),
|
||||
})
|
||||
}))
|
||||
renderedComponents.forEach(it =>
|
||||
it.tags.forEach(t => {
|
||||
tags.push({
|
||||
count: 0,
|
||||
...t,
|
||||
filter: components =>
|
||||
components.filter(c => {
|
||||
if (t.name === 'all') {
|
||||
return true
|
||||
}
|
||||
return c.tags.some(tag => tag.name === t.name)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
const counts = lodash.countBy(tags, (t: ComponentTag) => t.name)
|
||||
return lodash.uniqBy(tags, t => t.name)
|
||||
return lodash
|
||||
.uniqBy(tags, t => t.name)
|
||||
.map(t => ({ ...t, count: counts[t.name] } as SettingsTag))
|
||||
},
|
||||
]
|
||||
|
||||
@ -6,17 +6,14 @@ import { ComponentEntry, ComponentMetadata } from './component'
|
||||
* @param styleImport 动态导入样式的函数
|
||||
* @param entry 组件入口函数
|
||||
*/
|
||||
export const styledComponentEntry = (
|
||||
styleImport: () => Promise<{ default: string }>,
|
||||
entry: ComponentEntry,
|
||||
): ComponentEntry => (
|
||||
export const styledComponentEntry =
|
||||
(styleImport: () => Promise<{ default: string }>, 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`, 展开至组件定义中即可, 也可以提供可选的组件入口函数
|
||||
@ -39,9 +36,7 @@ export const toggleStyle = (
|
||||
}
|
||||
return {
|
||||
name,
|
||||
entry: context => (
|
||||
styleEntry().then(() => entry(context))
|
||||
),
|
||||
entry: context => styleEntry().then(() => entry(context)),
|
||||
reload: styleEntry,
|
||||
unload: () => {
|
||||
styleElement?.remove()
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
TestPattern,
|
||||
Executable,
|
||||
VueModule,
|
||||
I18nDescription,
|
||||
} from '@/core/common-types'
|
||||
import { TestPattern, Executable, VueModule, I18nDescription } from '@/core/common-types'
|
||||
import { ComponentSettings } from '@/core/settings'
|
||||
import { CoreApis } from '@/core/core-apis'
|
||||
import { PluginMinimalData } from '@/plugins/plugin'
|
||||
@ -72,8 +67,10 @@ export interface OptionMetadata<V = unknown> {
|
||||
step?: number
|
||||
}
|
||||
/** `number`, `string`或`Range`类型的选项, 可以添加验证函数来阻止非法输入 */
|
||||
validator?: ComponentOptionValidator<Range<string>> |
|
||||
ComponentOptionValidator<string> | ComponentOptionValidator<number>
|
||||
validator?:
|
||||
| ComponentOptionValidator<Range<string>>
|
||||
| ComponentOptionValidator<string>
|
||||
| ComponentOptionValidator<number>
|
||||
}
|
||||
|
||||
/** 多个选项的信息 */
|
||||
@ -155,9 +152,7 @@ export const componentsTags = {
|
||||
}
|
||||
|
||||
/** 组件入口函数的参数 */
|
||||
export interface ComponentEntryContext<
|
||||
O extends UnknownOptions = UnknownOptions
|
||||
> {
|
||||
export interface ComponentEntryContext<O extends UnknownOptions = UnknownOptions> {
|
||||
/** 当前组件的设置 */
|
||||
settings: ComponentSettings<O>
|
||||
/** 当前组件的信息 */
|
||||
@ -167,17 +162,12 @@ export interface ComponentEntryContext<
|
||||
}
|
||||
|
||||
/** 组件入口函数 */
|
||||
export type ComponentEntry<
|
||||
O extends UnknownOptions = UnknownOptions,
|
||||
T = unknown
|
||||
> = (
|
||||
context: ComponentEntryContext<O>
|
||||
export type ComponentEntry<O extends UnknownOptions = UnknownOptions, T = unknown> = (
|
||||
context: ComponentEntryContext<O>,
|
||||
) => T | Promise<T>
|
||||
|
||||
/** 带有函数/复杂对象的组件信息 */
|
||||
export interface FunctionalMetadata<
|
||||
O extends UnknownOptions = UnknownOptions
|
||||
> {
|
||||
export interface FunctionalMetadata<O extends UnknownOptions = UnknownOptions> {
|
||||
/** 主入口, 重新开启时不会再运行 */
|
||||
entry: ComponentEntry<O>
|
||||
/** 导出小组件 */
|
||||
@ -208,9 +198,9 @@ export interface FunctionalMetadata<
|
||||
}
|
||||
|
||||
/** 组件基本信息 */
|
||||
export interface ComponentMetadata<
|
||||
O extends UnknownOptions = UnknownOptions
|
||||
> extends FeatureBase, FunctionalMetadata<O> {
|
||||
export interface ComponentMetadata<O extends UnknownOptions = UnknownOptions>
|
||||
extends FeatureBase,
|
||||
FunctionalMetadata<O> {
|
||||
/** 组件名称 */
|
||||
name: string
|
||||
/** 显示名称 */
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { componentToSettings } from '@/core/settings'
|
||||
import { isBuiltInComponent } from './built-in-components'
|
||||
import {
|
||||
ComponentMetadata, componentsMap,
|
||||
} from './component'
|
||||
import { ComponentMetadata, componentsMap } from './component'
|
||||
|
||||
/**
|
||||
* 安装自定义组件
|
||||
@ -66,13 +64,19 @@ export const installComponent = async (code: string) => {
|
||||
export const uninstallComponent = async (nameOrDisplayName: string) => {
|
||||
const { settings } = await import('@/core/settings')
|
||||
const { components } = await import('./component')
|
||||
const existingComponent = Object.entries(settings.userComponents)
|
||||
.find(([name, { metadata: { displayName } }]) => {
|
||||
const existingComponent = Object.entries(settings.userComponents).find(
|
||||
([
|
||||
name,
|
||||
{
|
||||
metadata: { displayName },
|
||||
},
|
||||
]) => {
|
||||
if (name === nameOrDisplayName || displayName === nameOrDisplayName) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
},
|
||||
)
|
||||
if (!existingComponent) {
|
||||
throw new Error(`没有找到与名称'${nameOrDisplayName}'相关联的组件`)
|
||||
}
|
||||
@ -104,13 +108,19 @@ export const uninstallComponent = async (nameOrDisplayName: string) => {
|
||||
*/
|
||||
export const toggleComponent = async (nameOrDisplayName: string) => {
|
||||
const { settings } = await import('@/core/settings')
|
||||
const existingComponent = Object.entries(settings.userComponents)
|
||||
.find(([name, { metadata: { displayName } }]) => {
|
||||
const existingComponent = Object.entries(settings.userComponents).find(
|
||||
([
|
||||
name,
|
||||
{
|
||||
metadata: { displayName },
|
||||
},
|
||||
]) => {
|
||||
if (name === nameOrDisplayName || displayName === nameOrDisplayName) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
},
|
||||
)
|
||||
if (!existingComponent) {
|
||||
throw new Error(`没有找到与名称'${nameOrDisplayName}'相关联的组件`)
|
||||
}
|
||||
|
||||
@ -40,14 +40,11 @@ export interface Category {
|
||||
subCategories: Record<string, string> | null
|
||||
}
|
||||
|
||||
export const rawData: [
|
||||
MainCategory[],
|
||||
LiveCategory[],
|
||||
SecondaryCategory[],
|
||||
SecondaryCategory[],
|
||||
] = raw as any
|
||||
export const rawData: [MainCategory[], LiveCategory[], SecondaryCategory[], SecondaryCategory[]] =
|
||||
raw as any
|
||||
|
||||
const urlNormalize = (url: string) => (url.startsWith('//') ? `https:${url}` : url.replace('http:', 'https:'))
|
||||
const urlNormalize = (url: string) =>
|
||||
url.startsWith('//') ? `https:${url}` : url.replace('http:', 'https:')
|
||||
const mainCategories = rawData[0].filter(it => typeof it.tid !== 'string')
|
||||
const secondaryCategories = rawData[3]
|
||||
const generalCategories: Record<string, Category> = {}
|
||||
@ -59,12 +56,16 @@ mainCategories.forEach(it => {
|
||||
route: it.route,
|
||||
code: it.tid,
|
||||
link: mainUrl,
|
||||
subCategories: it.sub ? Object.fromEntries(
|
||||
it.sub.map(sub => {
|
||||
const subUrl = urlNormalize(!sub.route ? sub.url : `https://www.bilibili.com/v/${it.route}/${sub.route}/`)
|
||||
return [sub.name, subUrl]
|
||||
}),
|
||||
) : null,
|
||||
subCategories: it.sub
|
||||
? Object.fromEntries(
|
||||
it.sub.map(sub => {
|
||||
const subUrl = urlNormalize(
|
||||
!sub.route ? sub.url : `https://www.bilibili.com/v/${it.route}/${sub.route}/`,
|
||||
)
|
||||
return [sub.name, subUrl]
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
}
|
||||
})
|
||||
// generalCategories.放映厅 = {
|
||||
@ -82,15 +83,15 @@ secondaryCategories.forEach(it => {
|
||||
code: null,
|
||||
route: it.route,
|
||||
link: urlNormalize(it.url),
|
||||
subCategories: it.sub ? Object.fromEntries(
|
||||
it.sub.map(sub => ([sub.name, urlNormalize(sub.url)])),
|
||||
) : null,
|
||||
subCategories: it.sub
|
||||
? Object.fromEntries(it.sub.map(sub => [sub.name, urlNormalize(sub.url)]))
|
||||
: null,
|
||||
}
|
||||
})
|
||||
export const categories = generalCategories
|
||||
export const categoryCodes = Object.fromEntries(mainCategories.map(it => [it.route, it.tid]))
|
||||
export const categoryLinks = Object.fromEntries(
|
||||
Object.values(generalCategories).map(data => ([data.icon, data.link])),
|
||||
Object.values(generalCategories).map(data => [data.icon, data.link]),
|
||||
)
|
||||
/** 插入主站导航图标 SVG 符号定义 */
|
||||
export const addCategoryIcons = async () => {
|
||||
|
||||
@ -2,7 +2,9 @@ import { getText } from '@/core/ajax'
|
||||
import { DownloadPackage } from '@/core/download'
|
||||
|
||||
export const updateCategories = async () => {
|
||||
const [script] = (dqa('script') as HTMLScriptElement[]).filter(it => it.src.includes('stardust-video'))
|
||||
const [script] = (dqa('script') as HTMLScriptElement[]).filter(it =>
|
||||
it.src.includes('stardust-video'),
|
||||
)
|
||||
if (!script) {
|
||||
throw new Error('no script found')
|
||||
}
|
||||
@ -27,7 +29,9 @@ export const updateIcons = () => {
|
||||
svgItems.forEach(svg => {
|
||||
const symbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol')
|
||||
symbol.innerHTML = svg.innerHTML
|
||||
symbol.id = svg.id.replace(/^channel-icon-/, 'header-icon-') || `header-icon-${(svg.parentElement as HTMLAnchorElement)?.href.match(/\/v\/(.+)$/)?.[1]}`
|
||||
symbol.id =
|
||||
svg.id.replace(/^channel-icon-/, 'header-icon-') ||
|
||||
`header-icon-${(svg.parentElement as HTMLAnchorElement)?.href.match(/\/v\/(.+)$/)?.[1]}`
|
||||
// 特殊: 电视剧的 icon 名称和 route 名称对不上
|
||||
if (symbol.id === 'header-icon-teleplay') {
|
||||
symbol.id = 'header-icon-tv'
|
||||
|
||||
@ -89,12 +89,13 @@ const observeItems = (area: CommentArea) => {
|
||||
area.items = dqa(area.element, '.list-item.reply-wrap').map(parseCommentItem)
|
||||
area.items.forEach(item => {
|
||||
itemAddedCallbacks.forEach(c => c(item))
|
||||
});
|
||||
[area.observer] = childListSubtree(area.element, records => {
|
||||
})
|
||||
;[area.observer] = childListSubtree(area.element, records => {
|
||||
records.forEach(r => {
|
||||
const isCommentItem = (n: Node): n is HTMLElement => n instanceof HTMLElement
|
||||
&& n.classList.contains('list-item')
|
||||
&& n.classList.contains('reply-wrap')
|
||||
const isCommentItem = (n: Node): n is HTMLElement =>
|
||||
n instanceof HTMLElement &&
|
||||
n.classList.contains('list-item') &&
|
||||
n.classList.contains('reply-wrap')
|
||||
r.addedNodes.forEach(n => {
|
||||
if (isCommentItem(n)) {
|
||||
const commentItem = parseCommentItem(n)
|
||||
@ -162,11 +163,14 @@ export const forEachCommentItem = (callbacks: {
|
||||
* @param item 评论
|
||||
* @param config 菜单项配置
|
||||
*/
|
||||
export const addMenuItem = (item: CommentReplyItem, config: {
|
||||
className: string
|
||||
text: string
|
||||
action: (e: MouseEvent) => void
|
||||
}) => {
|
||||
export const addMenuItem = (
|
||||
item: CommentReplyItem,
|
||||
config: {
|
||||
className: string
|
||||
text: string
|
||||
action: (e: MouseEvent) => void
|
||||
},
|
||||
) => {
|
||||
const operationList = dq(item.element, '.opera-list ul') as HTMLUListElement
|
||||
const { className, text, action } = config
|
||||
if (!operationList || dq(operationList, `.${className}`)) {
|
||||
|
||||
@ -3,12 +3,7 @@
|
||||
<div
|
||||
v-for="item of items"
|
||||
:key="item.name"
|
||||
class="
|
||||
be-video-control-bar-extend-item
|
||||
bilibili-player-video-btn
|
||||
squirtle-block-wrap
|
||||
bpx-player-ctrl-btn
|
||||
"
|
||||
class="be-video-control-bar-extend-item bilibili-player-video-btn squirtle-block-wrap bpx-player-ctrl-btn"
|
||||
:style="{ order: item.order.toString() }"
|
||||
:data-name="item.name"
|
||||
@click="item.action($event)"
|
||||
|
||||
@ -6,7 +6,9 @@ const parseHexColor = (hexColor: string) => {
|
||||
const green = hexColor.substring(2, 4)
|
||||
const blue = hexColor.substring(4, 6)
|
||||
return {
|
||||
red, green, blue,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
}
|
||||
}
|
||||
export const convertHexColorForDialogue = (hexColor: string) => {
|
||||
@ -15,7 +17,9 @@ export const convertHexColorForDialogue = (hexColor: string) => {
|
||||
}
|
||||
export const convertHexColorForStyle = (hexColor: string, opacity = 1) => {
|
||||
const { red, green, blue } = parseHexColor(hexColor)
|
||||
const hexOpacity = Math.round(255 * (1 - opacity)).toString(16).padStart(2, '0')
|
||||
const hexOpacity = Math.round(255 * (1 - opacity))
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
return `&H${hexOpacity}${blue}${green}${red}`.toUpperCase()
|
||||
}
|
||||
const round = (number: number) => {
|
||||
@ -35,12 +39,14 @@ const secondsToTime = (seconds: number) => {
|
||||
}
|
||||
return `${hours}:${String(minutes).padStart(2, '0')}:${round(seconds)}`
|
||||
}
|
||||
export const convertTimeByDuration = (startTime: number, duration: number) => (
|
||||
[secondsToTime(startTime), secondsToTime(startTime + duration)]
|
||||
)
|
||||
export const convertTimeByEndTime = (startTime: number, endTime: number) => (
|
||||
[secondsToTime(startTime), secondsToTime(endTime)]
|
||||
)
|
||||
export const convertTimeByDuration = (startTime: number, duration: number) => [
|
||||
secondsToTime(startTime),
|
||||
secondsToTime(startTime + duration),
|
||||
]
|
||||
export const convertTimeByEndTime = (startTime: number, endTime: number) => [
|
||||
secondsToTime(startTime),
|
||||
secondsToTime(endTime),
|
||||
]
|
||||
export const normalizeContent = (content: string) => {
|
||||
const map = {
|
||||
'{': '{',
|
||||
|
||||
@ -3,14 +3,17 @@ import { select } from '@/core/spin-query'
|
||||
import { preventEvent } from '@/core/utils'
|
||||
|
||||
const playerModePolyfill = async () => {
|
||||
const bpxContainer = await select('.bpx-player-container') as HTMLElement
|
||||
const bpxContainer = (await select('.bpx-player-container')) as HTMLElement
|
||||
if (!bpxContainer) {
|
||||
console.warn('[bpx player polyfill] bpxContainer not found')
|
||||
return
|
||||
}
|
||||
attributes(bpxContainer, () => {
|
||||
const dataScreen = bpxContainer.getAttribute('data-screen')
|
||||
document.body.classList.toggle('player-mode-webfullscreen', dataScreen === 'full' || dataScreen === 'web')
|
||||
document.body.classList.toggle(
|
||||
'player-mode-webfullscreen',
|
||||
dataScreen === 'full' || dataScreen === 'web',
|
||||
)
|
||||
dataScreen === 'wide' ? document.body.classList.add('player-mode-widescreen') : ''
|
||||
})
|
||||
}
|
||||
@ -49,7 +52,7 @@ const idPolyfill = async () => {
|
||||
* 如果以后视频区兼容了弹幕点赞层, 需要将双击全屏组件更换为阻止双击全屏, 并取消对此函数的使用.
|
||||
*/
|
||||
const doubleClickPolyfill = async () => {
|
||||
const layer = await select('.bpx-player-video-perch') as HTMLElement
|
||||
const layer = (await select('.bpx-player-video-perch')) as HTMLElement
|
||||
if (!layer) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -2,6 +2,6 @@ import { bpxPlayerPolyfill } from './bpx'
|
||||
import { v2PlayerPolyfill } from './v2'
|
||||
import { v3PlayerPolyfill } from './v3'
|
||||
|
||||
export const playerPolyfill = lodash.once(
|
||||
() => Promise.allSettled([bpxPlayerPolyfill(), v2PlayerPolyfill(), v3PlayerPolyfill()]),
|
||||
export const playerPolyfill = lodash.once(() =>
|
||||
Promise.allSettled([bpxPlayerPolyfill(), v2PlayerPolyfill(), v3PlayerPolyfill()]),
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@ import { select } from '@/core/spin-query'
|
||||
|
||||
const idPolyfill = async () => {
|
||||
const player = await select(() => unsafeWindow.player)
|
||||
if (!(player?.getVideoMessage)) {
|
||||
if (!player?.getVideoMessage) {
|
||||
return
|
||||
}
|
||||
const { useScopedConsole } = await import('@/core/utils/log')
|
||||
|
||||
@ -3,7 +3,7 @@ import { select } from '@/core/spin-query'
|
||||
|
||||
const idPolyfill = async () => {
|
||||
const player = await select(() => unsafeWindow.player)
|
||||
if (!(player?.getUserParams)) {
|
||||
if (!player?.getUserParams) {
|
||||
return
|
||||
}
|
||||
const { useScopedConsole } = await import('@/core/utils/log')
|
||||
|
||||
@ -63,7 +63,7 @@ interface PlayerQuery<QueryResult> extends CustomNestedQuery<QueryResult> {
|
||||
fullscreen: QueryResult
|
||||
}
|
||||
settings: {
|
||||
wrap: QueryResult,
|
||||
wrap: QueryResult
|
||||
lightOff: QueryResult
|
||||
}
|
||||
}
|
||||
@ -101,7 +101,7 @@ export abstract class PlayerAgent {
|
||||
return {
|
||||
...this,
|
||||
custom,
|
||||
} as (this & { custom: { [key in keyof CustomQueryType]: ElementQuery } })
|
||||
} as this & { custom: { [key in keyof CustomQueryType]: ElementQuery } }
|
||||
}
|
||||
widescreen() {
|
||||
return click(this.query.control.buttons.widescreen)
|
||||
@ -133,7 +133,7 @@ export abstract class PlayerAgent {
|
||||
|
||||
/** true 开灯,false 关灯 */
|
||||
async toggleLight(on: boolean) {
|
||||
const checkbox = await this.query.control.settings.lightOff() as HTMLInputElement
|
||||
const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement
|
||||
// 关灯状态 && 要开灯 -> 开灯
|
||||
checkbox.checked && on && checkbox.click()
|
||||
// 开灯状态 && 要关灯 -> 关灯
|
||||
@ -142,11 +142,7 @@ export abstract class PlayerAgent {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getPlayerConfig(target: string) {
|
||||
return lodash.get(
|
||||
JSON.parse(localStorage.getItem('bilibili_player_settings')),
|
||||
target,
|
||||
false,
|
||||
)
|
||||
return lodash.get(JSON.parse(localStorage.getItem('bilibili_player_settings')), target, false)
|
||||
}
|
||||
|
||||
isAutoPlay() {
|
||||
@ -211,7 +207,8 @@ export class VideoPlayerV2Agent extends PlayerAgent {
|
||||
},
|
||||
settings: {
|
||||
wrap: '.bilibili-player-video-btn-setting-wrap',
|
||||
lightOff: '.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input',
|
||||
lightOff:
|
||||
'.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input',
|
||||
},
|
||||
},
|
||||
toastWrap: '.bilibili-player-video-toast-wrp',
|
||||
@ -275,7 +272,9 @@ export class VideoPlayerV2Agent extends PlayerAgent {
|
||||
this.nativeApi.play()
|
||||
setTimeout(() => {
|
||||
this.nativeApi.seek(time)
|
||||
const toastText = dq('.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)')
|
||||
const toastText = dq(
|
||||
'.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)',
|
||||
)
|
||||
if (toastText) {
|
||||
toastText.textContent = ' 00:00'
|
||||
}
|
||||
@ -430,7 +429,8 @@ export class VideoPlayerMixedAgent extends VideoPlayerV2Agent {
|
||||
pageList: '.bilibili-player-video-btn-pagelist, .bpx-player-ctrl-eplist',
|
||||
speed: '.bilibili-player-video-btn-speed, .bpx-player-ctrl-playbackrate',
|
||||
subtitle: '.bilibili-player-video-btn-subtitle, .bpx-player-ctrl-subtitle',
|
||||
volume: '.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume, .bpx-player-ctrl-volume .bpx-player-ctrl-volume-icon',
|
||||
volume:
|
||||
'.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume, .bpx-player-ctrl-volume .bpx-player-ctrl-volume-icon',
|
||||
settings: '.bilibili-player-video-btn-setting, .bpx-player-ctrl-setting',
|
||||
pip: '.bilibili-player-video-btn-pip, .bpx-player-ctrl-pip',
|
||||
widescreen: '.bilibili-player-video-btn-widescreen, .bpx-player-ctrl-wide',
|
||||
@ -439,7 +439,8 @@ export class VideoPlayerMixedAgent extends VideoPlayerV2Agent {
|
||||
},
|
||||
settings: {
|
||||
wrap: '.bilibili-player-video-btn-setting-wrap, .bpx-player-ctrl-setting-box',
|
||||
lightOff: '.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input, .bpx-player-ctrl-setting-lightoff .bui-checkbox-input',
|
||||
lightOff:
|
||||
'.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input, .bpx-player-ctrl-setting-lightoff .bui-checkbox-input',
|
||||
},
|
||||
},
|
||||
toastWrap: '.bilibili-player-video-toast-wrp, .bpx-player-dialog-wrap',
|
||||
@ -460,7 +461,9 @@ export class VideoPlayerMixedAgent extends VideoPlayerV2Agent {
|
||||
this.nativeApi.play()
|
||||
setTimeout(() => {
|
||||
this.nativeApi.seek(time)
|
||||
const toastText = dq('.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)')
|
||||
const toastText = dq(
|
||||
'.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)',
|
||||
)
|
||||
if (toastText) {
|
||||
toastText.textContent = ' 00:00'
|
||||
}
|
||||
|
||||
@ -18,10 +18,7 @@ const setLight = (on: boolean) => {
|
||||
} = playerAgentInstance
|
||||
|
||||
// if (!initialized) {
|
||||
loadLazyPlayerSettingsPanel(
|
||||
buttons.settings.selector,
|
||||
settings.wrap.selector,
|
||||
)
|
||||
loadLazyPlayerSettingsPanel(buttons.settings.selector, settings.wrap.selector)
|
||||
// initialized = true
|
||||
// }
|
||||
playerAgentInstance.toggleLight(on)
|
||||
|
||||
@ -14,14 +14,14 @@ export interface PlayerContextMenu {
|
||||
}
|
||||
/** 在播放器菜单打开/关闭时执行特定操作 */
|
||||
export const forEachContextMenu = async (action: {
|
||||
open?: (menu: PlayerContextMenu) => void,
|
||||
close?: (menu: PlayerContextMenu) => void,
|
||||
open?: (menu: PlayerContextMenu) => void
|
||||
close?: (menu: PlayerContextMenu) => void
|
||||
}) => {
|
||||
const { open, close } = action
|
||||
if (!hasVideo()) {
|
||||
return
|
||||
}
|
||||
const player = await select('.bilibili-player') as HTMLElement
|
||||
const player = (await select('.bilibili-player')) as HTMLElement
|
||||
if (!player) {
|
||||
return
|
||||
}
|
||||
@ -41,9 +41,11 @@ export const forEachContextMenu = async (action: {
|
||||
},
|
||||
}
|
||||
childList(ul, () => {
|
||||
if (menu.isOpen) { // 打开菜单
|
||||
if (menu.isOpen) {
|
||||
// 打开菜单
|
||||
open?.(menu)
|
||||
} else { // 关闭菜单
|
||||
} else {
|
||||
// 关闭菜单
|
||||
close?.(menu)
|
||||
}
|
||||
})
|
||||
@ -54,25 +56,26 @@ export const forEachContextMenu = async (action: {
|
||||
export const addMenuItem = async (
|
||||
element: HTMLElement,
|
||||
cleanUp?: (menu: PlayerContextMenu) => void,
|
||||
) => forEachContextMenu({
|
||||
open: menu => {
|
||||
if (menu.listElement.contains(element)) {
|
||||
return
|
||||
}
|
||||
// <li class="context-line context-menu-function" data-append="1">
|
||||
// <a class="context-menu-a js-action" href="javascript:void(0);"></a>
|
||||
// </li>
|
||||
const menuItem = document.createElement('li')
|
||||
menuItem.classList.add('context-line', 'context-menu-function')
|
||||
menuItem.setAttribute('data-append', '1')
|
||||
const linkWrapper = document.createElement('a')
|
||||
linkWrapper.classList.add('context-menu-a', 'js-action')
|
||||
linkWrapper.href = 'javascript:void(0);'
|
||||
linkWrapper.appendChild(element)
|
||||
menuItem.addEventListener('mouseover', () => menuItem.classList.add('hover'))
|
||||
menuItem.addEventListener('mouseout', () => menuItem.classList.remove('hover'))
|
||||
menuItem.appendChild(linkWrapper)
|
||||
menu.listElement.appendChild(menuItem)
|
||||
},
|
||||
close: menu => cleanUp?.(menu),
|
||||
})
|
||||
) =>
|
||||
forEachContextMenu({
|
||||
open: menu => {
|
||||
if (menu.listElement.contains(element)) {
|
||||
return
|
||||
}
|
||||
// <li class="context-line context-menu-function" data-append="1">
|
||||
// <a class="context-menu-a js-action" href="javascript:void(0);"></a>
|
||||
// </li>
|
||||
const menuItem = document.createElement('li')
|
||||
menuItem.classList.add('context-line', 'context-menu-function')
|
||||
menuItem.setAttribute('data-append', '1')
|
||||
const linkWrapper = document.createElement('a')
|
||||
linkWrapper.classList.add('context-menu-a', 'js-action')
|
||||
linkWrapper.href = 'javascript:void(0);'
|
||||
linkWrapper.appendChild(element)
|
||||
menuItem.addEventListener('mouseover', () => menuItem.classList.add('hover'))
|
||||
menuItem.addEventListener('mouseout', () => menuItem.classList.remove('hover'))
|
||||
menuItem.appendChild(linkWrapper)
|
||||
menu.listElement.appendChild(menuItem)
|
||||
},
|
||||
close: menu => cleanUp?.(menu),
|
||||
})
|
||||
|
||||
@ -22,7 +22,7 @@ const startRecording = (container: HTMLElement, callback: DanmakuRecordCallback)
|
||||
if (danmakuContainerObserver) {
|
||||
danmakuContainerObserver.disconnect()
|
||||
}
|
||||
[danmakuContainerObserver] = childListSubtree(container, records => {
|
||||
;[danmakuContainerObserver] = childListSubtree(container, records => {
|
||||
records.forEach(record => {
|
||||
record.addedNodes.forEach(node => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
|
||||
@ -130,7 +130,9 @@ export const toggleWatchlater = async (aid: string | number, add?: boolean | und
|
||||
if (add === undefined) {
|
||||
add = !watchlaterList.includes(id)
|
||||
}
|
||||
const api = add ? 'https://api.bilibili.com/x/v2/history/toview/add' : 'https://api.bilibili.com/x/v2/history/toview/del'
|
||||
const api = add
|
||||
? 'https://api.bilibili.com/x/v2/history/toview/add'
|
||||
: 'https://api.bilibili.com/x/v2/history/toview/del'
|
||||
const { getCsrf } = await import('@/core/utils')
|
||||
const csrf = getCsrf()
|
||||
const { postTextWithCredentials } = await import('@/core/ajax')
|
||||
|
||||
135
src/core/ajax.ts
135
src/core/ajax.ts
@ -1,7 +1,7 @@
|
||||
import { logError } from './utils/log'
|
||||
|
||||
type XhrBody = Document | XMLHttpRequestBodyInit
|
||||
type XhrConfig = (xhr: XMLHttpRequest) => { isText?: boolean, body?: XhrBody }
|
||||
type XhrConfig = (xhr: XMLHttpRequest) => { isText?: boolean; body?: XhrBody }
|
||||
const send = <T = any>(config: XhrConfig) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const { isText = true, body } = config(xhr)
|
||||
@ -17,13 +17,15 @@ const withCredentials = (config: XhrConfig) => (xhr: XMLHttpRequest) => {
|
||||
}
|
||||
|
||||
// GET
|
||||
const blobRequest = (url: string): XhrConfig => (xhr: XMLHttpRequest) => {
|
||||
xhr.responseType = 'blob'
|
||||
xhr.open('GET', url)
|
||||
return {
|
||||
isText: false,
|
||||
const blobRequest =
|
||||
(url: string): XhrConfig =>
|
||||
(xhr: XMLHttpRequest) => {
|
||||
xhr.responseType = 'blob'
|
||||
xhr.open('GET', url)
|
||||
return {
|
||||
isText: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取二进制`Blob`对象
|
||||
* @param url 链接
|
||||
@ -33,17 +35,17 @@ export const getBlob = (url: string) => send<Blob>(blobRequest(url))
|
||||
* 获取二进制`Blob`对象(带身份验证)
|
||||
* @param url 链接
|
||||
*/
|
||||
export const getBlobWithCredentials = (url: string) => send<Blob>(
|
||||
withCredentials(blobRequest(url)),
|
||||
)
|
||||
export const getBlobWithCredentials = (url: string) => send<Blob>(withCredentials(blobRequest(url)))
|
||||
|
||||
const textRequest = (url: string): XhrConfig => (xhr: XMLHttpRequest) => {
|
||||
xhr.responseType = 'text'
|
||||
xhr.open('GET', url)
|
||||
return {
|
||||
isText: true,
|
||||
const textRequest =
|
||||
(url: string): XhrConfig =>
|
||||
(xhr: XMLHttpRequest) => {
|
||||
xhr.responseType = 'text'
|
||||
xhr.open('GET', url)
|
||||
return {
|
||||
isText: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取文本
|
||||
* @param url 链接
|
||||
@ -54,17 +56,18 @@ export const getText = (url: string) => send<string>(textRequest(url))
|
||||
* 获取文本(带身份验证)
|
||||
* @param url 链接
|
||||
*/
|
||||
export const getTextWithCredentials = (url: string) => send<string>(
|
||||
withCredentials(textRequest(url)),
|
||||
)
|
||||
export const getTextWithCredentials = (url: string) =>
|
||||
send<string>(withCredentials(textRequest(url)))
|
||||
|
||||
const jsonRequest = (url: string): XhrConfig => (xhr: XMLHttpRequest) => {
|
||||
xhr.responseType = 'json'
|
||||
xhr.open('GET', url)
|
||||
return {
|
||||
isText: false,
|
||||
const jsonRequest =
|
||||
(url: string): XhrConfig =>
|
||||
(xhr: XMLHttpRequest) => {
|
||||
xhr.responseType = 'json'
|
||||
xhr.open('GET', url)
|
||||
return {
|
||||
isText: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
const convertToJson = <T = any>(response: any) => {
|
||||
if (typeof response === 'string') {
|
||||
return JSON.parse(response) as T
|
||||
@ -94,61 +97,65 @@ export const getJsonWithCredentials = async <T = any>(url: string) => {
|
||||
* @param url 链接
|
||||
* @param text 文本
|
||||
*/
|
||||
export const postText = (url: string, text: XhrBody) => send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
return {
|
||||
isText: false,
|
||||
body: text,
|
||||
}
|
||||
})
|
||||
export const postText = (url: string, text: XhrBody) =>
|
||||
send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
return {
|
||||
isText: false,
|
||||
body: text,
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 发送文本 (`application/x-www-form-urlencoded`)(带身份验证)
|
||||
* @param url 链接
|
||||
* @param text 文本
|
||||
*/
|
||||
export const postTextWithCredentials = (url: string, text: XhrBody) => send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.withCredentials = true
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
return {
|
||||
isText: false,
|
||||
body: text,
|
||||
}
|
||||
})
|
||||
export const postTextWithCredentials = (url: string, text: XhrBody) =>
|
||||
send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.withCredentials = true
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
return {
|
||||
isText: false,
|
||||
body: text,
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 发送 JSON 数据 (`application/json`)
|
||||
* @param url 链接
|
||||
* @param json JSON 对象
|
||||
*/
|
||||
export const postJson = (url: string, json: any) => send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
return {
|
||||
isText: false,
|
||||
body: JSON.stringify(json),
|
||||
}
|
||||
})
|
||||
export const postJson = (url: string, json: any) =>
|
||||
send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
return {
|
||||
isText: false,
|
||||
body: JSON.stringify(json),
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 发送 JSON 数据 (`application/json`)(带身份验证)
|
||||
* @param url 链接
|
||||
* @param json JSON 对象
|
||||
*/
|
||||
export const postJsonWithCredentials = (url: string, json: any) => send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.withCredentials = true
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
return {
|
||||
isText: false,
|
||||
body: JSON.stringify(json),
|
||||
}
|
||||
})
|
||||
export const postJsonWithCredentials = (url: string, json: any) =>
|
||||
send<string>(xhr => {
|
||||
xhr.open('POST', url)
|
||||
xhr.withCredentials = true
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
return {
|
||||
isText: false,
|
||||
body: JSON.stringify(json),
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 调用 Tampermonkey API 进行请求 (`GM_xmlhttpRequest`)
|
||||
* @param details 参数
|
||||
*/
|
||||
export const monkey = <T = any>(details: MonkeyXhrBasicDetails) => (
|
||||
export const monkey = <T = any>(details: MonkeyXhrBasicDetails) =>
|
||||
new Promise<T>((resolve, reject) => {
|
||||
const fullDetails: MonkeyXhrDetails = {
|
||||
nocache: true,
|
||||
@ -170,7 +177,6 @@ export const monkey = <T = any>(details: MonkeyXhrBasicDetails) => (
|
||||
}
|
||||
GM_xmlhttpRequest(fullDetails)
|
||||
})
|
||||
)
|
||||
/**
|
||||
* 获取全部的分页数据, 返回一个会随翻页变化的数组, 可用于响应式数据
|
||||
* @param config 配置参数
|
||||
@ -186,7 +192,7 @@ export const responsiveGetPages = <T = any>(config: {
|
||||
let responsivePromise: Promise<T[]>
|
||||
const totalPromise = new Promise<T[]>(resolveTotal => {
|
||||
responsivePromise = new Promise(resolveResponsive => {
|
||||
(async () => {
|
||||
;(async () => {
|
||||
const { api, getList, getTotal } = config
|
||||
let page = 1
|
||||
let total = Infinity
|
||||
@ -194,7 +200,10 @@ export const responsiveGetPages = <T = any>(config: {
|
||||
while (result.length < total) {
|
||||
const json = await api(page)
|
||||
if (json.code !== 0) {
|
||||
console.warn(`api failed in ajax.getPages. message = ${json.message}, page = ${page}, total = ${total}, api = `, api)
|
||||
console.warn(
|
||||
`api failed in ajax.getPages. message = ${json.message}, page = ${page}, total = ${total}, api = `,
|
||||
api,
|
||||
)
|
||||
}
|
||||
const list = getList(json)
|
||||
result.push(...list)
|
||||
|
||||
@ -12,7 +12,9 @@ const defaultOwner = 'the1812'
|
||||
/** 根据分支名和仓库 owner 检索 CDN 链接 */
|
||||
export const cdnRoots: Record<CdnTypes, (branch: string, owner?: string) => string> = {
|
||||
/** @deprecated */
|
||||
jsDelivr: (branch, owner) => `https://cdn.jsdelivr.net/gh/${owner || defaultOwner}/Bilibili-Evolved@${branch}/`,
|
||||
jsDelivr: (branch, owner) =>
|
||||
`https://cdn.jsdelivr.net/gh/${owner || defaultOwner}/Bilibili-Evolved@${branch}/`,
|
||||
AltCdn: (branch, owner) => meta.compilationInfo.altCdn.root(branch, owner),
|
||||
GitHub: (branch, owner) => `https://raw.githubusercontent.com/${owner || defaultOwner}/Bilibili-Evolved/${branch}/`,
|
||||
GitHub: (branch, owner) =>
|
||||
`https://raw.githubusercontent.com/${owner || defaultOwner}/Bilibili-Evolved/${branch}/`,
|
||||
}
|
||||
|
||||
@ -8,13 +8,15 @@ export type ExecutableWithParameter<Parameters extends any[] = never[], ReturnTy
|
||||
export type TestPattern = (string | RegExp)[]
|
||||
export type ArrayContent<T> = T extends Array<infer R> ? R : T
|
||||
export type VueModule =
|
||||
Component
|
||||
| Component
|
||||
| { default: Component }
|
||||
| VueConstructor
|
||||
| { default: VueConstructor }
|
||||
|
||||
type DescriptionInput = string | Executable<string>
|
||||
export type I18nDescription = DescriptionInput | { 'zh-CN': DescriptionInput; [key: string]: DescriptionInput }
|
||||
export type I18nDescription =
|
||||
| DescriptionInput
|
||||
| { 'zh-CN': DescriptionInput; [key: string]: DescriptionInput }
|
||||
export type WithName = {
|
||||
name: string
|
||||
displayName: string
|
||||
|
||||
@ -115,7 +115,7 @@ export const externalApis = {
|
||||
...componentApis.userComponent,
|
||||
...componentApis.styledComponent,
|
||||
...componentApis.launchBar,
|
||||
...(lodash.omit(componentApis, 'component', 'userComponent', 'styledComponent', 'launchBar')),
|
||||
...lodash.omit(componentApis, 'component', 'userComponent', 'styledComponent', 'launchBar'),
|
||||
},
|
||||
pluginApis: {
|
||||
...pluginApis.style,
|
||||
|
||||
@ -61,9 +61,8 @@ export class DownloadPackage {
|
||||
if (!filename || this.entries.length === 1) {
|
||||
filename = this.entries[0].name
|
||||
}
|
||||
const isIndividualMode = (
|
||||
const isIndividualMode =
|
||||
getGeneralSettings().downloadPackageEmitMode === DownloadPackageEmitMode.Individual
|
||||
)
|
||||
if (isIndividualMode && this.entries.length > 1) {
|
||||
await Promise.all(this.entries.map(e => DownloadPackage.single(e.name, e.data, e.options)))
|
||||
return
|
||||
@ -88,9 +87,13 @@ export class DownloadPackage {
|
||||
console.log(finalFilename)
|
||||
document.body.appendChild(a)
|
||||
// 阻止 spm id 的事件 (#2247)
|
||||
a.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
}, { capture: true })
|
||||
a.addEventListener(
|
||||
'click',
|
||||
e => {
|
||||
e.stopPropagation()
|
||||
},
|
||||
{ capture: true },
|
||||
)
|
||||
a.click()
|
||||
a.remove()
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import {
|
||||
loadFeatureCode, LoadFeatureCodeResult,
|
||||
} from '@/core/external-input/load-feature-code'
|
||||
import { loadFeatureCode, LoadFeatureCodeResult } from '@/core/external-input/load-feature-code'
|
||||
|
||||
type LdRes<X> = LoadFeatureCodeResult<X>
|
||||
type SettledRes<T> = PromiseSettledResult<T>
|
||||
@ -8,13 +6,9 @@ type FilledRes<T> = PromiseFulfilledResult<T>
|
||||
|
||||
const unwrapSettledResult = <T>(r: SettledRes<T>): T => (r as FilledRes<T>).value
|
||||
|
||||
const mapSettledArray = <T>(arr: SettledRes<T>[]): T[] => (
|
||||
arr.map(unwrapSettledResult)
|
||||
)
|
||||
const mapSettledArray = <T>(arr: SettledRes<T>[]): T[] => arr.map(unwrapSettledResult)
|
||||
|
||||
const mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => (
|
||||
p.then(mapSettledArray)
|
||||
)
|
||||
const mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => p.then(mapSettledArray)
|
||||
|
||||
/**
|
||||
* 批量加载组件或插件的代码字符串,获取其导出 feature
|
||||
@ -24,7 +18,8 @@ const mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => (
|
||||
*/
|
||||
export const loadFeatureCodeAllSettled = <X>(
|
||||
codes: string[],
|
||||
): Promise<LoadFeatureCodeResult<X>[]> => lodash(codes)
|
||||
): Promise<LoadFeatureCodeResult<X>[]> =>
|
||||
lodash(codes)
|
||||
.map<Promise<LdRes<X>>>(loadFeatureCode)
|
||||
.thru<Promise<SettledRes<LdRes<X>>[]>>(arr => Promise.allSettled(arr))
|
||||
.thru(mapSettleResult)
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
import {
|
||||
loadFeatureCode, LoadFeatureCodeResult, LoadFeatureCodeResultError,
|
||||
loadFeatureCode,
|
||||
LoadFeatureCodeResult,
|
||||
LoadFeatureCodeResultError,
|
||||
LoadFeatureCodeResultOk,
|
||||
} from '@/core/external-input/load-feature-code'
|
||||
import { FeatureBase } from '@/components/types'
|
||||
|
||||
interface ResultInstance {
|
||||
readonly isOk: <X extends FeatureBase>(
|
||||
this: LoadFeatureCodeAllResult<X>
|
||||
this: LoadFeatureCodeAllResult<X>,
|
||||
) => this is LoadFeatureCodeAllResultOk<X>
|
||||
|
||||
readonly isError: (
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeAllResultError
|
||||
|
||||
readonly isNoExport: (
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeAllResultNoExport
|
||||
|
||||
readonly isCodeThrew: (
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeAllResultCodeThrew
|
||||
}
|
||||
|
||||
@ -29,13 +31,13 @@ interface ResultInstance {
|
||||
* @property features 从代码中获取的导出值
|
||||
*/
|
||||
interface LoadFeatureCodeAllResultOk<X extends FeatureBase> extends ResultInstance {
|
||||
readonly tag: 'Ok',
|
||||
readonly features: X[],
|
||||
readonly tag: 'Ok'
|
||||
readonly features: X[]
|
||||
}
|
||||
|
||||
/** 代码没有导出任何值 */
|
||||
interface LoadFeatureCodeAllResultNoExport extends ResultInstance {
|
||||
readonly tag: 'NoExport',
|
||||
readonly tag: 'NoExport'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,45 +47,47 @@ interface LoadFeatureCodeAllResultNoExport extends ResultInstance {
|
||||
* @property thrown 抛出的值
|
||||
*/
|
||||
interface LoadFeatureCodeAllResultCodeThrew extends ResultInstance {
|
||||
readonly tag: 'CodeThrew',
|
||||
readonly thrown: unknown,
|
||||
readonly tag: 'CodeThrew'
|
||||
readonly thrown: unknown
|
||||
}
|
||||
|
||||
type LoadFeatureCodeAllResultError =
|
||||
LoadFeatureCodeAllResultNoExport
|
||||
| LoadFeatureCodeAllResultNoExport
|
||||
| LoadFeatureCodeAllResultCodeThrew
|
||||
type LoadFeatureCodeAllResult<X extends FeatureBase> =
|
||||
LoadFeatureCodeAllResultOk<X>
|
||||
| LoadFeatureCodeAllResultOk<X>
|
||||
| LoadFeatureCodeAllResultError
|
||||
|
||||
const resultProto: ResultInstance = {
|
||||
isOk() { return this.tag === 'Ok' },
|
||||
isError() { return this.tag !== 'Ok' },
|
||||
isNoExport() { return this.tag === 'NoExport' },
|
||||
isCodeThrew() { return this.tag === 'CodeThrew' },
|
||||
isOk() {
|
||||
return this.tag === 'Ok'
|
||||
},
|
||||
isError() {
|
||||
return this.tag !== 'Ok'
|
||||
},
|
||||
isNoExport() {
|
||||
return this.tag === 'NoExport'
|
||||
},
|
||||
isCodeThrew() {
|
||||
return this.tag === 'CodeThrew'
|
||||
},
|
||||
}
|
||||
|
||||
const okResult = <X extends FeatureBase>(
|
||||
features: X[],
|
||||
): LoadFeatureCodeAllResultOk<X> => lodash.create(
|
||||
resultProto,
|
||||
{
|
||||
tag: 'Ok' as const,
|
||||
features,
|
||||
},
|
||||
)
|
||||
const okResult = <X extends FeatureBase>(features: X[]): LoadFeatureCodeAllResultOk<X> =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'Ok' as const,
|
||||
features,
|
||||
})
|
||||
|
||||
const noExportResult = lodash.create(resultProto, {
|
||||
tag: 'NoExport' as const,
|
||||
})
|
||||
|
||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeAllResultCodeThrew => lodash.create(
|
||||
resultProto,
|
||||
{
|
||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeAllResultCodeThrew =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'CodeThrew' as const,
|
||||
thrown,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Task<Ok, Err = never> = Promise<Ok>
|
||||
@ -99,45 +103,37 @@ type LdAllErr = LoadFeatureCodeAllResultError
|
||||
type LoadCodesTask<X> = Task<LdOk<X>[], [number, LdErr]>
|
||||
|
||||
// covert `Task<LdRes<X>>` to `Task<LdOk<X>, LdErr>`
|
||||
const rejectErrorResult = <X>(t: Task<LdRes<X>>): Task<LdOk<X>, LdErr> => (
|
||||
const rejectErrorResult = <X>(t: Task<LdRes<X>>): Task<LdOk<X>, LdErr> =>
|
||||
t.then(r => (r.isOk() ? r : Promise.reject(r)))
|
||||
)
|
||||
|
||||
// load feature code, and return `Task<LdOk<X>, LdErr>`
|
||||
const loadCode = <X>(code: string): Task<LdOk<X>, LdErr> => (
|
||||
rejectErrorResult(loadFeatureCode(code))
|
||||
)
|
||||
const loadCode = <X>(code: string): Task<LdOk<X>, LdErr> => rejectErrorResult(loadFeatureCode(code))
|
||||
|
||||
// covert `Task`'s `Err` type from `T` to `[number, T]`
|
||||
const addIndexToRejected = <N extends number, O, E>(
|
||||
t: Task<O, E>,
|
||||
i: N,
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
): Task<O, [N, E]> => t.catch(e => Promise.reject([i, e]))
|
||||
|
||||
// create `LdAllOk` from an array of `LdOk`
|
||||
const createOkResult = <X>(arr: LdOk<X>[]): LdAllOk<X> => (
|
||||
okResult(arr.map(r => r.feature))
|
||||
)
|
||||
const createOkResult = <X>(arr: LdOk<X>[]): LdAllOk<X> => okResult(arr.map(r => r.feature))
|
||||
|
||||
// create `LdAllErr` from `[number, LdErr]`
|
||||
const createErrResult = (t: [number, LdErr]): LdAllErr => (
|
||||
t[1].isCodeThrew()
|
||||
? codeThrewResult(t[1].thrown)
|
||||
: noExportResult
|
||||
)
|
||||
const createErrResult = (t: [number, LdErr]): LdAllErr =>
|
||||
t[1].isCodeThrew() ? codeThrewResult(t[1].thrown) : noExportResult
|
||||
|
||||
// load all feature codes, and return `LoadCodesTask`
|
||||
const loadCodes = <X>(codes: string[]): LoadCodesTask<X> => lodash(codes)
|
||||
.map<Task<LdOk<X>, LdErr>>(loadCode)
|
||||
.map(addIndexToRejected)
|
||||
.thru<LoadCodesTask<X>>(arr => Promise.all(arr))
|
||||
.value()
|
||||
const loadCodes = <X>(codes: string[]): LoadCodesTask<X> =>
|
||||
lodash(codes)
|
||||
.map<Task<LdOk<X>, LdErr>>(loadCode)
|
||||
.map(addIndexToRejected)
|
||||
.thru<LoadCodesTask<X>>(arr => Promise.all(arr))
|
||||
.value()
|
||||
|
||||
// create a `LdAllRes` wrapped by `Task`
|
||||
const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> => t
|
||||
.then(createOkResult)
|
||||
.catch(createErrResult)
|
||||
const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> =>
|
||||
t.then(createOkResult).catch(createErrResult)
|
||||
|
||||
/**
|
||||
* 批量加载组件或插件的代码字符串,获取其导出 feature
|
||||
@ -147,12 +143,8 @@ const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> => t
|
||||
* @param codes 代码字符串数组
|
||||
* @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult`
|
||||
*/
|
||||
const loadFeatureCodeAll = <X>(codes: string[]): Promise<LoadFeatureCodeAllResult<X>> => (
|
||||
lodash(codes)
|
||||
.thru<LoadCodesTask<X>>(loadCodes)
|
||||
.thru(createTaskResult)
|
||||
.value()
|
||||
)
|
||||
const loadFeatureCodeAll = <X>(codes: string[]): Promise<LoadFeatureCodeAllResult<X>> =>
|
||||
lodash(codes).thru<LoadCodesTask<X>>(loadCodes).thru(createTaskResult).value()
|
||||
|
||||
export {
|
||||
loadFeatureCodeAll,
|
||||
|
||||
@ -2,19 +2,17 @@ import { FeatureBase } from '@/components/types'
|
||||
|
||||
interface ResultInstance {
|
||||
readonly isOk: <X extends FeatureBase>(
|
||||
this: LoadFeatureCodeResult<X>
|
||||
this: LoadFeatureCodeResult<X>,
|
||||
) => this is LoadFeatureCodeResultOk<X>
|
||||
|
||||
readonly isError: (
|
||||
this: LoadFeatureCodeResult<FeatureBase>
|
||||
) => this is LoadFeatureCodeResultError
|
||||
readonly isError: (this: LoadFeatureCodeResult<FeatureBase>) => this is LoadFeatureCodeResultError
|
||||
|
||||
readonly isNoExport: (
|
||||
this: LoadFeatureCodeResult<FeatureBase>
|
||||
this: LoadFeatureCodeResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeResultNoExport
|
||||
|
||||
readonly isCodeThrew: (
|
||||
this: LoadFeatureCodeResult<FeatureBase>
|
||||
this: LoadFeatureCodeResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeResultCodeThrew
|
||||
}
|
||||
|
||||
@ -25,13 +23,13 @@ interface ResultInstance {
|
||||
* @property feature 从代码中获取的导出值
|
||||
*/
|
||||
interface LoadFeatureCodeResultOk<X extends FeatureBase> extends ResultInstance {
|
||||
readonly tag: 'Ok',
|
||||
readonly feature: X,
|
||||
readonly tag: 'Ok'
|
||||
readonly feature: X
|
||||
}
|
||||
|
||||
/** 代码没有导出任何值 */
|
||||
interface LoadFeatureCodeResultNoExport extends ResultInstance {
|
||||
readonly tag: 'NoExport',
|
||||
readonly tag: 'NoExport'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,43 +39,45 @@ interface LoadFeatureCodeResultNoExport extends ResultInstance {
|
||||
* @property thrown 抛出的值
|
||||
*/
|
||||
interface LoadFeatureCodeResultCodeThrew extends ResultInstance {
|
||||
readonly tag: 'CodeThrew',
|
||||
readonly thrown: unknown,
|
||||
readonly tag: 'CodeThrew'
|
||||
readonly thrown: unknown
|
||||
}
|
||||
|
||||
type LoadFeatureCodeResultError =
|
||||
LoadFeatureCodeResultNoExport
|
||||
| LoadFeatureCodeResultCodeThrew
|
||||
type LoadFeatureCodeResultError = LoadFeatureCodeResultNoExport | LoadFeatureCodeResultCodeThrew
|
||||
type LoadFeatureCodeResult<X extends FeatureBase> =
|
||||
LoadFeatureCodeResultOk<X>
|
||||
| LoadFeatureCodeResultOk<X>
|
||||
| LoadFeatureCodeResultError
|
||||
|
||||
const resultProto: ResultInstance = {
|
||||
isOk() { return this.tag === 'Ok' },
|
||||
isError() { return this.tag !== 'Ok' },
|
||||
isNoExport() { return this.tag === 'NoExport' },
|
||||
isCodeThrew() { return this.tag === 'CodeThrew' },
|
||||
isOk() {
|
||||
return this.tag === 'Ok'
|
||||
},
|
||||
isError() {
|
||||
return this.tag !== 'Ok'
|
||||
},
|
||||
isNoExport() {
|
||||
return this.tag === 'NoExport'
|
||||
},
|
||||
isCodeThrew() {
|
||||
return this.tag === 'CodeThrew'
|
||||
},
|
||||
}
|
||||
|
||||
const okResult = <X extends FeatureBase>(feature: X): LoadFeatureCodeResultOk<X> => lodash.create(
|
||||
resultProto,
|
||||
{
|
||||
const okResult = <X extends FeatureBase>(feature: X): LoadFeatureCodeResultOk<X> =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'Ok' as const,
|
||||
feature,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const noExportResult = lodash.create(resultProto, {
|
||||
tag: 'NoExport' as const,
|
||||
})
|
||||
|
||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeResultCodeThrew => lodash.create(
|
||||
resultProto,
|
||||
{
|
||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeResultCodeThrew =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'CodeThrew' as const,
|
||||
thrown,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载组件或插件的代码字符串,获取其导出 feature
|
||||
|
||||
@ -1,31 +1,27 @@
|
||||
import {
|
||||
LoadFeatureCodeResultError, LoadFeatureCodeResultOk,
|
||||
LoadFeatureCodeResultError,
|
||||
LoadFeatureCodeResultOk,
|
||||
} from '@/core/external-input/load-feature-code'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import { ComponentMetadata } from '@/components/types'
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import {
|
||||
loadFeatureCodeAllSettled,
|
||||
} from '@/core/external-input/load-feature-code-all-settled'
|
||||
import { loadFeatureCodeAllSettled } from '@/core/external-input/load-feature-code-all-settled'
|
||||
|
||||
const curConsole = useScopedConsole(
|
||||
'@/core/external-input/load-features-from-codes.ts',
|
||||
)
|
||||
const curConsole = useScopedConsole('@/core/external-input/load-features-from-codes.ts')
|
||||
|
||||
export enum FeatureKind {
|
||||
Component = 'Component',
|
||||
Plugin = 'Plugin',
|
||||
}
|
||||
|
||||
const logError = (kind: FeatureKind): (
|
||||
featureName: string, err: LoadFeatureCodeResultError) => void => {
|
||||
const logError = (
|
||||
kind: FeatureKind,
|
||||
): ((featureName: string, err: LoadFeatureCodeResultError) => void) => {
|
||||
const prefix = kind === FeatureKind.Component ? 'component' : 'plugin'
|
||||
return (featureName, err) => {
|
||||
if (err.isNoExport()) {
|
||||
curConsole.error(
|
||||
`${prefix} '${featureName}' exports no value, failed to load`,
|
||||
)
|
||||
curConsole.error(`${prefix} '${featureName}' exports no value, failed to load`)
|
||||
} else {
|
||||
curConsole.error(
|
||||
`${prefix} '${featureName}' throws something when importing, failed to load`,
|
||||
@ -35,10 +31,7 @@ const logError = (kind: FeatureKind): (
|
||||
}
|
||||
}
|
||||
|
||||
const reportErrToUser = (
|
||||
featureKind: FeatureKind,
|
||||
errNames: string[],
|
||||
): void => {
|
||||
const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => {
|
||||
type ErrInfo = number | string[]
|
||||
|
||||
const emptyErrInfo: () => string[] = () => []
|
||||
@ -117,8 +110,5 @@ export async function loadFeaturesFromCodes(
|
||||
reportErrToUser(kind, errNames)
|
||||
}
|
||||
|
||||
return lodash.map(
|
||||
namedOk,
|
||||
([, r]) => (r as LoadFeatureCodeResultOk<FeatureMetadata>).feature,
|
||||
)
|
||||
return lodash.map(namedOk, ([, r]) => (r as LoadFeatureCodeResultOk<FeatureMetadata>).feature)
|
||||
}
|
||||
|
||||
@ -5,15 +5,11 @@ import { installPlugin, PluginMetadata } from '@/plugins/plugin'
|
||||
import { installStyle, UserStyle } from '@/plugins/style'
|
||||
|
||||
type FeatureType = ComponentMetadata | PluginMetadata | UserStyle
|
||||
const isComponent = (item: FeatureType): item is ComponentMetadata => (
|
||||
const isComponent = (item: FeatureType): item is ComponentMetadata =>
|
||||
Boolean((item as ComponentMetadata)?.entry)
|
||||
)
|
||||
const isPlugin = (item: FeatureType): item is PluginMetadata => (
|
||||
const isPlugin = (item: FeatureType): item is PluginMetadata =>
|
||||
Boolean((item as PluginMetadata)?.setup)
|
||||
)
|
||||
const isStyle = (item: FeatureType): item is UserStyle => (
|
||||
Boolean((item as UserStyle)?.style)
|
||||
)
|
||||
const isStyle = (item: FeatureType): item is UserStyle => Boolean((item as UserStyle)?.style)
|
||||
|
||||
/** 如果输入的功能链接是 .zip, 则尝试解压. 仅支持单个功能, 不能批量, 只是为了能方便在 GitHub 直接以 .zip 格式分享功能. */
|
||||
export const tryParseZip = async (url: string) => {
|
||||
@ -21,7 +17,7 @@ export const tryParseZip = async (url: string) => {
|
||||
const { monkey } = await import('../core/ajax')
|
||||
const isZip = url.endsWith('.zip')
|
||||
const responseType = isZip ? 'blob' : 'text'
|
||||
const response = await monkey({ url, method: 'GET', responseType }) as Blob | string
|
||||
const response = (await monkey({ url, method: 'GET', responseType })) as Blob | string
|
||||
if (!isZip || typeof response === 'string') {
|
||||
return response as string
|
||||
}
|
||||
@ -33,7 +29,10 @@ export const tryParseZip = async (url: string) => {
|
||||
}
|
||||
return files[0].async('text')
|
||||
}
|
||||
export const installFeatureFromCode = async (code: string, url?: string): Promise<{
|
||||
export const installFeatureFromCode = async (
|
||||
code: string,
|
||||
url?: string,
|
||||
): Promise<{
|
||||
metadata: FeatureType
|
||||
message: string
|
||||
}> => {
|
||||
@ -67,7 +66,9 @@ export const installFeatureFromCode = async (code: string, url?: string): Promis
|
||||
await after(installerResult.metadata)
|
||||
return installerResult
|
||||
}
|
||||
export const installFeature = async (url: string): Promise<{
|
||||
export const installFeature = async (
|
||||
url: string,
|
||||
): Promise<{
|
||||
metadata: FeatureType
|
||||
message: string
|
||||
}> => {
|
||||
|
||||
@ -4,7 +4,7 @@ import { childList } from './observer'
|
||||
* `<head>`载入后运行
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const headLoaded = <T> (callback: () => T) => (
|
||||
export const headLoaded = <T>(callback: () => T) =>
|
||||
new Promise<T>(resolve => {
|
||||
if (document.head !== null) {
|
||||
resolve(callback())
|
||||
@ -17,13 +17,12 @@ export const headLoaded = <T> (callback: () => T) => (
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* `<body>`载入后运行 (`DOMContentLoaded`)
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const contentLoaded = <T> (callback: () => T) => (
|
||||
export const contentLoaded = <T>(callback: () => T) =>
|
||||
new Promise<T>(resolve => {
|
||||
if (document.readyState !== 'loading') {
|
||||
resolve(callback())
|
||||
@ -31,13 +30,12 @@ export const contentLoaded = <T> (callback: () => T) => (
|
||||
document.addEventListener('DOMContentLoaded', () => resolve(callback()))
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* 网页`load`事件时运行
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const fullyLoaded = <T> (callback: () => T) => (
|
||||
export const fullyLoaded = <T>(callback: () => T) =>
|
||||
new Promise<T>(resolve => {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve(callback())
|
||||
@ -45,7 +43,6 @@ export const fullyLoaded = <T> (callback: () => T) => (
|
||||
unsafeWindow.addEventListener('load', () => resolve(callback()))
|
||||
}
|
||||
})
|
||||
)
|
||||
/**
|
||||
* 脚本的生命周期事件类型
|
||||
*/
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import commonMeta from '@/client/common.meta.json'
|
||||
import { cdnRoots } from './cdn-types'
|
||||
|
||||
commonMeta.copyright = commonMeta.copyright.replace(/\[year\]/g, new Date().getFullYear().toString())
|
||||
commonMeta.copyright = commonMeta.copyright.replace(
|
||||
/\[year\]/g,
|
||||
new Date().getFullYear().toString(),
|
||||
)
|
||||
/** 分支表 */
|
||||
export const branches = {
|
||||
stable: 'master',
|
||||
|
||||
@ -26,91 +26,105 @@ export const mutationObserve = (
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const childList = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: true,
|
||||
subtree: false,
|
||||
attributes: false,
|
||||
}, callback)
|
||||
export const childList = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: true,
|
||||
subtree: false,
|
||||
attributes: false,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
/** 监听所有子孙元素
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const childListSubtree = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
}, callback)
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const childListSubtree = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
/** 监听自身的HTML属性变化
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const attributes = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: false,
|
||||
subtree: false,
|
||||
attributes: true,
|
||||
}, callback)
|
||||
export const attributes = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: false,
|
||||
subtree: false,
|
||||
attributes: true,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
/** 监听自身及其子孙元素的HTML属性变化
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const attributesSubtree = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: false,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
}, callback)
|
||||
export const attributesSubtree = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: false,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
/** 监听自身的文本内容变化
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const characterData = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: false,
|
||||
subtree: false,
|
||||
attributes: false,
|
||||
characterData: true,
|
||||
}, callback)
|
||||
export const characterData = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: false,
|
||||
subtree: false,
|
||||
attributes: false,
|
||||
characterData: true,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
/** 监听自身及其子孙元素的文本内容变化
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const characterDataSubtree = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: false,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
characterData: true,
|
||||
}, callback)
|
||||
export const characterDataSubtree = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: false,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
characterData: true,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
/** 监听指定目标上的所有变化, 包括自身及子孙元素的元素增减, 属性变化, 文本内容变化
|
||||
*
|
||||
* 若需要监听 `document.body` 上的, 请使用 allMutations
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const allMutationsOn = (
|
||||
target: ObserverTarget,
|
||||
callback: MutationCallback,
|
||||
) => mutationObserve(resolveTargets(target), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
}, callback)
|
||||
export const allMutationsOn = (target: ObserverTarget, callback: MutationCallback) =>
|
||||
mutationObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
|
||||
const everyNodesObserver: {
|
||||
observer: MutationObserver
|
||||
@ -128,9 +142,8 @@ const everyNodesObserver: {
|
||||
export const allMutations = (callback: MutationCallback) => {
|
||||
if (!everyNodesObserver.observer) {
|
||||
everyNodesObserver.callbacks.push(callback)
|
||||
const [observer, config] = allMutationsOn(
|
||||
document.body,
|
||||
records => everyNodesObserver.callbacks.forEach(c => c(records, everyNodesObserver.observer)),
|
||||
const [observer, config] = allMutationsOn(document.body, records =>
|
||||
everyNodesObserver.callbacks.forEach(c => c(records, everyNodesObserver.observer)),
|
||||
)
|
||||
everyNodesObserver.observer = observer
|
||||
everyNodesObserver.config = config
|
||||
@ -154,10 +167,8 @@ export const intersectionObserve = (
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const visible = (
|
||||
target: ObserverTarget,
|
||||
callback: IntersectionObserverCallback,
|
||||
) => intersectionObserve(resolveTargets(target), {}, callback)
|
||||
export const visible = (target: ObserverTarget, callback: IntersectionObserverCallback) =>
|
||||
intersectionObserve(resolveTargets(target), {}, callback)
|
||||
/**
|
||||
* 监听元素进入指定容器内/变为可见
|
||||
* @param target 监听目标
|
||||
@ -170,10 +181,15 @@ export const visibleInside = (
|
||||
container: HTMLElement,
|
||||
margin: string,
|
||||
callback: IntersectionObserverCallback,
|
||||
) => intersectionObserve(resolveTargets(target), {
|
||||
root: container,
|
||||
rootMargin: margin,
|
||||
}, callback)
|
||||
) =>
|
||||
intersectionObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
root: container,
|
||||
rootMargin: margin,
|
||||
},
|
||||
callback,
|
||||
)
|
||||
|
||||
export const resizeObserve = (
|
||||
targets: Element[],
|
||||
@ -189,12 +205,14 @@ export const resizeObserve = (
|
||||
* @param target 监听目标
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const sizeChange = (
|
||||
target: ObserverTarget,
|
||||
callback: ResizeObserverCallback,
|
||||
) => resizeObserve(resolveTargets(target), {
|
||||
box: 'border-box',
|
||||
}, callback)
|
||||
export const sizeChange = (target: ObserverTarget, callback: ResizeObserverCallback) =>
|
||||
resizeObserve(
|
||||
resolveTargets(target),
|
||||
{
|
||||
box: 'border-box',
|
||||
},
|
||||
callback,
|
||||
)
|
||||
|
||||
const setupUrlChangeListener = lodash.once(() => {
|
||||
let lastUrl = document.URL
|
||||
@ -221,13 +239,15 @@ export const urlChange = (callback: (url: string) => void, config?: AddEventList
|
||||
}
|
||||
|
||||
/** 等待 cid */
|
||||
const selectCid = lodash.once(() => select(() => {
|
||||
import('@/components/video/player-adaptor').then(({ playerPolyfill }) => playerPolyfill())
|
||||
if (unsafeWindow.cid) {
|
||||
return unsafeWindow.cid
|
||||
}
|
||||
return null
|
||||
}))
|
||||
const selectCid = lodash.once(() =>
|
||||
select(() => {
|
||||
import('@/components/video/player-adaptor').then(({ playerPolyfill }) => playerPolyfill())
|
||||
if (unsafeWindow.cid) {
|
||||
return unsafeWindow.cid
|
||||
}
|
||||
return null
|
||||
}),
|
||||
)
|
||||
|
||||
let cidHooked = false
|
||||
export type VideoChangeCallback = (id: { aid: string; cid: string }) => void
|
||||
@ -275,6 +295,10 @@ export const videoChange = async (
|
||||
cidHooked = true
|
||||
}
|
||||
callback(getId())
|
||||
window.addEventListener('videoChange', (e: CustomEvent<ReturnType<typeof getId>>) => callback(e.detail), config)
|
||||
window.addEventListener(
|
||||
'videoChange',
|
||||
(e: CustomEvent<ReturnType<typeof getId>>) => callback(e.detail),
|
||||
config,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export const promiseLoadTime = new Map<{ name: string }, number>()
|
||||
export const promiseResolveTime = new Map<{ name: string }, number>()
|
||||
export const promiseLoadTrace = async <T> (name: string, promiseFunc: () => Promise<T>) => {
|
||||
export const promiseLoadTrace = async <T>(name: string, promiseFunc: () => Promise<T>) => {
|
||||
const { getGeneralSettings } = await import('../settings')
|
||||
if (!getGeneralSettings().devMode) {
|
||||
return promiseFunc()
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
export const logStats = <T extends { name: string }> (series: string, map: Map<T, number>) => {
|
||||
export const logStats = <T extends { name: string }>(series: string, map: Map<T, number>) => {
|
||||
const entries = [...map.entries()]
|
||||
const total = entries.reduce((sum, it) => sum + it[1], 0)
|
||||
console.groupCollapsed(`${series} time:`, `${Math.round(total * 100) / 100}ms`, `for ${entries.length} items`)
|
||||
console.groupCollapsed(
|
||||
`${series} time:`,
|
||||
`${Math.round(total * 100) / 100}ms`,
|
||||
`for ${entries.length} items`,
|
||||
)
|
||||
entries.forEach(([p, time]) => {
|
||||
console.log(
|
||||
`%c${p.name} %c${Math.round(time * 100) / 100}ms ${Math.round((time / total) * 10000) / 100}% %c`,
|
||||
`%c${p.name} %c${Math.round(time * 100) / 100}ms ${
|
||||
Math.round((time / total) * 10000) / 100
|
||||
}% %c`,
|
||||
'color: #00A0D8',
|
||||
'color: #888',
|
||||
'color: unset',
|
||||
|
||||
@ -4,10 +4,14 @@ export const ReorderEnabledClassName = 'reorder-enabled'
|
||||
export const ReorderingClassName = 'reordering'
|
||||
|
||||
/** 表示重排列事件的监听函数 */
|
||||
export type ReorderEventHandler = (event: CustomEvent<{
|
||||
element: HTMLElement
|
||||
order: number
|
||||
}[]>) => void
|
||||
export type ReorderEventHandler = (
|
||||
event: CustomEvent<
|
||||
{
|
||||
element: HTMLElement
|
||||
order: number
|
||||
}[]
|
||||
>,
|
||||
) => void
|
||||
/** 表示重排列项目的一个状态(位置信息) */
|
||||
export interface ReorderItemSnapshot {
|
||||
/** 元素 */
|
||||
@ -26,7 +30,7 @@ export interface ReorderOrientation {
|
||||
snapshots: ReorderItemSnapshot[],
|
||||
currentElement: HTMLElement,
|
||||
xOffset: number,
|
||||
yOffset: number
|
||||
yOffset: number,
|
||||
) => void
|
||||
}
|
||||
/** 支持的重排列方向 */
|
||||
@ -58,11 +62,14 @@ export const ReorderOrientations: { [key: string]: ReorderOrientation } = {
|
||||
snapshot.element.classList.remove(ReorderIncreaseClassName)
|
||||
})
|
||||
rightSide.forEach(snapshot => {
|
||||
if (currentRect.left + xOffset + currentRect.width
|
||||
>= snapshot.rect.left + snapshot.rect.width / 2
|
||||
if (
|
||||
currentRect.left + xOffset + currentRect.width >=
|
||||
snapshot.rect.left + snapshot.rect.width / 2
|
||||
) {
|
||||
snapshot.element.classList.add(ReorderDecreaseClassName)
|
||||
snapshot.element.style.transform = `translateX(-${firstSnapshot.rect.left - currentRect.left}px)`
|
||||
snapshot.element.style.transform = `translateX(-${
|
||||
firstSnapshot.rect.left - currentRect.left
|
||||
}px)`
|
||||
} else {
|
||||
snapshot.element.classList.remove(ReorderDecreaseClassName)
|
||||
snapshot.element.style.transform = ''
|
||||
@ -77,14 +84,20 @@ export const ReorderOrientations: { [key: string]: ReorderOrientation } = {
|
||||
leftSide.forEach(snapshot => {
|
||||
if (currentRect.left + xOffset <= snapshot.rect.left + snapshot.rect.width / 2) {
|
||||
snapshot.element.classList.add(ReorderIncreaseClassName)
|
||||
snapshot.element.style.transform = `translateX(${currentRect.left + currentRect.width - firstSnapshot.rect.left - firstSnapshot.rect.width}px)`
|
||||
snapshot.element.style.transform = `translateX(${
|
||||
currentRect.left +
|
||||
currentRect.width -
|
||||
firstSnapshot.rect.left -
|
||||
firstSnapshot.rect.width
|
||||
}px)`
|
||||
} else {
|
||||
snapshot.element.classList.remove(ReorderIncreaseClassName)
|
||||
snapshot.element.style.transform = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 50,
|
||||
},
|
||||
50,
|
||||
),
|
||||
},
|
||||
/** 纵向 */
|
||||
@ -114,11 +127,14 @@ export const ReorderOrientations: { [key: string]: ReorderOrientation } = {
|
||||
snapshot.element.classList.remove(ReorderIncreaseClassName)
|
||||
})
|
||||
lowerSide.forEach(snapshot => {
|
||||
if (currentRect.top + yOffset + currentRect.height
|
||||
>= snapshot.rect.top + snapshot.rect.height / 2
|
||||
if (
|
||||
currentRect.top + yOffset + currentRect.height >=
|
||||
snapshot.rect.top + snapshot.rect.height / 2
|
||||
) {
|
||||
snapshot.element.classList.add(ReorderDecreaseClassName)
|
||||
snapshot.element.style.transform = `translateY(-${firstSnapshot.rect.top - currentRect.top}px)`
|
||||
snapshot.element.style.transform = `translateY(-${
|
||||
firstSnapshot.rect.top - currentRect.top
|
||||
}px)`
|
||||
} else {
|
||||
snapshot.element.classList.remove(ReorderDecreaseClassName)
|
||||
snapshot.element.style.transform = ''
|
||||
@ -133,14 +149,20 @@ export const ReorderOrientations: { [key: string]: ReorderOrientation } = {
|
||||
upperSide.forEach(snapshot => {
|
||||
if (currentRect.top + yOffset <= snapshot.rect.top + snapshot.rect.height / 2) {
|
||||
snapshot.element.classList.add(ReorderIncreaseClassName)
|
||||
snapshot.element.style.transform = `translateY(${currentRect.top + currentRect.height - firstSnapshot.rect.top - firstSnapshot.rect.height}px)`
|
||||
snapshot.element.style.transform = `translateY(${
|
||||
currentRect.top +
|
||||
currentRect.height -
|
||||
firstSnapshot.rect.top -
|
||||
firstSnapshot.rect.height
|
||||
}px)`
|
||||
} else {
|
||||
snapshot.element.classList.remove(ReorderIncreaseClassName)
|
||||
snapshot.element.style.transform = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 50,
|
||||
},
|
||||
50,
|
||||
),
|
||||
// getFinalTransform: (changedSnapshots, currentSnapshot) => {
|
||||
// return ''
|
||||
@ -169,10 +191,18 @@ export class Reorder extends EventTarget {
|
||||
})
|
||||
}
|
||||
}
|
||||
addEventListener(type: 'reorder', listener: ReorderEventHandler, options?: boolean | AddEventListenerOptions) {
|
||||
addEventListener(
|
||||
type: 'reorder',
|
||||
listener: ReorderEventHandler,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
) {
|
||||
super.addEventListener(type, listener, options)
|
||||
}
|
||||
removeEventListener(type: 'reorder', callback: ReorderEventHandler, options?: boolean | EventListenerOptions) {
|
||||
removeEventListener(
|
||||
type: 'reorder',
|
||||
callback: ReorderEventHandler,
|
||||
options?: boolean | EventListenerOptions,
|
||||
) {
|
||||
super.addEventListener(type, callback, options)
|
||||
}
|
||||
/** 获取各元素的`order`映射 */
|
||||
@ -208,9 +238,11 @@ export class Reorder extends EventTarget {
|
||||
element.style.transition = 'none'
|
||||
element.style.userSelect = 'none'
|
||||
this.generateSnapshots()
|
||||
this.children.filter(e => e !== element).forEach(e => {
|
||||
e.style.transition = 'transform .2s ease-out'
|
||||
})
|
||||
this.children
|
||||
.filter(e => e !== element)
|
||||
.forEach(e => {
|
||||
e.style.transition = 'transform .2s ease-out'
|
||||
})
|
||||
xInit = x
|
||||
yInit = y
|
||||
reordering = true
|
||||
@ -236,7 +268,10 @@ export class Reorder extends EventTarget {
|
||||
const yOffset = y - yInit
|
||||
element.style.transform = this.orientation.getMoveTransform(xOffset, yOffset)
|
||||
this.orientation.setOtherTransform(
|
||||
[...this.snapshots.values()], element, xOffset, yOffset,
|
||||
[...this.snapshots.values()],
|
||||
element,
|
||||
xOffset,
|
||||
yOffset,
|
||||
)
|
||||
}
|
||||
const mousemove = (e: MouseEvent) => {
|
||||
@ -295,12 +330,14 @@ export class Reorder extends EventTarget {
|
||||
element.style.order = (parseInt(element.style.order) + orderOffset).toString()
|
||||
element.style.transform = ''
|
||||
element.style.transition = ''
|
||||
this.dispatchEvent(new CustomEvent('reorder', {
|
||||
detail: this.children.map(c => ({
|
||||
element: c,
|
||||
order: parseInt(c.style.order),
|
||||
})),
|
||||
}))
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('reorder', {
|
||||
detail: this.children.map(c => ({
|
||||
element: c,
|
||||
order: parseInt(c.style.order),
|
||||
})),
|
||||
}),
|
||||
)
|
||||
// setTimeout(() => element.style.transition = '', 200);
|
||||
// setTimeout(() => {
|
||||
// other.forEach(e => {
|
||||
|
||||
@ -23,11 +23,11 @@ export class RuntimeLibrary<LibraryType> implements PromiseLike<LibraryType> {
|
||||
this.modulePromise = (async () => {
|
||||
console.log(`[Runtime Library] Start download from ${url}`)
|
||||
const code: string = await monkey({ url })
|
||||
console.log(`[Runtime Library] Downloaded from ${url} , length = ${code.length}`);
|
||||
(function runEval() {
|
||||
console.log(`[Runtime Library] Downloaded from ${url} , length = ${code.length}`)
|
||||
;(function runEval() {
|
||||
return eval(code)
|
||||
// eslint-disable-next-line no-extra-bind
|
||||
}).bind(window)()
|
||||
}.bind(window)())
|
||||
return getModule(window)
|
||||
})()
|
||||
}
|
||||
|
||||
@ -14,9 +14,8 @@ import { matchUrlPattern } from '../utils'
|
||||
* 生成组件选项设置
|
||||
* @param options 组件选项定义
|
||||
*/
|
||||
export const metadataToOptions = <O extends UnknownOptions>(
|
||||
options: OptionsMetadata<O>,
|
||||
): O => lodash.mapValues(options, m => m.defaultValue) as O
|
||||
export const metadataToOptions = <O extends UnknownOptions>(options: OptionsMetadata<O>): O =>
|
||||
lodash.mapValues(options, m => m.defaultValue) as O
|
||||
|
||||
/**
|
||||
* 生成组件设置
|
||||
@ -51,11 +50,14 @@ export const isUserPlugin = (plugin: PluginMetadata | string) => {
|
||||
}
|
||||
const emptySettings: ComponentSettings = {
|
||||
enabled: false,
|
||||
options: new Proxy({}, {
|
||||
get() {
|
||||
return false
|
||||
options: new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
// TODO: 参考 discussion #3041。
|
||||
@ -98,7 +100,8 @@ export const getComponentSettings = <R extends UnknownOptions = UnknownOptions>(
|
||||
/**
|
||||
* 获取通用设置 (`settingsPanel`组件的`options`)
|
||||
*/
|
||||
export const getGeneralSettings = () => getComponentSettings<SettingsPanelOptions>('settingsPanel').options
|
||||
export const getGeneralSettings = () =>
|
||||
getComponentSettings<SettingsPanelOptions>('settingsPanel').options
|
||||
/**
|
||||
* 判断此组件是否启用, 启用的条件为:
|
||||
* - 若定义了排除列表, 当前URL必须不匹配其排除列表中任意一项(`Component.urlExclude`)
|
||||
|
||||
@ -22,9 +22,8 @@ export const initInternalSettings = () => {
|
||||
})
|
||||
// 载入组件设置
|
||||
components.forEach(component => {
|
||||
settingsInternalState.internalSettings.components[component.name] = (
|
||||
settingsInternalState.internalSettings.components[component.name] =
|
||||
componentToSettings(component)
|
||||
)
|
||||
})
|
||||
}
|
||||
/** 默认设置 */
|
||||
|
||||
@ -11,10 +11,7 @@ export const isProxy = Symbol('isProxy')
|
||||
export const createProxy = (targetObj: any, valueChangeListener?: ValueChangeListener) => {
|
||||
const applyProxy = (obj: any, rootProp?: Property, propPath: Property[] = []) => {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const shouldApplyProxy = (
|
||||
typeof value === 'object'
|
||||
&& !(value instanceof RegExp)
|
||||
)
|
||||
const shouldApplyProxy = typeof value === 'object' && !(value instanceof RegExp)
|
||||
if (shouldApplyProxy) {
|
||||
obj[key] = applyProxy(value, rootProp || key, [...propPath, key])
|
||||
}
|
||||
@ -28,9 +25,8 @@ export const createProxy = (targetObj: any, valueChangeListener?: ValueChangeLis
|
||||
},
|
||||
set(o, prop, value) {
|
||||
const oldValue = o[prop]
|
||||
const isImplicitProp = (
|
||||
const isImplicitProp =
|
||||
!Object.prototype.hasOwnProperty.call(o, prop) && oldValue !== undefined
|
||||
)
|
||||
if (unsafeWindow.proxyDebug) {
|
||||
console.log({ isImplicitProp, prop, value })
|
||||
}
|
||||
@ -41,12 +37,11 @@ export const createProxy = (targetObj: any, valueChangeListener?: ValueChangeLis
|
||||
* - 不能是已经启用过的
|
||||
* - 不能是上游原型链里的
|
||||
*/
|
||||
const deep = (
|
||||
typeof value === 'object'
|
||||
&& !(value instanceof RegExp)
|
||||
&& !(value[isProxy] === true)
|
||||
&& !isImplicitProp
|
||||
)
|
||||
const deep =
|
||||
typeof value === 'object' &&
|
||||
!(value instanceof RegExp) &&
|
||||
!(value[isProxy] === true) &&
|
||||
!isImplicitProp
|
||||
if (deep) {
|
||||
value = applyProxy(value, rootProp || prop, [...propPath, prop])
|
||||
}
|
||||
|
||||
@ -23,18 +23,24 @@ export interface Settings {
|
||||
/** 用户安装的样式 */
|
||||
userStyles: Record<string, Required<UserStyle>>
|
||||
/** 用户安装的插件 */
|
||||
userPlugins: Record<string, (Omit<PluginMetadata, 'setup'> & {
|
||||
code: string
|
||||
})>
|
||||
userPlugins: Record<
|
||||
string,
|
||||
Omit<PluginMetadata, 'setup'> & {
|
||||
code: string
|
||||
}
|
||||
>
|
||||
/** 用户安装的组件 */
|
||||
userComponents: Record<string, {
|
||||
/** 原始代码, 开启时将执行并获取完整的组件信息 */
|
||||
code: string
|
||||
/** 部分可序列化的组件信息 */
|
||||
metadata: UserComponentMetadata
|
||||
/** 组件设置 */
|
||||
settings: ComponentSettings
|
||||
}>
|
||||
userComponents: Record<
|
||||
string,
|
||||
{
|
||||
/** 原始代码, 开启时将执行并获取完整的组件信息 */
|
||||
code: string
|
||||
/** 部分可序列化的组件信息 */
|
||||
metadata: UserComponentMetadata
|
||||
/** 组件设置 */
|
||||
settings: ComponentSettings
|
||||
}
|
||||
>
|
||||
/** 组件更新的代码 */
|
||||
update?: string
|
||||
/** 实例 ID */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user