mirror of
https://github.com/octopusYan/dayz-mod-translator.git
synced 2024-11-23 12:46:42 +08:00
Compare commits
3 Commits
50032cc599
...
9edc015af0
Author | SHA1 | Date | |
---|---|---|---|
9edc015af0 | |||
1dc7a64833 | |||
943056168f |
5
.gitignore
vendored
5
.gitignore
vendored
@ -7,6 +7,11 @@ target/
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
/bin
|
||||
/tmp
|
||||
/bak
|
||||
config.yaml
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/
|
||||
.idea/modules.xml
|
||||
|
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
100
README.md
100
README.md
@ -2,46 +2,35 @@
|
||||
|
||||
# DayZ Mod Translator
|
||||
|
||||
![JDK](https://img.shields.io/badge/JDK-21-%2300599C)
|
||||
[![JavaFX](https://img.shields.io/badge/JavaFX-21.0.4-%2300599C)](https://openjfx.io/)
|
||||
![platform](https://img.shields.io/badge/platform-Windows-blueviolet)
|
||||
<br>
|
||||
<div>
|
||||
<img alt="JDK" src="https://img.shields.io/badge/JDK-17-%2300599C">
|
||||
<img alt="platform" src="https://img.shields.io/badge/platform-Windows-blueviolet">
|
||||
</div>
|
||||
|
||||
[//]: # (<div>)
|
||||
|
||||
[//]: # ( <img alt="license" src="https://img.shields.io/github/license/octopusYan/dayz-mod-translator">)
|
||||
|
||||
[//]: # ( <img alt="commit" src="https://img.shields.io/github/commit-activity/m/octopusYan/dayz-mod-translator?color=%23ff69b4">)
|
||||
|
||||
[//]: # (</div>)
|
||||
|
||||
[//]: # (<div>)
|
||||
|
||||
[//]: # ( <img alt="stars" src="https://img.shields.io/github/stars/octopusYan/dayz-mod-translator?style=social">)
|
||||
|
||||
[//]: # ( <img alt="GitHub all releases" src="https://img.shields.io/github/downloads/octopusYan/dayz-mod-translator/total?style=social">)
|
||||
|
||||
[//]: # (</div>)
|
||||
[![license](https://img.shields.io/github/license/octopusYan/dayz-mod-translator)](https://github.com/octopusYan/dayz-mod-translator)
|
||||
![commit](https://img.shields.io/github/commit-activity/m/octopusYan/dayz-mod-translator?color=%23ff69b4)
|
||||
<br>
|
||||
![stars](https://img.shields.io/github/stars/octopusYan/dayz-mod-translator?style=social)
|
||||
![GitHub all releases](https://img.shields.io/github/downloads/octopusYan/dayz-mod-translator/total?style=social)
|
||||
|
||||
<br>
|
||||
|
||||
使用 JavaFx 编写的 DayZ 游戏mod 汉化GUI工具
|
||||
使用JavaFx编写的DayZ/ArmA游戏模组汉化工具
|
||||
|
||||
</div>
|
||||
|
||||
### 使用
|
||||
|
||||
- 设置 -> 翻译,选择翻译接口,填写配置信息,点击确定
|
||||
- 点击左侧打开文件选择需要翻译的模组pbo文件
|
||||
- 待翻译文本获取完成后,点击右侧翻译按钮
|
||||
- 点击打开文件按钮选择需要翻译的模组pbo文件
|
||||
- 等待待可翻译文本获取完成,点击右侧翻译按钮
|
||||
- 翻译完成后,点击打包按钮,选择保存位置
|
||||
|
||||
<details><summary>截图</summary>
|
||||
<details open>
|
||||
<summary>截图</summary>
|
||||
|
||||
![Main window start](doc/img/screenshot01.png 'Main application window start')
|
||||
|
||||
![Main window open file](doc/img/screenshot02.png 'Main window open file')
|
||||
![start](doc/img/screenshot01.png 'start')
|
||||
![open file](doc/img/screenshot02.png 'open file')
|
||||
![edit](doc/img/screenshot03.png 'edit')
|
||||
|
||||
</details>
|
||||
|
||||
@ -50,42 +39,49 @@
|
||||
#### 环境说明
|
||||
|
||||
| 名称 | 描述 |
|
||||
|-------|---------------------------------------------------------|
|
||||
| 系统环境 | windows11/10 |
|
||||
| JDK版本 | 17 |
|
||||
| 构建工具 | Mavne |
|
||||
|------|---------------------------------------------------------|
|
||||
| 系统环境 | windows 10/11 |
|
||||
| JDK | 21 |
|
||||
| 构建工具 | Maven |
|
||||
| 打包工具 | [JavaPackager](https://github.com/fvarrui/JavaPackager) |
|
||||
|
||||
#### 步骤
|
||||
#### 本地运行
|
||||
|
||||
1. 环境配置
|
||||
- 打开 [JavaFx](https://gluonhq.com/products/javafx/) 官网环境下载页面
|
||||
- 在下方 Downloads 处
|
||||
- `JavaFX version` 选择 `17.0.12[LTS]`
|
||||
- `Operating System` 选择 `Windows`
|
||||
- `Type` 选择 `jmods`
|
||||
- 点击右侧绿色按钮下载解压
|
||||
- 将解压文件夹内所有 `.jmod` 后缀名的文件复制到 `jdk环境目录` 的`jmods`文件夹中
|
||||
2. 下载源代码并使用 [IntelliJ IDEA](https://www.jetbrains.com/zh-cn/idea/download/?section=windows) 打开
|
||||
1. 克隆代码
|
||||
```bash
|
||||
git clone https://github.com/octopusYan/dayz-mod-translator.git
|
||||
git clone https://github.com/octopusYan/dayz-mod-translator
|
||||
```
|
||||
3. 打包
|
||||
- 使用 `IntelliJ IDEA` Maven UI
|
||||
- 点击右侧工具栏的`Maven`
|
||||
- 展开 `DayzModTranslator\Lifecycle`
|
||||
- 点击 package
|
||||
- 使用 mvn 命令
|
||||
2. 运行
|
||||
```bash
|
||||
mvn package
|
||||
mvn clean javafx:run
|
||||
```
|
||||
|
||||
#### 打包
|
||||
|
||||
1. 克隆代码
|
||||
```bash
|
||||
git clone https://github.com/octopusYan/dayz-mod-translator
|
||||
```
|
||||
2. 运行
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
### 可能会用到
|
||||
|
||||
吾爱论坛 / 谷歌翻译修复:[谷歌浏览器右键翻译失效了咋办,一键修复,才13KB](https://www.52pojie.cn/thread-1781877-1-1.html)
|
||||
|
||||
## 致谢
|
||||
### 依赖/引用的项目
|
||||
|
||||
### 开源库
|
||||
<figure>
|
||||
|
||||
- [pboman3](https://github.com/winseros/pboman3):打开、打包和解包 ArmA PBO 文件的工具。
|
||||
| | |
|
||||
|-----------------------------------------------------------------------------|--------------------------|
|
||||
| [PBO Manager](https://github.com/winseros/pboman3) | 打开、打包和解包 ArmA PBO 文件的工具。 |
|
||||
| [JavaFX](https://openjfx.io/) | Java 桌面开发 |
|
||||
| [AtlantaFX](https://mkpaz.github.io/atlantafx/) | JavaFX CSS 主题集合 |
|
||||
| [JavaPackager](https://github.com/fvarrui/JavaPackager) | 打包插件 |
|
||||
| [Apache Commons](https://commons.apache.org/proper/commons-exec/index.html) | 工具包 |
|
||||
| [SLF4J](https://slf4j.org/) | 日志工具 |
|
||||
|
||||
</figure>
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 44 KiB |
Binary file not shown.
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 146 KiB |
BIN
doc/img/screenshot03.png
Normal file
BIN
doc/img/screenshot03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
162
pom.xml
162
pom.xml
@ -5,28 +5,40 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cn.octopusyan</groupId>
|
||||
<artifactId>dayz-mod-translator</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<name>DayzModTranslator</name>
|
||||
<artifactId>dmt</artifactId>
|
||||
<version>0.0.2</version>
|
||||
<name>dmt</name>
|
||||
|
||||
<organization>
|
||||
<name>octopus_yan</name>
|
||||
<url>octopus_yan@foxmail.com</url>
|
||||
</organization>
|
||||
|
||||
<inceptionYear>2024</inceptionYear>
|
||||
<description>DayZ 模组汉化工具</description>
|
||||
<description>DayZ/ArmA 模组汉化工具</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<java.version>17</java.version>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<java.version>21</java.version>
|
||||
|
||||
<exec.mainClass>cn.octopusyan.dmt.AppLauncher</exec.mainClass>
|
||||
<cssSrcPath>${project.basedir}/src/main/resources/css</cssSrcPath>
|
||||
<cssTargetPath>${project.basedir}/target/classes/css</cssTargetPath>
|
||||
|
||||
<junit.version>5.11.0</junit.version>
|
||||
<javafx.version>17.0.12</javafx.version>
|
||||
<javafx.version>21.0.4</javafx.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
<logback.version>1.5.7</logback.version>
|
||||
<fastjson.version>2.0.52</fastjson.version>
|
||||
<common-lang3.version>3.16.0</common-lang3.version>
|
||||
<common-io.version>2.17.0</common-io.version>
|
||||
<common-exec.version>1.4.0</common-exec.version>
|
||||
<lombok.version>1.18.32</lombok.version>
|
||||
<jackson.version>2.15.4</jackson.version>
|
||||
<ikonli.version>12.3.1</ikonli.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -47,12 +59,13 @@
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- slf4j -->
|
||||
<!--<dependency>-->
|
||||
<!-- <groupId>org.slf4j</groupId>-->
|
||||
<!-- <artifactId>slf4j-simple</artifactId>-->
|
||||
<!-- <version>${slf4j.version}</version>-->
|
||||
<!--</dependency>-->
|
||||
<!-- https://mkpaz.github.io/atlantafx/ -->
|
||||
<dependency>
|
||||
<groupId>io.github.mkpaz</groupId>
|
||||
<artifactId>atlantafx-base</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
@ -88,30 +101,62 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.16.0</version>
|
||||
<version>${common-lang3.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.16.1</version>
|
||||
<version>${common-io.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-exec</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<version>${common-exec.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
|
||||
<!-- lombok -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- jackson -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://kordamp.org/ikonli/ -->
|
||||
<dependency>
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-javafx</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-fontawesome-pack</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-feather-pack</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-bom</artifactId>
|
||||
<version>${ikonli.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>nexus</id>
|
||||
@ -141,8 +186,17 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
<encoding>UTF-8</encoding>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@ -162,20 +216,25 @@
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<configuration>
|
||||
<stripDebug>true</stripDebug>
|
||||
<compress>2</compress>
|
||||
<noHeaderFiles>true</noHeaderFiles>
|
||||
<noManPages>true</noManPages>
|
||||
<launcher>launcher</launcher>
|
||||
<jlinkImageName>app</jlinkImageName>
|
||||
<jlinkZipName>app</jlinkZipName>
|
||||
<mainClass>cn.octopusyan.dmt/${exec.mainClass}</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<!-- Default configuration for running with: mvn clean javafx:run -->
|
||||
<id>default-cli</id>
|
||||
<configuration>
|
||||
<mainClass>
|
||||
cn.octopusyan.dayzmodtranslator/cn.octopusyan.dayzmodtranslator.AppLuncher
|
||||
</mainClass>
|
||||
<launcher>launcher</launcher>
|
||||
<jlinkZipName>app</jlinkZipName>
|
||||
<jlinkImageName>app</jlinkImageName>
|
||||
<noManPages>true</noManPages>
|
||||
<stripDebug>true</stripDebug>
|
||||
<noHeaderFiles>true</noHeaderFiles>
|
||||
<options>
|
||||
<option>--enable-preview</option>
|
||||
<!-- <option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005</option>-->
|
||||
</options>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
@ -186,20 +245,53 @@
|
||||
<artifactId>javapackager</artifactId>
|
||||
<version>1.7.7-SNAPSHOT</version>
|
||||
<configuration>
|
||||
<mainClass>${exec.mainClass}</mainClass>
|
||||
<bundleJre>true</bundleJre>
|
||||
<mainClass>cn.octopusyan.dayzmodtranslator.AppLuncher</mainClass>
|
||||
<generateInstaller>false</generateInstaller>
|
||||
<copyDependencies>true</copyDependencies>
|
||||
<vmArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
<arg>-Xmx100m</arg>
|
||||
</vmArgs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>bundling-for-windows</id>
|
||||
<id>windows</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>package</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<platform>windows</platform>
|
||||
<zipballName>${project.name}-windows</zipballName>
|
||||
<createZipball>true</createZipball>
|
||||
<winConfig>
|
||||
<headerType>gui</headerType>
|
||||
<generateMsi>false</generateMsi>
|
||||
</winConfig>
|
||||
<additionalResources>
|
||||
<additionalResource>${project.basedir}/src/main/resources/bin</additionalResource>
|
||||
</additionalResources>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>windows-nojre</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>package</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<zipballName>${project.name}-windows-nojre</zipballName>
|
||||
<platform>windows</platform>
|
||||
<createZipball>true</createZipball>
|
||||
<bundleJre>false</bundleJre>
|
||||
<winConfig>
|
||||
<headerType>gui</headerType>
|
||||
<generateMsi>false</generateMsi>
|
||||
</winConfig>
|
||||
<additionalResources>
|
||||
<additionalResource>${project.basedir}/src/main/resources/bin</additionalResource>
|
||||
</additionalResources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -1,19 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator;
|
||||
|
||||
/**
|
||||
* 启动类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class AppLuncher {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// try {
|
||||
// Runtime.getRuntime().exec("Taskkill /IM " + FrpManager.FRPC_CLIENT_FILE_NAME + " /f");
|
||||
// } catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
Application.launch(Application.class, args);
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
|
||||
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.http.HttpConfig;
|
||||
import cn.octopusyan.dayzmodtranslator.controller.MainController;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.CfgConvertUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.PBOUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProxySelector;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Application extends javafx.application.Application {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Application.class);
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
logger.info("application init ...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws IOException {
|
||||
|
||||
logger.info("application start ...");
|
||||
|
||||
// bin转换 工具初始化
|
||||
CfgConvertUtil.init();
|
||||
|
||||
// PBO 工具初始化
|
||||
PBOUtil.init(CfgConvertUtil.getInstance());
|
||||
|
||||
// 客户端配置初始化
|
||||
CustomConfig.init();
|
||||
|
||||
// 初始化弹窗工具
|
||||
AlertUtil.initOwner(stage);
|
||||
|
||||
// http请求工具初始化
|
||||
HttpConfig httpConfig = new HttpConfig();
|
||||
if (CustomConfig.hasProxy()) {
|
||||
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(CustomConfig.proxyHost(), CustomConfig.proxyPort());
|
||||
httpConfig.setProxySelector(ProxySelector.of(unresolved));
|
||||
}
|
||||
httpConfig.setConnectTimeout(10);
|
||||
HttpUtil.init(httpConfig);
|
||||
|
||||
// TODO 全局异常处理
|
||||
Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
|
||||
Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
|
||||
|
||||
// 启动主界面
|
||||
try {
|
||||
FXMLLoader fxmlLoader = FxmlUtil.load("main-view");
|
||||
VBox root = fxmlLoader.load();//底层面板
|
||||
Scene scene = new Scene(root);
|
||||
scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
|
||||
stage.setScene(scene);
|
||||
stage.setMinHeight(330);
|
||||
stage.setMinWidth(430);
|
||||
stage.setMaxWidth(Double.MAX_VALUE);
|
||||
stage.setMaxHeight(Double.MAX_VALUE);
|
||||
stage.setTitle(AppConstant.APP_TITLE + " v" + AppConstant.APP_VERSION);
|
||||
stage.show();
|
||||
|
||||
MainController controller = fxmlLoader.getController();
|
||||
controller.setApplication(this);
|
||||
} catch (Throwable t) {
|
||||
showErrorDialog(Thread.currentThread(), t);
|
||||
}
|
||||
|
||||
logger.info("application start over ...");
|
||||
}
|
||||
|
||||
private void showErrorDialog(Thread t, Throwable e) {
|
||||
logger.error("", e);
|
||||
AlertUtil.exceptionAlert(new Exception(e)).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
logger.info("application stop ...");
|
||||
|
||||
// 清除翻译任务
|
||||
TranslateUtil.getInstance().clear();
|
||||
// 停止所有线程
|
||||
ThreadPoolManager.getInstance().shutdown();
|
||||
// 保存应用数据
|
||||
CustomConfig.store();
|
||||
// 清理缓存
|
||||
PBOUtil.clear();
|
||||
}
|
||||
}
|
@ -1,217 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.base;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
|
||||
import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.Loading;
|
||||
import cn.octopusyan.dayzmodtranslator.util.TooltipUtil;
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* 通用视图控制器基类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public abstract class BaseController<P extends Pane> implements Initializable {
|
||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private Application application;
|
||||
|
||||
private double xOffSet = 0, yOffSet = 0;
|
||||
|
||||
private volatile Loading loading;
|
||||
|
||||
protected TooltipUtil tooltipUtil;
|
||||
|
||||
public void jumpTo(BaseController<P> controller) throws IOException {
|
||||
FXMLLoader fxmlLoader = FxmlUtil.load(controller.getRootFxml());
|
||||
|
||||
Scene scene = getRootPanel().getScene();
|
||||
double oldHeight = getRootPanel().getPrefHeight();
|
||||
double oldWidth = getRootPanel().getPrefWidth();
|
||||
|
||||
Pane root = fxmlLoader.load();
|
||||
Stage stage = (Stage) scene.getWindow();
|
||||
// 窗口大小
|
||||
double newWidth = root.getPrefWidth();
|
||||
double newHeight = root.getPrefHeight();
|
||||
// 窗口位置
|
||||
double newX = stage.getX() - (newWidth - oldWidth) / 2;
|
||||
double newY = stage.getY() - (newHeight - oldHeight) / 2;
|
||||
scene.setRoot(root);
|
||||
stage.setX(newX < 0 ? 0 : newX);
|
||||
stage.setY(newY < 0 ? 0 : newY);
|
||||
stage.setWidth(newWidth);
|
||||
stage.setHeight(newHeight);
|
||||
|
||||
controller = fxmlLoader.getController();
|
||||
controller.setApplication(getApplication());
|
||||
}
|
||||
|
||||
protected void open(Class<? extends BaseController<?>> clazz, String title) {
|
||||
try {
|
||||
FXMLLoader load = FxmlUtil.load(clazz.getDeclaredConstructor().newInstance().getRootFxml());
|
||||
Parent root = load.load();
|
||||
Scene scene = new Scene(root);
|
||||
scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
|
||||
Stage stage = new Stage();
|
||||
stage.setScene(scene);
|
||||
stage.setTitle(title);
|
||||
stage.initOwner(getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.show();
|
||||
load.getController();
|
||||
} catch (Exception e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||
// 全局窗口拖拽
|
||||
if (dragWindow()) {
|
||||
// 窗口拖拽
|
||||
getRootPanel().setOnMousePressed(event -> {
|
||||
xOffSet = event.getSceneX();
|
||||
yOffSet = event.getSceneY();
|
||||
});
|
||||
getRootPanel().setOnMouseDragged(event -> {
|
||||
Stage stage = (Stage) getWindow();
|
||||
stage.setX(event.getScreenX() - xOffSet);
|
||||
stage.setY(event.getScreenY() - yOffSet);
|
||||
});
|
||||
}
|
||||
|
||||
// 窗口初始化完成监听
|
||||
getRootPanel().sceneProperty().addListener((observable, oldValue, newValue) -> {
|
||||
newValue.windowProperty().addListener(new ChangeListener<Window>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Window> observable, Window oldValue, Window newValue) {
|
||||
//关闭窗口监听
|
||||
getWindow().setOnCloseRequest(windowEvent -> onDestroy());
|
||||
|
||||
// app 版本信息
|
||||
if (getAppVersionLabel() != null) getAppVersionLabel().setText("v" + AppConstant.APP_VERSION);
|
||||
|
||||
// 初始化数据
|
||||
initData();
|
||||
|
||||
// 初始化视图样式
|
||||
initViewStyle();
|
||||
|
||||
// 初始化视图事件
|
||||
initViewAction();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void showLoading() {
|
||||
showLoading(null);
|
||||
}
|
||||
|
||||
public void showLoading(String message) {
|
||||
if (loading == null) loading = new Loading((Stage) getWindow());
|
||||
|
||||
if (StringUtils.isNotEmpty(message)) loading.showMessage(message);
|
||||
|
||||
loading.show();
|
||||
}
|
||||
|
||||
public void setApplication(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
public Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public boolean isLoadShowing() {
|
||||
return loading != null && loading.showing();
|
||||
}
|
||||
|
||||
public void stopLoading() {
|
||||
if (isLoadShowing())
|
||||
loading.closeStage();
|
||||
}
|
||||
|
||||
protected TooltipUtil getTooltipUtil() {
|
||||
if (tooltipUtil == null) tooltipUtil = TooltipUtil.getInstance(getRootPanel());
|
||||
return tooltipUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口拖拽设置
|
||||
*
|
||||
* @return 是否启用
|
||||
*/
|
||||
public abstract boolean dragWindow();
|
||||
|
||||
/**
|
||||
* 获取根布局
|
||||
*
|
||||
* @return 根布局对象
|
||||
*/
|
||||
public abstract P getRootPanel();
|
||||
|
||||
/**
|
||||
* 获取根布局
|
||||
* <p> 搭配 <code>FxmlUtil.load</code> 使用
|
||||
*
|
||||
* @return 根布局对象
|
||||
* @see cn.octopusyan.dayzmodtranslator.util.FxmlUtil#load(String)
|
||||
*/
|
||||
public abstract String getRootFxml();
|
||||
|
||||
protected Window getWindow() {
|
||||
return getRootPanel().getScene().getWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* App版本信息标签
|
||||
*/
|
||||
public Label getAppVersionLabel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
public abstract void initData();
|
||||
|
||||
/**
|
||||
* 视图样式
|
||||
*/
|
||||
public abstract void initViewStyle();
|
||||
|
||||
/**
|
||||
* 视图事件
|
||||
*/
|
||||
public abstract void initViewAction();
|
||||
|
||||
/**
|
||||
* 关闭窗口
|
||||
*/
|
||||
public void onDestroy() {
|
||||
Stage stage = (Stage) getWindow();
|
||||
stage.hide();
|
||||
stage.close();
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.config;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.util.PropertiesUtils;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 应用信息
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class AppConstant {
|
||||
public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
|
||||
public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
|
||||
public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
|
||||
public static final String DATA_DIR_PATH = System.getProperty("user.home") + File.separator + "AppData" + File.separator + "Local" + File.separator + APP_NAME;
|
||||
public static final String TMP_DIR_PATH = FileUtils.getTempDirectoryPath() + APP_NAME;
|
||||
public static final String CUSTOM_CONFIG_PATH = DATA_DIR_PATH + File.separator + "config.properties";
|
||||
public static final String BAK_FILE_PATH = AppConstant.TMP_DIR_PATH + File.separator + "bak";
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.config;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 客户端设置
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class CustomConfig {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomConfig.class);
|
||||
private static final Properties properties = new Properties();
|
||||
public static final String PROXY_HOST_KEY = "proxy.host";
|
||||
public static final String PROXY_PORT_KEY = "proxy.port";
|
||||
public static final String TRANSLATE_SOURCE_KEY = "translate.source";
|
||||
public static final String TRANSLATE_SOURCE_APPID_KEY = "translate.{}.appid";
|
||||
public static final String TRANSLATE_SOURCE_APIKEY_KEY = "translate.{}.apikey";
|
||||
public static final String TRANSLATE_SOURCE_QPS_KEY = "translate.{}.qps";
|
||||
|
||||
public static void init() {
|
||||
File customConfigFile = new File(AppConstant.CUSTOM_CONFIG_PATH);
|
||||
try {
|
||||
if (!customConfigFile.exists()) {
|
||||
// 初始配置
|
||||
properties.put(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName());
|
||||
// 保存配置文件
|
||||
store();
|
||||
} else {
|
||||
properties.load(new FileInputStream(customConfigFile));
|
||||
}
|
||||
} catch (IOException ignore) {
|
||||
logger.error("读取配置文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否配置代理
|
||||
*/
|
||||
public static boolean hasProxy() {
|
||||
String host = proxyHost();
|
||||
Integer port = proxyPort();
|
||||
|
||||
return StringUtils.isNoneBlank(host) && null != port;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理地址
|
||||
*/
|
||||
public static String proxyHost() {
|
||||
return properties.getProperty(PROXY_HOST_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理地址
|
||||
*/
|
||||
public static void proxyHost(String host) {
|
||||
properties.setProperty(PROXY_HOST_KEY, host);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理端口
|
||||
*/
|
||||
public static Integer proxyPort() {
|
||||
try {
|
||||
return Integer.parseInt(properties.getProperty(PROXY_PORT_KEY));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理端口
|
||||
*/
|
||||
public static void proxyPort(int port) {
|
||||
properties.setProperty(PROXY_PORT_KEY, String.valueOf(port));
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译源
|
||||
*/
|
||||
public static TranslateSource translateSource() {
|
||||
String name = properties.getProperty(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName());
|
||||
return TranslateSource.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译源
|
||||
*/
|
||||
public static void translateSource(TranslateSource source) {
|
||||
properties.setProperty(TRANSLATE_SOURCE_KEY, source.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否配置接口认证
|
||||
*
|
||||
* @param source 翻译源
|
||||
*/
|
||||
public static boolean hasTranslateApiKey(TranslateSource source) {
|
||||
return StringUtils.isNoneBlank(translateSourceAppid(source))
|
||||
&& StringUtils.isNoneBlank(translateSourceApikey(source));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置翻译源appid
|
||||
*
|
||||
* @param source 翻译源
|
||||
* @param appid appid
|
||||
*/
|
||||
public static void translateSourceAppid(TranslateSource source, String appid) {
|
||||
properties.setProperty(getTranslateSourceAppidKey(source), appid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取翻译源appid
|
||||
*
|
||||
* @param source 翻译源
|
||||
* @return appid
|
||||
*/
|
||||
public static String translateSourceAppid(TranslateSource source) {
|
||||
return properties.getProperty(getTranslateSourceAppidKey(source));
|
||||
}
|
||||
|
||||
public static void translateSourceApikey(TranslateSource source, String apikey) {
|
||||
properties.setProperty(getTranslateSourceApikeyKey(source), apikey);
|
||||
}
|
||||
|
||||
public static String translateSourceApikey(TranslateSource source) {
|
||||
return properties.getProperty(getTranslateSourceApikeyKey(source));
|
||||
}
|
||||
|
||||
public static Integer translateSourceQps(TranslateSource source) {
|
||||
String qpsStr = properties.getProperty(getTranslateSourceQpsKey(source));
|
||||
return qpsStr == null ? source.getDefaultQps() : Integer.parseInt(qpsStr);
|
||||
}
|
||||
|
||||
public static void translateSourceQps(TranslateSource source, int qps) {
|
||||
properties.setProperty(getTranslateSourceQpsKey(source), String.valueOf(qps));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
*/
|
||||
public static void store() {
|
||||
// 生成配置文件
|
||||
try {
|
||||
properties.store(new PrintStream(AppConstant.CUSTOM_CONFIG_PATH), String.valueOf(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
logger.error("保存客户端配置失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getTranslateSourceAppidKey(TranslateSource source) {
|
||||
return StringUtils.replace(TRANSLATE_SOURCE_APPID_KEY, "{}", source.getName());
|
||||
}
|
||||
|
||||
private static String getTranslateSourceApikeyKey(TranslateSource source) {
|
||||
return StringUtils.replace(TRANSLATE_SOURCE_APIKEY_KEY, "{}", source.getName());
|
||||
}
|
||||
|
||||
private static String getTranslateSourceQpsKey(TranslateSource source) {
|
||||
return StringUtils.replace(TRANSLATE_SOURCE_QPS_KEY, "{}", source.getName());
|
||||
}
|
||||
}
|
@ -1,515 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.controller;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.base.BaseController;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.PBOUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.file.FileTreeItem;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.word.WordItem;
|
||||
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.ClipUtil;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 主控制器
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class MainController extends BaseController<VBox> {
|
||||
|
||||
public VBox root;
|
||||
public MenuItem openFileSetupBtn;
|
||||
public MenuItem translateSetupBtn;
|
||||
public MenuItem proxySetupBtn;
|
||||
public Label filePath;
|
||||
public StackPane fileBox;
|
||||
public VBox openFileBox;
|
||||
public Button openFile;
|
||||
public VBox dragFileBox;
|
||||
public Label dragFileLabel;
|
||||
public VBox loadFileBox;
|
||||
public Label loadFileLabel;
|
||||
public ProgressBar loadFileProgressBar;
|
||||
public TreeView<String> treeFileBox;
|
||||
public StackPane wordBox;
|
||||
public TableView<WordItem> wordTableBox;
|
||||
public VBox wordMsgBox;
|
||||
public Label wordMsgLabel;
|
||||
public ProgressBar loadWordProgressBar;
|
||||
public Button translateWordBtn;
|
||||
public Button packBtn;
|
||||
private final PBOUtil pboUtil = PBOUtil.getInstance();
|
||||
private final TranslateUtil translateUtil = TranslateUtil.getInstance();
|
||||
|
||||
/**
|
||||
* 翻译标志 用于停止翻译
|
||||
*/
|
||||
private AtomicBoolean transTag;
|
||||
/**
|
||||
* 已翻译文本下标缓存
|
||||
*/
|
||||
private Set<Integer> transNum;
|
||||
|
||||
@Override
|
||||
public boolean dragWindow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VBox getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRootFxml() {
|
||||
return "main-view";
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
@Override
|
||||
public void initData() {
|
||||
|
||||
// 解包监听
|
||||
pboUtil.setOnUnpackListener(new PBOUtil.OnUnpackListener() {
|
||||
@Override
|
||||
public void onStart() {
|
||||
refreshWordBox();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnpackSuccess(String unpackDirPath) {
|
||||
loadFileLabel.textProperty().setValue("加载完成,正在获取文件目录");
|
||||
// 展示解包文件内容
|
||||
logger.info("正在获取文件目录。。");
|
||||
showDirectory(new File(unpackDirPath));
|
||||
// 展示可翻译语句
|
||||
logger.info("正在查询待翻译文本目录。。");
|
||||
showTranslateWord();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnpackError(String msg) {
|
||||
loadFileLabel.textProperty().setValue("打开文件失败");
|
||||
logger.info("打开文件失败: \n" + msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnpackOver() {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 打包监听
|
||||
pboUtil.setOnPackListener(new PBOUtil.OnPackListener() {
|
||||
@Override
|
||||
public void onStart() {
|
||||
showLoading("正在打包pbo文件");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(long current, long all) {
|
||||
showLoading(String.format("正在打包pbo文件(%d / %d)", current, all));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackSuccess(File packFile) {
|
||||
// 选择文件保存地址
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
|
||||
fileChooser.getExtensionFilters().add(extFilter);
|
||||
File file = fileChooser.showSaveDialog(getWindow());
|
||||
if (file == null)
|
||||
return;
|
||||
if (file.exists()) {
|
||||
//文件已存在,则删除覆盖文件
|
||||
FileUtils.deleteQuietly(file);
|
||||
}
|
||||
String exportFilePath = file.getAbsolutePath();
|
||||
logger.info("导出文件的路径 =>" + exportFilePath);
|
||||
|
||||
try {
|
||||
FileUtils.copyFile(packFile, file);
|
||||
} catch (IOException e) {
|
||||
logger.error("保存文件失败!", e);
|
||||
|
||||
Platform.runLater(() -> AlertUtil.exception(e).content("保存文件失败!").show());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackError(String msg) {
|
||||
AlertUtil.error("保存文件失败!").show();
|
||||
logger.info("保存文件失败: \n" + msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackOver() {
|
||||
stopLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// 获取待翻译文字
|
||||
pboUtil.setOnFindTransWordListener((words, isOver) -> {
|
||||
loadWordProgressBar.setVisible(false);
|
||||
|
||||
if (words == null || words.isEmpty()) {
|
||||
if (isOver) {
|
||||
wordMsgLabel.textProperty().set("未找到待翻译文本");
|
||||
}
|
||||
} else {
|
||||
// 展示翻译按钮
|
||||
translateWordBtn.setVisible(true);
|
||||
// 展示打包按钮
|
||||
packBtn.setVisible(true);
|
||||
|
||||
// 绑定TableView
|
||||
boolean isCsvItem = (words.get(0) instanceof WordCsvItem);
|
||||
bindWordTable(words, isCsvItem);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图样式
|
||||
*/
|
||||
@Override
|
||||
public void initViewStyle() {
|
||||
wordMsgLabel.textProperty().setValue("请打开PBO文件");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图事件
|
||||
*/
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
// 翻译设置
|
||||
translateSetupBtn.setOnAction(event -> open(SetupTranslateController.class, "翻译源设置"));
|
||||
// 代理设置
|
||||
proxySetupBtn.setOnAction(event -> open(SetupProxyController.class, "代理设置"));
|
||||
|
||||
// 选择pbo文件
|
||||
EventHandler<ActionEvent> selectPboFileAction = actionEvent -> {
|
||||
// 文件选择器
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
|
||||
fileChooser.getExtensionFilters().add(extFilter);
|
||||
selectFile(fileChooser.showOpenDialog(getWindow()));
|
||||
};
|
||||
openFileSetupBtn.setOnAction(selectPboFileAction);
|
||||
openFile.setOnAction(selectPboFileAction);
|
||||
|
||||
// 拖拽效果 start ---------------------
|
||||
fileBox.setOnDragEntered(dragEvent -> {
|
||||
Dragboard dragboard = dragEvent.getDragboard();
|
||||
if (dragboard.hasFiles() && isPboFile(dragboard.getFiles().get(0))) {
|
||||
disableBox();
|
||||
dragFileBox.setVisible(true);
|
||||
}
|
||||
});
|
||||
fileBox.setOnDragExited(dragEvent -> {
|
||||
if (!loadFileBox.isVisible()) {
|
||||
disableBox();
|
||||
openFileBox.setVisible(true);
|
||||
}
|
||||
});
|
||||
fileBox.setOnDragOver(dragEvent -> {
|
||||
Dragboard dragboard = dragEvent.getDragboard();
|
||||
if (dragEvent.getGestureSource() != fileBox && dragboard.hasFiles()) {
|
||||
/* allow for both copying and moving, whatever user chooses */
|
||||
dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
dragEvent.consume();
|
||||
});
|
||||
fileBox.setOnDragDropped(dragEvent -> {
|
||||
disableBox();
|
||||
openFileBox.setVisible(true);
|
||||
|
||||
Dragboard db = dragEvent.getDragboard();
|
||||
boolean success = false;
|
||||
File file = db.getFiles().get(0);
|
||||
if (db.hasFiles() && isPboFile(file)) {
|
||||
selectFile(file);
|
||||
success = true;
|
||||
}
|
||||
/* 让源知道字符串是否已成功传输和使用 */
|
||||
dragEvent.setDropCompleted(success);
|
||||
|
||||
dragEvent.consume();
|
||||
});
|
||||
// 拖拽效果 end ---------------------
|
||||
|
||||
// 翻译按钮
|
||||
translateWordBtn.setOnMouseClicked(mouseEvent -> {
|
||||
|
||||
// 是否初次翻译
|
||||
if (transTag == null) {
|
||||
transNum = new HashSet<>();
|
||||
transTag = new AtomicBoolean(true);
|
||||
// 开始翻译
|
||||
startTranslate();
|
||||
} else {
|
||||
// 获取翻译列表
|
||||
ObservableList<WordItem> items = wordTableBox.getItems();
|
||||
// 未获取到翻译列表 或 翻译完成 则不做处理
|
||||
if (items == null || items.isEmpty() || transNum.size() == items.size())
|
||||
return;
|
||||
|
||||
// 设置翻译标识
|
||||
transTag.set(!transTag.get());
|
||||
|
||||
if (Boolean.FALSE.equals(transTag.get())) {
|
||||
stopTranslate();
|
||||
} else {
|
||||
startTranslate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// 打包按钮
|
||||
packBtn.setOnAction(event -> pboUtil.pack(wordTableBox.getItems()));
|
||||
|
||||
// 复制文本
|
||||
getRootPanel().getScene().getAccelerators()
|
||||
.put(new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TablePosition tablePosition = wordTableBox.getSelectionModel().getSelectedCells().get(0);
|
||||
Object cellData = tablePosition.getTableColumn().getCellData(tablePosition.getRow());
|
||||
ClipUtil.setClip(String.valueOf(cellData));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始翻译
|
||||
*/
|
||||
private void startTranslate() {
|
||||
// 获取翻译列表
|
||||
ObservableList<WordItem> items = wordTableBox.getItems();
|
||||
if (items == null || items.isEmpty()) return;
|
||||
|
||||
// 开始/继续 翻译
|
||||
String label = translateWordBtn.getText().replaceAll("已暂停|一键翻译", "正在翻译");
|
||||
translateWordBtn.textProperty().setValue(label);
|
||||
|
||||
// 禁用打包按钮
|
||||
packBtn.setDisable(true);
|
||||
|
||||
boolean isCsvItem = (items.get(0) instanceof WordCsvItem);
|
||||
// 循环提交翻译任务
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
// 跳过已翻译文本
|
||||
if (transNum.contains(i)) continue;
|
||||
|
||||
WordItem item = items.get(i);
|
||||
|
||||
// 提交翻译任务
|
||||
int finalI = i;
|
||||
translateUtil.translate(finalI, item.getOriginal(), new TranslateUtil.OnTranslateListener() {
|
||||
@Override
|
||||
public void onTranslate(String result) {
|
||||
// 防止多线程执行时停止不及时
|
||||
if (Boolean.FALSE.equals(transTag.get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 含有中文则不翻译
|
||||
if (!containsChinese(item.getChinese()))
|
||||
item.setChinese(result);
|
||||
|
||||
// 设置简中文本
|
||||
if (isCsvItem) {
|
||||
WordCsvItem csvItem = ((WordCsvItem) item);
|
||||
if (!containsChinese(csvItem.getChineseSimp()))
|
||||
csvItem.setChineseSimp(result);
|
||||
}
|
||||
|
||||
// 设置翻译进度
|
||||
transNum.add(finalI);
|
||||
String label;
|
||||
if (transNum.size() >= items.size()) {
|
||||
label = "翻译完成(" + items.size() + ")";
|
||||
transTag.set(false);
|
||||
// 启用打包按钮
|
||||
packBtn.setDisable(false);
|
||||
} else {
|
||||
label = "正在翻译(" + transNum.size() + "/" + items.size() + ")";
|
||||
}
|
||||
translateWordBtn.textProperty().setValue(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止翻译
|
||||
*/
|
||||
private void stopTranslate() {
|
||||
// 清除未完成的翻译任务
|
||||
translateUtil.clear();
|
||||
// 设置翻译状态
|
||||
String label = translateWordBtn.getText().replace("正在翻译", "已暂停");
|
||||
translateWordBtn.textProperty().setValue(label);
|
||||
// 启用打包按钮
|
||||
packBtn.setDisable(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择待汉化pbo文件
|
||||
* <p>TODO 多文件汉化
|
||||
*
|
||||
* @param file 待汉化文件
|
||||
*/
|
||||
private void selectFile(File file) {
|
||||
if (file == null || !file.exists()) return;
|
||||
|
||||
filePath.textProperty().set(file.getName());
|
||||
|
||||
// 重置文件界面
|
||||
disableBox();
|
||||
loadFileBox.setVisible(true);
|
||||
// 重置翻译文本状态
|
||||
wordBox.getChildren().remove(wordTableBox);
|
||||
wordTableBox = null;
|
||||
|
||||
loadFileLabel.textProperty().setValue("正在加载模组文件");
|
||||
pboUtil.unpack(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示文件夹内容
|
||||
*
|
||||
* @param file 根目录
|
||||
*/
|
||||
private void showDirectory(File file) {
|
||||
if (file == null || !file.exists() || !file.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
disableBox();
|
||||
treeFileBox.setVisible(true);
|
||||
|
||||
// 加载pbo文件目录
|
||||
FileTreeItem fileTreeItem = new FileTreeItem(file, File::listFiles);
|
||||
treeFileBox.setRoot(fileTreeItem);
|
||||
treeFileBox.setShowRoot(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示待翻译语句
|
||||
*/
|
||||
private void showTranslateWord() {
|
||||
wordMsgLabel.textProperty().setValue("正在获取可翻译文本");
|
||||
loadWordProgressBar.setVisible(true);
|
||||
|
||||
pboUtil.startFindWord();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定表格数据
|
||||
*
|
||||
* @param words 单词列表
|
||||
* @param isCsvItem 是否csv
|
||||
*/
|
||||
private void bindWordTable(List<WordItem> words, boolean isCsvItem) {
|
||||
if (wordTableBox == null) {
|
||||
wordTableBox = new TableView<>();
|
||||
wordBox.getChildren().add(wordTableBox);
|
||||
// 可编辑
|
||||
wordTableBox.setEditable(true);
|
||||
// 单元格选择模式而不是行选择
|
||||
wordTableBox.getSelectionModel().setCellSelectionEnabled(true);
|
||||
// 不允许选择多个单元格
|
||||
wordTableBox.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
// 鼠标事件清空
|
||||
wordTableBox.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.isControlDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wordTableBox.getEditingCell() == null) {
|
||||
wordTableBox.getSelectionModel().clearSelection();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建列
|
||||
wordTableBox.getColumns().add(createColumn("原始文本", WordItem::originalProperty));
|
||||
wordTableBox.getColumns().add(createColumn("中文", WordItem::chineseProperty));
|
||||
|
||||
if (isCsvItem) {
|
||||
wordTableBox.getColumns().add(createColumn("简体中文", WordCsvItem::chineseSimpProperty));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加表数据
|
||||
wordTableBox.getItems().addAll(words);
|
||||
}
|
||||
|
||||
private <T extends WordItem> TableColumn<WordItem, String> createColumn(String colName, Function<T, StringProperty> colField) {
|
||||
TableColumn<WordItem, String> tableColumn = new TableColumn<>(colName);
|
||||
tableColumn.setCellValueFactory(features -> colField.apply((T) features.getValue()));
|
||||
tableColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
tableColumn.setPrefWidth(150);
|
||||
tableColumn.setSortable(false);
|
||||
tableColumn.setEditable(!"原始文本".equals(colName));
|
||||
return tableColumn;
|
||||
}
|
||||
|
||||
private void disableBox() {
|
||||
openFileBox.setVisible(false);
|
||||
dragFileBox.setVisible(false);
|
||||
loadFileBox.setVisible(false);
|
||||
treeFileBox.setVisible(false);
|
||||
}
|
||||
|
||||
private void refreshWordBox() {
|
||||
if (wordTableBox != null) {
|
||||
wordBox.getChildren().remove(wordTableBox);
|
||||
wordTableBox = null;
|
||||
}
|
||||
wordMsgLabel.textProperty().setValue("请打开pbo文件");
|
||||
loadWordProgressBar.setVisible(false);
|
||||
translateWordBtn.textProperty().setValue("一键翻译");
|
||||
translateWordBtn.setVisible(false);
|
||||
packBtn.setVisible(false);
|
||||
}
|
||||
|
||||
private boolean isPboFile(File file) {
|
||||
if (file == null) return false;
|
||||
return Pattern.compile(".*(.pbo)$").matcher(file.getName()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定字符串是否含有中文
|
||||
*
|
||||
* @param str 需要判断的字符串
|
||||
* @return 是否含有中文
|
||||
*/
|
||||
private boolean containsChinese(String str) {
|
||||
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.controller;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.base.BaseController;
|
||||
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
|
||||
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* 应用配置
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class SetupProxyController extends BaseController<StackPane> {
|
||||
public StackPane root;
|
||||
public TextField hostField;
|
||||
public TextField portField;
|
||||
public TextField testPath;
|
||||
public static final String PROXY_ERROR = "ProxyError";
|
||||
|
||||
/**
|
||||
* 窗口拖拽设置
|
||||
*
|
||||
* @return 是否启用
|
||||
*/
|
||||
@Override
|
||||
public boolean dragWindow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根布局
|
||||
*
|
||||
* @return 根布局对象
|
||||
*/
|
||||
@Override
|
||||
public StackPane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根布局
|
||||
* <p> 搭配 <code>FxmlUtil.load</code> 使用
|
||||
*
|
||||
* @return 根布局对象
|
||||
* @see FxmlUtil#load(String)
|
||||
*/
|
||||
@Override
|
||||
public String getRootFxml() {
|
||||
return "proxy-view";
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
@Override
|
||||
public void initData() {
|
||||
// 是否已有代理配置
|
||||
if (CustomConfig.hasProxy()) {
|
||||
hostField.textProperty().setValue(CustomConfig.proxyHost());
|
||||
portField.textProperty().setValue(String.valueOf(CustomConfig.proxyPort()));
|
||||
}
|
||||
|
||||
// 默认测试地址
|
||||
testPath.textProperty().setValue("https://translate.googleapis.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图样式
|
||||
*/
|
||||
@Override
|
||||
public void initViewStyle() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图事件
|
||||
*/
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
|
||||
}
|
||||
|
||||
private String getHost() {
|
||||
String text = hostField.getText();
|
||||
if (StringUtils.isBlank(text)) {
|
||||
throw new RuntimeException(PROXY_ERROR);
|
||||
}
|
||||
try {
|
||||
URI.create(text);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(PROXY_ERROR);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private int getPort() {
|
||||
String text = portField.getText();
|
||||
if (StringUtils.isBlank(text)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
boolean creatable = NumberUtils.isCreatable(text);
|
||||
if (!creatable) {
|
||||
throw new RuntimeException(PROXY_ERROR);
|
||||
}
|
||||
|
||||
return Integer.parseInt(text);
|
||||
}
|
||||
|
||||
private String getTestPath() {
|
||||
String text = testPath.getText();
|
||||
if (StringUtils.isBlank(text)) {
|
||||
throw new RuntimeException(PROXY_ERROR);
|
||||
}
|
||||
return (text.startsWith("http") ? "" : "http://") + text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试代理有效性
|
||||
*/
|
||||
public void test() {
|
||||
HttpUtil.getInstance().clearProxy();
|
||||
try {
|
||||
String resp = HttpUtil.getInstance().proxy(getHost(), getPort())
|
||||
.get(getTestPath(), null, null);
|
||||
AlertUtil.info("成功").show();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.error("代理访问失败", e);
|
||||
AlertUtil.error("失败!").show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存代理配置
|
||||
*/
|
||||
public void save() {
|
||||
CustomConfig.proxyHost(getHost());
|
||||
CustomConfig.proxyPort(getPort());
|
||||
|
||||
CustomConfig.store();
|
||||
|
||||
onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
public void close() {
|
||||
HttpUtil.getInstance().clearProxy();
|
||||
|
||||
onDestroy();
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.controller;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.base.BaseController;
|
||||
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 翻译设置控制器
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class SetupTranslateController extends BaseController<StackPane> {
|
||||
public StackPane root;
|
||||
public ComboBox<TranslateSource> translateSourceCombo;
|
||||
public TextField qps;
|
||||
public VBox appidBox;
|
||||
public TextField appid;
|
||||
public VBox apikeyBox;
|
||||
public TextField apikey;
|
||||
|
||||
@Override
|
||||
public boolean dragWindow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StackPane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRootFxml() {
|
||||
return "translate-view";
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
@Override
|
||||
public void initData() {
|
||||
// 翻译源
|
||||
for (TranslateSource value : TranslateSource.values()) {
|
||||
ObservableList<TranslateSource> items = translateSourceCombo.getItems();
|
||||
items.addAll(value);
|
||||
}
|
||||
translateSourceCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(TranslateSource object) {
|
||||
if (object == null) return null;
|
||||
|
||||
return object.getLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TranslateSource fromString(String string) {
|
||||
return TranslateSource.getByLabel(string);
|
||||
}
|
||||
});
|
||||
translateSourceCombo.getSelectionModel()
|
||||
.selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> {
|
||||
boolean needApiKey = newValue.needApiKey();
|
||||
appidBox.setVisible(needApiKey);
|
||||
apikeyBox.setVisible(needApiKey);
|
||||
if (needApiKey) {
|
||||
appid.textProperty().setValue(CustomConfig.translateSourceAppid(newValue));
|
||||
apikey.textProperty().setValue(CustomConfig.translateSourceApikey(newValue));
|
||||
|
||||
}
|
||||
|
||||
qps.textProperty().setValue(String.valueOf(CustomConfig.translateSourceQps(newValue)));
|
||||
});
|
||||
|
||||
// 当前翻译源
|
||||
translateSourceCombo.getSelectionModel().select(CustomConfig.translateSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图样式
|
||||
*/
|
||||
@Override
|
||||
public void initViewStyle() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图事件
|
||||
*/
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
qps.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue.matches("\\d*")) {
|
||||
qps.setText(oldValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void save() {
|
||||
TranslateSource source = translateSourceCombo.getValue();
|
||||
String apikey = this.apikey.getText();
|
||||
String appid = this.appid.getText();
|
||||
int qps = Integer.parseInt(this.qps.getText());
|
||||
|
||||
CustomConfig.translateSource(source);
|
||||
if (source.needApiKey()) {
|
||||
if (StringUtils.isBlank(apikey) || StringUtils.isBlank(appid)) {
|
||||
AlertUtil.error("认证信息不能为空");
|
||||
}
|
||||
|
||||
CustomConfig.translateSourceApikey(source, apikey);
|
||||
CustomConfig.translateSourceAppid(source, appid);
|
||||
CustomConfig.translateSourceQps(source, qps);
|
||||
}
|
||||
// 保存到文件
|
||||
CustomConfig.store();
|
||||
// 退出
|
||||
onDestroy();
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
|
||||
import cn.octopusyan.dayzmodtranslator.util.FileUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Cfg文件转换工具类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class CfgConvertUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CfgConvertUtil.class);
|
||||
private static CfgConvertUtil util;
|
||||
private static final String CfgConvert_DIR_PATH = AppConstant.DATA_DIR_PATH + File.separator + "CfgConvert";
|
||||
private static final File CfgConvert_DIR = new File(CfgConvert_DIR_PATH);
|
||||
private static final String CfgConvert_FILE_PATH = CfgConvert_DIR_PATH + File.separator + "CfgConvert.exe";
|
||||
private static final File CfgConvert_FILE = new File(CfgConvert_FILE_PATH);
|
||||
private static final String COMMAND = CfgConvert_FILE_PATH + " %s -dst %s %s";
|
||||
|
||||
private CfgConvertUtil() {
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (util == null) {
|
||||
util = new CfgConvertUtil();
|
||||
}
|
||||
// 检查pbo解析文件
|
||||
util.checkCfgConvert();
|
||||
}
|
||||
|
||||
public static synchronized CfgConvertUtil getInstance() {
|
||||
if (util == null)
|
||||
throw new RuntimeException("are you ready ?");
|
||||
return util;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Cfg转换文件
|
||||
*/
|
||||
private void checkCfgConvert() {
|
||||
if (!CfgConvert_FILE.exists()) initCfgConvert();
|
||||
}
|
||||
|
||||
private void initCfgConvert() {
|
||||
try {
|
||||
FileUtils.forceMkdir(CfgConvert_DIR);
|
||||
String cfgConvertFileName = "CfgConvert.exe";
|
||||
FileUtil.copyFile(Objects.requireNonNull(CfgConvertUtil.class.getResourceAsStream("/static/CfgConvert/" + cfgConvertFileName)), new File(CfgConvert_DIR_PATH + File.separator + cfgConvertFileName));
|
||||
} catch (IOException e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void toTxt(File binFile, String outPath, OnToTxtListener onToTxtListener) {
|
||||
String fileName = binFile.getAbsolutePath();
|
||||
ProcessesUtil.exec(toTxtCommand(binFile, outPath), new ProcessesUtil.OnExecuteListener() {
|
||||
@Override
|
||||
public void onExecute(String msg) {
|
||||
logger.info(fileName + " : " + msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteSuccess(int exitValue) {
|
||||
logger.info(fileName + " : to txt success");
|
||||
String outFilePath = outFilePath(binFile, outPath, ".cpp");
|
||||
if (onToTxtListener != null) {
|
||||
onToTxtListener.onToTxtSuccess(outFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteError(Exception e) {
|
||||
logger.error(fileName + " : to txt error", e);
|
||||
if (onToTxtListener != null) {
|
||||
onToTxtListener.onToTxtError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteOver() {
|
||||
logger.info(fileName + " : to txt end...");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void toBin(File cppFile) {
|
||||
ProcessesUtil.exec(toBinCommand(cppFile, cppFile.getParentFile().getAbsolutePath()), new ProcessesUtil.OnExecuteListener() {
|
||||
@Override
|
||||
public void onExecute(String msg) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteSuccess(int exitValue) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteError(Exception e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteOver() {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String toBinCommand(File cppFile, String outPath) {
|
||||
String outFilePath = outFilePath(cppFile, outPath, ".bin");
|
||||
return String.format(COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private String toTxtCommand(File binFile, String outPath) {
|
||||
String outFilePath = outFilePath(binFile, outPath, ".cpp");
|
||||
return String.format(COMMAND, "-txt", outFilePath, binFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private String outFilePath(File file, String outPath, String suffix) {
|
||||
return outPath + File.separator + FileUtil.mainName(file) + suffix;
|
||||
}
|
||||
|
||||
public interface OnToTxtListener {
|
||||
void onToTxtSuccess(String txtFilePath);
|
||||
|
||||
void onToTxtError(Exception e);
|
||||
}
|
||||
}
|
@ -1,628 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.word.WordItem;
|
||||
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.FileUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil;
|
||||
import javafx.application.Platform;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.LineIterator;
|
||||
import org.apache.commons.io.filefilter.NameFileFilter;
|
||||
import org.apache.commons.io.filefilter.TrueFileFilter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* PBO 工具类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
* @see <a href="https://github.com/winseros/pboman3">https://github.com/winseros/pboman3</a>
|
||||
*/
|
||||
public class PBOUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PBOUtil.class);
|
||||
private static PBOUtil util;
|
||||
private static final String PBOC_DIR_PATH = AppConstant.DATA_DIR_PATH + File.separator + "pboman";
|
||||
private static final File PBOC_DIR = new File(PBOC_DIR_PATH);
|
||||
private static final String PBOC_FILE_PATH = PBOC_DIR_PATH + File.separator + "pboc.exe";
|
||||
private static final File PBOC_FILE = new File(PBOC_FILE_PATH);
|
||||
private static final String UNPACK_COMMAND = PBOC_FILE_PATH + " unpack -o " + AppConstant.TMP_DIR_PATH + " %s";
|
||||
private static final String PACK_COMMAND = PBOC_FILE_PATH + " pack -o %s %s";
|
||||
private OnPackListener onPackListener;
|
||||
private OnUnpackListener onUnpackListener;
|
||||
private OnFindTransWordListener onFindTransWordListener;
|
||||
private String unpackPath;
|
||||
private CfgConvertUtil cfgConvertUtil;
|
||||
private static final String FILE_NAME_STRING_TABLE = "stringtable.csv";
|
||||
private static final String FILE_NAME_CONFIG_BIN = "config.bin";
|
||||
private static final String FILE_NAME_CONFIG_CPP = "config.cpp";
|
||||
|
||||
private PBOUtil() {
|
||||
}
|
||||
|
||||
public static void init(CfgConvertUtil cfgConvertUtil) {
|
||||
if (util == null) {
|
||||
util = new PBOUtil();
|
||||
}
|
||||
// cfg转换工具
|
||||
util.cfgConvertUtil = cfgConvertUtil;
|
||||
// 检查pbo解析文件
|
||||
util.checkPboc();
|
||||
}
|
||||
|
||||
public static synchronized PBOUtil getInstance() {
|
||||
if (util == null)
|
||||
throw new RuntimeException("are you ready ?");
|
||||
return util;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置打包监听器
|
||||
*
|
||||
* @param onPackListener 打包监听器
|
||||
*/
|
||||
public void setOnPackListener(OnPackListener onPackListener) {
|
||||
this.onPackListener = onPackListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置解包监听器
|
||||
*
|
||||
* @param onUnpackListener 监听器
|
||||
*/
|
||||
public void setOnUnpackListener(OnUnpackListener onUnpackListener) {
|
||||
this.onUnpackListener = onUnpackListener;
|
||||
}
|
||||
|
||||
public void setOnFindTransWordListener(OnFindTransWordListener onFindTransWordListener) {
|
||||
this.onFindTransWordListener = onFindTransWordListener;
|
||||
}
|
||||
|
||||
private void checkPboc() {
|
||||
if (!PBOC_FILE.exists()) initPboc();
|
||||
}
|
||||
|
||||
private void initPboc() {
|
||||
try {
|
||||
FileUtils.forceMkdir(PBOC_DIR);
|
||||
String pbocFileName = "pboc.exe";
|
||||
String dllFileName = "Qt6Core.dll";
|
||||
FileUtil.copyFile(Objects.requireNonNull(PBOUtil.class.getResourceAsStream("/static/pboc/" + pbocFileName)), new File(PBOC_DIR_PATH + File.separator + pbocFileName));
|
||||
FileUtil.copyFile(Objects.requireNonNull(PBOUtil.class.getResourceAsStream("/static/pboc/" + dllFileName)), new File(PBOC_DIR_PATH + File.separator + dllFileName));
|
||||
} catch (IOException e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压PBO文件
|
||||
*
|
||||
* @param file PBO文件
|
||||
*/
|
||||
public void unpack(File file) {
|
||||
// 检查pbo解包程序
|
||||
checkPboc();
|
||||
// 清理缓存
|
||||
clear();
|
||||
|
||||
if (onUnpackListener != null) {
|
||||
onUnpackListener.onStart();
|
||||
}
|
||||
|
||||
String filePath = file.getAbsolutePath();
|
||||
if (filePath.contains(" ")) filePath = "\"" + filePath + "\"";
|
||||
|
||||
String command = String.format(UNPACK_COMMAND, filePath);
|
||||
logger.info(command);
|
||||
try {
|
||||
FileUtils.forceMkdir(new File(AppConstant.TMP_DIR_PATH));
|
||||
|
||||
// 执行命令
|
||||
ProcessesUtil.exec(command, new ProcessesUtil.OnExecuteListener() {
|
||||
@Override
|
||||
public void onExecute(String msg) {
|
||||
logger.info(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteSuccess(int exitValue) {
|
||||
if (exitValue != 0) {
|
||||
String msg = "打开PBO文件失败!";
|
||||
logger.error(msg);
|
||||
if (onUnpackListener != null) {
|
||||
Platform.runLater(() -> {
|
||||
onUnpackListener.onUnpackError(msg);
|
||||
onUnpackListener.onUnpackOver();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
logger.info("打开PBO文件成功!");
|
||||
unpackPath = AppConstant.TMP_DIR_PATH + File.separator + FileUtil.mainName(file);
|
||||
if (onUnpackListener != null) {
|
||||
Platform.runLater(() -> {
|
||||
onUnpackListener.onUnpackSuccess(unpackPath);
|
||||
onUnpackListener.onUnpackOver();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteError(Exception e) {
|
||||
logger.error("", e);
|
||||
if (onUnpackListener != null) {
|
||||
Platform.runLater(() -> {
|
||||
onUnpackListener.onUnpackError(e.getMessage());
|
||||
onUnpackListener.onUnpackOver();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteOver() {
|
||||
if (onUnpackListener != null) {
|
||||
Platform.runLater(() -> onUnpackListener.onUnpackOver());
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("", e);
|
||||
if (onUnpackListener != null) {
|
||||
Platform.runLater(() -> {
|
||||
onUnpackListener.onUnpackError(e.getMessage());
|
||||
onUnpackListener.onUnpackOver();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待翻译单词列表
|
||||
*/
|
||||
public void startFindWord() {
|
||||
// 检查pbo解包文件
|
||||
if (unpackPath == null || StringUtils.isBlank(unpackPath))
|
||||
throw new RuntimeException("No PBO file was obtained !");
|
||||
|
||||
ThreadPoolManager.getInstance().execute(() -> {
|
||||
if (hasStringTable()) {
|
||||
List<WordItem> worlds = new ArrayList<>(readCsvFile());
|
||||
if (onFindTransWordListener != null) {
|
||||
Platform.runLater(() -> onFindTransWordListener.onFoundWords(worlds, true));
|
||||
}
|
||||
} else {
|
||||
findConfigWord();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取csv中 原文及 简中单词
|
||||
*
|
||||
* @return 待翻译语句列表
|
||||
*/
|
||||
private List<WordCsvItem> readCsvFile() {
|
||||
List<WordCsvItem> list = new ArrayList<>();
|
||||
AtomicInteger position = new AtomicInteger(0);
|
||||
File stringTable = new File(unpackPath + File.separator + FILE_NAME_STRING_TABLE);
|
||||
try (LineIterator it = FileUtils.lineIterator(stringTable, StandardCharsets.UTF_8.name())) {
|
||||
while (it.hasNext()) {
|
||||
String line = it.nextLine();
|
||||
|
||||
if (line.isEmpty() || line.startsWith("//") || line.startsWith("\"Language\"")) {
|
||||
position.addAndGet(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 原句
|
||||
int startIndex = line.indexOf(",\"") + 2;
|
||||
int endIndex = line.indexOf("\"", startIndex);
|
||||
String original = line.substring(startIndex, endIndex);
|
||||
|
||||
// 中文
|
||||
startIndex = StringUtils.ordinalIndexOf(line, ",\"", 11) + 2;
|
||||
endIndex = line.indexOf("\"", startIndex);
|
||||
int[] chinesePosition = new int[]{startIndex, endIndex};
|
||||
String chinese = line.substring(startIndex, endIndex);
|
||||
|
||||
// 简中
|
||||
startIndex = StringUtils.ordinalIndexOf(line, ",\"", 14) + 2;
|
||||
endIndex = line.indexOf("\"", startIndex);
|
||||
int[] chineseSimpPosition = new int[]{startIndex, endIndex};
|
||||
String chineseSimp = line.substring(startIndex, endIndex);
|
||||
|
||||
// 添加单词
|
||||
list.add(new WordCsvItem(stringTable, position.get(), original, chinese, chinesePosition, chineseSimp, chineseSimpPosition));
|
||||
|
||||
position.addAndGet(1);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparingInt(WordItem::getLines));
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 config.bin 文件内 可翻译内容
|
||||
*/
|
||||
private void findConfigWord() {
|
||||
|
||||
// 搜索所有的 config.bin 文件,并按路径解包到bak文件夹
|
||||
List<File> files = new ArrayList<>(FileUtils.listFiles(new File(unpackPath), new NameFileFilter(FILE_NAME_CONFIG_BIN, FILE_NAME_CONFIG_CPP), TrueFileFilter.INSTANCE));
|
||||
|
||||
files.forEach(file -> {
|
||||
|
||||
// 转换bin文件为cpp可读取文件
|
||||
if (file.getName().endsWith("bin")) {
|
||||
cfgConvertUtil.toTxt(file, file.getParentFile().getAbsolutePath(), new CfgConvertUtil.OnToTxtListener() {
|
||||
@Override
|
||||
public void onToTxtSuccess(String txtFilePath) {
|
||||
// 读取 cpp 文件
|
||||
readCppFile(new File(txtFilePath), file.equals(files.get(files.size() - 1)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToTxtError(Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
AlertUtil.exception(e).content(FILE_NAME_CONFIG_BIN + "文件转换失败").show();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 读取 cpp 文件
|
||||
readCppFile(file, file.equals(files.get(files.size() - 1)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static final Pattern pattern = Pattern.compile(".*((displayName|descriptionShort).?=.?\").*");
|
||||
|
||||
/**
|
||||
* 读取cpp文件,查询可翻译文本
|
||||
*
|
||||
* @param file cpp文件
|
||||
*/
|
||||
private void readCppFile(File file, boolean isEnd) {
|
||||
List<WordItem> list = new ArrayList<>();
|
||||
AtomicInteger lines = new AtomicInteger(0);
|
||||
try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
|
||||
while (it.hasNext()) {
|
||||
String line = it.nextLine();
|
||||
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
if (!line.contains("$") && matcher.find()) {
|
||||
|
||||
String name = matcher.group(1);
|
||||
|
||||
// 原始文本
|
||||
int startIndex = line.indexOf(name) + name.length();
|
||||
|
||||
int endIndex = line.indexOf("\"", startIndex);
|
||||
String original;
|
||||
try {
|
||||
original = line.substring(startIndex, endIndex);
|
||||
} catch (Exception e) {
|
||||
lines.addAndGet(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加单词
|
||||
if (!"".endsWith(original) && !containsChinese(original)) {
|
||||
list.add(new WordItem(file, lines.get(), original, "", new int[]{startIndex, endIndex}));
|
||||
}
|
||||
}
|
||||
|
||||
lines.addAndGet(1);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparingInt(WordItem::getLines));
|
||||
|
||||
if (onFindTransWordListener != null) {
|
||||
Platform.runLater(() -> onFindTransWordListener.onFoundWords(list, isEnd));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定字符串是否存在中文
|
||||
*
|
||||
* @param str 字符串
|
||||
* @return 是否存在中文
|
||||
*/
|
||||
private boolean containsChinese(String str) {
|
||||
// [\u4e00-\u9fa5]
|
||||
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打包PBO文件
|
||||
*/
|
||||
public void pack(List<WordItem> words) {
|
||||
if (onPackListener != null) {
|
||||
Platform.runLater(() -> onPackListener.onStart());
|
||||
}
|
||||
|
||||
ThreadPoolManager.getInstance().execute(() -> {
|
||||
File unpackDir;
|
||||
if (StringUtils.isBlank(unpackPath)
|
||||
|| !(unpackDir = new File(unpackPath)).exists()
|
||||
|| !unpackDir.isDirectory()
|
||||
) {
|
||||
AlertUtil.error("未获取到打开的pbo文件!").show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 写入翻译后文本
|
||||
try {
|
||||
writeWords(words);
|
||||
} catch (Exception e) {
|
||||
logger.error("writeWords error", e);
|
||||
if (onPackListener != null) {
|
||||
Platform.runLater(() -> {
|
||||
onPackListener.onPackOver();
|
||||
onPackListener.onPackError("writeWords error ==> " + e.getMessage());
|
||||
});
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// 打包文件临时保存路径
|
||||
String packFilePath = unpackPath + ".pbo";
|
||||
File packFile = new File(packFilePath);
|
||||
if (packFile.exists()) {
|
||||
// 如果存在则删除
|
||||
FileUtils.deleteQuietly(packFile);
|
||||
}
|
||||
|
||||
// 执行打包指令
|
||||
String command = String.format(PACK_COMMAND, AppConstant.TMP_DIR_PATH, unpackPath);
|
||||
logger.info(command);
|
||||
ProcessesUtil.exec(command, new ProcessesUtil.OnExecuteListener() {
|
||||
@Override
|
||||
public void onExecute(String msg) {
|
||||
logger.info(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteSuccess(int exitValue) {
|
||||
Platform.runLater(() -> {
|
||||
if (exitValue != 0) {
|
||||
logger.error("保存PBO文件失败!");
|
||||
if (onPackListener != null) {
|
||||
onPackListener.onPackOver();
|
||||
onPackListener.onPackError("保存PBO文件失败!");
|
||||
}
|
||||
} else {
|
||||
if (onPackListener != null) {
|
||||
onPackListener.onPackOver();
|
||||
onPackListener.onPackSuccess(packFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteError(Exception e) {
|
||||
logger.error("保存PBO文件失败!");
|
||||
if (onPackListener != null) {
|
||||
Platform.runLater(() -> {
|
||||
onPackListener.onPackOver();
|
||||
onPackListener.onPackError(e.getMessage());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteOver() {
|
||||
if (onPackListener != null) {
|
||||
Platform.runLater(() -> onPackListener.onPackOver());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入翻译文本
|
||||
*
|
||||
* @param words 已经翻译好的文本对象
|
||||
*/
|
||||
private void writeWords(List<WordItem> words) throws Exception {
|
||||
Map<File, List<WordItem>> wordMap = words.stream()
|
||||
.collect(Collectors.groupingBy(WordItem::getFile, Collectors.toList()));
|
||||
|
||||
AtomicInteger progress = new AtomicInteger(0);
|
||||
|
||||
// 0 执行成功 大于0 执行失败
|
||||
List<Future<Integer>> result = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<File, List<WordItem>> entry : wordMap.entrySet()) {
|
||||
Future<Integer> submit = ThreadPoolManager.getInstance().submit(() -> {
|
||||
try {
|
||||
entry.getValue().sort(Comparator.comparingInt(WordItem::getLines));
|
||||
|
||||
File file = entry.getKey();
|
||||
// 创建备份文件
|
||||
File bakFile = getWordBakFile(file);
|
||||
// 判断重复打包时 备份文件处理
|
||||
if (!bakFile.exists()) {
|
||||
FileUtils.copyFile(file, bakFile);
|
||||
}
|
||||
|
||||
// 清空原始文件
|
||||
FileWriter fileWriter = new FileWriter(file);
|
||||
fileWriter.write("");
|
||||
fileWriter.flush();
|
||||
fileWriter.close();
|
||||
|
||||
// 遍历拼接翻译文本并写入
|
||||
long lines = 0;
|
||||
String line;
|
||||
LineIterator it = FileUtils.lineIterator(bakFile, StandardCharsets.UTF_8.name());
|
||||
while (it.hasNext() && !entry.getValue().isEmpty()) {
|
||||
line = it.nextLine();
|
||||
|
||||
WordItem item = entry.getValue().get(0);
|
||||
int[] ip = item.getPosition();
|
||||
|
||||
// 拼接翻译后的文本
|
||||
if (lines == item.getLines()) {
|
||||
if (item instanceof WordCsvItem csv) {
|
||||
int[] simp = csv.getPositionSimp();
|
||||
// 判断 ip 是否比 simp 靠前
|
||||
boolean tag = ip[0] < simp[0];
|
||||
line = line.substring(0, (tag ? ip : simp)[0])
|
||||
+ (tag ? csv.getChinese() : csv.getChineseSimp())
|
||||
+ line.substring((tag ? ip : simp)[1], (tag ? simp : ip)[0])
|
||||
+ (tag ? csv.getChineseSimp() : csv.getChinese())
|
||||
+ line.substring((tag ? simp : ip)[1]);
|
||||
} else {
|
||||
try {
|
||||
line = line.substring(0, ip[0])
|
||||
+ item.getChinese()
|
||||
+ line.substring(ip[1]);
|
||||
} catch (Exception e) {
|
||||
System.out.println(line);
|
||||
}
|
||||
}
|
||||
|
||||
entry.getValue().remove(item);
|
||||
if (onPackListener != null) {
|
||||
Platform.runLater(() -> onPackListener.onProgress(progress.addAndGet(1), words.size()));
|
||||
}
|
||||
}
|
||||
|
||||
// 写入原始文件
|
||||
FileUtils.writeStringToFile(file, line + System.lineSeparator(), StandardCharsets.UTF_8, true);
|
||||
|
||||
lines++;
|
||||
}
|
||||
|
||||
// 关闭流
|
||||
IOUtils.closeQuietly(it);
|
||||
|
||||
// cpp 文件需要 转换为 bin
|
||||
if (file.getName().endsWith("cpp")) {
|
||||
File binFile = new File(file.getParent() + File.separator + FileUtil.mainName(file) + ".bin");
|
||||
if (binFile.exists()) {
|
||||
// 转为bin文件
|
||||
cfgConvertUtil.toBin(file);
|
||||
// 删除cpp文件
|
||||
FileUtils.deleteQuietly(file);
|
||||
}
|
||||
// 同目录下不存在bin文件,说明原始文件为cpp 无需转换
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("写入翻译文本失败", e);
|
||||
// 执行失败
|
||||
return 1;
|
||||
}
|
||||
// 执行成功
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 添加执行结果
|
||||
result.add(submit);
|
||||
}
|
||||
|
||||
for (Future<Integer> future : result) {
|
||||
if (future.get() > 0)
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private File getWordBakFile(File wordFile) {
|
||||
return new File(wordFile.getAbsolutePath().replace(unpackPath, AppConstant.BAK_FILE_PATH));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有国际化翻译文件
|
||||
*
|
||||
* @return 是否有 <code>stringtable.csv</code> 文件
|
||||
*/
|
||||
private boolean hasStringTable() {
|
||||
List<String> fileNames = FileUtil.listFileNames(unpackPath);
|
||||
return fileNames.stream().anyMatch(FILE_NAME_STRING_TABLE::equals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存文件
|
||||
*/
|
||||
public static void clear() {
|
||||
File tmpDest = new File(AppConstant.TMP_DIR_PATH);
|
||||
|
||||
if (tmpDest.exists())
|
||||
FileUtils.deleteQuietly(tmpDest);
|
||||
}
|
||||
|
||||
public interface OnUnpackListener {
|
||||
/**
|
||||
* 开始解包
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* 解包完成
|
||||
*
|
||||
* @param unpackDirPath 解包文件夹绝对路径
|
||||
*/
|
||||
void onUnpackSuccess(String unpackDirPath);
|
||||
|
||||
/**
|
||||
* 解包失败
|
||||
*
|
||||
* @param msg 失败信息
|
||||
*/
|
||||
void onUnpackError(String msg);
|
||||
|
||||
void onUnpackOver();
|
||||
}
|
||||
|
||||
public interface OnPackListener {
|
||||
/**
|
||||
* 开始解包
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
void onProgress(long current, long all);
|
||||
|
||||
/**
|
||||
* 打包完成
|
||||
*/
|
||||
void onPackSuccess(File packFile);
|
||||
|
||||
/**
|
||||
* 打包失败
|
||||
*
|
||||
* @param msg 失败信息
|
||||
*/
|
||||
void onPackError(String msg);
|
||||
|
||||
void onPackOver();
|
||||
}
|
||||
|
||||
public interface OnFindTransWordListener {
|
||||
void onFoundWords(List<WordItem> worlds, boolean isOver);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.file;
|
||||
|
||||
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritablePixelFormat;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.filechooser.FileSystemView;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.io.File;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
/**
|
||||
* 文件图标
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class FileIcon {
|
||||
//设置图标
|
||||
public static Canvas getFileIconToNode(File file) {
|
||||
//获取系统文件的图标
|
||||
Image image = ((ImageIcon) FileSystemView.getFileSystemView().getSystemIcon(file)).getImage();
|
||||
//构建图片缓冲区,设定图片缓冲区的大小和背景,背景为透明
|
||||
BufferedImage bi = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.BITMASK);
|
||||
//把图片画到图片缓冲区
|
||||
bi.getGraphics().drawImage(image, 0, 0, null);
|
||||
//将图片缓冲区的数据转换成int型数组
|
||||
int[] data = ((DataBufferInt) bi.getData().getDataBuffer()).getData();
|
||||
//获得写像素的格式模版
|
||||
WritablePixelFormat<IntBuffer> pixelFormat = PixelFormat.getIntArgbInstance();
|
||||
//新建javafx的画布
|
||||
Canvas canvas = new Canvas(bi.getWidth() + 2, bi.getHeight() + 2);
|
||||
//获取像素的写入器
|
||||
PixelWriter pixelWriter = canvas.getGraphicsContext2D().getPixelWriter();
|
||||
//根据写像素的格式模版把int型数组写到画布
|
||||
pixelWriter.setPixels(0, 0, bi.getWidth(), bi.getHeight(), pixelFormat, data, 0, bi.getWidth());
|
||||
//设置树节点的图标
|
||||
return canvas;
|
||||
|
||||
}
|
||||
|
||||
public static String getFileName(File file) {
|
||||
return FileSystemView.getFileSystemView().getSystemDisplayName(file);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.file;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.TreeItem;
|
||||
|
||||
import javax.swing.filechooser.FileSystemView;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 文件目录
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class FileTreeItem extends TreeItem<String> {
|
||||
public static File ROOT_FILE = FileSystemView.getFileSystemView().getRoots()[0];
|
||||
|
||||
//判断树节点是否被初始化,没有初始化为真
|
||||
private boolean notInitialized = true;
|
||||
|
||||
private final File file;
|
||||
private final Function<File, File[]> supplier;
|
||||
|
||||
public FileTreeItem(File file) {
|
||||
super(FileIcon.getFileName(file), FileIcon.getFileIconToNode(file));
|
||||
this.file = file;
|
||||
supplier = (File f) -> {
|
||||
if (((FileTreeItem) this.getParent()).getFile() == ROOT_FILE) {
|
||||
String name = FileIcon.getFileName(f);
|
||||
if (name.equals("网络") || name.equals("家庭组")) {
|
||||
return new File[0];
|
||||
}
|
||||
}
|
||||
return f.listFiles();
|
||||
};
|
||||
}
|
||||
|
||||
public FileTreeItem(File file, Function<File, File[]> supplier) {
|
||||
super(FileIcon.getFileName(file), FileIcon.getFileIconToNode(file));
|
||||
this.file = file;
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
|
||||
//重写getchildren方法,让节点被展开时加载子目录
|
||||
@Override
|
||||
public ObservableList<TreeItem<String>> getChildren() {
|
||||
|
||||
ObservableList<TreeItem<String>> children = super.getChildren();
|
||||
//没有加载子目录时,则加载子目录作为树节点的孩子
|
||||
if (this.notInitialized && this.isExpanded()) {
|
||||
|
||||
this.notInitialized = false; //设置没有初始化为假
|
||||
|
||||
/*
|
||||
*判断树节点的文件是否是目录,
|
||||
*如果是目录,着把目录里面的所有的文件添加入树节点的孩子中。
|
||||
*/
|
||||
if (this.getFile().isDirectory()) {
|
||||
List<FileTreeItem> fileList = new ArrayList<>();
|
||||
for (File f : supplier.apply(this.getFile())) {
|
||||
if (f.isDirectory())
|
||||
children.add(new FileTreeItem(f));
|
||||
else
|
||||
fileList.add(new FileTreeItem(f));
|
||||
}
|
||||
children.addAll(fileList);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
//重写叶子方法,如果该文件不是目录,则返回真
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
|
||||
return !file.isDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the file
|
||||
*/
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.http;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.net.Authenticator;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Http配置参数
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class HttpConfig {
|
||||
private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
|
||||
/**
|
||||
* http版本
|
||||
*/
|
||||
private HttpClient.Version version = HttpClient.Version.HTTP_2;
|
||||
|
||||
/**
|
||||
* 转发策略
|
||||
*/
|
||||
private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
|
||||
|
||||
/**
|
||||
* 线程池
|
||||
*/
|
||||
private Executor executor;
|
||||
|
||||
/**
|
||||
* 认证
|
||||
*/
|
||||
private Authenticator authenticator;
|
||||
|
||||
/**
|
||||
* 代理
|
||||
*/
|
||||
private ProxySelector proxySelector;
|
||||
|
||||
/**
|
||||
* CookieHandler
|
||||
*/
|
||||
private CookieHandler cookieHandler;
|
||||
|
||||
/**
|
||||
* sslContext
|
||||
*/
|
||||
private SSLContext sslContext;
|
||||
|
||||
/**
|
||||
* sslParams
|
||||
*/
|
||||
private SSLParameters sslParameters;
|
||||
|
||||
/**
|
||||
* 连接超时时间毫秒
|
||||
*/
|
||||
private int connectTimeout = 10000;
|
||||
|
||||
/**
|
||||
* 默认读取数据超时时间
|
||||
*/
|
||||
private int defaultReadTimeout = 1200000;
|
||||
|
||||
|
||||
public HttpConfig() {
|
||||
TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0]; // Not relevant.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
}};
|
||||
sslParameters = new SSLParameters();
|
||||
sslParameters.setEndpointIdentificationAlgorithm("");
|
||||
|
||||
|
||||
try {
|
||||
sslContext = SSLContext.getInstance("TLSv1.2");
|
||||
System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
|
||||
sslContext.init(null, trustAllCertificates, new SecureRandom());
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public HttpClient.Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(HttpClient.Version version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public int getConnectTimeout() {
|
||||
return connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(int connectTimeout) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
|
||||
public HttpClient.Redirect getRedirect() {
|
||||
return redirect;
|
||||
}
|
||||
|
||||
public void setRedirect(HttpClient.Redirect redirect) {
|
||||
this.redirect = redirect;
|
||||
}
|
||||
|
||||
public Executor getExecutor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
public void setExecutor(Executor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public Authenticator getAuthenticator() {
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
public void setAuthenticator(Authenticator authenticator) {
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
public ProxySelector getProxySelector() {
|
||||
return proxySelector;
|
||||
}
|
||||
|
||||
public void setProxySelector(ProxySelector proxySelector) {
|
||||
this.proxySelector = proxySelector;
|
||||
}
|
||||
|
||||
public CookieHandler getCookieHandler() {
|
||||
return cookieHandler;
|
||||
}
|
||||
|
||||
public void setCookieHandler(CookieHandler cookieHandler) {
|
||||
this.cookieHandler = cookieHandler;
|
||||
}
|
||||
|
||||
public int getDefaultReadTimeout() {
|
||||
return defaultReadTimeout;
|
||||
}
|
||||
|
||||
public void setDefaultReadTimeout(int defaultReadTimeout) {
|
||||
this.defaultReadTimeout = defaultReadTimeout;
|
||||
}
|
||||
|
||||
public SSLContext getSslContext() {
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
public SSLParameters getSslParameters() {
|
||||
return sslParameters;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.thread;
|
||||
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 线程池管理类
|
||||
*/
|
||||
public final class ThreadPoolManager extends ThreadPoolExecutor {
|
||||
|
||||
private static volatile ThreadPoolManager sInstance;
|
||||
|
||||
private static ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
private ThreadPoolManager() {
|
||||
super(32,
|
||||
200,
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(200),
|
||||
new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX),
|
||||
new ThreadPoolExecutor.DiscardPolicy());
|
||||
}
|
||||
|
||||
public static ThreadPoolManager getInstance() {
|
||||
if (sInstance == null) sInstance = new ThreadPoolManager();
|
||||
return sInstance;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate;
|
||||
|
||||
/**
|
||||
* API 密钥配置
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class ApiKey {
|
||||
private String appid;
|
||||
private String apiKey;
|
||||
|
||||
public ApiKey() {
|
||||
}
|
||||
|
||||
public ApiKey(String appid, String apiKey) {
|
||||
this.appid = appid;
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getAppid() {
|
||||
return appid;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate;
|
||||
|
||||
/**
|
||||
* 翻译引擎类型
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public enum TranslateSource {
|
||||
FREE_GOOGLE("free_google", "谷歌(免费)", false, 50),
|
||||
BAIDU("baidu", "百度(需认证)", true),
|
||||
|
||||
;
|
||||
private final String name;
|
||||
private final String label;
|
||||
private final boolean needApiKey;
|
||||
private Integer defaultQps;
|
||||
|
||||
TranslateSource(String name, String label, boolean needApiKey) {
|
||||
// 设置接口默认qps=10
|
||||
this(name, label, needApiKey, 10);
|
||||
}
|
||||
|
||||
TranslateSource(String name, String label, boolean needApiKey, int defaultQps) {
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
this.needApiKey = needApiKey;
|
||||
this.defaultQps = defaultQps;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public boolean needApiKey() {
|
||||
return needApiKey;
|
||||
}
|
||||
|
||||
public Integer getDefaultQps() {
|
||||
return defaultQps;
|
||||
}
|
||||
|
||||
public String getDefaultQpsStr() {
|
||||
return String.valueOf(defaultQps);
|
||||
}
|
||||
|
||||
public static TranslateSource get(String type) {
|
||||
for (TranslateSource value : values()) {
|
||||
if (value.getName().equals(type))
|
||||
return value;
|
||||
}
|
||||
throw new RuntimeException("类型不存在");
|
||||
}
|
||||
|
||||
public static TranslateSource getByLabel(String label) {
|
||||
for (TranslateSource value : values()) {
|
||||
if (value.getLabel().equals(label))
|
||||
return value;
|
||||
}
|
||||
throw new RuntimeException("类型不存在");
|
||||
}
|
||||
}
|
@ -1,227 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadFactory;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactory;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactoryImpl;
|
||||
import javafx.application.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 翻译工具
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class TranslateUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TranslateUtil.class);
|
||||
private static TranslateUtil util;
|
||||
private static final DelayQueue<DelayWord> delayQueue = new DelayQueue<>();
|
||||
private final TranslateFactory factory;
|
||||
private static WordThread wordThread;
|
||||
private static ThreadPoolExecutor threadPoolExecutor;
|
||||
|
||||
private TranslateUtil(TranslateFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public static TranslateUtil getInstance() {
|
||||
if (util == null) {
|
||||
util = new TranslateUtil(TranslateFactoryImpl.getInstance());
|
||||
}
|
||||
return util;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交翻译任务
|
||||
*
|
||||
* @param index 序号
|
||||
* @param original 原始文本
|
||||
* @param listener 翻译结果回调 (主线程)
|
||||
*/
|
||||
public void translate(int index, String original, OnTranslateListener listener) {
|
||||
|
||||
// 设置延迟时间
|
||||
DelayWord word = factory.getDelayWord(CustomConfig.translateSource(), index, original, listener);
|
||||
// 添加到延迟队列
|
||||
delayQueue.add(word);
|
||||
|
||||
if (wordThread == null) {
|
||||
wordThread = new WordThread();
|
||||
wordThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除翻译任务
|
||||
*/
|
||||
public void clear() {
|
||||
// 尝试停止所有线程
|
||||
getThreadPoolExecutor().shutdownNow();
|
||||
// 清空队列
|
||||
delayQueue.clear();
|
||||
// 设置停止标记
|
||||
if (wordThread != null)
|
||||
wordThread.setStop(true);
|
||||
wordThread = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取翻译任务用线程池
|
||||
*/
|
||||
public static ThreadPoolExecutor getThreadPoolExecutor() {
|
||||
if (threadPoolExecutor == null || threadPoolExecutor.isShutdown()) {
|
||||
threadPoolExecutor = new ThreadPoolExecutor(32,
|
||||
200,
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(200),
|
||||
new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX),
|
||||
new ThreadPoolExecutor.DiscardPolicy());
|
||||
}
|
||||
return threadPoolExecutor;
|
||||
}
|
||||
|
||||
public interface OnTranslateListener {
|
||||
void onTranslate(String result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟翻译对象
|
||||
*/
|
||||
public static class DelayWord implements Delayed {
|
||||
private TranslateSource source;
|
||||
private final int index;
|
||||
private final String original;
|
||||
private final OnTranslateListener listener;
|
||||
private long time;
|
||||
|
||||
public DelayWord(int index, String original, OnTranslateListener listener) {
|
||||
this.index = index;
|
||||
this.original = original;
|
||||
this.listener = listener;
|
||||
|
||||
}
|
||||
|
||||
public void setSource(TranslateSource source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public void setTime(long time, TimeUnit timeUnit) {
|
||||
this.time = System.currentTimeMillis() + (time > 0 ? timeUnit.toMillis(time) : 0);
|
||||
}
|
||||
|
||||
public TranslateSource getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public String getOriginal() {
|
||||
return original;
|
||||
}
|
||||
|
||||
public OnTranslateListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
return time - System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed o) {
|
||||
DelayWord word = (DelayWord) o;
|
||||
return Integer.compare(this.index, word.index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟队列处理线程
|
||||
*/
|
||||
private static class WordThread extends Thread {
|
||||
private boolean stop = false;
|
||||
|
||||
public void setStop(boolean stop) {
|
||||
this.stop = stop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
List<DelayWord> tmp = new ArrayList<>();
|
||||
while (!delayQueue.isEmpty()) {
|
||||
// 停止处理
|
||||
if (stop) {
|
||||
this.interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 取出待翻译文本
|
||||
DelayWord take = delayQueue.take();
|
||||
tmp.add(take);
|
||||
|
||||
if (tmp.size() < CustomConfig.translateSourceQps(take.source))
|
||||
continue;
|
||||
|
||||
tmp.forEach(word -> {
|
||||
try {
|
||||
getThreadPoolExecutor().execute(() -> {
|
||||
// 翻译
|
||||
try {
|
||||
String translate = util.factory.translate(word.getSource(), word.getOriginal());
|
||||
// 回调监听器
|
||||
if (word.getListener() != null)
|
||||
// 主线程处理翻译结果
|
||||
Platform.runLater(() -> word.getListener().onTranslate(translate));
|
||||
} catch (InterruptedException ignored) {
|
||||
} catch (Exception e) {
|
||||
logger.error("翻译出错", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("翻译出错", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
tmp.clear();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余
|
||||
tmp.forEach(word -> {
|
||||
try {
|
||||
ThreadPoolManager.getInstance().execute(() -> {
|
||||
// 翻译
|
||||
try {
|
||||
String translate = util.factory.translate(word.getSource(), word.getOriginal());
|
||||
// 回调监听器
|
||||
if (word.getListener() != null)
|
||||
// 主线程处理翻译结果
|
||||
Platform.runLater(() -> word.getListener().onTranslate(translate));
|
||||
} catch (Exception e) {
|
||||
logger.error("翻译出错", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("翻译出错", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
tmp.clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate.factory;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
|
||||
|
||||
/**
|
||||
* 翻译器接口
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public interface TranslateFactory {
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param source 翻译源
|
||||
* @param sourceString 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
String translate(TranslateSource source, String sourceString) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取延迟翻译对象
|
||||
*
|
||||
* @param source 翻译源
|
||||
* @param index 序号
|
||||
* @param original 原始文本
|
||||
* @param listener 监听器
|
||||
* @return 延迟翻译对象
|
||||
*/
|
||||
TranslateUtil.DelayWord getDelayWord(TranslateSource source, int index, String original, TranslateUtil.OnTranslateListener listener);
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate.factory;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.processor.AbstractTranslateProcessor;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.processor.BaiduTranslateProcessor;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.processor.FreeGoogleTranslateProcessor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 翻译处理器
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class TranslateFactoryImpl implements TranslateFactory {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TranslateFactoryImpl.class);
|
||||
private static TranslateFactoryImpl impl;
|
||||
private final Map<String, AbstractTranslateProcessor> processorMap = new HashMap<>();
|
||||
private final List<AbstractTranslateProcessor> processorList = new ArrayList<>();
|
||||
|
||||
private TranslateFactoryImpl() {
|
||||
}
|
||||
|
||||
public static synchronized TranslateFactoryImpl getInstance() {
|
||||
if (impl == null) {
|
||||
impl = new TranslateFactoryImpl();
|
||||
impl.initProcessor();
|
||||
}
|
||||
return impl;
|
||||
}
|
||||
|
||||
private void initProcessor() {
|
||||
processorList.addAll(Arrays.asList(
|
||||
new FreeGoogleTranslateProcessor(TranslateSource.FREE_GOOGLE),
|
||||
new BaiduTranslateProcessor(TranslateSource.BAIDU)
|
||||
));
|
||||
for (AbstractTranslateProcessor processor : processorList) {
|
||||
processorMap.put(processor.getSource(), processor);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractTranslateProcessor getProcessor(TranslateSource source) {
|
||||
return processorMap.get(source.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取延迟翻译对象
|
||||
*
|
||||
* @param source 翻译源
|
||||
* @param index 序号
|
||||
* @param original 原始文本
|
||||
* @param listener 监听器
|
||||
* @return 延迟翻译对象
|
||||
*/
|
||||
@Override
|
||||
public TranslateUtil.DelayWord getDelayWord(TranslateSource source, int index, String original, TranslateUtil.OnTranslateListener listener) {
|
||||
// 生产翻译对象
|
||||
TranslateUtil.DelayWord word = new TranslateUtil.DelayWord(index, original, listener);
|
||||
// 设置延迟
|
||||
getProcessor(source).setDelayTime(word);
|
||||
|
||||
return word;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译(英->中)
|
||||
* <p> TODO 切换语种
|
||||
*
|
||||
* @param source 翻译源
|
||||
* @param sourceString 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
@Override
|
||||
public String translate(TranslateSource source, String sourceString) throws Exception {
|
||||
return getProcessor(source).translate(sourceString);
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 翻译处理器抽象类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public abstract class AbstractTranslateProcessor implements TranslateProcessor {
|
||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
protected static final HttpUtil httpUtil = HttpUtil.getInstance();
|
||||
protected final TranslateSource translateSource;
|
||||
protected ApiKey apiKey;
|
||||
|
||||
public AbstractTranslateProcessor(TranslateSource translateSource) {
|
||||
this.translateSource = translateSource;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return translateSource.getName();
|
||||
}
|
||||
|
||||
public TranslateSource source() {
|
||||
return translateSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needApiKey() {
|
||||
return source().needApiKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredKey() {
|
||||
return CustomConfig.hasTranslateApiKey(source());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int qps() {
|
||||
return CustomConfig.translateSourceQps(source());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Api配置信息
|
||||
*/
|
||||
protected ApiKey getApiKey() {
|
||||
if (!configuredKey()) {
|
||||
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
|
||||
logger.error(message);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
String appid = CustomConfig.translateSourceAppid(source());
|
||||
String apikey = CustomConfig.translateSourceApikey(source());
|
||||
return new ApiKey(appid, apikey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String translate(String source) throws Exception {
|
||||
|
||||
if (needApiKey() && !configuredKey()) {
|
||||
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
|
||||
logger.error(message);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
return customTranslate(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param source 原始文本
|
||||
* @return 翻译结果
|
||||
*/
|
||||
public abstract String customTranslate(String source) throws Exception;
|
||||
|
||||
/**
|
||||
* 设置延迟对象
|
||||
*
|
||||
* @param word 带翻译单词
|
||||
*/
|
||||
public void setDelayTime(TranslateUtil.DelayWord word) {
|
||||
// 设置翻译源
|
||||
word.setSource(source());
|
||||
|
||||
// 设置翻译延迟
|
||||
int time = word.getIndex() / qps();
|
||||
word.setTime(time, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.word;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* csv单词对象
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class WordCsvItem extends WordItem {
|
||||
/**
|
||||
* 简体中文
|
||||
*/
|
||||
private StringProperty chineseSimp;
|
||||
|
||||
/**
|
||||
* 文件中坐标(简体中文)
|
||||
*/
|
||||
private int[] positionSimp;
|
||||
|
||||
public WordCsvItem() {
|
||||
}
|
||||
|
||||
public WordCsvItem(File stringTable, int lines, String original, String chineses, int[] position, String chineseSimp, int[] positionSimp) {
|
||||
super(stringTable, lines, original, chineses, position);
|
||||
this.chineseSimp = new SimpleStringProperty(chineseSimp);
|
||||
this.positionSimp = positionSimp;
|
||||
}
|
||||
|
||||
public StringProperty chineseSimpProperty() {
|
||||
return chineseSimp;
|
||||
}
|
||||
|
||||
public String getChineseSimp() {
|
||||
return chineseSimp.get();
|
||||
}
|
||||
|
||||
public void setChineseSimp(String chineseSimp) {
|
||||
this.chineseSimp.setValue(chineseSimp);
|
||||
}
|
||||
|
||||
public int[] getPositionSimp() {
|
||||
return positionSimp;
|
||||
}
|
||||
|
||||
public void setPositionSimp(int[] positionSimp) {
|
||||
this.positionSimp = positionSimp;
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.word;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 待翻译单词子项
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class WordItem {
|
||||
/**
|
||||
* 所在文件
|
||||
* <p>PS: 文本所在的文件
|
||||
*/
|
||||
private File file;
|
||||
|
||||
/**
|
||||
* 原始文本
|
||||
*/
|
||||
private StringProperty original;
|
||||
|
||||
/**
|
||||
* 汉化
|
||||
*/
|
||||
private StringProperty chinese;
|
||||
|
||||
/**
|
||||
* 行内下标
|
||||
*/
|
||||
private int[] position;
|
||||
|
||||
/**
|
||||
* 文件第几行
|
||||
*/
|
||||
private int lines;
|
||||
|
||||
public WordItem() {
|
||||
}
|
||||
|
||||
public WordItem(File file, int lines, String original, String chinese, int[] position) {
|
||||
this.file = file;
|
||||
this.original = new SimpleStringProperty(original);
|
||||
this.chinese = new SimpleStringProperty(chinese);
|
||||
this.position = position;
|
||||
this.lines = lines;
|
||||
}
|
||||
|
||||
public StringProperty originalProperty() {
|
||||
return original;
|
||||
}
|
||||
|
||||
public String getOriginal() {
|
||||
return original.get();
|
||||
}
|
||||
|
||||
public void setOriginal(String original) {
|
||||
this.original.setValue(original);
|
||||
}
|
||||
|
||||
public StringProperty chineseProperty() {
|
||||
return chinese;
|
||||
}
|
||||
|
||||
public String getChinese() {
|
||||
return chinese.get();
|
||||
}
|
||||
|
||||
public void setChinese(String chinese) {
|
||||
this.chinese.setValue(chinese);
|
||||
}
|
||||
|
||||
public int[] getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(int[] position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public int getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public void setLines(int lines) {
|
||||
this.lines = lines;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setFile(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 弹窗工具
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class AlertUtil {
|
||||
private static Window mOwner;
|
||||
private static Builder builder;
|
||||
|
||||
public static void initOwner(Stage stage) {
|
||||
AlertUtil.mOwner = stage;
|
||||
}
|
||||
|
||||
public static class Builder<T extends Dialog> {
|
||||
T alert;
|
||||
|
||||
public Builder(T alert) {
|
||||
this.alert = alert;
|
||||
if (mOwner != null) this.alert.initOwner(mOwner);
|
||||
}
|
||||
|
||||
public Builder<T> title(String title) {
|
||||
alert.setTitle(title);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> header(String header) {
|
||||
alert.setHeaderText(header);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> content(String content) {
|
||||
alert.setContentText(content);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> icon(String path) {
|
||||
icon(new Image(Objects.requireNonNull(this.getClass().getResource(path)).toString()));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> icon(Image image) {
|
||||
getStage().getIcons().add(image);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (AlertUtil.builder == null) {
|
||||
AlertUtil.builder = this;
|
||||
} else if (AlertUtil.builder.alert.isShowing()) {
|
||||
if (!Objects.equals(AlertUtil.builder.alert.getContentText(), alert.getContentText()))
|
||||
((Alert) AlertUtil.builder.alert).setOnHidden(event -> {
|
||||
AlertUtil.builder = null;
|
||||
show();
|
||||
});
|
||||
}
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.confirm
|
||||
*/
|
||||
public void show(OnClickListener listener) {
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
|
||||
listener.onClicked(result.get().getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.confirm
|
||||
*/
|
||||
public void show(OnChoseListener listener) {
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if (result.get() == ButtonType.OK) {
|
||||
listener.confirm();
|
||||
} else {
|
||||
listener.cancelOrClose(result.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.input
|
||||
* 如果用户点击了取消按钮,将会返回null
|
||||
*/
|
||||
public String getInput() {
|
||||
Optional<String> result = alert.showAndWait();
|
||||
if (result.isPresent()) {
|
||||
return result.get();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.choices
|
||||
*/
|
||||
public <R> R getChoice(R... choices) {
|
||||
Optional result = alert.showAndWait();
|
||||
return (R) result.get();
|
||||
}
|
||||
|
||||
private Stage getStage() {
|
||||
return (Stage) alert.getDialogPane().getScene().getWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder<Alert> info(String content) {
|
||||
return new Builder<Alert>(new Alert(Alert.AlertType.INFORMATION)).content(content).header(null);
|
||||
}
|
||||
|
||||
public static Builder<Alert> info() {
|
||||
return new Builder<Alert>(new Alert(Alert.AlertType.INFORMATION));
|
||||
}
|
||||
|
||||
public static Builder<Alert> error(String message) {
|
||||
return new Builder<Alert>(new Alert(Alert.AlertType.ERROR)).header(null).content(message);
|
||||
}
|
||||
|
||||
public static Builder<Alert> warning() {
|
||||
return new Builder<Alert>(new Alert(Alert.AlertType.WARNING));
|
||||
}
|
||||
|
||||
public static Builder<Alert> exception(Exception ex) {
|
||||
return new Builder<Alert>(exceptionAlert(ex));
|
||||
}
|
||||
|
||||
public static Alert exceptionAlert(Exception ex) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Exception Dialog");
|
||||
alert.setHeaderText(ex.getClass().getSimpleName());
|
||||
alert.setContentText(ex.getMessage());
|
||||
|
||||
// 创建可扩展的异常。
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
ex.printStackTrace(pw);
|
||||
String exceptionText = sw.toString();
|
||||
|
||||
Label label = new Label("The exception stacktrace was :");
|
||||
|
||||
TextArea textArea = new TextArea(exceptionText);
|
||||
textArea.setEditable(false);
|
||||
textArea.setWrapText(true);
|
||||
|
||||
textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
textArea.setMaxHeight(Double.MAX_VALUE);
|
||||
GridPane.setVgrow(textArea, Priority.ALWAYS);
|
||||
GridPane.setHgrow(textArea, Priority.ALWAYS);
|
||||
|
||||
GridPane expContent = new GridPane();
|
||||
expContent.setMaxWidth(Double.MAX_VALUE);
|
||||
expContent.add(label, 0, 0);
|
||||
expContent.add(textArea, 0, 1);
|
||||
|
||||
// 将可扩展异常设置到对话框窗格中。
|
||||
alert.getDialogPane().setExpandableContent(expContent);
|
||||
return alert;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框
|
||||
*/
|
||||
public static Builder<Alert> confirm() {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("确认对话框");
|
||||
return new Builder<Alert>(alert);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义确认对话框 <p>
|
||||
* <code>"Cancel"</code> OR <code>"取消"</code> 为取消按钮
|
||||
*/
|
||||
public static Builder<Alert> confirm(String... buttons) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
|
||||
List<ButtonType> buttonList = Arrays.stream(buttons).map((type) -> {
|
||||
ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER;
|
||||
if ("Cancel".equals(type) || "取消".equals(type))
|
||||
buttonData = ButtonBar.ButtonData.CANCEL_CLOSE;
|
||||
return new ButtonType(type, buttonData);
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
alert.getButtonTypes().setAll(buttonList);
|
||||
|
||||
return new Builder<Alert>(alert);
|
||||
}
|
||||
|
||||
public static Builder<TextInputDialog> input(String content) {
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setContentText(content);
|
||||
return new Builder<TextInputDialog>(dialog);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Builder<ChoiceDialog<T>> choices(String hintText, T... choices) {
|
||||
ChoiceDialog<T> dialog = new ChoiceDialog<T>(choices[0], choices);
|
||||
dialog.setContentText(hintText);
|
||||
return new Builder<ChoiceDialog<T>>(dialog);
|
||||
}
|
||||
|
||||
|
||||
public interface OnChoseListener {
|
||||
void confirm();
|
||||
|
||||
void cancelOrClose(ButtonType buttonType);
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
void onClicked(String result);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
/**
|
||||
* 加载等待弹窗
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class Loading {
|
||||
protected Stage stage;
|
||||
protected StackPane root;
|
||||
protected Label messageLb;
|
||||
protected ImageView loadingView = new ImageView(new Image("https://blog-static.cnblogs.com/files/miaoqx/loading.gif"));
|
||||
|
||||
public Loading(Stage owner) {
|
||||
|
||||
messageLb = new Label("请耐心等待...");
|
||||
messageLb.setFont(Font.font(20));
|
||||
|
||||
root = new StackPane();
|
||||
root.setMouseTransparent(true);
|
||||
root.setPrefSize(owner.getWidth(), owner.getHeight());
|
||||
root.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.3), null, null)));
|
||||
root.getChildren().addAll(loadingView, messageLb);
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
|
||||
stage = new Stage();
|
||||
stage.setX(owner.getX());
|
||||
stage.setY(owner.getY());
|
||||
stage.setScene(scene);
|
||||
stage.setResizable(false);
|
||||
stage.initOwner(owner);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.initModality(Modality.APPLICATION_MODAL);
|
||||
stage.getIcons().addAll(owner.getIcons());
|
||||
stage.setX(owner.getX());
|
||||
stage.setY(owner.getY());
|
||||
stage.setHeight(owner.getHeight());
|
||||
stage.setWidth(owner.getWidth());
|
||||
}
|
||||
|
||||
// 更改信息
|
||||
public Loading showMessage(String message) {
|
||||
Platform.runLater(() -> messageLb.setText(message));
|
||||
return this;
|
||||
}
|
||||
|
||||
// 更改信息
|
||||
public Loading image(Image image) {
|
||||
Platform.runLater(() -> loadingView.imageProperty().set(image));
|
||||
return this;
|
||||
}
|
||||
|
||||
// 显示
|
||||
public void show() {
|
||||
Platform.runLater(() -> stage.show());
|
||||
}
|
||||
|
||||
// 关闭
|
||||
public void closeStage() {
|
||||
Platform.runLater(() -> stage.close());
|
||||
}
|
||||
|
||||
// 是否正在展示
|
||||
public boolean showing() {
|
||||
return stage.isShowing();
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
|
||||
import org.apache.commons.exec.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 命令工具类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class ProcessesUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProcessesUtil.class);
|
||||
private static final String NEWLINE = System.lineSeparator();
|
||||
private static final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
|
||||
|
||||
public static boolean exec(String command) {
|
||||
try {
|
||||
exec(command, new OnExecuteListener() {
|
||||
@Override
|
||||
public void onExecute(String msg) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteSuccess(int exitValue) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteError(Exception e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteOver() {
|
||||
|
||||
}
|
||||
});
|
||||
handler.waitFor();
|
||||
} catch (Exception e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
return 0 == handler.getExitValue();
|
||||
}
|
||||
|
||||
public static void exec(String command, OnExecuteListener listener) {
|
||||
LogOutputStream logout = new LogOutputStream() {
|
||||
@Override
|
||||
protected void processLine(String line, int logLevel) {
|
||||
if (listener != null) listener.onExecute(line + NEWLINE);
|
||||
}
|
||||
};
|
||||
|
||||
CommandLine commandLine = CommandLine.parse(command);
|
||||
DefaultExecutor executor = DefaultExecutor.builder().get();
|
||||
executor.setStreamHandler(new PumpStreamHandler(logout, logout));
|
||||
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
|
||||
@Override
|
||||
public void onProcessComplete(int exitValue) {
|
||||
if (listener != null) {
|
||||
listener.onExecuteSuccess(exitValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProcessFailed(ExecuteException e) {
|
||||
if (listener != null) {
|
||||
listener.onExecuteError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
executor.execute(commandLine, handler);
|
||||
} catch (IOException e) {
|
||||
if (listener != null) listener.onExecuteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnExecuteListener {
|
||||
void onExecute(String msg);
|
||||
|
||||
void onExecuteSuccess(int exitValue);
|
||||
|
||||
void onExecuteError(Exception e);
|
||||
void onExecuteOver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent construction.
|
||||
*/
|
||||
private ProcessesUtil() {
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* 提示工具
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class TooltipUtil {
|
||||
private static TooltipUtil util;
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
private Window owner;
|
||||
private ChangeListener<Number> xListener;
|
||||
private ChangeListener<Number> yListener;
|
||||
private boolean paneMove = false;
|
||||
|
||||
private TooltipUtil(Window window) {
|
||||
this.owner = window;
|
||||
this.tooltip.styleProperty().set(
|
||||
"-fx-background-color: white;" +
|
||||
"-fx-text-fill: grey;" +
|
||||
"-fx-font-size: 12px;"
|
||||
);
|
||||
}
|
||||
|
||||
public static TooltipUtil getInstance(Pane pane) {
|
||||
if (pane == null) return null;
|
||||
Window window = pane.getScene().getWindow();
|
||||
if (window == null) return null;
|
||||
|
||||
if (util == null) {
|
||||
util = new TooltipUtil(window);
|
||||
// 窗口位置监听
|
||||
util.xListener = (observable, oldValue, newValue) -> {
|
||||
util.tooltip.setAnchorX(util.tooltip.getAnchorX() + (newValue.doubleValue() - oldValue.doubleValue()));
|
||||
util.paneMove = true;
|
||||
};
|
||||
util.yListener = (observable, oldValue, newValue) -> {
|
||||
util.tooltip.setAnchorY(util.tooltip.getAnchorY() + (newValue.doubleValue() - oldValue.doubleValue()));
|
||||
util.paneMove = true;
|
||||
};
|
||||
util.tooltip.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) util.paneMove = false;
|
||||
});
|
||||
// 随窗口移动
|
||||
util.owner.xProperty().addListener(util.xListener);
|
||||
util.owner.yProperty().addListener(util.yListener);
|
||||
}
|
||||
|
||||
if (!window.equals(util.owner)) {
|
||||
// 删除旧监听
|
||||
util.owner.xProperty().removeListener(util.xListener);
|
||||
util.owner.yProperty().removeListener(util.yListener);
|
||||
// 新窗口
|
||||
util.owner = window;
|
||||
// 随窗口移动
|
||||
util.owner.xProperty().addListener(util.xListener);
|
||||
util.owner.yProperty().addListener(util.yListener);
|
||||
}
|
||||
|
||||
// 点击关闭
|
||||
pane.setOnMouseClicked(event -> {
|
||||
if (!util.paneMove) util.tooltip.hide();
|
||||
util.paneMove = false;
|
||||
});
|
||||
|
||||
util.tooltip.hide();
|
||||
|
||||
return util;
|
||||
}
|
||||
|
||||
public void showProxyTypeTip(MouseEvent event) {
|
||||
tooltip.setText(
|
||||
"提示:XTCP 映射成功率并不高,具体取决于 NAT 设备的复杂度。\n" +
|
||||
"TCP :基础的 TCP 映射,适用于大多数服务,例如远程桌面、SSH、Minecraft、泰拉瑞亚等\n" +
|
||||
"UDP :基础的 UDP 映射,适用于域名解析、部分基于 UDP 协议的游戏等\n" +
|
||||
"HTTP :搭建网站专用映射,并通过 80 端口访问\n" +
|
||||
"HTTPS :带有 SSL 加密的网站映射,通过 443 端口访问,服务器需要支持 SSL\n" +
|
||||
"XTCP :客户端之间点对点 (P2P) 连接协议,流量不经过服务器,适合大流量传输的场景,需要两台设备之间都运行一个客户端\n" +
|
||||
"STCP :安全交换 TCP 连接协议,基于 TCP,访问此服务的用户也需要运行一个客户端,才能建立连接,流量由服务器转发"
|
||||
);
|
||||
show(event);
|
||||
}
|
||||
|
||||
private void show(MouseEvent event) {
|
||||
|
||||
if (tooltip.isShowing()) {
|
||||
tooltip.hide();
|
||||
} else {
|
||||
tooltip.show(owner);
|
||||
double mx = event.getScreenX();
|
||||
double my = event.getScreenY();
|
||||
double tw = tooltip.widthProperty().doubleValue();
|
||||
double th = tooltip.heightProperty().doubleValue();
|
||||
|
||||
tooltip.setX(mx - tw / 2);
|
||||
tooltip.setY(my - th - 10);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
tooltip.hide();
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return tooltip.isShowing();
|
||||
}
|
||||
}
|
13
src/main/java/cn/octopusyan/dmt/AppLauncher.java
Normal file
13
src/main/java/cn/octopusyan/dmt/AppLauncher.java
Normal file
@ -0,0 +1,13 @@
|
||||
package cn.octopusyan.dmt;
|
||||
|
||||
/**
|
||||
* 启动类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class AppLauncher {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Application.launch(Application.class, args);
|
||||
}
|
||||
}
|
109
src/main/java/cn/octopusyan/dmt/Application.java
Normal file
109
src/main/java/cn/octopusyan/dmt/Application.java
Normal file
@ -0,0 +1,109 @@
|
||||
package cn.octopusyan.dmt;
|
||||
|
||||
import cn.octopusyan.dmt.common.config.Constants;
|
||||
import cn.octopusyan.dmt.common.config.Context;
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.common.manager.http.CookieManager;
|
||||
import cn.octopusyan.dmt.common.manager.http.HttpConfig;
|
||||
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
|
||||
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dmt.common.util.ProcessesUtil;
|
||||
import cn.octopusyan.dmt.utils.PBOUtil;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Application extends javafx.application.Application {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Application.class);
|
||||
@Getter
|
||||
private static Stage primaryStage;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
logger.info("application init ...");
|
||||
|
||||
// 初始化客户端配置
|
||||
ConfigManager.load();
|
||||
|
||||
// 初始化 PBO工具
|
||||
PBOUtil.init();
|
||||
|
||||
// http请求工具初始化
|
||||
HttpConfig httpConfig = new HttpConfig();
|
||||
httpConfig.setCookieHandler(CookieManager.get());
|
||||
httpConfig.setExecutor(ThreadPoolManager.getInstance("http-pool"));
|
||||
// 加载代理设置
|
||||
switch (ConfigManager.proxySetup()) {
|
||||
case NO_PROXY -> httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
|
||||
case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
|
||||
case MANUAL -> {
|
||||
if (ConfigManager.hasProxy()) {
|
||||
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(
|
||||
Objects.requireNonNull(ConfigManager.proxyHost()),
|
||||
ConfigManager.getProxyPort()
|
||||
);
|
||||
httpConfig.setProxySelector(ProxySelector.of(unresolved));
|
||||
}
|
||||
}
|
||||
}
|
||||
httpConfig.setConnectTimeout(3000);
|
||||
HttpUtil.init(httpConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws IOException {
|
||||
|
||||
logger.info("application start ...");
|
||||
|
||||
Application.primaryStage = primaryStage;
|
||||
|
||||
Context.setApplication(this);
|
||||
|
||||
// 全局异常处理
|
||||
Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
|
||||
Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
|
||||
|
||||
// 主题样式
|
||||
Application.setUserAgentStylesheet(ConfigManager.theme().getUserAgentStylesheet());
|
||||
|
||||
// 启动主界面
|
||||
primaryStage.setTitle(String.format("%s %s", Constants.APP_TITLE, Constants.APP_VERSION));
|
||||
Scene scene = Context.initScene();
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.show();
|
||||
}
|
||||
|
||||
private void showErrorDialog(Thread t, Throwable e) {
|
||||
logger.error("未知异常", e);
|
||||
Platform.runLater(() -> AlertUtil.getInstance(primaryStage).exception(new Exception(e)).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
logger.info("application stop ...");
|
||||
// 关闭所有命令
|
||||
ProcessesUtil.destroyAll();
|
||||
// 保存应用数据
|
||||
ConfigManager.save();
|
||||
// 停止所有线程
|
||||
ThreadPoolManager.shutdownAll();
|
||||
// 删除缓存
|
||||
FileUtils.deleteQuietly(new File(Constants.TMP_DIR_PATH));
|
||||
FileUtils.deleteQuietly(new File(Constants.BAK_DIR_PATH));
|
||||
// 关闭主界面
|
||||
Platform.exit();
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
58
src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java
Normal file
58
src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java
Normal file
@ -0,0 +1,58 @@
|
||||
package cn.octopusyan.dmt.common.base;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.stage.Window;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Getter
|
||||
public abstract class BaseBuilder<T extends BaseBuilder<T, ?>, D extends Dialog<?>> {
|
||||
protected D dialog;
|
||||
|
||||
public BaseBuilder(D dialog, Window mOwner) {
|
||||
this.dialog = dialog;
|
||||
if (mOwner != null)
|
||||
this.dialog.initOwner(mOwner);
|
||||
}
|
||||
|
||||
public T title(String title) {
|
||||
dialog.setTitle(title);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public T header(String header) {
|
||||
dialog.setHeaderText(header);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public T content(String content) {
|
||||
dialog.setContentText(content);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
|
||||
Node dialogPane = dialog.getDialogPane().getContent();
|
||||
if (dialogPane != null && ConfigManager.theme().isDarkMode()) {
|
||||
dialogPane.setStyle(STR."""
|
||||
\{dialogPane.getStyle()}
|
||||
-fx-border-color: rgb(209, 209, 214, 0.5);
|
||||
-fx-border-width: 1;
|
||||
-fx-border-radius: 10;
|
||||
""");
|
||||
}
|
||||
|
||||
Platform.runLater(() -> dialog.showAndWait());
|
||||
}
|
||||
|
||||
public void close() {
|
||||
Platform.runLater(() -> {
|
||||
if (dialog.isShowing()) dialog.close();
|
||||
});
|
||||
}
|
||||
}
|
144
src/main/java/cn/octopusyan/dmt/common/base/BaseController.java
Normal file
144
src/main/java/cn/octopusyan/dmt/common/base/BaseController.java
Normal file
@ -0,0 +1,144 @@
|
||||
package cn.octopusyan.dmt.common.base;
|
||||
|
||||
import cn.octopusyan.dmt.Application;
|
||||
import cn.octopusyan.dmt.common.config.Context;
|
||||
import cn.octopusyan.dmt.common.util.FxmlUtil;
|
||||
import cn.octopusyan.dmt.common.util.ViewUtil;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* 通用视图控制器基类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public abstract class BaseController<VM extends BaseViewModel> implements Initializable {
|
||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
protected final VM viewModel;
|
||||
|
||||
public BaseController() {
|
||||
//初始化时保存当前Controller实例
|
||||
Context.getControllers().put(this.getClass().getSimpleName(), this);
|
||||
|
||||
// view model
|
||||
VM vm = null;
|
||||
Type superclass = getClass().getGenericSuperclass();
|
||||
if (superclass instanceof ParameterizedType type) {
|
||||
Class<VM> clazz = (Class<VM>) type.getActualTypeArguments()[0];
|
||||
try {
|
||||
vm = clazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
}
|
||||
viewModel = vm;
|
||||
viewModel.setController(this);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||
// 全局窗口拖拽
|
||||
if (dragWindow() && getRootPanel() != null) {
|
||||
// 窗口拖拽
|
||||
ViewUtil.bindDragged(getRootPanel());
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
initData();
|
||||
|
||||
// 初始化视图样式
|
||||
initViewStyle();
|
||||
|
||||
// 初始化视图事件
|
||||
initViewAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口拖拽设置
|
||||
*
|
||||
* @return 是否启用
|
||||
*/
|
||||
public boolean dragWindow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根布局
|
||||
*
|
||||
* @return 根布局对象
|
||||
*/
|
||||
public abstract Pane getRootPanel();
|
||||
|
||||
/**
|
||||
* 获取根布局
|
||||
* <p> 搭配 {@link FxmlUtil#load(String)} 使用
|
||||
*
|
||||
* @return 根布局对象
|
||||
*/
|
||||
protected String getRootFxml() {
|
||||
System.out.println(getClass().getSimpleName());
|
||||
return "";
|
||||
}
|
||||
|
||||
public Stage getWindow() {
|
||||
try {
|
||||
return (Stage) getRootPanel().getScene().getWindow();
|
||||
} catch (Throwable _) {
|
||||
return Application.getPrimaryStage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
public abstract void initData();
|
||||
|
||||
/**
|
||||
* 视图样式
|
||||
*/
|
||||
public void initViewStyle() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图事件
|
||||
*/
|
||||
public abstract void initViewAction();
|
||||
|
||||
private static List<Field> getAllField(Class<?> class1) {
|
||||
List<Field> list = new ArrayList<>();
|
||||
while (class1 != Object.class) {
|
||||
list.addAll(Arrays.stream(class1.getDeclaredFields()).toList());
|
||||
//获取父类
|
||||
class1 = class1.getSuperclass();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public void exit() {
|
||||
Platform.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭窗口
|
||||
*/
|
||||
public void onDestroy() {
|
||||
Stage stage = getWindow();
|
||||
stage.hide();
|
||||
stage.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package cn.octopusyan.dmt.common.base;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* View Model
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Setter
|
||||
public abstract class BaseViewModel<T extends BaseController> {
|
||||
|
||||
protected T controller;
|
||||
|
||||
}
|
27
src/main/java/cn/octopusyan/dmt/common/config/Constants.java
Normal file
27
src/main/java/cn/octopusyan/dmt/common/config/Constants.java
Normal file
@ -0,0 +1,27 @@
|
||||
package cn.octopusyan.dmt.common.config;
|
||||
|
||||
|
||||
import cn.octopusyan.dmt.common.util.PropertiesUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 应用信息
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class Constants {
|
||||
public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
|
||||
public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
|
||||
public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
|
||||
|
||||
public static final String DATA_DIR_PATH = Paths.get("").toFile().getAbsolutePath();
|
||||
public static final String BIN_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bin";
|
||||
public static final String TMP_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}tmp";
|
||||
public static final String BAK_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bak";
|
||||
|
||||
public static final String CONFIG_FILE_PATH = STR."\{DATA_DIR_PATH}\{File.separator}config.yaml";
|
||||
public static final String PBOC_FILE = STR."\{BIN_DIR_PATH}\{File.separator}pboc.exe";
|
||||
public static final String CFG_CONVERT_FILE = STR."\{BIN_DIR_PATH}\{File.separator}CfgConvert.exe";
|
||||
}
|
106
src/main/java/cn/octopusyan/dmt/common/config/Context.java
Normal file
106
src/main/java/cn/octopusyan/dmt/common/config/Context.java
Normal file
@ -0,0 +1,106 @@
|
||||
package cn.octopusyan.dmt.common.config;
|
||||
|
||||
import cn.octopusyan.dmt.Application;
|
||||
import cn.octopusyan.dmt.common.base.BaseController;
|
||||
import cn.octopusyan.dmt.common.util.FxmlUtil;
|
||||
import cn.octopusyan.dmt.common.util.ProcessesUtil;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Callback;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 上下文
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class Context {
|
||||
@Getter
|
||||
private static Application application;
|
||||
private static final Logger log = LoggerFactory.getLogger(Context.class);
|
||||
public static final ObjectProperty<Scene> sceneProperty = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* 控制器集合
|
||||
*/
|
||||
@Getter
|
||||
private static final Map<String, BaseController<?>> controllers = new HashMap<>();
|
||||
|
||||
|
||||
private Context() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
// 获取控制工厂
|
||||
public static Callback<Class<?>, Object> getControlFactory() {
|
||||
return type -> {
|
||||
try {
|
||||
return type.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
log.error("", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void setApplication(Application application) {
|
||||
Context.application = application;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化场景
|
||||
*
|
||||
* @return Scene
|
||||
*/
|
||||
public static Scene initScene() {
|
||||
try {
|
||||
FXMLLoader loader = FxmlUtil.load("main-view");
|
||||
//底层面板
|
||||
Pane root = loader.load();
|
||||
Optional.ofNullable(sceneProperty.get()).ifPresentOrElse(
|
||||
s -> s.setRoot(root),
|
||||
() -> {
|
||||
Scene scene = new Scene(root, root.getPrefWidth() + 20, root.getPrefHeight() + 20, Color.TRANSPARENT);
|
||||
URL resource = Objects.requireNonNull(Context.class.getResource("/css/main-view.css"));
|
||||
scene.getStylesheets().addAll(resource.toExternalForm());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
sceneProperty.set(scene);
|
||||
}
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
log.error("loadScene error", e);
|
||||
}
|
||||
return sceneProperty.get();
|
||||
}
|
||||
|
||||
public static void openUrl(String url) {
|
||||
getApplication().getHostServices().showDocument(url);
|
||||
}
|
||||
|
||||
public static void openFolder(File file) {
|
||||
openFile(file);
|
||||
}
|
||||
|
||||
public static void openFile(File file) {
|
||||
if (!file.exists()) return;
|
||||
|
||||
if (file.isDirectory()) {
|
||||
ProcessesUtil.init(file.getAbsolutePath()).exec("explorer.exe .");
|
||||
} else {
|
||||
ProcessesUtil.init(file.getParentFile().getAbsolutePath()).exec(STR."explorer.exe /select,\{file.getName()}");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package cn.octopusyan.dmt.common.config;
|
||||
|
||||
/**
|
||||
* 名称常量
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class LabelConstants {
|
||||
public static final String CONFIRM = "确认";
|
||||
public static final String CANCEL = "取消";
|
||||
}
|
29
src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java
Normal file
29
src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java
Normal file
@ -0,0 +1,29 @@
|
||||
package cn.octopusyan.dmt.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 代理类型
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ProxySetup {
|
||||
/**
|
||||
* 不使用代理
|
||||
*/
|
||||
NO_PROXY("no_proxy", "不使用代理"),
|
||||
/**
|
||||
* 系统代理
|
||||
*/
|
||||
SYSTEM("system", "系统代理"),
|
||||
/**
|
||||
* 自定义代理
|
||||
*/
|
||||
MANUAL("manual", "自定义代理");
|
||||
|
||||
private final String code;
|
||||
private final String name;
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
package cn.octopusyan.dmt.common.manager;
|
||||
|
||||
import atlantafx.base.theme.*;
|
||||
import cn.octopusyan.dmt.Application;
|
||||
import cn.octopusyan.dmt.common.config.Constants;
|
||||
import cn.octopusyan.dmt.common.enums.ProxySetup;
|
||||
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
|
||||
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dmt.model.ConfigModel;
|
||||
import cn.octopusyan.dmt.model.ProxyInfo;
|
||||
import cn.octopusyan.dmt.model.Translate;
|
||||
import cn.octopusyan.dmt.model.UpgradeConfig;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import javafx.application.Platform;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 客户端设置
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class ConfigManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
|
||||
public static ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
|
||||
|
||||
public static final UpgradeConfig upgradeConfig = new UpgradeConfig();
|
||||
public static final String DEFAULT_THEME = new PrimerLight().getName();
|
||||
public static List<Theme> THEME_LIST = List.of(
|
||||
new PrimerLight(), new PrimerDark(),
|
||||
new NordLight(), new NordDark(),
|
||||
new CupertinoLight(), new CupertinoDark(),
|
||||
new Dracula()
|
||||
);
|
||||
public static Map<String, Theme> THEME_MAP = THEME_LIST.stream()
|
||||
.collect(Collectors.toMap(Theme::getName, Function.identity()));
|
||||
|
||||
private static ConfigModel configModel;
|
||||
|
||||
static {
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
configModel = loadConfig(Constants.CONFIG_FILE_PATH, ConfigModel.class);
|
||||
if (configModel == null)
|
||||
configModel = new ConfigModel();
|
||||
}
|
||||
|
||||
public static <T> T loadConfig(String path, Class<T> clazz) {
|
||||
File src = new File(path);
|
||||
try {
|
||||
if (!src.exists()) {
|
||||
checkFile(src, clazz);
|
||||
}
|
||||
return objectMapper.readValue(src, clazz);
|
||||
} catch (Exception e) {
|
||||
logger.error(String.format("load %s error", clazz.getSimpleName()), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static <T> void checkFile(File src, Class<T> clazz) throws Exception {
|
||||
if (!src.exists()) {
|
||||
File parentDir = FileUtils.createParentDirectories(src);
|
||||
if (!parentDir.exists())
|
||||
logger.error("{} 创建失败", src.getAbsolutePath());
|
||||
}
|
||||
objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance());
|
||||
}
|
||||
|
||||
public static void save() {
|
||||
try {
|
||||
objectMapper.writeValue(new File(Constants.CONFIG_FILE_PATH), configModel);
|
||||
} catch (IOException e) {
|
||||
logger.error("save config error", e);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------{ 主题 }------------------------------------------
|
||||
|
||||
public static String themeName() {
|
||||
return configModel.getTheme();
|
||||
}
|
||||
|
||||
public static Theme theme() {
|
||||
return THEME_MAP.get(themeName());
|
||||
}
|
||||
|
||||
public static void theme(Theme theme) {
|
||||
Application.setUserAgentStylesheet(theme.getUserAgentStylesheet());
|
||||
configModel.setTheme(theme.getName());
|
||||
}
|
||||
|
||||
// --------------------------------{ 翻译接口配置 }------------------------------------------
|
||||
|
||||
public static TranslateApi translateApi() {
|
||||
return TranslateApi.get(configModel.getTranslate().getUse());
|
||||
}
|
||||
|
||||
public static void translateApi(TranslateApi api) {
|
||||
configModel.getTranslate().setUse(api.getName());
|
||||
}
|
||||
|
||||
public static Translate.Config getTranslateConfig(TranslateApi api) {
|
||||
return Optional.of(configModel.getTranslate().getConfig().get(api.getName()))
|
||||
.orElse(api.translate());
|
||||
}
|
||||
|
||||
public static boolean hasTranslateApiKey(TranslateApi api) {
|
||||
return StringUtils.isNoneEmpty(getTranslateConfig(api).getAppId());
|
||||
}
|
||||
|
||||
public static void translateAppid(TranslateApi api, String appId) {
|
||||
getTranslateConfig(api).setAppId(appId);
|
||||
}
|
||||
|
||||
public static String translateAppid(TranslateApi api) {
|
||||
return getTranslateConfig(api).getAppId();
|
||||
}
|
||||
|
||||
public static void translateApikey(TranslateApi api, String secretKey) {
|
||||
getTranslateConfig(api).setSecretKey(secretKey);
|
||||
}
|
||||
|
||||
public static String translateApikey(TranslateApi api) {
|
||||
return getTranslateConfig(api).getSecretKey();
|
||||
}
|
||||
|
||||
public static void translateQps(TranslateApi api, int qps) {
|
||||
getTranslateConfig(api).setQps(qps);
|
||||
}
|
||||
|
||||
public static int translateQps(TranslateApi api) {
|
||||
return getTranslateConfig(api).getQps();
|
||||
}
|
||||
|
||||
// --------------------------------{ 网络代理 }------------------------------------------
|
||||
|
||||
public static ProxySetup proxySetup() {
|
||||
return ProxySetup.valueOf(StringUtils.upperCase(getProxyInfo().getSetup()));
|
||||
}
|
||||
|
||||
public static void proxyTestUrl(String url) {
|
||||
getProxyInfo().setTestUrl(url);
|
||||
}
|
||||
|
||||
public static String proxyTestUrl() {
|
||||
return getProxyInfo().getTestUrl();
|
||||
}
|
||||
|
||||
public static void proxySetup(ProxySetup setup) {
|
||||
getProxyInfo().setSetup(setup.getCode());
|
||||
|
||||
switch (setup) {
|
||||
case NO_PROXY -> HttpUtil.getInstance().clearProxy();
|
||||
case SYSTEM, MANUAL -> {
|
||||
if (ProxySetup.MANUAL.equals(setup) && !hasProxy())
|
||||
return;
|
||||
HttpUtil.getInstance().proxy(setup, ConfigManager.getProxyInfo());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasProxy() {
|
||||
if (configModel == null)
|
||||
return false;
|
||||
ProxyInfo proxyInfo = getProxyInfo();
|
||||
return proxyInfo != null
|
||||
&& StringUtils.isNoneEmpty(proxyInfo.getHost())
|
||||
&& StringUtils.isNoneEmpty(proxyInfo.getPort())
|
||||
&& Integer.parseInt(proxyInfo.getPort()) > 0;
|
||||
}
|
||||
|
||||
public static ProxyInfo getProxyInfo() {
|
||||
ProxyInfo proxyInfo = configModel.getProxy();
|
||||
|
||||
if (proxyInfo == null)
|
||||
setProxyInfo(new ProxyInfo());
|
||||
|
||||
return configModel.getProxy();
|
||||
}
|
||||
|
||||
private static void setProxyInfo(ProxyInfo info) {
|
||||
configModel.setProxy(info);
|
||||
}
|
||||
|
||||
public static String proxyHost() {
|
||||
return getProxyInfo().getHost();
|
||||
}
|
||||
|
||||
public static void proxyHost(String host) {
|
||||
getProxyInfo().setHost(host);
|
||||
}
|
||||
|
||||
public static String proxyPort() {
|
||||
return getProxyInfo().getPort();
|
||||
}
|
||||
|
||||
public static int getProxyPort() {
|
||||
return Integer.parseInt(proxyPort());
|
||||
}
|
||||
|
||||
public static void proxyPort(String port) {
|
||||
if (!NumberUtils.isParsable(port)) return;
|
||||
|
||||
getProxyInfo().setPort(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* 端口检查
|
||||
*
|
||||
* @param consumer 检查结果,信息
|
||||
*/
|
||||
public static void checkProxy(BiConsumer<Boolean, String> consumer) {
|
||||
if (ProxySetup.SYSTEM.equals(proxySetup())) {
|
||||
consumer.accept(true, "");
|
||||
return;
|
||||
}
|
||||
if (!hasProxy()) return;
|
||||
|
||||
ThreadPoolManager.getInstance().execute(() -> {
|
||||
try {
|
||||
try (Socket socket = new Socket(proxyHost(), getProxyPort())) {
|
||||
Platform.runLater(() -> consumer.accept(true, "success"));
|
||||
} catch (IOException e) {
|
||||
Platform.runLater(() -> consumer.accept(false, "connection timed out"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(STR."host=\{proxyHost()},port=\{proxyPort()}", e);
|
||||
Platform.runLater(() -> consumer.accept(false, e.getMessage()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------{ 版本检查 }------------------------------------------
|
||||
|
||||
public static UpgradeConfig upgradeConfig() {
|
||||
return upgradeConfig;
|
||||
}
|
||||
}
|
@ -0,0 +1,371 @@
|
||||
package cn.octopusyan.dmt.common.manager.http;
|
||||
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Cookie 管理
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class CookieManager {
|
||||
|
||||
private static final InMemoryCookieStore inMemoryCookieStore = new InMemoryCookieStore();
|
||||
private static final java.net.CookieManager cookieManager =
|
||||
new java.net.CookieManager(inMemoryCookieStore, CookiePolicy.ACCEPT_ALL);
|
||||
|
||||
public static java.net.CookieManager get() {
|
||||
return cookieManager;
|
||||
}
|
||||
|
||||
public static InMemoryCookieStore getStore() {
|
||||
return inMemoryCookieStore;
|
||||
}
|
||||
|
||||
public static class InMemoryCookieStore implements CookieStore {
|
||||
// the in-memory representation of cookies
|
||||
private final List<HttpCookie> cookieJar;
|
||||
|
||||
// the cookies are indexed by its domain and associated uri (if present)
|
||||
// CAUTION: when a cookie removed from main data structure (i.e. cookieJar),
|
||||
// it won't be cleared in domainIndex & uriIndex. Double-check the
|
||||
// presence of cookie when retrieve one form index store.
|
||||
private final Map<String, List<HttpCookie>> domainIndex;
|
||||
private final Map<URI, List<HttpCookie>> uriIndex;
|
||||
|
||||
// use ReentrantLock instead of synchronized for scalability
|
||||
private final ReentrantLock lock;
|
||||
|
||||
|
||||
/**
|
||||
* The default ctor
|
||||
*/
|
||||
public InMemoryCookieStore() {
|
||||
cookieJar = new ArrayList<>();
|
||||
domainIndex = new HashMap<>();
|
||||
uriIndex = new HashMap<>();
|
||||
|
||||
lock = new ReentrantLock(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one cookie into cookie store.
|
||||
*/
|
||||
public void add(URI uri, HttpCookie cookie) {
|
||||
// pre-condition : argument can't be null
|
||||
if (cookie == null) {
|
||||
throw new NullPointerException("cookie is null");
|
||||
}
|
||||
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
// remove the ole cookie if there has had one
|
||||
cookieJar.remove(cookie);
|
||||
|
||||
// add new cookie if it has a non-zero max-age
|
||||
if (cookie.getMaxAge() != 0) {
|
||||
cookieJar.add(cookie);
|
||||
// and add it to domain index
|
||||
if (cookie.getDomain() != null) {
|
||||
addIndex(domainIndex, cookie.getDomain(), cookie);
|
||||
}
|
||||
if (uri != null) {
|
||||
// add it to uri index, too
|
||||
addIndex(uriIndex, getEffectiveURI(uri), cookie);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all cookies, which:
|
||||
* 1) given uri domain-matches with, or, associated with
|
||||
* given uri when added to the cookie store.
|
||||
* 3) not expired.
|
||||
* See RFC 2965 sec. 3.3.4 for more detail.
|
||||
*/
|
||||
public List<HttpCookie> get(URI uri) {
|
||||
// argument can't be null
|
||||
if (uri == null) {
|
||||
throw new NullPointerException("uri is null");
|
||||
}
|
||||
|
||||
List<HttpCookie> cookies = new ArrayList<>();
|
||||
boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
|
||||
lock.lock();
|
||||
try {
|
||||
// check domainIndex first
|
||||
getInternal1(cookies, domainIndex, uri.getHost(), secureLink);
|
||||
// check uriIndex then
|
||||
getInternal2(cookies, uriIndex, getEffectiveURI(uri), secureLink);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cookies in cookie store, except those have expired
|
||||
*/
|
||||
public List<HttpCookie> getCookies() {
|
||||
List<HttpCookie> rt;
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
cookieJar.removeIf(HttpCookie::hasExpired);
|
||||
} finally {
|
||||
rt = Collections.unmodifiableList(cookieJar);
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all URIs, which are associated with at least one cookie
|
||||
* of this cookie store.
|
||||
*/
|
||||
public List<URI> getURIs() {
|
||||
List<URI> uris;
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
Iterator<URI> it = uriIndex.keySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
URI uri = it.next();
|
||||
List<HttpCookie> cookies = uriIndex.get(uri);
|
||||
if (cookies == null || cookies.isEmpty()) {
|
||||
// no cookies list or an empty list associated with
|
||||
// this uri entry, delete it
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
uris = new ArrayList<>(uriIndex.keySet());
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return uris;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove a cookie from store
|
||||
*/
|
||||
public boolean remove(URI uri, HttpCookie ck) {
|
||||
// argument can't be null
|
||||
if (ck == null) {
|
||||
throw new NullPointerException("cookie is null");
|
||||
}
|
||||
|
||||
boolean modified;
|
||||
lock.lock();
|
||||
try {
|
||||
modified = cookieJar.remove(ck);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove all cookies in this cookie store.
|
||||
*/
|
||||
public boolean removeAll() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (cookieJar.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
cookieJar.clear();
|
||||
domainIndex.clear();
|
||||
uriIndex.clear();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------- Private operations -------------- */
|
||||
|
||||
|
||||
/*
|
||||
* This is almost the same as HttpCookie.domainMatches except for
|
||||
* one difference: It won't reject cookies when the 'H' part of the
|
||||
* domain contains a dot ('.').
|
||||
* I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com
|
||||
* and the cookie domain is .domain.com, then it should be rejected.
|
||||
* However that's not how the real world works. Browsers don't reject and
|
||||
* some sites, like yahoo.com do actually expect these cookies to be
|
||||
* passed along.
|
||||
* And should be used for 'old' style cookies (aka Netscape type of cookies)
|
||||
*/
|
||||
private boolean netscapeDomainMatches(String domain, String host) {
|
||||
if (domain == null || host == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if there's no embedded dot in domain and domain is not .local
|
||||
boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
|
||||
int embeddedDotInDomain = domain.indexOf('.');
|
||||
if (embeddedDotInDomain == 0) {
|
||||
embeddedDotInDomain = domain.indexOf('.', 1);
|
||||
}
|
||||
if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the host name contains no dot and the domain name is .local
|
||||
int firstDotInHost = host.indexOf('.');
|
||||
if (firstDotInHost == -1 && isLocalDomain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int domainLength = domain.length();
|
||||
int lengthDiff = host.length() - domainLength;
|
||||
if (lengthDiff == 0) {
|
||||
// if the host name and the domain name are just string-compare equal
|
||||
return host.equalsIgnoreCase(domain);
|
||||
} else if (lengthDiff > 0) {
|
||||
// need to check H & D component
|
||||
String H = host.substring(0, lengthDiff);
|
||||
String D = host.substring(lengthDiff);
|
||||
|
||||
return (D.equalsIgnoreCase(domain));
|
||||
} else if (lengthDiff == -1) {
|
||||
// if domain is actually .host
|
||||
return (domain.charAt(0) == '.' &&
|
||||
host.equalsIgnoreCase(domain.substring(1)));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void getInternal1(List<HttpCookie> cookies, Map<String, List<HttpCookie>> cookieIndex,
|
||||
String host, boolean secureLink) {
|
||||
// Use a separate list to handle cookies that need to be removed so
|
||||
// that there is no conflict with iterators.
|
||||
ArrayList<HttpCookie> toRemove = new ArrayList<>();
|
||||
for (Map.Entry<String, List<HttpCookie>> entry : cookieIndex.entrySet()) {
|
||||
String domain = entry.getKey();
|
||||
List<HttpCookie> lst = entry.getValue();
|
||||
for (HttpCookie c : lst) {
|
||||
if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) ||
|
||||
(c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) {
|
||||
if ((cookieJar.contains(c))) {
|
||||
// the cookie still in main cookie store
|
||||
if (!c.hasExpired()) {
|
||||
// don't add twice and make sure it's the proper
|
||||
// security level
|
||||
if ((secureLink || !c.getSecure()) &&
|
||||
!cookies.contains(c)) {
|
||||
cookies.add(c);
|
||||
}
|
||||
} else {
|
||||
toRemove.add(c);
|
||||
}
|
||||
} else {
|
||||
// the cookie has been removed from main store,
|
||||
// so also remove it from domain indexed store
|
||||
toRemove.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear up the cookies that need to be removed
|
||||
for (HttpCookie c : toRemove) {
|
||||
lst.remove(c);
|
||||
cookieJar.remove(c);
|
||||
|
||||
}
|
||||
toRemove.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// @param cookies [OUT] contains the found cookies
|
||||
// @param cookieIndex the index
|
||||
// @param comparator the prediction to decide whether or not
|
||||
// a cookie in index should be returned
|
||||
private <T> void getInternal2(List<HttpCookie> cookies,
|
||||
Map<T, List<HttpCookie>> cookieIndex,
|
||||
Comparable<T> comparator, boolean secureLink) {
|
||||
for (T index : cookieIndex.keySet()) {
|
||||
if (comparator.compareTo(index) == 0) {
|
||||
List<HttpCookie> indexedCookies = cookieIndex.get(index);
|
||||
// check the list of cookies associated with this domain
|
||||
if (indexedCookies != null) {
|
||||
Iterator<HttpCookie> it = indexedCookies.iterator();
|
||||
while (it.hasNext()) {
|
||||
HttpCookie ck = it.next();
|
||||
if (cookieJar.contains(ck)) {
|
||||
// the cookie still in main cookie store
|
||||
if (!ck.hasExpired()) {
|
||||
// don't add twice
|
||||
if ((secureLink || !ck.getSecure()) &&
|
||||
!cookies.contains(ck))
|
||||
cookies.add(ck);
|
||||
} else {
|
||||
it.remove();
|
||||
cookieJar.remove(ck);
|
||||
}
|
||||
} else {
|
||||
// the cookie has been removed from main store,
|
||||
// so also remove it from domain indexed store
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
} // end of indexedCookies != null
|
||||
} // end of comparator.compareTo(index) == 0
|
||||
} // end of cookieIndex iteration
|
||||
}
|
||||
|
||||
// add 'cookie' indexed by 'index' into 'indexStore'
|
||||
private <T> void addIndex(Map<T, List<HttpCookie>> indexStore,
|
||||
T index,
|
||||
HttpCookie cookie) {
|
||||
if (index != null) {
|
||||
List<HttpCookie> cookies = indexStore.get(index);
|
||||
if (cookies != null) {
|
||||
// there may already have the same cookie, so remove it first
|
||||
cookies.remove(cookie);
|
||||
|
||||
cookies.add(cookie);
|
||||
} else {
|
||||
cookies = new ArrayList<>();
|
||||
cookies.add(cookie);
|
||||
indexStore.put(index, cookies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// for cookie purpose, the effective uri should only be http://host
|
||||
// the path will be taken into account when path-match algorithm applied
|
||||
//
|
||||
private URI getEffectiveURI(URI uri) {
|
||||
URI effectiveURI;
|
||||
try {
|
||||
effectiveURI = new URI("http",
|
||||
uri.getHost(),
|
||||
null, // path component
|
||||
null, // query component
|
||||
null // fragment component
|
||||
);
|
||||
} catch (URISyntaxException ignored) {
|
||||
effectiveURI = uri;
|
||||
}
|
||||
|
||||
return effectiveURI;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package cn.octopusyan.dmt.common.manager.http;
|
||||
|
||||
import lombok.Data;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.net.Authenticator;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Http配置参数
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
@Data
|
||||
public class HttpConfig {
|
||||
private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
|
||||
|
||||
static {
|
||||
// 使用系统默认代理
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
}
|
||||
|
||||
/**
|
||||
* http版本
|
||||
*/
|
||||
private HttpClient.Version version = HttpClient.Version.HTTP_2;
|
||||
|
||||
/**
|
||||
* 转发策略
|
||||
*/
|
||||
private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
|
||||
|
||||
/**
|
||||
* 线程池
|
||||
*/
|
||||
private Executor executor;
|
||||
|
||||
/**
|
||||
* 认证
|
||||
*/
|
||||
private Authenticator authenticator;
|
||||
|
||||
/**
|
||||
* 代理
|
||||
*/
|
||||
private ProxySelector proxySelector;
|
||||
|
||||
/**
|
||||
* CookieHandler
|
||||
*/
|
||||
private CookieHandler cookieHandler;
|
||||
|
||||
/**
|
||||
* sslContext
|
||||
*/
|
||||
private SSLContext sslContext;
|
||||
|
||||
/**
|
||||
* sslParams
|
||||
*/
|
||||
private SSLParameters sslParameters;
|
||||
|
||||
/**
|
||||
* 连接超时时间毫秒
|
||||
*/
|
||||
private int connectTimeout = 10000;
|
||||
|
||||
/**
|
||||
* 默认读取数据超时时间
|
||||
*/
|
||||
private int defaultReadTimeout = 1200000;
|
||||
|
||||
|
||||
public HttpConfig() {
|
||||
// SSL
|
||||
sslParameters = new SSLParameters();
|
||||
sslParameters.setEndpointIdentificationAlgorithm("");
|
||||
sslParameters.setProtocols(new String[]{"TLSv1.2"});
|
||||
try {
|
||||
sslContext = SSLContext.getInstance("TLSv1.2");
|
||||
System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
|
||||
sslContext.init(null, trustAllCertificates, new SecureRandom());
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0]; // Not relevant.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
}};
|
||||
}
|
@ -1,7 +1,13 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.http;
|
||||
package cn.octopusyan.dmt.common.manager.http;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import cn.octopusyan.dmt.common.enums.ProxySetup;
|
||||
import cn.octopusyan.dmt.common.manager.http.response.BodyHandler;
|
||||
import cn.octopusyan.dmt.common.util.JsonUtil;
|
||||
import cn.octopusyan.dmt.model.ProxyInfo;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProxySelector;
|
||||
@ -11,15 +17,19 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* 网络请求封装
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
@Slf4j
|
||||
public class HttpUtil {
|
||||
private volatile static HttpUtil util;
|
||||
private volatile HttpClient httpClient;
|
||||
@ -57,15 +67,20 @@ public class HttpUtil {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public HttpUtil proxy(String host, int port) {
|
||||
public void proxy(ProxySetup setup, ProxyInfo proxy) {
|
||||
if (httpClient == null)
|
||||
throw new RuntimeException("are you ready ?");
|
||||
|
||||
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(host, port);
|
||||
ProxySelector other = ProxySelector.of(unresolved);
|
||||
this.httpConfig.setProxySelector(other);
|
||||
switch (setup) {
|
||||
case NO_PROXY -> clearProxy();
|
||||
case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
|
||||
case MANUAL -> {
|
||||
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(proxy.getHost(), Integer.parseInt(proxy.getPort()));
|
||||
httpConfig.setProxySelector(ProxySelector.of(unresolved));
|
||||
}
|
||||
}
|
||||
|
||||
this.httpClient = createClient(httpConfig);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void clearProxy() {
|
||||
@ -76,24 +91,26 @@ public class HttpUtil {
|
||||
httpClient = createClient(httpConfig);
|
||||
}
|
||||
|
||||
public HttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
public void close() {
|
||||
if (httpClient == null) return;
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
public String get(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
|
||||
public String get(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
|
||||
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header).GET();
|
||||
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
return response.body();
|
||||
}
|
||||
|
||||
public String post(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
|
||||
public String post(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
|
||||
HttpRequest.Builder request = getRequest(uri, header)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(param.toJSONString()));
|
||||
.header("Content-Type", "application/json;charset=utf-8")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJsonString(param)));
|
||||
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
return response.body();
|
||||
}
|
||||
|
||||
public String postForm(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
|
||||
public String postForm(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
|
||||
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
|
||||
.POST(HttpRequest.BodyPublishers.noBody());
|
||||
|
||||
@ -101,39 +118,73 @@ public class HttpUtil {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
private HttpRequest.Builder getRequest(String uri, JSONObject header) {
|
||||
public void download(String url, String savePath, BiConsumer<Long, Long> listener) throws IOException, InterruptedException {
|
||||
HttpRequest request = getRequest(url, null).build();
|
||||
// 检查bin目录
|
||||
File binDir = new File(savePath);
|
||||
if (!binDir.exists()) {
|
||||
log.debug(STR."dir [\{savePath}] not exists");
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
binDir.mkdirs();
|
||||
log.debug(STR."created dir [\{savePath}]");
|
||||
}
|
||||
|
||||
// 下载处理器
|
||||
var handler = BodyHandler.create(
|
||||
Path.of(savePath),
|
||||
StandardOpenOption.CREATE, StandardOpenOption.WRITE
|
||||
);
|
||||
|
||||
// 下载监听
|
||||
if (listener != null)
|
||||
handler.listener(listener);
|
||||
|
||||
HttpResponse<Path> response = httpClient.send(request, handler);
|
||||
}
|
||||
|
||||
private HttpRequest.Builder getRequest(String uri, JsonNode header) {
|
||||
HttpRequest.Builder request = HttpRequest.newBuilder();
|
||||
// 请求地址
|
||||
request.uri(URI.create(uri));
|
||||
// 请求头
|
||||
if (header != null && !header.isEmpty()) {
|
||||
for (String key : header.keySet()) {
|
||||
request.header(key, header.getString(key));
|
||||
for (Map.Entry<String, JsonNode> property : header.properties()) {
|
||||
String key = property.getKey();
|
||||
request.header(key, JsonUtil.toJsonString(property.getValue()));
|
||||
}
|
||||
}
|
||||
// Cookie
|
||||
// List<HttpCookie> cookies = CookieManager.getStore().get(URI.create(uri));
|
||||
// if (!cookies.isEmpty()) {
|
||||
// String cookie = cookies.stream()
|
||||
// .map(item -> STR."\{item.getName()}=\{item.getValue()}")
|
||||
// .collect(Collectors.joining(";"));
|
||||
// request.header("Cookie", cookie);
|
||||
// }
|
||||
return request;
|
||||
}
|
||||
|
||||
private String createFormParams(JSONObject params) {
|
||||
private String createFormParams(JsonNode params) {
|
||||
StringBuilder formParams = new StringBuilder();
|
||||
if (params == null) {
|
||||
return formParams.toString();
|
||||
}
|
||||
for (String key : params.keySet()) {
|
||||
Object value = params.get(key);
|
||||
if (value instanceof String) {
|
||||
value = URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8);
|
||||
for (Map.Entry<String, JsonNode> property : params.properties()) {
|
||||
String key = property.getKey();
|
||||
JsonNode value = params.get(key);
|
||||
if (value.isTextual()) {
|
||||
String value_ = URLEncoder.encode(String.valueOf(value.asText()), StandardCharsets.UTF_8);
|
||||
formParams.append("&").append(key).append("=").append(value_);
|
||||
} else if (value.isNumber()) {
|
||||
formParams.append("&").append(key).append("=").append(value);
|
||||
} else if (value instanceof Number) {
|
||||
formParams.append("&").append(key).append("=").append(value);
|
||||
} else if (value instanceof List) {
|
||||
formParams.append("&").append(key).append("=").append(params.getJSONArray(key));
|
||||
} else if (value.isArray()) {
|
||||
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
|
||||
} else {
|
||||
formParams.append("&").append(key).append("=").append(params.getJSONObject(key));
|
||||
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
|
||||
}
|
||||
}
|
||||
if (!formParams.isEmpty()) {
|
||||
formParams = new StringBuilder("?" + formParams.substring(1));
|
||||
formParams = new StringBuilder(STR."?\{formParams.substring(1)}");
|
||||
}
|
||||
|
||||
return formParams.toString();
|
@ -0,0 +1,102 @@
|
||||
package cn.octopusyan.dmt.common.manager.http.response;
|
||||
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 下载处理
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Slf4j
|
||||
public class BodyHandler implements HttpResponse.BodyHandler<Path> {
|
||||
private final HttpResponse.BodyHandler<Path> handler;
|
||||
private BiConsumer<Long, Long> consumer;
|
||||
|
||||
private BodyHandler(HttpResponse.BodyHandler<Path> handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public static BodyHandler create(Path directory, OpenOption... openOptions) {
|
||||
return new BodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse.BodySubscriber<Path> apply(HttpResponse.ResponseInfo responseInfo) {
|
||||
AtomicLong length = new AtomicLong(-1);
|
||||
// 获取文件大小
|
||||
Optional<String> string = responseInfo.headers().firstValue("content-length");
|
||||
string.ifPresentOrElse(s -> {
|
||||
length.set(Long.parseLong(s));
|
||||
log.debug(STR."========={content-length = \{s}}=========");
|
||||
}, () -> {
|
||||
String msg = "response not has header [content-length]";
|
||||
log.error(msg);
|
||||
});
|
||||
|
||||
BodySubscriber subscriber = new BodySubscriber(handler.apply(responseInfo));
|
||||
subscriber.setConsumer(progress -> consumer.accept(length.get(), progress));
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
public void listener(BiConsumer<Long, Long> consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
public static class BodySubscriber implements HttpResponse.BodySubscriber<Path> {
|
||||
private final HttpResponse.BodySubscriber<Path> subscriber;
|
||||
private final AtomicLong progress = new AtomicLong(0);
|
||||
@Setter
|
||||
private Consumer<Long> consumer;
|
||||
|
||||
public BodySubscriber(HttpResponse.BodySubscriber<Path> subscriber) {
|
||||
this.subscriber = subscriber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Path> getBody() {
|
||||
return subscriber.getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
subscriber.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<ByteBuffer> item) {
|
||||
subscriber.onNext(item);
|
||||
|
||||
// 记录进度
|
||||
for (ByteBuffer byteBuffer : item) {
|
||||
progress.addAndGet(byteBuffer.limit());
|
||||
}
|
||||
consumer.accept(progress.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
subscriber.onError(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
subscriber.onComplete();
|
||||
|
||||
consumer.accept(progress.get());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.thread;
|
||||
package cn.octopusyan.dmt.common.manager.thread;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -26,7 +26,7 @@ public class ThreadFactory implements java.util.concurrent.ThreadFactory {
|
||||
|
||||
public ThreadFactory(String prefix) {
|
||||
group = Thread.currentThread().getThreadGroup();
|
||||
namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
|
||||
namePrefix = STR."\{prefix}-\{poolNumber.getAndIncrement()}-thread-";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -36,9 +36,7 @@ public class ThreadFactory implements java.util.concurrent.ThreadFactory {
|
||||
namePrefix + threadNumber.getAndIncrement(),
|
||||
0);
|
||||
|
||||
t.setUncaughtExceptionHandler((t1, e) -> {
|
||||
logger.error("thread : {}, error", t1.getName(), e);
|
||||
});
|
||||
t.setUncaughtExceptionHandler((t1, e) -> logger.error("thread : {}, error", t1.getName(), e));
|
||||
|
||||
if (t.isDaemon())
|
||||
t.setDaemon(false);
|
@ -0,0 +1,55 @@
|
||||
package cn.octopusyan.dmt.common.manager.thread;
|
||||
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 线程池管理类
|
||||
*/
|
||||
public final class ThreadPoolManager extends ThreadPoolExecutor {
|
||||
|
||||
private static volatile ThreadPoolManager sInstance;
|
||||
private static final List<ThreadPoolManager> poolManagerList = new ArrayList<>();
|
||||
|
||||
private ThreadPoolManager() {
|
||||
this("");
|
||||
}
|
||||
|
||||
private ThreadPoolManager(String threadPoolName) {
|
||||
super(32,
|
||||
200,
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(200),
|
||||
new ThreadFactory(StringUtils.isEmpty(threadPoolName) ? ThreadFactory.DEFAULT_THREAD_PREFIX : threadPoolName),
|
||||
new DiscardPolicy());
|
||||
}
|
||||
|
||||
public static ThreadPoolManager getInstance(String threadPoolName) {
|
||||
ThreadPoolManager threadPoolManager = new ThreadPoolManager(threadPoolName);
|
||||
poolManagerList.add(threadPoolManager);
|
||||
return threadPoolManager;
|
||||
}
|
||||
|
||||
public static ThreadPoolManager getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (ThreadPoolManager.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ThreadPoolManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public static void shutdownAll() {
|
||||
getInstance().shutdown();
|
||||
poolManagerList.forEach(ThreadPoolExecutor::shutdown);
|
||||
}
|
||||
}
|
@ -1,18 +1,21 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* <p> author : octopus yan
|
||||
* <p> email : octopus_yan@foxmail.com
|
||||
* <p> description : 剪切板工具
|
||||
* <p> create : 2022-4-14 23:21
|
||||
* 剪切板工具
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class ClipUtil {
|
||||
//获取系统剪切板
|
||||
private static final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
private static final Logger log = LoggerFactory.getLogger(ClipUtil.class);
|
||||
|
||||
public static void setClip(String data) {
|
||||
//构建String数据类型
|
||||
@ -28,7 +31,7 @@ public class ClipUtil {
|
||||
//从数据中获取文本值
|
||||
return (String) content.getTransferData(DataFlavor.stringFlavor);
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("", e);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import cn.octopusyan.dmt.common.config.Context;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.JavaFXBuilderFactory;
|
||||
|
||||
@ -19,7 +20,7 @@ public class FxmlUtil {
|
||||
FxmlUtil.class.getResource(prefix + name + suffix),
|
||||
null,
|
||||
new JavaFXBuilderFactory(),
|
||||
null,
|
||||
Context.getControlFactory(),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
}
|
187
src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java
Normal file
187
src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java
Normal file
@ -0,0 +1,187 @@
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
/**
|
||||
* Jackson 封装工具类
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class JsonUtil {
|
||||
private static final Logger log = LoggerFactory.getLogger(JsonUtil.class);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 时间日期格式
|
||||
*/
|
||||
private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
static {
|
||||
//对象的所有字段全部列入序列化
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
|
||||
//取消默认转换timestamps形式
|
||||
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||
//忽略空Bean转json的错误
|
||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
//所有的日期格式都统一为以下的格式,即yyyy-MM-dd HH:mm:ss
|
||||
objectMapper.setDateFormat(new SimpleDateFormat(STANDARD_FORMAT));
|
||||
//忽略 在json字符串中存在,但在java对象中不存在对应属性的情况。防止错误
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Json字符串 转 JavaBean
|
||||
*
|
||||
* @param jsonString Json字符串
|
||||
* @param clazz Java类对象
|
||||
* @param <T> Java类
|
||||
* @return JavaBean
|
||||
*/
|
||||
public static <T> T parseObject(String jsonString, Class<T> clazz) {
|
||||
T t = null;
|
||||
try {
|
||||
t = objectMapper.readValue(jsonString, clazz);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取Json文件 转 JavaBean
|
||||
*
|
||||
* @param file Json文件
|
||||
* @param clazz Java类对象
|
||||
* @param <T> Java类
|
||||
* @return JavaBean
|
||||
*/
|
||||
public static <T> T parseObject(File file, Class<T> clazz) {
|
||||
T t = null;
|
||||
try {
|
||||
t = objectMapper.readValue(file, clazz);
|
||||
} catch (IOException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取Json字符串 转 JavaBean集合
|
||||
*
|
||||
* @param jsonArray Json字符串
|
||||
* @param reference 类型
|
||||
* @param <T> JavaBean类型
|
||||
* @return JavaBean集合
|
||||
*/
|
||||
public static <T> T parseJsonArray(String jsonArray, TypeReference<T> reference) {
|
||||
T t = null;
|
||||
try {
|
||||
t = objectMapper.readValue(jsonArray, reference);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* JavaBean 转 Json字符串
|
||||
*
|
||||
* @param object JavaBean
|
||||
* @return Json字符串
|
||||
*/
|
||||
public static String toJsonString(Object object) {
|
||||
String jsonString = null;
|
||||
try {
|
||||
jsonString = objectMapper.writeValueAsString(object);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaBean 转 字节数组
|
||||
*
|
||||
* @param object JavaBean
|
||||
* @return 字节数组
|
||||
*/
|
||||
public static byte[] toByteArray(Object object) {
|
||||
byte[] bytes = null;
|
||||
try {
|
||||
bytes = objectMapper.writeValueAsBytes(object);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaBean序列化到文件
|
||||
*
|
||||
* @param file 写入文件对象
|
||||
* @param object JavaBean
|
||||
*/
|
||||
public static void objectToFile(File file, Object object) {
|
||||
try {
|
||||
objectMapper.writeValue(file, object);
|
||||
} catch (Exception e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Json字符串 转 JsonNode
|
||||
*
|
||||
* @param jsonString Json字符串
|
||||
* @return JsonNode
|
||||
*/
|
||||
public static JsonNode parseJsonObject(String jsonString) {
|
||||
JsonNode jsonNode = null;
|
||||
try {
|
||||
jsonNode = objectMapper.readTree(jsonString);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return jsonNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaBean 转 JsonNode
|
||||
*
|
||||
* @param object JavaBean
|
||||
* @return JsonNode
|
||||
*/
|
||||
public static JsonNode parseJsonObject(Object object) {
|
||||
return objectMapper.valueToTree(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* JsonNode 转 Json字符串
|
||||
*
|
||||
* @param jsonNode JsonNode
|
||||
* @return Json字符串
|
||||
*/
|
||||
public static String toJsonString(JsonNode jsonNode) {
|
||||
String jsonString = null;
|
||||
try {
|
||||
jsonString = objectMapper.writeValueAsString(jsonNode);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("失败:{}", e.getMessage());
|
||||
}
|
||||
return jsonString;
|
||||
}
|
||||
}
|
119
src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java
Normal file
119
src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java
Normal file
@ -0,0 +1,119 @@
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.exec.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 命令工具类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
@Slf4j
|
||||
public class ProcessesUtil {
|
||||
private static final String NEW_LINE = System.lineSeparator();
|
||||
public static final int[] EXIT_VALUES = {0, 1};
|
||||
|
||||
private final DefaultExecutor executor;
|
||||
private final ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
|
||||
private OnExecuteListener listener;
|
||||
private CommandLine commandLine;
|
||||
|
||||
private static final Set<ProcessesUtil> set = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Prevent construction.
|
||||
*/
|
||||
private ProcessesUtil(String workingDirectory) {
|
||||
this(new File(workingDirectory));
|
||||
}
|
||||
|
||||
private ProcessesUtil(File workingDirectory) {
|
||||
LogOutputStream logout = new LogOutputStream() {
|
||||
@Override
|
||||
protected void processLine(String line, int logLevel) {
|
||||
if (listener != null)
|
||||
listener.onExecute(line + NEW_LINE);
|
||||
}
|
||||
};
|
||||
PumpStreamHandler streamHandler = new PumpStreamHandler(logout, logout);
|
||||
executor = DefaultExecutor.builder()
|
||||
.setExecuteStreamHandler(streamHandler)
|
||||
.setWorkingDirectory(workingDirectory)
|
||||
.get();
|
||||
executor.setExitValues(EXIT_VALUES);
|
||||
executor.setProcessDestroyer(processDestroyer);
|
||||
}
|
||||
|
||||
public static ProcessesUtil init(String workingDirectory) {
|
||||
return init(new File(workingDirectory));
|
||||
}
|
||||
public static ProcessesUtil init(File workingDirectory) {
|
||||
ProcessesUtil util = new ProcessesUtil(workingDirectory);
|
||||
set.add(util);
|
||||
return util;
|
||||
}
|
||||
|
||||
public boolean exec(String command) {
|
||||
commandLine = CommandLine.parse(command);
|
||||
try {
|
||||
int execute = executor.execute(commandLine);
|
||||
return Arrays.stream(EXIT_VALUES).anyMatch(item -> item == execute);
|
||||
} catch (Exception e) {
|
||||
log.error("exec error", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void exec(String command, OnExecuteListener listener) {
|
||||
this.listener = listener;
|
||||
commandLine = CommandLine.parse(command);
|
||||
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
|
||||
@Override
|
||||
public void onProcessComplete(int exitValue) {
|
||||
if (listener != null) {
|
||||
listener.onExecuteSuccess(Arrays.stream(EXIT_VALUES).noneMatch(item -> item == exitValue));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProcessFailed(ExecuteException e) {
|
||||
if (listener != null) {
|
||||
listener.onExecuteError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
executor.execute(commandLine, handler);
|
||||
} catch (Exception e) {
|
||||
if (listener != null) listener.onExecuteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (processDestroyer.isEmpty()) return;
|
||||
processDestroyer.run();
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return !processDestroyer.isEmpty();
|
||||
}
|
||||
|
||||
public static void destroyAll() {
|
||||
set.forEach(ProcessesUtil::destroy);
|
||||
}
|
||||
|
||||
public interface OnExecuteListener {
|
||||
void onExecute(String msg);
|
||||
|
||||
default void onExecuteSuccess(boolean success) {
|
||||
}
|
||||
|
||||
default void onExecuteError(Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import cn.octopusyan.dmt.utils.Resources;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
@ -17,14 +20,16 @@ public class PropertiesUtils {
|
||||
/**
|
||||
* 主配置文件
|
||||
*/
|
||||
private Properties properties;
|
||||
private final Properties properties;
|
||||
/**
|
||||
* 启用配置文件
|
||||
*/
|
||||
private Properties propertiesCustom;
|
||||
private final Properties propertiesCustom;
|
||||
|
||||
private static PropertiesUtils propertiesUtils = new PropertiesUtils();
|
||||
|
||||
public static final Logger logger = LoggerFactory.getLogger(PropertiesUtils.class);
|
||||
|
||||
/**
|
||||
* 私有构造,禁止直接创建
|
||||
*/
|
||||
@ -32,17 +37,18 @@ public class PropertiesUtils {
|
||||
// 读取配置启用的配置文件名
|
||||
properties = new Properties();
|
||||
propertiesCustom = new Properties();
|
||||
InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("application.properties");
|
||||
|
||||
InputStream in = Resources.getResourceAsStream("application.properties");
|
||||
try {
|
||||
properties.load(in);
|
||||
properties.load(new InputStreamReader(in));
|
||||
// 加载启用的配置
|
||||
String property = properties.getProperty("profiles.active");
|
||||
if (!StringUtils.isBlank(property)) {
|
||||
InputStream cin = PropertiesUtils.class.getClassLoader().getResourceAsStream("application-" + property + ".properties");
|
||||
propertiesCustom.load(cin);
|
||||
InputStream cin = Resources.getResourceAsStream("application-" + property + ".properties");
|
||||
propertiesCustom.load(new InputStreamReader(cin));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("读取配置文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,17 +64,6 @@ public class PropertiesUtils {
|
||||
return propertiesUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例
|
||||
*
|
||||
* @return PropertiesUtils
|
||||
*/
|
||||
public static PropertiesUtils getInstance(File file) {
|
||||
PropertiesUtils util = new PropertiesUtils();
|
||||
|
||||
return util;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据属性名读取值
|
||||
* 先去主配置查询,如果查询不到,就去启用配置查询
|
106
src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java
Normal file
106
src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java
Normal file
@ -0,0 +1,106 @@
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import cn.octopusyan.dmt.Application;
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 工具
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class ViewUtil {
|
||||
// 获取系统缩放比
|
||||
public static final double scaleX = Screen.getPrimary().getOutputScaleX();
|
||||
public static final double scaleY = Screen.getPrimary().getOutputScaleY();
|
||||
|
||||
|
||||
private static final Map<Pane, Double> paneXOffset = new HashMap<>();
|
||||
private static final Map<Pane, Double> paneYOffset = new HashMap<>();
|
||||
|
||||
public static void bindShadow(Pane pane) {
|
||||
pane.setStyle("""
|
||||
-fx-background-radius: 5;
|
||||
-fx-border-radius: 5;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 15, 0, 0, 0);
|
||||
-fx-background-insets: 20;
|
||||
-fx-padding: 20;
|
||||
""");
|
||||
}
|
||||
|
||||
public static void bindDragged(Pane pane) {
|
||||
Stage stage = getStage(pane);
|
||||
bindDragged(pane, stage);
|
||||
}
|
||||
|
||||
public static void unbindDragged(Pane pane) {
|
||||
pane.setOnMousePressed(null);
|
||||
pane.setOnMouseDragged(null);
|
||||
paneXOffset.remove(pane);
|
||||
paneYOffset.remove(pane);
|
||||
}
|
||||
|
||||
public static void bindDragged(Pane pane, Stage stage) {
|
||||
pane.setOnMousePressed(event -> {
|
||||
paneXOffset.put(pane, stage.getX() - event.getScreenX());
|
||||
paneYOffset.put(pane, stage.getY() - event.getScreenY());
|
||||
});
|
||||
pane.setOnMouseDragged(event -> {
|
||||
stage.setX(event.getScreenX() + paneXOffset.get(pane));
|
||||
stage.setY(event.getScreenY() + paneYOffset.get(pane));
|
||||
});
|
||||
}
|
||||
|
||||
public static void openDecorated(String title, String fxml) {
|
||||
Stage open = Objects.requireNonNull(open(StageStyle.DECORATED, title, fxml));
|
||||
open.setResizable(false);
|
||||
open.showAndWait();
|
||||
}
|
||||
|
||||
public static Stage open(StageStyle style, String title, String fxml) {
|
||||
FXMLLoader load = FxmlUtil.load(fxml);
|
||||
try {
|
||||
return open(style, title, (Pane) load.load());
|
||||
} catch (IOException e) {
|
||||
AlertUtil.getInstance().exception(e).show();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Stage open(StageStyle style, String title, Pane pane) {
|
||||
Stage stage = new Stage();
|
||||
stage.initOwner(Application.getPrimaryStage());
|
||||
stage.initStyle(style);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setTitle(title);
|
||||
Scene scene = new Scene(pane);
|
||||
scene.setUserAgentStylesheet(ConfigManager.theme().getUserAgentStylesheet());
|
||||
stage.setScene(scene);
|
||||
stage.sizeToScene();
|
||||
return stage;
|
||||
}
|
||||
|
||||
public static Stage getStage() {
|
||||
return Application.getPrimaryStage();
|
||||
}
|
||||
|
||||
public static Stage getStage(Pane pane) {
|
||||
try {
|
||||
return (Stage) pane.getScene().getWindow();
|
||||
} catch (Throwable e) {
|
||||
return getStage();
|
||||
}
|
||||
}
|
||||
}
|
448
src/main/java/cn/octopusyan/dmt/controller/MainController.java
Normal file
448
src/main/java/cn/octopusyan/dmt/controller/MainController.java
Normal file
@ -0,0 +1,448 @@
|
||||
package cn.octopusyan.dmt.controller;
|
||||
|
||||
import atlantafx.base.controls.ModalPane;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import atlantafx.base.theme.Theme;
|
||||
import cn.octopusyan.dmt.common.base.BaseController;
|
||||
import cn.octopusyan.dmt.common.config.Context;
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.common.util.ClipUtil;
|
||||
import cn.octopusyan.dmt.common.util.FxmlUtil;
|
||||
import cn.octopusyan.dmt.common.util.ViewUtil;
|
||||
import cn.octopusyan.dmt.controller.component.WordEditController;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.task.TranslateTask;
|
||||
import cn.octopusyan.dmt.view.ConsoleLog;
|
||||
import cn.octopusyan.dmt.view.EditButtonTableCell;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import cn.octopusyan.dmt.view.filemanager.DirectoryTree;
|
||||
import cn.octopusyan.dmt.viewModel.MainViewModel;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 主界面
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class MainController extends BaseController<MainViewModel> {
|
||||
private static final ConsoleLog consoleLog = ConsoleLog.getInstance(MainController.class);
|
||||
|
||||
public Pane root;
|
||||
|
||||
public Menu viewStyle;
|
||||
|
||||
public static final ToggleGroup viewStyleGroup = new ToggleGroup();
|
||||
|
||||
public StackPane mainView;
|
||||
//
|
||||
public VBox translateView;
|
||||
// 打开文件
|
||||
public StackPane selectFileBox;
|
||||
public VBox openFileView;
|
||||
public VBox dragFileView;
|
||||
public VBox loadFileView;
|
||||
// 工具栏
|
||||
public Button fileNameLabel;
|
||||
public Button translate;
|
||||
public ProgressBar translateProgress;
|
||||
// 文件树加载
|
||||
public DirectoryTree treeFileBox;
|
||||
public VBox loadWordBox;
|
||||
public ProgressBar loadWordProgressBar;
|
||||
// 翻译界面
|
||||
public Pane wordBox;
|
||||
public TableView<WordItem> wordTable;
|
||||
// 信息
|
||||
public TitledPane titledPane;
|
||||
public TextArea logArea;
|
||||
|
||||
public final ModalPane modalPane = new ModalPane();
|
||||
// 文件选择器
|
||||
public static final FileChooser fileChooser = new FileChooser();
|
||||
|
||||
static {
|
||||
var extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
|
||||
fileChooser.getExtensionFilters().add(extFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initData() {
|
||||
// 信息
|
||||
ConsoleLog.init(logArea);
|
||||
// 界面样式
|
||||
List<MenuItem> list = ConfigManager.THEME_LIST.stream().map(this::createViewStyleItem).toList();
|
||||
viewStyle.getItems().addAll(list);
|
||||
|
||||
fileNameLabel.textProperty().bind(viewModel.fileNameProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViewStyle() {
|
||||
// 遮罩
|
||||
getRootPanel().getChildren().add(modalPane);
|
||||
modalPane.displayProperty().addListener((_, _, val) -> {
|
||||
if (!val) {
|
||||
modalPane.setAlignment(Pos.CENTER);
|
||||
modalPane.usePredefinedTransitionFactories(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
|
||||
// 文件拖拽
|
||||
setDragAction(mainView);
|
||||
|
||||
// 复制单元格内容
|
||||
Context.sceneProperty.addListener(_ -> Context.sceneProperty.get()
|
||||
.getAccelerators()
|
||||
.put(new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY), () -> {
|
||||
ObservableList<TablePosition> selectedCells = wordTable.getSelectionModel().getSelectedCells();
|
||||
for (TablePosition tablePosition : selectedCells) {
|
||||
Object cellData = tablePosition.getTableColumn().getCellData(tablePosition.getRow());
|
||||
// 设置剪切板
|
||||
ClipUtil.setClip(cellData.toString());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 日志栏清空
|
||||
logArea.contextMenuProperty().addListener(_ ->
|
||||
logArea.getContextMenu().getItems().addListener((ListChangeListener<MenuItem>) _ -> {
|
||||
MenuItem clearLog = new MenuItem("清空");
|
||||
clearLog.setOnAction(_ -> logArea.clear());
|
||||
logArea.getContextMenu().getItems().add(clearLog);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件拖拽效果
|
||||
*/
|
||||
private void setDragAction(Pane fileBox) {
|
||||
|
||||
// 进入
|
||||
fileBox.setOnDragEntered(dragEvent -> {
|
||||
var dragboard = dragEvent.getDragboard();
|
||||
if (dragboard.hasFiles() && isPboFile(dragboard.getFiles().getFirst())) {
|
||||
selectFileBox.setVisible(true);
|
||||
dragFileView.setVisible(true);
|
||||
}
|
||||
});
|
||||
|
||||
//离开
|
||||
fileBox.setOnDragExited(_ -> {
|
||||
selectFileBox.setVisible(false);
|
||||
dragFileView.setVisible(false);
|
||||
});
|
||||
|
||||
//
|
||||
fileBox.setOnDragOver(dragEvent -> {
|
||||
var dragboard = dragEvent.getDragboard();
|
||||
if (dragEvent.getGestureSource() != fileBox && dragboard.hasFiles()) {
|
||||
/* allow for both copying and moving, whatever user chooses */
|
||||
dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
dragEvent.consume();
|
||||
});
|
||||
|
||||
// 松手
|
||||
fileBox.setOnDragDropped(dragEvent -> {
|
||||
dragFileView.setVisible(false);
|
||||
|
||||
var db = dragEvent.getDragboard();
|
||||
boolean success = false;
|
||||
var file = db.getFiles().getFirst();
|
||||
if (db.hasFiles() && isPboFile(file)) {
|
||||
selectFile(file);
|
||||
success = true;
|
||||
}
|
||||
/* 让源知道字符串是否已成功传输和使用 */
|
||||
dragEvent.setDropCompleted(success);
|
||||
|
||||
dragEvent.consume();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件选择器
|
||||
*/
|
||||
public void selectFile() {
|
||||
selectFile(fileChooser.showOpenDialog(getWindow()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开代理设置
|
||||
*/
|
||||
public void openSetupProxy() {
|
||||
ViewUtil.openDecorated("网络代理设置", "setup/proxy-view");
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开翻译设置
|
||||
*/
|
||||
public void openSetupTranslate() {
|
||||
ViewUtil.openDecorated("翻译设置", "setup/translate-view");
|
||||
}
|
||||
|
||||
/**
|
||||
* 关于
|
||||
*/
|
||||
public void openAbout() {
|
||||
ViewUtil.openDecorated("关于", "about-view");
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载PBO文件
|
||||
*/
|
||||
public void onLoad() {
|
||||
// 展示加载
|
||||
selectFileBox.setVisible(true);
|
||||
loadFileView.setVisible(true);
|
||||
wordBox.getChildren().remove(wordTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示解包完成
|
||||
*
|
||||
* @param path 解包路径
|
||||
*/
|
||||
public void onUnpack(File path) {
|
||||
// 加载解包目录
|
||||
treeFileBox.loadRoot(path);
|
||||
// 隐藏文件选择
|
||||
loadFileView.setVisible(false);
|
||||
selectFileBox.setVisible(false);
|
||||
// 展示翻译界面
|
||||
translateView.setVisible(true);
|
||||
loadWordBox.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载可翻译文本数据
|
||||
*
|
||||
* @param wordItems 文本列表
|
||||
*/
|
||||
public void onLoadWord(List<WordItem> wordItems) {
|
||||
loadWordBox.setVisible(false);
|
||||
wordBox.setVisible(true);
|
||||
bindWordTable(wordItems);
|
||||
translate.setDisable(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打包完成
|
||||
*
|
||||
* @param packFile 打包临时文件
|
||||
*/
|
||||
public void onPackOver(File packFile) {
|
||||
// 选择文件保存地址
|
||||
fileChooser.setInitialFileName(packFile.getName());
|
||||
File file = fileChooser.showSaveDialog(getWindow());
|
||||
|
||||
if (file == null)
|
||||
return;
|
||||
|
||||
if (file.exists()) {
|
||||
//文件已存在,则删除覆盖文件
|
||||
FileUtils.deleteQuietly(file);
|
||||
}
|
||||
|
||||
String exportFilePath = file.getAbsolutePath();
|
||||
consoleLog.info(STR."导出文件路径 => \{exportFilePath}");
|
||||
|
||||
try {
|
||||
FileUtils.copyFile(packFile, file);
|
||||
} catch (IOException e) {
|
||||
consoleLog.error("保存文件失败!", e);
|
||||
Platform.runLater(() -> AlertUtil.getInstance(getWindow()).exception(e).content("保存文件失败!").show());
|
||||
}
|
||||
}
|
||||
|
||||
public void startTranslate() {
|
||||
viewModel.startTranslate();
|
||||
}
|
||||
|
||||
public void startPack() {
|
||||
viewModel.pack();
|
||||
}
|
||||
|
||||
public void selectAllLog() {
|
||||
logArea.selectAll();
|
||||
}
|
||||
|
||||
public void copyLog() {
|
||||
logArea.copy();
|
||||
}
|
||||
|
||||
public void clearLog() {
|
||||
logArea.clear();
|
||||
}
|
||||
|
||||
// ======================================{ }========================================
|
||||
|
||||
/**
|
||||
* 打开文件
|
||||
*/
|
||||
private void selectFile(File file) {
|
||||
viewModel.selectFile(file);
|
||||
viewModel.unpack();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定表格数据
|
||||
*
|
||||
* @param words 单词列表
|
||||
*/
|
||||
private void bindWordTable(List<WordItem> words) {
|
||||
|
||||
if (wordTable == null) {
|
||||
wordTable = new TableView<>();
|
||||
// 填满
|
||||
VBox.setVgrow(wordTable, Priority.ALWAYS);
|
||||
// 可编辑
|
||||
wordTable.setEditable(true);
|
||||
// 自动调整列宽
|
||||
wordTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
|
||||
// 边框
|
||||
Styles.toggleStyleClass(wordTable, Styles.BORDERED);
|
||||
// // 行分隔
|
||||
// Styles.toggleStyleClass(wordTable, Styles.STRIPED);
|
||||
// 单元格选择模式而不是行选择
|
||||
wordTable.getSelectionModel().setCellSelectionEnabled(true);
|
||||
// 不允许选择多个单元格
|
||||
wordTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
|
||||
// 创建列
|
||||
TableColumn<WordItem, String> colFile = createColumn("文件");
|
||||
colFile.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getFile().getName()));
|
||||
TableColumn<WordItem, String> colOriginal = createColumn("原文");
|
||||
colOriginal.setCellValueFactory(param -> param.getValue().getOriginalProperty());
|
||||
TableColumn<WordItem, String> colChinese = createColumn("中文翻译");
|
||||
colChinese.setCellValueFactory(param -> param.getValue().getChineseProperty());
|
||||
colChinese.setEditable(true);
|
||||
TableColumn<WordItem, WordItem> colIcon = new TableColumn<>("");
|
||||
colIcon.setSortable(false);
|
||||
colIcon.setCellFactory(EditButtonTableCell.forTableColumn(item -> {
|
||||
// 展示编辑弹窗
|
||||
try {
|
||||
showModal(getEditWordPane(item), false);
|
||||
} catch (IOException e) {
|
||||
consoleLog.error("加载布局失败", e);
|
||||
}
|
||||
}, item -> {
|
||||
// 翻译当前文本
|
||||
new TranslateTask(Collections.singletonList(item)).execute();
|
||||
}));
|
||||
|
||||
wordTable.getColumns().add(colFile);
|
||||
wordTable.getColumns().add(colOriginal);
|
||||
wordTable.getColumns().add(colChinese);
|
||||
wordTable.getColumns().add(colIcon);
|
||||
}
|
||||
|
||||
// 添加表数据
|
||||
wordTable.getItems().clear();
|
||||
wordBox.getChildren().addFirst(wordTable);
|
||||
wordTable.getItems().addAll(words);
|
||||
}
|
||||
|
||||
/**
|
||||
* 表字段创建
|
||||
*
|
||||
* @param colName 列名
|
||||
* @return 列定义
|
||||
*/
|
||||
private TableColumn<WordItem, String> createColumn(String colName) {
|
||||
TableColumn<WordItem, String> tableColumn = new TableColumn<>(colName);
|
||||
tableColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
tableColumn.setPrefWidth(150);
|
||||
tableColumn.setSortable(false);
|
||||
tableColumn.setEditable("中文翻译".equals(colName));
|
||||
return tableColumn;
|
||||
}
|
||||
|
||||
private MenuItem createViewStyleItem(Theme theme) {
|
||||
var item = new RadioMenuItem(theme.getName());
|
||||
item.setSelected(theme.getName().equals(ConfigManager.themeName()));
|
||||
item.setToggleGroup(viewStyleGroup);
|
||||
item.setUserData(theme);
|
||||
item.selectedProperty().subscribe(selected -> {
|
||||
if (!selected) return;
|
||||
ConfigManager.theme(theme);
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否PBO文件
|
||||
*/
|
||||
private boolean isPboFile(File file) {
|
||||
if (file == null) return false;
|
||||
return Pattern.compile(".*(.pbo)$").matcher(file.getName()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示遮罩弹窗
|
||||
* <p>
|
||||
* 当{@code persistent}为{@code true}时,需要调用{@link #hideModal}才能关闭
|
||||
*
|
||||
* @param node 展示内容
|
||||
* @param persistent 是否持久性内容
|
||||
*/
|
||||
private void showModal(Node node, boolean persistent) {
|
||||
modalPane.setAlignment(Pos.CENTER);
|
||||
modalPane.usePredefinedTransitionFactories(null);
|
||||
modalPane.show(node);
|
||||
modalPane.setPersistent(persistent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭/隐藏遮罩弹窗
|
||||
*/
|
||||
public void hideModal() {
|
||||
modalPane.hide(false);
|
||||
modalPane.setPersistent(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
private Node getEditWordPane(WordItem data) throws IOException {
|
||||
FXMLLoader load = FxmlUtil.load("component/edit-view");
|
||||
Pane pane = load.load();
|
||||
WordEditController ctrl = load.getController();
|
||||
ctrl.bindData(data);
|
||||
return pane;
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package cn.octopusyan.dmt.controller.component;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseController;
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.translate.factory.TranslateFactoryImpl;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import cn.octopusyan.dmt.viewModel.WordEditViewModel;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* 文本编辑
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class WordEditController extends BaseController<WordEditViewModel> {
|
||||
|
||||
public VBox root;
|
||||
public TextArea original;
|
||||
public Button translate;
|
||||
public TextArea chinese;
|
||||
public ProgressIndicator progress;
|
||||
|
||||
@Override
|
||||
public Pane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initData() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
|
||||
}
|
||||
|
||||
public void bindData(WordItem data) {
|
||||
viewModel.setData(data);
|
||||
original.textProperty().bind(viewModel.getOriginalProperty());
|
||||
chinese.textProperty().bindBidirectional(viewModel.getChineseProperty());
|
||||
}
|
||||
|
||||
public void startTranslate() {
|
||||
progress.setVisible(true);
|
||||
ThreadPoolManager.getInstance().execute(() -> {
|
||||
try {
|
||||
String result = TranslateFactoryImpl.getInstance().translate(ConfigManager.translateApi(), original.getText());
|
||||
Platform.runLater(() -> chinese.setText(result));
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> AlertUtil.getInstance(getWindow()).exception(e).show());
|
||||
}
|
||||
Platform.runLater(() -> progress.setVisible(false));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package cn.octopusyan.dmt.controller.help;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseController;
|
||||
import cn.octopusyan.dmt.common.config.Constants;
|
||||
import cn.octopusyan.dmt.common.config.Context;
|
||||
import cn.octopusyan.dmt.viewModel.AboutViewModel;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* 关于
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class AboutController extends BaseController<AboutViewModel> {
|
||||
public VBox root;
|
||||
public Label title;
|
||||
public Label infoTitle;
|
||||
public Label version;
|
||||
|
||||
@Override
|
||||
public Pane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initData() {
|
||||
title.setText(Constants.APP_TITLE);
|
||||
infoTitle.setText(Constants.APP_TITLE);
|
||||
version.setText(STR."版本:\{Constants.APP_VERSION}(x64)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
|
||||
}
|
||||
|
||||
public void openGitee() {
|
||||
Context.openUrl("https://gitee.com/octopus_yan/dayz-mod-translator");
|
||||
}
|
||||
|
||||
public void openGithub() {
|
||||
Context.openUrl("https://github.com/octopusYan/dayz-mod-translator");
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package cn.octopusyan.dmt.controller.setup;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseController;
|
||||
import cn.octopusyan.dmt.common.enums.ProxySetup;
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.viewModel.ProxyViewModel;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* 设置
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class ProxyController extends BaseController<ProxyViewModel> {
|
||||
public VBox root;
|
||||
|
||||
public RadioButton noneProxy;
|
||||
public RadioButton systemProxy;
|
||||
public RadioButton manualProxy;
|
||||
public ToggleGroup proxyGroup = new ToggleGroup();
|
||||
|
||||
public GridPane manualProxyView;
|
||||
public TextField proxyHost;
|
||||
public TextField proxyPort;
|
||||
|
||||
@Override
|
||||
public Pane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initData() {
|
||||
noneProxy.setUserData(ProxySetup.NO_PROXY);
|
||||
systemProxy.setUserData(ProxySetup.SYSTEM);
|
||||
manualProxy.setUserData(ProxySetup.MANUAL);
|
||||
|
||||
noneProxy.setToggleGroup(proxyGroup);
|
||||
systemProxy.setToggleGroup(proxyGroup);
|
||||
manualProxy.setToggleGroup(proxyGroup);
|
||||
|
||||
manualProxyView.disableProperty().bind(
|
||||
Bindings.createBooleanBinding(() -> !manualProxy.selectedProperty().get(), manualProxy.selectedProperty())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
proxyGroup.selectedToggleProperty().addListener((_, _, value) -> {
|
||||
viewModel.proxySetupProperty().set((ProxySetup) value.getUserData());
|
||||
});
|
||||
proxyGroup.selectToggle(switch (ConfigManager.proxySetup()) {
|
||||
case ProxySetup.SYSTEM -> systemProxy;
|
||||
case ProxySetup.MANUAL -> manualProxy;
|
||||
default -> noneProxy;
|
||||
});
|
||||
|
||||
proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty());
|
||||
proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty());
|
||||
}
|
||||
|
||||
public void proxyTest() {
|
||||
viewModel.proxyTest();
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package cn.octopusyan.dmt.controller.setup;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseController;
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import cn.octopusyan.dmt.viewModel.TranslateViewModel;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 翻译
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class TranslateController extends BaseController<TranslateViewModel> {
|
||||
public VBox root;
|
||||
public ComboBox<TranslateApi> translateSourceCombo;
|
||||
public TextField qps;
|
||||
public VBox appidBox;
|
||||
public TextField appid;
|
||||
public VBox apikeyBox;
|
||||
public TextField apikey;
|
||||
|
||||
@Override
|
||||
public Pane getRootPanel() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initData() {
|
||||
|
||||
// 翻译源
|
||||
for (TranslateApi value : TranslateApi.values()) {
|
||||
ObservableList<TranslateApi> items = translateSourceCombo.getItems();
|
||||
items.addAll(value);
|
||||
}
|
||||
|
||||
translateSourceCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(TranslateApi object) {
|
||||
if (object == null) return null;
|
||||
|
||||
return object.getLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TranslateApi fromString(String string) {
|
||||
return TranslateApi.getByLabel(string);
|
||||
}
|
||||
});
|
||||
|
||||
// 当前翻译源
|
||||
translateSourceCombo.getSelectionModel().select(ConfigManager.translateApi());
|
||||
viewModel.getSource().bind(translateSourceCombo.getSelectionModel().selectedItemProperty());
|
||||
|
||||
qps.textProperty().bindBidirectional(viewModel.getQps());
|
||||
appid.textProperty().bindBidirectional(viewModel.getAppId());
|
||||
apikey.textProperty().bindBidirectional(viewModel.getApiKey());
|
||||
|
||||
appidBox.visibleProperty().bind(viewModel.getNeedApiKey());
|
||||
apikeyBox.visibleProperty().bind(viewModel.getNeedApiKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViewAction() {
|
||||
}
|
||||
|
||||
public void save() {
|
||||
TranslateApi source = translateSourceCombo.getValue();
|
||||
String apikey = this.apikey.getText();
|
||||
String appid = this.appid.getText();
|
||||
int qps = Integer.parseInt(this.qps.getText());
|
||||
|
||||
ConfigManager.translateApi(source);
|
||||
ConfigManager.translateQps(source, qps);
|
||||
if (source.needApiKey()) {
|
||||
if (StringUtils.isBlank(apikey) || StringUtils.isBlank(appid)) {
|
||||
AlertUtil.getInstance(getWindow()).error("认证信息不能为空").show();
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigManager.translateApikey(source, apikey);
|
||||
ConfigManager.translateAppid(source, appid);
|
||||
}
|
||||
|
||||
onDestroy();
|
||||
}
|
||||
}
|
33
src/main/java/cn/octopusyan/dmt/model/ConfigModel.java
Normal file
33
src/main/java/cn/octopusyan/dmt/model/ConfigModel.java
Normal file
@ -0,0 +1,33 @@
|
||||
package cn.octopusyan.dmt.model;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import lombok.Data;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* GUI配置信息
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Data
|
||||
public class ConfigModel {
|
||||
private static final Logger log = LoggerFactory.getLogger(ConfigModel.class);
|
||||
|
||||
/**
|
||||
* 主题
|
||||
*/
|
||||
private String theme = ConfigManager.DEFAULT_THEME;
|
||||
|
||||
/**
|
||||
* 代理设置
|
||||
*/
|
||||
private ProxyInfo proxy = new ProxyInfo();
|
||||
|
||||
/**
|
||||
* 代理设置
|
||||
*/
|
||||
private Translate translate = new Translate();
|
||||
|
||||
|
||||
}
|
39
src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java
Normal file
39
src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java
Normal file
@ -0,0 +1,39 @@
|
||||
package cn.octopusyan.dmt.model;
|
||||
|
||||
import cn.octopusyan.dmt.common.enums.ProxySetup;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 代理信息
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Data
|
||||
public class ProxyInfo {
|
||||
/**
|
||||
* 主机地址
|
||||
*/
|
||||
private String host = "";
|
||||
/**
|
||||
* 端口
|
||||
*/
|
||||
private String port = "";
|
||||
/**
|
||||
* 登录名
|
||||
*/
|
||||
private String username = "";
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password = "";
|
||||
/**
|
||||
* 测试Url
|
||||
*/
|
||||
private String testUrl = "http://";
|
||||
/**
|
||||
* 代理类型
|
||||
*
|
||||
* @see ProxySetup
|
||||
*/
|
||||
private String setup = ProxySetup.NO_PROXY.getCode();
|
||||
}
|
52
src/main/java/cn/octopusyan/dmt/model/Translate.java
Normal file
52
src/main/java/cn/octopusyan/dmt/model/Translate.java
Normal file
@ -0,0 +1,52 @@
|
||||
package cn.octopusyan.dmt.model;
|
||||
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 翻译配置
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Data
|
||||
public class Translate {
|
||||
/**
|
||||
* 当前使用接口
|
||||
*/
|
||||
private String use = TranslateApi.FREE_BAIDU.getName();
|
||||
/**
|
||||
* 接口配置
|
||||
*/
|
||||
private Map<String, Config> config = new HashMap<>() {
|
||||
{
|
||||
// 初始化
|
||||
for (TranslateApi api : TranslateApi.values()) {
|
||||
put(api.getName(), new Config("", "", api.getDefaultQps()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class Config {
|
||||
/**
|
||||
* api key
|
||||
*/
|
||||
private String appId;
|
||||
/**
|
||||
* api 密钥
|
||||
*/
|
||||
private String secretKey;
|
||||
/**
|
||||
* 请求速率
|
||||
*/
|
||||
private Integer qps;
|
||||
|
||||
}
|
||||
}
|
29
src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java
Normal file
29
src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java
Normal file
@ -0,0 +1,29 @@
|
||||
package cn.octopusyan.dmt.model;
|
||||
|
||||
import cn.octopusyan.dmt.common.util.PropertiesUtils;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Data
|
||||
public class UpgradeConfig {
|
||||
|
||||
private final String owner = "octopusYan";
|
||||
|
||||
private final String repo = "dayz-mod-translator";
|
||||
|
||||
private String releaseFile = "DMT-windows-nojre.zip";
|
||||
|
||||
private String version = PropertiesUtils.getInstance().getProperty("app.version");
|
||||
|
||||
public String getReleaseApi() {
|
||||
return STR."https://api.github.com/repos/\{getOwner()}/\{getRepo()}/releases/latest";
|
||||
}
|
||||
|
||||
public String getDownloadUrl(String version) {
|
||||
return STR."https://github.com/\{getOwner()}/\{getRepo()}/releases/download/\{version}/\{getReleaseFile()}";
|
||||
}
|
||||
}
|
52
src/main/java/cn/octopusyan/dmt/model/WordItem.java
Normal file
52
src/main/java/cn/octopusyan/dmt/model/WordItem.java
Normal file
@ -0,0 +1,52 @@
|
||||
package cn.octopusyan.dmt.model;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 翻译文本
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Data
|
||||
public class WordItem {
|
||||
/**
|
||||
* 所在文件
|
||||
*/
|
||||
private File file;
|
||||
/**
|
||||
* 行数
|
||||
*/
|
||||
private Integer lines;
|
||||
/**
|
||||
* 开始下标
|
||||
*/
|
||||
private Integer index;
|
||||
/**
|
||||
* 原文
|
||||
*/
|
||||
private StringProperty originalProperty = new SimpleStringProperty();
|
||||
/**
|
||||
* 中文
|
||||
*/
|
||||
private StringProperty chineseProperty = new SimpleStringProperty();
|
||||
|
||||
public WordItem(File file, Integer lines, Integer index, String original, String chinese) {
|
||||
this.file = file;
|
||||
this.lines = lines;
|
||||
this.index = index;
|
||||
this.originalProperty.set(original);
|
||||
this.chineseProperty.set(chinese);
|
||||
}
|
||||
|
||||
public String getChinese() {
|
||||
return chineseProperty.get();
|
||||
}
|
||||
|
||||
public String getOriginal() {
|
||||
return originalProperty.get();
|
||||
}
|
||||
}
|
80
src/main/java/cn/octopusyan/dmt/task/PackTask.java
Normal file
80
src/main/java/cn/octopusyan/dmt/task/PackTask.java
Normal file
@ -0,0 +1,80 @@
|
||||
package cn.octopusyan.dmt.task;
|
||||
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.task.base.BaseTask;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import cn.octopusyan.dmt.utils.PBOUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 打包任务
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class PackTask extends BaseTask<PackTask.PackListener> {
|
||||
|
||||
private static final Function<List<WordItem>, List<WordItem>> sortFunc = items -> items.stream().sorted(Comparator.comparing(WordItem::getLines)).toList();
|
||||
private static final Collector<WordItem, Object, List<WordItem>> downstream = Collectors.collectingAndThen(Collectors.toList(), sortFunc);
|
||||
|
||||
private final Map<File, List<WordItem>> wordFileMap;
|
||||
private final String unpackPath;
|
||||
|
||||
public PackTask(List<WordItem> words, String unpackPath) {
|
||||
super("Pack");
|
||||
|
||||
if (words == null)
|
||||
throw new RuntimeException("参数为null!");
|
||||
|
||||
this.unpackPath = unpackPath;
|
||||
wordFileMap = words.stream().collect(Collectors.groupingBy(WordItem::getFile, downstream));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void task() throws Exception {
|
||||
if (wordFileMap.isEmpty()) return;
|
||||
|
||||
// 写入文件
|
||||
PBOUtil.writeWords(wordFileMap);
|
||||
if (listener != null) listener.onWriteOver();
|
||||
// 打包
|
||||
File packFile = PBOUtil.pack(unpackPath);
|
||||
if (listener != null) listener.onPackOver(packFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包监听
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public abstract static class PackListener extends DefaultTaskListener {
|
||||
|
||||
public PackListener() {
|
||||
super(true);
|
||||
getProgress().setWidth(550);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSucceed() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入完成
|
||||
*/
|
||||
public abstract void onWriteOver();
|
||||
|
||||
/**
|
||||
* 打包完成
|
||||
*
|
||||
* @param file 文件地址
|
||||
*/
|
||||
public abstract void onPackOver(File file);
|
||||
}
|
||||
}
|
28
src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java
Normal file
28
src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java
Normal file
@ -0,0 +1,28 @@
|
||||
package cn.octopusyan.dmt.task;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
|
||||
import cn.octopusyan.dmt.task.base.BaseTask;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 代理检测任务
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Slf4j
|
||||
public class ProxyCheckTask extends BaseTask<DefaultTaskListener> {
|
||||
private final String checkUrl;
|
||||
|
||||
public ProxyCheckTask(String checkUrl) {
|
||||
super(STR."ProxyCheck[\{checkUrl}]");
|
||||
this.checkUrl = checkUrl;
|
||||
this.updateProgress(0d, 1d);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void task() throws Exception {
|
||||
String response = HttpUtil.getInstance().get(checkUrl, null, null);
|
||||
log.debug(STR."Proxy check response result => \n\{response}");
|
||||
}
|
||||
}
|
91
src/main/java/cn/octopusyan/dmt/task/TranslateTask.java
Normal file
91
src/main/java/cn/octopusyan/dmt/task/TranslateTask.java
Normal file
@ -0,0 +1,91 @@
|
||||
package cn.octopusyan.dmt.task;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.task.base.BaseTask;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import cn.octopusyan.dmt.translate.DelayWord;
|
||||
import cn.octopusyan.dmt.translate.TranslateUtil;
|
||||
import cn.octopusyan.dmt.view.ConsoleLog;
|
||||
import javafx.application.Platform;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 翻译工具
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class TranslateTask extends BaseTask<DefaultTaskListener> {
|
||||
|
||||
public static final ConsoleLog consoleLog = ConsoleLog.getInstance(TranslateTask.class);
|
||||
@Getter
|
||||
private final ThreadPoolManager threadPoolManager = ThreadPoolManager.getInstance("translate-pool");
|
||||
private final DelayQueue<DelayWord> delayQueue;
|
||||
private final long total;
|
||||
private final AtomicLong quantity = new AtomicLong();
|
||||
private final CountDownLatch countDownLatch;
|
||||
|
||||
public TranslateTask(List<WordItem> data) {
|
||||
this(TranslateUtil.getDelayQueue(data), data.size());
|
||||
}
|
||||
|
||||
public TranslateTask(DelayQueue<DelayWord> queue, int total) {
|
||||
super("Translate");
|
||||
this.delayQueue = queue;
|
||||
this.total = total;
|
||||
this.quantity.set(total - delayQueue.size());
|
||||
countDownLatch = new CountDownLatch(queue.size());
|
||||
|
||||
updateProgress(quantity.get(), total);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void task() throws Exception {
|
||||
|
||||
while (!delayQueue.isEmpty() && !isCancelled()) {
|
||||
// 取出文本
|
||||
DelayWord word = delayQueue.take();
|
||||
|
||||
// 多线程处理
|
||||
threadPoolManager.execute(() -> {
|
||||
// 翻译
|
||||
try {
|
||||
if (total == 1) {
|
||||
consoleLog.info("正在翻译:{}", word.getWord().getOriginal());
|
||||
}
|
||||
|
||||
String translate = TranslateUtil.translate(word.getApi(), word.getWord().getOriginal());
|
||||
// 回调监听器
|
||||
if (StringUtils.isEmpty(translate)) return;
|
||||
|
||||
synchronized (quantity) {
|
||||
long progress = quantity.addAndGet(1);
|
||||
// 设置翻译结果
|
||||
Platform.runLater(() -> word.getWord().getChineseProperty().setValue(translate));
|
||||
// 更新进度
|
||||
updateProgress(progress, total);
|
||||
// 输出信息
|
||||
if (total != 1) {
|
||||
consoleLog.info("正在翻译({}/{})", progress, total);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
if (!(e instanceof InterruptedException) || !isCancelled()) {
|
||||
consoleLog.error("翻译失败", e);
|
||||
}
|
||||
delayQueue.add(word);
|
||||
}
|
||||
countDownLatch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
countDownLatch.await();
|
||||
}
|
||||
}
|
63
src/main/java/cn/octopusyan/dmt/task/UnpackTask.java
Normal file
63
src/main/java/cn/octopusyan/dmt/task/UnpackTask.java
Normal file
@ -0,0 +1,63 @@
|
||||
package cn.octopusyan.dmt.task;
|
||||
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.task.base.BaseTask;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import cn.octopusyan.dmt.utils.PBOUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 解包PBO文件任务
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class UnpackTask extends BaseTask<UnpackTask.UnpackListener> {
|
||||
|
||||
private final File pboFile;
|
||||
|
||||
public UnpackTask(File pboFile) {
|
||||
super("Unpack " + pboFile.getName());
|
||||
this.pboFile = pboFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void task() throws Exception {
|
||||
// 解包
|
||||
String path = PBOUtil.unpack(pboFile);
|
||||
if (listener != null)
|
||||
listener.onUnpackOver(path);
|
||||
|
||||
List<WordItem> wordItems = PBOUtil.findWord(path);
|
||||
if (listener != null)
|
||||
listener.onFindWordOver(wordItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包监听
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public abstract static class UnpackListener extends DefaultTaskListener {
|
||||
|
||||
@Override
|
||||
protected void onSucceed() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包完成
|
||||
*
|
||||
* @param path 输出路径
|
||||
*/
|
||||
public abstract void onUnpackOver(String path);
|
||||
|
||||
/**
|
||||
* 查找可翻译文本
|
||||
*
|
||||
* @param wordItems 可翻译文本列表
|
||||
*/
|
||||
public abstract void onFindWordOver(List<WordItem> wordItems);
|
||||
}
|
||||
}
|
55
src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java
Normal file
55
src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java
Normal file
@ -0,0 +1,55 @@
|
||||
package cn.octopusyan.dmt.task;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
|
||||
import cn.octopusyan.dmt.common.util.JsonUtil;
|
||||
import cn.octopusyan.dmt.model.UpgradeConfig;
|
||||
import cn.octopusyan.dmt.task.base.BaseTask;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 检查更新任务
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class UpgradeTask extends BaseTask<UpgradeTask.UpgradeListener> {
|
||||
|
||||
private final UpgradeConfig upgradeConfig = ConfigManager.upgradeConfig();
|
||||
|
||||
protected UpgradeTask() {
|
||||
super("Check Update");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void task() throws Exception {
|
||||
String responseStr = HttpUtil.getInstance().get(upgradeConfig.getReleaseApi(), null, null);
|
||||
JsonNode response = JsonUtil.parseJsonObject(responseStr);
|
||||
|
||||
// TODO 校验返回内容
|
||||
String newVersion = response.get("tag_name").asText();
|
||||
|
||||
if (listener != null)
|
||||
listener.onChecked(!StringUtils.equals(upgradeConfig.getVersion(), newVersion), newVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新监听默认实现
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public abstract static class UpgradeListener extends DefaultTaskListener {
|
||||
|
||||
public UpgradeListener() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
public abstract void onChecked(boolean hasUpgrade, String version);
|
||||
|
||||
@Override
|
||||
protected void onSucceed() {
|
||||
// do nothing ...
|
||||
}
|
||||
}
|
||||
}
|
50
src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java
Normal file
50
src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java
Normal file
@ -0,0 +1,50 @@
|
||||
package cn.octopusyan.dmt.task.base;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import javafx.concurrent.Task;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public abstract class BaseTask<T extends Listener> extends Task<Void> {
|
||||
private final ThreadPoolManager Executor = ThreadPoolManager.getInstance("task-pool");
|
||||
protected T listener;
|
||||
@Getter
|
||||
private final String name;
|
||||
|
||||
protected BaseTask(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getNameTag() {
|
||||
return "Task " + name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
if (listener != null) listener.onStart();
|
||||
task();
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract void task() throws Exception;
|
||||
|
||||
public void onListen(T listener) {
|
||||
this.listener = listener;
|
||||
if (this.listener == null)
|
||||
return;
|
||||
if (listener instanceof DefaultTaskListener lis)
|
||||
lis.setTask((BaseTask) this);
|
||||
|
||||
setOnRunning(_ -> listener.onRunning());
|
||||
setOnCancelled(_ -> listener.onCancelled());
|
||||
setOnFailed(_ -> listener.onFailed(getException()));
|
||||
setOnSucceeded(_ -> listener.onSucceeded());
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
Executor.execute(this);
|
||||
}
|
||||
}
|
23
src/main/java/cn/octopusyan/dmt/task/base/Listener.java
Normal file
23
src/main/java/cn/octopusyan/dmt/task/base/Listener.java
Normal file
@ -0,0 +1,23 @@
|
||||
package cn.octopusyan.dmt.task.base;
|
||||
|
||||
/**
|
||||
* 任务监听
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public interface Listener {
|
||||
|
||||
default void onStart() {
|
||||
}
|
||||
|
||||
default void onRunning() {
|
||||
}
|
||||
|
||||
default void onCancelled() {
|
||||
}
|
||||
|
||||
default void onFailed(Throwable throwable) {
|
||||
}
|
||||
|
||||
void onSucceeded();
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package cn.octopusyan.dmt.task.listener;
|
||||
|
||||
import cn.octopusyan.dmt.task.base.BaseTask;
|
||||
import cn.octopusyan.dmt.task.base.Listener;
|
||||
import cn.octopusyan.dmt.view.ConsoleLog;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import cn.octopusyan.dmt.view.alert.builder.ProgressBuilder;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 任务监听器默认实现
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class DefaultTaskListener implements Listener {
|
||||
private ConsoleLog consoleLog;
|
||||
@Getter
|
||||
private BaseTask<? extends DefaultTaskListener> task;
|
||||
/**
|
||||
* 加载弹窗
|
||||
*/
|
||||
@Getter
|
||||
final ProgressBuilder progress = AlertUtil.getInstance().progress();
|
||||
|
||||
/**
|
||||
* 是否展示加载弹窗
|
||||
*/
|
||||
private final boolean showProgress;
|
||||
|
||||
public DefaultTaskListener() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public DefaultTaskListener(boolean showProgress) {
|
||||
this.showProgress = showProgress;
|
||||
}
|
||||
|
||||
public <L extends DefaultTaskListener> void setTask(BaseTask<L> task) {
|
||||
this.task = task;
|
||||
consoleLog = ConsoleLog.getInstance(task.getClass().getSimpleName());
|
||||
progress.onCancel(task::cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
consoleLog.info(STR."\{task.getNameTag()} start ...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRunning() {
|
||||
// 展示加载弹窗
|
||||
if (showProgress)
|
||||
progress.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled() {
|
||||
progress.close();
|
||||
consoleLog.info(STR."\{task.getNameTag()} cancel ...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(Throwable throwable) {
|
||||
progress.close();
|
||||
consoleLog.error(STR."\{task.getNameTag()} fail ...", throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSucceeded() {
|
||||
progress.close();
|
||||
consoleLog.info(STR."\{task.getNameTag()} success ...");
|
||||
onSucceed();
|
||||
}
|
||||
|
||||
protected abstract void onSucceed();
|
||||
}
|
10
src/main/java/cn/octopusyan/dmt/translate/ApiKey.java
Normal file
10
src/main/java/cn/octopusyan/dmt/translate/ApiKey.java
Normal file
@ -0,0 +1,10 @@
|
||||
package cn.octopusyan.dmt.translate;
|
||||
|
||||
/**
|
||||
* API 密钥配置
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public record ApiKey(String appid, String apiKey) {
|
||||
|
||||
}
|
39
src/main/java/cn/octopusyan/dmt/translate/DelayWord.java
Normal file
39
src/main/java/cn/octopusyan/dmt/translate/DelayWord.java
Normal file
@ -0,0 +1,39 @@
|
||||
package cn.octopusyan.dmt.translate;
|
||||
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 延迟翻译对象
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
@Getter
|
||||
public class DelayWord implements Delayed {
|
||||
@Setter
|
||||
private TranslateApi api;
|
||||
private final WordItem word;
|
||||
private long delayTime;
|
||||
|
||||
public DelayWord(WordItem word) {
|
||||
this.word = word;
|
||||
}
|
||||
|
||||
public void setDelayTime(long time, TimeUnit timeUnit) {
|
||||
this.delayTime = System.currentTimeMillis() + (time > 0 ? TimeUnit.MILLISECONDS.convert(time, timeUnit) : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed o) {
|
||||
return Long.compare(this.delayTime, ((DelayWord) o).delayTime);
|
||||
}
|
||||
}
|
60
src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java
Normal file
60
src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java
Normal file
@ -0,0 +1,60 @@
|
||||
package cn.octopusyan.dmt.translate;
|
||||
|
||||
import cn.octopusyan.dmt.model.Translate;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 翻译引擎类型
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
@Getter
|
||||
public enum TranslateApi {
|
||||
FREE_BAIDU("free_baidu", "百度", false),
|
||||
FREE_GOOGLE("free_google", "谷歌", false),
|
||||
BAIDU("baidu", "百度(需认证)", true),
|
||||
;
|
||||
|
||||
@Getter
|
||||
private final String name;
|
||||
@Getter
|
||||
private final String label;
|
||||
private final boolean needApiKey;
|
||||
private final Integer defaultQps;
|
||||
|
||||
TranslateApi(String name, String label, boolean needApiKey) {
|
||||
// 设置接口默认qps=10
|
||||
this(name, label, needApiKey, 10);
|
||||
}
|
||||
|
||||
TranslateApi(String name, String label, boolean needApiKey, int defaultQps) {
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
this.needApiKey = needApiKey;
|
||||
this.defaultQps = defaultQps;
|
||||
}
|
||||
|
||||
public boolean needApiKey() {
|
||||
return needApiKey;
|
||||
}
|
||||
|
||||
public Translate.Config translate() {
|
||||
return new Translate.Config("", "", defaultQps);
|
||||
}
|
||||
|
||||
public static TranslateApi get(String name) {
|
||||
for (TranslateApi value : values()) {
|
||||
if (value.getName().equals(name))
|
||||
return value;
|
||||
}
|
||||
throw new RuntimeException("类型不存在");
|
||||
}
|
||||
|
||||
public static TranslateApi getByLabel(String label) {
|
||||
for (TranslateApi value : values()) {
|
||||
if (value.getLabel().equals(label))
|
||||
return value;
|
||||
}
|
||||
throw new RuntimeException("类型不存在");
|
||||
}
|
||||
}
|
66
src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java
Normal file
66
src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java
Normal file
@ -0,0 +1,66 @@
|
||||
package cn.octopusyan.dmt.translate;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.translate.factory.TranslateFactory;
|
||||
import cn.octopusyan.dmt.translate.factory.TranslateFactoryImpl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
|
||||
/**
|
||||
* 翻译
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class TranslateUtil {
|
||||
private static final TranslateFactory factory = TranslateFactoryImpl.getInstance();
|
||||
|
||||
/**
|
||||
* 翻译(英->中)
|
||||
* <p> TODO 切换语种
|
||||
*
|
||||
* @param api 翻译源
|
||||
* @param sourceString 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
public static String translate(TranslateApi api, String sourceString) throws Exception {
|
||||
return factory.translate(api, sourceString);
|
||||
}
|
||||
|
||||
public static String translate(String sourceString) throws Exception {
|
||||
return factory.translate(ConfigManager.translateApi(), sourceString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取延迟翻译对象
|
||||
*
|
||||
* @param words 待翻译文本列表
|
||||
* @return 延迟对象
|
||||
*/
|
||||
public static DelayQueue<DelayWord> getDelayQueue(List<WordItem> words) {
|
||||
return getDelayQueue(ConfigManager.translateApi(), words);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取延迟翻译对象
|
||||
*
|
||||
* @param source 翻译接口
|
||||
* @param words 待翻译文本列表
|
||||
* @return 延迟对象
|
||||
*/
|
||||
public static DelayQueue<DelayWord> getDelayQueue(TranslateApi source, List<WordItem> words) {
|
||||
return factory.getDelayQueue(source, words);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重设延迟时间
|
||||
*
|
||||
* @param index 序列号
|
||||
* @param delayWord 延迟对象
|
||||
*/
|
||||
public static void resetDelayTime(int index, DelayWord delayWord) {
|
||||
factory.resetDelayTime(ConfigManager.translateApi(), index, delayWord);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package cn.octopusyan.dmt.translate.factory;
|
||||
|
||||
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.translate.DelayWord;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
|
||||
/**
|
||||
* 翻译器接口
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public interface TranslateFactory {
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param api 翻译源
|
||||
* @param sourceString 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
String translate(TranslateApi api, String sourceString) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取延迟翻译对象
|
||||
*
|
||||
* @param api 翻译源
|
||||
* @param word 待翻译文本对象
|
||||
* @return 延迟翻译对象
|
||||
*/
|
||||
DelayQueue<DelayWord> getDelayQueue(TranslateApi api, List<WordItem> word);
|
||||
|
||||
/**
|
||||
* 重设延迟时间
|
||||
*
|
||||
* @param api 翻译接口
|
||||
* @param index 序列号
|
||||
* @param delayWord 延迟对象
|
||||
*/
|
||||
void resetDelayTime(TranslateApi api, int index, DelayWord delayWord);
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package cn.octopusyan.dmt.translate.factory;
|
||||
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.translate.DelayWord;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import cn.octopusyan.dmt.translate.processor.AbstractTranslateProcessor;
|
||||
import cn.octopusyan.dmt.translate.processor.BaiduTranslateProcessor;
|
||||
import cn.octopusyan.dmt.translate.processor.FreeBaiduTranslateProcessor;
|
||||
import cn.octopusyan.dmt.translate.processor.FreeGoogleTranslateProcessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 翻译处理器
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
@Slf4j
|
||||
public class TranslateFactoryImpl implements TranslateFactory {
|
||||
private static TranslateFactoryImpl impl;
|
||||
private final Map<String, AbstractTranslateProcessor> processorMap = new HashMap<>();
|
||||
private final List<AbstractTranslateProcessor> processorList = new ArrayList<>();
|
||||
|
||||
private TranslateFactoryImpl() {
|
||||
}
|
||||
|
||||
public static synchronized TranslateFactoryImpl getInstance() {
|
||||
if (impl == null) {
|
||||
impl = new TranslateFactoryImpl();
|
||||
impl.initProcessor();
|
||||
}
|
||||
return impl;
|
||||
}
|
||||
|
||||
private void initProcessor() {
|
||||
processorList.addAll(Arrays.asList(
|
||||
new FreeGoogleTranslateProcessor(),
|
||||
new FreeBaiduTranslateProcessor(),
|
||||
new BaiduTranslateProcessor()
|
||||
));
|
||||
for (AbstractTranslateProcessor processor : processorList) {
|
||||
processorMap.put(processor.getSource(), processor);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractTranslateProcessor getProcessor(TranslateApi api) {
|
||||
return processorMap.get(api.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DelayQueue<DelayWord> getDelayQueue(TranslateApi api, List<WordItem> words) {
|
||||
var queue = new DelayQueue<DelayWord>();
|
||||
|
||||
// 设置翻译延迟
|
||||
AbstractTranslateProcessor processor = getProcessor(api);
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
// 翻译对象
|
||||
DelayWord delayWord = new DelayWord(words.get(i));
|
||||
|
||||
// 设置翻译源
|
||||
delayWord.setApi(api);
|
||||
long time = 1000L / processor.qps();
|
||||
delayWord.setDelayTime(time * (i + 1), TimeUnit.MILLISECONDS);
|
||||
|
||||
queue.add(delayWord);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetDelayTime(TranslateApi api, int index, DelayWord delayWord) {
|
||||
AbstractTranslateProcessor processor = getProcessor(api);
|
||||
// 设置翻译源
|
||||
delayWord.setApi(api);
|
||||
long time = 1000L / processor.qps();
|
||||
delayWord.setDelayTime(time * (index + 1), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译(英->中)
|
||||
* <p> TODO 切换语种
|
||||
*
|
||||
* @param api 翻译源
|
||||
* @param sourceString 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
@Override
|
||||
public String translate(TranslateApi api, String sourceString) throws Exception {
|
||||
return getProcessor(api).translate(sourceString);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.ConfigManager;
|
||||
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
|
||||
import cn.octopusyan.dmt.translate.ApiKey;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 翻译处理器抽象类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public abstract class AbstractTranslateProcessor implements TranslateProcessor {
|
||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
protected static final HttpUtil httpUtil = HttpUtil.getInstance();
|
||||
protected final TranslateApi translateApi;
|
||||
|
||||
public AbstractTranslateProcessor(TranslateApi translateApi) {
|
||||
this.translateApi = translateApi;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return translateApi.getName();
|
||||
}
|
||||
|
||||
public TranslateApi source() {
|
||||
return translateApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needApiKey() {
|
||||
return source().needApiKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredKey() {
|
||||
return ConfigManager.hasTranslateApiKey(source());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int qps() {
|
||||
return ConfigManager.translateQps(source());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Api配置信息
|
||||
*/
|
||||
protected ApiKey getApiKey() {
|
||||
if (!configuredKey()) {
|
||||
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
|
||||
logger.error(message);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
String appid = ConfigManager.translateAppid(source());
|
||||
String apikey = ConfigManager.translateApikey(source());
|
||||
return new ApiKey(appid, apikey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String translate(String original) throws Exception {
|
||||
|
||||
if (needApiKey() && !configuredKey()) {
|
||||
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
|
||||
logger.error(message);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
return customTranslate(original);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param source 原始文本
|
||||
* @return 翻译结果
|
||||
*/
|
||||
public abstract String customTranslate(String source) throws Exception;
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey;
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import cn.octopusyan.dmt.common.util.JsonUtil;
|
||||
import cn.octopusyan.dmt.translate.ApiKey;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@ -19,8 +21,8 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
|
||||
|
||||
private ApiKey apiKey;
|
||||
|
||||
public BaiduTranslateProcessor(TranslateSource translateSource) {
|
||||
super(translateSource);
|
||||
public BaiduTranslateProcessor() {
|
||||
super(TranslateApi.BAIDU);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -37,10 +39,10 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
|
||||
@Override
|
||||
public String customTranslate(String source) throws IOException, InterruptedException {
|
||||
apiKey = getApiKey();
|
||||
String appid = apiKey.getAppid();
|
||||
String appid = apiKey.appid();
|
||||
String salt = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
JSONObject param = new JSONObject();
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("q", source);
|
||||
param.put("from", "auto");
|
||||
param.put("to", "zh");
|
||||
@ -48,20 +50,20 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
|
||||
param.put("salt", salt);
|
||||
param.put("sign", getSign(appid, source, salt));
|
||||
|
||||
String resp = httpUtil.get(url(), null, param);
|
||||
JSONObject jsonObject = JSON.parseObject(resp);
|
||||
String resp = httpUtil.get(url(), null, JsonUtil.parseJsonObject(param));
|
||||
JsonNode json = JsonUtil.parseJsonObject(resp);
|
||||
|
||||
if (!jsonObject.containsKey("trans_result")) {
|
||||
Object errorMsg = jsonObject.get("error_msg");
|
||||
if (!json.has("trans_result")) {
|
||||
Object errorMsg = json.get("error_msg");
|
||||
logger.error("翻译失败: {}", errorMsg);
|
||||
throw new RuntimeException("翻译失败: " + errorMsg);
|
||||
throw new RuntimeException(String.valueOf(errorMsg));
|
||||
}
|
||||
|
||||
return jsonObject.getJSONArray("trans_result").getJSONObject(0).getString("dst");
|
||||
return json.get("trans_result").get(0).get("dst").asText();
|
||||
}
|
||||
|
||||
private String getSign(String appid, String q, String salt) {
|
||||
return encrypt2ToMD5(appid + q + salt + apiKey.getApiKey());
|
||||
return encrypt2ToMD5(appid + q + salt + apiKey.apiKey());
|
||||
}
|
||||
|
||||
/**
|
@ -0,0 +1,107 @@
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
import cn.octopusyan.dmt.common.manager.http.CookieManager;
|
||||
import cn.octopusyan.dmt.common.util.JsonUtil;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 谷歌 免费翻译接口
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class FreeBaiduTranslateProcessor extends AbstractTranslateProcessor {
|
||||
private static final Map<String, Object> header = new HashMap<>();
|
||||
|
||||
static {
|
||||
header.put("Origin", "https://fanyi.baidu.com");
|
||||
header.put("Referer", "https://fanyi.baidu.com");
|
||||
header.put("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
|
||||
header.put("Accept","*/*");
|
||||
}
|
||||
|
||||
public FreeBaiduTranslateProcessor() {
|
||||
super(TranslateApi.FREE_BAIDU);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String url() {
|
||||
return "https://fanyi.baidu.com/transapi";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int qps() {
|
||||
return source().getDefaultQps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param source 待翻译单词
|
||||
* @return 翻译结果
|
||||
*/
|
||||
@Override
|
||||
public String customTranslate(String source) throws IOException, InterruptedException {
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("query", source);
|
||||
param.put("from", "auto");
|
||||
param.put("to", "zh");
|
||||
param.put("source", "txt");
|
||||
|
||||
String resp = httpUtil.post(url(), JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(param));
|
||||
JsonNode json = JsonUtil.parseJsonObject(resp);
|
||||
|
||||
if (!json.has("data")) {
|
||||
String errorMsg = json.get("errmsg").asText();
|
||||
if("访问出现异常,请刷新后重试!".equals(errorMsg) && !header.containsKey("Cookie")) {
|
||||
checkCookie();
|
||||
return customTranslate(source);
|
||||
}
|
||||
throw new RuntimeException(errorMsg);
|
||||
}
|
||||
|
||||
return json.get("data").get(0).get("dst").asText();
|
||||
}
|
||||
|
||||
private void checkCookie() throws IOException, InterruptedException {
|
||||
// 短时大量请求会被ban,需要添加验证cookie
|
||||
|
||||
if (header.containsKey("Cookie")) return;
|
||||
|
||||
List<HttpCookie> cookieList = CookieManager.getStore().get(URI.create("https://baidu.com"));
|
||||
boolean noneMatch = cookieList.stream()
|
||||
.filter(cookie -> "ab_sr".equals(cookie.getName()) || "BAIDUID".equals(cookie.getName()))
|
||||
.count() < 2;
|
||||
|
||||
if (noneMatch) {
|
||||
String url = "https://miao.baidu.com/abdr?_o=https%3A%2F%2Ffanyi.baidu.com";
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("data", miao_data);
|
||||
param.put("key_id", "6e75c85adea0454a");
|
||||
param.put("enc", 2);
|
||||
httpUtil.post(url, JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(param));
|
||||
}
|
||||
|
||||
cookieList = CookieManager.getStore().get(URI.create("https://baidu.com"));
|
||||
List<HttpCookie> cookies = cookieList.stream()
|
||||
.filter(cookie -> "ab_sr".equals(cookie.getName()) || "BAIDUID".equals(cookie.getName()))
|
||||
.toList();
|
||||
|
||||
String collect = cookies.stream()
|
||||
.map(cookie -> cookie.getName() + "=" + cookie.getValue())
|
||||
.collect(Collectors.joining(";"));
|
||||
|
||||
header.put("Cookie", collect);
|
||||
}
|
||||
|
||||
private static final String miao_data = "+wPJcl395nhVS+Fg/zTBIkBognAAK5IQi6wkGt3z1jk1jMuJOn+IkhaL1M7GHYxq/0V6+zKXaEVRs77WDfP/fDS0suNzgl3NdbhGSoHJEJk4fVMeU0pZxrgFq7Bzch6Aehw7MD6WlUDfyHWMarBN9OxH1UiW6Dn38uUkBjpsTDZYeNWOlmwy8YKq3FlRZiWzGYaP7K3DfBwWbT059D0KT2/mGUrHVgPVAkU572qNLzTxFyFneYTmPDQC0j+fl6+kxhA460WIG5aL19v4Gx+T7YJwIGe07QdhlQuy8Q6bpAIb8lj1SGjX9kS8P8GtI7TQv7gOlZ8ShUgV7941JXbIsJZgoWHFNq5Xge6XcVOUJsSQzAKsAgIlhgnlfC9IBAGK85KzeDC7JoBi4t0BXKgKUjYM9Veh7cA3yfudWKjSpyN6mv97apGR4Vl+8wbugPSCQRQlxPDunGUTGionf4hKxNZJYz62dwm0E/pP21PGPwnihDi7mQNxh40aE7IG0kORudLChvrcj85n4U/Rxkoq3TXTnSK1YAc4Pi/dIuR5HSUZyUBEqlvldU3jXqgUlfqeqjkn5Ug9gS6IqYEHE6FZdAGw/3y247pojWs2L56RSrUc6LoyQ7P9QNPOBr/v9HngHLRnFovrrZ57KmMl2hCiX5r1Q473CyjBhTjix3gQIFCwmM6rmEPIIU+cr0lvQrdhjxsiETk2sd17dR9gR2EjbNjeTby9QyHJTima5T6lk0K0orADrF2lNA/Xo2Iv/cMFXkc1ss7a19UQTdMLmiKgWdllWxH8Yp8B5o4KrLh+pMCGy6NeFn6FuWeAQgY8EB1r9H3PuenKaQ02VckkUPN1h1szjDI7otIFHYnQFJrp4vy4aM8wwcQS/+R2Aw0FwA5e2IWcZSwoTIc1i9YIeKgUpiConxhqZeKIhUEz33qExW0IS9mMs558JbAYsUG8KcMFtyqVo2sgtXteMAXMB9yX5/huZMR9HOO0v8x4KCZ+hwZczoVe3AmmUZLq1+rY9ngCp5D/3CcElcH3SU9HmlVXZ2VvLpa9wGIRP1UAHFuTxwVLvxJP07fbR31rLkoC22DbemRshBnv4EbIP/mkO8l/P1Y6RGEkS3b7b2MoBIWh4atrqOmUCG6w1KwESSi4Y5VKpdEkU/gL4ea7kr/GRJVWomSKSDqUEuloUI4hfLpmJs4h1JZTotOBcVjarOIW5d3j2Gm6R8X6vk24sUukOR1QOzP1gkoUaOYR+6Jcl+20IQZtIFMwFeMYtKrB5HquwYgHmUvO5hdxuFRwXosegVJmo+9MxkYjHTJbAgqZfKMc8kRjr64YBhwE30P+KLw+TafbgrA3trZQCyN3u5O5/2rk3GJmbhcDhZlYrYepj4Rymw8FB6LF0PkZZzRtWtQWv2gnMZuLmo7i5flL/MbiUMXKnsy3+Z6HHKNOqUzGUOZx84xizLkxwVMzGHrtSOub4wIhSWr7/z9GNa9qJuNJYsPXIE8wn245mHaheCm+MCxjIJMKdb72PBbQFF517kV+BxsNrxbFGe6ffJ/03KhXg0jJ1lAQc07obvOkdGVRr8DHTiS3IBfN7ofZMjVhXXpH2kUtepLfWvQf053yH2VQIEq62uxZP+RfpmdiBePEXQRBk5EA+178YbyavPE6yeu8zlub6GGlvuvrtnQmPUyvmO1hebiQK8B/V5TXxB+7IhPQ8okEll6bldVCoIxkbyAIBTK7rrDhD53HJ9capFgh/Q0XHMchYSYielT+Et8UIqUmVO94K5yl/nQhO1Vc2FsHkp2JWDDMQTuZyfmv+FS5OTeu39UznwXD36kyr2HvBEVoCMBqJrhI9WnEng53aEtcWQk4ewS+HllIoBgeJgexfs3MQK0mMP+6qV/idvB3S8Kzt/+SZsA0cHGvPvQzdePCRGNgCFOHPcpHPW/Yz4bwo4dzM64CYnL17Z4/fwcodMBG+KqG6ZyXe7EBby5RUE5OnhAGdR8T6WlIjNlU5TAIPbZ32i5nugWAcYJWJKsuITjAl4sRYY6Tpj19yIdHX2CDnU/O4arL8ijniydgcIEyYJbSCFmeJwyIv+ZSrB3RW0fYuAs4Q3AbeSK5TA9EICpB2w22kgditKO+uvrxw2LzGGND5C684YZq+XGmocAfpcte6+1PFs4NwuN41lOxDry8dwXr2mUIugcpnzsoF5Wgwyen0ISb2qaXODTvd/T5HEJcmpZdUJ1c+g+nEPi2OyX+MBXR";
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import cn.octopusyan.dmt.common.util.JsonUtil;
|
||||
import cn.octopusyan.dmt.translate.TranslateApi;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 谷歌 免费翻译接口
|
||||
@ -13,8 +16,8 @@ import java.io.IOException;
|
||||
*/
|
||||
public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
|
||||
|
||||
public FreeGoogleTranslateProcessor(TranslateSource translateSource) {
|
||||
super(translateSource);
|
||||
public FreeGoogleTranslateProcessor() {
|
||||
super(TranslateApi.FREE_GOOGLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -22,6 +25,11 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
|
||||
return "https://translate.googleapis.com/translate_a/single";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int qps() {
|
||||
return source().getDefaultQps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
@ -31,21 +39,22 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
|
||||
@Override
|
||||
public String customTranslate(String source) throws IOException, InterruptedException {
|
||||
|
||||
JSONObject form = new JSONObject();
|
||||
Map<String, Object> form = new HashMap<>();
|
||||
form.put("client", "gtx");
|
||||
form.put("dt", "t");
|
||||
form.put("sl", "auto");
|
||||
form.put("tl", "zh-CN");
|
||||
form.put("q", source);
|
||||
|
||||
JSONObject header = new JSONObject();
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
StringBuilder retStr = new StringBuilder();
|
||||
// TODO 短时大量请求会被ban,需要浏览器验证添加cookie
|
||||
String resp = httpUtil.get(url(), header, form);
|
||||
JSONArray jsonObject = JSONArray.parseArray(resp);
|
||||
for (Object o : jsonObject.getJSONArray(0)) {
|
||||
JSONArray a = (JSONArray) o;
|
||||
retStr.append(a.getString(0));
|
||||
|
||||
String resp = httpUtil.get(url(), JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(form));
|
||||
JsonNode json = JsonUtil.parseJsonObject(resp);
|
||||
|
||||
for (JsonNode o : json.get(0)) {
|
||||
retStr.append(o.get(0).asText());
|
||||
}
|
||||
|
||||
return retStr.toString();
|
@ -1,4 +1,4 @@
|
||||
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
/**
|
||||
* 翻译处理器
|
||||
@ -6,6 +6,7 @@ package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public interface TranslateProcessor {
|
||||
|
||||
/**
|
||||
* 翻译源 api接口地址
|
||||
*/
|
||||
@ -29,9 +30,9 @@ public interface TranslateProcessor {
|
||||
/**
|
||||
* 翻译
|
||||
*
|
||||
* @param source 原始文本
|
||||
* @param original 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
String translate(String source) throws Exception;
|
||||
String translate(String original) throws Exception;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package cn.octopusyan.dayzmodtranslator.util;
|
||||
package cn.octopusyan.dmt.utils;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.CanReadFileFilter;
|
||||
@ -6,11 +6,9 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@ -96,8 +94,24 @@ public class FileUtil {
|
||||
return fileName.substring(0, fileName.lastIndexOf("."));
|
||||
}
|
||||
|
||||
public static String getMimeType(Path path) {
|
||||
try {
|
||||
return Files.probeContentType(path);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Collection<File> listFile(File file) {
|
||||
return FileUtils.listFiles(file, CanReadFileFilter.CAN_READ, null);
|
||||
}
|
||||
|
||||
public static List<String> listFileNames(String path) {
|
||||
Collection<File> files = FileUtils.listFiles(new File(path), CanReadFileFilter.CAN_READ, null);
|
||||
return listFileNames(new File(path));
|
||||
}
|
||||
|
||||
public static List<String> listFileNames(File file) {
|
||||
Collection<File> files = listFile(file);
|
||||
return files.stream().map(File::getName).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -132,4 +146,36 @@ public class FileUtil {
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件的编码格式
|
||||
*
|
||||
* @return 文件编码格式
|
||||
*/
|
||||
public static String getCharsets(File file) {
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
int p = (in.read() << 8) + in.read();
|
||||
String code = "GBK";
|
||||
|
||||
switch (p) {
|
||||
case 59524:
|
||||
code = "UTF-8";
|
||||
break;
|
||||
case 0xfffe:
|
||||
code = "Unicode";
|
||||
break;
|
||||
case 0xfeff:
|
||||
code = "UTF-16BE";
|
||||
break;
|
||||
case 48581:
|
||||
code = "GBK";
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return code;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
407
src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
Normal file
407
src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
Normal file
@ -0,0 +1,407 @@
|
||||
package cn.octopusyan.dmt.utils;
|
||||
|
||||
import cn.octopusyan.dmt.common.config.Constants;
|
||||
import cn.octopusyan.dmt.common.util.ProcessesUtil;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.view.ConsoleLog;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.LineIterator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* PBO 文件工具
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class PBOUtil {
|
||||
public static final ConsoleLog consoleLog = ConsoleLog.getInstance(PBOUtil.class);
|
||||
|
||||
private static final ProcessesUtil processesUtil = ProcessesUtil.init(Constants.BIN_DIR_PATH);
|
||||
|
||||
private static final String UNPACK_COMMAND = STR."\{Constants.PBOC_FILE} unpack -o \{Constants.TMP_DIR_PATH} %s";
|
||||
private static final String PACK_COMMAND = STR."\{Constants.PBOC_FILE} pack -o %s %s";
|
||||
private static final String CFG_COMMAND = STR."\{Constants.CFG_CONVERT_FILE} %s -dst %s %s";
|
||||
|
||||
private static final String FILE_NAME_STRING_TABLE = "stringtable.csv";
|
||||
private static final String FILE_NAME_CONFIG_BIN = "config.bin";
|
||||
private static final String FILE_NAME_CONFIG_CPP = "config.cpp";
|
||||
private static final String[] FILE_NAME_LIST = new String[]{"csv", "bin", "cpp", "layout"};
|
||||
|
||||
private static final Pattern CPP_PATTERN = Pattern.compile(".*(displayName|descriptionShort) ?= ?\"(.*)\";.*");
|
||||
private static final Pattern LAYOUT_PATTERN = Pattern.compile(".*text \"(.*)\".*");
|
||||
|
||||
|
||||
public static void init() {
|
||||
|
||||
String srcFilePath = Objects.requireNonNull(PBOUtil.class.getResource("/bin")).getPath();
|
||||
try {
|
||||
File destDir = new File(Constants.BIN_DIR_PATH);
|
||||
FileUtils.forceMkdir(destDir);
|
||||
FileUtils.copyDirectory(new File(srcFilePath), destDir);
|
||||
} catch (IOException e) {
|
||||
consoleLog.error("Util 初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包pbo文件
|
||||
*
|
||||
* @param path pbo文件地址
|
||||
* @return 解包输出路径
|
||||
*/
|
||||
public static String unpack(String path) {
|
||||
return unpack(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包pbo文件
|
||||
*
|
||||
* @param pboFile pbo文件
|
||||
* @return 解包输出路径
|
||||
*/
|
||||
public static String unpack(File pboFile) {
|
||||
if (!pboFile.exists())
|
||||
throw new RuntimeException("文件不存在!");
|
||||
|
||||
File directory = new File(Constants.TMP_DIR_PATH);
|
||||
String outputPath = Constants.TMP_DIR_PATH + File.separator + FileUtil.mainName(pboFile);
|
||||
try {
|
||||
FileUtils.deleteQuietly(new File(outputPath));
|
||||
FileUtils.forceMkdir(directory);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("文件夹创建失败", e);
|
||||
}
|
||||
|
||||
String command = String.format(UNPACK_COMMAND, pboFile.getAbsolutePath());
|
||||
consoleLog.debug(STR."unpack command ==> [\{command}]");
|
||||
boolean exec = processesUtil.exec(command);
|
||||
if (!exec)
|
||||
throw new RuntimeException("解包失败!");
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打包pbo文件
|
||||
*
|
||||
* @param unpackPath pbo解包文件路径
|
||||
* @return 打包文件
|
||||
*/
|
||||
public static File pack(String unpackPath) {
|
||||
String outputPath = STR."\{unpackPath}.pbo";
|
||||
|
||||
// 打包文件临时保存路径
|
||||
File packFile = new File(outputPath);
|
||||
if (packFile.exists()) {
|
||||
// 如果存在则删除
|
||||
FileUtils.deleteQuietly(packFile);
|
||||
}
|
||||
|
||||
String command = String.format(PACK_COMMAND, Constants.TMP_DIR_PATH, unpackPath);
|
||||
consoleLog.debug(STR."pack command ==> [\{command}]");
|
||||
|
||||
boolean exec = processesUtil.exec(command);
|
||||
if (!exec) throw new RuntimeException("打包失败!");
|
||||
|
||||
return packFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可翻译文本
|
||||
*
|
||||
* @param path 根目录
|
||||
*/
|
||||
public static List<WordItem> findWord(String path) {
|
||||
return findWord(new File(path));
|
||||
}
|
||||
|
||||
public static List<WordItem> findWord(File file) {
|
||||
ArrayList<WordItem> wordItems = new ArrayList<>();
|
||||
if (!file.exists())
|
||||
return wordItems;
|
||||
|
||||
List<File> files = new ArrayList<>(FileUtils.listFiles(file, FILE_NAME_LIST, true));
|
||||
for (File item : files) {
|
||||
wordItems.addAll(findWordByFile(item));
|
||||
}
|
||||
|
||||
return wordItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入文件
|
||||
*
|
||||
* @param wordFileMap 文件对应文本map
|
||||
*/
|
||||
public static void writeWords(Map<File, List<WordItem>> wordFileMap) {
|
||||
|
||||
for (Map.Entry<File, List<WordItem>> entry : wordFileMap.entrySet()) {
|
||||
|
||||
Map<Integer, WordItem> wordMap = entry.getValue().stream()
|
||||
.collect(Collectors.toMap(WordItem::getLines, Function.identity()));
|
||||
|
||||
File file = entry.getKey();
|
||||
|
||||
// 需要转bin文件时,写入bak目录下cpp文件
|
||||
boolean hasBin = new File(outFilePath(file, ".bin")).exists();
|
||||
String writePath = file.getAbsolutePath().replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
|
||||
File writeFile = hasBin ? file : new File(writePath);
|
||||
|
||||
AtomicInteger lineIndex = new AtomicInteger(0);
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
consoleLog.info("正在写入文件[{}]", writeFile.getAbsolutePath());
|
||||
|
||||
try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
|
||||
while (it.hasNext()) {
|
||||
String line = it.next();
|
||||
WordItem word = wordMap.get(lineIndex.get());
|
||||
|
||||
// 当前行是否有需要替换的文本
|
||||
// TODO 是否替换空文本
|
||||
if (word != null && line.contains(word.getOriginal())) {
|
||||
line = line.substring(0, word.getIndex()) +
|
||||
line.substring(word.getIndex()).replace(word.getOriginal(), word.getChinese());
|
||||
}
|
||||
|
||||
// 缓存行内容
|
||||
lines.add(line);
|
||||
|
||||
lineIndex.addAndGet(1);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
|
||||
}
|
||||
|
||||
try {
|
||||
// 写入文件
|
||||
String charsets = writeFile.getName().endsWith(".layout") ? FileUtil.getCharsets(writeFile) : StandardCharsets.UTF_8.name();
|
||||
FileUtils.writeLines(writeFile, charsets, lines);
|
||||
} catch (IOException e) {
|
||||
consoleLog.error(STR."文件(\{writeFile.getAbsoluteFile()})写入失败", e);
|
||||
}
|
||||
|
||||
// CPP转BIN (覆盖TMP下BIN文件)
|
||||
if (hasBin) cpp2bin(writeFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找文件内可翻译文本
|
||||
*
|
||||
* @param file 文件
|
||||
* @return 可翻译文本信息列表
|
||||
*/
|
||||
private static List<WordItem> findWordByFile(File file) {
|
||||
if (!FILE_NAME_CONFIG_CPP.equals(file.getName())
|
||||
&& !FILE_NAME_CONFIG_BIN.equals(file.getName())
|
||||
&& !FILE_NAME_STRING_TABLE.equals(file.getName())
|
||||
&& !file.getName().endsWith(".layout")
|
||||
) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 创建备份(在bak文件夹下的同级目录
|
||||
file = createBak(file);
|
||||
|
||||
// bin转cpp
|
||||
if (FILE_NAME_CONFIG_BIN.equals(file.getName())) {
|
||||
file = bin2cpp(file);
|
||||
}
|
||||
|
||||
String charset = file.getName().endsWith(".layout") ? FileUtil.getCharsets(file) : StandardCharsets.UTF_8.name();
|
||||
|
||||
try (LineIterator it = FileUtils.lineIterator(file, charset)) {
|
||||
// CPP
|
||||
if (FILE_NAME_CONFIG_CPP.equals(file.getName())) {
|
||||
return findWordByCPP(file, it);
|
||||
}
|
||||
// CSV
|
||||
if (FILE_NAME_STRING_TABLE.equals(file.getName())) {
|
||||
return findWordByCSV(file, it);
|
||||
}
|
||||
// layout
|
||||
if (file.getName().endsWith(".layout")) {
|
||||
return findWordByLayout(file, it);
|
||||
}
|
||||
|
||||
// TODO 待添加更多文件格式
|
||||
return Collections.emptyList();
|
||||
|
||||
} catch (IOException e) {
|
||||
consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从csv文件中读取可翻译文本
|
||||
*
|
||||
* @param file csv文件
|
||||
* @param it 行内容遍历器
|
||||
* @return 可翻译文本列表
|
||||
*/
|
||||
private static List<WordItem> findWordByCSV(File file, LineIterator it) {
|
||||
ArrayList<WordItem> wordItems = new ArrayList<>();
|
||||
AtomicInteger lines = new AtomicInteger(0);
|
||||
int index = -1;
|
||||
String line;
|
||||
while (it.hasNext()) {
|
||||
line = it.next();
|
||||
List<String> split = Arrays.stream(line.split(",")).toList();
|
||||
|
||||
if (lines.get() == 0) {
|
||||
index = split.indexOf("\"chinese\"");
|
||||
} else if (index < split.size()) {
|
||||
// 原文
|
||||
String original = split.get(index).replaceAll("\"", "");
|
||||
// 开始下标
|
||||
Integer startIndex = line.indexOf(original);
|
||||
// 添加单词
|
||||
if (original.length() > 1) {
|
||||
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
|
||||
}
|
||||
}
|
||||
|
||||
lines.addAndGet(1);
|
||||
}
|
||||
return wordItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从layout文件中读取可翻译文本
|
||||
*
|
||||
* @param file layout文件
|
||||
* @param it 行内容遍历器
|
||||
* @return 可翻译文本列表
|
||||
*/
|
||||
private static List<WordItem> findWordByLayout(File file, LineIterator it) {
|
||||
ArrayList<WordItem> wordItems = new ArrayList<>();
|
||||
AtomicInteger lines = new AtomicInteger(0);
|
||||
String line;
|
||||
Matcher matcher;
|
||||
while (it.hasNext()) {
|
||||
line = it.next();
|
||||
matcher = LAYOUT_PATTERN.matcher(line);
|
||||
if (StringUtils.isNoneEmpty(line) && matcher.matches()) {
|
||||
// 原文
|
||||
String original = matcher.group(1);
|
||||
// 开始下标
|
||||
Integer startIndex = line.indexOf(original);
|
||||
// 添加单词
|
||||
if (original.length() > 1) {
|
||||
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
|
||||
}
|
||||
}
|
||||
|
||||
lines.addAndGet(1);
|
||||
}
|
||||
return wordItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取cpp文件内可翻译文本
|
||||
*
|
||||
* @param file cpp文件
|
||||
* @param it 行内容遍历器
|
||||
* @return 可翻译文本列表
|
||||
*/
|
||||
private static List<WordItem> findWordByCPP(File file, LineIterator it) {
|
||||
ArrayList<WordItem> wordItems = new ArrayList<>();
|
||||
AtomicInteger lines = new AtomicInteger(0);
|
||||
|
||||
while (it.hasNext()) {
|
||||
String line = it.next();
|
||||
|
||||
Matcher matcher = CPP_PATTERN.matcher(line);
|
||||
if (!line.contains("$") && matcher.matches()) {
|
||||
|
||||
String name = matcher.group(1);
|
||||
|
||||
// 原始文本
|
||||
int startIndex = line.indexOf(name) + name.length();
|
||||
String original = matcher.group(2);
|
||||
|
||||
// 添加单词
|
||||
if (original.length() > 1) {
|
||||
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
|
||||
}
|
||||
}
|
||||
|
||||
lines.addAndGet(1);
|
||||
}
|
||||
return wordItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份文件
|
||||
*/
|
||||
private static File createBak(File file) {
|
||||
try {
|
||||
String absolutePath = file.getAbsolutePath().replace(Constants.TMP_DIR_PATH, Constants.BAK_DIR_PATH);
|
||||
File destFile = new File(absolutePath);
|
||||
FileUtils.copyFile(file, destFile);
|
||||
return destFile;
|
||||
} catch (IOException e) {
|
||||
consoleLog.error(STR."创建备份文件失败[\{file.getAbsolutePath()}]", e);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* bin 转 cpp
|
||||
*
|
||||
* @param file bin文件
|
||||
* @return cpp文件
|
||||
*/
|
||||
private static File bin2cpp(File file) {
|
||||
boolean exec = processesUtil.exec(toTxtCommand(file));
|
||||
|
||||
if (!exec) throw new RuntimeException("bin2cpp 失败");
|
||||
|
||||
return new File(outFilePath(file, ".cpp"));
|
||||
}
|
||||
|
||||
/**
|
||||
* cpp 转 bin
|
||||
*
|
||||
* @param file bin文件
|
||||
*/
|
||||
private static void cpp2bin(File file) {
|
||||
boolean exec = processesUtil.exec(toBinCommand(file));
|
||||
|
||||
if (!exec) throw new RuntimeException("cpp2bin 失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* cpp to bin 命令
|
||||
*/
|
||||
private static String toBinCommand(File cppFile) {
|
||||
String outFilePath = outFilePath(cppFile, ".bin");
|
||||
outFilePath = outFilePath.replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
|
||||
return String.format(CFG_COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* bin to cpp 命令
|
||||
*/
|
||||
private static String toTxtCommand(File binFile) {
|
||||
String outFilePath = outFilePath(binFile, ".cpp");
|
||||
return String.format(CFG_COMMAND, "-txt", outFilePath, binFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static String outFilePath(File file, String suffix) {
|
||||
return file.getParentFile().getAbsolutePath() + File.separator + FileUtil.mainName(file) + suffix;
|
||||
}
|
||||
}
|
43
src/main/java/cn/octopusyan/dmt/utils/Resources.java
Normal file
43
src/main/java/cn/octopusyan/dmt/utils/Resources.java
Normal file
@ -0,0 +1,43 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package cn.octopusyan.dmt.utils;
|
||||
|
||||
import cn.octopusyan.dmt.AppLauncher;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
public final class Resources {
|
||||
|
||||
public static final String MODULE_DIR = "/";
|
||||
|
||||
public static InputStream getResourceAsStream(String resource) {
|
||||
String path = resolve(resource);
|
||||
return Objects.requireNonNull(
|
||||
AppLauncher.class.getResourceAsStream(resolve(path)),
|
||||
"Resource not found: " + path
|
||||
);
|
||||
}
|
||||
|
||||
public static URI getResource(String resource) {
|
||||
String path = resolve(resource);
|
||||
URL url = Objects.requireNonNull(AppLauncher.class.getResource(resolve(path)), "Resource not found: " + path);
|
||||
return URI.create(url.toExternalForm());
|
||||
}
|
||||
|
||||
public static String resolve(String resource) {
|
||||
Objects.requireNonNull(resource);
|
||||
return resource.startsWith("/") ? resource : MODULE_DIR + resource;
|
||||
}
|
||||
|
||||
public static String getPropertyOrEnv(String propertyKey, String envKey) {
|
||||
return System.getProperty(propertyKey, System.getenv(envKey));
|
||||
}
|
||||
|
||||
public static Preferences getPreferences() {
|
||||
return Preferences.userRoot().node("atlantafx");
|
||||
}
|
||||
}
|
151
src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java
Normal file
151
src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java
Normal file
@ -0,0 +1,151 @@
|
||||
package cn.octopusyan.dmt.view;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.TextArea;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 模拟控制台输出
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class ConsoleLog {
|
||||
public static final String format = "yyyy/MM/dd hh:mm:ss";
|
||||
private static final Logger log = LoggerFactory.getLogger(ConsoleLog.class);
|
||||
private static Logger markerLog;
|
||||
private static TextArea logArea;
|
||||
private final String tag;
|
||||
@Setter
|
||||
private static boolean showDebug = false;
|
||||
|
||||
public static void init(TextArea logArea) {
|
||||
ConsoleLog.logArea = logArea;
|
||||
}
|
||||
|
||||
private ConsoleLog(String tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
public static <T> ConsoleLog getInstance(Class<T> clazz) {
|
||||
markerLog = LoggerFactory.getLogger(clazz);
|
||||
return getInstance(clazz.getSimpleName());
|
||||
}
|
||||
|
||||
public static ConsoleLog getInstance(String tag) {
|
||||
return new ConsoleLog(tag);
|
||||
}
|
||||
|
||||
public static boolean isInit() {
|
||||
return log != null;
|
||||
}
|
||||
|
||||
public void info(String message, Object... param) {
|
||||
printLog(tag, Level.INFO, message, param);
|
||||
}
|
||||
|
||||
public void warning(String message, Object... param) {
|
||||
printLog(tag, Level.WARN, message, param);
|
||||
}
|
||||
|
||||
public void debug(String message, Object... param) {
|
||||
if (!showDebug) return;
|
||||
printLog(tag, Level.DEBUG, message, param);
|
||||
}
|
||||
|
||||
public void error(String message, Object... param) {
|
||||
printLog(tag, Level.ERROR, message, param);
|
||||
}
|
||||
|
||||
public void error(String message, Throwable throwable) {
|
||||
markerLog.error(message, throwable);
|
||||
message = STR."\{message} \{throwable.getMessage()}";
|
||||
printLog(tag, Level.ERROR, message);
|
||||
}
|
||||
|
||||
public void msg(String message, Object... params) {
|
||||
if (StringUtils.isEmpty(message) || !isInit()) return;
|
||||
message = format(message, params);
|
||||
message = resetConsoleColor(message);
|
||||
|
||||
print(message);
|
||||
}
|
||||
|
||||
final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public void printLog(String tag, Level level, String message, Object... params) {
|
||||
if (!isInit()) return;
|
||||
|
||||
// 时间
|
||||
String time = LocalDateTime.now().format(formatter);
|
||||
// 级别
|
||||
String levelStr = level.code;
|
||||
// 消息
|
||||
message = format(message, params);
|
||||
|
||||
// 拼接后输出
|
||||
String input = STR."\{time} \{levelStr} [\{tag}] - \{message.replace(tag, "")}";
|
||||
|
||||
switch (level) {
|
||||
case WARN -> markerLog.warn(message);
|
||||
case DEBUG -> markerLog.debug(message);
|
||||
// case ERROR -> markerLog.error(message);
|
||||
default -> markerLog.info(message);
|
||||
}
|
||||
|
||||
print(input);
|
||||
}
|
||||
|
||||
private static void print(String message) {
|
||||
var msg = message + (message.endsWith("\n") ? "" : "\n");
|
||||
Platform.runLater(() -> {
|
||||
ConsoleLog.logArea.appendText(msg);
|
||||
// 滚动到底部
|
||||
ConsoleLog.logArea.setScrollTop(Double.MAX_VALUE);
|
||||
});
|
||||
}
|
||||
|
||||
//==========================================={ 私有方法 }===================================================
|
||||
|
||||
private static String format(String msg, Object... params) {
|
||||
int i = 0;
|
||||
while (msg.contains("{}") && params != null) {
|
||||
msg = msg.replaceFirst("\\{}", String.valueOf(params[i++]).replace("\\", "\\\\"));
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理控制台输出颜色
|
||||
*
|
||||
* @param msg 输出消息
|
||||
* @return 信息
|
||||
*/
|
||||
private static String resetConsoleColor(String msg) {
|
||||
if (!msg.contains("\033[")) return msg;
|
||||
|
||||
return msg.replaceAll("\\033\\[(\\d;)?(\\d+)m", "");
|
||||
}
|
||||
|
||||
//============================{ 枚举 }================================
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Level {
|
||||
INFO("INFO", null),
|
||||
DEBUG("DEBUG", null),
|
||||
WARN("WARN", "-color-danger-emphasis"),
|
||||
ERROR("ERROR", "-color-danger-fg"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
private final String color;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package cn.octopusyan.dmt.view;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.utils.Resources;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.util.Callback;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 按钮
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class EditButtonTableCell extends TableCell<WordItem, WordItem> {
|
||||
|
||||
public static Callback<TableColumn<WordItem, WordItem>, TableCell<WordItem, WordItem>> forTableColumn(Consumer<WordItem> edit, Consumer<WordItem> translate) {
|
||||
return _ -> new EditButtonTableCell("", edit, translate);
|
||||
}
|
||||
|
||||
private final Button edit;
|
||||
private final Button translate;
|
||||
|
||||
private static final ImageView translateIcon = new ImageView(new Image(Resources.getResourceAsStream("images/icon/translate.png")));
|
||||
|
||||
static {
|
||||
translateIcon.setFitHeight(20);
|
||||
translateIcon.setFitWidth(20);
|
||||
}
|
||||
|
||||
public EditButtonTableCell(String text, Consumer<WordItem> edit, Consumer<WordItem> translate) {
|
||||
// 编辑
|
||||
this.edit = new Button(text);
|
||||
this.edit.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT);
|
||||
this.edit.setOnMouseClicked(_ -> {
|
||||
WordItem data = getTableView().getItems().get(getIndex());
|
||||
edit.accept(data);
|
||||
});
|
||||
this.edit.setGraphic(new FontIcon(Feather.EDIT));
|
||||
|
||||
// 翻译
|
||||
ImageView translateIcon = new ImageView(new Image(Resources.getResourceAsStream("images/icon/translate.png")));
|
||||
translateIcon.setFitHeight(20);
|
||||
translateIcon.setFitWidth(20);
|
||||
this.translate = new Button("", translateIcon);
|
||||
this.translate.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT);
|
||||
this.translate.setOnMouseClicked(_ -> {
|
||||
WordItem data = getTableView().getItems().get(getIndex());
|
||||
translate.accept(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(WordItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
/*
|
||||
* TODO 添加多个操作按钮
|
||||
* setGraphic(Hbox(btn1,btn2));
|
||||
*/
|
||||
setGraphic(new HBox(edit, translate));
|
||||
}
|
||||
}
|
||||
}
|
137
src/main/java/cn/octopusyan/dmt/view/PopupMenu.java
Normal file
137
src/main/java/cn/octopusyan/dmt/view/PopupMenu.java
Normal file
@ -0,0 +1,137 @@
|
||||
package cn.octopusyan.dmt.view;
|
||||
|
||||
import atlantafx.base.controls.CaptionMenuItem;
|
||||
import cn.octopusyan.dmt.common.config.Constants;
|
||||
import cn.octopusyan.dmt.common.util.ViewUtil;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
/**
|
||||
* 托盘图标 菜单
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class PopupMenu {
|
||||
// 用来隐藏弹出窗口的任务栏图标
|
||||
private static final Stage utilityStage = new Stage();
|
||||
// 菜单栏
|
||||
private final ContextMenu root = new ContextMenu();
|
||||
|
||||
static {
|
||||
utilityStage.initStyle(StageStyle.UTILITY);
|
||||
utilityStage.setScene(new Scene(new Region()));
|
||||
utilityStage.setOpacity(0);
|
||||
}
|
||||
|
||||
public PopupMenu() {
|
||||
|
||||
root.focusedProperty().addListener((_, _, focused) -> {
|
||||
if (!focused)
|
||||
Platform.runLater(() -> {
|
||||
root.hide();
|
||||
utilityStage.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public PopupMenu addItem(String label, EventHandler<ActionEvent> handler) {
|
||||
return addItem(new MenuItem(label), handler);
|
||||
}
|
||||
|
||||
public PopupMenu addItem(StringBinding bind, EventHandler<ActionEvent> handler) {
|
||||
MenuItem menuItem = new MenuItem();
|
||||
menuItem.textProperty().bind(bind);
|
||||
return addItem(menuItem, handler);
|
||||
}
|
||||
|
||||
public PopupMenu addItem(MenuItem node, EventHandler<ActionEvent> handler) {
|
||||
node.setOnAction(handler);
|
||||
return addItem(node);
|
||||
}
|
||||
|
||||
public PopupMenu addSeparator() {
|
||||
return addItem(new SeparatorMenuItem());
|
||||
}
|
||||
|
||||
public PopupMenu addCaptionItem() {
|
||||
return addCaptionItem(null);
|
||||
}
|
||||
|
||||
public PopupMenu addCaptionItem(String title) {
|
||||
return addItem(new CaptionMenuItem(title));
|
||||
}
|
||||
|
||||
public PopupMenu addMenu(String label, MenuItem... items) {
|
||||
return addMenu(new Menu(label), items);
|
||||
}
|
||||
|
||||
public PopupMenu addMenu(StringBinding label, MenuItem... items) {
|
||||
Menu menu = new Menu();
|
||||
menu.textProperty().bind(label);
|
||||
return addMenu(menu, items);
|
||||
}
|
||||
|
||||
public PopupMenu addMenu(Menu menu, MenuItem... items) {
|
||||
menu.getItems().addAll(items);
|
||||
return addItem(menu);
|
||||
}
|
||||
|
||||
public PopupMenu addTitleItem() {
|
||||
return addTitleItem(Constants.APP_TITLE);
|
||||
}
|
||||
|
||||
public PopupMenu addTitleItem(String label) {
|
||||
return addExitItem(label);
|
||||
}
|
||||
|
||||
public PopupMenu addExitItem() {
|
||||
return addExitItem("Exit");
|
||||
}
|
||||
|
||||
public PopupMenu addExitItem(String label) {
|
||||
return addItem(label, _ -> Platform.exit());
|
||||
}
|
||||
|
||||
private PopupMenu addItem(MenuItem node) {
|
||||
root.getItems().add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void show(java.awt.event.MouseEvent event) {
|
||||
// 必须调用show才会隐藏任务栏图标
|
||||
utilityStage.show();
|
||||
|
||||
if (root.isShowing())
|
||||
root.hide();
|
||||
|
||||
root.show(utilityStage,
|
||||
event.getX() / ViewUtil.scaleX,
|
||||
event.getY() / ViewUtil.scaleY
|
||||
);
|
||||
// 获取焦点 (失去焦点隐藏自身)
|
||||
root.requestFocus();
|
||||
}
|
||||
|
||||
public static MenuItem menuItem(String label, EventHandler<ActionEvent> handler) {
|
||||
MenuItem menuItem = new MenuItem(label);
|
||||
menuItem.setOnAction(handler);
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
public static MenuItem menuItem(StringBinding stringBinding, EventHandler<ActionEvent> handler) {
|
||||
MenuItem menuItem = new MenuItem();
|
||||
menuItem.textProperty().bind(stringBinding);
|
||||
menuItem.setOnAction(handler);
|
||||
return menuItem;
|
||||
}
|
||||
}
|
113
src/main/java/cn/octopusyan/dmt/view/alert/AlertUtil.java
Normal file
113
src/main/java/cn/octopusyan/dmt/view/alert/AlertUtil.java
Normal file
@ -0,0 +1,113 @@
|
||||
package cn.octopusyan.dmt.view.alert;
|
||||
|
||||
import cn.octopusyan.dmt.Application;
|
||||
import cn.octopusyan.dmt.view.alert.builder.*;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* 弹窗工具
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class AlertUtil {
|
||||
private final Window mOwner;
|
||||
private static volatile AlertUtil alertUtil;
|
||||
|
||||
private AlertUtil(Window mOwner) {
|
||||
this.mOwner = mOwner;
|
||||
}
|
||||
|
||||
public static synchronized AlertUtil getInstance() {
|
||||
if (alertUtil == null) {
|
||||
alertUtil = new AlertUtil(Application.getPrimaryStage());
|
||||
}
|
||||
return alertUtil;
|
||||
}
|
||||
|
||||
public static AlertUtil getInstance(Stage stage) {
|
||||
return new AlertUtil(stage);
|
||||
}
|
||||
|
||||
public DefaultBuilder builder() {
|
||||
return new DefaultBuilder(mOwner, true);
|
||||
}
|
||||
|
||||
public DefaultBuilder builder(boolean transparent) {
|
||||
return new DefaultBuilder(mOwner, transparent);
|
||||
}
|
||||
|
||||
public AlertBuilder info(String content) {
|
||||
return info().content(content).header(null);
|
||||
}
|
||||
|
||||
public AlertBuilder info() {
|
||||
return alert(Alert.AlertType.INFORMATION);
|
||||
}
|
||||
|
||||
public AlertBuilder error(String message) {
|
||||
return alert(Alert.AlertType.ERROR).header(null).content(message);
|
||||
}
|
||||
|
||||
public AlertBuilder warning() {
|
||||
return alert(Alert.AlertType.WARNING);
|
||||
}
|
||||
|
||||
public AlertBuilder exception(Exception ex) {
|
||||
return alert(Alert.AlertType.ERROR).exception(ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框
|
||||
*/
|
||||
public AlertBuilder confirm() {
|
||||
return alert(Alert.AlertType.CONFIRMATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义确认对话框 <p>
|
||||
*
|
||||
* @param buttons <code>"Cancel"</code> OR <code>"取消"</code> 为取消按钮
|
||||
*/
|
||||
public AlertBuilder confirm(String... buttons) {
|
||||
return confirm().buttons(buttons);
|
||||
}
|
||||
|
||||
public AlertBuilder confirm(ButtonType... buttons) {
|
||||
return confirm().buttons(buttons);
|
||||
}
|
||||
|
||||
public AlertBuilder alert(Alert.AlertType type) {
|
||||
return new AlertBuilder(mOwner, type);
|
||||
}
|
||||
|
||||
public TextInputBuilder input(String content) {
|
||||
return new TextInputBuilder(mOwner);
|
||||
}
|
||||
|
||||
public TextInputBuilder input(String content, String defaultResult) {
|
||||
return new TextInputBuilder(mOwner, defaultResult).content(content);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final <T> ChoiceBuilder<T> choices(String hintText, T... choices) {
|
||||
return new ChoiceBuilder<>(mOwner, choices).content(hintText);
|
||||
}
|
||||
|
||||
public ProgressBuilder progress() {
|
||||
return new ProgressBuilder(mOwner);
|
||||
}
|
||||
|
||||
public interface OnChoseListener {
|
||||
void confirm();
|
||||
|
||||
default void cancelOrClose(ButtonType buttonType) {
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
void onClicked(String result);
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package cn.octopusyan.dmt.view.alert.builder;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseBuilder;
|
||||
import cn.octopusyan.dmt.common.config.LabelConstants;
|
||||
import cn.octopusyan.dmt.view.alert.AlertUtil;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.Window;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class AlertBuilder extends BaseBuilder<AlertBuilder, Alert> {
|
||||
|
||||
public AlertBuilder(Window owner, Alert.AlertType alertType) {
|
||||
super(new Alert(alertType), owner);
|
||||
}
|
||||
|
||||
public AlertBuilder buttons(String... buttons) {
|
||||
dialog.getButtonTypes().addAll(getButtonList(buttons));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AlertBuilder buttons(ButtonType... buttons) {
|
||||
dialog.getButtonTypes().addAll(buttons);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AlertBuilder exception(Exception ex) {
|
||||
dialog.setTitle("Exception Dialog");
|
||||
dialog.setHeaderText(ex.getClass().getSimpleName());
|
||||
dialog.setContentText(ex.getMessage());
|
||||
|
||||
// 创建可扩展的异常。
|
||||
var sw = new StringWriter();
|
||||
var pw = new PrintWriter(sw);
|
||||
ex.printStackTrace(pw);
|
||||
var exceptionText = sw.toString();
|
||||
|
||||
var label = new Label("The exception stacktrace was :");
|
||||
|
||||
var textArea = new TextArea(exceptionText);
|
||||
textArea.setEditable(false);
|
||||
textArea.setWrapText(true);
|
||||
|
||||
textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
textArea.setMaxHeight(Double.MAX_VALUE);
|
||||
GridPane.setVgrow(textArea, Priority.ALWAYS);
|
||||
GridPane.setHgrow(textArea, Priority.ALWAYS);
|
||||
|
||||
var expContent = new GridPane();
|
||||
expContent.setMaxWidth(Double.MAX_VALUE);
|
||||
expContent.add(label, 0, 0);
|
||||
expContent.add(textArea, 0, 1);
|
||||
|
||||
// 将可扩展异常设置到对话框窗格中。
|
||||
dialog.getDialogPane().setExpandableContent(expContent);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取按钮列表
|
||||
*
|
||||
* @param buttons "Cancel" / "取消" 为取消按钮
|
||||
*/
|
||||
private List<ButtonType> getButtonList(String[] buttons) {
|
||||
if (ArrayUtils.isEmpty(buttons)) return Collections.emptyList();
|
||||
|
||||
return Arrays.stream(buttons).map((type) -> {
|
||||
ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER;
|
||||
if ("cancel".equals(StringUtils.lowerCase(type)) || LabelConstants.CANCEL.equals(type)) {
|
||||
return ButtonType.CANCEL;
|
||||
}
|
||||
return new ButtonType(type, buttonData);
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.confirm
|
||||
*/
|
||||
public void show(AlertUtil.OnClickListener listener) {
|
||||
Optional<ButtonType> result = dialog.showAndWait();
|
||||
result.ifPresent(r -> listener.onClicked(r.getText()));
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.confirm
|
||||
*/
|
||||
public void show(AlertUtil.OnChoseListener listener) {
|
||||
Optional<ButtonType> result = dialog.showAndWait();
|
||||
result.ifPresent(r -> {
|
||||
if (r == ButtonType.OK) {
|
||||
listener.confirm();
|
||||
} else {
|
||||
listener.cancelOrClose(r);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package cn.octopusyan.dmt.view.alert.builder;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseBuilder;
|
||||
import javafx.scene.control.ChoiceDialog;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class ChoiceBuilder<R> extends BaseBuilder<ChoiceBuilder<R>, ChoiceDialog<R>> {
|
||||
|
||||
@SafeVarargs
|
||||
public ChoiceBuilder(Window mOwner, R... choices) {
|
||||
this(new ChoiceDialog<>(choices[0], choices), mOwner);
|
||||
}
|
||||
|
||||
public ChoiceBuilder(ChoiceDialog<R> dialog, Window mOwner) {
|
||||
super(dialog, mOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.choices
|
||||
*/
|
||||
public R showAndGetChoice() {
|
||||
Optional<R> result = dialog.showAndWait();
|
||||
return result.orElse(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package cn.octopusyan.dmt.view.alert.builder;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseBuilder;
|
||||
import cn.octopusyan.dmt.common.util.ViewUtil;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* 默认弹窗
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class DefaultBuilder extends BaseBuilder<DefaultBuilder, Dialog<?>> {
|
||||
|
||||
public DefaultBuilder(Window mOwner) {
|
||||
this(mOwner, true);
|
||||
}
|
||||
|
||||
public DefaultBuilder(Window mOwner, boolean transparent) {
|
||||
super(new Dialog<>(), mOwner);
|
||||
|
||||
header(null);
|
||||
|
||||
DialogPane dialogPane = dialog.getDialogPane();
|
||||
if (transparent) {
|
||||
dialogPane.getScene().setFill(Color.TRANSPARENT);
|
||||
ViewUtil.bindDragged(dialogPane);
|
||||
ViewUtil.bindShadow(dialogPane);
|
||||
ViewUtil.getStage(dialogPane).initStyle(StageStyle.TRANSPARENT);
|
||||
}
|
||||
|
||||
dialogPane.getButtonTypes().add(new ButtonType("取消", ButtonType.CANCEL.getButtonData()));
|
||||
|
||||
for (Node child : dialogPane.getChildren()) {
|
||||
if (child instanceof ButtonBar) {
|
||||
dialogPane.getChildren().remove(child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DefaultBuilder content(Node content) {
|
||||
dialog.getDialogPane().setContent(content);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package cn.octopusyan.dmt.view.alert.builder;
|
||||
|
||||
import cn.octopusyan.dmt.common.config.LabelConstants;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* 加载弹窗
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class ProgressBuilder extends DefaultBuilder {
|
||||
|
||||
private HBox hBox;
|
||||
|
||||
public ProgressBuilder(Window mOwner) {
|
||||
super(mOwner);
|
||||
content(getContent());
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
hBox.setPrefWidth(width);
|
||||
}
|
||||
|
||||
private Pane getContent() {
|
||||
hBox = new HBox();
|
||||
hBox.setPrefWidth(350);
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
hBox.setSpacing(10);
|
||||
hBox.setPadding(new Insets(10, 0, 10, 0));
|
||||
|
||||
// 取消按钮
|
||||
Button cancel = new Button(LabelConstants.CANCEL);
|
||||
cancel.setCancelButton(true);
|
||||
cancel.setOnAction(_ -> dialog.close());
|
||||
|
||||
// 进度条
|
||||
ProgressBar progressBar = new ProgressBar(-1);
|
||||
progressBar.prefWidthProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> hBox.widthProperty().get() - cancel.widthProperty().get() - 40,
|
||||
hBox.widthProperty(), cancel.widthProperty()
|
||||
));
|
||||
|
||||
hBox.getChildren().add(progressBar);
|
||||
hBox.getChildren().add(cancel);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
public ProgressBuilder onCancel(Runnable run) {
|
||||
dialog.setOnCloseRequest(_ -> run.run());
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package cn.octopusyan.dmt.view.alert.builder;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseBuilder;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 获取用户输入弹窗
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class TextInputBuilder extends BaseBuilder<TextInputBuilder, TextInputDialog> {
|
||||
|
||||
public TextInputBuilder(Window mOwner) {
|
||||
this(new TextInputDialog(), mOwner);
|
||||
}
|
||||
|
||||
public TextInputBuilder(Window mOwner, String defaultResult) {
|
||||
this(new TextInputDialog(defaultResult), mOwner);
|
||||
}
|
||||
|
||||
public TextInputBuilder(TextInputDialog dialog, Window mOwner) {
|
||||
super(dialog, mOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertUtil.input
|
||||
* 如果用户点击了取消按钮,将会返回null
|
||||
*/
|
||||
public String getInput() {
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
return result.orElse(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package cn.octopusyan.dmt.view.filemanager;
|
||||
|
||||
import atlantafx.base.theme.Tweaks;
|
||||
import cn.octopusyan.dmt.utils.FileUtil;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Comparator;
|
||||
|
||||
public final class DirectoryTree extends TreeView<File> {
|
||||
public static final FileIconRepository fileIcon = new FileIconRepository();
|
||||
|
||||
// 文件夹在前
|
||||
static final Comparator<TreeItem<File>> FILE_TYPE_COMPARATOR = Comparator.comparing(
|
||||
item -> !Files.isDirectory(item.getValue().toPath())
|
||||
);
|
||||
|
||||
public DirectoryTree() {
|
||||
super();
|
||||
|
||||
getStyleClass().add(Tweaks.ALT_ICON);
|
||||
|
||||
setCellFactory(_ -> new TreeCell<>() {
|
||||
@Override
|
||||
protected void updateItem(File item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
|
||||
var image = new ImageView(item.isDirectory() ?
|
||||
FileIconRepository.FOLDER :
|
||||
fileIcon.getByMimeType(FileUtil.getMimeType(item.toPath()))
|
||||
);
|
||||
image.setFitWidth(20);
|
||||
image.setFitHeight(20);
|
||||
setGraphic(image);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadRoot(File value) {
|
||||
var root = new TreeItem<>(value);
|
||||
root.setExpanded(true);
|
||||
setRoot(root);
|
||||
|
||||
// scan file tree two levels deep for starters
|
||||
scan(root, 5);
|
||||
|
||||
// scan deeper as the user navigates down the tree
|
||||
root.addEventHandler(TreeItem.branchExpandedEvent(), event -> {
|
||||
TreeItem parent = event.getTreeItem();
|
||||
parent.getChildren().forEach(child -> {
|
||||
var item = (TreeItem<File>) child;
|
||||
if (item.getChildren().isEmpty()) {
|
||||
scan(item, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static void scan(TreeItem<File> parent, int depth) {
|
||||
File[] files = parent.getValue().listFiles();
|
||||
depth--;
|
||||
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
var item = new TreeItem<>(f);
|
||||
parent.getChildren().add(item);
|
||||
|
||||
if (depth > 0) {
|
||||
scan(item, depth);
|
||||
}
|
||||
}
|
||||
// 文件类型+名称排序
|
||||
parent.getChildren().sort(FILE_TYPE_COMPARATOR.thenComparing(TreeItem::getValue));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package cn.octopusyan.dmt.view.filemanager;
|
||||
|
||||
import cn.octopusyan.dmt.utils.Resources;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import javax.swing.filechooser.FileSystemView;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FileIconRepository {
|
||||
|
||||
public static final String IMAGE_DIRECTORY = "images/papirus/";
|
||||
public static final Image UNKNOWN_FILE = new Image(
|
||||
Resources.getResourceAsStream(IMAGE_DIRECTORY + "mimetypes/text-plain.png")
|
||||
);
|
||||
public static final Image FOLDER = new Image(
|
||||
Resources.getResourceAsStream(IMAGE_DIRECTORY + "places/folder-paleorange.png")
|
||||
);
|
||||
|
||||
private final Map<String, Image> cache = new HashMap<>();
|
||||
private final Set<String> unknownMimeTypes = new HashSet<>();
|
||||
|
||||
public Image getByMimeType(String mimeType) {
|
||||
if (mimeType == null || unknownMimeTypes.contains(mimeType)) {
|
||||
return UNKNOWN_FILE;
|
||||
}
|
||||
|
||||
var cachedImage = cache.get(mimeType);
|
||||
if (cachedImage != null) {
|
||||
return cachedImage;
|
||||
}
|
||||
|
||||
var fileName = mimeType.replaceAll("/", "-") + ".png";
|
||||
|
||||
try {
|
||||
var image = new Image(Resources.getResourceAsStream(IMAGE_DIRECTORY + "mimetypes/" + fileName));
|
||||
cache.put(mimeType, image);
|
||||
return image;
|
||||
} catch (Exception e) {
|
||||
unknownMimeTypes.add(mimeType);
|
||||
return UNKNOWN_FILE;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFileName(File file) {
|
||||
return FileSystemView.getFileSystemView().getSystemDisplayName(file);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cn.octopusyan.dmt.viewModel;
|
||||
|
||||
import cn.octopusyan.dmt.common.base.BaseViewModel;
|
||||
import cn.octopusyan.dmt.controller.help.AboutController;
|
||||
|
||||
/**
|
||||
* 关于
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class AboutViewModel extends BaseViewModel<AboutController> {
|
||||
}
|
210
src/main/java/cn/octopusyan/dmt/viewModel/MainViewModel.java
Normal file
210
src/main/java/cn/octopusyan/dmt/viewModel/MainViewModel.java
Normal file
@ -0,0 +1,210 @@
|
||||
package cn.octopusyan.dmt.viewModel;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import cn.octopusyan.dmt.common.base.BaseViewModel;
|
||||
import cn.octopusyan.dmt.controller.MainController;
|
||||
import cn.octopusyan.dmt.model.WordItem;
|
||||
import cn.octopusyan.dmt.task.PackTask;
|
||||
import cn.octopusyan.dmt.task.TranslateTask;
|
||||
import cn.octopusyan.dmt.task.UnpackTask;
|
||||
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
|
||||
import cn.octopusyan.dmt.translate.DelayWord;
|
||||
import cn.octopusyan.dmt.translate.TranslateUtil;
|
||||
import cn.octopusyan.dmt.view.ConsoleLog;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 主界面
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
public class MainViewModel extends BaseViewModel<MainController> {
|
||||
private static final ConsoleLog consoleLog = ConsoleLog.getInstance(MainViewModel.class);
|
||||
/**
|
||||
* 解包任务
|
||||
*/
|
||||
private UnpackTask unpackTask;
|
||||
/**
|
||||
* 翻译任务
|
||||
*/
|
||||
private TranslateTask translateTask;
|
||||
private DelayQueue<DelayWord> delayQueue;
|
||||
private String unpackPath;
|
||||
private int total;
|
||||
|
||||
FontIcon startIcon = new FontIcon(Feather.PLAY);
|
||||
FontIcon pauseIcon = new FontIcon(Feather.PAUSE);
|
||||
private List<WordItem> wordItems;
|
||||
|
||||
private final StringProperty fileName = new SimpleStringProperty();
|
||||
|
||||
public StringProperty fileNameProperty() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载PBO文件
|
||||
*/
|
||||
public void selectFile(File pboFile) {
|
||||
if (pboFile == null) return;
|
||||
|
||||
fileName.setValue(pboFile.getAbsolutePath());
|
||||
|
||||
unpackTask = new UnpackTask(pboFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包
|
||||
*/
|
||||
public void unpack() {
|
||||
if (unpackTask == null) return;
|
||||
|
||||
unpackTask.onListen(new UnpackTask.UnpackListener() {
|
||||
|
||||
@Override
|
||||
public void onRunning() {
|
||||
// 展示加载
|
||||
controller.onLoad();
|
||||
// 重置进度
|
||||
resetProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnpackOver(String path) {
|
||||
MainViewModel.this.unpackPath = path;
|
||||
Platform.runLater(() -> controller.onUnpack(new File(path)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindWordOver(List<WordItem> wordItems) {
|
||||
total = wordItems.size();
|
||||
MainViewModel.this.wordItems = wordItems;
|
||||
Platform.runLater(() -> controller.onLoadWord(wordItems));
|
||||
}
|
||||
});
|
||||
|
||||
unpackTask.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始翻译
|
||||
*/
|
||||
public void startTranslate() {
|
||||
if(wordItems.isEmpty()) return;
|
||||
|
||||
if (translateTask == null) {
|
||||
List<WordItem> words = wordItems.stream().filter(item -> StringUtils.isEmpty(item.getChinese())).toList();
|
||||
delayQueue = TranslateUtil.getDelayQueue(words);
|
||||
translateTask = createTask();
|
||||
}
|
||||
|
||||
if (!translateTask.isRunning()) {
|
||||
// 检查进度
|
||||
if (!delayQueue.isEmpty()) {
|
||||
AtomicInteger index = new AtomicInteger(0);
|
||||
delayQueue.forEach(item -> TranslateUtil.resetDelayTime(index.getAndIncrement(), item));
|
||||
translateTask = createTask();
|
||||
}
|
||||
|
||||
if (translateTask.getState() != Worker.State.SUCCEEDED) {
|
||||
translateTask.execute();
|
||||
// 展示进度
|
||||
controller.translateProgress.setVisible(true);
|
||||
controller.translateProgress.progressProperty().bind(translateTask.progressProperty());
|
||||
}
|
||||
} else {
|
||||
translateTask.cancel();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 打包
|
||||
*/
|
||||
public void pack() {
|
||||
if(wordItems.isEmpty()) return;
|
||||
|
||||
PackTask packTask = new PackTask(wordItems, unpackPath);
|
||||
packTask.onListen(new PackTask.PackListener() {
|
||||
@Override
|
||||
public void onWriteOver() {
|
||||
consoleLog.info("写入完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackOver(File file) {
|
||||
Platform.runLater(() -> controller.onPackOver(file));
|
||||
}
|
||||
|
||||
});
|
||||
packTask.execute();
|
||||
}
|
||||
|
||||
private TranslateTask createTask() {
|
||||
TranslateTask task = new TranslateTask(delayQueue, total);
|
||||
task.onListen(new DefaultTaskListener() {
|
||||
|
||||
@Override
|
||||
public void onRunning() {
|
||||
ProgressIndicator graphic = new ProgressIndicator();
|
||||
graphic.setPrefWidth(15);
|
||||
graphic.setPrefHeight(15);
|
||||
graphic.setOnMouseClicked(_ -> controller.startTranslate());
|
||||
controller.translate.setGraphic(graphic);
|
||||
controller.translateProgress.setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled() {
|
||||
task.getThreadPoolManager().shutdownNow();
|
||||
TranslateTask.consoleLog.info("翻译暂停");
|
||||
Platform.runLater(() -> controller.translate.setGraphic(pauseIcon));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSucceed() {
|
||||
if (delayQueue.isEmpty()) {
|
||||
Platform.runLater(() -> controller.translate.setGraphic(startIcon));
|
||||
} else {
|
||||
Platform.runLater(() -> controller.translate.setGraphic(pauseIcon));
|
||||
}
|
||||
}
|
||||
});
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载PBO文件后,重置进度
|
||||
*/
|
||||
private void resetProgress() {
|
||||
translateTask = null;
|
||||
controller.translate.setGraphic(startIcon);
|
||||
Styles.toggleStyleClass(controller.translateProgress, Styles.SMALL);
|
||||
controller.translateProgress.progressProperty().unbind();
|
||||
controller.translateProgress.setProgress(0);
|
||||
controller.translateProgress.setVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定字符串是否含有中文
|
||||
*
|
||||
* @param str 需要判断的字符串
|
||||
* @return 是否含有中文
|
||||
*/
|
||||
private boolean containsChinese(String str) {
|
||||
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user