init
@@ -0,0 +1,10 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "python"
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
runtime_version = "3.x.x"
|
||||||
|
|
||||||
|
[[transformers]]
|
||||||
|
name = "ruff"
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
|
||||||
|
# UV lock
|
||||||
|
uv.lock
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0 # 选择最新的稳定版本
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace # 去除多余的空格
|
||||||
|
- id: end-of-file-fixer # 确保文件以空行结束
|
||||||
|
- id: check-yaml # 检查 YAML 文件格式
|
||||||
|
- id: check-added-large-files # 检查提交中是否包含过大的文件
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.13.1
|
||||||
|
hooks:
|
||||||
|
# Run the linter.
|
||||||
|
- id: ruff
|
||||||
|
args: [ --fix ]
|
||||||
|
# Run the formatter.
|
||||||
|
- id: ruff-format
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
[lint]
|
||||||
|
select = [
|
||||||
|
"F", # Pyflakes
|
||||||
|
"E", # pycodestyle error
|
||||||
|
"W", # pycodestyle warning
|
||||||
|
"I", # isort
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"ASYNC", # fflake8-async
|
||||||
|
"S", # flake8-bandit
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"ISC", # flake8-implicit-str-concat
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"T20", # flake8-print
|
||||||
|
"PYI", # flake8-pyi
|
||||||
|
"PT", # flake8-pytest-style
|
||||||
|
"Q", # flake8-quotes
|
||||||
|
"RSE", # flake8-raise
|
||||||
|
"RET", # flake8-return
|
||||||
|
"SLOT", # flake8-slots
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
"TID", # flake8-tidy-imports
|
||||||
|
"TCH", # flake8-type-checking
|
||||||
|
"ARG", # flake8-unused-arguments
|
||||||
|
"PTH", # flake8-use-pathlib
|
||||||
|
"ERA", # eradicate
|
||||||
|
"PL", # Pylint
|
||||||
|
"TRY", # tryceratops
|
||||||
|
"PERF", # Perflint
|
||||||
|
# "FURB", # refurb
|
||||||
|
"RUF", # Ruff-specific rules
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E402", # module-import-not-at-top-of-file
|
||||||
|
"E501", # line-too-long
|
||||||
|
"B009", # get-attr-with-constant
|
||||||
|
"B010", # set-attr-with-constant
|
||||||
|
"PLC0414", # useless-import-alias
|
||||||
|
"PLR0913", # too-many-arguments
|
||||||
|
"TRY003", # raise-vanilla-args
|
||||||
|
"RUF001", # ambiguous-unicode-character-string
|
||||||
|
"RUF002", # ambiguous-unicode-character-docstring
|
||||||
|
"RUF003", # ambiguous-unicode-character-comment
|
||||||
|
|
||||||
|
# Avoid formatter conflicts, see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
|
||||||
|
"W191", # tab-indentation
|
||||||
|
"E111", # indentation-with-invalid-multiple
|
||||||
|
"E114", # indentation-with-invalid-multiple-comment
|
||||||
|
"E117", # over-indented
|
||||||
|
"D206", # indent-with-spaces
|
||||||
|
"D300", # triple-single-quotes
|
||||||
|
"Q000", # bad-quotes-inline-string
|
||||||
|
"Q001", # bad-quotes-multiline-string
|
||||||
|
"Q002", # bad-quotes-docstring
|
||||||
|
"Q003", # avoidable-escaped-quote
|
||||||
|
"COM812", # missing-trailing-comma
|
||||||
|
"COM819", # prohibited-trailing-comma
|
||||||
|
"ISC001", # single-line-implicit-string-concatenation
|
||||||
|
"ISC002", # multi-line-implicit-string-concatenation
|
||||||
|
]
|
||||||
|
unfixable = [
|
||||||
|
"F401", # unused-import
|
||||||
|
"F841", # unused-variable
|
||||||
|
"ERA001", # commented-out-code
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 divandia
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://v2.nonebot.dev/store"><img src="https://github.com/Cvandia/nonebot-plugin-whateat-pic/blob/main/res/ico.jpg" width="180" height="180" alt="NoneBotPluginLogo"></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# nonebot-plugin-whateat-pic
|
||||||
|
|
||||||
|
_⭐基于Nonebot2的一款今天吃什么喝什么的插件⭐_
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://www.python.org/downloads/release/python-390/"><img src="https://img.shields.io/badge/python-3.8+-blue"></a> <a href=""><img src="https://img.shields.io/badge/QQ-1141538825-yellow"></a> <a href="https://github.com/Cvandia/nonebot-plugin-whateat-pic/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue"></a> <a href="https://v2.nonebot.dev/"><img src="https://img.shields.io/badge/Nonebot2-rc1+-red"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## ⭐ 介绍
|
||||||
|
|
||||||
|
一款离线版决定今天吃喝什么的nb2插件,功能及其简单。
|
||||||
|
~~抄袭~~改编自hosinoBot的插件[今天吃什么](https://github.com/A-kirami/whattoeat)
|
||||||
|
有不足的地方还请指出
|
||||||
|
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
### 或者你有啥关于该插件的新想法的,可以提issue或者pr (>A<)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 💿 安装
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>安装</summary>
|
||||||
|
|
||||||
|
pip 安装
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install nonebot-plugin-whateat-pic
|
||||||
|
```
|
||||||
|
|
||||||
|
nb-cli安装
|
||||||
|
|
||||||
|
```
|
||||||
|
nb plugin install nonebot-plugin-whateat-pic --upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>注意</summary>
|
||||||
|
|
||||||
|
由于包含有图片,包容量较大,推荐镜像站下载
|
||||||
|
|
||||||
|
清华源```https://pypi.tuna.tsinghua.edu.cn/simple```
|
||||||
|
|
||||||
|
阿里源```https://mirrors.aliyun.com/pypi/simple/```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## ⚙️ 配置
|
||||||
|
### 在env.中添加以下配置
|
||||||
|
|
||||||
|
|名称|类型|默认值|范围|说明|
|
||||||
|
|:-----:|:----:|:----:|:------:|:------|
|
||||||
|
|whateat_cd|int|10|0-9999|内置触发cd|
|
||||||
|
|whateat_max|int|0|0-9999|每日用户触发的最大次数,默认0时为无上限|
|
||||||
|
|
||||||
|
|
||||||
|
> 机器的名字“脑积水”是默认值,(我不相信有人不会配置nonebot2的nick_name)
|
||||||
|
|
||||||
|
## ⭐ 使用
|
||||||
|
|
||||||
|
### 指令:
|
||||||
|
| 指令 | 需要@ | 范围 | 说明 |权限|
|
||||||
|
|:-----:|:----:|:----:|:----:|:----:|
|
||||||
|
|今天早上吃什么|否|私聊、群聊|随机发送食物|任何|
|
||||||
|
|今天早上喝什么|否|私聊、群聊|随机发送饮品|任何|
|
||||||
|
|查看全部菜单|否|私聊、群聊|查看全部菜单|任何|
|
||||||
|
|查看菜单|否|私聊、群聊|查看指定菜单|任何|
|
||||||
|
|添加菜单|否|私聊、群聊|自定义添加菜单|群主,超管,管理员|
|
||||||
|
|删除菜单|否|私聊、群聊|自定义删除菜单|群主,超管,管理员|
|
||||||
|
|
||||||
|
**注意**
|
||||||
|
|
||||||
|
默认情况下, 您应该在指令前加上命令前缀, 通常是 /
|
||||||
|
|
||||||
|
## 🌙 未来
|
||||||
|
- [x] 或许添加更多的美食图片吧……
|
||||||
|
- [x] 添加更多功能
|
||||||
|
- [x] 自定义添加菜单,饮品(~~我懒,或者帮我写个?~~)QwQ(~~最后还是自己写~~)
|
||||||
|
- [x] 图片化```全部菜单```
|
||||||
|
- [x] 细化分类菜单,饮料(懒癌患者,>a<)
|
||||||
|
|
||||||
|
--- 喜欢记得点个star⭐---
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from nonebot import require
|
||||||
|
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna")
|
||||||
|
require("nonebot_plugin_localstore")
|
||||||
|
require("nonebot_plugin_apscheduler")
|
||||||
|
|
||||||
|
from . import check_on_statup as _
|
||||||
|
from . import matcher as _ # noqa
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="今天吃什么(图片版)",
|
||||||
|
description="随机发送吃的或者喝的图片",
|
||||||
|
usage="""
|
||||||
|
今天吃什么
|
||||||
|
今天喝什么
|
||||||
|
今天蜜什么
|
||||||
|
添加菜单
|
||||||
|
删除菜单
|
||||||
|
查看菜单
|
||||||
|
""",
|
||||||
|
type="application",
|
||||||
|
homepage="https://github.com/tamakyi/nonebot-plugin-whateat-pic",
|
||||||
|
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
|
||||||
|
)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from nonebot import get_driver
|
||||||
|
from nonebot.log import logger
|
||||||
|
from rich.progress import Progress
|
||||||
|
|
||||||
|
from .config import config
|
||||||
|
|
||||||
|
available_urls = [
|
||||||
|
"https://raw.githubusercontent.com/tamakyi/nonebot-plugin-whateat-pic/",
|
||||||
|
"https://fastly.jsdelivr.net/gh/tamakyi/nonebot-plugin-whateat-pic@",
|
||||||
|
"https://raw.gitmirror.com/tamakyi/nonebot-plugin-whateat-pic/",
|
||||||
|
"https://ghproxy.cfd/https:/raw.githubusercontent.com/tamakyi/nonebot-plugin-whateat-pic/",
|
||||||
|
"https://ghfast.top/https:/raw.githubusercontent.com/tamakyi/nonebot-plugin-whateat-pic/",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def check_resource():
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
|
||||||
|
async def _download(client: httpx.AsyncClient, name: str):
|
||||||
|
async with semaphore:
|
||||||
|
for base_url in available_urls:
|
||||||
|
url = base_url + "refs/heads/main/res/" + name
|
||||||
|
try:
|
||||||
|
resp = await client.get(url, timeout=20, follow_redirects=True)
|
||||||
|
if resp.raise_for_status():
|
||||||
|
logger.debug(f"{url} download success!")
|
||||||
|
return resp.content
|
||||||
|
except httpx.HTTPError:
|
||||||
|
pass
|
||||||
|
logger.warning(f"{url} download failed!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 下载资源列表
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
if content := await _download(client, "download_list.json"):
|
||||||
|
resource_list = json.loads(content.decode("utf-8"))
|
||||||
|
else:
|
||||||
|
logger.warning("download_list.json not found!")
|
||||||
|
return
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
logger.warning("download_list.json is not a valid json file!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查资源目录下文件是否完整
|
||||||
|
drink_pic_path = Path(config.whatpic_res_path) / "drink_pic" # 本地资源目录
|
||||||
|
eat_pic_path = Path(config.whatpic_res_path) / "eat_pic" # 本地资源目录
|
||||||
|
me_pic_path = Path(config.whatpic_res_path) / "me_pic"
|
||||||
|
# TODO: 检查文件是否完整
|
||||||
|
|
||||||
|
# 创建下载任务列表
|
||||||
|
download_list: list[tuple[Path, str]] = []
|
||||||
|
|
||||||
|
# 添加下载任务
|
||||||
|
for item in resource_list["drink_pic"]:
|
||||||
|
path_name = (
|
||||||
|
"drink_pic/" + item["name"]
|
||||||
|
) # 在线资源路径: github.com/Cvandia/nonebot-plugin-whateat-pic/main/res/+ path_name
|
||||||
|
download_list.append((drink_pic_path / item["name"], path_name))
|
||||||
|
for item in resource_list["eat_pic"]:
|
||||||
|
path_name = "eat_pic/" + item["name"] # 在线资源路径
|
||||||
|
download_list.append((eat_pic_path / item["name"], path_name))
|
||||||
|
for item in resource_list["me_pic"]:
|
||||||
|
path_name = "me_pic/" + item["name"] # 在线资源路径
|
||||||
|
download_list.append((me_pic_path / item["name"], path_name))
|
||||||
|
|
||||||
|
if download_list:
|
||||||
|
logger.info("Downloading images ...")
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 下载资源
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
exist_count = 0
|
||||||
|
|
||||||
|
async def download_image(file_path: Path, file_name: str):
|
||||||
|
if file_path.exists():
|
||||||
|
logger.debug(f"{file_path} already exists, skipping download.")
|
||||||
|
nonlocal exist_count
|
||||||
|
exist_count += 1
|
||||||
|
return
|
||||||
|
if content := await _download(client, file_name):
|
||||||
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with file_path.open("wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
with Progress(
|
||||||
|
*Progress.get_default_columns(), "[yellow]{task.completed}/{task.total}"
|
||||||
|
) as progress:
|
||||||
|
progress_task = progress.add_task(
|
||||||
|
"[green]Downloading pic...", total=len(download_list)
|
||||||
|
)
|
||||||
|
tasks = [
|
||||||
|
download_image(file_path, file_name)
|
||||||
|
for file_path, file_name in download_list
|
||||||
|
]
|
||||||
|
for task in asyncio.as_completed(tasks):
|
||||||
|
await task
|
||||||
|
progress.update(progress_task, advance=1)
|
||||||
|
|
||||||
|
logger.info(f"Downloaded {len(download_list) - exist_count} images.")
|
||||||
|
|
||||||
|
|
||||||
|
driver = get_driver()
|
||||||
|
|
||||||
|
|
||||||
|
@driver.on_startup
|
||||||
|
async def on_startup():
|
||||||
|
logger.info("Checking resources...")
|
||||||
|
task = asyncio.create_task(check_resource())
|
||||||
|
await task
|
||||||
|
|
||||||
|
|
||||||
|
# 测试代码
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
from .config import config
|
||||||
|
|
||||||
|
cd = config.whateat_cd
|
||||||
|
max_count = config.whateat_max
|
||||||
|
|
||||||
|
|
||||||
|
def check_iscd(last_time: float) -> tuple[bool, float, float]:
|
||||||
|
"""
|
||||||
|
判断是否在冷却时间内
|
||||||
|
|
||||||
|
Args:
|
||||||
|
last_time(float): 上次使用时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- tuple[bool, float, float]: 是否在冷却时间内, 剩余冷却时间, 当前时间
|
||||||
|
"""
|
||||||
|
now_time = time.time()
|
||||||
|
if now_time - last_time < cd:
|
||||||
|
return True, cd - (now_time - last_time), last_time
|
||||||
|
return False, 0, now_time
|
||||||
|
|
||||||
|
|
||||||
|
def check_ismax(message: Event, user_count: dict) -> tuple[bool, dict]:
|
||||||
|
"""
|
||||||
|
判断是否达到最大次数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message(Event): 消息事件
|
||||||
|
user_count(dict): 用户使用次数记录
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- tuple[bool, dict]: 是否达到最大次数, 用户使用次数记录
|
||||||
|
"""
|
||||||
|
user_id = message.get_user_id()
|
||||||
|
if max_count == 0:
|
||||||
|
return False, {}
|
||||||
|
if user_id not in user_count:
|
||||||
|
user_count[f"{user_id}"] = 0
|
||||||
|
if user_count[f"{user_id}"] < max_count:
|
||||||
|
user_count[f"{user_id}"] += 1
|
||||||
|
return False, user_count
|
||||||
|
return True, user_count
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import nonebot_plugin_localstore as store
|
||||||
|
from nonebot.plugin import get_plugin_config
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
whateat_cd: int = Field(default=10)
|
||||||
|
whateat_max: int = Field(default=0)
|
||||||
|
whatpic_res_path: str = Field(default=str(store.get_plugin_cache_dir()))
|
||||||
|
|
||||||
|
|
||||||
|
config = get_plugin_config(Config)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from nonebot.log import logger
|
||||||
|
|
||||||
|
from .config import config
|
||||||
|
|
||||||
|
|
||||||
|
def save_pic(img: bytes, img_type: str, name: str) -> None:
|
||||||
|
"""
|
||||||
|
保存图片
|
||||||
|
"""
|
||||||
|
save_path = Path(config.whatpic_res_path) / f"{img_type}_pic" / (name + ".jpg")
|
||||||
|
if isinstance(img, bytes):
|
||||||
|
with Path.open(save_path, "wb") as f:
|
||||||
|
f.write(img)
|
||||||
|
else:
|
||||||
|
logger.error(f"img must be bytes, but got {type(img)}")
|
||||||
|
raise TypeError("img must be bytes")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_pic(img_type: str, name: str) -> None:
|
||||||
|
"""
|
||||||
|
删除图片
|
||||||
|
"""
|
||||||
|
delete_path = Path(config.whatpic_res_path) / f"{img_type}_pic" / (name + ".jpg")
|
||||||
|
if delete_path.exists():
|
||||||
|
try:
|
||||||
|
delete_path.unlink()
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Error: {e}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(f"{delete_path} not found")
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import io
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from nonebot import get_driver
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.permission import SUPERUSER
|
||||||
|
from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna
|
||||||
|
from nonebot_plugin_alconna.uniseg import Image, Text, UniMessage
|
||||||
|
from nonebot_plugin_alconna.uniseg.tools import image_fetch
|
||||||
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
|
||||||
|
from .check_user_data import check_iscd, check_ismax
|
||||||
|
from .files import delete_pic, save_pic
|
||||||
|
from .menu import Menu
|
||||||
|
from .random_pic import random_pic
|
||||||
|
|
||||||
|
NICKNAME = list(get_driver().config.nickname)
|
||||||
|
BOT_NAME = NICKNAME[0] if NICKNAME else "脑积水"
|
||||||
|
|
||||||
|
TIME = 0.0
|
||||||
|
USER_DATA = {}
|
||||||
|
MAX_MSG = [
|
||||||
|
"你今天吃的够多了!不许再吃了(´-ωก`)",
|
||||||
|
"吃吃吃,就知道吃,你都吃饱了!明天再来(▼皿▼#)",
|
||||||
|
"(*`へ´*)你猜我会不会再给你发好吃的图片",
|
||||||
|
f"没得吃的了,{BOT_NAME}的食物都被你这坏蛋吃光了!",
|
||||||
|
"你在等我给你发好吃的?做梦哦!你都吃那么多了,不许再吃了!ヽ(≧Д≦)ノ",
|
||||||
|
]
|
||||||
|
|
||||||
|
eat_pic_matcher = on_alconna(
|
||||||
|
Alconna("今天吃什么"),
|
||||||
|
use_cmd_start=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
drink_pic_matcher = on_alconna(
|
||||||
|
Alconna("今天喝什么"),
|
||||||
|
use_cmd_start=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
me_pic_matcher = on_alconna(
|
||||||
|
Alconna("今天蜜什么"),
|
||||||
|
use_cmd_start=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
view_menu_matcher = on_alconna(
|
||||||
|
Alconna("全部菜单", Args["img_type?", str]),
|
||||||
|
use_cmd_start=True,
|
||||||
|
aliases=("查看菜单", "查看菜品"),
|
||||||
|
)
|
||||||
|
|
||||||
|
add_menu_matcher = on_alconna(
|
||||||
|
Alconna(
|
||||||
|
"添加菜单", Args["name?", str], Args["img_type?", str], Args["img?", Image]
|
||||||
|
),
|
||||||
|
use_cmd_start=True,
|
||||||
|
permission=SUPERUSER,
|
||||||
|
)
|
||||||
|
|
||||||
|
del_menu_matcher = on_alconna(
|
||||||
|
Alconna("删除菜单", Args["name?", str], Args["img_type?", str]),
|
||||||
|
use_cmd_start=True,
|
||||||
|
permission=SUPERUSER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
eat_pic_matcher.shortcut(
|
||||||
|
r"^[今|明|后]?[天|日]?(早|中|晚)?(上|午|餐|饭|夜宵|宵夜|早|晚)吃(什么|啥|点啥)$",
|
||||||
|
fuzzy=False,
|
||||||
|
prefix=True,
|
||||||
|
)
|
||||||
|
drink_pic_matcher.shortcut(
|
||||||
|
r"^[今|明|后]?[天|日]?(早|中|晚)?(上|午|餐|饭|夜宵|宵夜|早|晚)喝(什么|啥|点啥)$",
|
||||||
|
fuzzy=False,
|
||||||
|
prefix=True,
|
||||||
|
)
|
||||||
|
me_pic_matcher.shortcut(
|
||||||
|
r"^(我要蜜)?[今|明|后]?[天|日]?(早|中|晚)?(上|午|餐|饭|夜宵|宵夜|早|晚)蜜(什么|啥|点啥)$",
|
||||||
|
fuzzy=False,
|
||||||
|
prefix=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@eat_pic_matcher.handle()
|
||||||
|
async def handle_eat_pic(event: Event):
|
||||||
|
global TIME
|
||||||
|
global USER_DATA
|
||||||
|
check_max_result, USER_DATA = check_ismax(event, USER_DATA)
|
||||||
|
check_result, remain_time, TIME = check_iscd(TIME)
|
||||||
|
if check_max_result:
|
||||||
|
await UniMessage.text(secrets.choice(MAX_MSG)).finish()
|
||||||
|
elif check_result:
|
||||||
|
await UniMessage.text(f"cd冷却中,还有{remain_time:.2f}秒").finish()
|
||||||
|
else:
|
||||||
|
pic_path, pic_name = random_pic("eat")
|
||||||
|
send_msg: UniMessage = UniMessage(Text(f"🎉{BOT_NAME}建议你吃🎉\n{pic_name}"))
|
||||||
|
async with aiofiles.open(pic_path, "rb") as f:
|
||||||
|
send_msg.append(Image(raw=await f.read()))
|
||||||
|
await send_msg.finish()
|
||||||
|
|
||||||
|
|
||||||
|
@drink_pic_matcher.handle()
|
||||||
|
async def handle_drink_pic(event: Event):
|
||||||
|
global TIME
|
||||||
|
global USER_DATA
|
||||||
|
check_max_result, USER_DATA = check_ismax(event, USER_DATA)
|
||||||
|
check_result, remain_time, TIME = check_iscd(TIME)
|
||||||
|
if check_max_result:
|
||||||
|
await UniMessage.text(secrets.choice(MAX_MSG)).finish()
|
||||||
|
elif check_result:
|
||||||
|
await UniMessage.text(f"cd冷却中,还有{remain_time:.2f}秒").finish()
|
||||||
|
else:
|
||||||
|
pic_path, pic_name = random_pic("drink")
|
||||||
|
send_msg: UniMessage = UniMessage(Text(f"🎉{BOT_NAME}建议你喝🎉\n{pic_name}"))
|
||||||
|
async with aiofiles.open(pic_path, "rb") as f:
|
||||||
|
send_msg.append(Image(raw=await f.read()))
|
||||||
|
await send_msg.finish()
|
||||||
|
|
||||||
|
|
||||||
|
@me_pic_matcher.handle()
|
||||||
|
async def handle_me_pic(event: Event):
|
||||||
|
global TIME
|
||||||
|
global USER_DATA
|
||||||
|
check_max_result, USER_DATA = check_ismax(event, USER_DATA)
|
||||||
|
check_result, remain_time, TIME = check_iscd(TIME)
|
||||||
|
if check_max_result:
|
||||||
|
await UniMessage.text(secrets.choice(MAX_MSG)).finish()
|
||||||
|
elif check_result:
|
||||||
|
await UniMessage.text(f"cd冷却中,还有{remain_time:.2f}秒").finish()
|
||||||
|
else:
|
||||||
|
pic_path, pic_name = random_pic("me")
|
||||||
|
send_msg = UniMessage(Text(f"🎉{BOT_NAME}建议你喝🎉\n{pic_name}"))
|
||||||
|
send_msg.append(Image(path=pic_path))
|
||||||
|
await send_msg.finish()
|
||||||
|
|
||||||
|
|
||||||
|
@view_menu_matcher.handle()
|
||||||
|
async def handle_view_menu(img_type: Match[str]):
|
||||||
|
if img_type.available:
|
||||||
|
view_menu_matcher.set_path_arg("img_type", img_type.result)
|
||||||
|
|
||||||
|
|
||||||
|
@view_menu_matcher.got_path("img_type", prompt=f"请告诉{BOT_NAME}具体菜单类型吧")
|
||||||
|
async def _(img_type: str):
|
||||||
|
menu_type = img_type.strip()
|
||||||
|
if menu_type in ["菜单", "菜品"]:
|
||||||
|
menu_type = "eat"
|
||||||
|
elif menu_type in ["饮料", "饮品"]:
|
||||||
|
menu_type = "drink"
|
||||||
|
elif menu_type in ["蜜雪", "蜜", "蜜雪冰城"]:
|
||||||
|
menu_type = "me"
|
||||||
|
else:
|
||||||
|
await UniMessage.text("菜单类型错误,请重新输入").finish()
|
||||||
|
|
||||||
|
try:
|
||||||
|
menu = Menu(menu_type)
|
||||||
|
send_msg_list = UniMessage(Text("菜单如下:"))
|
||||||
|
for img in menu.draw_menu():
|
||||||
|
img_bytesio = io.BytesIO()
|
||||||
|
img.save(img_bytesio, format="JPEG")
|
||||||
|
send_msg_list.append(Image(raw=img_bytesio)) # type: ignore
|
||||||
|
await send_msg_list.finish()
|
||||||
|
except OSError as e:
|
||||||
|
await UniMessage.text(f"没有找到菜单,请稍后重试\n{e}").finish()
|
||||||
|
|
||||||
|
|
||||||
|
@add_menu_matcher.handle()
|
||||||
|
async def _(name: Match[str], img_type: Match[str]):
|
||||||
|
if name.available:
|
||||||
|
add_menu_matcher.set_path_arg("name", name.result)
|
||||||
|
if img_type.available:
|
||||||
|
add_menu_matcher.set_path_arg("img_type", img_type.result)
|
||||||
|
|
||||||
|
|
||||||
|
@add_menu_matcher.got_path("name", prompt=f"请告诉{BOT_NAME}具体菜名或者饮品名吧")
|
||||||
|
async def _(name: str):
|
||||||
|
if not name:
|
||||||
|
await UniMessage.text("菜名不能为空,请重新输入").finish()
|
||||||
|
|
||||||
|
|
||||||
|
@add_menu_matcher.got_path("img_type", prompt=f"请告诉{BOT_NAME}具体菜单类型吧")
|
||||||
|
async def _(img_type: str):
|
||||||
|
if img_type in ["菜品", "菜单"]:
|
||||||
|
add_menu_matcher.set_path_arg("img_type", "eat")
|
||||||
|
elif img_type in ["饮料", "饮品"]:
|
||||||
|
add_menu_matcher.set_path_arg("img_type", "drink")
|
||||||
|
elif img_type in ["蜜雪", "蜜", "蜜雪冰城"]:
|
||||||
|
add_menu_matcher.set_path_arg("img_type", "me")
|
||||||
|
else:
|
||||||
|
await UniMessage.text("菜单类型错误,请重新输入").finish()
|
||||||
|
|
||||||
|
|
||||||
|
@add_menu_matcher.got_path(
|
||||||
|
"img", prompt=f"请告诉{BOT_NAME}图片吧", middleware=image_fetch
|
||||||
|
)
|
||||||
|
async def _(
|
||||||
|
name: str,
|
||||||
|
img_type: str,
|
||||||
|
img: bytes,
|
||||||
|
):
|
||||||
|
if not img:
|
||||||
|
await UniMessage.text("图片不能为空,请重新输入").finish()
|
||||||
|
try:
|
||||||
|
save_pic(img, img_type=img_type, name=name)
|
||||||
|
await UniMessage.text(f"成功添加{name}").finish()
|
||||||
|
except OSError as e:
|
||||||
|
await UniMessage.text(f"添加失败,请稍后重试\n{e}").finish()
|
||||||
|
|
||||||
|
|
||||||
|
@del_menu_matcher.handle()
|
||||||
|
async def _(name: Match[str], img_type: Match[str]):
|
||||||
|
if name.available:
|
||||||
|
del_menu_matcher.set_path_arg("name", name.result)
|
||||||
|
if img_type.available:
|
||||||
|
del_menu_matcher.set_path_arg("img_type", img_type.result)
|
||||||
|
|
||||||
|
|
||||||
|
@del_menu_matcher.got_path("name", prompt=f"请告诉{BOT_NAME}具体菜名或者饮品名吧")
|
||||||
|
async def _(name: str):
|
||||||
|
if not name:
|
||||||
|
await UniMessage.text("菜名不能为空,请重新输入").finish()
|
||||||
|
|
||||||
|
|
||||||
|
@del_menu_matcher.got_path("img_type", prompt=f"请告诉{BOT_NAME}具体菜单类型吧")
|
||||||
|
async def _(img_type: str, name: str):
|
||||||
|
if img_type in ["菜品", "菜单"]:
|
||||||
|
img_type = "eat"
|
||||||
|
elif img_type in ["饮料", "饮品"]:
|
||||||
|
img_type = "drink"
|
||||||
|
elif img_type in ["蜜雪", "蜜", "蜜雪冰城"]:
|
||||||
|
img_type = "me"
|
||||||
|
else:
|
||||||
|
await UniMessage.text("菜单类型错误,请重新输入").finish()
|
||||||
|
try:
|
||||||
|
delete_pic(img_type, name) # type: ignore
|
||||||
|
await UniMessage.text(f"成功删除{name}").finish()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
await UniMessage.text(f"删除失败, {e}").finish()
|
||||||
|
|
||||||
|
|
||||||
|
# 每日8点清空用户数据
|
||||||
|
@scheduler.scheduled_job("cron", hour=8)
|
||||||
|
async def _():
|
||||||
|
USER_DATA.clear()
|
||||||
|
logger.info("已清空用户数据")
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
from .config import config
|
||||||
|
|
||||||
|
|
||||||
|
class Menu:
|
||||||
|
"""
|
||||||
|
生成菜单的类
|
||||||
|
"""
|
||||||
|
|
||||||
|
dish_path: Path # 菜品图片的路径
|
||||||
|
menu_background: Image.Image # 菜单背景图
|
||||||
|
menu_font: ImageFont.FreeTypeFont # 菜单字体
|
||||||
|
|
||||||
|
def __init__(self, menu_type: Literal["drink", "eat", "me"]) -> None:
|
||||||
|
# 菜单图片的路径
|
||||||
|
self.dish_path = Path(config.whatpic_res_path) / f"{menu_type}_pic"
|
||||||
|
# 获取所有菜品的名字
|
||||||
|
self.all_dish_name = [i.stem for i in self.dish_path.iterdir()]
|
||||||
|
# 菜单背景图
|
||||||
|
self.menu_background = Image.open(
|
||||||
|
Path(__file__).parent / "menu_res" / "menu_bg.jpg"
|
||||||
|
)
|
||||||
|
# 字体大小
|
||||||
|
self.font_size = 30
|
||||||
|
# 加载字体
|
||||||
|
self.menu_font = ImageFont.truetype(
|
||||||
|
str(Path(__file__).parent / "menu_res" / "FZSJ-QINGCRJ.TTF"), self.font_size
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def menu_bg_size(self) -> tuple:
|
||||||
|
"""
|
||||||
|
获取菜单背景图的大小
|
||||||
|
"""
|
||||||
|
return self.menu_background.size
|
||||||
|
|
||||||
|
# 生成迭代器用于for循环遍历所有的生成的菜单
|
||||||
|
def draw_menu(self) -> Iterator[Image.Image]:
|
||||||
|
# 每个菜单的行数: 背景图高度 - 200(上下边距) // (字体大小+10(行间距)) = 菜单行数
|
||||||
|
line_num = (self.menu_bg_size[1] - 150) // (self.font_size + 10)
|
||||||
|
# 总共需要合成的图片数量
|
||||||
|
img_num = len(self.all_dish_name) // line_num + 1
|
||||||
|
# 生成所有菜单的图片
|
||||||
|
for i in range(img_num):
|
||||||
|
# 生成一张背景图的副本
|
||||||
|
menu_img = self.menu_background.copy()
|
||||||
|
# 生成draw对象
|
||||||
|
draw = ImageDraw.Draw(menu_img)
|
||||||
|
# 生成菜单的名字
|
||||||
|
for j in range(line_num):
|
||||||
|
# 如果菜单名字已经全部生成完毕
|
||||||
|
if i * line_num + j >= len(self.all_dish_name):
|
||||||
|
break
|
||||||
|
# 生成菜单序号+名字,(居中显示)
|
||||||
|
draw.text(
|
||||||
|
((self.menu_bg_size[0] - 300) // 2, 75 + j * (self.font_size + 10)),
|
||||||
|
f"{i * line_num + j + 1}.{self.all_dish_name[i * line_num + j]}",
|
||||||
|
font=self.menu_font,
|
||||||
|
fill="black",
|
||||||
|
)
|
||||||
|
yield menu_img
|
||||||
|
|
||||||
|
|
||||||
|
# 测试程序
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[tool.pdm]
|
||||||
|
distribution = true
|
||||||
|
|
||||||
|
[tool.pdm.build]
|
||||||
|
includes = ["nonebot_plugin_whateat_pic"]
|
||||||
|
[build-system]
|
||||||
|
requires = ["pdm-backend"]
|
||||||
|
build-backend = "pdm.backend"
|
||||||
|
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "nonebot-plugin-whateat-pic"
|
||||||
|
version = "1.5.9"
|
||||||
|
description = "基于Nonebot2的今天吃什么(离线版)"
|
||||||
|
authors = [
|
||||||
|
{ name = "Cvandia", email = "[email protected]" },
|
||||||
|
{ name = "Cvandia" },
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"nonebot2>=2.0.0",
|
||||||
|
"httpx>=0.19.0",
|
||||||
|
"pillow>=10.4.0",
|
||||||
|
"nonebot-plugin-alconna<1.0.0,>=0.53.1",
|
||||||
|
"rich<14.0.0,>=13.9.4",
|
||||||
|
"nonebot-plugin-apscheduler>=0.5.0",
|
||||||
|
"nonebot-plugin-localstore>=0.7.4",
|
||||||
|
"aiofiles>=0.7.0",
|
||||||
|
]
|
||||||
|
requires-python = "<4,>=3.9"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
homepage = "https://github.com/Cvandia/nonebot-plugin-whateat-pic"
|
||||||
|
repository = "https://github.com/Cvandia/nonebot-plugin-whateat-pic"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"black<23.0.0,>=22.1.0",
|
||||||
|
"ruff>=0.8.1",
|
||||||
|
"pre-commit>=4.0.1",
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 842 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |