mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge branch 'preview-features' into preview
This commit is contained in:
commit
45fd78f006
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -31,6 +31,7 @@
|
||||
"cntr",
|
||||
"compositionend",
|
||||
"compositionstart",
|
||||
"contenteditable",
|
||||
"csrf",
|
||||
"ctnr",
|
||||
"Danmaku",
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@ -1,6 +1,45 @@
|
||||
<!-- spell-checker: disable -->
|
||||
# 更新日志
|
||||
|
||||
## v2.9.3-preview
|
||||
主要是各种修复, 见 [v2.9.3](https://github.com/the1812/Bilibili-Evolved/releases/tag/v2.9.3).
|
||||
|
||||
## v2.9.3
|
||||
|
||||
<details>
|
||||
<summary>正式版用户将获得 v2.9.1-preview ~ v2.9.2-preview 的所有改动, 点击展开查看</summary>
|
||||
|
||||
✨新增
|
||||
- `查看封面` 可以为 aria2 输出提供直接的封面下载. (PR #4798 by [Oxygenくん](https://github.com/oxygenkun))
|
||||
- 新增组件 `保存视频元数据`. (PR #4840 by [WakelessSloth56](https://github.com/WakelessSloth56))
|
||||
> - 保存视频元数据为 [FFMETADATA](https://ffmpeg.org/ffmpeg-formats.html#Metadata-2) 格式
|
||||
> - 使用组件 `下载视频` 时指定 `WASM` 输出方式(插件 `下载视频 - WASM 混流输出`)可选择是否直接混流入输出文件。
|
||||
> - 保存视频章节为 OGM 格式 (https://github.com/the1812/Bilibili-Evolved/discussions/2069#discussioncomment-10110916)
|
||||
|
||||
- `简化首页` 支持隐藏轮播图. (PR #4852 by [Lime](https://github.com/Liumingxun))
|
||||
- 新增组件 `添加直播间用户超链接`. (PR #4856 by [Light_Quanta](https://github.com/LightQuanta))
|
||||
> 网页版直播间右上角的房间观众和大航海界面的用户列表只可查看用户名,不可进行点击。该组件为用户头像和用户名称处添加点击效果,允许通过点击直接查看用户空间。
|
||||
|
||||
- 插件 `下载视频 - WASM 混流输出` 支持并行下载库和音视频流. (PR #4864 by [WakelessSloth56](https://github.com/WakelessSloth56))
|
||||
- `弹幕转义` 支持对正斜杠的换行 (`/n`) 进行转义. (#4865)
|
||||
- `自定义顶栏` 支持直接在功能中打开布局设置. (#2666)
|
||||
- `高分辨率图片` 支持处理没有指定高度的图片, 支持在专栏页面中请求原图. (#2868)
|
||||
- `直播间网页全屏自适应` 样式适配较低的宽度值. (#4895)
|
||||
|
||||
☕开发者相关
|
||||
- 外部资源接入 Subresource Integrity. (#4896)
|
||||
|
||||
</details>
|
||||
|
||||
🐛修复
|
||||
- 修复 `快速收起评论` 对旧版评论区的兼容性. (#4905)
|
||||
- 修复 `快捷键扩展` 的发送评论在新版评论区失效. (#4843)
|
||||
- 修复 `禁用评论区搜索词` 偶现样式失效. (#4843)
|
||||
|
||||
☕开发者相关
|
||||
- `ShadowDomObserver` 在使用前无需再调用 `observe()`.
|
||||
- 增加工具方法 `getActiveElement` 检测当前页面的聚焦元素.
|
||||
|
||||
## v2.9.2-preview
|
||||
`2024-09-08`
|
||||
|
||||
|
||||
@ -28,10 +28,13 @@
|
||||
}
|
||||
}
|
||||
.bili-comment-container {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
&.bili-dyn-comment .reply-list {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
.fold-comment {
|
||||
order: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.at-panel,
|
||||
|
||||
@ -32,10 +32,7 @@ const entry = async () => {
|
||||
}
|
||||
if (feedsCardsManager.managerType === 'v2') {
|
||||
const getExistingComment = () => dq(card, commentSelector) as HTMLElement
|
||||
const isCommentAreaReady = () => {
|
||||
const existingComment = getExistingComment()
|
||||
return existingComment !== null && dq(existingComment, 'bili-comments')
|
||||
}
|
||||
const isCommentAreaReady = () => getExistingComment() !== null
|
||||
const handler = () => {
|
||||
const button = dq(card, '.bili-dyn-action.comment') as HTMLElement
|
||||
button?.click()
|
||||
|
||||
@ -21,9 +21,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat-input-ctnr-new .medal-section {
|
||||
min-width: 0 !important;
|
||||
max-width: 0 !important;
|
||||
.chat-input-ctnr-new {
|
||||
.medal-section {
|
||||
display: none !important;
|
||||
}
|
||||
.chat-input-new:not(:has(textarea.focus)) {
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { getComponentSettings } from '@/core/settings'
|
||||
import { registerAndGetData } from '@/plugins/data'
|
||||
import { Options } from '.'
|
||||
import { KeyBindingAction, KeyBindingActionContext } from './bindings'
|
||||
import { simulateClick } from '@/core/utils'
|
||||
import { getActiveElement, simulateClick } from '@/core/utils'
|
||||
|
||||
export const keyboardEventToPointer = (event: KeyboardEvent): PointerEventInit => {
|
||||
return {
|
||||
@ -239,15 +239,24 @@ export const builtInActions: Record<string, KeyBindingAction> = {
|
||||
sendComment: {
|
||||
displayName: '发送评论',
|
||||
ignoreTyping: false,
|
||||
prevent: true,
|
||||
run: () => {
|
||||
const { activeElement } = document
|
||||
if (!activeElement || !(activeElement instanceof HTMLTextAreaElement)) {
|
||||
const activeElement = getActiveElement()
|
||||
if (!activeElement) {
|
||||
return null
|
||||
}
|
||||
const isEditable =
|
||||
activeElement instanceof HTMLTextAreaElement ||
|
||||
activeElement.hasAttribute('contenteditable')
|
||||
if (!isEditable) {
|
||||
return null
|
||||
}
|
||||
const getShadowRoot = (node: Node) => node.getRootNode() as ShadowRoot | null
|
||||
const sendButton = (() => {
|
||||
const candidates = [
|
||||
() => activeElement.nextElementSibling,
|
||||
() => activeElement.parentElement.nextElementSibling,
|
||||
() => getShadowRoot(getShadowRoot(activeElement)?.host)?.querySelector('#pub button'),
|
||||
() => dq('.reply-box:focus-within .reply-box-send'),
|
||||
]
|
||||
const match = candidates.find(fn => fn() !== null)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { isTyping, matchUrlPattern } from '@/core/utils'
|
||||
import { mediaListUrls, watchlaterUrls } from '@/core/utils/urls'
|
||||
import { clickElement, changeVideoTime, showTip } from './actions'
|
||||
import { shadowDomObserver } from '@/core/shadow-root'
|
||||
|
||||
export interface KeyBindingActionContext {
|
||||
binding: KeyBinding
|
||||
@ -29,7 +30,7 @@ export const loadKeyBindings = lodash.once((bindings: KeyBinding[]) => {
|
||||
enable: true,
|
||||
bindings,
|
||||
}
|
||||
document.body.addEventListener('keydown', (e: KeyboardEvent & { [key: string]: boolean }) => {
|
||||
const keyboardHandler = (e: KeyboardEvent & { [key: string]: boolean }) => {
|
||||
if (!config.enable) {
|
||||
return
|
||||
}
|
||||
@ -87,10 +88,15 @@ export const loadKeyBindings = lodash.once((bindings: KeyBinding[]) => {
|
||||
|
||||
const actionSuccess = !lodash.isNil(actionResult)
|
||||
if (binding.action.prevent ?? actionSuccess) {
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
document.body.addEventListener('keydown', keyboardHandler, { capture: true })
|
||||
shadowDomObserver.watchShadowDom({
|
||||
added: shadowDom =>
|
||||
shadowDom.shadowRoot.addEventListener('keydown', keyboardHandler, { capture: true }),
|
||||
})
|
||||
return config
|
||||
})
|
||||
|
||||
@ -49,6 +49,9 @@
|
||||
<div class="download-video-config-title">使用备用下载地址:</div>
|
||||
<SwitchBox v-model="useBackupUrls" />
|
||||
</div>
|
||||
<div class="download-video-config-description">
|
||||
若默认下载地址速度缓慢, 可以尝试更换备用下载地址.
|
||||
</div>
|
||||
<component
|
||||
:is="a.component"
|
||||
v-for="a of assetsWithOptions"
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="rpc-config download-video-config-section">
|
||||
<div>
|
||||
<div>aria2下载附属资源(若支持):</div>
|
||||
<SwitchBox v-model="isPluginDownloadAssets" @change="saveSettings" />
|
||||
<div class="online-assets-download">
|
||||
<div class="download-video-config-item">
|
||||
<div class="download-video-config-title">使用 aria2 下载附属资源:</div>
|
||||
<SwitchBox v-model="isPluginDownloadAssets" @change="saveAssetsSettings" />
|
||||
</div>
|
||||
<div class="download-video-config-description">
|
||||
存在于服务器的附属资源 (例如封面) 可以一并发送到 aria2 下载.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isRenaming" class="profile-select">
|
||||
<div class="profile-item-name">重命名 RPC 预设:</div>
|
||||
@ -117,11 +122,15 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveSettings() {
|
||||
saveProfileSettings() {
|
||||
options.selectedRpcProfileName = this.selectedRpcProfile.name
|
||||
options.rpcProfiles = this.rpcProfiles
|
||||
storedOptions.selectedRpcProfileName = options.selectedRpcProfileName
|
||||
storedOptions.rpcProfiles = options.rpcProfiles
|
||||
},
|
||||
saveAssetsSettings() {
|
||||
options.isPluginDownloadAssets = this.isPluginDownloadAssets
|
||||
Object.assign(storedOptions, options)
|
||||
storedOptions.isPluginDownloadAssets = options.isPluginDownloadAssets
|
||||
},
|
||||
async startRename() {
|
||||
this.profileRename = this.selectedRpcProfile.name
|
||||
@ -143,7 +152,7 @@ export default Vue.extend({
|
||||
}
|
||||
this.selectedRpcProfile.name = this.profileRename
|
||||
this.isRenaming = false
|
||||
this.saveSettings()
|
||||
this.saveProfileSettings()
|
||||
},
|
||||
newProfile() {
|
||||
const newProfile: Aria2RpcProfile = { ...this.selectedRpcProfile }
|
||||
@ -206,5 +215,9 @@ export default Vue.extend({
|
||||
.profile-method {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.online-assets-download {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,7 +2,10 @@ import { getJson, monkey, postJson } from '@/core/ajax'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { UserAgent } from '@/core/utils/constants'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { DownloadVideoOutput } from '../../../../components/video/download/types'
|
||||
import {
|
||||
DownloadVideoAssets,
|
||||
DownloadVideoOutput,
|
||||
} from '../../../../components/video/download/types'
|
||||
import { Aria2RpcProfile } from './rpc-profiles'
|
||||
|
||||
interface RpcParam {
|
||||
@ -138,7 +141,7 @@ export const aria2Rpc: DownloadVideoOutput = {
|
||||
const { selectedRpcProfile, isPluginDownloadAssets } = instance
|
||||
const { secretKey, dir, other } = selectedRpcProfile
|
||||
const referer = document.URL.replace(window.location.search, '')
|
||||
const ariaParamsGenerator = (url: string, title: string) => {
|
||||
const getAria2Params = (url: string, title: string) => {
|
||||
const singleInfoParams = []
|
||||
if (secretKey) {
|
||||
singleInfoParams.push(`token:${secretKey}`)
|
||||
@ -158,30 +161,29 @@ export const aria2Rpc: DownloadVideoOutput = {
|
||||
}
|
||||
}
|
||||
|
||||
// handle video params
|
||||
const videoParams = infos
|
||||
.map(info =>
|
||||
info.titledFragments.map(fragment => {
|
||||
const { url, title } = fragment
|
||||
return ariaParamsGenerator(url, title)
|
||||
return getAria2Params(url, title)
|
||||
}),
|
||||
)
|
||||
.flat()
|
||||
|
||||
// handle assets
|
||||
const assetsAriaParams = []
|
||||
const extraAssetsForBrowerDownload = []
|
||||
for (const { asset, instance: assetInstance } of extraOnlineAssets) {
|
||||
if (isPluginDownloadAssets && 'getUrls' in asset) {
|
||||
// get asset from aria2
|
||||
const results = await asset.getUrls(infos, assetInstance)
|
||||
assetsAriaParams.push(...results.map(({ name, url }) => ariaParamsGenerator(url, name)))
|
||||
} else {
|
||||
// remain asset in `extraOnlineAssets`
|
||||
extraAssetsForBrowerDownload.push({ asset, instance: assetInstance })
|
||||
}
|
||||
}
|
||||
action.extraOnlineAssets = extraAssetsForBrowerDownload
|
||||
const isAriaAsset = (asset: DownloadVideoAssets) =>
|
||||
isPluginDownloadAssets && asset.getUrls !== undefined
|
||||
const assetsAriaParams = (
|
||||
await Promise.all(
|
||||
extraOnlineAssets
|
||||
.filter(it => isAriaAsset(it.asset))
|
||||
.map(async it => {
|
||||
const { asset, instance: assetInstance } = it
|
||||
const results = await asset.getUrls(infos, assetInstance)
|
||||
return results.map(({ name, url }) => getAria2Params(url, name))
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
action.extraOnlineAssets = extraOnlineAssets.filter(it => !isAriaAsset(it.asset))
|
||||
|
||||
const totalParams = [...videoParams, ...assetsAriaParams]
|
||||
const results = await sendRpc(selectedRpcProfile, totalParams)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.9.2",
|
||||
"version": "2.9.3",
|
||||
"author": "Grant Howard, Coulomb-G",
|
||||
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
||||
"license": "MIT",
|
||||
|
||||
@ -33,7 +33,6 @@ export class CommentAreaV3 extends CommentArea {
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
super(element)
|
||||
shadowDomObserver.observe()
|
||||
this.shadowDomObserver = shadowDomObserver
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { contentLoaded } from '../life-cycle'
|
||||
import { childListSubtree } from '../observer'
|
||||
import { deleteValue } from '../utils'
|
||||
import { ShadowDomCallback, ShadowDomEntry } from './dom-entry'
|
||||
@ -81,6 +82,7 @@ export class ShadowDomObserver extends ShadowRootObserver {
|
||||
}
|
||||
|
||||
forEachShadowDom(callback: ShadowDomCallback) {
|
||||
this.observe()
|
||||
const callCurrentAndNextLevel = (currentEntry: ShadowDomEntry) => {
|
||||
callback(currentEntry)
|
||||
currentEntry.children.forEach(child => {
|
||||
@ -94,6 +96,7 @@ export class ShadowDomObserver extends ShadowRootObserver {
|
||||
}
|
||||
|
||||
watchShadowDom(callbacks: { added?: ShadowDomCallback; removed?: ShadowDomCallback }) {
|
||||
this.observe()
|
||||
this.forEachShadowDom(it => callbacks.added?.(it))
|
||||
const addedListener = (e: CustomEvent<ShadowDomEntry>) => callbacks?.added?.(e.detail)
|
||||
const removedListener = (e: CustomEvent<ShadowDomEntry>) => callbacks?.removed?.(e.detail)
|
||||
@ -109,10 +112,14 @@ export class ShadowDomObserver extends ShadowRootObserver {
|
||||
if (this.observing) {
|
||||
return
|
||||
}
|
||||
const existingRoots = ShadowRootObserver.queryAllShadowRoots()
|
||||
existingRoots.forEach(root => this.addEntry(root))
|
||||
;[this.rootObserver] = childListSubtree(document.body, records => this.mutationHandler(records))
|
||||
this.observing = true
|
||||
contentLoaded(() => {
|
||||
const existingRoots = ShadowRootObserver.queryAllShadowRoots()
|
||||
existingRoots.forEach(root => this.addEntry(root))
|
||||
;[this.rootObserver] = childListSubtree(document.body, records =>
|
||||
this.mutationHandler(records),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
||||
@ -29,7 +29,6 @@ export class ShadowRootStyles {
|
||||
|
||||
async addStyle(definition: ShadowRootStyleDefinition) {
|
||||
const { id, style } = definition
|
||||
this.observer.observe()
|
||||
const entryId = `shadow-dom-style-${id !== undefined ? lodash.kebabCase(id) : getRandomId()}`
|
||||
const styleSheet = new CSSStyleSheet()
|
||||
await styleSheet.replace(style)
|
||||
|
||||
@ -518,9 +518,18 @@ export const playerReady = async () => {
|
||||
// unsafeWindow.aid = info.aid.toString()
|
||||
// return info.aid as string
|
||||
// }
|
||||
|
||||
/** 获取当前聚焦的元素 */
|
||||
export const getActiveElement = () => {
|
||||
let { activeElement } = document
|
||||
while (activeElement.shadowRoot !== null) {
|
||||
activeElement = activeElement.shadowRoot.activeElement
|
||||
}
|
||||
return activeElement
|
||||
}
|
||||
/** 是否正在打字 */
|
||||
export const isTyping = () => {
|
||||
const { activeElement } = document
|
||||
const activeElement = getActiveElement()
|
||||
if (!activeElement) {
|
||||
return false
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user