Compare commits

...

90 Commits

Author SHA1 Message Date
kangfenmao
6aaa6bf042 chore(version): 0.7.10 2024-09-29 23:29:28 +08:00
kangfenmao
aa578194c7 fix: add markdown rendering input msg switcher #143 #142 2024-09-29 23:21:31 +08:00
kangfenmao
220600070c fix: paste long text issue 2024-09-29 22:37:33 +08:00
kangfenmao
32cdfbbfb0 feat: add assistant setting popup 2024-09-29 22:31:07 +08:00
kangfenmao
33b83bf242 feat: add webdav settings component and backup user data files #69 2024-09-29 16:44:18 +08:00
dray
2e1b433365 feat: 添加 WebDAV 配置项
为应用程序添加了 WebDAV 配置项,包括主机、用户、密码和路径。这样用户就可以将备份文件定时上传到 WebDAV 服务器,并从 WebDAV 服务器恢复备份文件。

- 添加了新的依赖项 "webdav": "^5.7.1"
- 修改了 package.json 文件
- 修改了 zh-tw.json、zh-cn.json 和 en-us.json 文件
- 修改了 settings.ts 文件
- 修改了 GeneralSettings.tsx 文件

https://github.com/kangfenmao/cherry-studio/issues/129
2024-09-29 09:27:42 +08:00
kangfenmao
2771a842fe fix: context count 2024-09-28 22:01:09 +08:00
牡丹凤凰
4af3d16e61 Merge pull request #137 from 1355873789/develop
add new app(Felo)
2024-09-28 17:58:00 +08:00
1355873789
eb47fb051b add new app(Felo) 2024-09-28 17:58:30 +08:00
牡丹凤凰
0f9655611b Update models.ts
fix: Confusion between Minimax and Hailuo logos
2024-09-28 11:21:01 +08:00
kangfenmao
0c72ccac12 chore(version): 0.7.9 2024-09-28 00:45:05 +08:00
kangfenmao
09f7fcd2b4 fix: about page minapp logo 2024-09-27 22:44:45 +08:00
kangfenmao
b9250df347 feat: backup all files
1. remove window.api.compress window.api.decompress
2024-09-27 22:35:22 +08:00
kangfenmao
ca897db0d2 fix: minimax hailuo logo 2024-09-27 14:14:10 +08:00
牡丹凤凰
af75d4139c fix: correct display for non-vision GPT-4 models (#135)
* Update models.ts

feat: add matching rules for EMBEDDING_REGEX
fix: correct display for non-vision gpt-4 models

* Update models.ts

feat:add matching rules for gpt-4

* Update models.ts

feat:add matching rules for gpt-4

* Update models.ts

feat:add matching rules for gpt-4
2024-09-27 11:47:03 +08:00
kangfenmao
d2e35a888d refactor: MessageContent component 2024-09-27 00:25:45 +08:00
kangfenmao
fb56c3744b docs: add dev docs 2024-09-27 00:13:24 +08:00
drfyup
26942cfd1f doc: add dev docs (#133)
* Create PR_FAQ.md

* Create Code_DSC.md
2024-09-27 00:04:46 +08:00
kangfenmao
1601fc6d81 fix: 在提问时携带图片会卡住软件 #108 2024-09-27 00:01:35 +08:00
kangfenmao
f543a9ff80 fix: Assistant 的 Prompt 过长时会超出组件 #95
close #95
2024-09-26 23:29:04 +08:00
kangfenmao
5299a2a687 feat: check update settings #131
close #131
2024-09-26 23:17:21 +08:00
kangfenmao
fcc627db6f feat: edit message 2024-09-26 22:45:59 +08:00
kangfenmao
1035019fc2 feat: translate settings persist 2024-09-26 19:15:26 +08:00
kangfenmao
9d311a7261 fix: filter unsupported models 2024-09-26 15:12:47 +08:00
kangfenmao
a973c5fb89 fix: remove filter messages 2024-09-26 14:55:09 +08:00
亢奋猫
be081ccf7a docs: new screenshot 2024-09-26 14:10:14 +08:00
kangfenmao
c25db02acf docs: add sponsor 2024-09-26 13:55:30 +08:00
1355873789
01f98235c6 更新智谱清言APP logo 2024-09-26 08:40:07 +08:00
1355873789
00f3b87215 繁体中文支持 2024-09-26 08:40:07 +08:00
1355873789
849958eeec 模型头像相关
修正部分模型头像错误
新增部分模型头像
2024-09-26 08:40:07 +08:00
牡丹凤凰
9655153e01 Update providers.ts 2024-09-26 03:10:44 +08:00
kangfenmao
74d5355e02 chore(version): 0.7.8 2024-09-25 12:52:42 +08:00
kangfenmao
bb137cc799 feat: auto-scroll to bottom on new messages, return null for empty suggestions
- Added functionality to automatically scroll to the bottom of the messages container upon receiving new messages.
- Return null when suggestions list is empty instead of displaying an empty container.
2024-09-25 12:33:03 +08:00
kangfenmao
6aee3d8088 feat: add hugging chat minapp 2024-09-25 12:09:51 +08:00
kangfenmao
51cedcb644 fix: auto scroll to bottom #120
close #120
2024-09-25 11:36:44 +08:00
kangfenmao
270c754c00 refactor: ant design styles separation 2024-09-25 11:29:38 +08:00
kangfenmao
8f68aca24c feat: add streaming output options #93 2024-09-25 11:23:45 +08:00
kangfenmao
93710c1e78 fix: gemini safety settings #110 2024-09-25 09:49:19 +08:00
kangfenmao
ac2a3fd38e docs(README.md): update readme 2024-09-25 00:26:14 +08:00
kangfenmao
750f1cd63d feat: new providers logos 2024-09-25 00:21:20 +08:00
kangfenmao
4413528d0e feat: add ocoolai provider 2024-09-25 00:21:20 +08:00
kangfenmao
e8b992c289 fix: transparent window 2024-09-25 00:21:20 +08:00
kangfenmao
938ff38aeb fix: windows ico icon 2024-09-25 00:20:10 +08:00
亢奋猫
77cb534e16 Update README.md 2024-09-25 00:20:10 +08:00
亢奋猫
58513b63a3 docs(README.md): add contributors 2024-09-25 00:20:10 +08:00
牡丹凤凰
712e7ff104 docs(README.md): add banners
Delete docs/images/1.png

update
2024-09-25 00:19:52 +08:00
kangfenmao
c68d283766 feat: improved layout and accessibility
- Added hidden overflow to HomePage content container for improved layout.
- Changed the upload list type to text to improve accessibility.
2024-09-25 00:00:34 +08:00
kangfenmao
4d198ff5f1 chore(version): 0.7.7 2024-09-25 00:00:34 +08:00
kangfenmao
10598d430a style: removed background color from agentcard component
- Removed background color from AgentCard component.
2024-09-25 00:00:34 +08:00
kangfenmao
3fbcce3b04 feat: new vi logo 2024-09-25 00:00:34 +08:00
kangfenmao
994dac3af4 fix: agent i18n tranlate 2024-09-25 00:00:34 +08:00
王叔叔
d76e7229fc Update README_zh.md 2024-09-25 00:00:20 +08:00
kangfenmao
26f072fac7 fix: appImage artifactName #112
close #112
2024-09-24 23:59:38 +08:00
kangfenmao
64f96f561b docs: add docs folder 2024-09-24 23:59:38 +08:00
kangfenmao
23c61b8099 fix: content container overflow 2024-09-24 23:59:38 +08:00
kangfenmao
3b06b9474c feat: new model logo 2024-09-24 23:59:38 +08:00
kangfenmao
3d3410b4fd feat: use rounded corner design 2024-09-24 23:59:38 +08:00
kangfenmao
d7f8eec59e feat: fix siliconflow reset api url 2024-09-22 09:24:55 +08:00
kangfenmao
f98879a1e5 chore(version): 0.7.6 2024-09-22 00:44:47 +08:00
kangfenmao
ef40e9db5f style: centered positioning and alignment added to modal and userpopup components
- Added centered positioning to Modal component in PromptPopup.
- Added centered alignment to UserPopup component.
2024-09-22 00:19:45 +08:00
kangfenmao
eb799879ff feat: export topic message as image #103 2024-09-22 00:16:36 +08:00
kangfenmao
13fddc8e7f feat: add loading spinner #86
close #86
2024-09-21 21:07:50 +08:00
kangfenmao
fa3d7f7f4a refactor: message component 2024-09-21 13:20:16 +08:00
kangfenmao
6845ee1664 feat: improved formula rendering with new escaping functions
- Improved formula rendering by removing unnecessary escaping of dollar numbers.
- Implemented two new string escaping functions to prevent incorrect LaTeX formula rendering.

#101
2024-09-21 10:27:32 +08:00
kangfenmao
c8b98681ef fix: After stopping content generation, messages cannot be cleared #66
close #66
2024-09-21 00:25:17 +08:00
kangfenmao
ae4542ce68 feat: improved input bar functionality and added text insertion feature
- Improved functionality for handling text input and file uploads in the input bar.
- Added functionality to insert text at cursor position in a text area.
2024-09-21 00:04:53 +08:00
kangfenmao
0140ff5f6e fix: anthropic api url #97 2024-09-20 23:10:25 +08:00
kangfenmao
a22a47c16a chore(version): 0.7.5 2024-09-20 17:01:52 +08:00
kangfenmao
6bb7b2ca5d feat: improved ui effects and rendering for components
- Added smooth all property transition effect to Icon component.
- Added hover effect and conditional rendering for Switch Topic Sidebar button on current assistant.
- Updated the existing conditional options array to consistently include both topic and settings options.
- Improved hover effects on topic list items.
2024-09-20 16:48:24 +08:00
kangfenmao
1ec7df9a7e feat: add new add topic button 2024-09-20 15:11:50 +08:00
kangfenmao
83925832be fix: attachment open handler 2024-09-20 11:38:30 +08:00
kangfenmao
4dadf98909 fix: improved api call validation
- Improved API call validation to account for additional usage properties.
2024-09-20 11:12:15 +08:00
kangfenmao
375c07e442 style: removed unnecessary import and optimized sidebar styling
- Removed unnecessary import and optimized sidebar styling for improved performance.
2024-09-20 10:49:50 +08:00
kangfenmao
9374541993 chore(version): 0.7.4 2024-09-20 00:15:24 +08:00
kangfenmao
372224469d feat: added minapp event handling and sidebar menu interactions #50
- Added functionality for handling MinApp window closure and provided a default close event handler.
- Added event listeners to Sidebar menus to interact with MinApp.
2024-09-19 23:28:06 +08:00
kangfenmao
60e87e8a22 fix: Disable topic switching and movement during rendering.
- Added functionality to disable topic switching and movement when rendering is in progress.
2024-09-19 23:01:21 +08:00
kangfenmao
353e497642 feat: Improved layout and added file content filtering.
- Added a margin bottom to the Upload component in the MessageAttachments page for improved layout.
- Added support for not displaying file contents for specific providers.
2024-09-19 22:58:12 +08:00
kangfenmao
0ee72a9ef8 chore(version): 0.7.3 2024-09-19 18:20:52 +08:00
kangfenmao
d9873b4261 fix: attachment select extension for windows 2024-09-19 17:40:45 +08:00
kangfenmao
934ab1a374 chore(version): 0.7.2 2024-09-19 16:56:58 +08:00
kangfenmao
33ac0937df feat: Added translations, new column, and UI improvements.
- Added translations for a new field.
- Added new column for file count in the FilesPage view.
- Improved handling of message tokens in the UI.
- Added functionality to display message tokens for messages with specific roles.
- Added window style selection and styling adjustments to the General Settings page.
- Added support for vision models in OpenAIProvider.
2024-09-19 16:56:44 +08:00
kangfenmao
f1c8922752 fix: openai sdk request error 2024-09-19 15:21:24 +08:00
kangfenmao
03bdbdb412 fix: support \(...\) and \[...\] style math formula #78 2024-09-19 15:21:06 +08:00
kangfenmao
cf9d4c5370 feat: add click assistant switch to topics settings 2024-09-19 13:55:44 +08:00
kangfenmao
bfa6bfa196 feat: enhanced user experience with layout adjustments.
- This commit addresses key feature enhancements and minor optimizations for improved user experience and functionality.
- Adjusted margin top for upload container to a positive value.
- Adjusted the max-height of the container to improve rendering on smaller screens.
2024-09-19 12:04:06 +08:00
kangfenmao
af8144d45e feat: Improved file management and added new features.
- Updated file manager to use FileManager class instead of File class.
- Improved file management functionality with features for finding duplicate files, file uploading, and storage management.
- Added styles to wrap and truncate text in a no-drag area.
- Added explicit file extensions to imageExts constant.
- Added the 'paste long text as file' input setting.
- Added image file display and UI improvements for file names and overflow.
- Improved file paste and long text handling functionality.
- awaited onSendMessage function call and added message to chat completion.
- Implemented new option to paste long text as file in the Settings page.
- Updated content display logic to include file origin name along with the file content for text files.
- Improved functionality for handling image and text file contents in the Gemini chat provider.
- Updated file content formatting logic for text files with origin name and content prefix.
- Added a new setting "pasteLongTextAsFile" and its corresponding action to the application settings.
2024-09-19 10:51:30 +08:00
kangfenmao
29605fbcdb feat: copy and paste files or images 2024-09-18 21:18:42 +08:00
kangfenmao
6e7e5cb1f1 feat: add file attachment 2024-09-18 18:00:49 +08:00
kangfenmao
6f5dccd595 feat: estimate completion usage calculation added to chat.
- Estimated usage calculation has been added to chat completion fetching to track message usage.
- Added functionality to estimate completion usage tokens based on input and prompt data.
2024-09-17 14:56:10 +08:00
kangfenmao
0af35b9f10 feat: Added functionality to move topics between assistants.
- Added functionality to move topics between assistants.
- Updated i18n translations to improve user interface clarity and accessibility.
- Improved code organization and functionality to support moving topics between assistants.
2024-09-17 14:37:42 +08:00
247 changed files with 7900 additions and 2221 deletions

1
.gitignore vendored
View File

@@ -41,6 +41,7 @@ Thumbs.db
node_modules
dist
out
build/icons
# ENV
.env

View File

@@ -1,14 +1,19 @@
<div align="center">
<a href="https://github.com/kangfenmao/cherry-studio/releases">
<img src="https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505" alt="banner" />
</a>
English | <a href="./docs/README.zh.md">中文</a>
</div>
# 🍒 Cherry Studio
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
# 🌠 Screenshot
![](https://github.com/user-attachments/assets/e24d1e7d-126a-4647-bd98-f470bfe26fde)
![](https://github.com/user-attachments/assets/3f3f0bfa-cb88-4abf-923a-a0859fa3c912)
![](https://github.com/user-attachments/assets/288560c1-d218-437c-87c2-2a5e87b43b93)
![](https://github.com/user-attachments/assets/2037a038-4731-49db-85b9-0d09e8c08bdb)
![](https://github.com/user-attachments/assets/b09f2acd-dcd4-42ea-be07-7e12665a729c)
![](https://github.com/user-attachments/assets/bac2a613-17ed-4a6c-bcc5-8f7b9617d457)
# 🌟 Features
@@ -57,6 +62,16 @@ $ yarn build:linux
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
# 🚀 Contributors
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
</a>
# Sponsor
[Buy Me a Coffee](docs/sponsor.md)
# 📃 License
[LICENSE](./LICENSE)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 210 KiB

BIN
build/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

96
docs/README.zh.md Normal file
View File

@@ -0,0 +1,96 @@
<div align="center">
<a href="https://github.com/kangfenmao/cherry-studio/releases">
<img src="https://github.com/user-attachments/assets/995910f3-177a-4d1e-97ea-04e3b009ba36" alt="banner"/>
</a>
English / <a href="https://github.com/kangfenmao/cherry-studio">中文</a>
</div>
# 🍒 Cherry Studio
Cherry Studio 是一款跨平台桌面客户端支持多个大语言模型LLM服务商兼容 Windows、Mac 和 Linux 系统,并拥丰富的个性化选项与领先的功能设计。
# 🌠 界面
<img width="1582" alt="Xnip2024-09-23_15-01-53" src="https://github.com/user-attachments/assets/554aa31b-87b6-49fe-877d-af313e1608b0">
<img width="1582" alt="Xnip2024-09-23_15-02-27" src="https://github.com/user-attachments/assets/f43fb4c8-194a-4f46-8575-6db2bd136cb9">
<img width="1582" alt="Xnip2024-09-23_16-12-19" src="https://github.com/user-attachments/assets/82ce3cc1-5a0b-49aa-9fe4-0376d34be1f8">
<img width="1582" alt="Xnip2024-09-23_16-11-44" src="https://github.com/user-attachments/assets/55e420c8-fc0f-40a0-868e-d75bebeb5af3">
<img width="1582" alt="Xnip2024-09-23_16-11-50" src="https://github.com/user-attachments/assets/7413384e-a7c7-4525-96ea-ccd395d7e51a">
<img width="1582" alt="Xnip2024-09-23_16-12-59" src="https://github.com/user-attachments/assets/894b5e97-569f-4471-813c-c48d19455215">
# 🌟 特性
## 😌 轻松上手
🍏WindowsMacLinux跨平台支持
📦开箱即用,无需 Python 与 Docker
🤝简洁、友好的界面与交互设计
## 🛠️多样化的 LLM 服务模式支持
☁️ 全面覆盖 LLM 云服务,支持自定义 api key 与模型管理OpenAIGeminiAnthropic硅基流动...
🔗汇聚流行的 AI Web 服务并计划通过功能增强提升体验ClaudePeplexityPoe腾讯元宝知乎直答...
💻支持 Ollama 运行本地模型
## 📲个性化的功能体验
📄完整的 Markdown 与 Mermaid 渲染支持
🤖使用与创建智能体提升工作效率
🔤持续迭代的翻译功能
🤲生成结果支持 Markdown 与图片分享
📎文件与图片上传RAG 与多模态对话
🎨透明窗口与明暗主题支持
# 🖥️ 开发指南
## 推荐的开发环境
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## 项目设置
### 安装依赖
```bash
$ yarn
```
### 启动开发环境
```bash
$ yarn dev
```
### 构建版本
```bash
# For windows
$ yarn build:win
# For macOS
$ yarn build:mac
# For Linux
$ yarn build:linux
```
# ⭐️ Star 记录
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
# 赞助
[微信赞赏码](docs/sponsor.md)
# 📃 许可证
[LICENSE](./LICENSE)

5
docs/Sponsor.md Normal file
View File

@@ -0,0 +1,5 @@
# Sponsor
<div align="center">
<img src="https://github.com/user-attachments/assets/4665f07f-5ecc-4bd8-8727-ae00f35d6d98" alt="Buy Me a Coffee" width="280"/>
</div>

95
docs/dev/FAQ.md Normal file
View File

@@ -0,0 +1,95 @@
# FAQ 文档
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
## 问题1Cherry Studio 支持哪些操作系统?
- **答案**Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
## 问题2Cherry Studio 的主要功能有哪些?
- **答案**Cherry Studio 的主要功能包括:
1. 支持多个 LLM 提供商
2. 允许创建多个助手
3. 支持创建多个主题
4. 允许在同一对话中使用多个模型来回答问题
5. 支持拖放排序
6. 代码高亮
7. Mermaid 图表支持
## 问题3Cherry Studio 的主要目录结构是怎样的?
- **答案**Cherry Studio 的主要目录结构如下:
- `/src`: 主要源代码目录
- `/build`: 构建相关文件
- `/docs`: 文档目录
- `/resources`: 资源文件目录
- `/scripts`: 脚本文件目录
## 问题4如何在 Windows 环境下 fork Cherry Studio 并修改部分功能?
- **答案**:在 Windows 环境下 fork Cherry Studio 并修改部分功能的步骤如下:
1. 在 GitHub 上 fork Cherry Studio 仓库
2. 克隆 fork 的仓库到本地:`git clone https://github.com/your-username/cherry-studio.git`
3. 进入项目目录:`cd cherry-studio`
4. 安装依赖:`yarn install`
5. 修改所需的功能代码
6. 测试修改:`yarn dev`
7. 提交修改:`git add .``git commit -m "描述你的修改"`
8. 推送到你的 fork 仓库:`git push origin main`
## 问题5Cherry Studio 使用了哪些主要技术栈?
- **答案**Cherry Studio 主要使用了以下技术栈:
- TypeScript
- SCSS
- Electron
- Vite
- Sequelize
## 问题6如何贡献代码到 Cherry Studio 项目?
- **答案**:贡献代码到 Cherry Studio 项目的步骤如下:
1. Fork 项目仓库
2. 创建你的特性分支:`git checkout -b feature/AmazingFeature`
3. 提交你的修改:`git commit -m 'Add some AmazingFeature'`
4. 推送到分支:`git push origin feature/AmazingFeature`
5. 打开一个 Pull Request
## 问题7Cherry Studio 的 `/src` 目录主要包含哪些内容?
- **答案**Cherry Studio 的 `/src` 目录主要包含以下内容:
- 主进程代码Electron 主进程)
- 渲染进程代码(用户界面)
- 组件
- 工具函数
- 状态管理
- 样式文件
## 问题8如何在 Cherry Studio 中添加新的 LLM 提供商?
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
1.`/src/services` 或类似目录下创建新的服务文件
2. 实现与新 LLM 提供商 API 的集成
3. 在用户界面中添加新提供商的选项
4. 更新配置和状态管理以支持新提供商
## 问题9Cherry Studio 的构建过程是怎样的?
- **答案**Cherry Studio 的构建过程主要包括:
1. 使用 Vite 构建前端资源
2. 使用 Electron Builder 打包桌面应用
3. 根据不同平台Windows、Mac、Linux生成相应的安装包
## 问题10如何在 Cherry Studio 中实现新的 UI 主题?
- **答案**:在 Cherry Studio 中实现新的 UI 主题的步骤:
1.`/src/styles` 目录下创建新的主题 SCSS 文件
2. 定义新主题的颜色变量和样式
3. 在主样式文件中导入新主题
4. 更新主题切换逻辑以包含新主题
5. 在用户界面中添加新主题的选项
## 问题11Cherry Studio 如何处理多语言支持?
- **答案**Cherry Studio 可能通过以下方式处理多语言支持:
1. 使用 i18n 库进行国际化
2.`/src/locales` 或类似目录下存储不同语言的翻译文件
3. 实现语言切换功能
4. 在组件中使用翻译函数或组件来显示多语言文本
## 问题12如何为 Cherry Studio 编写单元测试?
- **答案**:为 Cherry Studio 编写单元测试的步骤:
1.`/tests` 目录下创建测试文件
2. 使用测试框架(如 Jest编写测试用例
3. 模拟 Electron 环境和其他依赖
4. 运行测试命令:`yarn test`
5. 确保测试覆盖主要功能和组件

95
docs/dev/faq.md Normal file
View File

@@ -0,0 +1,95 @@
# FAQ 文档
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
## 问题1Cherry Studio 支持哪些操作系统?
- **答案**Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
## 问题2Cherry Studio 的主要功能有哪些?
- **答案**Cherry Studio 的主要功能包括:
1. 支持多个 LLM 提供商
2. 允许创建多个助手
3. 支持创建多个主题
4. 允许在同一对话中使用多个模型来回答问题
5. 支持拖放排序
6. 代码高亮
7. Mermaid 图表支持
## 问题3Cherry Studio 的主要目录结构是怎样的?
- **答案**Cherry Studio 的主要目录结构如下:
- `/src`: 主要源代码目录
- `/build`: 构建相关文件
- `/docs`: 文档目录
- `/resources`: 资源文件目录
- `/scripts`: 脚本文件目录
## 问题4如何在 Windows 环境下 fork Cherry Studio 并修改部分功能?
- **答案**:在 Windows 环境下 fork Cherry Studio 并修改部分功能的步骤如下:
1. 在 GitHub 上 fork Cherry Studio 仓库
2. 克隆 fork 的仓库到本地:`git clone https://github.com/your-username/cherry-studio.git`
3. 进入项目目录:`cd cherry-studio`
4. 安装依赖:`yarn install`
5. 修改所需的功能代码
6. 测试修改:`yarn dev`
7. 提交修改:`git add .``git commit -m "描述你的修改"`
8. 推送到你的 fork 仓库:`git push origin main`
## 问题5Cherry Studio 使用了哪些主要技术栈?
- **答案**Cherry Studio 主要使用了以下技术栈:
- TypeScript
- SCSS
- Electron
- Vite
- Sequelize
## 问题6如何贡献代码到 Cherry Studio 项目?
- **答案**:贡献代码到 Cherry Studio 项目的步骤如下:
1. Fork 项目仓库
2. 创建你的特性分支:`git checkout -b feature/AmazingFeature`
3. 提交你的修改:`git commit -m 'Add some AmazingFeature'`
4. 推送到分支:`git push origin feature/AmazingFeature`
5. 打开一个 Pull Request
## 问题7Cherry Studio 的 `/src` 目录主要包含哪些内容?
- **答案**Cherry Studio 的 `/src` 目录主要包含以下内容:
- 主进程代码Electron 主进程)
- 渲染进程代码(用户界面)
- 组件
- 工具函数
- 状态管理
- 样式文件
## 问题8如何在 Cherry Studio 中添加新的 LLM 提供商?
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
1.`/src/services` 或类似目录下创建新的服务文件
2. 实现与新 LLM 提供商 API 的集成
3. 在用户界面中添加新提供商的选项
4. 更新配置和状态管理以支持新提供商
## 问题9Cherry Studio 的构建过程是怎样的?
- **答案**Cherry Studio 的构建过程主要包括:
1. 使用 Vite 构建前端资源
2. 使用 Electron Builder 打包桌面应用
3. 根据不同平台Windows、Mac、Linux生成相应的安装包
## 问题10如何在 Cherry Studio 中实现新的 UI 主题?
- **答案**:在 Cherry Studio 中实现新的 UI 主题的步骤:
1.`/src/styles` 目录下创建新的主题 SCSS 文件
2. 定义新主题的颜色变量和样式
3. 在主样式文件中导入新主题
4. 更新主题切换逻辑以包含新主题
5. 在用户界面中添加新主题的选项
## 问题11Cherry Studio 如何处理多语言支持?
- **答案**Cherry Studio 可能通过以下方式处理多语言支持:
1. 使用 i18n 库进行国际化
2.`/src/locales` 或类似目录下存储不同语言的翻译文件
3. 实现语言切换功能
4. 在组件中使用翻译函数或组件来显示多语言文本
## 问题12如何为 Cherry Studio 编写单元测试?
- **答案**:为 Cherry Studio 编写单元测试的步骤:
1.`/tests` 目录下创建测试文件
2. 使用测试框架(如 Jest编写测试用例
3. 模拟 Electron 环境和其他依赖
4. 运行测试命令:`yarn test`
5. 确保测试覆盖主要功能和组件

72
docs/dev/structure.md Normal file
View File

@@ -0,0 +1,72 @@
## Cherry Studio目录结构和功能
### 1. `/src`: 主要源代码目录
- ** `/main`**: Electron主进程相关代码
- 负责应用的生命周期管理、窗口创建、IPC通信等
- ** `/renderer`**: Electron渲染进程相关代码
- 包含用户界面的实现使用TypeScript和SCSS
- ** `/preload`**: 预加载脚本
- 用于在渲染进程中安全地暴露主进程功能
- ** `/components`**: React组件
- 可复用的UI组件如对话框、输入框等
- ** `/pages`**: 应用的主要页面
- 如聊天界面、设置页面等
- ** `/store`**: 状态管理
- 可能使用Redux或MobX来管理应用状态
- ** `/utils`**: 工具函数
- 包含各种辅助函数和工具类
- ** `/styles`**: 全局样式文件
- 包含SCSS文件定义全局样式和主题
### 2. `/public`: 静态资源目录
- 包含图标、字体等静态文件
### 3. `/electron`: Electron相关配置
- 包含Electron的构建和打包配置
### 4. `/scripts`: 构建和开发脚本
- 包含npm脚本用于开发、构建和部署
### 5. `/types`: TypeScript类型定义
- 包含自定义的类型定义文件
### 6. `/tests`: 测试文件目录
- 包含单元测试和集成测试
### 7. `/docs`: 文档目录
- 包含项目文档、API文档等
### 8. `/config`: 配置文件目录
- 包含各种配置文件如webpack配置、环境变量等
### 9. `/migrations`: 数据库迁移文件
- 由于使用了Sequelize这里可能包含数据库结构的变更记录
### 10. `/models`: 数据模型
- 定义Sequelize的数据模型对应数据库表结构
## 主要功能实现
### 1. LLM提供商集成
- 可能在`/src/utils``/src/services`中实现与不同LLM API的集成
### 2. 多助手和多主题支持
-`/src/store`中管理助手和主题的状态
-`/src/components`中实现相关的UI组件
### 3. 多模型对话
-`/src/pages`的聊天界面中实现
- 可能使用`/src/store`来管理对话状态
### 4. 拖放排序
-`/src/components`中实现相关的可拖拽组件
### 5. 代码高亮
- 可能使用第三方库如Prism.js集成在`/src/components`
### 6. Mermaid图表支持
-`/src/components`中集成Mermaid库
### 7. 数据持久化
- 使用Sequelize在`/models`中定义数据模型
-`/migrations`中管理数据库结构变更

5
docs/sponsor.md Normal file
View File

@@ -0,0 +1,5 @@
# Sponsor
<div align="center">
<img src="https://github.com/user-attachments/assets/4665f07f-5ecc-4bd8-8727-ae00f35d6d98" alt="Buy Me a Coffee" width="280"/>
</div>

View File

@@ -21,6 +21,8 @@ nsis:
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
allowToChangeInstallationDirectory: true
oneClick: false
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
@@ -42,13 +44,16 @@ dmg:
artifactName: ${productName}-${version}-${arch}.${ext}
linux:
target:
- AppImage
- target: AppImage
arch:
- arm64
- x64
# - snap
# - deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${productName}-${version}.${ext}
artifactName: ${productName}-${version}-${arch}.${ext}
npmRebuild: false
publish:
provider: github
@@ -60,13 +65,15 @@ afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
本次更新:
数据结构调整,更新后会自动迁移数据
支持从特定消息创建新分支
长代码添加底部复制按钮
AI 回复过程中支持复制代码
优化消息列表渲染性能
增加 WebDAV 备份功能 by @DrayChou
增加使用 Markdown 渲染用户消息开关
新的助手设置界面
增加 Felo 小程序
修复输入框文字撤销问题
修复上下文数量为0时报错
近期更新:
支持 Vision 模型
新增文件功能
智能助理和消息列表合并
支持行内公式
全新应用图标
Windows 安装程序支持修改安装位置了
增加流式输出开关
支持关闭自动检测更新
支持消息的编辑

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.7.1",
"version": "0.7.10",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -26,15 +26,21 @@
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
"release": "node scripts/version.js",
"publish": "yarn release patch push"
"publish": "yarn release patch push",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"archiver": "^7.0.1",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3"
"electron-window-state": "^5.0.3",
"fs-extra": "^11.2.0",
"html2canvas": "^1.4.1",
"unzipper": "^0.12.3",
"webdav": "4.11.4"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.24.3",
@@ -45,10 +51,13 @@
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/tinycolor2": "^1",
"@types/unzipper": "^0",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.18.3",
"axios": "^1.7.3",
@@ -59,6 +68,8 @@
"dotenv-cli": "^7.4.2",
"electron": "^28.3.3",
"electron-builder": "^24.9.1",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.0.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
@@ -67,7 +78,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.0.0",
"gpt-tokens": "^1.3.6",
"gpt-tokens": "^1.3.10",
"i18next": "^23.11.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
@@ -90,6 +101,7 @@
"remark-math": "^6.0.0",
"sass": "^1.77.2",
"styled-components": "^6.1.11",
"tinycolor2": "^1.6.0",
"typescript": "^5.6.2",
"uuid": "^10.0.0",
"vite": "^5.0.12"

View File

@@ -21,13 +21,13 @@ export const DATA_PATH = getDataPath()
export const appConfig = new Store()
export const titleBarOverlayDark = {
height: 41,
height: 40,
color: '#00000000',
symbolColor: '#ffffff'
}
export const titleBarOverlayLight = {
height: 41,
height: 40,
color: '#00000000',
symbolColor: '#000000'
}

View File

@@ -1,5 +1,6 @@
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { registerIpc } from './ipc'
import { updateUserDataPath } from './utils/upgrade'
@@ -30,6 +31,12 @@ app.whenReady().then(async () => {
const mainWindow = createMainWindow()
registerIpc(mainWindow, app)
if (process.env.NODE_ENV === 'development') {
installExtension(REDUX_DEVTOOLS)
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
}
})
// Quit when all windows are closed, except on macOS. There, it's common

View File

@@ -1,17 +1,13 @@
import { FileType } from '@types'
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
import Logger from 'electron-log'
import fs from 'fs'
import path from 'path'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import File from './services/File'
import { openFile, saveFile } from './utils/file'
import { compress, decompress } from './utils/zip'
import BackupManager from './services/BackupManager'
import FileManager from './services/FileManager'
import { createMinappWindow } from './window'
const fileManager = new File()
const fileManager = new FileManager()
const backupManager = new BackupManager()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow)
@@ -31,36 +27,26 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
})
ipcMain.handle('save-file', saveFile)
ipcMain.handle('open-file', openFile)
ipcMain.handle('reload', () => mainWindow.reload())
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
ipcMain.handle('backup:backup', backupManager.backup)
ipcMain.handle('backup:restore', backupManager.restore)
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
ipcMain.handle('image:base64', async (_, filePath) => {
try {
const data = await fs.promises.readFile(filePath)
const base64 = data.toString('base64')
const mime = `image/${path.extname(filePath).slice(1)}`
return {
mime,
base64,
data: `data:${mime};base64,${base64}`
}
} catch (error) {
Logger.error('Error reading file:', error)
return ''
}
})
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
ipcMain.handle('file:clear', async () => await fileManager.clear())
ipcMain.handle('file:delete', async (_, fileId: string) => {
await fileManager.deleteFile(fileId)
return { success: true }
})
ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile)
ipcMain.handle('file:upload', fileManager.uploadFile)
ipcMain.handle('file:clear', fileManager.clear)
ipcMain.handle('file:read', fileManager.readFile)
ipcMain.handle('file:delete', fileManager.deleteFile)
ipcMain.handle('file:get', fileManager.getFile)
ipcMain.handle('file:selectFolder', fileManager.selectFolder)
ipcMain.handle('file:create', fileManager.createTempFile)
ipcMain.handle('file:write', fileManager.writeFile)
ipcMain.handle('file:saveImage', fileManager.saveImage)
ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('minapp', (_, args) => {
createMinappWindow({

View File

@@ -0,0 +1,110 @@
import { WebDavConfig } from '@types'
import archiver from 'archiver'
import { app } from 'electron'
import Logger from 'electron-log'
import * as fs from 'fs-extra'
import * as path from 'path'
import * as unzipper from 'unzipper'
import WebDav from './WebDav'
class BackupManager {
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
constructor() {
this.backup = this.backup.bind(this)
this.restore = this.restore.bind(this)
this.backupToWebdav = this.backupToWebdav.bind(this)
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
}
async backup(
_: Electron.IpcMainInvokeEvent,
fileName: string,
data: string,
destinationPath: string = this.backupDir
): Promise<string> {
try {
// 创建临时目录
await fs.ensureDir(this.tempDir)
// 将 data 写入临时文件
const tempDataPath = path.join(this.tempDir, 'data.json')
await fs.writeFile(tempDataPath, data)
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
await fs.copy(sourcePath, tempDataDir)
// 创建 zip 文件
const output = fs.createWriteStream(path.join(destinationPath, fileName))
const archive = archiver('zip', { zlib: { level: 9 } })
archive.pipe(output)
archive.directory(this.tempDir, false)
await archive.finalize()
// 清理临时目录
await fs.remove(this.tempDir)
Logger.log('Backup completed successfully')
const backupedFilePath = path.join(destinationPath, fileName)
return backupedFilePath
} catch (error) {
Logger.error('Backup failed:', error)
throw error
}
}
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
// 创建临时目录
await fs.ensureDir(this.tempDir)
// 解压备份文件到临时目录
await fs
.createReadStream(backupPath)
.pipe(unzipper.Extract({ path: this.tempDir }))
.promise()
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
// 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
await fs.remove(destPath)
await fs.copy(sourcePath, destPath)
// 清理临时目录
await fs.remove(this.tempDir)
Logger.log('Restore completed successfully')
return data
}
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data)
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true
})
}
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
const filename = 'cherry-studio.backup.zip'
const webdavClient = new WebDav(webdavConfig)
const retrievedFile = await webdavClient.getFileContents(filename)
const backupedFilePath = path.join(this.backupDir, filename)
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
return await this.restore(_, backupedFilePath)
}
}
export default BackupManager

View File

@@ -1,144 +0,0 @@
import { getFileType } from '@main/utils/file'
import { FileType } from '@types'
import * as crypto from 'crypto'
import { app, dialog, OpenDialogOptions } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
class File {
private storageDir: string
constructor() {
this.storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
this.initStorageDir()
}
private initStorageDir(): void {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
}
private async getFileHash(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5')
const stream = fs.createReadStream(filePath)
stream.on('data', (data) => hash.update(data))
stream.on('end', () => resolve(hash.digest('hex')))
stream.on('error', reject)
})
}
async findDuplicateFile(filePath: string): Promise<FileType | null> {
const stats = fs.statSync(filePath)
const fileSize = stats.size
const files = await fs.promises.readdir(this.storageDir)
for (const file of files) {
const storedFilePath = path.join(this.storageDir, file)
const storedStats = fs.statSync(storedFilePath)
if (storedStats.size === fileSize) {
const [originalHash, storedHash] = await Promise.all([
this.getFileHash(filePath),
this.getFileHash(storedFilePath)
])
if (originalHash === storedHash) {
const ext = path.extname(file)
const id = path.basename(file, ext)
return {
id,
origin_name: file,
name: file + ext,
path: storedFilePath,
created_at: storedStats.birthtime,
size: storedStats.size,
ext,
type: getFileType(ext),
count: 2
}
}
}
}
return null
}
async selectFile(options?: OpenDialogOptions): Promise<FileType[] | null> {
const defaultOptions: OpenDialogOptions = {
properties: ['openFile']
}
const dialogOptions = { ...defaultOptions, ...options }
const result = await dialog.showOpenDialog(dialogOptions)
if (result.canceled || result.filePaths.length === 0) {
return null
}
const fileMetadataPromises = result.filePaths.map(async (filePath) => {
const stats = fs.statSync(filePath)
const ext = path.extname(filePath)
const fileType = getFileType(ext)
return {
id: uuidv4(),
origin_name: path.basename(filePath),
name: path.basename(filePath),
path: filePath,
created_at: stats.birthtime,
size: stats.size,
ext: ext,
type: fileType,
count: 1
}
})
return Promise.all(fileMetadataPromises)
}
async uploadFile(file: FileType): Promise<FileType> {
const duplicateFile = await this.findDuplicateFile(file.path)
if (duplicateFile) {
return duplicateFile
}
const uuid = uuidv4()
const origin_name = path.basename(file.path)
const ext = path.extname(origin_name)
const destPath = path.join(this.storageDir, uuid + ext)
await fs.promises.copyFile(file.path, destPath)
const stats = await fs.promises.stat(destPath)
const fileType = getFileType(ext)
const fileMetadata: FileType = {
id: uuid,
origin_name,
name: uuid + ext,
path: destPath,
created_at: stats.birthtime,
size: stats.size,
ext: ext,
type: fileType,
count: 1
}
return fileMetadata
}
async deleteFile(id: string): Promise<void> {
await fs.promises.unlink(path.join(this.storageDir, id))
}
async clear(): Promise<void> {
await fs.promises.rmdir(this.storageDir, { recursive: true })
await this.initStorageDir()
}
}
export default File

