Merge branch 'v2.7.1-preview' into preview-fixes

This commit is contained in:
the1812 2023-06-01 08:56:40 +08:00
commit 5f98a1d5a0
106 changed files with 2926 additions and 766 deletions

View File

@ -19,7 +19,7 @@
"type": "component",
"name": "extendFeedsLive",
"displayName": "直播信息扩充",
"description": "在动态的`正在直播`中, 为每一个直播间加上标题, 并且能够显示超过10个的直播间.",
"description": "替换动态的 `正在直播` 面板, 能够显示超过 10 个的直播间.\r\n",
"fullRelativePath": "../../registry/dist/components/feeds/extend-live.js",
"fullAbsolutePath": "registry/dist/components/feeds/extend-live.js"
},
@ -71,6 +71,14 @@
"fullRelativePath": "../../registry/dist/components/feeds/hide-comment-preview.js",
"fullAbsolutePath": "registry/dist/components/feeds/hide-comment-preview.js"
},
{
"type": "component",
"name": "imageAutoBackToTop",
"displayName": "动态看图自动回顶",
"description": "在动态里查看图片详情时 (非全屏), 切换图片自动回到图片顶部; 退出查看图片模式时, 自动将动态移入视图内.\r\n",
"fullRelativePath": "../../registry/dist/components/feeds/image-auto-back-to-top.js",
"fullAbsolutePath": "registry/dist/components/feeds/image-auto-back-to-top.js"
},
{
"type": "component",
"name": "unfoldFeeds",
@ -215,6 +223,22 @@
"fullRelativePath": "../../registry/dist/components/style/hide/banner.js",
"fullAbsolutePath": "registry/dist/components/style/hide/banner.js"
},
{
"type": "component",
"name": "hideTrendingSearch",
"displayName": "隐藏热搜",
"description": "隐藏搜索栏和搜索页面中的 `bilibili 热搜`. 请注意这只是视觉上的隐藏, 如果不输入任何关键词就点 Enter 或搜索按钮, 仍然会跳转至推荐的热搜词.",
"fullRelativePath": "../../registry/dist/components/style/hide/trending-search.js",
"fullAbsolutePath": "registry/dist/components/style/hide/trending-search.js"
},
{
"type": "component",
"name": "hideUserCard",
"displayName": "隐藏用户信息卡片",
"description": "by [@WakelessSloth56](https://github.com/WakelessSloth56)\n\n隐藏鼠标指向用户名或用户头像时弹出的浮动用户信息卡片",
"fullRelativePath": "../../registry/dist/components/style/hide/user-card.js",
"fullAbsolutePath": "registry/dist/components/style/hide/user-card.js"
},
{
"type": "component",
"name": "hideRecommendedLive",
@ -231,6 +255,14 @@
"fullRelativePath": "../../registry/dist/components/style/hide/video/related-videos.js",
"fullAbsolutePath": "registry/dist/components/style/hide/video/related-videos.js"
},
{
"type": "component",
"name": "hideVideoShare",
"displayName": "隐藏视频分享",
"description": "隐藏视频和番剧播放器下方的分享按钮.\r\n",
"fullRelativePath": "../../registry/dist/components/style/hide/video/share.js",
"fullAbsolutePath": "registry/dist/components/style/hide/video/share.js"
},
{
"type": "component",
"name": "hideVideoTopMask",
@ -255,6 +287,14 @@
"fullRelativePath": "../../registry/dist/components/style/home-redesign/minimal.js",
"fullAbsolutePath": "registry/dist/components/style/home-redesign/minimal.js"
},
{
"type": "component",
"name": "playerOnTopNew",
"displayName": "播放器置顶(新)",
"description": "by [@RieN7](https://github.com/rien7)\n\n原来的播放器置顶插件现在已经不可用了这是一个新的版本可以在视频页面中将播放器放在页面最上方.",
"fullRelativePath": "../../registry/dist/components/style/player-on-top-new.js",
"fullAbsolutePath": "registry/dist/components/style/player-on-top-new.js"
},
{
"type": "component",
"name": "playerOnTop",
@ -367,6 +407,14 @@
"fullRelativePath": "../../registry/dist/components/touch/player-gestures.js",
"fullAbsolutePath": "registry/dist/components/touch/player-gestures.js"
},
{
"type": "component",
"name": "autoLike",
"displayName": "自动点赞",
"description": "进入视频 / 查看动态时, 自动点赞.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/auto-like.js",
"fullAbsolutePath": "registry/dist/components/utils/auto-like.js"
},
{
"type": "component",
"name": "blackList",
@ -475,7 +523,7 @@
"type": "component",
"name": "urlParamsClean",
"displayName": "网址参数清理",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/url-params-clean.js",
"fullAbsolutePath": "registry/dist/components/utils/url-params-clean.js"
},
@ -551,6 +599,14 @@
"fullRelativePath": "../../registry/dist/components/video/danmaku/expand.js",
"fullAbsolutePath": "registry/dist/components/video/danmaku/expand.js"
},
{
"type": "component",
"name": "unescapeDanmaku",
"displayName": "弹幕转义",
"description": "将弹幕中的 `\\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.\r\n",
"fullRelativePath": "../../registry/dist/components/video/danmaku/unescape.js",
"fullAbsolutePath": "registry/dist/components/video/danmaku/unescape.js"
},
{
"type": "component",
"name": "videoDefaultLocation",
@ -619,7 +675,7 @@
"type": "component",
"name": "disableScrollVolume",
"displayName": "禁止滚轮调音量",
"description": "在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量.",
"description": "在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量. 请注意不能和 `反转滚轮调音量` 一同使用.\r\n",
"fullRelativePath": "../../registry/dist/components/video/player/disable-scroll-volume.js",
"fullAbsolutePath": "registry/dist/components/video/player/disable-scroll-volume.js"
},
@ -655,6 +711,14 @@
"fullRelativePath": "../../registry/dist/components/video/player/intersection-actions.js",
"fullAbsolutePath": "registry/dist/components/video/player/intersection-actions.js"
},
{
"type": "component",
"name": "invertScrollVolume",
"displayName": "反转滚轮调音量",
"description": "反转在网页全屏 / 全屏模式下使用滚轮调节音量的方向, 使其更符合使用触控板时的操作方向. 请注意不能和 `禁止滚轮调音量` 一同使用.\r\n\r\n- 手指向上推时, 增加音量\r\n- 手指向下推时, 减少音量\r\n- 可以自定义 `灵敏度`, 同样的滚动幅度下, 灵敏度越高变化的音量越多",
"fullRelativePath": "../../registry/dist/components/video/player/invert-scroll-volume.js",
"fullAbsolutePath": "registry/dist/components/video/player/invert-scroll-volume.js"
},
{
"type": "component",
"name": "legacyAutoPlay",
@ -715,7 +779,7 @@
"type": "component",
"name": "showUploadTime",
"displayName": "显示视频投稿时间",
"description": "by [@wisokey](https://github.com/wisokey)\n\n为视频播放页面的推荐列表中的视频添加显示视频投稿时间.",
"description": "by [@wisokey](https://github.com/wisokey)\n\n为视频播放页面的推荐列表中的视频添加显示视频投稿时间.\r\n\r\n`时间格式` 替换up名的文本格式 (默认为'up · yyyy-MM-dd'):\r\n - y: 年\r\n - M: 月\r\n - d: 日\r\n - h: 时\r\n - m: 分\r\n - s: 秒\r\n - q: 季度\r\n - up: up名\r\n",
"fullRelativePath": "../../registry/dist/components/video/player/show-upload-time.js",
"fullAbsolutePath": "registry/dist/components/video/player/show-upload-time.js"
},
@ -760,6 +824,38 @@
"description": "by FoundTheWOUT\n\n在视频播放器右上角显示系统时间.",
"owner": "FoundTheWOUT"
},
{
"type": "plugin",
"name": "launchBar.actions.audioSearch",
"displayName": "搜索栏 - 音频跳转",
"description": "在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/audio-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/audio-search.js"
},
{
"type": "plugin",
"name": "launchBar.actions.bangumiSearch",
"displayName": "搜索栏 - 番剧跳转",
"description": "在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/bangumi-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/bangumi-search.js"
},
{
"type": "plugin",
"name": "launchBar.actions.cvSearch",
"displayName": "搜索栏 - 专栏跳转",
"description": "在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/cv-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/cv-search.js"
},
{
"type": "plugin",
"name": "launchBar.actions.numberSearch",
"displayName": "搜索栏 - 数字联想",
"description": "在输入纯数字时, 提供以下选项:\r\n- 跳转至相应的视频 (视为 av 号)\r\n- 跳转至相应的专栏 (视为 cv 号)\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/number-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/number-search.js"
},
{
"type": "plugin",
"name": "launchBar.trendingSearch",
@ -768,6 +864,14 @@
"fullRelativePath": "../../registry/dist/plugins/launch-bar/trending-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/trending-search.js"
},
{
"type": "plugin",
"name": "launchBar.actions.uidSearch",
"displayName": "搜索栏 - UID 跳转",
"description": "在输入 UID (用户 ID) 时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/uid-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/uid-search.js"
},
{
"type": "plugin",
"name": "settingsPanel.tagFilters.recentComponents",
@ -816,6 +920,14 @@
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-danmaku-list.js",
"fullAbsolutePath": "registry/dist/plugins/utils/keymap-toggle-danmaku-list.js"
},
{
"type": "plugin",
"name": "keymap.actions.togglePlayerLight",
"displayName": "快捷键扩展 - 开关灯",
"description": "在快捷键的动作列表里添加一个 \"开关灯\".",
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-player-light.js",
"fullAbsolutePath": "registry/dist/plugins/utils/keymap-toggle-player-light.js"
},
{
"type": "plugin",
"name": "keymap.actions.toggleSubtitle",

View File

@ -27,7 +27,7 @@
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/feeds/extend-live.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/feeds/extend-live.js)
在动态的`正在直播`中, 为每一个直播间加上标题, 并且能够显示超过10个的直播间.
替换动态的 `正在直播` 面板, 能够显示超过 10 个的直播间.
### [动态过滤器](../../registry/dist/components/feeds/filter.js)
`feedsFilter`
@ -83,6 +83,15 @@
隐藏动态评论按钮上方的精选评论预览. (详细可看 [#3322](https://github.com/the1812/Bilibili-Evolved/discussions/3322))
### [动态看图自动回顶](../../registry/dist/components/feeds/image-auto-back-to-top.js)
`imageAutoBackToTop`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/feeds/image-auto-back-to-top.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/feeds/image-auto-back-to-top.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/feeds/image-auto-back-to-top.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/feeds/image-auto-back-to-top.js)
在动态里查看图片详情时 (非全屏), 切换图片自动回到图片顶部; 退出查看图片模式时, 自动将动态移入视图内.
### [动态反折叠](../../registry/dist/components/feeds/unfold.js)
`unfoldFeeds`
@ -255,6 +264,26 @@
隐藏首页顶部横幅.
### [隐藏热搜](../../registry/dist/components/style/hide/trending-search.js)
`hideTrendingSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/hide/trending-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/hide/trending-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/hide/trending-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/hide/trending-search.js)
隐藏搜索栏和搜索页面中的 `bilibili 热搜`. 请注意这只是视觉上的隐藏, 如果不输入任何关键词就点 Enter 或搜索按钮, 仍然会跳转至推荐的热搜词.
### [隐藏用户信息卡片](../../registry/dist/components/style/hide/user-card.js)
`hideUserCard`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/hide/user-card.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/hide/user-card.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/hide/user-card.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/hide/user-card.js)
by [@WakelessSloth56](https://github.com/WakelessSloth56)
隐藏鼠标指向用户名或用户头像时弹出的浮动用户信息卡片
### [隐藏直播推荐](../../registry/dist/components/style/hide/video/recommended-live.js)
`hideRecommendedLive`
@ -273,6 +302,15 @@
隐藏番剧和视频页面右侧的推荐视频列表. 注意: 如果你想关闭 b 站的自动连播 (自动播放下一个推荐视频) 功能, 需要先取消隐藏视频推荐才能看到开关.
### [隐藏视频分享](../../registry/dist/components/style/hide/video/share.js)
`hideVideoShare`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/hide/video/share.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/hide/video/share.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/hide/video/share.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/hide/video/share.js)
隐藏视频和番剧播放器下方的分享按钮.
### [隐藏视频标题层](../../registry/dist/components/style/hide/video/top-mask.js)
`hideVideoTopMask`
@ -310,6 +348,17 @@
- 个性化推荐: 启用时展示推荐视频, 禁用时展示热门视频
- 自定义列数: 为 `0` 时根据视图宽度推断, 大于 `0` 的值将作为固定的列数
### [播放器置顶(新)](../../registry/dist/components/style/player-on-top-new.js)
`playerOnTopNew`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/player-on-top-new.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/player-on-top-new.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/player-on-top-new.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/player-on-top-new.js)
by [@RieN7](https://github.com/rien7)
原来的播放器置顶插件,现在已经不可用了,这是一个新的版本,可以在视频页面中将播放器放在页面最上方.
### [播放器置顶](../../registry/dist/components/style/player-on-top.js)
`playerOnTop`
@ -449,6 +498,15 @@
- 进度调整可在左上角和右上角取消
- 进度调整时在不同位置滑动, 可以使用3档不同的灵敏度.
### [自动点赞](../../registry/dist/components/utils/auto-like.js)
`autoLike`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/auto-like.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/utils/auto-like.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/auto-like.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/auto-like.js)
进入视频 / 查看动态时, 自动点赞.
### [屏蔽黑名单up主](../../registry/dist/components/utils/black-list.js)
`blackList`
@ -691,6 +749,15 @@ by [@kdxcxs](https://github.com/kdxcxs)
每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿. (`最大弹幕数量` 是和视频的总弹幕量对比, 不一定和播放器实际装载的弹幕量相等)
### [弹幕转义](../../registry/dist/components/video/danmaku/unescape.js)
`unescapeDanmaku`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/video/danmaku/unescape.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/video/danmaku/unescape.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/unescape.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/unescape.js)
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
### [视频页默认定位](../../registry/dist/components/video/default-location.js)
`videoDefaultLocation`
@ -794,7 +861,7 @@ by [@kdxcxs](https://github.com/kdxcxs)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/player/disable-scroll-volume.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/player/disable-scroll-volume.js)
在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量.
在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量. 请注意不能和 `反转滚轮调音量` 一同使用.
### [双击全屏](../../registry/dist/components/video/player/double-click-fullscreen.js)
`doubleClickFullscreen`
@ -862,6 +929,19 @@ by [@FoundTheWOUT](https://github.com/FoundTheWOUT)
设置当播放器移出视图的位置变化时执行的动作.
### [反转滚轮调音量](../../registry/dist/components/video/player/invert-scroll-volume.js)
`invertScrollVolume`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/video/player/invert-scroll-volume.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/video/player/invert-scroll-volume.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/player/invert-scroll-volume.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/player/invert-scroll-volume.js)
反转在网页全屏 / 全屏模式下使用滚轮调节音量的方向, 使其更符合使用触控板时的操作方向. 请注意不能和 `禁止滚轮调音量` 一同使用.
- 手指向上推时, 增加音量
- 手指向下推时, 减少音量
- 可以自定义 `灵敏度`, 同样的滚动幅度下, 灵敏度越高变化的音量越多
### [传统连播模式](../../registry/dist/components/video/player/legacy-auto-play.js)
`legacyAutoPlay`
@ -960,6 +1040,16 @@ by [@wisokey](https://github.com/wisokey)
为视频播放页面的推荐列表中的视频添加显示视频投稿时间.
`时间格式` 替换up名的文本格式 (默认为'up · yyyy-MM-dd'):
- y: 年
- M: 月
- d: 日
- h: 时
- m: 分
- s: 秒
- q: 季度
- up: up名
### [跳过充电鸣谢](../../registry/dist/components/video/player/skip-charge-list.js)
`skipChargeList`
@ -1008,6 +1098,44 @@ by FoundTheWOUT
在视频播放器右上角显示系统时间.
## 插件
### [搜索栏 - 音频跳转](../../registry/dist/plugins/launch-bar/audio-search.js)
`launchBar.actions.audioSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/audio-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/audio-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/audio-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/audio-search.js)
在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.
### [搜索栏 - 番剧跳转](../../registry/dist/plugins/launch-bar/bangumi-search.js)
`launchBar.actions.bangumiSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/bangumi-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/bangumi-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/bangumi-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/bangumi-search.js)
在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.
### [搜索栏 - 专栏跳转](../../registry/dist/plugins/launch-bar/cv-search.js)
`launchBar.actions.cvSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/cv-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/cv-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/cv-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/cv-search.js)
在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.
### [搜索栏 - 数字联想](../../registry/dist/plugins/launch-bar/number-search.js)
`launchBar.actions.numberSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/number-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/number-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/number-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/number-search.js)
在输入纯数字时, 提供以下选项:
- 跳转至相应的视频 (视为 av 号)
- 跳转至相应的专栏 (视为 cv 号)
### [搜索栏 - 搜索推荐](../../registry/dist/plugins/launch-bar/trending-search.js)
`launchBar.trendingSearch`
@ -1017,6 +1145,15 @@ by FoundTheWOUT
在脚本的搜索栏中默认显示类似 b 站搜索栏的搜索推荐词, 替代原来的 "搜索" 两字.
### [搜索栏 - UID 跳转](../../registry/dist/plugins/launch-bar/uid-search.js)
`launchBar.actions.uidSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/uid-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/uid-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/uid-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/uid-search.js)
在输入 UID (用户 ID) 时, 提供对应的跳转选项.
### [设置面板 - "最近使用" 类别](../../registry/dist/plugins/settings-panel/recent-components.js)
`settingsPanel.tagFilters.recentComponents`
@ -1071,6 +1208,15 @@ by FoundTheWOUT
在快捷键的动作列表里添加一个 "开关弹幕列表".
### [快捷键扩展 - 开关灯](../../registry/dist/plugins/utils/keymap-toggle-player-light.js)
`keymap.actions.togglePlayerLight`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/utils/keymap-toggle-player-light.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/utils/keymap-toggle-player-light.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/utils/keymap-toggle-player-light.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/utils/keymap-toggle-player-light.js)
在快捷键的动作列表里添加一个 "开关灯".
### [快捷键扩展 - 开关 CC 字幕](../../registry/dist/plugins/utils/keymap-toggle-subtitle.js)
`keymap.actions.toggleSubtitle`

