first commit

This commit is contained in:
octopus_yan 2024-02-19 14:17:43 +08:00
commit 08c80f20ae
51 changed files with 4950 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
.mvn/
mvnw
mvnw.cmd
log/
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# DayZ Mod Translator
使用 JavaFx 编写的 DayZ 游戏mod 汉化GUI工具
## 截图
![Main window start](doc/img/screenshot01.png 'Main application window start')
![Main window open file](doc/img/screenshot02.png 'Main window open file')

BIN
doc/img/screenshot01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
doc/img/screenshot02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

194
pom.xml Normal file
View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.octopusyan</groupId>
<artifactId>dayz-mod-translator</artifactId>
<version>0.0.1</version>
<name>DayzModTranslator</name>
<organization>
<name>octopus_yan</name>
<url>octopus_yan@foxmail.com</url>
</organization>
<inceptionYear>2024</inceptionYear>
<description>DayZ 模组汉化工具</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>
<junit.version>5.10.0</junit.version>
<javafx.version>17.0.6</javafx.version>
<slf4j.version>2.0.12</slf4j.version>
<logback.version>1.4.14</logback.version>
<fastjson.version>2.0.46</fastjson.version>
<hutool.version>5.8.25</hutool.version>
</properties>
<dependencies>
<!-- javafx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- slf4j -->
<!--<dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-simple</artifactId>-->
<!-- <version>${slf4j.version}</version>-->
<!--</dependency>-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- common -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</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>
</dependency>
<!-- JSON -->
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>exe</nonFilteredFileExtension>
<nonFilteredFileExtension>dll</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<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>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.github.fvarrui</groupId>
<artifactId>javapackager</artifactId>
<version>1.7.5</version>
<configuration>
<bundleJre>true</bundleJre>
<mainClass>cn.octopusyan.dayzmodtranslator.AppLuncher</mainClass>
<generateInstaller>false</generateInstaller>
</configuration>
<executions>
<execution>
<id>bundling-for-windows</id>
<phase>package</phase>
<goals>
<goal>package</goal>
</goals>
<configuration>
<platform>windows</platform>
<createZipball>true</createZipball>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,105 @@
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

@ -0,0 +1,217 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,174 @@
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

@ -0,0 +1,515 @@
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

@ -0,0 +1,162 @@
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

@ -0,0 +1,127 @@
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

@ -0,0 +1,137 @@
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

@ -0,0 +1,628 @@
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

@ -0,0 +1,49 @@
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

@ -0,0 +1,90 @@
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

@ -0,0 +1,183 @@
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

@ -0,0 +1,141 @@
package cn.octopusyan.dayzmodtranslator.manager.http;
import com.alibaba.fastjson2.JSONObject;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
/**
* 网络请求封装
*
* @author octopus_yan@foxmail.com
*/
public class HttpUtil {
private volatile static HttpUtil util;
private volatile HttpClient httpClient;
private final HttpConfig httpConfig;
private HttpUtil(HttpConfig httpConfig) {
this.httpConfig = httpConfig;
this.httpClient = createClient(httpConfig);
}
public static HttpUtil getInstance() {
if (util == null) {
throw new RuntimeException("are you ready ?");
}
return util;
}
public static void init(HttpConfig httpConfig) {
synchronized (HttpUtil.class) {
util = new HttpUtil(httpConfig);
}
}
private static HttpClient createClient(HttpConfig httpConfig) {
HttpClient.Builder builder = HttpClient.newBuilder()
.version(httpConfig.getVersion())
.connectTimeout(Duration.ofMillis(httpConfig.getConnectTimeout()))
.sslContext(httpConfig.getSslContext())
.sslParameters(httpConfig.getSslParameters())
.followRedirects(httpConfig.getRedirect());
Optional.ofNullable(httpConfig.getAuthenticator()).ifPresent(builder::authenticator);
Optional.ofNullable(httpConfig.getCookieHandler()).ifPresent(builder::cookieHandler);
Optional.ofNullable(httpConfig.getProxySelector()).ifPresent(builder::proxy);
Optional.ofNullable(httpConfig.getExecutor()).ifPresent(builder::executor);
return builder.build();
}
public HttpUtil proxy(String host, int port) {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(host, port);
ProxySelector other = ProxySelector.of(unresolved);
this.httpConfig.setProxySelector(other);
this.httpClient = createClient(httpConfig);
return this;
}
public void clearProxy() {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
httpClient = createClient(httpConfig);
}
public HttpClient getHttpClient() {
return httpClient;
}
public String get(String uri, JSONObject header, JSONObject 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 {
HttpRequest.Builder request = getRequest(uri, header)
.POST(HttpRequest.BodyPublishers.ofString(param.toJSONString()));
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 {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
.POST(HttpRequest.BodyPublishers.noBody());
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
private HttpRequest.Builder getRequest(String uri, JSONObject 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));
}
}
return request;
}
private String createFormParams(JSONObject 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);
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 {
formParams.append("&").append(key).append("=").append(params.getJSONObject(key));
}
}
if (!formParams.isEmpty()) {
formParams = new StringBuilder("?" + formParams.substring(1));
}
return formParams.toString();
}
}

