Compare commits

...

3 Commits

Author SHA1 Message Date
9edc015af0 feat: 添加about页面 2024-11-15 17:03:48 +08:00
1dc7a64833 chore: 整理 2024-11-15 17:02:52 +08:00
943056168f pref: 更新界面、添加翻译进度反馈、展示日志信息
feat: 添加百度free翻译接口
2024-11-15 01:55:34 +08:00
1103 changed files with 7543 additions and 4106 deletions

5
.gitignore vendored
View File

@ -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
View File

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

108
README.md
View File

@ -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>
@ -49,43 +38,50 @@
#### 环境说明
| 名称 | 描述 |
|-------|---------------------------------------------------------|
| 系统环境 | windows11/10 |
| JDK版本 | 17 |
| 构建工具 | Mavne |
| 打包工具 | [JavaPackager](https://github.com/fvarrui/JavaPackager) |
| 名称 | 描述 |
|------|---------------------------------------------------------|
| 系统环境 | 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
```
2. 运行
```bash
mvn clean javafx:run
```
#### 打包
1. 克隆代码
```bash
git clone https://github.com/octopusYan/dayz-mod-translator
```
2. 运行
```bash
mvn clean package
```
3. 打包
- 使用 `IntelliJ IDEA` Maven UI
- 点击右侧工具栏的`Maven`
- 展开 `DayzModTranslator\Lifecycle`
- 点击 package
- 使用 mvn 命令
```bash
mvn 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

162
pom.xml
View File

@ -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>

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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";
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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("类型不存在");
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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);
}
}

View File

@ -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();
}
}

View File

@ -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() {
}
}

View File

@ -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();
}
}

View 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);
}
}

View 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);
}
}

View 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();
});
}
}

View 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();
}
}

View File

@ -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;
}

View 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";
}

View 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()}");
}
}
}

View File

@ -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 = "取消";
}

View 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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}};
}

View File

@ -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();

View File

@ -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());
}
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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
);
}

View 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;
}
}

View 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) {
}
}
}

View File

@ -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;
}
/**
* 根据属性名读取值
* 先去主配置查询如果查询不到就去启用配置查询

View 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();
}
}
}

View 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;
}
}

View File

@ -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));
});
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View 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();
}

View 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();
}

View 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;
}
}

View 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()}";
}
}

View 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();
}
}

View 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);
}
}

View 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}");
}
}

View 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();
}
}

View 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);
}
}

View 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 ...
}
}
}

View 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);
}
}

View 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();
}

View File

@ -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();
}

View File

@ -0,0 +1,10 @@
package cn.octopusyan.dmt.translate;
/**
* API 密钥配置
*
* @author octopus_yan@foxmail.com
*/
public record ApiKey(String appid, String apiKey) {
}

View 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);
}
}

View 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("类型不存在");
}
}

View 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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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());
}
/**

View File

@ -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";
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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);
}
}
}

View 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;
}
}

View 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");
}
}

View 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;
}
}

View File

@ -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));
}
}
}

View 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;
}
}

View 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);
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View 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> {
}

View 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