View File

@@ -0,0 +1,299 @@
import { getFileType } from '@main/utils/file'
import { FileType } from '@types'
import * as crypto from 'crypto'
import {
app,
dialog,
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue
} from 'electron'
import logger from 'electron-log'
import * as fs from 'fs'
import { writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
class FileManager {
private storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
constructor() {
this.initStorageDir()
}
private initStorageDir = (): void => {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
}
private getFileHash = async (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5')
const stream = fs.createReadStream(filePath)
stream.on('data', (data) => hash.update(data))
stream.on('end', () => resolve(hash.digest('hex')))
stream.on('error', reject)
})
}
findDuplicateFile = async (filePath: string): Promise<FileType | null> => {
const stats = fs.statSync(filePath)
const fileSize = stats.size
const files = await fs.promises.readdir(this.storageDir)
for (const file of files) {
const storedFilePath = path.join(this.storageDir, file)
const storedStats = fs.statSync(storedFilePath)
if (storedStats.size === fileSize) {
const [originalHash, storedHash] = await Promise.all([
this.getFileHash(filePath),
this.getFileHash(storedFilePath)
])
if (originalHash === storedHash) {
const ext = path.extname(file)
const id = path.basename(file, ext)
return {
id,
origin_name: file,
name: file + ext,
path: storedFilePath,
created_at: storedStats.birthtime,
size: storedStats.size,
ext,
type: getFileType(ext),
count: 2
}
}
}
}
return null
}
public selectFile = async (
_: Electron.IpcMainInvokeEvent,
options?: OpenDialogOptions
): Promise<FileType[] | null> => {
const defaultOptions: OpenDialogOptions = {
properties: ['openFile']
}
const dialogOptions = { ...defaultOptions, ...options }
const result = await dialog.showOpenDialog(dialogOptions)
if (result.canceled || result.filePaths.length === 0) {
return null
}
const fileMetadataPromises = result.filePaths.map(async (filePath) => {
const stats = fs.statSync(filePath)
const ext = path.extname(filePath)
const fileType = getFileType(ext)
return {
id: uuidv4(),
origin_name: path.basename(filePath),
name: path.basename(filePath),
path: filePath,
created_at: stats.birthtime,
size: stats.size,
ext: ext,
type: fileType,
count: 1
}
})
return Promise.all(fileMetadataPromises)
}
public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise<FileType> => {
const duplicateFile = await this.findDuplicateFile(file.path)
if (duplicateFile) {
return duplicateFile
}
const uuid = uuidv4()
const origin_name = path.basename(file.path)
const ext = path.extname(origin_name)
const destPath = path.join(this.storageDir, uuid + ext)
await fs.promises.copyFile(file.path, destPath)
const stats = await fs.promises.stat(destPath)
const fileType = getFileType(ext)
const fileMetadata: FileType = {
id: uuid,
origin_name,
name: uuid + ext,
path: destPath,
created_at: stats.birthtime,
size: stats.size,
ext: ext,
type: fileType,
count: 1
}
return fileMetadata
}
public getFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<FileType | null> => {
if (!fs.existsSync(filePath)) {
return null
}
const stats = fs.statSync(filePath)
const ext = path.extname(filePath)
const fileType = getFileType(ext)
const fileInfo: FileType = {
id: uuidv4(),
origin_name: path.basename(filePath),
name: path.basename(filePath),
path: filePath,
created_at: stats.birthtime,
size: stats.size,
ext: ext,
type: fileType,
count: 1
}
return fileInfo
}
public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
await fs.promises.unlink(path.join(this.storageDir, id))
}
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => {
const filePath = path.join(this.storageDir, id)
return fs.readFileSync(filePath, 'utf8')
}
public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {
const tempDir = path.join(app.getPath('temp'), 'CherryStudio')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
const tempFilePath = path.join(tempDir, `temp_file_${uuidv4()}_${fileName}`)
return tempFilePath
}
public writeFile = async (
_: Electron.IpcMainInvokeEvent,
filePath: string,
data: Uint8Array | string
): Promise<void> => {
await fs.promises.writeFile(filePath, data)
}
public base64Image = async (
_: Electron.IpcMainInvokeEvent,
id: string
): Promise<{ mime: string; base64: string; data: string }> => {
const filePath = path.join(this.storageDir, id)
const data = await fs.promises.readFile(filePath)
const base64 = data.toString('base64')
const mime = `image/${path.extname(filePath).slice(1)}`
return {
mime,
base64,
data: `data:${mime};base64,${base64}`
}
}
public clear = async (): Promise<void> => {
await fs.promises.rmdir(this.storageDir, { recursive: true })
await this.initStorageDir()
}
public open = async (
_: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions
): Promise<{ fileName: string; filePath: string; content: Buffer } | null> => {
try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '打开文件',
properties: ['openFile'],
filters: [{ name: '所有文件', extensions: ['*'] }],
...options
})
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0]
const fileName = filePath.split('/').pop() || ''
const content = await readFile(filePath)
return { fileName, filePath, content }
}
return null
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
return null
}
}
public save = async (
_: Electron.IpcMainInvokeEvent,
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<void> => {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
defaultPath: fileName,
...options
})
if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
}
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
}
public saveImage = async (_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise<void> => {
try {
const filePath = dialog.showSaveDialogSync({
defaultPath: `${name}.png`,
filters: [{ name: 'PNG Image', extensions: ['png'] }]
})
if (filePath) {
const base64Data = data.replace(/^data:image\/png;base64,/, '')
fs.writeFileSync(filePath, base64Data, 'base64')
}
} catch (error) {
logger.error('[IPC - Error]', 'An error occurred saving the image:', error)
}
}
public selectFolder = async (_: Electron.IpcMainInvokeEvent, options: OpenDialogOptions): Promise<string | null> => {
try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '选择文件夹',
properties: ['openDirectory'],
...options
})
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]
}
return null
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred selecting the folder:', err)
return null
}
}
}
export default FileManager

