From c69659d901782ec38c2571d2d9093d646e1548ea Mon Sep 17 00:00:00 2001 From: eric2788 Date: Wed, 6 Mar 2024 18:01:15 +0800 Subject: [PATCH 1/6] updated jsdocs --- CONTRIBUTING.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 10 ++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ed3b2e53 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# 貢獻指南 + +歡迎您對本項目進行貢獻!在開始之前,請先閱讀以下內容。 + +## 目錄 + +- [事前準備](#事前準備) +- [貢獻流程](#貢獻流程) +- [程式碼規範](#程式碼規範) +- [問題回報](#問題回報) +- [討論與支援](#討論與支援) + +## 事前準備 + +在開始貢獻本項目之前,請確保您擁有以下條件: + +- 擁有 NodeJS v20+ 運行環境 +- 具備開發 TypeScript, React, TailwindCSS 的知識 +- 具備開發 ManifestV3 瀏覽器擴展 的知識 +- 初步掌握 [Plasmo](https://www.plasmo.com) 開發框架 的知識 +- 初步掌握 [PlayWright](https://playwright.dev) 測試框架 的知識 (如開發新功能) + +## 貢獻流程 + +請按照以下步驟進行貢獻: + +1. [步驟一] +2. [步驟二] +3. [步驟三] + +## 程式碼規範 + +請遵守以下程式碼規範: + +- [規範一] +- [規範二] +- [規範三] + +## 問題回報 + +如果您在使用本項目時遇到任何問題,請按照以下方式回報: + +>(基於問題嚴重性程度排序) + +1. 在 [Discussion](https://github.com/eric2788/bilibili-vup-stream-enhancer/discussions) 發文 +2. 在 [Issue](https://github.com/eric2788/bilibili-vup-stream-enhancer/issues) 發文 +3. 聯絡[作者](https://t.me/eric1008818) + +## 討論與支援 + +如果您有任何疑問或需要支援,請參考以下資源: + +- 所有相關技術的官方文檔 +- [資源二] +- [資源三] + +感謝您的貢獻! diff --git a/README.md b/README.md index bcc9b796..96191e00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # bilibili vup观众直播增强扩展 -不只是同传字幕过滤! +![thumgnail](https://github.com/eric2788/bilibili-jimaku-filter/raw/web/assets_v2/main.png) + +> 前身为 bilibili-jimaku-filter 同传字幕过滤插件 ## ➵ 下载 @@ -25,6 +27,12 @@ 当所有功能完善后,我们将会为 bilibili-jimaku-filter 推出正式的 v2.0 版本 😎😎 +## ➵ 使用方式 + +1. [下载](#-下载)本扩展。 +2. 点击扩展图标进入设定页面,并根据你的偏好进入设定。完成后,然后按下保存设定。 +3. 进入B站任一直播间即可开始使用。 + ## ➵ 功能简介 **所有主要功能已全部改为可选**,例如: 你可以启用醒目留言记录而不启用同传字幕过滤, 且每个主要功能都有各自的房间黑/白名单 From 393ddd75a7f1536ce5f77f06196fdda1a2470300 Mon Sep 17 00:00:00 2001 From: eric2788 Date: Wed, 6 Mar 2024 21:47:32 +0800 Subject: [PATCH 2/6] reshaped css structure and slient debug/trace when production --- src/components/TailwindScope.tsx | 2 +- src/contents/index/index.tsx | 6 +++--- src/logger.ts | 4 ++-- src/{styles => }/style.css | 0 src/styles/index.ts | 6 ------ src/tabs/jimaku.tsx | 2 +- src/tabs/settings.tsx | 2 +- src/tabs/stream.tsx | 2 +- src/tailwindcss.ts | 18 ------------------ 9 files changed, 9 insertions(+), 33 deletions(-) rename src/{styles => }/style.css (100%) delete mode 100644 src/styles/index.ts delete mode 100644 src/tailwindcss.ts diff --git a/src/components/TailwindScope.tsx b/src/components/TailwindScope.tsx index 6b54b2ba..f3f2df5a 100644 --- a/src/components/TailwindScope.tsx +++ b/src/components/TailwindScope.tsx @@ -1,5 +1,5 @@ import ShadowRoot from './ShadowRoot'; -import styleText from '~styles'; +import styleText from 'data-text:~style.css'; function TailwindScope({ children, dark }: { children: React.ReactNode, dark?: boolean }): JSX.Element { return ( diff --git a/src/contents/index/index.tsx b/src/contents/index/index.tsx index fb4c3cc2..510a78b4 100644 --- a/src/contents/index/index.tsx +++ b/src/contents/index/index.tsx @@ -1,13 +1,13 @@ import type { PlasmoCSConfig, PlasmoGetRootContainer, PlasmoGetStyle, PlasmoRender } from "plasmo"; import { toast } from 'sonner/dist'; -import { ensureLogin, getNeptuneIsMyWaifu, getStreamInfo, type StreamInfo } from '~api/bilibili'; +import { getNeptuneIsMyWaifu, getStreamInfo, type StreamInfo } from '~api/bilibili'; import { getForwarder } from '~background/forwards'; import { getRoomId, isOutThemedPage } from '~utils/bilibili'; import { withFallbacks, withRetries } from '~utils/fetch'; import { injectFunction } from '~utils/inject'; -import { getSettingStorage, processing, withProcessingFlag } from '~utils/storage'; +import { getSettingStorage, withProcessingFlag } from '~utils/storage'; -import styleText from '~styles'; +import styleText from 'data-text:~style.css'; import createApp from './mounter'; import '~logger'; diff --git a/src/logger.ts b/src/logger.ts index 92758844..9e75a91f 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,6 @@ console.info = console.info.bind(console, '[bilibili-vup-stream-enhancer]') console.warn = console.warn.bind(console, '[bilibili-vup-stream-enhancer]') console.error = console.error.bind(console, '[bilibili-vup-stream-enhancer]') -console.debug = console.debug.bind(console, '[bilibili-vup-stream-enhancer]') console.log = console.log.bind(console, '[bilibili-vup-stream-enhancer]') -console.trace = console.trace.bind(console, '[bilibili-vup-stream-enhancer]') \ No newline at end of file +console.debug = process.env.NODE_ENV === 'production' ? () => {} : console.debug.bind(console, '[bilibili-vup-stream-enhancer]') +console.trace = process.env.NODE_ENV === 'production' ? () => {} : console.trace.bind(console, '[bilibili-vup-stream-enhancer]') \ No newline at end of file diff --git a/src/styles/style.css b/src/style.css similarity index 100% rename from src/styles/style.css rename to src/style.css diff --git a/src/styles/index.ts b/src/styles/index.ts deleted file mode 100644 index be4c8573..00000000 --- a/src/styles/index.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import styleText from 'data-text:./style.css' - -const styleContent = styleText - -export default styleContent \ No newline at end of file diff --git a/src/tabs/jimaku.tsx b/src/tabs/jimaku.tsx index 145f7182..845fb3a1 100644 --- a/src/tabs/jimaku.tsx +++ b/src/tabs/jimaku.tsx @@ -1,4 +1,4 @@ -import '~tailwindcss'; +import '~style.css'; import { Checkbox, diff --git a/src/tabs/settings.tsx b/src/tabs/settings.tsx index cd5dbbd7..bc395d91 100644 --- a/src/tabs/settings.tsx +++ b/src/tabs/settings.tsx @@ -1,4 +1,4 @@ -import '~tailwindcss'; +import '~style.css'; import { Fragment, useContext, useRef, type Ref, type RefObject } from 'react'; import BJFThemeProvider from '~components/BJFThemeProvider'; diff --git a/src/tabs/stream.tsx b/src/tabs/stream.tsx index a5b4cc52..706a72ee 100644 --- a/src/tabs/stream.tsx +++ b/src/tabs/stream.tsx @@ -1,4 +1,4 @@ -import '~tailwindcss'; +import '~style.css'; import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; diff --git a/src/tailwindcss.ts b/src/tailwindcss.ts deleted file mode 100644 index 2187a616..00000000 --- a/src/tailwindcss.ts +++ /dev/null @@ -1,18 +0,0 @@ -import styleCss from '~styles' - -let style = document.getElementById('bjf-tailwindcss') -if (style == null) { - style = document.createElement("style") - style.id = 'bjf-tailwindcss' - style.textContent = styleCss - document.head.appendChild(style) -} - -export function injectTailwind(element: Element, scoped: boolean = true) { - const style = document.createElement('style') - style.textContent = styleCss - if (scoped) { - style.setAttribute('scoped', '') - } - element.appendChild(style) -} \ No newline at end of file From e1d79d83f59dd3fa2ab15f09c74502ffd77db2f6 Mon Sep 17 00:00:00 2001 From: eric2788 Date: Wed, 6 Mar 2024 21:56:18 +0800 Subject: [PATCH 3/6] added CONTRIBUTING.md --- CONTRIBUTING.md | 166 ++++++++++++++++++++++++++++--------- docs/examples/features.md | 1 + docs/examples/pages.md | 1 + docs/examples/settings.md | 1 + docs/src/adapters.md | 0 docs/src/api.md | 0 docs/src/background.md | 0 docs/src/components.md | 0 docs/src/contents.md | 0 docs/src/contexts.md | 0 docs/src/database.md | 0 docs/src/features.md | 0 docs/src/hooks.md | 0 docs/src/migrations.md | 0 docs/src/players.md | 0 docs/src/settings.md | 0 docs/src/styles.md | 0 docs/src/tabs.md | 0 docs/src/toaster.md | 0 docs/src/types.md | 0 docs/src/updaters.md | 0 docs/src/utils.md | 0 docs/tests/content.spec.md | 0 docs/tests/features.md | 0 docs/tests/fixtures.md | 0 docs/tests/helpers.md | 0 docs/tests/options.md | 0 docs/tests/pages.md | 0 docs/tests/theme.setup.md | 0 docs/tests/utils.md | 0 30 files changed, 132 insertions(+), 37 deletions(-) create mode 100644 docs/examples/features.md create mode 100644 docs/examples/pages.md create mode 100644 docs/examples/settings.md create mode 100644 docs/src/adapters.md create mode 100644 docs/src/api.md create mode 100644 docs/src/background.md create mode 100644 docs/src/components.md create mode 100644 docs/src/contents.md create mode 100644 docs/src/contexts.md create mode 100644 docs/src/database.md create mode 100644 docs/src/features.md create mode 100644 docs/src/hooks.md create mode 100644 docs/src/migrations.md create mode 100644 docs/src/players.md create mode 100644 docs/src/settings.md create mode 100644 docs/src/styles.md create mode 100644 docs/src/tabs.md create mode 100644 docs/src/toaster.md create mode 100644 docs/src/types.md create mode 100644 docs/src/updaters.md create mode 100644 docs/src/utils.md create mode 100644 docs/tests/content.spec.md create mode 100644 docs/tests/features.md create mode 100644 docs/tests/fixtures.md create mode 100644 docs/tests/helpers.md create mode 100644 docs/tests/options.md create mode 100644 docs/tests/pages.md create mode 100644 docs/tests/theme.setup.md create mode 100644 docs/tests/utils.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed3b2e53..7e66e567 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,57 +1,149 @@ -# 貢獻指南 +# 贡献指南 + +欢迎您对本项目进行贡献!在开始之前,请先阅读以下内容。 + +## 目录 + +- [事前准备](#事前准备) +- [贡献流程](#贡献流程) +- [程式码规范](#程式码规范) +- [项目架构](#项目架构) + - [源代码](#源代码) + - [测试代码](#测试代码) +- [问题回报](#问题回报) +- [讨论与支援](#讨论与支援) + +## 事前准备 + +在开始贡献本项目之前,请确保您拥有以下条件: -歡迎您對本項目進行貢獻!在開始之前,請先閱讀以下內容。 +- 拥有 NodeJS v20+ 运行环境 +- 具备开发 TypeScript, React, TailwindCSS 的知识 +- 具备开发 ManifestV3 浏览器扩展 的知识 +- 初步掌握 [Plasmo](https://www.plasmo.com) 开发框架 的知识 +- 初步掌握 [PlayWright](https://playwright.dev) 测试框架 的知识 (如开发新功能) + +## 贡献流程 + +请按照以下步骤进行贡献: + +1. Fork 本仓库到你的本地仓库 +2. 在你的本地仓库中进行修改 +3. 如开发新功能,请自行添加合理且可行的单元/集成测试 +4. 完成后,确保你的代码通过所有单元/集成测试 +5. 提交 Pull Request 到本仓库的 `develop` 分支 -## 目錄 +## 程式码规范 -- [事前準備](#事前準備) -- [貢獻流程](#貢獻流程) -- [程式碼規範](#程式碼規範) -- [問題回報](#問題回報) -- [討論與支援](#討論與支援) +请遵守以下程式码规范: -## 事前準備 +- 各个编程语言的官方标准写法 +- 本仓库原有的代码及项目架构 -在開始貢獻本項目之前,請確保您擁有以下條件: +> 请放心,我们会在您提交 Pull Request 时进行代码检查,如有任何问题我们会在检查时提出。 -- 擁有 NodeJS v20+ 運行環境 -- 具備開發 TypeScript, React, TailwindCSS 的知識 -- 具備開發 ManifestV3 瀏覽器擴展 的知識 -- 初步掌握 [Plasmo](https://www.plasmo.com) 開發框架 的知識 -- 初步掌握 [PlayWright](https://playwright.dev) 測試框架 的知識 (如開發新功能) +## 项目架构 -## 貢獻流程 +本项目的架构如下: +```plaintext +assets/ # 项目资源文件 +src/ # 项目源代码 +tests/ # 项目测试代码 +``` -請按照以下步驟進行貢獻: +本文档只集中讲述**源代码**及**测试代码**的架构。 -1. [步驟一] -2. [步驟二] -3. [步驟三] +### 源代码 -## 程式碼規範 +本项目的源代码位于 `src/` 目录下,其架构如下: +``` +src/ +├── adapters/ # 适配器代码,用于连接和转换不同的接口或数据源。 +├── api/ # API 接口定义和实现。 +├── background/ # 浏览器扩展的后台脚本。 +├── components/ # 全局 React 组件。 +├── contents/ # 内容脚本,用于在网页上运行的脚本。 +├── contexts/ # React Contexts,用于全局状态管理。 +├── database/ # 数据库相关代码,包括模型定义和数据库操作。 +├── features/ # 特性模块,每个特性模块包含一组相关的功能。 +├── hooks/ # 自定义 React Hooks。 +├── migrations/ # 数据库迁移脚本。 +├── players/ # 播放器相关代码。 +├── settings/ # 设置相关代码,包括设置界面和设置存储。 +├── tabs/ # 标签页相关代码。 +├── types/ # 类型定义文件。 +├── updaters/ # 更新器代码,用于处理扩展的更新逻辑。(目前仅限 Chrome) +├── utils/ # 实用工具函数。 +├── logger.ts # 日志前缀注入。 +├── style.css # 包含 TailwindCSS 的全局样式。 +└── toaster.ts # 消息提示(Toast)相关代码。 +``` -請遵守以下程式碼規範: +各个目录的具体功能及范例请参考 [`docs/src/`](/docs/src/) 下的 `.md` 文件。 -- [規範一] -- [規範二] -- [規範三] +### 测试代码 -## 問題回報 +本项目的测试代码位于 `tests/` 目录下,其架构如下: -如果您在使用本項目時遇到任何問題,請按照以下方式回報: +``` +tests/ +├── features/ # 功能模块的集成测试代码。 +├── fixtures/ # 测试中需要用到的前置依赖。 +├── helpers/ # 使用类形式包装的测试辅助工具。 +├── pages/ # 扩展页面的集成测试代码。 +├── utils/ # 辅助测试的函数和工具。 +├── content.spec.ts # 内容脚本的集成测试代码。 +├── options.ts # fixtures 选项类型定义文件。 +└── theme.setup.ts # 大海报房间测试的前置依赖。 +``` ->(基於問題嚴重性程度排序) +各个目录的具体功能及范例请参考 [`docs/tests/`](/docs/tests) 下的 `.md` 文件。 -1. 在 [Discussion](https://github.com/eric2788/bilibili-vup-stream-enhancer/discussions) 發文 -2. 在 [Issue](https://github.com/eric2788/bilibili-vup-stream-enhancer/issues) 發文 -3. 聯絡[作者](https://t.me/eric1008818) +### 快速开始 -## 討論與支援 +1. 首先,完成安装 `nodejs v20+` 和 `pnpm` 等环境; +2. 克隆本仓库到本地; +3. 运行 `pnpm install` 安装依赖; +4. 最后,运行 `pnpm dev` 开始开发。 +5. 有关如何编写贡献代码,请参阅 [入门指南](#入门指南) 。 -如果您有任何疑問或需要支援,請參考以下資源: -- 所有相關技術的官方文檔 -- [資源二] -- [資源三] +#### 如要在本地运行集成测试: +- 请先运行 `pnpm dlx playwright install` 安装 PlayWright 的浏览器引擎 +- 完成后,运行 `pnpm build && pnpm test:prepare` 编译并部署测试环境 +- 最后,运行 `pnpm test` 运行测试 (或者用 playwright vscode 插件运行测试) +- 每次更新后可以运行 `pnpm test:rebuild` 重新编译并部署测试环境 -感謝您的貢獻! +#### 入门指南 + +请参阅 [`docs/examples`](/docs/examples) 下的 `.md` 文件来查看详细的代码编写流程。 + +目录如下: +``` +docs/examples/ +├── features.md # 新增功能模块 +├── pages.md # 新增扩展页面 +└── settings.md # 新增设定区块 +``` + +## 问题回报 + +如果您在使用本项目时遇到任何问题,请按照以下方式回报: + +>(基于问题严重性程度排序) + +1. 在 [Discussion](https://github.com/eric2788/bilibili-vup-stream-enhancer/discussions) 发文 +2. 在 [Issue](https://github.com/eric2788/bilibili-vup-stream-enhancer/issues) 发文 +3. 联络[作者](https://t.me/eric1008818) + +## 讨论与支援 + +如果您有任何疑问或需要支援,请参考以下资源: + +- [Typescript 官方文档](https://www.typescriptlang.org/docs/) +- [Plasmo 官方文档](https://www.plasmo.com/docs/) +- [TailwindCSS 官方文档](https://tailwindcss.com/docs) +- [PlayWright 官方文档](https://playwright.dev/docs/intro) +- 或其他相关技术的文档或讨论区 + +感谢您的贡献! diff --git a/docs/examples/features.md b/docs/examples/features.md new file mode 100644 index 00000000..c156ec88 --- /dev/null +++ b/docs/examples/features.md @@ -0,0 +1 @@ +## 新增功能 \ No newline at end of file diff --git a/docs/examples/pages.md b/docs/examples/pages.md new file mode 100644 index 00000000..ea7894a4 --- /dev/null +++ b/docs/examples/pages.md @@ -0,0 +1 @@ +## 新增页面 \ No newline at end of file diff --git a/docs/examples/settings.md b/docs/examples/settings.md new file mode 100644 index 00000000..c82805e6 --- /dev/null +++ b/docs/examples/settings.md @@ -0,0 +1 @@ +## 新增设定区块 \ No newline at end of file diff --git a/docs/src/adapters.md b/docs/src/adapters.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/background.md b/docs/src/background.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/components.md b/docs/src/components.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/contents.md b/docs/src/contents.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/contexts.md b/docs/src/contexts.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/database.md b/docs/src/database.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/features.md b/docs/src/features.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/hooks.md b/docs/src/hooks.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/migrations.md b/docs/src/migrations.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/players.md b/docs/src/players.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/settings.md b/docs/src/settings.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/styles.md b/docs/src/styles.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/tabs.md b/docs/src/tabs.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/toaster.md b/docs/src/toaster.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/types.md b/docs/src/types.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/updaters.md b/docs/src/updaters.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/utils.md b/docs/src/utils.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/content.spec.md b/docs/tests/content.spec.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/features.md b/docs/tests/features.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/fixtures.md b/docs/tests/fixtures.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/helpers.md b/docs/tests/helpers.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/options.md b/docs/tests/options.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/pages.md b/docs/tests/pages.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/theme.setup.md b/docs/tests/theme.setup.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/tests/utils.md b/docs/tests/utils.md new file mode 100644 index 00000000..e69de29b From d7667dde3dd418cf2cd0a39f41b4b0ba5a9cf904 Mon Sep 17 00:00:00 2001 From: eric2788 Date: Wed, 6 Mar 2024 22:29:38 +0800 Subject: [PATCH 4/6] make pnpm script support multi-platform --- CONTRIBUTING.md | 33 ++- README.md | 4 + docs/{src => }/background.md | 0 docs/examples/features.md | 1 - docs/examples/pages.md | 1 - docs/examples/settings.md | 1 - docs/features.md | 205 ++++++++++++++++++ docs/pages.md | 71 ++++++ docs/settings.md | 183 ++++++++++++++++ docs/src/adapters.md | 0 docs/src/api.md | 0 docs/src/components.md | 0 docs/src/contents.md | 0 docs/src/contexts.md | 0 docs/src/database.md | 0 docs/src/features.md | 0 docs/src/hooks.md | 0 docs/src/migrations.md | 0 docs/src/players.md | 0 docs/src/settings.md | 0 docs/src/styles.md | 0 docs/src/tabs.md | 0 docs/src/toaster.md | 0 docs/src/types.md | 0 docs/src/updaters.md | 0 docs/src/utils.md | 0 docs/tests/content.spec.md | 0 docs/tests/features.md | 0 docs/tests/fixtures.md | 0 docs/tests/helpers.md | 0 docs/tests/options.md | 0 docs/tests/pages.md | 0 docs/tests/theme.setup.md | 0 docs/tests/utils.md | 0 package.json | 9 +- pnpm-lock.yaml | 8 + src/components/BJFThemeProvider.tsx | 51 ++++- src/components/BLiveThemeProvider.tsx | 19 +- src/components/ConditionalWrapper.tsx | 35 ++- src/components/DraggableFloatingButton.tsx | 43 +++- src/components/OfflineRecordsProvider.tsx | 32 ++- src/components/PromiseHandler.tsx | 4 + src/components/ShadowRoot.tsx | 13 +- src/components/TailwindScope.tsx | 8 + src/components/Tutorial.tsx | 96 +++++++- src/contents/index/App.tsx | 4 +- src/contents/index/components/ButtonList.tsx | 4 +- src/contents/index/components/Header.tsx | 4 +- src/contents/index/mounter.tsx | 10 +- ...reamInfoContexts.ts => ContentContexts.ts} | 6 +- src/features/jimaku/components/ButtonArea.tsx | 4 +- src/features/jimaku/components/JimakuArea.tsx | 6 +- .../jimaku/components/JimakuCaptureLayer.tsx | 4 +- src/features/jimaku/index.tsx | 4 +- .../superchat/components/SuperChatArea.tsx | 4 +- .../components/SuperChatCaptureLayer.tsx | 4 +- src/settings/components/Selector.tsx | 2 +- tests/fixtures/background.ts | 2 + tests/fixtures/content.ts | 5 +- tests/helpers/bilibili-api.ts | 45 +++- tests/helpers/bilibili-page.ts | 57 +++++ .../dismiss-login-dialog-listener.ts | 3 +- tests/helpers/listeners/type.ts | 27 ++- tests/helpers/page-frame.ts | 15 ++ tests/helpers/room-finder.ts | 88 ++++++-- tests/utils/bilibili.ts | 13 ++ tests/utils/file.ts | 29 ++- tests/utils/misc.ts | 57 +++++ tests/utils/playwright.ts | 14 ++ 69 files changed, 1110 insertions(+), 118 deletions(-) rename docs/{src => }/background.md (100%) delete mode 100644 docs/examples/features.md delete mode 100644 docs/examples/pages.md delete mode 100644 docs/examples/settings.md create mode 100644 docs/features.md create mode 100644 docs/pages.md create mode 100644 docs/settings.md delete mode 100644 docs/src/adapters.md delete mode 100644 docs/src/api.md delete mode 100644 docs/src/components.md delete mode 100644 docs/src/contents.md delete mode 100644 docs/src/contexts.md delete mode 100644 docs/src/database.md delete mode 100644 docs/src/features.md delete mode 100644 docs/src/hooks.md delete mode 100644 docs/src/migrations.md delete mode 100644 docs/src/players.md delete mode 100644 docs/src/settings.md delete mode 100644 docs/src/styles.md delete mode 100644 docs/src/tabs.md delete mode 100644 docs/src/toaster.md delete mode 100644 docs/src/types.md delete mode 100644 docs/src/updaters.md delete mode 100644 docs/src/utils.md delete mode 100644 docs/tests/content.spec.md delete mode 100644 docs/tests/features.md delete mode 100644 docs/tests/fixtures.md delete mode 100644 docs/tests/helpers.md delete mode 100644 docs/tests/options.md delete mode 100644 docs/tests/pages.md delete mode 100644 docs/tests/theme.setup.md delete mode 100644 docs/tests/utils.md rename src/contexts/{StreamInfoContexts.ts => ContentContexts.ts} (56%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e66e567..9577a031 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,7 @@ - [项目架构](#项目架构) - [源代码](#源代码) - [测试代码](#测试代码) + - [快速开始](#快速开始) - [问题回报](#问题回报) - [讨论与支援](#讨论与支援) @@ -51,26 +52,24 @@ src/ # 项目源代码 tests/ # 项目测试代码 ``` -本文档只集中讲述**源代码**及**测试代码**的架构。 - ### 源代码 本项目的源代码位于 `src/` 目录下,其架构如下: ``` src/ -├── adapters/ # 适配器代码,用于连接和转换不同的接口或数据源。 -├── api/ # API 接口定义和实现。 +├── adapters/ # 适配器代码,用于连接或转换不同的数据源以供内容脚本使用。 +├── api/ # 不同 API 的接口定义和实现。 ├── background/ # 浏览器扩展的后台脚本。 -├── components/ # 全局 React 组件。 -├── contents/ # 内容脚本,用于在网页上运行的脚本。 -├── contexts/ # React Contexts,用于全局状态管理。 -├── database/ # 数据库相关代码,包括模型定义和数据库操作。 +├── components/ # 全局用 React 组件,适用于内容脚本和扩展页面。 +├── contents/ # 内容脚本,用于挂钩在网页上运行的脚本。 +├── contexts/ # 全局用 React 状态管理。 +├── database/ # 数据库相关代码,包括模型定义和数据库迁移操作。 ├── features/ # 特性模块,每个特性模块包含一组相关的功能。 -├── hooks/ # 自定义 React Hooks。 -├── migrations/ # 数据库迁移脚本。 -├── players/ # 播放器相关代码。 -├── settings/ # 设置相关代码,包括设置界面和设置存储。 -├── tabs/ # 标签页相关代码。 +├── hooks/ # 全局用的自定义 React Hooks。 +├── migrations/ # 设定迁移脚本(从MV2到MV3)。 +├── players/ # 直播解析器相关代码。 +├── settings/ # 设定库相关代码,包括对设定区块和功能设定区块的定义。 +├── tabs/ # 浏览器扩展页面。 ├── types/ # 类型定义文件。 ├── updaters/ # 更新器代码,用于处理扩展的更新逻辑。(目前仅限 Chrome) ├── utils/ # 实用工具函数。 @@ -79,8 +78,6 @@ src/ └── toaster.ts # 消息提示(Toast)相关代码。 ``` -各个目录的具体功能及范例请参考 [`docs/src/`](/docs/src/) 下的 `.md` 文件。 - ### 测试代码 本项目的测试代码位于 `tests/` 目录下,其架构如下: @@ -97,8 +94,6 @@ tests/ └── theme.setup.ts # 大海报房间测试的前置依赖。 ``` -各个目录的具体功能及范例请参考 [`docs/tests/`](/docs/tests) 下的 `.md` 文件。 - ### 快速开始 1. 首先,完成安装 `nodejs v20+` 和 `pnpm` 等环境; @@ -116,11 +111,11 @@ tests/ #### 入门指南 -请参阅 [`docs/examples`](/docs/examples) 下的 `.md` 文件来查看详细的代码编写流程。 +请参阅 [`docs/`](/docs/) 下的 `.md` 文件来查看详细的代码编写流程。 目录如下: ``` -docs/examples/ +docs/ ├── features.md # 新增功能模块 ├── pages.md # 新增扩展页面 └── settings.md # 新增设定区块 diff --git a/README.md b/README.md index 96191e00..862c80b9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ 2. 点击扩展图标进入设定页面,并根据你的偏好进入设定。完成后,然后按下保存设定。 3. 进入B站任一直播间即可开始使用。 +## ➵ 贡献 + +请参阅 [贡献指南](CONTRIBUTING.md)。 + ## ➵ 功能简介 **所有主要功能已全部改为可选**,例如: 你可以启用醒目留言记录而不启用同传字幕过滤, 且每个主要功能都有各自的房间黑/白名单 diff --git a/docs/src/background.md b/docs/background.md similarity index 100% rename from docs/src/background.md rename to docs/background.md diff --git a/docs/examples/features.md b/docs/examples/features.md deleted file mode 100644 index c156ec88..00000000 --- a/docs/examples/features.md +++ /dev/null @@ -1 +0,0 @@ -## 新增功能 \ No newline at end of file diff --git a/docs/examples/pages.md b/docs/examples/pages.md deleted file mode 100644 index ea7894a4..00000000 --- a/docs/examples/pages.md +++ /dev/null @@ -1 +0,0 @@ -## 新增页面 \ No newline at end of file diff --git a/docs/examples/settings.md b/docs/examples/settings.md deleted file mode 100644 index c82805e6..00000000 --- a/docs/examples/settings.md +++ /dev/null @@ -1 +0,0 @@ -## 新增设定区块 \ No newline at end of file diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 00000000..05bef937 --- /dev/null +++ b/docs/features.md @@ -0,0 +1,205 @@ +## 新增功能 + +> 本扩展基于 [Plasmo CSUI](https://docs.plasmo.com/framework/content-scripts-ui) 进行前端渲染。 + +如要新增功能,你需要到以下地方新增(其余未列明的地方均为可选): + +``` +src/ + features/ <- 內容腳本 + settings/ + features/ <- 功能设定区块 +``` + +`src/features/`: 此目录用于存放功能,虽然主要集中在内容脚本的渲染,但是也是定义结构的地方,因此必须添加。 + +`src/settings/features/`: 此目录用于存放功能的设定区块,为了让用户能够切换功能的开关,此处也必须添加。 + +### 新增内容脚本 + +在 `src/features/` 目录下新增一个新的内容脚本,例如 `src/features/hello-world.tsx`。 + +根据 `src/features/index.ts`,你需要定义三个东西,其中两个为可选: +```ts +export type FeatureHookRender = (settings: Readonly, info: StreamInfo) => Promise<(React.ReactPortal | React.ReactNode)[] | undefined> +export type FeatureAppRender = React.FC<{}> + +export interface FeatureHandler { + default: FeatureHookRender, + App?: FeatureAppRender, + FeatureContext?: React.Context +} +``` + +以下为定义 `src/features/hello-world.tsx` 的范例: + +```tsx +// 忽略 import.... +const handler: FeatureHookRender = async (settings, info) => { + // settings 是用户设定的内容 + // info 是当前直播间的信息 + // 返回的则是在内容脚本中的每个部件渲染,支援 react portal 以使用与网页元素挂钩 + // 如果不使用portal, 则默认在扩展专用的 shadow-root 进行渲染 + // 如果你不想在内容脚本中渲染任何东西,可以返回空数组 [] + // 如果你想基于条件禁用功能,可以返回 undefined + return [ +
Hello, World!
, // 在 shadow-root 内渲染 + createPortal(
Hello, World!
, document.body) // 在网页元素内渲染 + ] +} + +// FeatureContext 主要用于提供该功能所属的设定内容(即功能设定区块内的内容) +// 一般情况下,FeatureContext 理应在 src/contexts/ 下创建以供其他 React 组件使用,但此处为了简化,直接在内容脚本内创建 +// 如果不需要提供设定内容,可以忽略 FeatureContext +export const FeatureContext = createContext(null) + +// 此处的渲染将直接在扩展专用的 shadow-root 内进行 +// 此处与 FeatureHookRender 不同的地方在于,FeatureHookRender 可以在正式渲染 React 组件之前进行异步操作 +// 同样,此处为可选。 +export function App(): JSX.Element { + return ( +
hello world from app
+ ) +} + +export default handler +``` + +一般情况下,功能都需要在内容脚本渲染其组件。假设你的功能不需要渲染任何内容,你可以这样定义: + +```tsx + +const handler: FeatureHookRender = async (settings, info) => { + return [] // 不渲染任何东西 +} + +export default handler; + +``` + +注意:如果你返回的是 `undefined`,则意味着你的功能将被禁用。 + +完成後,別忘了到 `src/features/index.ts` 中新增你的功能: + +```ts +import * as jimaku from './jimaku' +import * as superchat from './superchat' +import * as helloWorld from './hello-world' // 新增的功能 + +//... + +const features = { + jimaku, + superchat, + helloWorld // 新增的功能 +} + +//... + +``` + +### 新增功能设定区块 + +在 `src/settings/features/` 目录下新增一个新的功能设定区块,例如 `src/settings/features/hello-world.tsx`: + +```tsx + +// 用于显示在功能设定区块的标题 +export const title: string = 'Hello, World 功能' + +export const define: FeatureSettingsDefinition = { + // 此处使用 false 为标示你的功能不支援离线记录 + // 本节将忽略离线记录的内容 + offlineTable: false +} + +// 这里用于定义你的功能的设定结构 +export type FeatureSettingSchema = { + name: string + age: string +} + +// 这里用于定义你的功能的默认设定内容 +export const defaultSettings: Readonly = { + name: 'world', + age: 99 +} + +// 这里用于渲染你的功能的设定区块 +function SuperchatFeatureSettings({state, useHandler}: StateProxy): JSX.Element { + + // 此处为 StateProxy 的使用范例, 详情请自行查看 hooks 文档 + const stringHandler = useHandler>((e) => e.target.value) + const numberHandler = useHandler, number>((e) => e.target.valueAsNumber) + + return ( +
+ + +
+ ) +} + + +export default SuperchatFeatureSettings +``` + +> 有关 `StateProxy` 的使用,请参考 [自定义Hooks](/src/hooks/binding.ts)。 + + +同样,假设你的功能不需要功能设定区块,你可以这样定义: + +```tsx +export const title: string = 'Hello, World 功能' + +export const define: FeatureSettingsDefinition = { + offlineTable: false +} + +export type FeatureSettingSchema = { + // 无设定内容 +} + +export const defaultSettings: Readonly = { + // 无设定内容 +} + +export default function HelloFeatureSettings(): JSX.Element { + return <> // 不渲染任何东西 +} +``` + +有关设定的更多详细信息,你可以参考 [`docs/settings.md`](/docs/settings.md) 文档。 + + +完成後,別忘了到 `src/settings/features/index.ts` 中新增你的功能设定区块: + +```ts + +import * as jimaku from './jimaku' +import * as superchat from './superchat' +import * as helloWorld from './hello-world' // 新增的功能 + +// ... + +const featureSettings = { + jimaku, + superchat, + helloWorld // 新增的功能 +} + +// ... +``` + +以上两个步骤完成后,你的功能就已经基本完成了。 + +### 进阶开发 + +你可以参考以下的源码以进行更进阶的功能开发: +- [现成功能参考](/src/features/) +- [功能设定区块参考](/src/settings/features/) +- [自定义Hooks](/src/hooks/) +- [自定义Context](/src/contexts/) +- [辅助函数](/src/utils/) +- [全局组件](/src/components) +- [设定区块用组件](/src/settings/components/) \ No newline at end of file diff --git a/docs/pages.md b/docs/pages.md new file mode 100644 index 00000000..b734bc0f --- /dev/null +++ b/docs/pages.md @@ -0,0 +1,71 @@ +## 新增页面 + +> 本扩展基于 [Plasmo Extension Page](https://docs.plasmo.com/framework/ext-pages) 进行页面渲染。 + +如要新增页面,只需要到以下地方新增即可: + +``` +src/ + tabs/ <- 扩展页面 +``` + +### 创建一个新的扩展页面 + +在 `src/tabs/` 目录下新增一个新的扩展页面,例如 `src/tabs/hello-world.tsx`。 + +```tsx + +import '~style.css' // 汇入 tailwindcss 样式以在页面中使用 + +function App(): JSX.Element { + return ( +
Hello, World!
+ ) +} + +export default App +``` + +完成后,扩展在建置时会自动生成 hello-world.html 文件,且会自动添加到扩展的 `manifest.json` 中。 + +### 跳转到扩展页面 + +如要在其他位置跳转到扩展页面,有以下两种方式: + +- 使用 `open-tab` messager 指令,进行跳转: + +范例如下: + +```tsx +function HelloWorldButton(): JSX.Element { + const openPage = () => sendMessager('open-tab', { tab: 'hello-world' }) + return +} + +export default HelloWorldButton +``` + +- 使用 `chrome.tabs.create` 和 `chrome.runtime.getURL` API,进行跳转: + +范例如下: + +```tsx +function HelloWorldButton(): JSX.Element { + const openPage = () => chrome.tabs.create({ url: chrome.runtime.getURL('/tabs/hello-world.html') }) + return +} +``` + +> 使用 chrome API 方式可能需要考虑到一些API无法在内容脚本中使用的问题,因此建议使用 messager 方式进行跳转。 + +有关 messager 的更多信息,你可以参阅 [`docs/background.md`](./background.md) 文档中有关 messager 的信息。 + + +### 进阶开发 + +你可以参考以下的源码以进行更进阶页面开发: + +- [现成的扩展页面参考](/src/tabs/) +- [自定义Hooks](/src/hooks/) +- [辅助函数](/src/utils/) +- [全局组件](/src/components) \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 00000000..8175084e --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,183 @@ +## 新增设定区块 + +如要新增页面,只需要到以下地方新增即可: + +``` +src/ + settings/ + fragments/ <- 设定区块列表 +``` + +### 创建一个新的设定区块 + +在 `src/settings/fragments/` 目录下新增一个新的设定区块,例如 `src/settings/fragments/hello-world.tsx`。 + +```tsx + +// 定义设定区块的数据结构 +export type SettingSchema = { + name: string + age: string +} + +// 定义设定区块的默认值 +export const defaultSettings: Readonly = { + name: 'John Doe', + age: '18' +} + +// 设定区块的标题 +export const title = 'Hello, World 设定' + +// 设定区块的描述 +export const description = [ + '此处的描述将会显示在设定页面的用户导航中。参考: src/components/Tutorial.tsx', +] + +// 设定区块的渲染内容 +function HelloWorldSettings({ state, useHandler }: StateProxy): JSX.Element { + + return ( +
+ state.name = e.target.value} + /> + state.age = e.target.value} + /> +
+ ) +} + +export default HelloWorldSettings +``` + +**使用 `StateProxy` 进行数据双向绑定** + +厌倦了每次写 React 都要手动处理 `onChange` ? 你可以使用本扩展内置的 `StateProxy` 进行数据双向绑定。 + +```tsx +function HelloWorldSettings({ state, useHandler }: StateProxy): JSX.Element { + + // 泛型参数为事件类型和目标值类型,这里是输入框的事件和提取输入框的值 + // 目标值类型默认为 string, 如果你的目标值类型不是 string, 你可以传入第二个泛型参数 + // 函数参数则为提取目标值的函数 + // 例如这里的 `e => e.target.value` 就是提取输入框的值 于 对应的 `string` 类型 + const stringHandler = useHandler>((e) => e.target.value) + const numberHandler = useHandler, number>((e) => e.target.valueAsNumber) + + return ( +
+ + +
+ ) +} +``` + +
+甚至... + +```tsx +function HelloWorldSettings({ state, useHandler }: StateProxy): JSX.Element { + + const handler = useHandler>((e) => e.target.value) + + const strHandle = (key: PickLeaves) => { + value: state[key], + onChange: handler(key) + } + + return +} +``` + +
+ + +> 有关 `StateProxy` 的使用,请参考 [自定义Hooks](/src/hooks/binding.ts)。 + + +完成后,你需要到 `src/settings/index.ts` 中新增你的设定区块: + +```ts +// ... +import * as capture from './fragments/capture' +import * as developer from './fragments/developer' +import * as display from './fragments/display' +import * as features from './fragments/features' +import * as listings from './fragments/listings' +import * as helloWorld from './fragments/hello-world' // 新增的设定区块 + +// ... + +// 此处也设置了设定区块的顺序 +const fragments = { + 'settings.features': features, + 'settings.listings': listings, + 'settings.capture': capture, + 'settings.display': display, + 'settings.developer': developer + 'settings.helloWorld': helloWorld // 新增的设定区块 +} + + +export default fragments +``` + + +### 获取设定内容 + +你可以在任何地方获取设定内容,获取方式有几种: + +- 使用 [`useStorage`](/src/hooks/storage.ts) 钩子: + +此功能来自 Plasmo, 你可以参考 [Plasmo 文档](https://docs.plasmo.com/framework/storage#react-hook-api) 以了解更多。 + +```tsx +function App(): JSX.Element { + const [helloWorld] = useStorage("settings.helloWorld") + return <> +} +``` + +- 使用 [`getSettingStorage`](/src/utils/storage.ts) 函数: + +```ts +const helloWorldSettings = getSettingStorage('settings.helloWorld') +``` + +> 此方式返回的数据本身包含设定结构,因此无需手动标注类型。 + +- 如果你在内容脚本中使用设定,则可以直接从 [`ContentContext`](/src/contexts/ContentContexts.ts) 获取设定: + +```tsx +function ContentScript(): JSX.Element { + const { settings } = useContext(ContentContext) + const helloWorld = settings['settings.helloWorld'] + return <> +} +``` + +### 进阶开发 + +你可以参考以下的源码以进行更进阶的设定开发: + +- [现成的设定区块参考](/src/settings/fragments/) +- [设定区块组件](/src/settings/components/) +- [自定义Hooks](/src/hooks/) +- [自定义Context](/src/contexts/) +- [辅助函数](/src/utils/) +- [全局组件](/src/components) \ No newline at end of file diff --git a/docs/src/adapters.md b/docs/src/adapters.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/api.md b/docs/src/api.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/components.md b/docs/src/components.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/contents.md b/docs/src/contents.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/contexts.md b/docs/src/contexts.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/database.md b/docs/src/database.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/features.md b/docs/src/features.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/hooks.md b/docs/src/hooks.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/migrations.md b/docs/src/migrations.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/players.md b/docs/src/players.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/settings.md b/docs/src/settings.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/styles.md b/docs/src/styles.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/tabs.md b/docs/src/tabs.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/toaster.md b/docs/src/toaster.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/types.md b/docs/src/types.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/updaters.md b/docs/src/updaters.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/utils.md b/docs/src/utils.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/content.spec.md b/docs/tests/content.spec.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/features.md b/docs/tests/features.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/fixtures.md b/docs/tests/fixtures.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/helpers.md b/docs/tests/helpers.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/options.md b/docs/tests/options.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/pages.md b/docs/tests/pages.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/theme.setup.md b/docs/tests/theme.setup.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/tests/utils.md b/docs/tests/utils.md deleted file mode 100644 index e69de29b..00000000 diff --git a/package.json b/package.json index f516ca31..87f1d0f3 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,13 @@ "dev:opera": "pnpm dev --target=opera-mv3", "build": "plasmo build --no-minify", "package": "plasmo package", - "clean": "rm -rf build && rm -rf .plasmo", + "clean": "run-script-os", + "clean:nix": "rm -rf build && rm -rf .plasmo", + "clean:windows": "rmdir /s /q build && rmdir /s /q .plasmo", "test": "playwright test", - "test:prepare": "mv build/chrome-mv3-prod build/extension", + "test:prepare": "run-script-os", + "test:prepare:nix": "mv build/chrome-mv3-prod build/extension", + "test:prepare:windows": "move build\\chrome-mv3-prod build\\extension", "test:rebuild": "pnpm clean && pnpm build && pnpm test:prepare" }, "dependencies": { @@ -56,6 +60,7 @@ "glob": "^10.3.10", "postcss": "^8.4.35", "prettier": "^3.2.5", + "run-script-os": "^1.1.6", "typescript": "^5.3.3" }, "manifest": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f442ab69..23249891 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ devDependencies: prettier: specifier: ^3.2.5 version: 3.2.5 + run-script-os: + specifier: ^1.1.6 + version: 1.1.6 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -6400,6 +6403,11 @@ packages: queue-microtask: 1.2.3 dev: false + /run-script-os@1.1.6: + resolution: {integrity: sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==} + hasBin: true + dev: true + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: diff --git a/src/components/BJFThemeProvider.tsx b/src/components/BJFThemeProvider.tsx index 7aab3ba2..03ef3c43 100644 --- a/src/components/BJFThemeProvider.tsx +++ b/src/components/BJFThemeProvider.tsx @@ -3,13 +3,26 @@ import { useEffect, useMemo } from 'react'; import { ThemeProvider } from '@material-tailwind/react'; import { usePreferredColorScheme } from '@react-hooks-library/core'; +/** + * Props for the SettingThemeProvider component. + */ export type SettingThemeProviderProps = { - children: React.ReactNode - dark?: boolean - controller?: Element | Element[] + /** + * The children components to be rendered. + */ + children: React.ReactNode; + + /** + * Whether to use the dark theme. + */ + dark?: boolean; + + /** + * The controller element(s) for the theme provider. + */ + controller?: Element | Element[]; } - type MaterialTheme = { [components: string]: { defaultProps?: Record, @@ -90,7 +103,35 @@ const darkTheme = { } -// controling material tailwind theme + tailwindcss dark mode +/** + * BJFThemeProvider component provides a theme provider for the BJF application. + * it also controls material tailwind theme + tailwindcss dark mode + * + * @param {SettingThemeProviderProps} props - The component props. + * @param {ReactNode} props.children - The child components to be wrapped by the theme provider. + * @param {boolean} props.dark - Indicates whether to use the dark theme. If `true`, the dark theme will be used. If `false`, the light theme will be used. If `undefined`, the theme will be determined based on the system color scheme. + * @param {HTMLElement | HTMLElement[]} props.controller - The HTML element(s) to apply the theme class to. If an array is provided, the theme class will be applied to each element in the array. If `undefined`, the theme class will be applied to the `document.documentElement`. + * + * @returns {JSX.Element} The JSX element representing the BJFThemeProvider component. + * + * @example + * // Using BJFThemeProvider with dark theme + * + * + * + * + * @example + * // Using BJFThemeProvider with light theme + * + * + * + * + * @example + * // Using BJFThemeProvider with system color scheme + * + * + * + */ function BJFThemeProvider({ children, dark, controller }: SettingThemeProviderProps): JSX.Element { const systemColor = usePreferredColorScheme() diff --git a/src/components/BLiveThemeProvider.tsx b/src/components/BLiveThemeProvider.tsx index 2218ebfa..7f0048e3 100644 --- a/src/components/BLiveThemeProvider.tsx +++ b/src/components/BLiveThemeProvider.tsx @@ -1,14 +1,27 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { isDarkThemeBilbili } from '~utils/bilibili'; -import { isDarkTheme } from '~utils/misc'; import { useMutationObserver } from '@react-hooks-library/core'; -import BJFThemeProvider from './BJFThemeProvider'; import BJFThemeDarkContext from '~contexts/BLiveThemeDarkContext'; +import BJFThemeProvider from './BJFThemeProvider'; const fetchDarkMode = () => /*isDarkTheme() &&*/ isDarkThemeBilbili() +/** + * BLiveThemeProvider component provides a theme context for the children components. + * + * @param children - The child components to be wrapped by the theme provider. + * @param element - The element or elements to be used for checking dark/light theme from bilibili. If not provided, the document.documentElement will be used. + * @returns The JSX element representing the theme provider. + * + * @example + * ```tsx + * + * + * + * ``` + */ function BLiveThemeProvider({ children, element }: { children: React.ReactNode, element?: Element | Element[] }): JSX.Element { const themeContext = useState(fetchDarkMode) diff --git a/src/components/ConditionalWrapper.tsx b/src/components/ConditionalWrapper.tsx index e1faf557..a6900bad 100644 --- a/src/components/ConditionalWrapper.tsx +++ b/src/components/ConditionalWrapper.tsx @@ -1,12 +1,39 @@ import type { ComponentType } from "react" +/** + * Props for the ConditionalWrapper component. + * @template P - The type of additional props for the wrapped component. + */ export type ConditionWrapperProps

