mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-01 21:26:08 +08:00
feat(examples): manual-pick 前端改由 web-src/ 构建,补充测试
插件市场要求带前端的插件以 web-src/ 源码提交、至少有 1 个测试。ab-plugin pack 不再打包 web-src/,运行时只需要构建出的 web/。
This commit is contained in:
@@ -15,7 +15,7 @@ from . import SDK_VERSION, points
|
||||
from .manifest import ID_RE, MANIFEST_NAME, TOOLING_DIRS, PluginManifest, check
|
||||
|
||||
# 打包时排除的目录名与文件后缀:开发产物、测试与作者侧的工程文件
|
||||
_PACK_EXCLUDE_DIRS = TOOLING_DIRS | {"tests"}
|
||||
_PACK_EXCLUDE_DIRS = TOOLING_DIRS | {"tests", "web-src"} # web-src 是前端源码,运行时只用构建出的 web/
|
||||
_PACK_EXCLUDE_FILES = frozenset({".DS_Store", "pyproject.toml", "uv.lock"})
|
||||
_ZIP_EPOCH = (1980, 1, 1, 0, 0, 0) # 固定时间戳,同样的内容打出同样的 sha256
|
||||
|
||||
|
||||
1
examples/plugins/manual-pick/.gitignore
vendored
Normal file
1
examples/plugins/manual-pick/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
web-src/node_modules
|
||||
@@ -9,6 +9,22 @@
|
||||
- 宿主的只读 API(`/api/v1/bangumi/<id>/torrents`,经 `host.api.get`)
|
||||
- 事件:路由发布 `manual-pick.picked`,组件用 `host.events.on` 订阅并刷新
|
||||
|
||||
## 构建前端
|
||||
|
||||
前端源码在 `web-src/`(TypeScript,Vite 库模式,模板来自 `webui/packages/plugin-ui/template/`),产物写到 `web/index.js`,即清单 `[[plugin.ui]]` 的 `entry`。`web/` 提交在仓库里,供本地开发和 `ab-plugin validate` 使用;插件仓库的 CI 会丢弃它并重新构建。
|
||||
|
||||
```bash
|
||||
cd web-src && pnpm install && pnpm build
|
||||
```
|
||||
|
||||
`@autobangumi/plugin-ui` 以 `link:` 指向仓库内的 `webui/packages/plugin-ui`(只提供类型)。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
uv run pytest -q -c pyproject.toml --rootdir . tests # 在 backend/ 目录下运行
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
|
||||
13
examples/plugins/manual-pick/pyproject.toml
Normal file
13
examples/plugins/manual-pick/pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
# 只用于本地开发与测试,不会被打进插件包(ab-plugin pack 排除它)
|
||||
[project]
|
||||
name = "ab-plugin-manual-pick"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["autobangumi-sdk"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8.0.0"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
testpaths = ["tests"]
|
||||
37
examples/plugins/manual-pick/tests/test_manual_pick.py
Normal file
37
examples/plugins/manual-pick/tests/test_manual_pick.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from manual_pick import ManualPick, Picked
|
||||
|
||||
from ab_sdk.testing import create_plugin
|
||||
|
||||
|
||||
def client():
|
||||
plugin, ctx = create_plugin(ManualPick)
|
||||
app = FastAPI()
|
||||
app.include_router(plugin.api())
|
||||
return TestClient(app), ctx
|
||||
|
||||
|
||||
def test_get_pick_nothing_picked_returns_null():
|
||||
http, _ = client()
|
||||
assert http.get("/picks/1").json() == {"torrent_id": None}
|
||||
|
||||
|
||||
def test_put_pick_saves_selection_per_bangumi():
|
||||
http, ctx = client()
|
||||
assert http.put("/picks/1", json={"torrent_id": 7}).json() == {"torrent_id": 7}
|
||||
assert http.get("/picks/1").json() == {"torrent_id": 7}
|
||||
assert http.get("/picks/2").json() == {"torrent_id": None}
|
||||
assert ctx.kv.data == {"pick:1": 7}
|
||||
|
||||
|
||||
def test_put_pick_publishes_picked_event():
|
||||
http, ctx = client()
|
||||
http.put("/picks/3", json={"torrent_id": 9})
|
||||
assert ctx.bus.published == [Picked(bangumi_id=3, torrent_id=9)]
|
||||
|
||||
|
||||
def test_put_pick_invalid_body_returns_422_and_publishes_nothing():
|
||||
http, ctx = client()
|
||||
assert http.put("/picks/1", json={"torrent_id": "x"}).status_code == 422
|
||||
assert ctx.bus.published == []
|
||||
14
examples/plugins/manual-pick/web-src/package.json
Normal file
14
examples/plugins/manual-pick/web-src/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "ab-plugin-manual-pick-web",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@autobangumi/plugin-ui": "link:../../../../webui/packages/plugin-ui",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.5.5"
|
||||
}
|
||||
}
|
||||
339
examples/plugins/manual-pick/web-src/pnpm-lock.yaml
generated
Normal file
339
examples/plugins/manual-pick/web-src/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,339 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@autobangumi/plugin-ui':
|
||||
specifier: link:../../../../webui/packages/plugin-ui
|
||||
version: link:../../../../webui/packages/plugin-ui
|
||||
typescript:
|
||||
specifier: ^4.9.5
|
||||
version: 4.9.5
|
||||
vite:
|
||||
specifier: ^4.5.5
|
||||
version: 4.5.14
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
esbuild@0.18.20:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
nanoid@3.3.20:
|
||||
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.5.29:
|
||||
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@3.30.0:
|
||||
resolution: {integrity: sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
source-map-js@1.2.2:
|
||||
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
typescript@4.9.5:
|
||||
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
|
||||
vite@4.5.14:
|
||||
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
esbuild@0.18.20:
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
'@esbuild/android-arm64': 0.18.20
|
||||
'@esbuild/android-x64': 0.18.20
|
||||
'@esbuild/darwin-arm64': 0.18.20
|
||||
'@esbuild/darwin-x64': 0.18.20
|
||||
'@esbuild/freebsd-arm64': 0.18.20
|
||||
'@esbuild/freebsd-x64': 0.18.20
|
||||
'@esbuild/linux-arm': 0.18.20
|
||||
'@esbuild/linux-arm64': 0.18.20
|
||||
'@esbuild/linux-ia32': 0.18.20
|
||||
'@esbuild/linux-loong64': 0.18.20
|
||||
'@esbuild/linux-mips64el': 0.18.20
|
||||
'@esbuild/linux-ppc64': 0.18.20
|
||||
'@esbuild/linux-riscv64': 0.18.20
|
||||
'@esbuild/linux-s390x': 0.18.20
|
||||
'@esbuild/linux-x64': 0.18.20
|
||||
'@esbuild/netbsd-x64': 0.18.20
|
||||
'@esbuild/openbsd-x64': 0.18.20
|
||||
'@esbuild/sunos-x64': 0.18.20
|
||||
'@esbuild/win32-arm64': 0.18.20
|
||||
'@esbuild/win32-ia32': 0.18.20
|
||||
'@esbuild/win32-x64': 0.18.20
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
nanoid@3.3.20: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.5.29:
|
||||
dependencies:
|
||||
nanoid: 3.3.20
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.2
|
||||
|
||||
rollup@3.30.0:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
source-map-js@1.2.2: {}
|
||||
|
||||
typescript@4.9.5: {}
|
||||
|
||||
vite@4.5.14:
|
||||
dependencies:
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.5.29
|
||||
rollup: 3.30.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
1
examples/plugins/manual-pick/web-src/src/env.d.ts
vendored
Normal file
1
examples/plugins/manual-pick/web-src/src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
127
examples/plugins/manual-pick/web-src/src/index.ts
Normal file
127
examples/plugins/manual-pick/web-src/src/index.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// 手动选种:番剧详情页标签。宿主在包裹元素的 Shadow DOM 中注入
|
||||
// --ab-* 主题变量,这里直接使用,不内联 tokens.css。
|
||||
import type { AbPluginElement } from '@autobangumi/plugin-ui';
|
||||
|
||||
interface Torrent {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
'zh-CN': {
|
||||
empty: '这条规则还没有种子记录',
|
||||
pick: '选用',
|
||||
picked: '已选',
|
||||
failed: '保存选择失败',
|
||||
},
|
||||
'en-US': {
|
||||
empty: 'This rule has no torrent records yet',
|
||||
pick: 'Pick',
|
||||
picked: 'Picked',
|
||||
failed: 'Failed to save the pick',
|
||||
},
|
||||
};
|
||||
|
||||
class ManualPick
|
||||
extends HTMLElement
|
||||
implements AbPluginElement<'bangumi.detail.tab'>
|
||||
{
|
||||
// 宿主在插入文档前设置;用 declare 不生成类字段,否则构造时会被重置为 undefined
|
||||
declare host: AbPluginElement['host'];
|
||||
declare context: AbPluginElement<'bangumi.detail.tab'>['context'];
|
||||
|
||||
private text = TEXT['en-US'];
|
||||
private bangumiId = 0;
|
||||
private body!: HTMLDivElement;
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
async connectedCallback() {
|
||||
this.text =
|
||||
TEXT[this.host.i18n.locale as keyof typeof TEXT] ?? TEXT['en-US'];
|
||||
this.bangumiId = this.context.bangumiId;
|
||||
const root = this.shadowRoot ?? this.attachShadow({ mode: 'open' });
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
||||
li {
|
||||
display: flex; align-items: center; gap: 12px; padding: 8px 12px;
|
||||
background: var(--ab-color-surface-2); border-radius: var(--ab-radius-sm);
|
||||
}
|
||||
.name { flex: 1; min-width: 0; overflow-wrap: anywhere; font-size: 13px; font-family: var(--ab-font-mono); }
|
||||
.mark { width: 8px; height: 8px; border-radius: 2px; background: var(--ab-color-primary); }
|
||||
button {
|
||||
padding: 4px 10px; border: 1px solid var(--ab-color-border); border-radius: var(--ab-radius-sm);
|
||||
background: var(--ab-color-surface); color: var(--ab-color-text); font: inherit; font-size: 12px; cursor: pointer;
|
||||
}
|
||||
button:focus-visible { outline: 2px solid var(--ab-color-primary); outline-offset: 2px; }
|
||||
p { margin: 0; font-size: 13px; color: var(--ab-color-text-secondary); }
|
||||
</style>
|
||||
<div></div>
|
||||
`;
|
||||
this.body = root.querySelector('div')!;
|
||||
// 别的会话或标签页选种后,打开着的详情页也会刷新
|
||||
this.unsubscribe = this.host.events.on('manual-pick.picked', (payload) => {
|
||||
if ((payload as { bangumi_id?: number } | null)?.bangumi_id === this.bangumiId)
|
||||
this.load();
|
||||
});
|
||||
await this.load();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.unsubscribe?.();
|
||||
}
|
||||
|
||||
private async load() {
|
||||
const [torrents, pick] = await Promise.all([
|
||||
// 宿主的只读 API:以 /api/v1/ 开头的路径只允许 GET
|
||||
this.host.api.get<Torrent[]>(`/api/v1/bangumi/${this.bangumiId}/torrents`),
|
||||
this.host.api.get<{ torrent_id: number | null }>(`picks/${this.bangumiId}`),
|
||||
]);
|
||||
this.render(torrents, pick.torrent_id);
|
||||
}
|
||||
|
||||
private render(torrents: Torrent[], pickedId: number | null) {
|
||||
this.body.replaceChildren();
|
||||
if (!torrents.length) {
|
||||
const empty = document.createElement('p');
|
||||
empty.textContent = this.text.empty;
|
||||
this.body.append(empty);
|
||||
return;
|
||||
}
|
||||
const list = document.createElement('ul');
|
||||
for (const torrent of torrents) {
|
||||
const item = document.createElement('li');
|
||||
const name = document.createElement('span');
|
||||
name.className = 'name';
|
||||
name.textContent = torrent.name;
|
||||
item.append(name);
|
||||
if (torrent.id === pickedId) {
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'mark';
|
||||
mark.title = this.text.picked;
|
||||
item.append(mark);
|
||||
} else {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = this.text.pick;
|
||||
button.addEventListener('click', () => this.pick(torrent.id));
|
||||
item.append(button);
|
||||
}
|
||||
list.append(item);
|
||||
}
|
||||
this.body.append(list);
|
||||
}
|
||||
|
||||
private async pick(torrentId: number) {
|
||||
try {
|
||||
await this.host.api.put(`picks/${this.bangumiId}`, { torrent_id: torrentId });
|
||||
} catch {
|
||||
this.host.toast(this.text.failed, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('ab-plugin-manual-pick')) {
|
||||
customElements.define('ab-plugin-manual-pick', ManualPick);
|
||||
}
|
||||
23
examples/plugins/manual-pick/web-src/vite.config.ts
Normal file
23
examples/plugins/manual-pick/web-src/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// 前端插件组件的 Vite 库模式模板。
|
||||
//
|
||||
// 把本目录复制到插件根目录下(如 my-plugin/web-src/),把 package.json 中
|
||||
// @autobangumi/plugin-ui 的 link: 路径改为指向 webui/packages/plugin-ui,
|
||||
// 然后 `pnpm install && pnpm build`,产物写到插件的 web/index.js,
|
||||
// 清单中对应写 `entry = "web/index.js"`。
|
||||
//
|
||||
// 产物必须是单个自包含的 ES module:浏览器里没有裸模块名解析,CSP
|
||||
// (script-src 'self')也不允许 import map 等内联脚本与远程脚本,
|
||||
// 所以依赖全部打包进来,不设 external。
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['es'],
|
||||
fileName: () => 'index.js',
|
||||
},
|
||||
outDir: '../web',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@@ -1,26 +1,25 @@
|
||||
// 手动选种:番剧详情页标签。无需构建;宿主在包裹元素的 Shadow DOM 中注入
|
||||
// --ab-* 主题变量,这里直接使用。
|
||||
const TEXT = {
|
||||
'zh-CN': {
|
||||
empty: '这条规则还没有种子记录',
|
||||
pick: '选用',
|
||||
picked: '已选',
|
||||
failed: '保存选择失败',
|
||||
},
|
||||
'en-US': {
|
||||
empty: 'This rule has no torrent records yet',
|
||||
pick: 'Pick',
|
||||
picked: 'Picked',
|
||||
failed: 'Failed to save the pick',
|
||||
const s = {
|
||||
"zh-CN": {
|
||||
empty: "这条规则还没有种子记录",
|
||||
pick: "选用",
|
||||
picked: "已选",
|
||||
failed: "保存选择失败"
|
||||
},
|
||||
"en-US": {
|
||||
empty: "This rule has no torrent records yet",
|
||||
pick: "Pick",
|
||||
picked: "Picked",
|
||||
failed: "Failed to save the pick"
|
||||
}
|
||||
};
|
||||
|
||||
class ManualPick extends HTMLElement {
|
||||
class c extends HTMLElement {
|
||||
constructor() {
|
||||
super(...arguments), this.text = s["en-US"], this.bangumiId = 0;
|
||||
}
|
||||
async connectedCallback() {
|
||||
this.text = TEXT[this.host.i18n.locale] ?? TEXT['en-US'];
|
||||
this.bangumiId = this.context.bangumiId;
|
||||
const root = this.shadowRoot ?? this.attachShadow({ mode: 'open' });
|
||||
root.innerHTML = `
|
||||
this.text = s[this.host.i18n.locale] ?? s["en-US"], this.bangumiId = this.context.bangumiId;
|
||||
const t = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
||||
t.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
||||
@@ -38,69 +37,48 @@ class ManualPick extends HTMLElement {
|
||||
p { margin: 0; font-size: 13px; color: var(--ab-color-text-secondary); }
|
||||
</style>
|
||||
<div></div>
|
||||
`;
|
||||
this.body = root.querySelector('div');
|
||||
// 别的会话或标签页选种后,打开着的详情页也会刷新
|
||||
this.unsubscribe = this.host.events.on('manual-pick.picked', (payload) => {
|
||||
if (payload?.bangumi_id === this.bangumiId) this.load();
|
||||
});
|
||||
await this.load();
|
||||
`, this.body = t.querySelector("div"), this.unsubscribe = this.host.events.on("manual-pick.picked", (i) => {
|
||||
(i == null ? void 0 : i.bangumi_id) === this.bangumiId && this.load();
|
||||
}), await this.load();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.unsubscribe?.();
|
||||
var t;
|
||||
(t = this.unsubscribe) == null || t.call(this);
|
||||
}
|
||||
|
||||
async load() {
|
||||
const [torrents, pick] = await Promise.all([
|
||||
const [t, i] = await Promise.all([
|
||||
// 宿主的只读 API:以 /api/v1/ 开头的路径只允许 GET
|
||||
this.host.api.get(`/api/v1/bangumi/${this.bangumiId}/torrents`),
|
||||
this.host.api.get(`picks/${this.bangumiId}`),
|
||||
this.host.api.get(`picks/${this.bangumiId}`)
|
||||
]);
|
||||
this.render(torrents, pick.torrent_id);
|
||||
this.render(t, i.torrent_id);
|
||||
}
|
||||
|
||||
render(torrents, pickedId) {
|
||||
this.body.replaceChildren();
|
||||
if (!torrents.length) {
|
||||
const empty = document.createElement('p');
|
||||
empty.textContent = this.text.empty;
|
||||
this.body.append(empty);
|
||||
render(t, i) {
|
||||
if (this.body.replaceChildren(), !t.length) {
|
||||
const n = document.createElement("p");
|
||||
n.textContent = this.text.empty, this.body.append(n);
|
||||
return;
|
||||
}
|
||||
const list = document.createElement('ul');
|
||||
for (const torrent of torrents) {
|
||||
const item = document.createElement('li');
|
||||
const name = document.createElement('span');
|
||||
name.className = 'name';
|
||||
name.textContent = torrent.name;
|
||||
item.append(name);
|
||||
if (torrent.id === pickedId) {
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'mark';
|
||||
mark.title = this.text.picked;
|
||||
item.append(mark);
|
||||
const r = document.createElement("ul");
|
||||
for (const n of t) {
|
||||
const a = document.createElement("li"), o = document.createElement("span");
|
||||
if (o.className = "name", o.textContent = n.name, a.append(o), n.id === i) {
|
||||
const e = document.createElement("span");
|
||||
e.className = "mark", e.title = this.text.picked, a.append(e);
|
||||
} else {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = this.text.pick;
|
||||
button.addEventListener('click', () => this.pick(torrent.id));
|
||||
item.append(button);
|
||||
const e = document.createElement("button");
|
||||
e.type = "button", e.textContent = this.text.pick, e.addEventListener("click", () => this.pick(n.id)), a.append(e);
|
||||
}
|
||||
list.append(item);
|
||||
r.append(a);
|
||||
}
|
||||
this.body.append(list);
|
||||
this.body.append(r);
|
||||
}
|
||||
|
||||
async pick(torrentId) {
|
||||
async pick(t) {
|
||||
try {
|
||||
await this.host.api.put(`picks/${this.bangumiId}`, { torrent_id: torrentId });
|
||||
await this.host.api.put(`picks/${this.bangumiId}`, { torrent_id: t });
|
||||
} catch {
|
||||
this.host.toast(this.text.failed, 'error');
|
||||
this.host.toast(this.text.failed, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('ab-plugin-manual-pick')) {
|
||||
customElements.define('ab-plugin-manual-pick', ManualPick);
|
||||
}
|
||||
customElements.get("ab-plugin-manual-pick") || customElements.define("ab-plugin-manual-pick", c);
|
||||
|
||||
Reference in New Issue
Block a user