View File

@@ -0,0 +1,66 @@
import { WebDavConfig } from '@types'
import Logger from 'electron-log'
import Stream from 'stream'
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
export default class WebDav {
public instance: WebDAVClient | undefined
private webdavPath: string
constructor(params: WebDavConfig) {
this.webdavPath = params.webdavPath.replace('/', '')
this.instance = createClient(params.webdavHost, {
username: params.webdavUser,
password: params.webdavPass
})
this.putFileContents = this.putFileContents.bind(this)
this.getFileContents = this.getFileContents.bind(this)
}
public putFileContents = async (
filename: string,
data: string | BufferLike | Stream.Readable,
options?: PutFileContentsOptions
) => {
if (!this.instance) {
return new Error('WebDAV client not initialized')
}
try {
if (!(await this.instance.exists(this.webdavPath))) {
await this.instance.createDirectory(this.webdavPath, {
recursive: true
})
}
} catch (error) {
Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
throw error
}
const remoteFilePath = `${this.webdavPath}/${filename}`
try {
return await this.instance.putFileContents(remoteFilePath, data, options)
} catch (error) {
Logger.error('[WebDAV] Error putting file contents on WebDAV:', error)
throw error
}
}
public getFileContents = async (filename: string, options?: GetFileContentsOptions) => {
if (!this.instance) {
throw new Error('WebDAV client not initialized')
}
const remoteFilePath = `${this.webdavPath}/${filename}`
try {
return await this.instance.getFileContents(remoteFilePath, options)
} catch (error) {
Logger.error('[WebDAV] Error getting file contents on WebDAV:', error)
throw error
}
}
}

View File

@@ -1,67 +1,106 @@
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import logger from 'electron-log'
import { writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
import { FileTypes } from '../../renderer/src/types'
export async function saveFile(
_: Electron.IpcMainInvokeEvent,
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<void> {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
defaultPath: fileName,
...options
})
if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
}
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
}
export async function openFile(
_: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions
): Promise<{ fileName: string; content: Buffer } | null> {
try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '打开文件',
properties: ['openFile'],
filters: [{ name: '所有文件', extensions: ['*'] }],
...options
})
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0]
const fileName = filePath.split('/').pop() || ''
const content = await readFile(filePath)
return { fileName, content }
}
return null
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
return null
}
}
export function getFileType(ext: string): FileTypes {
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt']
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.properties', // 配置属性文件
'.latex', // LaTeX 文档文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.gradle', // Gradle 构建文件
'.kts' // Kotlin Script 文件
]
ext = ext.toLowerCase()
if (imageExts.includes(ext)) return FileTypes.IMAGE
if (videoExts.includes(ext)) return FileTypes.VIDEO
if (audioExts.includes(ext)) return FileTypes.AUDIO
if (textExts.includes(ext)) return FileTypes.TEXT
if (documentExts.includes(ext)) return FileTypes.DOCUMENT
return FileTypes.OTHER
}

