init
55
.gitea/workflows/nuitka-build.yml
Normal 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:
|
||||
# 步骤1:Gitea 原生拉取代码(解决 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
|
||||
|
||||
# 步骤3:Nuitka 编译 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
@@ -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
|
||||
52
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
@@ -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.
|
||||
3
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,3 @@
|
||||
api有关的需要用cloudreve v4 api的mcp来查询
|
||||
|
||||
写完代码后不需要运行一遍main.py
|
||||
661
LICENSE
Normal 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
@@ -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
@@ -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>
|
||||
|
||||
> **开源·高效·智能** 您的云端文件管家
|
||||
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://www.qt.io/qt-for-python)
|
||||
[](https://cloudreve.apifox.cn/doc-4217598)
|
||||
|
||||
**官方网站**: [https://mp.miaostars.com](https://mp.miaostars.com)
|
||||
**开发公司**: 武汉市喵星创想互联网科技有限公司
|
||||
|
||||
---
|
||||
|
||||
## ✨ 产品介绍
|
||||
|
||||
LeonPan客户端是官方推出的桌面应用程序,基于Cloudreve后端API开发,采用AGPL3开源协议。我们扩展了多项实用功能,为您提供更完善的云端文件管理体验。
|
||||

|
||||
## 🚀 特色功能
|
||||
|
||||
### 文件管理
|
||||
- 📁 **智能相册** - 自动分类照片,支持人脸识别和场景识别
|
||||
- ⏳ **文件历史版本** - 保留文件修改历史,随时回溯
|
||||
- 🕰️ **时间线整理** - 按时间自动整理照片和文档
|
||||
|
||||
### 下载传输
|
||||
- ⚡ **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
@@ -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
@@ -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
@@ -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", "昵称更新失败")}
|
||||
1440
app/core/services/file_thread.py
Normal file
166
app/core/services/login_thread.py
Normal 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("系统错误,请稍后重试")
|
||||
246
app/core/services/preview_thread.py
Normal 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)}")
|
||||
287
app/core/services/text_speech.py
Normal 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}")
|
||||
199
app/core/services/user_thread.py
Normal 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))
|
||||
1
app/core/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .exceptions import getCode
|
||||
202
app/core/utils/config.py
Normal 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()
|
||||
77
app/core/utils/encryption.py
Normal 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)
|
||||
14
app/core/utils/exceptions.py
Normal 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
@@ -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"
|
||||
90
app/core/utils/morelang.py
Normal 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
@@ -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
|
||||
34
app/core/utils/signal_bus.py
Normal 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()
|
||||
6
app/core/utils/version.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Version information for the application
|
||||
"""
|
||||
|
||||
version = "0.0.2"
|
||||
1
app/resource/icons/3D.svg
Normal 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 |
1
app/resource/icons/Application.svg
Normal 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 |
3
app/resource/icons/BgImage.svg
Normal 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 |
1
app/resource/icons/Config.svg
Normal 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 |
1
app/resource/icons/Database.svg
Normal 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 |
4
app/resource/icons/Date.svg
Normal 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 |
3
app/resource/icons/Email.svg
Normal 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 |
1
app/resource/icons/Excel.svg
Normal 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 |
1
app/resource/icons/Exe.svg
Normal 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 |
1
app/resource/icons/Folder.svg
Normal 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 |
1
app/resource/icons/Gif.svg
Normal 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 |
3
app/resource/icons/Group.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
1
app/resource/icons/Image.svg
Normal 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 |
5
app/resource/icons/Info.svg
Normal 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 |
1
app/resource/icons/Music.svg
Normal 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 |
1
app/resource/icons/Myfile.svg
Normal 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 |
3
app/resource/icons/Nickname.svg
Normal 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 |
1
app/resource/icons/None.svg
Normal 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 |
3
app/resource/icons/Opacity.svg
Normal 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 |
1
app/resource/icons/PPT.svg
Normal 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 |
1
app/resource/icons/Pdf.svg
Normal 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 |
1
app/resource/icons/Programme.svg
Normal 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 |
1
app/resource/icons/SavePath.svg
Normal 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 |
1
app/resource/icons/Score.svg
Normal 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 |
1
app/resource/icons/Storage.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
1
app/resource/icons/Task.svg
Normal 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 |
1
app/resource/icons/Txt.svg
Normal 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 |
1
app/resource/icons/Video.svg
Normal 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 |
1
app/resource/icons/WPS.svg
Normal 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 |
1
app/resource/icons/Word.svg
Normal 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 |
1
app/resource/icons/Zip.svg
Normal 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 |
1
app/resource/icons/login.svg
Normal 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 |
1
app/resource/icons/register.svg
Normal 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 |
BIN
app/resource/images/background.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
app/resource/images/bg0.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
app/resource/images/bg1.png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
app/resource/images/bg2.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
app/resource/images/bg3.png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
app/resource/images/bg4.png
Normal file
|
After Width: | Height: | Size: 7.9 MiB |
BIN
app/resource/images/bg5.png
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
app/resource/images/empty.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
app/resource/images/error.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
app/resource/images/load.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
app/resource/images/loadFailure.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
app/resource/images/logo.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
app/resource/images/title.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
103
app/resource/lang/en.json
Normal 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
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"你好": "你好",
|
||||
"我的文件": "我的文件",
|
||||
"存储配额": "存储配额",
|
||||
"任务管理": "任务管理",
|
||||
"应用信息": "应用信息",
|
||||
"此页面正在建设中...": "此页面正在建设中...",
|
||||
"语言设置:": "语言设置:",
|
||||
"中文": "中文",
|
||||
"English": "English",
|
||||
"上传": "上传",
|
||||
"设置": "设置",
|
||||
"确定": "确定",
|
||||
"取消": "取消",
|
||||
"关闭": "关闭",
|
||||
"刷新": "刷新",
|
||||
"删除": "删除",
|
||||
"重命名": "重命名",
|
||||
"移动": "移动",
|
||||
"复制": "复制",
|
||||
"分享": "分享",
|
||||
"新建文件夹": "新建文件夹",
|
||||
"文件信息": "文件信息",
|
||||
"大小": "大小",
|
||||
"类型": "类型",
|
||||
"修改时间": "修改时间",
|
||||
"创建时间": "创建时间",
|
||||
"状态": "状态",
|
||||
"进度": "进度",
|
||||
"速度": "速度",
|
||||
"剩余时间": "剩余时间",
|
||||
"暂停": "暂停",
|
||||
"继续": "继续",
|
||||
"重试": "重试",
|
||||
"完成": "完成",
|
||||
"失败": "失败",
|
||||
"等待中": "等待中",
|
||||
"进行中": "进行中",
|
||||
"仓内搜索": "仓内搜索",
|
||||
"站内搜索": "站内搜索",
|
||||
"搜索文件": "搜索文件",
|
||||
"添加标签": "添加标签",
|
||||
"标签名称": "标签名称",
|
||||
"标签通配符": "标签通配符",
|
||||
"文件上传": "文件上传",
|
||||
"文件下载": "文件下载",
|
||||
"用户组基础容量": "用户组基础容量",
|
||||
"有效容量包附加附加容量": "有效容量包附加附加容量",
|
||||
"已使用容量": "已使用容量",
|
||||
"总容量": "总容量",
|
||||
"LeonPan": "LeonPan",
|
||||
"修改昵称": "修改昵称",
|
||||
"用户信息": "用户信息",
|
||||
"修改头像": "修改头像",
|
||||
"用户头像": "用户头像",
|
||||
"点击修改头像": "点击修改头像",
|
||||
"电子邮箱": "电子邮箱",
|
||||
"当前用户组": "当前用户组",
|
||||
"用户注册时间": "用户注册时间",
|
||||
"修改成功": "修改成功",
|
||||
"昵称修改成功": "昵称修改成功",
|
||||
"选择图片": "选择图片",
|
||||
"头像修改成功": "头像修改成功",
|
||||
"选择下载保存路径": "选择下载保存路径",
|
||||
"选择文件夹": "选择文件夹",
|
||||
"下载保存路径修改成功": "下载保存路径修改成功",
|
||||
"背景图片设置": "背景图片设置",
|
||||
"官方背景图": "官方背景图",
|
||||
"选择自定义背景": "选择自定义背景",
|
||||
"图片背景透明度": "图片背景透明度",
|
||||
"设置图片背景透明度": "设置图片背景透明度",
|
||||
"透明度": "透明度",
|
||||
"透明度范围": "透明度范围",
|
||||
"选择自定义图片,选择后请不要更改图片位置": "选择自定义图片,选择后请不要更改图片位置",
|
||||
"隐私协议": "隐私协议",
|
||||
"用户协议": "用户协议",
|
||||
"官方预设背景图片": "官方预设背景图片",
|
||||
"选择背景图片": "选择背景图片",
|
||||
"自定义背景图片": "自定义背景图片",
|
||||
"选择保存路径": "选择保存路径",
|
||||
"用户昵称": "用户昵称",
|
||||
"更新设置": "更新设置",
|
||||
"检查更新": "检查更新",
|
||||
"检查是否有新版本可用": "检查是否有新版本可用",
|
||||
"当前版本": "当前版本",
|
||||
"发现新版本": "发现新版本",
|
||||
"最新版本": "最新版本",
|
||||
"更新内容": "更新内容",
|
||||
"立即更新": "立即更新",
|
||||
"稍后更新": "稍后更新",
|
||||
"检查更新失败": "检查更新失败",
|
||||
"无法连接到更新服务器,请稍后再试。": "无法连接到更新服务器,请稍后再试。",
|
||||
"开启自动更新": "开启自动更新",
|
||||
"在应用启动时自动检查更新": "在应用启动时自动检查更新",
|
||||
"已是最新版本": "已是最新版本",
|
||||
"语言设置": "语言设置",
|
||||
"下载": "下载",
|
||||
"预览": "预览",
|
||||
"进入": "进入",
|
||||
"刷新当前": "刷新当前",
|
||||
"上传文件": "上传文件",
|
||||
"设置存储策略": "设置存储策略"
|
||||
}
|
||||
334284
app/resource/resource.py
Normal file
47
app/resource/resource.qrc
Normal 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>
|
||||
300
app/view/app_info_interface.py
Normal 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重新启动应用,但为了安全考虑,
|
||||
# 这里仅退出当前进程,让用户手动重启
|
||||
49
app/view/components/empty_card.py
Normal 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("这里空空如也")
|
||||
362
app/view/components/file_card.py
Normal 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)
|
||||
263
app/view/components/file_deal_cards.py
Normal 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)
|
||||
37
app/view/components/gb_information_card.py
Normal 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))
|
||||
362
app/view/components/linkage_switching.py
Normal 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
@@ -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
@@ -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)
|
||||
)
|
||||
138
app/view/ownFiled_interface.py
Normal 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)
|
||||
363
app/view/setting_interface.py
Normal 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)
|
||||
76
app/view/storagespace_interface.py
Normal 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("用户配额加载,已刷新")
|
||||
54
app/view/task_interface.py
Normal 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)
|
||||
45
app/view/widgets/add_tag_messageBox.py
Normal 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}")
|
||||
23
app/view/widgets/custom_background_messageBox.py
Normal 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()
|
||||
363
app/view/widgets/custom_fluent_window.py
Normal 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())
|
||||
42
app/view/widgets/download_widget.py
Normal 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,
|
||||
)
|
||||
)
|
||||
117
app/view/widgets/login_widget.py
Normal 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()
|
||||
142
app/view/widgets/new_folder_messageBox.py
Normal 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)
|
||||
297
app/view/widgets/ownFiled_widgets.py
Normal 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)
|
||||
91
app/view/widgets/ownfile_scroll_widget.py
Normal 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)
|
||||
162
app/view/widgets/page_flip_widget.py
Normal 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)
|
||||
165
app/view/widgets/policy_messageBox.py
Normal 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)
|
||||
292
app/view/widgets/preview_box.py
Normal 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)
|
||||
# 文本预览框会自动适应大小,无需特殊处理
|
||||
86
app/view/widgets/register_widget.py
Normal 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()
|
||||
147
app/view/widgets/share_file_messageBox.py
Normal 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"
|
||||