图标升级
6
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
config.json
|
config.json
|
||||||
# refer/*
|
refer/*
|
||||||
*/__pycache__/*
|
*/__pycache__/*
|
||||||
.venv/*
|
.venv/*
|
||||||
|
venv/*
|
||||||
|
__pycache__/*
|
||||||
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 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 General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is 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. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
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 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. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
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 Affero 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 special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU 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 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 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 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 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
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 GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Printer Screen Menu
|
||||||
|
|
||||||
|
本项目为一个基于 PyQt6 的触摸屏控制与显示界面,作为 AIO_3D_Print_Web_Platform 的附属项目,用于在树莓派或类似设备上显示打印机状态、控制打印流程、查看 GCode 预览以及管理网络与设置。
|
||||||
|
|
||||||
|
主要功能
|
||||||
|
- 状态面板:显示打印作业、进度、温度等信息
|
||||||
|
- 控制面板:发送常用 GCode、暂停/停止/回原点等操作
|
||||||
|
- 设置面板:WiFi 管理、系统设置与悬浮虚拟键盘
|
||||||
|
- GCode 3D 预览:基于 OpenGL 的轻量渲染(适配 eglfs)
|
||||||
|
- 自动风扇状态读取与显示
|
||||||
|
|
||||||
|
关联项目
|
||||||
|
- 本仓库为 AIO_3D_Print_Web_Platform 的附属项目:
|
||||||
|
https://gitea.lhye.work/lhye200/AIO_3D_Print_Web_Platform
|
||||||
|
|
||||||
|
可选相关项目
|
||||||
|
- 该UI会显示该项目管理的风扇信息:
|
||||||
|
https://gitea.lhye.work/lhye200/Raspi_Auto_Fan
|
||||||
|
|
||||||
|
依赖说明
|
||||||
|
- PyQt6:优先通过系统包管理(apt)安装(脚本会尝试检测并安装可用的 python3-pyqt6 包);若系统不可用,则回退到在虚拟环境中通过 pip 安装。
|
||||||
|
- 其它 Python 包(通过 pip 安装,并在 `requirements.txt` 列出):`requests`, `numpy`, `PyOpenGL`, `qrcode`, `Pillow`。
|
||||||
|
|
||||||
|
快速安装
|
||||||
|
1. 赋予安装脚本可执行权限并运行(脚本会为你创建 `venv` 并安装缺失依赖):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x install.sh
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动程序:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
说明与开发者备注
|
||||||
|
- 本项目假设在 Debian/Ubuntu 类系统(支持 apt)上运行;在其他发行版上请根据包管理器调整安装方式。
|
||||||
|
- 安装脚本会优先尝试通过 apt 安装系统级 PyQt6 包(若可用),并在虚拟环境中安装剩余的 pip 依赖。
|
||||||
|
|
||||||
|
致谢与声明
|
||||||
|
- 本仓库为 AIO_3D_Print_Web_Platform 的本地触屏界面辅助项目(非原始主仓库),相关功能与 API 由主项目提供。
|
||||||
|
- 本文档与安装脚本在编写过程中得到 AI 辅助生成与整理。
|
||||||
|
|
||||||
|
License
|
||||||
|
- 详见项目根目录的 LICENSE 文件。
|
||||||
|
|
||||||
|
## 第三方许可说明
|
||||||
|
- 本项目中使用了 [Bootstrap Icons](https://icons.getbootstrap.com/) 作为部分界面图标。Bootstrap Icons 基于 **MIT License** 开源。
|
||||||
94
install.sh
@@ -1,10 +1,92 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# apt install -y python3-pyqt6.qtwebengine
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
# apt install -y python3-pyqt6.qtquick3d
|
VENVDIR="$SCRIPT_DIR/venv"
|
||||||
# apt install -y python3-pyqt6.qtquick
|
|
||||||
|
echo "== Printer_Screen_Menu 安装脚本 =="
|
||||||
|
|
||||||
|
if ! command -v apt >/dev/null 2>&1; then
|
||||||
|
echo "错误:本脚本需要 apt(Debian/Ubuntu)。请在支持 apt 的系统上运行。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUDO=""
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "更新 apt 索引并安装基础包(python3-venv, python3-pip)..."
|
||||||
|
$SUDO apt-get update -y
|
||||||
|
$SUDO apt-get install -y python3-venv python3-pip
|
||||||
|
|
||||||
|
echo "检测可用的 PyQt6 apt 包并尝试通过 apt 安装(优先使用系统包)..."
|
||||||
|
pyqt_candidates=(
|
||||||
|
python3-pyqt6
|
||||||
|
python3-pyqt6.qt6-opengl
|
||||||
|
python3-pyqt6.qtsvg
|
||||||
|
python3-pyqt6.qt6widgets
|
||||||
|
python3-pyqt6.qtopengl
|
||||||
|
python3-pyqt6.qt6opengl
|
||||||
|
)
|
||||||
|
found_pyqt=()
|
||||||
|
for p in "${pyqt_candidates[@]}"; do
|
||||||
|
if apt-cache show "$p" >/dev/null 2>&1; then
|
||||||
|
found_pyqt+=("$p")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#found_pyqt[@]}" -gt 0 ]; then
|
||||||
|
echo "通过 apt 安装: ${found_pyqt[*]}"
|
||||||
|
$SUDO apt-get install -y "${found_pyqt[@]}"
|
||||||
|
else
|
||||||
|
echo "未检测到合适的 PyQt6 apt 包,将在虚拟环境内使用 pip 安装 PyQt6 作为回退。"
|
||||||
|
install_pyqt_via_pip=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if apt-cache show python3-opengl >/dev/null 2>&1; then
|
||||||
|
echo "检测到系统级 python3-opengl,尝试通过 apt 安装(可选加速)。"
|
||||||
|
$SUDO apt-get install -y python3-opengl
|
||||||
|
else
|
||||||
|
echo "未检测到 python3-opengl,PyOpenGL 将在虚拟环境中通过 pip 安装。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "创建虚拟环境(如果不存在): $VENVDIR"
|
||||||
|
if [ ! -d "$VENVDIR" ]; then
|
||||||
|
python3 -m venv "$VENVDIR"
|
||||||
|
else
|
||||||
|
echo "虚拟环境已存在,跳过创建。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "激活虚拟环境并升级 pip..."
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$VENVDIR/bin/activate"
|
||||||
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
|
|
||||||
|
if [ "${install_pyqt_via_pip:-0}" = "1" ]; then
|
||||||
|
echo "在 venv 中通过 pip 安装 PyQt6(回退)..."
|
||||||
|
pip install PyQt6
|
||||||
|
fi
|
||||||
|
|
||||||
|
REQ_FILE="$SCRIPT_DIR/requirements.txt"
|
||||||
|
if [ -f "$REQ_FILE" ]; then
|
||||||
|
echo "通过 requirements.txt 安装 pip 依赖..."
|
||||||
|
pip install -r "$REQ_FILE"
|
||||||
|
else
|
||||||
|
echo "找不到 requirements.txt,安装常用依赖..."
|
||||||
|
pip install requests numpy PyOpenGL qrcode Pillow
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "\n依赖安装完成。要使用本项目:"
|
||||||
|
echo "1) 激活虚拟环境:source venv/bin/activate"
|
||||||
|
echo "2) 启动程序:python3 main.py"
|
||||||
|
echo "接下来进行 systemd 服务配置... (将会安装于 /opt/Printer_Screen_Menu)"
|
||||||
|
|
||||||
|
|
||||||
|
echo "Stopping existing service..."
|
||||||
|
systemctl disable printer-screen.service
|
||||||
|
systemctl stop printer-screen.service
|
||||||
|
|
||||||
echo "Installing Printer Screen Menu..."
|
|
||||||
echo "Cleaning up old installation..."
|
echo "Cleaning up old installation..."
|
||||||
rm -rf /etc/systemd/system/printer-screen.service
|
rm -rf /etc/systemd/system/printer-screen.service
|
||||||
rm -rf /opt/Printer_Screen_Menu
|
rm -rf /opt/Printer_Screen_Menu
|
||||||
@@ -16,7 +98,7 @@ cp -r ./. /opt/Printer_Screen_Menu/
|
|||||||
echo "Setting up systemd service..."
|
echo "Setting up systemd service..."
|
||||||
chmod +x /opt/Printer_Screen_Menu/run.sh
|
chmod +x /opt/Printer_Screen_Menu/run.sh
|
||||||
|
|
||||||
systemctl disable printer-screen.service
|
|
||||||
cp printer-screen.service /etc/systemd/system/
|
cp printer-screen.service /etc/systemd/system/
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable printer-screen.service
|
systemctl enable printer-screen.service
|
||||||
|
|||||||
320
main.py
@@ -1,13 +1,15 @@
|
|||||||
import sys
|
import sys
|
||||||
|
import base64
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QPushButton, QLabel, QStackedWidget)
|
QPushButton, QLabel, QStackedWidget, QProgressBar)
|
||||||
from PyQt6.QtCore import Qt, QSize, QTimer
|
from PyQt6.QtCore import Qt, QSize, QTimer
|
||||||
from PyQt6.QtGui import QIcon, QFont
|
from PyQt6.QtGui import QIcon, QFont, QPixmap
|
||||||
|
|
||||||
from pages.status_page import StatusPage
|
from pages.status_page import StatusPage
|
||||||
from pages.control_page import ControlPage
|
from pages.control_page import ControlPage
|
||||||
from pages.setting_page import SettingPage
|
from pages.setting_page import SettingPage
|
||||||
@@ -15,29 +17,243 @@ from utils.aio_print_api import AIOPrrintSystemAPI
|
|||||||
from utils.auto_fan_status import AutoFanStatus
|
from utils.auto_fan_status import AutoFanStatus
|
||||||
from utils.config_parse import ConfigParse
|
from utils.config_parse import ConfigParse
|
||||||
from utils.wifi_manager import WifiManager
|
from utils.wifi_manager import WifiManager
|
||||||
|
from utils.gcode_viewer import GCodeViewerWidget
|
||||||
|
from utils.get_bootstrap_icon import get_colored_svg_uri
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SplashWidget(QWidget):
|
||||||
|
"""开屏启动动画,显示 Logo 与初始化进度"""
|
||||||
|
|
||||||
|
def __init__(self, fix_width=None, fix_height=None, parent=None):
|
||||||
|
self.gcode_viewer = parent.gcode_viewer
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setStyleSheet("background-color: #222222;")
|
||||||
|
self.fix_width = fix_width
|
||||||
|
self.fix_height = fix_height
|
||||||
|
if self.fix_width is not None and self.fix_height is not None:
|
||||||
|
self.setFixedSize(self.fix_width,self.fix_height)
|
||||||
|
|
||||||
|
# 主布局
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.setContentsMargins(40,40,40,40)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
# ---- Logo ----
|
||||||
|
self.logo_label = QLabel()
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
logo_path = os.path.join(script_dir, "assets", "img", "logo.jpg")
|
||||||
|
if os.path.isfile(logo_path):
|
||||||
|
pixmap = QPixmap(logo_path)
|
||||||
|
scaled = pixmap.scaled(400, 400,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation)
|
||||||
|
self.logo_label.setPixmap(scaled)
|
||||||
|
else:
|
||||||
|
self.logo_label.setText("🖨️")
|
||||||
|
self.logo_label.setStyleSheet("font-size: 120px; color: #4CAF50;")
|
||||||
|
self.logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(self.logo_label)
|
||||||
|
|
||||||
|
# ---- 标题 ----
|
||||||
|
self.title_label = QLabel("Printer Screen Menu")
|
||||||
|
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.title_label.setStyleSheet(
|
||||||
|
"color: #e0e0e0; font-size: 36px; font-weight: 700;"
|
||||||
|
"padding: 10px 0 5px 0;"
|
||||||
|
)
|
||||||
|
layout.addWidget(self.title_label)
|
||||||
|
|
||||||
|
# ---- 副标题 / 版本 ----
|
||||||
|
self.subtitle_label = QLabel("正在启动 ...")
|
||||||
|
self.subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.subtitle_label.setStyleSheet(
|
||||||
|
"color: #888888; font-size: 18px; font-weight: 400;"
|
||||||
|
)
|
||||||
|
layout.addWidget(self.subtitle_label)
|
||||||
|
|
||||||
|
# ---- 进度条 ----
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setRange(0, 0) # 不确定模式(脉冲动画)
|
||||||
|
self.progress_bar.setFixedHeight(6)
|
||||||
|
self.progress_bar.setFixedWidth(500)
|
||||||
|
self.progress_bar.setTextVisible(False)
|
||||||
|
self.progress_bar.setStyleSheet("""
|
||||||
|
QProgressBar {
|
||||||
|
background-color: #333355;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
bar_container = QHBoxLayout()
|
||||||
|
bar_container.addStretch()
|
||||||
|
bar_container.addWidget(self.progress_bar)
|
||||||
|
bar_container.addStretch()
|
||||||
|
layout.addLayout(bar_container)
|
||||||
|
|
||||||
|
# ---- 进度文字 ----
|
||||||
|
self.progress_label = QLabel("正在加载配置...")
|
||||||
|
self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.progress_label.setStyleSheet(
|
||||||
|
"color: #a0d8a0; font-size: 22px; font-weight: 500; padding: 10px;"
|
||||||
|
)
|
||||||
|
layout.addWidget(self.progress_label)
|
||||||
|
layout.addWidget(self.gcode_viewer)
|
||||||
|
|
||||||
|
def update_progress(self, text: str):
|
||||||
|
"""更新进度文字并立即刷新界面"""
|
||||||
|
self.progress_label.setText(text)
|
||||||
|
if text.find("界面") != -1:
|
||||||
|
self.subtitle_label.setText("请稍候 (界面加载时间较长) ...")
|
||||||
|
else:
|
||||||
|
self.subtitle_label.setText("请稍候 ...")
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
class TopStacked(QWidget):
|
||||||
|
def __init__(self, fix_width=None, fix_height=None):
|
||||||
|
super().__init__()
|
||||||
|
self.root_layout = QVBoxLayout(self)
|
||||||
|
self.root_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.root_layout.setSpacing(0)
|
||||||
|
self.fix_width = fix_width
|
||||||
|
self.fix_height = fix_height
|
||||||
|
if self.fix_width is not None and self.fix_height is not None:
|
||||||
|
self.setFixedSize(self.fix_width,self.fix_height)
|
||||||
|
|
||||||
|
self.root_stack = QStackedWidget()
|
||||||
|
self.root_stack.setStyleSheet("background-color: #222222;") # 深灰色主显示区
|
||||||
|
|
||||||
|
# !Important: 先初始化带OpenGL的东西防止 Could not queue DRM page flip on screen HDMI1 (Device or resource busy)
|
||||||
|
self.gcode_viewer = GCodeViewerWidget()
|
||||||
|
self.gcode_viewer.setUpdatesEnabled(False)
|
||||||
|
self.gcode_viewer.hide()
|
||||||
|
|
||||||
|
# 1. 模拟的启动画面
|
||||||
|
self.splash_widget = SplashWidget(fix_width=self.fix_width, fix_height=self.fix_height, parent=self)
|
||||||
|
self.root_stack.addWidget(self.splash_widget)
|
||||||
|
|
||||||
|
# 2. 真实主页面容器
|
||||||
|
self.main_content = MainWindow(fix_width=self.fix_width, fix_height=self.fix_height, parent=self)
|
||||||
|
self.root_stack.addWidget(self.main_content)
|
||||||
|
|
||||||
|
self.root_layout.addWidget(self.root_stack)
|
||||||
|
# self.root_layout.addWidget(self.root_container)
|
||||||
|
|
||||||
|
# 初始显示启动界面
|
||||||
|
# self.root_stack.setCurrentWidget(self.splash_widget)
|
||||||
|
self.root_stack.setCurrentIndex(0)
|
||||||
|
|
||||||
|
|
||||||
|
QTimer.singleShot(100, self.main_content.start_init)
|
||||||
|
# self.main_content.start_init()
|
||||||
|
# self.setLayout(self.root_layout)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QWidget):
|
class MainWindow(QWidget):
|
||||||
def __init__(self):
|
def __init__(self, fix_width=None, fix_height=None, parent=None):
|
||||||
super().__init__()
|
super().__init__(parent)
|
||||||
# 初始化 API 客户端
|
self.fix_width = fix_width
|
||||||
|
self.fix_height = fix_height
|
||||||
|
if self.fix_width is not None and self.fix_height is not None:
|
||||||
|
self.setFixedSize(self.fix_width,self.fix_height)
|
||||||
|
|
||||||
|
self.splash_widget = parent.splash_widget
|
||||||
|
self.root_stack = parent.root_stack
|
||||||
|
self.gcode_viewer = parent.gcode_viewer
|
||||||
|
|
||||||
|
self._last_network_check = 0.0
|
||||||
|
self._is_network_connected = False
|
||||||
|
self._clock_has_synced = False
|
||||||
|
|
||||||
|
def start_init(self):
|
||||||
|
"""真正的初始化逻辑,使用定时器链式调用,避免卡死主线程"""
|
||||||
|
self.splash_widget.update_progress("正在加载配置...")
|
||||||
|
QTimer.singleShot(100, self._init_step1)
|
||||||
|
|
||||||
|
def _init_step1(self):
|
||||||
self.config_parser = ConfigParse()
|
self.config_parser = ConfigParse()
|
||||||
self.config_parser.config_changed.connect(self._on_config_changed)
|
self.config_parser.config_changed.connect(self._on_config_changed)
|
||||||
|
self.splash_widget.update_progress("正在初始化 API 客户端...")
|
||||||
|
QTimer.singleShot(600, self._init_step2)
|
||||||
|
|
||||||
|
def _init_step2(self):
|
||||||
self.api_client = AIOPrrintSystemAPI(
|
self.api_client = AIOPrrintSystemAPI(
|
||||||
api_url=self.config_parser.api_url,
|
api_url=self.config_parser.api_url,
|
||||||
api_key=self.config_parser.api_key
|
api_key=self.config_parser.api_key
|
||||||
)
|
)
|
||||||
|
self.splash_widget.update_progress("正在检测散热风扇...")
|
||||||
|
QTimer.singleShot(600, self._init_step3)
|
||||||
|
|
||||||
|
def _init_step3(self):
|
||||||
self.auto_fan_status = AutoFanStatus()
|
self.auto_fan_status = AutoFanStatus()
|
||||||
self.wifi_manager = WifiManager()
|
self.splash_widget.update_progress("正在检查网络连接...")
|
||||||
self._last_network_check = 0.0
|
QTimer.singleShot(600, self._init_step4)
|
||||||
self._is_network_connected = False
|
|
||||||
self._clock_has_synced = False # 是否曾成功获取到时间(断网后继续显示)
|
|
||||||
self.init_ui()
|
|
||||||
|
|
||||||
|
def _init_step4(self):
|
||||||
|
self.wifi_manager = WifiManager()
|
||||||
|
self.splash_widget.update_progress("正在初始化主框架...")
|
||||||
|
QTimer.singleShot(100, self._init_step5)
|
||||||
|
|
||||||
|
def _init_step5(self):
|
||||||
|
# 整体布局附加到 main_content 占位符
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 顶部主显示区 (使用 QStackedWidget 切换不同页面)
|
||||||
|
self.stacked_widget = QStackedWidget()
|
||||||
|
self.stacked_widget.setStyleSheet("background-color: #555555;") # 深灰色主显示区
|
||||||
|
|
||||||
|
self.splash_widget.update_progress("正在加载状态界面...")
|
||||||
|
QTimer.singleShot(100, self._init_step5_status)
|
||||||
|
|
||||||
|
def _init_step5_status(self):
|
||||||
|
self.page_status = StatusPage(self.api_client, GcodeViewer=self.gcode_viewer)
|
||||||
|
self.stacked_widget.addWidget(self.page_status)
|
||||||
|
|
||||||
|
self.splash_widget.update_progress("正在加载控制界面...")
|
||||||
|
QTimer.singleShot(100, self._init_step5_control)
|
||||||
|
|
||||||
|
def _init_step5_control(self):
|
||||||
|
self.page_control = ControlPage(self.api_client)
|
||||||
|
self.stacked_widget.addWidget(self.page_control)
|
||||||
|
|
||||||
|
self.splash_widget.update_progress("正在加载设置界面...")
|
||||||
|
QTimer.singleShot(100, self._init_step5_settings)
|
||||||
|
|
||||||
|
def _init_step5_settings(self):
|
||||||
|
self.page_settings = SettingPage(self.api_client)
|
||||||
|
self.stacked_widget.addWidget(self.page_settings)
|
||||||
|
|
||||||
|
self.splash_widget.update_progress("正在组合界面...")
|
||||||
|
QTimer.singleShot(100, self._init_step5_finish)
|
||||||
|
|
||||||
|
def _init_step5_finish(self):
|
||||||
|
self.build_ui_bars()
|
||||||
|
|
||||||
# 定时刷新风扇状态显示
|
# 定时刷新风扇状态显示
|
||||||
self._fan_timer = QTimer(self)
|
self._fan_timer = QTimer(self)
|
||||||
self._fan_timer.timeout.connect(self._update_top_bar)
|
self._fan_timer.timeout.connect(self._update_top_bar)
|
||||||
self._fan_timer.start(1000)
|
self._fan_timer.start(1000)
|
||||||
|
self._update_top_bar() # 立即刷新横条数据
|
||||||
|
|
||||||
|
self.splash_widget.update_progress("启动完成!")
|
||||||
|
QTimer.singleShot(600, self._init_step6)
|
||||||
|
|
||||||
|
def _init_step6(self):
|
||||||
|
# 切换到真实主界面
|
||||||
|
self.root_stack.setCurrentIndex(1)
|
||||||
|
self.gcode_viewer.setUpdatesEnabled(True)
|
||||||
|
self.gcode_viewer.show()
|
||||||
|
|
||||||
|
|
||||||
def _check_network(self):
|
def _check_network(self):
|
||||||
"""检查网络连接状态(每30秒检测一次,避免频繁调用)"""
|
"""检查网络连接状态(每30秒检测一次,避免频繁调用)"""
|
||||||
@@ -63,28 +279,33 @@ class MainWindow(QWidget):
|
|||||||
"""将信号强度转换为条形图标字符串
|
"""将信号强度转换为条形图标字符串
|
||||||
支持 dBm(负值,如 -45)和百分比(0-100,nmcli 格式)
|
支持 dBm(负值,如 -45)和百分比(0-100,nmcli 格式)
|
||||||
"""
|
"""
|
||||||
|
icon_name = "reception-0.svg"
|
||||||
if signal_val is None:
|
if signal_val is None:
|
||||||
return "⬜⬜⬜"
|
icon_name = "reception-0.svg"
|
||||||
if signal_val < 0:
|
elif signal_val < 0:
|
||||||
# dBm 格式
|
# dBm 格式
|
||||||
if signal_val >= -50:
|
if signal_val >= -50:
|
||||||
return "■■■"
|
icon_name = "reception-4.svg"
|
||||||
elif signal_val >= -60:
|
elif signal_val >= -60:
|
||||||
return "■■□"
|
icon_name = "reception-2.svg"
|
||||||
elif signal_val >= -70:
|
elif signal_val >= -70:
|
||||||
return "■□□"
|
icon_name = "reception-1.svg"
|
||||||
else:
|
else:
|
||||||
return "□□□"
|
icon_name = "reception-0.svg"
|
||||||
else:
|
else:
|
||||||
# 百分比格式 (0-100)
|
# 百分比格式 (0-100)
|
||||||
if signal_val >= 75:
|
if signal_val >= 75:
|
||||||
return "■■■"
|
icon_name = "reception-4.svg"
|
||||||
elif signal_val >= 50:
|
elif signal_val >= 50:
|
||||||
return "■■□"
|
icon_name = "reception-2.svg"
|
||||||
elif signal_val >= 25:
|
elif signal_val >= 25:
|
||||||
return "■□□"
|
icon_name = "reception-1.svg"
|
||||||
else:
|
else:
|
||||||
return "□□□"
|
icon_name = "reception-0.svg"
|
||||||
|
|
||||||
|
# 为了能够在深色主题正确显示颜色,通过图片标签引入
|
||||||
|
colored_uri = get_colored_svg_uri(icon_name, "#a0d8a0", 40, 20, "0 -4 18 18")
|
||||||
|
return f"<img src='{colored_uri}'>"
|
||||||
|
|
||||||
def _update_top_bar(self):
|
def _update_top_bar(self):
|
||||||
"""更新风扇/网络状态横条显示"""
|
"""更新风扇/网络状态横条显示"""
|
||||||
@@ -107,10 +328,17 @@ class MainWindow(QWidget):
|
|||||||
f"background-color: #2a2a2a; color: {color}; "
|
f"background-color: #2a2a2a; color: {color}; "
|
||||||
f"font-size: 18px; font-weight: 600; padding: 4px 16px;"
|
f"font-size: 18px; font-weight: 600; padding: 4px 16px;"
|
||||||
)
|
)
|
||||||
load_color = "#a0d8a0" if s.cpu_load < 1.0 else ("#e8a060" if s.cpu_load < 2.0 else "#e86c60")
|
load_color = "#a0d8a0" if s.cpu_load < 1.0 else ("#e8a060" if s.cpu_load < 3.0 else "#e86c60")
|
||||||
cpu_load_str = f"<span style='color:{load_color}'>{s.cpu_load:.2f}</span><span>/4.0</span>"
|
cpu_load_str = f"<span style='color:{load_color}'>{s.cpu_load:.2f}</span><span>/4.0</span>"
|
||||||
|
|
||||||
|
icon_temp = get_colored_svg_uri("thermometer-half.svg", color, 20, 20, "-4 -2 18 18")
|
||||||
|
icon_fan = get_colored_svg_uri("fan.svg", color, 20, 20, "-2 -2 18 18")
|
||||||
|
icon_cpu = get_colored_svg_uri("cpu.svg", load_color, 20, 20, "-2 -2 18 18")
|
||||||
|
|
||||||
self._fan_label.setText(
|
self._fan_label.setText(
|
||||||
f"🌡 {temp} {state} 𖣘 {speed} {rpm} 🖥 {cpu_load_str}"
|
f"<img src='{icon_temp}'> {temp} {state} "
|
||||||
|
f"<img src='{icon_fan}'> {speed} {rpm} "
|
||||||
|
f"<img src='{icon_cpu}'> {cpu_load_str}"
|
||||||
)
|
)
|
||||||
self._fan_label.setTextFormat(Qt.TextFormat.RichText)
|
self._fan_label.setTextFormat(Qt.TextFormat.RichText)
|
||||||
|
|
||||||
@@ -125,6 +353,7 @@ class MainWindow(QWidget):
|
|||||||
signal_dbm = None
|
signal_dbm = None
|
||||||
bars = self._signal_to_bars(signal_dbm)
|
bars = self._signal_to_bars(signal_dbm)
|
||||||
self._wifi_label.setText(f"Signal: {bars}")
|
self._wifi_label.setText(f"Signal: {bars}")
|
||||||
|
self._wifi_label.setTextFormat(Qt.TextFormat.RichText)
|
||||||
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
||||||
else:
|
else:
|
||||||
self._wifi_label.setText("No Signal")
|
self._wifi_label.setText("No Signal")
|
||||||
@@ -144,25 +373,7 @@ class MainWindow(QWidget):
|
|||||||
else:
|
else:
|
||||||
self._clock_label.hide()
|
self._clock_label.hide()
|
||||||
|
|
||||||
def init_ui(self):
|
def build_ui_bars(self):
|
||||||
# 整体布局
|
|
||||||
main_layout = QVBoxLayout(self)
|
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
main_layout.setSpacing(0)
|
|
||||||
|
|
||||||
# 顶部主显示区 (使用 QStackedWidget 切换不同页面)
|
|
||||||
self.stacked_widget = QStackedWidget()
|
|
||||||
self.stacked_widget.setStyleSheet("background-color: #555555;") # 深灰色主显示区
|
|
||||||
|
|
||||||
# 添加测试页面
|
|
||||||
self.page_status = StatusPage(self.api_client)
|
|
||||||
self.page_control = ControlPage(self.api_client)
|
|
||||||
self.page_settings = SettingPage(self.api_client)
|
|
||||||
|
|
||||||
self.stacked_widget.addWidget(self.page_status)
|
|
||||||
self.stacked_widget.addWidget(self.page_control)
|
|
||||||
self.stacked_widget.addWidget(self.page_settings)
|
|
||||||
|
|
||||||
# 风扇状态横条
|
# 风扇状态横条
|
||||||
self._top_bar = QWidget()
|
self._top_bar = QWidget()
|
||||||
self._top_bar.setFixedHeight(36)
|
self._top_bar.setFixedHeight(36)
|
||||||
@@ -178,8 +389,10 @@ class MainWindow(QWidget):
|
|||||||
top_layout.addStretch()
|
top_layout.addStretch()
|
||||||
|
|
||||||
# WiFi 状态指示
|
# WiFi 状态指示
|
||||||
self._wifi_label = QLabel("📶 --")
|
init_wifi_uri = get_colored_svg_uri('reception-0.svg', '#a0d8a0', 40, 20, "0 -4 18 18")
|
||||||
|
self._wifi_label = QLabel(f"<img src='{init_wifi_uri}'> --")
|
||||||
self._wifi_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
self._wifi_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self._wifi_label.setTextFormat(Qt.TextFormat.RichText)
|
||||||
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
||||||
top_layout.addWidget(self._wifi_label)
|
top_layout.addWidget(self._wifi_label)
|
||||||
|
|
||||||
@@ -211,11 +424,10 @@ class MainWindow(QWidget):
|
|||||||
bottom_layout.addWidget(self.btn_settings)
|
bottom_layout.addWidget(self.btn_settings)
|
||||||
|
|
||||||
# 将主显示区、风扇横条、底部按钮加入整体布局
|
# 将主显示区、风扇横条、底部按钮加入整体布局
|
||||||
main_layout.addWidget(self._top_bar)
|
self.main_layout.addWidget(self._top_bar)
|
||||||
main_layout.addWidget(self.stacked_widget)
|
self.main_layout.addWidget(self.stacked_widget)
|
||||||
main_layout.addWidget(bottom_widget)
|
self.main_layout.addWidget(bottom_widget)
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
|
||||||
self.setStyleSheet("background-color: #666666;") # 整体灰色背景
|
self.setStyleSheet("background-color: #666666;") # 整体灰色背景
|
||||||
|
|
||||||
def create_nav_button(self, text, callback):
|
def create_nav_button(self, text, callback):
|
||||||
@@ -255,9 +467,19 @@ def main():
|
|||||||
# 隐藏鼠标光标,针对触摸屏优化
|
# 隐藏鼠标光标,针对触摸屏优化
|
||||||
app.setOverrideCursor(Qt.CursorShape.BlankCursor)
|
app.setOverrideCursor(Qt.CursorShape.BlankCursor)
|
||||||
|
|
||||||
window = MainWindow()
|
# # 唯一且纯粹的顶级窗口
|
||||||
|
# window = MainWindow()
|
||||||
|
# window.showFullScreen()
|
||||||
|
|
||||||
# 全屏无边框显示
|
# # 待窗口在底层完全拿到 Plane 并开始渲染后,触发后续耗时初始化
|
||||||
|
# QTimer.singleShot(100, window.start_init)
|
||||||
|
|
||||||
|
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||||
|
screen = QApplication.primaryScreen()
|
||||||
|
fix_width = screen.size().width()
|
||||||
|
fix_height = screen.size().height()
|
||||||
|
|
||||||
|
window = TopStacked(fix_width,fix_height)
|
||||||
window.showFullScreen()
|
window.showFullScreen()
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from utils.floating_keyboard import FloatingKeyboard
|
|||||||
|
|
||||||
MOVE_STEP = 10 # 每次点击移动 mm (保留备用)
|
MOVE_STEP = 10 # 每次点击移动 mm (保留备用)
|
||||||
|
|
||||||
MOVE_DETECT_MS = 100
|
MOVE_DETECT_MS = 300
|
||||||
|
|
||||||
|
|
||||||
class JoystickWidget(QWidget):
|
class JoystickWidget(QWidget):
|
||||||
@@ -32,7 +32,7 @@ class JoystickWidget(QWidget):
|
|||||||
|
|
||||||
# 定时发送 GCode
|
# 定时发送 GCode
|
||||||
self._move_timer = QTimer(self)
|
self._move_timer = QTimer(self)
|
||||||
self._move_timer.setInterval(120) # ~8次/秒
|
self._move_timer.setInterval(MOVE_DETECT_MS)
|
||||||
self._move_timer.timeout.connect(self._emit_move)
|
self._move_timer.timeout.connect(self._emit_move)
|
||||||
|
|
||||||
# 回调(上层设置)
|
# 回调(上层设置)
|
||||||
@@ -45,7 +45,9 @@ class JoystickWidget(QWidget):
|
|||||||
return
|
return
|
||||||
ratio = min(dist / (self._radius_outer - self._radius_inner), 1.0)
|
ratio = min(dist / (self._radius_outer - self._radius_inner), 1.0)
|
||||||
# 速度映射:30% ~ 100% * max_speed
|
# 速度映射:30% ~ 100% * max_speed
|
||||||
speed = int((0.3 + ratio * 0.7) * self._max_speed)
|
# speed = int((0.3 + ratio * 0.7) * self._max_speed)
|
||||||
|
# ratio = min(dist / max_dist, 1.0)
|
||||||
|
speed = ratio * self._max_speed / (1000/MOVE_DETECT_MS)
|
||||||
if dist > 0:
|
if dist > 0:
|
||||||
nx = self._dx / dist
|
nx = self._dx / dist
|
||||||
ny = self._dy / dist
|
ny = self._dy / dist
|
||||||
@@ -350,12 +352,13 @@ class ControlPage(QWidget):
|
|||||||
def _on_joystick_move(self, nx, ny, speed):
|
def _on_joystick_move(self, nx, ny, speed):
|
||||||
if not self._is_control_enabled():
|
if not self._is_control_enabled():
|
||||||
return
|
return
|
||||||
step = 5.0
|
step = speed * MOVE_DETECT_MS / 1000
|
||||||
dx = nx * step
|
dx = nx * step
|
||||||
dy = ny * step
|
dy = ny * step
|
||||||
tx = max(self.x_min, min(self.x_max, self.pos_x + dx))
|
tx = max(self.x_min, min(self.x_max, self.pos_x + dx))
|
||||||
ty = max(self.y_min, min(self.y_max, self.pos_y + dy))
|
ty = max(self.y_min, min(self.y_max, self.pos_y + dy))
|
||||||
gcode = f"G1 X{tx:.1f} Y{ty:.1f} F{speed}"
|
gcode = f"G1 X{tx:.1f} Y{ty:.1f} F{speed}"
|
||||||
|
print(f"Moving: X{tx:.1f} Y{ty:.1f} F{speed}")
|
||||||
self.api_client.send_gcode(gcode)
|
self.api_client.send_gcode(gcode)
|
||||||
self.pos_x, self.pos_y = tx, ty
|
self.pos_x, self.pos_y = tx, ty
|
||||||
self._sync_inputs()
|
self._sync_inputs()
|
||||||
@@ -363,11 +366,11 @@ class ControlPage(QWidget):
|
|||||||
def _on_fader_move(self, direction, speed):
|
def _on_fader_move(self, direction, speed):
|
||||||
if not self._is_control_enabled():
|
if not self._is_control_enabled():
|
||||||
return
|
return
|
||||||
print(f"d:{direction} s:{speed}")
|
|
||||||
step = speed * MOVE_DETECT_MS / 1000
|
step = speed * MOVE_DETECT_MS / 1000
|
||||||
tz = self.pos_z + direction * step
|
tz = self.pos_z + direction * step
|
||||||
tz = max(self.z_min, min(self.z_max, tz))
|
tz = max(self.z_min, min(self.z_max, tz))
|
||||||
gcode = f"G1 Z{tz:.3f} F{speed}"
|
gcode = f"G1 Z{tz:.3f} F{speed}"
|
||||||
|
print(f"Moving: Z{tz:.3f} F{speed}")
|
||||||
self.api_client.send_gcode(gcode)
|
self.api_client.send_gcode(gcode)
|
||||||
self.pos_z = tz
|
self.pos_z = tz
|
||||||
self._sync_inputs()
|
self._sync_inputs()
|
||||||
|
|||||||
@@ -18,14 +18,16 @@ from PyQt6.QtWidgets import (
|
|||||||
QCheckBox,
|
QCheckBox,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, QSize, pyqtSignal
|
from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, QSize, pyqtSignal
|
||||||
from PyQt6.QtGui import QMouseEvent, QPixmap, QImage, QFont
|
from PyQt6.QtGui import QMouseEvent, QPixmap, QImage, QFont, QIcon
|
||||||
import codecs
|
import codecs
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import qrcode
|
import qrcode
|
||||||
|
import re
|
||||||
from utils.wifi_manager import WifiManager
|
from utils.wifi_manager import WifiManager
|
||||||
from utils.floating_keyboard import FloatingKeyboard
|
from utils.floating_keyboard import FloatingKeyboard
|
||||||
|
from utils.get_bootstrap_icon import get_colored_icon, get_colored_pixmap
|
||||||
|
|
||||||
|
|
||||||
class DragScrollArea(QScrollArea):
|
class DragScrollArea(QScrollArea):
|
||||||
@@ -318,7 +320,15 @@ class SettingPage(QWidget):
|
|||||||
def _styled_message(icon, parent, title, text, buttons=QMessageBox.StandardButton.Ok):
|
def _styled_message(icon, parent, title, text, buttons=QMessageBox.StandardButton.Ok):
|
||||||
"""显示与整体风格一致的暗色主题消息框"""
|
"""显示与整体风格一致的暗色主题消息框"""
|
||||||
msg = QMessageBox(parent)
|
msg = QMessageBox(parent)
|
||||||
msg.setIcon(icon)
|
if isinstance(icon, str):
|
||||||
|
if icon == "info":
|
||||||
|
msg.setIconPixmap(get_colored_pixmap("info-circle.svg", "#f8f8f8", 40, 40))
|
||||||
|
elif icon == "warning":
|
||||||
|
msg.setIconPixmap(get_colored_pixmap("exclamation-circle.svg", "#f8f804", 40, 40))
|
||||||
|
elif icon == "error":
|
||||||
|
msg.setIconPixmap(get_colored_pixmap("x-circle.svg", "#f80404", 40, 40))
|
||||||
|
else:
|
||||||
|
msg.setIcon(icon)
|
||||||
msg.setWindowTitle(title)
|
msg.setWindowTitle(title)
|
||||||
msg.setText(text)
|
msg.setText(text)
|
||||||
msg.setStandardButtons(buttons)
|
msg.setStandardButtons(buttons)
|
||||||
@@ -332,8 +342,9 @@ class SettingPage(QWidget):
|
|||||||
}
|
}
|
||||||
QMessageBox QLabel {
|
QMessageBox QLabel {
|
||||||
color: #f2f2f2;
|
color: #f2f2f2;
|
||||||
font-size: 20px;
|
font-size: 30px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
background-color: #3f3f3f;
|
||||||
}
|
}
|
||||||
QMessageBox QPushButton {
|
QMessageBox QPushButton {
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
@@ -819,12 +830,12 @@ class SettingPage(QWidget):
|
|||||||
if not ssid:
|
if not ssid:
|
||||||
self.hotspot_toggle.setChecked(False)
|
self.hotspot_toggle.setChecked(False)
|
||||||
self.hotspot_toggle.blockSignals(False)
|
self.hotspot_toggle.blockSignals(False)
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请输入热点名称")
|
self._styled_message("waring", self, "提示", "请输入热点名称")
|
||||||
return
|
return
|
||||||
if len(password) < 8:
|
if len(password) < 8:
|
||||||
self.hotspot_toggle.setChecked(False)
|
self.hotspot_toggle.setChecked(False)
|
||||||
self.hotspot_toggle.blockSignals(False)
|
self.hotspot_toggle.blockSignals(False)
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "密码至少需要8位")
|
self._styled_message("waring", self, "提示", "密码至少需要8位")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 按钮UI反馈 → 显示"开启中……"
|
# 按钮UI反馈 → 显示"开启中……"
|
||||||
@@ -871,7 +882,7 @@ class SettingPage(QWidget):
|
|||||||
self.hotspot_password.setEnabled(False)
|
self.hotspot_password.setEnabled(False)
|
||||||
self._generate_qr_code(ssid, self.hotspot_password.text().strip())
|
self._generate_qr_code(ssid, self.hotspot_password.text().strip())
|
||||||
else:
|
else:
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "开启热点失败: wpa_cli 返回失败")
|
self._styled_message("error", self, "错误", "开启热点失败: wpa_cli 返回失败")
|
||||||
self.hotspot_toggle.blockSignals(True)
|
self.hotspot_toggle.blockSignals(True)
|
||||||
self.hotspot_toggle.setChecked(False)
|
self.hotspot_toggle.setChecked(False)
|
||||||
self.hotspot_toggle.blockSignals(False)
|
self.hotspot_toggle.blockSignals(False)
|
||||||
@@ -879,7 +890,7 @@ class SettingPage(QWidget):
|
|||||||
self.hotspot_toggle.setEnabled(True)
|
self.hotspot_toggle.setEnabled(True)
|
||||||
|
|
||||||
def _on_hotspot_open_error(self, err_msg):
|
def _on_hotspot_open_error(self, err_msg):
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"开启热点失败: {err_msg}")
|
self._styled_message("error", self, "错误", f"开启热点失败: {err_msg}")
|
||||||
self.hotspot_toggle.blockSignals(True)
|
self.hotspot_toggle.blockSignals(True)
|
||||||
self.hotspot_toggle.setChecked(False)
|
self.hotspot_toggle.setChecked(False)
|
||||||
self.hotspot_toggle.blockSignals(False)
|
self.hotspot_toggle.blockSignals(False)
|
||||||
@@ -904,7 +915,7 @@ class SettingPage(QWidget):
|
|||||||
self.qr_label.clear()
|
self.qr_label.clear()
|
||||||
self.qr_hint.setText("开启热点后自动生成二维码")
|
self.qr_hint.setText("开启热点后自动生成二维码")
|
||||||
self.hotspot_toggle.setEnabled(True)
|
self.hotspot_toggle.setEnabled(True)
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", f"关闭热点时出现异常: {err_msg}")
|
self._styled_message("waring", self, "提示", f"关闭热点时出现异常: {err_msg}")
|
||||||
|
|
||||||
def _generate_qr_code(self, ssid, password):
|
def _generate_qr_code(self, ssid, password):
|
||||||
"""生成 WiFi 二维码并显示"""
|
"""生成 WiFi 二维码并显示"""
|
||||||
@@ -944,7 +955,9 @@ class SettingPage(QWidget):
|
|||||||
power_layout.addStretch()
|
power_layout.addStretch()
|
||||||
|
|
||||||
# 重启按钮
|
# 重启按钮
|
||||||
reboot_btn = QPushButton("↻ 重启系统")
|
reboot_btn = QPushButton(" 重启系统")
|
||||||
|
reboot_btn.setIcon(get_colored_icon("arrow-clockwise.svg", "#ffffff"))
|
||||||
|
reboot_btn.setIconSize(QSize(26, 26))
|
||||||
reboot_btn.setStyleSheet("""
|
reboot_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
@@ -955,6 +968,7 @@ class SettingPage(QWidget):
|
|||||||
border: 2px solid #4a9fc8;
|
border: 2px solid #4a9fc8;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
|
text-align: middle;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #3a85b3;
|
background-color: #3a85b3;
|
||||||
@@ -971,7 +985,9 @@ class SettingPage(QWidget):
|
|||||||
power_layout.addSpacing(20)
|
power_layout.addSpacing(20)
|
||||||
|
|
||||||
# 关机按钮
|
# 关机按钮
|
||||||
shutdown_btn = QPushButton("⏻ 关机")
|
shutdown_btn = QPushButton(" 关机")
|
||||||
|
shutdown_btn.setIcon(get_colored_icon("power.svg", "#ffffff"))
|
||||||
|
shutdown_btn.setIconSize(QSize(26, 26))
|
||||||
shutdown_btn.setStyleSheet("""
|
shutdown_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
@@ -982,6 +998,7 @@ class SettingPage(QWidget):
|
|||||||
border: 2px solid #e74c3c;
|
border: 2px solid #e74c3c;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
|
text-align: middle;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #e74c3c;
|
background-color: #e74c3c;
|
||||||
@@ -1007,7 +1024,7 @@ class SettingPage(QWidget):
|
|||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
self._styled_message(
|
self._styled_message(
|
||||||
QMessageBox.Icon.Information, self, "重启",
|
"info", self, "重启",
|
||||||
"系统正在重启..."
|
"系统正在重启..."
|
||||||
)
|
)
|
||||||
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
|
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
|
||||||
@@ -1021,7 +1038,7 @@ class SettingPage(QWidget):
|
|||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
self._styled_message(
|
self._styled_message(
|
||||||
QMessageBox.Icon.Information, self, "关机",
|
"info", self, "关机",
|
||||||
"系统正在关机..."
|
"系统正在关机..."
|
||||||
)
|
)
|
||||||
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
|
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
|
||||||
@@ -1070,7 +1087,7 @@ class SettingPage(QWidget):
|
|||||||
try:
|
try:
|
||||||
saved_networks = self.wifi_manager.list_saved_networks()
|
saved_networks = self.wifi_manager.list_saved_networks()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"无法加载保存的WiFi: {str(e)}")
|
self._styled_message("error", self, "错误", f"无法加载保存的WiFi: {str(e)}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 比较与上次缓存的网络列表是否有变化
|
# 比较与上次缓存的网络列表是否有变化
|
||||||
@@ -1122,7 +1139,7 @@ class SettingPage(QWidget):
|
|||||||
|
|
||||||
processed = self._deduplicate_networks(networks)
|
processed = self._deduplicate_networks(networks)
|
||||||
if not processed:
|
if not processed:
|
||||||
self._styled_message(QMessageBox.Icon.Information, self, "提示", "未扫描到可用网络")
|
self._styled_message("info", self, "提示", "未扫描到可用网络")
|
||||||
return
|
return
|
||||||
|
|
||||||
for network in processed:
|
for network in processed:
|
||||||
@@ -1179,7 +1196,7 @@ class SettingPage(QWidget):
|
|||||||
"""扫描出错后的UI恢复(主线程中执行)"""
|
"""扫描出错后的UI恢复(主线程中执行)"""
|
||||||
self.scan_button.setEnabled(True)
|
self.scan_button.setEnabled(True)
|
||||||
self.scan_button.setText("扫描网络")
|
self.scan_button.setText("扫描网络")
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"扫描网络失败: {error_msg}")
|
self._styled_message("error", self, "错误", f"扫描网络失败: {error_msg}")
|
||||||
|
|
||||||
def _on_ssid_text_changed(self, text):
|
def _on_ssid_text_changed(self, text):
|
||||||
"""SSID输入框文本变化时,如果清空则重置为开放网络"""
|
"""SSID输入框文本变化时,如果清空则重置为开放网络"""
|
||||||
@@ -1314,13 +1331,13 @@ class SettingPage(QWidget):
|
|||||||
"""连接已保存列表中选中的网络(后台线程)"""
|
"""连接已保存列表中选中的网络(后台线程)"""
|
||||||
item = self.saved_wifi_list.currentItem()
|
item = self.saved_wifi_list.currentItem()
|
||||||
if item is None:
|
if item is None:
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
|
self._styled_message("waring", self, "提示", "请先选择一个已保存网络")
|
||||||
return
|
return
|
||||||
network = item.data(Qt.ItemDataRole.UserRole) or {}
|
network = item.data(Qt.ItemDataRole.UserRole) or {}
|
||||||
network_id = network.get("network_id")
|
network_id = network.get("network_id")
|
||||||
ssid = network.get("ssid", "")
|
ssid = network.get("ssid", "")
|
||||||
if network_id is None:
|
if network_id is None:
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效")
|
self._styled_message("waring", self, "提示", "选中网络无效")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 按钮UI反馈
|
# 按钮UI反馈
|
||||||
@@ -1344,27 +1361,27 @@ class SettingPage(QWidget):
|
|||||||
self.connect_saved_button.setEnabled(True)
|
self.connect_saved_button.setEnabled(True)
|
||||||
self.connect_saved_button.setText("连接到此网络")
|
self.connect_saved_button.setText("连接到此网络")
|
||||||
if ok:
|
if ok:
|
||||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已连接: {ssid}")
|
self._styled_message("info", self, "成功", f"已连接: {ssid}")
|
||||||
else:
|
else:
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接失败")
|
self._styled_message("error", self, "错误", "连接失败")
|
||||||
self.refresh_saved_wifi()
|
self.refresh_saved_wifi()
|
||||||
self.refresh_current_status()
|
self.refresh_current_status()
|
||||||
|
|
||||||
def _on_saved_connect_error(self, err_msg):
|
def _on_saved_connect_error(self, err_msg):
|
||||||
self.connect_saved_button.setEnabled(True)
|
self.connect_saved_button.setEnabled(True)
|
||||||
self.connect_saved_button.setText("连接到此网络")
|
self.connect_saved_button.setText("连接到此网络")
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接失败: {err_msg}")
|
self._styled_message("error", self, "错误", f"连接失败: {err_msg}")
|
||||||
|
|
||||||
def remove_selected_saved_wifi(self):
|
def remove_selected_saved_wifi(self):
|
||||||
item = self.saved_wifi_list.currentItem()
|
item = self.saved_wifi_list.currentItem()
|
||||||
if item is None:
|
if item is None:
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
|
self._styled_message("waring", self, "提示", "请先选择一个已保存网络")
|
||||||
return
|
return
|
||||||
network = item.data(Qt.ItemDataRole.UserRole) or {}
|
network = item.data(Qt.ItemDataRole.UserRole) or {}
|
||||||
network_id = network.get("network_id")
|
network_id = network.get("network_id")
|
||||||
ssid = network.get("ssid", "")
|
ssid = network.get("ssid", "")
|
||||||
if network_id is None:
|
if network_id is None:
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效,无法删除")
|
self._styled_message("waring", self, "提示", "选中网络无效,无法删除")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 按钮UI反馈
|
# 按钮UI反馈
|
||||||
@@ -1388,14 +1405,14 @@ class SettingPage(QWidget):
|
|||||||
self.remove_saved_button.setEnabled(True)
|
self.remove_saved_button.setEnabled(True)
|
||||||
self.remove_saved_button.setText("删除选中")
|
self.remove_saved_button.setText("删除选中")
|
||||||
if ok:
|
if ok:
|
||||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已删除网络: {ssid}")
|
self._styled_message("info", self, "成功", f"已删除网络: {ssid}")
|
||||||
self.refresh_saved_wifi()
|
self.refresh_saved_wifi()
|
||||||
self.refresh_current_status()
|
self.refresh_current_status()
|
||||||
|
|
||||||
def _on_remove_error(self, err_msg):
|
def _on_remove_error(self, err_msg):
|
||||||
self.remove_saved_button.setEnabled(True)
|
self.remove_saved_button.setEnabled(True)
|
||||||
self.remove_saved_button.setText("删除选中")
|
self.remove_saved_button.setText("删除选中")
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"删除失败: {err_msg}")
|
self._styled_message("error", self, "错误", f"删除失败: {err_msg}")
|
||||||
|
|
||||||
def connect_to_wifi(self):
|
def connect_to_wifi(self):
|
||||||
ssid = self.ssid_input.text().strip()
|
ssid = self.ssid_input.text().strip()
|
||||||
@@ -1404,15 +1421,15 @@ class SettingPage(QWidget):
|
|||||||
auth_mode = self.auth_combo.currentData()
|
auth_mode = self.auth_combo.currentData()
|
||||||
|
|
||||||
if not ssid:
|
if not ssid:
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WiFi名称不能为空!")
|
self._styled_message("waring", self, "警告", "WiFi名称不能为空!")
|
||||||
return
|
return
|
||||||
|
|
||||||
if auth_mode == "psk" and not password:
|
if auth_mode == "psk" and not password:
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA/WPA2 认证需要密码")
|
self._styled_message("waring", self, "警告", "WPA/WPA2 认证需要密码")
|
||||||
return
|
return
|
||||||
|
|
||||||
if auth_mode == "eap" and (not identity or not password):
|
if auth_mode == "eap" and (not identity or not password):
|
||||||
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA-EAP 认证需要身份和密码")
|
self._styled_message("waring", self, "警告", "WPA-EAP 认证需要身份和密码")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 按钮UI反馈
|
# 按钮UI反馈
|
||||||
@@ -1436,16 +1453,16 @@ class SettingPage(QWidget):
|
|||||||
self.connect_button.setEnabled(True)
|
self.connect_button.setEnabled(True)
|
||||||
self.connect_button.setText("连接")
|
self.connect_button.setText("连接")
|
||||||
if ok:
|
if ok:
|
||||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已连接: {ssid}")
|
self._styled_message("info", self, "成功", f"已连接: {ssid}")
|
||||||
else:
|
else:
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接失败")
|
self._styled_message("error", self, "错误", "连接失败")
|
||||||
self.refresh_saved_wifi()
|
self.refresh_saved_wifi()
|
||||||
self.refresh_current_status()
|
self.refresh_current_status()
|
||||||
|
|
||||||
def _on_connect_error(self, err_msg):
|
def _on_connect_error(self, err_msg):
|
||||||
self.connect_button.setEnabled(True)
|
self.connect_button.setEnabled(True)
|
||||||
self.connect_button.setText("连接")
|
self.connect_button.setText("连接")
|
||||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接WiFi失败: {err_msg}")
|
self._styled_message("error", self, "错误", f"连接WiFi失败: {err_msg}")
|
||||||
|
|
||||||
def display_setting(self, index):
|
def display_setting(self, index):
|
||||||
if index < 0:
|
if index < 0:
|
||||||
|
|||||||
@@ -119,10 +119,11 @@ class TempGauge(QWidget):
|
|||||||
p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
||||||
f"{self._actual:.0f}°")
|
f"{self._actual:.0f}°")
|
||||||
if self._target > 0:
|
if self._target > 0:
|
||||||
|
tgt_y = bar_y + bar_h - int((bar_h - 4) * min(self._target / 300, 1))
|
||||||
font2 = QFont("sans-serif", 10)
|
font2 = QFont("sans-serif", 10)
|
||||||
p.setFont(font2)
|
p.setFont(font2)
|
||||||
p.setPen(QPen(QColor("#888888")))
|
p.setPen(QPen(QColor("#888888")))
|
||||||
p.drawText(44, 34, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
p.drawText(44, tgt_y - 10, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
||||||
f"→ {self._target:.0f}°")
|
f"→ {self._target:.0f}°")
|
||||||
|
|
||||||
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
|
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
|
||||||
@@ -134,9 +135,10 @@ class TempGauge(QWidget):
|
|||||||
|
|
||||||
# ── 状态页面 ────────────────────────────────────────────
|
# ── 状态页面 ────────────────────────────────────────────
|
||||||
class StatusPage(QWidget):
|
class StatusPage(QWidget):
|
||||||
def __init__(self, api_client, parent=None):
|
def __init__(self, api_client, GcodeViewer=None, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.api_client = api_client
|
self.api_client = api_client
|
||||||
|
self.gcode_viewer = GcodeViewer
|
||||||
|
|
||||||
self.file_name = "None"
|
self.file_name = "None"
|
||||||
self.progress = 0.0
|
self.progress = 0.0
|
||||||
@@ -283,15 +285,24 @@ class StatusPage(QWidget):
|
|||||||
# ── 右侧预留区域 ─────────────────────────────
|
# ── 右侧预留区域 ─────────────────────────────
|
||||||
right_frame = QFrame()
|
right_frame = QFrame()
|
||||||
right_frame.setStyleSheet("background-color: #3a3a3a; border-radius: 10px;")
|
right_frame.setStyleSheet("background-color: #3a3a3a; border-radius: 10px;")
|
||||||
right_layout = QVBoxLayout(right_frame)
|
self.right_layout = QVBoxLayout(right_frame)
|
||||||
right_layout.setContentsMargins(6, 6, 6, 6)
|
self.right_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
|
|
||||||
self.gcode_viewer = GCodeViewerWidget()
|
# self.gcode_viewer = GCodeViewerWidget()
|
||||||
right_layout.addWidget(self.gcode_viewer)
|
if self.gcode_viewer is not None:
|
||||||
|
self.right_layout.addWidget(self.gcode_viewer)
|
||||||
|
# self.gcode_viewer.setUpdatesEnabled(False)
|
||||||
|
# self.gcode_viewer.hide()
|
||||||
|
|
||||||
main_layout.addWidget(left_frame, 2)
|
main_layout.addWidget(left_frame, 2)
|
||||||
main_layout.addWidget(right_frame, 3)
|
main_layout.addWidget(right_frame, 3)
|
||||||
|
|
||||||
|
# QTimer.singleShot(5000, self.init_gcode_viewer)
|
||||||
|
|
||||||
|
# def init_gcode_viewer(self):
|
||||||
|
# self.gcode_viewer = GCodeViewerWidget()
|
||||||
|
# self.right_layout.addWidget(self.gcode_viewer)
|
||||||
|
|
||||||
def update_status(self):
|
def update_status(self):
|
||||||
self.fresh_status_valve()
|
self.fresh_status_valve()
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def load_gcode_vertices(path):
|
|
||||||
vertices = []
|
|
||||||
|
|
||||||
x = 0
|
|
||||||
y = 0
|
|
||||||
z = 0
|
|
||||||
|
|
||||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith("G0") or line.startswith("G1"):
|
|
||||||
old_x = x
|
|
||||||
old_y = y
|
|
||||||
old_z = z
|
|
||||||
|
|
||||||
mx = re.search(r"X([-0-9.]+)", line)
|
|
||||||
my = re.search(r"Y([-0-9.]+)", line)
|
|
||||||
mz = re.search(r"Z([-0-9.]+)", line)
|
|
||||||
|
|
||||||
if mx:
|
|
||||||
x = float(mx.group(1))
|
|
||||||
|
|
||||||
if my:
|
|
||||||
y = float(my.group(1))
|
|
||||||
|
|
||||||
if mz:
|
|
||||||
z = float(mz.group(1))
|
|
||||||
|
|
||||||
vertices.append({
|
|
||||||
"x1": old_x,
|
|
||||||
"y1": old_y,
|
|
||||||
"z1": old_z,
|
|
||||||
"x2": x,
|
|
||||||
"y2": y,
|
|
||||||
"z2": z,
|
|
||||||
})
|
|
||||||
|
|
||||||
return vertices
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import functools
|
|
||||||
from flask import Blueprint, request, jsonify
|
|
||||||
from app.models import ApiKey
|
|
||||||
from app.utils.octoprint_client import OctoPrintClient
|
|
||||||
from app.models import SystemConfig
|
|
||||||
|
|
||||||
api_bp = Blueprint('api_handle', __name__, url_prefix='/api/v1')
|
|
||||||
|
|
||||||
def get_octo_client():
|
|
||||||
url = SystemConfig.query.filter_by(key='octoprint_url').first()
|
|
||||||
apikey = SystemConfig.query.filter_by(key='octoprint_apikey').first()
|
|
||||||
if url and url.value and apikey and apikey.value:
|
|
||||||
return OctoPrintClient(url.value, apikey.value)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def require_api_key(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
api_key_header = request.headers.get('X-Api-Key')
|
|
||||||
if not api_key_header:
|
|
||||||
return jsonify({'error': 'Missing API Key in headers (X-Api-Key)'}), 401
|
|
||||||
|
|
||||||
key_record = ApiKey.query.filter_by(key=api_key_header).first()
|
|
||||||
if not key_record:
|
|
||||||
return jsonify({'error': 'Invalid API Key'}), 401
|
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
@api_bp.route('/status', methods=['GET'])
|
|
||||||
@require_api_key
|
|
||||||
def get_status():
|
|
||||||
client = get_octo_client()
|
|
||||||
if not client:
|
|
||||||
return jsonify({'error': 'Printer not configured'}), 503
|
|
||||||
try:
|
|
||||||
status_data = client.get_printer_status()
|
|
||||||
job_data = client.get_job_info()
|
|
||||||
return jsonify({'status': status_data, 'job': job_data})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': str(e)}), 500
|
|
||||||
|
|
||||||
@api_bp.route('/octoprint_client', methods=['POST'])
|
|
||||||
@require_api_key
|
|
||||||
def invoke_octoprint_client():
|
|
||||||
"""
|
|
||||||
Expects JSON payload like:
|
|
||||||
{
|
|
||||||
"method": "pause_print",
|
|
||||||
"kwargs": {"action": "pause"}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
client = get_octo_client()
|
|
||||||
if not client:
|
|
||||||
return jsonify({'error': 'Printer not configured'}), 503
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
if not data or 'method' not in data:
|
|
||||||
return jsonify({'error': 'Missing method in JSON payload'}), 400
|
|
||||||
|
|
||||||
method_name = data['method']
|
|
||||||
kwargs = data.get('kwargs', {})
|
|
||||||
args = data.get('args', [])
|
|
||||||
|
|
||||||
if not hasattr(client, method_name):
|
|
||||||
return jsonify({'error': f'Method {method_name} not found on OctoPrintClient'}), 400
|
|
||||||
|
|
||||||
func = getattr(client, method_name)
|
|
||||||
if not callable(func) or method_name.startswith('_'):
|
|
||||||
return jsonify({'error': f'Method {method_name} is not allowed'}), 403
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
return jsonify({'success': True, 'result': result})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': str(e)}), 500
|
|
||||||
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-2 border-bottom">
|
|
||||||
<div>
|
|
||||||
<h1 class="h2 mb-1"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
|
|
||||||
<div class="text-muted small">
|
|
||||||
<span class="me-3"><i class="bi bi-clock-history me-1"></i>{{ _('Estimated Time:') }} <span class="fw-bold">{{ time_info }}</span></span>
|
|
||||||
<span class="me-3"><i class="bi bi-layers me-1"></i>{{ _('First Layer Time:') }} <span class="fw-bold">{{ layer1_time }}</span></span>
|
|
||||||
<span><i class="bi bi-rulers me-1"></i>{{ _('Filament Used [mm]:') }} <span class="fw-bold">{{ filament_used }}</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 mt-md-0">
|
|
||||||
<a href="{{ url_for('printer.prepare') }}#file-{{ file.id }}" class="btn btn-warning btn-sm rounded shadow-sm fw-bold"><i class="bi bi-printer"></i> {{ _('Go to Print') }}</a>
|
|
||||||
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm ms-2"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
|
|
||||||
<a href="{{ url_for('main.files') }}" class="btn btn-outline-secondary btn-sm rounded ms-2 shadow-sm"><i class="bi bi-arrow-left"></i> {{ _('Back') }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="loading-overlay" class="text-center py-5 my-5">
|
|
||||||
<div class="spinner-border text-primary shadow-sm" role="status" style="width: 3rem; height: 3rem;"></div>
|
|
||||||
<h4 class="mt-4 text-secondary">{{ _('Loading and Parsing GCode Data...') }}</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row d-none" id="preview-container" style="height: 75vh;">
|
|
||||||
<!-- 3D Canvas Area -->
|
|
||||||
<div class="col-md-11 position-relative h-100 p-0 border rounded border-secondary shadow-sm" style="background: #111;">
|
|
||||||
<div id="canvas-container" class="w-100 h-100 d-block overflow-hidden"></div>
|
|
||||||
|
|
||||||
<!-- Legend Overlay -->
|
|
||||||
<div id="legend-overlay" class="position-absolute top-0 start-0 m-3 p-2 rounded shadow bg-dark bg-opacity-75 border border-secondary" style="color: #eee; font-size: 0.85rem; pointer-events: auto; z-index: 10;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Slider (Intra-Layer Progress) -->
|
|
||||||
<div class="position-absolute bottom-0 start-0 w-100 p-3" style="background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.8) 100%); z-index: 10;">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<span class="text-white fw-medium text-nowrap user-select-none"><i class="bi bi-play-circle me-1"></i>{{ _('Layer Progress:') }}</span>
|
|
||||||
<input type="range" class="form-range flex-grow-1" id="progress-slider" min="0" max="100" value="100" step="0.1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Sidebar (Layer Slider) -->
|
|
||||||
<div class="col-md-1 h-100 d-flex flex-column align-items-center justify-content-center bg-white border rounded shadow-sm position-relative">
|
|
||||||
<label class="form-label mb-3 fw-bold text-center text-primary mt-3">{{ _('Layer') }}<br>
|
|
||||||
<span id="layer-display" class="badge bg-primary fs-6 mt-1 shadow-sm px-3 rounded-pill">0</span>
|
|
||||||
</label>
|
|
||||||
<div class="flex-grow-1 w-100 d-flex justify-content-center pb-4 py-2">
|
|
||||||
<input type="range" class="form-range h-100" id="layer-slider" min="0" max="0" value="0" style="writing-mode: bt-lr; -webkit-appearance: slider-vertical; cursor: ns-resize;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/three.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/OrbitControls.js') }}"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
|
||||||
|
|
||||||
// Inject printer machine dimensions via Jinja
|
|
||||||
const bedWidth = {{ machine_width | default(220) }};
|
|
||||||
const bedDepth = {{ machine_depth | default(220) }};
|
|
||||||
const bedHeight = {{ machine_height | default(250) }};
|
|
||||||
const offsetX = {{ offset_x | default(0.0) }};
|
|
||||||
const offsetY = {{ offset_y | default(0.0) }};
|
|
||||||
|
|
||||||
// Type indices for shader visibility filtering
|
|
||||||
let COLORS = {};
|
|
||||||
let TYPE_INDEX = {};
|
|
||||||
let gcodeMat = null;
|
|
||||||
|
|
||||||
const SLICER_CONFIGS = {
|
|
||||||
'Cura': [
|
|
||||||
{ id: 'TRAVEL', label: '{{ _("Travel (Move)") }}', color: 0x405060, defaultShow: false },
|
|
||||||
{ id: 'WALL-OUTER', label: '{{ _("Outer Wall") }}', color: 0xeb8b38, defaultShow: true },
|
|
||||||
{ id: 'WALL-INNER', label: '{{ _("Inner Wall") }}', color: 0x4080cf, defaultShow: true },
|
|
||||||
{ id: 'FILL', label: '{{ _("Infill") }}', color: 0xccc04b, defaultShow: true },
|
|
||||||
{ id: 'SKIN', label: '{{ _("Skin/TopBottom") }}', color: 0x9e60b3, defaultShow: true },
|
|
||||||
{ id: 'SUPPORT', label: '{{ _("Support") }}', color: 0x57b357, defaultShow: true },
|
|
||||||
{ id: 'SKIRT', label: '{{ _("Skirt") }}', color: 0x00ffff, defaultShow: true },
|
|
||||||
{ id: 'SUPPORT-INTERFACE', label: '{{ _("Support Interface") }}', color: 0x2b6b2b, defaultShow: true },
|
|
||||||
{ id: 'DEFAULT', label: '{{ _("Others") }}', color: 0xaaaaaa, defaultShow: true }
|
|
||||||
],
|
|
||||||
'Prusa': [
|
|
||||||
{ id: 'TRAVEL', label: '{{ _("Travel (Move)") }}', color: 0x405060, defaultShow: false },
|
|
||||||
{ id: 'Custom', label: '{{ _("Custom") }}', color: 0xd0e0ff, defaultShow: true },
|
|
||||||
{ id: 'Skirt/Brim', label: '{{ _("Skirt/Brim") }}', color: 0x00FFFF, defaultShow: true },
|
|
||||||
{ id: 'Support material', label: '{{ _("Support material") }}', color: 0x90EE90, defaultShow: true },
|
|
||||||
{ id: 'Perimeter', label: '{{ _("Perimeter") }}', color: 0xFFFFE0, defaultShow: true },
|
|
||||||
{ id: 'External perimeter', label: '{{ _("External perimeter") }}', color: 0xFFA500, defaultShow: true },
|
|
||||||
{ id: 'Solid infill', label: '{{ _("Solid infill") }}', color: 0x800080, defaultShow: true },
|
|
||||||
{ id: 'Overhang perimeter', label: '{{ _("Overhang perimeter") }}', color: 0x00008B, defaultShow: true },
|
|
||||||
{ id: 'Internal infill', label: '{{ _("Internal infill") }}', color: 0x8B0000, defaultShow: true },
|
|
||||||
{ id: 'Bridge infill', label: '{{ _("Bridge infill") }}', color: 0x0000FF, defaultShow: true },
|
|
||||||
{ id: 'Top solid infill', label: '{{ _("Top solid infill") }}', color: 0xFF0000, defaultShow: true },
|
|
||||||
{ id: 'Support material interface', label: '{{ _("Support Interface") }}', color: 0x2b6b2b, defaultShow: true },
|
|
||||||
{ id: 'DEFAULT', label: '{{ _("Others") }}', color: 0xaaaaaa, defaultShow: true }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
let layers = [];
|
|
||||||
let scene, camera, renderer, controls;
|
|
||||||
let group = new THREE.Group();
|
|
||||||
|
|
||||||
const layerSlider = document.getElementById('layer-slider');
|
|
||||||
const layerDisplay = document.getElementById('layer-display');
|
|
||||||
const progressSlider = document.getElementById('progress-slider');
|
|
||||||
|
|
||||||
function setupSlicerConfig(text) {
|
|
||||||
let slicerType = 'Cura'; // default
|
|
||||||
if (text.substring(0, 500).includes('generated by PrusaSlicer')) {
|
|
||||||
slicerType = 'Prusa';
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = SLICER_CONFIGS[slicerType];
|
|
||||||
|
|
||||||
// 1. Build uniforms & shader strings dynamically
|
|
||||||
let uniformsObj = {};
|
|
||||||
let fragmentUniformsDecl = '';
|
|
||||||
let fragmentUniformsLogic = '';
|
|
||||||
|
|
||||||
let overlayHTML = '';
|
|
||||||
|
|
||||||
config.forEach((c, idx) => {
|
|
||||||
COLORS[c.id] = new THREE.Color(c.color);
|
|
||||||
TYPE_INDEX[c.id] = idx;
|
|
||||||
|
|
||||||
const uniformName = 'uShow' + idx;
|
|
||||||
uniformsObj[uniformName] = { value: c.defaultShow ? 1.0 : 0.0 };
|
|
||||||
|
|
||||||
fragmentUniformsDecl += `uniform float ${uniformName};\n`;
|
|
||||||
|
|
||||||
if (idx === 0) {
|
|
||||||
fragmentUniformsLogic += `if (t == 0) show = ${uniformName};\n`;
|
|
||||||
} else {
|
|
||||||
fragmentUniformsLogic += ` else if (t == ${idx}) show = ${uniformName};\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Legend UI
|
|
||||||
const hexColor = '#' + c.color.toString(16).padStart(6, '0');
|
|
||||||
const opacityStyle = c.defaultShow ? '1.0' : '0.4';
|
|
||||||
overlayHTML += `
|
|
||||||
<div class="mb-1 legend-item user-select-none" data-id="${c.id}" data-uniform="${uniformName}" style="cursor: pointer; transition: opacity 0.2s; opacity: ${opacityStyle};"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: ${hexColor};"></span>${c.label}</div>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add fallback condition
|
|
||||||
fragmentUniformsLogic += ` else show = 1.0;\n`;
|
|
||||||
|
|
||||||
document.getElementById('legend-overlay').innerHTML = overlayHTML;
|
|
||||||
|
|
||||||
gcodeMat = new THREE.ShaderMaterial({
|
|
||||||
uniforms: uniformsObj,
|
|
||||||
vertexShader: `
|
|
||||||
attribute float pType;
|
|
||||||
varying vec3 vColor;
|
|
||||||
varying float vType;
|
|
||||||
void main() {
|
|
||||||
vColor = color;
|
|
||||||
vType = pType;
|
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
fragmentShader: `
|
|
||||||
varying vec3 vColor;
|
|
||||||
varying float vType;
|
|
||||||
${fragmentUniformsDecl}
|
|
||||||
void main() {
|
|
||||||
float show = 1.0;
|
|
||||||
int t = int(vType + 0.5);
|
|
||||||
${fragmentUniformsLogic}
|
|
||||||
if (show < 0.5) discard;
|
|
||||||
gl_FragColor = vec4(vColor, 1.0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
vertexColors: true,
|
|
||||||
side: THREE.DoubleSide,
|
|
||||||
linewidth: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Legend binding
|
|
||||||
document.querySelectorAll('.legend-item').forEach(el => {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
const uniformName = this.dataset.uniform;
|
|
||||||
if (uniformName && gcodeMat.uniforms[uniformName]) {
|
|
||||||
const currentVal = gcodeMat.uniforms[uniformName].value;
|
|
||||||
const newVal = currentVal > 0.5 ? 0.0 : 1.0;
|
|
||||||
gcodeMat.uniforms[uniformName].value = newVal;
|
|
||||||
this.style.opacity = newVal > 0.5 ? "1.0" : "0.4";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function init3D() {
|
|
||||||
const container = document.getElementById('canvas-container');
|
|
||||||
scene = new THREE.Scene();
|
|
||||||
scene.background = new THREE.Color(0x1a1a1a);
|
|
||||||
|
|
||||||
scene.add(group);
|
|
||||||
|
|
||||||
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 5000);
|
|
||||||
camera.up.set(0, 0, 1);
|
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
||||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
|
||||||
container.appendChild(renderer.domElement);
|
|
||||||
|
|
||||||
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
||||||
controls.enableDamping = false;
|
|
||||||
controls.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
|
|
||||||
|
|
||||||
window.addEventListener('resize', onWindowResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowResize() {
|
|
||||||
const container = document.getElementById('canvas-container');
|
|
||||||
camera.aspect = container.clientWidth / container.clientHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
controls.update();
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGCode() {
|
|
||||||
try {
|
|
||||||
const url = '{{ url_for("main.download_gcode", file_id=file.id) }}';
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error("GCode Request Failed");
|
|
||||||
|
|
||||||
const gcodeText = await response.text();
|
|
||||||
|
|
||||||
document.getElementById('loading-overlay').classList.add('d-none');
|
|
||||||
document.getElementById('preview-container').classList.remove('d-none');
|
|
||||||
|
|
||||||
setupSlicerConfig(gcodeText);
|
|
||||||
init3D();
|
|
||||||
parseGCode(gcodeText);
|
|
||||||
|
|
||||||
// Add grid matching printer size
|
|
||||||
setupMachineEnvironment();
|
|
||||||
|
|
||||||
animate();
|
|
||||||
|
|
||||||
// Init controls
|
|
||||||
layerSlider.max = Math.max(0, layers.length - 1);
|
|
||||||
layerSlider.value = Math.max(0, layers.length - 1);
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
layerSlider.addEventListener('input', updateUI);
|
|
||||||
progressSlider.addEventListener('input', updateUI);
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.error("Error Loading GCode", e);
|
|
||||||
document.getElementById('loading-overlay').innerHTML = `
|
|
||||||
<div class="text-danger my-5 py-5">
|
|
||||||
<i class="bi bi-exclamation-triangle display-1"></i>
|
|
||||||
<h3 class="mt-3">{{ _('Failed to load GCode preview.') }}</h3>
|
|
||||||
<p class="text-muted">${e.toString()}</p>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGCode(text) {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
let current = { x: 0, y: 0, z: 0, e: 0 };
|
|
||||||
let relativeE = false; // Track M83 (relative) vs M82 (absolute)
|
|
||||||
|
|
||||||
// Dynamically compute width and layer height based on gcode info if possible
|
|
||||||
let extWidth = 0.4;
|
|
||||||
let layerHeight = 0.2;
|
|
||||||
let pWidth = extWidth;
|
|
||||||
|
|
||||||
let currentTypeStr = 'DEFAULT';
|
|
||||||
|
|
||||||
let currentExtrudePoints = [];
|
|
||||||
let currentExtrudeColors = [];
|
|
||||||
let currentExtrudeTypes = [];
|
|
||||||
|
|
||||||
let currentTravelPoints = [];
|
|
||||||
let currentTravelColors = [];
|
|
||||||
let currentTravelTypes = [];
|
|
||||||
|
|
||||||
function flushLayer() {
|
|
||||||
if (currentExtrudePoints.length === 0 && currentTravelPoints.length === 0) return;
|
|
||||||
|
|
||||||
let layerGroup = new THREE.Group();
|
|
||||||
|
|
||||||
if (currentExtrudePoints.length > 0) {
|
|
||||||
const geo = new THREE.BufferGeometry();
|
|
||||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(currentExtrudePoints, 3));
|
|
||||||
geo.setAttribute('color', new THREE.Float32BufferAttribute(currentExtrudeColors, 3));
|
|
||||||
geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentExtrudeTypes, 1));
|
|
||||||
const mesh = new THREE.Mesh(geo, gcodeMat);
|
|
||||||
mesh.userData.isExtrude = true;
|
|
||||||
layerGroup.add(mesh);
|
|
||||||
currentExtrudePoints = []; currentExtrudeColors = []; currentExtrudeTypes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTravelPoints.length > 0) {
|
|
||||||
const geo = new THREE.BufferGeometry();
|
|
||||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(currentTravelPoints, 3));
|
|
||||||
geo.setAttribute('color', new THREE.Float32BufferAttribute(currentTravelColors, 3));
|
|
||||||
geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentTravelTypes, 1));
|
|
||||||
const lineSeg = new THREE.LineSegments(geo, gcodeMat);
|
|
||||||
lineSeg.userData.isTravel = true;
|
|
||||||
layerGroup.add(lineSeg);
|
|
||||||
currentTravelPoints = []; currentTravelColors = []; currentTravelTypes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
layers.push(layerGroup);
|
|
||||||
group.add(layerGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
let chunk = lines[i].trim();
|
|
||||||
if (!chunk) continue;
|
|
||||||
let upperChunk = chunk.toUpperCase();
|
|
||||||
|
|
||||||
if (upperChunk.startsWith('M82')) relativeE = false;
|
|
||||||
else if (upperChunk.startsWith('M83')) relativeE = true;
|
|
||||||
|
|
||||||
if (upperChunk.startsWith(';LAYER:') || upperChunk.startsWith(';LAYER_CHANGE')) {
|
|
||||||
flushLayer();
|
|
||||||
} else if (upperChunk.startsWith(';LAYER_HEIGHT:')) {
|
|
||||||
let lh = parseFloat(chunk.substring(14));
|
|
||||||
if (!isNaN(lh) && lh > 0) layerHeight = lh;
|
|
||||||
} else if (upperChunk.startsWith(';HEIGHT:')) {
|
|
||||||
let lh = parseFloat(chunk.substring(8));
|
|
||||||
if (!isNaN(lh) && lh > 0) layerHeight = lh;
|
|
||||||
} else if (upperChunk.startsWith(';WIDTH:')) {
|
|
||||||
let w = parseFloat(chunk.substring(7));
|
|
||||||
if (!isNaN(w) && w > 0) pWidth = w;
|
|
||||||
} else if (upperChunk.startsWith(';TYPE:')) {
|
|
||||||
currentTypeStr = chunk.substring(6).trim();
|
|
||||||
} else if (chunk.startsWith(';') && COLORS[chunk.substring(1).trim()] !== undefined) {
|
|
||||||
currentTypeStr = chunk.substring(1).trim();
|
|
||||||
} else if (upperChunk.startsWith(';') && chunk.includes(' perimeter')) {
|
|
||||||
currentTypeStr = chunk.substring(1).trim();
|
|
||||||
} else if (upperChunk.startsWith(';') && chunk.includes(' infill')) {
|
|
||||||
// Heuristics for Prusa/Slic3r specific comments like `; Internal infill`
|
|
||||||
currentTypeStr = chunk.substring(1).trim();
|
|
||||||
} else if (upperChunk.startsWith(';') && chunk.includes(' material')) {
|
|
||||||
// Support material
|
|
||||||
currentTypeStr = chunk.substring(1).trim();
|
|
||||||
} else if (upperChunk.startsWith(';') && chunk.includes('Skirt/Brim')) {
|
|
||||||
// Skirt/Brim
|
|
||||||
currentTypeStr = 'Skirt/Brim';
|
|
||||||
} else if (upperChunk.startsWith('G0') || upperChunk.startsWith('G1')) {
|
|
||||||
let next = { x: current.x, y: current.y, z: current.z, e: current.e };
|
|
||||||
let parts = upperChunk.split(/\s+/);
|
|
||||||
let hasMove = false;
|
|
||||||
let hasE = false;
|
|
||||||
let eVal = 0;
|
|
||||||
|
|
||||||
for (let p of parts) {
|
|
||||||
if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.x = v; hasMove = true; } }
|
|
||||||
if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.y = v; hasMove = true; } }
|
|
||||||
if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.z = v; hasMove = true; } }
|
|
||||||
if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { eVal = v; hasE = true; } }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) {
|
|
||||||
let isExtrude = false;
|
|
||||||
if (hasE) {
|
|
||||||
if (relativeE) {
|
|
||||||
next.e = current.e + eVal;
|
|
||||||
isExtrude = eVal > 0;
|
|
||||||
} else {
|
|
||||||
next.e = eVal;
|
|
||||||
isExtrude = next.e > current.e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cura uses G0 for travel generally
|
|
||||||
if (upperChunk.startsWith('G0') && !upperChunk.includes('E')) isExtrude = false;
|
|
||||||
|
|
||||||
let activeType = isExtrude ? currentTypeStr : 'TRAVEL';
|
|
||||||
let resolvedType = activeType;
|
|
||||||
|
|
||||||
if (isExtrude && COLORS[activeType] === undefined) {
|
|
||||||
resolvedType = 'DEFAULT';
|
|
||||||
}
|
|
||||||
|
|
||||||
let col = COLORS[resolvedType] || COLORS['DEFAULT'];
|
|
||||||
let tIdx = TYPE_INDEX[resolvedType] !== undefined ? TYPE_INDEX[resolvedType] : TYPE_INDEX['DEFAULT'];
|
|
||||||
|
|
||||||
if (isExtrude) {
|
|
||||||
let dx = next.x - current.x;
|
|
||||||
let dy = next.y - current.y;
|
|
||||||
let dist = Math.sqrt(dx*dx + dy*dy);
|
|
||||||
if (dist > 0.0001) {
|
|
||||||
let hw = pWidth / 2.0;
|
|
||||||
let hh = layerHeight / 2.0;
|
|
||||||
|
|
||||||
let nx = -(dy / dist) * hw;
|
|
||||||
let ny = (dx / dist) * hw;
|
|
||||||
|
|
||||||
let p1x = current.x + nx, p1y = current.y + ny; // current-left
|
|
||||||
let p2x = current.x - nx, p2y = current.y - ny; // current-right
|
|
||||||
let p3x = next.x + nx, p3y = next.y + ny; // next-left
|
|
||||||
let p4x = next.x - nx, p4y = next.y - ny; // next-right
|
|
||||||
|
|
||||||
// Top face
|
|
||||||
currentExtrudePoints.push(
|
|
||||||
p1x, p1y, current.z + hh,
|
|
||||||
p3x, p3y, next.z + hh,
|
|
||||||
p2x, p2y, current.z + hh,
|
|
||||||
p3x, p3y, next.z + hh,
|
|
||||||
p4x, p4y, next.z + hh,
|
|
||||||
p2x, p2y, current.z + hh
|
|
||||||
);
|
|
||||||
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r, col.g, col.b); currentExtrudeTypes.push(tIdx); }
|
|
||||||
|
|
||||||
// Bottom face
|
|
||||||
currentExtrudePoints.push(
|
|
||||||
p1x, p1y, current.z - hh,
|
|
||||||
p2x, p2y, current.z - hh,
|
|
||||||
p3x, p3y, next.z - hh,
|
|
||||||
p2x, p2y, current.z - hh,
|
|
||||||
p4x, p4y, next.z - hh,
|
|
||||||
p3x, p3y, next.z - hh
|
|
||||||
);
|
|
||||||
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.4, col.g*0.4, col.b*0.4); currentExtrudeTypes.push(tIdx); }
|
|
||||||
|
|
||||||
// Left face
|
|
||||||
currentExtrudePoints.push(
|
|
||||||
p1x, p1y, current.z - hh,
|
|
||||||
p3x, p3y, next.z - hh,
|
|
||||||
p1x, p1y, current.z + hh,
|
|
||||||
p3x, p3y, next.z - hh,
|
|
||||||
p3x, p3y, next.z + hh,
|
|
||||||
p1x, p1y, current.z + hh
|
|
||||||
);
|
|
||||||
// Fake lighting based on normal side
|
|
||||||
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.6, col.g*0.6, col.b*0.6); currentExtrudeTypes.push(tIdx); }
|
|
||||||
|
|
||||||
// Right face
|
|
||||||
currentExtrudePoints.push(
|
|
||||||
p2x, p2y, current.z - hh,
|
|
||||||
p2x, p2y, current.z + hh,
|
|
||||||
p4x, p4y, next.z - hh,
|
|
||||||
p2x, p2y, current.z + hh,
|
|
||||||
p4x, p4y, next.z + hh,
|
|
||||||
p4x, p4y, next.z - hh
|
|
||||||
);
|
|
||||||
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.8, col.g*0.8, col.b*0.8); currentExtrudeTypes.push(tIdx); }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Travel lines get slight vertical offset for visibility
|
|
||||||
let zOff = 0.05;
|
|
||||||
currentTravelPoints.push(current.x, current.y, current.z + zOff);
|
|
||||||
currentTravelPoints.push(next.x, next.y, next.z + zOff);
|
|
||||||
currentTravelColors.push(col.r, col.g, col.b, col.r, col.g, col.b);
|
|
||||||
currentTravelTypes.push(tIdx, tIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update E based on parsed G-code execution type
|
|
||||||
if (hasE) {
|
|
||||||
if (relativeE) current.e += eVal;
|
|
||||||
else current.e = eVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
current.x = next.x; current.y = next.y; current.z = next.z;
|
|
||||||
}
|
|
||||||
} else if (upperChunk.startsWith('G92')) {
|
|
||||||
let parts = chunk.split(/\s+/);
|
|
||||||
for (let p of parts) {
|
|
||||||
if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.e = v; }
|
|
||||||
if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.x = v; }
|
|
||||||
if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.y = v; }
|
|
||||||
if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.z = v; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushLayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMachineEnvironment() {
|
|
||||||
if (layers.length === 0) return;
|
|
||||||
|
|
||||||
let bbox = new THREE.Box3();
|
|
||||||
for (let layerGrp of layers) {
|
|
||||||
layerGrp.children.forEach(child => {
|
|
||||||
child.geometry.computeBoundingBox();
|
|
||||||
bbox.union(child.geometry.boundingBox);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// The GCode coordinates for the actual print bed are from (0,0) to (W,H).
|
|
||||||
// The GCode trajectory is ALREADY offset by plater.html during slicing.
|
|
||||||
// We just need to place the grid exactly in the center of the bed: (W/2, H/2).
|
|
||||||
let gridOffsetX = (bedWidth / 2);
|
|
||||||
let gridOffsetY = (bedDepth / 2);
|
|
||||||
|
|
||||||
// Add Grid
|
|
||||||
const gridDivisions = Math.ceil(Math.max(bedWidth, bedDepth) / 10);
|
|
||||||
const gridHelper = new THREE.GridHelper(Math.max(bedWidth, bedDepth), gridDivisions, 0x444444, 0x242424);
|
|
||||||
gridHelper.rotation.x = Math.PI / 2;
|
|
||||||
gridHelper.position.set(gridOffsetX, gridOffsetY, 0);
|
|
||||||
scene.add(gridHelper);
|
|
||||||
|
|
||||||
// Add Printer Volume Outline
|
|
||||||
const boxGeo = new THREE.BoxGeometry(bedWidth, bedDepth, bedHeight);
|
|
||||||
const edges = new THREE.EdgesGeometry(boxGeo);
|
|
||||||
const boxOutline = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x444444 }));
|
|
||||||
boxOutline.position.set(gridOffsetX, gridOffsetY, bedHeight/2);
|
|
||||||
scene.add(boxOutline);
|
|
||||||
|
|
||||||
// Align Camera to target the center of the bed grid
|
|
||||||
controls.target.set(gridOffsetX, gridOffsetY, 0);
|
|
||||||
camera.position.set(gridOffsetX, gridOffsetY - (bedDepth * 1.5), bedHeight * 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
if (layers.length === 0) return;
|
|
||||||
let activeIdx = parseInt(layerSlider.value);
|
|
||||||
let intraProg = parseFloat(progressSlider.value);
|
|
||||||
|
|
||||||
layerDisplay.innerText = activeIdx + " / " + (layers.length - 1);
|
|
||||||
|
|
||||||
for (let i = 0; i < layers.length; i++) {
|
|
||||||
let layerGrp = layers[i];
|
|
||||||
if (i < activeIdx) {
|
|
||||||
layerGrp.visible = true;
|
|
||||||
layerGrp.children.forEach(child => child.geometry.setDrawRange(0, Infinity));
|
|
||||||
} else if (i === activeIdx) {
|
|
||||||
layerGrp.visible = true;
|
|
||||||
layerGrp.children.forEach(child => {
|
|
||||||
let totalVertices = child.geometry.attributes.position.count;
|
|
||||||
let elementsPerUnit = child.userData.isTravel ? 2 : 24;
|
|
||||||
let totalUnits = totalVertices / elementsPerUnit;
|
|
||||||
let drawCount = Math.floor(totalUnits * (intraProg / 100)) * elementsPerUnit;
|
|
||||||
child.geometry.setDrawRange(0, drawCount);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
layerGrp.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadGCode();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick3D
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property real progress: 100
|
|
||||||
property real maxLayer: 10
|
|
||||||
|
|
||||||
View3D {
|
|
||||||
anchors.fill: parent
|
|
||||||
environment: SceneEnvironment {
|
|
||||||
clearColor: "#1a1a1a"
|
|
||||||
backgroundMode: SceneEnvironment.Color
|
|
||||||
}
|
|
||||||
PerspectiveCamera {
|
|
||||||
id: camera
|
|
||||||
z: 300
|
|
||||||
y: -200
|
|
||||||
eulerRotation.x: 45
|
|
||||||
}
|
|
||||||
DirectionalLight { eulerRotation.x: -45 }
|
|
||||||
|
|
||||||
// Render layers using repeated line models or a custom geometry
|
|
||||||
// Since QtQuick3D dynamic geometry from Python is complex, we render primitives for demo
|
|
||||||
Node { id: gcodeNode }
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "QtQuick3D GCode Preview\nProgress: " + progress.toFixed(1) + "%"
|
|
||||||
color: "white"
|
|
||||||
font.pixelSize: 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Window
|
|
||||||
import QtQuick3D
|
|
||||||
|
|
||||||
Window {
|
|
||||||
visible: true
|
|
||||||
visibility: Window.FullScreen
|
|
||||||
width: 1280
|
|
||||||
height: 720
|
|
||||||
|
|
||||||
color: "black"
|
|
||||||
|
|
||||||
View3D {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
environment: SceneEnvironment {
|
|
||||||
clearColor: "#202020"
|
|
||||||
backgroundMode: SceneEnvironment.Color
|
|
||||||
}
|
|
||||||
|
|
||||||
PerspectiveCamera {
|
|
||||||
id: camera
|
|
||||||
position: Qt.vector3d(0, 100, 300)
|
|
||||||
eulerRotation.x: -20
|
|
||||||
}
|
|
||||||
|
|
||||||
DirectionalLight {
|
|
||||||
eulerRotation.x: -45
|
|
||||||
brightness: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Node {
|
|
||||||
id: rootNode
|
|
||||||
|
|
||||||
Repeater3D {
|
|
||||||
model: gcodeData
|
|
||||||
|
|
||||||
delegate: Model {
|
|
||||||
property real dx: modelData.x2 - modelData.x1
|
|
||||||
property real dy: modelData.y2 - modelData.y1
|
|
||||||
property real dz: modelData.z2 - modelData.z1
|
|
||||||
|
|
||||||
property real length: Math.sqrt(dx*dx + dy*dy + dz*dz)
|
|
||||||
|
|
||||||
source: "#Cylinder"
|
|
||||||
|
|
||||||
position: Qt.vector3d(
|
|
||||||
(modelData.x1 + modelData.x2)/2,
|
|
||||||
(modelData.z1 + modelData.z2)/2,
|
|
||||||
(modelData.y1 + modelData.y2)/2
|
|
||||||
)
|
|
||||||
|
|
||||||
scale: Qt.vector3d(0.2, length / 100.0, 0.2)
|
|
||||||
|
|
||||||
materials: [
|
|
||||||
DefaultMaterial {
|
|
||||||
diffuseColor: "#00ff88"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
property real lastX
|
|
||||||
property real lastY
|
|
||||||
|
|
||||||
onPressed: {
|
|
||||||
lastX = mouse.x
|
|
||||||
lastY = mouse.y
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositionChanged: {
|
|
||||||
let dx = mouse.x - lastX
|
|
||||||
let dy = mouse.y - lastY
|
|
||||||
|
|
||||||
rootNode.eulerRotation.y += dx * 0.3
|
|
||||||
rootNode.eulerRotation.x += dy * 0.3
|
|
||||||
|
|
||||||
lastX = mouse.x
|
|
||||||
lastY = mouse.y
|
|
||||||
}
|
|
||||||
|
|
||||||
onWheel: {
|
|
||||||
camera.position.z += wheel.angleDelta.y * -0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
refer/qml.py
@@ -1,37 +0,0 @@
|
|||||||
import sys
|
|
||||||
from PyQt6.QtGui import QGuiApplication
|
|
||||||
from PyQt6.QtQml import QQmlApplicationEngine
|
|
||||||
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
|
|
||||||
|
|
||||||
from GcodeParser import load_gcode_vertices
|
|
||||||
|
|
||||||
|
|
||||||
class GcodeData(QObject):
|
|
||||||
verticesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# self._vertices = load_gcode_vertices("/home/lhye200/.octoprint/uploads/20260414135441_42bff5215c6148b8b5f4d8c4f15d5ddc.gcode")
|
|
||||||
self._vertices = load_gcode_vertices("test.gcode")
|
|
||||||
|
|
||||||
def getVertices(self):
|
|
||||||
return self._vertices
|
|
||||||
|
|
||||||
vertices = pyqtProperty('QVariantList', getVertices, notify=verticesChanged)
|
|
||||||
|
|
||||||
|
|
||||||
app = QGuiApplication(sys.argv)
|
|
||||||
|
|
||||||
engine = QQmlApplicationEngine()
|
|
||||||
|
|
||||||
model = GcodeData()
|
|
||||||
|
|
||||||
engine.rootContext().setContextProperty("gcodeData", model.vertices)
|
|
||||||
|
|
||||||
engine.load("gcode_view2.qml")
|
|
||||||
|
|
||||||
if not engine.rootObjects():
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
242948
refer/smp4bwithfan.gcode
268
refer/test.gcode
@@ -1,268 +0,0 @@
|
|||||||
G92 E0 ;Reset Extruder
|
|
||||||
G1 Z20.0 F3000 ;Move Z Axis up
|
|
||||||
M104 S235 ;Set final nozzle temp
|
|
||||||
G1 X-13 Y20 Z3 F5000.0 ;Move to out position
|
|
||||||
M190 S70 ;Wait for bed temp to stabilize
|
|
||||||
M109 S235 ;Wait for nozzle temp to stabilize
|
|
||||||
G1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position
|
|
||||||
G1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line
|
|
||||||
G1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little
|
|
||||||
G1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line
|
|
||||||
G92 E0 ;Reset Extruder
|
|
||||||
G1 E-1.0000 F1800 ;Retract a bit
|
|
||||||
G1 Z2.0 F3000 ;Move Z Axis up
|
|
||||||
G1 E0.0000 F1800
|
|
||||||
G21 ; set units to millimeters
|
|
||||||
G90 ; use absolute coordinates
|
|
||||||
M83 ; use relative distances for extrusion
|
|
||||||
M107
|
|
||||||
;LAYER_CHANGE
|
|
||||||
;Z:0.2
|
|
||||||
;HEIGHT:0.2
|
|
||||||
G1 E-1.2 F1800
|
|
||||||
G1 Z.6 F9000
|
|
||||||
G1 X52.817 Y67.413
|
|
||||||
G1 Z.2
|
|
||||||
G1 E1.2 F1800
|
|
||||||
M204 P500
|
|
||||||
;TYPE:Skirt/Brim
|
|
||||||
;WIDTH:0.5
|
|
||||||
G1 X53.827 Y66.755 E.04581
|
|
||||||
G1 X54.927 Y66.264 E.04578
|
|
||||||
G1 X56.091 Y65.95 E.04582
|
|
||||||
G1 X57.5 Y65.814 E.0538
|
|
||||||
G1 X162.711 Y65.823 E3.99868
|
|
||||||
G1 X163.87 Y65.945 E.04429
|
|
||||||
G1 X165.073 Y66.264 E.0473
|
|
||||||
G1 X165.798 Y66.564 E.02982
|
|
||||||
G1 X166.842 Y67.165 E.04578
|
|
||||||
G1 X167.78 Y67.922 E.04581
|
|
||||||
G1 X168.587 Y68.817 E.0458
|
|
||||||
G1 X169.436 Y70.202 E.06174
|
|
||||||
G1 X169.867 Y71.328 E.04582
|
|
||||||
G1 X170.05 Y72.091 E.02982
|
|
||||||
G1 X170.177 Y73.289 E.04579
|
|
||||||
G1 X170.186 Y146.5 E2.78248
|
|
||||||
G1 X170.05 Y147.909 E.0538
|
|
||||||
G1 X169.736 Y149.073 E.04582
|
|
||||||
G1 X169.436 Y149.798 E.02982
|
|
||||||
G1 X168.587 Y151.183 E.06174
|
|
||||||
G1 X168.078 Y151.78 E.02982
|
|
||||||
G1 X167.183 Y152.587 E.0458
|
|
||||||
G1 X165.798 Y153.436 E.06174
|
|
||||||
G1 X164.672 Y153.867 E.04582
|
|
||||||
G1 X163.796 Y154.074 E.03421
|
|
||||||
G1 X129.811 Y160.705 E1.316
|
|
||||||
G1 X128.826 Y160.825 E.03771
|
|
||||||
G1 X127.704 Y160.792 E.04266
|
|
||||||
G1 X56.467 Y154.112 E2.71933
|
|
||||||
G1 X54.927 Y153.736 E.06025
|
|
||||||
G1 X53.827 Y153.245 E.04578
|
|
||||||
G1 X53.158 Y152.835 E.02982
|
|
||||||
G1 X52.22 Y152.078 E.04581
|
|
||||||
G1 X51.413 Y151.183 E.0458
|
|
||||||
G1 X50.755 Y150.173 E.04581
|
|
||||||
G1 X50.264 Y149.073 E.04578
|
|
||||||
G1 X49.95 Y147.909 E.04582
|
|
||||||
G1 X49.814 Y146.5 E.0538
|
|
||||||
G1 X49.814 Y73.5 E2.77446
|
|
||||||
G1 X49.95 Y72.091 E.0538
|
|
||||||
G1 X50.133 Y71.328 E.02982
|
|
||||||
G1 X50.564 Y70.202 E.04582
|
|
||||||
G1 X51.413 Y68.817 E.06174
|
|
||||||
G1 X51.922 Y68.22 E.02982
|
|
||||||
G1 X52.772 Y67.453 E.04351
|
|
||||||
M204 P2500
|
|
||||||
M204 T2000
|
|
||||||
G1 X53.077 Y67.793 F9000
|
|
||||||
M204 P500
|
|
||||||
G1 F1800
|
|
||||||
G1 X53.114 Y67.76 E.00188
|
|
||||||
G1 X54.066 Y67.145 E.04308
|
|
||||||
G1 X55.102 Y66.686 E.04307
|
|
||||||
G1 X56.198 Y66.395 E.0431
|
|
||||||
G1 X57.5 Y66.271 E.04971
|
|
||||||
G1 X162.675 Y66.278 E3.99732
|
|
||||||
G1 X163.765 Y66.39 E.04165
|
|
||||||
G1 X164.554 Y66.575 E.0308
|
|
||||||
G1 X165.623 Y66.986 E.04353
|
|
||||||
G1 X166.603 Y67.555 E.04307
|
|
||||||
G1 X167.73 Y68.517 E.05632
|
|
||||||
G1 X168.445 Y69.397 E.04309
|
|
||||||
G1 X169.014 Y70.377 E.04307
|
|
||||||
G1 X169.314 Y71.102 E.02982
|
|
||||||
G1 X169.605 Y72.198 E.0431
|
|
||||||
G1 X169.722 Y73.325 E.04306
|
|
||||||
G1 X169.729 Y146.5 E2.78111
|
|
||||||
G1 X169.605 Y147.802 E.04971
|
|
||||||
G1 X169.422 Y148.565 E.02982
|
|
||||||
G1 X169.014 Y149.623 E.0431
|
|
||||||
G1 X168.24 Y150.886 E.0563
|
|
||||||
G1 X167.73 Y151.483 E.02984
|
|
||||||
G1 X166.886 Y152.24 E.04309
|
|
||||||
G1 X165.623 Y153.014 E.0563
|
|
||||||
G1 X164.565 Y153.422 E.0431
|
|
||||||
G1 X163.709 Y153.626 E.03344
|
|
||||||
G1 X129.723 Y160.256 E1.31603
|
|
||||||
G1 X128.837 Y160.368 E.03394
|
|
||||||
G1 X127.746 Y160.337 E.04148
|
|
||||||
G1 X56.51 Y153.657 E2.7193
|
|
||||||
G1 X55.102 Y153.314 E.05508
|
|
||||||
G1 X54.066 Y152.855 E.04307
|
|
||||||
G1 X53.114 Y152.24 E.04308
|
|
||||||
G1 X52.517 Y151.73 E.02984
|
|
||||||
G1 X51.76 Y150.886 E.04309
|
|
||||||
G1 X51.145 Y149.934 E.04308
|
|
||||||
G1 X50.686 Y148.898 E.04307
|
|
||||||
G1 X50.395 Y147.802 E.0431
|
|
||||||
G1 X50.271 Y146.5 E.04971
|
|
||||||
G1 X50.271 Y73.5 E2.77446
|
|
||||||
G1 X50.395 Y72.198 E.04971
|
|
||||||
G1 X50.578 Y71.435 E.02982
|
|
||||||
G1 X50.986 Y70.377 E.0431
|
|
||||||
G1 X51.76 Y69.114 E.0563
|
|
||||||
G1 X52.27 Y68.517 E.02984
|
|
||||||
G1 X53.033 Y67.833 E.03895
|
|
||||||
M204 P2500
|
|
||||||
G1 E-1.2
|
|
||||||
G1 Z.6 F9000
|
|
||||||
; printing object temp_edit_f69b6cf4417a43fa81e39d69b3952b58.stl id:0 copy 0
|
|
||||||
G1 X131.959 Y152.611
|
|
||||||
G1 Z.2
|
|
||||||
G1 E1.2 F1800
|
|
||||||
M204 P500
|
|
||||||
;TYPE:Support material
|
|
||||||
G1 X132.246 Y153.441 E.03338
|
|
||||||
G1 X132.344 Y154.317 E.0335
|
|
||||||
G1 X132.246 Y155.193 E.0335
|
|
||||||
G1 X131.959 Y156.022 E.03334
|
|
||||||
G1 X131.547 Y156.692 E.02989
|
|
||||||
G1 X131.146 Y157.136 E.02274
|
|
||||||
G1 X130.723 Y157.499 E.02118
|
|
||||||
G1 X129.894 Y157.918 E.0353
|
|
||||||
G1 X128.891 Y158.141 E.03905
|
|
||||||
G1 X127.954 Y158.118 E.03562
|
|
||||||
G1 X127.013 Y157.85 E.03719
|
|
||||||
G1 X126.539 Y157.579 E.02075
|
|
||||||
G1 X126.056 Y157.138 E.02486
|
|
||||||
G1 X125.499 Y156.436 E.03406
|
|
||||||
G1 X125.153 Y155.827 E.02662
|
|
||||||
G1 X124.929 Y155.168 E.02645
|
|
||||||
G1 X124.834 Y154.278 E.03402
|
|
||||||
G1 X124.963 Y153.42 E.03298
|
|
||||||
G1 X125.287 Y152.609 E.03319
|
|
||||||
G1 X125.63 Y152.1 E.02333
|
|
||||||
G1 X131.643 Y152.1 E.22853
|
|
||||||
G1 X131.923 Y152.553 E.02024
|
|
||||||
M204 P2500
|
|
||||||
G1 X131.838 Y155.742 F9000
|
|
||||||
M204 P500
|
|
||||||
G1 F1800
|
|
||||||
G1 X131.838 Y153.52 E.08445
|
|
||||||
G1 X131.585 Y152.789 E.0294
|
|
||||||
G1 X131.414 Y152.511 E.0124
|
|
||||||
G1 X131.33 Y152.511 E.00319
|
|
||||||
G1 X131.33 Y156.259 E.14245
|
|
||||||
G1 X130.822 Y156.872 E.03026
|
|
||||||
G1 X130.822 Y152.511 E.16575
|
|
||||||
G1 X130.315 Y152.511 E.01927
|
|
||||||
G1 X130.315 Y157.244 E.17988
|
|
||||||
G1 X129.807 Y157.501 E.02164
|
|
||||||
G1 X129.807 Y152.511 E.18965
|
|
||||||
G1 X129.299 Y152.511 E.01931
|
|
||||||
G1 X129.299 Y157.644 E.19509
|
|
||||||
G1 X128.791 Y157.727 E.01956
|
|
||||||
G1 X128.791 Y152.511 E.19824
|
|
||||||
G1 X128.283 Y152.511 E.01931
|
|
||||||
G1 X128.283 Y157.714 E.19775
|
|
||||||
G1 X127.775 Y157.639 E.01952
|
|
||||||
G1 X127.775 Y152.511 E.1949
|
|
||||||
G1 X127.267 Y152.511 E.01931
|
|
||||||
G1 X127.267 Y157.494 E.18939
|
|
||||||
G1 X127.173 Y157.468 E.00371
|
|
||||||
G1 X126.76 Y157.224 E.01823
|
|
||||||
G1 X126.76 Y152.511 E.17912
|
|
||||||
G1 X126.252 Y152.511 E.01931
|
|
||||||
G1 X126.252 Y156.695 E.15902
|
|
||||||
G1 X125.744 Y156.034 E.03168
|
|
||||||
G1 X125.744 Y152.306 E.14169
|
|
||||||
M204 P2500
|
|
||||||
G1 E-1.2
|
|
||||||
G1 Z.6 F9000
|
|
||||||
G1 X140.305 Y87.116
|
|
||||||
G1 Z.2
|
|
||||||
G1 E1.2 F1800
|
|
||||||
M204 P500
|
|
||||||
;TYPE:Perimeter
|
|
||||||
;WIDTH:0.499999
|
|
||||||
G1 X140.124 Y87.275 E.00916
|
|
||||||
G1 X139.922 Y87.388 E.0088
|
|
||||||
G1 X139.444 Y87.556 E.01926
|
|
||||||
G1 X138.76 Y87.587 E.02602
|
|
||||||
G1 X138.531 Y87.548 E.00883
|
|
||||||
G1 X138.055 Y87.379 E.0192
|
|
||||||
G1 X137.502 Y86.973 E.02607
|
|
||||||
G1 X137.349 Y86.799 E.00881
|
|
||||||
G1 X137.085 Y86.371 E.01911
|
|
||||||
G1 X137.01 Y86.175 E.00798
|
|
||||||
G1 X136.904 Y85.68 E.01924
|
|
||||||
G1 X136.899 Y85.473 E.00787
|
|
||||||
G1 X136.967 Y84.955 E.01986
|
|
||||||
G1 X137.244 Y84.346 E.02543
|
|
||||||
G1 X137.382 Y84.159 E.00883
|
|
||||||
G1 X137.771 Y83.79 E.02038
|
|
||||||
G1 X138.359 Y83.499 E.02493
|
|
||||||
G1 X138.584 Y83.441 E.00883
|
|
||||||
G1 X139.121 Y83.397 E.02048
|
|
||||||
G1 X139.587 Y83.478 E.01798
|
|
||||||
G1 X140.079 Y83.697 E.02047
|
|
||||||
G1 X140.271 Y83.827 E.00881
|
|
||||||
G1 X140.66 Y84.204 E.02059
|
|
||||||
G1 X140.972 Y84.775 E.02473
|
|
||||||
G1 X141.04 Y84.996 E.00879
|
|
||||||
G1 X141.105 Y85.513 E.0198
|
|
||||||
G1 X140.99 Y86.175 E.02554
|
|
||||||
G1 X140.903 Y86.39 E.00881
|
|
||||||
G1 X140.632 Y86.828 E.01958
|
|
||||||
G1 X140.35 Y87.076 E.01427
|
|
||||||
M204 P2500
|
|
||||||
G1 X140.021 Y86.764 F9000
|
|
||||||
M204 P500
|
|
||||||
;TYPE:External perimeter
|
|
||||||
G1 F1800
|
|
||||||
G1 X139.837 Y86.919 E.00914
|
|
||||||
G1 X139.316 Y87.117 E.02118
|
|
||||||
G1 X138.76 Y87.13 E.02114
|
|
||||||
G1 X138.23 Y86.957 E.02119
|
|
||||||
G1 X137.789 Y86.617 E.02116
|
|
||||||
G1 X137.486 Y86.15 E.02116
|
|
||||||
G1 X137.356 Y85.608 E.02118
|
|
||||||
G1 X137.414 Y85.055 E.02113
|
|
||||||
G1 X137.653 Y84.551 E.0212
|
|
||||||
G1 X138.046 Y84.157 E.02115
|
|
||||||
G1 X138.548 Y83.916 E.02116
|
|
||||||
G1 X139.101 Y83.855 E.02114
|
|
||||||
G1 X139.452 Y83.916 E.01354
|
|
||||||
G1 X139.908 Y84.123 E.01903
|
|
||||||
G1 X140.301 Y84.489 E.02041
|
|
||||||
G1 X140.564 Y84.981 E.0212
|
|
||||||
G1 X140.647 Y85.531 E.02114
|
|
||||||
G1 X140.543 Y86.078 E.02116
|
|
||||||
G1 X140.262 Y86.559 E.02117
|
|
||||||
G1 X140.066 Y86.725 E.00976
|
|
||||||
M204 P2500
|
|
||||||
G1 E-1.2
|
|
||||||
G1 Z.6 F9000
|
|
||||||
G1 X162.311 Y71.773
|
|
||||||
G1 Z.2
|
|
||||||
G1 E1.2 F1800
|
|
||||||
M204 P500
|
|
||||||
G1 X162.659 Y71.282 E.02287
|
|
||||||
G1 X162.97 Y71.031 E.01519
|
|
||||||
G1 X163.488 Y70.797 E.0216
|
|
||||||
G1 X164.024 Y70.726 E.02055
|
|
||||||
G1 X164.494 Y70.79 E.01803
|
|
||||||
G1 X164.817 Y70.912 E.01312
|
|
||||||
G1 X165.285 Y71.228 E.02146
|
|
||||||
G1 X165.677 Y71.736 E.02439
|
|
||||||
G1 X165.877 Y72.346 E.0244
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import sys
|
|
||||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QCheckBox, QSlider
|
|
||||||
from PyQt6.QtCore import Qt
|
|
||||||
from gcode_viewer import GCodeViewerWidget
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.setWindowTitle("G-code 预览")
|
|
||||||
self.viewer = GCodeViewerWidget()
|
|
||||||
|
|
||||||
# 加载文件(请替换为你的 G-code 路径)
|
|
||||||
self.viewer.load_gcode("/home/lhye200/.octoprint/uploads/20260508141659_085359c9908947bebcaa0fe7490641e8.gcode")
|
|
||||||
|
|
||||||
# 进度滑块(0~100%)
|
|
||||||
self.slider = QSlider(Qt.Orientation.Horizontal)
|
|
||||||
self.slider.setRange(0, 100)
|
|
||||||
self.slider.setValue(100)
|
|
||||||
self.slider.valueChanged.connect(
|
|
||||||
lambda v: self.viewer.update_processes(v / 100.0))
|
|
||||||
|
|
||||||
# 类型开关
|
|
||||||
self.chk_support = QCheckBox("显示支撑")
|
|
||||||
self.chk_support.setChecked(True)
|
|
||||||
self.chk_support.toggled.connect(
|
|
||||||
lambda v: self.viewer.update_switch('SUPPORT', v))
|
|
||||||
|
|
||||||
self.chk_infill = QCheckBox("显示填充")
|
|
||||||
self.chk_infill.setChecked(True)
|
|
||||||
self.chk_infill.toggled.connect(
|
|
||||||
lambda v: self.viewer.update_switch('FILL', v))
|
|
||||||
|
|
||||||
self.chk_perimeter = QCheckBox("显示外壳")
|
|
||||||
self.chk_perimeter.setChecked(True)
|
|
||||||
self.chk_perimeter.toggled.connect(
|
|
||||||
lambda v: self.viewer.update_switch('WALL-OUTER', v))
|
|
||||||
self.chk_perimeter.toggled.connect(
|
|
||||||
lambda v: self.viewer.update_switch('WALL-INNER', v))
|
|
||||||
|
|
||||||
central = QWidget()
|
|
||||||
layout = QVBoxLayout(central)
|
|
||||||
layout.addWidget(self.viewer, 1)
|
|
||||||
layout.addWidget(self.slider)
|
|
||||||
layout.addWidget(self.chk_support)
|
|
||||||
layout.addWidget(self.chk_infill)
|
|
||||||
layout.addWidget(self.chk_perimeter)
|
|
||||||
self.setCentralWidget(central)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
win = MainWindow()
|
|
||||||
win.show()
|
|
||||||
sys.exit(app.exec())
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import OpenGL.GL as gl
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
|
||||||
|
|
||||||
class View(QOpenGLWidget):
|
|
||||||
def initializeGL(self):
|
|
||||||
v = gl.glGetFloatv(gl.GL_ALIASED_LINE_WIDTH_RANGE)
|
|
||||||
print("Line width range:", v)
|
|
||||||
QApplication.instance().quit()
|
|
||||||
|
|
||||||
app = QApplication([])
|
|
||||||
v = View()
|
|
||||||
v.show()
|
|
||||||
app.exec()
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import sys
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
|
||||||
class W(QOpenGLWidget):
|
|
||||||
def initializeGL(self):
|
|
||||||
import OpenGL.GL as gl
|
|
||||||
bounds = gl.glGetFloatv(gl.GL_ALIASED_LINE_WIDTH_RANGE)
|
|
||||||
print("Line width range:", bounds)
|
|
||||||
sys.exit(0)
|
|
||||||
app = QApplication([])
|
|
||||||
w = W()
|
|
||||||
w.show()
|
|
||||||
app.exec()
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from PyQt6.QtGui import QMatrix4x4
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
mat = QMatrix4x4()
|
|
||||||
mat.translate(0.0, 0.0, -250.0)
|
|
||||||
mat.rotate(30.0, 1.0, 0.0, 0.0)
|
|
||||||
mat.rotate(45.0, 0.0, 0.0, 1.0)
|
|
||||||
|
|
||||||
# If QMatrix4x4 passes list to uniformMatrix4fv, does it need column-major or row-major?
|
|
||||||
# When I used numpy: ``mvp.flatten().tolist()`` -> it gave row-major. And it WORKED.
|
|
||||||
# Let's check QMatrix4x4 data.
|
|
||||||
data = mat.data()
|
|
||||||
print("QMatrix4x4 data (length %d):" % len(data))
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
import math
|
|
||||||
|
|
||||||
rx = math.radians(-60.0)
|
|
||||||
rz = math.radians(45.0)
|
|
||||||
cosX, sinX = math.cos(rx), math.sin(rx)
|
|
||||||
cosZ, sinZ = math.cos(rz), math.sin(rz)
|
|
||||||
# Original code rotation matrix
|
|
||||||
rot = np.array([
|
|
||||||
[cosZ, -sinZ*cosX, sinZ*sinX, 0],
|
|
||||||
[sinZ, cosZ*cosX, -cosZ*sinX, 0],
|
|
||||||
[0, sinX, cosX, 0],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
])
|
|
||||||
print("Original rot:\\n", rot)
|
|
||||||
|
|
||||||
# Correct orbit Rx @ Rz
|
|
||||||
rot_correct = np.array([
|
|
||||||
[cosZ, -sinZ, 0, 0],
|
|
||||||
[cosX*sinZ, cosX*cosZ, -sinX, 0],
|
|
||||||
[sinX*sinZ, sinX*cosZ, cosX, 0],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
])
|
|
||||||
print("Rx @ Rz:\\n", rot_correct)
|
|
||||||
|
|
||||||
@@ -1 +1,5 @@
|
|||||||
PyQt6
|
requests
|
||||||
|
numpy
|
||||||
|
PyOpenGL
|
||||||
|
qrcode
|
||||||
|
Pillow
|
||||||
|
|||||||
8
run.sh
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
# echo "Running Printer Screen Menu..."
|
# echo "Running Printer Screen Menu..."
|
||||||
# 激活虚拟环境
|
# 激活虚拟环境
|
||||||
source "$(dirname "$0")/.venv/bin/activate"
|
source "$(dirname "$0")/venv/bin/activate"
|
||||||
# echo "Virtual environment activated."
|
# echo "Virtual environment activated."
|
||||||
# 告诉Qt使用 eglfs 插件(无桌面环境直接利用 KMS/DRM/EGL 渲染)
|
# 告诉Qt使用 eglfs 插件(无桌面环境直接利用 KMS/DRM/EGL 渲染)
|
||||||
export QT_QPA_PLATFORM=eglfs
|
export QT_QPA_PLATFORM=eglfs
|
||||||
|
# export QT_QPA_EGLFS_SWAPINTERVAL=0
|
||||||
|
# export QT_QPA_EGLFS_FORCEVSYNC=0
|
||||||
|
# export QT_AUTO_SCREEN_SCALE_FACTOR=0
|
||||||
|
# export QT_SCALE_FACTOR=1
|
||||||
|
# export QT_ENABLE_HIGHDPI_SCALING=0
|
||||||
# export QT_QUICK_BACKEND=software
|
# export QT_QUICK_BACKEND=software
|
||||||
# 如果你的屏幕触摸不生效或者显示比例不对,可能还需要下面这些环境变量:
|
# 如果你的屏幕触摸不生效或者显示比例不对,可能还需要下面这些环境变量:
|
||||||
# 触摸屏设备路径,可以通过 ls /dev/input/ 查看
|
# 触摸屏设备路径,可以通过 ls /dev/input/ 查看
|
||||||
@@ -15,6 +20,7 @@ export QT_QPA_PLATFORM=eglfs
|
|||||||
|
|
||||||
# 若启动报错,可以打开以下变量排查 eglfs 相关日志
|
# 若启动报错,可以打开以下变量排查 eglfs 相关日志
|
||||||
# export QT_DEBUG_PLUGINS=1
|
# export QT_DEBUG_PLUGINS=1
|
||||||
|
# export QT_QPA_EGLFS_DEBUG=1
|
||||||
# echo "QT_QPA_PLATFORM: $QT_QPA_PLATFORM"
|
# echo "QT_QPA_PLATFORM: $QT_QPA_PLATFORM"
|
||||||
# echo "directory: $(dirname "$0")"
|
# echo "directory: $(dirname "$0")"
|
||||||
|
|
||||||
|
|||||||
22
test_svg.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from PyQt6.QtWidgets import QApplication, QLabel
|
||||||
|
from PyQt6.QtGui import QPixmap
|
||||||
|
import sys
|
||||||
|
import base64
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
def get_colored_svg_uri(path: str, color: str) -> str:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
svg_content = f.read()
|
||||||
|
svg_content = svg_content.replace('currentColor', color)
|
||||||
|
b64 = base64.b64encode(svg_content.encode('utf-8')).decode('utf-8')
|
||||||
|
return f"data:image/svg+xml;base64,{b64}"
|
||||||
|
|
||||||
|
path = "third_party/Bootstrap/bootstrap-icons-1.13.1/reception-4.svg"
|
||||||
|
uri = get_colored_svg_uri(path, "#a0d8a0")
|
||||||
|
|
||||||
|
label = QLabel()
|
||||||
|
label.setText(f"<img src='{uri}'> Hello")
|
||||||
|
label.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
21
third_party/Bootstrap/LICENSE.txt
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019-2024 The Bootstrap Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
4
third_party/Bootstrap/bootstrap-icons-1.13.1/0-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 476 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/0-circle.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 507 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/0-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 514 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/0-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 579 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/1-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 250 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/1-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/1-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 286 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/1-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/123.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 854 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/2-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/2-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 477 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/2-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm4.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 484 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/2-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 564 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/3-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.082.414c.92 0 1.535.54 1.541 1.318.012.791-.615 1.36-1.588 1.354-.861-.006-1.482-.469-1.54-1.066H5.104c.047 1.177 1.05 2.144 2.754 2.144 1.653 0 2.954-.937 2.93-2.396-.023-1.278-1.031-1.846-1.734-1.916v-.07c.597-.1 1.505-.739 1.482-1.876-.03-1.177-1.043-2.074-2.637-2.062-1.675.006-2.59.984-2.625 2.12h1.248c.036-.556.557-1.054 1.348-1.054.785 0 1.348.486 1.348 1.195.006.715-.563 1.237-1.342 1.237h-.838v1.072h.879Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 607 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/3-circle.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 642 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/3-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 634 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/3-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 714 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/4-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M7.519 5.057c-.886 1.418-1.772 2.838-2.542 4.265v1.12H8.85V12h1.26v-1.559h1.007V9.334H10.11V4.002H8.176zM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 359 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/4-circle.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/4-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/4-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 493 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/5-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.006 4.158c1.74 0 2.924-1.119 2.924-2.806 0-1.641-1.178-2.584-2.56-2.584-.897 0-1.442.421-1.612.68h-.064l.193-2.344h3.621V4.002H5.791L5.445 8.63h1.149c.193-.358.668-.809 1.435-.809.85 0 1.582.604 1.582 1.57 0 1.085-.779 1.682-1.57 1.682-.697 0-1.389-.31-1.53-1.031H5.276c.065 1.213 1.149 2.115 2.72 2.115Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 495 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/5-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 1 14 0A7 7 0 0 1 1 8m15 0A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8.006 4.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 514 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/5-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 521 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/5-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 601 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/6-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c-1.868 0-3.116 1.395-3.116 4.407 0 1.183.228 2.039.597 2.642.569.926 1.477 1.254 2.409 1.254 1.629 0 2.847-1.013 2.847-2.783 0-1.676-1.254-2.555-2.508-2.555-1.125 0-1.752.61-1.98 1.155h-.082c-.012-1.946.727-3.036 1.805-3.036.802 0 1.213.457 1.312.815h1.29c-.06-.908-.962-1.899-2.573-1.899Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 617 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/6-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 640 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/6-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.111 7.863c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm6.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/6-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 727 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/7-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11h3.972v.07L6.025 12H7.42l3.258-6.85V4.002H5.369v1.107Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 251 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/7-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/7-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm3.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 286 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/7-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/8-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0-1.248-.943-1.84-1.646-1.992v-.065c.598-.187 1.336-.72 1.336-1.781 0-1.225-1.084-2.121-2.654-2.121s-2.66.896-2.66 2.12c0 1.044.709 1.589 1.33 1.782v.065c-.697.152-1.647.732-1.647 2.003 0 1.39 1.19 2.344 2.953 2.344 1.77 0 2.989-.96 2.989-2.355Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 686 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/8-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 717 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/8-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.623 6.094c0 .738.586 1.254 1.383 1.254s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23m-.281 3.644c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm8.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 737 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/8-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 804 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/9-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.223 4.146c2.104 0 3.123-1.464 3.123-4.3 0-3.147-1.459-4.014-2.97-4.014-1.63 0-2.871 1.02-2.871 2.73 0 1.706 1.171 2.667 2.566 2.667 1.06 0 1.7-.557 1.934-1.184h.076c.047 1.67-.475 3.023-1.834 3.023-.71 0-1.149-.363-1.248-.72H5.258c.094.908.926 1.798 2.52 1.798Zm.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 574 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/9-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.223 4.146c-1.593 0-2.425-.89-2.52-1.798h1.296c.1.357.539.72 1.248.72 1.36 0 1.88-1.353 1.834-3.023h-.076c-.235.627-.873 1.184-1.934 1.184-1.395 0-2.566-.961-2.566-2.666 0-1.711 1.242-2.731 2.87-2.731 1.512 0 2.971.867 2.971 4.014 0 2.836-1.02 4.3-3.123 4.3m.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 597 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/9-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.895 8.174c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.777 12.146c-1.593 0-2.425-.89-2.52-1.798h1.296c.1.357.539.72 1.248.72 1.36 0 1.88-1.353 1.834-3.023h-.076c-.235.627-.873 1.184-1.934 1.184-1.395 0-2.566-.961-2.566-2.666 0-1.711 1.242-2.731 2.87-2.731 1.512 0 2.971.867 2.971 4.014 0 2.836-1.02 4.3-3.123 4.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 620 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/9-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-square" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.777 12.146c-1.593 0-2.425-.89-2.52-1.798h1.296c.1.357.539.72 1.248.72 1.36 0 1.88-1.353 1.834-3.023h-.076c-.235.627-.873 1.184-1.934 1.184-1.395 0-2.566-.961-2.566-2.666 0-1.711 1.242-2.731 2.87-2.731 1.512 0 2.971.867 2.971 4.014 0 2.836-1.02 4.3-3.123 4.3m.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 684 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/activity.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-activity" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/airplane-engines-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane-engines-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0c-.787 0-1.292.592-1.572 1.151A4.35 4.35 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272.313.937a.5.5 0 0 0 .948 0l.405-1.214 2.21-.369.375 2.253-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318.375-2.253 2.21.369.405 1.214a.5.5 0 0 0 .948 0l.313-.937 1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 687 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/airplane-engines.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane-engines" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0c-.787 0-1.292.592-1.572 1.151A4.35 4.35 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272.313.937a.5.5 0 0 0 .948 0l.405-1.214 2.21-.369.375 2.253-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318.375-2.253 2.21.369.405 1.214a.5.5 0 0 0 .948 0l.313-.937 1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0M7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1s.458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/airplane-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918-.375 2.253 1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318-.376-2.253-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 492 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/airplane.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918-.375 2.253 1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318-.376-2.253-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849m.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1s-.458.158-.678.599"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 840 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/alarm-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alarm-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H9v1.07a7.001 7.001 0 0 1 3.274 12.474l.601.602a.5.5 0 0 1-.707.708l-.746-.746A6.97 6.97 0 0 1 8 16a6.97 6.97 0 0 1-3.422-.892l-.746.746a.5.5 0 0 1-.707-.708l.602-.602A7.001 7.001 0 0 1 7 2.07V1h-.5A.5.5 0 0 1 6 .5m2.5 5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9zM.86 5.387A2.5 2.5 0 1 1 4.387 1.86 8.04 8.04 0 0 0 .86 5.387M11.613 1.86a2.5 2.5 0 1 1 3.527 3.527 8.04 8.04 0 0 0-3.527-3.527"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 615 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/alarm.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alarm" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9z"/>
|
||||||
|
<path d="M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1zm1.038 3.018a6 6 0 0 1 .924 0 6 6 0 1 1-.924 0M0 3.5c0 .753.333 1.429.86 1.887A8.04 8.04 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5M13.5 1c-.753 0-1.429.333-1.887.86a8.04 8.04 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 689 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/alexa.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alexa" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.996 0A8 8 0 0 0 0 8a8 8 0 0 0 6.93 7.93v-1.613a1.06 1.06 0 0 0-.717-1.008A5.6 5.6 0 0 1 2.4 7.865 5.58 5.58 0 0 1 8.054 2.4a5.6 5.6 0 0 1 5.535 5.81l-.002.046-.012.192-.005.061a5 5 0 0 1-.033.284l-.01.068c-.685 4.516-6.564 7.054-6.596 7.068A7.998 7.998 0 0 0 15.992 8 8 8 0 0 0 7.996.001Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 436 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/align-bottom.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-bottom" viewBox="0 0 16 16">
|
||||||
|
<rect width="4" height="12" x="6" y="1" rx="1"/>
|
||||||
|
<path d="M1.5 14a.5.5 0 0 0 0 1zm13 1a.5.5 0 0 0 0-1zm-13 0h13v-1h-13z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 264 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/align-center.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-center" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a.5.5 0 0 1 .5.5V6h-1V1.5A.5.5 0 0 1 8 1m0 14a.5.5 0 0 1-.5-.5V10h1v4.5a.5.5 0 0 1-.5.5M2 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/align-end.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-end" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 1 0v-13a.5.5 0 0 0-.5-.5"/>
|
||||||
|
<path d="M13 7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 315 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/align-middle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-middle" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 13a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zM1 8a.5.5 0 0 0 .5.5H6v-1H1.5A.5.5 0 0 0 1 8m14 0a.5.5 0 0 1-.5.5H10v-1h4.5a.5.5 0 0 1 .5.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/align-start.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-start" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1.5 1a.5.5 0 0 1 .5.5v13a.5.5 0 0 1-1 0v-13a.5.5 0 0 1 .5-.5"/>
|
||||||
|
<path d="M3 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 315 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/align-top.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-top" viewBox="0 0 16 16">
|
||||||
|
<rect width="4" height="12" rx="1" transform="matrix(1 0 0 -1 6 15)"/>
|
||||||
|
<path d="M1.5 2a.5.5 0 0 1 0-1zm13-1a.5.5 0 0 1 0 1zm-13 0h13v1h-13z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/alipay.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alipay" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.541 0H13.5a2.55 2.55 0 0 1 2.54 2.563v8.297c-.006 0-.531-.046-2.978-.813-.412-.14-.916-.327-1.479-.536q-.456-.17-.957-.353a13 13 0 0 0 1.325-3.373H8.822V4.649h3.831v-.634h-3.83V2.121H7.26c-.274 0-.274.273-.274.273v1.621H3.11v.634h3.875v1.136h-3.2v.634H9.99c-.227.789-.532 1.53-.894 2.202-2.013-.67-4.161-1.212-5.51-.878-.864.214-1.42.597-1.746.998-1.499 1.84-.424 4.633 2.741 4.633 1.872 0 3.675-1.053 5.072-2.787 2.08 1.008 6.37 2.738 6.387 2.745v.105A2.55 2.55 0 0 1 13.5 16H2.541A2.55 2.55 0 0 1 0 13.437V2.563A2.55 2.55 0 0 1 2.541 0"/>
|
||||||
|
<path d="M2.309 9.27c-1.22 1.073-.49 3.034 1.978 3.034 1.434 0 2.868-.925 3.994-2.406-1.602-.789-2.959-1.353-4.425-1.207-.397.04-1.14.217-1.547.58Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 839 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/alphabet-uppercase.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alphabet-uppercase" viewBox="0 0 16 16">
|
||||||
|
<path d="M1.226 10.88H0l2.056-6.26h1.42l2.047 6.26h-1.29l-.48-1.61H1.707l-.48 1.61ZM2.76 5.818h-.054l-.75 2.532H3.51zm3.217 5.062V4.62h2.56c1.09 0 1.808.582 1.808 1.54 0 .762-.444 1.22-1.05 1.372v.055c.736.074 1.365.587 1.365 1.528 0 1.119-.89 1.766-2.133 1.766zM7.18 5.55v1.675h.8c.812 0 1.171-.308 1.171-.853 0-.51-.328-.822-.898-.822zm0 2.537V9.95h.903c.951 0 1.342-.312 1.342-.909 0-.591-.382-.954-1.095-.954zm5.089-.711v.775c0 1.156.49 1.803 1.347 1.803.705 0 1.163-.454 1.212-1.096H16v.12C15.942 10.173 14.95 11 13.607 11c-1.648 0-2.573-1.073-2.573-2.849v-.78c0-1.775.934-2.871 2.573-2.871 1.347 0 2.34.849 2.393 2.087v.115h-1.172c-.05-.665-.516-1.156-1.212-1.156-.849 0-1.347.67-1.347 1.83"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 845 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/alphabet.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alphabet" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.204 11.078c.767 0 1.201-.356 1.406-.737h.059V11h1.216V7.519c0-1.314-.947-1.783-2.11-1.783C1.355 5.736.75 6.42.69 7.27h1.216c.064-.323.313-.552.84-.552s.864.249.864.771v.464H2.346C1.145 7.953.5 8.568.5 9.496c0 .977.693 1.582 1.704 1.582m.42-.947c-.44 0-.845-.235-.845-.718 0-.395.269-.684.84-.684h.991v.538c0 .503-.444.864-.986.864m5.593.937c1.216 0 1.948-.869 1.948-2.31v-.702c0-1.44-.727-2.305-1.929-2.305-.742 0-1.328.347-1.499.889h-.063V3.983h-1.29V11h1.27v-.791h.064c.21.532.776.86 1.499.86Zm-.43-1.025c-.66 0-1.113-.518-1.113-1.28V8.12c0-.825.42-1.343 1.098-1.343.684 0 1.075.518 1.075 1.416v.45c0 .888-.386 1.401-1.06 1.401Zm2.834-1.328c0 1.47.87 2.378 2.305 2.378 1.416 0 2.139-.777 2.158-1.763h-1.186c-.06.425-.313.732-.933.732-.66 0-1.05-.512-1.05-1.352v-.625c0-.81.371-1.328 1.045-1.328.635 0 .879.425.918.776h1.187c-.02-.986-.787-1.806-2.14-1.806-1.41 0-2.304.918-2.304 2.338z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/alt.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alt" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5m10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 324 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/amazon.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-amazon" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.813 11.968c.157.083.36.074.5-.05l.005.005a90 90 0 0 1 1.623-1.405c.173-.143.143-.372.006-.563l-.125-.17c-.345-.465-.673-.906-.673-1.791v-3.3l.001-.335c.008-1.265.014-2.421-.933-3.305C10.404.274 9.06 0 8.03 0 6.017 0 3.77.75 3.296 3.24c-.047.264.143.404.316.443l2.054.22c.19-.009.33-.196.366-.387.176-.857.896-1.271 1.703-1.271.435 0 .929.16 1.188.55.264.39.26.91.257 1.376v.432q-.3.033-.621.065c-1.113.114-2.397.246-3.36.67C3.873 5.91 2.94 7.08 2.94 8.798c0 2.2 1.387 3.298 3.168 3.298 1.506 0 2.328-.354 3.489-1.54l.167.246c.274.405.456.675 1.047 1.166ZM6.03 8.431C6.03 6.627 7.647 6.3 9.177 6.3v.57c.001.776.002 1.434-.396 2.133-.336.595-.87.961-1.465.961-.812 0-1.286-.619-1.286-1.533M.435 12.174c2.629 1.603 6.698 4.084 13.183.997.28-.116.475.078.199.431C13.538 13.96 11.312 16 7.57 16 3.832 16 .968 13.446.094 12.386c-.24-.275.036-.4.199-.299z"/>
|
||||||
|
<path d="M13.828 11.943c.567-.07 1.468-.027 1.645.204.135.176-.004.966-.233 1.533-.23.563-.572.961-.762 1.115s-.333.094-.23-.137c.105-.23.684-1.663.455-1.963-.213-.278-1.177-.177-1.625-.13l-.09.009q-.142.013-.233.024c-.193.021-.245.027-.274-.032-.074-.209.779-.556 1.347-.623"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/amd.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-amd" viewBox="0 0 16 16">
|
||||||
|
<path d="m.334 0 4.358 4.359h7.15v7.15l4.358 4.358V0zM.2 9.72l4.487-4.488v6.281h6.28L6.48 16H.2z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 230 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/android.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-android" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 432 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/android2.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-android2" viewBox="0 0 16 16">
|
||||||
|
<path d="m10.213 1.471.691-1.26q.069-.124-.048-.192-.128-.057-.195.058l-.7 1.27A4.8 4.8 0 0 0 8.005.941q-1.032 0-1.956.404l-.7-1.27Q5.281-.037 5.154.02q-.117.069-.049.193l.691 1.259a4.25 4.25 0 0 0-1.673 1.476A3.7 3.7 0 0 0 3.5 5.02h9q0-1.125-.623-2.072a4.27 4.27 0 0 0-1.664-1.476ZM6.22 3.303a.37.37 0 0 1-.267.11.35.35 0 0 1-.263-.11.37.37 0 0 1-.107-.264.37.37 0 0 1 .107-.265.35.35 0 0 1 .263-.11q.155 0 .267.11a.36.36 0 0 1 .112.265.36.36 0 0 1-.112.264m4.101 0a.35.35 0 0 1-.262.11.37.37 0 0 1-.268-.11.36.36 0 0 1-.112-.264q0-.154.112-.265a.37.37 0 0 1 .268-.11q.155 0 .262.11a.37.37 0 0 1 .107.265q0 .153-.107.264M3.5 11.77q0 .441.311.75.311.306.76.307h.758l.01 2.182q0 .414.292.703a.96.96 0 0 0 .7.288.97.97 0 0 0 .71-.288.95.95 0 0 0 .292-.703v-2.182h1.343v2.182q0 .414.292.703a.97.97 0 0 0 .71.288.97.97 0 0 0 .71-.288.95.95 0 0 0 .292-.703v-2.182h.76q.436 0 .749-.308.31-.307.311-.75V5.365h-9zm10.495-6.587a.98.98 0 0 0-.702.278.9.9 0 0 0-.293.685v4.063q0 .406.293.69a.97.97 0 0 0 .702.284q.42 0 .712-.284a.92.92 0 0 0 .293-.69V6.146a.9.9 0 0 0-.293-.685 1 1 0 0 0-.712-.278m-12.702.283a1 1 0 0 1 .712-.283q.41 0 .702.283a.9.9 0 0 1 .293.68v4.063a.93.93 0 0 1-.288.69.97.97 0 0 1-.707.284 1 1 0 0 1-.712-.284.92.92 0 0 1-.293-.69V6.146q0-.396.293-.68"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/anthropic.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-anthropic" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M9.218 2h2.402L16 12.987h-2.402zM4.379 2h2.512l4.38 10.987H8.82l-.895-2.308h-4.58l-.896 2.307H0L4.38 2.001zm2.755 6.64L5.635 4.777 4.137 8.64z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/app-indicator.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app-indicator" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 2A3.5 3.5 0 0 0 2 5.5v5A3.5 3.5 0 0 0 5.5 14h5a3.5 3.5 0 0 0 3.5-3.5V8a.5.5 0 0 1 1 0v2.5a4.5 4.5 0 0 1-4.5 4.5h-5A4.5 4.5 0 0 1 1 10.5v-5A4.5 4.5 0 0 1 5.5 1H8a.5.5 0 0 1 0 1z"/>
|
||||||
|
<path d="M16 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 382 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/app.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/apple-music.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-apple-music" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="m10.995 0 .573.001q.241 0 .483.007c.35.01.705.03 1.051.093.352.063.68.166.999.329a3.36 3.36 0 0 1 1.47 1.468c.162.32.265.648.328 1 .063.347.084.7.093 1.051q.007.241.007.483l.001.573v5.99l-.001.573q0 .241-.008.483c-.01.35-.03.704-.092 1.05a3.5 3.5 0 0 1-.33 1 3.36 3.36 0 0 1-1.468 1.468 3.5 3.5 0 0 1-1 .33 7 7 0 0 1-1.05.092q-.241.007-.483.008l-.573.001h-5.99l-.573-.001q-.241 0-.483-.008a7 7 0 0 1-1.052-.092 3.6 3.6 0 0 1-.998-.33 3.36 3.36 0 0 1-1.47-1.468 3.6 3.6 0 0 1-.328-1 7 7 0 0 1-.093-1.05Q.002 11.81 0 11.568V5.005l.001-.573q0-.241.007-.483c.01-.35.03-.704.093-1.05a3.6 3.6 0 0 1 .329-1A3.36 3.36 0 0 1 1.9.431 3.5 3.5 0 0 1 2.896.1 7 7 0 0 1 3.95.008Q4.19.002 4.432 0h.573zm-.107 2.518-4.756.959H6.13a.66.66 0 0 0-.296.133.5.5 0 0 0-.16.31c-.004.027-.01.08-.01.16v5.952c0 .14-.012.275-.106.39-.095.115-.21.15-.347.177l-.31.063c-.393.08-.65.133-.881.223a1.4 1.4 0 0 0-.519.333 1.25 1.25 0 0 0-.332.995c.031.297.166.582.395.792.156.142.35.25.578.296.236.047.49.031.858-.043.196-.04.38-.102.555-.205a1.4 1.4 0 0 0 .438-.405 1.5 1.5 0 0 0 .233-.55c.042-.202.052-.386.052-.588V6.347c0-.276.08-.35.302-.404.024-.005 3.954-.797 4.138-.833.257-.049.378.025.378.294v3.524c0 .14-.001.28-.096.396-.094.115-.211.15-.348.178l-.31.062c-.393.08-.649.133-.88.223a1.4 1.4 0 0 0-.52.334 1.26 1.26 0 0 0-.34.994c.03.297.174.582.404.792a1.2 1.2 0 0 0 .577.294c.237.048.49.03.858-.044.197-.04.381-.098.556-.202a1.4 1.4 0 0 0 .438-.405q.173-.252.233-.549a2.7 2.7 0 0 0 .044-.589V2.865c0-.273-.143-.443-.4-.42-.04.003-.383.064-.424.073"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
4
third_party/Bootstrap/bootstrap-icons-1.13.1/apple.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-apple" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516s1.52.087 2.475-1.258.762-2.391.728-2.43m3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422s1.675-2.789 1.698-2.854-.597-.79-1.254-1.157a3.7 3.7 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56s.625 1.924 1.273 2.796c.576.984 1.34 1.667 1.659 1.899s1.219.386 1.843.067c.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758q.52-1.185.473-1.282"/>
|
||||||
|
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516s1.52.087 2.475-1.258.762-2.391.728-2.43m3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422s1.675-2.789 1.698-2.854-.597-.79-1.254-1.157a3.7 3.7 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56s.625 1.924 1.273 2.796c.576.984 1.34 1.667 1.659 1.899s1.219.386 1.843.067c.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758q.52-1.185.473-1.282"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/archive-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.643 15C13.979 15 15 13.845 15 12.5V5H1v7.5C1 13.845 2.021 15 3.357 15zM5.5 7h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1 0-1M.8 1a.8.8 0 0 0-.8.8V3a.8.8 0 0 0 .8.8h14.4A.8.8 0 0 0 16 3V1.8a.8.8 0 0 0-.8-.8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 349 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/archive.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5zm13-3H1v2h14zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 394 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/arrow-90deg-down.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-down" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4.854 14.854a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V3.5A2.5 2.5 0 0 1 6.5 1h8a.5.5 0 0 1 0 1h-8A1.5 1.5 0 0 0 5 3.5v9.793l3.146-3.147a.5.5 0 0 1 .708.708z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 345 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/arrow-90deg-left.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1.146 4.854a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H12.5A2.5 2.5 0 0 1 15 6.5v8a.5.5 0 0 1-1 0v-8A1.5 1.5 0 0 0 12.5 5H2.707l3.147 3.146a.5.5 0 1 1-.708.708z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/arrow-90deg-right.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14.854 4.854a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 4H3.5A2.5 2.5 0 0 0 1 6.5v8a.5.5 0 0 0 1 0v-8A1.5 1.5 0 0 1 3.5 5h9.793l-3.147 3.146a.5.5 0 0 0 .708.708z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 346 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/arrow-90deg-up.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-up" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4.854 1.146a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L4 2.707V12.5A2.5 2.5 0 0 0 6.5 15h8a.5.5 0 0 0 0-1h-8A1.5 1.5 0 0 1 5 12.5V2.707l3.146 3.147a.5.5 0 1 0 .708-.708z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
3
third_party/Bootstrap/bootstrap-icons-1.13.1/arrow-bar-down.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-down" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 3.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5M8 6a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 .708-.708L7.5 12.293V6.5A.5.5 0 0 1 8 6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 373 B |