View File

@ -0,0 +1,49 @@
package cn.octopusyan.dayzmodtranslator.manager.thread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义线程工厂
*
* @author octopus_yan@foxmail.com
*/
public class ThreadFactory implements java.util.concurrent.ThreadFactory {
private static final Logger logger = LoggerFactory.getLogger(ThreadFactory.class);
public static final String DEFAULT_THREAD_PREFIX = "thread-factory-pool";
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public ThreadFactory() {
this(DEFAULT_THREAD_PREFIX);
}
public ThreadFactory(String prefix) {
group = Thread.currentThread().getThreadGroup();
namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable runnable) {
Thread t = new Thread(group, runnable,
namePrefix + threadNumber.getAndIncrement(),
0);
t.setUncaughtExceptionHandler((t1, e) -> {
logger.error("thread : {}, error", t1.getName(), e);
});
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}

View File

@ -0,0 +1,29 @@
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

@ -0,0 +1,27 @@
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

@ -0,0 +1,65 @@
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

@ -0,0 +1,227 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,81 @@
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

@ -0,0 +1,99 @@
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

@ -0,0 +1,111 @@
package cn.octopusyan.dayzmodtranslator.manager.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 java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.UUID;
/**
* 谷歌 免费翻译接口
*
* @author octopus_yan@foxmail.com
*/
public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
private ApiKey apiKey;
public BaiduTranslateProcessor(TranslateSource translateSource) {
super(translateSource);
}
@Override
public String url() {
return "https://fanyi-api.baidu.com/api/trans/vip/translate";
}
/**
* 翻译处理
*
* @param source 待翻译单词
* @return 翻译结果
*/
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
apiKey = getApiKey();
String appid = apiKey.getAppid();
String salt = UUID.randomUUID().toString().replace("-", "");
JSONObject param = new JSONObject();
param.put("q", source);
param.put("from", "auto");
param.put("to", "zh");
param.put("appid", appid);
param.put("salt", salt);
param.put("sign", getSign(appid, source, salt));
String resp = httpUtil.get(url(), null, param);
JSONObject jsonObject = JSON.parseObject(resp);
if (!jsonObject.containsKey("trans_result")) {
Object errorMsg = jsonObject.get("error_msg");
logger.error("翻译失败: {}", errorMsg);
throw new RuntimeException("翻译失败: " + errorMsg);
}
return jsonObject.getJSONArray("trans_result").getJSONObject(0).getString("dst");
}
private String getSign(String appid, String q, String salt) {
return encrypt2ToMD5(appid + q + salt + apiKey.getApiKey());
}
/**
* MD5加密
*
* @param str 待加密字符串
* @return 16进制加密字符串
*/
public String encrypt2ToMD5(String str) {
// 加密后的16进制字符串
String hexStr = "";
try {
// MessageDigest 类为应用程序提供信息摘要算法的功能
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 转换为MD5码
byte[] digest = md5.digest(str.getBytes(StandardCharsets.UTF_8));
hexStr = bytesToHexString(digest);
} catch (Exception e) {
logger.error("", e);
}
return hexStr.toLowerCase();
}
/**
* 将byte数组转换成string类型表示
*/
private String bytesToHexString(byte[] src) {
StringBuilder builder = new StringBuilder();
if (src == null || src.length == 0) {
return null;
}
String hv;
for (byte b : src) {
// 以十六进制基数 16无符号整数形式返回一个整数参数的字符串表示形式并转换为大写
hv = Integer.toHexString(b & 0xFF).toUpperCase();
if (hv.length() < 2) {
builder.append(0);
}
builder.append(hv);
}
return builder.toString();
}
}

