mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge branch 'v2.7.1-preview' into preview-fixes
This commit is contained in:
commit
5f98a1d5a0
@ -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",
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
1
registry/dist/components/style/player-on-top-new.js
vendored
Normal file
1
registry/dist/components/style/player-on-top-new.js
vendored
Normal 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})()));
|
||||
227
registry/lib/components/feeds/extend-live/LiveList.vue
Normal file
227
registry/lib/components/feeds/extend-live/LiveList.vue
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
1
registry/lib/components/feeds/extend-live/index.md
Normal file
1
registry/lib/components/feeds/extend-live/index.md
Normal file
@ -0,0 +1 @@
|
||||
替换动态的 `正在直播` 面板, 能够显示超过 10 个的直播间.
|
||||
@ -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\/$/],
|
||||
|
||||
@ -0,0 +1 @@
|
||||
在动态里查看图片详情时 (非全屏), 切换图片自动回到图片顶部; 退出查看图片模式时, 自动将动态移入视图内.
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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%;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
.search-panel {
|
||||
> .trending {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
隐藏搜索栏和搜索页面中的 `bilibili 热搜`. 请注意这只是视觉上的隐藏, 如果不输入任何关键词就点 Enter 或搜索按钮, 仍然会跳转至推荐的热搜词.
|
||||
27
registry/lib/components/style/hide/trending-search/index.ts
Normal file
27
registry/lib/components/style/hide/trending-search/index.ts
Normal 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 = '搜索'
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
17
registry/lib/components/style/hide/user-card/index.ts
Normal file
17
registry/lib/components/style/hide/user-card/index.ts
Normal 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')),
|
||||
})
|
||||
@ -0,0 +1,5 @@
|
||||
.user-card,
|
||||
.user-card-m-exp,
|
||||
.bili-user-profile {
|
||||
display: none !important;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
1
registry/lib/components/style/hide/video/share/index.md
Normal file
1
registry/lib/components/style/hide/video/share/index.md
Normal file
@ -0,0 +1 @@
|
||||
隐藏视频和番剧播放器下方的分享按钮.
|
||||
14
registry/lib/components/style/hide/video/share/index.ts
Normal file
14
registry/lib/components/style/hide/video/share/index.ts
Normal 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,
|
||||
})
|
||||
@ -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>
|
||||
@ -12,7 +12,7 @@
|
||||
</fragment>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { freshHomeOptions } from './types'
|
||||
import { freshHomeOptions } from './options'
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -5,6 +5,6 @@ export interface FreshLayoutItem extends WithName {
|
||||
grow?: boolean
|
||||
}
|
||||
export interface FreshLayoutItemSettings {
|
||||
name: string
|
||||
linebreak: boolean
|
||||
order: number
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
36
registry/lib/components/style/player-on-top-new/index.ts
Normal file
36
registry/lib/components/style/player-on-top-new/index.ts
Normal 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,
|
||||
})
|
||||
1
registry/lib/components/utils/auto-like/index.md
Normal file
1
registry/lib/components/utils/auto-like/index.md
Normal file
@ -0,0 +1 @@
|
||||
进入视频 / 查看动态时, 自动点赞.
|
||||
53
registry/lib/components/utils/auto-like/index.ts
Normal file
53
registry/lib/components/utils/auto-like/index.ts
Normal 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)
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
1
registry/lib/components/utils/url-params-clean/index.md
Normal file
1
registry/lib/components/utils/url-params-clean/index.md
Normal file
@ -0,0 +1 @@
|
||||
自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.
|
||||
@ -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\//],
|
||||
})
|
||||
|
||||
1
registry/lib/components/video/danmaku/unescape/index.md
Normal file
1
registry/lib/components/video/danmaku/unescape/index.md
Normal file
@ -0,0 +1 @@
|
||||
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
||||
19
registry/lib/components/video/danmaku/unescape/index.ts
Normal file
19
registry/lib/components/video/danmaku/unescape/index.ts
Normal 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
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@ -1 +1 @@
|
||||
在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量.
|
||||
在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量. 请注意不能和 `反转滚轮调音量` 一同使用.
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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>) => {
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
反转在网页全屏 / 全屏模式下使用滚轮调节音量的方向, 使其更符合使用触控板时的操作方向. 请注意不能和 `禁止滚轮调音量` 一同使用.
|
||||
|
||||
- 手指向上推时, 增加音量
|
||||
- 手指向下推时, 减少音量
|
||||
- 可以自定义 `灵敏度`, 同样的滚动幅度下, 灵敏度越高变化的音量越多
|
||||
@ -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,
|
||||
})
|
||||
@ -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()
|
||||
|
||||
1
registry/lib/plugins/launch-bar/audio-search/index.md
Normal file
1
registry/lib/plugins/launch-bar/audio-search/index.md
Normal file
@ -0,0 +1 @@
|
||||
在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.
|
||||
36
registry/lib/plugins/launch-bar/audio-search/index.ts
Normal file
36
registry/lib/plugins/launch-bar/audio-search/index.ts
Normal 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,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
1
registry/lib/plugins/launch-bar/bangumi-search/index.md
Normal file
1
registry/lib/plugins/launch-bar/bangumi-search/index.md
Normal file
@ -0,0 +1 @@
|
||||
在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.
|
||||
60
registry/lib/plugins/launch-bar/bangumi-search/index.ts
Normal file
60
registry/lib/plugins/launch-bar/bangumi-search/index.ts
Normal 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,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
34
registry/lib/plugins/launch-bar/common.ts
Normal file
34
registry/lib/plugins/launch-bar/common.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
1
registry/lib/plugins/launch-bar/cv-search/index.md
Normal file
1
registry/lib/plugins/launch-bar/cv-search/index.md
Normal file
@ -0,0 +1 @@
|
||||
在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.
|
||||
46
registry/lib/plugins/launch-bar/cv-search/index.ts
Normal file
46
registry/lib/plugins/launch-bar/cv-search/index.ts
Normal 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,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
3
registry/lib/plugins/launch-bar/number-search/index.md
Normal file
3
registry/lib/plugins/launch-bar/number-search/index.md
Normal file
@ -0,0 +1,3 @@
|
||||
在输入纯数字时, 提供以下选项:
|
||||
- 跳转至相应的视频 (视为 av 号)
|
||||
- 跳转至相应的专栏 (视为 cv 号)
|
||||
42
registry/lib/plugins/launch-bar/number-search/index.ts
Normal file
42
registry/lib/plugins/launch-bar/number-search/index.ts
Normal 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,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
1
registry/lib/plugins/launch-bar/uid-search/index.md
Normal file
1
registry/lib/plugins/launch-bar/uid-search/index.md
Normal file
@ -0,0 +1 @@
|
||||
在输入 UID (用户 ID) 时, 提供对应的跳转选项.
|
||||
32
registry/lib/plugins/launch-bar/uid-search/index.ts
Normal file
32
registry/lib/plugins/launch-bar/uid-search/index.ts
Normal 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,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
@ -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'
|
||||
})
|
||||
},
|
||||
}
|
||||
69
src/components/bisector/DialogContent.vue
Normal file
69
src/components/bisector/DialogContent.vue
Normal 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>
|
||||
43
src/components/bisector/DialogTitle.vue
Normal file
43
src/components/bisector/DialogTitle.vue
Normal 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>
|
||||
54
src/components/bisector/ResultToastContent.vue
Normal file
54
src/components/bisector/ResultToastContent.vue
Normal 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>
|
||||
198
src/components/bisector/api.ts
Normal file
198
src/components/bisector/api.ts
Normal 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)
|
||||
}
|
||||
92
src/components/bisector/bisect.ts
Normal file
92
src/components/bisector/bisect.ts
Normal 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
|
||||
46
src/components/bisector/index.ts
Normal file
46
src/components/bisector/index.ts
Normal 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()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
31
src/components/bisector/options.ts
Normal file
31
src/components/bisector/options.ts
Normal 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>
|
||||
@ -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,
|
||||
]
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
},
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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),
|
||||
)
|
||||
|
||||
@ -20,6 +20,8 @@ export interface LaunchBarAction {
|
||||
deleteAction?: Executable
|
||||
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
|
||||
explicitSelect?: boolean
|
||||
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
|
||||
order?: number
|
||||
}
|
||||
/** LaunchBar 动作提供者的插件key, 可注入其他提供者 */
|
||||
export const LaunchBarActionProviders = 'launchBar.actions'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()
|
||||
// 开灯状态 && 要关灯 -> 关灯
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>(
|
||||
|
||||
@ -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()
|
||||
@ -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,
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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
134
src/core/local-storage.ts
Normal 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,
|
||||
})
|
||||
},
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user