View File

@@ -1,39 +0,0 @@
import util from 'node:util'
import zlib from 'node:zlib'
import logger from 'electron-log'
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
const gzipPromise = util.promisify(zlib.gzip)
const gunzipPromise = util.promisify(zlib.gunzip)
/**
* 压缩字符串
* @param {string} string - 要压缩的 JSON 字符串
* @returns {Promise<Buffer>} 压缩后的 Buffer
*/
export async function compress(str) {
try {
const buffer = Buffer.from(str, 'utf-8')
const compressedBuffer = await gzipPromise(buffer)
return compressedBuffer
} catch (error) {
logger.error('Compression failed:', error)
throw error
}
}
/**
* 解压缩 Buffer 到 JSON 字符串
* @param {Buffer} compressedBuffer - 压缩的 Buffer
* @returns {Promise<string>} 解压缩后的 JSON 字符串
*/
export async function decompress(compressedBuffer) {
try {
const buffer = await gunzipPromise(compressedBuffer)
return buffer.toString('utf-8')
} catch (error) {
logger.error('Decompression failed:', error)
throw error
}
}

View File

@@ -27,6 +27,7 @@ export function createMainWindow() {
autoHideMenuBar: true,
transparent: process.platform === 'darwin',
vibrancy: 'fullscreen-ui',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
trafficLightPosition: { x: 8, y: 12 },

View File

@@ -1,6 +1,8 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types'
import type { OpenDialogOptions } from 'electron'
import { Readable } from 'stream'
declare global {
interface Window {
@@ -14,21 +16,31 @@ declare global {
checkForUpdate: () => void
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
saveFile: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
openFile: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null>
setTheme: (theme: 'light' | 'dark') => void
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
reload: () => void
compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string>
backup: {
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
restore: (backupPath: string) => Promise<string>
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
}
file: {
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
upload: (file: FileType) => Promise<FileType>
delete: (fileId: string) => Promise<{ success: boolean }>
delete: (fileId: string) => Promise<void>
read: (fileId: string) => Promise<string>
clear: () => Promise<void>
}
image: {
base64: (filePath: string) => Promise<{ mime: string; base64: string; data: string }>
get: (filePath: string) => Promise<FileType | null>
selectFolder: () => Promise<string | null>
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
saveImage: (name: string, data: string) => void
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
}
}
}

View File

@@ -1,4 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload'
import { WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer
@@ -9,21 +10,30 @@ const api = {
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
reload: () => ipcRenderer.invoke('reload'),
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
return ipcRenderer.invoke('save-file', path, content, options)
backup: {
backup: (fileName: string, data: string, destinationPath?: string) =>
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
},
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
clear: () => ipcRenderer.invoke('file:clear')
},
image: {
base64: (filePath: string) => ipcRenderer.invoke('image:base64', filePath)
read: (fileId: string) => ipcRenderer.invoke('file:read', fileId),
clear: () => ipcRenderer.invoke('file:clear'),
get: (filePath: string) => ipcRenderer.invoke('file:get', filePath),
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
save: (path: string, content: string, options?: { compress: boolean }) =>
ipcRenderer.invoke('file:save', path, content, options),
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId)
}
}

View File

@@ -6,9 +6,31 @@
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
<style>
html,
body {
margin: 0;
}
#spinner {
position: fixed;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.5);
}
#spinner img {
width: 100px;
}
</style>
</head>
<body>
<div id="root"></div>
<div id="spinner">
<img src="/src/assets/images/logo/cherry-text.svg" />
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
<path
fill="#FFD21E"
d="M4 15.55C4 9.72 8.72 5 14.55 5h4.11a9.34 9.34 0 1 1 0 18.68H7.58l-2.89 2.8a.41.41 0 0 1-.69-.3V15.55Z"
/>
<path
fill="#32343D"
d="M19.63 12.48c.37.14.52.9.9.7.71-.38.98-1.27.6-1.98a1.46 1.46 0 0 0-1.98-.61 1.47 1.47 0 0 0-.6 1.99c.17.34.74-.21 1.08-.1ZM12.72 12.48c-.37.14-.52.9-.9.7a1.47 1.47 0 0 1-.6-1.98 1.46 1.46 0 0 1 1.98-.61c.71.38.98 1.27.6 1.99-.18.34-.74-.21-1.08-.1ZM16.24 19.55c2.89 0 3.82-2.58 3.82-3.9 0-1.33-1.71.7-3.82.7-2.1 0-3.8-2.03-3.8-.7 0 1.32.92 3.9 3.8 3.9Z"
/>
<path
fill="#FF323D"
d="M18.56 18.8c-.57.44-1.33.75-2.32.75-.92 0-1.65-.27-2.2-.68.3-.63.87-1.11 1.55-1.32.12-.03.24.17.36.38.12.2.24.4.37.4s.26-.2.39-.4.26-.4.38-.36a2.56 2.56 0 0 1 1.47 1.23Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
<g>
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
</g>
<g>
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
</g>
<g>
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.39 115.44">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M25.31,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M40.64,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04c3.17-3.23,7.36-5.01,11.81-5.01s8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M40.64,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M10.19,90.22l.39-.28c.49-.29.83-.43,1.03-.43.45,0,.93.4,1.44,1.21.32.5.47.9.47,1.21s-.1.55-.29.75c-.19.2-.42.38-.68.54-.26.16-.51.31-.74.45-.24.14-.72.33-1.45.56-.73.23-1.44.34-2.12.34s-1.37-.09-2.07-.27c-.7-.18-1.41-.48-2.15-.9-.74-.42-1.4-.94-1.99-1.55-.59-.61-1.07-1.39-1.45-2.35-.38-.96-.57-1.99-.57-3.11s.19-2.14.56-3.05c.37-.91.85-1.67,1.43-2.26.58-.6,1.25-1.09,1.99-1.5,1.41-.78,2.82-1.16,4.24-1.16.67,0,1.36.1,2.06.31.7.21,1.22.42,1.58.64l.52.3c.26.16.46.29.6.39.37.3.56.64.56,1.02s-.15.78-.45,1.2c-.56.78-1.06,1.16-1.51,1.16-.26,0-.62-.16-1.1-.47-.6-.49-1.41-.73-2.41-.73-.93,0-1.85.32-2.76.97-.43.32-.79.76-1.08,1.34-.29.57-.43,1.22-.43,1.95s.14,1.37.43,1.95c.29.57.65,1.03,1.1,1.36.88.63,1.79.95,2.74.95.45,0,.87-.06,1.26-.17.39-.11.68-.23.85-.34Z"/>
<path class="cls-3" d="M24.7,79.2c.11-.22.31-.37.58-.45.27-.09.62-.13,1.03-.13s.75.04.99.11c.24.07.43.16.56.26.13.1.23.24.3.43.07.24.11.62.11,1.12v11.95c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-4.37h-5.71v4.39c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v4.39h5.71v-4.42c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57Z"/>
<path class="cls-3" d="M33.82,90.58h6.63c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-8.53c-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98c0-.73.14-1.23.41-1.5.27-.27.79-.4,1.55-.4h8.49c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-6.61v2.18h4.26c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.82,1.77-.24.09-.62.13-1.12.13h-4.22v2.18Z"/>
<path class="cls-3" d="M83.34,79c.7.49,1.06.96,1.06,1.42,0,.27-.17.65-.5,1.14l-4.65,6.96v4.11c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.11.22-.31.37-.58.45-.27.09-.64.13-1.1.13s-.83-.04-1.1-.13c-.27-.09-.47-.24-.58-.46-.11-.22-.18-.42-.2-.58-.02-.17-.03-.42-.03-.76v-4.07l-4.65-6.96c-.33-.49-.5-.87-.5-1.14,0-.46.32-.9.95-1.32.63-.42,1.08-.64,1.36-.64s.49.06.65.17c.24.16.5.45.78.88l3.34,5.34,3.34-5.34c.27-.43.51-.71.71-.85s.43-.2.7-.2.69.18,1.26.54Z"/>
<g>
<path class="cls-3" d="M1.66,112.96c-.37-.46-.56-.87-.56-1.24s.31-.85.93-1.45c.36-.34.74-.52,1.14-.52s.96.36,1.68,1.08c.2.24.49.48.86.7.37.22.72.33,1.03.33,1.34,0,2-.55,2-1.64,0-.33-.18-.61-.55-.83-.37-.22-.82-.38-1.37-.48-.55-.1-1.13-.26-1.77-.48-.63-.22-1.22-.48-1.77-.79-.55-.3-1-.78-1.37-1.43-.37-.65-.55-1.44-.55-2.36,0-1.26.47-2.37,1.41-3.31s2.22-1.41,3.84-1.41c.86,0,1.65.11,2.36.33.71.22,1.2.45,1.48.68l.54.41c.45.42.67.77.67,1.06s-.17.68-.52,1.18c-.49.72-.99,1.08-1.51,1.08-.3,0-.67-.14-1.12-.43-.04-.03-.13-.1-.25-.22-.12-.11-.23-.21-.33-.28-.3-.19-.69-.28-1.15-.28s-.85.11-1.16.33c-.31.22-.46.53-.46.93s.18.71.55.96c.37.24.82.41,1.37.5.55.09,1.14.22,1.79.4.65.18,1.24.4,1.79.66.55.26,1,.71,1.37,1.35.37.64.55,1.42.55,2.36s-.19,1.76-.56,2.47c-.37.71-.86,1.26-1.46,1.65-1.16.76-2.4,1.14-3.73,1.14-.68,0-1.31-.08-1.92-.25-.6-.17-1.09-.37-1.46-.61-.76-.46-1.29-.9-1.59-1.34l-.19-.24Z"/>
<path class="cls-3" d="M15.02,99.37h11.98c.46,0,.8.05,1.01.16.22.11.36.28.43.51.07.23.11.53.11.9s-.04.67-.11.89c-.07.22-.19.38-.37.46-.26.13-.62.19-1.1.19h-4.11v10.83c0,.33-.01.57-.03.73s-.09.34-.19.55c-.11.21-.3.36-.57.44-.27.09-.63.13-1.08.13s-.8-.04-1.07-.13c-.27-.09-.45-.23-.56-.44-.11-.21-.17-.4-.19-.56-.02-.17-.03-.41-.03-.74v-10.81h-4.14c-.46,0-.8-.05-1.01-.16-.22-.11-.36-.28-.43-.51-.07-.23-.11-.53-.11-.9s.04-.67.11-.89c.07-.22.19-.38.37-.46.26-.13.62-.19,1.1-.19Z"/>
<path class="cls-3" d="M40.05,99.98c.14-.23.35-.39.62-.47.27-.09.61-.13,1.02-.13s.74.04.98.11c.24.07.43.16.56.26.13.1.22.25.28.45.09.24.13.62.13,1.12v6.5c0,1.9-.59,3.62-1.77,5.17-.57.73-1.31,1.32-2.22,1.78s-1.91.68-3,.68-2.1-.23-2.99-.69c-.9-.46-1.63-1.06-2.19-1.81-1.16-1.52-1.74-3.25-1.74-5.17v-6.48c0-.34.01-.6.03-.76.02-.17.09-.36.2-.57.11-.22.31-.37.58-.45.27-.09.64-.13,1.1-.13s.83.04,1.1.13c.27.09.46.24.56.45.17.33.26.78.26,1.36v6.46c0,.88.22,1.71.65,2.5.22.4.54.72.97.97.43.24.94.37,1.53.37,1.05,0,1.83-.39,2.35-1.16.52-.78.78-1.67.78-2.69v-6.59c0-.56.07-.95.22-1.18Z"/>
<path class="cls-3" d="M47.28,99.37l3.98.02c2.08,0,3.91.75,5.49,2.25,1.58,1.5,2.37,3.35,2.37,5.54s-.77,4.07-2.32,5.63c-1.54,1.57-3.41,2.35-5.61,2.35h-3.94c-.88,0-1.42-.18-1.64-.54-.17-.3-.26-.76-.26-1.38v-11.98c0-.34.01-.6.03-.75s.09-.34.2-.56c.2-.39.76-.58,1.68-.58ZM51.27,111.35c1.03,0,1.97-.38,2.8-1.15.83-.77,1.25-1.73,1.25-2.9s-.41-2.14-1.22-2.92c-.81-.78-1.76-1.17-2.85-1.17h-2.07v8.14h2.09Z"/>
<path class="cls-3" d="M60.53,101.29c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v11.98c0,.34-.01.6-.03.75s-.09.34-.2.56c-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98Z"/>
<path class="cls-3" d="M73.47,99.2c2.13,0,3.97.77,5.54,2.3,1.57,1.54,2.35,3.44,2.35,5.72s-.75,4.21-2.24,5.82c-1.49,1.6-3.33,2.4-5.51,2.4s-4.04-.79-5.57-2.37c-1.53-1.58-2.29-3.46-2.29-5.64,0-1.19.22-2.31.65-3.35.43-1.04,1.01-1.91,1.72-2.62.72-.7,1.54-1.26,2.48-1.66.93-.4,1.9-.6,2.89-.6ZM69.55,107.32c0,1.28.41,2.32,1.24,3.11.83.8,1.75,1.2,2.77,1.2s1.94-.39,2.76-1.16c.82-.78,1.23-1.82,1.23-3.12s-.41-2.35-1.24-3.14c-.83-.79-1.75-1.18-2.77-1.18s-1.94.4-2.76,1.2c-.82.8-1.23,1.83-1.23,3.11Z"/>
</g>
<g>
<path class="cls-3" d="M69.11,80.61c-.02-.17-.09-.36-.2-.57-.2-.39-.76-.58-1.68-.58h-4.65c-1.26,0-2.49.46-3.68,1.38-.57.45-1.05,1.05-1.42,1.81-.37.76-.56,1.61-.56,2.54,0,1.62.54,2.96,1.62,4.01-.32.76-.8,1.89-1.46,3.38-.22.52-.32.89-.32,1.12,0,.55.45,1.01,1.34,1.38.46.2.83.3,1.11.3s.51-.07.69-.2c.18-.14.31-.28.4-.42.14-.27.7-1.57,1.68-3.9l.67.04h2.71v2.43c0,.33.01.58.03.74.02.17.09.36.2.57.2.39.76.58,1.68.58,1.01,0,1.59-.27,1.77-.8.09-.24.13-.62.13-1.12v-11.95c0-.33-.01-.58-.03-.74ZM66.19,86.56c-.06.1-.34.54-.88.65-.14.03-.26.02-.34.02-.01.1-.03.19-.04.27-.11.59-.27.74-.4.76-.21.03-.36-.25-.66-.64-.16,0-.32.01-.49.01-.36,0-.72-.02-1.06-.06-.18-.02-.35-.05-.52-.08-.84-.22-1.44-.9-1.44-1.7v-.97c0-.8.61-1.48,1.44-1.7.17-.03.34-.06.52-.08.34-.04.69-.06,1.06-.06s.72.02,1.06.06c.18.02.35.05.52.08.43.12.8.35,1.06.66.24.29.39.65.39,1.05v.97c0,.16,0,.47-.21.78Z"/>
<circle class="cls-2" cx="62.23" cy="85.3" r=".35"/>
<circle class="cls-2" cx="63.4" cy="85.33" r=".35"/>
<circle class="cls-2" cx="64.61" cy="85.33" r=".35"/>
</g>
<g>
<path class="cls-3" d="M43.62,78.61c.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58h4.65c1.26,0,2.49.46,3.68,1.38.57.45,1.05,1.05,1.42,1.81.37.76.56,1.61.56,2.54,0,1.62-.54,2.96-1.62,4.01.32.76.8,1.89,1.46,3.38.22.52.32.89.32,1.12,0,.55-.45,1.01-1.34,1.38-.46.2-.83.3-1.11.3s-.51-.07-.69-.2c-.18-.14-.31-.28-.4-.42-.14-.27-.7-1.57-1.68-3.9l-.67.04h-2.71v2.43c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74ZM46.53,84.56c.06.1.34.54.88.65.14.03.26.02.34.02.01.1.03.19.04.27.11.59.27.74.4.76.21.03.36-.25.66-.64.16,0,.32.01.49.01.36,0,.72-.02,1.06-.06.18-.02.35-.05.52-.08.84-.22,1.44-.9,1.44-1.7v-.97c0-.8-.61-1.48-1.44-1.7-.17-.03-.34-.06-.52-.08-.34-.04-.69-.06-1.06-.06s-.72.02-1.06.06c-.18.02-.35.05-.52.08-.43.12-.8.35-1.06.66-.24.29-.39.65-.39,1.05v.97c0,.16,0,.47.21.78Z"/>
<circle class="cls-2" cx="50.49" cy="83.3" r=".35"/>
<circle class="cls-2" cx="49.32" cy="83.33" r=".35"/>
<circle class="cls-2" cx="48.11" cy="83.33" r=".35"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Some files were not shown because too many files have changed in this diff Show More