mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-11-27 10:35:05 +08:00
📝 Docs: 升级新版 NonePress 主题 (#2375)
This commit is contained in:
parent
7754f6da1d
commit
842c6ff4c6
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@ -0,0 +1,6 @@
|
||||
dist
|
||||
node_modules
|
||||
.yarn
|
||||
.history
|
||||
build
|
||||
lib
|
85
.eslintrc.js
Normal file
85
.eslintrc.js
Normal file
@ -0,0 +1,85 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json", "./website/tsconfig.json"],
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:regexp/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||
},
|
||||
typescript: true,
|
||||
},
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.ts", "*.tsx"],
|
||||
rules: {
|
||||
"import/no-unresolved": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.js", "*.cjs"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
quotes: ["error", "double", { avoidEscape: true }],
|
||||
semi: ["error", "always"],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
],
|
||||
pathGroups: [
|
||||
{ pattern: "react", group: "builtin", position: "before" },
|
||||
{ pattern: "fs-extra", group: "builtin" },
|
||||
{ pattern: "lodash", group: "external", position: "before" },
|
||||
{ pattern: "clsx", group: "external", position: "before" },
|
||||
{ pattern: "@theme/**", group: "internal" },
|
||||
{ pattern: "@site/**", group: "internal" },
|
||||
{ pattern: "@theme-init/**", group: "internal" },
|
||||
{ pattern: "@theme-original/**", group: "internal" },
|
||||
],
|
||||
pathGroupsExcludedImportTypes: [],
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
12
.github/actions/setup-node/action.yml
vendored
12
.github/actions/setup-node/action.yml
vendored
@ -7,15 +7,7 @@ runs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "yarn"
|
||||
|
||||
- id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn install --frozen-lockfile
|
||||
shell: bash
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -139,7 +139,7 @@ fabric.properties
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
# Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
31
.stylelintrc.js
Normal file
31
.stylelintrc.js
Normal file
@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
extends: ["stylelint-config-standard", "stylelint-prettier/recommended"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.css"],
|
||||
rules: {
|
||||
"function-no-unknown": [true, { ignoreFunctions: ["theme"] }],
|
||||
"selector-class-pattern": [
|
||||
"^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
|
||||
{
|
||||
resolveNestedSelectors: true,
|
||||
message: (selector) =>
|
||||
`Expected class selector "${selector}" to be kebab-case`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.module.css"],
|
||||
rules: {
|
||||
"selector-class-pattern": [
|
||||
"^[a-z][a-zA-Z0-9]+$",
|
||||
{
|
||||
message: (selector) =>
|
||||
`Expected class selector "${selector}" to be lowerCamelCase`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
@ -207,7 +207,7 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
|
||||
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
||||
|
||||
- 其他插件请查看 [商店](https://nonebot.dev/store)
|
||||
- 其他插件请查看 [商店](https://nonebot.dev/store/plugins)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
@ -1,30 +1,30 @@
|
||||
from .model import URL as URL
|
||||
from .driver import Mixin as Mixin
|
||||
from .model import RawURL as RawURL
|
||||
from .driver import Driver as Driver
|
||||
from .abstract import Mixin as Mixin
|
||||
from .model import Cookies as Cookies
|
||||
from .model import Request as Request
|
||||
from .abstract import Driver as Driver
|
||||
from .model import FileType as FileType
|
||||
from .model import Response as Response
|
||||
from .model import DataTypes as DataTypes
|
||||
from .model import FileTypes as FileTypes
|
||||
from .model import WebSocket as WebSocket
|
||||
from .driver import ASGIMixin as ASGIMixin
|
||||
from .model import FilesTypes as FilesTypes
|
||||
from .model import QueryTypes as QueryTypes
|
||||
from .abstract import ASGIMixin as ASGIMixin
|
||||
from .model import CookieTypes as CookieTypes
|
||||
from .model import FileContent as FileContent
|
||||
from .model import HTTPVersion as HTTPVersion
|
||||
from .model import HeaderTypes as HeaderTypes
|
||||
from .model import SimpleQuery as SimpleQuery
|
||||
from .model import ContentTypes as ContentTypes
|
||||
from .driver import ForwardMixin as ForwardMixin
|
||||
from .driver import ReverseMixin as ReverseMixin
|
||||
from .model import QueryVariable as QueryVariable
|
||||
from .driver import ForwardDriver as ForwardDriver
|
||||
from .driver import ReverseDriver as ReverseDriver
|
||||
from .driver import combine_driver as combine_driver
|
||||
from .abstract import ForwardMixin as ForwardMixin
|
||||
from .abstract import ReverseMixin as ReverseMixin
|
||||
from .abstract import ForwardDriver as ForwardDriver
|
||||
from .abstract import ReverseDriver as ReverseDriver
|
||||
from .combine import combine_driver as combine_driver
|
||||
from .model import HTTPServerSetup as HTTPServerSetup
|
||||
from .driver import HTTPClientMixin as HTTPClientMixin
|
||||
from .abstract import HTTPClientMixin as HTTPClientMixin
|
||||
from .model import WebSocketServerSetup as WebSocketServerSetup
|
||||
from .driver import WebSocketClientMixin as WebSocketClientMixin
|
||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||
|
@ -2,18 +2,7 @@ import abc
|
||||
import asyncio
|
||||
from typing_extensions import TypeAlias
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Set,
|
||||
Dict,
|
||||
Type,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
AsyncGenerator,
|
||||
overload,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Env, Config
|
||||
@ -33,8 +22,6 @@ if TYPE_CHECKING:
|
||||
from nonebot.internal.adapter import Bot, Adapter
|
||||
|
||||
|
||||
D = TypeVar("D", bound="Driver")
|
||||
|
||||
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
||||
|
||||
|
||||
@ -295,44 +282,3 @@ ReverseDriver: TypeAlias = ReverseMixin
|
||||
|
||||
**Deprecated**,请使用 {ref}`nonebot.drivers.ReverseMixin` 或其子类代替。
|
||||
"""
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class CombinedDriver(Driver, Mixin):
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def combine_driver(driver: Type[D]) -> Type[D]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def combine_driver(driver: Type[D], *mixins: Type[Mixin]) -> Type["CombinedDriver"]:
|
||||
...
|
||||
|
||||
|
||||
def combine_driver(
|
||||
driver: Type[D], *mixins: Type[Mixin]
|
||||
) -> Union[Type[D], Type["CombinedDriver"]]:
|
||||
"""将一个驱动器和多个混入类合并。"""
|
||||
# check first
|
||||
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
|
||||
assert all(
|
||||
issubclass(m, Mixin) for m in mixins
|
||||
), "`mixins` must be subclass of Mixin"
|
||||
|
||||
if not mixins:
|
||||
return driver
|
||||
|
||||
def type_(self: "CombinedDriver") -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
||||
)
|
||||
|
||||
return type(
|
||||
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
|
||||
) # type: ignore
|
45
nonebot/internal/driver/combine.py
Normal file
45
nonebot/internal/driver/combine.py
Normal file
@ -0,0 +1,45 @@
|
||||
from typing import TYPE_CHECKING, Type, Union, TypeVar, overload
|
||||
|
||||
from .abstract import Mixin, Driver
|
||||
|
||||
D = TypeVar("D", bound="Driver")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class CombinedDriver(Driver, Mixin):
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def combine_driver(driver: Type[D]) -> Type[D]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def combine_driver(driver: Type[D], *mixins: Type[Mixin]) -> Type["CombinedDriver"]:
|
||||
...
|
||||
|
||||
|
||||
def combine_driver(
|
||||
driver: Type[D], *mixins: Type[Mixin]
|
||||
) -> Union[Type[D], Type["CombinedDriver"]]:
|
||||
"""将一个驱动器和多个混入类合并。"""
|
||||
# check first
|
||||
if not issubclass(driver, Driver):
|
||||
raise TypeError("`driver` must be subclass of Driver")
|
||||
if not all(issubclass(m, Mixin) for m in mixins):
|
||||
raise TypeError("`mixins` must be subclass of Mixin")
|
||||
|
||||
if not mixins:
|
||||
return driver
|
||||
|
||||
def type_(self: "CombinedDriver") -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
||||
)
|
||||
|
||||
return type(
|
||||
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
|
||||
) # type: ignore
|
@ -29,7 +29,7 @@
|
||||
- `load_builtin_plugins` =>
|
||||
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 0
|
||||
@ -77,7 +77,7 @@ def get_plugin(name: str) -> Optional["Plugin"]:
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
|
||||
参数:
|
||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
||||
"""
|
||||
return _plugins.get(name)
|
||||
|
||||
@ -88,7 +88,7 @@ def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
||||
如果提供的模块名为某个插件的子模块,同样会返回该插件。
|
||||
|
||||
参数:
|
||||
module_name: 模块名,即 {ref}`nonebot.plugin.plugin.Plugin.module_name`。
|
||||
module_name: 模块名,即 {ref}`nonebot.plugin.model.Plugin.module_name`。
|
||||
"""
|
||||
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
|
||||
has_parent = True
|
||||
@ -111,9 +111,9 @@ def get_available_plugin_names() -> Set[str]:
|
||||
from .on import on as on
|
||||
from .manager import PluginManager
|
||||
from .on import on_type as on_type
|
||||
from .model import Plugin as Plugin
|
||||
from .load import require as require
|
||||
from .on import on_regex as on_regex
|
||||
from .plugin import Plugin as Plugin
|
||||
from .on import on_notice as on_notice
|
||||
from .on import on_command as on_command
|
||||
from .on import on_keyword as on_keyword
|
||||
@ -129,8 +129,8 @@ from .load import load_plugins as load_plugins
|
||||
from .on import on_startswith as on_startswith
|
||||
from .load import load_from_json as load_from_json
|
||||
from .load import load_from_toml as load_from_toml
|
||||
from .model import PluginMetadata as PluginMetadata
|
||||
from .on import on_shell_command as on_shell_command
|
||||
from .plugin import PluginMetadata as PluginMetadata
|
||||
from .load import load_all_plugins as load_all_plugins
|
||||
from .load import load_builtin_plugin as load_builtin_plugin
|
||||
from .load import load_builtin_plugins as load_builtin_plugins
|
||||
|
@ -12,7 +12,7 @@ from typing import Set, Union, Iterable, Optional
|
||||
|
||||
from nonebot.utils import path_to_module_name
|
||||
|
||||
from .plugin import Plugin
|
||||
from .model import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
|
||||
|
||||
@ -160,7 +160,7 @@ def require(name: str) -> ModuleType:
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
|
||||
参数:
|
||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
||||
|
||||
异常:
|
||||
RuntimeError: 插件无法加载
|
||||
|
@ -20,7 +20,7 @@ from typing import Set, Dict, List, Iterable, Optional, Sequence
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
|
||||
from .plugin import Plugin, PluginMetadata
|
||||
from .model import Plugin, PluginMetadata
|
||||
from . import (
|
||||
_managers,
|
||||
_new_plugin,
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 3
|
||||
description: nonebot.plugin.plugin 模块
|
||||
description: nonebot.plugin.model 模块
|
||||
"""
|
||||
|
||||
import contextlib
|
@ -30,7 +30,7 @@ from nonebot.rule import (
|
||||
shell_command,
|
||||
)
|
||||
|
||||
from .plugin import Plugin
|
||||
from .model import Plugin
|
||||
from . import get_plugin_by_module_name
|
||||
from .manager import _current_plugin_chain
|
||||
|
||||
|
@ -10,7 +10,7 @@ from nonebot.rule import Rule, ArgumentParser
|
||||
from nonebot.matcher import Matcher, MatcherSource
|
||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||
|
||||
from .plugin import Plugin
|
||||
from .model import Plugin
|
||||
|
||||
def store_matcher(matcher: type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Plugin | None: ...
|
||||
|
@ -599,7 +599,7 @@ def shell_command(
|
||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典
|
||||
(例: `{"arg": "arg", "h": True}`)。
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs`
|
||||
获取的将是 {ref}`nonebot.exception.ParserExit` 异常。
|
||||
:::
|
||||
|
23
package.json
23
package.json
@ -12,11 +12,30 @@
|
||||
"serve": "yarn workspace nonebot serve",
|
||||
"clear": "yarn workspace nonebot clear",
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\"",
|
||||
"lint": "yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --cache --report-unused-disable-directives \"**/*.{js,jsx,ts,tsx,mjs}\"",
|
||||
"lint:js:fix": "eslint --cache --report-unused-disable-directives --fix \"**/*.{js,jsx,ts,tsx,mjs}\"",
|
||||
"lint:style": "stylelint \"**/*.css\"",
|
||||
"lint:style:fix": "stylelint --fix \"**/*.css\"",
|
||||
"pyright": "pyright"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/parser": "^6.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"prettier": "^2.5.0",
|
||||
"pyright": "^1.1.317"
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-regexp": "^1.15.0",
|
||||
"prettier": "^3.0.3",
|
||||
"pyright": "^1.1.317",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-prettier": "^4.0.2"
|
||||
}
|
||||
}
|
||||
|
45
tsconfig.json
Normal file
45
tsconfig.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ESNext"],
|
||||
"module": "NodeNext",
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"jsx": "react-native",
|
||||
"noEmit": true,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": false, // ensured by eslint, should not block compilation
|
||||
// "noImplicitReturns": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Disabled on purpose (handled by ESLint, should not block compilation) */
|
||||
"noUnusedParameters": false,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "nodenext",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
/* Advanced Options */
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true, // @types/webpack and webpack/types.d.ts are not the same thing
|
||||
|
||||
/* Use tslib */
|
||||
"importHelpers": true,
|
||||
"noEmitHelpers": true
|
||||
},
|
||||
"include": ["./**/.eslintrc.js", "./**/.stylelintrc.js"],
|
||||
"exclude": ["node_modules", "**/lib/**/*"]
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
@ -4,8 +4,8 @@ description: 注册适配器与指定平台交互
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 使用适配器
|
||||
@ -158,4 +158,4 @@ is_tome: bool = event.is_tome()
|
||||
|
||||
## 更多
|
||||
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
||||
|
@ -4,8 +4,8 @@ description: 通过依赖注入获取上下文信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# 依赖注入
|
||||
@ -557,7 +557,7 @@ async def _(x: httpx.AsyncClient = Depends(get_client)):
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
生成器作为依赖时,其中只能进行一次 `yield`,否则将会触发异常。如果对此有疑问并想探究原因,可以参考 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档。事实上,NoneBot 内部就使用了这两个装饰器。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 选择合适的驱动器运行机器人
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# 选择驱动器
|
||||
@ -118,7 +118,7 @@ DRIVER=~fastapi
|
||||
|
||||
##### `fastapi_reload`
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
@ -200,7 +200,7 @@ DRIVER=~quart
|
||||
|
||||
##### `quart_reload`
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
@ -252,7 +252,7 @@ nonebot.run(app="bot:app")
|
||||
|
||||
**类型:**HTTP 客户端驱动器
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||
:::
|
||||
|
||||
@ -266,7 +266,7 @@ DRIVER=~httpx
|
||||
|
||||
**类型:**WebSocket 客户端驱动器
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 自定义事件响应器存储
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 110
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 110
|
||||
---
|
||||
|
||||
# 事件响应器存储
|
||||
|
@ -4,8 +4,8 @@ description: 事件响应器组成与内置响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# 事件响应器进阶
|
||||
|
@ -4,8 +4,8 @@ description: 填写与获取插件相关的信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# 插件信息
|
||||
@ -14,7 +14,7 @@ NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。
|
||||
|
||||
## 插件元数据
|
||||
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
|
||||
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 编写与加载嵌套插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# 嵌套插件
|
||||
|
@ -4,8 +4,8 @@ description: 使用其他插件提供的功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# 跨插件访问
|
||||
|
@ -4,8 +4,8 @@ description: 添加服务端路由规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 100
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# 添加路由
|
||||
@ -21,10 +21,11 @@ NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则
|
||||
|
||||
在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断:
|
||||
|
||||
```python {3}
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
from nonebot.drivers import ASGIMixin
|
||||
|
||||
# highlight-next-line
|
||||
can_use = isinstance(get_driver(), ASGIMixin)
|
||||
```
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 在特定的生命周期中执行代码
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 90
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 90
|
||||
---
|
||||
|
||||
# 钩子函数
|
||||
|
@ -4,8 +4,8 @@ description: 控制会话响应对象
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# 会话更新
|
||||
@ -56,4 +56,4 @@ async def _(matcher: Matcher) -> Permission:
|
||||
|
||||
请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。
|
||||
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store)。
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。
|
||||
|
@ -4,14 +4,13 @@ description: 使用平台接口,完成更多功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# 使用平台接口
|
||||
|
||||
import Messenger from "@site/src/components/Messenger";
|
||||
import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
||||
import Messenger from "@/components/Messenger";
|
||||
|
||||
在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。
|
||||
|
||||
@ -19,7 +18,7 @@ import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
||||
|
||||
在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
||||
:::
|
||||
|
||||
@ -49,7 +48,11 @@ async def got_location(location: str = ArgPlainText()):
|
||||
{ position: "right", msg: "/天气" },
|
||||
{ position: "left", msg: "❓请输入地名" },
|
||||
{ position: "right", msg: "北京" },
|
||||
{ position: "left", msg: MarkdownText },
|
||||
{
|
||||
position: "left",
|
||||
monospace: true,
|
||||
msg: "┏━━━━━━━━━━━━━━━━┓\n┃ 北京 ┃\n┗━━━━━━━━━━━━━━━━┛\n• 今天\n⛅ 多云 20℃~24℃",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -100,7 +103,7 @@ result = await bot.get_user_info(user_id=12345678)
|
||||
result = await bot.call_api("get_user_info", user_id=12345678)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。
|
||||
:::
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
┏━━━━━━━━━━━━━━━━┓
|
||||
┃ 北京 ┃
|
||||
┗━━━━━━━━━━━━━━━━┛
|
||||
|
||||
• 今天
|
||||
⛅ 多云 20℃~24℃
|
@ -4,8 +4,8 @@ description: 读取用户配置来控制插件行为
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# 配置
|
||||
@ -62,7 +62,7 @@ export CUSTOM_CONFIG="config in environment variables"
|
||||
|
||||
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 记录与控制日志
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# 日志
|
||||
|
@ -4,8 +4,8 @@ description: 根据事件类型进行不同的处理
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# 事件类型与重载
|
||||
@ -28,7 +28,7 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
|
||||
|
||||
在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。
|
||||
:::
|
||||
|
||||
@ -63,7 +63,7 @@ async def handle_onebot(bot: OneBot):
|
||||
await bot.send_group_message(group_id=123123, message="OneBot")
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
||||
|
||||
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
|
@ -4,8 +4,8 @@ description: 控制事件响应器的权限
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# 权限控制
|
||||
|
@ -4,8 +4,8 @@ description: 自定义响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 响应规则
|
||||
|
@ -4,8 +4,8 @@ description: 更灵活的会话控制
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# 会话控制
|
||||
@ -322,7 +322,7 @@ async def _(matcher: Matcher):
|
||||
matcher.stop_propagation()
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
`stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 会话状态信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# 会话状态
|
||||
|
@ -180,7 +180,7 @@ docker compose build
|
||||
|
||||
将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称:
|
||||
|
||||
```yaml title=.github/workflows/build.yml {34}
|
||||
```yaml title=.github/workflows/build.yml
|
||||
name: Docker Hub Release
|
||||
|
||||
on:
|
||||
@ -213,6 +213,7 @@ jobs:
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
# highlight-next-line
|
||||
{organization}/{repository}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
|
@ -27,7 +27,7 @@ nb plugin install nonebot-plugin-sentry
|
||||
|
||||
### 配置插件
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。
|
||||
:::
|
||||
|
||||
|
@ -58,7 +58,7 @@ scheduler.add_job(
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。
|
||||
|
||||
相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务!
|
||||
|
@ -189,7 +189,7 @@ async def _(bot: Bot):
|
||||
|
||||
然后我们对该插件进行测试:
|
||||
|
||||
```python {19,20,23,24} title=tests/test_example.py
|
||||
```python title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
@ -210,12 +210,16 @@ async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
# highlight-start
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
bot = ctx.create_bot(base=Bot, adapter=adapter)
|
||||
# highlight-end
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
# highlight-start
|
||||
ctx.should_call_send(event, "message", result=None, bot=bot)
|
||||
ctx.should_call_api("bell", {}, result=None, adapter=adapter)
|
||||
# highlight-end
|
||||
```
|
||||
|
||||
请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。
|
||||
|
@ -576,6 +576,6 @@ class Message(BaseMessage[MessageSegment]):
|
||||
|
||||
## 后续工作
|
||||
|
||||
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。
|
||||
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。
|
||||
|
||||
另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。
|
||||
|
@ -62,7 +62,7 @@ NoneBot 插件使用下述命名规范:
|
||||
|
||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
|
||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
||||
@ -102,7 +102,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||
|
||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||
@ -183,7 +183,7 @@ twine upload dist/* # 只发布先前的构建
|
||||
|
||||
### 提交申请
|
||||
|
||||
完成在 PyPI 的插件发布流程后,前往[商店](/store)页面,切换到插件页签,点击 **发布插件** 按钮。
|
||||
完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。
|
||||
|
||||
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
|
||||
|
||||
@ -199,4 +199,4 @@ twine upload dist/* # 只发布先前的构建
|
||||
|
||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
||||
|
||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||
|
@ -4,8 +4,8 @@ description: 尝试使用 NoneBot
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 10
|
||||
---
|
||||
|
||||
import Asciinema from "@site/src/components/Asciinema";
|
||||
@ -13,7 +13,7 @@ import Messenger from "@site/src/components/Messenger";
|
||||
|
||||
# 快速上手
|
||||
|
||||
:::warning 前提条件
|
||||
:::caution 前提条件
|
||||
|
||||
- 请确保你的 Python 版本 >= 3.8
|
||||
- **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!!
|
||||
|
@ -4,15 +4,15 @@ description: 创建一个 NoneBot 项目
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 手动创建项目
|
||||
|
||||
在[快速上手](./quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。
|
||||
在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。
|
||||
|
||||
:::warning
|
||||
:::caution 警告
|
||||
我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。
|
||||
:::
|
||||
|
||||
@ -44,7 +44,7 @@ options:
|
||||
pip install 'nonebot2[fastapi]'
|
||||
```
|
||||
|
||||
驱动器包名可以在 [驱动器商店](/store) 中找到。
|
||||
驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。
|
||||
|
||||
3. 安装适配器
|
||||
|
||||
@ -52,7 +52,7 @@ options:
|
||||
pip install nonebot-adapter-console
|
||||
```
|
||||
|
||||
适配器包名可以在 [适配器商店](/store) 中找到。
|
||||
适配器包名可以在 [适配器商店](/store/adapters) 中找到。
|
||||
|
||||
## 创建配置文件
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 创建并加载自定义插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# 插件编写准备
|
||||
@ -41,7 +41,7 @@ options:
|
||||
|
||||
## 创建插件
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改:
|
||||
|
||||
1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`
|
||||
@ -63,7 +63,7 @@ options:
|
||||
|
||||
:::
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果在之前的[创建项目](./application.md)章节中手动创建了相关文件,那么你需要做出如下修改:
|
||||
|
||||
1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`
|
||||
@ -144,7 +144,7 @@ nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件
|
||||
nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。
|
||||
:::
|
||||
|
||||
@ -156,7 +156,7 @@ nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件
|
||||
nonebot.load_plugins("src/plugins", "path/to/your/plugins")
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 通过依赖注入获取所需事件信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# 获取事件信息
|
||||
|
@ -4,8 +4,8 @@ description: NoneBot 机器人构成及基本使用
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# 机器人的构成
|
||||
|
@ -4,8 +4,8 @@ description: 处理接收到的特定事件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# 事件处理
|
||||
|
@ -4,8 +4,8 @@ description: 响应接收到的特定事件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# 事件响应器
|
||||
|
@ -4,8 +4,8 @@ description: 处理消息序列与消息段
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 90
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 90
|
||||
---
|
||||
|
||||
# 处理消息
|
||||
@ -26,7 +26,7 @@ options:
|
||||
|
||||
顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。**
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。
|
||||
|
||||
在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。
|
||||
@ -34,7 +34,7 @@ options:
|
||||
|
||||
## 使用消息序列
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
||||
:::
|
||||
|
||||
@ -297,7 +297,7 @@ msg == Message(
|
||||
|
||||
如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的:
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。:
|
||||
:::
|
||||
|
||||
@ -337,7 +337,7 @@ Message(
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 从商店安装适配器和插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# 获取商店内容
|
||||
@ -15,10 +15,12 @@ import TabItem from "@theme/TabItem";
|
||||
import Asciinema from "@site/src/components/Asciinema";
|
||||
|
||||
:::tip 提示
|
||||
|
||||
如果你暂时没有获取商店内容的需求,可以跳过本章节。
|
||||
|
||||
:::
|
||||
|
||||
NoneBot 提供了一个[商店](/store),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。
|
||||
NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。
|
||||
|
||||
商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。
|
||||
|
||||
|
@ -1,26 +1,226 @@
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
// color mode config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["colorMode"]} */
|
||||
const colorMode = {
|
||||
defaultMode: "light",
|
||||
respectPrefersColorScheme: true,
|
||||
};
|
||||
|
||||
// navbar config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["navbar"]} */
|
||||
const navbar = {
|
||||
title: "NoneBot",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
hideOnScroll: false,
|
||||
items: [
|
||||
{
|
||||
label: "指南",
|
||||
type: "docsMenu",
|
||||
category: "tutorial",
|
||||
},
|
||||
{
|
||||
label: "深入",
|
||||
type: "docsMenu",
|
||||
category: "appendices",
|
||||
},
|
||||
{
|
||||
label: "进阶",
|
||||
type: "docsMenu",
|
||||
category: "advanced",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "doc",
|
||||
docId: "api/index",
|
||||
},
|
||||
{
|
||||
label: "更多",
|
||||
type: "dropdown",
|
||||
to: "/store/plugins",
|
||||
items: [
|
||||
{
|
||||
label: "最佳实践",
|
||||
type: "doc",
|
||||
docId: "best-practice/scheduler",
|
||||
},
|
||||
{
|
||||
label: "开发者",
|
||||
type: "doc",
|
||||
docId: "developer/plugin-publishing",
|
||||
},
|
||||
{ label: "社区", type: "doc", docId: "community/contact" },
|
||||
{ label: "开源之夏", type: "doc", docId: "ospp/2023" },
|
||||
{ label: "商店", to: "/store/plugins" },
|
||||
{ label: "更新日志", to: "/changelog" },
|
||||
{ label: "论坛", href: "https://discussions.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// footer config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["footer"]} */
|
||||
const footer = {
|
||||
style: "light",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
|
||||
links: [
|
||||
{
|
||||
title: "Learn",
|
||||
items: [
|
||||
{ label: "Introduction", to: "/docs/" },
|
||||
{ label: "QuickStart", to: "/docs/quick-start" },
|
||||
{ label: "Changelog", to: "/changelog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "NoneBot Team",
|
||||
items: [
|
||||
{
|
||||
label: "Homepage",
|
||||
href: "https://nonebot.dev",
|
||||
},
|
||||
{
|
||||
label: "NoneBot V1",
|
||||
href: "https://v1.nonebot.dev",
|
||||
},
|
||||
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Related",
|
||||
items: [
|
||||
{ label: "OneBot", href: "https://onebot.dev/" },
|
||||
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
|
||||
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// prism config
|
||||
/** @type {import('prism-react-renderer').PrismTheme} */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/order
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
/** @type {import('prism-react-renderer').PrismTheme} */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/order
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["prism"]} */
|
||||
const prism = {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["docker", "ini"],
|
||||
};
|
||||
|
||||
// algolia config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["algolia"]} */
|
||||
const algolia = {
|
||||
appId: "X0X5UACHZQ",
|
||||
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
|
||||
indexName: "nonebot",
|
||||
contextualSearch: true,
|
||||
};
|
||||
|
||||
// nonepress config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["nonepress"]} */
|
||||
const nonepress = {
|
||||
tailwindConfig: require("./tailwind.config"),
|
||||
navbar: {
|
||||
docsVersionDropdown: {
|
||||
dropdownItemsAfter: [
|
||||
{
|
||||
label: "1.x",
|
||||
href: "https://v1.nonebot.dev/",
|
||||
},
|
||||
],
|
||||
},
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "qq"],
|
||||
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "telegram"],
|
||||
href: "https://t.me/botuniverse",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "discord"],
|
||||
href: "https://discord.gg/VKtE6Gdc4h",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// theme config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig} */
|
||||
const themeConfig = {
|
||||
colorMode,
|
||||
navbar,
|
||||
footer,
|
||||
prism,
|
||||
algolia,
|
||||
nonepress,
|
||||
};
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
const siteConfig = {
|
||||
title: "NoneBot",
|
||||
tagline: "跨平台 Python 异步机器人框架",
|
||||
url: "https://nonebot.dev",
|
||||
baseUrl: process.env.BASE_URL || "/",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
favicon: "icons/favicon.ico",
|
||||
|
||||
// Set the production url of your site here
|
||||
url: "https://nonebot.dev",
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: process.env.BASE_URL || "/",
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: "nonebot", // Usually your GitHub org/user name.
|
||||
projectName: "nonebot2", // Usually your repo name.
|
||||
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "zh-Hans",
|
||||
locales: ["zh-Hans"],
|
||||
localeConfigs: {
|
||||
"zh-Hans": { label: "简体中文" },
|
||||
},
|
||||
},
|
||||
|
||||
scripts: [
|
||||
@ -34,8 +234,8 @@ const config = {
|
||||
|
||||
presets: [
|
||||
[
|
||||
"docusaurus-preset-nonepress",
|
||||
/** @type {import('docusaurus-preset-nonepress').Options} */
|
||||
"@nullbot/docusaurus-preset-nonepress",
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
@ -49,160 +249,42 @@ const config = {
|
||||
// "**/*.test.{js,jsx,ts,tsx}",
|
||||
// "**/__tests__/**",
|
||||
// ],
|
||||
// async sidebarItemsGenerator({
|
||||
// isCategoryIndex: defaultCategoryIndexMatcher,
|
||||
// defaultSidebarItemsGenerator,
|
||||
// ...args
|
||||
// }) {
|
||||
// return defaultSidebarItemsGenerator({
|
||||
// ...args,
|
||||
// isCategoryIndex(doc) {
|
||||
// // disable category index convention for generated API docs
|
||||
// if (
|
||||
// doc.directories.length > 0 &&
|
||||
// doc.directories.at(-1) === "api"
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// return defaultCategoryIndexMatcher(doc);
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
},
|
||||
// theme: {
|
||||
// customCss: require.resolve("./src/css/custom.css"),
|
||||
// },
|
||||
sitemap: {
|
||||
changefreq: "daily",
|
||||
priority: 0.5,
|
||||
},
|
||||
gtag: {
|
||||
trackingID: "G-MRS1GMZG0F",
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
plugins: [require("./src/plugins/webpack-plugin.cjs")],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('docusaurus-preset-nonepress').ThemeConfig} */
|
||||
({
|
||||
colorMode: {
|
||||
defaultMode: "light",
|
||||
},
|
||||
logo: {
|
||||
alt: "",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
},
|
||||
navbar: {
|
||||
hideOnScroll: true,
|
||||
items: [
|
||||
{
|
||||
label: "指南",
|
||||
type: "docsMenu",
|
||||
category: "tutorial",
|
||||
},
|
||||
{
|
||||
label: "深入",
|
||||
type: "docsMenu",
|
||||
category: "appendices",
|
||||
},
|
||||
{
|
||||
label: "进阶",
|
||||
type: "docsMenu",
|
||||
category: "advanced",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "docLink",
|
||||
docId: "api/index",
|
||||
},
|
||||
{
|
||||
label: "更多",
|
||||
type: "dropdown",
|
||||
to: "/store",
|
||||
items: [
|
||||
{
|
||||
label: "最佳实践",
|
||||
type: "docLink",
|
||||
docId: "best-practice/scheduler",
|
||||
},
|
||||
{
|
||||
label: "开发者",
|
||||
type: "docLink",
|
||||
docId: "developer/plugin-publishing",
|
||||
},
|
||||
{ label: "社区", type: "docLink", docId: "community/contact" },
|
||||
{ label: "开源之夏", type: "docLink", docId: "ospp/2023" },
|
||||
{ label: "商店", to: "/store" },
|
||||
{ label: "更新日志", to: "/changelog" },
|
||||
{ label: "论坛", href: "https://discussions.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
],
|
||||
docsVersionItemAfter: [
|
||||
{
|
||||
label: "2.0.0a16",
|
||||
href: "https://61d3d9dbcadf413fd3238e89--nonebot2.netlify.app/",
|
||||
},
|
||||
{
|
||||
label: "1.x",
|
||||
href: "https://v1.nonebot.dev/",
|
||||
},
|
||||
],
|
||||
},
|
||||
hideableSidebar: true,
|
||||
footer: {
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
|
||||
iconLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
description: "GitHub",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "qq"],
|
||||
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "telegram"],
|
||||
href: "https://t.me/botuniverse",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "discord"],
|
||||
href: "https://discord.gg/VKtE6Gdc4h",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
title: "Learn",
|
||||
icon: ["fas", "book"],
|
||||
items: [
|
||||
{ label: "Introduction", to: "/docs/" },
|
||||
// { label: "QuickStart", to: "/docs/quick-start" },
|
||||
{ label: "Changelog", to: "/changelog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "NoneBot Team",
|
||||
icon: ["fas", "user-friends"],
|
||||
items: [
|
||||
{
|
||||
label: "Homepage",
|
||||
href: "https://nonebot.dev",
|
||||
},
|
||||
{
|
||||
label: "NoneBot V1",
|
||||
href: "https://docs.nonebot.dev",
|
||||
},
|
||||
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Related",
|
||||
icon: ["fas", "external-link-alt"],
|
||||
items: [
|
||||
{ label: "OneBot", href: "https://onebot.dev/" },
|
||||
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
|
||||
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["docker", "ini"],
|
||||
},
|
||||
algolia: {
|
||||
appId: "X0X5UACHZQ",
|
||||
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
|
||||
indexName: "nonebot",
|
||||
contextualSearch: true,
|
||||
},
|
||||
tailwindConfig: require("./tailwind.config"),
|
||||
customCss: [require.resolve("./src/css/custom.css")],
|
||||
}),
|
||||
themeConfig,
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
module.exports = siteConfig;
|
||||
|
@ -11,7 +11,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start": "docusaurus start --host 0.0.0.0 --port 3000",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
@ -22,27 +22,23 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-beta.9",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"clsx": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"docusaurus-preset-nonepress": "canary",
|
||||
"file-loader": "^6.2.0",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
"@docusaurus/core": "^2.4.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@nullbot/docusaurus-preset-nonepress": "^2.1.2",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-use-pagination": "^2.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"url-loader": "^4.1.1"
|
||||
"react-use-pagination": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-beta.9",
|
||||
"@tsconfig/docusaurus": "^1.0.4",
|
||||
"asciinema-player": "^3.0.0-rc.1",
|
||||
"typescript": "^4.3.5"
|
||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"asciinema-player": "^3.5.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -55,5 +51,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ const sidebars = {
|
||||
{
|
||||
type: "category",
|
||||
label: "开源之夏",
|
||||
collapsible: false,
|
||||
collapsible: true,
|
||||
items: [
|
||||
{
|
||||
type: "autogenerated",
|
||||
@ -102,8 +102,28 @@ const sidebars = {
|
||||
items: [
|
||||
{
|
||||
type: "link",
|
||||
label: "商店",
|
||||
href: "/store",
|
||||
label: "插件商店",
|
||||
href: "/store/plugins",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "适配器商店",
|
||||
href: "/store/adapters",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "驱动器商店",
|
||||
href: "/store/drivers",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "机器人商店",
|
||||
href: "/store/bots",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "Awesome NoneBot",
|
||||
href: "https://awesome.nonebot.dev",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
|
@ -1,260 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ChromePicker } from "react-color";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import adapters from "../../static/adapters.json";
|
||||
import { Tag, useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Modal from "./Modal";
|
||||
import ModalAction from "./ModalAction";
|
||||
import ModalContent from "./ModalContent";
|
||||
import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Adapter(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredAdapters,
|
||||
} = useFilteredObjs(adapters);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredAdapters.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
name: string;
|
||||
desc: string;
|
||||
projectLink: string;
|
||||
moduleName: string;
|
||||
homepage: string;
|
||||
}>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [label, setLabel] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("#ea5252");
|
||||
|
||||
const urlEncode = (str: string) =>
|
||||
encodeURIComponent(str).replace(/%2B/gi, "+");
|
||||
|
||||
const onSubmit = () => {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "adapter_publish.yml" },
|
||||
{ key: "title", value: form.name && `Adapter: ${form.name}` },
|
||||
{ key: "labels", value: "Adapter" },
|
||||
{ key: "name", value: form.name },
|
||||
{ key: "description", value: form.desc },
|
||||
{ key: "pypi", value: form.projectLink },
|
||||
{ key: "module", value: form.moduleName },
|
||||
{ key: "homepage", value: form.homepage },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
.filter((query) => !!query.value)
|
||||
.map((query) => `${query.key}=${urlEncode(query.value)}`)
|
||||
.join("&");
|
||||
window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`);
|
||||
};
|
||||
const onChange = (event) => {
|
||||
const target = event.target;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
[name]: value,
|
||||
});
|
||||
event.preventDefault();
|
||||
};
|
||||
const onChangeLabel = (event) => {
|
||||
setLabel(event.target.value);
|
||||
};
|
||||
const onChangeColor = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
const validateTag = () => {
|
||||
return label.length >= 1 && label.length <= 10;
|
||||
};
|
||||
const newTag = () => {
|
||||
if (tags.length >= 3) {
|
||||
return;
|
||||
}
|
||||
if (validateTag()) {
|
||||
const tag = { label, color };
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
const insertTagType = (text: string) => {
|
||||
setLabel(text + label);
|
||||
ref.current.value = text + label;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索适配器"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="w-full rounded-lg bg-hero text-white"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
发布适配器
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentAdapters.map((adapter, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
{...adapter}
|
||||
action={`nb adapter install ${adapter.project_link}`}
|
||||
actionDisabled={!adapter.project_link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<Modal active={modalOpen} setActive={setModalOpen}>
|
||||
<ModalTitle title={"适配器信息"} />
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">适配器名称:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
maxLength={20}
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">适配器介绍:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="desc"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">PyPI 项目名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="projectLink"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">import 包名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="moduleName"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">仓库/主页:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="homepage"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">标签:</span>
|
||||
{tags.map((tag, index) => (
|
||||
<TagComponent
|
||||
key={index}
|
||||
{...tag}
|
||||
className="cursor-pointer"
|
||||
onClick={() => delTag(index)}
|
||||
/>
|
||||
))}
|
||||
</label>
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChangeLabel}
|
||||
/>
|
||||
<ChromePicker
|
||||
className="mt-2"
|
||||
color={color}
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
<div className="flex flex-wrap mt-2 items-center">
|
||||
<span className="mr-2">Type:</span>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("a:")}
|
||||
>
|
||||
Adapter
|
||||
</button>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("t:")}
|
||||
>
|
||||
Topic
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||
{ "pointer-events-none opacity-60": !validateTag() }
|
||||
)}
|
||||
onClick={newTag}
|
||||
>
|
||||
添加标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalAction>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
</ModalAction>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as AsciinemaPlayer from "asciinema-player";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import * as AsciinemaPlayer from "asciinema-player";
|
||||
|
||||
export type AsciinemaOptions = {
|
||||
cols: number;
|
||||
rows: number;
|
||||
@ -16,7 +17,7 @@ export type AsciinemaOptions = {
|
||||
fontSize: string;
|
||||
};
|
||||
|
||||
export type AsciinemaProps = {
|
||||
export type Props = {
|
||||
url: string;
|
||||
options?: Partial<AsciinemaOptions>;
|
||||
};
|
||||
@ -24,12 +25,12 @@ export type AsciinemaProps = {
|
||||
export default function AsciinemaContainer({
|
||||
url,
|
||||
options = {},
|
||||
}: AsciinemaProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
AsciinemaPlayer.create(url, ref.current, options);
|
||||
}, []);
|
||||
}, [url, options]);
|
||||
|
||||
return <div ref={ref} className="not-prose w-full max-w-full my-4"></div>;
|
||||
return <div ref={ref} className="not-prose ap-container"></div>;
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
import "asciinema-player/dist/bundle/asciinema-player.css";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import "asciinema-player/dist/bundle/asciinema-player.css";
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
|
||||
export default function Asciinema(props): JSX.Element {
|
||||
import "./styles.css";
|
||||
import type { Props } from "./container";
|
||||
|
||||
export type { Props } from "./container";
|
||||
|
||||
export default function Asciinema(props: Props): JSX.Element {
|
||||
return (
|
||||
<BrowserOnly fallback={<div></div>}>
|
||||
<BrowserOnly
|
||||
fallback={
|
||||
<a href={props.url} title="Asciinema video player">
|
||||
Asciinema cast
|
||||
</a>
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsciinemaContainer = require("./container.tsx").default;
|
||||
return <AsciinemaContainer {...props} />;
|
||||
}}
|
||||
|
@ -1,3 +1,7 @@
|
||||
.asciinema-player svg {
|
||||
display: inline-block;
|
||||
.ap-player svg {
|
||||
@apply inline-block;
|
||||
}
|
||||
|
||||
.ap-container {
|
||||
@apply w-full my-4;
|
||||
}
|
||||
|
@ -1,233 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ChromePicker } from "react-color";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import bots from "../../static/bots.json";
|
||||
import { Tag, useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Modal from "./Modal";
|
||||
import ModalAction from "./ModalAction";
|
||||
import ModalContent from "./ModalContent";
|
||||
import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Bot(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredBots,
|
||||
} = useFilteredObjs(bots);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredBots.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentBots = filteredBots.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
name: string;
|
||||
desc: string;
|
||||
homepage: string;
|
||||
}>({ name: "", desc: "", homepage: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [label, setLabel] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("#ea5252");
|
||||
|
||||
const urlEncode = (str: string) =>
|
||||
encodeURIComponent(str).replace(/%2B/gi, "+");
|
||||
|
||||
const onSubmit = () => {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "bot_publish.yml" },
|
||||
{ key: "title", value: form.name && `Bot: ${form.name}` },
|
||||
{ key: "labels", value: "Bot" },
|
||||
{ key: "name", value: form.name },
|
||||
{ key: "description", value: form.desc },
|
||||
{ key: "homepage", value: form.homepage },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
.filter((query) => !!query.value)
|
||||
.map((query) => `${query.key}=${urlEncode(query.value)}`)
|
||||
.join("&");
|
||||
window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`);
|
||||
};
|
||||
const onChange = (event) => {
|
||||
const target = event.target;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
[name]: value,
|
||||
});
|
||||
event.preventDefault();
|
||||
};
|
||||
const onChangeLabel = (event) => {
|
||||
setLabel(event.target.value);
|
||||
};
|
||||
const onChangeColor = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
const validateTag = () => {
|
||||
return label.length >= 1 && label.length <= 10;
|
||||
};
|
||||
const newTag = () => {
|
||||
if (tags.length >= 3) {
|
||||
return;
|
||||
}
|
||||
if (validateTag()) {
|
||||
const tag = { label, color };
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
const insertTagType = (text: string) => {
|
||||
setLabel(text + label);
|
||||
ref.current.value = text + label;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索机器人"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="w-full rounded-lg bg-hero text-white"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
发布机器人
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentBots.map((bot, index) => (
|
||||
<Card key={index} {...bot} />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<Modal active={modalOpen} setActive={setModalOpen}>
|
||||
<ModalTitle title={"机器人信息"} />
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">机器人名称:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
maxLength={20}
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">机器人介绍:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="desc"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">仓库/主页:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="homepage"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">标签:</span>
|
||||
{tags.map((tag, index) => (
|
||||
<TagComponent
|
||||
key={index}
|
||||
{...tag}
|
||||
className="cursor-pointer"
|
||||
onClick={() => delTag(index)}
|
||||
/>
|
||||
))}
|
||||
</label>
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChangeLabel}
|
||||
/>
|
||||
<ChromePicker
|
||||
className="mt-2"
|
||||
color={color}
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
<div className="flex flex-wrap mt-2 items-center">
|
||||
<span className="mr-2">Type:</span>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("a:")}
|
||||
>
|
||||
Adapter
|
||||
</button>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("t:")}
|
||||
>
|
||||
Topic
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||
{ "pointer-events-none opacity-60": !validateTag() }
|
||||
)}
|
||||
onClick={newTag}
|
||||
>
|
||||
添加标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalAction>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
</ModalAction>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import copy from "copy-to-clipboard";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import type { Obj } from "../../libs/store";
|
||||
import Tag from "../Tag";
|
||||
|
||||
export default function Card({
|
||||
module_name,
|
||||
project_link,
|
||||
name,
|
||||
desc,
|
||||
author,
|
||||
homepage,
|
||||
tags,
|
||||
is_official,
|
||||
action,
|
||||
actionDisabled = false,
|
||||
actionLabel = "点击复制安装命令",
|
||||
}: Obj & {
|
||||
action?: string;
|
||||
actionLabel?: string;
|
||||
actionDisabled?: boolean;
|
||||
}): JSX.Element {
|
||||
const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(homepage);
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const copyAction = () => {
|
||||
copy(action);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="block max-w-full px-4 border-2 rounded-lg outline-none no-underline bg-light-nonepress-100 dark:bg-dark-nonepress-100 border-light-nonepress-200 dark:border-dark-nonepress-200 shadow-md shadow-light-nonepress-300 dark:shadow-dark-nonepress-300">
|
||||
<div className="flex justify-between pt-4 text-lg font-medium">
|
||||
<span>
|
||||
{name}
|
||||
{is_official && (
|
||||
<FontAwesomeIcon
|
||||
icon={["fas", "check-circle"]}
|
||||
className="text-green-600 ml-2"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{homepage && (
|
||||
<Link
|
||||
href={homepage}
|
||||
className="text-black dark:text-white opacity-60 hover:text-hero dark:hover:text-white hover:opacity-100"
|
||||
>
|
||||
{isGithub ? (
|
||||
<FontAwesomeIcon icon={["fab", "github"]} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={["fas", "link"]} />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{tags && (
|
||||
<div className="pt-2 pb-4">
|
||||
{tags.map((tag, index) => (
|
||||
<Tag key={index} {...tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* FIXME: full height */}
|
||||
{desc && (
|
||||
<div className="pb-4 text-sm font-normal opacity-60">{desc}</div>
|
||||
)}
|
||||
{project_link && (
|
||||
<div className="my-2 text-sm font-normal opacity-60 font-mono">
|
||||
<FontAwesomeIcon icon={["fas", "cube"]} className="mr-2" />
|
||||
{project_link}
|
||||
</div>
|
||||
)}
|
||||
{module_name && (
|
||||
<div className="my-2 text-sm font-normal opacity-60 font-mono">
|
||||
<FontAwesomeIcon icon={["fas", "fingerprint"]} className="mr-2" />
|
||||
{module_name}
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: add user avatar */}
|
||||
{/* link: https://github.com/<username>.png */}
|
||||
{author && (
|
||||
<div className="my-2 text-sm font-normal opacity-60 font-mono">
|
||||
<FontAwesomeIcon icon={["fas", "user"]} className="mr-2" />
|
||||
{author}
|
||||
</div>
|
||||
)}
|
||||
{action && actionLabel && (
|
||||
<button
|
||||
className={clsx(
|
||||
"my-2 text-sm py-2 w-full rounded select-none bg-light-nonepress-200 dark:bg-dark-nonepress-200 active:bg-light-nonepress-300 active:dark:bg-dark-nonepress-300",
|
||||
{ "opacity-60 pointer-events-none": actionDisabled }
|
||||
)}
|
||||
onClick={copyAction}
|
||||
>
|
||||
<span className="flex grow items-center justify-center">
|
||||
{copied ? "复制成功" : actionLabel}
|
||||
<FontAwesomeIcon
|
||||
icon={["fas", copied ? "check-circle" : "copy"]}
|
||||
className="ml-2"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import React from "react";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import drivers from "../../static/drivers.json";
|
||||
import { useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Paginate from "./Paginate";
|
||||
|
||||
export default function Driver(): JSX.Element {
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredDrivers,
|
||||
} = useFilteredObjs(drivers);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredDrivers.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索驱动器"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button className="w-full rounded-lg bg-hero text-white opacity-60">
|
||||
发布驱动器
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentDrivers.map((driver, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
{...driver}
|
||||
action={`nb driver install ${driver.project_link}`}
|
||||
actionDisabled={!driver.project_link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Logo from "@theme/Logo";
|
||||
|
||||
export function Hero(): JSX.Element {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap p-16 mx-auto max-w-7xl h-screen relative px-4 sm:p-24">
|
||||
<div className="flex-grow self-center text-center">
|
||||
<Logo imageClassName="max-h-48" />
|
||||
<h1 className="text-5xl tracking-tight font-light sm:text-5xl md:text-5xl">
|
||||
<span className="text-hero">N</span>one
|
||||
<span className="text-hero">B</span>ot
|
||||
</h1>
|
||||
<p className="my-3 max-w-md mx-auto text-sm font-medium tracking-wide uppercase opacity-70 md:mt-5 md:max-w-3xl">
|
||||
{siteConfig.tagline}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<Link
|
||||
to="/docs/"
|
||||
className="inline-block bg-hero text-white font-bold rounded-lg px-6 py-3"
|
||||
>
|
||||
开始使用 <FontAwesomeIcon icon={["fas", "chevron-right"]} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute flex-grow flex items-center justify-between bottom-0 right-0 w-full">
|
||||
<div className="mx-auto self-start animate-bounce">
|
||||
<FontAwesomeIcon
|
||||
className="text-4xl text-hero"
|
||||
icon={["fas", "angle-down"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type Feature = {
|
||||
readonly title: string;
|
||||
readonly tagline?: string;
|
||||
readonly description?: string;
|
||||
readonly annotaion?: string;
|
||||
};
|
||||
|
||||
export function HeroFeature(props: PropsWithChildren<Feature>): JSX.Element {
|
||||
const { title, tagline, description, annotaion, children } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mt-3 mb-3 max-w-md mx-auto text-sm font-medium tracking-wide uppercase opacity-70 md:mt-5 md:max-w-3xl">
|
||||
{tagline}
|
||||
</p>
|
||||
<h1 className="font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-hero">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-10 mb-6">{description}</p>
|
||||
{children}
|
||||
<p className="text-sm italic opacity-70">{annotaion}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroFeatureSingle(
|
||||
props: PropsWithChildren<Feature>
|
||||
): JSX.Element {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 text-center md:px-16">
|
||||
<HeroFeature {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroFeatureDouble(
|
||||
props: PropsWithChildren<{ features: [Feature, Feature] }>
|
||||
): JSX.Element {
|
||||
const {
|
||||
features: [feature1, feature2],
|
||||
children,
|
||||
} = props;
|
||||
|
||||
let children1, children2;
|
||||
if (Array.isArray(children) && children.length === 2) {
|
||||
[children1, children2] = children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 md:grid md:grid-cols-2 md:gap-6 md:px-16">
|
||||
<div className="pb-16 text-center md:pb-0">
|
||||
<HeroFeature {...feature1} children={children1} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<HeroFeature {...feature2} children={children2} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
174
website/src/components/Home/Feature.tsx
Normal file
174
website/src/components/Home/Feature.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import React from "react";
|
||||
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
|
||||
export type Feature = {
|
||||
title: string;
|
||||
tagline?: string;
|
||||
description?: string;
|
||||
annotaion?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function HomeFeature({
|
||||
title,
|
||||
tagline,
|
||||
description,
|
||||
annotaion,
|
||||
children,
|
||||
}: Feature): JSX.Element {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<p className="text-sm text-base-content/70 font-medium tracking-wide uppercase">
|
||||
{tagline}
|
||||
</p>
|
||||
<h1 className="mt-3 font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-primary">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-10 mb-6">{description}</p>
|
||||
{children}
|
||||
<p className="text-sm italic text-base-content/70">{annotaion}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatureSingleColumn(props: Feature): JSX.Element {
|
||||
return (
|
||||
<div className="grid mx-auto px-4 py-8 md:px-16">
|
||||
<HomeFeature {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatureDoubleColumn({
|
||||
features: [feature1, feature2],
|
||||
children,
|
||||
}: {
|
||||
features: [Feature, Feature];
|
||||
children?: [React.ReactNode, React.ReactNode];
|
||||
}): JSX.Element {
|
||||
const [children1, children2] = children ?? [];
|
||||
|
||||
return (
|
||||
<div className="grid gap-x-6 gap-y-8 grid-cols-1 lg:grid-cols-2 max-w-7xl mx-auto px-4 py-8 md:px-16">
|
||||
<HomeFeature {...feature1}>{children1}</HomeFeature>
|
||||
<HomeFeature {...feature2}>{children2}</HomeFeature>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatures(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HomeFeatureSingleColumn
|
||||
title="开箱即用"
|
||||
tagline="out of box"
|
||||
description="使用 NB-CLI 快速构建属于你的机器人"
|
||||
>
|
||||
<CodeBlock
|
||||
title="Installation"
|
||||
language="bash"
|
||||
className="home-codeblock"
|
||||
>
|
||||
{[
|
||||
"$ pipx install nb-cli",
|
||||
"$ nb",
|
||||
// "d8b db .d88b. d8b db d88888b d8888b. .d88b. d888888b",
|
||||
// "888o 88 .8P Y8. 888o 88 88' 88 `8D .8P Y8. `~~88~~'",
|
||||
// "88V8o 88 88 88 88V8o 88 88ooooo 88oooY' 88 88 88",
|
||||
// "88 V8o88 88 88 88 V8o88 88~~~~~ 88~~~b. 88 88 88",
|
||||
// "88 V888 `8b d8' 88 V888 88. 88 8D `8b d8' 88",
|
||||
// "VP V8P `Y88P' VP V8P Y88888P Y8888P' `Y88P' YP",
|
||||
"[?] What do you want to do?",
|
||||
"❯ Create a NoneBot project.",
|
||||
" Run the bot in current folder.",
|
||||
" Manage bot driver.",
|
||||
" Manage bot adapters.",
|
||||
" Manage bot plugins.",
|
||||
" ...",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HomeFeatureSingleColumn>
|
||||
<HomeFeatureDoubleColumn
|
||||
features={[
|
||||
{
|
||||
title: "插件系统",
|
||||
tagline: "plugin system",
|
||||
description: "插件化开发,模块化管理",
|
||||
},
|
||||
{
|
||||
title: "跨平台支持",
|
||||
tagline: "cross-platform support",
|
||||
description: "支持多种平台,以及多样的事件响应方式",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"import nonebot",
|
||||
"# 加载一个插件",
|
||||
'nonebot.load_plugin("path.to.your.plugin")',
|
||||
"# 从文件夹加载插件",
|
||||
'nonebot.load_plugins("plugins")',
|
||||
"# 从配置文件加载多个插件",
|
||||
'nonebot.load_from_json("plugins.json")',
|
||||
'nonebot.load_from_toml("pyproject.toml")',
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"import nonebot",
|
||||
"# OneBot",
|
||||
"from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter",
|
||||
"# QQ 频道",
|
||||
"from nonebot.adapters.qqguild import Adapter as QQGuildAdapter",
|
||||
"driver = nonebot.get_driver()",
|
||||
"driver.register_adapter(OneBotAdapter)",
|
||||
"driver.register_adapter(QQGuildAdapter)",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HomeFeatureDoubleColumn>
|
||||
<HomeFeatureDoubleColumn
|
||||
features={[
|
||||
{
|
||||
title: "异步开发",
|
||||
tagline: "asynchronous first",
|
||||
description: "异步优先式开发,提高运行效率",
|
||||
},
|
||||
{
|
||||
title: "依赖注入",
|
||||
tagline: "builtin dependency injection system",
|
||||
description: "简单清晰的依赖注入系统,内置依赖函数减少用户代码",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"from nonebot import on_message",
|
||||
"# 注册一个消息响应器",
|
||||
"matcher = on_message()",
|
||||
"# 注册一个消息处理器",
|
||||
"# 并重复收到的消息",
|
||||
"@matcher.handle()",
|
||||
"async def handler(event: Event) -> None:",
|
||||
" await matcher.send(event.get_message())",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"from nonebot import on_command",
|
||||
"# 注册一个命令响应器",
|
||||
'matcher = on_command("help", alias={"帮助"})',
|
||||
"# 注册一个命令处理器",
|
||||
"# 通过依赖注入获得命令名以及参数",
|
||||
"@matcher.handle()",
|
||||
"async def handler(cmd = Command(), arg = CommandArg()) -> None:",
|
||||
" await matcher.finish()",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HomeFeatureDoubleColumn>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(HomeFeatures);
|
69
website/src/components/Home/Hero.tsx
Normal file
69
website/src/components/Home/Hero.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client";
|
||||
// @ts-expect-error: we need to make package have type: module
|
||||
import copy from "copy-text-to-clipboard";
|
||||
|
||||
import IconCopy from "@theme/Icon/Copy";
|
||||
import IconSuccess from "@theme/Icon/Success";
|
||||
|
||||
function HomeHeroInstallButton(): JSX.Element {
|
||||
const code = "pipx run nb-cli create";
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyTimeout = useRef<number | undefined>(undefined);
|
||||
|
||||
const handleCopyCode = useCallback(() => {
|
||||
copy(code);
|
||||
setIsCopied(true);
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 1500);
|
||||
}, [code]);
|
||||
|
||||
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn no-animation home-hero-copy"
|
||||
onClick={handleCopyCode}
|
||||
>
|
||||
<code>$ {code}</code>
|
||||
{isCopied ? <IconSuccess className="text-success" /> : <IconCopy />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeHero(): JSX.Element {
|
||||
const {
|
||||
siteConfig: { tagline },
|
||||
} = useDocusaurusContext();
|
||||
const {
|
||||
navbar: { logo },
|
||||
} = useNonepressThemeConfig();
|
||||
|
||||
return (
|
||||
<div className="home-hero">
|
||||
<img src={logo!.src} alt={logo!.alt} className="home-hero-logo" />
|
||||
<h1 className="home-hero-title">
|
||||
<span className="text-primary">None</span>
|
||||
Bot
|
||||
</h1>
|
||||
<p className="home-hero-tagline">{tagline}</p>
|
||||
<div className="home-hero-actions">
|
||||
<Link to="/docs/" className="btn btn-primary">
|
||||
开始使用 <FontAwesomeIcon icon={["fas", "chevron-right"]} />
|
||||
</Link>
|
||||
<HomeHeroInstallButton />
|
||||
</div>
|
||||
<div className="home-hero-next">
|
||||
<FontAwesomeIcon icon={["fas", "angle-down"]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(HomeHero);
|
14
website/src/components/Home/index.tsx
Normal file
14
website/src/components/Home/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
import "./styles.css";
|
||||
import HomeFeatures from "./Feature";
|
||||
import HomeHero from "./Hero";
|
||||
|
||||
export default function HomeContent(): JSX.Element {
|
||||
return (
|
||||
<div className="home-container">
|
||||
<HomeHero />
|
||||
<HomeFeatures />
|
||||
</div>
|
||||
);
|
||||
}
|
41
website/src/components/Home/styles.css
Normal file
41
website/src/components/Home/styles.css
Normal file
@ -0,0 +1,41 @@
|
||||
.home {
|
||||
&-container {
|
||||
@apply -mt-16;
|
||||
}
|
||||
|
||||
&-hero {
|
||||
@apply relative flex flex-col items-center justify-center gap-4 h-screen;
|
||||
|
||||
&-logo {
|
||||
@apply h-48 w-auto;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply text-5xl font-normal tracking-tight;
|
||||
}
|
||||
|
||||
&-tagline {
|
||||
@apply text-sm font-medium uppercase tracking-wide text-base-content/70;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
@apply flex flex-col sm:flex-row gap-4;
|
||||
}
|
||||
|
||||
&-copy {
|
||||
@apply font-normal normal-case text-base-content/70;
|
||||
}
|
||||
|
||||
&-next {
|
||||
@apply absolute bottom-4;
|
||||
|
||||
& svg {
|
||||
@apply animate-bounce text-primary text-4xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-codeblock {
|
||||
@apply inline-block !max-w-[600px];
|
||||
}
|
||||
}
|
@ -1,40 +1,56 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Logo from "@theme/Logo";
|
||||
import clsx from "clsx";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client";
|
||||
|
||||
import "./styles.css";
|
||||
import ThemedImage from "@theme/ThemedImage";
|
||||
|
||||
export type Message = {
|
||||
position?: "left" | "right";
|
||||
msg: string;
|
||||
position?: "left" | "right";
|
||||
monospace?: boolean;
|
||||
};
|
||||
|
||||
function MessageBox({
|
||||
msg,
|
||||
isRight,
|
||||
}: {
|
||||
msg: string;
|
||||
isRight: boolean;
|
||||
}): JSX.Element {
|
||||
position = "left",
|
||||
monospace = false,
|
||||
}: Message): JSX.Element {
|
||||
const {
|
||||
navbar: { logo },
|
||||
} = useNonepressThemeConfig();
|
||||
const sources = {
|
||||
light: useBaseUrl(logo!.src),
|
||||
dark: useBaseUrl(logo!.srcDark || logo!.src),
|
||||
};
|
||||
|
||||
const isRight = position === "right";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.message, {
|
||||
[styles.messageRight]: isRight,
|
||||
})}
|
||||
>
|
||||
{isRight ? (
|
||||
<div className={clsx("bg-cyan-600 text-base", styles.messageAvatar)}>
|
||||
<FontAwesomeIcon icon={["fas", "user"]} />
|
||||
<div className={clsx("chat", isRight ? "chat-end" : "chat-start")}>
|
||||
<div className="chat-image avatar">
|
||||
<div
|
||||
className={clsx(
|
||||
"messenger-chat-avatar",
|
||||
isRight && "messenger-chat-avatar-user"
|
||||
)}
|
||||
>
|
||||
{isRight ? (
|
||||
<FontAwesomeIcon icon={["fas", "user"]} />
|
||||
) : (
|
||||
<ThemedImage sources={sources} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={clsx("transparent", styles.messageAvatar)}>
|
||||
<Logo imageClassName="h-full w-full" disabled />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.messageBox, { "order-first": isRight })}
|
||||
className={clsx(
|
||||
"chat-bubble messenger-chat-bubble",
|
||||
monospace && "font-mono"
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: msg.replace(/\n/g, "<br/>").replace(/ /g, " "),
|
||||
}}
|
||||
@ -48,62 +64,55 @@ export default function Messenger({
|
||||
}: {
|
||||
msgs?: Message[];
|
||||
}): JSX.Element {
|
||||
const isRight = (msg: Message): boolean => msg.position === "right";
|
||||
|
||||
return (
|
||||
<div className="block w-full max-w-full my-4 rounded shadow-md outline-none no-underline bg-light-nonepress-100 dark:bg-dark-nonepress-100">
|
||||
<header className="flex items-center h-12 px-4 bg-blue-500 text-white rounded-t-[inherit]">
|
||||
<div className="text-left text-base grow">
|
||||
<div className="messenger-container">
|
||||
<header className="messenger-title">
|
||||
<div className="messenger-title-back">
|
||||
<FontAwesomeIcon icon={["fas", "chevron-left"]} />
|
||||
</div>
|
||||
<div className="flex-initial grow-0">
|
||||
<span className="text-xl font-bold">NoneBot</span>
|
||||
<div className="messenger-title-name">
|
||||
<span>NoneBot</span>
|
||||
</div>
|
||||
<div className="text-right text-base grow">
|
||||
<FontAwesomeIcon icon={["fas", "user"]} />
|
||||
<div className="messenger-title-more">
|
||||
<FontAwesomeIcon icon={["fas", "bars"]} />
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-3 min-h-[150px]">
|
||||
<div className="messenger-chat">
|
||||
{msgs.map((msg, i) => (
|
||||
<MessageBox msg={msg.msg} isRight={isRight(msg)} key={i} />
|
||||
<MessageBox {...msg} key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-1 p-1 max-w-full">
|
||||
<div className="messenger-footer">
|
||||
<div className="messenger-footer-action">
|
||||
<div className="messenger-footer-action-input">
|
||||
<input
|
||||
className="w-full rounded bg-light dark:bg-dark focus:outline-none focus:ring focus:border-blue-500"
|
||||
className="input input-xs input-bordered input-info w-full"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-initial grow-0 w-fit">
|
||||
<button
|
||||
className={clsx(
|
||||
"h-7 px-3 rounded-full bg-blue-500 text-white",
|
||||
styles.messageSendButton
|
||||
)}
|
||||
>
|
||||
<span>发送</span>
|
||||
<div className="messenger-footer-action-send">
|
||||
<button className="btn btn-xs btn-info no-animation text-white">
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center text-center text-base text-gray-600">
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div className="messenger-footer-tools">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "microphone"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "image"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "camera"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "wallet"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "smile-wink"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "plus-circle"]} />
|
||||
</div>
|
||||
</div>
|
||||
|
66
website/src/components/Messenger/styles.css
Normal file
66
website/src/components/Messenger/styles.css
Normal file
@ -0,0 +1,66 @@
|
||||
.messenger {
|
||||
&-container {
|
||||
@apply block w-full my-4 overflow-hidden;
|
||||
@apply rounded-lg outline-none bg-base-200;
|
||||
@apply transition-[background-color] duration-500;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply flex items-center h-12 px-4 bg-info text-white;
|
||||
|
||||
&-back {
|
||||
@apply text-left text-base grow;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply flex-initial grow-0 text-xl font-bold;
|
||||
}
|
||||
|
||||
&-more {
|
||||
@apply text-right text-base grow;
|
||||
}
|
||||
}
|
||||
|
||||
&-chat {
|
||||
@apply p-4 min-h-[150px];
|
||||
|
||||
&-avatar {
|
||||
@apply !flex items-center justify-center;
|
||||
@apply w-10 rounded-full;
|
||||
|
||||
&-user {
|
||||
@apply bg-info text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&-bubble {
|
||||
@apply bg-base-100 text-base-content [word-break:break-word];
|
||||
@apply transition-[color,background-color] duration-500;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
@apply px-4;
|
||||
|
||||
&-action {
|
||||
@apply flex items-center gap-2;
|
||||
|
||||
&-input {
|
||||
@apply flex-1;
|
||||
|
||||
& > input {
|
||||
@apply transition-[color,background-color] duration-500;
|
||||
}
|
||||
}
|
||||
|
||||
&-send {
|
||||
@apply flex-initial w-fit;
|
||||
}
|
||||
}
|
||||
|
||||
&-tools {
|
||||
@apply grid grid-cols-6 items-center py-1;
|
||||
@apply text-center text-base text-base-content/60;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
.message {
|
||||
@apply flex flex-row flex-wrap justify-start;
|
||||
}
|
||||
|
||||
.messageRight {
|
||||
@apply !justify-end;
|
||||
}
|
||||
|
||||
.message .messageAvatar {
|
||||
@apply relative inline-flex items-center justify-center text-center align-middle h-9 w-9 rounded-full;
|
||||
}
|
||||
|
||||
.message .messageBox {
|
||||
@apply relative w-fit max-w-[55%] px-2 py-[0.375rem] mx-3 my-2 rounded-lg bg-light;
|
||||
}
|
||||
:global(.dark) .message .messageBox {
|
||||
@apply !bg-dark;
|
||||
}
|
||||
|
||||
.message .messageBox::after {
|
||||
content: "";
|
||||
border-bottom: 7px solid;
|
||||
@apply absolute top-0 right-full w-2 h-3 text-light rounded-bl-lg;
|
||||
}
|
||||
:global(.dark) .message .messageBox::after {
|
||||
@apply !text-dark;
|
||||
}
|
||||
.message.messageRight .messageBox::after {
|
||||
@apply !left-full !right-auto !rounded-bl-[0] !rounded-br-lg;
|
||||
}
|
||||
|
||||
.messageSendButton {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
export default function Modal({
|
||||
active,
|
||||
setActive,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
setActive: (active: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{/* overlay */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center transition z-[200] pointer-events-auto",
|
||||
{ "!hidden": !active }
|
||||
)}
|
||||
onClick={() => setActive(false)}
|
||||
>
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0 h-full w-full bg-gray-800 opacity-[.46]"></div>
|
||||
</div>
|
||||
{/* modal */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed top-0 left-0 flex items-center justify-center h-full w-full z-[201] pointer-events-none",
|
||||
{ "!hidden": !active }
|
||||
)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="w-full max-w-[600px] max-h-[90%] overflow-y-auto rounded shadow-lg m-6 z-[inherit] pointer-events-auto thin-scrollbar">
|
||||
<div className="bg-light-nonepress-100 dark:bg-dark-nonepress-100">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ModalAction({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div className="px-4 py-2 flex justify-end">{children}</div>;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ModalContent({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div className="px-6 pb-5 w-full">{children}</div>;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ModalTitle({ title }: { title: string }): JSX.Element {
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-2 font-medium text-xl">
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,39 +1,55 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { usePagination } from "react-use-pagination";
|
||||
|
||||
import { useContentWidth } from "../../libs/width";
|
||||
import styles from "./styles.module.css";
|
||||
import "./styles.css";
|
||||
|
||||
const MAX_LENGTH = 7;
|
||||
|
||||
export type Props = Pick<
|
||||
ReturnType<typeof usePagination>,
|
||||
| "totalPages"
|
||||
| "currentPage"
|
||||
| "setNextPage"
|
||||
| "setPreviousPage"
|
||||
| "setPage"
|
||||
| "previousEnabled"
|
||||
| "nextEnabled"
|
||||
> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function Paginate({
|
||||
className,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setPreviousPage,
|
||||
setNextPage,
|
||||
setPage,
|
||||
currentPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
}: ReturnType<typeof usePagination>): JSX.Element {
|
||||
const [containerElement, setContainerElement] = useState<HTMLElement | null>(
|
||||
null
|
||||
);
|
||||
}: Props): JSX.Element {
|
||||
// const [containerElement, setContainerElement] = useState<HTMLElement | null>(
|
||||
// null
|
||||
// );
|
||||
|
||||
const ref = useCallback(
|
||||
(element: HTMLElement | null) => {
|
||||
setContainerElement(element);
|
||||
},
|
||||
[setContainerElement]
|
||||
);
|
||||
// const ref = useCallback(
|
||||
// (element: HTMLElement | null) => {
|
||||
// setContainerElement(element);
|
||||
// },
|
||||
// [setContainerElement]
|
||||
// );
|
||||
|
||||
const maxWidth = useContentWidth(
|
||||
containerElement?.parentElement ?? undefined
|
||||
);
|
||||
const maxLength = Math.min(
|
||||
(maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages,
|
||||
totalPages
|
||||
);
|
||||
// const maxWidth = useContentWidth(
|
||||
// containerElement?.parentElement ?? undefined
|
||||
// );
|
||||
// const maxLength = Math.min(
|
||||
// (maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages,
|
||||
// totalPages
|
||||
// );
|
||||
|
||||
const range = useCallback((start: number, end: number) => {
|
||||
const result = [];
|
||||
@ -47,12 +63,12 @@ export default function Paginate({
|
||||
const pages: (React.ReactNode | number)[] = [];
|
||||
const ellipsis = <FontAwesomeIcon icon="ellipsis-h" />;
|
||||
|
||||
const even = maxLength % 2 === 0 ? 1 : 0;
|
||||
const left = Math.floor(maxLength / 2);
|
||||
const even = MAX_LENGTH % 2 === 0 ? 1 : 0;
|
||||
const left = Math.floor(MAX_LENGTH / 2);
|
||||
const right = totalPages - left + even + 1;
|
||||
currentPage = currentPage + 1;
|
||||
|
||||
if (totalPages <= maxLength) {
|
||||
if (totalPages <= MAX_LENGTH) {
|
||||
pages.push(...range(1, totalPages));
|
||||
} else if (currentPage > left && currentPage < right) {
|
||||
const firstItem = 1;
|
||||
@ -74,34 +90,44 @@ export default function Paginate({
|
||||
}
|
||||
|
||||
return (
|
||||
<nav role="navigation" aria-label="Pagination Navigation" ref={ref}>
|
||||
<ul className={styles.container}>
|
||||
<li
|
||||
className={clsx(styles.li, { [styles.disabled]: !previousEnabled })}
|
||||
>
|
||||
<button className={styles.button} onClick={setPreviousPage}>
|
||||
<FontAwesomeIcon icon="chevron-left" />
|
||||
</button>
|
||||
</li>
|
||||
<nav
|
||||
className={clsx("paginate-container", className)}
|
||||
role="navigation"
|
||||
aria-label="Pagination Navigation"
|
||||
>
|
||||
<button
|
||||
className="paginate-button"
|
||||
onClick={setPreviousPage}
|
||||
disabled={!previousEnabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={["fas", "chevron-left"]} />
|
||||
</button>
|
||||
<ul className="paginate-pager">
|
||||
{pages.map((page, index) => (
|
||||
<li className={styles.li} key={index}>
|
||||
<button
|
||||
className={clsx(styles.button, {
|
||||
[styles.active]: page === currentPage,
|
||||
"pointer-events-none": typeof page !== "number",
|
||||
})}
|
||||
onClick={() => typeof page === "number" && setPage(page - 1)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
<li
|
||||
key={index}
|
||||
className={clsx(
|
||||
"paginate-button",
|
||||
typeof page !== "number" && "ellipsis",
|
||||
currentPage === page && "active"
|
||||
)}
|
||||
onClick={() =>
|
||||
typeof page === "number" &&
|
||||
currentPage !== page &&
|
||||
setPage(page - 1)
|
||||
}
|
||||
>
|
||||
{page}
|
||||
</li>
|
||||
))}
|
||||
<li className={clsx(styles.li, { [styles.disabled]: !nextEnabled })}>
|
||||
<button className={styles.button} onClick={setNextPage}>
|
||||
<FontAwesomeIcon icon="chevron-right" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
className="paginate-button"
|
||||
onClick={setNextPage}
|
||||
disabled={!nextEnabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={["fas", "chevron-right"]} />
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
30
website/src/components/Paginate/styles.css
Normal file
30
website/src/components/Paginate/styles.css
Normal file
@ -0,0 +1,30 @@
|
||||
.paginate {
|
||||
&-container {
|
||||
@apply flex items-center justify-center gap-2;
|
||||
}
|
||||
|
||||
&-button {
|
||||
@apply flex items-center justify-center cursor-pointer select-none;
|
||||
@apply w-8 h-8 text-sm leading-8 text-center bg-base-200 text-base-content;
|
||||
|
||||
&.ellipsis {
|
||||
@apply !text-base-content cursor-default;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-primary !text-primary-content cursor-default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply !text-base-content/80 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&-pager {
|
||||
@apply flex items-center justify-center gap-2;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
.container {
|
||||
@apply w-full max-w-full inline-flex justify-center items-center m-0 pl-0 list-none select-none;
|
||||
}
|
||||
|
||||
.li {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 34px;
|
||||
width: auto;
|
||||
min-width: 34px;
|
||||
@apply m-1 px-1 border-2 rounded shadow-lg text-center;
|
||||
@apply border-light-nonepress-200 shadow-light-nonepress-300;
|
||||
@apply text-black bg-light-nonepress-100;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply border-dark-nonepress-200 shadow-dark-nonepress-300;
|
||||
@apply text-white bg-dark-nonepress-100;
|
||||
}
|
||||
|
||||
.button.active {
|
||||
@apply bg-hero text-white border-hero;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
@apply opacity-60 pointer-events-none;
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ChromePicker } from "react-color";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import plugins from "../../static/plugins.json";
|
||||
import { Tag, useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Modal from "./Modal";
|
||||
import ModalAction from "./ModalAction";
|
||||
import ModalContent from "./ModalContent";
|
||||
import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Plugin(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredPlugins,
|
||||
} = useFilteredObjs(plugins);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredPlugins.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
projectLink: string;
|
||||
moduleName: string;
|
||||
}>({ projectLink: "", moduleName: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [label, setLabel] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("#ea5252");
|
||||
|
||||
const urlEncode = (str: string) =>
|
||||
encodeURIComponent(str).replace(/%2B/gi, "+");
|
||||
|
||||
const onSubmit = () => {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "plugin_publish.yml" },
|
||||
{
|
||||
key: "title",
|
||||
value: form.projectLink && `Plugin: ${form.projectLink}`,
|
||||
},
|
||||
{ key: "labels", value: "Plugin" },
|
||||
{ key: "pypi", value: form.projectLink },
|
||||
{ key: "module", value: form.moduleName },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
.filter((query) => !!query.value)
|
||||
.map((query) => `${query.key}=${urlEncode(query.value)}`)
|
||||
.join("&");
|
||||
window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`);
|
||||
};
|
||||
const onChange = (event) => {
|
||||
const target = event.target;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
[name]: value,
|
||||
});
|
||||
event.preventDefault();
|
||||
};
|
||||
const onChangeLabel = (event) => {
|
||||
setLabel(event.target.value);
|
||||
};
|
||||
const onChangeColor = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
const validateTag = () => {
|
||||
return label.length >= 1 && label.length <= 10;
|
||||
};
|
||||
const newTag = () => {
|
||||
if (tags.length >= 3) {
|
||||
return;
|
||||
}
|
||||
if (validateTag()) {
|
||||
const tag = { label, color };
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索插件"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="w-full rounded-lg bg-hero text-white"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
发布插件
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentPlugins.map((plugin, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
{...plugin}
|
||||
action={`nb plugin install ${plugin.project_link}`}
|
||||
actionDisabled={!plugin.project_link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<Modal active={modalOpen} setActive={setModalOpen}>
|
||||
<ModalTitle title={"插件信息"} />
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">PyPI 项目名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="projectLink"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">import 包名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="moduleName"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">标签:</span>
|
||||
{tags.map((tag, index) => (
|
||||
<TagComponent
|
||||
key={index}
|
||||
{...tag}
|
||||
className="cursor-pointer"
|
||||
onClick={() => delTag(index)}
|
||||
/>
|
||||
))}
|
||||
</label>
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChangeLabel}
|
||||
/>
|
||||
<ChromePicker
|
||||
className="mt-2"
|
||||
color={color}
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||
{ "pointer-events-none opacity-60": !validateTag() }
|
||||
)}
|
||||
onClick={newTag}
|
||||
>
|
||||
添加标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalAction>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
</ModalAction>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
118
website/src/components/Resource/Card/index.tsx
Normal file
118
website/src/components/Resource/Card/index.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import "./styles.css";
|
||||
import Tag from "@/components/Resource/Tag";
|
||||
import type { Resource } from "@/libs/store";
|
||||
|
||||
export type Props = {
|
||||
resource: Resource;
|
||||
onClick?: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
onAuthorClick: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ResourceCard({
|
||||
resource,
|
||||
onClick,
|
||||
onTagClick,
|
||||
onAuthorClick,
|
||||
className,
|
||||
}: Props): JSX.Element {
|
||||
const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(
|
||||
resource.homepage
|
||||
);
|
||||
|
||||
const isPlugin = resource.resourceType === "plugin";
|
||||
const registryLink =
|
||||
isPlugin &&
|
||||
`https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`;
|
||||
|
||||
const authorLink = `https://github.com/${resource.author}`;
|
||||
const authorAvatar = `${authorLink}.png?size=80`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"resource-card-container",
|
||||
onClick && "resource-card-container-clickable",
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="resource-card-header">
|
||||
<div className="resource-card-header-title">
|
||||
{resource.name}
|
||||
{resource.is_official && (
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-header-check"
|
||||
icon={["fas", "circle-check"]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="resource-card-header-expand">
|
||||
<FontAwesomeIcon icon={["fas", "expand"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="resource-card-desc">{resource.desc}</div>
|
||||
<div className="resource-card-footer">
|
||||
<div className="resource-card-footer-tags">
|
||||
{resource.tags.map((tag, index) => (
|
||||
<Tag
|
||||
className="resource-card-footer-tag"
|
||||
key={index}
|
||||
{...tag}
|
||||
onClick={() => onTagClick(tag.label)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="divider resource-card-footer-divider"></div>
|
||||
<div className="resource-card-footer-info">
|
||||
<div className="resource-card-footer-group">
|
||||
<Link href={resource.homepage}>
|
||||
{isGithub ? (
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-footer-icon"
|
||||
icon={["fab", "github"]}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-footer-icon"
|
||||
icon={["fas", "link"]}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
{isPlugin && (
|
||||
<Link href={registryLink as string}>
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-footer-icon"
|
||||
icon={["fas", "cube"]}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="resource-card-footer-group">
|
||||
<div className="avatar">
|
||||
<div className="resource-card-footer-avatar">
|
||||
<Link href={authorLink}>
|
||||
<img src={authorAvatar} key={resource.author} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="resource-card-footer-author"
|
||||
onClick={onAuthorClick}
|
||||
>
|
||||
{resource.author}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
75
website/src/components/Resource/Card/styles.css
Normal file
75
website/src/components/Resource/Card/styles.css
Normal file
@ -0,0 +1,75 @@
|
||||
.resource-card {
|
||||
&-container {
|
||||
@apply flex flex-col gap-y-2 w-full min-h-[12rem] p-4;
|
||||
@apply transition-colors duration-500 bg-base-200;
|
||||
@apply border-2 border-base-200 rounded-lg;
|
||||
|
||||
&-clickable {
|
||||
@apply cursor-pointer hover:border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
@apply flex items-center w-full text-lg font-medium;
|
||||
|
||||
&-title {
|
||||
@apply grow text-left truncate;
|
||||
}
|
||||
|
||||
&-check {
|
||||
@apply ml-2 text-success w-5 h-5 fill-current;
|
||||
}
|
||||
|
||||
&-expand {
|
||||
@apply flex-none fill-current;
|
||||
}
|
||||
}
|
||||
|
||||
&-desc {
|
||||
@apply flex-1 w-full text-sm text-ellipsis break-words;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
@apply flex flex-col w-full cursor-default;
|
||||
|
||||
&-tags {
|
||||
@apply flex flex-wrap gap-1;
|
||||
}
|
||||
|
||||
&-tag {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
&-divider {
|
||||
@apply m-0;
|
||||
}
|
||||
|
||||
&-info {
|
||||
@apply flex items-center justify-between w-full;
|
||||
}
|
||||
|
||||
&-group {
|
||||
@apply flex items-center justify-center gap-x-1 leading-none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@apply w-5 h-5 fill-current opacity-80;
|
||||
|
||||
&:hover {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
@apply w-5 h-5 rounded-full transition-shadow;
|
||||
|
||||
&:hover {
|
||||
@apply ring-1 ring-primary ring-offset-base-100 ring-offset-1;
|
||||
}
|
||||
}
|
||||
|
||||
&-author {
|
||||
@apply text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
32
website/src/components/Resource/Tag/index.tsx
Normal file
32
website/src/components/Resource/Tag/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./styles.css";
|
||||
import { pickTextColor } from "@/libs/color";
|
||||
import type { Tag } from "@/types/tag";
|
||||
|
||||
export type Props = Tag & {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
};
|
||||
|
||||
export default function ResourceTag({
|
||||
label,
|
||||
color,
|
||||
className,
|
||||
onClick,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<span
|
||||
className={clsx("resource-tag", className)}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
color: pickTextColor(color, "#fff", "#000"),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
4
website/src/components/Resource/Tag/styles.css
Normal file
4
website/src/components/Resource/Tag/styles.css
Normal file
@ -0,0 +1,4 @@
|
||||
.resource-tag {
|
||||
@apply inline-flex items-center justify-center;
|
||||
@apply text-xs font-mono w-fit h-5 px-2 rounded;
|
||||
}
|
118
website/src/components/Searcher/index.tsx
Normal file
118
website/src/components/Searcher/index.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./styles.css";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export type Props = {
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (value: string) => void;
|
||||
onBackspace: () => void;
|
||||
onClear: () => void;
|
||||
onTagClick: (index: number) => void;
|
||||
tags?: string[];
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function Searcher({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onBackspace,
|
||||
onClear,
|
||||
onTagClick,
|
||||
tags = [],
|
||||
className,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
onSubmit(e.currentTarget.value);
|
||||
e.currentTarget.value = "";
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleEscape = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.currentTarget.value = "";
|
||||
};
|
||||
|
||||
const handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.value === "") {
|
||||
onBackspace();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
switch (e.key) {
|
||||
case "Enter": {
|
||||
handleSubmit(e);
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
handleEscape(e);
|
||||
break;
|
||||
}
|
||||
case "Backspace": {
|
||||
handleBackspace(e);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
if (ref.current) {
|
||||
ref.current.value = "";
|
||||
ref.current.focus();
|
||||
}
|
||||
onClear();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("searcher-box", className)}>
|
||||
<div className="searcher-container">
|
||||
{tags.map((tag, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="badge badge-primary searcher-tag"
|
||||
onClick={() => onTagClick(index)}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
ref={ref}
|
||||
className="searcher-input"
|
||||
placeholder={
|
||||
placeholder ??
|
||||
translate({
|
||||
id: "theme.searcher.input.placeholder",
|
||||
description: "Search input placeholder",
|
||||
message: "搜索",
|
||||
})
|
||||
}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="searcher-action" onClick={handleClear}>
|
||||
<FontAwesomeIcon
|
||||
className="searcher-action-icon search"
|
||||
icon={["fas", "magnifying-glass"]}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
className="searcher-action-icon close"
|
||||
icon={["fas", "xmark"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
37
website/src/components/Searcher/styles.css
Normal file
37
website/src/components/Searcher/styles.css
Normal file
@ -0,0 +1,37 @@
|
||||
.searcher {
|
||||
&-box {
|
||||
@apply flex items-center w-full rounded-3xl bg-base-200;
|
||||
@apply transition-[color,background-color] duration-500;
|
||||
}
|
||||
|
||||
&-container {
|
||||
@apply flex-1 flex items-center flex-wrap gap-x-1 gap-y-2;
|
||||
@apply pl-5 py-3;
|
||||
}
|
||||
|
||||
&-tag {
|
||||
@apply flex-initial shrink-0;
|
||||
@apply font-medium cursor-pointer select-none;
|
||||
}
|
||||
|
||||
&-input {
|
||||
@apply flex-1 text-sm min-w-[10rem];
|
||||
@apply bg-transparent border-none outline-none;
|
||||
}
|
||||
|
||||
&-action {
|
||||
@apply flex-initial shrink-0 flex items-center justify-center cursor-pointer w-12 h-10;
|
||||
|
||||
&-icon {
|
||||
@apply h-4 opacity-50;
|
||||
}
|
||||
|
||||
&:hover &-icon.search {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
&:not(:hover) &-icon.close {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
175
website/src/components/Store/Content/Adapter.tsx
Normal file
175
website/src/components/Store/Content/Adapter.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Adapter } from "@/types/adapter";
|
||||
|
||||
export default function AdapterPage(): JSX.Element {
|
||||
const [adapters, setAdapters] = useState<Adapter[] | null>(null);
|
||||
const adapterCount = adapters?.length ?? 0;
|
||||
const loading = adapters === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredAdapters,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Adapter>(adapters ?? []);
|
||||
const filteredAdapterCount = filteredAdapters.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredAdapters.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load adapters asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("adapter")
|
||||
.then(setAdapters)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { filters: filterTools } = useToolbar({
|
||||
resources: adapters ?? [],
|
||||
addFilter,
|
||||
});
|
||||
|
||||
const actionTool: Action = {
|
||||
label: "发布适配器",
|
||||
icon: ["fas", "plus"],
|
||||
onClick: () => {
|
||||
// TODO: open adapter release modal
|
||||
window.open(
|
||||
"https://github.com/nonebot/nonebot2/issues/new?template=adapter_publish.yml"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const onCardClick = useCallback((adapter: Adapter) => {
|
||||
// TODO: open adapter modal
|
||||
console.log(adapter, "clicked");
|
||||
}, []);
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onCardAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{adapterCount === filteredAdapterCount ? (
|
||||
<Translate
|
||||
id="pages.store.adapter.info"
|
||||
description="Adapters info of the adapter store page"
|
||||
values={{ adapterCount }}
|
||||
>
|
||||
{"当前共有 {adapterCount} 个适配器"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.adapter.searchInfo"
|
||||
description="Adapters search info of the adapter store page"
|
||||
values={{
|
||||
adapterCount,
|
||||
filteredAdapterCount,
|
||||
}}
|
||||
>
|
||||
{"当前共有 {filteredAdapterCount} / {adapterCount} 个插件"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentAdapters.map((adapter, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={adapter}
|
||||
onClick={() => onCardClick(adapter)}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onCardAuthorClick(adapter.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
169
website/src/components/Store/Content/Bot.tsx
Normal file
169
website/src/components/Store/Content/Bot.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Bot } from "@/types/bot";
|
||||
|
||||
export default function PluginPage(): JSX.Element {
|
||||
const [bots, setBots] = useState<Bot[] | null>(null);
|
||||
const botCount = bots?.length ?? 0;
|
||||
const loading = bots === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredBots,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Bot>(bots ?? []);
|
||||
const filteredBotCount = filteredBots.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredBots.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentBots = filteredBots.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load bots asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("bot")
|
||||
.then(setBots)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { filters: filterTools } = useToolbar({
|
||||
resources: bots ?? [],
|
||||
addFilter,
|
||||
});
|
||||
|
||||
const actionTool: Action = {
|
||||
label: "发布机器人",
|
||||
icon: ["fas", "plus"],
|
||||
onClick: () => {
|
||||
// TODO: open bot release modal
|
||||
window.open(
|
||||
"https://github.com/nonebot/nonebot2/issues/new?template=bot_publish.yml"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{botCount === filteredBotCount ? (
|
||||
<Translate
|
||||
id="pages.store.bot.info"
|
||||
description="Bots info of the bot store page"
|
||||
values={{ botCount }}
|
||||
>
|
||||
{"当前共有 {botCount} 个机器人"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.bot.searchInfo"
|
||||
description="Bots search info of the bot store page"
|
||||
values={{
|
||||
botCount,
|
||||
filteredBotCount,
|
||||
}}
|
||||
>
|
||||
{"当前共有 {filteredBotCount} / {botCount} 个机器人"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentBots.map((bot, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={bot}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onAuthorClick(bot.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
151
website/src/components/Store/Content/Driver.tsx
Normal file
151
website/src/components/Store/Content/Driver.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import type { Driver } from "@/types/driver";
|
||||
|
||||
export default function DriverPage(): JSX.Element {
|
||||
const [drivers, setDrivers] = useState<Driver[] | null>(null);
|
||||
const driverCount = drivers?.length ?? 0;
|
||||
const loading = drivers === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredDrivers,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Driver>(drivers ?? []);
|
||||
const filteredDriverCount = filteredDrivers.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredDrivers.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load drivers asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("driver")
|
||||
.then(setDrivers)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onCardClick = useCallback((driver: Driver) => {
|
||||
// TODO: open driver modal
|
||||
console.log(driver, "clicked");
|
||||
}, []);
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{driverCount === filteredDriverCount ? (
|
||||
<Translate
|
||||
id="pages.store.driver.info"
|
||||
description="Drivers info of the driver store page"
|
||||
values={{ driverCount }}
|
||||
>
|
||||
{"当前共有 {driverCount} 个插件"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.driver.searchInfo"
|
||||
description="Drivers search info of the driver store page"
|
||||
values={{
|
||||
driverCount,
|
||||
filteredDriverCount,
|
||||
}}
|
||||
>
|
||||
{"当前共有 {filteredDriverCount} / {driverCount} 个插件"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentDrivers.map((driver, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={driver}
|
||||
onClick={() => onCardClick(driver)}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onAuthorClick(driver.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
172
website/src/components/Store/Content/Plugin.tsx
Normal file
172
website/src/components/Store/Content/Plugin.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Plugin } from "@/types/plugin";
|
||||
|
||||
export default function PluginPage(): JSX.Element {
|
||||
const [plugins, setPlugins] = useState<Plugin[] | null>(null);
|
||||
const pluginCount = plugins?.length ?? 0;
|
||||
const loading = plugins === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredPlugins,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Plugin>(plugins ?? []);
|
||||
const filteredPluginCount = filteredPlugins.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredPlugins.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load plugins asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("plugin")
|
||||
.then(setPlugins)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { filters: filterTools } = useToolbar({
|
||||
resources: plugins ?? [],
|
||||
addFilter,
|
||||
});
|
||||
|
||||
const actionTool: Action = {
|
||||
label: "发布插件",
|
||||
icon: ["fas", "plus"],
|
||||
onClick: () => {
|
||||
// TODO: open plugin release modal
|
||||
window.open(
|
||||
"https://github.com/nonebot/nonebot2/issues/new?template=plugin_publish.yml"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const onCardClick = useCallback((plugin: Plugin) => {
|
||||
// TODO: open plugin modal
|
||||
console.log(plugin, "clicked");
|
||||
}, []);
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onCardAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{pluginCount === filteredPluginCount ? (
|
||||
<Translate
|
||||
id="pages.store.plugin.info"
|
||||
description="Plugins info of the plugin store page"
|
||||
values={{ pluginCount }}
|
||||
>
|
||||
{"当前共有 {pluginCount} 个插件"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.plugin.searchInfo"
|
||||
description="Plugins search info of the plugin store page"
|
||||
values={{ pluginCount, filteredPluginCount: filteredPluginCount }}
|
||||
>
|
||||
{"当前共有 {filteredPluginCount} / {pluginCount} 个插件"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentPlugins.map((plugin, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={plugin}
|
||||
onClick={() => onCardClick(plugin)}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onCardAuthorClick(plugin.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
50
website/src/components/Store/Layout.tsx
Normal file
50
website/src/components/Store/Layout.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
|
||||
import { PageMetadata } from "@docusaurus/theme-common";
|
||||
import { useDocsVersionCandidates } from "@docusaurus/theme-common/internal";
|
||||
import { useVersionedSidebar } from "@nullbot/docusaurus-plugin-getsidebar/client";
|
||||
import { SidebarContentFiller } from "@nullbot/docusaurus-theme-nonepress/contexts";
|
||||
|
||||
import BackToTopButton from "@theme/BackToTopButton";
|
||||
import Layout from "@theme/Layout";
|
||||
import Page from "@theme/Page";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
const SIDEBAR_ID = "ecosystem";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function StorePage({ title, children }: Props): JSX.Element {
|
||||
const sidebarItems = useVersionedSidebar(
|
||||
useDocsVersionCandidates()[0].name,
|
||||
SIDEBAR_ID
|
||||
)!;
|
||||
|
||||
return (
|
||||
<Page hideTableOfContents reduceContentWidth={false}>
|
||||
<SidebarContentFiller items={sidebarItems} />
|
||||
<article className="prose max-w-full">
|
||||
<h1 className="store-title">{title}</h1>
|
||||
{children}
|
||||
</article>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StoreLayout({ title, ...props }: Props): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetadata title={title} />
|
||||
|
||||
<Layout>
|
||||
<BackToTopButton />
|
||||
|
||||
<StorePage title={title} {...props} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
128
website/src/components/Store/Toolbar.tsx
Normal file
128
website/src/components/Store/Toolbar.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import type { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export type Filter = {
|
||||
label: string;
|
||||
icon: IconProp;
|
||||
choices?: string[];
|
||||
onSubmit: (query: string) => void;
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
label: string;
|
||||
icon: IconProp;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
filters?: Filter[];
|
||||
action?: Action;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ToolbarFilter({
|
||||
label,
|
||||
icon,
|
||||
choices,
|
||||
onSubmit,
|
||||
}: Filter): JSX.Element {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const filteredChoices = choices
|
||||
?.filter((choice) => choice.toLowerCase().includes(query.toLowerCase()))
|
||||
?.slice(0, 5);
|
||||
|
||||
const handleQuerySubmit = () => {
|
||||
if (filteredChoices && filteredChoices.length > 0) {
|
||||
onSubmit(filteredChoices[0]);
|
||||
} else if (choices === null) {
|
||||
onSubmit(query);
|
||||
}
|
||||
};
|
||||
|
||||
const onQueryKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleQuerySubmit();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onChoiceKeyDown = (e: React.KeyboardEvent<HTMLLIElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
onSubmit(e.currentTarget.innerText);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<label
|
||||
className="btn btn-sm btn-outline btn-primary no-animation"
|
||||
tabIndex={0}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{label}
|
||||
</label>
|
||||
<div className="dropdown-content store-toolbar-dropdown">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={onQueryKeyDown}
|
||||
className="input input-sm input-bordered w-full"
|
||||
/>
|
||||
{filteredChoices && (
|
||||
<ul className="menu menu-sm">
|
||||
{filteredChoices.map((choice, index) => (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() => onSubmit(choice)}
|
||||
onKeyDown={onChoiceKeyDown}
|
||||
>
|
||||
<a tabIndex={0}>{choice}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StoreToolbar({
|
||||
filters,
|
||||
action,
|
||||
className,
|
||||
}: Props): JSX.Element | null {
|
||||
if (!(filters && filters.length > 0) && !action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("store-toolbar", className)}>
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="store-toolbar-filters">
|
||||
{filters.map((filter, index) => (
|
||||
<ToolbarFilter key={index} {...filter} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{action && (
|
||||
<div className="store-toolbar-action">
|
||||
<button
|
||||
className="btn btn-sm btn-primary no-animation"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<FontAwesomeIcon icon={action.icon} />
|
||||
{action.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
38
website/src/components/Store/styles.css
Normal file
38
website/src/components/Store/styles.css
Normal file
@ -0,0 +1,38 @@
|
||||
.store {
|
||||
&-title {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
&-description {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
&-searcher {
|
||||
@apply max-w-2xl mx-auto my-4;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
@apply flex items-center justify-center my-4;
|
||||
|
||||
&-filters {
|
||||
@apply flex grow gap-2;
|
||||
}
|
||||
|
||||
&-dropdown {
|
||||
@apply w-36 z-10 m-0 p-2;
|
||||
@apply rounded-md bg-base-100 shadow-lg border border-base-200;
|
||||
}
|
||||
}
|
||||
|
||||
&-loading {
|
||||
@apply text-primary;
|
||||
|
||||
&-container {
|
||||
@apply text-center;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
@apply grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-6 mt-4 mb-8;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { Tag as TagType } from "../../libs/store";
|
||||
|
||||
function pickTextColor(bgColor, lightColor, darkColor) {
|
||||
var color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
||||
var r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
var g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
var b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
||||
}
|
||||
|
||||
export default function Tag({
|
||||
label,
|
||||
color,
|
||||
className,
|
||||
onClick,
|
||||
}: TagType & {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"font-mono inline-flex px-3 rounded-full items-center align-middle mr-2",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
color: pickTextColor(color, "#fff", "#000"),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.homeCodeBlock {
|
||||
width: 602px;
|
||||
}
|
16
website/src/libs/color.ts
Normal file
16
website/src/libs/color.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Choose fg color by bg color
|
||||
* @see https://www.npmjs.com/package/colord
|
||||
* @see https://www.w3.org/TR/AERT/#color-contrast
|
||||
*/
|
||||
export function pickTextColor(
|
||||
bgColor: string,
|
||||
lightColor: string,
|
||||
darkColor: string
|
||||
) {
|
||||
const color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
||||
const r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
const g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
const b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
||||
}
|
130
website/src/libs/filter.ts
Normal file
130
website/src/libs/filter.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import type { Resource } from "./store";
|
||||
|
||||
export type Filter<T extends Resource = Resource> = {
|
||||
type: string;
|
||||
id: string;
|
||||
displayName: string;
|
||||
filter: (resource: T) => boolean;
|
||||
};
|
||||
|
||||
export const tagFilter = <T extends Resource = Resource>(
|
||||
tag: string
|
||||
): Filter<T> => ({
|
||||
type: "tag",
|
||||
id: `tag-${tag}`,
|
||||
displayName: translate(
|
||||
{
|
||||
id: "pages.store.filter.tagDisplayName",
|
||||
description: "The display name of tag filter",
|
||||
message: "标签: {tag}",
|
||||
},
|
||||
{ tag }
|
||||
),
|
||||
filter: (resource: Resource): boolean =>
|
||||
resource.tags.map((tag) => tag.label).includes(tag),
|
||||
});
|
||||
export const officialFilter = <T extends Resource = Resource>(
|
||||
official: boolean = true
|
||||
): Filter<T> => ({
|
||||
type: "official",
|
||||
id: `official-${official}`,
|
||||
displayName: translate({
|
||||
id: "pages.store.filter.officialDisplayName",
|
||||
description: "The display name of official filter",
|
||||
message: "非官方|官方",
|
||||
}).split("|")[Number(official)],
|
||||
filter: (resource: Resource): boolean => resource.is_official === official,
|
||||
});
|
||||
export const authorFilter = <T extends Resource = Resource>(
|
||||
author: string
|
||||
): Filter<T> => ({
|
||||
type: "author",
|
||||
id: `author-${author}`,
|
||||
displayName: translate(
|
||||
{
|
||||
id: "pages.store.filter.authorDisplayName",
|
||||
description: "The display name of author filter",
|
||||
message: "作者: {author}",
|
||||
},
|
||||
{ author }
|
||||
),
|
||||
filter: (resource: Resource): boolean => resource.author === author,
|
||||
});
|
||||
export const queryFilter = <T extends Resource = Resource>(
|
||||
query: string
|
||||
): Filter<T> => ({
|
||||
type: "query",
|
||||
id: `query-${query}`,
|
||||
displayName: query,
|
||||
filter: (resource: Resource): boolean => {
|
||||
if (!query) return true;
|
||||
const queryLower = query.toLowerCase();
|
||||
const pluginMatch =
|
||||
resource.resourceType === "plugin" &&
|
||||
(resource.module_name?.toLowerCase().includes(queryLower) ||
|
||||
resource.project_link?.toLowerCase().includes(queryLower));
|
||||
const commonMatch =
|
||||
resource.name.toLowerCase().includes(queryLower) ||
|
||||
resource.desc.toLowerCase().includes(queryLower) ||
|
||||
resource.author.toLowerCase().includes(queryLower) ||
|
||||
resource.tags.filter((t) => t.label.toLowerCase().includes(queryLower))
|
||||
.length > 0;
|
||||
return pluginMatch || commonMatch;
|
||||
},
|
||||
});
|
||||
|
||||
export function filterResources<T extends Resource>(
|
||||
resources: T[],
|
||||
filters: Filter<T>[]
|
||||
): T[] {
|
||||
return resources.filter((resource) =>
|
||||
filters.every((filter) => filter.filter(resource))
|
||||
);
|
||||
}
|
||||
|
||||
type useFilteredResourcesReturn<T extends Resource> = {
|
||||
filters: Filter<T>[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
removeFilter: (filter: Filter<T> | string) => void;
|
||||
filteredResources: T[];
|
||||
};
|
||||
|
||||
export function useFilteredResources<T extends Resource>(
|
||||
resources: T[]
|
||||
): useFilteredResourcesReturn<T> {
|
||||
const [filters, setFilters] = useState<Filter<T>[]>([]);
|
||||
|
||||
const addFilter = useCallback(
|
||||
(filter: Filter<T>) => {
|
||||
if (filters.some((f) => f.id === filter.id)) return;
|
||||
setFilters((filters) => [...filters, filter]);
|
||||
},
|
||||
[filters, setFilters]
|
||||
);
|
||||
const removeFilter = useCallback(
|
||||
(filter: Filter<T> | string) => {
|
||||
setFilters((filters) =>
|
||||
filters.filter((f) =>
|
||||
typeof filter === "string" ? f.id !== filter : f !== filter
|
||||
)
|
||||
);
|
||||
},
|
||||
[setFilters]
|
||||
);
|
||||
|
||||
const filteredResources = useCallback(
|
||||
() => filterResources(resources, filters),
|
||||
[resources, filters]
|
||||
);
|
||||
|
||||
return {
|
||||
filters,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
filteredResources: filteredResources(),
|
||||
};
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
|
||||
export function useResizeNotifier(
|
||||
element: HTMLElement | undefined,
|
||||
callback: () => void
|
||||
) {
|
||||
const callBackRef = useRef(callback);
|
||||
useLayoutEffect(() => {
|
||||
callBackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!element) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(
|
||||
withResizeLoopDetection(() => {
|
||||
callBackRef.current!();
|
||||
})
|
||||
);
|
||||
|
||||
resizeObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [element]);
|
||||
}
|
||||
|
||||
function withResizeLoopDetection(callback: () => void) {
|
||||
return (entries: ResizeObserverEntry[], resizeObserver: ResizeObserver) => {
|
||||
const elements = entries.map((entry) => entry.target);
|
||||
|
||||
const rectsBefore = elements.map((element) =>
|
||||
element.getBoundingClientRect()
|
||||
);
|
||||
|
||||
callback();
|
||||
|
||||
const rectsAfter = elements.map((element) =>
|
||||
element.getBoundingClientRect()
|
||||
);
|
||||
|
||||
const changedElements = elements.filter(
|
||||
(_, i) => !areRectSizesEqual(rectsBefore[i], rectsAfter[i])
|
||||
);
|
||||
|
||||
changedElements.forEach((element) =>
|
||||
unobserveUntilNextFrame(element, resizeObserver)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function unobserveUntilNextFrame(
|
||||
element: Element,
|
||||
resizeObserver: ResizeObserver
|
||||
) {
|
||||
resizeObserver.unobserve(element);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
resizeObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
function areRectSizesEqual(rect1: DOMRect, rect2: DOMRect) {
|
||||
return rect1.width === rect2.width && rect1.height === rect2.height;
|
||||
}
|
88
website/src/libs/search.ts
Normal file
88
website/src/libs/search.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { type Filter, useFilteredResources, queryFilter } from "./filter";
|
||||
import type { Resource } from "./store";
|
||||
|
||||
type useSearchControlReturn<T extends Resource> = {
|
||||
filteredResources: T[];
|
||||
searcherTags: string[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
onSearchQueryChange: (query: string) => void;
|
||||
onSearchQuerySubmit: () => void;
|
||||
onSearchBackspace: () => void;
|
||||
onSearchClear: () => void;
|
||||
onSearchTagClick: (index: number) => void;
|
||||
};
|
||||
|
||||
export function useSearchControl<T extends Resource>(
|
||||
resources: T[]
|
||||
): useSearchControlReturn<T> {
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter<T> | null>(null);
|
||||
|
||||
const { filters, addFilter, removeFilter, filteredResources } =
|
||||
useFilteredResources(resources);
|
||||
|
||||
// display tags in searcher (except current filter)
|
||||
const [searcherFilters, setSearcherFilters] = useState<Filter<T>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearcherFilters(
|
||||
filters.filter((f) => !(currentFilter && f === currentFilter))
|
||||
);
|
||||
}, [filters, currentFilter]);
|
||||
|
||||
const onSearchQueryChange = useCallback(
|
||||
(newQuery: string) => {
|
||||
// remove current filter if query is empty
|
||||
if (newQuery === "") {
|
||||
currentFilter && removeFilter(currentFilter);
|
||||
setCurrentFilter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilter = queryFilter<T>(newQuery);
|
||||
// do nothing if filter is not changed
|
||||
if (currentFilter?.id === newFilter.id) return;
|
||||
|
||||
// remove old currentFilter
|
||||
currentFilter && removeFilter(currentFilter);
|
||||
// add new filter
|
||||
setCurrentFilter(newFilter);
|
||||
addFilter(newFilter);
|
||||
},
|
||||
[currentFilter, setCurrentFilter, addFilter, removeFilter]
|
||||
);
|
||||
|
||||
const onSearchQuerySubmit = useCallback(() => {
|
||||
// set current filter to null to make filter permanent
|
||||
setCurrentFilter(null);
|
||||
}, [setCurrentFilter]);
|
||||
|
||||
const onSearchBackspace = useCallback(() => {
|
||||
// remove last filter
|
||||
removeFilter(searcherFilters[searcherFilters.length - 1]);
|
||||
}, [removeFilter, searcherFilters]);
|
||||
|
||||
const onSearchClear = useCallback(() => {
|
||||
// remove all filters
|
||||
searcherFilters.forEach((filter) => removeFilter(filter));
|
||||
}, [removeFilter, searcherFilters]);
|
||||
|
||||
const onSearchTagClick = useCallback(
|
||||
(index: number) => {
|
||||
removeFilter(searcherFilters[index]);
|
||||
},
|
||||
[removeFilter, searcherFilters]
|
||||
);
|
||||
|
||||
return {
|
||||
filteredResources,
|
||||
searcherTags: searcherFilters.map((filter) => filter.displayName),
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user