diff --git a/doc/features/features.json b/doc/features/features.json index 6a3363199..251f53f15 100644 --- a/doc/features/features.json +++ b/doc/features/features.json @@ -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", diff --git a/doc/features/features.md b/doc/features/features.md index 1a49bc053..0f94d4def 100644 --- a/doc/features/features.md +++ b/doc/features/features.md @@ -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` diff --git a/doc/features/pack/pack.json b/doc/features/pack/pack.json index 1883bc0bf..ecf335f80 100644 --- a/doc/features/pack/pack.json +++ b/doc/features/pack/pack.json @@ -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" }, diff --git a/registry/dist/components/style/player-on-top-new.js b/registry/dist/components/style/player-on-top-new.js new file mode 100644 index 000000000..b291589f7 --- /dev/null +++ b/registry/dist/components/style/player-on-top-new.js @@ -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})())); \ No newline at end of file diff --git a/registry/lib/components/feeds/extend-live/LiveList.vue b/registry/lib/components/feeds/extend-live/LiveList.vue new file mode 100644 index 000000000..f60cb2325 --- /dev/null +++ b/registry/lib/components/feeds/extend-live/LiveList.vue @@ -0,0 +1,227 @@ + + + diff --git a/registry/lib/components/feeds/extend-live/extend-feeds-live.scss b/registry/lib/components/feeds/extend-live/extend-feeds-live.scss index 3554d1072..c54f267e8 100644 --- a/registry/lib/components/feeds/extend-live/extend-feeds-live.scss +++ b/registry/lib/components/feeds/extend-live/extend-feeds-live.scss @@ -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; } diff --git a/registry/lib/components/feeds/extend-live/index.md b/registry/lib/components/feeds/extend-live/index.md new file mode 100644 index 000000000..c29569cc5 --- /dev/null +++ b/registry/lib/components/feeds/extend-live/index.md @@ -0,0 +1 @@ +替换动态的 `正在直播` 面板, 能够显示超过 10 个的直播间. diff --git a/registry/lib/components/feeds/extend-live/index.ts b/registry/lib/components/feeds/extend-live/index.ts index 0f8725769..519ddd432 100644 --- a/registry/lib/components/feeds/extend-live/index.ts +++ b/registry/lib/components/feeds/extend-live/index.ts @@ -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\/$/], diff --git a/registry/lib/components/feeds/image-auto-back-to-top/index.md b/registry/lib/components/feeds/image-auto-back-to-top/index.md new file mode 100644 index 000000000..7e4d6f438 --- /dev/null +++ b/registry/lib/components/feeds/image-auto-back-to-top/index.md @@ -0,0 +1 @@ +在动态里查看图片详情时 (非全屏), 切换图片自动回到图片顶部; 退出查看图片模式时, 自动将动态移入视图内. diff --git a/registry/lib/components/feeds/image-auto-back-to-top/index.ts b/registry/lib/components/feeds/image-auto-back-to-top/index.ts new file mode 100644 index 000000000..bb7414e0f --- /dev/null +++ b/registry/lib/components/feeds/image-auto-back-to-top/index.ts @@ -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') + }) + }) + }, + }) + }, +}) diff --git a/registry/lib/components/style/custom-navbar/favorites/FavoritesFolderSelect.vue b/registry/lib/components/style/custom-navbar/favorites/FavoritesFolderSelect.vue index a3d0f689c..4886f2e09 100644 --- a/registry/lib/components/style/custom-navbar/favorites/FavoritesFolderSelect.vue +++ b/registry/lib/components/style/custom-navbar/favorites/FavoritesFolderSelect.vue @@ -7,9 +7,7 @@ :value="folder" @change="change($event)" > - + diff --git a/registry/lib/components/style/custom-navbar/subscriptions/CinemaSubscriptions.vue b/registry/lib/components/style/custom-navbar/subscriptions/CinemaSubscriptions.vue index de2e49cb7..2e90e615e 100644 --- a/registry/lib/components/style/custom-navbar/subscriptions/CinemaSubscriptions.vue +++ b/registry/lib/components/style/custom-navbar/subscriptions/CinemaSubscriptions.vue @@ -1,5 +1,5 @@ diff --git a/registry/lib/components/style/custom-navbar/subscriptions/NavbarSubscriptions.vue b/registry/lib/components/style/custom-navbar/subscriptions/NavbarSubscriptions.vue index 906d29b0f..dc827ee1b 100644 --- a/registry/lib/components/style/custom-navbar/subscriptions/NavbarSubscriptions.vue +++ b/registry/lib/components/style/custom-navbar/subscriptions/NavbarSubscriptions.vue @@ -1,43 +1,107 @@ diff --git a/registry/lib/components/style/custom-navbar/subscriptions/SubscriptionsList.vue b/registry/lib/components/style/custom-navbar/subscriptions/SubscriptionsList.vue index a24478bbd..c894f3757 100644 --- a/registry/lib/components/style/custom-navbar/subscriptions/SubscriptionsList.vue +++ b/registry/lib/components/style/custom-navbar/subscriptions/SubscriptionsList.vue @@ -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 } diff --git a/registry/lib/components/style/custom-navbar/subscriptions/types.ts b/registry/lib/components/style/custom-navbar/subscriptions/types.ts new file mode 100644 index 000000000..1b236bed5 --- /dev/null +++ b/registry/lib/components/style/custom-navbar/subscriptions/types.ts @@ -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 +} diff --git a/registry/lib/components/style/custom-navbar/watchlater/NavbarWatchlater.vue b/registry/lib/components/style/custom-navbar/watchlater/NavbarWatchlater.vue index b015afb7f..7e520793c 100644 --- a/registry/lib/components/style/custom-navbar/watchlater/NavbarWatchlater.vue +++ b/registry/lib/components/style/custom-navbar/watchlater/NavbarWatchlater.vue @@ -1,6 +1,7 @@ + diff --git a/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/Default.vue b/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/Default.vue index e337f005e..ee1be552a 100644 --- a/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/Default.vue +++ b/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/Default.vue @@ -11,19 +11,31 @@
- - 排行榜 - - +
+ + 排行榜 + + + + + + + +
+ +
diff --git a/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/_rank-list.scss b/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/_rank-list.scss new file mode 100644 index 000000000..d941757ee --- /dev/null +++ b/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/_rank-list.scss @@ -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; + } + } +} diff --git a/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/rank-list.ts b/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/rank-list.ts index 8643135ac..ef7142d0b 100644 --- a/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/rank-list.ts +++ b/registry/lib/components/style/home-redesign/fresh/layouts/categories/content/rank-list.ts @@ -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, +} diff --git a/registry/lib/components/style/home-redesign/fresh/layouts/fresh-layout-item.ts b/registry/lib/components/style/home-redesign/fresh/layouts/fresh-layout-item.ts index e8e28b1f9..ddb354a37 100644 --- a/registry/lib/components/style/home-redesign/fresh/layouts/fresh-layout-item.ts +++ b/registry/lib/components/style/home-redesign/fresh/layouts/fresh-layout-item.ts @@ -5,6 +5,6 @@ export interface FreshLayoutItem extends WithName { grow?: boolean } export interface FreshLayoutItemSettings { - name: string linebreak: boolean + order: number } diff --git a/registry/lib/components/style/home-redesign/fresh/layouts/trending/Trending.vue b/registry/lib/components/style/home-redesign/fresh/layouts/trending/Trending.vue index 66f9157be..220ece819 100644 --- a/registry/lib/components/style/home-redesign/fresh/layouts/trending/Trending.vue +++ b/registry/lib/components/style/home-redesign/fresh/layouts/trending/Trending.vue @@ -24,7 +24,7 @@ + + diff --git a/src/components/bisector/DialogTitle.vue b/src/components/bisector/DialogTitle.vue new file mode 100644 index 000000000..2ae4acad4 --- /dev/null +++ b/src/components/bisector/DialogTitle.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/components/bisector/ResultToastContent.vue b/src/components/bisector/ResultToastContent.vue new file mode 100644 index 000000000..09b3fc143 --- /dev/null +++ b/src/components/bisector/ResultToastContent.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/components/bisector/api.ts b/src/components/bisector/api.ts new file mode 100644 index 000000000..866c62952 --- /dev/null +++ b/src/components/bisector/api.ts @@ -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 + +let bisectorOptions: BisectorOptions +let scopedConsole: ReturnType +let bisectorGenerator: Generator, BisectReturn> +let groupedComponents: Awaited> +let dialog: DialogInstance + +export const setOptions = (options: BisectorOptions) => { + if (!bisectorOptions) { + bisectorOptions = options + } else { + Object.assign(bisectorOptions, options) + } +} + +export const setConsole = (console: ReturnType) => { + 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 */ `
`, '组件二等分结果') + 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 + 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 + 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) +} diff --git a/src/components/bisector/bisect.ts b/src/components/bisector/bisect.ts new file mode 100644 index 000000000..c434b57f2 --- /dev/null +++ b/src/components/bisector/bisect.ts @@ -0,0 +1,92 @@ +/* eslint-disable no-bitwise */ + +export interface BisectNext { + low: number + high: number + mid: number + slice: O[] + rouge: number +} + +export interface BisectReturn extends BisectNext { + target: O +} + +export interface InitialState { + low?: number + high?: number +} + +export function* bisectLeft( + data: readonly O[], + initialState?: InitialState, +): Generator, BisectReturn> { + 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( + data: readonly O[], + initialState?: InitialState, +): Generator, BisectReturn> { + 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 diff --git a/src/components/bisector/index.ts b/src/components/bisector/index.ts new file mode 100644 index 000000000..fe5438e4c --- /dev/null +++ b/src/components/bisector/index.ts @@ -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() + }, + }, + ], + }) + }) + }, + }, +}) diff --git a/src/components/bisector/options.ts b/src/components/bisector/options.ts new file mode 100644 index 000000000..d96d8342c --- /dev/null +++ b/src/components/bisector/options.ts @@ -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, + hidden: true, + }, + // 保持禁用状态的组件内部名称数组 + keepDisabledComponents: { + defaultValue: [] as string[], + hidden: true, + }, + // 保持启用状态的组件内部名称数组 + keepEnabledComponents: { + defaultValue: [] as string[], + hidden: true, + }, + // bisect 生成器的初始状态 + bisectInitialState: { + defaultValue: {} as Partial, + hidden: true, + }, +}) + +export const getBisectorOptions = () => + getComponentSettings>('bisector').options + +export type BisectorOptions = ReturnType diff --git a/src/components/built-in-components.ts b/src/components/built-in-components.ts index 217d5a2d2..b4f5748d5 100644 --- a/src/components/built-in-components.ts +++ b/src/components/built-in-components.ts @@ -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, ] diff --git a/src/components/component.ts b/src/components/component.ts index 7e589aa80..9ef3d8115 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -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 () => { diff --git a/src/components/feeds/UpInfo.vue b/src/components/feeds/UpInfo.vue index 092a96a8f..d9cddb56d 100644 --- a/src/components/feeds/UpInfo.vue +++ b/src/components/feeds/UpInfo.vue @@ -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(); diff --git a/src/components/i18n/dom-translator.ts b/src/components/i18n/dom-translator.ts index 57d8c5675..9b95f4ec8 100644 --- a/src/components/i18n/dom-translator.ts +++ b/src/components/i18n/dom-translator.ts @@ -12,11 +12,15 @@ export class Translator { static map: Map 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) } diff --git a/src/components/launch-bar/ActionItem.vue b/src/components/launch-bar/ActionItem.vue index a8a851ff3..7969536bd 100644 --- a/src/components/launch-bar/ActionItem.vue +++ b/src/components/launch-bar/ActionItem.vue @@ -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) }, }, diff --git a/src/components/launch-bar/LaunchBar.vue b/src/components/launch-bar/LaunchBar.vue index fe641294a..7e7213e0f 100644 --- a/src/components/launch-bar/LaunchBar.vue +++ b/src/components/launch-bar/LaunchBar.vue @@ -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 } diff --git a/src/components/launch-bar/history-provider.ts b/src/components/launch-bar/history-provider.ts index a3e86c6a2..e4719e58a 100644 --- a/src/components/launch-bar/history-provider.ts +++ b/src/components/launch-bar/history-provider.ts @@ -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), ) diff --git a/src/components/launch-bar/launch-bar-action.ts b/src/components/launch-bar/launch-bar-action.ts index b90cbbabe..0eb5700fc 100644 --- a/src/components/launch-bar/launch-bar-action.ts +++ b/src/components/launch-bar/launch-bar-action.ts @@ -20,6 +20,8 @@ export interface LaunchBarAction { deleteAction?: Executable /** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */ explicitSelect?: boolean + /** 手动指定在搜索结果中的顺序, 数字越小越排前面 */ + order?: number } /** LaunchBar 动作提供者的插件key, 可注入其他提供者 */ export const LaunchBarActionProviders = 'launchBar.actions' diff --git a/src/components/types.ts b/src/components/types.ts index c3b7c411a..38261f968 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -216,3 +216,6 @@ export interface ComponentMetadata /** 用户组件的非函数基本信息, 用于直接保存为 JSON */ export type UserComponentMetadata = Omit + +/** 推断 Record 的 Value 类型 */ +export type RecordValue = R extends Record ? V : never diff --git a/src/components/user-component.ts b/src/components/user-component.ts index d2c81f969..357aef233 100644 --- a/src/components/user-component.ts +++ b/src/components/user-component.ts @@ -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(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 +} diff --git a/src/components/utils/comment-apis.ts b/src/components/utils/comment-apis.ts index 2cf7d06c8..4df7a9050 100644 --- a/src/components/utils/comment-apis.ts +++ b/src/components/utils/comment-apis.ts @@ -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) { diff --git a/src/components/video/player-agent/base.ts b/src/components/video/player-agent/base.ts index 7286e4036..e24264fb4 100644 --- a/src/components/video/player-agent/base.ts +++ b/src/components/video/player-agent/base.ts @@ -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() // 开灯状态 && 要关灯 -> 关灯 diff --git a/src/components/video/player-light.ts b/src/components/video/player-light.ts index f5f07106c..91a164ada 100644 --- a/src/components/video/player-light.ts +++ b/src/components/video/player-light.ts @@ -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() diff --git a/src/core/core-apis.ts b/src/core/core-apis.ts index 50974c6d0..9c874e46a 100644 --- a/src/core/core-apis.ts +++ b/src/core/core-apis.ts @@ -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, diff --git a/src/core/dialog/index.ts b/src/core/dialog/index.ts index b7c160a6f..8c21794a1 100644 --- a/src/core/dialog/index.ts +++ b/src/core/dialog/index.ts @@ -10,7 +10,7 @@ export interface DialogInputs { } export interface DialogInstance extends Required { open: boolean - close: Promise + close(): Promise closeListeners: (() => void)[] } export const showDialog = (inputs: DialogInputs) => { diff --git a/src/core/external-input/index.ts b/src/core/external-input/index.ts index 1a56cd3c1..653c992b1 100644 --- a/src/core/external-input/index.ts +++ b/src/core/external-input/index.ts @@ -1,5 +1,7 @@ import { pickFile } from '@/core/file-picker' +export * from './load-feature-code' + /** 外部输入包装类型, 详见`parseExternalInput`的文档 */ export type ExternalInput = undefined | string | T // const arrayReturn = ( diff --git a/src/core/external-input/load-feature-code-all-settled.ts b/src/core/external-input/load-feature-code-all-settled.ts deleted file mode 100644 index f66075b74..000000000 --- a/src/core/external-input/load-feature-code-all-settled.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { loadFeatureCode, LoadFeatureCodeResult } from '@/core/external-input/load-feature-code' - -type LdRes = LoadFeatureCodeResult -type SettledRes = PromiseSettledResult -type FilledRes = PromiseFulfilledResult - -const unwrapSettledResult = (r: SettledRes): T => (r as FilledRes).value - -const mapSettledArray = (arr: SettledRes[]): T[] => arr.map(unwrapSettledResult) - -const mapSettleResult = (p: Promise[]>): Promise => p.then(mapSettledArray) - -/** - * 批量加载组件或插件的代码字符串,获取其导出 feature - * - * @param codes 代码字符串数组 - * @returns 不会失败的 `Promise`。其结果为一个数组,其中每个元素都是代表代码执行结果的对象 - */ -export const loadFeatureCodeAllSettled = ( - codes: string[], -): Promise[]> => - lodash(codes) - .map>>(loadFeatureCode) - .thru>[]>>(arr => Promise.allSettled(arr)) - .thru(mapSettleResult) - .value() diff --git a/src/core/external-input/load-feature-code-all.ts b/src/core/external-input/load-feature-code-all.ts deleted file mode 100644 index 24d7a69e5..000000000 --- a/src/core/external-input/load-feature-code-all.ts +++ /dev/null @@ -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: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultOk - - readonly isError: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultError - - readonly isNoExport: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultNoExport - - readonly isCodeThrew: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultCodeThrew -} - -/** - * 成功从代码中获取 features - * - * @namespace - * @property features 从代码中获取的导出值 - */ -interface LoadFeatureCodeAllResultOk 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 = - | LoadFeatureCodeAllResultOk - | 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 = (features: X[]): LoadFeatureCodeAllResultOk => - 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 = Promise - -type LdRes = LoadFeatureCodeResult -type LdOk = LoadFeatureCodeResultOk -type LdErr = LoadFeatureCodeResultError - -type LdAllRes = LoadFeatureCodeAllResult -type LdAllOk = LoadFeatureCodeAllResultOk -type LdAllErr = LoadFeatureCodeAllResultError - -type LoadCodesTask = Task[], [number, LdErr]> - -// covert `Task>` to `Task, LdErr>` -const rejectErrorResult = (t: Task>): Task, LdErr> => - t.then(r => (r.isOk() ? r : Promise.reject(r))) - -// load feature code, and return `Task, LdErr>` -const loadCode = (code: string): Task, LdErr> => rejectErrorResult(loadFeatureCode(code)) - -// covert `Task`'s `Err` type from `T` to `[number, T]` -const addIndexToRejected = ( - t: Task, - i: N, - // eslint-disable-next-line prefer-promise-reject-errors -): Task => t.catch(e => Promise.reject([i, e])) - -// create `LdAllOk` from an array of `LdOk` -const createOkResult = (arr: LdOk[]): LdAllOk => 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 = (codes: string[]): LoadCodesTask => - lodash(codes) - .map, LdErr>>(loadCode) - .map(addIndexToRejected) - .thru>(arr => Promise.all(arr)) - .value() - -// create a `LdAllRes` wrapped by `Task` -const createTaskResult = (t: LoadCodesTask): Task> => - t.then(createOkResult).catch(createErrResult) - -/** - * 批量加载组件或插件的代码字符串,获取其导出 feature - * - * 只要有一个代码出现了错误,则返回错误。 - * - * @param codes 代码字符串数组 - * @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult` - */ -const loadFeatureCodeAll = (codes: string[]): Promise> => - lodash(codes).thru>(loadCodes).thru(createTaskResult).value() - -export { - loadFeatureCodeAll, - LoadFeatureCodeAllResult, - LoadFeatureCodeAllResultOk, - LoadFeatureCodeAllResultError, - LoadFeatureCodeAllResultNoExport, - LoadFeatureCodeAllResultCodeThrew, -} diff --git a/src/core/external-input/load-feature-code.ts b/src/core/external-input/load-feature-code.ts index a3e1fb9dc..0b283d780 100644 --- a/src/core/external-input/load-feature-code.ts +++ b/src/core/external-input/load-feature-code.ts @@ -1,116 +1,87 @@ -import { FeatureBase } from '@/components/types' +export class LoadFeatureCodeError extends Error {} -interface ResultInstance { - readonly isOk: ( - this: LoadFeatureCodeResult, - ) => this is LoadFeatureCodeResultOk - - readonly isError: (this: LoadFeatureCodeResult) => this is LoadFeatureCodeResultError - - readonly isNoExport: ( - this: LoadFeatureCodeResult, - ) => this is LoadFeatureCodeResultNoExport - - readonly isCodeThrew: ( - this: LoadFeatureCodeResult, - ) => 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 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 = - | LoadFeatureCodeResultOk - | 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 = (feature: X): LoadFeatureCodeResultOk => - 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 ( - code: string, -): Promise> => { - // 收集代码导出值 - 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 } diff --git a/src/core/external-input/load-features-from-codes.ts b/src/core/external-input/load-features-from-codes.ts deleted file mode 100644 index 400da78de..000000000 --- a/src/core/external-input/load-features-from-codes.ts +++ /dev/null @@ -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 -export async function loadFeaturesFromCodes( - kind: FeatureKind.Plugin, - names: string[], - codes: string[], -): Promise -export async function loadFeaturesFromCodes( - kind: FeatureKind, - names: string[], - codes: string[], -): Promise { - const results = await loadFeatureCodeAllSettled(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).feature) -} diff --git a/src/core/install-feature.ts b/src/core/install-feature.ts index 1bb85a519..bfb6bc690 100644 --- a/src/core/install-feature.ts +++ b/src/core/install-feature.ts @@ -36,8 +36,8 @@ export const installFeatureFromCode = async ( metadata: FeatureType message: string }> => { - const { parseExternalInput } = await import('../core/external-input') - const item = await parseExternalInput(code) + const { loadFeatureCode } = await import('../core/external-input') + const item = loadFeatureCode(code) as FeatureType const { type, installer } = (() => { if (isComponent(item)) { return { diff --git a/src/core/local-storage.ts b/src/core/local-storage.ts new file mode 100644 index 000000000..df839fd52 --- /dev/null +++ b/src/core/local-storage.ts @@ -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 = (handler: (messageId: string) => void) => { + return () => + new Promise(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: (key: string) => { + return wrapMessageApi(id => { + postMessage({ + id, + type: 'COLS_GET', + key, + }) + })() + }, + removeItem: (key: string) => { + return wrapMessageApi(id => { + postMessage({ + id, + type: 'COLS_RM', + key, + }) + })() + }, + keys: (prefix: string) => { + return wrapMessageApi(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, + }) + }, +} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index aa9caad71..33753d9b8 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -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() } diff --git a/src/core/version.ts b/src/core/version.ts index 3f19c1fc5..795dd4c42 100644 --- a/src/core/version.ts +++ b/src/core/version.ts @@ -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(feature) + feature = loadFeatureCode(feature) as FeatureBase } // 无效代码 if (feature === null || feature === undefined) { diff --git a/src/plugins/id-search/index.ts b/src/plugins/id-search/index.ts index 0094bd61a..551e511a8 100644 --- a/src/plugins/id-search/index.ts +++ b/src/plugins/id-search/index.ts @@ -1,5 +1,6 @@ import { LaunchBarActionProvider, LaunchBarAction } from '@/components/launch-bar/launch-bar-action' import { PluginMetadata } from '../plugin' +import { IdSearchProvider } from './types' const getCopyItem = async (name: string, id: string, original: string) => { const item: LaunchBarAction = { @@ -17,77 +18,82 @@ const getCopyItem = async (name: string, id: string, original: string) => { } return [item] } -const idMatches = [ +const idMatches: IdSearchProvider[] = [ { pattern: /^av([\d]+)$/i, - name: (match: RegExpMatchArray) => `av${match[1]}`, - badge: 'av号跳转', - link: (match: RegExpMatchArray) => `https://www.bilibili.com/av${match[1]}`, - extend: async (match: RegExpMatchArray) => { + getActions: async match => { const { getJsonWithCredentials } = await import('@/core/ajax') const json = await getJsonWithCredentials( `https://api.bilibili.com/x/web-interface/view?aid=${match[1]}`, ) - const bv = lodash.get(json, 'data.bvid', null) - if (bv === null) { - return [] + const data = lodash.get(json, 'data', {}) + const { bvid, title } = data + const indexer = `av${match[1]}` + + return { + name: title || indexer, + description: 'av号跳转', + indexer, + link: `https://www.bilibili.com/av${match[1]}`, + extraActions: bvid ? await getCopyItem('BV号', bvid, indexer) : [], } - return getCopyItem('BV号', bv, `av${match[1]}`) }, }, { pattern: /^bv([\da-zA-Z]+)$/i, - name: (match: RegExpMatchArray) => `BV${match[1]}`, - badge: 'BV号跳转', - link: (match: RegExpMatchArray) => `https://www.bilibili.com/BV${match[1]}`, - extend: async (match: RegExpMatchArray) => { + getActions: async match => { const { getJsonWithCredentials } = await import('@/core/ajax') const json = await getJsonWithCredentials( `https://api.bilibili.com/x/web-interface/view?bvid=${match[1]}`, ) - const av = lodash.get(json, 'data.aid', null) - if (av === null) { - return [] + const data = lodash.get(json, 'data', {}) + const { aid, title } = data + const indexer = `BV${match[1]}` + + return { + name: title || indexer, + description: 'BV号跳转', + indexer, + link: `https://www.bilibili.com/BV${match[1]}`, + extraActions: aid ? await getCopyItem('av号', `av${aid}`, indexer) : [], } - return getCopyItem('av号', `av${av}`, `BV${match[1]}`) }, }, ] export const plugin: PluginMetadata = { name: 'launchBar.actions.IDSearch', - displayName: 'ID搜索快速跳转', + displayName: '搜索栏 - 视频跳转', async setup() { const { addData } = await import('../data') const { LaunchBarActionProviders } = await import('@/components/launch-bar/launch-bar-action') addData(LaunchBarActionProviders, (providers: LaunchBarActionProvider[]) => { providers.push({ name: 'IDSearchProvider', - // getEnterAction: input => { - // for (const it of idMatches) { - // const match = input.match(it.pattern) - // if (match) { - // return () => window.open(it.link(match), '_blank') - // } - // } - // return null - // }, getActions: async input => { const results: LaunchBarAction[] = [] for (const it of idMatches) { const match = input.match(it.pattern) if (match) { - results.push({ - name: it.name(match), - icon: 'mdi-open-in-new', - description: it.badge, - action: () => { - window.open(it.link(match), '_blank') + const { + name, + description = '', + indexer, + link, + extraActions = [], + } = await it.getActions(match) + results.push( + { + name, + icon: 'mdi-open-in-new', + indexer, + description, + action: () => { + window.open(link, '_blank') + }, + order: 0, }, - }) - if (it.extend) { - const actions = await it.extend(match) - results.push(...actions) - } + ...extraActions, + ) } } return results diff --git a/src/plugins/id-search/types.ts b/src/plugins/id-search/types.ts new file mode 100644 index 000000000..b76240ecf --- /dev/null +++ b/src/plugins/id-search/types.ts @@ -0,0 +1,19 @@ +import type { LaunchBarAction } from '@/components/launch-bar/launch-bar-action' + +export interface IdSearchProvider { + /** 匹配的正则 */ + pattern: RegExp + /** 匹配时运行此函数获取结果 */ + getActions: (match: RegExpMatchArray) => Promise<{ + /** 显示的选项名称 */ + name: string + /** 选择此选项后打开的链接 */ + link: string + /** 提供给搜索栏过滤的关键词 */ + indexer: string + /** 显示的选项描述 */ + description?: string + /** 额外提供一些自定义的 LaunchBarAction */ + extraActions?: LaunchBarAction[] + }> +} diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 62e690a87..d93ea2859 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -54,10 +54,12 @@ export const plugins: PluginMetadata[] = getBuiltInPlugins() * @param code 插件代码 */ export const installPlugin = async (code: string) => { - const { parseExternalInput } = await import('../core/external-input') - const plugin = await parseExternalInput(code) - if (plugin === null) { - throw new Error('无效的插件代码') + const { loadFeatureCode } = await import('@/core/external-input') + let plugin: PluginMetadata + try { + plugin = loadFeatureCode(code) as PluginMetadata + } catch (e) { + throw new Error('无效的插件代码', e) } const { settings } = await import('@/core/settings') const existingPlugin = settings.userPlugins[plugin.name] @@ -155,22 +157,27 @@ export const loadPlugin = async (plugin: PluginMetadata) => { */ export const loadAllPlugins = async (components: ComponentMetadata[]) => { const { settings, getGeneralSettings } = await import('@/core/settings') - const { loadFeaturesFromCodes, FeatureKind } = await import( - '@/core/external-input/load-features-from-codes' - ) - const otherPlugins = lodash(components) - .map(extractPluginFromComponent) - .filter(p => p !== null) - .map(p => p as PluginMetadata) - .concat( - await loadFeaturesFromCodes( - FeatureKind.Plugin, - Object.keys(settings.userPlugins), - Object.values(settings.userPlugins).map(p => p.code), - ), - ) - .value() - plugins.push(...otherPlugins) + const { loadFeatureCode } = await import('@/core/external-input/load-feature-code') + for (const component of components) { + const plugin = extractPluginFromComponent(component) + if (plugin) { + plugins.push(plugin) + } + } + for (const [name, setting] of Object.entries(settings.userPlugins)) { + const { code } = setting + let metadata: PluginMetadata + try { + metadata = loadFeatureCode(code) as PluginMetadata + } catch (e) { + console.error('从代码加载用户插件失败。代码可能包含语法错误或执行时产生了异常', { + pluginName: name, + error: e, + }) + continue + } + plugins.push(metadata) + } return Promise.allSettled(plugins.map(loadPlugin)).then(async () => { if (getGeneralSettings().devMode) { const { pluginLoadTime, pluginResolveTime } = await import('@/core/performance/plugin-trace') diff --git a/src/plugins/style.ts b/src/plugins/style.ts index 4888058ee..a4cadfcc9 100644 --- a/src/plugins/style.ts +++ b/src/plugins/style.ts @@ -29,9 +29,9 @@ export const styles: Required[] = Object.values(settings.userStyles) export const installStyle = async (input: UserStyle | string) => { try { let userStyle: UserStyle - const { parseExternalInput } = await import('../core/external-input') + const { loadFeatureCode } = await import('@/core/external-input') if (typeof input === 'string') { - userStyle = await parseExternalInput(input) + userStyle = loadFeatureCode(input) as UserStyle } else { userStyle = input } @@ -59,8 +59,8 @@ export const installStyle = async (input: UserStyle | string) => { metadata: userStyle, message: `已安装样式'${displayName || name}'`, } - } catch (error) { - throw new Error('无效的样式代码') + } catch (e) { + throw new Error('无效的样式代码', { cause: e }) } } /** diff --git a/src/ui/TabControl.vue b/src/ui/TabControl.vue index e71c39d44..23df0cfc6 100644 --- a/src/ui/TabControl.vue +++ b/src/ui/TabControl.vue @@ -84,18 +84,24 @@ export default Vue.extend({ }, }, data() { + const tabs = this.tabs as TabMappings return { - selectedTab: (this.tabs.find((t: TabMapping) => t.name === this.defaultTab) ?? - this.tabs[0]) as TabMapping, + selectedTabName: + tabs.find((t: TabMapping) => t.name === this.defaultTab)?.name ?? tabs[0].name, } }, + computed: { + selectedTab() { + return this.tabs.find((t: TabMapping) => t.name === this.selectedTabName) + }, + }, mounted() { this.$emit('change', this.selectedTab.activeLink) }, methods: { selectTab(tab: TabMapping) { - if (this.selectedTab !== tab) { - this.selectedTab = tab + if (this.selectedTabName !== tab.name) { + this.selectedTabName = tab.name tab.count = 0 this.$emit('change', this.selectedTab.activeLink) } else if (tab.activeLink) { diff --git a/src/ui/_effects.scss b/src/ui/_effects.scss index 32836ee75..da6883618 100644 --- a/src/ui/_effects.scss +++ b/src/ui/_effects.scss @@ -22,7 +22,7 @@ } animation: bounce-y-#{$offset-in-px} 0.4s ease-out; } -@mixin scroll-mask-x($mask-width: 24px) { +@mixin scroll-mask-x($mask-width: 24px, $base-color: #fff) { &::before, &::after { content: ""; @@ -39,14 +39,14 @@ height: 100%; flex-shrink: 0; top: 0; - z-index: 1; + z-index: 100; } &.not-empty::before { - background: linear-gradient(to right, var(--home-base-color) 40%, rgba(0, 0, 0, 0) 100%); + background: linear-gradient(to right, $base-color 40%, rgba(0, 0, 0, 0) 100%); left: -2px; } &.not-empty::after { - background: linear-gradient(to left, var(--home-base-color) 40%, rgba(0, 0, 0, 0) 100%); + background: linear-gradient(to left, $base-color 40%, rgba(0, 0, 0, 0) 100%); right: -2px; } &.scroll-top::before, @@ -54,6 +54,38 @@ width: 0; } } +@mixin scroll-mask-y($mask-height: 24px, $base-color: #fff) { + &::before, + &::after { + content: ""; + display: block; + opacity: 0; + } + &.not-empty::before, + &.not-empty::after { + position: absolute; + pointer-events: none; + transition: .1s ease-out; + height: $mask-height; + opacity: 1; + width: 100%; + flex-shrink: 0; + left: 0; + z-index: 100; + } + &.not-empty::before { + background: linear-gradient(to bottom, $base-color 40%, rgba(0, 0, 0, 0) 100%); + top: -2px; + } + &.not-empty::after { + background: linear-gradient(to top, $base-color 40%, rgba(0, 0, 0, 0) 100%); + bottom: -2px; + } + &.scroll-top::before, + &.scroll-bottom::after { + height: 0; + } +} @mixin item-slides-y($offset: 48px) { @keyframes item-slides-y { 0% {