This commit is contained in:
2025-10-29 22:20:21 +08:00
commit 32b3b7b29a
111 changed files with 344425 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
name: Nuitka 编译 Linux 可执行文件
on:
workflow_dispatch:
inputs:
build_reason:
description: "触发构建的原因(选填)"
required: false
default: "手动触发 Linux 可执行文件构建"
jobs:
compile-linux-exe:
runs-on: ubuntu-latest
steps:
# 步骤1Gitea 原生拉取代码(解决 GitHub 超时)
- name: Gitea 原生拉取仓库代码
uses: actions/checkout@v4
# 步骤2配置 Linux 编译环境与依赖(移除 mingw-w64 交叉工具)
- name: 配置编译环境与依赖
run: |
sudo apt update && sudo apt install -y python3-venv python3-dev patchelf
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \
fake_useragent==2.2.0 \
loguru==0.7.3 \
numpy==2.3.3 \
Pillow==11.3.0 \
pycryptodome==3.21.0 \
PyQt6==6.9.2 \
PyQt6-fluent-widgets==1.8.4 \
PySideSix_Frameless_Window==0.4.3 \
Requests==2.32.5 \
rsa==4.9.1 \
nuitka
# 步骤3Nuitka 编译 Linux 可执行文件(删除 --mingw64 交叉参数)
- name: Nuitka 编译 Linux 可执行文件
run: |
source .venv/bin/activate
nuitka --standalone \
--assume-yes-for-downloads \
--nofollow-import-to=numpy,scipy,PIL,colorthief \
--enable-plugins=PyQt6 \
--show-progress \
--output-dir=build \
./main.py
# 步骤4上传 Linux 产物(路径不变,产物为无后缀名的可执行文件)
- name: 上传 Linux 可执行产物
uses: actions/upload-artifact@v3
with:
name: Linux-Executable-Output
path: ./build/* # Linux 产物无后缀,默认与源码主文件同名
retention-days: 7

68
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: Bug 反馈
description: 报告 App 中的功能异常、崩溃或显示问题
title: "[BUG] "
labels: ["bug", "to-triage"]
assignees: []
body:
- type: textarea
id: bug-description
attributes:
label: 1. 问题描述
description: 清晰说明 Bug 现象点击支付按钮后页面卡住3秒后闪退
placeholder: 请简要描述你遇到的问题...
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: 2. 复现步骤
description: 提供可复现的详细操作步骤,越具体越易定位问题
placeholder: 1. 打开 App 并登录账号\n2. 进入「订单」页面\n3. 点击「待支付」订单\n4. 选择「微信支付」后触发异常
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: 3. 预期结果
description: 描述你认为正常的功能表现
placeholder: 点击「微信支付」后应唤起微信支付界面...
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: 4. 实际结果
description: 描述当前的异常结果(含错误提示、日志等)
placeholder: 点击后页面无响应,弹出「支付通道异常」提示框...
validations:
required: true
- type: input
id: device-info
attributes:
label: 5. 设备信息
description: iPhone 15 (iOS 17.5) / 华为 Mate 60 (Android 14)
placeholder: 请填写设备型号与系统版本
validations:
required: true
- type: input
id: app-version
attributes:
label: 6. App 版本
description: 可在「我的-关于」页面查看v3.1.2
placeholder: 请填写当前使用的 App 版本号
validations:
required: true
- type: textarea
id: extra-info
attributes:
label: 7. 补充信息
description: 可附上截图/录屏链接、复现概率10次中9次出现
placeholder: 复现概率100%已附上闪退日志链接xxx...
validations:
required: false

View File

@@ -0,0 +1,52 @@
name: 功能需求
description: 提出 App 的新功能或现有功能优化建议
title: "[FEATURE] "
labels: ["enhancement", "needs-discuss"]
assignees: []
body:
- type: textarea
id: feature-description
attributes:
label: 1. 需求描述
description: 清晰说明新增/优化的功能内容(含核心目标)
placeholder: 新增「消息免打扰」功能,支持按聊天对象单独设置免打扰时段...
validations:
required: true
- type: textarea
id: feature-background
attributes:
label: 2. 需求背景
description: 解释需求的价值(如用户痛点、业务目标等)
placeholder: 现有消息通知无细分控制,夜间频繁消息影响用户休息,导致部分用户关闭全局通知...
validations:
required: true
- type: textarea
id: feature-details
attributes:
label: 3. 功能细节
description: 说明交互逻辑、设计参考、特殊场景适配等
validations:
required: true
- type: dropdown
id: priority-level
attributes:
label: 4. 需求优先级
description: 根据影响范围与紧急度选择
options:
- 高(解决核心痛点,影响百万级用户)
- 中(提升关键体验,用户反馈较多)
- 低(优化项,非核心诉求)
validations:
required: true
- type: textarea
id: reference-info
attributes:
label: 5. 参考信息
description: 可附上竞品截图、设计稿链接、用户调研数据等
placeholder: 参考「钉钉」的免打扰设置逻辑已上传竞品截图链接xxx...
validations:
required: false

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Build and Release Folders
bin-debug/
bin-release/
__pycache__/
logs/
config/
test/
build/
image_cache/
download/
text_cache/
.vscode/
[Oo]bj/
[Bb]in/
venv/
# Other files and folders
.settings/
# Executables
*.swf
*.air
*.ipa
*.apk
*.pyc
*.exe
/.idea/
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
# should NOT be excluded as they contain compiler settings and other important
# information for Eclipse / Flash Builder.

View File

@@ -0,0 +1,3 @@
api有关的需要用cloudreve v4 api的mcp来查询
写完代码后不需要运行一遍main.py

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

101
Miaostars-0.0.2.txt Normal file
View File

@@ -0,0 +1,101 @@
1. 09-88-4069
2. 72-33-8317
3. 93-54-4878
4. 93-86-1869
5. 14-04-1203
6. 84-00-2929
7. 04-82-6399
8. 79-11-1229
9. 11-06-1474
10. 07-72-4607
11. 56-87-7920
12. 53-25-2426
13. 67-39-6157
14. 63-39-2233
15. 17-96-4158
16. 17-62-6178
17. 15-37-0827
18. 59-52-8112
19. 03-21-4560
20. 55-11-5109
21. 67-99-2556
22. 91-20-2424
23. 22-43-2052
24. 77-64-5252
25. 59-35-8073
26. 55-31-5493
27. 32-76-3175
28. 84-91-2685
29. 87-12-2589
30. 80-19-6183
31. 01-53-7563
32. 48-27-6989
33. 11-13-8688
34. 09-54-6589
35. 07-23-1493
36. 59-42-4357
37. 35-19-0434
38. 14-36-4400
39. 02-87-5750
40. 70-48-9729
41. 72-69-7715
42. 46-99-7577
43. 38-14-9272
44. 40-42-7732
45. 96-91-2081
46. 23-68-6080
47. 91-39-5462
48. 19-92-2732
49. 72-23-5102
50. 27-88-0690
51. 15-45-5394
52. 05-99-9553
53. 99-05-6922
54. 01-89-7062
55. 48-19-1418
56. 55-07-8466
57. 96-22-4957
58. 53-82-8031
59. 18-11-2941
60. 98-93-7633
61. 63-94-6925
62. 12-74-5129
63. 34-17-2602
64. 03-04-7664
65. 90-38-9969
66. 24-89-8276
67. 97-89-9536
68. 80-32-7705
69. 72-32-7795
70. 12-80-4790
71. 73-84-9545
72. 91-03-5053
73. 52-40-1281
74. 74-75-2213
75. 17-50-3710
76. 60-36-2783
77. 98-57-1091
78. 83-59-9231
79. 29-48-8192
80. 12-91-0807
81. 74-25-2849
82. 64-82-0610
83. 10-23-2036
84. 91-63-7704
85. 28-68-1631
86. 47-23-2650
87. 38-04-5634
88. 96-08-5695
89. 03-56-8427
90. 39-51-0578
91. 44-87-6122
92. 64-20-1925
93. 93-14-6077
94. 98-76-4163
95. 94-60-5635
96. 52-43-0973
97. 33-16-2106
98. 12-66-8985
99. 24-57-6007
100. 40-23-4384
<##-##-####>

81
README.md Normal file
View File

@@ -0,0 +1,81 @@
<p align="center">
<img width="50%" align="center" src="https://gitee.com/samwulqy/meow-nebula-disk-client/raw/master/logo.png" alt="logo">
</p>
<h1 align="center">
LeonPan客户端电脑版
</h1>
> **开源·高效·智能** 您的云端文件管家
[![AGPL3 License](https://img.shields.io/badge/license-AGPL3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![PyQt6](https://img.shields.io/badge/GUI-PyQt6-green.svg)](https://www.qt.io/qt-for-python)
[![Cloudreve API](https://img.shields.io/badge/Backend-Cloudreve_API-orange.svg)](https://cloudreve.apifox.cn/doc-4217598)
**官方网站**: [https://mp.miaostars.com](https://mp.miaostars.com)
**开发公司**: 武汉市喵星创想互联网科技有限公司
---
## ✨ 产品介绍
LeonPan客户端是官方推出的桌面应用程序基于Cloudreve后端API开发采用AGPL3开源协议。我们扩展了多项实用功能为您提供更完善的云端文件管理体验。
![实图展示](demoImg1.png)
## 🚀 特色功能
### 文件管理
- 📁 **智能相册** - 自动分类照片,支持人脸识别和场景识别
-**文件历史版本** - 保留文件修改历史,随时回溯
- 🕰️ **时间线整理** - 按时间自动整理照片和文档
### 下载传输
-**BT种子下载** - 直接解析种子文件到云盘
- 📶 **离线传输** - 添加链接后台自动下载
- 🛠️ **批量任务管理** - 多任务并行控制,智能限速
### 文件处理
- 🔄 **格式转换** - 下载后自动转换文档/图片格式
- 🖼️ **图片压缩** - 智能压缩图片体积
- 📚 **PDF工具** - 合并/拆分PDF文档
- ✏️ **简易文本编辑** - 云端文件快速修改
# 产品版本对比
| 项目 | 社区版 | 商业版 |
|------|--------|--------|
| 价格 | 免费 | 188元/年 或 688元/永久 |
| 授权商业化 | 否 | 是 |
| 基础功能 | 是 | 是 |
| 文件转换 | 否 | 是 |
| 智能相册 | 否 | 是 |
| 安全登录 | 否 | 是 |
| 离线传输 | 否 | 是 |
| 安全检验 | 是 | 是 |
| 无视AGPL-3 | 否 | 是 |
| 版本悬挂(社区版或商业版) | 是 | 是 |
| 搜索 | 否 | 是 |
| AI功能 | 否 | 是 |
| 官方交流 | 否 | 是 |
## 📞 联系我们
- 客服邮箱: support@miaostars.com
- 官方网站: [官方网站](https://mp.miaostars.com)
- 用户社区: [官方论坛](https://miaoclub.top)
- 公司官网: [公司网站](https://www.miaostars.com)
- 授权: [商业版授权](https://mp.miaostars.com/client)
# Linux运行
进入Action页面找到最新的build记录下载里面编译好的zip文件
解压这个zip文件打开main.dist文件夹终端cd到这个文件夹然后授予运行权限并运行
```sh
$ chmod -x main.bin
$ ./main.bin
```
<!-- 这tm是啥玩意html是这么写的吗 -->
<div align="center">
---
<!-- <div> -->
© 2025 武汉市喵星创想互联网科技有限公司 · 保留所有权利
</div>

35
app/core/__init__.py Normal file
View File

@@ -0,0 +1,35 @@
from .api import miaoStarsBasicApi
from .utils.morelang import lang
from .utils.config import qconfig, cfg, userConfig, policyConfig
from .utils.signal_bus import signalBus
from .utils.format import getFileIcon, formatSize, formatDate
from .services.login_thread import CaptchaThread, LoginThread, RegisterThread
from .services.user_thread import (
UserNickNameUpdateThread,
UserAvatarUpdateThread,
GetUserAvatarThread,
GetPackThread,
GetPoliciesThread,
ChangePolicyThread,
DeleteTagThread,
AddTagThread,
)
from .services.file_thread import (
ListFileThread,
CreateFolderThread,
DeleteFileThread,
ListSearchThread,
ListShareThread,
UploadThread,
DownloadShareThread,
DownloadThread,
GetShareFileInfoThread,
UpdateFileContentThread,
)
from .services.preview_thread import TextLoaderThread, ImageLoaderThread

9
app/core/api/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
from .basicApi import MiaoStarsBasicApi
from ..utils.config import userConfig
miaoStarsBasicApi = MiaoStarsBasicApi()
# 从userConfig中恢复token如果有
token = userConfig.getToken()
if token:
miaoStarsBasicApi.setToken(token)

578
app/core/api/basicApi.py Normal file
View File

@@ -0,0 +1,578 @@
"""这里存放基本API包括:
登录、注册、图形验证码获取
用户配置获取、用户头像获取
用户存储策略获取、用户仓内文件获取
"""
import time
from typing import Literal, Optional
from urllib.parse import quote_plus
import requests
from loguru import logger
from PyQt6.QtCore import QBuffer, QByteArray, QIODevice
from PyQt6.QtGui import QPixmap
from ..utils import getCode
from ..utils.config import policyConfig, userConfig
class MiaoStarsBasicApi:
_publicHeader = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
}
def __init__(self, token: Optional[str] = None):
self.basicApi = "http://leonmmcoset.jjxmm.win:5212/api/v4"
self.session = requests.Session()
self.session.verify = True
self.session.headers.update(MiaoStarsBasicApi._publicHeader)
# Cloudreve V4 使用 JWT 认证
# 优先使用传入的token如果没有则尝试从userConfig获取
self.token = token or userConfig.getToken()
if self.token:
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
self.userId = None
def returnSession(self) -> requests.Session:
return self.session
def setToken(self, token: str):
"""设置 JWT token 并同步到全局配置"""
# 确保token是字符串类型
if isinstance(token, str):
self.token = token
self.session.headers.update({"Authorization": f"Bearer {token}"})
# 同步更新到全局userConfig确保认证信息持久化
userConfig.setToken(token)
elif isinstance(token, dict) and token.get("access_token"):
# 兼容处理如果传入的是token对象提取access_token
access_token = token["access_token"]
self.token = access_token
self.session.headers.update({"Authorization": f"Bearer {access_token}"})
userConfig.setToken(access_token)
def request(
self, method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"], url, **kwargs
) -> dict:
maxRetries = 3
timeout = 20
for attempt in range(maxRetries):
try:
# 发起请求
response = self.session.request(
method=method, url=self.basicApi + url, timeout=timeout, **kwargs
)
# 检查HTTP状态码
response.raise_for_status()
# 解析JSON响应
try:
r = response.json()
# 保留原始响应格式不要过早转换Cloudreve V4的响应
# Cloudreve V4的错误响应通常包含{code, msg},而不是{error}
# 让具体的API方法来处理响应格式转换
return r
except:
return response.content
except requests.exceptions.RequestException as e:
# 网络相关错误,进行重试
if attempt < maxRetries - 1:
time.sleep(1) # 简单固定延迟
continue
else:
logger.error(f"请求失败: {str(e)}")
return {"code": -1, "msg": str(e)}
except Exception as e:
logger.error(f"处理响应时出错: {str(e)}")
return {"code": -1, "msg": str(e)}
def getCaptcha(self):
"""获取图形验证码 (Cloudreve V4 API)"""
import logging
logger = logging.getLogger(__name__)
logger.debug(f"请求验证码API: {self.basicApi}/site/captcha")
r = self.request(method="GET", url="/site/captcha")
logger.debug(f"验证码API响应: {r}")
# 根据Cloudreve V4 API文档响应格式为
# {"code": 0, "data": {"image": "data:image/png;base64,...", "ticket": "..."}, "msg": ""}
# 转换响应格式以保持与前端的兼容性
if isinstance(r, dict):
# 检查是否是标准的Cloudreve V4成功响应
if r.get("code") == 0 and isinstance(r.get("data"), dict):
data = r["data"]
# 确保image字段存在且是有效的base64格式
if "image" in data:
# 保存ticket到实例变量供后续登录/注册使用
self.captcha_ticket = data.get("ticket", "")
logger.debug(f"成功获取验证码已保存ticket: {self.captcha_ticket}")
# 前端代码在CaptchaThread.run()中使用response["data"].split(",")[1]
# 所以需要确保返回的data格式符合前端期望
captcha_image = data["image"]
# 检查是否已经包含data:image/png;base64,前缀
if captcha_image.startswith("data:image/png;base64,"):
# 保持原样让前端去split
result = {"code": 0, "data": captcha_image}
else:
# 如果没有前缀加上前缀以确保前端能正确split
result = {"code": 0, "data": f"data:image/png;base64,{captcha_image}"}
logger.debug(f"返回前端验证码数据格式: {result}")
return result
# 处理可能的错误响应
elif "msg" in r:
logger.error(f"验证码API错误: {r['msg']}")
return {"code": -1, "msg": r["msg"]}
elif "error" in r:
logger.error(f"验证码API错误: {r['error']}")
return {"code": -1, "msg": r["error"]}
else:
logger.error(f"验证码API返回非字典格式: {type(r)}")
# 默认返回失败格式
logger.error("获取验证码失败,返回默认错误格式")
return {"code": -1, "msg": "获取验证码失败"}
def login(self, username, password, captcha):
"""登录 (Cloudreve V4 JWT认证)"""
url = "/session/token"
# Cloudreve V4 API参数规范使用email, password, captcha和ticket
payload = {
"email": username, # 更正参数名username -> email
"password": password,
"captcha": captcha # 更正参数名captcha_code -> captcha
}
# 如果有保存的ticket则添加到请求中使用正确的参数名ticket
if hasattr(self, 'captcha_ticket') and self.captcha_ticket:
payload['ticket'] = self.captcha_ticket # 更正参数名captcha_ticket -> ticket
r = self.request(
"POST",
url,
json=payload,
)
# 输出服务器返回的原始信息到控制台
print(f"登录API服务器返回原始信息: {r}")
# 处理登录响应
# Cloudreve V4的响应格式{code: 0, data: {user: {...}, token: {...}}, msg: ""}
if isinstance(r, dict):
# 检查是否是成功响应
if r.get("code") == 0 and r.get("data"):
data = r["data"]
# 设置 JWT token
if data.get("token") and isinstance(data["token"], dict):
# 提取access_token字段作为Bearer token值
access_token = data["token"].get("access_token")
if access_token:
# 设置到当前API实例
self.setToken(access_token)
# 同时保存到全局userConfig中确保认证信息持久化
userConfig.setToken(access_token)
# 存储用户信息
user_info = {
"code": 0,
"data": {
"id": data.get("user", {}).get("id"),
"nick": data.get("user", {}).get("nickname", data.get("user", {}).get("nick")),
"email": username
}
}
self.userId = data.get("user", {}).get("id")
return user_info
# 处理错误响应
# 优先检查msg字段因为Cloudreve V4返回的错误格式是{code, msg}而不是{error}
error_msg = r.get("msg", r.get("error", "登录失败"))
return {"code": -1, "msg": error_msg}
return {"code": -1, "msg": "登录失败:响应格式错误"}
def register(self, username, password, captcha):
"""注册"""
url = "/user"
# Cloudreve V4 API参数规范使用email, password, captcha和ticket
payload = {
"email": username,
"password": password,
"captcha": captcha # 更正参数名captcha_code -> captcha
}
# 如果有保存的ticket则添加到请求中使用正确的参数名ticket
if hasattr(self, 'captcha_ticket') and self.captcha_ticket:
payload['ticket'] = self.captcha_ticket # 更正参数名captcha_ticket -> ticket
r = self.request(
"POST",
url,
json=payload,
)
# 输出服务器返回的原始信息到控制台
print(f"注册API服务器返回原始信息: {r}")
# 处理注册响应
# Cloudreve V4的响应格式{code: 0, data: {...}, msg: ""} 或 {code: 错误码, msg: "错误信息"}
if isinstance(r, dict):
# 检查是否是成功响应
if r.get("code") == 0 or r.get("id"):
return {"code": 203, "msg": "注册成功"}
# 处理错误响应
# 优先检查msg字段因为Cloudreve V4返回的错误格式是{code, msg}而不是{error}
error_msg = r.get("msg", r.get("error", "注册失败"))
return {"code": -1, "msg": error_msg}
return {"code": -1, "msg": "注册失败:响应格式错误"}
def updateUserNickname(self, nickname):
"""更新用户昵称"""
url = "/user/profile"
r = self.request(
"PUT",
url,
json={"nick": nickname},
)
# 转换响应格式
if r.get("id"):
return {"code": 0, "msg": "更新成功"}
else:
return {"code": -1, "msg": r.get("error", "更新失败")}
def updateUserAvatar(self, qimage):
"""更新用户头像 - 专门处理 QImage 对象"""
url = "/user/avatar"
# 检查 QImage 是否有效
if qimage.isNull():
return {"code": -1, "msg": "QImage 对象为空"}
# 将 QImage 转换为 JPEG 格式字节数据
byte_array = QByteArray()
buffer = QBuffer(byte_array)
buffer.open(QIODevice.WriteOnly)
# 转换为 JPEG 格式,质量设为 90
success = qimage.save(buffer, "JPEG", 90)
buffer.close()
if not success:
return {"code": -1, "msg": "图像转换失败"}
# 获取字节数据
file_data = byte_array.data()
# 构建 multipart 请求
files = {
'avatar': ('avatar.jpg', file_data, 'image/jpeg')
}
# 移除 Content-Type 以便 requests 自动设置
headers = self.session.headers.copy()
if 'Content-Type' in headers:
del headers['Content-Type']
r = self.request("POST", url, files=files, headers=headers)
# 转换响应格式
if r.get("url"):
return {"code": 0, "msg": "头像更新成功"}
else:
return {"code": -1, "msg": r.get("error", "头像更新失败")}
def getUserAvatar(self, size: Literal["s", "m", "l"]):
"""获取用户头像"""
# Cloudreve V4 获取用户信息以获取头像URL
user_info = self.getUserInfo()
if user_info.get("code") != 0:
return QPixmap(":app/images/logo.png")
avatar_url = user_info.get("data", {}).get("avatar")
if not avatar_url:
return QPixmap(":app/images/logo.png")
try:
# 直接下载头像图片
response = requests.get(avatar_url, timeout=10)
response.raise_for_status()
pixmap = QPixmap()
if pixmap.loadFromData(response.content):
userConfig.setUserAvatarPixmap(pixmap)
return pixmap
else:
return QPixmap(":app/images/logo.png")
except Exception as e:
logger.error(f"获取头像失败: {e}")
return QPixmap(":app/images/logo.png")
def getUserInfo(self):
"""获取用户信息 (Cloudreve V4 API)"""
# 使用正确的API端点
if self.userId:
url = f"/user/info/{self.userId}"
else:
# 如果没有userId尝试获取当前用户信息
url = "/user/profile" # 这可能需要根据实际情况调整
r = self.request("GET", url)
# 转换响应格式
if isinstance(r, dict):
return {"code": 0, "data": r}
else:
return {"code": -1, "msg": "获取用户信息失败"}
def getUserPack(self):
"""获取用户存储详细 (Cloudreve V4 API)"""
# 使用正确的API端点
url = "/user/capacity"
r = self.request("GET", url)
# 转换响应格式以保持向后兼容
if isinstance(r, dict) and "used" in r:
return {
"code": 0,
"data": {
"base": r.get("total", 0) - r.get("extra", 0),
"pack": r.get("extra", 0),
"used": r.get("used", 0),
"total": r.get("total", 0),
"packs": []
}
}
else:
return {
"code": 0,
"data": {
"base": 0,
"pack": 0,
"used": 0,
"total": 0,
"packs": []
}
}
def list(self, path="/"):
"""列出用户仓内文件 (Cloudreve V4 API)"""
# 使用正确的API端点和必需参数
url = "/file"
# 将path转换为Cloudreve V4要求的URI格式
# 根目录映射到 cloudreve://my/
if path == "/" or path == "":
uri = "cloudreve://my/"
else:
# 使用quote_plus正确编码路径确保特殊字符被正确处理
# 首先规范化路径分隔符
normalized_path = path.strip("/").replace("\\", "/")
# 对路径部分进行URL编码
encoded_path = quote_plus(normalized_path)
# 由于quote_plus会将斜杠也编码我们需要恢复它们
encoded_path = encoded_path.replace("%2F", "/")
uri = f"cloudreve://my/{encoded_path}"
# 添加必需的分页参数
params = {
"uri": uri,
"page": 0, # 从第一页开始
"page_size": 100 # 使用合理的默认值
}
logger.debug(f"发送文件列表请求: URI={uri}")
r = self.request("GET", url, params=params)
# 转换响应格式以保持向后兼容
# 根据API规范文件列表在data.files中
if isinstance(r, dict) and "data" in r and "files" in r["data"]:
return {
"code": 0,
"data": r["data"]["files"]
}
else:
logger.warning(f"无效的响应格式: {r}")
return {"code": 0, "data": []}
def getPolicy(self):
"""获取用户存储策略"""
url = "/user/policies"
r = self.request("GET", url)
# 转换响应格式
if isinstance(r, list):
return {"code": 0, "data": r}
else:
return {"code": 0, "data": []}
def changePolicy(self, path, policy):
"""修改用户存储策略"""
url = "/file/move"
r = self.request("POST", url, json={"items": [path], "dst": path, "policy": policy})
# 转换响应格式
if r.get("count") == 1:
return {"code": 0, "msg": "存储策略修改成功"}
else:
return {"code": -1, "msg": r.get("error", "存储策略修改失败")}
def createFolder(self, name):
"""创建文件夹"""
url = "/file/create"
currentPath = policyConfig.returnCurrentPath()
# 根据Cloudreve V4 API规范构建uri参数确保不会有重复斜杠
if currentPath == "/":
uri = f"cloudreve://my/{name}"
else:
uri = f"cloudreve://my{currentPath}/{name}"
r = self.request("POST", url, json={"uri": uri, "type": "folder", "err_on_conflict": True})
# 转换响应格式
if r.get("data") and r.get("code") == 0:
return {"code": 0, "msg": "文件夹创建成功"}
else:
return {"code": -1, "msg": r.get("msg", "文件夹创建失败")}
def deleteFile(self, fileId, fileType: Literal["file", "dir"]):
"""删除文件"""
url = "/file/delete"
deleteData = {
"items": [fileId] if fileType == "file" else [],
"dirs": [fileId] if fileType == "dir" else []
}
r = self.request("POST", url, json=deleteData)
# 转换响应格式
if r.get("count") > 0:
return {"code": 0, "msg": "删除成功"}
else:
return {"code": -1, "msg": r.get("error", "删除失败")}
def wareSearch(
self,
searchContent,
searchType: Literal["keyword", "internalTag", "externalTag"],
):
"""搜索文件 (Cloudreve V4 API)"""
# 根据Cloudreve V4 API使用/file端点并添加搜索参数
url = "/file"
# 使用Cloudreve V4的URI格式
uri = "cloudreve://my/"
# 构建搜索参数
params = {
"uri": uri,
"keyword": searchContent,
"page": 0,
"page_size": 100
}
# 根据搜索类型调整参数
if searchType == "internalTag":
params["type"] = "internal"
elif searchType == "externalTag":
params["type"] = "tag"
logger.debug(f"发送文件搜索请求: 关键词={searchContent}, 类型={searchType}")
r = self.request("GET", url, params=params)
# 转换响应格式以保持向后兼容
if isinstance(r, dict):
if "data" in r:
if "files" in r["data"]:
# Cloudreve V4 API 格式
return {"code": 0, "data": {"objects": r["data"]["files"]}}
elif isinstance(r["data"], list):
# 如果data是列表直接使用
return {"code": 0, "data": {"objects": r["data"]}}
logger.warning(f"搜索响应格式不正确: {r}")
return {"code": 0, "data": {"objects": []}}
def shareSearch(self, keyword, orderBy, order, page):
"""搜索分享 (Cloudreve V4 API)"""
# 使用正确的API端点 - Cloudreve V4使用/share端点获取分享列表
url = "/share"
params = {
"page": page,
"page_size": 50, # 添加默认页面大小以避免"PageSize cannot be empty"错误
"order_by": orderBy,
"order": order,
"keyword": keyword, # 根据实际API支持情况调整
}
r = self.request("GET", url, params=params)
return r
def deleteTag(self, tagId):
"""删除标签"""
url = f"/tag/{tagId}"
r = self.request("DELETE", url)
# 转换响应格式
if r is None or (isinstance(r, dict) and not r.get("error")):
return {"code": 0, "msg": "标签删除成功"}
else:
return {"code": -1, "msg": r.get("error", "标签删除失败")}
def addTag(self, name, expression):
"""添加标签"""
url = "/tag/filter"
jsons = {
"expression": expression,
"name": name,
"color": "#ff9800",
"icon": "Circle",
}
r = self.request("POST", url, json=jsons)
# 转换响应格式
if r.get("id"):
return {"code": 0, "msg": "标签添加成功", "data": r}
else:
return {"code": -1, "msg": r.get("error", "标签添加失败")}
def getShareFileInfo(self, shareId):
"""获取分享文件信息"""
url = f"/share/{shareId}"
r = self.request("GET", url)
# 转换响应格式
if r.get("id"):
return {"code": 0, "data": r}
else:
return {"code": -1, "msg": r.get("error", "获取分享信息失败")}
def updateFileContent(self, fileId, content):
"""更新文件内容"""
url = f"/file/content/{fileId}"
headers = {
"Content-Type": "text/plain"
}
r = self.request("PUT", url, data=content.encode("utf-8"), headers=headers)
# 转换响应格式
if r.get("size") is not None:
return {"code": 0, "msg": "文件内容更新成功"}
else:
return {"code": -1, "msg": r.get("error", "文件内容更新失败")}
def updateUserNickname(self, nickName):
"""更新用户昵称 (Cloudreve V4 API)"""
url = "/user/profile"
data = {
"nick": nickName
}
r = self.request("PUT", url, json=data)
# 转换响应格式
if isinstance(r, dict) and not r.get("error"):
return {"code": 0, "msg": "昵称更新成功"}
else:
return {"code": -1, "msg": r.get("error", "昵称更新失败")}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
import base64
from loguru import logger
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import (QColor, QPainter, QPainterPath, QPen, QPixmap)
from ..api import miaoStarsBasicApi
from ...core import cfg, qconfig, userConfig
class CaptchaThread(QThread):
captchaReady = pyqtSignal(QPixmap)
captchaFailed = pyqtSignal(str)
def __init__(self):
super().__init__()
@staticmethod
def _createRoundedPixmap(pixmap, radius=10):
"""创建圆角图片"""
try:
# 获取原始图片尺寸
if pixmap.isNull():
logger.error("原始图片为空,无法创建圆角图片")
return pixmap
size = pixmap.size()
# 创建透明背景的图片
rounded_pixmap = QPixmap(size)
rounded_pixmap.fill(Qt.GlobalColor.transparent)
# 创建对象
painter = QPainter(rounded_pixmap)
painter.setRenderHints(
QPainter.RenderHint.Antialiasing
| QPainter.RenderHint.SmoothPixmapTransform
)
# 创建圆角矩形路径
path = QPainterPath()
path.addRoundedRect(0, 0, size.width(), size.height(), radius, radius)
# 设置裁剪区域
painter.setClipPath(path)
# 绘制原始图片
painter.drawPixmap(0, 0, pixmap)
# 绘制边框
pen = QPen(QColor(200, 200, 200)) # 浅灰色边框
pen.setWidth(1)
painter.setPen(pen)
painter.drawRoundedRect(
0, 0, size.width() - 1, size.height() - 1, radius, radius
)
painter.end()
return rounded_pixmap
except Exception as e:
logger.error(f"创建圆角图片失败:{e}")
return pixmap # 如果出错,返回原始图片
def run(self):
try:
logger.debug("开始获取验证码")
response = miaoStarsBasicApi.getCaptcha()
logger.debug(f"验证码API返回响应: {response}")
if response["code"] == 0:
# 确保data字段存在且为字符串
if "data" in response and isinstance(response["data"], str):
# 分割base64前缀和实际数据
try:
captchaImageData = response["data"].split(",")[1]
logger.debug(f"成功提取base64数据长度: {len(captchaImageData)}")
# 解码base64数据
captchaImage = base64.b64decode(captchaImageData)
logger.debug(f"成功解码base64数据长度: {len(captchaImage)} bytes")
# 加载图片
pixmap = QPixmap()
load_success = pixmap.loadFromData(captchaImage)
if load_success:
logger.debug(f"成功加载图片,尺寸: {pixmap.width()}x{pixmap.height()}")
# 创建圆角图片
pixmap = self._createRoundedPixmap(pixmap, radius=10)
self.captchaReady.emit(pixmap)
else:
logger.error("图片加载失败")
self.captchaFailed.emit("验证码图片加载失败")
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
except (IndexError, ValueError, TypeError) as e:
logger.error(f"验证码数据格式错误: {e}")
self.captchaFailed.emit(f"验证码数据格式错误: {str(e)}")
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
else:
logger.error("验证码响应中缺少有效的data字段")
self.captchaFailed.emit("验证码数据无效")
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
else:
error_msg = response.get("msg", "获取验证码失败")
logger.error(f"获取验证码失败: {error_msg}")
self.captchaFailed.emit(error_msg)
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
except Exception as e:
logger.exception(f"获取验证码过程中发生异常: {e}")
self.captchaFailed.emit(str(e))
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
class LoginThread(QThread):
successLogin = pyqtSignal()
errorLogin = pyqtSignal(str)
def __init__(self, email: str, password: str, captchaCode: str):
super().__init__()
logger.debug(f"初始化许可证服务线程 - 邮箱: {email}")
self.email = email
self.password = password
self.captchaCode = captchaCode
def run(self):
logger.info(f"开始验证用户登录 - 邮箱: {self.email}")
try:
loginResponse = miaoStarsBasicApi.login(
self.email, self.password, self.captchaCode
)
if loginResponse["code"] == 0:
self.successLogin.emit()
qconfig.set(cfg.email, self.email)
qconfig.set(cfg.activationCode, self.password)
userConfig.userData = loginResponse
# 从登录响应中提取token并保存
# 在basicApi的login方法中已经处理了token的设置这里保存到userConfig中以便程序启动时恢复
token = miaoStarsBasicApi.token
if token:
userConfig.setToken(token)
else:
self.errorLogin.emit(loginResponse["msg"])
except Exception as e:
logger.error(f"登录验证过程中发生异常: {e}")
self.errorLogin.emit("系统错误,请稍后重试")
class RegisterThread(QThread):
successRegister = pyqtSignal()
errorRegister = pyqtSignal(str)
def __init__(self, email: str, password: str, captchaCode: str):
super().__init__()
logger.debug(f"初始化许可证服务线程 - 邮箱: {email}")
self.email = email
self.password = password
self.captchaCode = captchaCode
def run(self):
logger.info(f"开始验证用户注册 - 邮箱: {self.email}")
try:
registerRespond = miaoStarsBasicApi.register(
self.email, self.password, self.captchaCode
)
if registerRespond["code"] == 203:
self.successRegister.emit()
else:
logger.error(f"注册失败: {registerRespond['msg']}")
self.errorRegister.emit(registerRespond["msg"])
except Exception as e:
logger.error(f"登录验证过程中发生异常: {e}")
self.errorRegister.emit("系统错误,请稍后重试")

View File

@@ -0,0 +1,246 @@
import os
from urllib.parse import urlparse
import requests
from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QImage, QPixmap
from app.core import miaoStarsBasicApi
class TextLoaderThread(QThread):
"""文本文件加载线程"""
textLoaded = pyqtSignal(str)
errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
"""线程执行函数"""
try:
# 1. 设置网络请求参数 - 优化连接参数
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=20,
pool_maxsize=20,
max_retries=5, # 增加重试次数
pool_block=False,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 2. 增加超时时间并添加重试机制
response = miaoStarsBasicApi.returnSession().get(
self.url,
stream=True,
timeout=(15, 30), # 增加超时时间连接15秒读取30秒
)
response.raise_for_status()
# 3. 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
# 4. 分块读取并处理 - 使用二进制读取提高速度
content_chunks = []
for chunk in response.iter_content(chunk_size=16384): # 增大块大小
if chunk:
content_chunks.append(chunk)
downloaded_size += len(chunk)
# 更新进度(如果知道总大小)
if total_size > 0:
progress = int((downloaded_size / total_size) * 100)
self.progressUpdated.emit(progress)
# 5. 合并内容并解码
binary_content = b"".join(content_chunks)
if not binary_content:
self.errorOccurred.emit("下载内容为空")
return
# 6. 智能编码检测和解码
text_content = self._decode_content(binary_content)
# 7. 发射加载完成的信号
self.textLoaded.emit(text_content)
except requests.exceptions.Timeout:
self.errorOccurred.emit("请求超时,请检查网络连接或尝试重新加载")
except requests.exceptions.ConnectionError:
self.errorOccurred.emit("网络连接错误,请检查网络设置")
except requests.exceptions.RequestException as e:
self.errorOccurred.emit(f"网络请求错误: {str(e)}")
except Exception as e:
self.errorOccurred.emit(f"文本处理错误: {str(e)}")
def _decode_content(self, binary_content):
"""智能解码二进制内容"""
# 优先尝试UTF-8
encodings = ["utf-8", "gbk", "gb2312", "latin-1", "iso-8859-1", "cp1252"]
for encoding in encodings:
try:
return binary_content.decode(encoding)
except UnicodeDecodeError:
continue
# 如果所有编码都失败,使用替换错误处理
try:
return binary_content.decode("utf-8", errors="replace")
except:
# 最后尝试忽略错误
return binary_content.decode("utf-8", errors="ignore")
def cancel(self):
"""取消下载"""
if self.isRunning():
self.terminate()
self.wait(1000) # 等待线程结束
class ImageLoaderThread(QThread):
"""优化的图片加载线程"""
imageLoaded = pyqtSignal(QPixmap)
errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024
): # 50MB缓存
super().__init__()
self.url = url
self.cache_dir = cache_dir
self.max_cache_size = max_cache_size
self._setup_cache()
def _setup_cache(self):
"""设置图片缓存目录"""
if not os.path.exists(self.cache_dir):
os.makedirs(self.cache_dir)
def _get_cache_filename(self):
"""生成缓存文件名"""
parsed_url = urlparse(self.url)
filename = os.path.basename(parsed_url.path) or "image"
# 添加URL哈希避免重名
import hashlib
url_hash = hashlib.md5(self.url.encode()).hexdigest()[:8]
return f"{url_hash}_{filename}"
def _get_cached_image(self):
"""获取缓存图片"""
cache_file = os.path.join(self.cache_dir, self._get_cache_filename())
if os.path.exists(cache_file):
try:
pixmap = QPixmap(cache_file)
if not pixmap.isNull():
return pixmap
except Exception:
pass
return None
def _save_to_cache(self, pixmap):
"""保存图片到缓存"""
try:
cache_file = os.path.join(self.cache_dir, self._get_cache_filename())
pixmap.save(cache_file, "JPG", 80) # 压缩质量80%
self._cleanup_cache() # 清理过期缓存
except Exception:
pass
def _cleanup_cache(self):
"""清理过期的缓存文件"""
# noinspection PyBroadException
try:
files = []
for f in os.listdir(self.cache_dir):
filepath = os.path.join(self.cache_dir, f)
if os.path.isfile(filepath):
files.append((filepath, os.path.getmtime(filepath)))
# 按修改时间排序
files.sort(key=lambda x: x[1])
# 计算总大小
total_size = sum(os.path.getsize(f[0]) for f in files)
# 如果超过最大缓存大小,删除最旧的文件
while total_size > self.max_cache_size and files:
oldest_file = files.pop(0)
total_size -= os.path.getsize(oldest_file[0])
os.remove(oldest_file[0])
except Exception:
pass
def run(self):
"""线程执行函数"""
try:
# 1. 首先检查缓存
cached_pixmap = self._get_cached_image()
if cached_pixmap:
self.imageLoaded.emit(cached_pixmap)
return
# 2. 设置更短的超时时间
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 3. 流式下载,支持进度显示
response = miaoStarsBasicApi.returnSession().get(
self.url, stream=True, timeout=(20, 30) # 连接超时5秒读取超时10秒
)
response.raise_for_status()
# 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
# 4. 分块读取并处理
image_data = b""
for chunk in response.iter_content(chunk_size=8192):
if chunk:
image_data += chunk
downloaded_size += len(chunk)
# 更新进度(如果知道总大小)
if total_size > 0:
progress = int((downloaded_size / total_size) * 100)
self.progressUpdated.emit(progress)
# 5. 从数据创建QImage比QPixmap更快
image = QImage()
image.loadFromData(image_data)
if image.isNull():
raise Exception("无法加载图片数据")
# 6. 转换为QPixmap
pixmap = QPixmap.fromImage(image)
# 7. 保存到缓存
self._save_to_cache(pixmap)
# 发射加载完成的信号
self.imageLoaded.emit(pixmap)
except requests.exceptions.Timeout:
self.errorOccurred.emit("请求超时,请检查网络连接")
except requests.exceptions.ConnectionError:
self.errorOccurred.emit("网络连接错误")
except requests.exceptions.RequestException as e:
self.errorOccurred.emit(f"网络请求错误: {str(e)}")
except Exception as e:
self.errorOccurred.emit(f"图片处理错误: {str(e)}")

View File

@@ -0,0 +1,287 @@
# coding: utf-8
import os
import subprocess
import tempfile
from loguru import logger
from PyQt6.QtCore import QEventLoop, QObject, QThread, pyqtSignal
from PyQt6.QtCore import QUrl
from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer
class LocalTextToSpeechThread(QThread):
"""本地文本转语音播放线程 - Windows优化版"""
# 信号定义
playback_started = pyqtSignal() # 播放开始
playback_finished = pyqtSignal() # 播放完成
playback_error = pyqtSignal(str) # 播放错误
progress_updated = pyqtSignal(int) # 播放进度更新
synthesis_completed = pyqtSignal(str) # 语音合成完成(返回文件路径)
def __init__(self, text, parent=None):
super().__init__(parent)
self.text = text
self.audio_file_path = None
self.media_player = None
self.audio_output = None
self._stop_requested = False
def run(self):
"""线程执行函数"""
try:
# 1. 将文本转换为语音文件
self.audio_file_path = self._text_to_speech(self.text)
if not self.audio_file_path or self._stop_requested:
return
# 发射合成完成信号
self.synthesis_completed.emit(self.audio_file_path)
# 2. 播放语音
self._play_audio(self.audio_file_path)
except Exception as e:
self.playback_error.emit(f"语音播放错误: {str(e)}")
def _text_to_speech(self, text):
"""使用本地TTS引擎将文本转换为语音文件"""
try:
# 检查文本长度
if not text or len(text.strip()) == 0:
self.playback_error.emit("文本内容为空")
return None
# 限制文本长度,避免合成时间过长
max_length = 1000
if len(text) > max_length:
text = text[:max_length] + "。文本过长,已截断。"
self.playback_error.emit(f"文本过长,已截断前{max_length}个字符")
# 优先使用pyttsx3效率最高
try:
import pyttsx3
return self._pyttsx3_tts(text)
except ImportError:
# 备用方案使用Windows内置TTS
return self._windows_tts(text)
except Exception as e:
self.playback_error.emit(f"语音合成失败: {str(e)}")
return None
def _pyttsx3_tts(self, text):
"""使用pyttsx3合成语音 - 优化版"""
try:
import pyttsx3
# 初始化TTS引擎
engine = pyttsx3.init()
# 设置语音属性 - 提高效率
engine.setProperty('rate', 200) # 提高语速
engine.setProperty('volume', 0.9) # 提高音量
# 创建临时文件保存音频
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
temp_path = temp_file.name
# 保存语音到文件
engine.save_to_file(text, temp_path)
engine.runAndWait()
# 检查文件是否成功创建
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
return temp_path
else:
logger.error("语音文件生成失败")
except Exception as e:
# 如果pyttsx3失败尝试Windows TTS
return self._windows_tts(text)
def _windows_tts(self, text):
"""Windows系统TTS - 优化版"""
try:
# 方法1: 使用PowerShell命令 - 最可靠
return self._powershell_tts(text)
except Exception as e:
logger.error(f"Windows TTS失败: {str(e)}")
def _powershell_tts(self, text):
"""使用PowerShell合成语音 - 优化版"""
try:
# 创建临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
temp_path = temp_file.name
# 转义文本中的特殊字符
escaped_text = text.replace('"', '`"').replace("'", "`'")
# 使用PowerShell的SpeechSynthesizer - 简化命令
ps_script = f"""
Add-Type -AssemblyName System.Speech
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
$speak.SetOutputToWaveFile("{temp_path}")
$speak.Speak("{escaped_text}")
$speak.Dispose()
"""
# 使用更高效的方式执行PowerShell
process = subprocess.Popen(
["powershell", "-Command", ps_script],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
# 等待进程完成,设置超时
try:
stdout, stderr = process.communicate(timeout=30)
if process.returncode != 0:
logger.error(f"PowerShell执行失败: {stderr.decode('gbk', errors='ignore')}")
except subprocess.TimeoutExpired:
process.kill()
logger.error(f"PowerShell超时")
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
return temp_path
else:
logger.error("语音文件生成失败")
except Exception as e:
# raise Exception(f"PowerShell TTS失败: {str(e)}")
logger.error(f"PowerShell TTS失败{e}")
def _play_audio(self, file_path):
"""播放音频文件 - 优化版"""
if self._stop_requested:
return
try:
# 创建媒体播放器和音频输出
self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.media_player.setAudioOutput(self.audio_output)
# 设置音量
self.audio_output.setVolume(1.0)
# 连接信号
self.media_player.playbackStateChanged.connect(self._on_playback_state_changed)
self.media_player.positionChanged.connect(self._on_position_changed)
self.media_player.durationChanged.connect(self._on_duration_changed)
self.media_player.errorOccurred.connect(self._on_player_error)
# 设置媒体源并开始播放
self.media_player.setSource(QUrl.fromLocalFile(file_path))
self.media_player.play()
# 使用事件循环等待播放完成
loop = QEventLoop()
self.media_player.playbackStateChanged.connect(
lambda state: loop.quit() if state == QMediaPlayer.PlaybackState.StoppedState else None
)
loop.exec()
except Exception as e:
raise Exception(f"音频播放失败: {str(e)}")
finally:
# 清理临时文件
if file_path and os.path.exists(file_path):
try:
os.unlink(file_path)
except:
pass
def _on_playback_state_changed(self, state):
"""处理播放状态变化"""
from PyQt6.QtMultimedia import QMediaPlayer
if state == QMediaPlayer.PlaybackState.StoppedState:
self.playback_finished.emit()
def _on_position_changed(self, position):
"""处理播放位置变化"""
if (self.media_player and
self.media_player.duration() > 0):
progress = int((position / self.media_player.duration()) * 100)
self.progress_updated.emit(progress)
def _on_duration_changed(self, duration):
"""处理时长变化"""
if duration > 0:
self.playback_started.emit()
def _on_player_error(self, error, error_string):
"""处理播放器错误"""
self.playback_error.emit(f"播放器错误: {error_string}")
def stop_playback(self):
"""停止播放"""
self._stop_requested = True
if self.media_player and self.media_player.playbackState() != QMediaPlayer.PlaybackState.StoppedState:
self.media_player.stop()
# 清理临时文件
if self.audio_file_path and os.path.exists(self.audio_file_path):
try:
os.unlink(self.audio_file_path)
except:
pass
class LocalSpeechController(QObject):
"""本地语音播放控制器"""
def __init__(self, parent=None):
super().__init__(parent)
self.speech_thread = None
def play_text(self, text):
"""播放文本语音"""
# 停止当前播放
self.stop_playback()
# 创建新的语音线程
self.speech_thread = LocalTextToSpeechThread(text)
# 连接信号
self.speech_thread.playback_started.connect(self._on_playback_started)
self.speech_thread.playback_finished.connect(self._on_playback_finished)
self.speech_thread.playback_error.connect(self._on_playback_error)
self.speech_thread.progress_updated.connect(self._on_progress_updated)
self.speech_thread.synthesis_completed.connect(self._on_synthesis_completed)
# 开始播放
self.speech_thread.start()
def stop_playback(self):
"""停止播放"""
if self.speech_thread and self.speech_thread.isRunning():
self.speech_thread.stop_playback()
self.speech_thread.wait(1000) # 等待线程结束最多1秒
def is_playing(self):
"""检查是否正在播放"""
return self.speech_thread and self.speech_thread.isRunning()
def _on_playback_started(self):
"""处理播放开始"""
logger.info("语音播放开始")
def _on_playback_finished(self):
"""处理播放完成"""
logger.success("语音播放完成")
def _on_playback_error(self, error_msg):
"""处理播放错误"""
logger.warning(f"语音播放错误: {error_msg}")
def _on_progress_updated(self, progress):
...
def _on_synthesis_completed(self, file_path):
"""处理语音合成完成"""
logger.info(f"语音合成完成,文件路径: {file_path}")

View File

@@ -0,0 +1,199 @@
from loguru import logger
from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QPixmap
from ..api import miaoStarsBasicApi
class UserNickNameUpdateThread(QThread):
successUpdate = pyqtSignal()
errorUpdate = pyqtSignal(str)
def __init__(self, nickName: str):
super().__init__()
logger.debug(f"初始化用户昵称服务线程 - 昵称: {nickName}")
self.nickName = nickName
def run(self):
logger.info(f"开始更新用户昵称 - 昵称: {self.nickName}")
try:
response = miaoStarsBasicApi.updateUserNickname(self.nickName)
print(response)
if response["code"] == 0:
self.successUpdate.emit()
else:
logger.error("更新失败:", response["msg"])
self.errorUpdate.emit(response["msg"])
except Exception as e:
logger.error(f"更新用户昵称过程中发生异常: {e}")
self.errorUpdate.emit("系统错误,请稍后重试")
class UserAvatarUpdateThread(QThread):
successUpdate = pyqtSignal()
errorUpdate = pyqtSignal(str)
def __init__(self, avatarPath: str):
super().__init__()
logger.debug(f"初始化用户头像服务线程 - 头像路径: {avatarPath}")
self.avatarPath = avatarPath
def run(self):
logger.info(f"开始更新用户头像 - 头像路径: {self.avatarPath}")
try:
response = miaoStarsBasicApi.updateUserAvatar(self.avatarPath)
if response["code"] == 0:
logger.info("头像更新成功")
self.successUpdate.emit()
else:
logger.error(f"更新失败,错误信息: {response['msg']}")
self.errorUpdate.emit(f"更新失败: {response['msg']}")
except Exception as e:
logger.error(f"更新用户头像过程中发生异常: {e}")
self.errorUpdate.emit("系统错误,请稍后重试")
class GetUserAvatarThread(QThread):
avatarPixmap = pyqtSignal(QPixmap)
def __init__(self, size: str):
super().__init__()
logger.debug(f"初始化获取用户头像服务线程 - 头像尺寸: {size}")
self.size = size
def run(self):
logger.info(f"开始获取用户头像 - 头像尺寸: {self.size}")
try:
response = miaoStarsBasicApi.getUserAvatar(self.size)
self.avatarPixmap.emit(response)
except Exception as e:
logger.error(f"获取用户头像过程中发生异常: {e}")
self.avatarPixmap.emit(QPixmap(":app/images/logo.png"))
class GetPackThread(QThread):
storageDictSignal = pyqtSignal(dict)
def __init__(self):
super().__init__()
def run(self):
logger.info("开始请求用户配额包")
try:
response = miaoStarsBasicApi.getUserPack()
self.storageDictSignal.emit(response)
except Exception as e:
logger.error(f"获取用户配额包过程中发生异常: {e}")
self.storageDictSignal.emit(
{
"code": 0,
"data": {
"base": 0,
"pack": 0,
"used": 0,
"total": 0,
"packs": [],
},
"msg": "",
}
)
class GetPoliciesThread(QThread):
"""获取策略列表线程"""
successGetSignal = pyqtSignal(list)
errorSignal = pyqtSignal(str)
def __init__(self):
super().__init__()
def run(self):
try:
response = miaoStarsBasicApi.getPolicy()
if response["code"] == 0:
self.successGetSignal.emit(response["data"])
else:
self.errorSignal.emit(f"API返回错误: {response.get('msg')}")
except Exception as e:
self.errorSignal.emit(f"获取策略列表失败: {str(e)}")
class ChangePolicyThread(QThread):
"""更改策略线程"""
successChangedSignal = pyqtSignal()
errorSignal = pyqtSignal(str)
def __init__(self, path, policy_id):
super().__init__()
self.path = path
self.policy_id = policy_id
def run(self):
try:
response = miaoStarsBasicApi.changePolicy(self.path, self.policy_id)
if response["code"] == 0:
self.successChangedSignal.emit()
else:
self.errorSignal.emit(
f"更改策略失败: {response.get('msg', '未知错误')}"
)
except Exception as e:
self.errorSignal.emit(f"更改策略请求失败: {str(e)}")
class DeleteTagThread(QThread):
"""删除标签线程"""
successDeleteSignal = pyqtSignal()
errorSignal = pyqtSignal(str)
def __init__(self, tagId):
super().__init__()
self.tagId = tagId
def run(self):
try:
response = miaoStarsBasicApi.deleteTag(self.tagId)
if response["code"] == 0:
self.successDeleteSignal.emit()
logger.info(f"删除标签成功: {self.tagId}")
else:
logger.error(f"删除标签失败: {response.get('msg')}")
self.errorSignal.emit(f"删除标签失败: {response.get('msg')}")
except Exception as e:
self.errorSignal.emit(f"{str(e)}")
logger.error(f"删除标签请求失败: {str(e)}")
class AddTagThread(QThread):
"""添加标签的线程类"""
successSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
errorSignal = pyqtSignal(str, str) # 标签名称, 错误信息
def __init__(self, name, expression, parent=None):
super().__init__(parent)
self.name = name
self.expression = expression
def run(self):
"""线程执行的主方法"""
try:
response = miaoStarsBasicApi.addTag(self.name, self.expression)
if response["code"] == 0:
logger.info(f"添加标签成功: {self.name}")
self.successSignal.emit(self.name, response)
else:
logger.error(f"添加标签失败: {self.name} - {response.get('msg')}")
self.errorSignal.emit(self.name, response.get("msg"))
except Exception as e:
logger.error(f"添加标签异常: {self.name} - {str(e)}")
self.errorSignal.emit(self.name, str(e))

View File

@@ -0,0 +1 @@
from .exceptions import getCode

202
app/core/utils/config.py Normal file
View File

@@ -0,0 +1,202 @@
# coding:utf-8
import sys
from datetime import datetime
from PyQt6.QtGui import QPixmap
from qfluentwidgets import (
BoolValidator,
ConfigItem,
ConfigSerializer,
OptionsConfigItem,
OptionsValidator,
qconfig,
QConfig,
FolderValidator,
Theme,
setThemeColor,
)
from .encryption import encrypt
from .setting import CONFIG_FILE, DOWNLOAD_FOLDER
def isWin11():
return sys.platform == "win32" and sys.getwindowsversion().build >= 22000
class EncrpytionSerializer(ConfigSerializer):
"""QColor serializer"""
def serialize(self, value):
return encrypt.encrypt(value)
def deserialize(self, value):
return encrypt.decrypt(value)
class Config(QConfig):
"""Config of application"""
# TODO: ADD YOUR CONFIG GROUP HERE
# register
rememberMe = ConfigItem(
"UmVnaXN0ZXI=", "UmVtZW1iZXJNZQ==", True, serializer=EncrpytionSerializer()
)
email = ConfigItem(
"UmVnaXN0ZXI=", "RW1haWw=", "", serializer=EncrpytionSerializer()
)
activationCode = ConfigItem(
"UmVnaXN0ZXI=", "QWN0aXZhdGlvbkNvZGU=", "", serializer=EncrpytionSerializer()
)
# main window
micaEnabled = ConfigItem("MainWindow", "MicaEnabled", isWin11(), BoolValidator())
dpiScale = OptionsConfigItem(
"MainWindow",
"DpiScale",
"Auto",
OptionsValidator([1, 1.25, 1.5, 1.75, 2, "Auto"]),
restart=True,
)
# software update
checkUpdateAtStartUp = ConfigItem(
"Update", "CheckUpdateAtStartUp", True, BoolValidator()
)
# bg
customBackground = ConfigItem(
"Background",
"CustomBackground",
"app\\resource\\images\\bg0.png",
)
customOpactity = ConfigItem("Background", "Opactity", 0.2)
downloadSavePath = ConfigItem(
"Download", "SavePath", DOWNLOAD_FOLDER, validator=FolderValidator()
)
# language
language = OptionsConfigItem(
"General", "Language", "zh", OptionsValidator(["zh", "en"]), restart=False
)
class UserConfig:
def __init__(self, userData):
self.userData = userData
self.token = None
self.avaterPixmap = None
@property
def userId(self):
if self.userData:
return self.userData["data"].get("id", "")
else:
return None
@property
def userAvatarURL(self):
if self.userData and "avatar" in self.userData["data"]:
return self.userData["data"].get("avatar", "")
else:
return ""
@property
def userName(self):
if self.userData:
return self.userData["data"].get("nickname", "")
else:
return ""
@property
def userEmail(self):
if self.userData:
return self.userData["data"].get("user_name", "")
else:
return ""
@property
def userGroup(self):
if self.userData:
return self.userData.get("data", {}).get("group", {}).get("name", "")
else:
return ""
@property
def userScore(self):
if self.userData:
return str(self.userData["data"].get("score", 0))
@property
def userCreatedTime(self):
if self.userData:
return self.format_date(self.userData["data"].get("created_at", ""))
def setUserAvatarPixmap(self, avaterPixmap):
self.avaterPixmap: QPixmap = avaterPixmap
def returnAvatarPixmap(self):
return self.avaterPixmap
def setToken(self, token):
"""设置JWT token"""
self.token = token
def getToken(self):
"""获取JWT token"""
return self.token
def format_date(self, date_str):
"""格式化日期时间"""
try:
# 处理带小数秒的情况
if "." in date_str:
# 分割日期和小数秒部分
date_part, fractional_part = date_str.split(".", 1)
# 去除末尾的"Z"并截取前6位小数
fractional_sec = fractional_part.rstrip("Z")[:6]
# 重新组合日期字符串
normalized_date_str = f"{date_part}.{fractional_sec}Z"
date_time = datetime.strptime(
normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ"
)
else:
# 处理没有小数秒的情况
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
# 如果所有格式都失败,返回原始字符串
return date_str
return date_time.strftime("%Y-%m-%d %H:%M:%S")
class PolicyConfig:
def __init__(self):
self.currentPolicy = {}
self.currentPath = "/"
def returnPolicy(self):
return self.currentPolicy
def setPolicy(self, policy):
self.currentPolicy = policy
def setCurrentPath(self, path):
self.currentPath = path
def returnCurrentPath(self):
return self.currentPath
cfg = Config()
cfg.themeMode.value = Theme.AUTO
# 设置默认主题色为蓝色 (使用RGB值)
setThemeColor('#2F80ED') # 这是一个标准的蓝色RGB值
qconfig.load(str(CONFIG_FILE.absolute()), cfg)
userConfig = UserConfig(None)
policyConfig = PolicyConfig()

View File

@@ -0,0 +1,77 @@
import json
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
# from .setting import ENCRYPTKEY
ENCRYPTKEY = b"lunminalinguaai_"
class AESCipher:
def __init__(self, key):
"""
初始化AES加密器
:param key: 加密密钥16/24/32字节可以是字符串或字节
"""
if isinstance(key, str):
key = key.encode("utf-8")
if len(key) not in [16, 24, 32]:
raise ValueError("密钥长度必须为16、24或32字节")
self.key = key
def encrypt(self, data):
"""
加密数据(支持字符串/列表/字典等JSON可序列化类型
:param data: 要加密的数据
:return: 返回Base64编码的加密字符串
"""
# 生成随机初始化向量
iv = get_random_bytes(AES.block_size)
# 创建AES加密器
cipher = AES.new(self.key, AES.MODE_CBC, iv)
# 序列化数据为JSON字符串并编码为字节
json_data = json.dumps(data)
plain_bytes = json_data.encode("utf-8")
# 填充并加密数据
padded_bytes = pad(plain_bytes, AES.block_size)
cipher_bytes = cipher.encrypt(padded_bytes)
# 组合IV和密文并进行Base64编码
encrypted_data = iv + cipher_bytes
return base64.b64encode(encrypted_data).decode("utf-8")
def decrypt(self, enc_data):
"""
解密数据并恢复原始格式
:param enc_data: Base64编码的加密字符串
:return: 原始数据(保持原始格式)
"""
# Base64解码
encrypted_data = base64.b64decode(enc_data)
# 提取初始化向量
iv = encrypted_data[: AES.block_size]
cipher_bytes = encrypted_data[AES.block_size :]
# 创建AES解密器
cipher = AES.new(self.key, AES.MODE_CBC, iv)
# 解密并去除填充
decrypted_bytes = cipher.decrypt(cipher_bytes)
unpadded_bytes = unpad(decrypted_bytes, AES.block_size)
# 解码JSON并恢复原始数据结构
json_data = unpadded_bytes.decode("utf-8")
return json.loads(json_data)
encrypt = AESCipher(ENCRYPTKEY)
if __name__ == '__main__':
data = "sk-3e47a49bf60e49e8ab08bb1f1550aa86"
enc_data = encrypt.encrypt(data)
print(enc_data)

View File

@@ -0,0 +1,14 @@
# API请求code
stateCodeList = {
"0": "成功",
"40026": "验证码错误",
"40001": "读取用户头像数据失败",
"40020": "邮箱或密码不正确",
"40018": "该账号未激活",
"40033": "用户未激活,已重新发送激活邮件",
}
def getCode(code):
return stateCodeList.get(str(code), "未知错误,请联系技术支持")

67
app/core/utils/format.py Normal file
View File

@@ -0,0 +1,67 @@
from datetime import datetime
def formatSize(size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def formatDate(date_str):
"""格式化日期时间"""
try:
# 处理带小数秒的情况
if "." in date_str:
# 分割日期和小数秒部分
date_part, fractional_part = date_str.split(".", 1)
# 去除末尾的'Z'并截取前6位小数
fractional_sec = fractional_part.rstrip("Z")[:6]
# 重新组合日期字符串
normalized_date_str = f"{date_part}.{fractional_sec}Z"
date_time = datetime.strptime(normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
else:
# 处理没有小数秒的情况
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
# 如果所有格式都失败,返回原始字符串
return date_str
return date_time.strftime("%Y-%m-%d %H:%M:%S")
def getFileIcon(fileType, fileName):
if fileType == "file":
suffix = fileName.split(".")[-1].lower()
icon_map = {
"txt": "Txt.svg",
"png": "Image.svg",
"jpg": "Image.svg",
"svg": "Image.svg",
"jpeg": "Image.svg",
"bmp": "Image.svg",
"gif": "Gif.svg",
"xls": "Excel.svg",
"xlsx": "Excel.svg",
"doc": "Word.svg",
"docx": "Word.svg",
"pdf": "Pdf.svg",
"ppt": "PPT.svg",
"mp4": "Video.svg",
"mkv": "Video.svg",
"mp3": "music.svg",
"wav": "music.svg",
"zip": "Zip.svg",
"rar": "Zip.svg",
"csv": "Excel.svg",
"db": "Database.svg",
"py": "Programme.svg",
"c": "Programme.svg",
"cpp": "Programme.svg",
"go": "Programme.svg",
}
return icon_map.get(suffix, "None.svg") # 默认图标
else:
return "Folder.svg"

View File

@@ -0,0 +1,90 @@
# coding: utf-8
import json
import os
from loguru import logger
from pathlib import Path
# 导入配置
from .config import cfg, qconfig
# 当前语言设置,默认为中文
_current_language = "zh"
# 翻译词典
_translations = {}
# 语言文件目录
_LANG_DIR = Path("app/resource/lang").absolute()
# print(Path("app/resource/lang").absolute())
def load_language(lang_code="zh"):
"""
加载指定语言的翻译文件
Args:
lang_code: 语言代码,如 "zh""en"
"""
global _current_language, _translations
try:
# 构建语言文件路径
lang_file = os.path.join(_LANG_DIR, f"{lang_code}.json")
# 检查文件是否存在
if not os.path.exists(lang_file):
logger.warning(f"语言文件不存在: {lang_file},使用默认语言")
lang_code = "zh"
lang_file = os.path.join(_LANG_DIR, "zh.json")
# 读取语言文件
with open(lang_file, "r", encoding="utf-8") as f:
_translations = json.load(f)
_current_language = lang_code
logger.info(f"已加载语言: {lang_code}")
except Exception as e:
logger.error(f"加载语言文件失败: {e}")
# 如果加载失败,使用空字典
_translations = {}
def lang(text):
"""
翻译文本
Args:
text: 要翻译的原文
Returns:
翻译后的文本,如果没有找到翻译,则返回原文
"""
# 如果翻译词典为空,尝试加载默认语言
if not _translations:
load_language(_current_language)
# 返回翻译后的文本,找不到则返回原文
return _translations.get(text, text)
def get_current_language():
"""
获取当前语言代码
Returns:
当前语言代码
"""
return _current_language
# 初始化时从配置加载语言
try:
if hasattr(cfg, "language"):
initial_lang = qconfig.get(cfg.language)
load_language(initial_lang)
else:
# 如果配置中没有语言设置,使用默认值
load_language()
except Exception as e:
logger.error(f"加载语言配置时出错: {e}")
# 出错时使用默认语言
load_language("zh")

16
app/core/utils/setting.py Normal file
View File

@@ -0,0 +1,16 @@
# coding: utf-8
from pathlib import Path
# change DEBUG to False if you want to compile the code to exe
DEBUG = "__compiled__" not in globals()
YEAR = 2025
AUTHOR = "Miao"
VERSION = "v0.0.1"
APP_NAME = "miaostarspan"
CONFIG_FOLDER = Path("config").absolute()
CONFIG_FILE = CONFIG_FOLDER / "config.json"
DOWNLOAD_FOLDER = Path("download").absolute()
# 23

View File

@@ -0,0 +1,34 @@
# coding: utf-8
from PyQt6.QtCore import QObject, pyqtSignal
from PyQt6.QtGui import QPixmap
class SignalBus(QObject):
"""Signal bus"""
checkUpdateSig = pyqtSignal()
micaEnableChanged = pyqtSignal(bool)
dirOpenSignal = pyqtSignal(str)
shareDirOpenSignal = pyqtSignal(str)
avatarUpdated = pyqtSignal(QPixmap) # 头像更新信号
imagePreviewSignal = pyqtSignal(str)
txtPreviewSignal = pyqtSignal(str)
opacityChanged = pyqtSignal() # 透明度变化信号
backgroundChanged = pyqtSignal() # 背景颜色变化信号
refreshFolderListSignal = pyqtSignal()
addUploadFileTask = pyqtSignal(str) # 添加upload任务信号
addDownloadFileTask = pyqtSignal(str, str, str) # 添加download任务信号
shareFolderViewSignal = pyqtSignal(str) # 分享文件夹
shareFileDownloadSignal = pyqtSignal() # 分享文件下载
languageChanged = pyqtSignal() # 语言变更信号
loginSuccessSignal = pyqtSignal() # 登录成功信号
signalBus = SignalBus()

View File

@@ -0,0 +1,6 @@
# coding: utf-8
"""
Version information for the application
"""
version = "0.0.2"

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a119.958 119.958 0 0 0-84.85-35.15z" fill="#8B72F7" /><path d="M862.07 216.83l-117.5-117.5a120.029 120.029 0 0 0-61.74-32.89v131.88c0 41.87 33.94 75.81 75.81 75.81H894a120.006 120.006 0 0 0-31.93-57.3zM470.34 532.68l-90.17-48.97c-14.7-6.9-26.82-0.52-26.95 14.18l-0.9 106.41c-0.12 14.7 11.8 32.38 26.5 39.28l90.17 48.97c14.7 6.9 26.82 0.52 26.95-14.18l0.9-106.41c0.12-14.7-11.8-32.37-26.5-39.28zM653.45 453.92c16.61-11.17 17.55-28.04 2.08-37.48L539.56 360.1c-16.34-8.45-30.61-9.12-50.33 1.13l-116.51 55.9c-16.61 11.17-17.55 28.04-2.08 37.48l111.97 60.34c15.47 9.45 38.27 9.2 58.33-1.13l112.51-59.9zM646.12 481.71l-97.17 51.97c-14.7 6.9-26.62 24.58-26.5 39.28l0.9 108.41c0.12 14.7 12.25 21.08 26.95 14.18l97.17-51.97c14.7-6.9 26.62-24.58 26.5-39.28l-0.9-108.41c-0.12-14.7-12.25-21.08-26.95-14.18z" fill="#7463EA" /><path d="M470.34 521.68l-90.17-48.97c-14.7-6.9-26.82-0.52-26.95 14.18l-0.9 106.41c-0.12 14.7 11.8 32.38 26.5 39.28l90.17 48.97c14.7 6.9 26.82 0.52 26.95-14.18l0.9-106.41c0.12-14.7-11.8-32.37-26.5-39.28zM653.45 442.92c16.61-11.17 17.55-28.04 2.08-37.48L539.56 349.1c-16.34-8.45-30.61-9.12-50.33 1.13l-116.51 55.9c-16.61 11.17-17.55 28.04-2.08 37.48l111.97 60.34c15.47 9.45 38.27 9.2 58.33-1.13l112.51-59.9zM673.07 484.89c-0.12-14.7-12.25-21.08-26.95-14.18l-97.17 51.97c-14.7 6.9-26.62 24.58-26.5 39.28l0.9 108.41c0.12 14.7 12.25 21.08 26.95 14.18l97.17-51.97c14.7-6.9 26.62-24.58 26.5-39.28l-0.9-108.41z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg t="1759502775435" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4881" width="200" height="200"><path d="M924.16 274.048v475.904L512 987.904l-412.16-237.952V274.048L512 36.032l412.16 238.016zM512 460.736L372.16 367.488l-47.36 71.04 144.512 96.32v147.84h85.376v-147.84l144.512-96.32-47.36-71.04L512 460.8z" p-id="4882" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758946089830" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4887"
width="200" height="200"><path d="M128.299 128C92.788 128 64 156.788 64 192.299v639.4C64 867.212 92.788 896 128.299 896H895.7c35.512 0 64.3-28.788 64.3-64.299V192.299C960 156.788 931.212 128 895.701 128H128.299zM128 588.313l178.162-178.162c24.795-24.795 64.996-24.795 89.792 0L817.803 832H128V588.313z m768 231.375L666.385 590.073l64.718-64.718c25.153-25.152 65.933-25.152 91.085 0L896 599.167v220.521zM694.65 471.299l-73.519 73.519L432.653 356.34c-45.064-45.064-118.127-45.064-163.19 0L128 497.803V192h768v316.657l-37.358-37.358c-45.286-45.285-118.707-45.285-163.992 0z" p-id="4888" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.8 64.2H270.4c-78.1 0-141.3 63.3-141.3 141.3v612.9c0 78.1 63.3 141.3 141.3 141.3h485.5c78.1 0 141.3-63.3 141.3-141.3V301.7c0-31.8-12.6-62.3-35.1-84.9L744.6 99.3c-22.5-22.5-53-35.1-84.8-35.1z" fill="#8B72F7" /><path d="M861.7 216.8L744.2 99.3c-17-17-38.5-28.3-61.7-32.9v131.9c0 41.9 33.9 75.8 75.8 75.8h135.4c-5.2-21.6-16.1-41.5-32-57.3zM585.6 350.4H440.8c-17.2 0-33.1 9.2-41.7 24.1l-72.4 125.4c-8.6 14.9-8.6 33.3 0 48.2l72.4 125.4c8.6 14.9 24.5 24.1 41.7 24.1h144.8c17.2 0 33.1-9.2 41.7-24.1l72.4-125.4c8.6-14.9 8.6-33.3 0-48.2l-72.4-125.4c-8.6-14.9-24.5-24.1-41.7-24.1z m-71 247.7c-42 0.8-76.2-33.4-75.5-75.5 0.7-39.6 33.1-71.9 72.7-72.7 42-0.8 76.2 33.4 75.5 75.5-0.8 39.6-33.1 71.9-72.7 72.7z" fill="#7463EA" /><path d="M585.6 338.4H440.8c-17.2 0-33.1 9.2-41.7 24.1l-72.4 125.4c-8.6 14.9-8.6 33.3 0 48.2l72.4 125.4c8.6 14.9 24.5 24.1 41.7 24.1h144.8c17.2 0 33.1-9.2 41.7-24.1l72.4-125.4c8.6-14.9 8.6-33.3 0-48.2l-72.4-125.4c-8.6-14.9-24.5-24.1-41.7-24.1z m-71 247.7c-42 0.8-76.2-33.4-75.5-75.5 0.7-39.6 33.1-71.9 72.7-72.7 42-0.8 76.2 33.4 75.5 75.5-0.8 39.6-33.1 71.9-72.7 72.7z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.7 64.2H270.4c-78.1 0-141.3 63.3-141.3 141.3v612.9c0 78.1 63.3 141.3 141.3 141.3h485.5c78.1 0 141.3-63.3 141.3-141.3V301.7c0-31.8-12.6-62.3-35.1-84.9L744.6 99.3c-22.5-22.5-53.1-35.1-84.9-35.1z" fill="#F98950" /><path d="M862.1 216.8L744.6 99.3c-17-17-38.5-28.3-61.7-32.9v131.9c0 41.9 33.9 75.8 75.8 75.8H894c-5.1-21.5-16-41.4-31.9-57.3z" fill="#F26C38" /><path d="M369.4 399.3a143.7 42.6 0 1 0 287.4 0 143.7 42.6 0 1 0-287.4 0Z" fill="#F26C38" /><path d="M513.1 532.1c79.3 0 143.7-19.1 143.7-42.6v-58.3c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 451 369.7 428.6c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.6 64.3 42.7 143.6 42.7z" fill="#F26C38" /><path d="M513.1 619.9c79.3 0 143.7-19.1 143.7-42.6V519c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 538.9 369.7 516.5c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.6 42.6z" fill="#F26C38" /><path d="M513.2 644.1c-76.5 0-139.1-17.8-143.4-40.2-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.7 42.6S657 688.2 657 664.7v-58.3c0-0.8-0.1-1.7-0.2-2.5-4.5 22.4-67.1 40.2-143.6 40.2z" fill="#F26C38" /><path d="M369.4 387.3a143.7 42.6 0 1 0 287.4 0 143.7 42.6 0 1 0-287.4 0Z" fill="#FFFFFF" /><path d="M513.1 520.1c79.3 0 143.7-19.1 143.7-42.6v-58.3c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 439 369.7 416.6c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.6 64.3 42.7 143.6 42.7z" fill="#FFFFFF" /><path d="M513.1 607.9c79.3 0 143.7-19.1 143.7-42.6V507c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 526.9 369.7 504.5c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.6 42.6z" fill="#FFFFFF" /><path d="M656.6 591.9c-4.3 22.4-66.9 40.2-143.4 40.2s-139.1-17.8-143.4-40.2c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.7 42.6 79.3 0 143.7-19.1 143.7-42.6v-58.3c-0.2-0.8-0.3-1.7-0.4-2.5z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758303622544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="11979"
width="200" height="200"><path d="M833 128c0-53-43-96-96-96h-16c-53 0-96 43-96 96H400c0-53-43-96-96-96h-16c-53 0-96 43-96 96H64c-35.3 0-64 28.7-64 64v736c0 35.3 28.7 64 64 64h896c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64H833zM729 96c22.1 0 40 17.9 40 40v80c0 22.1-17.9 40-40 40s-40-17.9-40-40v-80c0-22.1 17.9-40 40-40z m-433 0c22.1 0 40 17.9 40 40v80c0 22.1-17.9 40-40 40s-40-17.9-40-40v-80c0-22.1 17.9-40 40-40z m632 832H96c-17.7 0-32-14.3-32-32V448h896v448c0 17.7-14.3 32-32 32z m32-544H64V224c0-17.7 14.3-32 32-32h96v32c0 53 43 96 96 96h16c53 0 96-43 96-96v-32h225v24c0 57.4 46.6 104 104 104s104-46.6 104-104v-24h95c17.7 0 32 14.3 32 32v160z" p-id="11980" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 959 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758303572643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8837"
width="200" height="200"><path d="M597.172 531.547c-9.668-9.857-9.514-25.686 0.344-35.353 9.857-9.668 25.686-9.514 35.353 0.343L941.85 811.58c9.667 9.858 9.514 25.686-0.344 35.354-9.857 9.668-25.686 9.514-35.354-0.343L597.172 531.547zM117.85 846.59c-9.668 9.857-25.497 10.01-35.354 0.343-9.858-9.668-10.011-25.496-0.344-35.354l308.98-315.042c9.667-9.857 25.496-10.011 35.353-0.343 9.858 9.667 10.012 25.496 0.344 35.353L117.848 846.59z" fill="#2F80ED" p-id="8838"></path><path d="M82.151 216.505c-9.667-9.857-9.514-25.686 0.344-35.354 9.857-9.667 25.686-9.514 35.354 0.344l340.605 347.29c29.004 29.572 76.489 30.033 106.061 1.03 0.347-0.34 0.69-0.684 1.03-1.03l340.606-347.29c9.668-9.858 25.497-10.011 35.354-0.344 9.858 9.668 10.011 25.497 0.344 35.354l-340.606 347.29a125 125 0 0 1-1.718 1.717c-49.287 48.339-128.429 47.57-176.768-1.718L82.151 216.505z" fill="#2F80ED" p-id="8839"></path><path d="M95 191v643h835V191H95z m0-60h835c33.137 0 60 26.863 60 60v643c0 33.137-26.863 60-60 60H95c-33.137 0-60-26.863-60-60V191c0-33.137 26.863-60 60-60z" fill="#2F80ED" p-id="8840"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.68 63.95H269.36c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.44c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#53D39C" /><path d="M655.49 388.82c-9.37-9.37-24.57-9.37-33.94 0L510.8 499.57 400.05 388.82c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l110.75 110.75L366.1 644.26c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L510.8 567.45 621.54 678.2c4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03c9.37-9.37 9.37-24.57 0-33.94L544.74 533.51l110.75-110.75c9.37-9.37 9.37-24.57 0-33.94zM861.03 216.59l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a120.058 120.058 0 0 0-31.93-57.29z" fill="#25BF79" /><path d="M544.74 521.51l110.75-110.75c9.37-9.37 9.37-24.57 0-33.94-9.37-9.37-24.57-9.37-33.94 0L510.8 487.57 400.05 376.82c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l110.75 110.75L366.1 632.26c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L510.8 555.45 621.54 666.2c4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03c9.37-9.37 9.37-24.57 0-33.94L544.74 521.51z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#8B72F7" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM627.66 386.46c13.25 0 24-10.75 24-24s-10.75-24-24-24H398.62c-13.25 0-24 10.75-24 24v323.08c0 13.25 10.75 24 24 24h229.05c13.25 0 24-10.75 24-24s-10.75-24-24-24H422.62V548h178.85c13.25 0 24-10.75 24-24s-10.75-24-24-24H422.62V386.46h205.04z" fill="#7463EA" /><path d="M627.66 649.54H422.62V536h178.85c13.25 0 24-10.75 24-24s-10.75-24-24-24H422.62V374.46h205.05c13.25 0 24-10.75 24-24s-10.75-24-24-24H398.62c-13.25 0-24 10.75-24 24v323.08c0 13.25 10.75 24 24 24h229.05c13.25 0 24-10.75 24-24s-10.75-24-24.01-24z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M739.82 290.16H64.21V234.1c0-58.59 47.5-106.09 106.09-106.09h393.16c43.85 0 84.74 22.11 108.75 58.81l67.61 103.34z" fill="#FFBA53" /><path d="M960.19 396.82v357.49c0 78.06-63.28 141.33-141.33 141.33H205.54c-78.06 0-141.33-63.28-141.33-141.33V255.49h754.64c78.06 0 141.34 63.28 141.34 141.33z" fill="#FFDC53" /></svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M788.14 927h-550c-77.32 0-140-62.68-140-140V237c0-77.32 62.68-140 140-140h550c77.32 0 140 62.68 140 140v550c0 77.32-62.68 140-140 140z" fill="#FF7878" /><path d="M671.15 510.18h-135.9c-13.25 0-24 10.75-24 24s10.75 24 24 24h111.9v91.1c-1.21 0.66-2.37 1.42-3.49 2.31-29.1 23.19-64.2 35.44-101.5 35.44-89.89 0-163.03-73.13-163.03-163.03 0-89.89 73.13-163.03 163.03-163.03 37.61 0 72.94 12.44 102.17 35.98 10.32 8.31 25.43 6.69 33.75-3.64 8.31-10.32 6.68-25.43-3.64-33.75-37.31-30.05-84.29-46.6-132.28-46.6-116.36 0-211.03 94.67-211.03 211.03s94.67 211.03 211.03 211.03c36.98 0 73.4-9.85 105.23-28.12 1.64 11.66 11.65 20.63 23.76 20.63 13.25 0 24-10.75 24-24V534.18c0-13.26-10.75-24-24-24z" fill="#EF5252" /><path d="M671.15 498.18h-135.9c-13.25 0-24 10.75-24 24s10.75 24 24 24h111.9v91.1c-1.21 0.66-2.37 1.42-3.49 2.31-29.1 23.19-64.2 35.44-101.5 35.44-89.89 0-163.03-73.13-163.03-163.03 0-89.89 73.13-163.03 163.03-163.03 37.61 0 72.94 12.44 102.17 35.98 10.32 8.31 25.43 6.69 33.75-3.64 8.31-10.32 6.68-25.43-3.64-33.75-37.31-30.05-84.29-46.6-132.28-46.6-116.36 0-211.03 94.67-211.03 211.03s94.67 211.03 211.03 211.03c36.98 0 73.4-9.85 105.23-28.12 1.64 11.66 11.65 20.63 23.76 20.63 13.25 0 24-10.75 24-24V522.18c0-13.26-10.75-24-24-24z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M788.14 928.23h-550c-77.32 0-140-62.68-140-140v-550c0-77.32 62.68-140 140-140h550c77.32 0 140 62.68 140 140v550c0 77.32-62.68 140-140 140z" fill="#FFC757" /><path d="M770.19 644.92l-67.56-117.01c-15.4-26.68-53.91-26.68-69.31 0l-64.01 110.88L450.3 432.65c-15.4-26.67-53.89-26.67-69.29 0L258.45 644.94c-15.4 26.67 3.85 60.01 34.65 60.01h442.44c30.8-0.01 50.05-33.35 34.65-60.03z" fill="#EF9F2B" /><path d="M595.103921 388.209544a55.27 55.27 0 1 0 100.661393-45.676862 55.27 55.27 0 1 0-100.661393 45.676862Z" fill="#EF9F2B" /><path d="M770.19 632.92l-67.56-117.01c-15.4-26.68-53.91-26.68-69.31 0l-64.01 110.88L450.3 420.65c-15.4-26.67-53.89-26.67-69.29 0L258.45 632.94c-15.4 26.67 3.85 60.01 34.65 60.01h442.44c30.8-0.01 50.05-33.35 34.65-60.03z" fill="#FFFFFF" /><path d="M595.104335 376.209454a55.27 55.27 0 1 0 100.661392-45.676862 55.27 55.27 0 1 0-100.661392 45.676862Z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M788.14 928.23h-550c-77.32 0-140-62.68-140-140v-550c0-77.32 62.68-140 140-140h550c77.32 0 140 62.68 140 140v550c0 77.32-62.68 140-140 140z" fill="#FF7878" /><path d="M682.2 326.8a27.8 27.8 0 0 0-10.04-21.39 27.807 27.807 0 0 0-22.87-5.93l-261.58 49.04c-13.15 2.46-22.67 13.94-22.67 27.32l0.18 250.12a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74 0 35.76 28.99 64.74 64.74 64.74s64.74-28.99 64.74-64.74V450.77l232.29-43.55v169.7a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74s28.99 64.74 64.74 64.74 64.74-28.99 64.74-64.74c-0.01-1.19-0.01-310.88-0.01-310.88z" fill="#EF5252" /><path d="M682.2 314.8a27.8 27.8 0 0 0-10.04-21.39 27.807 27.807 0 0 0-22.87-5.93l-261.58 49.04c-13.15 2.46-22.67 13.94-22.67 27.32l0.18 250.12a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74 0 35.76 28.99 64.74 64.74 64.74s64.74-28.99 64.74-64.74V438.77l232.29-43.55v169.7a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74s28.99 64.74 64.74 64.74 64.74-28.99 64.74-64.74c-0.01-1.19-0.01-310.88-0.01-310.88z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759421169656" class="icon" viewBox="0 0 1102 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6039" xmlns:xlink="http://www.w3.org/1999/xlink" width="215.234375" height="200"><path d="M1026.914462 133.041231H516.883692v-63.015385C516.962462 28.041846 482.540308 0 441.107692 0H75.854769C34.500923 0 0 34.973538 0 70.025846v210.077539h1102.769231v-70.104616a77.115077 77.115077 0 0 0-75.854769-76.957538zM82.707692 329.097846H0v574.148923c0 41.984 34.422154 76.957538 82.707692 76.957539h937.353846c48.285538 0 82.707692-34.973538 82.707693-76.957539v-574.227692H82.707692z" fill="#2F80ED" p-id="6040"></path></svg>

After

Width:  |  Height:  |  Size: 770 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758303555137" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7732"
width="200" height="200"><path d="M512 981.333333C251.733333 981.333333 42.666667 772.266667 42.666667 512S251.733333 42.666667 512 42.666667s469.333333 209.066667 469.333333 469.333333-209.066667 469.333333-469.333333 469.333333z m0-874.666666C288 106.666667 106.666667 288 106.666667 512s181.333333 405.333333 405.333333 405.333333 405.333333-181.333333 405.333333-405.333333S736 106.666667 512 106.666667z" fill="#2F80ED" p-id="7733"></path><path d="M768 800c-17.066667 0-32-14.933333-32-32 0-123.733333-100.266667-224-224-224S290.133333 644.266667 290.133333 768c0 17.066667-14.933333 32-32 32s-32-14.933333-32-32c0-157.866667 128-288 288-288s288 128 288 288c-2.133333 17.066667-17.066667 32-34.133333 32z" fill="#2F80ED" p-id="7734"></path><path d="M512 544c-87.466667 0-160-70.4-160-160 0-87.466667 70.4-160 160-160 87.466667 0 160 70.4 160 160 0 87.466667-72.533333 160-160 160z m0-256c-53.333333 0-96 42.666667-96 96s42.666667 96 96 96 96-42.666667 96-96-42.666667-96-96-96z" fill="#2F80ED" p-id="7735"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#C7DADD" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM512.53 295.92c-82.1 0-148.9 66.8-148.9 148.9 0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68c0-57.09 46.45-103.54 103.54-103.54 27.63 0 53.62 10.78 73.19 30.35s30.35 45.57 30.35 73.19c0 21.11-6.31 41.41-18.24 58.71-11.68 16.92-27.89 29.89-46.9 37.48-37.11 14.83-61.08 49.9-61.08 89.33V632c0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68v-1.66c0-20.78 12.78-39.31 32.56-47.21 27.33-10.92 50.63-29.54 67.4-53.84 17.18-24.9 26.26-54.11 26.26-84.47 0-39.74-15.5-77.13-43.64-105.27-28.13-28.13-65.52-43.63-105.26-43.63z" fill="#9DC0C9" /><path d="M500.973351 750.25961a30.24 30.24 0 1 0 23.144694-55.876234 30.24 30.24 0 1 0-23.144694 55.876234Z" fill="#9DC0C9" /><path d="M618.4 327.31c-28.14-28.14-65.52-43.64-105.26-43.64-82.1 0-148.9 66.8-148.9 148.9 0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68c0-57.09 46.45-103.54 103.54-103.54 27.63 0 53.62 10.78 73.19 30.35s30.35 45.57 30.35 73.19c0 21.11-6.31 41.41-18.24 58.71-11.68 16.92-27.89 29.89-46.9 37.48-37.11 14.83-61.08 49.9-61.08 89.33v1.66c0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68v-1.66c0-20.78 12.78-39.31 32.56-47.21 27.33-10.92 50.63-29.54 67.4-53.84 17.18-24.9 26.26-54.11 26.26-84.47 0-39.74-15.5-77.12-43.64-105.26z" fill="#FFFFFF" /><path d="M513.14 710.09m-30.24 0a30.24 30.24 0 1 0 60.48 0 30.24 30.24 0 1 0-60.48 0Z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758946130633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5976"
width="200" height="200"><path d="M512 0a512 512 0 0 1 17.554286 1023.707429L512 1024A512 512 0 0 1 512 0z m0 73.142857a438.857143 438.857143 0 1 0 0 877.714286V73.142857z" fill="#2F80ED" p-id="5977"></path></svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.58 63.94H269.26c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.44c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#F98950" /><path d="M860.93 216.59l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a119.93 119.93 0 0 0-31.93-57.29zM556.02 348.38H380.37c-13.25 0-24 10.75-24 24v302.74c0 13.25 10.75 24 24 24s24-10.75 24-24V595.6h151.65c68.16 0 123.61-55.45 123.61-123.61s-55.45-123.61-123.61-123.61z m0 199.22H404.37V396.38h151.65c41.69 0 75.61 33.92 75.61 75.61s-33.92 75.61-75.61 75.61z" fill="#F26C38" /><path d="M556.02 336.38H380.37c-13.25 0-24 10.75-24 24v302.74c0 13.25 10.75 24 24 24s24-10.75 24-24V583.6h151.65c68.16 0 123.61-55.45 123.61-123.61s-55.45-123.61-123.61-123.61z m0 199.22H404.37V384.38h151.65c41.69 0 75.61 33.92 75.61 75.61s-33.92 75.61-75.61 75.61z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.68 63.95H269.36c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.44c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#FF7878" /><path d="M861.03 216.59l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a120.058 120.058 0 0 0-31.93-57.29zM649.24 554.59c-22.79 0-45.38 3.31-68.16 7.6-26.87-24.76-49.66-54-66.99-86.56 18.5-60.63 19.47-101.76 5.45-121.26-6.43-8.58-17.14-14.04-28.04-14.04-14.02-0.97-27.07 5.46-33.5 17.35-19.47 32.56 8.76 96.5 21.81 122.43-15 46.59-33.5 91.04-57.25 134.32-102.82 44.45-104.97 71.54-104.97 81.29 0 11.89 6.43 23.78 18.31 29.24 4.28 3.12 10.71 4.29 16.16 4.29 27.07 0 58.42-30.41 91.92-90.06 42.26-17.35 84.32-31.39 128.73-41.13 22.79 19.49 50.83 30.41 80.04 32.56 18.5 0 54.14 0 53.75-37.04 1.17-14.04-6.43-37.82-57.26-38.99zM353.53 696.45l-3.26 1.17c9.6-14.06 22.26-25 38.38-31.64-8.44 14.26-20.15 25-35.12 30.47z m132.59-322.36c1.02-1.16 3.17-2.35 4.4-2.35l3.57 1.19c5.52 18.41 5.52 37.8-1.23 56.02-7.96-17.26-11.23-36.64-6.74-54.86zM542.26 584c-23.61 5.5-49.55 13.17-73.16 21.81l-2.45 0.96 0.32-1.94c11.81-23.97 22.64-49.13 32.32-74.28l1.16-2.45 1.17 1.47c12 18.47 27.2 37.51 43.46 53.82l-2.82 0.61z m111.17 13.58c-10.87-0.95-21.54-3.04-32.4-8.35 9.7-2.09 18.24-2.09 27.94-2.09 21.54 0 25.81 5.32 26 8.35-6.41 2.09-13.98 3.23-21.54 2.09z" fill="#F25555" /><path d="M648.96 575.14c-9.7 0-18.24 0-27.94 2.09 10.87 5.32 21.54 7.4 32.4 8.35 7.57 1.14 15.14 0 21.54-2.09-0.19-3.03-4.46-8.35-26-8.35z m-147.34-57.57l-1.17-1.47-1.16 2.45c-9.68 25.15-20.52 50.31-32.32 74.28l-0.32 1.94 2.45-0.96c23.61-8.65 49.55-16.31 73.16-21.81l2.81-0.61c-16.25-16.32-31.45-35.35-43.45-53.82z m-7.52-156.65l-3.57-1.19c-1.23 0-3.38 1.19-4.4 2.35-4.5 18.22-1.23 37.6 6.75 54.85 6.74-18.21 6.74-37.59 1.22-56.01z m-143.83 324.7l3.26-1.17c14.97-5.47 26.68-16.21 35.12-30.47-16.12 6.64-28.78 17.58-38.38 31.64z m302.48-67c-29.21-2.14-57.25-13.06-80.04-32.56-44.4 9.75-86.47 23.78-128.73 41.13-33.5 59.65-64.85 90.06-91.92 90.06-5.45 0-11.88-1.17-16.16-4.29-11.88-5.46-18.31-17.35-18.31-29.24 0-9.75 2.14-36.84 104.97-81.29 23.76-43.28 42.26-87.73 57.25-134.32-13.05-25.93-41.29-89.87-21.81-122.43 6.43-11.89 19.47-18.32 33.5-17.35 10.91 0 21.62 5.46 28.04 14.04 14.02 19.49 13.05 60.63-5.45 121.26 17.33 32.56 40.12 61.8 66.99 86.56 22.79-4.29 45.38-7.6 68.16-7.6 50.83 1.17 58.42 24.95 57.25 38.99 0.4 37.04-35.24 37.04-53.74 37.04z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#53B7F4" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM410.97 407.96c-9.37-9.37-24.57-9.37-33.94 0L274.97 510.01c-9.37 9.37-9.37 24.57 0 33.94L377.02 646a23.919 23.919 0 0 0 16.97 7.03c6.14 0 12.29-2.34 16.97-7.03 9.37-9.37 9.37-24.57 0-33.94l-85.08-85.08 85.08-85.08c9.38-9.37 9.38-24.57 0.01-33.94zM649.25 407.96c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l85.08 85.08-85.08 85.08c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L751.3 543.95c9.37-9.37 9.37-24.57 0-33.94L649.25 407.96zM566.34 360.5c-12.64-3.99-26.12 3.02-30.11 15.66l-91.94 291.23c-3.99 12.64 3.02 26.12 15.66 30.11 2.4 0.76 4.84 1.12 7.23 1.12 10.19 0 19.65-6.55 22.88-16.78L582 390.61c3.99-12.64-3.02-26.12-15.66-30.11z" fill="#29A3D3" /><path d="M410.97 395.96c-9.37-9.37-24.57-9.37-33.94 0L274.97 498.01c-9.37 9.37-9.37 24.57 0 33.94L377.02 634a23.919 23.919 0 0 0 16.97 7.03c6.14 0 12.29-2.34 16.97-7.03 9.37-9.37 9.37-24.57 0-33.94l-85.08-85.08 85.08-85.08c9.38-9.37 9.38-24.57 0.01-33.94zM751.31 498.01L649.25 395.96c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l85.08 85.08-85.08 85.08c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L751.3 531.95c9.38-9.37 9.38-24.56 0.01-33.94zM566.34 348.5c-12.64-3.99-26.12 3.02-30.11 15.66l-91.94 291.23c-3.99 12.64 3.02 26.12 15.66 30.11 2.4 0.76 4.84 1.12 7.23 1.12 10.19 0 19.65-6.55 22.88-16.78L582 378.61c3.99-12.64-3.02-26.12-15.66-30.11z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759421129815" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4996" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.515625" height="200"><path d="M447.783497 0c12.068571 0 23.552 5.485714 31.305143 14.921143L602.553783 166.034286h469.357714c52.955429 0.585143 95.451429 43.885714 95.085714 96.914285v664.137143a96.109714 96.109714 0 0 1-95.085714 96.914286H95.08864A96.109714 96.109714 0 0 1 0.002926 927.158857V96.841143C0.002926 43.446857 42.572069 0.073143 95.015497 0h352.768z m-26.038857 95.085714H95.08864v831.926857l0.146286 1.755429 976.457143 0.146286 0.146285-1.755429-0.219428-665.819428 0.292571-0.219429H557.42464L421.74464 95.085714z m323.730286 448.512c22.454857 0 40.667429 21.211429 40.667428 47.542857 0 26.258286-18.212571 47.542857-40.667428 47.542858H419.769783c-22.454857 0-40.667429-21.284571-40.667429-47.542858 0-26.331429 18.285714-47.542857 40.667429-47.542857h325.705143z" fill="#2F80ED" p-id="4997"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758303610044" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10913" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M844.8 182.4c-34.4-16.8-80.8-30.4-140-40C645.6 132.8 581.6 128 512 128c-69.6 0-133.6 4.8-192.8 14.4-59.2 9.6-105.6 23.2-140 40-34.4 16.8-51.2 35.2-51.2 55.2v103.2c0 20 16.8 38.4 51.2 55.2 34.4 16.8 80.8 30.4 140 40 59.2 9.6 123.2 14.4 192.8 14.4 69.6 0 133.6-4.8 192.8-14.4 59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2V237.6c0-20-16.8-38.4-51.2-55.2zM832 328.8c-3.2 2.4-8 5.6-16 8.8-28.8 14.4-69.6 25.6-122.4 34.4-55.2 9.6-116.8 13.6-181.6 13.6-65.6 0-126.4-4.8-181.6-13.6-52.8-8.8-94.4-20-122.4-34.4-8-4-12.8-7.2-16-8.8v-80c3.2-2.4 8-5.6 16-8.8 28.8-14.4 69.6-25.6 122.4-34.4C385.6 196 447.2 192 512 192c65.6 0 126.4 4.8 181.6 13.6 52.8 8.8 93.6 20.8 122.4 34.4 8 4 12.8 7.2 16 8.8v80zM128 760v26.4c0 20 16.8 38.4 51.2 55.2s80.8 30.4 140 40c59.2 9.6 123.2 14.4 192.8 14.4s133.6-4.8 192.8-14.4c59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2v-32.8c-40 24-93.6 42.4-162.4 54.4-68.8 12-142.4 18.4-221.6 18.4s-152.8-6.4-221.6-18.4C221.6 795.2 168 777.6 128 753.6M128 608v29.6c0 20 16.8 38.4 51.2 55.2 34.4 16.8 80.8 30.4 140 40 59.2 9.6 123.2 14.4 192.8 14.4s133.6-4.8 192.8-14.4c59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2v-32.8c-40 24-93.6 42.4-162.4 54.4-68.8 12-142.4 18.4-221.6 18.4s-152.8-6.4-221.6-18.4C221.6 646.4 168 628.8 128 604.8M128 456v29.6c0 20 16.8 38.4 51.2 55.2 34.4 16.8 80.8 30.4 140 40 59.2 9.6 123.2 14.4 192.8 14.4s133.6-4.8 192.8-14.4c59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2v-32.8c-40 24-93.6 42.4-162.4 54.4-68.8 12-142.4 18.4-221.6 18.4s-152.8-6.4-221.6-18.4C221.6 494.4 168 476.8 128 452.8" p-id="10914" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759421203348" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9072" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M128 0h64v128a64 64 0 0 0 64 64h512a64 64 0 0 0 64-64V0h64a128 128 0 0 1 128 128v768a128 128 0 0 1-128 128H128a128 128 0 0 1-128-128V128a128 128 0 0 1 128-128z m641.152 355.776l-303.936 303.936-175.872-205.184-72.896 62.464 243.328 283.904 377.216-377.216-67.84-67.904zM768 0v64a64 64 0 0 1-64 64H320a64 64 0 0 1-64-64V0h512z" fill="#2F80ED" p-id="9073"></path></svg>

After

Width:  |  Height:  |  Size: 700 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#4FD397" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM670.25 381.88H517.97a24.052 24.052 0 0 0-9.66 0H356.03c-13.25 0-24 10.75-24 24s10.75 24 24 24h133.11v258.73c0 13.25 10.75 24 24 24s24-10.75 24-24V429.88h133.11c13.25 0 24-10.75 24-24s-10.75-24-24-24z" fill="#19BC6E" /><path d="M670.25 369.88H517.97a24.052 24.052 0 0 0-9.66 0H356.03c-13.25 0-24 10.75-24 24s10.75 24 24 24h133.11v258.73c0 13.25 10.75 24 24 24s24-10.75 24-24V417.88h133.11c13.25 0 24-10.75 24-24s-10.75-24-24-24z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M65.3 755.4V269.9c0-78.1 63.3-141.3 141.3-141.3h612.9c78.1 0 141.3 63.3 141.3 141.3v485.5c0 78.1-63.3 141.3-141.3 141.3H206.7c-78.1 0-141.4-63.3-141.4-141.3z" fill="#8B72F7" /><path d="M557.3 406.6H308c-22.1 0-40 17.9-40 40v155.9c0 22.1 17.9 40 40 40h249.3c22.1 0 40-17.9 40-40V446.6c0-22-17.9-40-40-40zM738.8 412.7L655 491c-19.5 18.2-19.5 49 0 67.2l83.8 78.3c14.2 13.3 37.5 3.2 37.5-16.2V428.9c0-19.4-23.3-29.5-37.5-16.2z" fill="#7463EA" /><path d="M557.3 394.6H308c-22.1 0-40 17.9-40 40v155.9c0 22.1 17.9 40 40 40h249.3c22.1 0 40-17.9 40-40V434.6c0-22-17.9-40-40-40zM738.8 400.7L655 479c-19.5 18.2-19.5 49 0 67.2l83.8 78.3c14.2 13.3 37.5 3.2 37.5-16.2V416.9c0-19.4-23.3-29.5-37.5-16.2z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 979 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.7 64.2H270.4c-78.1 0-141.3 63.3-141.3 141.3v612.9c0 78.1 63.3 141.3 141.3 141.3h485.5c78.1 0 141.3-63.3 141.3-141.3V301.7c0-31.8-12.6-62.3-35.1-84.9L744.6 99.3c-22.5-22.5-53.1-35.1-84.9-35.1z" fill="#53B7F4" /><path d="M758.6 274.1H894c-5.1-21.5-16-41.4-31.9-57.3L744.6 99.3c-17-17-38.5-28.3-61.7-32.9v131.9c-0.1 41.9 33.9 75.8 75.7 75.8zM741.4 401.2H591.2c-8.3 0-16.1 4.3-20.4 11.4L465 584.4l-1.2 2-26.2 42.5-109.9-179.7h96.8l54.7 81.4 27.6-45.1-49.5-73.8c-4.5-6.6-11.9-10.6-19.9-10.6H284.9c-8.7 0-16.7 4.7-20.9 12.3-4.2 7.6-4.1 16.9 0.5 24.3l152.7 249.6c4.3 7.1 12.1 11.5 20.4 11.5 8.3 0 16.1-4.3 20.4-11.4l146.5-238.2h96.4l-98.1 179.4-46.8-69.8-27.7 44.9 56.7 84.5c4.5 6.7 11.9 10.6 19.9 10.6h1.1c8.4-0.4 15.9-5.1 20-12.5l136.4-249.6c4.1-7.4 3.9-16.5-0.4-23.8-4.3-7.2-12.1-11.7-20.6-11.7z" fill="#29A3D3" /><path d="M762.4 424.7L626 674.3c-4 7.4-11.6 12.1-20 12.5h-1.1c-8 0-15.4-4-19.9-10.6l-56.7-84.5 27.7-44.9 46.9 69.8L701 437.2h-96.4L458 675.4c-4.4 7.1-12.1 11.4-20.4 11.4-8.4 0-16.1-4.4-20.4-11.5L264.4 425.7c-4.5-7.4-4.7-16.7-0.5-24.3 4.2-7.6 12.2-12.3 20.9-12.3h152.4c8 0 15.5 4 19.9 10.6l49.5 73.8-27.6 45.1-54.7-81.4h-96.8l109.9 179.7 26.2-42.5 1.2-2 105.7-171.8c4.4-7.1 12.1-11.4 20.4-11.4h150.2c8.5 0 16.3 4.5 20.6 11.8 4.6 7.2 4.8 16.3 0.7 23.7z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.78 64.39H269.46c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.88c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#53B7F4" /><path d="M647.85 360.65c-13.25 0-24 10.75-24 24v226.26l-94.35-93.29c-9.33-9.22-24.34-9.25-33.69-0.05l-95.26 93.6V383.99c0-13.25-10.75-24-24-24s-24 10.75-24 24V668.4c0 9.67 5.8 18.39 14.72 22.13a24.002 24.002 0 0 0 26.1-5.01L512.58 568.4l118.4 117.07a23.993 23.993 0 0 0 16.88 6.93c3.11 0 6.25-0.61 9.24-1.85a24 24 0 0 0 14.75-22.15V384.65c0-13.25-10.74-24-24-24zM861.13 217.03l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a119.975 119.975 0 0 0-31.93-57.29z" fill="#29A3D3" /><path d="M647.86 680.4c-6.2 0-12.3-2.4-16.88-6.93L512.58 556.4 393.37 673.52a23.99 23.99 0 0 1-26.1 5.01 23.992 23.992 0 0 1-14.72-22.13V371.99c0-13.25 10.75-24 24-24s24 10.75 24 24v227.19l95.26-93.6c9.36-9.19 24.37-9.17 33.69 0.05l94.35 93.29V372.65c0-13.25 10.75-24 24-24s24 10.75 24 24V656.4c0 9.68-5.82 18.42-14.75 22.15a24.112 24.112 0 0 1-9.24 1.85z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.81H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V302.3c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#FFC757" /><path d="M304.85 196.81h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 196.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM304.85 396.81h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 396.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM455.65 552.02H321.52c-17.07 0-30.91 13.84-30.91 30.91v155.48c0 17.07 13.84 30.91 30.91 30.91h134.13c17.07 0 30.91-13.84 30.91-30.91V582.93c0-17.07-13.84-30.91-30.91-30.91z m-4.93 164.59c0 13.72-11.12 24.84-24.84 24.84h-74.6c-13.72 0-24.84-11.12-24.84-24.84v-27.36c0-13.72 11.12-24.84 24.84-24.84h74.6c13.72 0 24.84 11.12 24.84 24.84v27.36zM862.07 217.45l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a120.021 120.021 0 0 0-31.93-57.29z" fill="#F79F2B" /><path d="M304.85 184.81h71.52c7.86 0 14.24-6.38 14.24-14.24V99.05c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 184.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM304.85 384.81h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 384.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM455.65 540.02H321.52c-17.07 0-30.91 13.84-30.91 30.91v155.48c0 17.07 13.84 30.91 30.91 30.91h134.13c17.07 0 30.91-13.84 30.91-30.91V570.93c0-17.07-13.84-30.91-30.91-30.91z m-4.93 164.59c0 13.72-11.12 24.84-24.84 24.84h-74.6c-13.72 0-24.84-11.12-24.84-24.84v-27.36c0-13.72 11.12-24.84 24.84-24.84h74.6c13.72 0 24.84 11.12 24.84 24.84v27.36z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#2F80ED" d="M819.292 623.785c-40.844-40.844-88.387-72.547-140.151-94.102 69.587-51.392 114.809-133.97 114.809-226.921 0-155.467-126.483-281.951-281.951-281.951s-281.951 126.483-281.951 281.951c0 92.95 45.221 175.529 114.809 226.921-51.762 21.555-99.308 53.26-140.151 94.102-82.080 82.080-127.284 191.213-127.284 307.292 0 32.174 26.082 58.254 58.254 58.254s58.254-26.080 58.254-58.254c0-175.385 142.685-318.068 318.068-318.068 175.385 0 318.068 142.685 318.068 318.068 0 32.174 26.080 58.254 58.254 58.254s58.254-26.080 58.254-58.254c0-116.081-45.204-225.211-127.284-307.292zM512 137.32c91.225 0 165.442 74.218 165.442 165.442s-74.218 165.442-165.442 165.442-165.442-74.218-165.442-165.442 74.218-165.442 165.442-165.442z" /></svg>

After

Width:  |  Height:  |  Size: 1001 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#2F80ED" d="M722.6 645.295c17.497-26.999 9.793-63.072-17.206-80.567-21.997-14.255-45.104-26.406-69.045-36.377 69.66-51.387 114.937-134.006 114.937-227.013 0-155.467-126.483-281.951-281.951-281.951s-281.951 126.483-281.951 281.951c0 92.95 45.223 175.527 114.811 226.919-51.763 21.557-99.309 53.261-140.153 94.103-82.080 82.080-127.284 191.213-127.284 307.292 0 32.174 26.082 58.254 58.254 58.254s58.254-26.080 58.254-58.254c0-175.385 142.685-318.068 318.068-318.068 61.579 0 121.298 17.606 172.699 50.915 27.002 17.498 63.074 9.793 80.567-17.206zM469.334 135.897c91.225 0 165.442 74.218 165.442 165.442s-74.218 165.442-165.442 165.442c-91.225 0-165.442-74.217-165.442-165.442s74.218-165.442 165.442-165.442zM926.63 743.197h-66.992v-66.992c0-32.174-26.080-58.254-58.254-58.254s-58.254 26.080-58.254 58.254v66.992h-66.992c-32.174 0-58.254 26.080-58.254 58.254s26.080 58.254 58.254 58.254h66.992v66.992c0 32.174 26.080 58.254 58.254 58.254s58.254-26.080 58.254-58.254v-66.992h66.992c32.174 0 58.254-26.080 58.254-58.254s-26.080-58.254-58.254-58.254z" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
app/resource/images/bg0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
app/resource/images/bg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
app/resource/images/bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
app/resource/images/bg3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
app/resource/images/bg4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

BIN
app/resource/images/bg5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

103
app/resource/lang/en.json Normal file
View File

@@ -0,0 +1,103 @@
{
"你好": "Hello",
"我的文件": "My Files",
"存储配额": "Storage Quota",
"任务管理": "Task Management",
"应用信息": "Application Information",
"此页面正在建设中...": "This page is under construction...",
"语言设置:": "Language Settings:",
"中文": "Chinese",
"English": "English",
"上传": "Upload",
"设置": "Settings",
"确定": "OK",
"取消": "Cancel",
"关闭": "Close",
"刷新": "Refresh",
"删除": "Delete",
"重命名": "Rename",
"移动": "Move",
"复制": "Copy",
"分享": "Share",
"新建文件夹": "New Folder",
"文件信息": "File Info",
"大小": "Size",
"类型": "Type",
"修改时间": "Modified Time",
"创建时间": "Created Time",
"状态": "Status",
"进度": "Progress",
"速度": "Speed",
"剩余时间": "Remaining Time",
"暂停": "Pause",
"继续": "Resume",
"重试": "Retry",
"完成": "Completed",
"失败": "Failed",
"等待中": "Waiting",
"进行中": "In Progress",
"仓内搜索": "Search Within Folder",
"站内搜索": "Search Within Site",
"搜索文件": "Search Files",
"添加标签": "Add Tag",
"标签名称": "Tag Name",
"标签通配符": "Tag Expression",
"文件上传": "File Upload",
"文件下载": "File Download",
"用户组基础容量": "User Group Basic Capacity",
"有效容量包附加附加容量": "Valid Capacity Package Additional Capacity",
"已使用容量": "Used Capacity",
"总容量": "Total Capacity",
"LeonPan": "LeonPan",
"修改昵称": "Modify Nickname",
"用户信息": "User Information",
"修改头像": "Modify Avatar",
"用户头像": "User Avatar",
"点击修改头像": "Click to Modify Avatar",
"电子邮箱": "Email Address",
"当前用户组": "Current User Group",
"用户注册时间": "User Registration Time",
"修改成功": "Modification Successful",
"昵称修改成功": "Nickname Modification Successful",
"选择图片": "Select Image",
"头像修改成功": "Avatar Modification Successful",
"选择下载保存路径": "Select Download Save Path",
"选择文件夹": "Select Folder",
"下载保存路径修改成功": "Download Save Path Modification Successful",
"背景图片设置": "Background Image Settings",
"官方背景图": "Official Background Image",
"选择自定义背景": "Select Custom Background",
"图片背景透明度": "Image Background Opacity",
"设置图片背景透明度": "Set Image Background Opacity",
"透明度": "Opacity",
"透明度范围": "Opacity Range",
"选择自定义图片,选择后请不要更改图片位置": "Select Custom Image, Do Not Change Image Location After Selection",
"隐私协议": "Privacy Policy",
"用户协议": "User Agreement",
"官方预设背景图片": "Official Preset Background Images",
"选择背景图片": "Select Background Image",
"自定义背景图片": "Custom Background Image",
"选择保存路径": "Select Save Path",
"用户昵称": "User Nickname",
"更新设置": "Update Settings",
"检查更新": "Check for Updates",
"检查是否有新版本可用": "Check if new version is available",
"当前版本": "Current Version",
"发现新版本": "New Version Found",
"最新版本": "Latest Version",
"更新内容": "Update Content",
"立即更新": "Update Now",
"稍后更新": "Update Later",
"检查更新失败": "Failed to Check Updates",
"无法连接到更新服务器,请稍后再试。": "Failed to connect to update server, please try again later.",
"开启自动更新": "Enable Auto Update",
"在应用启动时自动检查更新": "Automatically check for updates when the application starts",
"已是最新版本": "Already Latest Version",
"语言设置": "Language Settings",
"下载": "download",
"预览": "preview",
"进入": "enter",
"刷新当前": "refresh current",
"上传文件": "upload file",
"设置存储策略": "Set Storage Strategy"
}

103
app/resource/lang/zh.json Normal file
View File

@@ -0,0 +1,103 @@
{
"你好": "你好",
"我的文件": "我的文件",
"存储配额": "存储配额",
"任务管理": "任务管理",
"应用信息": "应用信息",
"此页面正在建设中...": "此页面正在建设中...",
"语言设置:": "语言设置:",
"中文": "中文",
"English": "English",
"上传": "上传",
"设置": "设置",
"确定": "确定",
"取消": "取消",
"关闭": "关闭",
"刷新": "刷新",
"删除": "删除",
"重命名": "重命名",
"移动": "移动",
"复制": "复制",
"分享": "分享",
"新建文件夹": "新建文件夹",
"文件信息": "文件信息",
"大小": "大小",
"类型": "类型",
"修改时间": "修改时间",
"创建时间": "创建时间",
"状态": "状态",
"进度": "进度",
"速度": "速度",
"剩余时间": "剩余时间",
"暂停": "暂停",
"继续": "继续",
"重试": "重试",
"完成": "完成",
"失败": "失败",
"等待中": "等待中",
"进行中": "进行中",
"仓内搜索": "仓内搜索",
"站内搜索": "站内搜索",
"搜索文件": "搜索文件",
"添加标签": "添加标签",
"标签名称": "标签名称",
"标签通配符": "标签通配符",
"文件上传": "文件上传",
"文件下载": "文件下载",
"用户组基础容量": "用户组基础容量",
"有效容量包附加附加容量": "有效容量包附加附加容量",
"已使用容量": "已使用容量",
"总容量": "总容量",
"LeonPan": "LeonPan",
"修改昵称": "修改昵称",
"用户信息": "用户信息",
"修改头像": "修改头像",
"用户头像": "用户头像",
"点击修改头像": "点击修改头像",
"电子邮箱": "电子邮箱",
"当前用户组": "当前用户组",
"用户注册时间": "用户注册时间",
"修改成功": "修改成功",
"昵称修改成功": "昵称修改成功",
"选择图片": "选择图片",
"头像修改成功": "头像修改成功",
"选择下载保存路径": "选择下载保存路径",
"选择文件夹": "选择文件夹",
"下载保存路径修改成功": "下载保存路径修改成功",
"背景图片设置": "背景图片设置",
"官方背景图": "官方背景图",
"选择自定义背景": "选择自定义背景",
"图片背景透明度": "图片背景透明度",
"设置图片背景透明度": "设置图片背景透明度",
"透明度": "透明度",
"透明度范围": "透明度范围",
"选择自定义图片,选择后请不要更改图片位置": "选择自定义图片,选择后请不要更改图片位置",
"隐私协议": "隐私协议",
"用户协议": "用户协议",
"官方预设背景图片": "官方预设背景图片",
"选择背景图片": "选择背景图片",
"自定义背景图片": "自定义背景图片",
"选择保存路径": "选择保存路径",
"用户昵称": "用户昵称",
"更新设置": "更新设置",
"检查更新": "检查更新",
"检查是否有新版本可用": "检查是否有新版本可用",
"当前版本": "当前版本",
"发现新版本": "发现新版本",
"最新版本": "最新版本",
"更新内容": "更新内容",
"立即更新": "立即更新",
"稍后更新": "稍后更新",
"检查更新失败": "检查更新失败",
"无法连接到更新服务器,请稍后再试。": "无法连接到更新服务器,请稍后再试。",
"开启自动更新": "开启自动更新",
"在应用启动时自动检查更新": "在应用启动时自动检查更新",
"已是最新版本": "已是最新版本",
"语言设置": "语言设置",
"下载": "下载",
"预览": "预览",
"进入": "进入",
"刷新当前": "刷新当前",
"上传文件": "上传文件",
"设置存储策略": "设置存储策略"
}

334284
app/resource/resource.py Normal file

File diff suppressed because it is too large Load Diff

47
app/resource/resource.qrc Normal file
View File

@@ -0,0 +1,47 @@
<RCC>
<qresource prefix="/app">
<file>images/logo.png</file>
<file>images/background.png</file>
<file>images/load.png</file>
<file>images/error.png</file>
<file>images/empty.png</file>
<file>images/title.jpg</file>
<file>images/loadFailure.png</file>
<file>icons/login.svg</file>
<file>icons/register.svg</file>
<file>icons/3D.svg</file>
<file>icons/Config.svg</file>
<file>icons/Database.svg</file>
<file>icons/Excel.svg</file>
<file>icons/Exe.svg</file>
<file>icons/Folder.svg</file>
<file>icons/Gif.svg</file>
<file>icons/Image.svg</file>
<file>icons/music.svg</file>
<file>icons/None.svg</file>
<file>icons/Pdf.svg</file>
<file>icons/PPT.svg</file>
<file>icons/Programme.svg</file>
<file>icons/Txt.svg</file>
<file>icons/Video.svg</file>
<file>icons/Word.svg</file>
<file>icons/WPS.svg</file>
<file>icons/Zip.svg</file>
<file>icons/Score.svg</file>
<file>icons/Date.svg</file>
<file>icons/Email.svg</file>
<file>icons/Group.svg</file>
<file>icons/Nickname.svg</file>
<file>icons/BgImage.svg</file>
<file>icons/Opacity.svg</file>
<file>icons/SavePath.svg</file>
<file>icons/Myfile.svg</file>
<file>icons/Task.svg</file>
<file>icons/Storage.svg</file>
<file>icons/Info.svg</file>
<file>icons/Application.svg</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,300 @@
# coding: utf-8
import os
import sys
import requests
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtWidgets import (
QLabel,
QMessageBox,
QSizePolicy,
QSpacerItem,
QVBoxLayout,
QWidget,
)
from qfluentwidgets import (ComboBoxSettingCard, FluentIcon, ImageLabel, MessageBox, PrimaryPushSettingCard,
SettingCardGroup, SwitchSettingCard)
from app.core import cfg, lang, qconfig, signalBus
from app.core.utils.version import version
class AppInfoInterface(QWidget):
"""
APP信息页面
包含语言切换功能
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("appInfoInterface")
self.initUI()
self.connectSignalToSlot()
# 移除初始化时的自动检查,改为由登录成功信号触发
def initUI(self):
# 创建主布局
mainLayout = QVBoxLayout(self)
mainLayout.setContentsMargins(30, 30, 30, 30)
mainLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.titleImageLabel = ImageLabel(":app/images/title.jpg", self)
self.titleImageLabel.scaledToHeight(130)
mainLayout.addWidget(self.titleImageLabel, 0, Qt.AlignmentFlag.AlignHCenter)
# 添加标题
self.titleLabel = QLabel(lang("应用信息"))
self.titleLabel.setStyleSheet("QLabel { font-size: 24px; font-weight: bold; }")
mainLayout.addWidget(self.titleLabel)
# 创建设置卡组
self.languageGroup = SettingCardGroup(lang("语言设置"), self)
# 语言选择设置卡
self.languageCard = ComboBoxSettingCard(
title=lang("语言设置"),
icon=FluentIcon.LANGUAGE,
texts=["中文", "English"],
configItem=cfg.language,
parent=self.languageGroup,
)
# 将设置卡添加到组
self.languageGroup.addSettingCard(self.languageCard)
# 将设置卡组添加到主布局
mainLayout.addWidget(self.languageGroup)
# 创建更新设置卡组
self.updateGroup = SettingCardGroup(lang("更新设置"), self)
# 自动更新设置开关
self.autoUpdateSwitch = SwitchSettingCard(
title=lang("开启自动更新"),
icon=FluentIcon.UPDATE,
configItem=cfg.checkUpdateAtStartUp,
parent=self.updateGroup,
)
# 手动检查更新设置卡
self.checkUpdateCard = PrimaryPushSettingCard(
title=lang("检查更新"),
text=lang("检查是否有新版本可用"),
icon=FluentIcon.UPDATE,
parent=self.updateGroup,
)
# 当前版本信息
self.versionLabel = QLabel(f"{lang('当前版本')}: {version}")
self.versionLabel.setStyleSheet(
"QLabel { font-size: 14px; color: #666; margin-top: 10px; margin-left: 10px; }"
)
# 将设置卡添加到组
self.updateGroup.addSettingCard(self.autoUpdateSwitch)
self.updateGroup.addSettingCard(self.checkUpdateCard)
self.updateGroup.layout().addWidget(self.versionLabel)
# 将更新设置卡组添加到主布局
mainLayout.addWidget(self.updateGroup)
# 添加空白占位符
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
mainLayout.addItem(spacer)
# 底部空间
bottomSpacer = QSpacerItem(20, 100, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
mainLayout.addItem(bottomSpacer)
def connectSignalToSlot(self):
# 连接语言变更信号
signalBus.languageChanged.connect(self.updateUI)
# 连接ComboBox的当前文本变更信号
self.languageCard.comboBox.currentTextChanged.connect(self.onLanguageChanged)
# 连接检查更新按钮信号
self.checkUpdateCard.clicked.connect(self.manualCheckUpdate)
# 自动更新开关的信号已通过configItem自动连接无需额外处理
# 连接登录成功信号,在用户登录后执行自动检查更新
signalBus.loginSuccessSignal.connect(self.onLoginSuccess)
def checkUpdate(self):
"""检查应用更新"""
try:
# 发送请求获取应用信息
url = "https://leon.miaostars.com/api.php?t=getappinfo&id=23"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON响应
data = response.json()
if data.get("status") == "success":
app_data = data.get("data", {})
versions = app_data.get("versions", [])
if versions:
# 获取最新版本
latest_version = versions[0].get("version", "")
# 比较版本号 - 只要版本不同就提示更新
if latest_version and latest_version != version:
# 有新版本
changelog = versions[0].get("changelog", "")
api_file_path = versions[0].get("file_path", "")
# 确保使用完整的URL如果路径不包含协议则添加域名前缀
if api_file_path:
# 检查是否是完整的URL
if not (
api_file_path.startswith("http://")
or api_file_path.startswith("https://")
):
# 添加域名前缀,确保链接完整
file_path = (
f"https://leon.miaostars.com/{api_file_path}"
)
else:
file_path = api_file_path
else:
# 使用默认下载链接
file_path = "https://leon.miaostars.com/app.php?id=23"
# 使用QFluentWidgets的MessageBox提示用户更新
msg_box = MessageBox(
lang("发现新版本"),
f"{lang('当前版本')}: {version}\n{lang('最新版本')}: {latest_version}\n\n{lang('更新内容')}:\n{changelog}",
self,
)
msg_box.yesButton.setText(lang("立即更新"))
msg_box.cancelButton.setText(lang("稍后更新"))
# QFluentWidgets的MessageBox.exec()返回True表示用户点击了确认按钮
if msg_box.exec():
# 添加下载更新的逻辑
# 例如:打开浏览器访问下载链接
if file_path:
import webbrowser
webbrowser.open(file_path)
return True
# 没有新版本或请求失败
return False
except Exception as e:
print(f"检查更新失败: {e}")
# 如果是手动检查更新,则显示错误提示
if hasattr(self, "is_manual_check") and self.is_manual_check:
error_box = MessageBox(
lang("检查更新失败"),
f"{lang('无法连接到更新服务器,请稍后再试。')}\n{str(e)}",
self,
)
error_box.cancelButton.setVisible(False)
error_box.exec()
return False
def manualCheckUpdate(self):
"""手动检查更新"""
self.is_manual_check = True
has_update = self.checkUpdate()
if not has_update and self.is_manual_check:
# 如果是手动检查且没有更新
no_update_box = MessageBox(
lang("已是最新版本"),
f"{lang('当前版本')} {version} {lang('已是最新版本。')}",
self,
)
no_update_box.cancelButton.setVisible(False)
no_update_box.exec()
self.is_manual_check = False
def autoCheckUpdate(self):
"""自动检查更新"""
print(f"自动检查更新已触发,配置状态: {cfg.checkUpdateAtStartUp.value}")
# 在单独的线程中执行避免阻塞UI
class UpdateCheckThread(QThread):
update_available = pyqtSignal(bool)
def run(self):
try:
print("开始检查更新...")
url = "https://leon.miaostars.com/api.php?t=getappinfo&id=23"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
if data.get("status") == "success":
app_data = data.get("data", {})
versions = app_data.get("versions", [])
if versions:
latest_version = versions[0].get("version", "")
print(f"当前版本: {version}, 最新版本: {latest_version}")
if latest_version and latest_version != version:
print("发现新版本,准备显示更新提示")
self.update_available.emit(True)
return
except Exception as e:
print(f"自动检查更新出错: {e}")
self.update_available.emit(False)
# 创建并启动线程
self.update_thread = UpdateCheckThread()
self.update_thread.update_available.connect(self.onAutoUpdateAvailable)
self.update_thread.start()
def onAutoUpdateAvailable(self, available):
"""自动检查更新结果处理"""
print(f"自动检查更新结果: {'有更新' if available else '无更新'}")
if available:
# 自动检查到更新时再次调用checkUpdate显示提示
self.is_manual_check = False
self.checkUpdate()
def onLoginSuccess(self):
"""用户登录成功后的处理"""
print(f"用户登录成功,检查是否开启自动更新: {cfg.checkUpdateAtStartUp.value}")
# 在用户登录成功后,根据配置决定是否自动检查更新
if cfg.checkUpdateAtStartUp.value:
self.autoCheckUpdate()
def onLanguageChanged(self, text):
# 语言变更处理
# 从选项映射获取对应的语言代码
lang_map = {"中文": "zh", "English": "en"}
lang_code = lang_map.get(text, "zh")
# 保存到配置
qconfig.set(cfg.language, lang_code)
# 显示重启提示
reply = QMessageBox.question(
self,
lang("语言变更"),
lang("语言已变更,是否立即重启应用以应用新语言?"),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply == QMessageBox.Yes:
# 重启应用
self.restartApplication()
def updateUI(self):
# 更新UI文本
self.titleLabel.setText(lang("应用信息"))
self.hintLabel.setText(lang("此页面正在建设中..."))
# 注意SettingCardGroup可能没有setTitle方法需要根据实际API调整
def restartApplication(self):
"""重启应用程序"""
# 保存配置
qconfig.save()
# 获取当前Python解释器路径和脚本路径
python = sys.executable
script = os.path.abspath(sys.argv[0])
# 退出当前进程
sys.exit()
# 注意在实际应用中这里应该使用subprocess重新启动应用但为了安全考虑
# 这里仅退出当前进程,让用户手动重启

View File

@@ -0,0 +1,49 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout
from qfluentwidgets import CardWidget, ImageLabel, SubtitleLabel
class EmptyCard(CardWidget):
def __init__(self, parent=None, text=None):
super().__init__(parent=parent)
self.setMinimumWidth(200)
self.setBorderRadius(10)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.iconLabel = ImageLabel(self)
self.iconLabel.setImage(":app/images/empty.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel = SubtitleLabel(self)
self.titleLabel.setText(text)
self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.vBoxLayout.addWidget(self.iconLabel, 0, Qt.AlignmentFlag.AlignHCenter)
self.vBoxLayout.addWidget(self.titleLabel)
def setText(self, text):
self.titleLabel.setText(text)
self.update()
def load(self):
self.iconLabel.setImage(":app/images/load.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("加载中...")
def error(self):
self.iconLabel.setImage(":app/images/error.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("加载失败,请重试")
def empty(self):
self.iconLabel.setImage(":app/images/empty.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("这里空空如也")

View File

@@ -0,0 +1,362 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import Action, BodyLabel, CardWidget, ImageLabel, InfoBar, InfoBarPosition, MenuAnimationType, \
MessageBox, PushButton, RoundMenu, StrongBodyLabel
from qfluentwidgets import FluentIcon as FIF
from app.core import (DeleteFileThread, formatDate, formatSize, getFileIcon, lang, signalBus)
from app.view.widgets.share_file_messageBox import ShareFileMessageBox
class FileCard(CardWidget):
def __init__(self, _id, fileName, fileType, path, date, size, parent=None):
super().__init__(parent)
self._id = _id
self.fileName = fileName
self.fileSize = size
self.changeTime = date
self.fileType = fileType
self.filePath = path
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.fileSizeLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
if self.fileType == "dir":
self.clicked.connect(self.dirClicked)
self.suffix = fileName.split(".")[-1].lower()
def mousePressEvent(self, event):
"""重写鼠标点击事件,区分左右键"""
if event.button() == Qt.MouseButton.RightButton:
# 使用globalPosition()获取全局位置并转换为适合菜单显示的坐标
global_pos = event.globalPosition().toPoint()
if self.fileType == "file":
self.showFileContextMenu(global_pos)
else:
self.showFolderContextMenu(global_pos)
else:
super().mousePressEvent(event)
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def dirClicked(self):
if self.filePath == "/":
paths = "/" + self.fileName
else:
paths = f"{self.filePath}/{self.fileName}"
signalBus.dirOpenSignal.emit(paths)
def selfPreview(self):
if self.fileType == "file" and self.suffix in [
"jpg",
"png",
"jpeg",
"bmp",
"gif",
]:
signalBus.imagePreviewSignal.emit(self._id)
if self.fileType == "file" and self.suffix in ["txt", "py", "md"]:
signalBus.txtPreviewSignal.emit(self._id)
def downloadFile(self):
if self.fileType == "file":
# 构建Cloudreve V4 API所需的正确路径格式
# 确保不会出现重复的前缀和文件名
if self.filePath == "/":
# 根目录情况
full_path = f"cloudreve://my/{self.fileName}"
else:
# 子目录情况,确保正确拼接路径
# 清理路径,避免重复的斜杠
clean_path = self.filePath.lstrip("/")
full_path = f"cloudreve://my/{clean_path}/{self.fileName}"
# 确保路径格式正确,没有重复的部分
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
# 确保没有重复的文件名
if f"/{self.fileName}/{self.fileName}" in full_path:
full_path = full_path.replace(f"/{self.fileName}/{self.fileName}", f"/{self.fileName}")
signalBus.addDownloadFileTask.emit(
f"own.{self.suffix}", self.fileName, full_path
)
def showFileContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(
FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile()
),
Action(
FIF.PROJECTOR, lang("预览"), triggered=lambda: self.selfPreview()
),
]
)
menu.addSeparator()
menu.addActions(
[
Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def showFolderContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
]
)
menu.addSeparator()
menu.addActions(
[
Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def deleteSelf(self):
w = MessageBox(
"确认删除",
f"你确定要删除{self.fileName}吗?\n删除后不可恢复噢!",
parent=self.window(),
)
if w.exec():
self.deleteThread = DeleteFileThread(self._id, self.fileType)
self.deleteThread.successDelete.connect(self.deleteSuccess)
self.deleteThread.errorDelete.connect(self.deleteError)
self.deleteThread.start()
else:
InfoBar.info(
"提示",
"删除已取消",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def deleteSuccess(self):
InfoBar.success(
"成功",
"成功删除",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
signalBus.refreshFolderListSignal.emit()
def deleteError(self, error_msg):
InfoBar.error(
"失败",
"删除失败",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
logger.error(f"删除文件失败:{error_msg}")
def contextMenuEvent(self, e):
"""重写上下文菜单事件,确保只有右键点击才会触发"""
pass
class ShareFileCard(CardWidget):
def __init__(self, data, parent=None):
super().__init__(parent)
self._id = data["key"]
self.fileName = data["source"]["name"]
self.fileSize = data["source"]["size"]
self.changeTime = data["create_date"]
self.fileType = "dir" if data["is_dir"] else "file"
self.preview = data["preview"]
self.passWord = data["password"]
self.remainDownloads = data["remain_downloads"]
self.downloads = data["downloads"]
self.score = data["score"]
self.views = data["views"]
self.expireTime = data["expire"]
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.viewButton = PushButton("查看", self)
self.viewButton.clicked.connect(self.viewFile)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.addWidget(self.viewButton)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
self.suffix = self.fileName.split(".")[-1].lower()
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def viewFile(self):
if self.fileType == "file":
w = ShareFileMessageBox(
self._id, self.iconLabel.pixmap(), self.suffix, self.window()
)
if w.exec():
...
else:
signalBus.shareFolderViewSignal.emit(self._id)
class SharedFolderFileCard(CardWidget):
shareFileDownloadSignal = pyqtSignal() # 共享文件下载信号
def __init__(self, key, _id, fileName, fileType, path, date, size, parent=None):
super().__init__(parent)
self._id = _id
self.key = key
self.fileName = fileName
self.fileSize = size
self.changeTime = date
self.fileType = fileType
self.filePath = path
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.fileSizeLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
self.suffix = self.fileName.split(".")[-1].lower()
if self.fileType == "dir":
# 连接左键点击信号
self.clicked.connect(self.dirClicked)
def mousePressEvent(self, event):
"""重写鼠标点击事件,区分左右键"""
if event.button() == Qt.MouseButton.RightButton:
# 右键点击,显示上下文菜单
if self.fileType == "file":
self.showFileContextMenu(event.globalPos())
else:
self.showFolderContextMenu(event.globalPos())
else:
# 左键或其他按钮点击,调用父类处理
super().mousePressEvent(event)
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def dirClicked(self):
if self.filePath == "/":
paths = "/" + self.fileName
else:
paths = f"{self.filePath}/{self.fileName}"
signalBus.shareDirOpenSignal.emit(paths)
def downloadFile(self):
if self.fileType == "file":
signalBus.addDownloadFileTask.emit(
f"share.{self.suffix}",
self.fileName,
f"{self.filePath}/{self.fileName}.{self.key}",
)
signalBus.shareFileDownloadSignal.emit()
def showFileContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(
FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile()
),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def showFolderContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)

View File

@@ -0,0 +1,263 @@
# coding: utf-8
import os
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, ImageLabel, InfoBar, InfoBarPosition, PrimaryToolButton,
ProgressBar, SubtitleLabel)
from app.core import (DownloadShareThread, DownloadThread, formatSize, getFileIcon, signalBus, UploadThread)
class UploadCard(CardWidget):
def __init__(self, fileType, filePath, parent=None):
super().__init__(parent=parent)
self.fileType = fileType
self.filePath = filePath
self.fileName = os.path.basename(filePath)
self.setFixedHeight(75)
self.hBoxLayout = QHBoxLayout(self)
self.infomationLayout = QVBoxLayout()
self.iconLabel = ImageLabel(self)
self.fileNameLabel = SubtitleLabel(self.fileName, self)
self.currentStatusLabel = BodyLabel("等待中...", self)
self.progressBar = ProgressBar(self)
self.progressBar.setValue(0)
self.cancelButton = PrimaryToolButton(FluentIcon.DELETE, self)
self.retryButton = PrimaryToolButton(FluentIcon.RETURN, self)
self.retryButton.clicked.connect(self.retryUpload)
self.cancelButton.clicked.connect(self.cancelUpload)
self.hBoxLayout.addWidget(
self.iconLabel, 0, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignLeft
)
self.hBoxLayout.addSpacing(5)
self.infomationLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(
self.currentStatusLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(self.progressBar)
self.hBoxLayout.addLayout(self.infomationLayout)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.retryButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
self.hBoxLayout.addWidget(
self.cancelButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
self.setIcon()
self.uploadThread = None
self.startUpload()
def startUpload(self):
self.retryButton.setEnabled(False)
self.uploadThread = UploadThread(self.filePath)
self.uploadThread.uploadApplicationApprovedSignal.connect(
self.uploadApplication
)
self.uploadThread.uploadFinished.connect(self.uploadFinished)
self.uploadThread.uploadFailed.connect(self.uploadFailed)
self.uploadThread.uploadProgress.connect(self.uploadProgress)
self.uploadThread.start()
def retryUpload(self):
self.currentStatusLabel.setText("重试...")
self.startUpload()
def cancelUpload(self):
if self.uploadThread:
self.progressBar.setValue(0)
self.selfDelete()
else:
self.selfDelete()
def selfDelete(self):
self.currentStatusLabel.setText("取消中...")
self.cancelButton.setEnabled(False)
self.retryButton.setEnabled(False)
if self.uploadThread:
self.uploadThread.cancelUpload()
self.uploadThread.terminate()
self.uploadThread = None
QTimer.singleShot(1000, self.deleteLater)
def uploadApplication(self):
self.currentStatusLabel.setText("已向服务器提交任务,读取文件中...")
def uploadFinished(self):
self.currentStatusLabel.setText("上传成功")
InfoBar.success(
"成功",
f"{self.fileName}上传成功,请注意查看~",
Qt.Orientation.Horizontal,
True,
5000,
InfoBarPosition.BOTTOM_RIGHT,
InfoBar.desktopView(),
)
signalBus.refreshFolderListSignal.emit()
self.retryButton.setEnabled(False)
self.progressBar.setValue(100)
def uploadProgress(self, progress, uploaded_size, total_size):
self.progressBar.setValue(int(progress))
self.currentStatusLabel.setText(
f"上传中...({formatSize(uploaded_size)}/{formatSize(total_size)})"
)
def uploadFailed(self, error_message):
self.currentStatusLabel.setText(f"上传失败:{error_message}")
self.progressBar.setValue(0)
self.retryButton.setEnabled(True)
self.uploadThread.terminate()
self.uploadThread = None
def setIcon(self):
icon_name = getFileIcon("file", self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(50)
self.iconLabel.scaledToWidth(50)
class DownloadCard(CardWidget):
def __init__(self, suffix, fileName, _id, parent=None):
super().__init__(parent=parent)
self._id = _id
self.suffix = suffix
self.setFixedHeight(75)
self.hBoxLayout = QHBoxLayout(self)
self.infomationLayout = QVBoxLayout()
self.iconLabel = ImageLabel(self)
self.fileNameLabel = SubtitleLabel(fileName, self)
self.currentStatusLabel = BodyLabel("请求中...", self)
self.progressBar = ProgressBar(self)
self.progressBar.setValue(0)
self.cancelButton = PrimaryToolButton(FluentIcon.DELETE, self)
self.cancelButton.clicked.connect(self.cancelDownload)
self.hBoxLayout.addWidget(
self.iconLabel, 0, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignLeft
)
self.hBoxLayout.addSpacing(5)
self.infomationLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(
self.currentStatusLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(self.progressBar)
self.hBoxLayout.addLayout(self.infomationLayout)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.cancelButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
suffix = self.suffix.split(".")[1].lower()
self._type = self.suffix.split(".")[0].lower()
self.setIcon(suffix)
self.downloadThread = None
self.startUpload()
def startUpload(self):
if self._type == "own":
# 同时传递file_id和file_path确保file_path不为空
# 对于own类型使用self._id作为file_path因为它已经包含了完整的cloudreve://格式路径
self.downloadThread = DownloadThread(self._id, self._id)
elif self._type == "share":
self.downloadThread = DownloadShareThread(self._id)
self.downloadThread.downloadUrlAcquired.connect(self.downloadUrlAcquired)
self.downloadThread.downloadFinished.connect(self.downloadFinished)
self.downloadThread.downloadFailed.connect(self.downloadFailed)
self.downloadThread.downloadProgress.connect(self.downloadProgress)
self.downloadThread.start()
def cancelDownload(self):
if self.downloadThread:
self.progressBar.setValue(0)
self.selfDelete()
else:
self.selfDelete()
def selfDelete(self):
self.currentStatusLabel.setText("取消中...")
self.cancelButton.setEnabled(False)
if self.downloadThread:
self.downloadThread.cancelDownload()
self.downloadThread.terminate()
self.downloadThread = None
QTimer.singleShot(1000, self.deleteLater)
def downloadUrlAcquired(self):
self.currentStatusLabel.setText("成功获取下载链接,准备下载...")
def downloadFinished(self):
self.currentStatusLabel.setText("下载成功")
InfoBar.success(
"成功",
f"{self._id}下载成功,请注意查看~",
Qt.Orientation.Horizontal,
True,
5000,
InfoBarPosition.BOTTOM_RIGHT,
InfoBar.desktopView(),
)
self.progressBar.setValue(100)
def downloadProgress(self, progress, uploaded_size, total_size):
self.progressBar.setValue(int(progress))
self.currentStatusLabel.setText(
f"下载中...({formatSize(uploaded_size)}/{formatSize(total_size)})"
)
def downloadFailed(self, error_message):
self.currentStatusLabel.setText(f"下载失败:{error_message}")
self.progressBar.setValue(0)
self.downloadThread.terminate()
self.downloadThread = None
def setIcon(self, fileName):
icon_name = getFileIcon("file", fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(50)
self.iconLabel.scaledToWidth(50)

View File

@@ -0,0 +1,37 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout
from qfluentwidgets import BodyLabel, ElevatedCardWidget, SubtitleLabel
class GbInformationCard(ElevatedCardWidget):
def __init__(self, amount,station,parent=None):
super().__init__(parent=parent)
self.currentAmountLabel = SubtitleLabel(self)
self.currentAmountLabel.setText(self.formatSize(amount))
self.currentAmountLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.stationLabel = BodyLabel(station,self)
self.stationLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(2,5,2,5)
self.vBoxLayout.addWidget(self.currentAmountLabel,0,Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addSpacing(0)
self.vBoxLayout.addWidget(self.stationLabel,0,Qt.AlignmentFlag.AlignTop)
@staticmethod
def formatSize(size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def updateValue(self,value):
self.currentAmountLabel.setText(self.formatSize(value))

View File

@@ -0,0 +1,362 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QFileDialog, QVBoxLayout, QWidget
from qfluentwidgets import (
Action,
InfoBar,
InfoBarPosition,
MenuAnimationType,
RoundMenu,
ScrollArea,
)
from qfluentwidgets import FluentIcon as FIF
from app.core import (lang, ListFileThread, ListSearchThread, ListShareThread, policyConfig, signalBus)
from app.view.components.file_card import FileCard, ShareFileCard
from app.view.widgets.new_folder_messageBox import NewFolderMessageBox
from app.view.widgets.policy_messageBox import PolicyChooseMessageBox
class LinkageSwitchingBase(ScrollArea):
"""文件卡片滚动区域组件基"""
def __init__(self, parent=None):
super().__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
self.widgets = QWidget()
self.layouts = QVBoxLayout(self.widgets)
self.layouts.setAlignment(Qt.AlignmentFlag.AlignTop)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.layouts.setContentsMargins(5, 5, 5, 0)
self.layouts.setSpacing(5)
self.widgets.setStyleSheet("background-color: transparent; border: none;")
self.setStyleSheet("background-color: transparent; border: none;")
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
def addFileCard(self, fileId, data):
"""
添加文件卡片
Args:
fileId: 文件的唯一标识符
data: 文件数据对象
Returns:
创建的文件卡片对象
"""
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
# 安全地获取对象属性提供默认值以避免KeyError
fileId = data.get("id", "")
fileName = data.get("name", "未知文件")
# Cloudreve V4 API使用数字1表示文件夹0表示文件
fileType_num = data.get("type", 0)
# 将数字类型转换为字符串表示
fileType = "folder" if fileType_num == 1 else "file"
filePath = data.get("path", "")
# 使用created_at或updated_at作为日期
fileDate = data.get("created_at", data.get("date", ""))
fileSize = data.get("size", 0)
fileCard = FileCard(
fileId,
fileName,
fileType,
filePath,
fileDate,
fileSize,
self,
)
fileCard.setObjectName(f"fileCard_{fileId}")
self.fileCardsDict[fileId] = fileCard
self.layouts.addWidget(fileCard)
return fileCard
def removeFileCard(self, fileId):
"""移除文件卡片"""
if fileId in self.fileCardsDict:
fileCard = self.fileCardsDict[fileId]
self.layouts.removeWidget(fileCard)
fileCard.deleteLater()
del self.fileCardsDict[fileId]
else:
logger.warning(f"尝试移除不存在的文件卡片: {fileId}")
def clearFileCards(self):
"""清除所有文件卡片"""
logger.debug("清除所有文件卡片")
fileIds = list(self.fileCardsDict.keys())
for fileId in fileIds:
self.removeFileCard(fileId)
def refreshFolderList(self):
logger.debug("刷新文件夹列表")
InfoBar.success(
"成功",
"刷新成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
signalBus.refreshFolderListSignal.emit()
# 个人文件浏览区域
class OwnFileLinkageSwitching(LinkageSwitchingBase):
"""个人件卡片滚动区域组件"""
def __init__(self, paths, parent=None):
super(OwnFileLinkageSwitching, self).__init__(parent)
self.currentPath = paths
self.fileCardsDict = {} # 存储所有文件卡片
self.loadDict("/")
def contextMenuEvent(self, e):
"""菜单事件"""
logger.debug("触发上下文菜单事件")
menu = RoundMenu(parent=self)
menu.addAction(
Action(FIF.SYNC, lang("刷新当前"), triggered=self.refreshFolderList)
)
menu.addSeparator()
menu.addAction(
Action(FIF.ADD, lang("新建文件夹"), triggered=self._createFolder)
)
menu.addSeparator()
menu.addAction(Action(FIF.UP, lang("上传文件"), triggered=self._uploadFile))
menu.addSeparator()
menu.addAction(
Action(FIF.CLOUD, lang("设置存储策略"), triggered=self._choosePolicy)
)
menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
def _choosePolicy(self):
w = PolicyChooseMessageBox(self.window())
if w.exec():
...
def _createFolder(self):
w = NewFolderMessageBox(self.window())
if w.exec():
...
def _uploadFile(self):
file_name, _ = QFileDialog.getOpenFileName(
self.window(), "选择文件", "", "所有文件 (*)"
)
if file_name:
signalBus.addUploadFileTask.emit(file_name)
def loadDict(self, paths):
"""加载目录数据"""
logger.info(f"加载目录数据: {paths}")
policyConfig.setCurrentPath(paths)
self.currentPath = paths
self.loadDataThread = ListFileThread(paths)
self.loadDataThread.listDictSignal.connect(self.dealData)
self.loadDataThread.errorSignal.connect(self._errorLoadDict)
self.loadDataThread.start()
def dealData(self, data):
"""处理目录数据"""
self.clearFileCards()
logger.info("设置当前页策略")
# 安全地访问策略信息考虑data["data"]可能是列表的情况
if isinstance(data, dict) and "data" in data:
data_content = data["data"]
if isinstance(data_content, dict):
# Cloudreve V4 API格式处理
if "storage_policy" in data_content:
policyConfig.setPolicy(data_content["storage_policy"])
elif "policy" in data_content:
policyConfig.setPolicy(data_content["policy"])
elif isinstance(data_content, list) and data_content:
# 如果data_content是列表尝试从第一个元素获取策略
logger.warning("data['data']是列表而不是字典可能需要调整API响应处理")
# 处理data["data"]可能是列表或字典的情况
data_content = data.get("data", {})
if isinstance(data_content, list):
# 如果是列表直接使用列表作为objects
logger.info(f"成功加载目录数据,对象数量: {len(data_content)}")
self.objects = data_content
elif isinstance(data_content, dict):
# Cloudreve V4 API格式处理先检查files字段
if "files" in data_content:
# Cloudreve V4 API 使用files字段存储文件列表
logger.info(f"成功加载目录数据,对象数量: {len(data_content['files'])}")
self.objects = data_content["files"]
elif "objects" in data_content:
# 向后兼容旧版API
logger.info(f"成功加载目录数据,对象数量: {len(data_content['objects'])}")
self.objects = data_content["objects"]
else:
logger.error("目录数据格式错误字典中没有files或objects字段")
return
else:
logger.error("目录数据格式错误")
return
for obj in self.objects:
self.addFileCard(obj["id"], obj)
def _errorLoadDict(self, error_msg):
"""处理加载目录数据失败"""
logger.error(f"加载目录数据失败: {error_msg}")
InfoBar.error(
"错误",
f"加载目录数据失败: {error_msg}",
Qt.Orientation.Horizontal,
True,
-1,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.loadDict("/")
# 搜索文件浏览区域
class SearchLinkageSwitching(LinkageSwitchingBase):
"""文件卡片滚动区域组件"""
def __init__(self, parent=None):
super(SearchLinkageSwitching, self).__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
def search(self, searchType, searchContent):
"""加载数据"""
self.loadDataThread = ListSearchThread(searchContent, searchType)
self.loadDataThread.listDictSignal.connect(self._dealData)
self.loadDataThread.errorSignal.connect(self._error)
self.loadDataThread.start()
def _dealData(self, data):
"""处理数据"""
if not data or "data" not in data or "objects" not in data["data"]:
logger.error("数据格式错误")
return
logger.info(f"成功加载数据,对象数量: {len(data['data']['objects'])}")
self.objects = data["data"]["objects"]
self.clearFileCards()
for obj in self.objects:
self.addFileCard(obj["id"], obj)
def _error(self, msg):
"""处理错误"""
logger.error(f"加载数据失败: {msg}")
InfoBar.error(
"错误",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 分享文件浏览区域
class ShareLinkageSwitching(LinkageSwitchingBase):
"""文件卡片滚动区域组件"""
totalItemsSignal = pyqtSignal(int) # 信号:传递总数量
def __init__(self, parent=None):
super().__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
logger.debug(f"初始化搜索卡片滚动区域")
def addFileCard(self, fileId, obj):
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
fileCard = ShareFileCard(
obj,
self,
)
fileCard.setObjectName(f"fileCard_{fileId}")
self.fileCardsDict[fileId] = fileCard
self.layouts.addWidget(fileCard)
return fileCard
def search(self, keyword, orderBy, order, page):
"""加载数据"""
self.loadDataThread = ListShareThread(keyword, orderBy, order, page)
self.loadDataThread.listDictSignal.connect(self._dealData)
self.loadDataThread.errorSignal.connect(self._error)
self.loadDataThread.start()
def _dealData(self, data):
"""处理数据"""
if not data or "data" not in data:
logger.error("数据格式错误缺少data字段")
return
# 处理data["data"]可能是列表或字典的情况
data_content = data["data"]
if isinstance(data_content, list):
logger.warning("data['data']是列表而不是字典,将直接使用列表数据")
self.objects = data_content
elif isinstance(data_content, dict):
# 尝试从字典中获取对象列表按照Cloudreve V4 API格式处理
if "files" in data_content:
# Cloudreve V4 API 使用files字段存储文件列表
self.objects = data_content["files"]
elif "items" in data_content:
self.objects = data_content["items"]
elif "objects" in data_content:
self.objects = data_content["objects"]
else:
logger.error("数据格式错误字典中没有files、items或objects字段")
return
else:
logger.error(f"数据格式错误data['data']类型为{type(data_content).__name__},应为列表或字典")
return
logger.info(f"成功加载数据,对象数量: {len(self.objects)}")
# 尝试获取总数,如果不存在则不发送信号
if isinstance(data_content, dict) and "total" in data_content:
self.totalItemsSignal.emit(data_content["total"])
self.clearFileCards()
for obj in self.objects:
# 使用obj中可能存在的不同键名
file_id = obj.get("key", obj.get("id", None))
file_path = obj.get("path", None)
if file_id:
self.addFileCard(file_id, obj)
def _error(self, error):
"""处理错误"""
logger.error(f"加载数据失败: {error}")
InfoBar.error(
"错误",
f"加载数据失败: {error}",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)

256
app/view/login_window.py Normal file
View File

@@ -0,0 +1,256 @@
# coding:utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import (
QColor,
QIcon,
)
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
isDarkTheme,
MSFluentTitleBar,
Pivot,
PopUpAniStackedWidget,
setThemeColor,
VerticalSeparator
)
from qframelesswindow import FramelessWindow as Window
from app.core import LoginThread, RegisterThread
from app.view.widgets.login_widget import LoginWidget
from app.view.widgets.register_widget import RegisterWidget
class RegisterWindow(Window):
"""登录注册页面"""
loginSignal = pyqtSignal()
def __init__(self, parent=None):
logger.info("初始化注册窗口")
super().__init__(parent=parent)
setThemeColor("#2F80ED")
self.setTitleBar(MSFluentTitleBar(self))
self.verificationCode = ""
self.hBoxLayout = QHBoxLayout(self)
self.loginLayout = QVBoxLayout()
self.promotionalImageLabel = ImageLabel(self)
self.pivot = Pivot(self)
self.stackedWidget = PopUpAniStackedWidget(self)
self.loginWidget = LoginWidget(self)
self.registerWidget = RegisterWidget(self)
self.__initWidgets()
logger.info("注册窗口初始化完成")
def __initWidgets(self):
logger.debug("初始化注册窗口组件")
self.titleBar.maxBtn.hide()
self.titleBar.setDoubleClickEnabled(False)
self.__initLayout()
color = QColor(25, 33, 42) if isDarkTheme() else QColor(240, 244, 249)
self.setStyleSheet(f"RegisterWindow{{background: {color.name()}}}")
self.setWindowIcon(QIcon(":app/images/logo.png"))
self.setFixedSize(690, 470)
self.promotionalImageLabel.setImage(":app/images/background.png")
self.promotionalImageLabel.scaledToWidth(300)
self.pivot.addItem("LoginWidget", "登录", icon=":app/icons/login.svg")
# TODO: 内测版本隐藏注册页面
self.pivot.addItem("RegisterWidget", "注册", icon=":app/icons/register.svg")
self.pivot.setCurrentItem("LoginWidget")
self.pivot.currentItemChanged.connect(
lambda routeKey: self.stackedWidget.setCurrentWidget(
self.findChild(QWidget, routeKey)
)
)
self.loginWidget.loginButton.clicked.connect(self.login)
self.registerWidget.registerButton.clicked.connect(self.register)
self.stackedWidget.setMaximumWidth(300)
self.stackedWidget.addWidget(self.loginWidget)
self.stackedWidget.addWidget(self.registerWidget)
self.titleBar.titleLabel.setStyleSheet(
"""
QLabel{
background: transparent;
font: 14px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC';
padding: 0 4px;
color: black
}
"""
)
desktop = QApplication.screens()[0].availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
self.titleBar.raise_()
logger.debug("注册窗口组件初始化完成")
def __initLayout(self):
logger.debug("初始化注册窗口布局")
self.loginLayout.setContentsMargins(10, 40, 10, 40)
self.hBoxLayout.setContentsMargins(25, 30, 15, 30)
self.loginLayout.addWidget(
self.pivot, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.loginLayout.addSpacing(25)
self.loginLayout.addWidget(self.stackedWidget)
self.hBoxLayout.addWidget(
self.promotionalImageLabel, 0, Qt.AlignmentFlag.AlignBottom
)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(VerticalSeparator(self))
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addLayout(self.loginLayout)
logger.debug("注册窗口布局初始化完成")
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
if self.stackedWidget.currentWidget() == self.loginWidget:
self.login()
elif self.stackedWidget.currentWidget() == self.registerWidget:
self.register()
else:
super().keyPressEvent(event)
def login(self):
"""登录"""
self.loginWidget.loginButton.setEnabled(False)
userName = self.loginWidget.emailLineEdit.text()
password = self.loginWidget.passwordLineEdit.text()
captcha = self.loginWidget.verificationCodeLineEdit.text()
if not userName or not password or not captcha:
InfoBar.warning(
"提示",
"你需要填写所有项",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
return
self.loginThread = LoginThread(userName, password, captcha)
self.loginThread.successLogin.connect(self._loginSuccess)
self.loginThread.errorLogin.connect(self._loginFailed)
self.loginThread.start()
def _loginSuccess(self):
InfoBar.success(
"成功",
"登录成功,正在跳转",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
QTimer.singleShot(500, self.loginSignal.emit)
def _loginFailed(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.loginWidget.refreshVerificationCode()
self.loginWidget.verificationCodeLineEdit.clear()
self.loginWidget.loginButton.setEnabled(True)
def register(self):
"""注册"""
self.registerWidget.registerButton.setEnabled(False)
userName = self.registerWidget.emailLineEdit.text()
password = self.registerWidget.passwordLineEdit.text()
confirmPassword = self.registerWidget.confirmPasswordLineEdit.text()
captchaCode = self.registerWidget.verificationCodeLineEdit.text()
if not userName or not password or not confirmPassword:
InfoBar.warning(
"提示",
"你需要填写所有项",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.registerButton.setEnabled(True)
return
if password != confirmPassword:
InfoBar.warning(
"提示",
"两次输入的密码不一致",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)
return
self.registerThread = RegisterThread(userName, password, captchaCode)
self.registerThread.successRegister.connect(self._registerSuccess)
self.registerThread.errorRegister.connect(self._registerFailed)
self.registerThread.start()
def _registerSuccess(self):
InfoBar.info(
"成功",
"注册成功,请前往邮箱激活账号",
Qt.Orientation.Horizontal,
True,
-1,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.stackedWidget.setCurrentWidget(self.loginWidget)
self.loginWidget.emailLineEdit.setText(self.registerWidget.emailLineEdit.text())
self.registerWidget.emailLineEdit.clear()
self.registerWidget.passwordLineEdit.clear()
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)
def _registerFailed(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.refreshVerificationCode()
self.registerWidget.verificationCodeLineEdit.clear()
self.registerWidget.passwordLineEdit.clear()
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.emailLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)

201
app/view/main_window.py Normal file
View File

@@ -0,0 +1,201 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import QSize
from PyQt6.QtGui import QColor, QIcon
from PyQt6.QtWidgets import QApplication, QWidget
from qfluentwidgets import NavigationAvatarWidget, NavigationItemPosition, SplashScreen
from app.core import cfg, qconfig, userConfig, GetUserAvatarThread,lang,signalBus
from app.view.app_info_interface import AppInfoInterface
from app.view.ownFiled_interface import OwnFiledInterface
from app.view.setting_interface import SettingInterface
from app.view.storagespace_interface import StoragespaceInterface
from app.view.task_interface import TaskInterface
from app.view.widgets.custom_fluent_window import CustomFluentWindow
from app.view.widgets.preview_box import OptimizedPreviewBox, PreviewTextBox
from app.view.widgets.share_folder_messageBox import ShareFolderMessageBox
class MainWindow(CustomFluentWindow):
def __init__(self):
logger.info("开始初始化主窗口")
super().__init__()
self.initWindow()
self.ownFiledInterface = OwnFiledInterface(self)
self.storagespaceInterface = StoragespaceInterface(self)
self.taskInterface = TaskInterface(self)
self.appInfoInterface = AppInfoInterface(self)
self.connectSignalToSlot()
self.initNavigation()
logger.info("主窗口初始化完成")
def connectSignalToSlot(self):
logger.debug("连接信号和槽")
signalBus.micaEnableChanged.connect(self.setMicaEffectEnabled)
# 预览信号连接
signalBus.imagePreviewSignal.connect(self.imagePreview)
signalBus.txtPreviewSignal.connect(self.txtPreview)
# 背景信号连接
signalBus.backgroundChanged.connect(self.updateBackground)
signalBus.opacityChanged.connect(self.updateBackground)
# 下载上传任务信号连接
signalBus.addUploadFileTask.connect(self.addUploadFileTask)
signalBus.addDownloadFileTask.connect(self.addDownloadFileTask)
signalBus.shareFolderViewSignal.connect(self.shareFolderView)
# 语言变更信号连接
signalBus.languageChanged.connect(self.updateNavigation)
def updateNavigation(self):
# 更新导航项文本
self.navigationInterface.setItemText(self.ownFiledInterface, lang("我的文件"))
self.navigationInterface.setItemText(
self.storagespaceInterface, lang("存储配额")
)
self.navigationInterface.setItemText(self.taskInterface, lang("任务管理"))
self.navigationInterface.setItemText(self.appInfoInterface, lang("应用信息"))
...
def initNavigation(self):
self.navigationInterface.setAcrylicEnabled(True)
self.navigationInterface.setExpandWidth(200)
logger.info("开始初始化导航界面")
self.addSubInterface(
self.ownFiledInterface,
QIcon(":app/icons/Myfile.svg"),
lang("我的文件"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.storagespaceInterface,
QIcon(":app/icons/Storage.svg"),
lang("存储配额"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.taskInterface,
QIcon(":app/icons/Task.svg"),
lang("任务管理"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.appInfoInterface,
QIcon(":app/icons/Application.svg"),
lang("应用信息"),
NavigationItemPosition.BOTTOM,
)
# 创建默认头像widget先使用本地默认头像
self.avatarWidget = NavigationAvatarWidget(
userConfig.userName, ":app/images/logo.png"
)
self.navigationInterface.addWidget(
routeKey="settingInterface",
widget=self.avatarWidget,
position=NavigationItemPosition.BOTTOM,
onClick=self.setPersonalInfoWidget,
)
self.settingInterface = SettingInterface(self)
self.stackedWidget.addWidget(self.settingInterface)
self.splashScreen.finish()
logger.info("导航界面初始化完成")
self.avatarThread = GetUserAvatarThread("l")
self.avatarThread.avatarPixmap.connect(self.onAvatarDownloaded)
self.avatarThread.start()
def shareFolderView(self, _id):
w = ShareFolderMessageBox(_id, self)
if w.exec():
...
def addUploadFileTask(self, filePath):
logger.info(f"添加上传文件任务: {filePath}")
self.taskInterface.uploadScrollWidget.addUploadTask(filePath)
self.stackedWidget.setCurrentWidget(self.taskInterface)
self.navigationInterface.setCurrentItem("taskInterface")
self.taskInterface._changePivot("Upload")
def addDownloadFileTask(self, suffix, fileName, _id):
logger.info(f"添加下载文件任务: {fileName}")
self.taskInterface.downloadScrollWidget.addDownloadTask(suffix, fileName, _id)
self.stackedWidget.setCurrentWidget(self.taskInterface)
self.navigationInterface.setCurrentItem("taskInterface")
self.taskInterface._changePivot("Download")
def setPersonalInfoWidget(self):
self.stackedWidget.setCurrentWidget(
self.stackedWidget.findChild(QWidget, "settingInterface")
)
self.navigationInterface.setCurrentItem("settingInterface")
def onAvatarDownloaded(self, pixmap):
userConfig.setUserAvatarPixmap(pixmap)
self.avatarWidget.setAvatar(pixmap)
self.settingInterface.updateAvatar(pixmap)
def initWindow(self):
logger.info("开始初始化窗口设置")
self.resize(960, 780)
self.setMinimumWidth(760)
self.setWindowIcon(QIcon(":app/images/logo.png"))
self.setWindowTitle(lang("LeonPan"))
logger.debug("已设置窗口基本属性")
self.setCustomBackgroundColor(QColor(240, 244, 249), QColor(32, 32, 32))
self.setMicaEffectEnabled(cfg.get(cfg.micaEnabled))
logger.debug("已设置窗口背景和Mica效果")
self.setBackgroundImage(
qconfig.get(cfg.customBackground), qconfig.get(cfg.customOpactity)
) # create splash screen
# 使用自定义的背景设置方法
# create splash screen
self.splashScreen = SplashScreen(self.windowIcon(), self)
self.splashScreen.setIconSize(QSize(106, 106))
self.splashScreen.raise_()
logger.debug("已创建并设置启动屏幕")
desktop = QApplication.primaryScreen().availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
logger.debug("已移动窗口到屏幕中心")
self.show()
QApplication.processEvents()
logger.info("窗口初始化完成并显示")
def resizeEvent(self, e):
super().resizeEvent(e)
if hasattr(self, "splashScreen"):
self.splashScreen.resize(self.size())
# 窗口大小改变时更新背景
def imagePreview(self, _id):
# 使用V4 API进行预览
url = f"/file/preview/{_id}"
self.previewBox = OptimizedPreviewBox(self, url)
if self.previewBox.exec():
pass
def txtPreview(self, _id):
# 使用V4 API获取内容
url = f"/file/content/{_id}"
self.previewBox = PreviewTextBox(self, url, _id)
if self.previewBox.exec():
pass
def updateBackground(self):
"""更新窗口背景"""
self.setBackgroundImage(
qconfig.get(cfg.customBackground), qconfig.get(cfg.customOpactity)
)

View File

@@ -0,0 +1,138 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
VerticalSeparator,
)
from app.core import signalBus
from app.view.widgets.ownfile_scroll_widget import OwnFileScrollWidget
from app.view.widgets.ownFiled_widgets import SearchWidget, TagWidget
from app.view.widgets.share_search_widgets import ShareSearchScrollWidget
from app.view.widgets.ware_search_widgets import WareSearchScrollWidget
class OwnFiledInterface(QWidget):
"""主文件管理界面"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("OwnFiledInterface")
self.currentPath = "/"
logger.debug("初始化主文件管理界面")
# 初始化组件
self.searchWidget = SearchWidget(self)
self.tagWidget = TagWidget(self)
self.ownFileScrollWidget = OwnFileScrollWidget(self)
self.wareSearchScrollWidget = WareSearchScrollWidget(self)
self.wareSearchScrollWidget.hide()
self.shareSearchScrollWidget = ShareSearchScrollWidget(self)
self.shareSearchScrollWidget.hide()
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
logger.debug("设置主文件管理界面UI")
# 设置主布局
self.initLayout()
def initLayout(self):
"""初始化布局"""
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(10, 0, 10, 0)
# 创建顶部布局
self.topLayout = QHBoxLayout()
self.topLayout.setContentsMargins(0, 0, 0, 0)
self.topLayout.setAlignment(
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignVCenter
)
self.topLayout.addWidget(
self.tagWidget, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.topLayout.addSpacing(20)
self.verticalSeparator = VerticalSeparator(self)
self.verticalSeparator.setFixedHeight(25)
self.topLayout.addWidget(self.verticalSeparator)
self.topLayout.addSpacing(20)
self.topLayout.addWidget(self.searchWidget, 0, Qt.AlignmentFlag.AlignTop)
# 添加所有组件到主布局
self.vBoxLayout.addLayout(self.topLayout)
self.vBoxLayout.addWidget(self.ownFileScrollWidget)
self.vBoxLayout.addWidget(self.wareSearchScrollWidget)
self.vBoxLayout.addWidget(self.shareSearchScrollWidget)
def tagSearch(self, types, keyword):
self.wareSearchScrollWidget.show()
self.ownFileScrollWidget.hide()
self.wareSearchScrollWidget.wareSearch(types, keyword)
def search(self):
keyword = self.searchWidget.searchLineEdit.text()
searchType = self.searchWidget.searchButton.text()
if keyword == "" or keyword == ".":
InfoBar.warning(
"注意",
"搜索内容为空",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
return
if searchType == "仓内搜索":
self.wareSearchScrollWidget.show()
self.ownFileScrollWidget.hide()
self.shareSearchScrollWidget.hide()
self.wareSearchScrollWidget.wareSearch("keyword", keyword)
self.tagWidget.tagScrollArea.clearChecked()
elif searchType == "站内搜索":
self.wareSearchScrollWidget.hide()
self.ownFileScrollWidget.hide()
self.shareSearchScrollWidget.show()
self.shareSearchScrollWidget.shareSearch(keyword, 1)
self.tagWidget.tagScrollArea.clearChecked()
def returnLinkageSwitchingPage(self):
self.wareSearchScrollWidget.hide()
self.shareSearchScrollWidget.hide()
self.ownFileScrollWidget.show()
self.tagWidget.tagScrollArea.clearChecked()
self.searchWidget.searchLineEdit.clear()
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接主文件管理界面信号")
# 连接搜索信号
signalBus.dirOpenSignal.connect(
lambda x: self.ownFileScrollWidget.onChangeDir(x)
)
signalBus.refreshFolderListSignal.connect(
self.ownFileScrollWidget.refreshCurrentDirectory
)
self.wareSearchScrollWidget.returnSignal.connect(
self.returnLinkageSwitchingPage
)
self.shareSearchScrollWidget.returnSignal.connect(
self.returnLinkageSwitchingPage
)
self.searchWidget.searchButton.clicked.connect(self.search)
self.tagWidget.tagScrollArea.tagClicked.connect(self.tagSearch)

View File

@@ -0,0 +1,363 @@
# coding:utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QFileDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
AvatarWidget,
BodyLabel,
GroupHeaderCardWidget,
HyperlinkLabel,
InfoBarPosition,
LineEdit,
PushButton,
ScrollArea,
Slider,
SubtitleLabel,
VerticalSeparator,
)
from qfluentwidgets import InfoBar, MessageBoxBase
from app.core import cfg, lang, qconfig, signalBus, UserAvatarUpdateThread, userConfig, UserNickNameUpdateThread
from app.view.widgets.custom_background_messageBox import CustomBgMessageBox
class NickNameEdit(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent)
self.titleLabel = SubtitleLabel(lang("修改昵称"), self)
self.lineEdit = LineEdit(self)
self.widget.setMinimumWidth(200)
self.viewLayout.addWidget(
self.titleLabel, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.viewLayout.addWidget(self.lineEdit)
self.yesButton.setText("确定")
self.cancelButton.setText("取消")
class BasicInformationSettingCard(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("basicInformationSettingCard")
self.setTitle(lang("用户信息"))
self.nickNameEdit = PushButton(lang("修改昵称"), self)
self.nickNameEdit.clicked.connect(self._changeNickName)
self.addGroup(":app/icons/Nickname.svg", userConfig.userId, "UID", QLabel(self))
self.addGroup(
":app/icons/Nickname.svg",
userConfig.userName,
lang("用户昵称"),
self.nickNameEdit,
)
self.addGroup(
":app/icons/Email.svg",
userConfig.userEmail,
lang("电子邮箱"),
QLabel(self),
)
self.addGroup(
":app/icons/Group.svg",
userConfig.userGroup,
lang("当前用户组"),
QLabel(self),
)
self.addGroup(
":app/icons/Score.svg", userConfig.userScore, "积分", QLabel(self)
)
self.addGroup(
":app/icons/Date.svg",
userConfig.userCreatedTime,
lang("用户注册时间"),
QLabel(self),
)
def _changeNickName(self):
w = NickNameEdit(self.window())
def _onNickNameSuccess():
self.groupWidgets[2].setTitle(newNickName)
InfoBar.success(
lang("修改成功"),
lang("昵称修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def _onNickNameError(error):
InfoBar.error(
lang("修改失败"),
error,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
if w.exec():
newNickName = w.lineEdit.text()
self.nickNameServiceThread = UserNickNameUpdateThread(newNickName)
self.nickNameServiceThread.successUpdate.connect(_onNickNameSuccess)
self.nickNameServiceThread.errorUpdate.connect(_onNickNameError)
self.nickNameServiceThread.start()
class SoftWardSettingWidget(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("软件设置")
self.downloadSavePathButton = PushButton(lang("选择保存路径"), self)
self.downloadSavePathButton.clicked.connect(self._chooseDownloadSavePath)
self.addGroup(
":app/icons/SavePath.svg",
qconfig.get(cfg.downloadSavePath),
lang("选择下载保存路径"),
self.downloadSavePathButton,
)
def _chooseDownloadSavePath(self):
folder_path = QFileDialog.getExistingDirectory(self, lang("选择文件夹"))
if folder_path:
print(f"选择的文件夹路径是: {folder_path}")
qconfig.set(cfg.downloadSavePath, folder_path)
self.groupWidgets[0].setTitle(folder_path)
InfoBar.success(
lang("修改成功"),
lang("下载保存路径修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
class ThemeSettingWidget(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle(lang("背景图片设置"))
self.officialBackgroundButton = PushButton(lang("官方背景图"), self)
self.officialBackgroundButton.clicked.connect(self.officialBackground)
self.customBackgroundButton = PushButton(lang("选择自定义背景"), self)
self.customBackgroundButton.clicked.connect(self.customBackground)
self.opacitySlider = Slider(Qt.Orientation.Horizontal, self)
self.opacitySlider.setRange(0, 10)
self.opacitySlider.setFixedWidth(100)
self.opacitySlider.setValue(int(qconfig.get(cfg.customOpactity) * 10))
self.opacitySlider.valueChanged.connect(self.setOpacity)
self.addGroup(
":app/icons/BgImage.svg",
lang("官方预设背景图片"),
lang("选择背景图片"),
self.officialBackgroundButton,
)
self.addGroup(
":app/icons/BgImage.svg",
lang("自定义背景图片"),
lang("选择自定义图片,选择后请不要更改图片位置"),
self.customBackgroundButton,
)
self.addGroup(
":app/icons/Opacity.svg",
lang("图片背景透明度"),
lang("设置图片背景透明度"),
self.opacitySlider,
)
def officialBackground(self):
w = CustomBgMessageBox(self.window())
if w.exec():
index = w.returnImage()
qconfig.set(cfg.customBackground, f"app\\resource\\images\\bg{index}.png")
signalBus.backgroundChanged.emit()
def customBackground(self):
file_name, _ = QFileDialog.getOpenFileName(
self,
"选择背景",
"",
"Image Files (*.png *.jpg *.jpeg *.bmp);;All Files (*)",
)
qconfig.set(cfg.customBackground, file_name)
signalBus.backgroundChanged.emit()
def setOpacity(self, opacity):
qconfig.set(cfg.customOpactity, opacity / 10)
signalBus.opacityChanged.emit()
class AgreementLabelWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.privacyPolicy = HyperlinkLabel(
lang("隐私协议"),
self,
)
self.userAgreement = HyperlinkLabel(
lang("用户协议"),
self,
)
self.verticalSeparator = VerticalSeparator(self)
self.verticalSeparator.setFixedHeight(15)
self.privacyPolicy.setUrl("https://mp.miaostars.com/ysxy")
self.userAgreement.setUrl("https://mp.miaostars.com/xy")
self.hBoxLayout.addWidget(self.privacyPolicy, 0, Qt.AlignmentFlag.AlignCenter)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(self.verticalSeparator)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.userAgreement,
)
class SettingInterface(ScrollArea):
"""Setting interface"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.avatarWidget = AvatarWidget(self.scrollWidget)
self.basicInformationSettingCard = BasicInformationSettingCard(
self.scrollWidget
)
self.softWardSettingWidget = SoftWardSettingWidget(self.scrollWidget)
self.themeSettingWidget = ThemeSettingWidget(self.scrollWidget)
self.agreementLabelWidget = AgreementLabelWidget(self.scrollWidget)
self.infoLabel = BodyLabel(
"增值电信业务经营许可证B1-20191399鄂ICP备2025132158号 \n ©2025 LeonPan \n 武汉喵星创想互联网科技有限公司",
self.scrollWidget,
)
self.__initWidget()
def __initWidget(self):
self.resize(1000, 800)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.setViewportMargins(0, 100, 0, 20)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("settingInterface")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.avatarWidget.setImage(QPixmap(":app/images/logo.png"))
# Connect click event to open file dialog for avatar selection
self.avatarWidget.clicked.connect(self._openAvatarDialog)
self.infoLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.infoLabel.setStyleSheet("color:gray;font-size:12px;")
# initialize layout
self.__initLayout()
self._connectSignalToSlot()
def __initLayout(self):
# add setting card group to layout
# self.expandLayout.setSpacing(28)
self.expandLayout.addWidget(
self.avatarWidget,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.expandLayout.addWidget(
self.basicInformationSettingCard, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.softWardSettingWidget, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.themeSettingWidget, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.agreementLabelWidget, 1, Qt.AlignmentFlag.AlignBottom
)
self.expandLayout.addSpacing(5)
self.expandLayout.addWidget(self.infoLabel, 0, Qt.AlignmentFlag.AlignBottom)
def _connectSignalToSlot(self): ...
def _openAvatarDialog(self):
"""Open file dialog to select avatar image"""
fileDialog = QFileDialog(self)
fileDialog.setWindowTitle(lang("选择图片"))
fileDialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
fileDialog.setFileMode(QFileDialog.FileMode.ExistingFile)
fileDialog.setNameFilter("图片文件 (*.png *.jpg *.jpeg *.bmp)")
if fileDialog.exec():
file_path = fileDialog.selectedFiles()[0]
try:
image = QImage(file_path)
if not image.isNull():
self.changeAvatar(image)
# Update the avatar widget with the new image
self.avatarWidget.setImage(QPixmap.fromImage(image))
except Exception as e:
InfoBar.error(
lang("选择失败"),
str(e),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def updateAvatar(self, avatarPixmap):
self.avatarWidget.setImage(avatarPixmap)
def changeAvatar(self, image):
def avatarUpdateError(error):
InfoBar.error(
lang("修改失败"),
error,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.avatarWidget.setEnabled(True)
def avatarUpdateSuccess():
InfoBar.success(
lang("修改成功"),
lang("头像修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.avatarWidget.setEnabled(True)
self.avatarUpdateThread = UserAvatarUpdateThread(image)
self.avatarUpdateThread.successUpdate.connect(avatarUpdateSuccess)
self.avatarUpdateThread.errorUpdate.connect(avatarUpdateError)
self.avatarUpdateThread.start()
self.avatarWidget.setEnabled(False)

View File

@@ -0,0 +1,76 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea, TitleLabel
from app.core import GetPackThread, lang
from app.view.components.gb_information_card import GbInformationCard
class NumInformationWidget(QWidget):
def __init__(self, parent=None):
super(NumInformationWidget, self).__init__(parent)
self.hBoxLayout = QHBoxLayout(self)
self.basicSizeCard = GbInformationCard(0, lang("用户组基础容量"), self)
self.packSizeCard = GbInformationCard(0, lang("有效容量包附加附加容量"), self)
self.usedSizeCard = GbInformationCard(0, lang("已使用容量"), self)
self.totalSizeCard = GbInformationCard(0, lang("总容量"), self)
self.hBoxLayout.setSpacing(10)
self.hBoxLayout.addWidget(self.basicSizeCard)
self.hBoxLayout.addWidget(self.packSizeCard)
self.hBoxLayout.addWidget(self.usedSizeCard)
self.hBoxLayout.addWidget(self.totalSizeCard)
class StoragespaceInterface(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.widgets = QWidget()
self.vBoxLayout = QVBoxLayout(self.widgets)
self.firstLoad = True
self.titleLabel = TitleLabel(lang("存储配额"), self)
self.numInformationWidget = NumInformationWidget(self)
self.__initWidget()
self._loadUserCustomStorage()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.setObjectName("storageInterface")
self.widgets.setObjectName("scrollWidgets")
self.widgets.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.titleLabel.setContentsMargins(10, 5, 5, 5)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addWidget(self.titleLabel)
self.vBoxLayout.addWidget(self.numInformationWidget)
def _loadUserCustomStorage(self):
if not self.firstLoad:
return
self.packThread = GetPackThread()
self.packThread.storageDictSignal.connect(self._successGetPack)
self.packThread.start()
def _successGetPack(self, datas):
self.packData = datas["data"]
self.firstLoad = False
self.numInformationWidget.packSizeCard.updateValue(self.packData["pack"])
self.numInformationWidget.basicSizeCard.updateValue(self.packData["base"])
self.numInformationWidget.usedSizeCard.updateValue(self.packData["used"])
self.numInformationWidget.totalSizeCard.updateValue(self.packData["total"])
logger.success("用户配额加载,已刷新")

View File

@@ -0,0 +1,54 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import PopUpAniStackedWidget, SegmentedWidget, TitleLabel
from app.core import lang
from app.view.widgets.download_widget import DownloadScrollWidget
from app.view.widgets.upload_widget import UploadScrollWidget
class TaskInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.vBoxLayout = QVBoxLayout(self)
self.titleLabel = TitleLabel(lang("任务管理"), self)
self.pivot = SegmentedWidget(self)
self.stackedWidget = PopUpAniStackedWidget(self)
self.uploadScrollWidget = UploadScrollWidget(self)
self.downloadScrollWidget = DownloadScrollWidget(self)
self.__initWidget()
def __initWidget(self):
self.setObjectName("taskInterface")
self.titleLabel.setContentsMargins(10, 5, 5, 5)
self.pivot.setMinimumWidth(200)
self.pivot.addItem("Upload", lang("文件上传"))
self.pivot.addItem("Download", lang("文件下载"))
self.pivot.setCurrentItem("Upload")
self.pivot.currentItemChanged.connect(self._changePivot)
self.stackedWidget.addWidget(self.uploadScrollWidget)
self.stackedWidget.addWidget(self.downloadScrollWidget)
self.__initLayout()
def _changePivot(self, routeKey):
self.stackedWidget.setCurrentWidget(
self.stackedWidget.findChild(QWidget, routeKey + "ScrollWidget")
)
self.pivot.setCurrentItem(routeKey)
def __initLayout(self):
self.vBoxLayout.addWidget(
self.titleLabel, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(5)
self.vBoxLayout.addWidget(
self.pivot, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(5)
self.vBoxLayout.addWidget(self.stackedWidget)

View File

@@ -0,0 +1,45 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import pyqtSignal
from qfluentwidgets import LineEdit, MessageBoxBase, SubtitleLabel
from app.core import AddTagThread,lang
class AddTagMessageBox(MessageBoxBase):
successAddTagSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
def __init__(self, parent=None):
super().__init__(parent=parent)
self.widget.setMinimumWidth(250)
self.titleLabel = SubtitleLabel(lang("添加标签"), self)
self.nameLineEdit = LineEdit(self)
self.nameLineEdit.setPlaceholderText(lang("标签名称"))
self.expressionLineEdit = LineEdit(self)
self.expressionLineEdit.setPlaceholderText(lang("标签通配符"))
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.nameLineEdit)
self.viewLayout.addWidget(self.expressionLineEdit)
self.yesButton.setText(lang("添加"))
self.yesButton.clicked.connect(self.addTag)
self.cancelButton.setText(lang("取消"))
def addTag(self):
name = self.nameLineEdit.text()
expression = self.expressionLineEdit.text()
if not name or not expression:
return
self.addTagThread = AddTagThread(name, expression)
self.addTagThread.successSignal.connect(self.onAddTagSuccess)
self.addTagThread.errorSignal.connect(self.onAddTagError)
self.addTagThread.start()
def onAddTagSuccess(self, name, result):
self.accept()
self.successAddTagSignal.emit(name, result)
def onAddTagError(self, name, error_msg):
logger.error(f"添加标签失败: {name} - {error_msg}")

View File

@@ -0,0 +1,23 @@
# coding: utf-8
from pathlib import Path
from qfluentwidgets import HorizontalFlipView, MessageBoxBase, SubtitleLabel
class CustomBgMessageBox(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.titleLabel = SubtitleLabel(parent=self)
self.titleLabel.setText("内设壁纸")
self.viewLayout.addWidget(self.titleLabel)
self.imageChoice = HorizontalFlipView(parent=self)
self.imageChoice.setBorderRadius(8)
for i in range(0, 6):
self.imageChoice.addImage(f"app\\resource\\images\\bg{i}.png")
self.viewLayout.addWidget(self.imageChoice)
self.yesButton.setText("确定")
self.cancelButton.hide()
def returnImage(self):
return self.imageChoice.currentIndex()

View File

@@ -0,0 +1,363 @@
# coding:utf-8
import sys
from typing import Union
from PyQt6.QtCore import QRect, QSize, Qt
from PyQt6.QtGui import QColor, QIcon, QPainter, QPixmap
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets.common.animation import BackgroundAnimationWidget
from qfluentwidgets.common.config import qconfig
from qfluentwidgets.common.icon import FluentIconBase
from qfluentwidgets.common.router import qrouter
from qfluentwidgets.common.style_sheet import (
FluentStyleSheet,
isDarkTheme,
)
from qfluentwidgets.components.navigation import (
NavigationInterface,
NavigationItemPosition,
NavigationTreeWidget,
)
from qfluentwidgets.components.widgets.frameless_window import FramelessWindow
from qframelesswindow import TitleBar, TitleBarBase
from app.view.widgets.stacked_widget import StackedWidget
class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
"""Fluent window base class"""
def __init__(self, parent=None):
self._isMicaEnabled = False
self._lightBackgroundColor = QColor(240, 244, 249)
self._darkBackgroundColor = QColor(32, 32, 32)
self._backgroundPixmap = None # 存储背景图片
self._backgroundOpacity = 1.0 # 背景图片不透明度
super().__init__(parent=parent)
self.hBoxLayout = QHBoxLayout(self)
self.stackedWidget = StackedWidget(self)
self.navigationInterface = None
# initialize layout
self.hBoxLayout.setSpacing(0)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
FluentStyleSheet.FLUENT_WINDOW.apply(self.stackedWidget)
# enable mica effect on win11
self.setMicaEffectEnabled(True)
# show system title bar buttons on macOS
if sys.platform == "darwin":
self.setSystemTitleBarButtonVisible(True)
qconfig.themeChangedFinished.connect(self._onThemeChangedFinished)
def addSubInterface(
self,
interface: QWidget,
icon: Union[FluentIconBase, QIcon, str],
text: str,
position=NavigationItemPosition.TOP,
):
"""add sub interface"""
raise NotImplementedError
def removeInterface(self, interface: QWidget, isDelete=False):
"""remove sub interface
Parameters
----------
interface: QWidget
sub interface to be removed
isDelete: bool
whether to delete the sub interface
"""
raise NotImplementedError
def switchTo(self, interface: QWidget):
self.stackedWidget.setCurrentWidget(interface, popOut=False)
def _onCurrentInterfaceChanged(self, index: int):
widget = self.stackedWidget.widget(index)
self.navigationInterface.setCurrentItem(widget.objectName())
qrouter.push(self.stackedWidget, widget.objectName())
self._updateStackedBackground()
def _updateStackedBackground(self):
isTransparent = self.stackedWidget.currentWidget().property(
"isStackedTransparent"
)
if bool(self.stackedWidget.property("isTransparent")) == isTransparent:
return
self.stackedWidget.setProperty("isTransparent", isTransparent)
self.stackedWidget.setStyle(QApplication.style())
def setCustomBackgroundColor(self, light, dark):
"""set custom background color
Parameters
----------
light, dark: QColor | Qt.GlobalColor | str
background color in light/dark theme mode
"""
self._lightBackgroundColor = QColor(light)
self._darkBackgroundColor = QColor(dark)
self._updateBackgroundColor()
def setBackgroundImage(self, imagePath: str, opacity: float = 1.0):
"""设置背景图片
Parameters
----------
imagePath: str
背景图片路径
opacity: float
背景图片不透明度范围0.0-1.0
"""
self._backgroundPixmap = QPixmap(imagePath)
if self._backgroundPixmap.isNull():
print(f"无法加载背景图片: {imagePath}")
return
self._backgroundOpacity = max(0.0, min(1.0, opacity)) # 确保在0-1范围内
# 设置StackedWidget为透明以便显示背景图片
self.stackedWidget.setProperty("isTransparent", True)
self.stackedWidget.setStyle(QApplication.style())
self.update() # 触发重绘
def removeBackgroundImage(self):
"""移除背景图片"""
self._backgroundPixmap = None
# 恢复StackedWidget的默认背景
self.stackedWidget.setProperty("isTransparent", False)
self.stackedWidget.setStyle(QApplication.style())
self.update()
def _normalBackgroundColor(self):
if not self.isMicaEffectEnabled():
return (
self._darkBackgroundColor
if isDarkTheme()
else self._lightBackgroundColor
)
return QColor(0, 0, 0, 0)
def _onThemeChangedFinished(self):
if self.isMicaEffectEnabled():
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
def paintEvent(self, e):
# 创建绘制器
painter = QPainter(self)
# 如果有背景图片,先绘制背景图片
if self._backgroundPixmap and not self._backgroundPixmap.isNull():
# 设置不透明度
painter.setOpacity(self._backgroundOpacity)
# 缩放图片以适应窗口大小
scaled_pixmap = self._backgroundPixmap.scaled(
self.size(), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation
)
painter.drawPixmap(0, 0, scaled_pixmap)
# 然后调用父类的绘制方法
super().paintEvent(e)
def setMicaEffectEnabled(self, isEnabled: bool):
"""set whether the mica effect is enabled, only available on Win11"""
if sys.platform != "win32" or sys.getwindowsversion().build < 22000:
return
self._isMicaEnabled = isEnabled
if isEnabled:
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
# 启用Mica效果时移除背景图片
self.removeBackgroundImage()
else:
self.windowEffect.removeBackgroundEffect(self.winId())
self.setBackgroundColor(self._normalBackgroundColor())
def isMicaEffectEnabled(self):
return self._isMicaEnabled
def systemTitleBarRect(self, size: QSize) -> QRect:
"""Returns the system title bar rect, only works for macOS
Parameters
----------
size: QSize
original system title bar rect
"""
return QRect(
size.width() - 75, 0 if self.isFullScreen() else 9, 75, size.height()
)
def setTitleBar(self, titleBar):
super().setTitleBar(titleBar)
# hide title bar buttons on macOS
if (
sys.platform == "darwin"
and self.isSystemButtonVisible()
and isinstance(titleBar, TitleBarBase)
):
titleBar.minBtn.hide()
titleBar.maxBtn.hide()
titleBar.closeBtn.hide()
class FluentTitleBar(TitleBar):
"""Fluent title bar"""
def __init__(self, parent):
super().__init__(parent)
self.setFixedHeight(48)
self.hBoxLayout.removeWidget(self.minBtn)
self.hBoxLayout.removeWidget(self.maxBtn)
self.hBoxLayout.removeWidget(self.closeBtn)
# add window icon
self.iconLabel = QLabel(self)
self.iconLabel.setFixedSize(18, 18)
self.hBoxLayout.insertWidget(
0, self.iconLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
self.window().windowIconChanged.connect(self.setIcon)
# add title label
self.titleLabel = QLabel(self)
self.hBoxLayout.insertWidget(
1, self.titleLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
self.titleLabel.setObjectName("titleLabel")
self.window().windowTitleChanged.connect(self.setTitle)
self.vBoxLayout = QVBoxLayout()
self.buttonLayout = QHBoxLayout()
self.buttonLayout.setSpacing(0)
self.buttonLayout.setContentsMargins(0, 0, 0, 0)
self.buttonLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.buttonLayout.addWidget(self.minBtn)
self.buttonLayout.addWidget(self.maxBtn)
self.buttonLayout.addWidget(self.closeBtn)
self.vBoxLayout.addLayout(self.buttonLayout)
self.vBoxLayout.addStretch(1)
self.hBoxLayout.addLayout(self.vBoxLayout, 0)
FluentStyleSheet.FLUENT_WINDOW.apply(self)
def setTitle(self, title):
self.titleLabel.setText(title)
self.titleLabel.adjustSize()
def setIcon(self, icon):
self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18))
class CustomFluentWindow(CustomFluentWindowBase):
"""Fluent window"""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitleBar(FluentTitleBar(self))
self.navigationInterface = NavigationInterface(self, showReturnButton=True)
self.widgetLayout = QHBoxLayout()
# initialize layout
self.hBoxLayout.addWidget(self.navigationInterface)
self.hBoxLayout.addLayout(self.widgetLayout)
self.hBoxLayout.setStretchFactor(self.widgetLayout, 1)
self.widgetLayout.addWidget(self.stackedWidget)
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_)
self.titleBar.raise_()
def addSubInterface(
self,
interface: QWidget,
icon: Union[FluentIconBase, QIcon, str],
text: str,
position=NavigationItemPosition.TOP,
parent=None,
isTransparent=False,
) -> NavigationTreeWidget:
"""add sub interface, the object name of `interface` should be set already
before calling this method
Parameters
----------
interface: QWidget
the subinterface to be added
icon: FluentIconBase | QIcon | str
the icon of navigation item
text: str
the text of navigation item
position: NavigationItemPosition
the position of navigation item
parent: QWidget
the parent of navigation item
isTransparent: bool
whether to use transparent background
"""
if not interface.objectName():
raise ValueError("The object name of `interface` can't be empty string.")
if parent and not parent.objectName():
raise ValueError("The object name of `parent` can't be empty string.")
interface.setProperty("isStackedTransparent", isTransparent)
self.stackedWidget.addWidget(interface)
# add navigation item
routeKey = interface.objectName()
item = self.navigationInterface.addItem(
routeKey=routeKey,
icon=icon,
text=text,
onClick=lambda: self.switchTo(interface),
position=position,
tooltip=text,
parentRouteKey=parent.objectName() if parent else None,
)
# initialize selected item
if self.stackedWidget.count() == 1:
self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged)
self.navigationInterface.setCurrentItem(routeKey)
qrouter.setDefaultRouteKey(self.stackedWidget, routeKey)
self._updateStackedBackground()
return item
def removeInterface(self, interface, isDelete=False):
self.navigationInterface.removeWidget(interface.objectName())
self.stackedWidget.removeWidget(interface)
interface.hide()
if isDelete:
interface.deleteLater()
def resizeEvent(self, e):
self.titleBar.move(46, 0)
self.titleBar.resize(self.width() - 46, self.titleBar.height())

View File

@@ -0,0 +1,42 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea
from app.view.components.file_deal_cards import DownloadCard
class DownloadScrollWidget(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
self.__initWidget()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("DownloadScrollWidget")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
def addDownloadTask(self, suffix, fileName, _id):
self.vBoxLayout.addWidget(
DownloadCard(
suffix,
fileName,
_id,
self.scrollWidget,
)
)

View File

@@ -0,0 +1,117 @@
from loguru import logger
from PyQt6.QtCore import QRegularExpression, Qt, pyqtSignal
from PyQt6.QtGui import (
QRegularExpressionValidator,
)
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
CheckBox,
LineEdit,
PasswordLineEdit,
PushButton,
)
from app.core import CaptchaThread,cfg, qconfig
class LoginWidget(QWidget):
loginSignal = pyqtSignal(dict)
def __init__(self, parent=None):
logger.debug("初始化登录组件")
super().__init__(parent)
self.setObjectName("LoginWidget")
self.setWindowTitle("LeonPan")
self.emailLineEdit = LineEdit(self)
self.emailLineEdit.setPlaceholderText("请输入邮箱")
self.passwordLineEdit = PasswordLineEdit(self)
self.passwordLineEdit.setPlaceholderText("请输入密码")
self.rememberMeCheckBox = CheckBox("记住我", self)
self.rememberMeCheckBox.checkStateChanged.connect(
lambda: qconfig.set(cfg.rememberMe, self.rememberMeCheckBox.isChecked())
)
self.loginButton = PushButton("登录", self)
self.loginButton.setDisabled(False)
self.verificationCodeLabel = QLabel(self)
self.verificationCodeLabel.setFixedSize(120, 35)
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
self.verificationCodeLabel.mousePressEvent = (
self.refreshVerificationCode
) # 绑定点击事件
self.verificationCodeLineEdit = LineEdit(self)
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
self.verificationLayout = QHBoxLayout()
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
self.verificationLayout.addWidget(self.verificationCodeLabel)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addSpacing(10)
self.vBoxLayout.addWidget(self.emailLineEdit)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.passwordLineEdit)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addLayout(self.verificationLayout)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(
self.rememberMeCheckBox, 0, Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.loginButton)
email_regex = QRegularExpression(
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
# TODO: 内测时用的邮箱匹配
# email_regex = QRegularExpression(r"^[a-zA-Z0-9_.+-]+@miaostars\.cn$")
validator = QRegularExpressionValidator(email_regex, self)
self.emailLineEdit.setValidator(validator)
self.emailLineEdit.textChanged.connect(self.checkEmail)
self.refreshVerificationCode()
self.rememberMe()
logger.debug("登录组件初始化完成")
def rememberMe(self):
logger.debug("检查记住我选项")
if qconfig.get(cfg.rememberMe):
logger.debug("已启用记住我功能,填充保存的邮箱和密码")
self.emailLineEdit.setText(qconfig.get(cfg.email))
self.passwordLineEdit.setText(qconfig.get(cfg.activationCode))
self.rememberMeCheckBox.setChecked(True)
else:
logger.debug("已禁用记住我功能,清空保存的邮箱和密码")
self.emailLineEdit.clear()
self.passwordLineEdit.clear()
def checkEmail(self, text):
# 检查当前输入是否通过验证器
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
if state == QRegularExpressionValidator.State.Acceptable:
logger.debug("邮箱格式验证通过")
self.loginButton.setDisabled(False)
else:
self.loginButton.setDisabled(True)
def refreshVerificationCode(self, event=None):
logger.debug("刷新验证码")
self.verificationCodeLabel.setEnabled(False)
self.captchaThread = CaptchaThread()
self.captchaThread.captchaReady.connect(self._showVerificationCode)
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
self.captchaThread.start()
def _showVerificationCode(self, pixmap):
logger.debug("显示验证码")
self.verificationCodeLabel.setEnabled(True)
self.verificationCodeLabel.setPixmap(pixmap)
def _showCaptchaFailed(self, message):
logger.debug(f"验证码刷新失败:{message}")
self.verificationCodeLabel.setEnabled(True)
self.verificationCodeLineEdit.clear()

View File

@@ -0,0 +1,142 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
LineEdit,
MessageBoxBase,
SubtitleLabel,
)
from app.core import CreateFolderThread, signalBus
class NewFolderMessageBox(MessageBoxBase):
"""新建文件夹对话框"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self._setupUi()
self._connectSignals()
# 线程引用,防止被垃圾回收
self.createFolderThread = None
def _setupUi(self):
"""设置UI界面"""
self.titleLabel = SubtitleLabel("新建文件夹", self)
self.nameLineEdit = LineEdit(self)
self.nameLineEdit.setPlaceholderText("请输入文件夹名称")
self.nameLineEdit.setClearButtonEnabled(True)
# 设置对话框属性
self.widget.setMinimumWidth(400)
self.yesButton.setText("新建")
self.cancelButton.setText("取消")
# 添加组件到布局
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.nameLineEdit)
# 初始时禁用确认按钮
self.yesButton.setEnabled(False)
def _connectSignals(self):
"""连接信号槽"""
self.yesButton.clicked.connect(self._onCreateClicked)
self.nameLineEdit.textChanged.connect(self._onTextChanged)
self.nameLineEdit.returnPressed.connect(self._onReturnPressed)
def _onTextChanged(self, text):
"""文本框内容变化时的处理"""
# 检查文件夹名称是否有效
is_valid = bool(text.strip()) and not any(char in text for char in '/\\:*?"<>|')
self.yesButton.setEnabled(is_valid)
if not is_valid and text.strip():
self.nameLineEdit.setToolTip('文件夹名称不能包含 /\\:*?"<>| 等特殊字符')
else:
self.nameLineEdit.setToolTip("")
def _onReturnPressed(self):
"""回车键处理"""
if self.yesButton.isEnabled():
self._onCreateClicked()
def _onCreateClicked(self):
"""创建文件夹按钮点击处理"""
folder_name = self.nameLineEdit.text().strip()
if not folder_name:
return
# 禁用按钮防止重复点击
self._setUiEnabled(False)
self.yesButton.setText("创建中...")
# 创建并启动线程
self.createFolderThread = CreateFolderThread(folder_name)
self.createFolderThread.successSignal.connect(self._onCreateSuccess)
self.createFolderThread.errorSignal.connect(self._onCreateError)
self.createFolderThread.start()
def _setUiEnabled(self, enabled):
"""设置UI启用状态"""
self.yesButton.setEnabled(enabled)
self.cancelButton.setEnabled(enabled)
self.nameLineEdit.setEnabled(enabled)
def _onCreateSuccess(self):
"""创建成功处理"""
self._showInfoBar("success", "操作成功", "新建文件夹成功")
signalBus.refreshFolderListSignal.emit()
"""线程完成时的清理工作"""
self.yesButton.setText("新建")
self._setUiEnabled(True)
self.accept()
def _onCreateError(self, error_msg):
"""创建失败处理"""
self._showInfoBar("error", "操作失败", error_msg)
# 不关闭对话框,让用户有机会修改后重试
self.nameLineEdit.setFocus()
self.nameLineEdit.selectAll()
"""线程完成时的清理工作"""
self.yesButton.setText("新建")
self._setUiEnabled(True)
def _showInfoBar(self, type_, title, content):
"""显示信息栏"""
if type_ == "success":
InfoBar.success(
title=title,
content=content,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=2000,
parent=self.window(),
)
else:
InfoBar.error(
title=title,
content=content,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000, # 错误信息显示稍长时间
parent=self.window(),
)
def showEvent(self, event):
"""显示事件处理"""
super().showEvent(event)
self.nameLineEdit.setFocus()
def closeEvent(self, event):
"""关闭事件处理"""
# 确保线程安全退出
if self.createFolderThread and self.createFolderThread.isRunning():
self.createFolderThread.quit()
self.createFolderThread.wait(1000) # 等待1秒
super().closeEvent(event)

View File

@@ -0,0 +1,297 @@
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
QHBoxLayout,
QWidget,
)
from qfluentwidgets import (Action, InfoBar, InfoBarPosition, LineEdit, MenuAnimationType, PillPushButton,
PrimarySplitPushButton, PushButton, RoundMenu, ScrollArea)
from app.core import DeleteTagThread, userConfig, lang
from app.view.widgets.add_tag_messageBox import AddTagMessageBox
class TagsScrollArea(ScrollArea):
"""标签滚动区域组件,支持动态添加和移除标签,支持单选模式"""
# 信号:标签被点击时发出,传递标签文本
tagClicked = pyqtSignal(str, str)
TAG_TYPES = {"video": "视频", "doc": "文档", "image": "图片", "audio": "音乐"}
def __init__(self, parent=None):
super().__init__(parent)
self.tagsDict = {} # 存储所有标签按钮
self.currentCheckedTag = None # 当前选中的标签ID
self.setupUi()
logger.debug("初始化标签滚动区域组件")
def setupUi(self):
"""初始化UI"""
self.widgets = QWidget()
self.layouts = QHBoxLayout(self.widgets)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.setMaximumWidth(400)
# 设置布局属性
self.layouts.setContentsMargins(0, 0, 0, 0)
self.layouts.setSpacing(10)
# 初始化默认标签
self.initDefaultTags()
# 设置样式
self.widgets.setStyleSheet("background-color: transparent; border: none;")
self.setStyleSheet("background-color: transparent; border: none;")
# 设置滚动策略
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# 安全地获取 tags 字段,如果不存在则使用空列表
self.tags = userConfig.userData.get("data", {}).get("tags", [])
for tag in self.tags:
self.addTag(tag["id"], tag["name"])
self.tagsDict[tag["id"]] = tag["name"]
def initDefaultTags(self):
"""初始化默认标签"""
logger.debug("初始化默认标签")
for tagId, tagText in self.TAG_TYPES.items():
self.addTag(tagId, tagText)
def addTag(self, tagId, text):
"""添加一个新标签
Args:
tagId: 标签的唯一标识符
text: 标签显示的文本
Returns:
创建的标签按钮对象
"""
self.tagsDict[tagId] = text
logger.debug(f"添加新标签: {tagId} - {text}")
tagBtn = PillPushButton(text, self.widgets)
tagBtn.setObjectName(f"tag_{tagId}")
tagBtn.setCheckable(True) # 设置为可选中状态
tagBtn.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu
) # 启用自定义上下文菜单
# 连接点击信号,实现单选逻辑
tagBtn.clicked.connect(
lambda checked, tid=tagId: self.onTagClicked(tid, checked)
)
# 连接右键菜单信号
tagBtn.customContextMenuRequested.connect(
lambda pos, tid=tagId: self.onTagRightClicked(tid, pos)
)
self.layouts.addWidget(tagBtn)
return tagBtn
def onTagClicked(self, tagId, checked):
"""处理标签点击事件,实现单选逻辑"""
if checked:
# 如果当前点击的标签被选中,取消之前选中的标签
if self.currentCheckedTag and self.currentCheckedTag != tagId:
# 找到之前选中的标签并取消选中
previousTagBtn = self.findChild(
QWidget, f"tag_{self.currentCheckedTag}"
)
if previousTagBtn:
previousTagBtn.setChecked(False)
# 更新当前选中的标签
self.currentCheckedTag = tagId
if tagId in ["video", "doc", "image", "audio"]:
self.tagClicked.emit("internalTag", tagId)
else:
self.tagClicked.emit("externalTag", tagId)
logger.debug(f"选中标签: {tagId}")
else:
# 如果取消选中当前标签,清空当前选中的标签
if self.currentCheckedTag == tagId:
self.currentCheckedTag = None
logger.debug(f"取消选中标签: {tagId}")
# 发出标签点击信号
def onTagRightClicked(self, tagId, pos):
"""处理标签右键点击事件"""
logger.debug(f"标签被右键点击: {tagId}")
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
global_pos = tagBtn.mapToGlobal(pos)
if tagBtn.text() in ["视频", "文档", "图片", "音乐"]:
return
menu = RoundMenu(parent=self)
menu.addAction(Action(lang("删除"), triggered=lambda: self.deleteTag(tagId)))
menu.exec(global_pos, aniType=MenuAnimationType.DROP_DOWN)
def deleteTag(self, tagId):
self.deleteTagThread = DeleteTagThread(tagId)
self.deleteTagThread.successDeleteSignal.connect(
lambda: self._onTagDeleteError(tagId)
)
self.deleteTagThread.errorSignal.connect(self._onTagDeleteError)
self.deleteTagThread.start()
def _onTagDeleteError(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def _onTagDeleteError(self, tagId):
self.removeTag(tagId)
InfoBar.success(
"成功",
"标签删除成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def removeTag(self, tagId):
"""移除指定标签"""
if tagId in self.tagsDict:
logger.debug(f"移除标签: {tagId}")
# 如果移除的是当前选中的标签,清空选中状态
if self.currentCheckedTag == tagId:
self.currentCheckedTag = None
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
self.layouts.removeWidget(tagBtn)
tagBtn.deleteLater()
del self.tagsDict[tagId]
else:
logger.warning(f"尝试移除不存在的标签: {tagId}")
def getCheckedTag(self):
"""获取当前选中的标签ID"""
return self.currentCheckedTag
def setCheckedTag(self, tagId):
"""设置指定标签为选中状态"""
if tagId in self.tagsDict:
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
tagBtn.setChecked(True)
# onTagClicked 方法会自动处理单选逻辑
else:
logger.warning(f"尝试选中不存在的标签: {tagId}")
def clearChecked(self):
"""清除所有选中状态"""
if self.currentCheckedTag:
tagBtn = self.findChild(QWidget, f"tag_{self.currentCheckedTag}")
if tagBtn:
tagBtn.setChecked(False)
self.currentCheckedTag = None
class TagWidget(QWidget):
"""标签管理组件"""
def __init__(self, parent=None):
super().__init__(parent)
self.tagScrollArea = TagsScrollArea(self)
self.addPushButton = PushButton(lang("添加标签"), self)
logger.debug("初始化标签管理组件")
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.tagScrollArea)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(self.addPushButton, Qt.AlignmentFlag.AlignRight)
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接标签管理组件信号")
self.addPushButton.clicked.connect(self.addTag)
def addTag(self):
w = AddTagMessageBox(self.window())
w.successAddTagSignal.connect(self.onAddTagSuccess)
if w.exec():
...
def onAddTagSuccess(self, name, result):
"""处理标签添加成功事件"""
InfoBar.success(
"成功",
"标签添加成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.tagScrollArea.addTag(result["data"], name)
class SearchWidget(QWidget):
"""搜索组件"""
# 信号:搜索请求时发出,传递搜索关键词
searchRequested = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.searchLineEdit = LineEdit(self)
self.searchButton = PrimarySplitPushButton(lang("仓内搜索"), self)
self.menu = RoundMenu(parent=self)
self.menu.addAction(
Action(
lang("仓内搜索"),
triggered=lambda: self.changeButtonText(lang("仓内搜索")),
)
)
self.menu.addAction(
Action(
lang("站内搜索"),
triggered=lambda: self.changeButtonText(lang("站内搜索")),
)
)
self.searchButton.setFlyout(self.menu)
logger.debug("初始化搜索组件")
self.setupUi()
def changeButtonText(self, text):
self.searchButton.setText(text)
def setupUi(self):
"""初始化UI"""
self.searchLineEdit.setPlaceholderText(lang("搜索文件"))
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setContentsMargins(0, 24, 0, 0)
self.hBoxLayout.addWidget(self.searchLineEdit)
self.hBoxLayout.addWidget(self.searchButton)

View File

@@ -0,0 +1,91 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import (
BreadcrumbBar,
setFont,
)
from app.view.components.linkage_switching import OwnFileLinkageSwitching
class OwnFileScrollWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentPath = "/"
self.breadcrumbBar = BreadcrumbBar(self)
self.ownFileLinkageSwitching = OwnFileLinkageSwitching("/", self)
self.__initWidget()
def __initWidget(self):
self.setObjectName("OwnFileScrollWidget")
self.breadcrumbBar.addItem("/", "/")
setFont(self.breadcrumbBar, 18)
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
self.breadcrumbBar.setSpacing(15)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setSpacing(5)
self.vBoxLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addWidget(self.ownFileLinkageSwitching)
def loadDict(self, paths):
self.ownFileLinkageSwitching.loadDict(paths)
def onChangeDir(self, path):
"""处理目录变更"""
logger.info(f"变更目录: {path}")
if path == "":
self.currentPath = "/"
else:
self.currentPath = path
self.ownFileLinkageSwitching.loadDict(path)
# 更新面包屑导航
displayName = path.split("/")[-1] if path != "/" else "/"
self.breadcrumbBar.addItem(displayName, displayName)
def clickChangeDir(self, name):
"""处理面包屑导航项点击事件"""
logger.info(f"面包屑导航项点击: {name}")
# 获取点击的路径
if name == "":
name = "/"
pathList = []
for item in self.breadcrumbBar.items:
if item.text == name:
pathList.append(item.text)
break
else:
pathList.append(item.text)
for i in pathList:
if i == "":
pathList.remove(i)
path = "/".join(pathList) if pathList else "/"
path = path[1:]
if path == "":
path = "/"
if path == self.currentPath:
logger.debug("路径未变化,跳过导航")
return
self.onChangeDir(path)
def refreshCurrentDirectory(self):
"""刷新当前目录的文件卡片"""
logger.info(f"刷新当前目录: {self.currentPath}")
self.loadDict(self.currentPath)

View File

@@ -0,0 +1,162 @@
# coding: utf-8
# flip bookmark
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QIntValidator
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, LineEdit, PushButton, ToolButton)
class PageFlipWidget(CardWidget):
# 定义页码变化信号
pageChangeSignal = pyqtSignal(int)
"""
page : 总页码,默认为10
currentPage : 当前页码
numberButtonList : 页码按钮列表
currentButtonNumbet : 当前显示的按钮数字列表
"""
def __init__(self, parent=None, page=10):
super().__init__(parent)
self.page = page
self.currentPage = 1
self.numberButtonList = []
self.currentButtonNumber = []
# 定义翻页按钮
self.leftPageButton = ToolButton(self)
self.rightPageButton = ToolButton(self)
# 定义跳转页面组件
self.pageLineEdit = LineEdit(self)
self.allPageLabel = BodyLabel(self)
self.turnPageButton = PushButton(self)
self.__initWidget()
# 动态设置按钮
self._addButton()
def __initWidget(self):
# 组件设置
self.leftPageButton.setIcon(FluentIcon.PAGE_LEFT)
self.leftPageButton.setFixedSize(40, 40)
self.leftPageButton.clicked.connect(self.backPage)
self.rightPageButton.setIcon(FluentIcon.PAGE_RIGHT)
self.rightPageButton.setFixedSize(40, 40)
self.rightPageButton.clicked.connect(self.forwardPage)
self.pageLineEdit.setText("1")
self.pageLineEdit.setValidator(QIntValidator())
self.pageLineEdit.editingFinished.connect(self._validator)
self.pageLineEdit.setFixedWidth(45)
self.allPageLabel.setText(f"/{self.page}")
self.turnPageButton.setText(self.tr("jump"))
self.turnPageButton.setFixedWidth(60)
self.turnPageButton.clicked.connect(self.turnPage)
self.__initLayout()
def __initLayout(self):
# 布局设置
self.layouts = QHBoxLayout(self)
self.layouts.addWidget(self.leftPageButton)
self.layouts.addWidget(self.rightPageButton)
self.layouts.addSpacing(30)
self.layouts.addWidget(self.pageLineEdit)
self.layouts.addWidget(self.allPageLabel)
self.layouts.addWidget(self.turnPageButton)
# 向后翻页
def forwardPage(self):
if self.page > 5:
if int(self.numberButtonList[-1].text()) < self.page:
for i in self.numberButtonList:
i.setText(str(int(i.text()) + 1))
for i in range(len(self.currentButtonNumber)):
self.currentButtonNumber[i] += 1
if self.currentPage < self.page:
self.currentPage += 1
self.pageLineEdit.setText(str(self.currentPage))
self.pageChangeSignal.emit(self.currentPage)
# 向前翻页
def backPage(self):
if int(self.numberButtonList[0].text()) > 1:
for i in self.numberButtonList:
i.setText(str(int(i.text()) - 1))
for i in range(len(self.currentButtonNumber)):
self.currentButtonNumber[i] -= 1
if self.currentPage > 1:
self.currentPage -= 1
self.pageLineEdit.setText(str(self.currentPage))
self.pageChangeSignal.emit(self.currentPage)
# 跳转页面
def turnPage(self, page: int):
"""
page : 目标跳转页
"""
page = int(self.pageLineEdit.text())
numberList = [1, 2, 3, 4, 5]
if page not in numberList:
while True:
numberList = [x + 1 for x in numberList]
if page in numberList and max(numberList) <= self.page:
break
self.currentButtonNumber = numberList
for i in self.numberButtonList:
i.setText(str(self.currentButtonNumber[self.numberButtonList.index(i)]))
self.pageChangeSignal.emit(page)
self.currentPage = page
# 输入框判断器,规定只可输入数字,并且数字不能超过规定范围
def _validator(self):
page = int(self.pageLineEdit.text())
if page <= 0:
page = 1
elif page > self.page:
page = self.page
self.pageLineEdit.setText(str(page))
# 动态添加按钮
def _addButton(self):
if self.page >= 5:
for i in range(1, 6):
numberButton = PushButton(str(i), self)
numberButton.setFixedSize(40, 40)
numberButton.clicked.connect(self._pageChanged)
self.layouts.insertWidget(i, numberButton)
self.numberButtonList.append(numberButton)
self.currentButtonNumber = [1, 2, 3, 4, 5]
else:
for i in range(1, self.page + 1):
numberButton = PushButton(str(i), self)
numberButton.setFixedSize(40, 40)
numberButton.clicked.connect(self._pageChanged)
self.layouts.insertWidget(i, numberButton)
self.numberButtonList.append(numberButton)
self.currentButtonNumber.append(i)
# 页面翻页时发出信号
def _pageChanged(self, checked):
sender = self.sender()
if sender:
button_text = sender.text()
self.pageChangeSignal.emit(int(button_text))
self.pageLineEdit.setText(button_text)
self.currentPage = int(button_text)

View File

@@ -0,0 +1,165 @@
# coding: utf-8
import logging
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QListWidgetItem
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
ListWidget,
MessageBoxBase,
SubtitleLabel,
)
from app.core import ChangePolicyThread, GetPoliciesThread, policyConfig, signalBus
class PolicyChooseMessageBox(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentPath = "/"
self.policyDict = {}
self.isLoading = True
self.originalTitle = "选择存储策略"
self.setClosableOnMaskClicked(True)
self.setupUI()
# 开始获取策略列表
self.getPoliciesThread = GetPoliciesThread()
self.getPoliciesThread.successGetSignal.connect(self.refreshPolicyList)
self.getPoliciesThread.errorSignal.connect(self._handleGetPoliciesError)
self.getPoliciesThread.start()
# 初始化更改策略线程(但不启动)
self.changePolicyThread = None
def setupUI(self):
"""设置UI界面"""
self.titleLabel = SubtitleLabel(self.originalTitle, self)
self.policyListWidget = ListWidget(self)
# 添加加载提示
self.loadingItem = QListWidgetItem("正在加载策略列表...")
self.policyListWidget.addItem(self.loadingItem)
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.policyListWidget)
# 隐藏确定取消按钮组
self.buttonGroup.hide()
def connectSignals(self):
"""连接信号与槽"""
self.policyListWidget.currentTextChanged.connect(self.onPolicyChanged)
self.policyListWidget.itemClicked.connect(self.selfClicked)
def selfClicked(self, listWidget: QListWidgetItem):
if listWidget.text() == policyConfig.returnPolicy()["name"]:
QTimer.singleShot(100, self.accept)
def onPolicyChanged(self, text):
"""处理策略更改"""
if not text or self.isLoading or text == "正在加载策略列表...":
return
policy_id = self.policyDict.get(text)
if not policy_id:
return
# 如果已经有更改线程在运行,先停止它
if self.changePolicyThread and self.changePolicyThread.isRunning():
self.changePolicyThread.quit()
self.changePolicyThread.wait()
# 创建并启动新的更改线程
self.changePolicyThread = ChangePolicyThread(self.currentPath, policy_id)
self.changePolicyThread.successChangedSignal.connect(
self._handlePolicyChangeSuccess
)
self.changePolicyThread.errorSignal.connect(self._handlePolicyChangeError)
self.changePolicyThread.start()
# 更新UI状态 - 只更改标题
self._setLoadingState(True, f"正在切换到策略: {text}")
def refreshPolicyList(self, policiesList):
"""刷新策略列表"""
self.isLoading = False
self.policyListWidget.clear()
self.policyDict.clear()
currentPolicy = policyConfig.returnPolicy()
currentIndex = 0
for i, policy in enumerate(policiesList):
self.policyListWidget.addItem(QListWidgetItem(policy["name"]))
self.policyDict[policy["name"]] = policy["id"]
if policy["id"] == currentPolicy["id"]:
currentIndex = i
# 设置当前选中项
if self.policyListWidget.count() > 0:
self.policyListWidget.setCurrentRow(currentIndex)
self.currentPath = policyConfig.returnCurrentPath()
self.connectSignals()
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)
def _handleGetPoliciesError(self, error_msg):
"""处理获取策略列表错误"""
self.policyListWidget.clear()
errorItem = QListWidgetItem(f"加载失败: {error_msg}")
self.policyListWidget.addItem(errorItem)
self.isLoading = False
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)
def _handlePolicyChangeSuccess(self):
"""处理策略更改成功"""
self._setLoadingState(False)
# 显示成功提示
if self.parent():
InfoBar.success(
title="操作成功",
content="存储策略已成功更改",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=2000,
parent=self.window(),
)
signalBus.refreshFolderListSignal.emit()
QTimer.singleShot(1000, self.accept)
def _handlePolicyChangeError(self, error_msg):
"""处理策略更改错误"""
self._setLoadingState(False)
# 显示错误提示
if self.parent():
InfoBar.error(
title="操作失败",
content=f"更改策略时出错: {error_msg}",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000,
parent=self.window(),
)
QTimer.singleShot(1000, self.reject)
def _setLoadingState(self, loading, message=None):
"""设置加载状态"""
if loading:
self.policyListWidget.setEnabled(False)
if message:
logging.info(message)
else:
self.policyListWidget.setEnabled(True)
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)

View File

@@ -0,0 +1,292 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
IndeterminateProgressBar,
MessageBoxBase,
PlainTextEdit,
PushButton,
)
from app.core import (ImageLoaderThread, TextLoaderThread, UpdateFileContentThread)
from app.core.services.text_speech import LocalSpeechController
from app.view.components.empty_card import EmptyCard
# 图片预览类
def createThumbnail(pixmap, max_size=200):
"""创建快速缩略图"""
if pixmap.isNull():
return pixmap
# 使用快速缩放算法
return pixmap.scaled(
max_size,
max_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.FastTransformation,
)
class OptimizedPreviewBox(MessageBoxBase):
def __init__(self, parent=None, url=None):
super().__init__(parent=parent)
self.widget.setMinimumSize(500, 500)
self.original_pixmap = None
self.current_scale = 1.0
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 图片显示标签
self.previewLabel = ImageLabel(self)
self.previewLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter)
# 使用优化的图片加载线程
self.imageLoaderThread = ImageLoaderThread(url)
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
self.imageLoaderThread.errorOccurred.connect(self.handleError)
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
# 延迟启动加载避免阻塞UI初始化
from PyQt6.QtCore import QTimer
QTimer.singleShot(100, self.startLoading)
def startLoading(self):
"""开始加载图片"""
self.imageLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
self.loadingCard.setText(f"正在加载图片... {progress}%")
def setPreviewImg(self, img: QPixmap):
"""设置预览图片"""
self.loadingCard.hide()
self.original_pixmap = img
# 立即显示缩略图
thumbnail = createThumbnail(img)
self.previewLabel.setPixmap(thumbnail)
# 然后异步加载高质量版本
self.adjustImageSize()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
if self.original_pixmap and not self.original_pixmap.isNull():
# 使用定时器延迟调整,避免频繁调整
from PyQt6.QtCore import QTimer
QTimer.singleShot(50, self.adjustImageSize)
def adjustImageSize(self):
"""根据窗口大小动态调整图片尺寸"""
if not self.original_pixmap or self.original_pixmap.isNull():
return
# 获取可用显示区域大小
margin = 80
available_width = self.width() - margin * 2
available_height = self.height() - margin * 2
# 获取原始图片尺寸
original_width = self.original_pixmap.width()
original_height = self.original_pixmap.height()
# 计算缩放比例
width_ratio = available_width / original_width
height_ratio = available_height / original_height
scale_ratio = min(width_ratio, height_ratio, 1.0)
# 只在需要时重新缩放
if abs(scale_ratio - self.current_scale) > 0.05: # 变化超过5%才重新缩放
self.current_scale = scale_ratio
new_width = int(original_width * scale_ratio)
new_height = int(original_height * scale_ratio)
# 使用平滑缩放
scaled_pixmap = self.original_pixmap.scaled(
new_width,
new_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self.previewLabel.setPixmap(scaled_pixmap)
def handleError(self, msg):
"""处理加载错误"""
self.loadingCard.error()
self.previewLabel.hide()
# 文本文档预览类
class PreviewTextBox(MessageBoxBase):
"""文本预览对话框"""
def __init__(self, parent=None, url=None, _id=None):
super().__init__(parent=parent)
self.updateTxtThread = None
self.widget.setMinimumSize(600, 400)
self._id = _id
self.isChanged = False
self.speech_controller = LocalSpeechController(self)
self.textSpeakButton = PushButton("朗读文本", self)
self.textSpeakButton.hide()
self.isSpeaking = False
self.textSpeakButton.clicked.connect(self.playTextSpeech)
self.viewLayout.addWidget(
self.textSpeakButton,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
# 创建文本编辑框
self.textEdit = PlainTextEdit(self)
self.textEdit.hide()
self.textEdit.setLineWrapMode(PlainTextEdit.LineWrapMode.NoWrap) # 不自动换行
# 设置等宽字体,便于阅读代码或日志
from PyQt6.QtGui import QFont
font = QFont("微软雅黑", 10) # 等宽字体
self.textEdit.setFont(font)
self.viewLayout.addWidget(self.textEdit)
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 使用文本加载线程
self.textLoaderThread = TextLoaderThread(url)
self.textLoaderThread.textLoaded.connect(self.setTextContent)
self.textLoaderThread.errorOccurred.connect(self.handleError)
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
self.yesButton.hide()
# 创建保存按钮
self.saveButton = PushButton("保存修改", self)
# 创建进度条
self.saveProgressBar = IndeterminateProgressBar(self)
self.saveProgressBar.setFixedHeight(4)
self.saveProgressBar.hide()
# 添加按钮和进度条到布局
self.buttonLayout.insertWidget(
0, self.saveButton, 1, Qt.AlignmentFlag.AlignVCenter
)
self.buttonLayout.insertWidget(
1, self.saveProgressBar, 1, Qt.AlignmentFlag.AlignVCenter
)
self.saveButton.setEnabled(False)
self.saveButton.clicked.connect(self.saveText)
self.cancelButton.setText("返回")
# 延迟启动加载避免阻塞UI初始化
QTimer.singleShot(100, self.startLoading)
def saveText(self):
logger.info("保存用户修改")
# 显示进度条并禁用按钮
self.saveProgressBar.show()
self.saveButton.setEnabled(False)
self.saveTextThread = UpdateFileContentThread(
self._id,
self.textEdit.toPlainText(),
)
self.saveTextThread.successUpdated.connect(self._successSave)
self.saveTextThread.errorUpdated.connect(self._errorSave)
self.saveTextThread.start()
def _successSave(self):
InfoBar.success(
"成功",
"修改保存成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条
self.saveProgressBar.hide()
QTimer.singleShot(700, self.accept)
def _errorSave(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条并重新启用按钮
self.saveProgressBar.hide()
self.saveButton.setEnabled(True)
def playTextSpeech(self):
"""播放文本语音"""
if not self.isSpeaking:
text = self.textEdit.toPlainText()
if text and len(text.strip()) > 0:
self.speech_controller.play_text(text)
self.isSpeaking = True
self.textSpeakButton.setText("暂停朗读")
else:
self.speech_controller.stop_playback()
self.isSpeaking = False
self.textSpeakButton.setText("朗读文本")
def startLoading(self):
"""开始加载文本"""
self.textLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
self.loadingCard.setText(f"正在加载文本... {progress}%")
def setTextContent(self, content):
"""设置文本内容"""
self.loadingCard.hide()
self.textEdit.show()
self.textSpeakButton.show()
self.saveButton.setEnabled(True)
# 限制显示的内容长度,避免性能问题
max_display_length = 100000 # 最多显示10万个字符
if len(content) > max_display_length:
content = (
content[:max_display_length]
+ f"\n\n... (内容过长,已截断前{max_display_length}个字符,完整内容请下载文件查看)"
)
self.textEdit.setPlainText(content)
def handleError(self, error_msg):
"""处理加载错误"""
self.loadingCard.error()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
# 文本预览框会自动适应大小,无需特殊处理

View File

@@ -0,0 +1,86 @@
from loguru import logger
from PyQt6.QtCore import QRegularExpression, Qt
from PyQt6.QtGui import (
QRegularExpressionValidator,
)
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
LineEdit,
PasswordLineEdit,
PrimaryPushButton,
)
from app.core import CaptchaThread
class RegisterWidget(QWidget):
def __init__(self, parent=None):
logger.debug("初始化注册组件")
super().__init__(parent)
self.setObjectName("RegisterWidget")
self.emailLineEdit = LineEdit(self)
self.emailLineEdit.setPlaceholderText("请输入注册邮箱")
self.passwordLineEdit = PasswordLineEdit(self)
self.passwordLineEdit.setPlaceholderText("请输入密码")
self.confirmPasswordLineEdit = PasswordLineEdit(self)
self.confirmPasswordLineEdit.setPlaceholderText("请确认您的密码")
self.verificationCodeLabel = QLabel(self)
self.verificationCodeLabel.setFixedSize(120, 35)
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
self.verificationCodeLabel.mousePressEvent = (
self.refreshVerificationCode
) # 绑定点击事件
self.verificationCodeLineEdit = LineEdit(self)
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
self.verificationLayout = QHBoxLayout()
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
self.verificationLayout.addWidget(self.verificationCodeLabel)
self.registerButton = PrimaryPushButton("注册", self)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.setSpacing(15)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.emailLineEdit)
self.vBoxLayout.addWidget(self.passwordLineEdit)
self.vBoxLayout.addWidget(self.confirmPasswordLineEdit)
self.vBoxLayout.addLayout(self.verificationLayout)
self.vBoxLayout.addWidget(self.registerButton)
email_regex = QRegularExpression(
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
validator = QRegularExpressionValidator(email_regex, self)
self.emailLineEdit.setValidator(validator)
self.emailLineEdit.textChanged.connect(self.checkeEmail)
logger.debug("注册组件初始化完成")
self.refreshVerificationCode()
def checkeEmail(self, text):
# 检查当前输入是否通过验证器
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
if state == QRegularExpressionValidator.Acceptable:
logger.debug("注册邮箱格式验证通过")
self.registerButton.setDisabled(False)
else:
self.registerButton.setDisabled(True)
def refreshVerificationCode(self, event=None):
logger.debug("刷新验证码")
self.captchaThread = CaptchaThread()
self.captchaThread.captchaReady.connect(self._showVerificationCode)
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
self.captchaThread.start()
def _showVerificationCode(self, pixmap):
logger.debug("显示验证码")
self.verificationCodeLabel.setPixmap(pixmap)
def _showCaptchaFailed(self, message):
logger.debug(f"验证码刷新失败:{message}")
self.verificationCodeLineEdit.clear()

View File

@@ -0,0 +1,147 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer, QUrl
from PyQt6.QtGui import QPixmap
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import (BodyLabel, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, MessageBoxBase,
SubtitleLabel)
from app.core import formatDate, formatSize, GetShareFileInfoThread, signalBus
class ShareFileMessageBox(MessageBoxBase):
def __init__(self, _id, fileIcon=None, suffix="", parent=None):
super().__init__(parent=parent)
self.widget.setFixedWidth(350)
self.suffix = suffix
self._id = _id
self.fileTypeImageLabel = ImageLabel(parent=self)
self.fileTypeImageLabel.setImage(fileIcon)
self.fileTypeImageLabel.scaledToHeight(60)
self.fileTypeImageLabel.scaledToWidth(60)
self.fileNameLabel = SubtitleLabel(parent=self)
self.fileSizeLabel = BodyLabel(parent=self)
self.fileInformationLabel = BodyLabel(parent=self)
self.fileInformationLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.userImageLabel = ImageLabel(":app/images/logo.png", parent=self)
self.userImageLabel.setBorderRadius(20, 20, 20, 20)
self.userImageLabel.setFixedSize(30, 30)
self.userImageLabel.scaledToHeight(30)
self.userImageLabel.scaledToWidth(30)
self.userNameLabel = SubtitleLabel(parent=self)
self.userLayout = QHBoxLayout()
self.userLayout.addWidget(self.userImageLabel)
self.userLayout.addWidget(self.userNameLabel)
self.viewLayout.addWidget(
self.fileTypeImageLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileSizeLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileInformationLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(HorizontalSeparator(parent=self))
self.viewLayout.addLayout(self.userLayout)
self.yesButton.setText("下载")
self.yesButton.clicked.connect(self.downloadFile)
self.cancelButton.setText("取消")
self.apiWorker = GetShareFileInfoThread(_id)
self.apiWorker.shareFileInfoSignal.connect(self.handleApiResponse)
self.apiWorker.errorSignal.connect(self.handleError)
self.apiWorker.start()
self.networkManager = QNetworkAccessManager(self)
self.networkManager.finished.connect(self.onAvatarDownloaded)
def downloadFile(self):
signalBus.addDownloadFileTask.emit(
f"share.{self.suffix}",
self.fileNameLabel.text(),
f"undefined/undefined.{self._id}",
)
self.accept()
def handleApiResponse(self, response_data):
response_data = response_data["data"]
self.fileNameLabel.setText(response_data["source"]["name"])
self.fileSizeLabel.setText(
f"大小: {formatSize(response_data['source']['size'])}"
)
infoLabel = f"创建时间: {formatDate(response_data['create_date'])}\n浏览次数: {response_data['views']}\n下载次数: {response_data['downloads']}"
self.fileInformationLabel.setText(infoLabel)
self.userNameLabel.setText(response_data["creator"]["nick"])
self.loadAvatarFromId(response_data["creator"]["key"])
def handleError(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
QTimer.singleShot(1000, self.accept)
def loadAvatarFromId(self, _id):
"""从网络URL加载头像"""
# 使用V4 API获取头像 - 假设格式变为/api/v4/user/avatar/{_id}/l
url = f"/user/avatar/{_id}/l"
logger.info(f"开始从网络加载头像")
request = QNetworkRequest(QUrl(url))
self.networkManager.get(request)
def onAvatarDownloaded(self, reply):
"""处理头像下载完成"""
if reply.error() == QNetworkReply.NetworkError.NoError:
# 读取下载的数据
data = reply.readAll()
# 创建QPixmap并加载数据
pixmap = QPixmap()
if pixmap.loadFromData(data):
# 更新头像
self.userImageLabel.setImage(pixmap)
logger.info("网络头像加载成功")
else:
logger.error("头像数据格式不支持")
else:
logger.error(f"头像下载失败: {reply.errorString()}")
pixmap = QPixmap(":app/images/logo.png")
self.userImageLabel.setImage(pixmap)
logger.info("使用默认头像")
self.userImageLabel.scaledToHeight(30)
self.userImageLabel.scaledToWidth(30)
reply.deleteLater()
def format_size(self, size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"

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