Format src folder

This commit is contained in:
the1812 2022-10-12 23:17:55 +08:00
parent dce37e217f
commit 92314cef6f
145 changed files with 1834 additions and 1902 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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请求) */

View File

@ -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} 的效果, 可以用来配合其他地方的生命周期 */

View File

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

View File

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

View File

@ -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':
'安装其他语言包可以更换界面语言, 机器翻译选择可以设定其他一些功能如`动态翻译`, `评论翻译`使用的翻译器. 机器翻译的选择不影响界面语言.',

View File

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

View File

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

View File

@ -68,7 +68,7 @@ export default Vue.extend({
})
</script>
<style lang="scss">
@import "common";
@import 'common';
.suggest-item {
outline: none !important;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,4 +39,6 @@ const builtInActions: SearchBarAction[] = [
},
},
]
export const [searchBarActions] = registerAndGetData('settingsPanel.searchBarActions', [...builtInActions])
export const [searchBarActions] = registerAndGetData('settingsPanel.searchBarActions', [
...builtInActions,
])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ export interface ManageItem<
ItemType extends {
displayName: string
name: string
}
},
> {
/** 唯一名称 */
key: string

View File

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

View File

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

View File

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

View File

@ -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,
])

View File

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

View File

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

View File

@ -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
/** 显示名称 */

View File

@ -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}'相关联的组件`)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {
'{': '',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}/`,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,9 +22,8 @@ export const initInternalSettings = () => {
})
// 载入组件设置
components.forEach(component => {
settingsInternalState.internalSettings.components[component.name] = (
settingsInternalState.internalSettings.components[component.name] =
componentToSettings(component)
)
})
}
/** 默认设置 */

View File

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

View File

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