View File

@ -216,7 +216,7 @@
"type": "component",
"name": "urlParamsClean",
"displayName": "网址参数清理",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/url-params-clean.js",
"fullAbsolutePath": "registry/dist/components/utils/url-params-clean.js"
},

View File

@ -0,0 +1 @@
!function(e,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports["style/player-on-top-new"]=o():e["style/player-on-top-new"]=o()}(globalThis,(()=>(()=>{"use strict";var e={d:(o,t)=>{for(var n in t)e.o(t,n)&&!e.o(o,n)&&Object.defineProperty(o,n,{enumerable:!0,get:t[n]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)},o={};e.d(o,{component:()=>p});const t=coreApis.componentApis.define,n=coreApis.utils;function r(e,o){const t=e.parentElement;t&&(t.removeChild(e),t.insertBefore(e,o.nextSibling))}const p=(0,t.defineComponentMetadata)({name:"playerOnTopNew",author:{name:"RieN7",link:"https://github.com/rien7"},tags:[componentsTags.style,componentsTags.video],displayName:"播放器置顶(新)",description:"原来的播放器置顶插件,现在已经不可用了,这是一个新的版本,可以在视频页面中将播放器放在页面最上方.",entry:async()=>{await(0,n.playerReady)();r(document.querySelector("#viewbox_report"),document.querySelector("#arc_toolbar_report"));r(document.querySelector("#v_upinfo").parentElement,document.querySelector("#danmukuBox"))},commitHash:"b08ebfbef7d0b5320ae67d8f0568d8bb0ed24635",coreVersion:"2.7.0"});return o=o.component})()));

View File

@ -0,0 +1,227 @@
<template>
<div class="be-live-list">
<div class="be-live-list-header">
<div class="be-live-list-title">
正在直播
<div class="be-live-list-count">({{ items.length }})</div>
</div>
<div class="be-live-list-actions">
<div v-if="loaded" class="be-live-list-refresh" title="刷新" @click="refresh">
<VIcon icon="mdi-refresh" :size="16" />
</div>
<a
class="be-live-list-more"
target="_blank"
href="https://link.bilibili.com/p/center/index#/user-center/follow/1"
>
更多
<VIcon icon="right-arrow" :size="14" />
</a>
</div>
</div>
<div class="be-live-list-search">
<VIcon icon="search" :size="18" />
<TextBox v-model="keyword" placeholder="搜索" />
</div>
<div class="be-live-list-content">
<VLoading v-if="!loaded" />
<VEmpty v-if="loaded && filteredItems.length === 0" />
<a
v-for="item of filteredItems"
:key="item.roomid"
class="be-live-list-item"
target="_blank"
:href="item.link"
>
<div class="be-live-list-item-avatar">
<DpiImage :src="item.face" :alt="item.title" :size="36" />
</div>
<div class="be-live-list-item-info">
<div class="be-live-list-item-title" :title="item.title">
{{ item.title }}
</div>
<div class="be-live-list-item-user" :title="item.uname">
{{ item.uname }}
</div>
</div>
</a>
</div>
</div>
</template>
<script lang="ts">
import { VIcon, TextBox, DpiImage, VEmpty, VLoading } from '@/ui'
import { getJsonWithCredentials, responsiveGetPages } from '@/core/ajax'
interface LiveInfo {
cover: string
face: string
uname: string
title: string
roomid: number
pic: string
online: number
uid: number
link: string
}
export default Vue.extend({
components: {
VIcon,
TextBox,
DpiImage,
VEmpty,
VLoading,
},
data() {
return {
items: [],
loaded: false,
keyword: '',
}
},
computed: {
filteredItems() {
if (this.keyword) {
return (this.items as LiveInfo[]).filter(it => {
const matchField = (field: keyof LiveInfo) => {
return it[field].toString().toLowerCase().includes(this.keyword.toLowerCase())
}
return (
matchField('title') || matchField('uname') || matchField('roomid') || matchField('uid')
)
})
}
return this.items
},
},
created() {
this.refresh()
},
methods: {
async refresh() {
try {
this.items = []
this.loaded = false
const pageSize = 24
const [promise] = responsiveGetPages<LiveInfo>({
api: page =>
getJsonWithCredentials(
`https://api.live.bilibili.com/relation/v1/feed/feed_list?page=${page}&pagesize=${pageSize}`,
),
getList: json => lodash.get(json, 'data.list', []),
getTotal: json => lodash.get(json, 'data.results', 0),
})
this.items = await promise
} finally {
this.loaded = true
}
},
},
})
</script>
<style lang="scss">
@import 'common';
.be-live-list {
@include v-stretch(12px);
padding: 12px 16px;
border-radius: 4px;
font-size: 12px;
color: #000;
background-color: #fff;
body.dark & {
background-color: #444;
color: #eee;
}
&-header {
@include h-center();
justify-content: space-between;
}
&-title {
@include h-center(4px);
font-size: 14px;
}
&-count {
opacity: 0.5;
font-size: 12px;
}
&-actions {
@include h-center(4px);
}
&-refresh {
cursor: pointer;
padding: 2px;
.be-icon {
opacity: 0.5;
transition: 0.35s ease-out;
}
&:hover .be-icon {
transform: rotate(360deg);
opacity: 1;
color: var(--theme-color);
}
}
&-more {
opacity: 0.5;
padding: 2px;
cursor: pointer;
@include h-center();
&:hover {
opacity: 1;
color: var(--theme-color);
}
}
&-search {
@include h-center(8px);
.be-icon {
opacity: 0.6;
}
.be-textbox {
flex-grow: 1;
}
}
&-content {
@include no-scrollbar();
@include v-stretch(8px);
//
max-height: calc(100vh - 358px);
//
body.enable-feeds-filter & {
max-height: calc(100vh - 414px);
}
// profile
body.feeds-filter-side-block-profile & {
max-height: calc(100vh - 218px);
}
}
&-item {
@include h-center(8px);
cursor: pointer;
&-avatar {
overflow: hidden;
border-radius: 50%;
display: flex;
border: 1px solid #8884;
}
&-info {
@include v-stretch(2px);
flex-grow: 1;
width: 0;
}
&-title,
&-user {
@include single-line();
}
&-title {
font-size: 14px;
transition: 0.2s ease-out;
}
&-user {
opacity: 0.6;
}
&:hover &-title {
color: var(--theme-color);
}
}
}
</style>

View File

@ -1,28 +1,4 @@
@import "common";
@import "../feeds-panel-shared";
.live-up-list {
@include no-scrollbar();
.live-detail[data-live-title] {
color: black;
body.dark & {
color: #eee;
}
.up-name {
font-size: 13px !important;
opacity: 0.6;
}
&::before {
content: attr(data-live-title);
color: black;
line-height: 1.5;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
body.dark & {
color: #eee;
}
}
}
.live-up-list,
.bili-dyn-home--member .bili-dyn-live-users {
display: none !important;
}

View File

@ -0,0 +1 @@
替换动态的 `正在直播` 面板, 能够显示超过 10 个的直播间.

View File

@ -1,83 +1,25 @@
import { defineComponentMetadata } from '@/components/define'
import { styledComponentEntry } from '@/components/styled-component'
import { select } from '@/core/spin-query'
import { mountVueComponent } from '@/core/utils'
import { useScopedConsole } from '@/core/utils/log'
interface LiveInfo {
cover: string
face: string
uname: string
title: string
roomid: number
pic: string
online: number
link: string
}
const entry = async () => {
const { select } = await import('@/core/spin-query')
const liveList = (await select('.live-up-list, .bili-dyn-live-users__body')) as HTMLElement
if (liveList === null) {
return
const console = useScopedConsole('extendFeedsLive')
const container = (await select(
'.bili-dyn-home--member aside.left section:last-child',
)) as HTMLElement
if (!container) {
console.error('container not found')
}
const pageSize = 24
const { getPages, getJsonWithCredentials } = await import('@/core/ajax')
const fullList: LiveInfo[] = await getPages({
api: page =>
getJsonWithCredentials(
`https://api.live.bilibili.com/relation/v1/feed/feed_list?page=${page}&pagesize=${pageSize}`,
),
getList: json => lodash.get(json, 'data.list', []),
getTotal: json => lodash.get(json, 'data.results', 0),
})
const upNameSelector = '.up-name, .bili-dyn-live-users__item__uname'
const liveListNames = dqa(liveList, upNameSelector)
const presentedNames = liveListNames.map((it: HTMLElement) => it.innerText.trim())
// const presentedNames = []
const presented = fullList.filter(it => presentedNames.includes(it.uname))
const extend = fullList.filter(it => !presentedNames.includes(it.uname))
const liveDetailItem = liveList.children[0] as HTMLElement
extend.forEach(it => {
const existingNames = dqa(liveList, upNameSelector) as HTMLElement[]
if (existingNames.some(n => n.innerText.trim() === it.uname)) {
return
}
const cloneItem = (() => {
const clone = liveDetailItem.cloneNode(true) as HTMLElement
const url = `https://live.bilibili.com/${it.roomid}`
dqa(clone, 'a[href]').forEach(a => a.setAttribute('href', url))
if (clone.matches('.bili-dyn-live-users__item, .bili-dyn-live-users__container')) {
clone.addEventListener('click', () => {
window.open(url, '_blank')
})
}
const face = dq(
clone,
'.live-up-img, .bili-dyn-live-users__item__face, .bili-awesome-img',
) as HTMLElement
face.style.backgroundImage = `url(${it.face})`
face.style.backgroundSize = 'cover'
const facepic = dq(clone, '.b-img--face') as HTMLElement
facepic.style.visibility = 'hidden'
const title = dq(clone, '.live-name, .bili-dyn-live-users__item__title') as HTMLElement
title.innerHTML = it.title
title.title = it.title
const name = dq(clone, upNameSelector) as HTMLElement
name.innerHTML = it.uname
name.title = it.uname
return clone
})()
liveList.insertAdjacentElement('beforeend', cloneItem)
})
const { disableProfilePopup } = await import('@/components/feeds/disable-profile-popup')
disableProfilePopup()
console.log(presented, extend)
const LiveList = await import('./LiveList.vue').then(m => m.default)
const liveList = mountVueComponent(LiveList)
container.appendChild(liveList.$el)
}
export const component = defineComponentMetadata({
name: 'extendFeedsLive',
displayName: '直播信息扩充',
description: {
'zh-CN': '在动态的`正在直播`中, 为每一个直播间加上标题, 并且能够显示超过10个的直播间.',
},
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
tags: [componentsTags.feeds, componentsTags.live],
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],

View File

@ -0,0 +1 @@
在动态里查看图片详情时 (非全屏), 切换图片自动回到图片顶部; 退出查看图片模式时, 自动将动态移入视图内.

View File

@ -0,0 +1,69 @@
import { defineComponentMetadata } from '@/components/define'
import { forEachFeedsCard } from '@/components/feeds/api'
import { attributesSubtree, mutationObserve } from '@/core/observer'
import { useScopedConsole } from '@/core/utils/log'
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
export const component = defineComponentMetadata({
name: 'imageAutoBackToTop',
displayName: '动态看图自动回顶',
tags: [componentsTags.feeds],
urlInclude: feedsUrlsWithoutDetail,
entry: () => {
const console = useScopedConsole('imageAutoBackToTop')
forEachFeedsCard({
added: card => {
const scrollIntoView = (element: Element) => {
element?.scrollIntoView()
window.scrollBy({ top: -75 })
}
const albums = dqa(card.element, '.bili-album')
mutationObserve(albums, { childList: true, attributes: true, subtree: true }, records => {
records.forEach(record => {
const isAlbumVisible = () => {
if (
record.target instanceof HTMLElement &&
record.target.classList.contains('bili-album__watch') &&
record.attributeName === 'style'
) {
return record.target.style.display !== 'none'
}
return null
}
const hasAlbumPreviewNode = (nodeList: NodeList) =>
[...nodeList].some(node => {
return node instanceof HTMLElement && node.classList.contains('bili-album__watch')
})
const isImagePreviewExited =
hasAlbumPreviewNode(record.removedNodes) || isAlbumVisible() === false
if (isImagePreviewExited) {
scrollIntoView(card.element)
console.log('imagePreviewExited')
}
const isImagePreviewEntered =
hasAlbumPreviewNode(record.addedNodes) || isAlbumVisible() === true
if (isImagePreviewEntered) {
const controlBar = dq(card.element, '.bili-album__watch__control')
scrollIntoView(controlBar)
console.log('imagePreviewEntered')
}
})
})
attributesSubtree(card.element, records => {
records.forEach(record => {
if (
!(record.target instanceof HTMLImageElement) ||
!record.target.matches('.bili-album__watch__content img') ||
record.attributeName !== 'src'
) {
return
}
const controlBar = dq(card.element, '.bili-album__watch__control')
scrollIntoView(controlBar)
console.log('imagePreviewSwitched')
})
})
},
})
},
})

View File

@ -7,9 +7,7 @@
:value="folder"
@change="change($event)"
>
<template #item="{ item }">
{{ item.name }}
</template>
<template #item="{ item }"> {{ item.name }} ({{ item.count }}) </template>
</VDropdown>
</template>
<script lang="ts">

View File

@ -1,5 +1,5 @@
<template>
<SubscriptionsList type="bangumi"></SubscriptionsList>
<SubscriptionsList type="bangumi" :filter="filter"></SubscriptionsList>
</template>
<script lang="ts">
import SubscriptionsList from './SubscriptionsList.vue'
@ -8,5 +8,11 @@ export default Vue.extend({
components: {
SubscriptionsList,
},
props: {
filter: {
type: [Object, null],
default: null,
},
},
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<SubscriptionsList type="cinema"></SubscriptionsList>
<SubscriptionsList type="cinema" :filter="filter"></SubscriptionsList>
</template>
<script lang="ts">
import SubscriptionsList from './SubscriptionsList.vue'
@ -8,5 +8,11 @@ export default Vue.extend({
components: {
SubscriptionsList,
},
props: {
filter: {
type: [Object, null],
default: null,
},
},
})
</script>

View File

@ -1,43 +1,107 @@
<template>
<div class="navbar-subscriptions">
<TabControl ref="tabControl" :tabs="tabs" :more-link="moreLink"></TabControl>
<TabControl ref="tabControl" :tabs="tabs" :more-link="moreLink">
<template #header-item>
<div class="navbar-subscriptions-filter">
<VDropdown v-model="selectedFilter" round :items="filterItems" />
</div>
</template>
</TabControl>
</div>
</template>
<script lang="ts">
import { TabControl } from '@/ui'
import { TabMappings, TabMapping } from '@/ui/tab-mapping'
import { TabControl, VDropdown } from '@/ui'
import { TabMapping, TabMappings } from '@/ui/tab-mapping'
import { getUID } from '@/core/utils'
import { popperMixin } from '../mixins'
import { SubscriptionTypes } from './subscriptions'
import { SubscriptionStatus, SubscriptionStatusFilter } from './types'
const filterItems: {
name: string
value: SubscriptionStatusFilter
displayName: string
}[] = [
{
name: 'all',
value: {
viewAll: true,
status: SubscriptionStatus.Viewing,
},
displayName: '全部',
},
{
name: 'viewing',
value: {
viewAll: false,
status: SubscriptionStatus.Viewing,
},
displayName: '在看',
},
{
name: 'toView',
value: {
viewAll: false,
status: SubscriptionStatus.ToView,
},
displayName: '想看',
},
{
name: 'viewed',
value: {
viewAll: false,
status: SubscriptionStatus.Viewed,
},
displayName: '看过',
},
]
export default Vue.extend({
components: {
TabControl,
VDropdown,
},
mixins: [popperMixin],
data() {
const uid = getUID()
return {
uid,
filterItems,
selectedFilter: filterItems[0],
moreLink: (tab: TabMapping) => `https://space.bilibili.com/${uid}/${tab.name}`,
tabs: [
filter: {
viewAll: true,
status: SubscriptionStatus.Viewing,
} as SubscriptionStatusFilter,
}
},
computed: {
tabs(): TabMappings {
return [
{
name: SubscriptionTypes.Bangumi,
displayName: '追番',
activeLink: `https://space.bilibili.com/${uid}/bangumi`,
activeLink: `https://space.bilibili.com/${this.uid}/bangumi`,
component: () => import('./BangumiSubscriptions.vue').then(m => m.default),
propsData: {
filter: this.selectedFilter.value,
},
},
{
name: SubscriptionTypes.Cinema,
displayName: '追剧',
activeLink: `https://space.bilibili.com/${uid}/cinema`,
activeLink: `https://space.bilibili.com/${this.uid}/cinema`,
component: () => import('./CinemaSubscriptions.vue').then(m => m.default),
propsData: {
filter: this.selectedFilter.value,
},
},
] as TabMappings,
}
]
},
},
})
</script>
<style lang="scss">
@import 'common';
@import '../popup';
.navbar-subscriptions {
@ -54,5 +118,10 @@ export default Vue.extend({
padding-bottom: 0;
}
}
&-filter {
@include h-stretch();
justify-content: flex-end;
height: 26px;
}
}
</style>

View File

@ -46,12 +46,8 @@ import { logError } from '@/core/utils/log'
import { DpiImage, VLoading, VEmpty, VIcon, ScrollTrigger } from '@/ui'
import { getJsonWithCredentials } from '@/core/ajax'
import { SubscriptionTypes } from './subscriptions'
import { SubscriptionItem, SubscriptionStatus, SubscriptionStatusFilter } from './types'
enum SubscriptionStatus {
ToView = 1,
Viewing,
Viewed,
}
const getStatusText = (status: SubscriptionStatus) => {
switch (status) {
case SubscriptionStatus.ToView:
@ -63,10 +59,7 @@ const getStatusText = (status: SubscriptionStatus) => {
return '看过'
}
}
const subscriptionSorter = (
a: { status: SubscriptionStatus },
b: { status: SubscriptionStatus },
) => {
const subscriptionSorter = (a: SubscriptionItem, b: SubscriptionItem) => {
let statusA = a.status
if (statusA !== SubscriptionStatus.Viewed) {
statusA = SubscriptionStatus.Viewed - statusA
@ -86,6 +79,10 @@ export default Vue.extend({
ScrollTrigger,
},
props: {
filter: {
type: [Object, null],
default: null,
},
type: {
type: String,
default: SubscriptionTypes.Bangumi,
@ -99,12 +96,21 @@ export default Vue.extend({
page: 1,
}
},
watch: {
filter() {
this.cards = []
this.loading = true
this.page = 1
this.nextPage()
},
},
async created() {
this.nextPage()
},
methods: {
async nextPage() {
try {
const filter = this.filter as SubscriptionStatusFilter
const json = await getJsonWithCredentials(
`https://api.bilibili.com/x/space/bangumi/follow/list?type=${
this.type !== SubscriptionTypes.Bangumi ? '2' : '1'
@ -114,10 +120,10 @@ export default Vue.extend({
logError(`加载订阅信息失败: ${json.message}`)
return
}
const cards = lodash
const newCards: SubscriptionItem[] = lodash
.uniqBy(
(this.cards as any[]).concat(
(lodash.get(json, 'data.list') as any[]).map(item => ({
(lodash.get(json, 'data.list') as any[]).map(
(item): SubscriptionItem => ({
title: item.title,
coverUrl: item.square_cover.replace('http:', 'https:'),
latest: item.new_ep.index_show,
@ -127,14 +133,20 @@ export default Vue.extend({
statusText: getStatusText(item.follow_status),
playUrl: `https://www.bilibili.com/bangumi/play/ss${item.season_id}`,
mediaUrl: `https://www.bilibili.com/bangumi/media/md${item.media_id}`,
})),
}),
),
card => card.id,
)
.filter(card => {
if (filter.viewAll) {
return true
}
return card.status === filter.status
})
.sort(subscriptionSorter)
this.page++
this.cards = cards
this.hasMorePage = lodash.get(json, 'data.total', 0) > this.cards.length
this.cards = this.cards.concat(newCards)
this.hasMorePage = newCards.length > 0
} finally {
this.loading = false
}

View File

@ -0,0 +1,20 @@
export enum SubscriptionStatus {
ToView = 1,
Viewing,
Viewed,
}
export interface SubscriptionStatusFilter {
viewAll: boolean
status: SubscriptionStatus
}
export interface SubscriptionItem {
title: string
coverUrl: string
latest: number
progress: string
id: string
status: SubscriptionStatus
statusText: string
playUrl: string
mediaUrl: string
}

View File

@ -1,6 +1,7 @@
<template>
<div class="watchlater-list">
<div class="header">
<div class="watchlater-list-summary"> {{ filteredCards.length }} </div>
<div class="search">
<TextBox v-model="search" linear placeholder="搜索"></TextBox>
</div>
@ -203,13 +204,17 @@ export default Vue.extend({
cursor: pointer;
}
.header {
@include h-stretch();
@include h-center();
justify-content: space-between;
align-self: stretch;
margin: 16px 12px;
.watchlater-list-summary {
margin-right: 6px;
}
.search {
position: relative;
flex-grow: 1;
align-self: stretch;
margin-right: 8px;
.be-textbox {
height: 100%;

View File

@ -0,0 +1,5 @@
.search-panel {
> .trending {
display: none !important;
}
}

View File

@ -0,0 +1 @@
隐藏搜索栏和搜索页面中的 `bilibili 热搜`. 请注意这只是视觉上的隐藏, 如果不输入任何关键词就点 Enter 或搜索按钮, 仍然会跳转至推荐的热搜词.

View File

@ -0,0 +1,27 @@
import { defineComponentMetadata } from '@/components/define'
import { allMutations } from '@/core/observer'
export const component = defineComponentMetadata({
name: 'hideTrendingSearch',
displayName: '隐藏热搜',
tags: [componentsTags.style],
instantStyles: [
{
name: 'hideTrendingSearch',
style: () => import('./hide-trending-search.scss'),
},
],
entry: async () => {
allMutations(records => {
records.forEach(record => {
if (
record.target instanceof HTMLInputElement &&
record.target.classList.contains('nav-search-input') &&
record.target.placeholder !== '搜索'
) {
record.target.placeholder = '搜索'
}
})
})
},
})

View File

@ -0,0 +1,17 @@
import { defineComponentMetadata } from '@/components/define'
import { toggleStyle } from '@/components/styled-component'
const name = 'hideUserCard'
export const component = defineComponentMetadata({
displayName: '隐藏用户信息卡片',
description: {
'zh-CN': '隐藏鼠标指向用户名或用户头像时弹出的浮动用户信息卡片',
},
author: {
name: 'WakelessSloth56',
link: 'https://github.com/WakelessSloth56',
},
tags: [componentsTags.style],
...toggleStyle(name, () => import('./user-card.scss')),
})

View File

@ -0,0 +1,5 @@
.user-card,
.user-card-m-exp,
.bili-user-profile {
display: none !important;
}

View File

@ -0,0 +1,15 @@
.video-toolbar-v1 {
.share-wrap {
display: none !important;
}
}
.video-toolbar-container {
.video-share-wrap {
display: none !important;
}
}
.player-left-components {
[class*="toolbar_share_info"] {
display: none !important;
}
}

View File

@ -0,0 +1 @@
隐藏视频和番剧播放器下方的分享按钮.

View File

@ -0,0 +1,14 @@
import { defineComponentMetadata } from '@/components/define'
export const component = defineComponentMetadata({
name: 'hideVideoShare',
displayName: '隐藏视频分享',
tags: [componentsTags.style, componentsTags.video],
instantStyles: [
{
name: 'hideVideoShare',
style: () => import('./hide-video-share.scss'),
},
],
entry: none,
})

View File

@ -0,0 +1,128 @@
<template>
<div class="fresh-home-extra-options">
<div class="fresh-home-extra-options-title">布局配置</div>
<VLoading v-if="!loaded" />
<div v-show="loaded" ref="sortList" class="fresh-home-extra-options-sort-list">
<div
v-for="item of sortItems"
:key="item.layoutItem.name"
class="fresh-home-extra-options-sort-item-container"
>
<div
:data-name="item.layoutItem.name"
:style="{ 'flex-grow': item.layoutItem.grow ? '1' : '0' }"
class="fresh-home-extra-options-sort-item"
>
<div class="fresh-home-extra-options-sort-item-name">
<VIcon v-if="item.layoutItem.grow" icon="mdi-arrow-expand-horizontal" :size="16" />
<VIcon v-else icon="mdi-arrow-collapse-horizontal" :size="16" />
{{ item.layoutItem.displayName }}
</div>
<div class="fresh-home-extra-options-sort-item-config">
<VButton
icon
round
@click="item.layoutSettings.linebreak = !item.layoutSettings.linebreak"
>
<VIcon v-if="item.layoutSettings.linebreak" icon="mdi-wrap" :size="16" />
<VIcon v-else icon="mdi-wrap-disabled" :size="16" />
</VButton>
</div>
</div>
<div
v-if="item.layoutSettings.linebreak"
class="fresh-home-extra-options-sort-item-linebreak"
></div>
</div>
</div>
</div>
</template>
<script lang="ts">
import type { SortableEvent } from 'sortablejs'
import { SortableJSLibrary } from '@/core/runtime-library'
import { ascendingSort } from '@/core/utils/sort'
import { VLoading, VIcon, VButton } from '@/ui'
import { FreshLayoutItem, FreshLayoutItemSettings } from './layouts/fresh-layout-item'
import { layouts } from './layouts/layouts'
import { freshHomeOptions } from './options'
interface SortItem {
layoutItem: FreshLayoutItem
layoutSettings: FreshLayoutItemSettings
}
export default Vue.extend({
components: { VLoading, VIcon, VButton },
data() {
return {
loaded: false,
layouts,
}
},
computed: {
sortItems() {
return (this.layouts as FreshLayoutItem[])
.map((layoutItem, index): SortItem => {
const layoutSettings: FreshLayoutItemSettings = {
linebreak: false,
order: index + 1,
...freshHomeOptions.layoutOptions[layoutItem.name],
}
return {
layoutItem,
layoutSettings,
}
})
.filter(it => it !== null)
.sort(ascendingSort(it => it.layoutSettings.order))
},
},
async mounted() {
const list: HTMLElement = this.$refs.sortList
const Sortable = await SortableJSLibrary
console.log({ list })
Sortable.create(list, {
delay: 100,
forceFallback: true,
onEnd: (e: SortableEvent) => {
console.log(e)
},
})
this.loaded = true
},
})
</script>
<style lang="scss">
@import 'common';
.fresh-home-extra-options {
@include v-stretch(10px);
padding: 4px 0;
&-sort-list {
@include h-stretch();
column-gap: 8px;
row-gap: 4px;
flex-wrap: wrap;
}
&-sort-item-container {
display: contents;
}
&-sort-item {
@include v-center(6px);
@include border-card();
padding: 8px;
transition: border-color 0.2s ease-out;
body.dark &:hover,
&:hover {
border-color: var(--theme-color);
cursor: move;
}
&-name {
@include h-center(4px);
}
&-linebreak {
flex: 1 0 100%;
}
}
}
</style>

View File

@ -12,7 +12,7 @@
</fragment>
</template>
<script lang="ts">
import { freshHomeOptions } from './types'
import { freshHomeOptions } from './options'
export default Vue.extend({
props: {

View File

@ -96,7 +96,7 @@ export default Vue.extend({
display: flex;
flex: 1 0 0;
width: 0;
@include scroll-mask-x(36px);
@include scroll-mask-x(36px, var(--home-base-color));
&-content {
@include h-center();

View File

@ -1,8 +1,9 @@
import { defineComponentMetadata } from '@/components/define'
import { contentLoaded } from '@/core/life-cycle'
import { addComponentListener } from '@/core/settings'
import { getNumberValidator, mountVueComponent } from '@/core/utils'
import { mountVueComponent } from '@/core/utils'
import { homeUrls } from '../urls'
import { freshHomeOptionsMetadata } from './types'
export const component = defineComponentMetadata({
name: 'freshHome',
@ -23,33 +24,8 @@ export const component = defineComponentMetadata({
document.body.appendChild(freshHome.$el)
})
},
options: {
layoutOptions: {
displayName: '版块设置',
defaultValue: {
trending: {
linebreak: true,
},
areas: {
linebreak: true,
},
},
hidden: true,
},
personalized: {
displayName: '个性化推荐',
defaultValue: false,
},
horizontalWheelScroll: {
displayName: '启用横向滚动',
defaultValue: false,
},
maxWidth: {
displayName: '最大宽度 (px)',
defaultValue: 1440,
validator: getNumberValidator(1000, 3000),
},
},
options: freshHomeOptionsMetadata,
// extraOptions: () => import('./ExtraOptions.vue'),
unload: () => document.body.classList.add('home-redesign-off'),
reload: () => document.body.classList.remove('home-redesign-off'),
instantStyles: [

View File

@ -39,7 +39,7 @@ import { ArrayContent } from '@/core/common-types'
import { Reorder } from '@/core/reorder'
import { ascendingSort } from '@/core/utils/sort'
import { VButton, VIcon } from '@/ui'
import { freshHomeOptions } from '../../types'
import { freshHomeOptions } from '../../options'
import { supportedCategories } from './filter'
import { getContent } from './content/content'

View File

@ -7,23 +7,31 @@
<BangumiTimeline :api="timelineApi" />
</div>
<div class="fresh-home-categories-bangumi-rank-list">
<a
class="fresh-home-categories-bangumi-rank-list-header"
:href="rankingsLink"
target="_blank"
>
<SubHeader> 排行榜 </SubHeader>
</a>
<RankList bangumi-mode :parse-json="parseJson" :api="rankingsApi" />
<div class="fresh-home-categories-bangumi-rank-list-header">
<a :href="rankingsLink" target="_blank">
<SubHeader> 排行榜 </SubHeader>
</a>
<VButton v-if="isCompactRankList" icon title="显示较少项目" @click="toggleRankListMode">
<VIcon icon="mdi-poll" :size="16" />
</VButton>
<VButton v-else icon title="显示较多项目" @click="toggleRankListMode">
<VIcon icon="mdi-format-list-text" :size="16" />
</VButton>
</div>
<CompactRankList v-if="isCompactRankList" :parse-json="parseJson" :api="rankingsApi" />
<RankList v-else bangumi-mode :parse-json="parseJson" :api="rankingsApi" />
</div>
</div>
</template>
<script lang="ts">
import { VButton, VIcon } from '@/ui'
import { applyContentFilter } from '@/components/feeds/api'
import SubHeader from '../../../SubHeader.vue'
import RankList from './RankList.vue'
import CompactRankList from './CompactRankList.vue'
import BangumiTimeline from './BangumiTimeline.vue'
import { RankListCard } from './rank-list'
import { compactRankListMixin } from '../../../../mixin'
const bangumiDataMap = {
anime: {
@ -41,7 +49,11 @@ export default Vue.extend({
SubHeader,
BangumiTimeline,
RankList,
CompactRankList,
VButton,
VIcon,
},
mixins: [compactRankListMixin()],
props: {
region: {
type: Object,
@ -116,10 +128,15 @@ export default Vue.extend({
}
&-rank-list {
@include v-stretch(var(--fresh-home-categories-header-gap));
// &-header {
// // Timeline header , 20px, padding 8px, line-height
// line-height: calc(20px + 8px);
// }
&-header {
// Timeline header , 20px, padding 8px, line-height
// line-height: calc(20px + 8px);
@include h-center();
justify-content: space-between;
.be-icon {
margin: 1px;
}
}
}
}
</style>

View File

@ -400,7 +400,7 @@ export default Vue.extend({
}
&-seasons-container {
@include h-stretch();
@include scroll-mask-x(18px);
@include scroll-mask-x(18px, var(--home-base-color));
width: 0;
flex: 1 0 0;
margin: 0 2px;

View File

@ -0,0 +1,204 @@
<template>
<div
class="fresh-home-compact-rank-list scroll-top scroll-bottom"
:class="{ loading, loaded, 'not-empty': items.length > 0 }"
>
<div v-if="!loaded" class="fresh-home-compact-rank-list-loading-container">
<VLoading v-if="loading" />
<div
v-if="(error || items.length === 0) && !loading"
class="fresh-home-compact-rank-list-empty"
>
<VEmpty />
<VButton class="fresh-home-compact-rank-list-refresh-button" round @click="reload">
<VIcon icon="mdi-refresh" />
刷新
</VButton>
</div>
</div>
<div v-if="loaded" ref="content" class="fresh-home-compact-rank-list-content">
<a
v-for="(video, index) of items"
:key="video.id"
ref="cards"
class="compact-rank-list-card"
:href="video.videoHref"
target="_blank"
>
<div class="compact-rank-list-card-cover">
<div class="compact-rank-list-card-rank" :class="{ 'top-three': index < 3 }">
{{ index + 1 }}
</div>
<DpiImage :src="video.coverUrl" :size="{ width: 128, height: 80 }" />
</div>
<div class="compact-rank-list-card-info">
<div class="compact-rank-list-card-title" :title="video.title">{{ video.title }}</div>
<UpInfo :up-face-url="video.upFaceUrl" :href="video.upHref" :up-name="video.upName">
<template #fallback-icon>
<VIcon v-bind="upInfoProps" />
</template>
</UpInfo>
<div class="compact-rank-list-card-stats">
<VIcon icon="mdi-fire" :size="16" />
{{ video.points | formatCount }}
<VIcon icon="play" :size="16" />
{{ video.playCount | formatCount }}
</div>
</div>
</a>
</div>
</div>
</template>
<script lang="ts">
import UpInfo from '@/components/feeds/UpInfo.vue'
import { formatCount } from '@/core/utils/formatters'
import { VIcon, VLoading, VEmpty, VButton, DpiImage } from '@/ui'
import { cssVariableMixin, requestMixin } from '../../../../mixin'
import { cleanUpScrollMask, setupScrollMask } from '../../../scroll-mask'
import { compactRankListCssVars } from './rank-list'
export default Vue.extend({
components: {
DpiImage,
UpInfo,
VIcon,
VLoading,
VEmpty,
VButton,
},
filters: {
formatCount,
},
mixins: [requestMixin(), cssVariableMixin(compactRankListCssVars)],
props: {
parseJson: {
type: Function,
required: true,
},
bangumiMode: {
type: Boolean,
default: false,
},
},
computed: {
upInfoProps() {
return {
size: 18,
icon: this.bangumiMode ? 'mdi-television-classic' : 'up-outline',
style: {
transform: this.bangumiMode ? 'translateY(-1px)' : 'none',
},
}
},
},
watch: {
loaded() {
if (this.loaded) {
this.setupIntersection()
}
},
},
beforeDestroy() {
cleanUpScrollMask(this.$el)
},
methods: {
async setupIntersection() {
await this.$nextTick()
console.log(this.$refs.cards)
setupScrollMask({
container: this.$el,
items: this.$refs.cards,
})
},
},
})
</script>
<style lang="scss">
@import 'common';
@import 'effects';
@import './rank-list';
.fresh-home-compact-rank-list {
@include rank-list-common();
@include v-stretch();
@include scroll-mask-y(8px, var(--home-base-color));
width: var(--panel-width);
height: var(--panel-height);
min-height: var(--panel-height);
padding: var(--padding);
margin: calc(0px - var(--padding));
position: relative;
&-content {
height: 0;
flex: 1 0 0;
@include v-stretch(12px);
@include no-scrollbar();
}
&.not-empty &-content {
scroll-snap-type: y mandatory;
}
.compact-rank-list-card {
scroll-snap-align: start;
cursor: pointer;
@include h-center(8px);
&-cover {
@include card(12px);
flex-shrink: 0;
display: flex;
align-self: center;
box-shadow: none;
overflow: hidden;
transform-origin: bottom;
position: relative;
img {
transition: 0.2s ease-out;
}
&:hover img {
transform: scale(1.05);
}
}
&-rank {
@include h-center();
justify-content: center;
z-index: 10;
position: absolute;
bottom: 4px;
left: 4px;
font-size: 12px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #333;
color: #eee;
font-weight: bold;
&.top-three {
background-color: var(--theme-color);
color: var(--foreground-color);
}
}
&-info {
@include v-stretch(6px);
flex: 1 0 0;
width: 0;
}
&-title {
line-height: 1.5;
transition: color 0.2s ease-out;
@include semi-bold();
@include single-line();
&:hover {
color: var(--theme-color);
}
}
&-stats {
@include h-center(12px);
font-size: 12px;
opacity: 0.5;
.be-icon {
margin-right: -8px;
}
}
}
}
</style>

View File

@ -11,19 +11,31 @@
</div>
</div>
<div class="fresh-home-categories-default-rank-list">
<a :href="rankingsLink" target="_blank">
<SubHeader> 排行榜 </SubHeader>
</a>
<RankList :parse-json="parseJson" :api="rankingsApi" />
<div class="fresh-home-categories-default-rank-list-header">
<a :href="rankingsLink" target="_blank">
<SubHeader> 排行榜 </SubHeader>
</a>
<VButton v-if="isCompactRankList" icon title="显示较少项目" @click="toggleRankListMode">
<VIcon icon="mdi-poll" :size="16" />
</VButton>
<VButton v-else icon title="显示较多项目" @click="toggleRankListMode">
<VIcon icon="mdi-format-list-text" :size="16" />
</VButton>
</div>
<CompactRankList v-if="isCompactRankList" :parse-json="parseJson" :api="rankingsApi" />
<RankList v-else :parse-json="parseJson" :api="rankingsApi" />
</div>
</div>
</template>
<script lang="ts">
import { VButton, VIcon } from '@/ui'
import { applyContentFilter } from '@/components/feeds/api'
import { RankListCard } from './rank-list'
import CompactRankList from './CompactRankList.vue'
import RankList from './RankList.vue'
import VideoSlides from './VideoSlides.vue'
import SubHeader from '../../../SubHeader.vue'
import { compactRankListMixin } from '../../../../mixin'
/*
TODO: 有几个区表面上是普通视频, 但内容走的还是番剧那套, 所以 RankList 也要换 API
@ -34,10 +46,14 @@ TODO: 有几个区表面上是普通视频, 但内容走的还是番剧那套,
export default Vue.extend({
components: {
CompactRankList,
RankList,
VideoSlides,
SubHeader,
VButton,
VIcon,
},
mixins: [compactRankListMixin()],
props: {
region: {
type: Object,
@ -92,6 +108,18 @@ export default Vue.extend({
}
&-rank-list {
@include v-stretch(var(--fresh-home-categories-header-gap));
&-header {
@include h-center();
justify-content: space-between;
position: relative;
.be-button {
@include absolute-v-center();
right: 0;
}
.be-icon {
margin: 1px;
}
}
}
}
</style>

View File

@ -163,12 +163,13 @@ export default Vue.extend({
</script>
<style lang="scss">
@import 'common';
@import './rank-list';
.fresh-home-rank-list {
position: relative;
flex: 1;
width: 400px;
overflow: hidden;
width: var(--panel-width);
min-height: var(--panel-height);
height: var(--panel-height);
padding: var(--padding);
@ -295,7 +296,7 @@ export default Vue.extend({
padding: 0 12px;
}
.be-up-info {
margin: 4px 8px;
margin: 4px 10px;
}
}
}
@ -328,7 +329,7 @@ export default Vue.extend({
padding: 0 12px;
}
.be-up-info {
margin: 4px 8px;
margin: 4px 10px;
}
}
}
@ -374,34 +375,12 @@ export default Vue.extend({
color: var(--foreground-color);
}
}
& &-loading-container {
@include v-center();
justify-content: center;
padding: var(--padding);
border-radius: var(--home-card-radius);
border: 2px dashed #8884;
height: 100%;
}
& &-empty {
@include v-center(12px);
justify-content: center;
.be-button {
padding: 4px 10px 4px 6px !important;
&:hover .be-icon {
transform: rotate(1turn);
}
}
.be-icon {
margin-right: 6px;
transition: 0.5s ease-out;
}
}
&.loaded {
@include no-scrollbar();
.animation {
animation-play-state: running;
}
}
@include rank-list-common();
}
</style>

View File

@ -0,0 +1,24 @@
@mixin rank-list-common {
& &-loading-container {
@include v-center();
justify-content: center;
padding: var(--padding);
border-radius: var(--home-card-radius);
border: 2px dashed #8884;
height: 100%;
}
& &-empty {
@include v-center(12px);
justify-content: center;
.be-button {
padding: 4px 10px 4px 6px !important;
&:hover .be-icon {
transform: rotate(1turn);
}
}
.be-icon {
margin-right: 6px;
transition: 0.5s ease-out;
}
}
}

View File

@ -11,7 +11,8 @@ export interface RankListCard {
dynamic?: string
}
export const rankListCssVars = {
panelHeight: 608,
panelWidth: 400,
panelHeight: 604,
padding: 12,
rankItemHeight: 110,
rankItemMargin: 24,
@ -23,3 +24,8 @@ export const rankListCssVars = {
thirdCoverHeight: 90,
thirdCoverWidth: 139,
}
export const compactRankListCssVars = {
panelWidth: 376,
panelHeight: 580,
padding: 0,
}

View File

@ -5,6 +5,6 @@ export interface FreshLayoutItem extends WithName {
grow?: boolean
}
export interface FreshLayoutItemSettings {
name: string
linebreak: boolean
order: number
}

View File

@ -24,7 +24,7 @@
<script lang="ts">
import { VButton, VIcon } from '@/ui'
import VideoList from '../../VideoList.vue'
import { freshHomeOptions } from '../../types'
import { freshHomeOptions } from '../../options'
import { getTrendingVideos } from '../../../trending'
export default Vue.extend({

View File

@ -0,0 +1,6 @@
import { OptionsOfMetadata } from '@/components/define'
import { getComponentSettings } from '@/core/settings'
import { freshHomeOptionsMetadata } from './types'
export const freshHomeOptions =
getComponentSettings<OptionsOfMetadata<typeof freshHomeOptionsMetadata>>('freshHome').options

View File

@ -1,9 +1,55 @@
import { getComponentSettings } from '@/core/settings'
import { FreshLayoutItemSettings } from './layouts/fresh-layout-item'
import { defineOptionsMetadata } from '@/components/define'
import { getNumberValidator } from '@/core/utils'
import type { FreshLayoutItemSettings } from './layouts/fresh-layout-item'
export const freshHomeOptions = getComponentSettings('freshHome').options as {
layoutOptions: Record<string, FreshLayoutItemSettings>
personalized: boolean
horizontalWheelScroll: boolean
[key: string]: any
export enum RankListMode {
Default = 'default',
Compact = 'compact',
}
export const freshHomeOptionsMetadata = defineOptionsMetadata({
layoutOptions: {
displayName: '版块设置',
defaultValue: {
blackboard: {
linebreak: false,
order: 1,
},
trending: {
linebreak: true,
order: 2,
},
feeds: {
linebreak: false,
order: 3,
},
areas: {
linebreak: true,
order: 4,
},
categories: {
linebreak: false,
order: 5,
},
} satisfies Record<string, FreshLayoutItemSettings>,
hidden: true,
},
personalized: {
displayName: '个性化推荐',
defaultValue: false,
},
horizontalWheelScroll: {
displayName: '启用横向滚动',
defaultValue: false,
},
maxWidth: {
displayName: '最大宽度 (px)',
defaultValue: 1440,
validator: getNumberValidator(1000, 3000),
},
rankListMode: {
displayName: '',
defaultValue: RankListMode.Default,
hidden: true,
},
})

View File

@ -1,4 +1,6 @@
import { getJson } from '@/core/ajax'
import { freshHomeOptions } from './fresh/options'
import { RankListMode } from './fresh/types'
/**
* API , 使 parseJson JSON , this.items
@ -70,3 +72,29 @@ export const cssVariableMixin = (variables: Record<string, string | number>) =>
})
},
})
/** 使用 CompactRankList, 提供一些与设置关联的 data */
export const compactRankListMixin = () => {
return Vue.extend({
data() {
return {
rankListMode: freshHomeOptions.rankListMode,
}
},
computed: {
isCompactRankList() {
return this.rankListMode === RankListMode.Compact
},
},
methods: {
toggleRankListMode() {
if (this.rankListMode === RankListMode.Compact) {
this.rankListMode = RankListMode.Default
} else if (this.rankListMode === RankListMode.Default) {
this.rankListMode = RankListMode.Compact
}
freshHomeOptions.rankListMode = this.rankListMode
},
},
})
}

View File

@ -0,0 +1,36 @@
import { defineComponentMetadata } from '@/components/define'
import { playerReady } from '@/core/utils'
function moveElementAfter(element: Element, after: Element) {
const parent = element.parentElement
if (!parent) {
return
}
parent.removeChild(element)
parent.insertBefore(element, after.nextSibling)
}
const playerOnTop = async () => {
await playerReady()
const title = document.querySelector('#viewbox_report')
const toolbar = document.querySelector('#arc_toolbar_report')
moveElementAfter(title, toolbar)
const author = document.querySelector('#v_upinfo').parentElement
const danmuku = document.querySelector('#danmukuBox')
moveElementAfter(author, danmuku)
}
export const component = defineComponentMetadata({
name: 'playerOnTopNew',
author: {
name: 'RieN7',
link: 'https://github.com/rien7',
},
tags: [componentsTags.style, componentsTags.video],
displayName: '播放器置顶(新)',
description:
'原来的播放器置顶插件,现在已经不可用了,这是一个新的版本,可以在视频页面中将播放器放在页面最上方.',
entry: playerOnTop,
reload: playerOnTop,
})

View File

@ -0,0 +1 @@
进入视频 / 查看动态时, 自动点赞.

View File

@ -0,0 +1,53 @@
import { defineComponentMetadata } from '@/components/define'
import { forEachFeedsCard } from '@/components/feeds/api'
import { select } from '@/core/spin-query'
import { matchUrlPattern, playerReady } from '@/core/utils'
import { feedsUrls, videoAndBangumiUrls } from '@/core/utils/urls'
const feedsLikeQueue = [] as HTMLElement[]
export const component = defineComponentMetadata({
name: 'autoLike',
displayName: '自动点赞',
tags: [componentsTags.utils, componentsTags.feeds, componentsTags.video],
urlInclude: [...videoAndBangumiUrls, ...feedsUrls],
options: {
video: {
defaultValue: true,
displayName: '对视频点赞',
},
feed: {
defaultValue: true,
displayName: '对动态点赞',
},
},
entry: async ({ settings: { options } }) => {
if (options.video && videoAndBangumiUrls.some(url => matchUrlPattern(url))) {
await playerReady()
const likeButton = (await select(
'.video-toolbar .like, .tool-bar .like-info, .video-toolbar-v1 .like',
)) as HTMLSpanElement
if (!likeButton || likeButton.classList.contains('on')) {
return
}
likeButton.click()
}
if (options.feed && feedsUrls.some(url => matchUrlPattern(url))) {
window.setInterval(() => {
if (feedsLikeQueue.length === 0) {
return
}
const button = feedsLikeQueue.shift()
button?.click()
}, 1000)
forEachFeedsCard({
added: card => {
const likeButton = dq(card.element, '.bili-dyn-action.like') as HTMLElement
if (!likeButton || likeButton.classList.contains('active')) {
return
}
feedsLikeQueue.push(likeButton)
},
})
}
},
})

View File

@ -19,7 +19,7 @@ const importSeries = async (sid, uid, csrf) => {
// 创建收藏夹获取新收藏夹id
let favId = 0
while (true) {
for (;;) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
method: 'POST',
credentials: 'include',
@ -43,7 +43,7 @@ const importSeries = async (sid, uid, csrf) => {
for (let i = 0; i < seriesVideos.length; i++) {
// 做个延迟,防止太快而遭服务器拒绝
await delay(500)
while (true) {
for (;;) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
method: 'POST',
credentials: 'include',
@ -72,7 +72,7 @@ const importSeries = async (sid, uid, csrf) => {
}
const importCollection = async (sid, csrf) => {
while (true) {
for (;;) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
method: 'POST',
credentials: 'include',

View File

@ -4,7 +4,7 @@
{{ row.displayName }}
</div>
<div
class="row-default-binding"
class="row-default-binding row-binding"
:class="{
overwritten: isOverwrittern(presets[selectedPreset], customKeyBindings),
'not-set': presetBase[row.name] === undefined,
@ -13,7 +13,7 @@
{{ showReadonlyKey(presetBase) }}
</div>
<div
class="row-preset-binding"
class="row-preset-binding row-binding"
:class="{
overwritten: isOverwrittern(customKeyBindings),
'not-set': presets[selectedPreset][row.name] === undefined,
@ -21,7 +21,7 @@
>
{{ showReadonlyKey(presets[selectedPreset]) }}
</div>
<div class="row-custom-binding">
<div class="row-custom-binding row-binding">
<div v-if="editable" class="custom-binding-edit">
<TextBox
ref="customBindingTextBox"
@ -129,6 +129,12 @@ export default Vue.extend({
opacity: 0.25;
}
}
.row-binding {
&,
& input {
font-family: monospace, sans-serif;
}
}
.row-custom-binding {
.be-button {
padding: 4px;

View File

@ -0,0 +1 @@
自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.

View File

@ -55,6 +55,7 @@ const entry = async () => {
'noTitleBar',
'msource',
'live_from',
'plat_id',
]
const [blockParams] = registerAndGetData('urlParamsClean.params', builtInBlockParams)
const builtInSiteSpecifiedParams = [
@ -90,12 +91,13 @@ const entry = async () => {
const builtInTailingSlash: { match: string | RegExp }[] = []
const [tailingSlash] = registerAndGetData('urlParamsClean.tailingSlash', builtInTailingSlash)
const clean = () => {
const urlParams = [...new URLSearchParams(window.location.search).entries()].map(
const getCleanUrl = (originalUrl: string) => {
const url = new URL(originalUrl, location.origin)
const urlParams = [...new URLSearchParams(url.search).entries()].map(
([key, value]) => `${key}=${value},`,
)
if (urlParams.some(param => noClean.some(it => param.includes(it)))) {
return
return originalUrl
}
const filteredParams = urlParams.filter(p => {
if (blockParams.some(b => p.startsWith(`${b}=`))) {
@ -111,19 +113,47 @@ const entry = async () => {
return true
})
const filteredParamsString = filteredParams.join('&')
let url = document.URL.replace(window.location.search, '')
tailingSlash.forEach(({ match }) => {
if (matchPattern(url, match) && url.endsWith('/')) {
url = url.slice(0, url.length - 1)
const pathName = url.pathname
if (matchPattern(pathName, match) && pathName.endsWith('/')) {
url.pathname = pathName.slice(0, pathName.length - 1)
}
})
const query = filteredParamsString ? `?${filteredParamsString}` : ''
const newUrl = url + query
url.search = query
return url.toString()
}
const createHistoryHook = (
original: (data: unknown, unused: string, url: string, ...restArgs: unknown[]) => void,
) => {
return function historyHook(
data: unknown,
unused: string,
url: string,
...restArgs: unknown[]
) {
const newUrl = getCleanUrl(url)
if (newUrl !== url) {
console.log('History API 拦截', url, newUrl)
return original.call(this, data, unused, newUrl, ...restArgs)
}
return original.call(this, data, unused, url, ...restArgs)
}
}
const originalPushState = unsafeWindow.history.pushState
unsafeWindow.history.pushState = createHistoryHook(originalPushState)
const originalReplaceState = unsafeWindow.history.replaceState
unsafeWindow.history.replaceState = createHistoryHook(originalReplaceState)
const clean = () => {
const newUrl = getCleanUrl(document.URL)
if (newUrl !== document.URL) {
console.log(document.URL, newUrl)
console.log('直接清理', document.URL, newUrl)
window.history.replaceState(history.state, '', newUrl)
}
}
const { fullyLoaded } = await import('@/core/life-cycle')
const { urlChange } = await import('@/core/observer')
fullyLoaded(() => {
@ -134,10 +164,6 @@ export const component = defineComponentMetadata({
name: 'urlParamsClean',
displayName,
entry,
description: {
'zh-CN':
'自动删除网址中的多余跟踪参数。请注意这会导致浏览器历史记录出现重复的标题(分别是转换前后的网址),并可能导致后退要多退几次。',
},
tags: [componentsTags.utils],
urlExclude: [/game\.bilibili\.com\/fgo/, /live\.bilibili\.com\/p\/html\/live-app-hotrank\//],
})

View File

@ -0,0 +1 @@
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.

View File

@ -0,0 +1,19 @@
import { defineComponentMetadata } from '@/components/define'
import { forEachVideoDanmaku } from '@/components/video/video-danmaku'
export const component = defineComponentMetadata({
name: 'unescapeDanmaku',
displayName: '弹幕转义',
tags: [componentsTags.video],
entry: () => {
const newLineRegex = /\\n/g
forEachVideoDanmaku({
added: danmaku => {
if (newLineRegex.test(danmaku.text)) {
const newText = danmaku.text.replace(newLineRegex, '\n')
danmaku.element.textContent = newText
}
},
})
},
})

View File

@ -1 +1 @@
在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量.
在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量. 请注意不能和 `反转滚轮调音量` 一同使用.

View File

@ -1,10 +1,10 @@
import { defineComponentMetadata } from '@/components/define'
import { preventEvent } from '@/core/utils'
import { allVideoUrls } from '@/core/utils/urls'
let cancel: () => void
const prevent = () => {
cancel?.()
cancel = preventEvent(unsafeWindow, 'mousewheel', () => {
export const disableScrollVolume = () => {
return preventEvent(unsafeWindow, 'mousewheel', () => {
const isFullscreen = ['player-mode-full', 'player-fullscreen-fix', 'player-full-win'].some(
token => document.body.classList.contains(token),
)
@ -15,9 +15,16 @@ export const component = defineComponentMetadata({
name: 'disableScrollVolume',
displayName: '禁止滚轮调音量',
tags: [componentsTags.video],
entry: prevent,
reload: prevent,
entry: () => {
cancel?.()
cancel = disableScrollVolume()
},
reload: () => {
cancel?.()
cancel = disableScrollVolume()
},
unload: () => {
cancel?.()
},
urlInclude: allVideoUrls,
})

View File

@ -444,6 +444,7 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
setTimeout(() => this.forceUpdateStyle(value))
}
// eslint-disable-next-line class-methods-use-this
protected readonly filterNativeSpeed =
() =>
({ subscribe, next }: PublishContext<number>) => {

View File

@ -0,0 +1,5 @@
反转在网页全屏 / 全屏模式下使用滚轮调节音量的方向, 使其更符合使用触控板时的操作方向. 请注意不能和 `禁止滚轮调音量` 一同使用.
- 手指向上推时, 增加音量
- 手指向下推时, 减少音量
- 可以自定义 `灵敏度`, 同样的滚动幅度下, 灵敏度越高变化的音量越多

View File

@ -0,0 +1,60 @@
import {
OptionsOfMetadata,
defineComponentMetadata,
defineOptionsMetadata,
} from '@/components/define'
import { disableScrollVolume } from '../disable-scroll-volume'
import { playerAgent } from '@/components/video/player-agent'
import { getComponentSettings } from '@/core/settings'
import { allVideoUrls } from '@/core/utils/urls'
let cancel: () => void
const MaxDeltaFactor = 30
const options = defineOptionsMetadata({
deltaFactor: {
defaultValue: 15,
slider: {
min: 1,
max: MaxDeltaFactor,
step: 1,
},
displayName: '灵敏度',
},
})
const invertScrollVolume = () => {
cancel?.()
const settings = getComponentSettings<OptionsOfMetadata<typeof options>>('invertScrollVolume')
const handleWheel = (e: WheelEvent) => {
const isFullscreen = [
'player-mode-webfullscreen',
'player-fullscreen-fix',
'player-full-win',
].some(token => document.body.classList.contains(token))
if (isFullscreen) {
playerAgent.changeVolume(e.deltaY / ((MaxDeltaFactor - settings.options.deltaFactor + 1) / 2))
}
}
unsafeWindow.addEventListener('wheel', handleWheel)
const cancelPrevent = disableScrollVolume()
return () => {
unsafeWindow.removeEventListener('wheel', handleWheel)
cancelPrevent()
}
}
export const component = defineComponentMetadata({
name: 'invertScrollVolume',
displayName: '反转滚轮调音量',
tags: [componentsTags.video],
options,
entry: () => {
cancel?.()
cancel = invertScrollVolume()
},
reload: () => {
cancel?.()
cancel = invertScrollVolume()
},
unload: () => cancel?.(),
urlInclude: allVideoUrls,
})

View File

@ -5,6 +5,7 @@ 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'
interface RecommendList extends Vue {
isOpen: boolean
@ -24,6 +25,7 @@ interface VideoPageCard extends Vue {
title: string
// 组件添加元素非b站自有元素
mark: boolean
oldname: string
item: {
aid: string
ctime: number
@ -41,17 +43,69 @@ export const component = defineComponentMetadata({
author: { name: 'wisokey', link: 'https://github.com/wisokey' },
name: 'showUploadTime',
displayName,
description: '为视频播放页面的推荐列表中的视频添加显示视频投稿时间.',
description:
"为视频播放页面的推荐列表中的视频添加显示视频投稿时间.\r\n\r\n`时间格式` 替换up名的文本格式 (默认为'up · yyyy-MM-dd'):\r\n - y: 年\r\n - M: 月\r\n - d: 日\r\n - h: 时\r\n - m: 分\r\n - s: 秒\r\n - q: 季度\r\n - up: up名\r\n",
tags: [componentsTags.video],
urlInclude: videoUrls,
entry: async () => {
const showUploadTime = (relist: VideoPageCard[]) => {
options: {
formatString: {
displayName: '文本格式',
defaultValue: 'up · yyyy-MM-dd',
validator: (value: string, oldValue: string) => (!value?.trim() ? oldValue : value),
},
},
entry: async ({ metadata }) => {
const getFormatStr = (time: Date, format: string, upName: string) => {
const formatMap: any = {
'M+': time.getMonth() + 1, // 月
'd+': time.getDate(), // 日
'h+': time.getHours(), // 时
'm+': time.getMinutes(), // 分
's+': time.getSeconds(), // 秒
'q+': Math.floor((time.getMonth() + 3) / 3), // 季度
}
// 处理年份
let matchResult: RegExpMatchArray | null = format.match(/(y+)/)
if (matchResult !== null) {
format = format.replace(
matchResult[0],
`${time.getFullYear()}`.substring(4 - matchResult[0].length),
)
}
// 处理除年份外的时间
for (const key in formatMap) {
if (!key) {
continue
}
matchResult = format.match(new RegExp(`(${key})`))
if (matchResult !== null) {
format = format.replace(
matchResult[0],
matchResult[0].length === 1
? formatMap[key]
: `00${formatMap[key]}`.substring(`${formatMap[key]}`.length),
)
}
}
// 处理up主名
matchResult = format.match(/(up)/)
if (matchResult !== null) {
format = format.replace(matchResult[0], upName)
}
return format
}
const showUploadTime = (relist: VideoPageCard[], forceUpdate = false, formatString = '') => {
if (!formatString) {
const { options } = getComponentSettings(metadata.name)
formatString = options.formatString?.toString()
}
relist.forEach(async video => {
// 确认存放推荐视频列表的List中的元素是否被更新
if (!video.item.owner.mark) {
if (forceUpdate || !video.item.owner.mark) {
video.item.owner.mark = true
// 确认推荐视频卡片是否被更新
if (!video.mark) {
if (forceUpdate || !video.mark) {
video.mark = true
let createTime: Date
if (video.item.ctime) {
@ -60,10 +114,13 @@ export const component = defineComponentMetadata({
const videoinfo = new VideoInfo(video.item.aid)
await videoinfo.fetchInfo()
createTime = videoinfo.createTime
// 保存查询到的ctime以便后续使用
video.item.ctime = createTime.getTime() / 1000
}
video.name = `${video.name} · ${createTime.getFullYear()}-${
createTime.getMonth() + 1
}-${createTime.getDate()}`
if (!video.oldname) {
video.oldname = video.name
}
video.name = getFormatStr(createTime, formatString, video.oldname)
}
// 保存生成后的name
video.item.owner.name = video.name
@ -85,6 +142,18 @@ export const component = defineComponentMetadata({
return recoList
}
addComponentListener(
`${metadata.name}.formatString`,
(value: string) => {
const recoList: RecommendList = getRecoList()
const relist: VideoPageCard[] = recoList.$children.filter(
video => video.$el.className.indexOf('special') === -1,
)
showUploadTime(relist, true, value)
},
false,
)
urlChange(async () => {
await playerReady()
const recoList: RecommendList = getRecoList()

View File

@ -0,0 +1 @@
在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.

View File

@ -0,0 +1,36 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.audioSearch',
displayName: '搜索栏 - 音频跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'audioSearchProvider',
getActions: async input => {
const { match, type, id, indexer } = matchInput(input, /^(a[um])(\d+)$/)
if (!match) {
return []
}
const json = await getJson(
type === 'am'
? `https://www.bilibili.com/audio/music-service-c/web/menu/info?sid=${id}`
: `https://www.bilibili.com/audio/music-service-c/web/song/info?sid=${id}`,
)
const { title } = lodash.get(json, 'data', {})
return [
createLinkAction({
name: title,
description: type === 'am' ? '播放列表跳转' : '音频跳转',
link: `https://www.bilibili.com/audio/${indexer}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1 @@
在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.

View File

@ -0,0 +1,60 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.bangumiSearch',
displayName: '搜索栏 - 番剧跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'bangumiSearchProvider',
getActions: async input => {
const { match, type, id, indexer } = matchInput(input, /^(md|ss|ep)(\d+)$/)
if (!match) {
return []
}
if (type === 'md') {
const json = await getJson(`https://api.bilibili.com/pgc/review/user?media_id=${id}`)
const { title } = lodash.get(json, 'result.media', {})
return [
createLinkAction({
name: title,
description: '番剧详情跳转',
link: `https://www.bilibili.com/bangumi/media/${indexer}`,
indexer,
}),
]
}
if (type === 'ep') {
const json = await getJson(`https://api.bilibili.com/pgc/view/web/season?ep_id=${id}`)
const episodes = lodash.get(json, 'result.episodes', []) as {
id: number
share_copy: string
}[]
const episode = episodes.find(e => e.id.toString() === id)
return [
createLinkAction({
name: episode?.share_copy,
description: '番剧跳转',
link: `https://www.bilibili.com/bangumi/play/${indexer}`,
indexer,
}),
]
}
const json = await getJson(`https://api.bilibili.com/pgc/view/web/season?season_id=${id}`)
const { title } = lodash.get(json, 'result', {})
return [
createLinkAction({
name: title,
description: '番剧跳转',
link: `https://www.bilibili.com/bangumi/play/${indexer}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1,34 @@
export const matchInput = (input: string, pattern: RegExp) => {
const match = input.match(pattern)
if (!match) {
return {}
}
const type = match[1]
const id = match[2]
const indexer = `${type}${id}`
return {
match,
type,
id,
indexer,
}
}
export const createLinkAction = (input: {
name: string
description?: string
indexer: string
link: string
}) => {
const { name, description, indexer, link } = input
return {
name: name || indexer,
icon: 'mdi-open-in-new',
indexer,
description,
action: () => {
window.open(link, '_blank')
},
order: 0,
}
}

View File

@ -0,0 +1 @@
在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.

View File

@ -0,0 +1,46 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.cvSearch',
displayName: '搜索栏 - 专栏跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'cvSearchProvider',
getActions: async input => {
const { match, type, id, indexer } = matchInput(input, /^(cv|rl)(\d+)$/)
if (!match) {
return []
}
if (type === 'rl') {
const json = await getJson(
`https://api.bilibili.com/x/article/list/web/articles?id=${id}`,
)
const { name } = lodash.get(json, 'data.list', {})
return [
createLinkAction({
name,
description: '文集跳转',
link: `https://www.bilibili.com/read/readlist/${indexer}`,
indexer,
}),
]
}
const json = await getJson(`https://api.bilibili.com/x/article/viewinfo?id=${id}`)
const { title } = lodash.get(json, 'data', {})
return [
createLinkAction({
name: title,
description: '专栏跳转',
link: `https://www.bilibili.com/read/${indexer}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1,3 @@
在输入纯数字时, 提供以下选项:
- 跳转至相应的视频 (视为 av 号)
- 跳转至相应的专栏 (视为 cv 号)

View File

@ -0,0 +1,42 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson, getJsonWithCredentials } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.numberSearch',
displayName: '搜索栏 - 数字联想',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'numberSearchProvider',
getActions: async input => {
const { match, id, indexer } = matchInput(input, /^()(\d+)$/)
if (!match) {
return []
}
const [aidJson, cvJson] = await Promise.all([
await getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/view?aid=${id}`),
await getJson(`https://api.bilibili.com/x/article/viewinfo?id=${id}`),
])
const { title: videoName } = lodash.get(aidJson, 'data', {})
const { title: articleName } = lodash.get(cvJson, 'data', {})
return [
createLinkAction({
name: videoName,
description: '视频跳转',
link: `https://www.bilibili.com/av${id}`,
indexer,
}),
createLinkAction({
name: articleName,
description: '专栏跳转',
link: `https://www.bilibili.com/read/cv${id}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1 @@
在输入 UID (用户 ID) 时, 提供对应的跳转选项.

View File

@ -0,0 +1,32 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.uidSearch',
displayName: '搜索栏 - UID 跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'uidSearchProvider',
getActions: async input => {
const { match, id, indexer } = matchInput(input, /^(uid)(\d+)$/)
if (!match) {
return []
}
const json = await getJson(`https://api.bilibili.com/x/space/wbi/acc/info?mid=${id}`)
const { name } = lodash.get(json, 'data', {})
return [
createLinkAction({
name,
description: '用户跳转',
link: `https://space.bilibili.com/${id}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1,22 @@
import { KeyBindingAction } from 'registry/lib/components/utils/keymap/bindings'
import { PluginMetadata } from '@/plugins/plugin'
import { toggleLight } from '@/components/video/player-light'
export const plugin: PluginMetadata = {
name: 'keymap.actions.togglePlayerLight',
displayName: '快捷键扩展 - 开关灯',
description: '在快捷键的动作列表里添加一个 "开关灯".',
setup: ({ addData }) => {
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
actions.togglePlayerLight = {
displayName: '开关灯',
run: async () => {
toggleLight()
},
}
})
addData('keymap.presets', (presetBase: Record<string, string>) => {
presetBase.togglePlayerLight = 'shift l'
})
},
}

View File

@ -0,0 +1,69 @@
<template>
<div class="bisector-dialog-content-container">
<div class="bisector-dialog-content-text">
组件二等分进行中预计剩余
<b style="color: var(--theme-color)">{{ rouge }} </b>轮次请确认当前情况
</div>
<div class="bisector-dialog-button-group">
<VButton @click="onGood">正常</VButton>
<VButton @click="onBad">不正常</VButton>
<VButton type="primary" @click="onAbort">终止</VButton>
</div>
<div class="bisector-dialog-content-text weak">
*可以点击×号关闭对话框刷新页面后会再次询问
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { VButton } from '@/ui'
export default Vue.extend({
components: { VButton },
props: {
rouge: {
type: Number,
required: true,
},
onAbort: {
type: Function,
default: none,
},
onGood: {
type: Function,
default: none,
},
onBad: {
type: Function,
default: none,
},
},
})
</script>
<style lang="scss" scoped>
.bisector-dialog-content-container {
padding: 0 16px;
.bisector-dialog-content-text {
&.weak {
opacity: 0.6;
}
}
.bisector-dialog-content-text,
.bisector-dialog-button-group {
margin-bottom: 8px;
}
.bisector-dialog-button-group {
display: grid;
gap: 8px;
grid-template-columns: 1fr 1fr;
.be-button:last-child {
grid-column: span 2;
}
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div>
组件二等分
<div
class="peek"
title="透视"
style="margin-left: auto"
@mouseover="peek = true"
@mouseout="peek = false"
>
<VIcon icon="eye" :size="18" />
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { VIcon } from '@/ui'
export default Vue.extend({
components: { VIcon },
data() {
return { peek: false }
},
watch: {
peek(value) {
this.$el.closest('.be-dialog').style.opacity = value ? '0.1' : '1'
},
},
})
</script>
<style lang="scss" scoped>
@import 'common';
div {
@include h-center(8px);
}
.peek {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div>
<div>可能出问题的组件是{{ displayName }}{{ name }}</div>
<div>
<span class="link" @click="restore">恢复原状{{ countdown }} </span>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
countdown: 30,
userComponent: undefined,
}
},
computed: {
displayName() {
return this.userComponent?.metadata?.displayName
},
name() {
return this.userComponent?.metadata?.name
},
},
watch: {
countdown(value) {
if (value === 0) {
this.restore()
}
},
},
created() {
this.interval = setInterval(() => {
if (this.countdown > 0) {
this.countdown--
} else {
clearInterval(this.interval)
}
}, 1e3)
},
destroyed() {
clearInterval(this.interval)
},
methods: {
restore() {
clearInterval(this.interval)
this.$emit('restore')
},
},
})
</script>

View File

@ -0,0 +1,198 @@
import { DialogInstance, showDialog } from '@/core/dialog'
import type { Settings } from '@/core/settings/types'
import { Toast } from '@/core/toast'
import { getRandomId, mountVueComponent, delay } from '@/core/utils'
import { useScopedConsole } from '@/core/utils/log'
import type { RecordValue } from '../types'
import type { BisectNext, BisectReturn } from './bisect'
import { bisect } from './bisect'
import { BisectorOptions } from './options'
import ResultToastContent from './ResultToastContent.vue'
type UserComponent = RecordValue<Settings['userComponents']>
let bisectorOptions: BisectorOptions
let scopedConsole: ReturnType<typeof useScopedConsole>
let bisectorGenerator: Generator<BisectNext<UserComponent>, BisectReturn<UserComponent>>
let groupedComponents: Awaited<ReturnType<typeof classifyComponents>>
let dialog: DialogInstance
export const setOptions = (options: BisectorOptions) => {
if (!bisectorOptions) {
bisectorOptions = options
} else {
Object.assign(bisectorOptions, options)
}
}
export const setConsole = (console: ReturnType<typeof useScopedConsole>) => {
scopedConsole = console
}
const classifyComponents = async () => {
const { settings } = await import('@/core/settings')
const { userComponents } = settings
const configurableUserComponents = lodash.pickBy(
userComponents,
v => v.metadata.configurable || true,
)
const { targetComponents, keepDisabledComponents, keepEnabledComponents } = lodash.transform(
configurableUserComponents,
(result, v, k) => {
if (bisectorOptions.keepEnabledComponents?.includes(k)) {
result.keepEnabledComponents.push(v)
return
}
if (bisectorOptions.keepDisabledComponents?.includes(k)) {
result.keepDisabledComponents.push(v)
return
}
result.targetComponents.push(v)
},
{
targetComponents: [] as UserComponent[],
keepDisabledComponents: [] as UserComponent[],
keepEnabledComponents: [] as UserComponent[],
},
)
return {
userComponents,
configurableUserComponents,
targetComponents,
keepDisabledComponents,
keepEnabledComponents,
}
}
const setComponentsEnabled = (components: UserComponent[], enabled: boolean) =>
components.forEach(component => (component.settings.enabled = enabled))
const getComponentNames = (components: UserComponent[]) =>
components
.map(component => `${component.metadata.displayName}${component.metadata.name}`)
.join(', ') || '无'
export const isRecover = () => !lodash.isEmpty(bisectorOptions.bisectInitialState)
export const stop = async () => {
scopedConsole?.log('stop - 准备停止组件二等分')
dialog?.close()
const { configurableUserComponents } = await classifyComponents()
const unmatchedComponents: UserComponent[] = []
for (const [componentName, componentSettings] of Object.entries(configurableUserComponents)) {
const originalStatus = bisectorOptions.originalComponentEnableState?.[componentName]
if (originalStatus == null) {
unmatchedComponents.push(componentSettings)
continue
}
componentSettings.settings.enabled = originalStatus
}
if (unmatchedComponents.length) {
const msg = `部分组件未能还原状态:${getComponentNames(unmatchedComponents)}`
scopedConsole?.warn(`stop - ${msg}`)
Toast.error(msg, '组件二等分')
await delay(3e3)
}
scopedConsole?.log('stop - 清理状态')
bisectorGenerator = null
bisectorOptions.bisectInitialState = {}
bisectorOptions.originalComponentEnableState = {}
scopedConsole?.log('stop - 重载页面')
location.reload()
}
export const next = async (seeingBad?: boolean, autoReload?: boolean) => {
dialog?.close()
scopedConsole?.log(
`next - 当前工作状态:${
// eslint-disable-next-line no-nested-ternary
seeingBad == null ? '未知' : seeingBad ? '异常' : '正常'
}`,
)
const { done, value } = bisectorGenerator.next(seeingBad)
if (done) {
const { low, high } = value
bisectorOptions.bisectInitialState = { low, high }
const elementId = `bisector-result-toast-content-${getRandomId()}`
Toast.info(/* html */ `<div id="${elementId}"></div>`, '组件二等分结果')
await delay()
const vm = mountVueComponent<{ userComponent: UserComponent }>(
ResultToastContent,
`#${elementId}`,
)
vm.userComponent = value.target
vm.$on('restore', () => {
stop()
})
} else {
const { slice, low, high } = value as BisectNext<UserComponent>
const needEnabled = slice
const needDisabled = lodash.difference(groupedComponents.targetComponents, slice)
bisectorOptions.bisectInitialState = { low, high }
scopedConsole?.log(`next - 关闭组件:${getComponentNames(needDisabled)}`)
scopedConsole?.log(`next - 开启组件:${getComponentNames(needEnabled)}`)
setComponentsEnabled(needDisabled, false)
setComponentsEnabled(needEnabled, true)
if (autoReload) {
scopedConsole?.log('next - 重载页面')
location.reload()
}
}
return { done, value }
}
export const recover = async () => {
scopedConsole?.log('recover - 准备恢复组件二等分')
groupedComponents = await classifyComponents()
const { targetComponents, keepDisabledComponents, keepEnabledComponents } = groupedComponents
setComponentsEnabled(keepEnabledComponents, true)
setComponentsEnabled(keepDisabledComponents, false)
scopedConsole?.log(`recover - 保持关闭组件:${getComponentNames(keepDisabledComponents)}`)
scopedConsole?.log(`recover - 保持开启组件:${getComponentNames(keepEnabledComponents)}`)
scopedConsole?.log(`recover - 全部目标组件:${getComponentNames(targetComponents)}`)
bisectorGenerator = bisect(targetComponents, bisectorOptions.bisectInitialState)
const { done, value } = await next()
if (!done) {
const { rouge } = value as BisectNext<UserComponent>
dialog = showDialog({
title: () => import('./DialogTitle.vue'),
content: () => import('./DialogContent.vue'),
contentProps: {
rouge,
onGood: () => next(false, true),
onBad: () => next(true, true),
onAbort: () => stop(),
},
})
}
}
export const start = async () => {
if (isRecover()) {
await recover()
return
}
scopedConsole?.log('start - 准备开始组件二等分')
groupedComponents = await classifyComponents()
const {
configurableUserComponents,
targetComponents,
keepDisabledComponents,
keepEnabledComponents,
} = groupedComponents
scopedConsole?.log('start - 保存组件初始启用状态')
bisectorOptions.originalComponentEnableState = lodash.mapValues(
configurableUserComponents,
v => v.settings.enabled,
)
setComponentsEnabled(targetComponents, true)
setComponentsEnabled(keepEnabledComponents, true)
setComponentsEnabled(keepDisabledComponents, false)
scopedConsole?.log(`start - 保持关闭组件:${getComponentNames(keepDisabledComponents)}`)
scopedConsole?.log(`start - 保持开启组件:${getComponentNames(keepEnabledComponents)}`)
scopedConsole?.log(`start - 启用全部目标组件:${getComponentNames(targetComponents)}`)
bisectorGenerator = bisect(targetComponents, bisectorOptions.bisectInitialState)
await next(undefined, true)
}

View File

@ -0,0 +1,92 @@
/* eslint-disable no-bitwise */
export interface BisectNext<O> {
low: number
high: number
mid: number
slice: O[]
rouge: number
}
export interface BisectReturn<O> extends BisectNext<O> {
target: O
}
export interface InitialState {
low?: number
high?: number
}
export function* bisectLeft<O>(
data: readonly O[],
initialState?: InitialState,
): Generator<BisectNext<O>, BisectReturn<O>> {
let low = initialState?.low ?? 0
let high = initialState?.high ?? data.length
let mid: number
while (low + 1 < high) {
mid = (low + high) >>> 1
const seeingBad = yield {
low,
high,
mid,
slice: data.slice(low, mid),
rouge: Math.trunc(Math.log2(high - low)),
}
if (seeingBad) {
high = mid
} else {
low = mid
}
}
return {
low,
high,
mid,
slice: data.slice(low, mid),
rouge: Math.trunc(Math.log2(high - low)),
target: data[low],
}
}
export function* bisectRight<O>(
data: readonly O[],
initialState?: InitialState,
): Generator<BisectNext<O>, BisectReturn<O>> {
let low = initialState?.low ?? 0
let high = initialState?.high ?? data.length
let mid = (low + high) >>> 1
while (low + 1 < high) {
mid = (low + high) >>> 1
const seeingBad = yield {
low,
high,
mid,
slice: data.slice(mid, high),
rouge: Math.trunc(Math.log2(high - low)),
}
if (seeingBad) {
low = mid
} else {
high = mid
}
}
return {
low,
high,
mid,
slice: data.slice(low, mid),
rouge: Math.trunc(Math.log2(high - low)),
target: data[low],
}
}
export const bisect = bisectLeft

View File

@ -0,0 +1,46 @@
import { defineComponentMetadata } from '@/components/define'
import { bisectorOptionsMetadata } from './options'
import { LifeCycleEventTypes } from '@/core/life-cycle'
import { componentsTags } from '@/components/types'
import { useScopedConsole } from '@/core/utils/log'
import type { LaunchBarActionProvider } from '../launch-bar/launch-bar-action'
export const component = defineComponentMetadata({
name: 'bisector',
displayName: '组件二等分',
tags: [componentsTags.general, componentsTags.utils],
hidden: true,
configurable: false,
entry: async ({ settings: { options } }) => {
const bisector = await import('./api')
bisector.setOptions(options)
bisector.setConsole(useScopedConsole('组件二等分'))
unsafeWindow.addEventListener(LifeCycleEventTypes.ComponentsLoaded, () => {
if (bisector.isRecover()) {
bisector.recover()
}
})
},
options: bisectorOptionsMetadata,
plugin: {
displayName: '组件二等分 - 功能扩展',
setup: ({ addData }) => {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'bisector-start',
getActions: async () => [
{
name: '开始 / 继续组件二等分',
description: 'Start/Continue component bisection',
icon: 'mdi-view-split-horizontal',
action: async () => {
const bisector = await import('./api')
await bisector.start()
},
},
],
})
})
},
},
})

View File

@ -0,0 +1,31 @@
import { defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
import { getComponentSettings } from '@/core/settings'
import type { InitialState } from './bisect'
export const bisectorOptionsMetadata = defineOptionsMetadata({
// 原始的组件启用状态
originalComponentEnableState: {
defaultValue: {} as Record<string, boolean>,
hidden: true,
},
// 保持禁用状态的组件内部名称数组
keepDisabledComponents: {
defaultValue: [] as string[],
hidden: true,
},
// 保持启用状态的组件内部名称数组
keepEnabledComponents: {
defaultValue: [] as string[],
hidden: true,
},
// bisect 生成器的初始状态
bisectInitialState: {
defaultValue: {} as Partial<InitialState>,
hidden: true,
},
})
export const getBisectorOptions = () =>
getComponentSettings<OptionsOfMetadata<typeof bisectorOptionsMetadata>>('bisector').options
export type BisectorOptions = ReturnType<typeof getBisectorOptions>

View File

@ -4,6 +4,7 @@ import { component as LaunchBar } from './launch-bar'
import { component as I18n } from './i18n'
import { component as AutoUpdate } from './auto-update'
import { component as NotifyNewVersion } from './notify-new-version'
import { component as Bisector } from './bisector'
import { component as Compatibilities } from './compatibilities'
export const getBuiltInComponents = (): ComponentMetadata[] => [
@ -12,6 +13,7 @@ export const getBuiltInComponents = (): ComponentMetadata[] => [
I18n,
AutoUpdate,
NotifyNewVersion,
Bisector,
Compatibilities,
]

View File

@ -141,19 +141,27 @@ 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 { loadFeatureCode } = await import('@/core/external-input/load-feature-code')
const loadUserComponent = (component: ComponentMetadata) => {
components.push(component)
componentsMap[component.name] = component
}
const userComponents = await loadFeaturesFromCodes(
FeatureKind.Component,
Object.keys(settings.userComponents),
Object.values(settings.userComponents).map(it => it.code),
)
userComponents.forEach(loadUserComponent)
for (const [name, setting] of Object.entries(settings.userComponents)) {
const { code } = setting
let metadata: ComponentMetadata
try {
metadata = loadFeatureCode(code) as ComponentMetadata
} catch (e) {
console.error('从代码加载用户组件失败。代码可能有语法错误或代码执行时有抛出值。', {
componentName: name,
error: e,
})
continue
}
loadUserComponent(metadata)
}
}
/** 载入所有组件 */
export const loadAllComponents = async () => {

View File

@ -76,7 +76,7 @@ export default Vue.extend({
justify-content: center;
height: 24px;
width: 18px;
margin: 0 -3px 0 3px;
margin-right: -3px;
}
&-name {
@include single-line();

View File

@ -12,11 +12,15 @@ export class Translator {
static map: Map<string, any>
static regex: [RegExp, string][]
// eslint-disable-next-line class-methods-use-this
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
// eslint-disable-next-line class-methods-use-this
protected getValue = (node: Node) => node.nodeValue
// eslint-disable-next-line class-methods-use-this
protected setValue = (node: Node, value: string) => {
node.nodeValue = value
}
// eslint-disable-next-line class-methods-use-this
protected getElement = (node: Node) => node as Element
translate(node: Node) {
let value = this.getValue(node)
@ -99,17 +103,23 @@ export class Translator {
}
}
export class TextNodeTranslator extends Translator {
// eslint-disable-next-line class-methods-use-this
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
// eslint-disable-next-line class-methods-use-this
getElement = (node: Node) => node.parentElement
}
export class TitleTranslator extends Translator {
// eslint-disable-next-line class-methods-use-this
getValue = (node: Node) => (node as Element).getAttribute('title')
// eslint-disable-next-line class-methods-use-this
setValue = (node: Node, value: string) => {
;(node as Element).setAttribute('title', value)
}
}
export class PlaceholderTranslator extends Translator {
// eslint-disable-next-line class-methods-use-this
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
// eslint-disable-next-line class-methods-use-this
setValue = (node: Node, value: string) => {
;(node as Element).setAttribute('placeholder', value)
}

View File

@ -53,15 +53,15 @@ export default Vue.extend({
},
},
methods: {
performAction(event: KeyboardEvent | MouseEvent) {
this.action.action()
async performAction(event: KeyboardEvent | MouseEvent) {
await this.action.action()
this.$emit('action', event)
},
performDelete(event: KeyboardEvent | MouseEvent) {
async performDelete(event: KeyboardEvent | MouseEvent) {
if (!this.action.deleteAction) {
return
}
this.action.deleteAction()
await this.action.deleteAction()
this.$emit('delete-item', event)
},
},

View File

@ -75,11 +75,16 @@ import {
} from './launch-bar-action'
import { searchProvider, search } from './search-provider'
import { historyProvider } from './history-provider'
import { ascendingSort } from '@/core/utils/sort'
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
searchProvider,
historyProvider,
]) as [LaunchBarActionProvider[]]
const sortActions = (actions: LaunchBarAction[]) => {
return [...actions].sort(ascendingSort(it => it.order ?? Infinity))
}
const generateKeys = (
provider: LaunchBarActionProvider,
actions: LaunchBarAction[],
@ -111,13 +116,15 @@ async function getOnlineActions() {
})
const fuseResult = fuse.search(this.keyword)
console.log(fuseResult)
this.actions = fuseResult.map(it => it.item).slice(0, 12)
this.actions = sortActions(fuseResult.map(it => it.item).slice(0, 12))
this.noActions = this.actions.length === 0
}
async function getActions() {
this.noActions = false
if (this.isHistory) {
this.actions = generateKeys(historyProvider, await historyProvider.getActions(this.keyword))
this.actions = sortActions(
generateKeys(historyProvider, await historyProvider.getActions(this.keyword)),
)
return
}
const actions: LaunchBarAction[] = []
@ -187,7 +194,7 @@ export default Vue.extend({
}
if (this.actions.length > 0 && !this.isHistory) {
const [first] = this.actions as LaunchBarAction[]
if (first.explicitSelect === false) {
if (first.explicitSelect !== true) {
first.action()
return
}

View File

@ -1,22 +1,22 @@
// import { formatDate } from '@/core/utils/formatters'
import { crossOriginLocalStorage } from '@/core/local-storage'
import { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
export interface HistoryItem {
value: string
isHistory: number
timestamp: number
}
const SearchHistoryKey = 'search_history'
const SearchHistoryMaxItems = 12
export const getHistoryItems = (key = SearchHistoryKey) => {
const historyText = localStorage.getItem(key)
const SearchHistoryKey = 'search_history:search_history'
const SearchHistoryMaxItems = 20
export const getHistoryItems = async (key = SearchHistoryKey) => {
const historyText = await crossOriginLocalStorage.getItem(key)
const historyItems: HistoryItem[] = historyText ? JSON.parse(historyText) : []
return historyItems
}
export const clearHistoryItems = (key = SearchHistoryKey) => localStorage.setItem(key, '[]')
export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
export const clearHistoryItems = (key = SearchHistoryKey) =>
crossOriginLocalStorage.setItem(key, '[]')
export const addHistoryItem = async (keyword: string, key = SearchHistoryKey) => {
console.log('add', keyword)
localStorage.setItem(
await crossOriginLocalStorage.setItem(
key,
JSON.stringify(
lodash
@ -25,10 +25,9 @@ export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
[
{
value: keyword,
isHistory: 1,
timestamp: Number(new Date()),
},
...getHistoryItems(),
...(await getHistoryItems()),
],
h => h.value,
),
@ -39,13 +38,13 @@ export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
),
)
}
export const deleteHistoryItem = (keyword: string, key = SearchHistoryKey) => {
const items = getHistoryItems()
export const deleteHistoryItem = async (keyword: string, key = SearchHistoryKey) => {
const items = await getHistoryItems()
const index = items.findIndex(it => it.value === keyword)
console.log('delete', keyword, index)
if (index !== -1) {
items.splice(index, 1)
localStorage.setItem(key, JSON.stringify(items))
await crossOriginLocalStorage.setItem(key, JSON.stringify(items))
}
}
export const historyProvider: LaunchBarActionProvider = {
@ -57,11 +56,12 @@ export const historyProvider: LaunchBarActionProvider = {
icon: 'mdi-trash-can-outline',
description: 'Clear History',
explicitSelect: true,
action: () => {
clearHistoryItems()
action: async () => {
await clearHistoryItems()
},
}
const items = getHistoryItems().map(
const historyItems = await getHistoryItems()
const items = historyItems.map(
it =>
({
name: it.value,
@ -71,8 +71,8 @@ export const historyProvider: LaunchBarActionProvider = {
action: () => {
search(it.value)
},
deleteAction: () => {
deleteHistoryItem(it.value)
deleteAction: async () => {
await deleteHistoryItem(it.value)
},
} as LaunchBarAction),
)

View File

@ -20,6 +20,8 @@ export interface LaunchBarAction {
deleteAction?: Executable
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
explicitSelect?: boolean
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
order?: number
}
/** LaunchBar 动作提供者的插件key, 可注入其他提供者 */
export const LaunchBarActionProviders = 'launchBar.actions'

View File

@ -216,3 +216,6 @@ export interface ComponentMetadata<O extends UnknownOptions = UnknownOptions>
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
export type UserComponentMetadata = Omit<ComponentMetadata, keyof FunctionalMetadata>
/** 推断 Record 的 Value 类型 */
export type RecordValue<R> = R extends Record<any, infer V> ? V : never

View File

@ -1,6 +1,8 @@
import { componentToSettings } from '@/core/settings'
import { isBuiltInComponent } from './built-in-components'
import { ComponentMetadata, componentsMap } from './component'
import * as bisector from './bisector/api'
import { BisectorOptions } from './bisector/options'
/**
*
@ -8,10 +10,12 @@ import { ComponentMetadata, componentsMap } from './component'
*/
export const installComponent = async (code: string) => {
const { components } = await import('./component')
const { parseExternalInput } = await import('../core/external-input')
const component = await parseExternalInput<ComponentMetadata>(code)
if (component === null) {
throw new Error('无效的组件代码')
const { loadFeatureCode } = await import('@/core/external-input')
let component: ComponentMetadata
try {
component = loadFeatureCode(code) as ComponentMetadata
} catch (e) {
throw new Error('无效的组件代码', { cause: e })
}
const { settings } = await import('@/core/settings')
if (isBuiltInComponent(component.name)) {
@ -130,3 +134,16 @@ export const toggleComponent = async (nameOrDisplayName: string) => {
const { displayName } = userComponent.metadata
return `${enabled ? '开启' : '关闭'}组件'${displayName}', 可能需要刷新后才能生效`
}
/**
*
*
* @param options
* @returns
*/
export const bisectComponent = async (options?: BisectorOptions) => {
if (options) {
bisector.setOptions(options)
}
return bisector
}

View File

@ -23,6 +23,8 @@ export interface CommentReplyItem {
}
/** 表示一条评论 */
export interface CommentItem extends CommentReplyItem {
/** 评论图片 */
pictures?: string[]
/** 回复 */
replies: CommentReplyItem[]
/** 回复有更新时调用此函数 */
@ -101,6 +103,9 @@ const parseCommentItemV2 = (element: HTMLElement) => {
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) {

View File

@ -70,8 +70,13 @@ export abstract class PlayerAgent {
}
/** true 开灯false 关灯 */
async toggleLight(on: boolean) {
async toggleLight(on?: boolean) {
const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement
// 无指定参数, 直接 toggle
if (on === undefined) {
checkbox.click()
return
}
// 关灯状态 && 要开灯 -> 开灯
checkbox.checked && on && checkbox.click()
// 开灯状态 && 要关灯 -> 关灯

View File

@ -5,7 +5,7 @@ import { playerAgent } from './player-agent'
// let initialized = false
const setLight = (on: boolean) => {
const setLight = (on?: boolean) => {
if (!playerUrls.some(url => matchUrlPattern(url))) {
return none
}
@ -27,3 +27,4 @@ const setLight = (on: boolean) => {
export const lightOn = setLight(true)
export const lightOff = setLight(false)
export const toggleLight = setLight()

View File

@ -8,6 +8,7 @@ import * as installFeature from '@/core/install-feature'
import * as horizontalScroll from '@/core/horizontal-scroll'
import * as lifeCycle from '@/core/life-cycle'
import * as loadingMode from '@/core/loading-mode'
import * as localStorage from '@/core/local-storage'
import * as meta from '@/core/meta'
import * as observer from '@/core/observer'
import * as reorder from '@/core/reorder'
@ -47,6 +48,7 @@ export const coreApis = {
horizontalScroll,
lifeCycle,
loadingMode,
localStorage,
meta,
observer,
reorder,
@ -87,6 +89,7 @@ export const externalApis = {
...horizontalScroll,
lifeCycle,
...loadingMode,
...localStorage,
...meta,
observer,
...reorder,

View File

@ -10,7 +10,7 @@ export interface DialogInputs {
}
export interface DialogInstance extends Required<DialogInputs> {
open: boolean
close: Promise<void>
close(): Promise<void>
closeListeners: (() => void)[]
}
export const showDialog = (inputs: DialogInputs) => {

View File

@ -1,5 +1,7 @@
import { pickFile } from '@/core/file-picker'
export * from './load-feature-code'
/** 外部输入包装类型, 详见`parseExternalInput`的文档 */
export type ExternalInput<T> = undefined | string | T
// const arrayReturn = <InputType extends any[], OutputType>(

View File

@ -1,26 +0,0 @@
import { loadFeatureCode, LoadFeatureCodeResult } from '@/core/external-input/load-feature-code'
type LdRes<X> = LoadFeatureCodeResult<X>
type SettledRes<T> = PromiseSettledResult<T>
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 mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => p.then(mapSettledArray)
/**
* feature
*
* @param codes
* @returns `Promise`
*/
export const loadFeatureCodeAllSettled = <X>(
codes: string[],
): Promise<LoadFeatureCodeResult<X>[]> =>
lodash(codes)
.map<Promise<LdRes<X>>>(loadFeatureCode)
.thru<Promise<SettledRes<LdRes<X>>[]>>(arr => Promise.allSettled(arr))
.thru(mapSettleResult)
.value()

View File

@ -1,156 +0,0 @@
import {
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 is LoadFeatureCodeAllResultOk<X>
readonly isError: (
this: LoadFeatureCodeAllResult<FeatureBase>,
) => this is LoadFeatureCodeAllResultError
readonly isNoExport: (
this: LoadFeatureCodeAllResult<FeatureBase>,
) => this is LoadFeatureCodeAllResultNoExport
readonly isCodeThrew: (
this: LoadFeatureCodeAllResult<FeatureBase>,
) => this is LoadFeatureCodeAllResultCodeThrew
}
/**
* features
*
* @namespace
* @property features
*/
interface LoadFeatureCodeAllResultOk<X extends FeatureBase> extends ResultInstance {
readonly tag: 'Ok'
readonly features: X[]
}
/** 代码没有导出任何值 */
interface LoadFeatureCodeAllResultNoExport extends ResultInstance {
readonly tag: 'NoExport'
}
/**
*
*
* @namespace
* @property thrown
*/
interface LoadFeatureCodeAllResultCodeThrew extends ResultInstance {
readonly tag: 'CodeThrew'
readonly thrown: unknown
}
type LoadFeatureCodeAllResultError =
| LoadFeatureCodeAllResultNoExport
| LoadFeatureCodeAllResultCodeThrew
type LoadFeatureCodeAllResult<X extends FeatureBase> =
| 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'
},
}
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, {
tag: 'CodeThrew' as const,
thrown,
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type Task<Ok, Err = never> = Promise<Ok>
type LdRes<X> = LoadFeatureCodeResult<X>
type LdOk<X> = LoadFeatureCodeResultOk<X>
type LdErr = LoadFeatureCodeResultError
type LdAllRes<X> = LoadFeatureCodeAllResult<X>
type LdAllOk<X> = LoadFeatureCodeAllResultOk<X>
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> =>
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))
// 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
): 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))
// create `LdAllErr` from `[number, LdErr]`
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()
// create a `LdAllRes` wrapped by `Task`
const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> =>
t.then(createOkResult).catch(createErrResult)
/**
* feature
*
*
*
* @param codes
* @returns `Promise` `LoadFeatureCodeAllResult`
*/
const loadFeatureCodeAll = <X>(codes: string[]): Promise<LoadFeatureCodeAllResult<X>> =>
lodash(codes).thru<LoadCodesTask<X>>(loadCodes).thru(createTaskResult).value()
export {
loadFeatureCodeAll,
LoadFeatureCodeAllResult,
LoadFeatureCodeAllResultOk,
LoadFeatureCodeAllResultError,
LoadFeatureCodeAllResultNoExport,
LoadFeatureCodeAllResultCodeThrew,
}

View File

@ -1,116 +1,87 @@
import { FeatureBase } from '@/components/types'
export class LoadFeatureCodeError extends Error {}
interface ResultInstance {
readonly isOk: <X extends FeatureBase>(
this: LoadFeatureCodeResult<X>,
) => this is LoadFeatureCodeResultOk<X>
readonly isError: (this: LoadFeatureCodeResult<FeatureBase>) => this is LoadFeatureCodeResultError
readonly isNoExport: (
this: LoadFeatureCodeResult<FeatureBase>,
) => this is LoadFeatureCodeResultNoExport
readonly isCodeThrew: (
this: LoadFeatureCodeResult<FeatureBase>,
) => this is LoadFeatureCodeResultCodeThrew
/**
* feature
*/
type CodeSandbox = {
/**
*
*
* @remarks
* {@link loadFeatureCode}
*
* @returns `[导出值, 返回值]`
* @throws {@link LoadFeatureCodeError}
*
*/
run(code: string): [unknown, unknown]
}
/**
* feature
* feature
*
* @namespace
* @property feature
* @returns `CodeSandbox`
*/
interface LoadFeatureCodeResultOk<X extends FeatureBase> extends ResultInstance {
readonly tag: 'Ok'
readonly feature: X
}
/** 代码没有导出任何值 */
interface LoadFeatureCodeResultNoExport extends ResultInstance {
readonly tag: 'NoExport'
}
/**
*
*
* @namespace
* @property thrown
*/
interface LoadFeatureCodeResultCodeThrew extends ResultInstance {
readonly tag: 'CodeThrew'
readonly thrown: unknown
}
type LoadFeatureCodeResultError = LoadFeatureCodeResultNoExport | LoadFeatureCodeResultCodeThrew
type LoadFeatureCodeResult<X extends FeatureBase> =
| 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'
},
}
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, {
tag: 'CodeThrew' as const,
thrown,
const createCodeSandbox = (): CodeSandbox => {
// 需要被注入到 `sandbox` 中的键值对
const injection = new Map([
// 加固,防止逃逸
[Symbol.unscopables, undefined],
['unsafeWindow', unsafeWindow],
] as [keyof any, unknown][])
// 目标代码执行时的全局对象代理
const sandbox = new Proxy(Object.create(null), {
has: () => true,
get: (_, p) => (injection.has(p) ? injection.get(p) : window[p as string]),
set: (_, p, v) => !injection.has(p) && (window[p as string] = v),
})
const codeKey = 'BILIBILI_EVOLVED_LOAD_FEATURE_CODE_CODE_KEY_3B63D912__'
// eslint-disable-next-line no-new-func
const fn = Function(
'window',
`with (window) {
return eval(${codeKey})
}`,
).bind(undefined, sandbox)
return {
run(code) {
injection.set('exports', {})
injection.set(codeKey, code)
let returned
try {
returned = fn()
} catch (e) {
throw new LoadFeatureCodeError(undefined, { cause: e })
}
const exportsValues = Object.values(injection.get('exports'))
const exported = exportsValues.length > 0 ? exportsValues[0] : undefined
return [exported, returned]
},
}
}
let staticCodeSandbox: CodeSandbox | undefined
/**
* feature
* feature (component, plugin, style)
*
* @param code
* @returns `Promise` {@link LoadFeatureCodeResult}
* @remarks
* feature
* 1. UMD
* 2.
*
* 线
*
* `use strict` feature
*
* `window`访 `unsafeWindow`
*
* @param code -
* @returns feature
* @throws {@link LoadFeatureCodeError}
*
*/
const loadFeatureCode = async <X extends FeatureBase>(
code: string,
): Promise<LoadFeatureCodeResult<X>> => {
// 收集代码导出值
const exports = {}
let result: X
try {
result = eval(code)
} catch (thrown) {
return codeThrewResult(thrown)
}
const values = Object.values(exports)
if (values.length === 0) {
if (typeof result === 'object') {
return okResult(result)
}
return noExportResult
}
return okResult(values[0] as X)
}
export {
loadFeatureCode,
LoadFeatureCodeResult,
LoadFeatureCodeResultOk,
LoadFeatureCodeResultError,
LoadFeatureCodeResultNoExport,
LoadFeatureCodeResultCodeThrew,
export const loadFeatureCode = (code: string): unknown => {
staticCodeSandbox || (staticCodeSandbox = createCodeSandbox())
const [exported, returned] = staticCodeSandbox.run(code)
return exported || returned
}

View File

@ -1,114 +0,0 @@
import {
LoadFeatureCodeResultError,
LoadFeatureCodeResultOk,
} from '@/core/external-input/load-feature-code'
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'
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 prefix = kind === FeatureKind.Component ? 'component' : 'plugin'
return (featureName, err) => {
if (err.isNoExport()) {
curConsole.error(`${prefix} '${featureName}' exports no value, failed to load`)
} else {
curConsole.error(
`${prefix} '${featureName}' throws something when importing, failed to load`,
{ thrown: err.thrown },
)
}
}
}
const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => {
type ErrInfo = number | string[]
const emptyErrInfo: () => string[] = () => []
const accErrInfo = (acc: ErrInfo, featureName: string): ErrInfo => {
if (Array.isArray(acc)) {
if (acc.length < 3) {
acc.push(featureName)
return acc
}
return 4
}
return acc + 1
}
const reportErrInfo = async (kind: FeatureKind, info: ErrInfo) => {
const { Toast } = await import('../toast')
const kindName = kind === FeatureKind.Component ? '组件' : '插件'
if (Array.isArray(info)) {
Toast.error(
`${kindName} "${info.join('", "')}" 加载失败。请向我们反馈,以解决此问题。`,
`${kindName}加载失败`,
)
} else {
Toast.error(
`${info}${kindName}加载失败,请向我们反馈,以解决此问题。`,
`${kindName}加载失败`,
)
}
}
const errInfo = errNames.reduce(accErrInfo, emptyErrInfo())
reportErrInfo(featureKind, errInfo)
}
export type FeatureMetadata = ComponentMetadata | PluginMetadata
/**
*
*
* console
*
* `names` `codes`
*
* @param kind
* @param names
* @param codes
* @return
*/
export async function loadFeaturesFromCodes(
kind: FeatureKind.Component,
names: string[],
codes: string[],
): Promise<ComponentMetadata[]>
export async function loadFeaturesFromCodes(
kind: FeatureKind.Plugin,
names: string[],
codes: string[],
): Promise<PluginMetadata[]>
export async function loadFeaturesFromCodes(
kind: FeatureKind,
names: string[],
codes: string[],
): Promise<FeatureMetadata[]> {
const results = await loadFeatureCodeAllSettled<FeatureMetadata>(codes)
const [namedOk, namedErr] = lodash(results)
.map((r, i) => [names[i], r] as const)
.partition(([, r]) => r.isOk())
.value()
// 输出日志
lodash.forEach(namedErr, lodash.spread(logError(kind)))
// 向用户输出错误报告
if (namedErr.length > 0) {
const errNames = namedErr.map(([name]) => name)
reportErrToUser(kind, errNames)
}
return lodash.map(namedOk, ([, r]) => (r as LoadFeatureCodeResultOk<FeatureMetadata>).feature)
}

View File

@ -36,8 +36,8 @@ export const installFeatureFromCode = async (
metadata: FeatureType
message: string
}> => {
const { parseExternalInput } = await import('../core/external-input')
const item = await parseExternalInput<FeatureType>(code)
const { loadFeatureCode } = await import('../core/external-input')
const item = loadFeatureCode(code) as FeatureType
const { type, installer } = (() => {
if (isComponent(item)) {
return {

134
src/core/local-storage.ts Normal file
View File

@ -0,0 +1,134 @@
import { select } from './spin-query'
import { deleteValue, getRandomId } from './utils'
interface CrossOriginMessage {
type: string
id?: string
key?: string
value?: unknown
}
interface CrossOriginMessageListener {
id: string
resolve: (result: unknown) => void
}
interface CrossOriginStorageListener {
id: string
callback: (newValue: string) => void
}
const postMessage = async (message: CrossOriginMessage) => {
const iframe = (await select(
'iframe[src^="https://s1.hdslb.com/bfs/seed/jinkela/short/cols/iframe.html"][data-loaded="true"]',
)) as HTMLIFrameElement
if (!iframe) {
throw new Error('COLS iframe not found')
}
iframe.contentWindow.postMessage(message, 'https://s1.hdslb.com')
}
const messageListeners: CrossOriginMessageListener[] = []
const storageListeners: CrossOriginStorageListener[] = []
window.addEventListener('message', e => {
if (e.data.type === 'COLS_RES') {
const data = e.data as CrossOriginMessage
const { id } = data
const handleMessageListeners = () => {
const listenerIndex = messageListeners.findIndex(it => it.id === id)
if (listenerIndex < 0) {
return
}
const [listener] = messageListeners.splice(listenerIndex, 1)
listener.resolve(data.value)
}
const handleStorageListeners = () => {
const index = storageListeners.findIndex(it => it.id === id)
if (index < 0) {
return
}
storageListeners[index].callback(data.value as string)
}
handleMessageListeners()
handleStorageListeners()
}
})
const wrapMessageApi = <ReturnType = void>(handler: (messageId: string) => void) => {
return () =>
new Promise<ReturnType>(resolve => {
const id = getRandomId(32)
messageListeners.push({
id,
resolve,
})
handler(id)
})
}
/** 跨域 Local Storage (由 b 站官方提供) */
export const crossOriginLocalStorage = {
setItem: (key: string, item: unknown) => {
return wrapMessageApi(id => {
postMessage({
id,
type: 'COLS_SET',
key,
value: item,
})
})()
},
getItem: <ReturnType = string>(key: string) => {
return wrapMessageApi<ReturnType>(id => {
postMessage({
id,
type: 'COLS_GET',
key,
})
})()
},
removeItem: (key: string) => {
return wrapMessageApi(id => {
postMessage({
id,
type: 'COLS_RM',
key,
})
})()
},
keys: (prefix: string) => {
return wrapMessageApi<string[]>(id => {
postMessage({
id,
type: 'COLS_KEYS',
key: prefix,
})
})()
},
clear: (prefix: string) => {
return wrapMessageApi(id => {
postMessage({
id,
type: 'COLS_CLR',
key: prefix,
})
})()
},
addStorageListener: async (key: string, callback: CrossOriginStorageListener['callback']) => {
const callbackId = getRandomId(32)
storageListeners.push({
id: callbackId,
callback,
})
postMessage({
type: 'COLS_LIS',
key,
id: callbackId,
})
return callbackId
},
removeStorageListener: async (key: string, callbackId: string) => {
deleteValue(storageListeners, it => it.id === callbackId)
postMessage({
type: 'COLS_LIS_UN',
key,
id: callbackId,
})
},
}

View File

@ -407,6 +407,7 @@ export class DoubleClickEvent {
singleClickHandler: (e: MouseEvent) => void = none
private clickedOnce = false
// eslint-disable-next-line class-methods-use-this
private readonly stopPropagationHandler = (e: MouseEvent) => {
e.stopImmediatePropagation()
}

View File

@ -1,5 +1,5 @@
import { FeatureBase } from '@/components/types'
import { parseExternalInput } from './external-input'
import { loadFeatureCode } from './external-input'
import { meta } from './meta'
export enum CompareResult {
@ -51,7 +51,7 @@ export class Version {
export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
try {
if (typeof feature === 'string') {
feature = await parseExternalInput<FeatureBase>(feature)
feature = loadFeatureCode(feature) as FeatureBase
}
// 无效代码
if (feature === null || feature === undefined) {

Some files were not shown because too many files have changed in this diff Show More