View File

@ -0,0 +1,53 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import java.io.IOException;
/**
* 谷歌 免费翻译接口
*
* @author octopus_yan@foxmail.com
*/
public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
public FreeGoogleTranslateProcessor(TranslateSource translateSource) {
super(translateSource);
}
@Override
public String url() {
return "https://translate.googleapis.com/translate_a/single";
}
/**
* 翻译处理
*
* @param source 待翻译单词
* @return 翻译结果
*/
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
JSONObject form = new JSONObject();
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();
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));
}
return retStr.toString();
}
}

View File

@ -0,0 +1,37 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
/**
* 翻译处理器
*
* @author octopus_yan@foxmail.com
*/
public interface TranslateProcessor {
/**
* 翻译源 api接口地址
*/
String url();
/**
* 是否需要配置API认证
*/
boolean needApiKey();
/**
* 已配置API认证
*/
boolean configuredKey();
/**
* qps 每秒访问的数量限制
*/
int qps();
/**
* 翻译
*
* @param source 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
String translate(String source) throws Exception;
}

View File

@ -0,0 +1,52 @@
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

@ -0,0 +1,98 @@
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

@ -0,0 +1,228 @@
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

@ -0,0 +1,37 @@
package cn.octopusyan.dayzmodtranslator.util;
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
*/
public class ClipUtil {
//获取系统剪切板
private static final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
public static void setClip(String data) {
//构建String数据类型
StringSelection stringSelection = new StringSelection(data);
//添加文本到系统剪切板
clipboard.setContents(stringSelection, null);
}
public static String getString() {
Transferable content = clipboard.getContents(null);//从系统剪切板中获取数据
if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {//判断是否为文本类型
try {
//从数据中获取文本值
return (String) content.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException | IOException e) {
e.printStackTrace();
return null;
}
}
return null;
}
}

View File

@ -0,0 +1,135 @@
package cn.octopusyan.dayzmodtranslator.util;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.CanReadFileFilter;
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.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 文件工具类
*
* @author octopus_yan@foxmail.com
*/
public class FileUtil {
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
public static File[] ls(String path) {
File dir = new File(path);
if (!dir.exists())
throw new RuntimeException(path + "不存在!");
if (!dir.isDirectory())
throw new RuntimeException(path + "不是一个文件夹!");
return dir.listFiles();
}
public static void copyFilesFromDir(String path, String dest) throws IOException {
if (StringUtils.isBlank(path) || StringUtils.isBlank(dest)) {
logger.error("path is blank !");
return;
}
File dir = new File(path);
if (!dir.exists()) {
logger.error("[" + path + "] 不存在!");
return;
}
if (!dir.isDirectory()) {
logger.error("[" + path + "] 不是一个文件夹!");
}
File[] files = dir.listFiles();
if (files == null) return;
File directory = new File(dest);
if (directory.exists() && !directory.isDirectory()) {
logger.error("[" + dest + "] 不是一个文件夹!");
}
FileUtils.forceMkdir(directory);
for (File file : files) {
copyFile(file, new File(dest + File.separator + file.getName()));
}
}
public static void copyFile(File in, File out) throws IOException {
copyFile(Files.newInputStream(in.toPath()), out);
}
public static void copyFile(InputStream input, File out) throws IOException {
OutputStream output = null;
try {
output = Files.newOutputStream(out.toPath());
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
} catch (IOException e) {
logger.error("", e);
} finally {
if (output != null) input.close();
if (output != null) output.close();
}
}
/**
* 获取文件主名称
*
* @param file 文件对象
* @return 文件名称
*/
public static String mainName(File file) {
//忽略判断
String fileName = file.getName();
return fileName.substring(0, fileName.lastIndexOf("."));
}
public static List<String> listFileNames(String path) {
Collection<File> files = FileUtils.listFiles(new File(path), CanReadFileFilter.CAN_READ, null);
return files.stream().map(File::getName).collect(Collectors.toList());
}
/**
* 返回被查找到的文件的绝对路径匹配到一个就返回
*
* @param root 根目录文件
* @param fileName 要找的文件名
* @return 绝对路径
*/
private static String findFiles(File root, String fileName) {
//定义一个返回值
String path = null;
//如果传进来的是目录并且存在
if (root.exists() && root.isDirectory()) {
//遍历文件夹中的各个文件
File[] files = root.listFiles();
if (files != null) {
for (File file : files) {
//如果path的值没有变化
if (path == null) {
if (file.isFile() && file.getName().contains(fileName)) {
path = file.getAbsolutePath();
} else {
path = findFiles(file, fileName);
}
} else {
break;//跳出循环增加性能
}
}
}
}
return path;
}
}

View File

@ -0,0 +1,26 @@
package cn.octopusyan.dayzmodtranslator.util;
import javafx.fxml.FXMLLoader;
import javafx.fxml.JavaFXBuilderFactory;
import java.nio.charset.StandardCharsets;
/**
* FXML 工具
*
* @author octopus_yan@foxmail.com
*/
public class FxmlUtil {
public static FXMLLoader load(String name) {
String prefix = "/fxml/";
String suffix = ".fxml";
return new FXMLLoader(
FxmlUtil.class.getResource(prefix + name + suffix),
null,
new JavaFXBuilderFactory(),
null,
StandardCharsets.UTF_8
);
}
}

View File

@ -0,0 +1,83 @@
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

@ -0,0 +1,96 @@
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

@ -0,0 +1,85 @@
package cn.octopusyan.dayzmodtranslator.util;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 配置文件读取工具
*
* @author liubin5620
* @see <a href="https://blog.csdn.net/liubin5620/article/details/104618950">配置文件信息读取工具类PropertiesUtils</a>
*/
public class PropertiesUtils {
/**
* 主配置文件
*/
private Properties properties;
/**
* 启用配置文件
*/
private Properties propertiesCustom;
private static PropertiesUtils propertiesUtils = new PropertiesUtils();
/**
* 私有构造禁止直接创建
*/
private PropertiesUtils() {
// 读取配置启用的配置文件名
properties = new Properties();
propertiesCustom = new Properties();
InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("application.properties");
try {
properties.load(in);
// 加载启用的配置
String property = properties.getProperty("profiles.active");
if (!StringUtils.isBlank(property)) {
InputStream cin = PropertiesUtils.class.getClassLoader().getResourceAsStream("application-" + property + ".properties");
propertiesCustom.load(cin);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取单例
*
* @return PropertiesUtils
*/
public static PropertiesUtils getInstance() {
if (propertiesUtils == null) {
propertiesUtils = new PropertiesUtils();
}
return propertiesUtils;
}
/**
* 获取单例
*
* @return PropertiesUtils
*/
public static PropertiesUtils getInstance(File file) {
PropertiesUtils util = new PropertiesUtils();
return util;
}
/**
* 根据属性名读取值
* 先去主配置查询如果查询不到就去启用配置查询
*
* @param name 名称
*/
public String getProperty(String name) {
String val = properties.getProperty(name);
if (StringUtils.isBlank(val)) {
val = propertiesCustom.getProperty(name);
}
return val;
}
}

View File

@ -0,0 +1,113 @@
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,22 @@
module cn.octopusyan.dayzmodtranslator {
requires java.net.http;
requires javafx.controls;
requires javafx.fxml;
requires javafx.swing;
requires org.apache.commons.io;
requires org.apache.commons.lang3;
requires org.apache.commons.exec;
requires org.slf4j;
requires ch.qos.logback.core;
requires ch.qos.logback.classic;
requires com.alibaba.fastjson2;
exports cn.octopusyan.dayzmodtranslator;
opens cn.octopusyan.dayzmodtranslator to javafx.fxml;
exports cn.octopusyan.dayzmodtranslator.base;
opens cn.octopusyan.dayzmodtranslator.base to javafx.fxml;
exports cn.octopusyan.dayzmodtranslator.controller;
opens cn.octopusyan.dayzmodtranslator.controller to javafx.fxml;
exports cn.octopusyan.dayzmodtranslator.manager.word;
opens cn.octopusyan.dayzmodtranslator.manager.word to javafx.base;
}

View File

@ -0,0 +1,3 @@
app.name=${project.name}
app.title=DayZ\u6A21\u7EC4\u6C49\u5316\u5DE5\u5177
app.version=${project.version}

View File

@ -0,0 +1,9 @@
#root{
-fx-background-color: white;
}
#dragFileLabel {
-fx-font-family: "Microsoft YaHei";
-fx-text-fill: black;
-fx-font-size: 20;
}

View File

@ -0,0 +1,10 @@
.box_class {
-fx-background-radius: 5;
-fx-border-style: solid;
-fx-border-color: rgba(136, 136, 136, 0.5);
-fx-background-color: white;
}
.conf_menu_item {
-fx-pref-width: 100;
}

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox fx:id="root" minHeight="330" minWidth="430" prefHeight="430.0" prefWidth="700" stylesheets="@../css/main-view.css"
xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cn.octopusyan.dayzmodtranslator.controller.MainController">
<MenuBar>
<Menu mnemonicParsing="false" text="文件">
<MenuItem fx:id="openFileSetupBtn" mnemonicParsing="false" styleClass="conf_menu_item" text="打开"/>
<SeparatorMenuItem/>
<MenuItem onAction="#onDestroy" mnemonicParsing="false" styleClass="conf_menu_item" text="退出"/>
</Menu>
<Menu mnemonicParsing="false" text="设置">
<MenuItem fx:id="translateSetupBtn" mnemonicParsing="false" styleClass="conf_menu_item" text="翻译"/>
<MenuItem fx:id="proxySetupBtn" mnemonicParsing="false" styleClass="conf_menu_item" text="代理"/>
</Menu>
<Menu mnemonicParsing="false" text="帮助">
<MenuItem mnemonicParsing="false" styleClass="conf_menu_item" text="关于"/>
</Menu>
</MenuBar>
<HBox alignment="CENTER_LEFT">
<Label text="PBO文件:">
<HBox.margin>
<Insets right="5.0"/>
</HBox.margin>
</Label>
<Label fx:id="filePath"/>
<HBox HBox.hgrow="ALWAYS"/>
<HBox>
<Button fx:id="translateWordBtn" text="一键翻译" visible="false"/>
<Button fx:id="packBtn" text="打包" visible="false">
<HBox.margin>
<Insets left="5"/>
</HBox.margin>
</Button>
</HBox>
<VBox.margin>
<Insets bottom="5.0" top="5.0"/>
</VBox.margin>
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</HBox>
<SplitPane dividerPositions="0.3" prefHeight="${root.height}">
<StackPane fx:id="fileBox" SplitPane.resizableWithParent="false" maxWidth="420" minWidth="210">
<VBox fx:id="openFileBox" alignment="CENTER" styleClass="box_class">
<Button fx:id="openFile" text="打开文件"/>
<Label style="-fx-text-fill: rgba(136,136,136,0.7)" text="或将 pbo文件 拖到此处">
<VBox.margin>
<Insets top="10.0"/>
</VBox.margin>
</Label>
</VBox>
<VBox fx:id="dragFileBox" alignment="CENTER" styleClass="box_class" visible="false">
<Label fx:id="dragFileLabel" alignment="CENTER" prefHeight="-Infinity" text="将文件拖放到此处"
textAlignment="CENTER"/>
</VBox>
<VBox fx:id="loadFileBox" alignment="CENTER" styleClass="box_class" visible="false">
<Label fx:id="loadFileLabel"/>
<ProgressBar fx:id="loadFileProgressBar"/>
</VBox>
<TreeView fx:id="treeFileBox" styleClass="box_class" visible="false"/>
</StackPane>
<StackPane fx:id="wordBox" styleClass="box_class" GridPane.columnIndex="1" minWidth="210">
<VBox fx:id="wordMsgBox" alignment="CENTER" styleClass="box_class">
<Label fx:id="wordMsgLabel"/>
<ProgressBar fx:id="loadWordProgressBar" visible="false"/>
</VBox>
</StackPane>
</SplitPane>
</VBox>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<StackPane fx:id="root" prefHeight="276.0" prefWidth="260.0" xmlns="http://javafx.com/javafx/17.0.2-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cn.octopusyan.dayzmodtranslator.controller.SetupProxyController">
<VBox alignment="CENTER_LEFT">
<Label alignment="CENTER" prefWidth="Infinity" text="网络代理设置"/>
<Label text="Host">
<VBox.margin>
<Insets top="20"/>
</VBox.margin>
</Label>
<TextField fx:id="hostField">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</TextField>
<Label text="port">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
</Label>
<TextField fx:id="portField">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</TextField>
<Label text="测试地址">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
</Label>
<TextField fx:id="testPath">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</TextField>
<VBox alignment="CENTER">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
<Button onAction="#test" style="-fx-background-color: transparent; -fx-text-fill: #000FFF"
text="网络代理测试"/>
</VBox>
<HBox alignment="CENTER_RIGHT">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
<Button text="确定" onAction="#save">
<padding>
<Insets bottom="5.0" left="15.0" right="15.0" top="5.0"/>
</padding>
</Button>
<Button text="取消" onAction="#close">
<HBox.margin>
<Insets left="5.0"/>
</HBox.margin>
<padding>
<Insets bottom="5.0" left="15.0" right="15.0" top="5.0"/>
</padding>
</Button>
</HBox>
<StackPane.margin>
<Insets left="20.0" right="20.0"/>
</StackPane.margin>
</VBox>
</StackPane>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<StackPane fx:id="root" prefHeight="155.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/17.0.2-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cn.octopusyan.dayzmodtranslator.controller.SetupTranslateController">
<VBox alignment="CENTER">
<VBox>
<Label text="翻译源">
<VBox.margin>
<Insets right="15.0"/>
</VBox.margin>
</Label>
<ComboBox fx:id="translateSourceCombo">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</ComboBox>
</VBox>
<VBox alignment="CENTER_LEFT">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
<Label text="QPS(每秒翻译数量)">
<VBox.margin>
<Insets right="15.0"/>
</VBox.margin>
</Label>
<TextField fx:id="qps">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</TextField>
</VBox>
<VBox alignment="CENTER_LEFT" fx:id="appidBox">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
<Label text="appid">
<VBox.margin>
<Insets right="15.0"/>
</VBox.margin>
</Label>
<TextField fx:id="appid">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</TextField>
</VBox>
<VBox alignment="CENTER_LEFT" fx:id="apikeyBox">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
<Label text="apikey">
<VBox.margin>
<Insets right="15.0"/>
</VBox.margin>
</Label>
<TextField fx:id="apikey">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
</TextField>
</VBox>
<HBox alignment="CENTER_RIGHT">
<VBox.margin>
<Insets top="15.0"/>
</VBox.margin>
<Button onAction="#save" text="确定">
<padding>
<Insets bottom="5.0" left="15.0" right="15.0" top="5.0"/>
</padding>
</Button>
<Button onAction="#onDestroy" text="取消">
<HBox.margin>
<Insets left="5.0"/>
</HBox.margin>
<padding>
<Insets bottom="5.0" left="15.0" right="15.0" top="5.0"/>
</padding>
</Button>
</HBox>
<StackPane.margin>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0"/>
</StackPane.margin>
</VBox>
</StackPane>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="logback.logdir" value="log"/>
<property name="CHARSET" value="utf-8"/>
<property name="logback.app" value="DayzModTranslator"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%highlight(%d{HH:mm:ss.SSS}) ${logback.app} %boldYellow([%thread]) %highlight(%-5level) %cyan(%logger{36}) - %mdc{client} [%X{trace_id}] %msg%n"/>
<!--输出到控制台 ConsoleAppender-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${CHARSET}</charset>
</encoder>
</appender>
<!--输出到文件 fileLog-->
<appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"/>
<File>${logback.logdir}/${logback.app}.info.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.info.log</FileNamePattern>
<!--只保留最近30天的日志-->
<maxHistory>30</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>${CHARSET}</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n</pattern>
</encoder>
<!--只打印错误日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="fileLog-debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"/>
<File>${logback.logdir}/${logback.app}.debug.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.debug.log</FileNamePattern>
<!--只保留最近30天的日志-->
<maxHistory>30</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n</pattern>
</encoder>
<!--只打印错误日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 错误日志 -->
<appender name="fileLog-err" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${logback.logdir}/${logback.app}.err.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.err.log</FileNamePattern>
<maxHistory>7</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n</pattern>
</encoder>
<!--只打印错误日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--指定最基础的日志输出级别-->
<root level="INFO">
<!--appender将会添加到这个logger-->
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
<appender-ref ref="fileLog-debug"/>
<appender-ref ref="fileLog-err"/>
</root>
</configuration>

Binary file not shown.

Binary file not shown.

Binary file not shown.