= { - condition: boolean - children: React.ReactNode - as: ComponentType

-} & P + condition: boolean; + children: React.ReactNode; + as: ComponentType

; +} & P; +/** + * Wraps the children with a specified component if a condition is met, otherwise returns the children as is. + * + * @template P - The props type of the component. + * @param {Object} props - The component props. + * @param {boolean} props.condition - The condition to check. + * @param {React.ReactNode} props.children - The children to wrap. + * @param {React.ComponentType

} props.as - The component to wrap the children with. + * @param {P} props.rest - The rest of the props to pass to the wrapped component. + * @returns {JSX.Element} - The wrapped or unwrapped children. + * + * @example + * // Wraps the children with a

component if the condition is true. + * + *

Hello, world!

+ *
+ * + * @example + * // Returns the children as is if the condition is false. + * + *

Hello, world!

+ *
+ */ function ConditionalWrapper

({ condition, children, as: Component, ...rest }: ConditionWrapperProps

): JSX.Element { if (condition) { return {children} diff --git a/src/components/DraggableFloatingButton.tsx b/src/components/DraggableFloatingButton.tsx index 029ca47f..9e27e8d0 100644 --- a/src/components/DraggableFloatingButton.tsx +++ b/src/components/DraggableFloatingButton.tsx @@ -1,13 +1,48 @@ import { Fragment, useDeferredValue, useState, type MouseEventHandler } from "react" import { Rnd } from "react-rnd" +/** + * Props for the DraggableFloatingButton component. + */ export type DraggableFloatingButtonProps = { - onClick?: MouseEventHandler - children: React.ReactNode - className?: string, - style?: React.CSSProperties + /** + * Event handler for the button click event. + */ + onClick?: MouseEventHandler; + + /** + * The content of the button. + */ + children: React.ReactNode; + + /** + * Additional CSS class name for the button. + */ + className?: string; + + /** + * Inline styles for the button. + */ + style?: React.CSSProperties; } + +/** + * Renders a draggable floating button component. + * + * @param {DraggableFloatingButtonProps} props - The component props. + * @returns {JSX.Element} The rendered component. + * + * @example + * // Example usage of DraggableFloatingButton component + * console.log('Button clicked')} + * className="bg-blue-500" + * style={{ fontSize: '16px' }} + * > + * Click me + * + */ function DraggableFloatingButton(props: DraggableFloatingButtonProps): JSX.Element { const { onClick, children, className, style } = props diff --git a/src/components/OfflineRecordsProvider.tsx b/src/components/OfflineRecordsProvider.tsx index 30e9eaa4..116d555a 100644 --- a/src/components/OfflineRecordsProvider.tsx +++ b/src/components/OfflineRecordsProvider.tsx @@ -5,20 +5,30 @@ import type { FeatureType } from "~features"; import type { Settings } from "~settings"; +/** + * Props for the OfflineRecordsProvider component. + * @template T - The type of the table. + */ export type OfflineRecordsProviderProps = { - feature: FeatureType - settings: Settings - room: string - table: T - filter?: (x: RecordType) => boolean - sortBy?: keyof RecordType - reverse?: boolean - loading: React.ReactNode - error: (err: any, retry: VoidFunction) => React.ReactNode - children: (data: RecordType[]) => React.ReactNode + feature: FeatureType; // The feature type. + settings: Settings; // The settings object. + room: string; // The room name. + table: T; // The table type. + filter?: (x: RecordType) => boolean; // Optional filter function. + sortBy?: keyof RecordType; // Optional key to sort the records by. + reverse?: boolean; // Optional flag to reverse the sorting order. + loading: React.ReactNode; // The loading indicator. + error: (err: any, retry: VoidFunction) => React.ReactNode; // Function to render the error message. + children: (data: RecordType[]) => React.ReactNode; // Function to render the children components. } - +/** + * Provides offline records for a specific table type. + * + * @template T - The type of the table. + * @param {OfflineRecordsProviderProps} props - The props for the OfflineRecordsProvider component. + * @returns {JSX.Element} - The rendered OfflineRecordsProvider component. + */ function OfflineRecordsProvider(props: OfflineRecordsProviderProps): JSX.Element { const { settings, table, feature, children, loading, error, room, reverse, sortBy, filter } = props diff --git a/src/components/PromiseHandler.tsx b/src/components/PromiseHandler.tsx index 45cef921..62a60d07 100644 --- a/src/components/PromiseHandler.tsx +++ b/src/components/PromiseHandler.tsx @@ -8,6 +8,10 @@ const ErrorContext = createContext(null) const PromiseHandlerContext = createContext(null) +/** + * Props for the PromiseHandler component. + * @template T The type of the resolved value of the promise. + */ export type PromiseHandlerProps = { promise: Promise | (() => Promise) fallback?: JSX.Element diff --git a/src/components/ShadowRoot.tsx b/src/components/ShadowRoot.tsx index a564faf9..bbbdea00 100644 --- a/src/components/ShadowRoot.tsx +++ b/src/components/ShadowRoot.tsx @@ -1,6 +1,17 @@ import { useEffect, useRef } from 'react'; -function ShadowRoot({ children }) { +/** + * Renders a component that attaches a shadow root to a DOM element and sets its inner HTML to a slot. + * @param {Object} props - The component props. + * @param {React.ReactNode} props.children - The content to be rendered inside the shadow root. + * @returns {React.ReactElement} The rendered component. + * @example + * // Usage: + * + *

Hello, world!
+ * + */ +function ShadowRoot({ children }: { children: React.ReactNode }): JSX.Element { const ref = useRef(null) useEffect(() => { diff --git a/src/components/TailwindScope.tsx b/src/components/TailwindScope.tsx index f3f2df5a..682fc8f6 100644 --- a/src/components/TailwindScope.tsx +++ b/src/components/TailwindScope.tsx @@ -1,6 +1,14 @@ import ShadowRoot from './ShadowRoot'; import styleText from 'data-text:~style.css'; +/** + * Renders a component that applies a Tailwind CSS scope to its children. + * + * @param {Object} props - The component props. + * @param {React.ReactNode} props.children - The children to be rendered within the Tailwind scope. + * @param {boolean} [props.dark] - Optional. Specifies whether the dark mode should be applied. + * @returns {JSX.Element} The rendered TailwindScope component. + */ function TailwindScope({ children, dark }: { children: React.ReactNode, dark?: boolean }): JSX.Element { return (
diff --git a/src/components/Tutorial.tsx b/src/components/Tutorial.tsx index efb8edcf..7f5e9d73 100644 --- a/src/components/Tutorial.tsx +++ b/src/components/Tutorial.tsx @@ -3,22 +3,71 @@ import type { Step } from "react-joyride" import Joyride, { EVENTS, STATUS } from "react-joyride" import { localStorage } from "~utils/storage" +/** + * Represents a tutorial step. + */ export type TutorialStep = Step & { + /** + * A function that is called before entering the step. + * @param element - The HTML element associated with the step. + */ beforeEnter?: (element: HTMLElement) => void + + /** + * A function that is called before leaving the step. + * @param element - The HTML element associated with the step. + */ beforeLeave?: (element: HTMLElement) => void } +/** + * Props for the Tutorial component. + */ export type TutorialProps = { - steps: Array - stateKey?: string - zIndex?: number - noScroll?: boolean - applyGlobalSettings?: (step: TutorialStep) => void + /** + * An array of tutorial steps. + */ + steps: Array; + + /** + * An optional key to identify the state of the tutorial component. + */ + stateKey?: string; + + /** + * The z-index of the tutorial component. + */ + zIndex?: number; + + /** + * A flag indicating whether scrolling should be disabled during the tutorial. + */ + noScroll?: boolean; + + /** + * A callback function to apply global settings for each tutorial step. + * @param step - The current tutorial step. + */ + applyGlobalSettings?: (step: TutorialStep) => void; } +/** + * Represents the props for the Tutorial component. + */ export type TutorialRefProps = { + /** + * Function to start the tutorial. + */ start: () => void + + /** + * Function to stop the tutorial. + */ stop: () => void + + /** + * Indicates whether the tutorial is currently running. + */ running: boolean } @@ -26,6 +75,43 @@ function defaultApplyGlobalSettings(step: TutorialStep) { if (!step.placement) step.placement = 'auto' } +/** + * Renders a tutorial component using the Joyride library. + * + * @param {TutorialProps} props - The props for the Tutorial component. + * @param {Ref} ref - The ref object for the Tutorial component. + * @returns {JSX.Element} The rendered Tutorial component. + * + * @example + * // Example usage of Tutorial component + * const steps = [ + * { + * target: '.step-1', + * content: 'This is step 1', + * }, + * { + * target: '.step-2', + * content: 'This is step 2', + * }, + * ]; + * + * function App() { + * const tutorialRef = useRef(null); + * + * const startTutorial = () => { + * if (tutorialRef.current) { + * tutorialRef.current.start(); + * } + * }; + * + * return ( + *
+ * + * + *
+ * ); + * } + */ function Tutorial(props: TutorialProps, ref: Ref): JSX.Element { const [run, setRun] = useState(false) diff --git a/src/contents/index/App.tsx b/src/contents/index/App.tsx index d92944e3..87350432 100644 --- a/src/contents/index/App.tsx +++ b/src/contents/index/App.tsx @@ -4,7 +4,7 @@ import { useToggle } from "@react-hooks-library/core"; import { Fragment, useContext, useMemo, useRef } from "react"; import Tutorial, { type TutorialRefProps, type TutorialStep } from "~components/Tutorial"; import GenericContext from "~contexts/GenericContext"; -import StreamInfoContext from "~contexts/StreamInfoContexts"; +import ContentContext from "~contexts/ContentContexts"; import { useWebScreenChange } from "~hooks/bilibili"; import ButtonList from "./components/ButtonList"; import FloatingMenuButton from "./components/FloatingMenuButtion"; @@ -60,7 +60,7 @@ const steps: Array = [ function App(): JSX.Element { - const streamInfo = useContext(StreamInfoContext) + const streamInfo = useContext(ContentContext) // i don't know why the hell this got rendered at the top level during HMR, should be the bug for plasmo if (!streamInfo) { diff --git a/src/contents/index/components/ButtonList.tsx b/src/contents/index/components/ButtonList.tsx index 782be636..4a924c3b 100644 --- a/src/contents/index/components/ButtonList.tsx +++ b/src/contents/index/components/ButtonList.tsx @@ -1,14 +1,14 @@ import { Button } from "@material-tailwind/react" import { useContext } from "react" import { sendForward } from "~background/forwards" -import StreamInfoContext from "~contexts/StreamInfoContexts" +import ContentContext from "~contexts/ContentContexts" import { usePopupWindow } from "~hooks/window" import { sendMessager } from "~utils/messaging" function ButtonList(): JSX.Element { - const streamInfo = useContext(StreamInfoContext) + const streamInfo = useContext(ContentContext) const { settings, info } = streamInfo const { "settings.display": displaySettings, "settings.features": { common: { enabledPip, monitorWindow }} } = settings diff --git a/src/contents/index/components/Header.tsx b/src/contents/index/components/Header.tsx index ed5f3842..55b37e6d 100644 --- a/src/contents/index/components/Header.tsx +++ b/src/contents/index/components/Header.tsx @@ -1,11 +1,11 @@ import { Typography, IconButton } from "@material-tailwind/react" import { useContext } from "react" -import StreamInfoContext from "~contexts/StreamInfoContexts" +import ContentContext from "~contexts/ContentContexts" function Header({ closeDrawer }: { closeDrawer: VoidFunction }): JSX.Element { - const { info } = useContext(StreamInfoContext) + const { info } = useContext(ContentContext) return (
diff --git a/src/contents/index/mounter.tsx b/src/contents/index/mounter.tsx index 2b683b0e..5241160f 100644 --- a/src/contents/index/mounter.tsx +++ b/src/contents/index/mounter.tsx @@ -4,7 +4,7 @@ import { toast } from "sonner/dist" import { ensureLogin, type StreamInfo } from "~api/bilibili" import { sendForward } from "~background/forwards" import BLiveThemeProvider from "~components/BLiveThemeProvider" -import StreamInfoContext from "~contexts/StreamInfoContexts" +import ContentContext from "~contexts/ContentContexts" import type { FeatureType } from "~features" import features from "~features" import type { Settings } from "~settings" @@ -81,12 +81,12 @@ function createMountPoints(plasmo: PlasmoSpec, info: StreamInfo): RootMountable[ root = createRoot(section) root.render( - + {App && } {portals} - + ) }, @@ -182,9 +182,9 @@ function createApp(roomId: string, plasmo: PlasmoSpec, info: StreamInfo): App { console.info('開始渲染主元素....') root.render( - + - + ) console.info('渲染主元素完成') diff --git a/src/contexts/StreamInfoContexts.ts b/src/contexts/ContentContexts.ts similarity index 56% rename from src/contexts/StreamInfoContexts.ts rename to src/contexts/ContentContexts.ts index 9bdc1f88..af930099 100644 --- a/src/contexts/StreamInfoContexts.ts +++ b/src/contexts/ContentContexts.ts @@ -2,11 +2,11 @@ import { createContext } from "react"; import type { StreamInfo } from "~api/bilibili"; import type { Settings } from "~settings"; -export type StreamInfoContextProps = { +export type ContentContextProps = { info: StreamInfo settings: Settings } -const StreamInfoContext = createContext(null) +const ContentContext = createContext(null) -export default StreamInfoContext \ No newline at end of file +export default ContentContext \ No newline at end of file diff --git a/src/features/jimaku/components/ButtonArea.tsx b/src/features/jimaku/components/ButtonArea.tsx index 7056b3d7..fa62476d 100644 --- a/src/features/jimaku/components/ButtonArea.tsx +++ b/src/features/jimaku/components/ButtonArea.tsx @@ -1,6 +1,6 @@ import { Fragment, useContext, useState } from "react"; import JimakuFeatureContext from "~contexts/JimakuFeatureContext"; -import StreamInfoContext from "~contexts/StreamInfoContexts"; +import ContentContext from "~contexts/ContentContexts"; import { useRecords } from "~hooks/records"; import { usePopupWindow } from "~hooks/window"; import JimakuButton from './JimakuButton'; @@ -15,7 +15,7 @@ export type ButtonAreaProps = { function ButtonArea({ clearJimaku, jimakus }: ButtonAreaProps): JSX.Element { - const { settings, info } = useContext(StreamInfoContext) + const { settings, info } = useContext(ContentContext) const { jimakuZone, buttonZone: btnStyle, jimakuPopupWindow } = useContext(JimakuFeatureContext) const { order } = jimakuZone diff --git a/src/features/jimaku/components/JimakuArea.tsx b/src/features/jimaku/components/JimakuArea.tsx index c6133636..04612fd1 100644 --- a/src/features/jimaku/components/JimakuArea.tsx +++ b/src/features/jimaku/components/JimakuArea.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { Rnd } from 'react-rnd'; import ConditionalWrapper from '~components/ConditionalWrapper'; import JimakuFeatureContext from '~contexts/JimakuFeatureContext'; -import StreamInfoContext from '~contexts/StreamInfoContexts'; +import ContentContext from '~contexts/ContentContexts'; import { useWebScreenChange } from '~hooks/bilibili'; import { useTeleport } from '~hooks/teleport'; import type { JimakuSchema } from '~settings/features/jimaku/components/JimakuFragment'; @@ -47,7 +47,7 @@ export type JimakuAreaProps = { function JimakuArea({ jimaku }: JimakuAreaProps): JSX.Element { - const { settings, info: { isTheme } } = useContext(StreamInfoContext) + const { settings, info: { isTheme } } = useContext(ContentContext) const { jimakuZone: jimakuStyle } = useContext(JimakuFeatureContext) const dev = settings['settings.developer'] @@ -106,7 +106,7 @@ function JimakuArea({ jimaku }: JimakuAreaProps): JSX.Element { style={{ zIndex: 9999, display: visible ? 'block' : 'none' }} minHeight={100} minWidth={200} - size={0.93} + scale={0.93} default={{ x: 100, y: -300, diff --git a/src/features/jimaku/components/JimakuCaptureLayer.tsx b/src/features/jimaku/components/JimakuCaptureLayer.tsx index 4758c4bb..61cf09b1 100644 --- a/src/features/jimaku/components/JimakuCaptureLayer.tsx +++ b/src/features/jimaku/components/JimakuCaptureLayer.tsx @@ -2,7 +2,7 @@ import { Fragment, useCallback, useContext, useRef, useState } from 'react'; import { getTimeStamp, randomString, toStreamingTime } from '~utils/misc'; import { sendForward } from '~background/forwards'; -import StreamInfoContext from '~contexts/StreamInfoContexts'; +import ContentContext from '~contexts/ContentContexts'; import db from '~database'; import { useBLiveSubscriber } from '~hooks/message'; import { parseJimaku } from '~utils/bilibili'; @@ -18,7 +18,7 @@ export type JimakuCaptureLayerProps = { function JimakuCaptureLayer(props: JimakuCaptureLayerProps): JSX.Element { - const { settings, info } = useContext(StreamInfoContext) + const { settings, info } = useContext(ContentContext) const { jimakuZone: jimakuStyle, danmakuZone, jimakuPopupWindow, listingZone } = useContext(JimakuFeatureContext) const { offlineRecords } = props diff --git a/src/features/jimaku/index.tsx b/src/features/jimaku/index.tsx index 4f032924..faf16d72 100644 --- a/src/features/jimaku/index.tsx +++ b/src/features/jimaku/index.tsx @@ -6,7 +6,7 @@ import { isNativeVtuber } from "~api/bilibili"; import OfflineRecordsProvider from '~components/OfflineRecordsProvider'; import TailwindScope from '~components/TailwindScope'; import JimakuFeatureContext from "~contexts/JimakuFeatureContext"; -import StreamInfoContext from "~contexts/StreamInfoContexts"; +import ContentContext from "~contexts/ContentContexts"; import { parseJimaku } from "~utils/bilibili"; import { retryCatcher } from "~utils/fetch"; import type { FeatureHookRender } from ".."; @@ -27,7 +27,7 @@ export const FeatureContext = JimakuFeatureContext export function App(): JSX.Element { - const { settings } = useContext(StreamInfoContext) + const { settings } = useContext(ContentContext) const { danmakuZone: { regex, opacity, hide } } = useContext(FeatureContext) const dev = settings['settings.developer'] diff --git a/src/features/superchat/components/SuperChatArea.tsx b/src/features/superchat/components/SuperChatArea.tsx index 8c8ebc85..0e1681b0 100644 --- a/src/features/superchat/components/SuperChatArea.tsx +++ b/src/features/superchat/components/SuperChatArea.tsx @@ -1,5 +1,5 @@ import { useContext, useRef } from "react" -import StreamInfoContext from "~contexts/StreamInfoContexts" +import ContentContext from "~contexts/ContentContexts" import { useScrollOptimizer } from "~hooks/optimizer" import { useRecords } from "~hooks/records" import SuperChatItem, { type SuperChatCard } from "./SuperChatItem" @@ -15,7 +15,7 @@ export type SuperChatAreaProps = { function SuperChatArea(props: SuperChatAreaProps): JSX.Element { const [ themeDark ] = useContext(BJFThemeDarkContext) - const { settings, info } = useContext(StreamInfoContext) + const { settings, info } = useContext(ContentContext) const { buttonColor } = useContext(SuperChatFeatureContext) const { superchats, clearSuperChat } = props const { enabledRecording } = settings['settings.features'] diff --git a/src/features/superchat/components/SuperChatCaptureLayer.tsx b/src/features/superchat/components/SuperChatCaptureLayer.tsx index 4ed939d3..3668ae8f 100644 --- a/src/features/superchat/components/SuperChatCaptureLayer.tsx +++ b/src/features/superchat/components/SuperChatCaptureLayer.tsx @@ -2,7 +2,7 @@ import { useCallback, useContext, useRef, useState } from "react" import { getTimeStamp, randomString, toStreamingTime } from "~utils/misc" import { useInterval } from "@react-hooks-library/core" -import StreamInfoContext from "~contexts/StreamInfoContexts" +import ContentContext from "~contexts/ContentContexts" import db from "~database" import { useBLiveSubscriber } from "~hooks/message" import SuperChatArea from "./SuperChatArea" @@ -16,7 +16,7 @@ export type SuperChatCaptureLayerProps = { function SuperChatCaptureLayer(props: SuperChatCaptureLayerProps): JSX.Element { - const { settings, info } = useContext(StreamInfoContext) + const { settings, info } = useContext(ContentContext) const { offlineRecords } = props const { enabledRecording, diff --git a/src/settings/components/Selector.tsx b/src/settings/components/Selector.tsx index b917c764..cfcc2132 100644 --- a/src/settings/components/Selector.tsx +++ b/src/settings/components/Selector.tsx @@ -41,7 +41,7 @@ function Selector(props: SelectorProps): JSX.Element {
-
+
{props.options.map((option, index) => ( selectOption(option)} className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-300" role="menuitem">{option.label} diff --git a/tests/fixtures/background.ts b/tests/fixtures/background.ts index 04e883db..cd887517 100644 --- a/tests/fixtures/background.ts +++ b/tests/fixtures/background.ts @@ -15,10 +15,12 @@ export type BackgroundFixtures = { export const test = extensionBase.extend({ + // 代表 设定页面 settings: async ({ page, tabUrl }, use) => { await page.goto(tabUrl('settings.html'), { waitUntil: 'domcontentloaded' }) await use(page) }, + // 直播间页面用 front: async ({ context, rooms, api, isThemeRoom, cacher }, use) => { const frontPage = await context.newPage() await using room = new BilibiliPage(frontPage, api) diff --git a/tests/fixtures/content.ts b/tests/fixtures/content.ts index f2cbedda..5c3b4ebd 100644 --- a/tests/fixtures/content.ts +++ b/tests/fixtures/content.ts @@ -15,6 +15,8 @@ export type ContentFixtures = { export const test = extensionBase.extend({ + // 代表内容页,可能是直播间页或者大海报直播间内的 iframe + // 建议使用 content 来获取内容页,而不是直接使用 page content: async ({ room, page }, use) => { page.frames().map(f => logger.debug(f.url())) const maybeFrame = await room.getContentLocator() @@ -23,6 +25,7 @@ export const test = extensionBase.extend({ await use(maybeFrame) }, + // 直播间实例 room: [ async ({ page, isThemeRoom, rooms, maxRoomRetries, cacher, api }, use) => { await using bilibiliPage = new BilibiliPage(page, api) @@ -37,7 +40,7 @@ export const test = extensionBase.extend({ { auto: true, timeout: 0 } ], - // force to theme room + // 强制使用大海报房间时使用 themeRoom: [ async ({ rooms, maxRoomRetries, cacher, room }, use) => { if (await room.isThemePage()) { diff --git a/tests/helpers/bilibili-api.ts b/tests/helpers/bilibili-api.ts index 3320bfd8..f89bd311 100644 --- a/tests/helpers/bilibili-api.ts +++ b/tests/helpers/bilibili-api.ts @@ -15,8 +15,15 @@ export interface LiveRoomInfo { } +/** + * Represents the Bilibili API. + */ export default class BilbiliApi { + /** + * 初始化Bilibili API。 + * @returns 一个解析为BilbiliApi实例的Promise。 + */ static async init(): Promise { const context = await request.newContext({ baseURL: 'https://api.live.bilibili.com' @@ -24,20 +31,41 @@ export default class BilbiliApi { return new BilbiliApi(context) } + /** + * 构造BilbiliApi的新实例。 + * @param context - API请求的上下文。 + */ constructor(private readonly context: APIRequestContext) { } + /** + * 从指定路径获取数据。 + * @param path - 要获取数据的路径。 + * @returns 一个解析为获取的数据的Promise。 + * @throws 如果获取操作失败,则抛出错误。 + */ private async fetch(path: string): Promise { const res = await this.context.get(path) - if (!res.ok()) throw new Error(`failed to fetch bilibili api: ${res.statusText()}`) + if (!res.ok()) throw new Error(`获取bilibili API失败:${res.statusText()}`) return await res.json() } + /** + * 获取房间的状态。 + * @param room - 房间号。 + * @returns 一个解析为房间状态('online'或'offline')的Promise。 + * @throws 如果无法获取房间状态,则抛出错误。 + */ async getRoomStatus(room: number): Promise<'online' | 'offline'> { const data = await this.fetch('/room/v1/Room/room_init?id=' + room) - if (data.code !== 0) throw new Error(`bili error: ${data.message}`) + if (data.code !== 0) throw new Error(`bili错误:${data.message}`) return data.data.live_status === 1 ? 'online' : 'offline' } + /** + * 查找直播房间的信息。 + * @param room - 房间号。 + * @returns 一个解析为直播房间信息的Promise,如果找不到房间则返回null。 + */ async findLiveRoom(room: number): Promise { const data = await this.fetch('/xlive/web-room/v1/index/getInfoByRoom?room_id=' + room) if (data.code !== 0) return null @@ -56,6 +84,11 @@ export default class BilbiliApi { } } + /** + * 获取一定范围内的直播房间。 + * @param pages - 要获取的页数。 + * @returns 一个解析为直播房间信息数组的Promise。 + */ async getLiveRoomsRange(pages: number): Promise { const rooms: LiveRoomInfo[] = [] for (let i = 0; i < pages; i++) { @@ -66,9 +99,15 @@ export default class BilbiliApi { return rooms } + /** + * 获取一页的直播房间。 + * @param page - 要获取的页码。 + * @returns 一个解析为直播房间信息数组的Promise。 + * @throws 如果无法获取直播房间列表,则抛出错误。 + */ async getLiveRooms(page: number = 1): Promise { const data = await this.fetch(`/xlive/web-interface/v1/second/getList?platform=web&parent_area_id=9&area_id=0&sort_type=online&page=${page}`) - if (data.code !== 0) throw new Error(`failed to fetch bilibili live room list: ${data.message}`) + if (data.code !== 0) throw new Error(`获取bilibili直播房间列表失败:${data.message}`) return data.data.list as LiveRoomInfo[] } diff --git a/tests/helpers/bilibili-page.ts b/tests/helpers/bilibili-page.ts index 44392f01..be5b135f 100644 --- a/tests/helpers/bilibili-page.ts +++ b/tests/helpers/bilibili-page.ts @@ -8,18 +8,34 @@ import type { PageListener } from "./listeners/type"; import logger from "./logger"; import { type PageFrame } from "./page-frame"; +/** + * Bilibili页面类,实现了AsyncDisposable接口。 + */ export class BilibiliPage implements AsyncDisposable { + /** + * 页面监听器集合。 + */ private readonly listeners: Set = new Set([ new DismissLoginDialogListener(), ]) + /** + * 构造函数。 + * @param page 页面对象。 + * @param api Bilibili API对象。 + * @param info 直播间信息。 + */ constructor( public readonly page: Page, private readonly api: BilbiliApi, public info?: LiveRoomInfo ) {} + /** + * 进入直播间。 + * @param info 直播间信息。 + */ async enterToRoom(info?: LiveRoomInfo): Promise { if (!info && !this.info) throw new Error('no room to enter') if (info) { @@ -31,19 +47,36 @@ export class BilibiliPage implements AsyncDisposable { await this.startListeners() } + /** + * 检查是否不支持。 + * @returns 如果不支持返回true,否则返回false。 + */ async checkIfNotSupport(): Promise { const p = await this.getContentLocator() return await p.getByText('您使用的浏览器版本偏低').isVisible() } + /** + * 检查直播间状态。 + * @param status 直播间状态,可选值为'online'或'offline'。 + * @returns 如果直播间状态与给定状态相同返回true,否则返回false。 + */ async isStatus(status: 'online' | 'offline'): Promise { return await this.api.getRoomStatus(this.info.roomid) === status } + /** + * 检查是否为主题页面。 + * @returns 如果是主题页面返回true,否则返回false。 + */ async isThemePage(): Promise { return this.page.frame({ url: /^(http)s:\/\/live\.bilibili\.com\/blanc\/(\d+)\?liteVersion=true&live_from=(\d+)/ }) !== null } + /** + * 获取内容定位器。 + * @returns 页面或帧对象。 + */ async getContentLocator(): Promise { const isTheme = await this.isThemePage() if (isTheme) { @@ -55,6 +88,10 @@ export class BilibiliPage implements AsyncDisposable { return this.page } + /** + * 模拟发送弹幕。 + * @param danmaku 弹幕内容。 + */ async sendDanmaku(danmaku: string): Promise { const f = await this.getContentLocator() await sendFakeBLiveMessage(f, 'DANMU_MSG', { @@ -104,6 +141,7 @@ export class BilibiliPage implements AsyncDisposable { undefined, undefined, undefined, + undefined, undefined ], dm_v2: "" @@ -111,6 +149,12 @@ export class BilibiliPage implements AsyncDisposable { await this.page.waitForTimeout(1000) } + /** + * 模拟发送醒目留言。 + * @param user 用户名。 + * @param price 价格。 + * @param message 消息内容。 + */ async sendSuperChat(user: string, price: number, message: string): Promise { const f = await this.getContentLocator() await sendFakeBLiveMessage(f, 'SUPER_CHAT_MESSAGE', { @@ -134,6 +178,10 @@ export class BilibiliPage implements AsyncDisposable { await this.page.waitForTimeout(1000) } + /** + * 重新加载并获取内容定位器。 + * @returns 页面或帧对象。 + */ async reloadAndGetLocator(): Promise { await this.page.reload({ waitUntil: 'domcontentloaded' }) await this.page.waitForTimeout(3000) @@ -141,15 +189,24 @@ export class BilibiliPage implements AsyncDisposable { return this.getContentLocator() } + /** + * 启动监听器。 + */ async startListeners(): Promise { const p = await this.getContentLocator() this.listeners.forEach(listener => listener.start(p)) } + /** + * 关闭页面。 + */ async close() { await this[Symbol.asyncDispose]() } + /** + * 释放资源。 + */ async [Symbol.asyncDispose](): Promise { logger.debug('disposing bilibili page') this.listeners.forEach(listener => listener.stop()) diff --git a/tests/helpers/listeners/dismiss-login-dialog-listener.ts b/tests/helpers/listeners/dismiss-login-dialog-listener.ts index a39db963..5a710f2c 100644 --- a/tests/helpers/listeners/dismiss-login-dialog-listener.ts +++ b/tests/helpers/listeners/dismiss-login-dialog-listener.ts @@ -1,8 +1,7 @@ import type { PageFrame } from "../page-frame"; import { PageListener } from "./type"; - - +// 监听器:关闭登录对话框 class DismissLoginDialogListener extends PageListener { constructor() { diff --git a/tests/helpers/listeners/type.ts b/tests/helpers/listeners/type.ts index 7b38e5b6..db905375 100644 --- a/tests/helpers/listeners/type.ts +++ b/tests/helpers/listeners/type.ts @@ -2,43 +2,62 @@ import { env } from "@tests/utils/misc"; import { createLogger, type Logger } from "../logger"; import { isClosed, type PageFrame } from "../page-frame"; +/** + * 页面监听器的抽象基类。 + */ export abstract class PageListener { protected readonly logger: Logger protected instance: NodeJS.Timeout | null = null + /** + * 创建一个页面监听器实例。 + * @param name 监听器的名称。 + * @param interval 监听器执行的时间间隔,默认为 1000 毫秒。 + */ constructor(name: string, private readonly interval: number = 1000) { this.logger = createLogger(name, env('CI', Boolean)) } + /** + * 启动监听器并开始监听页面内容。 + * @param content 页面的内容。 + */ start(content: PageFrame): void { if (this.instance) { - this.logger.info('clear last interval') + this.logger.info('清除上一个时间间隔') clearInterval(this.instance) } if (content === null) { - this.logger.warn('content is null, cannot start listener') + this.logger.warn('内容为空,无法启动监听器') return } this.instance = setInterval(() => { if (isClosed(content)) { - this.logger.info('frame/page is closed, listener aborted') + this.logger.info('帧/页面已关闭,监听器中止') clearInterval(this.instance) return } this.run(content) - .catch(err => this.logger.warn('listener error: ', err)) + .catch(err => this.logger.warn('监听器错误: ', err)) }, this.interval) } + /** + * 在子类中实现的方法,用于执行监听器的逻辑。 + * @param content 页面的内容。 + */ protected abstract run(content: PageFrame): Promise + /** + * 停止监听器。 + */ stop(): void { if (this.instance) { clearInterval(this.instance) diff --git a/tests/helpers/page-frame.ts b/tests/helpers/page-frame.ts index 5fd50514..48e6ee55 100644 --- a/tests/helpers/page-frame.ts +++ b/tests/helpers/page-frame.ts @@ -2,14 +2,29 @@ import type { Frame, Page } from "@playwright/test"; export type PageFrame = Page | Frame +/** + * Checks if a page frame is closed or detached. + * @param page - The page frame to check. + * @returns A boolean indicating whether the page is closed or detached. + */ export function isClosed(page: PageFrame): boolean { return ('isClosed' in page && page.isClosed()) || ('isDetached' in page && page.isDetached()) } +/** + * Checks if the given object is an instance of `PageFrame`. + * @param page - The object to be checked. + * @returns `true` if the object is an instance of `Page`, `false` otherwise. + */ export function isPage(page: PageFrame): page is Page { return 'isClosed' in page } +/** + * Checks if the given page is a frame. + * @param page - The page to check. + * @returns True if the page is a frame, false otherwise. + */ export function isFrame(page: PageFrame): page is Frame { return 'isDetached' in page } \ No newline at end of file diff --git a/tests/helpers/room-finder.ts b/tests/helpers/room-finder.ts index 0ba9b550..a3b82d24 100644 --- a/tests/helpers/room-finder.ts +++ b/tests/helpers/room-finder.ts @@ -7,11 +7,18 @@ import logger from "./logger"; export type RoomTypeChecker = (page: BilibiliPage) => Promise +/** + * 房间类型查找器 + */ export default class RoomTypeFinder { - private readonly checkers: { type: string, checker: RoomTypeChecker }[] = [] private readonly cached: Record = {} + /** + * 创建一个房间类型查找器实例 + * @param browser 浏览器实例 + * @param api Bilibili API 实例 + */ constructor( private readonly browser: Browser, private readonly api: BilbiliApi @@ -21,29 +28,49 @@ export default class RoomTypeFinder { this.registerRoomType('theme', page => page.isThemePage()) } + /** + * 注册房间类型 + * @param name 类型名称 + * @param checker 类型检查器 + */ registerRoomType(name: string, checker: RoomTypeChecker) { if (this.checkers.find(c => c.type === name)) { - console.warn(`Room checker type ${name} already registered.`) + console.warn(`房间类型 ${name} 已经注册。`) return } this.checkers.push({ type: name, checker }) } + /** + * 获取房间类型 + * @param page Bilibili 页面 + * @returns 房间类型 + */ async getRoomType(page: BilibiliPage): Promise { for (const { type, checker } of this.checkers) { if (await checker(page)) { - logger.info(`room ${page.info.roomid} type is: ${type}`) + logger.info(`房间 ${page.info.roomid} 的类型是: ${type}`) return type } } - logger.info(`room ${page.info.roomid} type is: normal`) + logger.info(`房间 ${page.info.roomid} 的类型是: 普通`) return 'normal' } + /** + * 检查缓存是否存在 + * @param check 检查项 + * @returns 如果缓存存在则返回 true,否则返回 false + */ isCached(check: string): boolean { return !!this.cached[check] } + /** + * 验证缓存是否有效 + * @param info 直播间信息 + * @param check 检查项 + */ async validateCache(info: LiveRoomInfo, check: string): Promise { if (!this.cached[check]) return const p = await this.browser.newPage() @@ -51,11 +78,17 @@ export default class RoomTypeFinder { await page.enterToRoom(info) const roomType = await this.getRoomType(page) if (roomType === check) return - console.info(`${page.info.roomid} 的緩存策略失效,已清除其 ${check} 的緩存。`) + console.info(`${page.info.roomid} 的缓存策略失效,已清除其 ${check} 的缓存。`) delete this.cached[check] await this.deleteToFileCache(check) } + /** + * 查找房间类型 + * @param check 检查项 + * @param rooms 直播间生成器 + * @returns 如果找到符合条件的直播间则返回直播间信息,否则返回 null + */ async findRoomType(check: string, rooms: Generator): Promise { const p = await this.browser.newPage() await using page = new BilibiliPage(p, this.api) @@ -63,61 +96,86 @@ export default class RoomTypeFinder { await page.enterToRoom(room) const roomType = await this.getRoomType(page) if (roomType === check) { - logger.info(`成功搜索到屬於 ${check} 類型的直播房間: ${page.info.roomid}`) + logger.info(`成功搜索到属于 ${check} 类型的直播房间: ${page.info.roomid}`) return room } } - logger.info(`找不到屬於 ${check} 類型的直播房間`) + logger.info(`找不到属于 ${check} 类型的直播房间`) return null } + /** + * 从缓存中查找房间类型 + * @param check 检查项 + * @returns 如果缓存中存在符合条件的直播间则返回直播间信息,否则返回 undefined + */ findRoomTypeFromCache(check: string): LiveRoomInfo | undefined { - logger.debug(`${check} 在緩存的直播間: `, this.cached[check]) + logger.debug(`${check} 在缓存的直播间: `, this.cached[check]) return this.cached[check] } + /** + * 使用缓存查找房间类型 + * @param check 检查项 + * @param rooms 直播间生成器 + * @param saveFile 是否保存到文件缓存 + * @returns 如果找到符合条件的直播间则返回直播间信息,否则返回 null + */ async findRoomTypeWithCache(check: string, rooms: Generator, saveFile: boolean = false): Promise { if (this.cached[check]) { - logger.debug(`已從緩存中找到屬於 ${check} 類型的直播房間: ${this.cached[check].roomid}`) + logger.debug(`已从缓存中找到属于 ${check} 类型的直播房间: ${this.cached[check].roomid}`) return this.cached[check] } - logger.debug(`正在搜索屬於 ${check} 類型的直播房間...`) + logger.debug(`正在搜索属于 ${check} 类型的直播房间...`) const info = await this.findRoomType(check, rooms) this.cached[check] = info if (saveFile) { await this.writeToFileCache(check, info) - logger.info(`已成功緩存屬於 ${check} 類型的直播房間: ${info?.roomid ?? '無'}`) + logger.info(`已成功缓存属于 ${check} 类型的直播房间: ${info?.roomid ?? '无'}`) } return info } + /** + * 删除文件缓存 + * @param check 检查项 + */ async deleteToFileCache(check: string): Promise { if (existsSync(`room.${check}.cache.json`)) { await fs.unlink(`room.${check}.cache.json`) } } - + /** + * 将房间信息写入文件缓存 + * @param check 检查项 + * @param room 直播间信息 + */ async writeToFileCache(check: string, room: LiveRoomInfo | null): Promise { await fs.writeFile(`room.${check}.cache.json`, room ? JSON.stringify(room) : JSON.stringify({})) } + /** + * 从文件缓存中加载房间信息 + * @param check 检查项 + * @returns 如果缓存文件存在则返回房间信息,如果缓存文件中无直播间则返回 'none',如果读取缓存文件出错则返回 'null' + */ async loadFromFileCache(check: string): Promise { try { if (!existsSync(`room.${check}.cache.json`)) { - logger.debug(`找不到 ${check} 的緩存檔案`) + logger.debug(`找不到 ${check} 的缓存文件`) return 'null' } const data = await fs.readFile(`room.${check}.cache.json`) const room = JSON.parse(data.toString()) as LiveRoomInfo if (room.roomid === undefined) { - logger.debug(`緩存檔案 ${check} 返回無直播間`) + logger.debug(`缓存文件 ${check} 返回无直播间`) return 'none' } this.cached[check] = room return room } catch (error) { - logger.debug(`讀取 ${check} 的緩存檔案時錯誤: `, error) + logger.debug(`读取 ${check} 的缓存文件时错误: `, error) return 'null' } } diff --git a/tests/utils/bilibili.ts b/tests/utils/bilibili.ts index 72bb33d5..dbf93f94 100644 --- a/tests/utils/bilibili.ts +++ b/tests/utils/bilibili.ts @@ -1,6 +1,13 @@ import logger from "@tests/helpers/logger"; import type { PageFrame } from "@tests/helpers/page-frame"; +/** + * Sends a fake BLive message to the specified page frame. + * @param content The page frame to send the message to. + * @param cmd The command to send. + * @param command The command object to send. + * @returns A promise that resolves when the message is sent. + */ export function sendFakeBLiveMessage(content: PageFrame, cmd: string, command: object) { logger.debug('sending blive fake message into: ', cmd, content.url()) return content.evaluate(([cmd, command]) => { @@ -16,6 +23,12 @@ export function sendFakeBLiveMessage(content: PageFrame, cmd: string, command: o }, [cmd, command]) } +/** + * Receives a single blive message from a page frame. + * @param content - The page frame to receive the message from. + * @param cmd - The command to filter the message by (optional). + * @returns A promise that resolves with the received message. + */ export function receiveOneBLiveMessage(content: PageFrame, cmd: string = ''): Promise { logger.debug('waiting for blive fake message: ', cmd, content.url()) return content.evaluate(([cmd]) => { diff --git a/tests/utils/file.ts b/tests/utils/file.ts index 0ea000f3..a6816562 100644 --- a/tests/utils/file.ts +++ b/tests/utils/file.ts @@ -1,20 +1,28 @@ import { glob, type GlobOptions as IOptions } from 'glob' import type { Readable } from 'stream' - export type IModule = { name: string, file: string, module: any } -// Imporntant!!! Only Node.js can use this function. +/** + * Retrieves an array of TypeScript file paths from the specified directory path. + * @param dirPath - The directory path to search for TypeScript files. + * @param options - Optional configuration options for the glob pattern matching. + * @returns An array of TypeScript file paths. + */ export function getTSFiles(dirPath: string, options?: IOptions): string[] { return glob.sync(`${dirPath}/**/*.{ts,tsx}`, options) as string[] } - -// Imporntant!!! Only Node.js can use this function. +/** + * Retrieves a stream of modules from a directory path. + * @param dirPath - The directory path to search for modules. + * @param options - Optional configuration options. + * @returns A generator that yields promises of modules. + */ export function* getModuleStream(dirPath: string, options: IOptions = { ignore: '**/index.ts' }): Generator, void, any> { for (const file of getTSFiles(dirPath, options)) { const name = file.split('/').pop().split('.')[0] @@ -22,9 +30,10 @@ export function* getModuleStream(dirPath: string, options: IOptions = { ignore: } } - - -// Imporntant!!! Only Node.js can use this function. +/** + * Retrieves a generator that yields module information synchronously. + * @param dirPath - The directory path to search for TypeScript files. + * @param options - The options for filtering files. Default value is { ignore: '**/ export function* getModuleStreamSync(dirPath: string, options: IOptions = { ignore: '**/index.ts' }): Generator { for (const file of getTSFiles(dirPath, options)) { const name = file.split('/').pop().split('.')[0] @@ -32,7 +41,11 @@ export function* getModuleStreamSync(dirPath: string, options: IOptions = { igno } } - +/** + * Reads the text from a Readable stream and returns it as a string. + * @param readable The Readable stream to read from. + * @returns A promise that resolves with the text from the Readable stream. + */ export async function readText(readable: Readable): Promise { return new Promise((res, rej) => { let data = '' diff --git a/tests/utils/misc.ts b/tests/utils/misc.ts index fc808d15..5640a292 100644 --- a/tests/utils/misc.ts +++ b/tests/utils/misc.ts @@ -1,12 +1,26 @@ +/** + * Returns a random item from the given array. + * @param items - The array of items. + * @returns A random item from the array. + */ export function random(items: T[]): T { return items[Math.floor(Math.random() * items.length)] } +/** + * A class that provides different strategies for generating values. + */ export class Strategy { + /** + * Generates a random sequence of values from an array. + * @param items - The array of items to generate values from. + * @param limit - The maximum number of values to generate (default: items.length). + * @returns A generator that yields random values from the array. + */ static *random(items: T[], limit: number = items.length): Generator { const remains = [...items] let count = 0 @@ -17,6 +31,12 @@ export class Strategy { } } + /** + * Generates a serial sequence of values from an array. + * @param items - The array of items to generate values from. + * @param limit - The maximum number of values to generate (default: items.length). + * @returns A generator that yields serial values from the array. + */ static *serial(items: T[], limit: number = items.length): Generator { for (const item of items.toSpliced(limit)) { yield item @@ -26,35 +46,72 @@ export class Strategy { } +/** + * Retrieves the value of an environment variable and converts it to the specified type. + * @param name - The name of the environment variable. + * @param convert - A function that converts the environment variable value to the desired type. + * @returns The converted value of the environment variable, or `undefined` if the variable is not defined. + * @template T - The type to convert the environment variable value to. + */ export function env(name: string, convert: (s: string) => T): T { const value = process.env[name] if (value === undefined) return undefined return convert(value) } +/** + * Retrieves the string value of the specified environment variable. + * @param name - The name of the environment variable. + * @returns The string value of the environment variable. + */ export function envStr(name: string): string { return env(name, String) } +/** + * Retrieves the integer value of the specified environment variable. + * @param name - The name of the environment variable. + * @returns The integer value of the environment variable. + */ export function envInt(name: string): number { return env(name, Number) } +/** + * Retrieves the boolean value of the specified environment variable. + * @param name - The name of the environment variable. + * @returns The boolean value of the environment variable. + */ export function envBool(name: string): boolean { return env(name, Boolean) } +/** + * Generates a random number with the specified length. + * @param length The length of the random number. Defaults to 20. + * @returns The generated random number. + */ export function randomNumber(length: number = 20): number { return Math.round(Math.random() * (10 ** length)) } +/** + * Creates a deferred execution object. + * @param run - The function to be executed when the deferred object is disposed. + * @returns An object with a `dispose` method that can be used to execute the deferred function. + */ export function defer(run: () => void): { [Symbol.dispose]: () => void } { return { [Symbol.dispose]: run } } +/** + * Creates a deferred async function. + * @param run - The async function to be executed. + * @returns An object with an asyncDispose method that can be used to dispose the async function. + */ export function deferAsync(run: () => Promise): { [Symbol.asyncDispose]: () => Promise } { return { [Symbol.asyncDispose]: run diff --git a/tests/utils/playwright.ts b/tests/utils/playwright.ts index da0a8d22..e0b0db76 100644 --- a/tests/utils/playwright.ts +++ b/tests/utils/playwright.ts @@ -1,5 +1,12 @@ import type { Locator } from "@playwright/test" +/** + * Finds the first locator in the given array that satisfies the provided predicate. + * + * @param locators - The array of locators to search through. + * @param predicate - The predicate function to test each locator. + * @returns A Promise that resolves to the first locator that satisfies the predicate, or undefined if no locator is found. + */ export async function findLocatorAsync(locators: Locator[], predicate: (locator: Locator) => Promise): Promise { for (const locator of locators) { if (await predicate(locator)) @@ -8,6 +15,13 @@ export async function findLocatorAsync(locators: Locator[], predicate: (locator: return undefined } +/** + * Retrieves a list of super chat elements within a given section. + * + * @param section - The locator for the section containing the super chat elements. + * @param options - Optional filtering options for the super chat elements. + * @returns A promise that resolves to an array of super chat locators. + */ export async function getSuperChatList(section: Locator, options?: { has?: Locator; hasNot?: Locator; From 99d4c39394f3b1b2ab46a0639e3569aa3197f528 Mon Sep 17 00:00:00 2001 From: eric2788 Date: Sat, 9 Mar 2024 21:03:42 +0800 Subject: [PATCH 5/6] finished docs and updated dependencies --- CONTRIBUTING.md | 3 + docs/adapters.md | 21 ++ docs/background.md | 415 +++++++++++++++++++++ docs/database.md | 64 ++++ docs/features.md | 10 +- docs/pages.md | 9 +- docs/settings.md | 10 +- package.json | 12 +- pnpm-lock.yaml | 248 ++++++------ src/background/messages/fetch-developer.ts | 1 - src/contents/index/mounter.tsx | 5 +- 11 files changed, 650 insertions(+), 148 deletions(-) create mode 100644 docs/adapters.md create mode 100644 docs/database.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9577a031..c8f54841 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,6 +116,9 @@ tests/ 目录如下: ``` docs/ +├── adapters.md # 适配器的模块 +├── background.md # 后台脚本的模块 +├── database.md # 数据库的结构定义 ├── features.md # 新增功能模块 ├── pages.md # 新增扩展页面 └── settings.md # 新增设定区块 diff --git a/docs/adapters.md b/docs/adapters.md new file mode 100644 index 00000000..83db151c --- /dev/null +++ b/docs/adapters.md @@ -0,0 +1,21 @@ +# 适配器 + +用于连接或转换不同的B站WS数据源以供内容脚本使用。 + +## 适配器列表 + +目前只有 `WebSocket` 和 `Dom` 两种适配器。 + +WebSocket 是目前最常用的适配器,它直接挂钩网页上的 WebSocket 客户端,以获取B站WS数据。 + +Dom 适配器则是通过解析网页上的DOM元素,以获取特定数据。 + +WebSocket 相比Dom适配器更加稳定和更加泛用。但万一 WebSocket 无法使用,Dom 适配器则是一个备选方案。 + +如果你需要新增一个适配器,你需要留意以下内容: + +- 你需要用到 [`utils/messaging.ts`](/src/utils/messaging.ts) 中的 `sendBLiveMessage` 方法,以发送数据到内容脚本。 + +- 新增方式可以在 `adapters/` 目录下创建文件,然后在 `adapters/index.ts` 中进行注册。 + +- 基于目前以WS为主的情况,你需要发送以 WS 结构为準的数据。 diff --git a/docs/background.md b/docs/background.md index e69de29b..0bdbd632 100644 --- a/docs/background.md +++ b/docs/background.md @@ -0,0 +1,415 @@ +## 后台脚本 + +> 后台脚本的架构基于 Plasmo 中的 [messaging API](https://docs.plasmo.com/framework/messaging) 。 + +后台脚本的架构如下: + +``` +background/ +├── context-menus/ # 右键菜单的定义和处理。 +├── forwards/ # 用于内容脚本和后台脚本之间的数据转换。 +├── functions/ # 用于放在网页上内嵌运行的函数。 +├── messages/ # 基于 Plasmo,从内容脚本到后台脚本的指令传输。 +├── scripts/ # 用于放在网页上注入运行的脚本。 +├── forwards.ts # 用于引入和存放所有的指令传输。 +├── index.ts # 后台脚本的入口文件。 +├── messages.ts # 用于引入和存放所有的消息传输。 +├── ports.ts # 基于 Plasmo 的 Ports API, 目前暂不使用 +└── update-listener.ts # 用于监听扩展更新的事件。 +``` + +## messages/ + +此目录用于存放基于 Plasmo, 从内容脚本到后台脚本的消息传输。 +有关其新增方式,请参阅 [Plasmo 文档](https://docs.plasmo.com/framework/messaging#message-flow)。 + +```ts +// 定义输入类型 +export type RequestBody = { + url?: string + tab?: string +} + +// 定义输出类型为可选 +const handler: PlasmoMessaging.MessageHandler = async (req, res) => { + const { url, tab } = req.body + const result = await chrome.tabs.create({ url: tab ? chrome.runtime.getURL(`/tabs/${tab}.html`) : url }) + res.send(result) // 定义输出类型后,必须用 res.send() 方法进行返回 +} + + +export default handler +``` + +完成后,你需要到 `background/messages.ts` 中新增你的消息处理器: + +```ts +import * as addBlackList from './messages/add-black-list' +// ... +import * as openTab from './messages/open-tab' // 新增的消息处理器 + +// ... + +const messagers = { + 'notify': notify, + // ... + 'open-tab': openTab, // 新增的消息处理器 +} +``` + +### 使用 + +在内容脚本中,你可以使用 `sendMessager` 方法发送消息: + +```ts +await sendMessager('open-tab', { tab: 'hello-world' }) +``` + +在后台脚本中,你可以使用 `sendInternal` 方法发送消息: + +```ts +await sendInternal('open-tab', { tab: 'hello-world' }) +``` + +> 以上的函数都有类型定义,你可以在编辑器中查看其使用方法。 + + +## context-menus/ + +在 `background/context-menus/` 目录下定义右键菜单,例如 `background/context-menus/add-black-list`。 + +```ts +export const properties: chrome.contextMenus.CreateProperties = { + id: 'add-black-list', + title: '添加到黑名单', + documentUrlPatterns: ['https://live.bilibili.com/*'], + contexts: ['page'], + enabled: true +} + + +export default async function (info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab): Promise { + const url = new URL(info.pageUrl) + const roomId = getRoomId(url.pathname) + + if (!roomId) { + console.warn(`unknown room id (${url.pathname})`) + // sendInternal 基于 messaging 发送指令传输(后台对后台) + return await sendInternal('notify', { + title: '添加失败', + message: `未知的直播间: ${url.pathname}` + }) + } + + await sendInternal('add-black-list', { roomId, sourceUrl: tab.url }) + +} +``` + +完成后,你需要到 `background/context-menus/index.ts` 中新增你的右键菜单处理器: + +```ts +import * as blacklist from './add-black-list' // 新增的右键菜单处理器 + +const { contextMenus } = chrome + +const menus = [ + blacklist // 新增的右键菜单处理器 +] +// ... +``` + +## forwards/ + +`forwards/` 主要用于内容脚本和后台脚本之间的数据转换。在本扩展使用例子有: + +- 把内容脚本的弹幕数据发送到扩展页面 +- 把内容脚本的同传字幕发送到扩展页面 +- 扩展页面的重启指令发送到内容脚本 + +例子如下: +```ts +// 输入数据结构 +export type ResponseBody = { + uname: string + text: string + color: string + pos: 'scroll' | 'top' | 'bottom' + room: string +} + +// 输出数据结构 +export type ForwardBody = { + uname: string + text: string + color: number + position: number + room: string +} + +// 支援 await 函数 +const handler: ForwardHandler = (req) => { + + let pos: 'scroll' | 'top' | 'bottom' = 'scroll' + switch (req.body.position) { + case 5: + pos = 'top' + break + case 4: + pos = 'bottom' + break + } + + return { + ...req, + body: { + room: req.body.room, + uname: req.body.uname, + text: req.body.text, + color: `#${req.body.color.toString(16)}`, + pos, + } + } +} + +export default handler +``` + +完成后,你需要到 `background/forwards.ts` 中新增你的数据转换处理器: + +```ts +import * as bliveData from './forwards/blive-data' +import * as command from './forwards/command' +import * as jimaku from './forwards/jimaku' +import * as redirect from './forwards/redirect' +import * as danmaku from './forwards/danmaku' // 新增的数据转换处理器 + +// ... + +const forwards = { + 'jimaku': jimaku, + 'command': command, + 'redirect': redirect, + 'blive-data': bliveData, + 'danmaku': danmaku // 新增的数据转换处理器 +} +``` + +### 使用 + +使用方式有以下几种,你可以在 [`background/forwards.ts`](/src/background/forwards.ts) 或 [`hooks/forwarder.ts`](/src/hooks/forwarder.ts) 中查看更多的使用方式。 + +- 使用 `getFowarder` + + +```ts +// contents/index/index.tsx + + // 参数1: 频道 + // 参数2: 来源 + const forwarder = getForwarder('command', 'content-script') + + const app = createApp(roomId, { rootContainer }, info) + + const start = withProcessingFlag(app.start) + const stop = withProcessingFlag(app.stop) + + // addhandler 为接收消息处理 + removeHandler = forwarder.addHandler(async data => { + if (data.command === 'stop') { + await stop() + } else if (data.command === 'restart') { + await stop() + await start() + } + }) + // start the app + await start() +``` + +- 使用 `sendForward` + +```ts +// features/jimaku/components/JimakuCaptureLayer.tsx +// ... +const jimakuBlock = { + date: datetime, + text: jimaku, + uid: data.info[2][0], + uname: data.info[2][1], + hash: randomString() + Date.now() + data.info[0][5], +} +push(jimakuBlock) +if (jimakuPopupWindow) { + // 参数1: 目标 + // 参数2: 频道 + // 参数3: 传入数据 + sendForward('pages', 'jimaku', { date: datetime, text: jimaku, room: info.room }) +} +// ... +``` + +- 使用 `useForwarder` (仅限 React 组件内) + + +> 跟 `getForwarder` 不同之处在于,`useForwarder` 是一个 React Hook,它会在组件卸载时自动清理消息处理器。 + +```ts +// tabs/jimaku.tsx + + const forwarder = useForwarder('jimaku', 'pages') + + useEffect(() => { + if (bottomRef.current) { + window.scrollTo(0, document.body.scrollHeight) + } + }, [messages]) + + useEffect(() => { + if (roomId) { + setTitle(roomTitle ?? `B站直播间 ${roomId} 的同传弹幕视窗`) + forwarder.addHandler((message) => { + if (message.room !== roomId) return + setMessages(messages => [...messages, message]) + }) + } else { + alert('未知房间Id, 此同传弹幕视窗不会运行。') + } + }, []); +``` + +## functions/ + +`functions/` 主要用于放在网页上内嵌运行的函数。 + +> 此原理是基于 manifest v3 的 `chrome.scripting` API。 + +```ts +function getWindowVariable(key: string): any { + const nested = key.split('.') + if (nested.length === 1) return window[key] + let current = window + for (const k of nested) { + current = current[k] + } + return current +} + +export default getWindowVariable +``` + +完成后,你需要到 `background/functions/index.ts` 中新增你的函数: + +```ts +import boostWebSocketHook from './boostWebsocketHook' +import getBLiveCachedData from './getBLiveCachedData' +import getWindowVariable from './getWindowVariable' // 新增的函数 + +// ... + +const functions = { + getWindowVariable, // 新增的函数 + getBLiveCachedData, + boostWebSocketHook +} + + +export default functions +``` + +### 使用 + +你可以借助 [`utils/injector.ts`](/src/utils/injector.ts) 中的 `injectFunction` 方法注入你的函数到网页上。 + +```ts +const chrome = await injectFunction('getWindowVariable', 'chrome') +``` + +## scripts/ + +`scripts/` 主要用于放在网页上注入运行的脚本。 +与 `functions/` 不同之处在于,`scripts/` 注入的是一个完整的脚本文件,而 `functions/` 只注入一个函数。 + +> 此原理同样是基于 manifest v3 的 `chrome.scripting` API。 + +现在比起新增一个文件,你需要新增一个文件夹,例如 `scripts/alert-hello-world`。 + +文件夹内的文件结构如下: + +``` +alert-hello-world/ +├── function.ts +├── index.ts +└── script.ts +``` + +`function.ts` 用于定义你的函数: + +```ts +async function alertHelloWorld(): Promise { + await sleep(5000) + alert('Hello, World!') +} +export default alertHelloWorld +``` + +`script.ts` 用于注入你的脚本: + +```ts +import { injectFuncAsListener } from '~utils/event' + +import alertHelloWorld from './function' + +injectFuncAsListener(alertHelloWorld) +``` + +`index.ts` 用于引入两者: + +```ts +// 注意此处,你需要以 url 形式注入你的脚本 +import url from 'url:./script.ts' + +import prototype from './function' + +export default { url, prototype } +``` + +最后,你需要到 `background/scripts/index.ts` 中新增你的脚本: + +```ts +import clearIndexedDbTable from './clearIndexedDbTable' +import alertHelloWorld from './alert-hello-world' // 新增的脚本 + +// ... + +const scripts = { + clearIndexedDbTable, + alertHelloWorld // 新增的脚本 +} + +export default scripts +``` + +> 注意: 目前 development 环境下首次运行脚本时会出现报错,目前原因未知。 + +### 使用 + +你可以借助 [`utils/injector.ts`](/src/utils/injector.ts) 中的 `injectScript` 方法注入你的脚本到网页上。 + +```ts +await injectScript('alert-hello-world') +``` + + +## 进阶开发 + +你可以参考以下的源码以进行更进阶的后台开发: + +- [现成的后台脚本参考](/src/background/) +- [自定义消息传输](/src/background/messages/) +- [自定义右键菜单](/src/background/context-menus/) +- [自定义数据转换](/src/background/forwards/) +- [自定义函数](/src/background/functions/) +- [自定义脚本](/src/background/scripts/) +- [自定义事件注入](/src/background/events/) +- [辅助函数](/src/utils/) diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 00000000..5130710f --- /dev/null +++ b/docs/database.md @@ -0,0 +1,64 @@ +# 数据库 + +本扩展的数据库采用 IndexedDB 来存储数据。IndexedDB 是一个浏览器端的数据库,它提供了一个对象存储的解决方案,可以存储大量的结构化数据。 + +> 关于 IndexedDB,本扩展使用了 [Dexie.js](https://dexie.org/) 这个库来进行 IndexedDB 的操作。 + +## 新增新的数据库 + +在 `src/database/tables` 目录下新增一个新的数据库,例如 `src/database/person.d.ts`。 + +```ts +import { Table } from 'dexie' +import { CommonSchema } from '~database' + +declare module '~database' { + interface IndexedDatabase { + persons: Table + } +} + +interface Person extends CommonSchema { + name: string + age: number + nickname?: string +} +``` + +完成后,你需要到 `src/database/migration.ts` 建立新版本迁移,然后添加你的数据库创建。 + +```ts +import type Dexie from "dexie" +import { commonSchema } from '~database' + +export default function (db: Dexie) { + // version 1 + db.version(1).stores({ + superchats: commonSchema + "text, scId, backgroundColor, backgroundImage, backgroundHeaderColor, userIcon, nameColor, uid, uname, price, message, hash, timestamp", + jimakus: commonSchema + "text, uid, uname, hash" + }) + + db.version(2).stores({ + persons: commonSchema + "name, age, nickname" + }) +} +``` + +> 关于 Migrate 使用方式,请参考 [Dexie.js 的文档](https://dexie.org/docs/Tutorial/Understanding-the-basics#changing-a-few-tables-only) + +## 使用 + +完成后,使用只需要使用 `src/database` 的 `db` 即可。 + +```ts +import { db } from '~database' + +// 执行数据库操作 +async function addPerson(){ + await db.persons.add({ name: 'world', age: 99 }) +} + +``` + +> 有关使用方式,还是请参考 [Dexie.js 的文档](https://dexie.org/docs/Collection/Collection) + diff --git a/docs/features.md b/docs/features.md index 05bef937..df49fe3c 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,4 +1,4 @@ -## 新增功能 +# 新增功能 > 本扩展基于 [Plasmo CSUI](https://docs.plasmo.com/framework/content-scripts-ui) 进行前端渲染。 @@ -15,7 +15,7 @@ src/ `src/settings/features/`: 此目录用于存放功能的设定区块,为了让用户能够切换功能的开关,此处也必须添加。 -### 新增内容脚本 +## 新增内容脚本 在 `src/features/` 目录下新增一个新的内容脚本,例如 `src/features/hello-world.tsx`。 @@ -98,7 +98,7 @@ const features = { ``` -### 新增功能设定区块 +## 新增功能设定区块 在 `src/settings/features/` 目录下新增一个新的功能设定区块,例如 `src/settings/features/hello-world.tsx`: @@ -144,7 +144,7 @@ function SuperchatFeatureSettings({state, useHandler}: StateProxy 有关 `StateProxy` 的使用,请参考 [自定义Hooks](/src/hooks/binding.ts)。 +> 有关 `StateProxy` 的使用,请参考 [自定义Hooks](/src/hooks/binding.ts) 或 [`docs/settings.md`](/docs/settings.md)。 同样,假设你的功能不需要功能设定区块,你可以这样定义: @@ -193,7 +193,7 @@ const featureSettings = { 以上两个步骤完成后,你的功能就已经基本完成了。 -### 进阶开发 +## 进阶开发 你可以参考以下的源码以进行更进阶的功能开发: - [现成功能参考](/src/features/) diff --git a/docs/pages.md b/docs/pages.md index b734bc0f..c1cb4b3b 100644 --- a/docs/pages.md +++ b/docs/pages.md @@ -1,4 +1,4 @@ -## 新增页面 +# 新增页面 > 本扩展基于 [Plasmo Extension Page](https://docs.plasmo.com/framework/ext-pages) 进行页面渲染。 @@ -9,7 +9,7 @@ src/ tabs/ <- 扩展页面 ``` -### 创建一个新的扩展页面 +## 创建一个新的扩展页面 在 `src/tabs/` 目录下新增一个新的扩展页面,例如 `src/tabs/hello-world.tsx`。 @@ -28,7 +28,7 @@ export default App 完成后,扩展在建置时会自动生成 hello-world.html 文件,且会自动添加到扩展的 `manifest.json` 中。 -### 跳转到扩展页面 +## 跳转到扩展页面 如要在其他位置跳转到扩展页面,有以下两种方式: @@ -54,6 +54,7 @@ function HelloWorldButton(): JSX.Element { const openPage = () => chrome.tabs.create({ url: chrome.runtime.getURL('/tabs/hello-world.html') }) return } +export default HelloWorldButton ``` > 使用 chrome API 方式可能需要考虑到一些API无法在内容脚本中使用的问题,因此建议使用 messager 方式进行跳转。 @@ -61,7 +62,7 @@ function HelloWorldButton(): JSX.Element { 有关 messager 的更多信息,你可以参阅 [`docs/background.md`](./background.md) 文档中有关 messager 的信息。 -### 进阶开发 +## 进阶开发 你可以参考以下的源码以进行更进阶页面开发: diff --git a/docs/settings.md b/docs/settings.md index 8175084e..14e878d4 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,4 +1,4 @@ -## 新增设定区块 +# 新增设定区块 如要新增页面,只需要到以下地方新增即可: @@ -8,7 +8,7 @@ src/ fragments/ <- 设定区块列表 ``` -### 创建一个新的设定区块 +## 创建一个新的设定区块 在 `src/settings/fragments/` 目录下新增一个新的设定区块,例如 `src/settings/fragments/hello-world.tsx`。 @@ -56,7 +56,7 @@ function HelloWorldSettings({ state, useHandler }: StateProxy): J export default HelloWorldSettings ``` -**使用 `StateProxy` 进行数据双向绑定** +### 使用 `StateProxy` 进行数据双向绑定 厌倦了每次写 React 都要手动处理 `onChange` ? 你可以使用本扩展内置的 `StateProxy` 进行数据双向绑定。 @@ -138,7 +138,7 @@ export default fragments ``` -### 获取设定内容 +## 获取设定内容 你可以在任何地方获取设定内容,获取方式有几种: @@ -171,7 +171,7 @@ function ContentScript(): JSX.Element { } ``` -### 进阶开发 +## 进阶开发 你可以参考以下的源码以进行更进阶的设定开发: diff --git a/package.json b/package.json index 87f1d0f3..3876489f 100644 --- a/package.json +++ b/package.json @@ -26,29 +26,29 @@ "@plasmohq/messaging": "^0.6.2", "@plasmohq/storage": "^1.9.3", "@react-hooks-library/core": "^0.5.2", - "autoprefixer": "^10.4.17", + "autoprefixer": "^10.4.18", "brotli": "^1.3.3", - "dexie": "^3.2.5", + "dexie": "^3.2.6", "dexie-react-hooks": "^1.1.7", "hash-wasm": "^4.11.0", "hls.js": "^1.5.7", "media-chrome": "^2.2.5", "mpegts.js": "^1.7.3", "n-danmaku": "^2.2.1", - "plasmo": "^0.84.2", + "plasmo": "^0.85.0", "react": "18.2.0", "react-contexify": "^6.0.0", "react-dom": "18.2.0", "react-joyride": "^2.7.4", "react-rnd": "^10.4.1", "react-state-proxy": "^1.4.11", - "sonner": "^1.4.2", + "sonner": "^1.4.3", "tailwindcss": "^3.4.1", "virtual-scroller": "^1.12.4" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.1.1", - "@playwright/test": "^1.42.0", + "@playwright/test": "^1.42.1", "@types/brotli": "^1.3.4", "@types/chrome": "^0.0.254", "@types/glob": "^8.1.0", @@ -61,7 +61,7 @@ "postcss": "^8.4.35", "prettier": "^3.2.5", "run-script-os": "^1.1.6", - "typescript": "^5.3.3" + "typescript": "^5.4.2" }, "manifest": { "host_permissions": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23249891..b86debb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,17 +24,17 @@ dependencies: specifier: ^0.5.2 version: 0.5.2(react@18.2.0) autoprefixer: - specifier: ^10.4.17 - version: 10.4.17(postcss@8.4.35) + specifier: ^10.4.18 + version: 10.4.18(postcss@8.4.35) brotli: specifier: ^1.3.3 version: 1.3.3 dexie: - specifier: ^3.2.5 - version: 3.2.5(karma@6.4.3) + specifier: ^3.2.6 + version: 3.2.6(karma@6.4.3) dexie-react-hooks: specifier: ^1.1.7 - version: 1.1.7(@types/react@18.2.37)(dexie@3.2.5)(react@18.2.0) + version: 1.1.7(@types/react@18.2.37)(dexie@3.2.6)(react@18.2.0) hash-wasm: specifier: ^4.11.0 version: 4.11.0 @@ -51,8 +51,8 @@ dependencies: specifier: ^2.2.1 version: 2.2.1 plasmo: - specifier: ^0.84.2 - version: 0.84.2(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0) + specifier: ^0.85.0 + version: 0.85.0(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -72,8 +72,8 @@ dependencies: specifier: ^1.4.11 version: 1.4.11 sonner: - specifier: ^1.4.2 - version: 1.4.2(react-dom@18.2.0)(react@18.2.0) + specifier: ^1.4.3 + version: 1.4.3(react-dom@18.2.0)(react@18.2.0) tailwindcss: specifier: ^3.4.1 version: 3.4.1 @@ -86,8 +86,8 @@ devDependencies: specifier: ^4.1.1 version: 4.1.1(prettier@3.2.5) '@playwright/test': - specifier: ^1.42.0 - version: 1.42.0 + specifier: ^1.42.1 + version: 1.42.1 '@types/brotli': specifier: ^1.3.4 version: 1.3.4 @@ -125,8 +125,8 @@ devDependencies: specifier: ^1.1.6 version: 1.1.6 typescript: - specifier: ^5.3.3 - version: 5.3.3 + specifier: ^5.4.2 + version: 5.4.2 packages: @@ -135,12 +135,12 @@ packages: engines: {node: '>=10'} dev: false - /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/gen-mapping': 0.3.4 - '@jridgewell/trace-mapping': 0.3.23 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 /@babel/code-frame@7.23.5: resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} @@ -157,7 +157,7 @@ packages: resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.1 + '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.23.5 '@babel/generator': 7.23.6 '@babel/helper-compilation-targets': 7.23.6 @@ -180,8 +180,8 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 - '@jridgewell/gen-mapping': 0.3.4 - '@jridgewell/trace-mapping': 0.3.23 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 /@babel/helper-compilation-targets@7.23.6: @@ -718,7 +718,7 @@ packages: /@gilbarbara/types@0.2.2: resolution: {integrity: sha512-QuQDBRRcm1Q8AbSac2W1YElurOhprj3Iko/o+P1fJxUWS4rOGKMVli98OXS7uo4z+cKAif6a+L9bcZFSyauQpQ==} dependencies: - type-fest: 4.10.3 + type-fest: 4.12.0 dev: false /@ianvs/prettier-plugin-sort-imports@4.1.1(prettier@3.2.5): @@ -752,13 +752,13 @@ packages: wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 - /@jridgewell/gen-mapping@0.3.4: - resolution: {integrity: sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==} + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.23 + '@jridgewell/trace-mapping': 0.3.25 /@jridgewell/resolve-uri@3.1.2: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} @@ -771,8 +771,8 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - /@jridgewell/trace-mapping@0.3.23: - resolution: {integrity: sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==} + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 @@ -797,8 +797,8 @@ packages: '@lezer/common': 1.2.1 dev: false - /@ljharb/through@2.3.12: - resolution: {integrity: sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==} + /@ljharb/through@2.3.13: + resolution: {integrity: sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 @@ -1417,7 +1417,7 @@ packages: '@parcel/plugin': 2.9.3(@parcel/core@2.9.3) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.9.3 - '@swc/core': 1.4.2 + '@swc/core': 1.4.6 nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' @@ -2248,8 +2248,8 @@ packages: - '@parcel/core' dev: false - /@plasmohq/parcel-config@0.40.2(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): - resolution: {integrity: sha512-YuE2wxtmMcygz+YC0YZbFRsRp6Vqe+qs+TJs15sn5jKVcOu01RZeKuThKOmcTCc1M6Bcst5OkGsV8RmkpGKKCQ==} + /@plasmohq/parcel-config@0.40.3(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): + resolution: {integrity: sha512-dYVXJZKRtYGo6oFVirXFw295HAC58F8LrJqq0iVLqvZDo7fr/XPLxnBmSz1MJHCiDsiWM1/j1iywwHlAdHqbJQ==} dependencies: '@parcel/compressor-raw': 2.9.3(@parcel/core@2.9.3) '@parcel/config-default': 2.9.3(@parcel/core@2.9.3)(postcss@8.4.35)(typescript@5.2.2) @@ -2283,7 +2283,7 @@ packages: '@plasmohq/parcel-runtime': 0.23.1 '@plasmohq/parcel-transformer-inject-env': 0.2.11 '@plasmohq/parcel-transformer-inline-css': 0.3.11 - '@plasmohq/parcel-transformer-manifest': 0.17.9 + '@plasmohq/parcel-transformer-manifest': 0.18.0 '@plasmohq/parcel-transformer-svelte': 0.5.2 '@plasmohq/parcel-transformer-vue': 0.5.0(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: @@ -2475,8 +2475,8 @@ packages: lightningcss: 1.21.8 dev: false - /@plasmohq/parcel-transformer-manifest@0.17.9: - resolution: {integrity: sha512-syL5AbC7sKCNd6jpNU6qKdeUVWUDhiWLoHC5IYTVGcdVjKBql6lgs+e2JehjNB1BYW2lTrY5v3lI26Z6P6+/9Q==} + /@plasmohq/parcel-transformer-manifest@0.18.0: + resolution: {integrity: sha512-6X6ubo37Ic7u1y3MjY+vt23Gb3ULtOsH1MbKw3jZMiG8nLGXqXRfiHSyNxJWAdpk7N7ASE8dHnZ+5g3E57bldA==} engines: {parcel: '>= 2.7.0'} dependencies: '@mischnic/json-sourcemap': 0.1.0 @@ -2579,12 +2579,12 @@ packages: react: 18.2.0 dev: false - /@playwright/test@1.42.0: - resolution: {integrity: sha512-2k1HzC28Fs+HiwbJOQDUwrWMttqSLUVdjCqitBOjdCD0svWOMQUVqrXX6iFD7POps6xXAojsX/dGBpKnjZctLA==} + /@playwright/test@1.42.1: + resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==} engines: {node: '>=16'} hasBin: true dependencies: - playwright: 1.42.0 + playwright: 1.42.1 dev: true /@pnpm/config.env-replace@1.1.0: @@ -2776,8 +2776,8 @@ packages: dev: false optional: true - /@swc/core-darwin-arm64@1.4.2: - resolution: {integrity: sha512-1uSdAn1MRK5C1m/TvLZ2RDvr0zLvochgrZ2xL+lRzugLlCTlSA+Q4TWtrZaOz+vnnFVliCpw7c7qu0JouhgQIw==} + /@swc/core-darwin-arm64@1.4.6: + resolution: {integrity: sha512-bpggpx/BfLFyy48aUKq1PsNUxb7J6CINlpAUk0V4yXfmGnpZH80Gp1pM3GkFDQyCfq7L7IpjPrIjWQwCrL4hYw==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -2794,8 +2794,8 @@ packages: dev: false optional: true - /@swc/core-darwin-x64@1.4.2: - resolution: {integrity: sha512-TYD28+dCQKeuxxcy7gLJUCFLqrwDZnHtC2z7cdeGfZpbI2mbfppfTf2wUPzqZk3gEC96zHd4Yr37V3Tvzar+lQ==} + /@swc/core-darwin-x64@1.4.6: + resolution: {integrity: sha512-vJn+/ZuBTg+vtNkcmgZdH6FQpa0hFVdnB9bAeqYwKkyqP15zaPe6jfC+qL2y/cIeC7ASvHXEKrnCZgBLxfVQ9w==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -2812,8 +2812,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm-gnueabihf@1.4.2: - resolution: {integrity: sha512-Eyqipf7ZPGj0vplKHo8JUOoU1un2sg5PjJMpEesX0k+6HKE2T8pdyeyXODN0YTFqzndSa/J43EEPXm+rHAsLFQ==} + /@swc/core-linux-arm-gnueabihf@1.4.6: + resolution: {integrity: sha512-hEmYcB/9XBAl02MtuVHszhNjQpjBzhk/NFulnU33tBMbNZpy2TN5yTsitezMq090QXdDz8sKIALApDyg07ZR8g==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -2830,8 +2830,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm64-gnu@1.4.2: - resolution: {integrity: sha512-wZn02DH8VYPv3FC0ub4my52Rttsus/rFw+UUfzdb3tHMHXB66LqN+rR0ssIOZrH6K+VLN6qpTw9VizjyoH0BxA==} + /@swc/core-linux-arm64-gnu@1.4.6: + resolution: {integrity: sha512-/UCYIVoGpm2YVvGHZM2QOA3dexa28BjcpLAIYnoCbgH5f7ulDhE8FAIO/9pasj+kixDBsdqewHfsNXFYlgGJjQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -2848,8 +2848,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm64-musl@1.4.2: - resolution: {integrity: sha512-3G0D5z9hUj9bXNcwmA1eGiFTwe5rWkuL3DsoviTj73TKLpk7u64ND0XjEfO0huVv4vVu9H1jodrKb7nvln/dlw==} + /@swc/core-linux-arm64-musl@1.4.6: + resolution: {integrity: sha512-LGQsKJ8MA9zZ8xHCkbGkcPSmpkZL2O7drvwsGKynyCttHhpwVjj9lguhD4DWU3+FWIsjvho5Vu0Ggei8OYi/Lw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -2866,8 +2866,8 @@ packages: dev: false optional: true - /@swc/core-linux-x64-gnu@1.4.2: - resolution: {integrity: sha512-LFxn9U8cjmYHw3jrdPNqPAkBGglKE3tCZ8rA7hYyp0BFxuo7L2ZcEnPm4RFpmSCCsExFH+LEJWuMGgWERoktvg==} + /@swc/core-linux-x64-gnu@1.4.6: + resolution: {integrity: sha512-10JL2nLIreMQDKvq2TECnQe5fCuoqBHu1yW8aChqgHUyg9d7gfZX/kppUsuimqcgRBnS0AjTDAA+JF6UsG/2Yg==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -2884,8 +2884,8 @@ packages: dev: false optional: true - /@swc/core-linux-x64-musl@1.4.2: - resolution: {integrity: sha512-dp0fAmreeVVYTUcb4u9njTPrYzKnbIH0EhH2qvC9GOYNNREUu2GezSIDgonjOXkHiTCvopG4xU7y56XtXj4VrQ==} + /@swc/core-linux-x64-musl@1.4.6: + resolution: {integrity: sha512-EGyjFVzVY6Do89x8sfah7I3cuP4MwtwzmA6OlfD/KASqfCFf5eIaEBMbajgR41bVfMV7lK72lwAIea5xEyq1AQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -2902,8 +2902,8 @@ packages: dev: false optional: true - /@swc/core-win32-arm64-msvc@1.4.2: - resolution: {integrity: sha512-HlVIiLMQkzthAdqMslQhDkoXJ5+AOLUSTV6fm6shFKZKqc/9cJvr4S8UveNERL9zUficA36yM3bbfo36McwnvQ==} + /@swc/core-win32-arm64-msvc@1.4.6: + resolution: {integrity: sha512-gfW9AuXvwSyK07Vb8Y8E9m2oJZk21WqcD+X4BZhkbKB0TCZK0zk1j/HpS2UFlr1JB2zPKPpSWLU3ll0GEHRG2A==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -2920,8 +2920,8 @@ packages: dev: false optional: true - /@swc/core-win32-ia32-msvc@1.4.2: - resolution: {integrity: sha512-WCF8faPGjCl4oIgugkp+kL9nl3nUATlzKXCEGFowMEmVVCFM0GsqlmGdPp1pjZoWc9tpYanoXQDnp5IvlDSLhA==} + /@swc/core-win32-ia32-msvc@1.4.6: + resolution: {integrity: sha512-ZuQm81FhhvNVYtVb9GfZ+Du6e7fZlkisWvuCeBeRiyseNt1tcrQ8J3V67jD2nxje8CVXrwG3oUIbPcybv2rxfQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -2938,8 +2938,8 @@ packages: dev: false optional: true - /@swc/core-win32-x64-msvc@1.4.2: - resolution: {integrity: sha512-oV71rwiSpA5xre2C5570BhCsg1HF97SNLsZ/12xv7zayGzqr3yvFALFJN8tHKpqUdCB4FGPjoP3JFdV3i+1wUw==} + /@swc/core-win32-x64-msvc@1.4.6: + resolution: {integrity: sha512-UagPb7w5V0uzWSjrXwOavGa7s9iv3wrVdEgWy+/inm0OwY4lj3zpK9qDnMWAwYLuFwkI3UG4Q3dH8wD+CUUcjw==} engines: {node: '>=10'} cpu: [x64] os: [win32] @@ -2971,8 +2971,8 @@ packages: '@swc/core-win32-x64-msvc': 1.3.82 dev: false - /@swc/core@1.4.2: - resolution: {integrity: sha512-vWgY07R/eqj1/a0vsRKLI9o9klGZfpLNOVEnrv4nrccxBgYPjcf22IWwAoaBJ+wpA7Q4fVjCUM8lP0m01dpxcg==} + /@swc/core@1.4.6: + resolution: {integrity: sha512-A7iK9+1qzTCIuc3IYcS8gPHCm9bZVKUJrfNnwveZYyo6OFp3jLno4WOM2yBy5uqedgYATEiWgBYHKq37KrU6IA==} engines: {node: '>=10'} requiresBuild: true peerDependencies: @@ -2984,16 +2984,16 @@ packages: '@swc/counter': 0.1.3 '@swc/types': 0.1.5 optionalDependencies: - '@swc/core-darwin-arm64': 1.4.2 - '@swc/core-darwin-x64': 1.4.2 - '@swc/core-linux-arm-gnueabihf': 1.4.2 - '@swc/core-linux-arm64-gnu': 1.4.2 - '@swc/core-linux-arm64-musl': 1.4.2 - '@swc/core-linux-x64-gnu': 1.4.2 - '@swc/core-linux-x64-musl': 1.4.2 - '@swc/core-win32-arm64-msvc': 1.4.2 - '@swc/core-win32-ia32-msvc': 1.4.2 - '@swc/core-win32-x64-msvc': 1.4.2 + '@swc/core-darwin-arm64': 1.4.6 + '@swc/core-darwin-x64': 1.4.6 + '@swc/core-linux-arm-gnueabihf': 1.4.6 + '@swc/core-linux-arm64-gnu': 1.4.6 + '@swc/core-linux-arm64-musl': 1.4.6 + '@swc/core-linux-x64-gnu': 1.4.6 + '@swc/core-linux-x64-musl': 1.4.6 + '@swc/core-win32-arm64-msvc': 1.4.6 + '@swc/core-win32-ia32-msvc': 1.4.6 + '@swc/core-win32-x64-msvc': 1.4.6 dev: false /@swc/counter@0.1.3: @@ -3140,7 +3140,7 @@ packages: '@vue/reactivity-transform': 3.3.4 '@vue/shared': 3.3.4 estree-walker: 2.0.2 - magic-string: 0.30.7 + magic-string: 0.30.8 postcss: 8.4.35 source-map-js: 1.0.2 dev: false @@ -3159,7 +3159,7 @@ packages: '@vue/compiler-core': 3.3.4 '@vue/shared': 3.3.4 estree-walker: 2.0.2 - magic-string: 0.30.7 + magic-string: 0.30.8 dev: false /@vue/reactivity@3.3.4: @@ -3284,15 +3284,15 @@ packages: engines: {node: '>=8'} dev: false - /autoprefixer@10.4.17(postcss@8.4.35): - resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==} + /autoprefixer@10.4.18(postcss@8.4.35): + resolution: {integrity: sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001591 + caniuse-lite: 1.0.30001596 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -3322,17 +3322,17 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - /bare-events@2.2.0: - resolution: {integrity: sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==} + /bare-events@2.2.1: + resolution: {integrity: sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==} requiresBuild: true dev: false optional: true - /bare-fs@2.2.0: - resolution: {integrity: sha512-+VhW202E9eTVGkX7p+TNXtZC4RTzj9JfJW7PtfIbZ7mIQ/QT9uOafQTx7lx2n9ERmWsXvLHF4hStAFn4gl2mQw==} + /bare-fs@2.2.1: + resolution: {integrity: sha512-+CjmZANQDFZWy4PGbVdmALIwmt33aJg8qTkVjClU6X4WmZkTPBDxRHiBn7fpqEWEfF3AC2io++erpViAIQbSjg==} requiresBuild: true dependencies: - bare-events: 2.2.0 + bare-events: 2.2.1 bare-os: 2.2.0 bare-path: 2.1.0 streamx: 2.16.1 @@ -3439,8 +3439,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001591 - electron-to-chromium: 1.4.687 + caniuse-lite: 1.0.30001596 + electron-to-chromium: 1.4.699 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: false @@ -3450,8 +3450,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001591 - electron-to-chromium: 1.4.687 + caniuse-lite: 1.0.30001596 + electron-to-chromium: 1.4.699 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) @@ -3533,8 +3533,8 @@ packages: engines: {node: '>=10'} dev: false - /caniuse-lite@1.0.30001591: - resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==} + /caniuse-lite@1.0.30001596: + resolution: {integrity: sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==} /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -3936,7 +3936,7 @@ packages: engines: {node: '>=8'} dev: false - /dexie-react-hooks@1.1.7(@types/react@18.2.37)(dexie@3.2.5)(react@18.2.0): + /dexie-react-hooks@1.1.7(@types/react@18.2.37)(dexie@3.2.6)(react@18.2.0): resolution: {integrity: sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==} peerDependencies: '@types/react': '>=16' @@ -3944,12 +3944,12 @@ packages: react: '>=16' dependencies: '@types/react': 18.2.37 - dexie: 3.2.5(karma@6.4.3) + dexie: 3.2.6(karma@6.4.3) react: 18.2.0 dev: false - /dexie@3.2.5(karma@6.4.3): - resolution: {integrity: sha512-MA7vYQvXxWN2+G50D0GLS4FqdYUyRYQsN0FikZIVebOmRoNCSCL9+eUbIF80dqrfns3kmY+83+hE2GN9CnAGyA==} + /dexie@3.2.6(karma@6.4.3): + resolution: {integrity: sha512-R9VzQ27/cncQymoAeD1kfu66NUrdxcnMNXVfEoFLnQ+apVVbS4++veUcSGxft9V++zaeiLkMAREOMf8EwgR/Vw==} engines: {node: '>=6.0'} dependencies: karma-safari-launcher: 1.0.0(karma@6.4.3) @@ -4043,8 +4043,8 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false - /electron-to-chromium@1.4.687: - resolution: {integrity: sha512-Ic85cOuXSP6h7KM0AIJ2hpJ98Bo4hyTUjc4yjMbkvD+8yTxEhfK9+8exT2KKYsSjnCn2tGsKVSZwE7ZgTORQCw==} + /electron-to-chromium@1.4.699: + resolution: {integrity: sha512-I7q3BbQi6e4tJJN5CRcyvxhK0iJb34TV8eJQcgh+fR2fQ8miMgZcEInckCo1U9exDHbfz7DLDnFn8oqH/VcRKw==} /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4756,7 +4756,7 @@ packages: resolution: {integrity: sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==} engines: {node: '>=14.18.0'} dependencies: - '@ljharb/through': 2.3.12 + '@ljharb/through': 2.3.13 ansi-escapes: 4.3.2 chalk: 5.3.0 cli-cursor: 3.1.0 @@ -5324,8 +5324,8 @@ packages: dependencies: yallist: 4.0.0 - /magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -5796,8 +5796,8 @@ packages: engines: {node: '>= 6'} dev: false - /plasmo@0.84.2(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-9fyuQubUdYOHEs7plvppB7tPfyqTpA1UlWEYUGeVOgKQ6/rhvpvjBLCWyhzy/S3CrzXL8nZ7HQ1xHka4Coe0PA==} + /plasmo@0.85.0(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sBhMWAlhgqYyHrvXm/3MzTMUONndNoTj46y0sWaYDMDKcawr7KBf6Qpi1HxsCW2yYcgQjbyFrmUAo1vLqwOn8g==} hasBin: true dependencies: '@expo/spawn-async': 1.7.2 @@ -5806,7 +5806,7 @@ packages: '@parcel/package-manager': 2.9.3(@parcel/core@2.9.3) '@parcel/watcher': 2.2.0 '@plasmohq/init': 0.7.0 - '@plasmohq/parcel-config': 0.40.2(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + '@plasmohq/parcel-config': 0.40.3(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) '@plasmohq/parcel-core': 0.1.8 buffer: 6.0.3 chalk: 5.3.0 @@ -5890,18 +5890,18 @@ packages: - whiskers dev: false - /playwright-core@1.42.0: - resolution: {integrity: sha512-0HD9y8qEVlcbsAjdpBaFjmaTHf+1FeIddy8VJLeiqwhcNqGCBe4Wp2e8knpqiYbzxtxarxiXyNDw2cG8sCaNMQ==} + /playwright-core@1.42.1: + resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} engines: {node: '>=16'} hasBin: true dev: true - /playwright@1.42.0: - resolution: {integrity: sha512-Ko7YRUgj5xBHbntrgt4EIw/nE//XBHOKVKnBjO1KuZkmkhlbgyggTe5s9hjqQ1LpN+Xg+kHsQyt5Pa0Bw5XpvQ==} + /playwright@1.42.1: + resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==} engines: {node: '>=16'} hasBin: true dependencies: - playwright-core: 1.42.0 + playwright-core: 1.42.1 optionalDependencies: fsevents: 2.3.2 dev: true @@ -5956,7 +5956,7 @@ packages: dependencies: lilconfig: 3.1.1 postcss: 8.4.35 - yaml: 2.4.0 + yaml: 2.4.1 dev: false /postcss-nested@6.0.1(postcss@8.4.35): @@ -6018,8 +6018,8 @@ packages: posthtml-render: 3.0.0 dev: false - /prebuild-install@7.1.1: - resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + /prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} engines: {node: '>=10'} hasBin: true dependencies: @@ -6087,7 +6087,7 @@ packages: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} dependencies: - side-channel: 1.0.5 + side-channel: 1.0.6 dev: false /queue-microtask@1.2.3: @@ -6231,7 +6231,7 @@ packages: scroll: 3.0.1 scrollparent: 2.1.0 tree-changes: 0.11.2 - type-fest: 4.10.3 + type-fest: 4.12.0 transitivePeerDependencies: - '@types/react' dev: false @@ -6501,7 +6501,7 @@ packages: color: 4.2.3 detect-libc: 2.0.2 node-addon-api: 6.1.0 - prebuild-install: 7.1.1 + prebuild-install: 7.1.2 semver: 7.5.4 simple-get: 4.0.1 tar-fs: 3.0.5 @@ -6518,8 +6518,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - /side-channel@1.0.5: - resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 @@ -6597,8 +6597,8 @@ packages: - utf-8-validate dev: false - /sonner@1.4.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-x3Kfzfhb56V/ErvUnH5dZcsu6QkZpyIlRAogO4vAbN+AkBsA/8CFqOV+5djqbE5pQCpejtO4JBWL1zRj2sO/Vg==} + /sonner@1.4.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 @@ -6665,7 +6665,7 @@ packages: fast-fifo: 1.3.2 queue-tick: 1.0.1 optionalDependencies: - bare-events: 2.2.0 + bare-events: 2.2.1 dev: false /string-width@4.2.3: @@ -6728,7 +6728,7 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: - '@jridgewell/gen-mapping': 0.3.4 + '@jridgewell/gen-mapping': 0.3.5 commander: 4.1.1 glob: 10.3.10 lines-and-columns: 1.2.4 @@ -6759,9 +6759,9 @@ packages: resolution: {integrity: sha512-7n2u7A5cu8xCY6MBiXh/Mg6Lh3+Mw2qXlTDBYhzvCvmSM4L4gc4MVo540UtGcjqBiA48E1VDW+EUpBr7iuBlPg==} engines: {node: '>=16'} dependencies: - '@ampproject/remapping': 2.2.1 + '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.23 + '@jridgewell/trace-mapping': 0.3.25 acorn: 8.11.3 aria-query: 5.3.0 axobject-query: 3.2.1 @@ -6770,7 +6770,7 @@ packages: estree-walker: 3.0.3 is-reference: 3.0.2 locate-character: 3.0.0 - magic-string: 0.30.7 + magic-string: 0.30.8 periscopic: 3.1.0 dev: false @@ -6846,7 +6846,7 @@ packages: pump: 3.0.0 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 2.2.0 + bare-fs: 2.2.1 bare-path: 2.1.0 dev: false @@ -7033,8 +7033,8 @@ packages: engines: {node: '>=12.20'} dev: false - /type-fest@4.10.3: - resolution: {integrity: sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==} + /type-fest@4.12.0: + resolution: {integrity: sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==} engines: {node: '>=16'} dev: false @@ -7052,8 +7052,8 @@ packages: hasBin: true dev: false - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + /typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -7243,8 +7243,8 @@ packages: engines: {node: '>= 6'} dev: false - /yaml@2.4.0: - resolution: {integrity: sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==} + /yaml@2.4.1: + resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} engines: {node: '>= 14'} hasBin: true dev: false diff --git a/src/background/messages/fetch-developer.ts b/src/background/messages/fetch-developer.ts index 8a035ea7..90bf73bf 100644 --- a/src/background/messages/fetch-developer.ts +++ b/src/background/messages/fetch-developer.ts @@ -1,7 +1,6 @@ import type { PlasmoMessaging } from "@plasmohq/messaging" import { sendInternal } from '~background/messages' import type { SettingSchema as DeveloperSchema } from "~settings/fragments/developer" -import { setSettingStorage } from '~utils/storage' const developerLink = `https://cdn.jsdelivr.net/gh/eric2788/bilibili-vup-stream-enhancer@web/cdn/developer_v2.json` diff --git a/src/contents/index/mounter.tsx b/src/contents/index/mounter.tsx index 5241160f..e3d339ec 100644 --- a/src/contents/index/mounter.tsx +++ b/src/contents/index/mounter.tsx @@ -1,4 +1,4 @@ -import type { PlasmoCSUIAnchor } from "plasmo" +import { memo } from "react" import { createRoot, type Root } from "react-dom/client" import { toast } from "sonner/dist" import { ensureLogin, type StreamInfo } from "~api/bilibili" @@ -12,10 +12,9 @@ import { shouldInit } from "~settings" import { getStreamInfoByDom } from "~utils/bilibili" import { injectAdapter } from "~utils/inject" import { addBLiveMessageCommandListener, sendMessager } from "~utils/messaging" +import { findOrCreateElement } from "~utils/react-node" import { getFullSettingStroage } from "~utils/storage" import App from "./App" -import { memo } from "react" -import { findOrCreateElement } from "~utils/react-node" interface RootMountable { feature: FeatureType From dda69bb00a3ed30d5c15e94fb1172e6cd5e988cb Mon Sep 17 00:00:00 2001 From: eric2788 Date: Sun, 10 Mar 2024 00:45:53 +0800 Subject: [PATCH 6/6] fixed 'no room to enter" --- tests/fixtures/background.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/fixtures/background.ts b/tests/fixtures/background.ts index cd887517..b7250006 100644 --- a/tests/fixtures/background.ts +++ b/tests/fixtures/background.ts @@ -26,6 +26,7 @@ export const test = extensionBase.extend({ await using room = new BilibiliPage(frontPage, api) const generator = Strategy.random(rooms, Math.min(rooms.length, 5)) const info = await cacher.findRoomTypeWithCache(isThemeRoom ? 'theme' : 'normal', generator) + test.skip(!info, `找不到${isThemeRoom ? '' : '不是'}大海報的房間。`) await room.enterToRoom(info) test.skip(await room.checkIfNotSupport(), '瀏覽器版本過低') await use(room)