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
87a8279ce0
@ -84,6 +84,7 @@ module.exports = {
|
||||
'lines-between-class-members': 'off',
|
||||
radix: ['error', 'as-needed'],
|
||||
'max-classes-per-file': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
'prefer-destructuring': [
|
||||
'error',
|
||||
{
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -76,6 +76,7 @@
|
||||
"preload",
|
||||
"protobuf",
|
||||
"pubdate",
|
||||
"rcount",
|
||||
"reimu",
|
||||
"reloadable",
|
||||
"reloadables",
|
||||
@ -84,6 +85,7 @@
|
||||
"reposts",
|
||||
"Roadmap",
|
||||
"roomid",
|
||||
"rpid",
|
||||
"saturationv",
|
||||
"Sendbar",
|
||||
"sortablejs",
|
||||
@ -95,8 +97,10 @@
|
||||
"Touhou",
|
||||
"truetype",
|
||||
"uname",
|
||||
"usercard",
|
||||
"userscript",
|
||||
"Violentmonkey",
|
||||
"vnode",
|
||||
"Vuex",
|
||||
"Wasm",
|
||||
"watchlater",
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@ -1,6 +1,47 @@
|
||||
<!-- spell-checker: disable -->
|
||||
# 更新日志
|
||||
|
||||
## v2.8.10-preview
|
||||
`2024-04-23`
|
||||
|
||||
包含 [v2.8.10](https://github.com/the1812/Bilibili-Evolved/releases/tag/v2.8.10) 的所有更新内容.
|
||||
|
||||
- 更新了 `动态过滤器` 的描述, 当开启 `直播信息扩充` 时不可配置 `正在直播` 的显隐. (#4705)
|
||||
|
||||
## v2.8.10
|
||||
`2024-04-23`
|
||||
|
||||
<details>
|
||||
<summary>获得 v2.8.9-preview 的所有新功能, 点击展开查看</summary>
|
||||
|
||||
- `下载视频 - WASM 混流输出` 支持更持久的缓存方式. (PR #4667 by [WakelessSloth56](https://github.com/WakelessSloth56))
|
||||
- `外置稍后再看` 和 `启用快速收藏` 增加了 `显示方式` 选项, 出现无法自动适配的排版问题时可以尝试手动调整此设置. (#4532)
|
||||
- 新增插件 `动态过滤器 - 移除商品带货动态`. (#4425)
|
||||
> 移除动态里的商品带货动态 (UP主的推荐 · 来自 XX), 装有 `动态过滤器` 时生效.
|
||||
|
||||
- 新增组件 `动态图片平铺展示`. (#4645)
|
||||
> 将动态中左右切换式的图片改回传统的平铺展示. (在动态详情中可能稍有延迟)
|
||||
|
||||
- 更换了 `自定义顶栏` 的动态提醒数字来源. (#4427)
|
||||
|
||||
</details>
|
||||
|
||||
✨新增
|
||||
- 夜间模式更新对视频和直播页面的样式. (#4701, PR #4675 by [WangYK](https://github.com/AnotiaWang))
|
||||
- 增强 `下载视频 - WASM 混流输出` 对错误输入流的检测, 支持 FLAC 音频流 (产生 `.mkv` 文件). (PR #4686 by [DBeidachazi](https://github.com/DBeidachazi))
|
||||
- `复制动态链接` 改用 `opus` 格式的链接, 并支持新版话题页面. (PR #4695, PR #4696 by [星海](https://github.com/lovelyCARDINAL))
|
||||
- 同时安装了 pakku 时, 支持下载处理后的弹幕. (PR #4712 by [xmcp](https://github.com/xmcp))
|
||||
- `传统连播模式` 支持识别和处理视频列表. (#4699, PR #4713 by [Wuch](https://github.com/IEXPECTATION))
|
||||
- `删除视频弹窗` 支持屏蔽收起后的弹窗. (#4703)
|
||||
|
||||
🐛修复
|
||||
- 再次修复 `快速收起评论` 遮挡评论的表情等弹窗.
|
||||
- 修复评论区相关功能失效. (#4690)
|
||||
- 修复 `自定义顶栏` 导致稍后再看页面布局错位. (#4668)
|
||||
|
||||
☕开发者相关
|
||||
- Comment API 现已重构并支持 Vue 3 版本的评论区. (#4690)
|
||||
|
||||
## v2.8.9-preview
|
||||
`2024-03-09`
|
||||
|
||||
|
||||
@ -29,6 +29,15 @@ https://afdian.net/@the1812?tab=sponsor
|
||||
|
||||
| 时间 | 用户名 | 单号后4位 | 金额 |
|
||||
| ------------------- | --------------------- | --------- | ------- |
|
||||
| 2024.04.08 16:02:38 | 匿名 | 2455 | ¥5.00 |
|
||||
| 2024.03.31 16:36:20 | N*r | 3391 | ¥5.20 |
|
||||
| 2024.03.27 21:25:21 | M*g | 0180 | ¥10.00 |
|
||||
| 2024.03.23 08:36:29 | 匿名 | 3363 | ¥7.00 |
|
||||
| 2024.03.23 04:28:31 | 智木人 | 2847 | ¥2.33 |
|
||||
| 2024.03.21 22:06:40 | X*r | 0185 | ¥5.00 |
|
||||
| 2024.03.20 19:34:30 | 匿名 | 5133 | ¥52.10 |
|
||||
| 2024.03.12 19:16:12 | 匿名 | 0321 | ¥20.00 |
|
||||
| 2024.03.09 21:40:26 | 匿名 | 1963 | ¥2.33 |
|
||||
| 2024.03.01 16:12:34 | 5*B | 0619 | ¥2.33 |
|
||||
| 2024.02.29 08:53:14 | M*z | 8723 | ¥2.33 |
|
||||
| 2024.02.20 14:47:10 | 匿名 | 4111 | ¥5.00 |
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"type": "component",
|
||||
"name": "feedsFilter",
|
||||
"displayName": "动态过滤器",
|
||||
"description": "按照类型或者关键词过滤动态首页的内容, 也可以移除动态页的一些侧边卡片. 详细设置请前往[动态首页](https://t.bilibili.com/)查看.\r\n",
|
||||
"description": "按照类型或者关键词过滤动态首页的内容, 也可以移除动态页的一些侧边卡片. 详细设置请前往[动态首页](https://t.bilibili.com/)查看.\r\n\r\n> 侧边卡片的 `正在直播` 指原版的板块, 如果你使用了 `直播信息扩充`, 则需要关闭该功能才能生效.\r\n",
|
||||
"fullRelativePath": "../../registry/dist/components/feeds/filter.js",
|
||||
"fullAbsolutePath": "registry/dist/components/feeds/filter.js"
|
||||
},
|
||||
@ -835,7 +835,7 @@
|
||||
"type": "component",
|
||||
"name": "removePlayerPopup",
|
||||
"displayName": "删除视频弹窗",
|
||||
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.\r\n",
|
||||
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择. 如果之前点了收起弹窗, 则收起后的小弹窗则会直接删除. (不受类别选择影响)\r\n",
|
||||
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
|
||||
"fullAbsolutePath": "registry/dist/components/video/player/remove-popup.js"
|
||||
},
|
||||
|
||||
@ -38,6 +38,8 @@
|
||||
|
||||
按照类型或者关键词过滤动态首页的内容, 也可以移除动态页的一些侧边卡片. 详细设置请前往[动态首页](https://t.bilibili.com/)查看.
|
||||
|
||||
> 侧边卡片的 `正在直播` 指原版的板块, 如果你使用了 `直播信息扩充`, 则需要关闭该功能才能生效.
|
||||
|
||||
### [强制固定动态侧栏](../../registry/dist/components/feeds/fixed-sidebars.js)
|
||||
`fixedFeedsSidebars`
|
||||
|
||||
@ -1158,7 +1160,7 @@ by [@JLoeve](https://github.com/LonelySteve)
|
||||
|
||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/player/remove-popup.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/player/remove-popup.js)
|
||||
|
||||
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.
|
||||
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择. 如果之前点了收起弹窗, 则收起后的小弹窗则会直接删除. (不受类别选择影响)
|
||||
|
||||
### [启用视频截图](../../registry/dist/components/video/player/screenshot.js)
|
||||
`videoScreenshot`
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"type": "component",
|
||||
"name": "removePlayerPopup",
|
||||
"displayName": "删除视频弹窗",
|
||||
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.\r\n",
|
||||
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择. 如果之前点了收起弹窗, 则收起后的小弹窗则会直接删除. (不受类别选择影响)\r\n",
|
||||
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
|
||||
"fullAbsolutePath": "registry/dist/components/video/player/remove-popup.js"
|
||||
},
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"caniuse-lite": "1.0.30001547",
|
||||
"caniuse-lite": "1.0.30001609",
|
||||
"consolidate": "1.0.1"
|
||||
}
|
||||
},
|
||||
|
||||
6135
pnpm-lock.yaml
generated
6135
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -99,6 +99,9 @@ const sideCards: { [id: number]: SideCardType } = {
|
||||
displayName: '发布动态',
|
||||
},
|
||||
}
|
||||
if (getComponentSettings('extendFeedsLive').enabled) {
|
||||
delete sideCards[3]
|
||||
}
|
||||
let cardsManager: typeof import('@/components/feeds/api').feedsCardsManager
|
||||
const sideBlock = 'feeds-filter-side-block-'
|
||||
|
||||
|
||||
@ -1 +1,3 @@
|
||||
按照类型或者关键词过滤动态首页的内容, 也可以移除动态页的一些侧边卡片. 详细设置请前往[动态首页](https://t.bilibili.com/)查看.
|
||||
|
||||
> 侧边卡片的 `正在直播` 指原版的板块, 如果你使用了 `直播信息扩充`, 则需要关闭该功能才能生效.
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
}
|
||||
.at-panel,
|
||||
.emoji-panel,
|
||||
.reply-box .box-expand,
|
||||
.reply-operation .operation-list {
|
||||
z-index: 111 !important;
|
||||
}
|
||||
|
||||
@ -13,7 +13,12 @@ export const component = defineComponentMetadata({
|
||||
const vueData = getVueData(card.element)
|
||||
|
||||
// 普通动态
|
||||
const path = 'data.modules.module_dynamic.major.opus.style'
|
||||
const cardType = vueData?.data?.type
|
||||
const path =
|
||||
cardType === 'DYNAMIC_TYPE_FORWARD'
|
||||
? 'data.orig.modules.module_dynamic.major.opus.style'
|
||||
: 'data.modules.module_dynamic.major.opus.style'
|
||||
|
||||
const imageViewerStyle: number | null = lodash.get(vueData, path, null)
|
||||
if (imageViewerStyle === 1) {
|
||||
lodash.set(vueData, path, undefined)
|
||||
|
||||
@ -33,7 +33,7 @@ li.nav-item[report-id="playpage_dynamic"] .i-frame,
|
||||
}
|
||||
#message-navbar,
|
||||
body:not(.history-page) #biliMainHeader:not(.z-top-container),
|
||||
.z-top-container:not(#biliMainHeader) {
|
||||
.z-top-container:not(#biliMainHeader,.has-menu) {
|
||||
max-height: var(--navbar-height) !important;
|
||||
}
|
||||
body.history-page #biliMainHeader:not(.z-top-container) {
|
||||
|
||||
@ -245,11 +245,11 @@ blockquote {
|
||||
&,
|
||||
&:link,
|
||||
&:visited {
|
||||
@include color('a');
|
||||
@include color('e');
|
||||
}
|
||||
}
|
||||
.live-skin-normal-text {
|
||||
@include color('a');
|
||||
@include color('e');
|
||||
}
|
||||
.live-skin-main-text,
|
||||
.live-skin-highlight-text {
|
||||
|
||||
@ -940,6 +940,9 @@ html {
|
||||
@include set-theme-color('border-bottom-color');
|
||||
@include theme-color();
|
||||
}
|
||||
.underline {
|
||||
@include theme-border-color();
|
||||
}
|
||||
}
|
||||
.name {
|
||||
@include set-color('border-bottom-color', 'a');
|
||||
|
||||
@ -344,6 +344,11 @@
|
||||
@include color('a');
|
||||
}
|
||||
}
|
||||
.gift-item-asset {
|
||||
.b-img {
|
||||
@include background-color();
|
||||
}
|
||||
}
|
||||
}
|
||||
.gift-sender-panel {
|
||||
.content {
|
||||
@ -374,6 +379,9 @@
|
||||
}
|
||||
.right-arrow {
|
||||
@include border-color('e');
|
||||
path {
|
||||
@include fill('e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -383,6 +391,9 @@
|
||||
@include background-color('3');
|
||||
.arrow {
|
||||
@include border-color('e');
|
||||
path {
|
||||
@include fill('e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -401,6 +412,9 @@
|
||||
}
|
||||
.bag-icon {
|
||||
@include border-color('e');
|
||||
path {
|
||||
@include fill('e');
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@include background-color('8884');
|
||||
@ -708,9 +722,7 @@
|
||||
@include set-color('border-top-color', '4');
|
||||
}
|
||||
#{contains('mediainfo_mediaRight')} {
|
||||
#{contains('mediainfo_media_desc_section')}
|
||||
#{contains('mediainfo_display_area')}
|
||||
#{contains('mediainfo_ellipsis')} {
|
||||
#{contains('mediainfo_media_desc_section')} #{contains('mediainfo_display_area')} #{contains('mediainfo_ellipsis')} {
|
||||
background: linear-gradient(90deg, hsla(0, 0%, 100%, 0), #222 20%, #222);
|
||||
@include theme-color();
|
||||
}
|
||||
@ -881,3 +893,67 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.left-part-ctnr {
|
||||
.gift-left-part > [class$='-text'] {
|
||||
@include color('e');
|
||||
}
|
||||
.pointer.arrow-box {
|
||||
@include set-color('--btnBg', '5');
|
||||
@include set-color('--btnHoverBg', '6');
|
||||
.arrow path {
|
||||
@include fill('e');
|
||||
}
|
||||
}
|
||||
.inner-part {
|
||||
@include set-color('--wrapBg', '4');
|
||||
}
|
||||
}
|
||||
.static-feed-number-list {
|
||||
.feed-button-root .content-box {
|
||||
@include color('e');
|
||||
}
|
||||
}
|
||||
.feed-button-box .custom-feed-input {
|
||||
@include color('e');
|
||||
}
|
||||
.gift-control-panel {
|
||||
.gift-panel-box {
|
||||
@include background-color();
|
||||
}
|
||||
}
|
||||
.gift-panel-container-root {
|
||||
@include set-color('--gift-panel-container_background-color', '4');
|
||||
@include set-color('--gift-panel-container_container-border-color', '5');
|
||||
@include set-color('--gift-panel-container_feed-user-info-text-color', 'e');
|
||||
}
|
||||
.left-container {
|
||||
.new-tabs {
|
||||
@include background-color('4');
|
||||
@include border-color();
|
||||
}
|
||||
}
|
||||
.card-box .switch-btn img {
|
||||
filter: invert(0.8) !important;
|
||||
}
|
||||
.package-gift-root {
|
||||
.des-box {
|
||||
@include color('a');
|
||||
.switch {
|
||||
@include set-color('--toggle-switch-bg-color', '3');
|
||||
&.activated {
|
||||
@include set-theme-color('--toggle-switch-bg-color');
|
||||
}
|
||||
}
|
||||
}
|
||||
.new-silver-wallet {
|
||||
@include color('e');
|
||||
@include background-color('3');
|
||||
}
|
||||
}
|
||||
.shortcuts-ctnr {
|
||||
.shortcut-item {
|
||||
.item-icon {
|
||||
@include to-white();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,7 +368,6 @@ button.submit-button.disabled,
|
||||
.rank-list-ctnr .tabs,
|
||||
.player-and-aside-area .aside-area,
|
||||
.player-and-aside-area .aside-area .rank-list-section,
|
||||
.gift-presets .gift-panel-box,
|
||||
.bilibili-live-player-ending-panel-recommend-card-pic,
|
||||
.rank-list-ctnr .own,
|
||||
.rank-cntr .item.own,
|
||||
|
||||
@ -13,6 +13,8 @@ const getIpLocation = (element: HTMLElement) => {
|
||||
let version = 2
|
||||
let bbComment: any
|
||||
|
||||
const marginLeft = 15
|
||||
|
||||
// 带IP属地显示的评论创建
|
||||
const createListCon = function listCon(item, i, pos) {
|
||||
const blCon = this._parentBlacklistDom(item, i, pos)
|
||||
@ -29,7 +31,9 @@ const createListCon = function listCon(item, i, pos) {
|
||||
'<span class="time-location">',
|
||||
'<span class="reply-time">'.concat(this._formateTime(item.ctime), '</span>'),
|
||||
item?.reply_control?.location
|
||||
? `<span class="reply-location">${item?.reply_control?.location || ''}</span>`
|
||||
? `<span class="reply-location" style="margin-left:${marginLeft}px;">${
|
||||
item?.reply_control?.location || ''
|
||||
}</span>`
|
||||
: '',
|
||||
'</span>',
|
||||
item.lottery_id
|
||||
@ -90,7 +94,9 @@ const createSubReplyItem = function subReply(item, i) {
|
||||
'<span class="time-location">',
|
||||
'<span class="reply-time">'.concat(this._formateTime(item.ctime), '</span>'),
|
||||
item?.reply_control?.location
|
||||
? `<span class="reply-location">${item?.reply_control?.location || ''}</span>`
|
||||
? `<span class="reply-location" style="margin-left:${marginLeft}px;">${
|
||||
item?.reply_control?.location || ''
|
||||
}</span>`
|
||||
: '',
|
||||
'</span>',
|
||||
`<span class="like ${item.action === 1 ? 'liked' : ''}"><i></i><span>${
|
||||
@ -135,7 +141,7 @@ const blackroomCreateListCon = function listCon(e, n, t) {
|
||||
this._createPlatformDom(e.content.plat),
|
||||
`<span class="time">${this._formateTime(e.ctime)}</span>`,
|
||||
e?.reply_control?.location
|
||||
? `<span style="margin-left:-15px;">${e?.reply_control?.location || ''}</span>`
|
||||
? `<span style="margin-left:${marginLeft - 15}px;">${e?.reply_control?.location || ''}</span>`
|
||||
: '',
|
||||
e.lottery_id
|
||||
? ''
|
||||
@ -197,7 +203,7 @@ const blackroomCreateSubReplyItem = function subReply(e, n) {
|
||||
'<div class="info">',
|
||||
`<span class="time">${this._formateTime(e.ctime)}</span>`,
|
||||
e?.reply_control?.location
|
||||
? `<span style="margin-left:-15px;">${e?.reply_control?.location || ''}</span>`
|
||||
? `<span style="margin-left:${marginLeft - 15}px;">${e?.reply_control?.location || ''}</span>`
|
||||
: '',
|
||||
`<span class="like ${1 === e.action ? 'liked' : ''}"><i></i><span>${
|
||||
e.like || ''
|
||||
@ -269,6 +275,7 @@ observer.observe(document.head, { childList: true })
|
||||
const processItems = (items: CommentReplyItem[]) => {
|
||||
items.forEach(item => {
|
||||
const location = getIpLocation(item.element)
|
||||
console.log(`location: ${location}`)
|
||||
if (location !== undefined) {
|
||||
const replyTime =
|
||||
item.element.querySelector('.reply-info>.reply-time') ??
|
||||
@ -276,7 +283,7 @@ const processItems = (items: CommentReplyItem[]) => {
|
||||
if (replyTime.childElementCount === 0) {
|
||||
// 避免在评论更新的情况下重复添加
|
||||
const replyLocation = document.createElement('span')
|
||||
replyLocation.style.marginLeft = '5px'
|
||||
replyLocation.style.marginLeft = `${marginLeft}px`
|
||||
replyLocation.innerText = location
|
||||
replyTime.appendChild(replyLocation)
|
||||
}
|
||||
|
||||
@ -286,9 +286,9 @@ const decode = lodash.curry(async (type: string, blob: Blob) => {
|
||||
export const decodeDanmakuSegment = decode('DmSegMobileReply')
|
||||
export const decodeDanmakuView = decode('DmWebViewReply')
|
||||
|
||||
// 这里为了兼容 pakku, 只能用 fetch https://github.com/xmcp/pakku.js/issues/153
|
||||
// 同时安装了 pakku 时,unsafeWindow.fetch 将调用 hook 过的 fetch 来获取修改后的弹幕
|
||||
const fetchBlob = async (url: string) => {
|
||||
const response = await fetch(url, { mode: 'cors', credentials: 'include' })
|
||||
const response = await unsafeWindow.fetch(url, { mode: 'cors', credentials: 'include' })
|
||||
return response.blob()
|
||||
}
|
||||
export const getDanmakuView = async (aid: string | number, cid: string | number) => {
|
||||
|
||||
@ -307,7 +307,6 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getSpeedContextMixin({ menuListElement }: SpeedContext): Partial<SpeedContext> {
|
||||
return {
|
||||
query: (speed: number) =>
|
||||
@ -444,7 +443,6 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
|
||||
setTimeout(() => this.forceUpdateStyle(value))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected readonly filterNativeSpeed =
|
||||
() =>
|
||||
({ subscribe, next }: PublishContext<number>) => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { getVueData } from '@/components/feeds/api'
|
||||
import { playerAgent } from '@/components/video/player-agent'
|
||||
import { childListSubtree, videoChange } from '@/core/observer'
|
||||
import { select } from '@/core/spin-query'
|
||||
@ -44,9 +45,50 @@ export const component = defineComponentMetadata({
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
await playerReady()
|
||||
|
||||
// 初始化状态
|
||||
const rightPanel = await Promise.any([
|
||||
select('.right-container-inner'),
|
||||
select('.playlist-container--right'),
|
||||
])
|
||||
// 如果未找到元素,提前退出不执行后续动作
|
||||
if (!rightPanel) {
|
||||
console.warn('未找到 rightPanelContainer 或 playListContainer')
|
||||
return
|
||||
}
|
||||
|
||||
const checkPlayListPlayMode = async () => {
|
||||
// 检查是不是播放列表中的最后一个视频或者最后一个分 P 视频的最后一个视频
|
||||
const videoSequentialNumber = dq('.list-count')
|
||||
const sequentialNumbers = videoSequentialNumber.innerHTML.split('/')
|
||||
// 检查最后一个元素是单个视频还是多 P 视频
|
||||
const lastVedioElement = dq(
|
||||
'.action-list .action-list-inner .action-list-item-wrap:last-child .action-list-item .actionlist-item-inner',
|
||||
)
|
||||
|
||||
let isLastVideo = false
|
||||
if (lastVedioElement.classList.contains('singlep-list-item-inner')) {
|
||||
isLastVideo = lastVedioElement.classList.contains('siglep-active')
|
||||
} else {
|
||||
isLastVideo =
|
||||
lastVedioElement.children[1].lastElementChild.classList.contains(
|
||||
'multip-list-item-active',
|
||||
)
|
||||
}
|
||||
|
||||
const shouldContinue = !(sequentialNumbers[0] >= sequentialNumbers[1] && isLastVideo)
|
||||
|
||||
const app = document.getElementById('app')
|
||||
const vueInstance = getVueData(app)
|
||||
// 不用判断当前状态是什么,直接将将是否需要继续播放赋值 vue 实例中的 continuousPlay
|
||||
vueInstance.setContinuousPlay(shouldContinue)
|
||||
}
|
||||
|
||||
const isChecked = (container: HTMLElement) =>
|
||||
Boolean(container.querySelector('.switch-button.on') || container.matches(':checked'))
|
||||
await playerReady()
|
||||
|
||||
const checkPlayMode = async () => {
|
||||
const element = (await select(
|
||||
[...autoPlayControls.disable, ...autoPlayControls.enable].join(','),
|
||||
@ -62,16 +104,19 @@ export const component = defineComponentMetadata({
|
||||
element.click()
|
||||
}
|
||||
}
|
||||
videoChange(async () => {
|
||||
checkPlayMode()
|
||||
const video = (await playerAgent.query.video.element()) as HTMLVideoElement
|
||||
video?.addEventListener('play', checkPlayMode, { once: true })
|
||||
})
|
||||
const rightPanelContainer = await select('.right-container-inner')
|
||||
if (!rightPanelContainer) {
|
||||
console.warn('未找到 rightPanelContainer')
|
||||
return
|
||||
|
||||
const checkRightPanelPlayMode = async () => {
|
||||
rightPanel.classList.contains('right-container-inner')
|
||||
? checkPlayMode()
|
||||
: checkPlayListPlayMode()
|
||||
}
|
||||
childListSubtree(rightPanelContainer, () => checkPlayMode())
|
||||
|
||||
videoChange(async () => {
|
||||
checkRightPanelPlayMode()
|
||||
const video = (await playerAgent.query.video.element()) as HTMLVideoElement
|
||||
video?.addEventListener('play', checkRightPanelPlayMode, { once: true })
|
||||
})
|
||||
|
||||
childListSubtree(rightPanel, () => checkRightPanelPlayMode())
|
||||
},
|
||||
})
|
||||
|
||||
@ -1 +1 @@
|
||||
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.
|
||||
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择. 如果之前点了收起弹窗, 则收起后的小弹窗则会直接删除. (不受类别选择影响)
|
||||
|
||||
@ -8,6 +8,13 @@ export const component = defineComponentMetadata({
|
||||
entry: ({ settings, metadata }) => {
|
||||
const { options } = settings
|
||||
const { kebabCase } = lodash
|
||||
addComponentListener(
|
||||
metadata.name,
|
||||
(enabled: boolean) => {
|
||||
document.body.classList.toggle(kebabCase(metadata.name), enabled)
|
||||
},
|
||||
true,
|
||||
)
|
||||
Object.keys(options).forEach(optionName => {
|
||||
addComponentListener(
|
||||
`${metadata.name}.${optionName}`,
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
body {
|
||||
&.remove-player-popup {
|
||||
// https://www.bilibili.com/video/BV1WA4m1L7xa/
|
||||
.bili-cmd-shrink {
|
||||
display: none !important;
|
||||
}
|
||||
// https://www.bilibili.com/video/BV1V54y1b7yW/
|
||||
&-combo-likes {
|
||||
.bili-guide,
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { childList, urlChange } from '@/core/observer'
|
||||
import { playerReady } from '@/core/utils'
|
||||
import { playerReady, getVue2Data } from '@/core/utils'
|
||||
import { videoUrls } from '@/core/utils/urls'
|
||||
import { getVueData } from '@/components/feeds/api'
|
||||
import { VideoInfo } from '@/components/video/video-info'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import { addComponentListener, getComponentSettings } from '@/core/settings'
|
||||
@ -147,7 +146,7 @@ export const component = defineComponentMetadata({
|
||||
|
||||
const getRecoList = () => {
|
||||
const reco_list = dq('#reco_list')
|
||||
let recoList: RecommendList = getVueData(reco_list)
|
||||
let recoList: RecommendList = getVue2Data(reco_list)
|
||||
if (recoList.isOpen === undefined) {
|
||||
recoList = recoList.$children[0]
|
||||
if (recoList.isOpen === undefined) {
|
||||
|
||||
@ -4,10 +4,11 @@ export const plugin: PluginMetadata = {
|
||||
name: 'feedsFilter.pluginBlocks.chargeFeeds',
|
||||
displayName: '动态过滤器 - 移除充电专属动态',
|
||||
async setup() {
|
||||
const { forEachFeedsCard, getVueData } = await import('@/components/feeds/api')
|
||||
const { getVue2Data } = await import('@/core/utils')
|
||||
const { forEachFeedsCard } = await import('@/components/feeds/api')
|
||||
forEachFeedsCard({
|
||||
added: card => {
|
||||
const vueData = getVueData(card.element)
|
||||
const vueData = getVue2Data(card.element)
|
||||
const blockedType: number | null = lodash.get(
|
||||
vueData,
|
||||
'data.modules.module_dynamic.major.blocked.blocked_type',
|
||||
|
||||
@ -34,21 +34,40 @@ async function load(toast: Toast) {
|
||||
),
|
||||
})
|
||||
}
|
||||
export async function run(name: string, videoUrl: string, audioUrl: string, toast: Toast) {
|
||||
export async function run(
|
||||
name: string,
|
||||
toast: Toast,
|
||||
videoUrl: string,
|
||||
audioUrl: string,
|
||||
isFlac: boolean,
|
||||
) {
|
||||
if (!ffmpeg.loaded) {
|
||||
await load(toast)
|
||||
}
|
||||
|
||||
ffmpeg.writeFile('video', await httpGet(videoUrl, toastProgress(toast, '正在下载视频流')))
|
||||
ffmpeg.writeFile('audio', await httpGet(audioUrl, toastProgress(toast, '正在下载音频流')))
|
||||
|
||||
toast.message = '混流中……'
|
||||
|
||||
await ffmpeg.exec(['-i', 'video', '-i', 'audio', '-c:v', 'copy', '-c:a', 'copy', 'output.mp4'])
|
||||
const outputExt = isFlac ? 'mkv' : 'mp4'
|
||||
name = isFlac ? name.replace(/.[^/.]+$/, `.${outputExt}`) : name
|
||||
await ffmpeg.exec([
|
||||
'-i',
|
||||
'video',
|
||||
'-i',
|
||||
'audio',
|
||||
'-c:v',
|
||||
'copy',
|
||||
'-c:a',
|
||||
isFlac ? 'flac' : 'copy',
|
||||
'-f',
|
||||
isFlac ? 'matroska' : 'mp4',
|
||||
`output.${outputExt}`,
|
||||
])
|
||||
|
||||
const output = await ffmpeg.readFile('output.mp4')
|
||||
const output = await ffmpeg.readFile(`output.${outputExt}`)
|
||||
const outputBlob = new Blob([output], {
|
||||
type: 'video/mp4',
|
||||
type: isFlac ? 'video/x-matroska' : 'video/mp4',
|
||||
})
|
||||
|
||||
toast.message = '完成!'
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Toast } from '@/core/toast'
|
||||
import { Toast, ToastType } from '@/core/toast'
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import { DownloadVideoOutput } from '../../../../components/video/download/types'
|
||||
import { run } from './handler'
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import type { Options } from '../../../../components/video/download'
|
||||
|
||||
const title = 'WASM 混流输出'
|
||||
const desc = '使用 WASM 在浏览器中下载并合并音视频'
|
||||
@ -22,12 +24,35 @@ export const plugin: PluginMetadata = {
|
||||
description: `${desc},运行过程中请勿关闭页面,初次使用或清除缓存后需要加载约 30 MB 的 WASM 文件`,
|
||||
runAction: async action => {
|
||||
const fragments = action.infos.flatMap(it => it.titledFragments)
|
||||
if (fragments.length !== 2) {
|
||||
Toast.error('仅支持 DASH 格式', title)
|
||||
const { dashAudioExtension, dashFlacAudioExtension, dashVideoExtension } =
|
||||
getComponentSettings<Options>('downloadVideo').options
|
||||
|
||||
if (
|
||||
fragments.length > 2 ||
|
||||
(fragments.length === 2 &&
|
||||
!(
|
||||
fragments[0].extension === dashVideoExtension &&
|
||||
(fragments[1].extension === dashAudioExtension ||
|
||||
fragments[1].extension === dashFlacAudioExtension)
|
||||
))
|
||||
) {
|
||||
Toast.error('仅支持单个视频下载', title).duration = 1500
|
||||
} else {
|
||||
const toast = Toast.info('正在加载', title)
|
||||
try {
|
||||
await run(fragments[0].title, fragments[0].url, fragments[1].url, toast)
|
||||
if (fragments.length === 2) {
|
||||
await run(
|
||||
fragments[0].title,
|
||||
toast,
|
||||
fragments[0].url,
|
||||
fragments[1].url,
|
||||
fragments[1].extension === dashFlacAudioExtension,
|
||||
) // [dash]
|
||||
} else {
|
||||
toast.type = ToastType.Error
|
||||
toast.message = '仅支持 dash 格式视频下载'
|
||||
toast.duration = 1500
|
||||
}
|
||||
} catch (error) {
|
||||
toast.close()
|
||||
Toast.error(String(error), title)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.8.9",
|
||||
"version": "2.8.10",
|
||||
"author": "Grant Howard, Coulomb-G",
|
||||
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
||||
"license": "MIT",
|
||||
|
||||
@ -5,9 +5,8 @@ import { ListAdaptorKey, FeedsCardsListAdaptor } from './adaptor'
|
||||
|
||||
export const feedsCardCallbacks: Required<FeedsCardCallback>[] = []
|
||||
|
||||
export const getVueData = (el: any) =>
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
el.__vue__ ?? el.parentElement.__vue__ ?? el.children[0].__vue__
|
||||
// TODO: 仅做兼容性导出, v2.8.11 之后可移除
|
||||
export { getVue2Data as getVueData } from '@/core/utils'
|
||||
|
||||
export const createNodeValidator =
|
||||
(className: string) =>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { childList } from '@/core/observer'
|
||||
import { getVue2Data } from '@/core/utils'
|
||||
import { sq } from '@/core/spin-query'
|
||||
import { FeedsCardsManager, FeedsCardsManagerEventType, getVueData } from './base'
|
||||
import { FeedsCardsManager, FeedsCardsManagerEventType } from './base'
|
||||
import { feedsCardTypes, isRepostType, FeedsCard, FeedsCardType } from '../types'
|
||||
|
||||
const getFeedsCardType = (element: HTMLElement) => {
|
||||
@ -85,7 +86,7 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
}
|
||||
const el = await sq(
|
||||
() => element,
|
||||
(it: any) => Boolean(getVueData(it) || !element.parentNode),
|
||||
(it: any) => Boolean(getVue2Data(it) || !element.parentNode),
|
||||
{ queryInterval: 100 },
|
||||
)
|
||||
if (element.parentNode === null) {
|
||||
@ -93,10 +94,10 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
return ''
|
||||
}
|
||||
if (el === null) {
|
||||
console.warn(el, element, getVueData(el), element.parentNode)
|
||||
console.warn(el, element, getVue2Data(el), element.parentNode)
|
||||
return ''
|
||||
}
|
||||
const vueData = getVueData(el)
|
||||
const vueData = getVue2Data(el)
|
||||
if (type === feedsCardTypes.repost) {
|
||||
const currentText = vueData.card.item.content
|
||||
const repostData = getRepostData(vueData)
|
||||
@ -135,7 +136,7 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
element.setAttribute('data-type', card.type.id.toString())
|
||||
if (isRepostType(card)) {
|
||||
const currentUsername = card.username
|
||||
const vueData = getVueData(card.element)
|
||||
const vueData = getVue2Data(card.element)
|
||||
const repostUsername = lodash.get(vueData, 'card.origin_user.info.uname', '')
|
||||
if (currentUsername === repostUsername) {
|
||||
element.setAttribute('data-self-repost', 'true')
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { childList } from '@/core/observer'
|
||||
import { descendingStringSort } from '@/core/utils/sort'
|
||||
import { pascalCase } from '@/core/utils'
|
||||
import {
|
||||
createNodeValidator,
|
||||
FeedsCardsManager,
|
||||
FeedsCardsManagerEventType,
|
||||
getVueData,
|
||||
} from './base'
|
||||
import { pascalCase, getVue2Data } from '@/core/utils'
|
||||
import { createNodeValidator, FeedsCardsManager, FeedsCardsManagerEventType } from './base'
|
||||
import { FeedsCard, FeedsCardType, feedsCardTypes, isRepostType } from '../types'
|
||||
import { selectAll } from '@/core/spin-query'
|
||||
|
||||
@ -78,7 +73,7 @@ const getText = (dynamicModule: any, cardType: FeedsCardType) => {
|
||||
return combineText(mainText, typeText)
|
||||
}
|
||||
const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
||||
const vueData = getVueData(element)
|
||||
const vueData = getVue2Data(element)
|
||||
const parseModules = (rawModules: any) => {
|
||||
if (Array.isArray(rawModules)) {
|
||||
return Object.fromEntries(
|
||||
@ -157,7 +152,7 @@ export class FeedsCardsManagerV2 extends FeedsCardsManager {
|
||||
if (!isNodeValid(node)) {
|
||||
return
|
||||
}
|
||||
const vueData = getVueData(node)
|
||||
const vueData = getVue2Data(node)
|
||||
if (!vueData) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -12,15 +12,11 @@ export class Translator {
|
||||
static map: Map<string, any>
|
||||
static regex: [RegExp, string][]
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected getValue = (node: Node) => node.nodeValue
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected setValue = (node: Node, value: string) => {
|
||||
node.nodeValue = value
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
protected getElement = (node: Node) => node as Element
|
||||
translate(node: Node) {
|
||||
let value = this.getValue(node)
|
||||
@ -103,23 +99,17 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
export class TextNodeTranslator extends Translator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getElement = (node: Node) => node.parentElement
|
||||
}
|
||||
export class TitleTranslator extends Translator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getValue = (node: Node) => (node as Element).getAttribute('title')
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setValue = (node: Node, value: string) => {
|
||||
;(node as Element).setAttribute('title', value)
|
||||
}
|
||||
}
|
||||
export class PlaceholderTranslator extends Translator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setValue = (node: Node, value: string) => {
|
||||
;(node as Element).setAttribute('placeholder', value)
|
||||
}
|
||||
|
||||
@ -1,307 +1,25 @@
|
||||
import { dqa } from '@/core/utils'
|
||||
import { allMutations, childList, childListSubtree } from '@/core/observer'
|
||||
import { contentLoaded } from '@/core/life-cycle'
|
||||
import { CommentAreaManager } from './comment/comment-area-manager'
|
||||
import { CommentAreaCallback, CommentItemCallback } from './comment/types'
|
||||
import { CommentReplyItem } from './comment/reply-item'
|
||||
|
||||
/** 表示一条评论回复 */
|
||||
export class CommentReplyItem extends EventTarget {
|
||||
/** 对应元素 */
|
||||
element: HTMLElement
|
||||
/** 评论ID */
|
||||
id: string
|
||||
/** 评论者UID */
|
||||
userId: string
|
||||
/** 用户名 */
|
||||
userName: string
|
||||
/** 评论内容 */
|
||||
content: string
|
||||
/** (仅 v1) 评论时间 (文本) */
|
||||
timeText?: string
|
||||
/** (仅 v2) 评论时间戳 */
|
||||
time?: number
|
||||
/** 点赞数 */
|
||||
likes: number
|
||||
|
||||
constructor(initParams: Omit<CommentReplyItem, keyof EventTarget>) {
|
||||
super()
|
||||
this.element = initParams.element
|
||||
this.id = initParams.id
|
||||
this.userId = initParams.userId
|
||||
this.userName = initParams.userName
|
||||
this.content = initParams.content
|
||||
this.timeText = initParams.timeText
|
||||
this.time = initParams.time
|
||||
this.likes = initParams.likes
|
||||
}
|
||||
}
|
||||
|
||||
/** 评论更新的事件类型 */
|
||||
export const RepliesUpdateEventType = 'repliesUpdate'
|
||||
/** 评论更新的事件回调类型 */
|
||||
export type RepliesUpdateEventCallback = (event: CustomEvent<CommentReplyItem[]>) => void
|
||||
const createRepliesUpdateEvent = (replies: CommentReplyItem[]) => {
|
||||
return new CustomEvent(RepliesUpdateEventType, {
|
||||
detail: replies,
|
||||
})
|
||||
}
|
||||
/** 表示一条评论 */
|
||||
export class CommentItem extends CommentReplyItem {
|
||||
/** 评论图片 */
|
||||
pictures?: string[]
|
||||
/** 回复 */
|
||||
replies: CommentReplyItem[]
|
||||
|
||||
constructor(initParams: Omit<CommentItem, keyof EventTarget | 'dispatchRepliesUpdate'>) {
|
||||
super(initParams)
|
||||
this.pictures = initParams.pictures
|
||||
this.replies = initParams.replies
|
||||
}
|
||||
|
||||
addEventListener(
|
||||
type: typeof RepliesUpdateEventType,
|
||||
callback: RepliesUpdateEventCallback | null,
|
||||
options?: EventListenerOptions | boolean,
|
||||
) {
|
||||
super.addEventListener(type, callback, options)
|
||||
}
|
||||
|
||||
removeEventListener(
|
||||
type: typeof RepliesUpdateEventType,
|
||||
callback: RepliesUpdateEventCallback | null,
|
||||
options?: EventListenerOptions | boolean,
|
||||
) {
|
||||
super.removeEventListener(type, callback, options)
|
||||
}
|
||||
|
||||
/** 触发评论更新事件 */
|
||||
dispatchRepliesUpdate(replies: CommentReplyItem[]) {
|
||||
return super.dispatchEvent(createRepliesUpdateEvent(replies))
|
||||
}
|
||||
}
|
||||
export type CommentItemCallback = (item: CommentItem) => void
|
||||
/** 表示一个评论区 */
|
||||
export interface CommentArea {
|
||||
/** 对应元素 */
|
||||
element: HTMLElement
|
||||
/** 评论列表 */
|
||||
items: CommentItem[]
|
||||
/** 与之关联的 MutationObserver */
|
||||
observer?: MutationObserver
|
||||
}
|
||||
/** 当前页面所有的评论区列表 */
|
||||
export const commentAreas: CommentArea[] = []
|
||||
export type CommentAreaCallback = (area: CommentArea) => void
|
||||
|
||||
const commentAreaClasses = ['bili-comment', 'bb-comment']
|
||||
const replyItemClasses = ['list-item.reply-wrap', 'reply-item']
|
||||
const itemAddedCallbacks: CommentItemCallback[] = []
|
||||
const itemRemovedCallbacks: CommentItemCallback[] = []
|
||||
const commentAreaCallbacks: CommentAreaCallback[] = []
|
||||
|
||||
/** (v2) 获取 Vue 数据 (评论 / 回复) */
|
||||
const getVueData = (element: HTMLElement) => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const props = (element as any).__vueParentComponent?.props
|
||||
return props?.reply ?? props?.subReply
|
||||
}
|
||||
/** (v2) 获取回复对应的元素 */
|
||||
const getReplyItemElement = (parent: HTMLElement, replyId: string) => {
|
||||
const [replyElement] = dqa(parent, '.sub-reply-item').filter(
|
||||
(it: HTMLElement) => getVueData(it).rpid_str === replyId,
|
||||
)
|
||||
return replyElement as HTMLElement
|
||||
}
|
||||
/** (v1 / v2) 获取评论 ID */
|
||||
const getCommentId = (element: HTMLElement) => {
|
||||
const attributeId = element.getAttribute('data-id')
|
||||
if (attributeId) {
|
||||
return attributeId
|
||||
}
|
||||
const vueData = getVueData(element)
|
||||
return vueData?.rpid_str ?? ''
|
||||
}
|
||||
|
||||
/** (v2) 解析评论对象 */
|
||||
const parseCommentItemV2 = (element: HTMLElement) => {
|
||||
const vueData = getVueData(element)
|
||||
if (!vueData) {
|
||||
throw new Error('Invalid comment item')
|
||||
}
|
||||
const parseReplies = () => {
|
||||
if (!vueData.replies) {
|
||||
return []
|
||||
}
|
||||
return vueData.replies.map((r: any): CommentReplyItem => {
|
||||
return new CommentReplyItem({
|
||||
id: r.rpid_str,
|
||||
element: getReplyItemElement(element, r.rpid_str),
|
||||
userId: r.member.mid,
|
||||
userName: r.member.uname,
|
||||
content: r.content.message,
|
||||
time: r.ctime * 1000,
|
||||
likes: r.like,
|
||||
})
|
||||
})
|
||||
}
|
||||
const item = new CommentItem({
|
||||
id: vueData.rpid_str,
|
||||
element,
|
||||
userId: vueData.member.mid,
|
||||
userName: vueData.member.uname,
|
||||
content: vueData.content.message,
|
||||
time: vueData.ctime * 1000,
|
||||
likes: vueData.like,
|
||||
pictures: vueData.content?.pictures?.map(img => {
|
||||
return img.img_src
|
||||
}),
|
||||
replies: parseReplies(),
|
||||
})
|
||||
if (item.replies.length < vueData.rcount) {
|
||||
const replyBox = dq(element, '.sub-reply-list')
|
||||
childList(replyBox, records => {
|
||||
item.replies = parseReplies()
|
||||
if (records.length !== 0) {
|
||||
item.dispatchRepliesUpdate(item.replies)
|
||||
}
|
||||
})
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
/** (v1 / v2) 解析评论对象 */
|
||||
const parseCommentItem = (element: HTMLElement) => {
|
||||
const user = element.querySelector('.con .user .name') as HTMLElement
|
||||
if (!user) {
|
||||
return parseCommentItemV2(element)
|
||||
}
|
||||
const parseReplyItem = (replyElement: HTMLElement) => {
|
||||
const replyFace = replyElement.querySelector('.reply-face') as HTMLElement
|
||||
const replyUser = replyElement.querySelector('.reply-con .user .name') as HTMLElement
|
||||
return new CommentReplyItem({
|
||||
id: replyElement.getAttribute('data-id'),
|
||||
element: replyElement,
|
||||
userId: replyFace.getAttribute('data-usercard-mid'),
|
||||
userName: replyUser.textContent,
|
||||
content: replyElement.querySelector('.text-con').textContent,
|
||||
timeText: replyElement.querySelector('.info .time, .info .time-location').textContent,
|
||||
likes: parseInt(replyElement.querySelector('.info .like span').textContent),
|
||||
})
|
||||
}
|
||||
const item = new CommentItem({
|
||||
id: element.getAttribute('data-id'),
|
||||
element,
|
||||
userId: user.getAttribute('data-usercard-mid'),
|
||||
userName: user.textContent,
|
||||
content: element.querySelector('.con .text').textContent,
|
||||
timeText: element.querySelector('.con .info .time, .info .time-location').textContent,
|
||||
likes: parseInt(element.querySelector('.con .like span').textContent),
|
||||
replies: [],
|
||||
})
|
||||
if (dq(element, '.reply-box .view-more')) {
|
||||
const replyBox = dq(element, '.reply-box') as HTMLElement
|
||||
childList(replyBox, records => {
|
||||
item.replies = dqa(element, '.reply-box .reply-item').map(parseReplyItem)
|
||||
if (records.length !== 0) {
|
||||
item.dispatchRepliesUpdate(item.replies)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
item.replies = dqa(element, '.reply-box .reply-item').map(parseReplyItem)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
const observeItems = (area: CommentArea) => {
|
||||
if (area.observer) {
|
||||
return
|
||||
}
|
||||
const replyItemSelector = replyItemClasses.map(c => `.${c}`).join(',')
|
||||
area.items = dqa(area.element, replyItemSelector).map(parseCommentItem)
|
||||
area.items.forEach(item => {
|
||||
itemAddedCallbacks.forEach(c => c(item))
|
||||
})
|
||||
;[area.observer] = childListSubtree(area.element, records => {
|
||||
records.forEach(r => {
|
||||
const isCommentItem = (n: Node): n is HTMLElement =>
|
||||
n instanceof HTMLElement && n.matches(replyItemSelector)
|
||||
r.addedNodes.forEach(n => {
|
||||
if (isCommentItem(n)) {
|
||||
const commentItem = parseCommentItem(n)
|
||||
area.items.push(commentItem)
|
||||
itemAddedCallbacks.forEach(c => c(commentItem))
|
||||
}
|
||||
})
|
||||
r.removedNodes.forEach(n => {
|
||||
if (isCommentItem(n)) {
|
||||
const id = getCommentId(n)
|
||||
const index = area.items.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
const [commentItem] = area.items.splice(index, 1)
|
||||
itemRemovedCallbacks.forEach(c => c(commentItem))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
const observeAreas = (node: Node) => {
|
||||
if (node instanceof HTMLElement && commentAreaClasses.some(c => node.classList.contains(c))) {
|
||||
const area: CommentArea = {
|
||||
element: node,
|
||||
items: [],
|
||||
}
|
||||
commentAreas.push(area)
|
||||
observeItems(area)
|
||||
commentAreaCallbacks.forEach(c => c(area))
|
||||
}
|
||||
}
|
||||
const init = () => {
|
||||
allMutations(records => {
|
||||
records.forEach(r => {
|
||||
r.addedNodes.forEach(n => observeAreas(n))
|
||||
})
|
||||
})
|
||||
dqa(commentAreaClasses.map(c => `.${c}`).join(',')).forEach(observeAreas)
|
||||
}
|
||||
contentLoaded(init)
|
||||
export const commentAreaManager = new CommentAreaManager()
|
||||
contentLoaded(() => {
|
||||
commentAreaManager.init()
|
||||
})
|
||||
|
||||
/** 为每一个评论区执行操作 */
|
||||
export const forEachCommentArea = (callback: CommentAreaCallback) => {
|
||||
commentAreas.forEach(it => callback(it))
|
||||
commentAreaCallbacks.push(callback)
|
||||
commentAreaManager.forEachCommentArea(callback)
|
||||
}
|
||||
/** 为每一条评论执行操作 (不包含评论的回复) */
|
||||
export const forEachCommentItem = (callbacks: {
|
||||
added?: CommentItemCallback
|
||||
removed?: CommentItemCallback
|
||||
}) => {
|
||||
const { added, removed } = callbacks
|
||||
forEachCommentArea(area => {
|
||||
if (added) {
|
||||
area.items.forEach(item => added(item))
|
||||
itemAddedCallbacks.push(added)
|
||||
}
|
||||
if (removed) {
|
||||
itemRemovedCallbacks.push(removed)
|
||||
}
|
||||
})
|
||||
commentAreaManager.forEachCommentItem(callbacks)
|
||||
}
|
||||
|
||||
const addMenuItemV2 = (...params: Parameters<typeof addMenuItem>) => {
|
||||
const [item, config] = params
|
||||
const operationList = dq(item.element, '.operation-list') as HTMLUListElement
|
||||
const { className, text, action } = config
|
||||
if (!operationList || dq(operationList, `.${className}`)) {
|
||||
return
|
||||
}
|
||||
const menuItem = document.createElement('li')
|
||||
menuItem.classList.add(className, 'operation-option')
|
||||
menuItem.innerHTML = `<span class="option-title">${text}</span>`
|
||||
menuItem.addEventListener('click', event => {
|
||||
action(event)
|
||||
operationList.style.display = 'none'
|
||||
})
|
||||
operationList.appendChild(menuItem)
|
||||
}
|
||||
/**
|
||||
* 向评论的菜单中添加菜单项
|
||||
* @param item 评论
|
||||
@ -315,21 +33,5 @@ export const addMenuItem = (
|
||||
action: (e: MouseEvent) => void
|
||||
},
|
||||
) => {
|
||||
const operationList = dq(item.element, '.opera-list ul') as HTMLUListElement
|
||||
if (!operationList) {
|
||||
addMenuItemV2(item, config)
|
||||
return
|
||||
}
|
||||
const { className, text, action } = config
|
||||
if (dq(operationList, `.${className}`)) {
|
||||
return
|
||||
}
|
||||
const menuItem = document.createElement('li')
|
||||
menuItem.classList.add(className)
|
||||
menuItem.textContent = text
|
||||
menuItem.addEventListener('click', event => {
|
||||
action(event)
|
||||
operationList.style.display = 'none'
|
||||
})
|
||||
operationList.appendChild(menuItem)
|
||||
commentAreaManager.forEachCommentArea(area => area.addMenuItem(item, config))
|
||||
}
|
||||
|
||||
111
src/components/utils/comment/areas/base.ts
Normal file
111
src/components/utils/comment/areas/base.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { childListSubtree } from '@/core/observer'
|
||||
import type { CommentItem } from '../comment-item'
|
||||
import type { CommentItemCallback } from '../types'
|
||||
import type { CommentReplyItem } from '../reply-item'
|
||||
import { getRandomId } from '@/core/utils'
|
||||
|
||||
/** 表示一个评论区 */
|
||||
export abstract class CommentArea {
|
||||
/** 对应元素 */
|
||||
element: HTMLElement
|
||||
/** 评论列表 */
|
||||
items: CommentItem[] = []
|
||||
/** 与之关联的 MutationObserver */
|
||||
protected observer?: MutationObserver
|
||||
protected itemAddedCallbacks: CommentItemCallback[] = []
|
||||
protected itemRemovedCallbacks: CommentItemCallback[] = []
|
||||
protected static replyItemClasses = ['list-item.reply-wrap', 'reply-item']
|
||||
protected static replyItemSelector = CommentArea.replyItemClasses.map(c => `.${c}`).join(',')
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
abstract parseCommentItem(element: HTMLElement): CommentItem
|
||||
abstract getCommentId(element: HTMLElement): string
|
||||
abstract addMenuItem(
|
||||
item: CommentReplyItem,
|
||||
config: {
|
||||
className: string
|
||||
text: string
|
||||
action: (e: MouseEvent) => void
|
||||
},
|
||||
): void
|
||||
|
||||
protected isCommentItem(n: Node): n is HTMLElement {
|
||||
return n instanceof HTMLElement && n.matches(CommentArea.replyItemSelector)
|
||||
}
|
||||
|
||||
/** 在每一轮 CommentItem 解析前调用 */
|
||||
protected beforeParse(elements: HTMLElement[]) {
|
||||
return lodash.noop(elements)
|
||||
}
|
||||
|
||||
observeItems() {
|
||||
if (this.observer) {
|
||||
return
|
||||
}
|
||||
performance.mark('observeItems start')
|
||||
const elements = dqa(this.element, CommentArea.replyItemSelector) as HTMLElement[]
|
||||
if (elements.length > 0) {
|
||||
this.beforeParse(elements)
|
||||
this.items = elements.map(it => this.parseCommentItem(it as HTMLElement))
|
||||
}
|
||||
this.items.forEach(item => {
|
||||
this.itemAddedCallbacks.forEach(c => c(item))
|
||||
})
|
||||
;[this.observer] = childListSubtree(this.element, records => {
|
||||
const observerCallId = getRandomId()
|
||||
performance.mark(`observeItems subtree start ${observerCallId}`)
|
||||
const addedCommentElements: HTMLElement[] = []
|
||||
const removedCommentElements: HTMLElement[] = []
|
||||
records.forEach(r => {
|
||||
r.addedNodes.forEach(n => {
|
||||
if (this.isCommentItem(n)) {
|
||||
addedCommentElements.push(n)
|
||||
}
|
||||
})
|
||||
r.removedNodes.forEach(n => {
|
||||
if (this.isCommentItem(n)) {
|
||||
removedCommentElements.push(n)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (addedCommentElements.length > 0) {
|
||||
this.beforeParse(addedCommentElements)
|
||||
}
|
||||
addedCommentElements.forEach(n => {
|
||||
const commentItem = this.parseCommentItem(n)
|
||||
this.items.push(commentItem)
|
||||
this.itemAddedCallbacks.forEach(c => c(commentItem))
|
||||
})
|
||||
removedCommentElements.forEach(n => {
|
||||
const id = this.getCommentId(n)
|
||||
const index = this.items.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
const [commentItem] = this.items.splice(index, 1)
|
||||
this.itemRemovedCallbacks.forEach(c => c(commentItem))
|
||||
}
|
||||
})
|
||||
performance.mark(`observeItems subtree end ${observerCallId}`)
|
||||
performance.measure(
|
||||
`observeItems subtree ${observerCallId}`,
|
||||
`observeItems subtree start ${observerCallId}`,
|
||||
`observeItems subtree end ${observerCallId}`,
|
||||
)
|
||||
})
|
||||
performance.mark('observeItems end')
|
||||
performance.measure('observeItems', 'observeItems start', 'observeItems end')
|
||||
}
|
||||
|
||||
forEachCommentItem(callbacks: { added?: CommentItemCallback; removed?: CommentItemCallback }) {
|
||||
const { added, removed } = callbacks
|
||||
if (added) {
|
||||
this.items.forEach(item => added(item))
|
||||
this.itemAddedCallbacks.push(added)
|
||||
}
|
||||
if (removed) {
|
||||
this.itemRemovedCallbacks.push(removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/components/utils/comment/areas/v1.ts
Normal file
79
src/components/utils/comment/areas/v1.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { childList } from '@/core/observer'
|
||||
import { CommentItem } from '../comment-item'
|
||||
import { CommentReplyItem } from '../reply-item'
|
||||
import { CommentArea } from './base'
|
||||
|
||||
export class CommentAreaV1 extends CommentArea {
|
||||
addMenuItem(
|
||||
item: CommentReplyItem,
|
||||
config: { className: string; text: string; action: (e: MouseEvent) => void },
|
||||
): void {
|
||||
if (!this.element.contains(item.element)) {
|
||||
return
|
||||
}
|
||||
const operationList = dq(item.element, '.opera-list ul') as HTMLUListElement
|
||||
if (!operationList) {
|
||||
throw new Error('Invalid operation list')
|
||||
}
|
||||
const { className, text, action } = config
|
||||
if (dq(operationList, `.${className}`)) {
|
||||
return
|
||||
}
|
||||
const menuItem = document.createElement('li')
|
||||
menuItem.classList.add(className)
|
||||
menuItem.textContent = text
|
||||
menuItem.addEventListener('click', event => {
|
||||
action(event)
|
||||
operationList.style.display = 'none'
|
||||
})
|
||||
operationList.appendChild(menuItem)
|
||||
}
|
||||
parseCommentItem(element: HTMLElement): CommentItem {
|
||||
const user = element.querySelector('.con .user .name') as HTMLElement
|
||||
if (!user) {
|
||||
throw new Error('Invalid comment item')
|
||||
}
|
||||
const parseReplyItem = (replyElement: HTMLElement) => {
|
||||
const replyFace = replyElement.querySelector('.reply-face') as HTMLElement
|
||||
const replyUser = replyElement.querySelector('.reply-con .user .name') as HTMLElement
|
||||
return new CommentReplyItem({
|
||||
id: replyElement.getAttribute('data-id'),
|
||||
element: replyElement,
|
||||
userId: replyFace.getAttribute('data-usercard-mid'),
|
||||
userName: replyUser.textContent,
|
||||
content: replyElement.querySelector('.text-con').textContent,
|
||||
timeText: replyElement.querySelector('.info .time, .info .time-location').textContent,
|
||||
likes: parseInt(replyElement.querySelector('.info .like span').textContent),
|
||||
})
|
||||
}
|
||||
const item = new CommentItem({
|
||||
id: element.getAttribute('data-id'),
|
||||
element,
|
||||
userId: user.getAttribute('data-usercard-mid'),
|
||||
userName: user.textContent,
|
||||
content: element.querySelector('.con .text').textContent,
|
||||
timeText: element.querySelector('.con .info .time, .info .time-location').textContent,
|
||||
likes: parseInt(element.querySelector('.con .like span').textContent),
|
||||
replies: [],
|
||||
})
|
||||
if (dq(element, '.reply-box .view-more')) {
|
||||
const replyBox = dq(element, '.reply-box') as HTMLElement
|
||||
childList(replyBox, records => {
|
||||
item.replies = dqa(element, '.reply-box .reply-item').map(parseReplyItem)
|
||||
if (records.length !== 0) {
|
||||
item.dispatchRepliesUpdate(item.replies)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
item.replies = dqa(element, '.reply-box .reply-item').map(parseReplyItem)
|
||||
}
|
||||
return item
|
||||
}
|
||||
getCommentId(element: HTMLElement): string {
|
||||
const attributeId = element.getAttribute('data-id')
|
||||
if (!attributeId) {
|
||||
throw new Error('Invalid comment item')
|
||||
}
|
||||
return attributeId
|
||||
}
|
||||
}
|
||||
157
src/components/utils/comment/areas/v2.ts
Normal file
157
src/components/utils/comment/areas/v2.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { childList } from '@/core/observer'
|
||||
import { CommentItem } from '../comment-item'
|
||||
import { CommentArea } from './base'
|
||||
import { CommentReplyItem } from '../reply-item'
|
||||
import { HTMLElementWithVue, VNodeManager } from '../vnode-manager'
|
||||
|
||||
export class CommentAreaV2 extends CommentArea {
|
||||
private vnodeManager: VNodeManager
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
super(element)
|
||||
this.vnodeManager = new VNodeManager(this.getVNodeRoot(element as HTMLElementWithVue))
|
||||
this.vnodeManager.isEnd = vnode => {
|
||||
if (vnode.props?.reply) {
|
||||
return vnode.props?.reply.replies.length === 0
|
||||
}
|
||||
return Boolean(vnode.props?.subReply)
|
||||
}
|
||||
}
|
||||
|
||||
protected beforeParse(elements: HTMLElementWithVue[]) {
|
||||
const rootElements = new Set<HTMLElementWithVue>()
|
||||
elements.forEach(replyElement => {
|
||||
rootElements.add(this.vnodeManager.traverseToRoot(replyElement))
|
||||
dqa(replyElement, '.sub-reply-item').forEach(subReplyElement => {
|
||||
rootElements.add(this.vnodeManager.traverseToRoot(subReplyElement as HTMLElementWithVue))
|
||||
})
|
||||
})
|
||||
rootElements.forEach(rootElement => this.vnodeManager.exposeVNode(rootElement._vnode))
|
||||
}
|
||||
|
||||
/** 获取 Vue 数据 (评论 / 回复) */
|
||||
protected getReplyFromVueData(element: HTMLElement) {
|
||||
const props = (element as HTMLElementWithVue)._vnode?.props
|
||||
return props?.reply ?? props?.subReply
|
||||
}
|
||||
|
||||
/** 获取回复对应的元素 */
|
||||
protected getReplyItemElement(parent: HTMLElement, replyId: string) {
|
||||
const [replyElement] = dqa(parent, '.sub-reply-item').filter(
|
||||
(it: HTMLElement) => this.getReplyFromVueData(it).rpid_str === replyId,
|
||||
)
|
||||
return replyElement as HTMLElement
|
||||
}
|
||||
|
||||
protected getVNodeRoot(element: HTMLElementWithVue) {
|
||||
if (element.__vue_app__) {
|
||||
return element
|
||||
}
|
||||
if (element.parentElement.__vue_app__) {
|
||||
return element.parentElement
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
addMenuItem(
|
||||
item: CommentReplyItem,
|
||||
config: { className: string; text: string; action: (e: MouseEvent) => void },
|
||||
): void {
|
||||
if (!this.element.contains(item.element)) {
|
||||
return
|
||||
}
|
||||
const operationList = dq(item.element, '.operation-list') as HTMLUListElement
|
||||
const { className, text, action } = config
|
||||
if (!operationList || dq(operationList, `.${className}`)) {
|
||||
return
|
||||
}
|
||||
const menuItem = document.createElement('li')
|
||||
menuItem.classList.add(className, 'operation-option')
|
||||
menuItem.innerHTML = `<span class="option-title">${text}</span>`
|
||||
menuItem.addEventListener('click', event => {
|
||||
action(event)
|
||||
operationList.style.display = 'none'
|
||||
})
|
||||
operationList.appendChild(menuItem)
|
||||
}
|
||||
|
||||
parseCommentItem(element: HTMLElement): CommentItem {
|
||||
const vueData = this.getReplyFromVueData(element)
|
||||
if (!vueData) {
|
||||
throw new Error('Invalid comment item')
|
||||
}
|
||||
const parseReplies = () => {
|
||||
if (!vueData.replies) {
|
||||
return []
|
||||
}
|
||||
return vueData.replies.map((r: any): CommentReplyItem => {
|
||||
return new CommentReplyItem({
|
||||
id: r.rpid_str,
|
||||
element: this.getReplyItemElement(element, r.rpid_str),
|
||||
userId: r.member.mid,
|
||||
userName: r.member.uname,
|
||||
content: r.content.message,
|
||||
time: r.ctime * 1000,
|
||||
likes: r.like,
|
||||
})
|
||||
})
|
||||
}
|
||||
const item = new CommentItem({
|
||||
id: vueData.rpid_str,
|
||||
element,
|
||||
userId: vueData.member.mid,
|
||||
userName: vueData.member.uname,
|
||||
content: vueData.content.message,
|
||||
time: vueData.ctime * 1000,
|
||||
likes: vueData.like,
|
||||
pictures: vueData.content?.pictures?.map((img: any) => {
|
||||
return img.img_src
|
||||
}),
|
||||
replies: parseReplies(),
|
||||
})
|
||||
if (item.replies.length < vueData.rcount) {
|
||||
const replyBox = dq(element, '.sub-reply-list')
|
||||
childList(replyBox, records => {
|
||||
performance.mark(`parseReplies start ${item.id}`)
|
||||
const addedCommentElements: HTMLElement[] = []
|
||||
records.forEach(r => {
|
||||
r.addedNodes.forEach(n => {
|
||||
if (n instanceof HTMLElement && n.matches('.sub-reply-item')) {
|
||||
addedCommentElements.push(n)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (addedCommentElements.length === 0) {
|
||||
return
|
||||
}
|
||||
this.beforeParse(addedCommentElements)
|
||||
item.replies = parseReplies()
|
||||
if (records.length !== 0) {
|
||||
item.dispatchRepliesUpdate(item.replies)
|
||||
}
|
||||
performance.mark(`parseReplies end ${item.id}`)
|
||||
performance.measure(
|
||||
`parseReplies ${item.id}`,
|
||||
`parseReplies start ${item.id}`,
|
||||
`parseReplies end ${item.id}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
return item
|
||||
}
|
||||
getCommentId(element: HTMLElement): string {
|
||||
const vueData = this.getReplyFromVueData(element)
|
||||
if (!vueData?.rpid_str) {
|
||||
throw new Error('Invalid comment item')
|
||||
}
|
||||
return vueData.rpid_str
|
||||
}
|
||||
|
||||
static isV2Area(element: HTMLElement) {
|
||||
return (
|
||||
(element as HTMLElementWithVue).__vue_app__ ||
|
||||
(element as HTMLElementWithVue).parentElement?.__vue_app__
|
||||
)
|
||||
}
|
||||
}
|
||||
44
src/components/utils/comment/comment-area-manager.ts
Normal file
44
src/components/utils/comment/comment-area-manager.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { allMutations } from '@/core/observer'
|
||||
import { getCommentArea } from './comment-area'
|
||||
import { CommentAreaCallback, CommentItemCallback } from './types'
|
||||
import { CommentArea } from './areas/base'
|
||||
|
||||
export class CommentAreaManager {
|
||||
/** 当前页面所有的评论区列表 */
|
||||
commentAreas: CommentArea[] = []
|
||||
commentAreaCallbacks: CommentAreaCallback[] = []
|
||||
|
||||
protected static commentAreaClasses = ['bili-comment', 'bb-comment']
|
||||
|
||||
init() {
|
||||
allMutations(records => {
|
||||
records.forEach(r => {
|
||||
r.addedNodes.forEach(n => this.observeAreas(n))
|
||||
})
|
||||
})
|
||||
dqa(CommentAreaManager.commentAreaClasses.map(c => `.${c}`).join(',')).forEach(it =>
|
||||
this.observeAreas(it),
|
||||
)
|
||||
}
|
||||
|
||||
observeAreas(node: Node) {
|
||||
if (
|
||||
node instanceof HTMLElement &&
|
||||
CommentAreaManager.commentAreaClasses.some(c => node.classList.contains(c))
|
||||
) {
|
||||
const area = getCommentArea(node)
|
||||
this.commentAreas.push(area)
|
||||
area.observeItems()
|
||||
this.commentAreaCallbacks.forEach(c => c(area))
|
||||
}
|
||||
}
|
||||
|
||||
forEachCommentArea(callback: CommentAreaCallback) {
|
||||
this.commentAreas.forEach(it => callback(it))
|
||||
this.commentAreaCallbacks.push(callback)
|
||||
}
|
||||
|
||||
forEachCommentItem(callbacks: { added?: CommentItemCallback; removed?: CommentItemCallback }) {
|
||||
this.forEachCommentArea(area => area.forEachCommentItem(callbacks))
|
||||
}
|
||||
}
|
||||
10
src/components/utils/comment/comment-area.ts
Normal file
10
src/components/utils/comment/comment-area.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { CommentArea } from './areas/base'
|
||||
import { CommentAreaV1 } from './areas/v1'
|
||||
import { CommentAreaV2 } from './areas/v2'
|
||||
|
||||
export const getCommentArea = (element: HTMLElement): CommentArea => {
|
||||
if (CommentAreaV2.isV2Area(element)) {
|
||||
return new CommentAreaV2(element)
|
||||
}
|
||||
return new CommentAreaV1(element)
|
||||
}
|
||||
42
src/components/utils/comment/comment-item.ts
Normal file
42
src/components/utils/comment/comment-item.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { CommentReplyItem } from './reply-item'
|
||||
import { RepliesUpdateEventCallback, RepliesUpdateEventType } from './types'
|
||||
|
||||
const createRepliesUpdateEvent = (replies: CommentReplyItem[]) => {
|
||||
return new CustomEvent(RepliesUpdateEventType, {
|
||||
detail: replies,
|
||||
})
|
||||
}
|
||||
/** 表示一条评论 */
|
||||
export class CommentItem extends CommentReplyItem {
|
||||
/** 评论图片 */
|
||||
pictures?: string[]
|
||||
/** 回复 */
|
||||
replies: CommentReplyItem[]
|
||||
|
||||
constructor(initParams: Omit<CommentItem, keyof EventTarget | 'dispatchRepliesUpdate'>) {
|
||||
super(initParams)
|
||||
this.pictures = initParams.pictures ?? []
|
||||
this.replies = initParams.replies
|
||||
}
|
||||
|
||||
addEventListener(
|
||||
type: typeof RepliesUpdateEventType,
|
||||
callback: RepliesUpdateEventCallback | null,
|
||||
options?: EventListenerOptions | boolean,
|
||||
) {
|
||||
super.addEventListener(type, callback, options)
|
||||
}
|
||||
|
||||
removeEventListener(
|
||||
type: typeof RepliesUpdateEventType,
|
||||
callback: RepliesUpdateEventCallback | null,
|
||||
options?: EventListenerOptions | boolean,
|
||||
) {
|
||||
super.removeEventListener(type, callback, options)
|
||||
}
|
||||
|
||||
/** 触发评论更新事件 */
|
||||
dispatchRepliesUpdate(replies: CommentReplyItem[]) {
|
||||
return super.dispatchEvent(createRepliesUpdateEvent(replies))
|
||||
}
|
||||
}
|
||||
31
src/components/utils/comment/reply-item.ts
Normal file
31
src/components/utils/comment/reply-item.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/** 表示一条评论回复 */
|
||||
export class CommentReplyItem extends EventTarget {
|
||||
/** 对应元素 */
|
||||
element: HTMLElement
|
||||
/** 评论ID */
|
||||
id: string
|
||||
/** 评论者UID */
|
||||
userId: string
|
||||
/** 用户名 */
|
||||
userName: string
|
||||
/** 评论内容 */
|
||||
content: string
|
||||
/** (仅 v1) 评论时间 (文本) */
|
||||
timeText?: string
|
||||
/** (仅 v2) 评论时间戳 */
|
||||
time?: number
|
||||
/** 点赞数 */
|
||||
likes: number
|
||||
|
||||
constructor(initParams: Omit<CommentReplyItem, keyof EventTarget>) {
|
||||
super()
|
||||
this.element = initParams.element
|
||||
this.id = initParams.id
|
||||
this.userId = initParams.userId
|
||||
this.userName = initParams.userName
|
||||
this.content = initParams.content
|
||||
this.timeText = initParams.timeText
|
||||
this.time = initParams.time
|
||||
this.likes = initParams.likes
|
||||
}
|
||||
}
|
||||
10
src/components/utils/comment/types.ts
Normal file
10
src/components/utils/comment/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { CommentReplyItem } from './reply-item'
|
||||
import type { CommentItem } from './comment-item'
|
||||
import type { CommentArea } from './comment-area'
|
||||
|
||||
/** 评论更新的事件类型 */
|
||||
export const RepliesUpdateEventType = 'repliesUpdate'
|
||||
/** 评论更新的事件回调类型 */
|
||||
export type RepliesUpdateEventCallback = (event: CustomEvent<CommentReplyItem[]>) => void
|
||||
export type CommentItemCallback = (item: CommentItem) => void
|
||||
export type CommentAreaCallback = (area: CommentArea) => void
|
||||
103
src/components/utils/comment/vnode-manager.ts
Normal file
103
src/components/utils/comment/vnode-manager.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
export const VNodeTargets = Symbol.for('VNodeTargets')
|
||||
export interface HTMLElementWithVue extends HTMLElement {
|
||||
_vnode?: any
|
||||
__vue_app__?: any
|
||||
parentElement: HTMLElementWithVue | null
|
||||
[VNodeTargets]?: HTMLElementWithVue[]
|
||||
}
|
||||
export interface Vue3Vnode {
|
||||
type: string | symbol
|
||||
el?: HTMLElementWithVue
|
||||
props?: Record<string, any>
|
||||
component?: {
|
||||
subTree?: Vue3Vnode
|
||||
}
|
||||
children?: Vue3Vnode[]
|
||||
}
|
||||
|
||||
export class VNodeManager {
|
||||
public isEnd: (vnode: Vue3Vnode) => boolean = () => true
|
||||
|
||||
constructor(public rootElement: HTMLElementWithVue) {}
|
||||
|
||||
/** 从结束元素向上写入递归范围 */
|
||||
traverseToRoot(element: HTMLElementWithVue): HTMLElementWithVue {
|
||||
performance.mark('traverseToRoot')
|
||||
if (element._vnode) {
|
||||
return element
|
||||
}
|
||||
let currentElement = element
|
||||
while (currentElement.parentElement !== null && currentElement !== this.rootElement) {
|
||||
if (currentElement.parentElement[VNodeTargets]) {
|
||||
if (!currentElement.parentElement[VNodeTargets].includes(currentElement)) {
|
||||
currentElement.parentElement[VNodeTargets].push(currentElement)
|
||||
}
|
||||
if (currentElement.parentElement._vnode) {
|
||||
return currentElement.parentElement
|
||||
}
|
||||
// 如果其他 element 已经标记过此处, 可以直接 break 掉完成循环
|
||||
break
|
||||
}
|
||||
|
||||
currentElement.parentElement[VNodeTargets] = [currentElement]
|
||||
if (currentElement.parentElement._vnode) {
|
||||
return currentElement.parentElement
|
||||
}
|
||||
currentElement = currentElement.parentElement
|
||||
}
|
||||
return this.rootElement
|
||||
}
|
||||
|
||||
/** 解开组件实例的包装 */
|
||||
private unwrapSubtree(vnode: Vue3Vnode): Vue3Vnode {
|
||||
if (vnode.component?.subTree) {
|
||||
performance.mark('unwrapSubtree')
|
||||
return this.unwrapSubtree(vnode.component?.subTree)
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
/**
|
||||
* 将评论区 VNode 进行暴露
|
||||
* @see https://github.com/the1812/Bilibili-Evolved/issues/4690#issuecomment-2059485344
|
||||
*/
|
||||
exposeVNode(vnode: Vue3Vnode = this.rootElement._vnode) {
|
||||
performance.mark('exposeVNode')
|
||||
if (vnode.el && !vnode.el._vnode) {
|
||||
vnode.el._vnode = vnode
|
||||
}
|
||||
if (this.isEnd(vnode)) {
|
||||
return
|
||||
}
|
||||
|
||||
const unwrapped = this.unwrapSubtree(vnode)
|
||||
if (!Array.isArray(unwrapped.children)) {
|
||||
return
|
||||
}
|
||||
const nextChildren = unwrapped.children.filter(child => {
|
||||
// fragment 需要将 VNodeTargets 直接向下传递一层
|
||||
if (child.type === Symbol.for('v-fgt')) {
|
||||
child.el[VNodeTargets] = unwrapped.el?.[VNodeTargets]
|
||||
return true
|
||||
}
|
||||
// 模板树需要 flatMap 并向下传递一层
|
||||
if (!child.el) {
|
||||
child[VNodeTargets] = unwrapped.el?.[VNodeTargets]?.flatMap(
|
||||
element => element?.[VNodeTargets],
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// 没有 el 时属于来自模板树的传递
|
||||
if (!unwrapped.el) {
|
||||
return unwrapped[VNodeTargets]?.includes(child.el)
|
||||
}
|
||||
return unwrapped.el?.[VNodeTargets]?.includes(child.el)
|
||||
})
|
||||
|
||||
nextChildren.forEach(child => {
|
||||
this.exposeVNode(child)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { select } from '@/core/spin-query'
|
||||
import { raiseEvent } from '@/core/utils'
|
||||
import {
|
||||
|
||||
@ -68,7 +68,6 @@ export class VideoInfo {
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get subtitles(): {
|
||||
id: number
|
||||
languageCode: string
|
||||
|
||||
@ -407,7 +407,6 @@ export class DoubleClickEvent {
|
||||
singleClickHandler: (e: MouseEvent) => void = none
|
||||
|
||||
private clickedOnce = false
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private readonly stopPropagationHandler = (e: MouseEvent) => {
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
@ -701,3 +700,8 @@ export const simulateClick = (target: EventTarget, eventParams?: PointerEventIni
|
||||
target.dispatchEvent(mouseUpEvent)
|
||||
target.dispatchEvent(clickEventEvent)
|
||||
}
|
||||
|
||||
/** 尝试获取元素对应的 Vue Data (仅适用于 Vue 2 组件) */
|
||||
export const getVue2Data = (el: any) =>
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
el.__vue__ ?? el.parentElement.__vue__ ?? el.children[0].__vue__ ?? el.__vueParentComponent
|
||||
|
||||
Loading…
Reference in New Issue
Block a user