Compare commits
37 Commits
a104d74ae6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96350bb8e2 | |||
| 28e35ea429 | |||
| a708bbfa72 | |||
| db53baba23 | |||
| d95bdef3f5 | |||
| 1a00811cfc | |||
| 836ccbe4aa | |||
| a2f453a638 | |||
| 7f23d64eb2 | |||
| 475d23f49e | |||
| 85697f35c6 | |||
| de0655adbc | |||
| 8f8e1ed1aa | |||
| 39fcaf35ca | |||
| da723409ca | |||
| 438cb8a1d9 | |||
| 5749dbbcee | |||
| eb9b8f5464 | |||
| b086957452 | |||
| 9e23810ccb | |||
| 964ba2ef8e | |||
| 62e80bfe1e | |||
| cd5a0f44c7 | |||
| ce9d0b64f2 | |||
| 06f39aa01b | |||
| c990a4eb36 | |||
| 7e3d3f4ac0 | |||
| 375ea9a686 | |||
| 9d703212ef | |||
| 8a07bb728e | |||
| b29750b56b | |||
| 909f1075b4 | |||
| b845b5994a | |||
| 64f226c714 | |||
| 5eafb11f0b | |||
| c6af350e4b | |||
| 1477097e17 |
674
LICENSE
Normal file
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>.
|
||||
22
build.bat
22
build.bat
@@ -1,38 +1,36 @@
|
||||
@echo off
|
||||
setlocal
|
||||
chcp 65001
|
||||
|
||||
echo === 尝试激活虚拟环境 ===
|
||||
echo === Activating virtual environment ===
|
||||
if exist ".venv\Scripts\activate.bat" (
|
||||
call .venv\Scripts\activate.bat
|
||||
) else (
|
||||
echo 未发现虚拟环境,尝试创建中...
|
||||
echo Virtual environment not found. Creating...
|
||||
python -m venv .venv
|
||||
if errorlevel 1 (
|
||||
echo [错误] 创建虚拟环境失败,请确认是否已安装 Python。
|
||||
echo [ERROR] Failed to create virtual environment. Please ensure Python is installed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo 虚拟环境创建成功,开始激活...
|
||||
echo Virtual environment created. Activating...
|
||||
call .venv\Scripts\activate.bat
|
||||
)
|
||||
|
||||
echo === 安装依赖项 ===
|
||||
pip install -r requirements.txt
|
||||
echo === Installing dependencies ===
|
||||
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple -r requirements.txt
|
||||
if errorlevel 1 (
|
||||
echo [错误] pip 安装依赖失败!
|
||||
echo [ERROR] pip failed to install dependencies.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo === 使用 pyinstaller 构建 ===
|
||||
echo === Building with pyinstaller ===
|
||||
python .\utils\hook.py
|
||||
pyinstaller .\main.spec
|
||||
if errorlevel 1 (
|
||||
echo [错误] 构建失败!
|
||||
echo [ERROR] Build failed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo === 构建完成 ===
|
||||
pause
|
||||
echo === Build completed ===
|
||||
|
||||
352
images/3rd/matplotlib.svg
Normal file
352
images/3rd/matplotlib.svg
Normal file
@@ -0,0 +1,352 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="167.76pt" height="167.76pt" viewBox="0 0 167.76 167.76" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<metadata>
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<cc:Work>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:date>2022-09-27T22:26:51.030457</dc:date>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Matplotlib v3.6.0, https://matplotlib.org/</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs>
|
||||
<style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}</style>
|
||||
</defs>
|
||||
<g id="figure_1">
|
||||
<g id="patch_1">
|
||||
<path d="M 0 167.76
|
||||
L 167.76 167.76
|
||||
L 167.76 0
|
||||
L 0 0
|
||||
L 0 167.76
|
||||
z
|
||||
" style="fill: none; opacity: 0"/>
|
||||
</g>
|
||||
<g id="axes_1">
|
||||
<g id="patch_2">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 167.808464 83.88
|
||||
C 167.808464 94.901385 165.637491 105.815601 161.41979 115.998033
|
||||
C 157.202089 126.180465 151.019682 135.43309 143.226386 143.226386
|
||||
C 135.43309 151.019682 126.180465 157.202089 115.998033 161.41979
|
||||
C 105.815601 165.637491 94.901385 167.808464 83.88 167.808464
|
||||
C 72.858615 167.808464 61.944399 165.637491 51.761967 161.41979
|
||||
C 41.579535 157.202089 32.32691 151.019682 24.533614 143.226386
|
||||
C 16.740318 135.43309 10.557911 126.180465 6.34021 115.998033
|
||||
C 2.122509 105.815601 -0.048464 94.901385 -0.048464 83.88
|
||||
C -0.048464 72.858615 2.122509 61.944399 6.34021 51.761967
|
||||
C 10.557911 41.579535 16.740318 32.32691 24.533614 24.533614
|
||||
C 32.32691 16.740318 41.579535 10.557911 51.761967 6.34021
|
||||
C 61.944399 2.122509 72.858615 -0.048464 83.88 -0.048464
|
||||
C 94.901385 -0.048464 105.815601 2.122509 115.998033 6.34021
|
||||
C 126.180465 10.557911 135.43309 16.740318 143.226386 24.533614
|
||||
C 151.019682 32.32691 157.202089 41.579535 161.41979 51.761967
|
||||
C 165.637491 61.944399 167.808464 72.858615 167.808464 83.88
|
||||
z
|
||||
" style="fill: #ffffff; fill-opacity: 0.9"/>
|
||||
</g>
|
||||
<g id="matplotlib.axis_1">
|
||||
<g id="xtick_1">
|
||||
<g id="line2d_1">
|
||||
<path d="M 83.88 83.88
|
||||
L 162.7272 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_2">
|
||||
<g id="line2d_2">
|
||||
<path d="M 83.88 83.88
|
||||
L 139.63339 28.12661
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_3">
|
||||
<g id="line2d_3">
|
||||
<path d="M 83.88 83.88
|
||||
L 83.88 5.0328
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_4">
|
||||
<g id="line2d_4">
|
||||
<path d="M 83.88 83.88
|
||||
L 28.12661 28.12661
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_5">
|
||||
<g id="line2d_5">
|
||||
<path d="M 83.88 83.88
|
||||
L 5.0328 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_6">
|
||||
<g id="line2d_6">
|
||||
<path d="M 83.88 83.88
|
||||
L 28.12661 139.63339
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_7">
|
||||
<g id="line2d_7">
|
||||
<path d="M 83.88 83.88
|
||||
L 83.88 162.7272
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_8">
|
||||
<g id="line2d_8">
|
||||
<path d="M 83.88 83.88
|
||||
L 139.63339 139.63339
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="matplotlib.axis_2">
|
||||
<g id="ytick_1">
|
||||
<g id="line2d_9">
|
||||
<path d="M 92.6408 83.88
|
||||
C 92.6408 82.729542 92.414185 81.590271 91.973924 80.527387
|
||||
C 91.533663 79.464503 90.888318 78.498675 90.074821 77.685179
|
||||
C 89.261325 76.871682 88.295497 76.226337 87.232613 75.786076
|
||||
C 86.169729 75.345815 85.030458 75.1192 83.88 75.1192
|
||||
C 82.729542 75.1192 81.590271 75.345815 80.527387 75.786076
|
||||
C 79.464503 76.226337 78.498675 76.871682 77.685179 77.685179
|
||||
C 76.871682 78.498675 76.226337 79.464503 75.786076 80.527387
|
||||
C 75.345815 81.590271 75.1192 82.729542 75.1192 83.88
|
||||
C 75.1192 85.030458 75.345815 86.169729 75.786076 87.232613
|
||||
C 76.226337 88.295497 76.871682 89.261325 77.685179 90.074821
|
||||
C 78.498675 90.888318 79.464503 91.533663 80.527387 91.973924
|
||||
C 81.590271 92.414185 82.729542 92.6408 83.88 92.6408
|
||||
C 85.030458 92.6408 86.169729 92.414185 87.232613 91.973924
|
||||
C 88.295497 91.533663 89.261325 90.888318 90.074821 90.074821
|
||||
C 90.888318 89.261325 91.533663 88.295497 91.973924 87.232613
|
||||
C 92.414185 86.169729 92.6408 85.030458 92.6408 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="ytick_2">
|
||||
<g id="line2d_10">
|
||||
<path d="M 110.1624 83.88
|
||||
C 110.1624 80.428627 109.482555 77.010814 108.161771 73.822161
|
||||
C 106.840988 70.633508 104.904953 67.736026 102.464463 65.295537
|
||||
C 100.023974 62.855047 97.126492 60.919012 93.937839 59.598229
|
||||
C 90.749186 58.277445 87.331373 57.5976 83.88 57.5976
|
||||
C 80.428627 57.5976 77.010814 58.277445 73.822161 59.598229
|
||||
C 70.633508 60.919012 67.736026 62.855047 65.295537 65.295537
|
||||
C 62.855047 67.736026 60.919012 70.633508 59.598229 73.822161
|
||||
C 58.277445 77.010814 57.5976 80.428627 57.5976 83.88
|
||||
C 57.5976 87.331373 58.277445 90.749186 59.598229 93.937839
|
||||
C 60.919012 97.126492 62.855047 100.023974 65.295537 102.464463
|
||||
C 67.736026 104.904953 70.633508 106.840988 73.822161 108.161771
|
||||
C 77.010814 109.482555 80.428627 110.1624 83.88 110.1624
|
||||
C 87.331373 110.1624 90.749186 109.482555 93.937839 108.161771
|
||||
C 97.126492 106.840988 100.023974 104.904953 102.464463 102.464463
|
||||
C 104.904953 100.023974 106.840988 97.126492 108.161771 93.937839
|
||||
C 109.482555 90.749186 110.1624 87.331373 110.1624 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="ytick_3">
|
||||
<g id="line2d_11">
|
||||
<path d="M 127.684 83.88
|
||||
C 127.684 78.127711 126.550925 72.431357 124.349619 67.116935
|
||||
C 122.148314 61.802513 118.921588 56.973377 114.854105 52.905895
|
||||
C 110.786623 48.838412 105.957487 45.611686 100.643065 43.410381
|
||||
C 95.328643 41.209075 89.632289 40.076 83.88 40.076
|
||||
C 78.127711 40.076 72.431357 41.209075 67.116935 43.410381
|
||||
C 61.802513 45.611686 56.973377 48.838412 52.905895 52.905895
|
||||
C 48.838412 56.973377 45.611686 61.802513 43.410381 67.116935
|
||||
C 41.209075 72.431357 40.076 78.127711 40.076 83.88
|
||||
C 40.076 89.632289 41.209075 95.328643 43.410381 100.643065
|
||||
C 45.611686 105.957487 48.838412 110.786623 52.905895 114.854105
|
||||
C 56.973377 118.921588 61.802513 122.148314 67.116935 124.349619
|
||||
C 72.431357 126.550925 78.127711 127.684 83.88 127.684
|
||||
C 89.632289 127.684 95.328643 126.550925 100.643065 124.349619
|
||||
C 105.957487 122.148314 110.786623 118.921588 114.854105 114.854105
|
||||
C 118.921588 110.786623 122.148314 105.957487 124.349619 100.643065
|
||||
C 126.550925 95.328643 127.684 89.632289 127.684 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="ytick_4">
|
||||
<g id="line2d_12">
|
||||
<path d="M 145.2056 83.88
|
||||
C 145.2056 75.826796 143.619294 67.851899 140.537467 60.411709
|
||||
C 137.455639 52.971519 132.938223 46.210728 127.243748 40.516252
|
||||
C 121.549272 34.821777 114.788481 30.304361 107.348291 27.222533
|
||||
C 99.908101 24.140706 91.933204 22.5544 83.88 22.5544
|
||||
C 75.826796 22.5544 67.851899 24.140706 60.411709 27.222533
|
||||
C 52.971519 30.304361 46.210728 34.821777 40.516252 40.516252
|
||||
C 34.821777 46.210728 30.304361 52.971519 27.222533 60.411709
|
||||
C 24.140706 67.851899 22.5544 75.826796 22.5544 83.88
|
||||
C 22.5544 91.933204 24.140706 99.908101 27.222533 107.348291
|
||||
C 30.304361 114.788481 34.821777 121.549272 40.516252 127.243748
|
||||
C 46.210728 132.938223 52.971519 137.455639 60.411709 140.537467
|
||||
C 67.851899 143.619294 75.826796 145.2056 83.88 145.2056
|
||||
C 91.933204 145.2056 99.908101 143.619294 107.348291 140.537467
|
||||
C 114.788481 137.455639 121.549272 132.938223 127.243748 127.243748
|
||||
C 132.938223 121.549272 137.455639 114.788481 140.537467 107.348291
|
||||
C 143.619294 99.908101 145.2056 91.933204 145.2056 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="patch_3">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 100.544032 78.465528
|
||||
C 100.827679 79.338503 101.042289 80.232419 101.18588 81.139018
|
||||
C 101.329471 82.045617 101.4016 82.9621 101.4016 83.88
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #004cff; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_4">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 102.349947 34.667001
|
||||
C 104.928058 35.634582 107.426154 36.803257 109.821161 38.162231
|
||||
C 112.216167 39.521205 114.500687 41.06628 116.653617 42.783184
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ceff29; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_5">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 38.963335 30.078544
|
||||
C 43.193773 26.546722 47.826049 23.526156 52.764087 21.079495
|
||||
C 57.702125 18.632834 62.911548 16.77711 68.284309 15.550812
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ff6800; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_6">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 23.539928 94.830109
|
||||
C 22.390882 88.498347 22.245596 82.025002 23.109411 75.648064
|
||||
C 23.973226 69.271127 25.835425 63.069714 28.627544 57.271819
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ffc400; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_7">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 55.074417 103.836559
|
||||
C 54.551888 103.082334 54.059183 102.307877 53.597441 101.51498
|
||||
C 53.135699 100.722082 52.705276 99.911356 52.307168 99.084675
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #29ffce; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_8">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 91.217443 127.065094
|
||||
C 88.388717 127.545714 85.519599 127.747241 82.651462 127.666769
|
||||
C 79.783325 127.586296 76.93002 127.224214 74.132693 126.585742
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #7dff7a; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_9">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 139.162587 126.960611
|
||||
C 137.470326 129.132181 135.651582 131.202154 133.71581 133.159767
|
||||
C 131.780039 135.117381 129.730602 136.959235 127.578156 138.675754
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ff6800; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_10">
|
||||
<path d="M 162.7272 83.88
|
||||
C 162.7272 73.525881 160.687664 63.272442 156.725314 53.706483
|
||||
C 152.762964 44.140524 146.954858 35.448078 139.63339 28.12661
|
||||
C 132.311922 20.805142 123.619476 14.997036 114.053517 11.034686
|
||||
C 104.487558 7.072336 94.234119 5.0328 83.88 5.0328
|
||||
C 73.525881 5.0328 63.272442 7.072336 53.706483 11.034686
|
||||
C 44.140524 14.997036 35.448078 20.805142 28.12661 28.12661
|
||||
C 20.805142 35.448078 14.997036 44.140524 11.034686 53.706483
|
||||
C 7.072336 63.272442 5.0328 73.525881 5.0328 83.88
|
||||
C 5.0328 94.234119 7.072336 104.487558 11.034686 114.053517
|
||||
C 14.997036 123.619476 20.805142 132.311922 28.12661 139.63339
|
||||
C 35.448078 146.954858 44.140524 152.762964 53.706483 156.725314
|
||||
C 63.272442 160.687664 73.525881 162.7272 83.88 162.7272
|
||||
C 94.234119 162.7272 104.487558 160.687664 114.053517 156.725314
|
||||
C 123.619476 152.762964 132.311922 146.954858 139.63339 139.63339
|
||||
C 146.954858 132.311922 152.762964 123.619476 156.725314 114.053517
|
||||
C 160.687664 104.487558 162.7272 94.234119 162.7272 83.88
|
||||
" style="fill: none; stroke: #11557c; stroke-width: 4; stroke-linejoin: miter; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="pccb0f66489">
|
||||
<path d="M 162.7272 83.88
|
||||
C 162.7272 73.525881 160.687664 63.272442 156.725314 53.706483
|
||||
C 152.762964 44.140524 146.954858 35.448078 139.63339 28.12661
|
||||
C 132.311922 20.805142 123.619476 14.997036 114.053517 11.034686
|
||||
C 104.487558 7.072336 94.234119 5.0328 83.88 5.0328
|
||||
C 73.525881 5.0328 63.272442 7.072336 53.706483 11.034686
|
||||
C 44.140524 14.997036 35.448078 20.805142 28.12661 28.12661
|
||||
C 20.805142 35.448078 14.997036 44.140524 11.034686 53.706483
|
||||
C 7.072336 63.272442 5.0328 73.525881 5.0328 83.88
|
||||
C 5.0328 94.234119 7.072336 104.487558 11.034686 114.053517
|
||||
C 14.997036 123.619476 20.805142 132.311922 28.12661 139.63339
|
||||
C 35.448078 146.954858 44.140524 152.762964 53.706483 156.725314
|
||||
C 63.272442 160.687664 73.525881 162.7272 83.88 162.7272
|
||||
C 94.234119 162.7272 104.487558 160.687664 114.053517 156.725314
|
||||
C 123.619476 152.762964 132.311922 146.954858 139.63339 139.63339
|
||||
C 146.954858 132.311922 152.762964 123.619476 156.725314 114.053517
|
||||
C 160.687664 104.487558 162.7272 94.234119 162.7272 83.88
|
||||
M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
M 162.7272 83.88
|
||||
z
|
||||
"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
BIN
images/3rd/packaging.png
Normal file
BIN
images/3rd/packaging.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/gplv3.png
Normal file
BIN
images/gplv3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
19
main.py
19
main.py
@@ -1,13 +1,28 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import sys
|
||||
import module.resources
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from ui.main import MainWindow
|
||||
from utils.function import DEVELOPMENT_ENV
|
||||
from utils.function import RELEASE_ENV
|
||||
|
||||
if __name__ == '__main__':
|
||||
if DEVELOPMENT_ENV:
|
||||
if RELEASE_ENV:
|
||||
import pyi_splash
|
||||
|
||||
pyi_splash.update_text('正在启动...')
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
VER_NUM = '1.3.14'
|
||||
VERSION = f'Release {VER_NUM}'
|
||||
COMPATIBLE_VERSION = ['7.3', '7.4', '7.6', '7.7', '8.0']
|
||||
|
||||
23
module/about/schema.py
Normal file
23
module/about/schema.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
class ThirdParty:
|
||||
def __init__(self, name: str, url: str, qrc: str = None):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.qrc = qrc
|
||||
|
||||
def __repr__(self):
|
||||
return f"ThirdParty(name={self.name}, url={self.url})"
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import io
|
||||
import os
|
||||
import traceback
|
||||
@@ -140,9 +155,14 @@ class DocxWriter:
|
||||
for performance in self.excel_reader.achievement_level[r_index]:
|
||||
non_none_count = 3 - performance.scores.count(None)
|
||||
if non_none_count > 1:
|
||||
try:
|
||||
cell_start = table.cell(row, col_span)
|
||||
cell_end = table.cell(row, col_span + non_none_count - 1)
|
||||
cell_start.merge(cell_end)
|
||||
except IndexError:
|
||||
pass
|
||||
# self.signal(f"单元格合并失败:({row}, {col_span}),需要自行检查表格准确性",
|
||||
# LOGLEVEL.WARNING)
|
||||
col_span += non_none_count
|
||||
|
||||
start = rows - X + 3 + self.excel_reader.kpi_number
|
||||
@@ -224,8 +244,8 @@ class DocxWriter:
|
||||
f". 课程目标达成情况的合理性评价")
|
||||
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
|
||||
|
||||
rows = 9
|
||||
cols = 4
|
||||
rows = 11
|
||||
cols = 6
|
||||
table = doc.add_table(rows=rows, cols=cols)
|
||||
# 设置外侧框线粗1.5磅,内侧框线粗0.5磅
|
||||
self.set_table_borders(table)
|
||||
@@ -235,9 +255,21 @@ class DocxWriter:
|
||||
cell_end = table.cell(0, cols - 1)
|
||||
cell_start.merge(cell_end)
|
||||
# 合并第二行至最后
|
||||
for i in range(1, 9):
|
||||
if i == 2:
|
||||
continue
|
||||
for i in range(1, rows):
|
||||
match i:
|
||||
case 2:
|
||||
table.cell(i, 2).merge(table.cell(i, 3))
|
||||
table.cell(i, 4).merge(table.cell(i, 5))
|
||||
table.cell(i, 1).width = Cm(7.42)
|
||||
table.cell(i, 2).width = Cm(7.42)
|
||||
table.cell(i, 4).width = Cm(7.41)
|
||||
case 8 | 10:
|
||||
table.cell(i - 1, 0).merge(table.cell(i, 0))
|
||||
table.cell(i, 1).width = Cm(11.23)
|
||||
table.cell(i, 2).width = Cm(1.48)
|
||||
table.cell(i, 3).width = Cm(3.4)
|
||||
table.cell(i, 4).width = Cm(1.39)
|
||||
case _:
|
||||
cell_start = table.cell(i, 1)
|
||||
cell_end = table.cell(i, cols - 1)
|
||||
cell_start.merge(cell_end)
|
||||
@@ -261,8 +293,36 @@ class DocxWriter:
|
||||
|
||||
for t_index, table in enumerate(doc.tables):
|
||||
self.set_table_borders(table)
|
||||
# part_3_table_index 表格第9和11行(索引8和10)特殊边框处理
|
||||
if t_index in part_3_table_index:
|
||||
for r_idx in [8, 10]:
|
||||
row = table.rows[r_idx]
|
||||
prev_row = table.rows[r_idx - 1]
|
||||
# 上一行(第8行和第10行,索引7和9)第2-6列移除下边框
|
||||
for c_idx in range(1, 6):
|
||||
self.set_cell_border(prev_row.cells[c_idx], bottom=0)
|
||||
# 第2列(索引1):没有上边框和右边框
|
||||
self.set_cell_border(row.cells[1], top=0, right=0)
|
||||
# 第3-5列(索引2-4):没有上边框和左右边框
|
||||
for c_idx in [2, 3, 4]:
|
||||
self.set_cell_border(row.cells[c_idx], top=0, left=0, right=0)
|
||||
# 第6列(索引5):没有上边框和左边框
|
||||
self.set_cell_border(row.cells[5], top=0, left=0)
|
||||
# 插入签名图片
|
||||
if self.excel_reader.major_director_signature_image is not None:
|
||||
self.insert_pil_image(table.cell(8, 3),
|
||||
self.excel_reader.major_director_signature_image,
|
||||
height=Cm(1.2))
|
||||
if self.excel_reader.course_leader_signature_image is not None:
|
||||
self.insert_pil_image(table.cell(10, 3),
|
||||
self.excel_reader.course_leader_signature_image,
|
||||
height=Cm(1.2))
|
||||
for r_index, row in enumerate(table.rows):
|
||||
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
|
||||
# part_3_table_index 表格第9和11行(索引8和10)行高为1.2cm
|
||||
if t_index in part_3_table_index and r_index in [8, 10]:
|
||||
row.height = Cm(1.2)
|
||||
else:
|
||||
row.height = Cm(0.7)
|
||||
for c_index, cell in enumerate(row.cells):
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||
@@ -321,6 +381,8 @@ class DocxWriter:
|
||||
(6, 1),
|
||||
(7, 1),
|
||||
(8, 1),
|
||||
(9, 1),
|
||||
(10, 1),
|
||||
]
|
||||
if r_index == 0:
|
||||
for run in paragraph.runs:
|
||||
@@ -364,6 +426,7 @@ class DocxWriter:
|
||||
"""
|
||||
设置单元格边框
|
||||
kwargs: top, bottom, left, right, inside_h, inside_v
|
||||
值为0时移除边框,值大于0时设置边框粗细
|
||||
"""
|
||||
tc = cell._tc
|
||||
tcPr = tc.get_or_add_tcPr()
|
||||
@@ -376,6 +439,10 @@ class DocxWriter:
|
||||
if value is not None:
|
||||
tag = 'w:{}'.format(key)
|
||||
border = OxmlElement(tag)
|
||||
if value == 0:
|
||||
# 移除边框
|
||||
border.set(qn('w:val'), 'nil')
|
||||
else:
|
||||
border.set(qn('w:val'), 'single')
|
||||
border.set(qn('w:sz'), str(int(value * 8)))
|
||||
border.set(qn('w:space'), '0')
|
||||
@@ -475,6 +542,15 @@ class DocxWriter:
|
||||
run = paragraph.add_run()
|
||||
run.add_picture(image_stream, width=width, height=height)
|
||||
|
||||
def insert_pil_image(self, cell, pil_image, width=None, height=Cm(4.5)):
|
||||
"""插入PIL Image对象到单元格"""
|
||||
image_stream = io.BytesIO()
|
||||
pil_image.save(image_stream, format='PNG')
|
||||
image_stream.seek(0)
|
||||
paragraph = cell.paragraphs[0]
|
||||
run = paragraph.add_run()
|
||||
run.add_picture(image_stream, width=width, height=height)
|
||||
|
||||
def is_chinese(self, char):
|
||||
"""判断字符是否为中文"""
|
||||
if '\u4e00' <= char <= '\u9fff':
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import datetime
|
||||
import traceback
|
||||
import io
|
||||
from typing import Optional, Callable
|
||||
|
||||
import openpyxl
|
||||
@@ -7,6 +23,7 @@ from openpyxl.utils import get_column_letter, column_index_from_string
|
||||
from openpyxl.workbook.workbook import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from packaging import version
|
||||
from PIL import Image
|
||||
|
||||
from module import LOGLEVEL, COMPATIBLE_VERSION
|
||||
from module.schema import Performance
|
||||
@@ -33,6 +50,35 @@ class ExcelReader:
|
||||
question_data: dict[str, list[tuple[str, int]]]
|
||||
ignore_version_check: bool
|
||||
pic_list: list
|
||||
suggestion_template_list: list[Optional[str]]
|
||||
major_director_signature_image: Optional[Image.Image]
|
||||
course_leader_signature_image: Optional[Image.Image]
|
||||
|
||||
class _SheetImageLoader:
|
||||
"""Lightweight image loader scoped for ExcelReader use."""
|
||||
|
||||
def __init__(self, sheet: Worksheet):
|
||||
self._images: dict[str, Callable[[], bytes]] = {}
|
||||
for image in getattr(sheet, "_images", []):
|
||||
row = image.anchor._from.row + 1
|
||||
col = get_column_letter(image.anchor._from.col + 1)
|
||||
self._images[f"{col}{row}"] = image._data
|
||||
|
||||
def image_in(self, cell: str) -> bool:
|
||||
return cell in self._images
|
||||
|
||||
def get(self, cell: str) -> Image.Image:
|
||||
if cell not in self._images:
|
||||
raise ValueError(f"Cell {cell} doesn't contain an image")
|
||||
image = io.BytesIO(self._images[cell]())
|
||||
return Image.open(image)
|
||||
|
||||
class ValidError(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __init__(self, file_path: str, version_check: bool = False,
|
||||
signal: Callable[[str, str], None] = lambda x, y: print(x)):
|
||||
@@ -58,6 +104,9 @@ class ExcelReader:
|
||||
self.ignore_version_check = version_check
|
||||
self.pic_list = []
|
||||
self.signal = signal
|
||||
self.suggestion_template_list = []
|
||||
self.major_director_signature_image = None
|
||||
self.course_leader_signature_image = None
|
||||
|
||||
def parse_excel(self):
|
||||
try:
|
||||
@@ -65,6 +114,7 @@ class ExcelReader:
|
||||
sheet: Worksheet = wb['初始录入']
|
||||
# 读取版本号
|
||||
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
|
||||
e_version = sheet['H1'].value if e_version is None else e_version
|
||||
if e_version is None:
|
||||
e_version = "0"
|
||||
status, _ = check_version(e_version, COMPATIBLE_VERSION)
|
||||
@@ -86,6 +136,13 @@ class ExcelReader:
|
||||
# 读取课程负责人
|
||||
self.course_lead_teacher_name = sheet["D8"].value
|
||||
|
||||
need_signature_images = CUR_VERSION >= version.parse("9.4") and sheet["H10"].value == "是"
|
||||
if need_signature_images:
|
||||
self._load_signature_images()
|
||||
else:
|
||||
self.major_director_signature_image = None
|
||||
self.course_leader_signature_image = None
|
||||
|
||||
# 读取班级和人数
|
||||
max_class_size = 4
|
||||
match CUR_VERSION:
|
||||
@@ -210,9 +267,26 @@ class ExcelReader:
|
||||
else:
|
||||
self.question_data[key] = [values]
|
||||
|
||||
self.validate_data()
|
||||
# 读取建议模板
|
||||
if CUR_VERSION >= version.parse("9.0"):
|
||||
sheet = wb['初始录入']
|
||||
|
||||
for i in range(29, 34):
|
||||
self.suggestion_template_list.append(sheet[f'I{i}'].value)
|
||||
|
||||
if len(self.suggestion_template_list) != 5:
|
||||
for i in range(len(self.suggestion_template_list), 5):
|
||||
self.suggestion_template_list.append(None)
|
||||
|
||||
if vd_lst := self.validate_data():
|
||||
raise self.ValidError("\n\n".join(vd_lst))
|
||||
|
||||
self.gen_picture()
|
||||
|
||||
except self.ValidError as ve:
|
||||
raise Exception(f"""
|
||||
数据验证失败:\n\n{str(ve)}
|
||||
""")
|
||||
except Exception as e:
|
||||
error_message = traceback.format_exc()
|
||||
raise Exception(f"""
|
||||
@@ -223,13 +297,40 @@ class ExcelReader:
|
||||
def set_file_path(self, file_path: str):
|
||||
self.file_path = file_path
|
||||
|
||||
def validate_data(self):
|
||||
def validate_data(self) -> list[str]:
|
||||
lst: list[str] = []
|
||||
self.signal("正在验证数据", LOGLEVEL.INFO)
|
||||
return 0
|
||||
if len(self.kpi_list) != self.kpi_number:
|
||||
self.signal("\"课程目标\"或\"目标支撑的毕业要求指标点\"数量与期望目标数量不符", LOGLEVEL.ERROR)
|
||||
lst.append(
|
||||
f"\"课程目标\"或\"目标支撑的毕业要求指标点\"数量与期望目标数量不符,请检查Excel表格中的\"课程目标\"和\"目标支撑的毕业要求指标点\"列是否填写完整。"
|
||||
f"期望得到 {self.kpi_number} 个,实际检测到 {len(self.kpi_list)} 个。"
|
||||
f"(如想暂时不填,请在Excel表格对应的位置添加一个空格)"
|
||||
)
|
||||
|
||||
return lst
|
||||
|
||||
def run(self):
|
||||
self.parse_excel()
|
||||
|
||||
def _load_signature_images(self):
|
||||
signature_cells = {
|
||||
"major_director_signature_image": ("I34", "K34"),
|
||||
"course_leader_signature_image": ("I35", "K35"),
|
||||
}
|
||||
wb_with_images: Workbook = openpyxl.load_workbook(self.file_path, data_only=True)
|
||||
try:
|
||||
sheet_with_images: Worksheet = wb_with_images["初始录入"]
|
||||
loader = self._SheetImageLoader(sheet_with_images)
|
||||
|
||||
for attr, (check_cell, image_cell) in signature_cells.items():
|
||||
if sheet_with_images[check_cell].value and loader.image_in(image_cell):
|
||||
setattr(self, attr, loader.get(image_cell))
|
||||
else:
|
||||
setattr(self, attr, None)
|
||||
finally:
|
||||
wb_with_images.close()
|
||||
|
||||
def clear_all_data(self):
|
||||
self.kpi_list = []
|
||||
self.kpi_number = 0
|
||||
@@ -249,6 +350,8 @@ class ExcelReader:
|
||||
self.hml_list = []
|
||||
self.question_data = {}
|
||||
self.pic_list = []
|
||||
self.major_director_signature_image = None
|
||||
self.course_leader_signature_image = None
|
||||
|
||||
def set_version_check(self, version_check: bool):
|
||||
self.ignore_version_check = version_check
|
||||
@@ -285,9 +388,14 @@ class ExcelReader:
|
||||
continue
|
||||
if index == 2:
|
||||
if len(self.n_evaluation_methods) == 6:
|
||||
yield f"期末考核\n({self.n_evaluation_methods[5]})"
|
||||
if self.n_evaluation_methods[5] == "试卷":
|
||||
yield "期末考核\n(试卷)"
|
||||
else:
|
||||
yield f"期末考核\n({j[0]})"
|
||||
yield "期末考核"
|
||||
elif j[0] == "试卷":
|
||||
yield "期末考核\n(试卷)"
|
||||
else:
|
||||
yield "期末考核"
|
||||
else:
|
||||
yield f"{j[0]}考核"
|
||||
|
||||
@@ -317,8 +425,9 @@ class ExcelReader:
|
||||
case 1:
|
||||
yield "\n".join([x for x in self.n_evaluation_methods[3:5] if x is not None])
|
||||
case 2:
|
||||
if (len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] != "试卷" or
|
||||
len(self.n_evaluation_methods) == 5 and self.evaluation_stage[2][0] != "试卷"):
|
||||
if len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] != "试卷":
|
||||
yield self.n_evaluation_methods[5]
|
||||
elif len(self.n_evaluation_methods) == 5 and self.evaluation_stage[2][0] != "试卷":
|
||||
yield self.evaluation_stage[2][0]
|
||||
else:
|
||||
# 中文数字到数字的映射
|
||||
@@ -450,8 +559,8 @@ class ExcelReader:
|
||||
yield analysis_results
|
||||
yield "改进措施"
|
||||
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||
"\n\n\n在这填入您的改进措施\n\n\n")
|
||||
for i in range(88888):
|
||||
f"{self.suggestion_template_list[0] if self.suggestion_template_list[0] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
|
||||
while True:
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
def get_word_template_part_2(self):
|
||||
@@ -540,20 +649,21 @@ class ExcelReader:
|
||||
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement},"
|
||||
f"{'、'.join(o_c_str)}的达成情况较好;")
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
analysis_results += "\n3.结果分析: \n在此填写您的结果分析\n\n"
|
||||
analysis_results += ("\n3.结果分析: \n"
|
||||
f"{self.suggestion_template_list[2] if self.suggestion_template_list[2] is not None else '\n\n\n在此填写您的结果分析\n\n\n'}")
|
||||
yield analysis_results
|
||||
yield "改进措施"
|
||||
yield "注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n\n\n\n"
|
||||
for i in range(88888):
|
||||
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||
f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
|
||||
while True:
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
@staticmethod
|
||||
def get_word_template_part_3():
|
||||
def get_word_template_part_3(self):
|
||||
yield "课程目标达成情况合理性评价"
|
||||
yield "评价样本的合理性"
|
||||
yield "R全体样本 £抽样样本"
|
||||
yield "评价依据的合理性"
|
||||
yield "考核方法 R合适 £不合适 "
|
||||
yield "考核方法 R合适 £不合适"
|
||||
yield "考核内容是否支撑课程目标 R是 £否"
|
||||
yield "评分标准 R明确 £不明确"
|
||||
yield "计算过程的合理性"
|
||||
@@ -566,17 +676,23 @@ class ExcelReader:
|
||||
yield "R合理 £基本合理 £不合理"
|
||||
yield "专业负责人/系主任(签字)"
|
||||
yield ("整改意见:\n"
|
||||
"\n\n\n\n\n"
|
||||
"\n\n\n"
|
||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
||||
f"{" " * 8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\n")
|
||||
yield ""
|
||||
yield "签字:"
|
||||
yield ""
|
||||
yield "日期:"
|
||||
yield datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
yield "课程负责人(签字)"
|
||||
yield ("拟整改计划与措施:\n"
|
||||
"\n\n\n\n\n"
|
||||
"\n\n\n"
|
||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
||||
for i in range(88888):
|
||||
f"{" " * 8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n")
|
||||
yield ""
|
||||
yield "签字:"
|
||||
yield ""
|
||||
yield "日期:"
|
||||
yield datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
while True:
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import pathlib
|
||||
from copy import deepcopy
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_BREAK
|
||||
from docx.shared import Cm, Mm
|
||||
|
||||
from module.schema import Course, Student
|
||||
@@ -22,10 +38,11 @@ class DocPaper:
|
||||
section.right_margin = Cm(2)
|
||||
|
||||
def add_paper(self, course: Course, student: Student):
|
||||
temp_table = self._template.tables[0]
|
||||
new_table = deepcopy(temp_table)
|
||||
new_table = deepcopy(self._template.tables[0])
|
||||
|
||||
para = self._doc.add_paragraph()
|
||||
para._p.addprevious(new_table._element)
|
||||
para.add_run().add_break(WD_BREAK.PAGE)
|
||||
|
||||
data_list = {
|
||||
'%CNAME%': course.name,
|
||||
@@ -33,9 +50,7 @@ class DocPaper:
|
||||
'%SNAME%': student.name,
|
||||
'%NO%': student.no,
|
||||
'%SO%': student.so,
|
||||
'%Q1%': student.picked_questions[0].topic,
|
||||
'%Q2%': student.picked_questions[1].topic,
|
||||
'%Q3%': student.picked_questions[2].topic
|
||||
'%Q%': '\n'.join([f'\t{idx + 1}、{i.topic}' for idx, i in enumerate(student.picked_questions)])
|
||||
}
|
||||
|
||||
# 替换表格中的占位符
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from openpyxl.reader.excel import load_workbook
|
||||
@@ -8,8 +24,9 @@ from openpyxl.worksheet.worksheet import Worksheet
|
||||
class PickerStudent:
|
||||
total_time: int = 0
|
||||
|
||||
def __init__(self, name: str, position: int, scores: list[int]):
|
||||
def __init__(self, name: str, so: str, position: int, scores: list[int]):
|
||||
self._name = name
|
||||
self._so = str(so)
|
||||
self._position = position
|
||||
self._scores = scores
|
||||
self._modify = False
|
||||
@@ -43,6 +60,24 @@ class PickerStudent:
|
||||
self._scores[index] = new_score
|
||||
self.modified()
|
||||
|
||||
@staticmethod
|
||||
def pick(student: list['PickerStudent']) -> Optional['PickerStudent']:
|
||||
filtered = [item for item in student if item.weight != 100 and item.weight > 0]
|
||||
if not filtered:
|
||||
return None
|
||||
|
||||
# 计算倒数权重
|
||||
weights = [1 / item.weight for item in filtered]
|
||||
total = sum(weights)
|
||||
r = random.uniform(0, total)
|
||||
|
||||
cumulative = 0
|
||||
for item, w in zip(filtered, weights):
|
||||
cumulative += w
|
||||
if r <= cumulative:
|
||||
return item
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def set_total_time(cls, total_time: int):
|
||||
cls.total_time = total_time
|
||||
@@ -63,6 +98,14 @@ class PickerStudent:
|
||||
def weight(self) -> int:
|
||||
return int(sum(1 for x in self._scores if x != 0) / self.total_time * 100)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def so(self) -> str:
|
||||
return self._so
|
||||
|
||||
def saved(self):
|
||||
self._modify = False
|
||||
|
||||
@@ -80,7 +123,8 @@ class PickerExcel:
|
||||
wb: Optional[Workbook]
|
||||
ws: Optional[Worksheet]
|
||||
path: str = ''
|
||||
max_time_position = 'L1'
|
||||
max_time_position = 'M1'
|
||||
start_row = 5
|
||||
|
||||
def __init__(self, path: str):
|
||||
self.open(path)
|
||||
@@ -88,7 +132,7 @@ class PickerExcel:
|
||||
@classmethod
|
||||
def open(cls, path: str):
|
||||
cls.path = path
|
||||
cls.wb = load_workbook(path)
|
||||
cls.wb = load_workbook(path, keep_vba=True)
|
||||
cls.ws = cls.wb.active
|
||||
PickerStudent.set_total_time(cls.ws[cls.max_time_position].value)
|
||||
|
||||
@@ -99,14 +143,41 @@ class PickerExcel:
|
||||
|
||||
if cls.wb and cls.ws:
|
||||
ret = []
|
||||
for row in cls.ws.iter_rows(min_row=4, max_col=4 + cls.ws[cls.max_time_position].value, values_only=True):
|
||||
for index, row in enumerate(cls.ws.iter_rows(
|
||||
min_row=cls.start_row,
|
||||
max_col=4 + cls.ws[cls.max_time_position].value,
|
||||
values_only=True)
|
||||
):
|
||||
if (name := row[2]) is None:
|
||||
break
|
||||
ret.append(PickerStudent(name, int(row[0]) + 3, row[4:]))
|
||||
ret.append(PickerStudent(name, row[1], cls.start_row + index, row[4:]))
|
||||
return ret
|
||||
else:
|
||||
raise Exception('No Workbook or Worksheet')
|
||||
|
||||
@classmethod
|
||||
def read_total_time(cls, path: Optional[str] = None) -> int:
|
||||
if path:
|
||||
cls.open(path)
|
||||
|
||||
if cls.wb and cls.ws:
|
||||
val = cls.ws[cls.max_time_position].value
|
||||
if isinstance(val, int):
|
||||
return val
|
||||
raise Exception(f'总次数读取错误,期待类型 {type(1)} 但实际为 {type(val)}\n'
|
||||
f'可能的解决方法:\n'
|
||||
f'1、确保总次数位于 \'{cls.max_time_position}\' 单元格;\n'
|
||||
f'2、确保选择了正确的 Excel 文件。')
|
||||
raise Exception('No Workbook or Worksheet')
|
||||
|
||||
@classmethod
|
||||
def save_total_time(cls, path: Optional[str] = None, value: Optional[int] = None) -> None:
|
||||
if path:
|
||||
cls.open(path)
|
||||
|
||||
if cls.wb and cls.ws and value:
|
||||
cls.ws[cls.max_time_position].value = value
|
||||
cls.save()
|
||||
|
||||
@classmethod
|
||||
def write_back(cls, student: PickerStudent):
|
||||
start_col = 5 # E列
|
||||
@@ -116,6 +187,7 @@ class PickerExcel:
|
||||
cls.ws.cell(row=row, column=start_col + i, value=val if val != 0 else '')
|
||||
|
||||
student.saved()
|
||||
cls.save()
|
||||
|
||||
@classmethod
|
||||
def write_all(cls, students: list[PickerStudent]):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Resource object code (Python 3)
|
||||
# Created by: object code
|
||||
# Created by: The Resource Compiler for Qt version 6.9.0
|
||||
# Created by: The Resource Compiler for Qt version 6.9.1
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -13461,6 +13461,399 @@ g\xa5\x7f\xb93\xf0\xa2\xe7X\xdc\x92TAmb\xa0\
|
||||
\x10#\x90\x1eoP\x94<n\x0c_\xbc\xeav\xce9\
|
||||
\xed\xa8y:m\x96\xed\xbe\xfe?\x17w\x06w\x08s\
|
||||
g^\x00\x00\x00\x00IEND\xaeB`\x82\
|
||||
\x00\x00\x18d\
|
||||
\x89\
|
||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||
\x00\x00\x88\x00\x00\x00D\x08\x06\x00\x00\x00Z\x90}\x9d\
|
||||
\x00\x00\x18+IDATx^\xed]\x0b\xdc]E\
|
||||
q?\xb5\xb4\xf6a[Zh\xd1\x866V\xa2RH\
|
||||
\x81\x82\x86\xf2\x10\x14P\xd1\xfa\xe0!\xf2\x08\xb4X|\
|
||||
!/\xd1\x88Q\x02\xdc\x0aJj\x95WQ\x04-`\
|
||||
%<\xd5`\x90\x08|g\xce\x0d\x04\xc2\x1bE \x10\
|
||||
\x92\x18H\x82D^\x09\x84G\x02|\xa4\xf3?3{\
|
||||
\xcf\xdc\xf9\xf6\xdc{\xee\xe3\xfb\x12\xc27\xbf\xdf\xfc\xbe\
|
||||
{vv\xe7\xcc\xd9\x9d\xdd\x9d\x9d\x9d\xdd/IFa\
|
||||
\xbd\x87z\x92\xfcQ\x9a$\x1bQ\x92\x8c\xc9\x92d'\
|
||||
\xfe\xfd\xef\x8cG\xf0\xef\xcfq\xda~L\x1f73I\
|
||||
\xfe\xdc\x97\x1b\x85\xf5\x1cX\x09\xde\xc5\x0ap<\xe3O\
|
||||
\x19\x171\xae1\xf8\xb2bx\xbe\x8d\xf3\x7f\xcc\xf3\xe8\
|
||||
\x09X\xf36e\xc6S\x18OY\x1f\x91{\xd8\xd7\xb9\
|
||||
\xd2N\xc4_O[\x87\xf1d\x96\xf7l\xfe\xbb\x84\xf1\
|
||||
y*\x14\xe0n\xc6\xc9\xd7%\xc9[oH\x92\xbf\xe6\
|
||||
\xb6\xdb\x18\xc8\xbf\xdf\xc4\xdf8\x91i\xcb\x91\x8f\x7f\xef\
|
||||
\xe5\xdb\xb9k`A>j\x04X_\xf1\xfcH\xda\xab\
|
||||
\x01\x97qc_\xc3m\xb4\x8fo\xb7\x18\xb0\xb2l@\
|
||||
\xa2D/{Z\xd7\xc0\xccj\x11\xc1\xd6'|~ \
|
||||
I\xde\x1dI_\x17p%\xe3\xad\x8c\x0f2>\xce8\
|
||||
\xa8\xe9)+\xc6a,\xf7\xdf\xfb\xf6j\x07\x98b\xc0\
|
||||
\x83\xff\x1e\xe0i\x1d\xc3\x9a$\xf9=\xd6\xce\x81\x88\xe0\
|
||||
\xeb\x0d\xf2\xf7]\xc4\xb8\x9dO_\xcb\xf8(7\xe07\
|
||||
\xf9\xeft\xc6U\x8c/i\xfa],\xeb\xd6ll\xbe\
|
||||
\xde\xb7UU`\x1e\x13H\xa6\xa5\x93=\xadc\xa8%\
|
||||
\xc9\xeb\x98\xd1s\x91\x0fX\x9fp?\xae\xf4-#\xe9\
|
||||
#\x8dh\xb4y\x8c\xdfb<\xc3\xd1\xa00G\xf8\xf6\
|
||||
\xe9\x06\xf8[we~/\xf2\xdf/yZ\xc7\xc0B\
|
||||
m\x11\xf9\x90\xf5\x09W\xf2\xbc<\x1e\x86\x5c\x846\x92\
|
||||
x\x19\xd7\xf5W\x19\x7f\xc8\xbf\x1fs\xb4\xb3\x18\xc7\xf8\
|
||||
\xb6\xe9\x16\x98\xd7\xa7\x94\xef\x04O\xeb\x18X\xcb\xbe\x12\
|
||||
\xf9\x18\xe0\x02\xc6_3\xdeS\x11\xef%\x99C=\x1f\
|
||||
\xcc\xa9\xe0\xe5\xf3W\xc5\x87i(\xcf\xdf\x19:d|\
|
||||
$\x92'\xe0\xbd\xdc(\x9b\xc0\x87\x10\xa1\x8d\x04\xde\xc7\
|
||||
v\xc4?\xf3\xdfs\x18W;\xdaKX\xc2\xfa6\xe9\
|
||||
\x05.O\x92?d\x9e?g\xde+=\xad+`F\
|
||||
\xb7\x18\x81\x9f`\xe6\x9f\xf1y:\x01\xe61\x86\xf1F\
|
||||
\xc3\xf3a,\xc1|\xbeN\x00\xf31\xf3\xb9\x98\xf1\x15\
|
||||
\xf0d\xa5>\xc8\xd2\xf9\xf9{\xe6}\x1e\xaf\x85e\xaf\
|
||||
<\xf2%\xe0\x08!\x14{2\xd7\xe7{I\x14\xda\xd3\
|
||||
o\xc52\xd5~G?\x80\xdf\xb7\x15\xf8\xf7ez\x81\
|
||||
\x81J\x85\xd5\x0c\xabw\x0fK\xbb&I\xfe\x8a\xd7\xdb\
|
||||
\x7f\xdb\x0eoL\x92?\xb3|\xb9\xc7\xecl*\xe2\x0e\
|
||||
\xf4\xde@CC\xf9\xf2\x1e\xa1P^\xa9H\x0c/\xd8\
|
||||
JO\x83\xbf\xa3=\x1ai\x80\x80\xe7 \xcf\x1dI\xf2\
|
||||
\x07$#\x8e\xa7\x0f\x07N\xe1\x06z?\xff\xfd\x89K\
|
||||
\x0f\x0a~!\x7f\xc3_\xd8o\xe8\x17\x90\xac\x86\x96b\
|
||||
\xd4\xf4\xb4\x8e\x81\x05\xdd\xc9\x08\xbf\xda\xd2\xe6$\xc9\x1f\
|
||||
s\xda\xfdHg|\xa1\x05\xa2\xec\xfblY~><\
|
||||
\xf0\xe5w\x5cji\xfc|l\x05\x9e\xa0_o\xcb\xf1\
|
||||
\x07\xef\xaf\xe9KX\xe1\xde\x18\xd2IF,?t\xdb\
|
||||
\x069\x0a\xf9\xd4?pu$O?\xf1>\x12y>\
|
||||
K\xb2|\xf5tt\xc2o\x14_5\x14\xb8~v\xd7\
|
||||
\xd5\xcd\x15\xfc\xf7L\xdf\xf9Z\x01\x97\xfd\x81\xbe\xe7\xf3\
|
||||
\x9e\xd6\x150\xa3\xc9F\xf8\xd4\xd2:1\xea\xb8\xe0F\
|
||||
\xb6,\x94\xc2\xd0\x9a,s\x12w\xf1\x10\x1e\x11\xac\xb9\
|
||||
r\xf0.\x82\xdf/mz&N>\xebj\xb6\xf8\x0a\
|
||||
\xe7\xdf\x13\xf9j\xb2Z;=\x92\xa7\x1f\xb8\x82q*\
|
||||
F=\xfe{I\x84\x9e#\xa6B+;\x00\xa34\xd3\
|
||||
vc\xfc?2\xa3\xb9)s\x91/\x13\x03\xed@(\
|
||||
\x93yZ\xd7\xc0L\xaf4\x82\x9c`i\xfc\xbc\x8f\x17\
|
||||
\xb6\x04\xe7^\x9b$\x7fj\xcb\x921V1'\x86t\
|
||||
V\xba7p\xda]\x11\x1eC\x90\xf3\xfeK(\xa7\xf6\
|
||||
C>\x5c\xb3\x5cg\x87t@&\xee\xf3|\xe8\x8e\xe0\
|
||||
\xd3\xdfn\xf2bS\xcb\xe7\xe9\x09\xf9\xfbnf\xbe\
|
||||
[\xa7bk,\xf2t\x8330\xcd\x05Y02p\
|
||||
\xdaI\x8cwF\xf2Z\x9c\x1c\xca\x94\x81N\xe9\x18E\
|
||||
\xef\xee\xdbf\xdd\xec$\xf9K2\x8d\x95\x1a\xfb\x03@\
|
||||
\xd5]\xd3\x97\xd8\x0f\xc7hb\xe9\x96'\x1a\x8bZ\xaf\
|
||||
8\xa2\xe5\xb4\x97a\xf8\x86\x82|4\xa4\xb3\xc5\xfe\xfb\
|
||||
$\x8e\xa6!\xe5\x15\x07\xafJ\x92?\x09\xf9Y\xb6\x0f\
|
||||
q\xda\x8b\x91|\xdd\xe24\xf0%ihO\xb3\xb8 \
|
||||
\xc8\xa0\xf91\x05a\xd4)S\xec\x80\xab\xd0\xa9lY\
|
||||
\x0f\x9cgG\xe5\x83\xa9\xf7\xcd\x9e\xde5\xb0\xd6\xfd#\
|
||||
3]\xa6\x82<\xc9\x15\xbf\xa5\xa5Sk\xc3\xaf\x81\x5c\
|
||||
\xee+\xb6\x1c?\xff\x9b\xa1\x93\xa5q\x03\xbd\x87\x0a\x8f\
|
||||
a+\xbc\xd5\x96\x83\xbb\x99t\x1a\xb1\x15\x86i\x10S\
|
||||
N\xa4|\xc0\x97,\x1f\xce\xbf\x0d\xc5\x97\xe2\x9d\xe2\xd3\
|
||||
\xfc\xdec\xd4\xc8\xbfL\xd3\xca\x1a\xfb9~\xef\xe6\xc8\
|
||||
\xab\xa3L\xae\xe8U\x90\xeb\xf2X\x94\xb3\xdf`\x81\xf3\
|
||||
|\x80q5\xf3\xfd-\xff\xdd\xd1\xd3{\x02~\xf9\xee\
|
||||
F\x98_\xdb%\x97\x1f\x05Z\xe0\x0bht\xc7\xb7a\
|
||||
\x7f\xe0\x03\x1d\xad\xea\x10\x7f\x8a-GR\x11H\x7f\xd8\
|
||||
\xa6\xf3\x8a\xe7\xad$=\xd1\x97\x0f\xf8\x98\xcd\xaf\xdf\x05\
|
||||
\x9f\x8c\xcfW\x19\xf9\x1b\x163n\x87\xfa\xe2\xe7;<\
|
||||
\xdd!\x94f\xaa\xca9\x83\xaau\x8e\xf0\x9ez\xdd\xac\
|
||||
\xfe<\x90\xd8\x8f\xe07\x8f\xbfk\x0bO\xef\x19\xf4\x05\
|
||||
A\xa0\x99\x96\x96\xeafOE\x84ae\xb1\xd1\x93P\
|
||||
1\x96/\xa7M\x8b\x94\x1f\x82\x5c9\xbb\xbar\xb9[\
|
||||
:sF\x1e\xf2\xf9\xb2\x0e\xe7\xda\xfc\x00N\xbb)\x92\
|
||||
\xaf*.\xc0\xeaN\xa7\xca\xe8*\xc5\xe1\x0b\x18a\xf9\
|
||||
\xef\xb3\x11Z+\x5c\x06\x87\x97\x97\x1d\x00\xa5\xa1\xc2\xd0\
|
||||
_\xd8\xcdf^%`\xe63\x8d@S-\x8d\x15\xe4\
|
||||
;\x11\xa1;\xc5gk\xbcr\xb0|9\xed\xa1H>\
|
||||
\x8f\xd8ym\xfah\x92\xe56\x14\xe4#6\x9d\xe5<\
|
||||
1R\xbe\x81L\xbf\xd9\xe6\x070\x8f\xff\xf2\xf9*\xe2\
|
||||
\x0c\x94\x87\xf1LC]\xe5e\x88M8\x9f\xd6\x0e\x17\
|
||||
\x949\xd0tJ\xc3>\x0e\xea#\x8c\x84\x0f\xa5f!\
|
||||
\xd07\xa0\x22\x08\xe5\x15_\xf1$\xdb\xcf^\xf0N1\
|
||||
\x85\x11\x19x\xc21\x14\xc93\x04aS\xc0(\x0d\xe5\
|
||||
\xea\xe2\xbf\xc8iu\xe3\xff\x00p\xda,_\xde!\xd9\
|
||||
\xfc\x80\xacyjm\x87\xf9h\xc82\x9d\x8b\xb2jC\
|
||||
\x0c\xa77v\x11\x7f\xe38/s\x00(\x08V\x8cX\
|
||||
`\xa0\x13e\xb2\xc4\x7f\x92qi'\xfe\x92\xb6\xa0s\
|
||||
b\x10\xea\x15\xab\xb1\xf0d\xf2\x8b\x7f\x13\x11\xbe#\xe4\
|
||||
\xca\xfc\x865\xb08m?\x9f\xa7\x04/\xb3\xab\x22~\
|
||||
~\x9f\xa6?\xe0+!R\xd6\xe3\xb56\x7f\x80H\xbe\
|
||||
V\x98\x8f\xae:\x9d\xf5s\x05\xe4q\xa1]qU\x05\
|
||||
.\xf7y\x92N~\x90\xa7u\x0d\xccp\x92\x11\xecQ\
|
||||
K\xe3\x86\xdd\x81Z\x1b~\x95\x90\x05\xfe\xb8\xe5K\xe2\
|
||||
\x08\x1a\x92\xcf#\x97\xfbOW\xee4\xa5]a\xe7e\
|
||||
\xeeA\xff\xe4\xcbF0\xaa \xfc\x8ez$\xef\x10\xc4\
|
||||
t\xa4\xf9\xb7\xa6b\xc57\x1c8\xddv\x8aN@\xa7\
|
||||
<\xf0\xf8\xb2\xa7u\x0dd6\xd3\xf8\xe3\x7f`i\xd0\
|
||||
\xc4\xc8\x07`.=\x99\xf1H\xc6\xa3J\x10A\xb5a\
|
||||
S\x0a\xc3p\xd3V3\x95/\x03=\xee\xe7\xcaa\xb7\
|
||||
\x16#\xd2\x896=+\xdf\x85\xb68\xcb\x96\x09\xc0\xbc\
|
||||
\x0e\x8b\xe4\x0d\x18\xe4<\x0dy\xeb\xe2Q\xae\xe4\xbb\xe9\
|
||||
\x12'\x95\x19\xa4U\x00\xe6\x81\xf2\xf9\xac\xa7u\x0d\xd4\
|
||||
\xbc\xdc\xda;\xa4\xd7\xd8\xa8Le\x1f\xc0\x7f\xc4\x93\xd6\
|
||||
\x9e\x88\x01\xe7\xd9\x8c\x0a\xcb~\x81\xb7\x17\xd0C\xaa\xa0\
|
||||
\x9d\x96\xd4u\x0d\x83\x10^\xc2&\xc5\xe1\x8a\x99\x1d\x91\
|
||||
\xd3c\x93?%\x80.w}^\x8b\xe7!\x1f<\xb8\
|
||||
\xfd\x98nKp\x01|Q^\xb6N\x81$\x96\x04>\
|
||||
\xa2\xfe\xf8AHvE\x1b>\x7f;\xaf\xabA8\xcb\
|
||||
\x7fL&k\xff\x0fb\xdaP<\x88\xc7\xee\xbf\xb3|\
|
||||
\xb3f\xe3o\x8e\xa5u\x0ah\x18\x96e\x1bn\xc8\x1f\
|
||||
+\xbf\xa70\xa5\x04\xbaZ\xf3U\x96\x99\xf3,\xdf\x00\
|
||||
\xbaT,\xf3aLG\x1e\xf5\xd2^\x10\xa1\xf7\x8a\xab\
|
||||
\xb8\xae\xce\xc6r\xd9\xcb\x05\xe3\x13\xf5\xeaw\xb2\xcb\x00\
|
||||
nu\x92\x15\xcd2O\xeb\x1aH\x8d\x1a\x15v\xa1\xa5\
|
||||
\xa9\x82\x84\x1d\xda\x96\x88y\xd9\x96\xcddO$\xd0\xcf\
|
||||
\xb04n\xe8/\x90LkU\xf0&\xacd\xa8y)\
|
||||
\xf9\x08d\x0b\xfc\xf8y[\xaa&\xe7r+G\x80\x9a\
|
||||
l\xdcM\x8d\xe4\x9f\x17\xb6\xe1\xa9\xbaQ\xdd\x09.\xcd\
|
||||
\x9c\x8fG;\x1e\x8e7\xcca\x9c\xab#\xd6|\xc6i\
|
||||
\xed\x8cV\xae\xa7\xbd\xc0\x97\xcb\x1c\xedi]\x03I\xe0\
|
||||
M.0\xbf\xe0\x87\x96\x96V\x0f?|\x12s\xb3+\
|
||||
{s\xa0gf\xbf\x04\xc0i\xd7ExT\xc6,\xb7\
|
||||
I\x9b\xf8}\x92\xaay%\x07m9\x0b\x99\xcc\xdd\xd6\
|
||||
O\xb1:,\xafu\x9f\xca\xf3\xea\x15\xef\xe7\xd1\xe1o\
|
||||
02\x0dH\x84Y\xe9\xae/\x90\xe5[\xcc\xf9\xff\xc1\
|
||||
\xcb\x1d@;\xf3\xaf\xc8y\x8b{\x02\x0cI\xb6!S\
|
||||
\x17=\xc6B}\xc9\x0bZ\x827Dvp\x1bt\x1b\
|
||||
\x0c\xc3\xef\xd8\x84$\x1c\xd1\xf3\xe8\x04'\xb9w\xfdO\
|
||||
$O\x0c\x07\xbd\x9c\x01T\xae\xa5\x9ao\xb5\xed\xd9\x14\
|
||||
\x99f{@\x84\x15\x22\x0e\x15S;\xbc\xd7\x0fD\xf2\
|
||||
\xc4\xf0\xbbV^\x0f0\xda\x91/\xabx^\xa6\x12\x90\
|
||||
\x18\x92\xc1\x22G\x0f\xdc\xc5\xd1\xaf\x8f\x08\x1a\xc3sj\
|
||||
\xc6KJ\xf2\xf19\x0d\x9aoX\x86\xe5hO\xce%\
|
||||
k\x7f`\xd8\xe5\xca\xb9\xc6\xe7)\xc1A\xef\xee\xb7@\
|
||||
E\x00Qc\xef'\x95\xb8\x8a*\xa3S\x15\xc4T\x8e\
|
||||
\xed|\xb8\xf7\xe1\xd0\xf2\xf4(r\x1d\xfe\xa2\xd5\xa2\x80\
|
||||
\xf3|\x98q\x10\xf5\xd0\xed\xf28\x0a\xa9\xf88\x82 \
|
||||
K|\xe5yAK\x10\x1f\x9dGi\x99r\xb5@O\
|
||||
\xd5\xebhha\xa3\xadk\xb4\xfc\xb0:\xe2\xb4\x85>\
|
||||
O\x09B\xd6\xddl\xf9\x00$\x91_\xd8\xb1\x9e_S\
|
||||
e\x87\xf2\xf1s\x1a\xe13b\x88F_\xe3\xb6(,\
|
||||
p\x9e\xb1$q'\xd8%\xde\xd4\xd3{\x02\x183F\
|
||||
\x98;\xec\xf0\x8bU\x83\x17\xb6\x04\xb1\xf1\xd4T\xe9\xfc\
|
||||
|\x9b\xa1\x7f\xd8\xd2\xf8\x83\xbf\x16\xe1\xd1\x09f\x96\x1f\
|
||||
\xcb\xb9y$O\x19\x22\xa2\xec0[\x1e\xa0\xab\xa0|\
|
||||
\x03\xd0.5I\x8c_\xcfc\xc4\x90?\xf4{\xb1\xd5\
|
||||
M\x00\x0d\x9c\xca\x158u\xf1;}\x01f\xfc3#\
|
||||
\xd0\xe5\x96\xc6/<\xc6\x0b\x5c\x82\x8f\xa3\x17\x87r\xba\
|
||||
dlL!\xfc\xbc\xa1\xe5K=\xce\xe7,\xd7q\x96\
|
||||
\x1f?\x1f\xe0\xf3\xb4\xc1Smy\xe5\xf1\x16\xe5}\x8c\
|
||||
M\xe7\xb4\x1fE\xca\x8f\x04>\x0f\xa3\xb9\xd6b\xe4\x00\
|
||||
PQ\x97\x9f\xf0\xb4\xbe\x003~&\x08\x95\xb9X\x8d\
|
||||
\xb4\xf09\xb4\xc3\x07m9\xe6\xb3=\x15'\xf3\xe6A\
|
||||
a,\x9d\xca\xe3E\xab\xe2\xb6\x96\x1f\xcb\xf9\xbf\x91<\
|
||||
\xa5\x88\xef\xb2\xe5\x01$6\xc1\xfdvSP\xd3\x87\x94\
|
||||
\x1ff\x84?\x0a\x9e\xe2\xb1V\x0e\x0f6\xe4\x92\xbf\xe7\
|
||||
LO\xef\x0b@\x08+\x1c7\xecv\x81\x06W/\x0c\
|
||||
#\x92\xab\x06Za~\x9e\xd4\xf1\xb5K\xce\x8b\xb1\xfc\
|
||||
\x0a4\xfe=\xde\xbe\xb3\x0b\x5c\x81\xa5\xa1{\x1f\xfc$\
|
||||
\x88\xa0\xf2\xb2YlD\x8d\xf1w\xcd\xb6S)\x156\
|
||||
Q\x93\x1d\x85\xfa\x08eF\x08!g\xdb\xe8st8\
|
||||
\x96\xed\x22-s\xba\xa7\xf7\x0d\xfc\x14b\xad\xe4\x1a\x0f\
|
||||
m\xd8\xd1\x85\x07\xaf\x15b\xa7\xb7\xeeb$\xb3\x22\xd4\
|
||||
\x1e\xda\xfdEG\xab\xb2_\xd2\x0a\xe7\xf8\x1d\x5c/S\
|
||||
\x0c3\xd9\x06\x0fJ;\xdfz}\xc1\x93\xf1\x09\xab\xc8\
|
||||
\x80\xac9Tr\xb8\xf1t\xaf\xf81\x80\xbb \xd5\x90\
|
||||
J\xfe\xfb\xd5V+\x9b\x9e\x81\x86Z\xe7S\xd0\xb3`\
|
||||
\x14AK;AX\xfb\x1a\xdfaw\x85\xe1K\xd8W\
|
||||
\xdd\xe4\xf94C\xcd'\xec:F\xe6w!d\xf4\xef\
|
||||
o\x87$\xc6f\x18E`\xedo\x03y\xea\xb2\xf3\x89\
|
||||
}\x9d!=\x97\xd3\x0e\xf5\xef\xef3\xc2)w\x8b_\
|
||||
9\x96\x81\xdaI\xf3H\xa6\xe8\xdeO\xe9\xb7\x03\x8a\x87\
|
||||
\xbd-H\xc5\xa7pU\x87\x08c\xe9)\xe5\x11\x10\xa7\
|
||||
\xc9g\x9b<\x983{\xf5'\xe0\x94\x18b9\xfd\xfb\
|
||||
\xdb!Q\xf3\x8d\x05\xf9\x86$z!\xff\x1e\xacE\x8c\
|
||||
A(\x91\xc9\xdfO\xc4R{\x06\x8cP\xff\xce2\xd0\
|
||||
\x11\x10Kp|\xc3!\x9e\xdew \xe9Q\xc3\x19\xec\
|
||||
\xb2\xae\xe3Y5\xd9\xa9\xbe\x9dt\xa76\x06\xdc0\x8b\
|
||||
#e{\xc1\x0b\xf8\x9d[`T\xf5\xef*\x03\x96\xe1\
|
||||
\x04\x92\xd1\xe6\x09k'\x0e+\x90\x9c\xc3\x08\xab\x89{\
|
||||
\xf8\xc5\xbb\xb2\xe0\xef\x09H=\xee\x95(\xde\xc9\xbc\xf6\
|
||||
\xe4\x9e8\x8e\xc4pm\xe5=\xcd/I\xc9$>\x14\
|
||||
>\x14\x8cH>O?\xf1A\x18\xe2\xfa{7_?\
|
||||
\x01Rq$\x0e9\xdd\xd6\x01\xa2\xc7\xc3}?\xd5\xdb\
|
||||
N\xed\x80\xeb\xed\x0d\x98R\x95\xcf=u\xe7.\x18V\
|
||||
\xe0\x17~\xdf|\xc4\x19X\xdeeb\x19\x9f\x97\xc9\xc9\
|
||||
x\xd8\x0ah\xa8\xfc\x94\x97\xf6$(\x8d\xddQ]\xce\
|
||||
\xe9\x03Y<>b\x99np\xfd7\xe3Ou\xcf\xe7\
|
||||
\x5c\xa5\xa1\xd2`\xff`\x87\x12\xcf\x8fi<%\x0e7\
|
||||
\xe7[\xeb$\xd1f\xd8x\xc2\x94\x84\xa9\x0bS\x0b\xf2\
|
||||
\xc2c\x1al\x899\x99D\x82\x85]\x5c\xec\xef\xe0\xe0\
|
||||
\x17\xee\xe3\xc23\x82\x95 \xb3\x9f\xfarL\xc5\x85\xbe\
|
||||
\x0as\xbb\xaf\x1f\x0b\xa9\xec\x90\xe6A\xd2\x15\xf1iF\
|
||||
\x5c\x17\xf55\xc6=\xba1$S\x89u\xcd\xdf\xc9\xbf\
|
||||
\xbf\xd9\x0d\x8f\xaeA5\xb3\x1e>\x08\x15\x95\xca\x093\
|
||||
\xec\xc9\xc0\x08\xc2\x05+\xf91G\x9d\x87g\xcd\x94\xdb\
|
||||
\xf3\xb6A/\xe7\xe7\xc7\xa0\x14X\xc1`O\xa4.\xae\
|
||||
n\xeb9m\xec\xb6\xa2\x92H\x1a\x14\xbd\x08{\x0fK\
|
||||
8m\x13(\x04z\x14IEND^.\xb3}\xd8\
|
||||
c\xe1\xdf\xfb\xd6\xe5\xa6E(\xd1\xa4\x01\xb9O\x0c\x9b\
|
||||
\x5c\xb8\xca\x00\xa1~\x13\xb0\x12\xa9\xcbA)T&\xde\
|
||||
;\xa6&\xdb\xf6\xb8=\xe8\x17\x1a\x1b1V\xe5\x1b\xa2\
|
||||
$\xa9,\x8d\xef\x06\x8fF\xe5\x94\x80\xf2B,\xec\x8f\
|
||||
H\xa2\xc7\xc3\xe1p\xd8q\xd8l\x83}\x85\x8d\xb7\x09\
|
||||
\xe0\xd7n[\xbe\x15\xb0\xec\xdf&\xb1S0\xc2\x97\x8e\
|
||||
n\xc3\x06\xa8X*.aY\xc9\xb8#z{\x18\x02\
|
||||
\xf9\xf9^=\xc9\x8f\x13_p\xa4a\x8f\xe24\xae\xd0\
|
||||
\xdbu\xc9\xf8\x1b\xc6-3\x09E\x9c\x8fF\xd5\x8f\x0a\
|
||||
\x95\x1fbK\x8e\x0c\x95\xafy\xe1U\xc5t\x83\xf8\x13\
|
||||
\xb8\xb4wf\x9eG\xf0\xef\xeb\xb1\xc1\x844~>.\
|
||||
\xd3\x1b\x06n\x90\x83H3\xb1:\xe2\xb4\xed2Y>\
|
||||
\x1f\x089\xf8\xf7\xc7I.\x82\xf9\x8c*\xedB\xf4\xb2\
|
||||
L\x8c\xe2A\xfcNe\x19\xbf@\xbf\xe9\x0a\x95\xa9\x09\
|
||||
9\x7f\xbdlww\xa4A\x95\x1f\x1d\x14\xb2e\x9dN\
|
||||
I}\x83\x01\x89=\x08\x95\xb4(\x95p;\xf4T\xf4\
|
||||
t\xf44J%\x0e\x04C\xf7<4\x9e6\xc8\x81\x99\
|
||||
L\x03w\x82\x0f\xe7\xf9\x18\x1a\x9e\xff\xeeA\xcd6\xcb\
|
||||
C\x99\xf1!0\xfdJU\xb8\xabQ\x8e\xc4!\x04\xda\
|
||||
\xd8L\x02c0B\xec\x8c4\xc8\xc6\xf9\xcf\xc4o(\
|
||||
\x13\xff\xdeA\x15\xf0\xed:\x12\xdc\x07e\xe4\xdf\xef`\
|
||||
\xdaD(\x02\xca3^\x86\x11M\xf9NW\xfb\x02v\
|
||||
\xd6\xde\xa9\xd8T\xd1\xf0\x02\xe65\xd0j\xafc$\x00\
|
||||
\xfe\x0f|\x13I\xc7\xc2({\xf8\x88N)\x1e\xd0\xeb\
|
||||
L%\xe1\xbc\xcbXj6\xc4\x8eJ52\x894\xc2\
|
||||
\x8c\xe4\xbc\xe9\xde\xfc!\xff\xca\x7f\x97\x22M?\xea\x93\
|
||||
\xda0\x98wC\xf9\xa5\xda\x9b\xb14\xbb@\x15r\x17\
|
||||
\xc6%\xca\xeb[L;\xba.\xfe\x09\x84\xdb\xc15\x0f\
|
||||
[\xe5\x19L+\x99:\xd3\xa0\x18\xba\x9bz\xb1\x9e^\
|
||||
\xcb\x95\x0e\xa3]*\x11i\xf9\xf1\x03\xfe;\x13\xfc\xf8\
|
||||
\xef'@g\xda\xcf\x83\xcc\x9c\xfeAj\xb1\xe1\xc6\xf4\
|
||||
\xd9k\xad\xa7&y[\x1cLEt\xfc\xf4\xb4\x1f\x97\
|
||||
\xbc\xf4\x0ad\x86\xdbL\xdc\xe96\xe4\x10\x15|p*\
|
||||
\x01-x\xc6\xee'|\x05\x98f\xf6&5\x10\xd1\xf3\
|
||||
\xd5F@\xc3\xc7\xee\x0c\xfb\x09\xd3\xc7\xc3\xd6He\x1a\
|
||||
\x09\xfc\xbf\xabS\x1c\xee\xbf@\xc5\xacTe\xc4\xd0\x8a\
|
||||
e\xf7!T\xc4\x92\xa0\xc1\x8f'9)\xf6.*\xfc\
|
||||
\x18S\xc0\x97e\xd8\x17#\x0c?\xdf\xc4\xbf\x8f\xd5o\
|
||||
\x09\xdfu\xb4\x9e\xe7\xc1\x95\x15\xc1^\x88\xe1|\xe4\xf3\
|
||||
u4\x9c\xa0#2\xa6\xc5\x10k\x03\xbb\xec`\x9fo\
|
||||
\xad\x015\xf7v\x8f\x0dEi\x81U\xf2\xc4\xb0\x93r\
|
||||
ey\xcb\xd2{\xc1\xfeD\x7fW\x00\x12C7\xdfd\
|
||||
c|\x86\x15\xe3\xb8\xfaH._\xdb\x01z^\xa4\x82\
|
||||
^\xeb\xf83_O\xfd\x06V\x84w\x92\xb8\x0c\x82s\
|
||||
\xf2TL\x9fkZ\x5c\xe5\xb0V \xab~\xdd\xc2k\
|
||||
\x0d\x0f\xf4u\xd5+\xc0\xa8\xce$\x10\xfa\x16}\x07<\
|
||||
\xa1\x97bj\xf6y\xd7\x19H\xab\xc7n\xbe\xd6\x10\x0e\
|
||||
\xb9\x9eO\xa2\xa9}\xb1o&\x1e\xd0\x87\x947\x9c|\
|
||||
\x932s\xf5\xd5:\x0b\xd4\xda\xdd=\x8ar\x87\xea\x9b\
|
||||
}\xbd\xb5\x02\xf5\xb9\xec\x95\xca\xfdn\xb8!!\xd8I\
|
||||
7\xb2R\xec\x84}\x97un*\x89\x01\xc9r\x16.\
|
||||
t\x9c \x1b\xc5\xa1\x88H\xae\xb0\xdb|'7\xee\x09\
|
||||
\x8c\xef\xd7e:\x96\xca\xdbf\xe2\xb0\xdb\x9d\xc4\xc7r\
|
||||
\x1e5_\x1f\x85\x15\x1e\xdc\x06'\x8d\xf4\xcah\x14F\
|
||||
\x00\xe0\x97I\xc5\xa9v\x1a\x15\x1e\xcdR\xe4\xbc\xbf\xc5\
|
||||
\xd2:\x95\x7fL\xf4\x11.\xbf\xb9\xe79\x0a\xeb!\xd4\
|
||||
\x92\xe4u\xdc\xd8\x1bh\xe0\xd4x\x8c\x18\x06w\xc5\x14\
|
||||
\x04O-\xec\x0dL/\xaf\x8a\xe9c\x14Fa\x14^\
|
||||
\x830b\xa3\x93\xba\xbc'\xa6\xb2\xb5\xdf@\xcc\x9dk\
|
||||
s/\xa2W\xc8d\xdf&\x85\xf1\xe8iU`\x8dD\
|
||||
\x95\xed\xe9\xeb\x85y\x1eX77;\x8f$\xf0\xfb\x8f\
|
||||
a\xfc%\xe3w<m\xd8\x80\xca\x0f7c\x1f\xa0\xe9\
|
||||
^\xf5W\x0b\xe8\x0epXR\xb6\xbd\x9e:\x06\xb0'\
|
||||
\xb8\xec\x13\x91z\xe9\x9ag\xaf@Ep\xd4\x14O\x1b\
|
||||
60/\x85\xf3\x06K1\x04\xf8\x00[\x9e\x18_\x97\
|
||||
\x01\x8aM\x125\x86\x00\xa7<J\xbdS \xd94\xc4\
|
||||
& v\xb3\xb1d\x0d\xf5\x82X\x93\xb7\xf9\xfc\xc3\x0d\
|
||||
u\xb9\xbb>\xac\x92\xb6\xf0\xf4a\x01\x13{\x89\x97\xee\
|
||||
\xe0\xe91`A7\xf4\xf3\x1f\xa7m\xdc*\xd8V\x8f\
|
||||
#T\xdex\xc2\x0a\x00\xb1\x10e\x01;\xcao\xe3Z\
|
||||
$\xe2<\x80\xae\x22J\xe9\xed -B\x1f\xa0dm\
|
||||
e\xc7\xfeI]\x8fp\x04\xc0j\x06!\x086\xcd\x03\
|
||||
x\xa3\x1d|z\x80\x1a\x7f\x03\x02\xa3Hv\xb3!\xcf\
|
||||
\xe3x\xf6y\xd0)\xea\xee\xfcN\x0ct\x05\xd68\xaf\
|
||||
\x84\xb6D\xd9Z\xac\xaeH\xb6\xe9\xf1\xd2\x17c\x0d\xcc\
|
||||
\xf3\xf7\xf6\x98\xf3Hb>\x0eEeQ~\x1eI\x94\
|
||||
\x82\xe4\xa2V{\x5c\xf2\xfb5\xf3\xa2L\x82\x89\xec\x1d\
|
||||
\x17\x0f\x96i?*\x97\xe4\xff\xa7\xe5\x17\xd1\x19\xc4\x96\
|
||||
\xfcf\xc8\x93\xca?\x03^dhK\x11]\xe6\xf8\xe0\
|
||||
\xb2\x14\xdc\x08\x84\x80\xeb\x0b5m\x5c&\xf1\xb1\xd32\
|
||||
\xf97\xe5\xf0l\xe2\xa8\x00\xe2T'\xd8\xf2\x00\xf5\x80\
|
||||
\x86c\x9b\xf7y: \x95P\x05\x84\x1b@&\xc4\xac\
|
||||
4:\x19\xc9\xa5\xf9\x99\x91\x13\xf5v\x88)\xbbQ*\
|
||||
\xf7\xbb\xd9p\x83Kl\x87P\x1e\xf6\x02\xe3\x80\xd7\x05\
|
||||
E\xcc\xe4\x16g\x7f#\xf54\xf0\x07\xbd\xc6m\x81:\
|
||||
\x89\xe1E\xf0\x14\x8ex\xe4\xf98\xcf\x17\xb5}\xc2\
|
||||
4\x8a\xab\xd2\x9b\x07\x09*\xfeK\x03\xe26\x0e\xcd$\
|
||||
\x0a\x0c\x81*\xbb\x80\xae\x95\x00\xfa\x0a*>f\xb26\
|
||||
B\xde\x90\xaa@\xd8\xa6\xce/\xdb\xe5\xe7\x0f)\xefp\
|
||||
m\x13b3\x11g\x92;\x98\xd2\xfc\x1f24\x83\x86\
|
||||
\x15\xda\xc3Z88\x0e\x0f&~?\x8f<\x99\x04$\
|
||||
\xe1\x19a\xfe3R9\x1f|\x92\x8f\xb0B\x8f$\xa9\
|
||||
\x10\xe4=E\xcb\x1e\xab\xcf!\x00\xc7\xe2\xe5\xb6<@\
|
||||
\xe5\x09\x9eP\xe2\xf2\xfbh\xdd\x1cPWw;\x15\x8d\
|
||||
\x97\x07o3>\x8d\x06\x86\x02R\x11\x9f\x82;E\x02\
|
||||
\x1d\xb7\x22\xbfI\xcb\xe2\xbc/\xd20]AY\xf3\xfc\
|
||||
\x5c\xf6\xd3\xa0g\x12\x9d\x17./\x86\x1c\xa8_|7\
|
||||
\x9e\xa7\xa2\xd7C\x0e*\x22\xf0\x1e\xe12\x03\xfa\x1b|\
|
||||
\xbe\x0e>x\x1f\x15\x01\xe0+5O\x90\x0d!\xa3\xf0\
|
||||
\x0e#\xf6\xa4\xb1\x93\xdc\xa8\x04\x15$z\xb6\x03\xda\x05\
|
||||
zZD\x9b#\xadq\x10X\xb5\x12\xe9\x97\xa0\x814\
|
||||
\xf2\x1d=\x14\xf9\xf6g\xe1\xdf\xa1\x02,\x1f\xd0+\x13\
|
||||
H\x15&\xcbomj\x06*\x0c\xe5\xe5u]!P\
|
||||
\xf1\x9f\x11\xf2-\xf7\xb4\xe8\xd1\x0f\x84\x8a\x8e\x81\x06\x1d\
|
||||
\x85\x0f\xceG\x072\x97\xdd0\x9f\xf7\xaa\x12\xe5\xa1\x86\
|
||||
\xa9\xbbZ\x0b\x80!8\xe4w\xf8\x1c\xe7\x7f\xa7\xf2l\
|
||||
\xc4\xce@yCY*\xfe\xf3E\xfen\xedd0\x98\
|
||||
\x17b\xd4E\xe3)\xfd|\xd0!\xaf\xd6\x1dl\x9d#\
|
||||
\xed\xb4\xcfx\x96\xca\x03e@\xc8!\x82\xb3'*\xdf\
|
||||
\xb0\xb9\x9aG\xf1\xe9\xbb\xc3w\xde\xaaJ\x14\xda\xa1\xd1\
|
||||
~\x99\x84r\x06\xb9\xb7\x84R\x93\x06ve\xf6\xff\x00\
|
||||
\xe9K12@0h\xf4\xe5\x8aW3mS\xedE\
|
||||
a\x98\xbc-\x0ck\xa0\x05e \x89M\x0d\x9a\x0d\x9c\
|
||||
\xab\x1fx\xaa>\xa3b\x1a\xb7\x04\x90\x8cB\x1fh\x08\
|
||||
\x91\xc82\xdb(j\xe3\x1ex*z\xfb\xa1\xfa\x1c\xe6\
|
||||
\xe0\xbcR\xb2\x92\xdb\x82\xd1X!\x1f\x9e\xd7H4{\
|
||||
~R\x10\x8d\x834\x8d}\xc11\x09T\xdc\x17\x9a9\
|
||||
\xe4\x95\x1f\xc2*q\xc1\xfe\xa5T\xd4\xcd\xf9\xfc\xfd\x1b\
|
||||
\xa4r%U\xfe\x8e\xd4\xdc)B\xc5\x94\x8d:\xc5\xb4\
|
||||
\x12\xe4\x05\xe6w\xa9R\xb1\xa7\x03\x99\xec\x1437\x15\
|
||||
\xe3\x1a\xd36\x9e\x17\x22b\xde\xc8\x83z~\x8a\x7fo\
|
||||
ebl\xf3\x00o\xf3~\x9c,D\xfa\x1c(H&\
|
||||
S\x08\x9eW\x05^\x99\x8e4\xe8pZf3\x92\xba\
|
||||
~1(_\x0e\x99\xc4\x85\xe2\xa5+B\xaf\xb0`+\
|
||||
\x91\xcc\xb2.\x938Q\xf4\x1e4>\xc2\xfc1\x8fO\
|
||||
\xe7\xf4O\xc3\xb0\x84\x82\xa4\xf2\xaf6Q\x0ege\xee\
|
||||
!9\xec4\x15\x1am\xdf\x01 \x09M\x0c\x95\x9d\xaf\
|
||||
\x0e OH\xb3F\x1e\x94\x82\x8a\xeb\x99p|\xb3\xe9\
|
||||
\x02<\x00\xc9F\x19\xe8\xd7\xe1\x19#\x12\x89\x22c\xc8\
|
||||
\xce\x8f\x0adr:?\xd8NMWF(\xfdB\xa5\
|
||||
\x91\xa7\x01\xd2\xc2\x80}\xd4\xcag\xde\x8d\x9dq\xd8^\
|
||||
\xb8]\xe0\x5c\xc6=\xa0X\xfa^\xd0\xa1$\xa8\xdb_\
|
||||
1\xedJ\xe57Fy\xe4gw\xa0\x98\xe1\xca(\xd2\
|
||||
\x7f\xb1F\x1a\xbb\xcb\xb4\xff\xd0\xe7g\xc31\x8a\x9at\
|
||||
\x84|Tc\xfa\xb7\x91\x96j\x907\x94BE\x04/\
|
||||
(/\x14\xeb\xdd\xfa\x8c\x1b\x0cP7\x88\xfb\x1d\x1f\xf2\
|
||||
\xa1\xf0\x89\xfa\x92\x87c\x96\xb6\x0eOy%f\xe6\x1a\
|
||||
\xcbTb=\xd1;^B\xe5#\x0d\xda\x1a\xf6\x1e0\
|
||||
\xd2dE\x0c\xe8\xf9\x96^\x8bX\xca\xda\xe8\xb92p\
|
||||
\xd9q\xa8H*\xe2%\x1eG\x9e\x1a\x97\x0b\xfb\x1b$\
|
||||
q\xa9\x88\xb0\x87\x82\x1e\xef\xd8A>\x9ci\x81\xcc\x9f\
|
||||
\xc33\xc9\xcd\xce/\xf3\xf3\xe2za?\xe4A\xccH\
|
||||
o*\xac\xa0\xfc\xc1#\xfa\xefB\xa9\xe8\xa9W\xe1{\
|
||||
C\xbaQ\xac\xc6\xbf\x14\xab\xe9\xfe\x0d~k\xdd\x81\x8e\
|
||||
\xfa\xcb\x15\x13u\x13\xf2\xea\xe8\x1b6\x03\xc1\x1bS\xdd\
|
||||
X*n\xa5&}O#\xb8\x8b\xf3\xbcQ\x8dj\x18\
|
||||
\xbd\xa8\x13\x8cLy\xa8dZ\x9c\xf6\xcf/ \xd46\
|
||||
E9|_n\xf8g\x85}\xf6;<\xe7\xa0\x82`\
|
||||
\xc8\x8c\xda\x04\x00\x92#\x91(\xb8\xca\xa6\xeb\xf0\x16F\
|
||||
\x16h\x1e|'\xb8&a\x0e\x0b\xb0iM,\xe70\
|
||||
\xcf\x02\xe7\x92La0\x1c\x0f\xb7\xbc\x00Y\xf3\xa5\xba\
|
||||
\x18q\xd0\x0bf\xe9\xf3,\xcd\x83\xe8+\xf8k\x10\x81\
|
||||
\x15\x0c\xd0\xe78}{\xcb\x0b\x87\xb8\x02\xafTO\xc5\
|
||||
\xa5\x85\xedrW\xc8\x97\x15wh\xcc,J\x0b\x90\x04\
|
||||
a\x87\xca\x1f\xe21U%\xc5a/\xe4\xc9\x8d\xe0\x00\
|
||||
*g\xf8\x16\xd4\x11B(p\xcc\xf4J-\x8bz\x0f\
|
||||
# \x1a\x12u\x87\xbaY\x0eEQ\x1e8\xbf\x13x\
|
||||
`!\x90O-\xfa\x5cC\x1e\xb5\xb3B\x1e\x98\x09v\
|
||||
\xe5\x97\xe7\xc1\xaa4\xa4\x19\xe3\x18\x07\xb8P7\xb7\xc3\
|
||||
nT3\x02\x07\xbe\x90\xaf\x08\xafLe\xae\xcb-`\
|
||||
\x8c$\x0d\x82\x81\xac\x18\x05\xf2\xa1\xda\x02*.5W\
|
||||
d\x92\xcc\xa5\x84)F\xe9\x1bfb\x0c\x05K\x1c\x8a\
|
||||
\xb8\x98\xcb\xec\xefy\xc1H2\x8d\x08\x0b\x1bK\xdd/\
|
||||
\xabl?F\x1e*\x86\xd8\x80\x08\xde\x19\xd2x\xa4\x07\
|
||||
\xafH}\x05\x90\x83\x8a\xca\xbb\xc0\xe4\x0bs\xff\xa7l\
|
||||
y\xa5!\x9e#\xaf\xf80\xe2XHe\xfa\xcb\x87i\
|
||||
\x8a\x84#\x92\xfc\x9b\xd7`\xa8\x02\xd1\xb8\x8d\xcb\x5c\xb8\
|
||||
\xfcVN\x09`P\xdf\x1bF\x22\xa6\xbf\x8d\x8a\xff\xd9\
|
||||
\xbb0\x13;\x22\x7f\xce\xcc5\x96\xa9\x9c9\xb2\x8aA\
|
||||
\x986\x82\xa2\xa5\x85\x1d\xb5\x22\x94\xa1b\xf5\x94\x1fc\
|
||||
\xd5\xba\xcfG\xdc4r?\xdb(\x8cB\x14\xfe\x1f\x08\
|
||||
}\xde\x01\xb5\xbc\xe4\x80\x00\x00\x00\x00IEND\xae\
|
||||
B`\x82\
|
||||
\x00\x00\x1d\xac\
|
||||
\x89\
|
||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||
@@ -14171,6 +14564,299 @@ My\xe1#i:\x00\x00\xe8)7|$M'\x00\
|
||||
\x03\x00<Nu\xe1#i:\x04\x00LSm\xf8H\
|
||||
\x9aN\x01\x00u\x84O\xea\xff\xec\x05\xdb\xe7\x85I\xd4\
|
||||
p\x00\x00\x00\x00IEND\xaeB`\x82\
|
||||
\x00\x00\x0dP\
|
||||
\x00\
|
||||
\x00;cx\xda\xed[mo\x1b\xc7\x11\xfe\xee_q\
|
||||
\xa5\xbf\xf4M\xc7}\x7fQd\x07\x8d\x9c\x06\x01\x926\
|
||||
h\xd2\x06\xfdT0$%\xb3\xa6H\x85\xa4m\xb9E\
|
||||
\xff{\x9f\xd9\xbb\xdb\x99\x93\xe8\xc4.\x0aYEm\x03\
|
||||
6o\xb8;;\xcf\xcc\xec\xbc\xec-\xcf>\xbd\xb9Z\
|
||||
7\xaf\x96\xbb\xfdj\xbby2\xd1\xad\x9a4\xcb\xcd|\
|
||||
\xbbXm.\x9fL^\x1e.N\xd2\xa4\xd9\x1ff\x9b\
|
||||
\xc5l\xbd\xdd,\x9fL6\xdb\xc9\xa7O\x1f\x9d\xfd\xe2\
|
||||
\xd9\x1f\xcf\xbf\xfb\xeb7\x9f7\xfbW\x97\xcd7\x7f\xfe\
|
||||
\xec\xab/\xcf\x9b\xc9\xc9t\xfa\xbd=\x9fN\x9f}\xf7\
|
||||
\xac\xf9\xf6/_4\xba\xd5\xd3\xe9\xe7\x7f\x98<j\x9a\
|
||||
\xc9\xf3\xc3\xe1\xfat:}\xfd\xfau\xfb\xda\xb6\xdb\xdd\
|
||||
\xe5\xf4\x8b\xdd\xec\xfa\xf9j\xbe\x9fb\xe8\x94\x86b\xda\
|
||||
\x14\xec\xb4n\x17\x87\xc5\x04\x8b\x10o\x88\xb7\xd9\x9f\xde\
|
||||
\xacW\x9b\x17O\x8e0\xd19\xe7i\xf9v\xd2\xbc^\
|
||||
-\x0e\xcf\x81!\xc46\x86\xeb\xc3\xa4y\xbe\x5c]>\
|
||||
?H\xca\xab\xd5\xf2\xf5g\xdb\x9b'\x13\xd5\xa8\xa6#\
|
||||
\xf7\xffM\xba\x95\x8e\xada\x94R$\xd8D\xeaIC\
|
||||
\xc0\xe6\xecjy\x98-f\x87\x19>7g\xbb\xc5\xc5\
|
||||
\xe9\x9f\x9e\xfd\xbe\x17y1\xaf\xbc\xae_\xee\xd6\x85\xd3\
|
||||
b>]\xae\x97W\xcb\xcda_\x10\xf7\x8b\x9e\xcey\
|
||||
\xec|\xb7\x9c\x1dV\xaf\x96\xf3\xed\xd5\xd5v\xb3/\xd3\
|
||||
6\xfb\xc7\xc3H\xac\xf1V5(35\xe6\x04#N\
|
||||
\xf6o6\x87\xd9\xcd\x09\xcd#\xc9\x9a\xb3\xf9\xfc\xf4\xfb\
|
||||
\xed\xeeEyh\xce\x16\xf3\xd3\xc3\x9b\xebeC\x02\xef\
|
||||
\x96\xfb\xed\xcb\xdd|yT\xd8\xc5\xfcjE#\xa7\xdf\
|
||||
\x1eV\xeb\xf5\x97W\xb3\xcb\xe5d\xcaL\x80|\xf9\xd4\
|
||||
(,\xaa\xf2\x89\x89\xdf\x19sj\xc2\xa9\x87\x13Y\xe5\
|
||||
|<\x9b\x0ec\xea\x8c\x8b\xed\xeejvx\xba\x22N\
|
||||
\xa4\xd2\xdf\x00U\x19\xd6\x7fQ\x07\x165lw\x1d\xa1\
|
||||
\xc8\xff\xbbK\xa8\xad\x7f\xee \xac\x0e\xeb\xe5\xd3\xafg\
|
||||
\x87\xeb\xf5\xf6\xb0^\xfd\xd0\xbc\xb2mh\xd5o\x1b\x02\
|
||||
\xb2\x07\x92\xab\xfaU\xc1S\xd6\xe9&\xf5\x5c\xa7c\xb6\
|
||||
e\x80\x5c\xb8\x0c\x18\xf4v6\xed\xedKv\x9f\x0a\xc3\
|
||||
\x9f-\x96\x17\xfb2b\x7fx\xb3^6\xa4\xb0'\x93\
|
||||
\xc3\xf2\xe60\x9d\xef\xf7\x93\xa7\xbf\xfe\xe7\xfe\xb0\xdb\xbe\
|
||||
X\x9e\xc0K\x97\x7f\xdf\xae6\xa7\xcdn\xfbr\xb3\xf8\
|
||||
\xa4\x11\xf4\xf9\xec\xfa\xb4\xf9\xe1\xe5\xe1\xf0\xaf\xb3ia\
|
||||
SV\xe99\x9f]6\xab\xc5\x93\xc9\xc5\xea\xf2\xe5n\
|
||||
\xf9\xb7\xe2y\x03\xedzv\x98?\xefI\xcd\x19\x9e\x9e\
|
||||
7\xa0~\xcd\xde\xfd\xe8\xab\xb1\x9f\x0b\x82\xa2\xcfj\xf8\
|
||||
o\xf8\xfa\x1f\x8fh\xc3C\x02Zp\xbd>m6\xd8\
|
||||
\xf7\x9f4\xdb\xeb\xd9\x1c\xbep\xda\xa8\xce\x03\xce\xa6\x97\
|
||||
B\x8c\xd9\xcdr_\xa5\x90\x92\x99I\xaf[\x16-\xd9\
|
||||
6\xa5\xfe\xdfG\xe7\xa3\xc7\xb7\xfd\xfbq\xd8\xc7a\xff\
|
||||
\x9da\x9d\xf3'\x95\x5cp<U\xd0\xb2k\xb3\xd26\
|
||||
y\x10}\x1bltY7Z\xf96i\x1f\x14>\x06\
|
||||
\xdd:\x9dcn\xb4\xf6m\xceIY[X\xf8\xd8\x22\
|
||||
\x14\xaa\x84/LhuR.\x80\x07\x05C\x9dC2\
|
||||
\x8d\xb6\xbeu\xd6*|\xeflkL\xb0)\xc8\x8f\xc4\
|
||||
\x83\x87\x88y\x92\x1b\xaf\xc1\x8b\xb3D\xc4BJZ\xe5\
|
||||
\x97\xa0n\xa1\x17\x04L\x8f\xa6M>\x05=\x1a\x08\xfe\
|
||||
\xd99\x9b\xb3d\x09\x01c\x80\x84q\xbc\xbe\xd3\xad\x8f\
|
||||
\xd9\xdb\x91\xb0\xd6\xb4\xd6\x04\xd2#\xc32\xae\xf5\xd6\x06\
|
||||
\xedn\xeb \xb4\xd1)\xab\x93\xd4\x86j=\xb8j-\
|
||||
\x95\x11Z\xeb\x94\xd1\xb7\xcc`Zm\x8c/s\xaa\x22\
|
||||
NT\xab\xdc-\xe32\xad:\x01\x93X\x0b\x95\x1d\xeb\
|
||||
`X\x97\x15P\xd4>H(\x14P\x91T\xfc\x0c\x9a\
|
||||
?a6\xeb\xa7N\x11l*k^q\x90\x01sY\
|
||||
\xb0*+\x8b\x7f\x1b%?c\xe61e\x08\xad\xb1&\
|
||||
Y\xc1b]\xe9\x96l\x1e6\x19\x9b\x91\xcd;\x02-\
|
||||
|A\xf8G\xf5\x19\xa9\xc7\xc1\xbf\xc6*g_\x94\x0e\
|
||||
Z\xbd\xf6\xa8+\xf7\xc6\xbe\x93\xe3\x1e_\x94?\x9f4\
|
||||
\xf4x\xc2\xb9\xae\xcd}\xbd\xd3\xa7\xbb!\xb9\x89\xd2b\
|
||||
v\xb3\xaa\xa9o\xf8\xfa\xe6\xb0\x9a\xbf\xa8\xc4\x81Jy\
|
||||
\xde,\x98\xfc\xd6\xa4HA\xca\xb4\xd1D3P&\xcd\
|
||||
|\xbd\xba>\xa1\xf1\xa8\xcaw\xeb_>\xbe\x9e\xcf\x7f\
|
||||
P\x17!\xb8\x94\x7fu4aw\xc5\x05\xa0-\x03\xfd\
|
||||
\xbd[m\xec\x7f|9\xdb\xd5z\xae\x22\x14\x1f$\x18\
|
||||
s\x1c\x8cy\x1706\xc3R\xd6\xc2E\x13\x9c*\x04\
|
||||
\xfd\x10\x00\xd9\xe3\x80\xec;\x00\xea>x\x94\xb9\xe6A\
|
||||
\xd8\xc6\x1d\x87\xe2\xde\x01J\xb5\xc8C2\x8d?\x8e\xc7\
|
||||
\xbf\x03\x9e\xde(\x0ff\xdb\x84\xe3P\xc2\xfb\x98\x86\xf7\
|
||||
\xcf\x03\x00\x14\x8f\x03\x8a\xef\xbcmjh{\x00`\xd2\
|
||||
q0\xe9\xbd\x82\xda\x076\xcf\xcfd&3\xceLo\
|
||||
~*3\xe5c\xb8\xb3i\x83S\xa2\xce\xae\x04\xb2c\
|
||||
\xf6\xce\x10\xc5i\xa7QI$d\xed\xacLD\xc9\x89\
|
||||
\x9c\x1cm6\xc8\xb9(\x0fL\xb4\xa9dm\x90\xa9\x04\
|
||||
\x08\xb6\x89\xb9EF\xf6\xca6Y\x81s\xa2Z!\xa6\
|
||||
\xd6\xe5\x14\xa2'\x9a\x8a.\xa1\xd6\x88\xb1\x0d\xc9\xeb\xae\
|
||||
\xbcL\xb95A[\xe3\x9b\x18\xda\x145U\x0f)\xb5\
|
||||
$F\x8eD\xa3J\xc3\xc6&\xa1\x8e\xb04\xb2\x89\xbe\
|
||||
\x8d)\xa8X\x8a\xcb\x84\x9a%dHMd\xeb<J\
|
||||
\x9d&\xf9\xee\xb8\x22\x11M\xeb<\xe4\xdc\xe1\x89\xa6U\
|
||||
\xa4uH\xc5)\x18U\x9c\xa35\x19'\x8b\xc78\x19\
|
||||
\x06\xe3\x1c!\x16\x03\xe4\xa4\x81\x113\xafk\x8e\xf4-\
|
||||
\xc4\x93\x22w \xee\xc2\x1al\x5c\x09R9\x03#\xa9\
|
||||
\xc4a\xcd\xaa\xefN\xe4\xc1\x0c\xd24\x83\xb9\xd8\x84\x15\
|
||||
'[\x9b\xa6K'\x18\x1c\x83\x9d\xa5:\x10\xe3d_\
|
||||
#c\xb1\x0b\xb2[JW\x15\xde<<\xd1\xb4\x8a\xb4\
|
||||
\x0e\xa98\x05\xa3\x8as\xbcf\xc5)\xc4\xab8\x19\x06\
|
||||
\xe3\x1c!\xe6\x01b\x123\x92\xcc\x87=%\xf5-\xc4\
|
||||
\x93\x22w \xee\xc2\xfa\xf0\xa9\xf1\xcdOU\x94Z\x1d\
|
||||
\x8bB\x1aM\x062\x87\xec\xd9+E\xb5\xce\xa4`\xd0\
|
||||
\x89*\xb8I2\xde\x17\xdfRZ%j/\x15\xf2h\
|
||||
\xd0\x91\x1c\x1fs\x8d\xd1\xa1k`\x14\x5c\xd2\xa9L[\
|
||||
]Q\x00\xf7P\x8eV\xd4\x149\xf4\x1d\x0d\x1d\x87\xd9\
|
||||
\xa0\x0c\x9ate\xc8\xfb\x1cl\x81\x9e\x03\xb6\xf0\xb6o\
|
||||
\xfa`DcsD\x8bL\xcd\x86W\x0e6\x8a\x94\xb5\
|
||||
\x1d\xf6N@\xf7\xa0\xd1a\xc1\xe9l\x9bmL\xc8\x10\
|
||||
>\xc39!C\xee\xcdN\xed\x0b\x1a#\x0f\x13\xc7\xe8\
|
||||
\x5cq0k\xb5\x8d\xb6A+\xe43m\xad\xae\xd8\xec\
|
||||
\x9f\xc8\xd9*\xde\x81\xc8`\x99\x11c\x1d\xad\xc9PY\
|
||||
<F\xca0\x18\xe8\x08\xb2\x18\xc0\x93*#f\xcek\
|
||||
\x8et.\xc4c\x91\x07\x9cw`USW\x02+\xa7\
|
||||
2\x12J\xack\xb2\xbeId6\x03\x9b\xa6\xc2\x906\
|
||||
\xac@\x85\xc1\x89\x81p\x84\xea\x1d\xd2e\xd8\x8f*V\
|
||||
\xe1t\xa4t\xe1\x8c\xd5C\x85\xdb\x8e}\xbb>\x92\xad\
|
||||
+\xe2Je\xc0\x82\x19#\x1e\xaf\xcc\x90\x85\x98\x02\xb3\
|
||||
@$@\x8f\xf1\x8f\xc6\x88\x99\x95\x9f\x5c\xa4.>\xb2\
|
||||
\x81\x94\x94\xc5\xaf\xb0\xef\x82|0A\xea-]\xa2>\
|
||||
\xda\xc4kC\x19M\xc6\xa8\x9e\x10\xa9\x94\x87V\xba\x03\
|
||||
,x^\xa6\xf4\x07%\x03\xb6\x87\x03\x18\x87\xe4\x9a\x83\
|
||||
\xce\xe4l\x1a!\xdc\xfan\xbai\xb5Cb(gp\
|
||||
I\x19\xafIG\xa9\xcdF{\x0a\x0a\x81\xf2\x81\x8d\xe4\
|
||||
B\x0e\x1e\xed\xb4\xf2\x8d7\xb0\x96O\xd9\x0fA\x12)\
|
||||
:\x18\xdb\xb8\xd4&\x9b\x9c6\xe5\x90'{d\x9f\xd8\
|
||||
8\xdf\x06\xacW\x9c\x09\x1e\xed\xac\x0a\xbeq\x16\xf9D\
|
||||
\xd9\xd4y\x10\xf2>\xdc\xd4Y:\x911*+\xe4\xe5\
|
||||
D\x05\xaf1tJCil\xd8\xa9\xfdC\x97\xc2{\
|
||||
\xbc=\x8d\xb12\x17\x86:Z\x90\x91\xb2p\x0c\x94Q\
|
||||
0\xce\x11b1\x80'UF\x82y]s\xa4r\x16\
|
||||
O\x88\xdcc\xb8\x0d\xaa\x9ayxf\xb5T.B}\
|
||||
uA\xa1j\x9a,L\xc0vac\xb1\x05\x19\xa70\
|
||||
7\x85G\xe1\x06\xd57\x84\xc3\x08/\xaaP\x85\xcbu\
|
||||
\xc7\xbe\xd5\x15\xd9?\x85\xd3J\xbf\x1e\x9e\xbar\xbcG\
|
||||
<\x10\x19\xaf\xe0$\x00\x8f\xd7\x15\x88\x85\x94\x02\xb2\x00\
|
||||
$0\x8f\xe1\x8f\xc6\xf0L\xc1O\xaeR\x97\x1f[A\
|
||||
H+!\xf4\xb8\xef\xc0|0\xf1\xe9-G?\xfa\xe8\
|
||||
\xb9\x9c\x86\xaf\x19\xe5\x85\xe7V\x0aJ\xf9dB\xcc\xdd\
|
||||
+\x09\xe8\xc7\xe4\xe2.\xa8\xcf\x13\x9d\xab\xc2\xc5\x91\x19\
|
||||
\x1d\x9d=#oi\x1dUg@\x1b[\xe7}\xa0\xd2\
|
||||
\x06\xde\x19\xb5'\xcdZ|\xb4\xc8\x84pz\xb4\x02Z\
|
||||
E\xd3y\x8dq`Q\xe2\x84G\x94\xf7\xa6S<\x0a\
|
||||
]\xe4\x8eh\x1a\x0b\xab\x1a$\x8f.\x9c\xc5\x94\x1c6\
|
||||
\x8bU-\xeaW[r*\xf2\x04\xb2\x08\x9d\xa3\x83\x97\
|
||||
1\xbe;\xf2\xcf\x19\xdb\x22iU\x8e\xd7!hT\xa1\
|
||||
T\xca\xd6\x1a\xe5\x1a\xb8\x81\xf7n\xb0\xd8\xf0\xd4\xb58\
|
||||
=\xe2\x81\xc8p\x99\x11\xa3\x1d\xad\xc9`Y>\xc6\xca\
|
||||
@\x18\xea\x08\xb4\x18\xc0\x93*#f\xcek\x8e\xb4.\
|
||||
\xc4c\x91{\x10waUc\x0f\x04\xa1\x1cV\x18+\
|
||||
\xb1\xae)\x14N2\xb3!\x84u\xd8dlG\x86*\
|
||||
\x8cN\x91\x8e\x9d\x81=D\xbaM\xf5%F+\x1c\xaf\
|
||||
+\xc5\xaaCV/\x95\xae;\xf2\xef\xfa\xd8\x9d;\xf4\
|
||||
\x98+\x95!\x0bf\x02\xf3xi\x01Z\x08*P\x0b\
|
||||
L\x02\xf6X\x03\xa31b&\xf3\x13\xab\xf0\xf2cC\
|
||||
\x08i%\x84\x01\xd5]\xa0\x1f0X\xdd:\x9c\xea\xee\
|
||||
\x04\xd8{\xbd\x13\xf0U\x89\xf4\xf0{e\xbb3\x94\xe0\
|
||||
\xbdIC\xff\x96L\x0ct\xe4\x92Q\x82&:_\x80\
|
||||
2[\xe5J\xb0G\x85\x8e&\xdb\x95\x5c\xa1[\xf4\xd7\
|
||||
\xc4\x14\x1f,\xba\x89\x9e\x81F\xaa\xc8\x0e\xe5n2\x98\
|
||||
\xe5Q\xfa\x16\xa2S:\x10)\x07zAY\x09\xfc\xf2\
|
||||
\xe9=,\xf1X)7?\xf6j*\x08\xfb\xb89\xfd\
|
||||
\xad\xf6)7\xa1N\x1bx\xd8\xc8d\xdd\xe5\x93\xab\xd5\
|
||||
a\xb9;\xfeZ\xab\xb3\x8f\xbbw\xfb\x18J\xca\xd0#\
|
||||
\xc5\x93\x10\xa2R\xba6\x1e&\xa1\xe4i,\xbd\xebs\
|
||||
\x9e^\x87c/8\x13\xb4w\x8dE\x07\x02\xa3\xfa\xae\
|
||||
\xf1G\x1c\xa2\xce\xcbR\x07\x82\x0d\xd6q\xd0\xa6\xa5~\
|
||||
\x0c{\xc8\xa2;4\xda\xf4\xd5\x83W*\xa4R\x8c*\
|
||||
T\x0bT8\x846xK\xf6s\x06;\xd0j*p\
|
||||
\xde\xd7R\xf3\xe5\xc5\x85\xc9\xf7h)\x7f\xcf\x96\x82r\
|
||||
3\x1du{\xca\x06*\xa6>\x95\x22 i4{h\
|
||||
\xe0\xa8\x8aB\xc42H\x02\x91\xa22\xda\xc6\xc6Xh\
|
||||
\x1e\x06\x0b\x14\xeb#\x9dA\xc5\x06\xc1O\xc5\xec\xba\x92\
|
||||
\x1d\xed}T\xb0\x0dL\x93\xa8\xd0J\xb6\x1c\xa8d\x0d\
|
||||
#\xa7\xf2&\xb9T\xa1\x01=\x7fr\xdd\xed\x08\xaa\xd5\
|
||||
\x12\xaa\xe3\xf76\xd1\xc5EHJ\xdd\xa3\x89\xc2=\x9b\
|
||||
\x88\xb4m3\xb6\x0d\xbd\xdfOV\xe9\xbed\xc0\x16C\
|
||||
\xaa\xe8\x8e\xcb]\x86\x8a#\xd1\x0c\xf2N.\xa1\x0a\xbd\
|
||||
\x82R\x86\xa6c\x06\x92/\xa5UhPuw\x05@\
|
||||
F\x8ba\xe8\xb4'\xb7\x06\xd60\x98\x8e\xbck\xbd\x83\
|
||||
\xd9\x82\xc56B\x16w\xf4\xce*\x98HnAo\xf2\
|
||||
\xa3NT\xe7\xbf\xbf\x8d\xe6\xee^m\x14\xef\xd9F\xde\
|
||||
\xd3\xe1\xaf+\xc9\x024\x8b\x8cT\x8c\xe4\x11\x98P\xde\
|
||||
\x94#\x1a\xa84\x19\x8b\x9d\x00\xa2\xf2Y\xa7\xeeX\xc6\
|
||||
b\xd7\xa1\xda\xf2\x96N\xc4\xc0\xa2d\x17\xafa\xd1\xc2\
|
||||
\x00\xd6\xb3>\xe4\xae\xbb\xc16\x04\x8f\xb2\xeb\x94G\x9e\
|
||||
+\xe5\x82\xa6\x01D\x03't\x9dD\xa3\xab\x11hU\
|
||||
\xdf\xdbN&\xc3R\xcb{\xb4S\xbag;\xa1\x96B\
|
||||
q\xeb\xfaN\x10\x9d\x22vF\x7f\xdcoS\x8ad?\
|
||||
\xd0\xbd\xf3\xe4\xfa\x09AIg\x9f\xbb&1\xba\x88\xca\
|
||||
\x816V\x80uBW\x0a\x07\xe4\xb6\x90\xfb\x97BH\
|
||||
2t\xce_8\xa4`\xb0\x0b#\xfaxK\x9b\xb0\x14\
|
||||
\x8d\xc6\x19p\x8d\xa8\xd0\xe9nL\xdfO'\xb4\xb1\xff\
|
||||
A\xd4\x8b\x8b\x8b\x8b8\xbbGK\xe5\xfb.!\x90\xde\
|
||||
\xa9\xe5H\xdd\x09F\x0e*h]\x1b\xd4\x88*\x01\xdd\
|
||||
\x81\xc9\xa4K\x9d\xba{J\xb0K\xa9',\x9d\xd5\x18\
|
||||
\xaa'\xb4\xb5-Z\x91\xf2=6\x12\x1d9w\xa5?\
|
||||
\x86Dd\x8d\xf2\xae\x97^\x8c\xc5r\x8a\x03v\xd1\xaa\
|
||||
@\xf6B%\x02\xcb\x1b\xdb\x9b3\xd2\x15*P\x11\x0c\
|
||||
\xa3G4\xfc\x1fHS\xf5\xad\x8b<.\xb8u\x09\xe9\
|
||||
\x9c)\x91r:\xeaa\xba\xb2\x06\xa9R\x0ctO\xd0\
|
||||
\x22\xe4\x1b\x07\x07\x05|\x8c\xf3t\xf8\x82\x90\x84\xfe\xd2\
|
||||
\xa5\xfe\x92$U\x00\xf0u\xd7\xb8\xd2zzC}\x0a\
|
||||
i\xcf\xa5\xae\xccs\xc891\x1d\xbd3\xd4\xf5O\x96\
|
||||
^L\x22Q\xa1vW\xd8Z\xb4[J?\xe4\xe8\x8a\
|
||||
3\x0a\xc6\x0ck\x87R\xe4)o=\xedQ\xba\x8f\xee\
|
||||
B\x7f\xbd\x105\xa5K\xd1\xd3\x1bM\x04aD\xd8@\
|
||||
\x89\xd2X\xa4\xbc<\xbe=2\xdc\xef9\x17h{\x1a\
|
||||
#\xadL\x18\xe7h=\x09s\x90\x8dQ2\x88#\xf7\
|
||||
p\xce\xc5\xd7B1\x95\x8d`]W\x1ci\xbb\xca\xc6\
|
||||
\xe2\xf6\xf2\xdf\x01\xc4\xafZ\xbagVIe\x224\xc7\
|
||||
\xeb\x095\x97\xce\xb4\xaa\x9fm\x22\x0c%\xacw\xe4n\
|
||||
\xcb\xf9\x08e\xf5\x09\x01\x93\xbdGh\x9b]\x8d\x0e\x08\
|
||||
\xd8\x05\xd9/\xa5\xb3\x8e<\x9a\xaf\xa2\x9c\x0b\xc4\x95*\
|
||||
\x1137\xe9Y\xa3\xb5\xa5\x1f\xb2\xa4\x02\xb4\x00u\xf4\
|
||||
\xf6\xc8\xf9x\x04\xcf\x13\xdc\xe4\x1au\xf1[V\x10\xb2\
|
||||
J\x04=\xaa#@\xeb\x01\xc1O\x1d\x01\xa0\x1a\xf7\xf1\
|
||||
N\xb8qo\x0b6?sV0\xfc|\xa0\xfc\xd7\xff\
|
||||
[\x7fGA\x91\xf2\x1b\x0aC%>q\x98\xbc\xf3+\
|
||||
\x87\x8f\x11\xeac\x84\xfa\x18\xa1\xfe\x7f\x22\xd4\xc7\x1f\x0f\
|
||||
}\x1c\xf6\x01\x87\xddM8\xa8\xeb\xfb_\xc3\x0d9K\
|
||||
\xfcn\xef\x8c~\xe1\xf8\xf4\xd1\xbf\x01:\x8f<\x83\
|
||||
\x00\x00\x04\xc0\
|
||||
\x89\
|
||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||
\x00\x000\x00\x00\x000\x08\x02\x00\x00\x00\xd8`n\xd0\
|
||||
\x00\x00\x04\x87IDATx^\xed\x95mL[U\
|
||||
\x18\x801\xfe\xf0\xa7\xc6e&\xd3-\xdb\x1c \x0e\xdd\
|
||||
\xb2\xa8\xf1+F\x9d3\xfe\xd0%#\x9a\x98\x99\x18\xff\
|
||||
\xb0\x1ft2M\x99LZ\x91.\xb4`\x08\x1f]\xc6\
|
||||
\xc7\x0f\xb1\xc0\xa0\x85\x01\xbd-0J[\xd6i\xa0\x11\
|
||||
R>\x1ci\xd6RF[\xa0@kK\xf9\x10h\xcb\
|
||||
G\xf3\xfa\x96S\xaf\xf4\x0e\xbc&\xa6\xdbL\xee\x93\xe6\
|
||||
\xe6\xe6\xbd\xef9\xf79\xf7\xbc\xefi\x02<d$0\
|
||||
\x03\x0f\x1aN\x88\x0dN\x88\x0dN\x88\x0dN\x88\x0dN\
|
||||
\x88\x0dN\x88\x8d\xff\x89\xd0rh\xc3\xb3\x14\x0c\xaeo\
|
||||
2\x1f\xc4\x1f\xa6\x90bh\xfa\xe4Uc\xaaD\x9fr\
|
||||
Y\x93\x94\xdb~\xfa\xaaabn\x99\x91\x13W\x98B\
|
||||
i?\x99|\xcb\xa1\xe3\x12\x1d\xda<\x9bC\x1d\xfc\xe6\
|
||||
\xfa\xb9\x9a\x1eFN\x5c\x89\x11\xda\x0c\xc3\xf1\xc2\x9b\x9f\
|
||||
\xd7\xf6'\x7f\xdfqD\xa0:\x98\xdd\xfc\x0c_~\xe6\
|
||||
\x8av{N\xbc\x89\x0a\xcd\xaf\xaek,\x1e\xe5\xed\x19\
|
||||
\xe9-[i\xb7\xb5Xw'\xad\xdc\xb0\x9f\xaf\xd8w\
|
||||
\xe1\xda\xe9\xd2N\x08\xdd\x86\xc0/\xb0\xe9\x8b\x1d\x0b^\
|
||||
\xafW\xa3\xd1\xd4\xd4\xd4\xb4\xb6\xb6\xce\xcf\xcfc$\x1c\
|
||||
\x0e\xeb\xf5\xfa\xf5\xf5u\x92`\xb7\xdb\x87\x86\x86\xc8\xbd\
|
||||
\xc3\xe1P\xa9T2\x99\x0c\x87\xac\xad\xad\x91\xa0\xcdf\
|
||||
knn\xc6`OO\x0f\x8e\x05\x22\xe4\xf4\xaf\xbey\
|
||||
\xc5\xf8b\xa1\xe1h\xbe69\xef\xc6\x11\xa1\xea\xd0\xa5\
|
||||
\x96\x03Y\x8d\xfb\xbe\xba\xb6\xf7\xbc\xec\xa3\x92\x0e\xf0K\
|
||||
`\xf6\x13\x98\xfd\x02 :\x11\x99K$\x12Y,\x16\
|
||||
T\xe9\xee\xee\x16\x08\x04\xf8\x9aP(\x94\x9e\x9e\x8eW\
|
||||
\x92\xa3\xd3\xe9\xaa\xaa\xaa\xf0\xc6`0H\xa5\xd2\x89\x89\
|
||||
\x09\xbf\xdf\x8f\x0b\xa8\xa8\xa8 \x09&\x93izz\xda\
|
||||
\xe5r\x15\x14\x14\xa8\xd5j B\xe5\xbd\x8ec?\xdc\
|
||||
:*\xd6='\xeaL\x14\xaa\x0f]j=p\xb1\xf1\
|
||||
\xe9\xaf\xeb\x9f\xfaR\xb6'\xa3\xfal\x85\x0e\xbcY\xe0\
|
||||
\xfa\x00&\xde\x82\x80)\xaa\x03`4\x1a\xc9\xcb\x08\xd9\
|
||||
\xd9\xd9\x83\x83\x83\xbb\x09)\x14\x8a\xae\xae.\x12\x5cX\
|
||||
X\xc0\x1c\xfc\xba\xf4X\xa4\xbf\xbf\xbf\xb8\xb8\x18\x88\x10\
|
||||
\x9f2\xa7\x8a\xf5)\x22M\xe2wm\x87\xbfU\x9e\x10\
|
||||
\xa9^\xbeL\x9d\xc8m>&h|GB\x0d\x8f\xea\
|
||||
`\xea\x148_\x07{*,\xc8\xe8)\x18BEE\
|
||||
Emmm\xbb\x09\xc9\xe5rZ\x08\xc9\xc8\xc8@{\
|
||||
\xdc\xa3\xce\xce\xce\x92\x92\x92\xca\xcaJ\x89D\x823\x00\
|
||||
\x11\xe2]\x1f&M\x9e\x92\xab\xfe\xd9\xdc\x0fK?\xc2\
|
||||
b%,V\xc0\xbc\x14|\xb90y\x12\x1c/\xc1x\
|
||||
\x12\xd8\x9e\x00\x9f\x84\x9e\xf4^!\x8a\xa2\xfe\xa5\x10\x8f\
|
||||
\xc7\xc3O\xd2\xdb\xdb+\x16\x8b\x83\xc1 F\xfa\xfa\xfa\
|
||||
\xfe\x16*\xd4Z\x22M.\xa0\xb2[L\xe0\xbd\x00\x9e\
|
||||
\xcf\xc0\xfd)\xcc\x9c\x81\xa9\xf7\xc1\xf9\x06\xd8_\x80\xbb\
|
||||
\xfba\xf41\xb0$\xc0\x12EO\xba]\x08\xd7\x9a\x95\
|
||||
\x95500\x80\xe5\x8cB\xb8)$N\x0b544\
|
||||
\xd0Bsss\x98\xe3\xf1x\xb0\x15\xea\xea\xeaH0\
|
||||
Fhx\xca\x9f(\x8c4y\x91\xe67p\x9f\x8d\xd4\
|
||||
\xaf\xebC\x98|\x17\x9c\xaf\xc0x2\xd8\x9e\x04\xeb#\
|
||||
p'\x01,{!\xbcJ\xc6\xc3\x96\x10\x9f\xcf\xd7j\
|
||||
\xb5X\xd7\xb5\xb5\xb5\xf9\xf9\xf9\x1b\x1b\x1b\x18/--\
|
||||
\xc5\xfa5\x9b\xcd\xd88B\xa1\x90\x16\xca\xcb\xcb\xc3!\
|
||||
\x18\xc7\x17\x13\x0f\xec\xbb\xcc\xccLL\xc3`YY\x19\
|
||||
n_T\x08\xb9i\x99\x15R\x83z\xf3$\xcc\xa4E\
|
||||
\xebw\xf6\x1cxE\xf0\xfbEp\xf3`\x96\x073\xe7\
|
||||
a\xf5W\xda\x06\xb6\x84\xca\xcb\xcbGFF\x9a\x9a\x9a\
|
||||
\xb0\x93\xc9\x97G\xb0\xd7\xb0\xf3[ZZpS\xf0\xab\
|
||||
TWWc\xb0\xbe\xbe^\xa9T\xe2\x1e\xe1\xde\xd1\x1d\
|
||||
\x8e8\x9dN\xcc\xc4U\xb9\xddn\x12a\x9c\xd4\x9b0\
|
||||
\xf5\x1e8_\x8b\xd4\xaf\xfdy\x18?\x0cw\x93`\xf2\
|
||||
\xe3\xd8\x9c((Dw\xefnttt`\x7f\xc1\x96\
|
||||
P{{;\xf3\xf1N0\x84\xc2\xe0|\x15\xc6\x13\xc1\
|
||||
\xf6xd\x9b\x9co\xc3\xda\x148N\xc5\xe6Da\x15\
|
||||
\xc2E\xe7\xe4\xe4X\xadV\xf8\x0fB\x00\x8br\x18\xdb\
|
||||
\x13\xa9_3\xfe\x1e\x8d\xd8\xac\xec\xfc_FJ\x87\x19\
|
||||
\xdd\xc2\xe7\xf3a\xa1\xa0\xee\xe8\xe8(\x89\xe0\xc9\xb9\xbd\
|
||||
\xcb\xfe\x81{\x84\x10\xac\x5c,\x97?\xb4\x10\x18\x8el\
|
||||
\xe2\xfd\x85)\x84\xe5\x86\xad\x8b\x15\x1a\x08\x04\xf0J\x1a\
|
||||
\xe7~\xc2\x14ZYY\xc1\x13\x02\x8b\x7fll\x0c\xaf\
|
||||
x\xc0\xa3\x19#'\xae0\x85\x08\xe1\xbf`>\x88?\
|
||||
;\x0b=@8!68!68!68!6\
|
||||
8!68!6\x1e:\xa1?\x01S\xe1\x9f|\x8b\
|
||||
j\x8f\xf5\x00\x00\x00\x00IEND\xaeB`\x82\
|
||||
"
|
||||
|
||||
qt_resource_name = b"\
|
||||
@@ -14186,6 +14872,10 @@ qt_resource_name = b"\
|
||||
\x05\xe2Y'\
|
||||
\x00l\
|
||||
\x00o\x00g\x00o\x00.\x00p\x00n\x00g\
|
||||
\x00\x09\
|
||||
\x03\x96\x8b\xa7\
|
||||
\x00g\
|
||||
\x00p\x00l\x00v\x003\x00.\x00p\x00n\x00g\
|
||||
\x00\x12\
|
||||
\x0a=M\xc7\
|
||||
\x00q\
|
||||
@@ -14195,21 +14885,35 @@ qt_resource_name = b"\
|
||||
\x07\x87WG\
|
||||
\x00q\
|
||||
\x00t\x00.\x00p\x00n\x00g\
|
||||
\x00\x0e\
|
||||
\x03\x9b+G\
|
||||
\x00m\
|
||||
\x00a\x00t\x00p\x00l\x00o\x00t\x00l\x00i\x00b\x00.\x00s\x00v\x00g\
|
||||
\x00\x0d\
|
||||
\x04\xbey'\
|
||||
\x00p\
|
||||
\x00a\x00c\x00k\x00a\x00g\x00i\x00n\x00g\x00.\x00p\x00n\x00g\
|
||||
"
|
||||
|
||||
qt_resource_struct = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x02\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x12\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04\
|
||||
\x00\x00\x00\x12\x00\x02\x00\x00\x00\x04\x00\x00\x00\x05\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x01\x97\x0d\xb6\xa1\x16\
|
||||
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x03f\x83\
|
||||
\x00\x00\x01\x97\x12L\xfd\x82\
|
||||
\x00\x00\x004\x00\x00\x00\x00\x00\x01\x00\x03H\xd3\
|
||||
\x00\x00\x01\x96\xed\x00qM\
|
||||
\x00\x00\x01\x97\xb7\xeb`\xa2\
|
||||
\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x01\x97\xa0\xe5\x95}\
|
||||
\x00\x00\x00\x88\x00\x01\x00\x00\x00\x01\x00\x03\x8d]\
|
||||
\x00\x00\x01\x97\xb7\xaf+u\
|
||||
\x00\x00\x00\xaa\x00\x00\x00\x00\x00\x01\x00\x03\x9a\xb1\
|
||||
\x00\x00\x01\x97\xb7\xb1\xa2\xd4\
|
||||
\x00\x00\x00v\x00\x00\x00\x00\x00\x01\x00\x03~\xeb\
|
||||
\x00\x00\x01\x97\xa0\xe5\x95|\
|
||||
\x00\x00\x00L\x00\x00\x00\x00\x00\x01\x00\x03a;\
|
||||
\x00\x00\x01\x97\xa0\xe5\x95{\
|
||||
"
|
||||
|
||||
def qInitResources():
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import random
|
||||
from typing import Optional, Tuple, NoReturn
|
||||
|
||||
@@ -8,6 +23,7 @@ class Question:
|
||||
def __init__(self, no: str, topic: str):
|
||||
self._no: str = no
|
||||
self._topic: str = topic
|
||||
self._count: int = 0
|
||||
|
||||
def __str__(self):
|
||||
return f"Question<No: {self._no}, Topic: {self._topic}>"
|
||||
@@ -43,6 +59,12 @@ class Question:
|
||||
def topic(self) -> str:
|
||||
return self._topic
|
||||
|
||||
def increase_count(self) -> None:
|
||||
self._count += 1
|
||||
|
||||
def can_pick(self, max_count: int) -> bool:
|
||||
return self._count <= max_count
|
||||
|
||||
|
||||
class Student:
|
||||
def __init__(self, no: str, so: str, name: str, major: str, class_name: str):
|
||||
@@ -105,10 +127,15 @@ class Student:
|
||||
def class_name(self) -> str:
|
||||
return self._class_name
|
||||
|
||||
def pick_question(self, questions: list[Question], num: int = 3) -> None:
|
||||
if len(questions) < num:
|
||||
raise ValueError("Not enough questions to pick from.")
|
||||
self._picked_questions = random.sample(questions, num)
|
||||
def pick_question(self, questions: list[Question], num: int = 3, max_count: int = 3) -> None:
|
||||
available_questions = [q for q in questions if q.can_pick(max_count)]
|
||||
|
||||
if len(available_questions) < num:
|
||||
raise ValueError("Not enough questions with count <= max_count")
|
||||
self._picked_questions = random.sample(available_questions, num)
|
||||
|
||||
for q in self._picked_questions:
|
||||
q.increase_count()
|
||||
|
||||
|
||||
class Course:
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import math
|
||||
import os
|
||||
import traceback
|
||||
from typing import Literal
|
||||
|
||||
import pythoncom
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from win32com import client
|
||||
from typing import Literal
|
||||
|
||||
from module.achievement.doc import DocxWriter
|
||||
from module.achievement.excel import ExcelReader
|
||||
@@ -39,11 +55,13 @@ class DTGWorker(QObject):
|
||||
students = Student.load_from_xls(self.input_filepath)
|
||||
questions = Question.load_from_xls(self.input_question_filepath)
|
||||
|
||||
d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx"))
|
||||
max_question_count = math.ceil(len(students) * 3 / len(questions))
|
||||
|
||||
d = DocPaper(self.output_filename, resource_path("template/template-defense-paper-paper.docx"))
|
||||
for index, student in enumerate(students):
|
||||
if (p := int((index + 1) / len(students) * 100)) != 100:
|
||||
self.progress[int].emit(p)
|
||||
student.pick_question(questions)
|
||||
student.pick_question(questions, max_count=max_question_count)
|
||||
d.add_paper(course, student)
|
||||
d.save(self.output_filepath)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pytest
|
||||
openpyxl~=3.1.5
|
||||
pyside6~=6.9.0
|
||||
python-docx~=1.1.2
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
<!--
|
||||
- Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
- #
|
||||
- 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/>.
|
||||
-->
|
||||
|
||||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<qresource>
|
||||
<file>./images/logo.png</file>
|
||||
<file>./images/3rd/qfluentwidgets.png</file>
|
||||
<file>./images/3rd/qt.png</file>
|
||||
<file>./images/3rd/matplotlib.svg</file>
|
||||
<file>./images/3rd/packaging.png</file>
|
||||
<file>./images/gplv3.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
BIN
template/template-achievement-file.xlsm
Normal file
BIN
template/template-achievement-file.xlsm
Normal file
Binary file not shown.
BIN
template/template-defense-oral-paper.docx
Normal file
BIN
template/template-defense-oral-paper.docx
Normal file
Binary file not shown.
BIN
template/template-defense-oral-student.xlsm
Normal file
BIN
template/template-defense-oral-student.xlsm
Normal file
Binary file not shown.
BIN
template/template-defense-paper-paper.docx
Normal file
BIN
template/template-defense-paper-paper.docx
Normal file
Binary file not shown.
BIN
template/template-defense-paper-questions.xlsm
Normal file
BIN
template/template-defense-paper-questions.xlsm
Normal file
Binary file not shown.
BIN
template/template-defense-paper-student-1.xlsm
Normal file
BIN
template/template-defense-paper-student-1.xlsm
Normal file
Binary file not shown.
BIN
template/template-defense-paper-student-2.xlsm
Normal file
BIN
template/template-defense-paper-student-2.xlsm
Normal file
Binary file not shown.
BIN
template/template-pick-student.xlsm
Normal file
BIN
template/template-pick-student.xlsm
Normal file
Binary file not shown.
Binary file not shown.
109
toolbox/config/achievement.default.excel.json
Normal file
109
toolbox/config/achievement.default.excel.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"name": "achievement",
|
||||
"version": "9.0",
|
||||
"compatibleVersion": [
|
||||
"9.0"
|
||||
],
|
||||
"config": [
|
||||
{
|
||||
"name": "version",
|
||||
"position": "H1",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "class_full_name",
|
||||
"position": "D10",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "course_name",
|
||||
"position": "D5",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "teacher_name",
|
||||
"position": "D7",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "master_name",
|
||||
"position": "D8",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "class_single_name",
|
||||
"position": "K",
|
||||
"type": "range",
|
||||
"start": 2,
|
||||
"end": 5
|
||||
},
|
||||
{
|
||||
"name": "class_single_number",
|
||||
"position": "M",
|
||||
"type": "range",
|
||||
"start": 2,
|
||||
"end": 5
|
||||
},
|
||||
{
|
||||
"name": "kpi_number",
|
||||
"position": "H8",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "hml",
|
||||
"position": "H",
|
||||
"type": "range",
|
||||
"start": 22,
|
||||
"end": null
|
||||
},
|
||||
{
|
||||
"name": "hml_goal",
|
||||
"position": "I",
|
||||
"type": "range",
|
||||
"start": 22,
|
||||
"end": null
|
||||
},
|
||||
{
|
||||
"name": "hml_indicate",
|
||||
"position": "Q",
|
||||
"type": "range",
|
||||
"start": 22,
|
||||
"end": null
|
||||
},
|
||||
{
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
toolbox/config/override.excel.json
Normal file
0
toolbox/config/override.excel.json
Normal file
112
toolbox/models/config.py
Normal file
112
toolbox/models/config.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from os import PathLike
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BaseExcelConfig(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def position(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class RangeExcelConfigMixin(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def start(self) -> int:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def end(self) -> Optional[int]:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def fposition(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class SingleExcelConfigItem(BaseExcelConfig):
|
||||
|
||||
def __init__(self, name: str, position: str):
|
||||
self._name = name
|
||||
self._position = position
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def position(self) -> str:
|
||||
return self._position
|
||||
|
||||
|
||||
class RangeExcelConfigItem(SingleExcelConfigItem, RangeExcelConfigMixin):
|
||||
|
||||
def __init__(self, name: str, position: str, start: int, end: Optional[int] = None):
|
||||
super().__init__(name, position)
|
||||
self._start = start
|
||||
self._end = end
|
||||
|
||||
@property
|
||||
def start(self) -> int:
|
||||
return self._start
|
||||
|
||||
@property
|
||||
def end(self) -> Optional[int]:
|
||||
return self._end
|
||||
|
||||
@property
|
||||
def fposition(self) -> str:
|
||||
return self.position + '{}'
|
||||
|
||||
|
||||
class AESConfig:
|
||||
_config_list: list[SingleExcelConfigItem | RangeExcelConfigItem]
|
||||
|
||||
def __init__(self, file_path: str | PathLike):
|
||||
self._file_path = file_path
|
||||
self._config_list = []
|
||||
self._init_config()
|
||||
|
||||
def __getitem__(self, item: str):
|
||||
return self.get_config(item)
|
||||
|
||||
def _init_config(self):
|
||||
with open(self._file_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
f.close()
|
||||
|
||||
for item in config['config']:
|
||||
itype = item.pop('type', None)
|
||||
if itype == 'range':
|
||||
self._config_list.append(RangeExcelConfigItem(**item))
|
||||
elif itype == 'single':
|
||||
self._config_list.append(SingleExcelConfigItem(**item))
|
||||
|
||||
def get_config(self, name: str) -> Optional[RangeExcelConfigItem | SingleExcelConfigItem]:
|
||||
for config in self._config_list:
|
||||
if config.name == name:
|
||||
return config
|
||||
return None
|
||||
32
toolbox/models/data_model.py
Normal file
32
toolbox/models/data_model.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClassInfo:
|
||||
full_name = ""
|
||||
|
||||
class_name: str
|
||||
class_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class CourseInfo:
|
||||
course_name: str
|
||||
# 任课教师
|
||||
course_teacher_name: str
|
||||
# 课程负责人
|
||||
course_master_name: str
|
||||
119
toolbox/services/excel_service.py
Normal file
119
toolbox/services/excel_service.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from os import PathLike
|
||||
from typing import Optional
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.workbook import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
from toolbox.models.data_model import ClassInfo
|
||||
from toolbox.models.config import AESConfig
|
||||
|
||||
|
||||
class BaseExcelService(ABC):
|
||||
_file_path: str | PathLike
|
||||
_workbook: Optional[Workbook]
|
||||
_sheet: None
|
||||
|
||||
@abstractmethod
|
||||
def open(self, *args, **kwargs) -> 'BaseExcelService':
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def save(self) -> 'BaseExcelService':
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def active_sheet(self, sheet_name: str) -> 'BaseExcelService':
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def cur_active_sheet(self) -> Worksheet:
|
||||
...
|
||||
|
||||
|
||||
class ExcelService(BaseExcelService):
|
||||
def __init__(self, file_path: str | PathLike):
|
||||
self._file_path = file_path
|
||||
self._workbook = None
|
||||
self._sheet = None
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
self._workbook = openpyxl.load_workbook(self._file_path, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def save(self):
|
||||
self._workbook.save(self._file_path)
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self._workbook.close()
|
||||
|
||||
def active_sheet(self, sheet_name: str):
|
||||
self._sheet = self._workbook[sheet_name]
|
||||
return self
|
||||
|
||||
@property
|
||||
def cur_active_sheet(self):
|
||||
return self._sheet
|
||||
|
||||
def load_value(self, cell: str):
|
||||
if self._sheet is None:
|
||||
raise ValueError("No active sheet. Please set an active sheet first.")
|
||||
return self._sheet[cell].value
|
||||
|
||||
|
||||
class AchievementExcelService(ExcelService):
|
||||
version = ''
|
||||
config = {}
|
||||
|
||||
def __init__(self, file_path: str | PathLike):
|
||||
super().__init__(file_path)
|
||||
self.open(read_only=True, data_only=True)
|
||||
|
||||
def load_config(self, config_path: str | PathLike):
|
||||
self.config = AESConfig(config_path)
|
||||
|
||||
def read_class_info(self) -> list[ClassInfo]:
|
||||
lst = []
|
||||
self.active_sheet('初始录入')
|
||||
full_name = self.load_value(self.config['class_full_name'].position)
|
||||
|
||||
for i in range(self.config['class_single_name'].start, self.config['class_single_name'].end):
|
||||
name = self.load_value(self.config['class_single_name'].fposition.format(i))
|
||||
number = self.load_value(self.config['class_single_number'].fposition.format(i))
|
||||
|
||||
if name is None or number is None:
|
||||
break
|
||||
|
||||
ci = ClassInfo(name, number)
|
||||
ci.full_name = full_name
|
||||
lst.append(ci)
|
||||
|
||||
if len(lst) == 0:
|
||||
raise ValueError("No class information found in the Excel file.")
|
||||
|
||||
return lst
|
||||
|
||||
def read_course_info(self):
|
||||
...
|
||||
19
toolbox/tests/__init__.py
Normal file
19
toolbox/tests/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
TEST_FILE_PATH = Path(os.path.join(PACKAGE_DIR, 'files'))
|
||||
41
toolbox/tests/test_config_model.py
Normal file
41
toolbox/tests/test_config_model.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
from toolbox.models.config import AESConfig, SingleExcelConfigItem, RangeExcelConfigItem
|
||||
from toolbox.tests import TEST_FILE_PATH
|
||||
|
||||
|
||||
def test_config_model():
|
||||
aesc = AESConfig(TEST_FILE_PATH / 'test_config_model_01.json')
|
||||
|
||||
a = aesc.get_config('A')
|
||||
assert isinstance(a, SingleExcelConfigItem)
|
||||
assert a.position == 'H1'
|
||||
|
||||
b = aesc.get_config('B')
|
||||
assert isinstance(b, SingleExcelConfigItem)
|
||||
assert b.position == 'D10'
|
||||
|
||||
c = aesc.get_config('C')
|
||||
assert isinstance(c, RangeExcelConfigItem)
|
||||
assert c.position == 'K'
|
||||
assert c.fposition.format(1) == 'K1'
|
||||
assert c.start == 2
|
||||
assert c.end == 5
|
||||
|
||||
d = aesc.get_config('D')
|
||||
assert isinstance(d, RangeExcelConfigItem)
|
||||
assert d.position == 'Q'
|
||||
assert d.fposition.format(d.start) == 'Q22'
|
||||
assert d.end is None
|
||||
79
toolbox/tests/test_excel_services.py
Normal file
79
toolbox/tests/test_excel_services.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from openpyxl.workbook import Workbook
|
||||
|
||||
from toolbox.services.excel_service import ExcelService, AchievementExcelService
|
||||
from toolbox.tests import TEST_FILE_PATH
|
||||
|
||||
SAVE_TEMP_FILE = False
|
||||
|
||||
|
||||
class TestExcelService:
|
||||
es = ExcelService(TEST_FILE_PATH / 'test_excel_services_01.xlsx')
|
||||
es.open(data_only=True)
|
||||
|
||||
def test_open(self):
|
||||
assert isinstance(self.es._workbook, Workbook)
|
||||
assert self.es._sheet is None
|
||||
|
||||
def test_open_failed(self):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
ExcelService(TEST_FILE_PATH / 'non_existent_file.xlsx').open()
|
||||
|
||||
def test_active_sheet(self):
|
||||
self.es.active_sheet('Sheet1')
|
||||
assert self.es._sheet.title == 'Sheet1'
|
||||
self.es.active_sheet('Sheet2')
|
||||
assert self.es._sheet.title == 'Sheet2'
|
||||
|
||||
def test_cur_active_sheet(self):
|
||||
self.es.active_sheet('Sheet1')
|
||||
assert self.es.cur_active_sheet.title == 'Sheet1'
|
||||
self.es.active_sheet('Sheet2')
|
||||
assert self.es.cur_active_sheet.title == 'Sheet2'
|
||||
|
||||
def test_save_and_close(self):
|
||||
temp_excel_file = TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx'
|
||||
shutil.copy(self.es._file_path, temp_excel_file)
|
||||
|
||||
self.es.active_sheet('Sheet1').cur_active_sheet['A1'] = 'Modified'
|
||||
self.es.save().close()
|
||||
|
||||
es2 = ExcelService(temp_excel_file).open().active_sheet('Sheet1')
|
||||
assert es2.cur_active_sheet['A1'].value == 'Modified'
|
||||
es2.close()
|
||||
|
||||
if not SAVE_TEMP_FILE:
|
||||
Path(TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx').unlink()
|
||||
|
||||
|
||||
class TestAchievementExcelService:
|
||||
aes = AchievementExcelService(TEST_FILE_PATH / 'test_achievement_excel_service_01.xlsm')
|
||||
aes.load_config(TEST_FILE_PATH / 'test_achievement.default.excel_01.json')
|
||||
|
||||
def test_read_class_info(self):
|
||||
cis = self.aes.read_class_info()
|
||||
assert len(cis) == 2
|
||||
assert cis[0].full_name == '22工程管理(1)(2)'
|
||||
assert cis[1].full_name == '22工程管理(1)(2)'
|
||||
|
||||
assert cis[0].class_name == '22工程管理(1)'
|
||||
assert cis[0].class_number == 34
|
||||
assert cis[1].class_name == '22工程管理(2)'
|
||||
assert cis[1].class_number == 36
|
||||
@@ -1,2 +1,17 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
MAIN_THEME_COLOR = "#0064b0"
|
||||
BLUE_BACKGROUND_COLOR = "#dbeafe"
|
||||
|
||||
16
ui/components/__init__.py
Normal file
16
ui/components/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition, InfoBarIcon, FluentIconBase, ProgressBar, IndeterminateProgressBar, \
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
from PySide6.QtWidgets import QFrame
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
from PySide6.QtCore import QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QLayout
|
||||
from qfluentwidgets import DisplayLabel, LargeTitleLabel, GroupHeaderCardWidget, FluentIconBase, CardGroupWidget
|
||||
|
||||
from module.picker.schema import PickerStudent
|
||||
|
||||
|
||||
class Widget(QFrame):
|
||||
@@ -7,3 +30,91 @@ class Widget(QFrame):
|
||||
super().__init__(parent=parent)
|
||||
# 必须给子界面设置全局唯一的对象名
|
||||
self.setObjectName(key.replace(' ', '-'))
|
||||
|
||||
|
||||
class RollingTextWidget(QWidget):
|
||||
finishSignal = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_index = 0
|
||||
self.items = []
|
||||
|
||||
self.soLabel = LargeTitleLabel("", self)
|
||||
self.nameLabel = DisplayLabel("", self)
|
||||
self.soLabel.setAlignment(Qt.AlignCenter)
|
||||
self.nameLabel.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(self.soLabel)
|
||||
self.layout.addWidget(self.nameLabel)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.rolling_timer = QTimer(self)
|
||||
self.rolling_timer.setInterval(50) # 滚动速度(毫秒)
|
||||
self.rolling_timer.timeout.connect(self.update_text)
|
||||
|
||||
self.stop_timer = QTimer(self)
|
||||
self.stop_timer.setSingleShot(True)
|
||||
self.stop_timer.timeout.connect(self.stop_rolling)
|
||||
|
||||
def update_text(self):
|
||||
# 每次显示下一个字符
|
||||
self.current_index = (self.current_index + 1) % len(self.items)
|
||||
stu = self.items[self.current_index]
|
||||
self.soLabel.setText(stu.so)
|
||||
self.nameLabel.setText(stu.name)
|
||||
|
||||
def start_rolling(self):
|
||||
if not self.rolling_timer.isActive():
|
||||
self.rolling_timer.start()
|
||||
self.stop_timer.start(2000) # 2秒后停止滚动
|
||||
|
||||
def stop_rolling(self):
|
||||
self.rolling_timer.stop()
|
||||
self.finishSignal.emit()
|
||||
|
||||
def set_items(self, items: list[PickerStudent]):
|
||||
self.items = items[:]
|
||||
random.shuffle(self.items)
|
||||
|
||||
def show_result(self, student: PickerStudent):
|
||||
self.soLabel.setText(student.so)
|
||||
self.nameLabel.setText(student.name)
|
||||
|
||||
def clear_text(self):
|
||||
self.soLabel.clear()
|
||||
self.nameLabel.clear()
|
||||
|
||||
|
||||
class MyCardGroupWidget(CardGroupWidget):
|
||||
def addLayout(self, layout: QLayout, stretch=0):
|
||||
self.hBoxLayout.addLayout(layout, stretch=stretch)
|
||||
|
||||
|
||||
class MyGroupHeaderCardWidget(GroupHeaderCardWidget):
|
||||
def addGroup(self, icon: Union[str, FluentIconBase, QIcon], title: str, content: str,
|
||||
object: Union[QWidget, QLayout],
|
||||
stretch=0) -> CardGroupWidget:
|
||||
group = MyCardGroupWidget(icon, title, content, self)
|
||||
|
||||
if isinstance(object, QWidget):
|
||||
group.addWidget(object, stretch=stretch)
|
||||
elif isinstance(object, QLayout):
|
||||
group.addLayout(object, stretch=stretch)
|
||||
|
||||
if self.groupWidgets:
|
||||
self.groupWidgets[-1].setSeparatorVisible(True)
|
||||
|
||||
self.groupLayout.addWidget(group)
|
||||
self.groupWidgets.append(group)
|
||||
return group
|
||||
|
||||
|
||||
class NotImplementedWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.label = DisplayLabel("🚧", self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.layout.addWidget(self.label)
|
||||
|
||||
31
ui/components/window.py
Normal file
31
ui/components/window.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtWidgets import QHBoxLayout
|
||||
from qfluentwidgets import FluentTitleBar
|
||||
from qfluentwidgets.window.fluent_window import FluentWindowBase
|
||||
|
||||
|
||||
class MyWindow(FluentWindowBase):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitleBar(FluentTitleBar(self))
|
||||
|
||||
self.widgetLayout = QHBoxLayout(self)
|
||||
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
|
||||
self.hBoxLayout.addLayout(self.widgetLayout)
|
||||
|
||||
self.titleBar.raise_()
|
||||
self.showMaximized()
|
||||
122
ui/main.py
122
ui/main.py
@@ -1,3 +1,22 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from PySide6.QtGui import QIcon, QShowEvent
|
||||
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor
|
||||
|
||||
@@ -7,7 +26,17 @@ from ui.pyui.achievement_ui import AchievementWidget
|
||||
from ui.pyui.defense_ui import DefenseWidget
|
||||
from ui.pyui.picker_ui import PickerWidget
|
||||
from ui.pyui.test_ui import TestWidget
|
||||
from utils.function import DEVELOPMENT_ENV
|
||||
from utils.function import RELEASE_ENV
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InterfaceSpec:
|
||||
key: str
|
||||
factory: Callable[[], object]
|
||||
icon: FluentIcon
|
||||
nav_text: str
|
||||
position: NavigationItemPosition = NavigationItemPosition.TOP
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class MainWindow(MSFluentWindow):
|
||||
@@ -16,28 +45,78 @@ class MainWindow(MSFluentWindow):
|
||||
|
||||
setThemeColor(MAIN_THEME_COLOR)
|
||||
self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR)
|
||||
|
||||
self.achievementInterface = AchievementWidget('Achievement Interface', self)
|
||||
self.defenseInterface = DefenseWidget('Defense Interface', self)
|
||||
self.aboutInterface = AboutWidget('About Interface', self)
|
||||
if not DEVELOPMENT_ENV:
|
||||
self.pickerInterface = PickerWidget('Picker Interface', self)
|
||||
self.testInterface = TestWidget('Test Interface', self)
|
||||
|
||||
self.achievementInterface.error.connect(self.showError)
|
||||
self.defenseInterface.errorSignal.connect(self.showError)
|
||||
self.interface_specs = self.build_interface_specs()
|
||||
self.interfaces = self.create_interfaces(self.interface_specs)
|
||||
self.bind_error_handlers()
|
||||
|
||||
self.initNavigation()
|
||||
self.initWindow()
|
||||
|
||||
def initNavigation(self):
|
||||
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
|
||||
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩')
|
||||
if not DEVELOPMENT_ENV:
|
||||
self.addSubInterface(self.pickerInterface, FluentIcon.PEOPLE, '抽答')
|
||||
self.addSubInterface(self.testInterface, FluentIcon.VIEW, '测试')
|
||||
def build_interface_specs(self) -> list[InterfaceSpec]:
|
||||
return [
|
||||
InterfaceSpec(
|
||||
key="achievement",
|
||||
factory=lambda: AchievementWidget('Achievement Interface', self),
|
||||
icon=FluentIcon.SPEED_HIGH,
|
||||
nav_text='达成度',
|
||||
enabled=True,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="defense",
|
||||
factory=lambda: DefenseWidget('Defense Interface', self),
|
||||
icon=FluentIcon.FEEDBACK,
|
||||
nav_text='答辩',
|
||||
enabled=True,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="picker",
|
||||
factory=lambda: PickerWidget('Picker Interface', self),
|
||||
icon=FluentIcon.PEOPLE,
|
||||
nav_text='提问',
|
||||
enabled=not RELEASE_ENV,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="test",
|
||||
factory=lambda: TestWidget('Test Interface', self),
|
||||
icon=FluentIcon.VIEW,
|
||||
nav_text='测试',
|
||||
enabled=not RELEASE_ENV,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="about",
|
||||
factory=lambda: AboutWidget('About Interface', self),
|
||||
icon=FluentIcon.INFO,
|
||||
nav_text='关于',
|
||||
position=NavigationItemPosition.BOTTOM,
|
||||
enabled=True
|
||||
),
|
||||
]
|
||||
|
||||
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
|
||||
def create_interfaces(self, specs: list[InterfaceSpec]) -> dict[str, object]:
|
||||
interfaces: dict[str, object] = {}
|
||||
for spec in specs:
|
||||
if not spec.enabled:
|
||||
continue
|
||||
widget = spec.factory()
|
||||
interfaces[spec.key] = widget
|
||||
setattr(self, f"{spec.key}Interface", widget)
|
||||
return interfaces
|
||||
|
||||
def bind_error_handlers(self):
|
||||
achievement = self.interfaces.get("achievement")
|
||||
defense = self.interfaces.get("defense")
|
||||
|
||||
if achievement and hasattr(achievement, "error"):
|
||||
achievement.error.connect(self.showError)
|
||||
if defense and hasattr(defense, "errorSignal"):
|
||||
defense.errorSignal.connect(self.showError)
|
||||
|
||||
def initNavigation(self):
|
||||
for spec in self.interface_specs:
|
||||
widget = self.interfaces.get(spec.key)
|
||||
if not widget:
|
||||
continue
|
||||
self.addSubInterface(widget, spec.icon, spec.nav_text, position=spec.position)
|
||||
|
||||
def initWindow(self):
|
||||
self.resize(900, 700)
|
||||
@@ -45,11 +124,14 @@ class MainWindow(MSFluentWindow):
|
||||
self.setWindowIcon(QIcon(':/images/logo.png'))
|
||||
|
||||
def showError(self, title: str, message: str):
|
||||
MessageBox(title, message, self).exec()
|
||||
box = MessageBox(title, message, self)
|
||||
box.yesButton.setText("关闭")
|
||||
box.cancelButton.hide()
|
||||
box.exec()
|
||||
|
||||
def showEvent(self, event: QShowEvent):
|
||||
super().showEvent(event)
|
||||
if DEVELOPMENT_ENV:
|
||||
if RELEASE_ENV:
|
||||
import pyi_splash
|
||||
pyi_splash.update_text('正在加载...')
|
||||
pyi_splash.close()
|
||||
|
||||
@@ -1,14 +1,121 @@
|
||||
from PySide6.QtGui import QDesktopServices, Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout
|
||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon, GroupHeaderCardWidget, PushButton, ImageLabel, TitleLabel
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
from PySide6.QtGui import QDesktopServices, Qt, QColor
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||
from qfluentwidgets import FluentIcon, GroupHeaderCardWidget, PushButton, ImageLabel, \
|
||||
TitleLabel, HeaderCardWidget, BodyLabel, HyperlinkLabel, SingleDirectionScrollArea
|
||||
from qfluentwidgets.components.widgets.card_widget import CardSeparator
|
||||
|
||||
from module.about.schema import ThirdParty
|
||||
from ui.components.widget import Widget
|
||||
from utils.function import DEVELOPMENT_ENV
|
||||
from utils.function import RELEASE_ENV
|
||||
|
||||
if RELEASE_ENV:
|
||||
from build_info import *
|
||||
|
||||
|
||||
class AboutWidget(Widget):
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
class AboutCard(HeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle('关于本程序')
|
||||
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.lineHBoxLayout = QHBoxLayout(self)
|
||||
self.lineHBoxLayout.setContentsMargins(24, 16, 24, 8)
|
||||
self.lineVBoxLayout = QVBoxLayout(self)
|
||||
self.lineVBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.textVBoxLayout = QVBoxLayout(self)
|
||||
self.textVBoxLayout.setContentsMargins(24, 8, 24, 16)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gplv3Image = ImageLabel(':/images/gplv3.png')
|
||||
self.lineHBoxLayout.addLayout(self.lineVBoxLayout)
|
||||
self.lineHBoxLayout.addWidget(self.gplv3Image)
|
||||
|
||||
self.addLine('程序名称', '教学工具箱')
|
||||
if RELEASE_ENV:
|
||||
self.addLine('程序版本', '1.0.0#' + GIT_HASH)
|
||||
self.addLine('作者', '许方杰')
|
||||
if RELEASE_ENV:
|
||||
self.addLine('构建时间', BUILD_TIME)
|
||||
self.addLine('许可证', 'GNU 通用公共许可证 第三版(GPLv3)')
|
||||
self.addLineUseLink('项目主页', 'https://cantyonion.site/git/cantyonion/JITToolBox')
|
||||
|
||||
self.addText(
|
||||
'教学工具箱是自由软件;您可以依据自由软件基金会发布的 GNU 通用公共许可证第三版条款,重新发布或修改它;许可证应使用第三版或(按您的选择)任何其更新的版本。')
|
||||
self.addText(
|
||||
'教学工具箱是以希望它有用为目的而发布的,但不附带任何担保;甚至没有适销性或特定用途适用性的隐含担保。请参看 GNU GPL 第三版了解更详细的内容。')
|
||||
self.addTextWithLink('您应该已收到一份 GNU 通用公共许可证的副本;如果没有,请查看<',
|
||||
'https://www.gnu.org/licenses/gpl-3.0.html')
|
||||
|
||||
self.vBoxLayout.addLayout(self.lineHBoxLayout)
|
||||
self.vBoxLayout.addWidget(CardSeparator(self))
|
||||
self.vBoxLayout.addLayout(self.textVBoxLayout)
|
||||
self.viewLayout.addLayout(self.vBoxLayout)
|
||||
|
||||
def addLine(self, title: str, content: str):
|
||||
hBox = QHBoxLayout(self)
|
||||
mTitlte = BodyLabel(title, self)
|
||||
mContent = BodyLabel(content, self)
|
||||
mContent.setTextColor(QColor(96, 96, 96), QColor(206, 206, 206))
|
||||
mTitlte.setFixedWidth(100)
|
||||
|
||||
hBox.addWidget(mTitlte)
|
||||
hBox.addWidget(mContent)
|
||||
|
||||
self.lineVBoxLayout.addLayout(hBox)
|
||||
|
||||
def addLineUseLink(self, title: str, content: str):
|
||||
hBox = QHBoxLayout(self)
|
||||
mTitle = BodyLabel(title, self)
|
||||
mContent = HyperlinkLabel(content, content)
|
||||
mTitle.setFixedWidth(100)
|
||||
|
||||
hBox.addWidget(mTitle)
|
||||
hBox.addWidget(mContent)
|
||||
|
||||
self.lineVBoxLayout.addLayout(hBox)
|
||||
mContent.clicked.connect(lambda: QDesktopServices.openUrl(content))
|
||||
|
||||
def addText(self, text: str):
|
||||
label = BodyLabel(text, self)
|
||||
label.setWordWrap(True)
|
||||
self.textVBoxLayout.addWidget(label)
|
||||
|
||||
def addLink(self, text: str, url: str):
|
||||
link = HyperlinkLabel(url, text)
|
||||
link.setUrl(url)
|
||||
self.textVBoxLayout.addWidget(link)
|
||||
|
||||
def addTextWithLink(self, text: str, url: str):
|
||||
hBox = QHBoxLayout(self)
|
||||
label = BodyLabel(text, self)
|
||||
link = HyperlinkLabel(url, url)
|
||||
link.setContentsMargins(0, 0, 0, 0)
|
||||
hBox.addWidget(label)
|
||||
hBox.addWidget(link)
|
||||
hBox.addWidget(BodyLabel(">。", self))
|
||||
self.textVBoxLayout.addLayout(hBox)
|
||||
link.clicked.connect(lambda: QDesktopServices.openUrl(url))
|
||||
|
||||
|
||||
class AboutMain(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.logoImage = ImageLabel(':/images/logo.png')
|
||||
self.logoImage.scaledToHeight(100)
|
||||
@@ -18,48 +125,46 @@ class AboutWidget(Widget):
|
||||
self.hBox.addWidget(self.logoImage, 0, Qt.AlignLeft)
|
||||
self.hBox.addWidget(self.appNameLabel, 1, Qt.AlignLeft)
|
||||
|
||||
build_time_str = ''
|
||||
if DEVELOPMENT_ENV:
|
||||
from build_info import BUILD_TIME
|
||||
build_time_str = f",构建时间:{BUILD_TIME}"
|
||||
third_parties = [
|
||||
ThirdParty("PySide6", "https://qt.io", ":/images/3rd/qt.png"),
|
||||
ThirdParty("QFluentWidgets", "https://qfluentwidgets.com", ":/images/3rd/qfluentwidgets.png"),
|
||||
ThirdParty("openpyxl", "https://openpyxl.readthedocs.io/en/stable"),
|
||||
ThirdParty("python-docx", "https://github.com/python-openxml/python-docx"),
|
||||
ThirdParty("Matplotlib", "https://matplotlib.org", ":/images/3rd/matplotlib.svg"),
|
||||
ThirdParty("packaging", "https://github.com/pypa/packaging", ":/images/3rd/packaging.png"),
|
||||
ThirdParty("pywin32", "https://github.com/mhammond/pywin32")
|
||||
]
|
||||
third_parties.sort(key=lambda item: item.name.lower())
|
||||
|
||||
self.version_card = PrimaryPushSettingCard(
|
||||
text="获取源码",
|
||||
icon=FluentIcon.INFO,
|
||||
title="关于",
|
||||
content=f"作者:许方杰。当前版本:1.0.0{build_time_str}\n"
|
||||
f"本软件使用 GPLv3 开源协议进行分发,作者不对使用本软件造成的任何损失负责。"
|
||||
)
|
||||
self.button_list = [
|
||||
PushButton("访问网站"),
|
||||
PushButton("访问网站"),
|
||||
PushButton("访问网站"),
|
||||
PushButton("访问网站"),
|
||||
]
|
||||
self.url_list = [
|
||||
"https://qt.io",
|
||||
"https://qfluentwidgets.com",
|
||||
"https://openpyxl.readthedocs.io/en/stable",
|
||||
"https://github.com/python-openxml/python-docx"
|
||||
]
|
||||
self.group_card = GroupHeaderCardWidget(self)
|
||||
self.group_card.setTitle("第三方框架")
|
||||
self.vbox = QVBoxLayout(self)
|
||||
|
||||
self.vbox.addLayout(self.hBox)
|
||||
self.vbox.addWidget(self.version_card)
|
||||
self.vbox.addWidget(AboutCard(self))
|
||||
self.vbox.addWidget(self.group_card)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
self.group_card.addGroup(":/images/3rd/qt.png", "PySide6", self.url_list[0], self.button_list[0])
|
||||
self.group_card.addGroup(":/images/3rd/qfluentwidgets", "QFluentWidgets", self.url_list[1], self.button_list[1])
|
||||
self.group_card.addGroup(FluentIcon.LAYOUT, "openpyxl", self.url_list[2], self.button_list[2])
|
||||
self.group_card.addGroup(FluentIcon.LAYOUT, "python-docx", self.url_list[3], self.button_list[3])
|
||||
[self.addThirdParty(x) for x in third_parties]
|
||||
|
||||
self.version_card.clicked.connect(
|
||||
lambda: QDesktopServices.openUrl("https://cantyonion.site/git/cantyonion/DefenseTopicGenerator")
|
||||
)
|
||||
self.button_list[0].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[0]))
|
||||
self.button_list[1].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[1]))
|
||||
self.button_list[2].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[2]))
|
||||
self.button_list[3].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[3]))
|
||||
def addThirdParty(self, third_party: ThirdParty):
|
||||
button = PushButton(FluentIcon.LINK, "访问网站")
|
||||
button.setFixedWidth(120)
|
||||
self.group_card.addGroup(third_party.qrc if third_party.qrc else FluentIcon.LAYOUT, third_party.name,
|
||||
third_party.url, button)
|
||||
button.clicked.connect(lambda: QDesktopServices.openUrl(third_party.url))
|
||||
|
||||
|
||||
class AboutWidget(Widget):
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.scrollArea = SingleDirectionScrollArea(orient=Qt.Vertical)
|
||||
self.scrollArea.setWidget(AboutMain(self))
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollArea.enableTransparentBackground()
|
||||
|
||||
self.vBox = QVBoxLayout(self)
|
||||
self.vBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.vBox.setSpacing(0)
|
||||
self.vBox.addWidget(self.scrollArea)
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Callable, Literal
|
||||
@@ -5,16 +20,17 @@ from typing import Callable, Literal
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout
|
||||
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \
|
||||
PrimaryPushButton, SwitchButton
|
||||
PrimaryPushButton, SwitchButton, HyperlinkButton, InfoBar, InfoBarPosition
|
||||
|
||||
from module import LOGLEVEL
|
||||
from module.worker import ARGWorker
|
||||
from ui.components.infobar import ProgressInfoBar
|
||||
from ui.components.widget import Widget
|
||||
from ui import MAIN_THEME_COLOR
|
||||
from ui.components.infobar import ProgressInfoBar
|
||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget
|
||||
from utils.function import open_template
|
||||
|
||||
|
||||
class InputSettingCard(GroupHeaderCardWidget):
|
||||
class InputSettingCard(MyGroupHeaderCardWidget):
|
||||
chooseSignal = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -22,15 +38,20 @@ class InputSettingCard(GroupHeaderCardWidget):
|
||||
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
self.btnHBoxLayout = QHBoxLayout(self)
|
||||
|
||||
self.openTemplateButton = HyperlinkButton("", "模板下载")
|
||||
self.chooseFileButton = PushButton("打开")
|
||||
|
||||
self.chooseFileButton.setFixedWidth(120)
|
||||
self.btnHBoxLayout.addWidget(self.openTemplateButton)
|
||||
self.btnHBoxLayout.addWidget(self.chooseFileButton)
|
||||
|
||||
self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.chooseFileButton)
|
||||
self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.btnHBoxLayout)
|
||||
|
||||
# ============================
|
||||
self.chooseFileButton.clicked.connect(self.choose_file)
|
||||
self.openTemplateButton.clicked.connect(lambda: open_template('template-achievement-file.xlsm', self))
|
||||
|
||||
def choose_file(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);")
|
||||
@@ -238,3 +259,23 @@ class AchievementWidget(Widget):
|
||||
def show_info(self, content: str, level: str):
|
||||
if level == LOGLEVEL.INFO:
|
||||
self.pib.set_title(content)
|
||||
elif level == LOGLEVEL.WARNING:
|
||||
InfoBar.warning(
|
||||
title='提示',
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=5000,
|
||||
parent=self
|
||||
)
|
||||
elif level == LOGLEVEL.ERROR:
|
||||
InfoBar.error(
|
||||
title='错误',
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
|
||||
@@ -1,18 +1,60 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from functools import wraps
|
||||
from typing import Literal, Callable
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog, QButtonGroup, QWidget
|
||||
from PySide6.QtCore import Qt, Signal, QThread, QEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog, QButtonGroup, QWidget, QApplication, QStackedWidget
|
||||
from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, BodyLabel, PrimaryPushButton, FluentIcon, \
|
||||
LineEdit, RadioButton
|
||||
LineEdit, RadioButton, HyperlinkButton, FlyoutViewBase, TeachingTip, TeachingTipTailPosition, SegmentedWidget, \
|
||||
SimpleCardWidget, DisplayLabel
|
||||
|
||||
from module.worker import DTGWorker
|
||||
from ui.components.infobar import ProgressInfoBar
|
||||
from ui.components.widget import Widget
|
||||
from ui import MAIN_THEME_COLOR
|
||||
from ui.components.infobar import ProgressInfoBar
|
||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget, NotImplementedWidget
|
||||
from ui.pyui.sub.defense import ODModeExportSettings, ODModeSettings
|
||||
from utils.function import open_template
|
||||
|
||||
|
||||
class InitSettingCard(GroupHeaderCardWidget):
|
||||
class ChooseTemplateView(FlyoutViewBase):
|
||||
closed = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
QApplication.instance().installEventFilter(self)
|
||||
|
||||
def paintEvent(self, e):
|
||||
...
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if event.type() == QEvent.MouseButtonPress:
|
||||
if not self.rect().contains(self.mapFromGlobal(event.globalPosition().toPoint())):
|
||||
self.closed.emit()
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def addTemplate(self, content: str, cb: Callable[[], None]):
|
||||
label = HyperlinkButton("", content)
|
||||
self.vBoxLayout.addWidget(label)
|
||||
label.clicked.connect(cb)
|
||||
label.clicked.connect(self.closed.emit)
|
||||
|
||||
|
||||
class InitSettingCard(MyGroupHeaderCardWidget):
|
||||
chooseSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -21,19 +63,29 @@ class InitSettingCard(GroupHeaderCardWidget):
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.sBtnHBoxLayout = QHBoxLayout(self)
|
||||
self.qBtnHBoxLayout = QHBoxLayout(self)
|
||||
self.sTemplateButton = HyperlinkButton("", "模板下载")
|
||||
self.chooseStudentButton = PushButton("打开")
|
||||
self.qTemplateButton = HyperlinkButton("", "模板下载")
|
||||
self.chooseQuestionButton = PushButton("打开")
|
||||
|
||||
self.chooseStudentButton.setFixedWidth(120)
|
||||
self.chooseQuestionButton.setFixedWidth(120)
|
||||
self.sBtnHBoxLayout.addWidget(self.sTemplateButton)
|
||||
self.sBtnHBoxLayout.addWidget(self.chooseStudentButton)
|
||||
self.qBtnHBoxLayout.addWidget(self.qTemplateButton)
|
||||
self.qBtnHBoxLayout.addWidget(self.chooseQuestionButton)
|
||||
|
||||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单文件", self.chooseStudentButton)
|
||||
self.QueGroup = self.addGroup(FluentIcon.DOCUMENT, "题库", "选择题库文件", self.chooseQuestionButton)
|
||||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单文件", self.sBtnHBoxLayout)
|
||||
self.QueGroup = self.addGroup(FluentIcon.DOCUMENT, "题库", "选择题库文件", self.qBtnHBoxLayout)
|
||||
|
||||
self.chooseStudentButton.clicked.connect(
|
||||
lambda: self.choose_file(self.stuGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('s', x)))
|
||||
self.chooseQuestionButton.clicked.connect(
|
||||
lambda: self.choose_file(self.QueGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('q', x)))
|
||||
self.qTemplateButton.clicked.connect(lambda: open_template('template-defense-paper-questions.xlsm', self))
|
||||
self.sTemplateButton.clicked.connect(self.show_template_list_view)
|
||||
|
||||
def choose_file(
|
||||
self,
|
||||
@@ -47,6 +99,19 @@ class InitSettingCard(GroupHeaderCardWidget):
|
||||
if cb:
|
||||
cb(file_path)
|
||||
|
||||
def show_template_list_view(self):
|
||||
view = ChooseTemplateView(self)
|
||||
view.addTemplate("普通模板", lambda: open_template("template-defense-paper-student-1.xlsm", self))
|
||||
view.addTemplate("达成度模板", lambda: open_template("template-defense-paper-student-2.xlsm", self))
|
||||
w = TeachingTip.make(
|
||||
target=self.sTemplateButton,
|
||||
view=view,
|
||||
tailPosition=TeachingTipTailPosition.TOP,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
view.closed.connect(w.close)
|
||||
|
||||
|
||||
class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
startSignal = Signal()
|
||||
@@ -55,7 +120,7 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("输入选项")
|
||||
self.setTitle("输出选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.chooseExportDirectoryButton = PushButton("选择")
|
||||
@@ -71,7 +136,7 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
self.radioGroup.addButton(self.wordRadio)
|
||||
self.radioHbox.addWidget(self.pdfRadio)
|
||||
self.radioHbox.addWidget(self.wordRadio)
|
||||
self.pdfRadio.setChecked(True)
|
||||
self.wordRadio.setChecked(True)
|
||||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||||
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
||||
self.chooseExportDirectoryButton.setFixedWidth(120)
|
||||
@@ -95,7 +160,8 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
self.chooseExportDirectoryButton)
|
||||
self.fnGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件名", "输入导出文件的名称",
|
||||
self.exportFileNameLineEdit)
|
||||
self.exportFormatGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件格式", "选择导出文件的格式", self.radioWidget)
|
||||
self.exportFormatGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件格式", "选择导出文件的格式",
|
||||
self.radioWidget)
|
||||
self.exportFormatGroup.setSeparatorVisible(True)
|
||||
|
||||
self.vBoxLayout.addLayout(self.bottomLayout)
|
||||
@@ -120,15 +186,16 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
self.updateSignal.emit('n', f_name)
|
||||
|
||||
|
||||
class DefenseWidget(Widget):
|
||||
class DPMode(QWidget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.initCard = InitSettingCard(self)
|
||||
self.exportCard = ExportSettingsCard(self)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.vbox.addWidget(self.initCard)
|
||||
self.vbox.addWidget(self.exportCard)
|
||||
@@ -252,3 +319,53 @@ class DefenseWidget(Widget):
|
||||
|
||||
def show_error(self, title: str, content: str):
|
||||
self.errorSignal.emit(title, content)
|
||||
|
||||
|
||||
class DOMode(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.odSettings = ODModeSettings(self)
|
||||
self.odExportSettings = ODModeExportSettings(self)
|
||||
|
||||
self.vbox.addWidget(self.odSettings)
|
||||
self.vbox.addWidget(self.odExportSettings)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
|
||||
class DefenseWidget(Widget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.stack = QStackedWidget(self)
|
||||
self.menu = SegmentedWidget(self)
|
||||
self.dpMode = DPMode(self)
|
||||
self.doMode = NotImplementedWidget(self)
|
||||
|
||||
self.addSubInterface(self.dpMode, 'DPMode', '书面答辩')
|
||||
self.addSubInterface(self.doMode, 'DOMode', '口头答辩')
|
||||
|
||||
self.menu.setCurrentItem('DPMode')
|
||||
self.vbox.addWidget(self.menu)
|
||||
self.vbox.addWidget(self.stack)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
|
||||
widget.setObjectName(objectName)
|
||||
self.stack.addWidget(widget)
|
||||
|
||||
# 使用全局唯一的 objectName 作为路由键
|
||||
self.menu.addItem(
|
||||
routeKey=objectName,
|
||||
text=text,
|
||||
onClick=lambda: self.stack.setCurrentWidget(widget)
|
||||
)
|
||||
|
||||
def onCurrentIndexChanged(self, index):
|
||||
widget = self.stack.widget(index)
|
||||
self.menu.setCurrentItem(widget.objectName())
|
||||
|
||||
@@ -1,27 +1,58 @@
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout
|
||||
from qfluentwidgets import GroupHeaderCardWidget, PushButton, FluentIcon, PrimaryPushButton, IconWidget, BodyLabel
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QTimer
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QFileDialog
|
||||
from qfluentwidgets import PushButton, FluentIcon, PrimaryPushButton, IconWidget, BodyLabel, \
|
||||
SpinBox, HyperlinkButton
|
||||
|
||||
from module.picker.schema import PickerExcel, PickerStudent
|
||||
from ui import MAIN_THEME_COLOR
|
||||
from ui.components.widget import Widget
|
||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget
|
||||
from ui.pyui.sub.picker import PickStudentLabelUi
|
||||
from utils.function import open_template
|
||||
|
||||
|
||||
class PickerWidget(Widget):
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
class PickStudentMode(QWidget):
|
||||
errorSignal = Signal(str)
|
||||
|
||||
self.card = GroupHeaderCardWidget(self)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.card = MyGroupHeaderCardWidget(self)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.btnHBox = QHBoxLayout(self)
|
||||
|
||||
self.openTemplateBtn = HyperlinkButton("", "模板下载")
|
||||
self.chooseBtn = PushButton("打开")
|
||||
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "抽签")
|
||||
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
|
||||
self.bottomLayout = QHBoxLayout()
|
||||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||||
self.hintLabel = BodyLabel("点击抽签按钮以开始抽签 👉")
|
||||
self.hintLabel = BodyLabel("点击开始按钮以开始抽签 👉")
|
||||
self.spinbox = SpinBox()
|
||||
self.psui = PickStudentLabelUi(self)
|
||||
|
||||
self.card.setTitle("输入选项")
|
||||
self.chooseBtn.setFixedWidth(120)
|
||||
self.startButton.setFixedWidth(120)
|
||||
self.startButton.setEnabled(False)
|
||||
self.spinbox.setRange(0, 6)
|
||||
self.spinbox.setFixedWidth(120)
|
||||
self.spinbox.setEnabled(False)
|
||||
self.psui.hide()
|
||||
|
||||
self.hintIcon.setFixedSize(16, 16)
|
||||
self.hintIcon.autoFillBackground()
|
||||
@@ -32,10 +63,89 @@ class PickerWidget(Widget):
|
||||
self.bottomLayout.addStretch(1)
|
||||
self.bottomLayout.addWidget(self.startButton, 0, Qt.AlignRight)
|
||||
self.bottomLayout.setAlignment(Qt.AlignVCenter)
|
||||
self.btnHBox.addWidget(self.openTemplateBtn)
|
||||
self.btnHBox.addWidget(self.chooseBtn)
|
||||
|
||||
self.group = self.card.addGroup(FluentIcon.DOCUMENT, "抽答名单", "选择抽答名单", self.chooseBtn)
|
||||
self.group.setSeparatorVisible(True)
|
||||
self.group = self.card.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单", self.btnHBox)
|
||||
self.spinGroup = self.card.addGroup(FluentIcon.SETTING, "提问次数", "设置提问的最大次数", self.spinbox)
|
||||
self.spinGroup.setSeparatorVisible(True)
|
||||
self.card.vBoxLayout.addLayout(self.bottomLayout)
|
||||
|
||||
self.vbox.addWidget(self.card)
|
||||
self.vbox.addWidget(self.psui)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
# ==============================
|
||||
self.chooseBtn.clicked.connect(self.choose_file)
|
||||
self.spinbox.valueChanged.connect(lambda: PickerExcel.save_total_time(value=self.spinbox.value()))
|
||||
self.startButton.clicked.connect(self.start_rolling)
|
||||
self.psui.rollingText.finishSignal.connect(self.finish_rolling)
|
||||
self.openTemplateBtn.clicked.connect(lambda: open_template("template-pick-student.xlsm", self))
|
||||
# ==============================
|
||||
self.filepath = ""
|
||||
self.students = []
|
||||
|
||||
def choose_file(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);")
|
||||
if file_path:
|
||||
self.group.setContent("已选择文件:" + file_path)
|
||||
self.filepath = file_path
|
||||
self.startButton.setEnabled(True)
|
||||
self.init_spinbox_value()
|
||||
|
||||
def init_spinbox_value(self):
|
||||
if not self.filepath:
|
||||
return
|
||||
|
||||
try:
|
||||
PickerExcel.open(self.filepath)
|
||||
self.spinbox.setValue(PickerExcel.read_total_time())
|
||||
self.spinbox.setEnabled(True)
|
||||
except Exception as e:
|
||||
self.errorSignal.emit(str(e))
|
||||
self.spinbox.setEnabled(False)
|
||||
self.startButton.setEnabled(False)
|
||||
|
||||
def start_rolling(self):
|
||||
self.students = PickerExcel.read_student()
|
||||
self.psui.show()
|
||||
self.psui.rollingText.set_items(self.students)
|
||||
self.psui.rollingText.start_rolling()
|
||||
self.startButton.setEnabled(False)
|
||||
|
||||
def finish_rolling(self):
|
||||
stu = PickerStudent.pick(self.students)
|
||||
if not (stu.so and stu.name):
|
||||
self.errorSignal.emit("学生信息读取失败")
|
||||
self.psui.rollingText.show_result(stu)
|
||||
|
||||
timer = QTimer(self)
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(lambda: self.show_screen(stu))
|
||||
timer.start(1000)
|
||||
|
||||
def show_screen(self, stu: PickerStudent):
|
||||
self.psui.show_scoring()
|
||||
self.psui.scoring.submitSignal.connect(lambda score: self.scoring_finished(score, stu))
|
||||
|
||||
def scoring_finished(self, score: int, student: PickerStudent):
|
||||
student.append_score(score)
|
||||
PickerExcel.write_back(student)
|
||||
self.psui.hide()
|
||||
self.startButton.setEnabled(True)
|
||||
|
||||
|
||||
class PickerWidget(Widget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.psm = PickStudentMode(self)
|
||||
|
||||
self.vbox.addWidget(self.psm)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
# ===========================
|
||||
self.psm.errorSignal.connect(lambda n: self.errorSignal.emit("😢 不好出错了", n))
|
||||
|
||||
88
ui/pyui/sub/defense.py
Normal file
88
ui/pyui/sub/defense.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
from PySide6.QtWidgets import QHBoxLayout
|
||||
from qfluentwidgets import PushButton, HyperlinkButton, SpinBox, LineEdit, PrimaryPushButton, FluentIcon, \
|
||||
GroupHeaderCardWidget
|
||||
|
||||
from ui.components.widget import MyGroupHeaderCardWidget
|
||||
|
||||
|
||||
class ODModeSettings(MyGroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle('输入选项')
|
||||
|
||||
self.sBtn = PushButton('打开', self)
|
||||
self.sBtnTemplate = HyperlinkButton('', '模板下载')
|
||||
|
||||
self.qBtn = PushButton('打开', self)
|
||||
self.bBtnTemplate = HyperlinkButton('', '模板下载')
|
||||
|
||||
self.qNumber = SpinBox(self)
|
||||
self.qNumber.setRange(0, 999)
|
||||
|
||||
self.defenseNameLineEdit = LineEdit(self)
|
||||
self.defenseNameLineEdit.setPlaceholderText('输入答辩名称')
|
||||
|
||||
self.so = SpinBox(self)
|
||||
self.so.setRange(0, 999)
|
||||
|
||||
self.sname = LineEdit(self)
|
||||
self.sname.setPlaceholderText('姓名')
|
||||
|
||||
self.startBtn = PrimaryPushButton(FluentIcon.PLAY_SOLID, '开始')
|
||||
|
||||
self.init_layout()
|
||||
|
||||
def init_layout(self):
|
||||
self.sBtn.setFixedWidth(120)
|
||||
self.qBtn.setFixedWidth(120)
|
||||
self.defenseNameLineEdit.setFixedWidth(360)
|
||||
self.so.setFixedWidth(120)
|
||||
self.sname.setFixedWidth(120)
|
||||
self.qNumber.setFixedWidth(120)
|
||||
self.startBtn.setFixedWidth(120)
|
||||
|
||||
hbox1 = QHBoxLayout(self)
|
||||
hbox1.addWidget(self.sBtnTemplate)
|
||||
hbox1.addWidget(self.sBtn)
|
||||
hbox2 = QHBoxLayout(self)
|
||||
hbox2.addWidget(self.bBtnTemplate)
|
||||
hbox2.addWidget(self.qBtn)
|
||||
|
||||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, '学生名单', '选择学生名单文件', hbox1)
|
||||
self.qGroup = self.addGroup(FluentIcon.DOCUMENT, '题库', '选择题库文件', hbox2)
|
||||
self.addGroup(FluentIcon.SETTING, '题目数量', '输入题目数量', self.qNumber)
|
||||
self.addGroup(FluentIcon.SETTING, '答辩名称', '输入答辩名称', self.defenseNameLineEdit)
|
||||
self.addGroup(FluentIcon.SETTING, '答辩序号', '输入答辩序号', self.so)
|
||||
self.addGroup(FluentIcon.SETTING, '学生姓名', '输入学生姓名', self.sname)
|
||||
|
||||
|
||||
class ODModeExportSettings(GroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle('导出选项')
|
||||
self.exportFilename = LineEdit(self)
|
||||
self.exportBtn = PrimaryPushButton(FluentIcon.PLAY_SOLID, '导出')
|
||||
|
||||
self.init_layout()
|
||||
|
||||
def init_layout(self):
|
||||
self.exportFilename.setPlaceholderText('输入导出的文件名')
|
||||
self.exportFilename.setFixedWidth(360)
|
||||
self.exportBtn.setFixedWidth(120)
|
||||
|
||||
self.addGroup(FluentIcon.DOCUMENT, '导出文件名', '输入导出文件名', self.exportFilename)
|
||||
119
ui/pyui/sub/picker.py
Normal file
119
ui/pyui/sub/picker.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QApplication, QGridLayout
|
||||
from qfluentwidgets import PushButton, SpinBox, PrimaryPushButton, \
|
||||
BodyLabel, SimpleCardWidget
|
||||
from qfluentwidgets.components.widgets.card_widget import CardSeparator
|
||||
|
||||
from ui.components.widget import RollingTextWidget
|
||||
|
||||
|
||||
class QuickScoringKeyBoard(QWidget):
|
||||
scoringSignal = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.keyBoardLayout = QGridLayout()
|
||||
buttons = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
|
||||
|
||||
for i, num in enumerate(buttons):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = PushButton(str(num))
|
||||
btn.setFixedWidth(120)
|
||||
btn.clicked.connect(lambda _, n=num: self.scoringSignal.emit(n))
|
||||
self.keyBoardLayout.addWidget(btn, row, col)
|
||||
|
||||
self.vBoxLayout.addLayout(self.keyBoardLayout)
|
||||
|
||||
|
||||
class QuickScoring(QWidget):
|
||||
submitSignal = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.btnVBoxLayout = QVBoxLayout(self)
|
||||
self.spinboxLayout = QHBoxLayout(self)
|
||||
self.spinboxLabel = BodyLabel("得分", self)
|
||||
self.gradeSpinBox = SpinBox(self)
|
||||
self.submitButton = PrimaryPushButton("确定", self)
|
||||
self.resetButton = PushButton("重置", self)
|
||||
self.keyboard = QuickScoringKeyBoard(self)
|
||||
|
||||
self.gradeSpinBox.setRange(0, 100)
|
||||
self.gradeSpinBox.setFixedWidth(150)
|
||||
self.submitButton.setFixedWidth(120)
|
||||
self.resetButton.setFixedWidth(120)
|
||||
|
||||
self.spinboxLayout.addWidget(self.spinboxLabel)
|
||||
self.spinboxLayout.addWidget(self.gradeSpinBox)
|
||||
|
||||
self.btnVBoxLayout.addStretch()
|
||||
self.btnVBoxLayout.addWidget(self.submitButton)
|
||||
self.btnVBoxLayout.addWidget(self.resetButton)
|
||||
self.btnVBoxLayout.addStretch()
|
||||
|
||||
self.hBoxLayout.addStretch()
|
||||
self.hBoxLayout.addLayout(self.spinboxLayout)
|
||||
self.hBoxLayout.addLayout(self.btnVBoxLayout)
|
||||
self.hBoxLayout.addWidget(self.keyboard)
|
||||
self.hBoxLayout.addStretch()
|
||||
|
||||
self.keyboard.scoringSignal.connect(lambda x: self.gradeSpinBox.setValue(x))
|
||||
self.resetButton.clicked.connect(lambda: self.gradeSpinBox.clear())
|
||||
self.submitButton.clicked.connect(lambda: self.submitSignal.emit(self.gradeSpinBox.value()))
|
||||
|
||||
|
||||
class PickStudentLabelUi(SimpleCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.rollingText = RollingTextWidget(self)
|
||||
self.scoring = QuickScoring(self)
|
||||
self.separator = CardSeparator(self)
|
||||
|
||||
self.scoring.hide()
|
||||
self.separator.hide()
|
||||
|
||||
self.vBoxLayout.addWidget(self.rollingText)
|
||||
self.vBoxLayout.addWidget(self.separator)
|
||||
self.vBoxLayout.addWidget(self.scoring)
|
||||
self.vBoxLayout.addStretch()
|
||||
|
||||
def show_scoring(self):
|
||||
self.scoring.show()
|
||||
self.separator.show()
|
||||
|
||||
def hideEvent(self, event, /):
|
||||
super().hideEvent(event)
|
||||
self.scoring.gradeSpinBox.clear()
|
||||
self.scoring.hide()
|
||||
self.separator.hide()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
window = PickStudentLabelUi()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout
|
||||
from qfluentwidgets import PushButton, InfoBarIcon, InfoBarPosition
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import os
|
||||
import sys
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import io
|
||||
import matplotlib
|
||||
from matplotlib import pyplot as plt
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
import matplotlib
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from matplotlib import pyplot as plt
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||
|
||||
|
||||
def format_ranges(nums):
|
||||
@@ -184,4 +206,35 @@ def resource_path(relative_path: str) -> str:
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
DEVELOPMENT_ENV = getattr(sys, 'frozen', False)
|
||||
def open_template(file_name: str, widget: QWidget) -> None:
|
||||
"""将模板文件复制到临时目录并打开"""
|
||||
file_path = resource_path("template/" + file_name)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Template file '{file_name}' not found.")
|
||||
|
||||
# 复制到临时目录
|
||||
tmp_dir = tempfile.gettempdir()
|
||||
tmp_file_path = os.path.join(tmp_dir, file_name)
|
||||
shutil.copy(file_path, tmp_file_path)
|
||||
|
||||
# 打开文件
|
||||
if sys.platform.startswith('win'):
|
||||
os.startfile(tmp_file_path)
|
||||
elif sys.platform.startswith('darwin'):
|
||||
os.system(f'open "{tmp_file_path}"')
|
||||
else:
|
||||
os.system(f'xdg-open "{tmp_file_path}"')
|
||||
|
||||
InfoBar.info(
|
||||
title='已打开文件',
|
||||
content="编辑后请将文件另存为,保存至其他目录",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=2000,
|
||||
parent=widget
|
||||
)
|
||||
|
||||
|
||||
RELEASE_ENV = getattr(sys, 'frozen', False)
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def gen_build_time():
|
||||
def gen_build_info():
|
||||
try:
|
||||
hash_str = subprocess.check_output(
|
||||
['git', 'rev-parse', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode('utf-8').strip()
|
||||
except FileNotFoundError:
|
||||
# git 未安装
|
||||
hash_str = 'unknown'
|
||||
except subprocess.CalledProcessError:
|
||||
# 不是 git 仓库(如从压缩包下载)
|
||||
hash_str = 'unknown'
|
||||
|
||||
with open('build_info.py', 'w', encoding='utf-8') as f:
|
||||
f.write(f"# Auto-generated build info\n")
|
||||
f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n")
|
||||
f.write(f"GIT_HASH = '{hash_str}'\n")
|
||||
|
||||
|
||||
gen_build_time()
|
||||
if __name__ == '__main__':
|
||||
gen_build_info()
|
||||
|
||||
Reference in New Issue
Block a user