nexus
@@ -141,8 +186,17 @@
maven-compiler-plugin
3.13.0
- 17
- 17
+ 21
+ 21
+ --enable-preview
+ UTF-8
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
@@ -162,20 +216,25 @@
org.openjfx
javafx-maven-plugin
0.0.8
+
+ true
+ 2
+ true
+ true
+ launcher
+ app
+ app
+ cn.octopusyan.dmt/${exec.mainClass}
+
default-cli
-
- cn.octopusyan.dayzmodtranslator/cn.octopusyan.dayzmodtranslator.AppLuncher
-
- launcher
- app
- app
- true
- true
- true
+
+ --enable-preview
+
+
@@ -186,20 +245,53 @@
javapackager
1.7.7-SNAPSHOT
+ ${exec.mainClass}
true
- cn.octopusyan.dayzmodtranslator.AppLuncher
false
+ true
+
+ --enable-preview
+ -Xmx100m
+
- bundling-for-windows
+ windows
package
package
windows
+ ${project.name}-windows
true
+
+ gui
+ false
+
+
+ ${project.basedir}/src/main/resources/bin
+
+
+
+
+ windows-nojre
+ package
+
+ package
+
+
+ ${project.name}-windows-nojre
+ windows
+ true
+ false
+
+ gui
+ false
+
+
+ ${project.basedir}/src/main/resources/bin
+
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/AppLuncher.java b/src/main/java/cn/octopusyan/dayzmodtranslator/AppLuncher.java
deleted file mode 100644
index 158706f..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/AppLuncher.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package cn.octopusyan.dayzmodtranslator;
-
-/**
- * 启动类
- *
- * @author octopus_yan@foxmail.com
- */
-public class AppLuncher {
-
- public static void main(String[] args) {
-// try {
-// Runtime.getRuntime().exec("Taskkill /IM " + FrpManager.FRPC_CLIENT_FILE_NAME + " /f");
-// } catch (IOException e) {
-// e.printStackTrace();
-// }
-
- Application.launch(Application.class, args);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/Application.java b/src/main/java/cn/octopusyan/dayzmodtranslator/Application.java
deleted file mode 100644
index a61f8fb..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/Application.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package cn.octopusyan.dayzmodtranslator;
-
-import cn.octopusyan.dayzmodtranslator.config.AppConstant;
-import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
-import cn.octopusyan.dayzmodtranslator.manager.http.HttpConfig;
-import cn.octopusyan.dayzmodtranslator.controller.MainController;
-import cn.octopusyan.dayzmodtranslator.manager.CfgConvertUtil;
-import cn.octopusyan.dayzmodtranslator.manager.PBOUtil;
-import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
-import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
-import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
-import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
-import javafx.fxml.FXMLLoader;
-import javafx.scene.Scene;
-import javafx.scene.layout.VBox;
-import javafx.stage.Stage;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.ProxySelector;
-import java.util.Objects;
-
-public class Application extends javafx.application.Application {
- private static final Logger logger = LoggerFactory.getLogger(Application.class);
-
- @Override
- public void init() throws Exception {
- logger.info("application init ...");
- }
-
- @Override
- public void start(Stage stage) throws IOException {
-
- logger.info("application start ...");
-
- // bin转换 工具初始化
- CfgConvertUtil.init();
-
- // PBO 工具初始化
- PBOUtil.init(CfgConvertUtil.getInstance());
-
- // 客户端配置初始化
- CustomConfig.init();
-
- // 初始化弹窗工具
- AlertUtil.initOwner(stage);
-
- // http请求工具初始化
- HttpConfig httpConfig = new HttpConfig();
- if (CustomConfig.hasProxy()) {
- InetSocketAddress unresolved = InetSocketAddress.createUnresolved(CustomConfig.proxyHost(), CustomConfig.proxyPort());
- httpConfig.setProxySelector(ProxySelector.of(unresolved));
- }
- httpConfig.setConnectTimeout(10);
- HttpUtil.init(httpConfig);
-
- // TODO 全局异常处理
- Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
- Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
-
- // 启动主界面
- try {
- FXMLLoader fxmlLoader = FxmlUtil.load("main-view");
- VBox root = fxmlLoader.load();//底层面板
- Scene scene = new Scene(root);
- scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
- stage.setScene(scene);
- stage.setMinHeight(330);
- stage.setMinWidth(430);
- stage.setMaxWidth(Double.MAX_VALUE);
- stage.setMaxHeight(Double.MAX_VALUE);
- stage.setTitle(AppConstant.APP_TITLE + " v" + AppConstant.APP_VERSION);
- stage.show();
-
- MainController controller = fxmlLoader.getController();
- controller.setApplication(this);
- } catch (Throwable t) {
- showErrorDialog(Thread.currentThread(), t);
- }
-
- logger.info("application start over ...");
- }
-
- private void showErrorDialog(Thread t, Throwable e) {
- logger.error("", e);
- AlertUtil.exceptionAlert(new Exception(e)).show();
- }
-
- @Override
- public void stop() throws Exception {
- logger.info("application stop ...");
-
- // 清除翻译任务
- TranslateUtil.getInstance().clear();
- // 停止所有线程
- ThreadPoolManager.getInstance().shutdown();
- // 保存应用数据
- CustomConfig.store();
- // 清理缓存
- PBOUtil.clear();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/base/BaseController.java b/src/main/java/cn/octopusyan/dayzmodtranslator/base/BaseController.java
deleted file mode 100644
index 7095c64..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/base/BaseController.java
+++ /dev/null
@@ -1,217 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.base;
-
-import cn.octopusyan.dayzmodtranslator.config.AppConstant;
-import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
-import cn.octopusyan.dayzmodtranslator.util.Loading;
-import cn.octopusyan.dayzmodtranslator.util.TooltipUtil;
-import javafx.application.Application;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.fxml.FXMLLoader;
-import javafx.fxml.Initializable;
-import javafx.scene.Parent;
-import javafx.scene.Scene;
-import javafx.scene.control.Label;
-import javafx.scene.layout.Pane;
-import javafx.stage.Modality;
-import javafx.stage.Stage;
-import javafx.stage.Window;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.URL;
-import java.util.Objects;
-import java.util.ResourceBundle;
-
-/**
- * 通用视图控制器基类
- *
- * @author octopus_yan@foxmail.com
- */
-public abstract class BaseController 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
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() {
- @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();
-
- /**
- * 获取根布局
- * 搭配 FxmlUtil.load
使用
- *
- * @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();
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/config/AppConstant.java b/src/main/java/cn/octopusyan/dayzmodtranslator/config/AppConstant.java
deleted file mode 100644
index 70c0d11..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/config/AppConstant.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.config;
-
-import cn.octopusyan.dayzmodtranslator.util.PropertiesUtils;
-import org.apache.commons.io.FileUtils;
-
-import java.io.File;
-
-/**
- * 应用信息
- *
- * @author octopus_yan@foxmail.com
- */
-public class AppConstant {
- public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
- public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
- public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
- public static final String DATA_DIR_PATH = System.getProperty("user.home") + File.separator + "AppData" + File.separator + "Local" + File.separator + APP_NAME;
- public static final String TMP_DIR_PATH = FileUtils.getTempDirectoryPath() + APP_NAME;
- public static final String CUSTOM_CONFIG_PATH = DATA_DIR_PATH + File.separator + "config.properties";
- public static final String BAK_FILE_PATH = AppConstant.TMP_DIR_PATH + File.separator + "bak";
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/config/CustomConfig.java b/src/main/java/cn/octopusyan/dayzmodtranslator/config/CustomConfig.java
deleted file mode 100644
index a36ece7..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/config/CustomConfig.java
+++ /dev/null
@@ -1,174 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.config;
-
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.charset.StandardCharsets;
-import java.util.Properties;
-
-/**
- * 客户端设置
- *
- * @author octopus_yan@foxmail.com
- */
-public class CustomConfig {
- private static final Logger logger = LoggerFactory.getLogger(CustomConfig.class);
- private static final Properties properties = new Properties();
- public static final String PROXY_HOST_KEY = "proxy.host";
- public static final String PROXY_PORT_KEY = "proxy.port";
- public static final String TRANSLATE_SOURCE_KEY = "translate.source";
- public static final String TRANSLATE_SOURCE_APPID_KEY = "translate.{}.appid";
- public static final String TRANSLATE_SOURCE_APIKEY_KEY = "translate.{}.apikey";
- public static final String TRANSLATE_SOURCE_QPS_KEY = "translate.{}.qps";
-
- public static void init() {
- File customConfigFile = new File(AppConstant.CUSTOM_CONFIG_PATH);
- try {
- if (!customConfigFile.exists()) {
- // 初始配置
- properties.put(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName());
- // 保存配置文件
- store();
- } else {
- properties.load(new FileInputStream(customConfigFile));
- }
- } catch (IOException ignore) {
- logger.error("读取配置文件失败");
- }
- }
-
- /**
- * 是否配置代理
- */
- public static boolean hasProxy() {
- String host = proxyHost();
- Integer port = proxyPort();
-
- return StringUtils.isNoneBlank(host) && null != port;
- }
-
- /**
- * 代理地址
- */
- public static String proxyHost() {
- return properties.getProperty(PROXY_HOST_KEY);
- }
-
- /**
- * 代理地址
- */
- public static void proxyHost(String host) {
- properties.setProperty(PROXY_HOST_KEY, host);
- }
-
- /**
- * 代理端口
- */
- public static Integer proxyPort() {
- try {
- return Integer.parseInt(properties.getProperty(PROXY_PORT_KEY));
- } catch (Exception ignored) {
- }
- return null;
- }
-
- /**
- * 代理端口
- */
- public static void proxyPort(int port) {
- properties.setProperty(PROXY_PORT_KEY, String.valueOf(port));
- }
-
- /**
- * 翻译源
- */
- public static TranslateSource translateSource() {
- String name = properties.getProperty(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName());
- return TranslateSource.get(name);
- }
-
- /**
- * 翻译源
- */
- public static void translateSource(TranslateSource source) {
- properties.setProperty(TRANSLATE_SOURCE_KEY, source.getName());
- }
-
- /**
- * 是否配置接口认证
- *
- * @param source 翻译源
- */
- public static boolean hasTranslateApiKey(TranslateSource source) {
- return StringUtils.isNoneBlank(translateSourceAppid(source))
- && StringUtils.isNoneBlank(translateSourceApikey(source));
- }
-
- /**
- * 设置翻译源appid
- *
- * @param source 翻译源
- * @param appid appid
- */
- public static void translateSourceAppid(TranslateSource source, String appid) {
- properties.setProperty(getTranslateSourceAppidKey(source), appid);
- }
-
- /**
- * 获取翻译源appid
- *
- * @param source 翻译源
- * @return appid
- */
- public static String translateSourceAppid(TranslateSource source) {
- return properties.getProperty(getTranslateSourceAppidKey(source));
- }
-
- public static void translateSourceApikey(TranslateSource source, String apikey) {
- properties.setProperty(getTranslateSourceApikeyKey(source), apikey);
- }
-
- public static String translateSourceApikey(TranslateSource source) {
- return properties.getProperty(getTranslateSourceApikeyKey(source));
- }
-
- public static Integer translateSourceQps(TranslateSource source) {
- String qpsStr = properties.getProperty(getTranslateSourceQpsKey(source));
- return qpsStr == null ? source.getDefaultQps() : Integer.parseInt(qpsStr);
- }
-
- public static void translateSourceQps(TranslateSource source, int qps) {
- properties.setProperty(getTranslateSourceQpsKey(source), String.valueOf(qps));
- }
-
-
- /**
- * 保存配置
- */
- public static void store() {
- // 生成配置文件
- try {
- properties.store(new PrintStream(AppConstant.CUSTOM_CONFIG_PATH), String.valueOf(StandardCharsets.UTF_8));
- } catch (IOException e) {
- logger.error("保存客户端配置失败", e);
- }
- }
-
- private static String getTranslateSourceAppidKey(TranslateSource source) {
- return StringUtils.replace(TRANSLATE_SOURCE_APPID_KEY, "{}", source.getName());
- }
-
- private static String getTranslateSourceApikeyKey(TranslateSource source) {
- return StringUtils.replace(TRANSLATE_SOURCE_APIKEY_KEY, "{}", source.getName());
- }
-
- private static String getTranslateSourceQpsKey(TranslateSource source) {
- return StringUtils.replace(TRANSLATE_SOURCE_QPS_KEY, "{}", source.getName());
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/MainController.java b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/MainController.java
deleted file mode 100644
index 46bd3d1..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/MainController.java
+++ /dev/null
@@ -1,515 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.controller;
-
-import cn.octopusyan.dayzmodtranslator.base.BaseController;
-import cn.octopusyan.dayzmodtranslator.manager.PBOUtil;
-import cn.octopusyan.dayzmodtranslator.manager.file.FileTreeItem;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
-import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem;
-import cn.octopusyan.dayzmodtranslator.manager.word.WordItem;
-import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
-import cn.octopusyan.dayzmodtranslator.util.ClipUtil;
-import javafx.application.Platform;
-import javafx.beans.property.StringProperty;
-import javafx.collections.ObservableList;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.scene.control.*;
-import javafx.scene.control.cell.TextFieldTableCell;
-import javafx.scene.input.*;
-import javafx.scene.layout.StackPane;
-import javafx.scene.layout.VBox;
-import javafx.stage.FileChooser;
-import org.apache.commons.io.FileUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
-import java.util.regex.Pattern;
-
-/**
- * 主控制器
- *
- * @author octopus_yan@foxmail.com
- */
-public class MainController extends BaseController {
-
- 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 treeFileBox;
- public StackPane wordBox;
- public TableView 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 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 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 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 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文件
- * 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 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 TableColumn createColumn(String colName, Function colField) {
- TableColumn 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();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupProxyController.java b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupProxyController.java
deleted file mode 100644
index 8694915..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupProxyController.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.controller;
-
-import cn.octopusyan.dayzmodtranslator.base.BaseController;
-import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
-import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
-import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
-import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
-import javafx.scene.control.TextField;
-import javafx.scene.layout.StackPane;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.math.NumberUtils;
-
-import java.io.IOException;
-import java.net.URI;
-
-/**
- * 应用配置
- *
- * @author octopus_yan@foxmail.com
- */
-public class SetupProxyController extends BaseController {
- 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;
- }
-
- /**
- * 获取根布局
- * 搭配 FxmlUtil.load
使用
- *
- * @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();
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupTranslateController.java b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupTranslateController.java
deleted file mode 100644
index f9c879f..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupTranslateController.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.controller;
-
-import cn.octopusyan.dayzmodtranslator.base.BaseController;
-import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
-import javafx.collections.ObservableList;
-import javafx.scene.control.ComboBox;
-import javafx.scene.control.TextField;
-import javafx.scene.layout.StackPane;
-import javafx.scene.layout.VBox;
-import javafx.util.StringConverter;
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * 翻译设置控制器
- *
- * @author octopus_yan@foxmail.com
- */
-public class SetupTranslateController extends BaseController {
- public StackPane root;
- public ComboBox 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 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();
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/CfgConvertUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/CfgConvertUtil.java
deleted file mode 100644
index 37b1882..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/CfgConvertUtil.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager;
-
-import cn.octopusyan.dayzmodtranslator.config.AppConstant;
-import cn.octopusyan.dayzmodtranslator.util.FileUtil;
-import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil;
-import org.apache.commons.io.FileUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Objects;
-
-/**
- * Cfg文件转换工具类
- *
- * @author octopus_yan@foxmail.com
- */
-public class CfgConvertUtil {
- private static final Logger logger = LoggerFactory.getLogger(CfgConvertUtil.class);
- private static CfgConvertUtil util;
- private static final String CfgConvert_DIR_PATH = AppConstant.DATA_DIR_PATH + File.separator + "CfgConvert";
- private static final File CfgConvert_DIR = new File(CfgConvert_DIR_PATH);
- private static final String CfgConvert_FILE_PATH = CfgConvert_DIR_PATH + File.separator + "CfgConvert.exe";
- private static final File CfgConvert_FILE = new File(CfgConvert_FILE_PATH);
- private static final String COMMAND = CfgConvert_FILE_PATH + " %s -dst %s %s";
-
- private CfgConvertUtil() {
- }
-
- public static void init() {
- if (util == null) {
- util = new CfgConvertUtil();
- }
- // 检查pbo解析文件
- util.checkCfgConvert();
- }
-
- public static synchronized CfgConvertUtil getInstance() {
- if (util == null)
- throw new RuntimeException("are you ready ?");
- return util;
- }
-
- /**
- * 检查Cfg转换文件
- */
- private void checkCfgConvert() {
- if (!CfgConvert_FILE.exists()) initCfgConvert();
- }
-
- private void initCfgConvert() {
- try {
- FileUtils.forceMkdir(CfgConvert_DIR);
- String cfgConvertFileName = "CfgConvert.exe";
- FileUtil.copyFile(Objects.requireNonNull(CfgConvertUtil.class.getResourceAsStream("/static/CfgConvert/" + cfgConvertFileName)), new File(CfgConvert_DIR_PATH + File.separator + cfgConvertFileName));
- } catch (IOException e) {
- logger.error("", e);
- }
- }
-
- public void toTxt(File binFile, String outPath, OnToTxtListener onToTxtListener) {
- String fileName = binFile.getAbsolutePath();
- ProcessesUtil.exec(toTxtCommand(binFile, outPath), new ProcessesUtil.OnExecuteListener() {
- @Override
- public void onExecute(String msg) {
- logger.info(fileName + " : " + msg);
- }
-
- @Override
- public void onExecuteSuccess(int exitValue) {
- logger.info(fileName + " : to txt success");
- String outFilePath = outFilePath(binFile, outPath, ".cpp");
- if (onToTxtListener != null) {
- onToTxtListener.onToTxtSuccess(outFilePath);
- }
- }
-
- @Override
- public void onExecuteError(Exception e) {
- logger.error(fileName + " : to txt error", e);
- if (onToTxtListener != null) {
- onToTxtListener.onToTxtError(e);
- }
- }
-
- @Override
- public void onExecuteOver() {
- logger.info(fileName + " : to txt end...");
- }
- });
- }
-
- public void toBin(File cppFile) {
- ProcessesUtil.exec(toBinCommand(cppFile, cppFile.getParentFile().getAbsolutePath()), new ProcessesUtil.OnExecuteListener() {
- @Override
- public void onExecute(String msg) {
-
- }
-
- @Override
- public void onExecuteSuccess(int exitValue) {
-
- }
-
- @Override
- public void onExecuteError(Exception e) {
-
- }
-
- @Override
- public void onExecuteOver() {
-
- }
- });
- }
-
- private String toBinCommand(File cppFile, String outPath) {
- String outFilePath = outFilePath(cppFile, outPath, ".bin");
- return String.format(COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath());
- }
-
- private String toTxtCommand(File binFile, String outPath) {
- String outFilePath = outFilePath(binFile, outPath, ".cpp");
- return String.format(COMMAND, "-txt", outFilePath, binFile.getAbsolutePath());
- }
-
- private String outFilePath(File file, String outPath, String suffix) {
- return outPath + File.separator + FileUtil.mainName(file) + suffix;
- }
-
- public interface OnToTxtListener {
- void onToTxtSuccess(String txtFilePath);
-
- void onToTxtError(Exception e);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/PBOUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/PBOUtil.java
deleted file mode 100644
index c942531..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/PBOUtil.java
+++ /dev/null
@@ -1,628 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager;
-
-import cn.octopusyan.dayzmodtranslator.config.AppConstant;
-import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
-import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem;
-import cn.octopusyan.dayzmodtranslator.manager.word.WordItem;
-import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
-import cn.octopusyan.dayzmodtranslator.util.FileUtil;
-import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil;
-import javafx.application.Platform;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.LineIterator;
-import org.apache.commons.io.filefilter.NameFileFilter;
-import org.apache.commons.io.filefilter.TrueFileFilter;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-/**
- * PBO 工具类
- *
- * @author octopus_yan@foxmail.com
- * @see https://github.com/winseros/pboman3
- */
-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 worlds = new ArrayList<>(readCsvFile());
- if (onFindTransWordListener != null) {
- Platform.runLater(() -> onFindTransWordListener.onFoundWords(worlds, true));
- }
- } else {
- findConfigWord();
- }
- });
- }
-
- /**
- * 获取csv中 原文及 简中单词
- *
- * @return 待翻译语句列表
- */
- private List readCsvFile() {
- List 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 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 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 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 words) throws Exception {
- Map> wordMap = words.stream()
- .collect(Collectors.groupingBy(WordItem::getFile, Collectors.toList()));
-
- AtomicInteger progress = new AtomicInteger(0);
-
- // 0 执行成功 大于0 执行失败
- List> result = new ArrayList<>();
-
- for (Map.Entry> entry : wordMap.entrySet()) {
- Future 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 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 是否有 stringtable.csv
文件
- */
- private boolean hasStringTable() {
- List 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 worlds, boolean isOver);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileIcon.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileIcon.java
deleted file mode 100644
index 1ce5598..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileIcon.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.file;
-
-
-import javafx.scene.canvas.Canvas;
-import javafx.scene.image.PixelFormat;
-import javafx.scene.image.PixelWriter;
-import javafx.scene.image.WritablePixelFormat;
-
-import javax.swing.*;
-import javax.swing.filechooser.FileSystemView;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.io.File;
-import java.nio.IntBuffer;
-
-/**
- * 文件图标
- *
- * @author octopus_yan@foxmail.com
- */
-public class FileIcon {
- //设置图标
- public static Canvas getFileIconToNode(File file) {
- //获取系统文件的图标
- Image image = ((ImageIcon) FileSystemView.getFileSystemView().getSystemIcon(file)).getImage();
- //构建图片缓冲区,设定图片缓冲区的大小和背景,背景为透明
- BufferedImage bi = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.BITMASK);
- //把图片画到图片缓冲区
- bi.getGraphics().drawImage(image, 0, 0, null);
- //将图片缓冲区的数据转换成int型数组
- int[] data = ((DataBufferInt) bi.getData().getDataBuffer()).getData();
- //获得写像素的格式模版
- WritablePixelFormat 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);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileTreeItem.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileTreeItem.java
deleted file mode 100644
index 9068700..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileTreeItem.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.file;
-
-import javafx.collections.ObservableList;
-import javafx.scene.control.TreeItem;
-
-import javax.swing.filechooser.FileSystemView;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Function;
-
-/**
- * 文件目录
- *
- * @author octopus_yan@foxmail.com
- */
-public class FileTreeItem extends TreeItem {
- public static File ROOT_FILE = FileSystemView.getFileSystemView().getRoots()[0];
-
- //判断树节点是否被初始化,没有初始化为真
- private boolean notInitialized = true;
-
- private final File file;
- private final Function 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 supplier) {
- super(FileIcon.getFileName(file), FileIcon.getFileIconToNode(file));
- this.file = file;
- this.supplier = supplier;
- }
-
-
- //重写getchildren方法,让节点被展开时加载子目录
- @Override
- public ObservableList> getChildren() {
-
- ObservableList> children = super.getChildren();
- //没有加载子目录时,则加载子目录作为树节点的孩子
- if (this.notInitialized && this.isExpanded()) {
-
- this.notInitialized = false; //设置没有初始化为假
-
- /*
- *判断树节点的文件是否是目录,
- *如果是目录,着把目录里面的所有的文件添加入树节点的孩子中。
- */
- if (this.getFile().isDirectory()) {
- List 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;
- }
-
-
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpConfig.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpConfig.java
deleted file mode 100644
index 0936834..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpConfig.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.http;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-import java.net.Authenticator;
-import java.net.CookieHandler;
-import java.net.ProxySelector;
-import java.net.http.HttpClient;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.util.concurrent.Executor;
-
-/**
- * Http配置参数
- *
- * @author octopus_yan@foxmail.com
- */
-public class HttpConfig {
- private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
- /**
- * http版本
- */
- private HttpClient.Version version = HttpClient.Version.HTTP_2;
-
- /**
- * 转发策略
- */
- private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
-
- /**
- * 线程池
- */
- private Executor executor;
-
- /**
- * 认证
- */
- private Authenticator authenticator;
-
- /**
- * 代理
- */
- private ProxySelector proxySelector;
-
- /**
- * CookieHandler
- */
- private CookieHandler cookieHandler;
-
- /**
- * sslContext
- */
- private SSLContext sslContext;
-
- /**
- * sslParams
- */
- private SSLParameters sslParameters;
-
- /**
- * 连接超时时间毫秒
- */
- private int connectTimeout = 10000;
-
- /**
- * 默认读取数据超时时间
- */
- private int defaultReadTimeout = 1200000;
-
-
- public HttpConfig() {
- TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0]; // Not relevant.
- }
-
- @Override
- public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
- // TODO Auto-generated method stub
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
- // TODO Auto-generated method stub
- }
- }};
- sslParameters = new SSLParameters();
- sslParameters.setEndpointIdentificationAlgorithm("");
-
-
- try {
- sslContext = SSLContext.getInstance("TLSv1.2");
- System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
- sslContext.init(null, trustAllCertificates, new SecureRandom());
- } catch (NoSuchAlgorithmException | KeyManagementException e) {
- logger.error("", e);
- }
-
- }
-
-
- public HttpClient.Version getVersion() {
- return version;
- }
-
- public void setVersion(HttpClient.Version version) {
- this.version = version;
- }
-
- public int getConnectTimeout() {
- return connectTimeout;
- }
-
- public void setConnectTimeout(int connectTimeout) {
- this.connectTimeout = connectTimeout;
- }
-
-
- public HttpClient.Redirect getRedirect() {
- return redirect;
- }
-
- public void setRedirect(HttpClient.Redirect redirect) {
- this.redirect = redirect;
- }
-
- public Executor getExecutor() {
- return executor;
- }
-
- public void setExecutor(Executor executor) {
- this.executor = executor;
- }
-
- public Authenticator getAuthenticator() {
- return authenticator;
- }
-
- public void setAuthenticator(Authenticator authenticator) {
- this.authenticator = authenticator;
- }
-
- public ProxySelector getProxySelector() {
- return proxySelector;
- }
-
- public void setProxySelector(ProxySelector proxySelector) {
- this.proxySelector = proxySelector;
- }
-
- public CookieHandler getCookieHandler() {
- return cookieHandler;
- }
-
- public void setCookieHandler(CookieHandler cookieHandler) {
- this.cookieHandler = cookieHandler;
- }
-
- public int getDefaultReadTimeout() {
- return defaultReadTimeout;
- }
-
- public void setDefaultReadTimeout(int defaultReadTimeout) {
- this.defaultReadTimeout = defaultReadTimeout;
- }
-
- public SSLContext getSslContext() {
- return sslContext;
- }
-
- public SSLParameters getSslParameters() {
- return sslParameters;
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadPoolManager.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadPoolManager.java
deleted file mode 100644
index be46215..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadPoolManager.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.thread;
-
-
-import java.util.concurrent.*;
-
-/**
- * 线程池管理类
- */
-public final class ThreadPoolManager extends ThreadPoolExecutor {
-
- private static volatile ThreadPoolManager sInstance;
-
- private static ScheduledExecutorService scheduledExecutorService;
-
- private ThreadPoolManager() {
- super(32,
- 200,
- 10,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(200),
- new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX),
- new ThreadPoolExecutor.DiscardPolicy());
- }
-
- public static ThreadPoolManager getInstance() {
- if (sInstance == null) sInstance = new ThreadPoolManager();
- return sInstance;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/ApiKey.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/ApiKey.java
deleted file mode 100644
index 0e22252..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/ApiKey.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate;
-
-/**
- * API 密钥配置
- *
- * @author octopus_yan@foxmail.com
- */
-public class ApiKey {
- private String appid;
- private String apiKey;
-
- public ApiKey() {
- }
-
- public ApiKey(String appid, String apiKey) {
- this.appid = appid;
- this.apiKey = apiKey;
- }
-
- public String getAppid() {
- return appid;
- }
-
- public String getApiKey() {
- return apiKey;
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateSource.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateSource.java
deleted file mode 100644
index b55db46..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateSource.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate;
-
-/**
- * 翻译引擎类型
- *
- * @author octopus_yan@foxmail.com
- */
-public enum TranslateSource {
- FREE_GOOGLE("free_google", "谷歌(免费)", false, 50),
- BAIDU("baidu", "百度(需认证)", true),
-
- ;
- private final String name;
- private final String label;
- private final boolean needApiKey;
- private Integer defaultQps;
-
- TranslateSource(String name, String label, boolean needApiKey) {
- // 设置接口默认qps=10
- this(name, label, needApiKey, 10);
- }
-
- TranslateSource(String name, String label, boolean needApiKey, int defaultQps) {
- this.name = name;
- this.label = label;
- this.needApiKey = needApiKey;
- this.defaultQps = defaultQps;
- }
-
- public String getName() {
- return name;
- }
-
- public String getLabel() {
- return label;
- }
-
- public boolean needApiKey() {
- return needApiKey;
- }
-
- public Integer getDefaultQps() {
- return defaultQps;
- }
-
- public String getDefaultQpsStr() {
- return String.valueOf(defaultQps);
- }
-
- public static TranslateSource get(String type) {
- for (TranslateSource value : values()) {
- if (value.getName().equals(type))
- return value;
- }
- throw new RuntimeException("类型不存在");
- }
-
- public static TranslateSource getByLabel(String label) {
- for (TranslateSource value : values()) {
- if (value.getLabel().equals(label))
- return value;
- }
- throw new RuntimeException("类型不存在");
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateUtil.java
deleted file mode 100644
index 8395bc4..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateUtil.java
+++ /dev/null
@@ -1,227 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate;
-
-import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
-import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadFactory;
-import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
-import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactory;
-import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactoryImpl;
-import javafx.application.Platform;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.*;
-
-/**
- * 翻译工具
- *
- * @author octopus_yan@foxmail.com
- */
-public class TranslateUtil {
- private static final Logger logger = LoggerFactory.getLogger(TranslateUtil.class);
- private static TranslateUtil util;
- private static final DelayQueue 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 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();
- }
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactory.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactory.java
deleted file mode 100644
index 76ca022..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactory.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate.factory;
-
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
-
-/**
- * 翻译器接口
- *
- * @author octopus_yan@foxmail.com
- */
-public interface TranslateFactory {
- /**
- * 翻译处理
- *
- * @param source 翻译源
- * @param sourceString 原始文本
- * @return 翻译结果
- * @throws Exception 翻译出错
- */
- String translate(TranslateSource source, String sourceString) throws Exception;
-
- /**
- * 获取延迟翻译对象
- *
- * @param source 翻译源
- * @param index 序号
- * @param original 原始文本
- * @param listener 监听器
- * @return 延迟翻译对象
- */
- TranslateUtil.DelayWord getDelayWord(TranslateSource source, int index, String original, TranslateUtil.OnTranslateListener listener);
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactoryImpl.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactoryImpl.java
deleted file mode 100644
index 01dda12..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactoryImpl.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate.factory;
-
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
-import cn.octopusyan.dayzmodtranslator.manager.translate.processor.AbstractTranslateProcessor;
-import cn.octopusyan.dayzmodtranslator.manager.translate.processor.BaiduTranslateProcessor;
-import cn.octopusyan.dayzmodtranslator.manager.translate.processor.FreeGoogleTranslateProcessor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.*;
-
-/**
- * 翻译处理器
- *
- * @author octopus_yan@foxmail.com
- */
-public class TranslateFactoryImpl implements TranslateFactory {
- private static final Logger logger = LoggerFactory.getLogger(TranslateFactoryImpl.class);
- private static TranslateFactoryImpl impl;
- private final Map processorMap = new HashMap<>();
- private final List 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;
- }
-
- /**
- * 翻译(英->中)
- * TODO 切换语种
- *
- * @param source 翻译源
- * @param sourceString 原始文本
- * @return 翻译结果
- * @throws Exception 翻译出错
- */
- @Override
- public String translate(TranslateSource source, String sourceString) throws Exception {
- return getProcessor(source).translate(sourceString);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/AbstractTranslateProcessor.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/AbstractTranslateProcessor.java
deleted file mode 100644
index 517b03e..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/AbstractTranslateProcessor.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
-
-import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
-import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
-import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * 翻译处理器抽象类
- *
- * @author octopus_yan@foxmail.com
- */
-public abstract class AbstractTranslateProcessor implements TranslateProcessor {
- protected final Logger logger = LoggerFactory.getLogger(this.getClass());
- protected static final HttpUtil httpUtil = HttpUtil.getInstance();
- protected final TranslateSource translateSource;
- protected ApiKey apiKey;
-
- public AbstractTranslateProcessor(TranslateSource translateSource) {
- this.translateSource = translateSource;
- }
-
- public String getSource() {
- return translateSource.getName();
- }
-
- public TranslateSource source() {
- return translateSource;
- }
-
- @Override
- public boolean needApiKey() {
- return source().needApiKey();
- }
-
- @Override
- public boolean configuredKey() {
- return CustomConfig.hasTranslateApiKey(source());
- }
-
- @Override
- public int qps() {
- return CustomConfig.translateSourceQps(source());
- }
-
- /**
- * 获取Api配置信息
- */
- protected ApiKey getApiKey() {
- if (!configuredKey()) {
- String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
- logger.error(message);
- throw new RuntimeException(message);
- }
-
- String appid = CustomConfig.translateSourceAppid(source());
- String apikey = CustomConfig.translateSourceApikey(source());
- return new ApiKey(appid, apikey);
- }
-
- @Override
- public String translate(String source) throws Exception {
-
- if (needApiKey() && !configuredKey()) {
- String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
- logger.error(message);
- throw new RuntimeException(message);
- }
-
- return customTranslate(source);
- }
-
- /**
- * 翻译处理
- *
- * @param source 原始文本
- * @return 翻译结果
- */
- public abstract String customTranslate(String source) throws Exception;
-
- /**
- * 设置延迟对象
- *
- * @param word 带翻译单词
- */
- public void setDelayTime(TranslateUtil.DelayWord word) {
- // 设置翻译源
- word.setSource(source());
-
- // 设置翻译延迟
- int time = word.getIndex() / qps();
- word.setTime(time, TimeUnit.SECONDS);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordCsvItem.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordCsvItem.java
deleted file mode 100644
index 41c4eae..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordCsvItem.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.word;
-
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-
-import java.io.File;
-
-/**
- * csv单词对象
- *
- * @author octopus_yan@foxmail.com
- */
-public class WordCsvItem extends WordItem {
- /**
- * 简体中文
- */
- private StringProperty chineseSimp;
-
- /**
- * 文件中坐标(简体中文)
- */
- private int[] positionSimp;
-
- public WordCsvItem() {
- }
-
- public WordCsvItem(File stringTable, int lines, String original, String chineses, int[] position, String chineseSimp, int[] positionSimp) {
- super(stringTable, lines, original, chineses, position);
- this.chineseSimp = new SimpleStringProperty(chineseSimp);
- this.positionSimp = positionSimp;
- }
-
- public StringProperty chineseSimpProperty() {
- return chineseSimp;
- }
-
- public String getChineseSimp() {
- return chineseSimp.get();
- }
-
- public void setChineseSimp(String chineseSimp) {
- this.chineseSimp.setValue(chineseSimp);
- }
-
- public int[] getPositionSimp() {
- return positionSimp;
- }
-
- public void setPositionSimp(int[] positionSimp) {
- this.positionSimp = positionSimp;
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordItem.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordItem.java
deleted file mode 100644
index aae0d84..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordItem.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.manager.word;
-
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-
-import java.io.File;
-
-/**
- * 待翻译单词子项
- *
- * @author octopus_yan@foxmail.com
- */
-public class WordItem {
- /**
- * 所在文件
- *
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;
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/AlertUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/AlertUtil.java
deleted file mode 100644
index 718cd03..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/AlertUtil.java
+++ /dev/null
@@ -1,228 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.util;
-
-import javafx.scene.control.*;
-import javafx.scene.image.Image;
-import javafx.scene.layout.GridPane;
-import javafx.scene.layout.Priority;
-import javafx.stage.Stage;
-import javafx.stage.Window;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * 弹窗工具
- *
- * @author octopus_yan@foxmail.com
- */
-public class AlertUtil {
- private static Window mOwner;
- private static Builder builder;
-
- public static void initOwner(Stage stage) {
- AlertUtil.mOwner = stage;
- }
-
- public static class Builder {
- T alert;
-
- public Builder(T alert) {
- this.alert = alert;
- if (mOwner != null) this.alert.initOwner(mOwner);
- }
-
- public Builder title(String title) {
- alert.setTitle(title);
- return this;
- }
-
- public Builder header(String header) {
- alert.setHeaderText(header);
- return this;
- }
-
- public Builder content(String content) {
- alert.setContentText(content);
- return this;
- }
-
- public Builder icon(String path) {
- icon(new Image(Objects.requireNonNull(this.getClass().getResource(path)).toString()));
- return this;
- }
-
- public Builder 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 result = alert.showAndWait();
-
- listener.onClicked(result.get().getText());
- }
-
- /**
- * AlertUtil.confirm
- */
- public void show(OnChoseListener listener) {
- Optional result = alert.showAndWait();
- if (result.get() == ButtonType.OK) {
- listener.confirm();
- } else {
- listener.cancelOrClose(result.get());
- }
- }
-
- /**
- * AlertUtil.input
- * 如果用户点击了取消按钮,将会返回null
- */
- public String getInput() {
- Optional result = alert.showAndWait();
- if (result.isPresent()) {
- return result.get();
- }
- return null;
- }
-
- /**
- * AlertUtil.choices
- */
- public R getChoice(R... choices) {
- Optional result = alert.showAndWait();
- return (R) result.get();
- }
-
- private Stage getStage() {
- return (Stage) alert.getDialogPane().getScene().getWindow();
- }
- }
-
- public static Builder info(String content) {
- return new Builder(new Alert(Alert.AlertType.INFORMATION)).content(content).header(null);
- }
-
- public static Builder info() {
- return new Builder(new Alert(Alert.AlertType.INFORMATION));
- }
-
- public static Builder error(String message) {
- return new Builder(new Alert(Alert.AlertType.ERROR)).header(null).content(message);
- }
-
- public static Builder warning() {
- return new Builder(new Alert(Alert.AlertType.WARNING));
- }
-
- public static Builder exception(Exception ex) {
- return new Builder(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 confirm() {
- Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
- alert.setTitle("确认对话框");
- return new Builder(alert);
- }
-
- /**
- * 自定义确认对话框
- * "Cancel"
OR "取消"
为取消按钮
- */
- public static Builder confirm(String... buttons) {
- Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
-
- List 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);
- }
-
- public static Builder input(String content) {
- TextInputDialog dialog = new TextInputDialog();
- dialog.setContentText(content);
- return new Builder(dialog);
- }
-
- @SafeVarargs
- public static Builder> choices(String hintText, T... choices) {
- ChoiceDialog dialog = new ChoiceDialog(choices[0], choices);
- dialog.setContentText(hintText);
- return new Builder>(dialog);
- }
-
-
- public interface OnChoseListener {
- void confirm();
-
- void cancelOrClose(ButtonType buttonType);
- }
-
- public interface OnClickListener {
- void onClicked(String result);
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/Loading.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/Loading.java
deleted file mode 100644
index a444ca8..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/Loading.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.util;
-
-import javafx.application.Platform;
-import javafx.scene.Scene;
-import javafx.scene.control.Label;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import javafx.scene.layout.Background;
-import javafx.scene.layout.BackgroundFill;
-import javafx.scene.layout.StackPane;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.stage.Modality;
-import javafx.stage.Stage;
-import javafx.stage.StageStyle;
-
-/**
- * 加载等待弹窗
- *
- * @author octopus_yan@foxmail.com
- */
-public class Loading {
- protected Stage stage;
- protected StackPane root;
- protected Label messageLb;
- protected ImageView loadingView = new ImageView(new Image("https://blog-static.cnblogs.com/files/miaoqx/loading.gif"));
-
- public Loading(Stage owner) {
-
- messageLb = new Label("请耐心等待...");
- messageLb.setFont(Font.font(20));
-
- root = new StackPane();
- root.setMouseTransparent(true);
- root.setPrefSize(owner.getWidth(), owner.getHeight());
- root.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.3), null, null)));
- root.getChildren().addAll(loadingView, messageLb);
-
- Scene scene = new Scene(root);
- scene.setFill(Color.TRANSPARENT);
-
- stage = new Stage();
- stage.setX(owner.getX());
- stage.setY(owner.getY());
- stage.setScene(scene);
- stage.setResizable(false);
- stage.initOwner(owner);
- stage.initStyle(StageStyle.TRANSPARENT);
- stage.initModality(Modality.APPLICATION_MODAL);
- stage.getIcons().addAll(owner.getIcons());
- stage.setX(owner.getX());
- stage.setY(owner.getY());
- stage.setHeight(owner.getHeight());
- stage.setWidth(owner.getWidth());
- }
-
- // 更改信息
- public Loading showMessage(String message) {
- Platform.runLater(() -> messageLb.setText(message));
- return this;
- }
-
- // 更改信息
- public Loading image(Image image) {
- Platform.runLater(() -> loadingView.imageProperty().set(image));
- return this;
- }
-
- // 显示
- public void show() {
- Platform.runLater(() -> stage.show());
- }
-
- // 关闭
- public void closeStage() {
- Platform.runLater(() -> stage.close());
- }
-
- // 是否正在展示
- public boolean showing() {
- return stage.isShowing();
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/ProcessesUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/ProcessesUtil.java
deleted file mode 100644
index 5b67cab..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/ProcessesUtil.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.util;
-
-import org.apache.commons.exec.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-/**
- * 命令工具类
- *
- * @author octopus_yan@foxmail.com
- */
-public class ProcessesUtil {
- private static final Logger logger = LoggerFactory.getLogger(ProcessesUtil.class);
- private static final String NEWLINE = System.lineSeparator();
- private static final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
-
- public static boolean exec(String command) {
- try {
- exec(command, new OnExecuteListener() {
- @Override
- public void onExecute(String msg) {
-
- }
-
- @Override
- public void onExecuteSuccess(int exitValue) {
-
- }
-
- @Override
- public void onExecuteError(Exception e) {
-
- }
-
- @Override
- public void onExecuteOver() {
-
- }
- });
- handler.waitFor();
- } catch (Exception e) {
- logger.error("", e);
- }
- return 0 == handler.getExitValue();
- }
-
- public static void exec(String command, OnExecuteListener listener) {
- LogOutputStream logout = new LogOutputStream() {
- @Override
- protected void processLine(String line, int logLevel) {
- if (listener != null) listener.onExecute(line + NEWLINE);
- }
- };
-
- CommandLine commandLine = CommandLine.parse(command);
- DefaultExecutor executor = DefaultExecutor.builder().get();
- executor.setStreamHandler(new PumpStreamHandler(logout, logout));
- DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
- @Override
- public void onProcessComplete(int exitValue) {
- if (listener != null) {
- listener.onExecuteSuccess(exitValue);
- }
- }
-
- @Override
- public void onProcessFailed(ExecuteException e) {
- if (listener != null) {
- listener.onExecuteError(e);
- }
- }
- };
- try {
- executor.execute(commandLine, handler);
- } catch (IOException e) {
- if (listener != null) listener.onExecuteError(e);
- }
- }
-
- public interface OnExecuteListener {
- void onExecute(String msg);
-
- void onExecuteSuccess(int exitValue);
-
- void onExecuteError(Exception e);
- void onExecuteOver();
- }
-
- /**
- * Prevent construction.
- */
- private ProcessesUtil() {
- }
-}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/TooltipUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/TooltipUtil.java
deleted file mode 100644
index 6c1b629..0000000
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/TooltipUtil.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package cn.octopusyan.dayzmodtranslator.util;
-
-import javafx.beans.value.ChangeListener;
-import javafx.scene.control.Tooltip;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.Pane;
-import javafx.stage.Window;
-
-/**
- * 提示工具
- *
- * @author octopus_yan@foxmail.com
- */
-public class TooltipUtil {
- private static TooltipUtil util;
- private final Tooltip tooltip = new Tooltip();
- private Window owner;
- private ChangeListener xListener;
- private ChangeListener 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();
- }
-}
diff --git a/src/main/java/cn/octopusyan/dmt/AppLauncher.java b/src/main/java/cn/octopusyan/dmt/AppLauncher.java
new file mode 100644
index 0000000..0f94358
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/AppLauncher.java
@@ -0,0 +1,13 @@
+package cn.octopusyan.dmt;
+
+/**
+ * 启动类
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public class AppLauncher {
+
+ public static void main(String[] args) {
+ Application.launch(Application.class, args);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/Application.java b/src/main/java/cn/octopusyan/dmt/Application.java
new file mode 100644
index 0000000..f840ea0
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/Application.java
@@ -0,0 +1,114 @@
+package cn.octopusyan.dmt;
+
+import cn.octopusyan.dmt.common.config.Constants;
+import cn.octopusyan.dmt.common.config.Context;
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.common.manager.http.CookieManager;
+import cn.octopusyan.dmt.common.manager.http.HttpConfig;
+import cn.octopusyan.dmt.common.manager.http.HttpUtil;
+import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
+import cn.octopusyan.dmt.common.util.ProcessesUtil;
+import cn.octopusyan.dmt.utils.PBOUtil;
+import cn.octopusyan.dmt.view.alert.AlertUtil;
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import lombok.Getter;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.net.http.HttpClient;
+import java.util.Objects;
+
+public class Application extends javafx.application.Application {
+ private static final Logger logger = LoggerFactory.getLogger(Application.class);
+ @Getter
+ private static Stage primaryStage;
+
+ @Override
+ public void init() {
+ logger.info("application init ...");
+
+ // 初始化客户端配置
+ ConfigManager.load();
+
+ // 初始化 PBO工具
+ PBOUtil.init();
+
+ // http请求工具初始化
+ HttpConfig httpConfig = new HttpConfig();
+ httpConfig.setCookieHandler(CookieManager.get());
+ httpConfig.setExecutor(ThreadPoolManager.getInstance("http-pool"));
+ // 加载代理设置
+ switch (ConfigManager.proxySetup()) {
+ case NO_PROXY -> httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
+ case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
+ case MANUAL -> {
+ if (ConfigManager.hasProxy()) {
+ InetSocketAddress unresolved = InetSocketAddress.createUnresolved(
+ Objects.requireNonNull(ConfigManager.proxyHost()),
+ ConfigManager.getProxyPort()
+ );
+ httpConfig.setProxySelector(ProxySelector.of(unresolved));
+ }
+ }
+ }
+ httpConfig.setConnectTimeout(3000);
+ HttpUtil.init(httpConfig);
+ }
+
+ @Override
+ public void start(Stage primaryStage) throws IOException {
+
+ logger.info("application start ...");
+
+ Application.primaryStage = primaryStage;
+
+ Context.setApplication(this);
+
+ // 初始化弹窗工具
+ AlertUtil.initOwner(primaryStage);
+
+ // 全局异常处理
+ Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
+ Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
+
+ // 主题样式
+ Application.setUserAgentStylesheet(ConfigManager.theme().getUserAgentStylesheet());
+
+ // 启动主界面
+ primaryStage.setTitle(String.format("%s %s", Constants.APP_TITLE, Constants.APP_VERSION));
+ Scene scene = Context.initScene();
+ primaryStage.setScene(scene);
+ primaryStage.show();
+
+
+ }
+
+ private void showErrorDialog(Thread t, Throwable e) {
+ logger.error("未知异常", e);
+ Platform.runLater(() -> AlertUtil.exception(new Exception(e)).show());
+ }
+
+ @Override
+ public void stop() {
+ logger.info("application stop ...");
+ // 关闭所有命令
+ ProcessesUtil.destroyAll();
+ // 保存应用数据
+ ConfigManager.save();
+ // 停止所有线程
+ ThreadPoolManager.shutdownAll();
+ // 删除缓存
+ FileUtils.deleteQuietly(new File(Constants.TMP_DIR_PATH));
+ FileUtils.deleteQuietly(new File(Constants.BAK_DIR_PATH));
+ // 关闭主界面
+ Platform.exit();
+ System.exit(0);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java b/src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java
new file mode 100644
index 0000000..2211134
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java
@@ -0,0 +1,57 @@
+package cn.octopusyan.dmt.common.base;
+
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import javafx.application.Platform;
+import javafx.scene.Node;
+import javafx.scene.control.Dialog;
+import javafx.stage.Window;
+import lombok.Getter;
+
+/**
+ * @author octopus_yan
+ */
+@Getter
+public abstract class BaseBuilder, D extends Dialog>> {
+ protected D dialog;
+
+ public BaseBuilder(D dialog, Window mOwner) {
+ this.dialog = dialog;
+ if (mOwner != null)
+ this.dialog.initOwner(mOwner);
+ }
+
+ public T title(String title) {
+ dialog.setTitle(title);
+ return (T) this;
+ }
+
+ public T header(String header) {
+ dialog.setHeaderText(header);
+ return (T) this;
+ }
+
+ public T content(String content) {
+ dialog.setContentText(content);
+ return (T) this;
+ }
+
+ public void show() {
+
+ Node dialogPane = dialog.getDialogPane().getContent();
+ if (dialogPane != null && ConfigManager.theme().isDarkMode()) {
+ dialogPane.setStyle(STR."""
+ \{dialogPane.getStyle()}
+ -fx-border-color: rgb(209, 209, 214, 0.5);
+ -fx-border-width: 1;
+ -fx-border-radius: 10;
+ """);
+ }
+
+ Platform.runLater(() -> dialog.showAndWait());
+ }
+
+ public void close() {
+ if (dialog.isShowing())
+ dialog.close();
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/base/BaseController.java b/src/main/java/cn/octopusyan/dmt/common/base/BaseController.java
new file mode 100644
index 0000000..22e133b
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/base/BaseController.java
@@ -0,0 +1,146 @@
+package cn.octopusyan.dmt.common.base;
+
+import cn.octopusyan.dmt.Application;
+import cn.octopusyan.dmt.common.config.Context;
+import cn.octopusyan.dmt.common.util.FxmlUtil;
+import cn.octopusyan.dmt.common.util.ViewUtil;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import lombok.Getter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ResourceBundle;
+
+/**
+ * 通用视图控制器基类
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public abstract class BaseController implements Initializable {
+ protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+ @Getter
+ protected final VM viewModel;
+
+ public BaseController() {
+ //初始化时保存当前Controller实例
+ Context.getControllers().put(this.getClass().getSimpleName(), this);
+
+ // view model
+ VM vm = null;
+ Type superclass = getClass().getGenericSuperclass();
+ if (superclass instanceof ParameterizedType type) {
+ Class clazz = (Class) type.getActualTypeArguments()[0];
+ try {
+ vm = clazz.getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ logger.error("", e);
+ }
+ }
+ viewModel = vm;
+ viewModel.setController(this);
+ }
+
+ @FXML
+ @Override
+ public void initialize(URL url, ResourceBundle resourceBundle) {
+ // 全局窗口拖拽
+ if (dragWindow() && getRootPanel() != null) {
+ // 窗口拖拽
+ ViewUtil.bindDragged(getRootPanel());
+ }
+
+ // 初始化数据
+ initData();
+
+ // 初始化视图样式
+ initViewStyle();
+
+ // 初始化视图事件
+ initViewAction();
+ }
+
+ /**
+ * 窗口拖拽设置
+ *
+ * @return 是否启用
+ */
+ public boolean dragWindow() {
+ return false;
+ }
+
+ /**
+ * 获取根布局
+ *
+ * @return 根布局对象
+ */
+ public abstract Pane getRootPanel();
+
+ /**
+ * 获取根布局
+ * 搭配 {@link FxmlUtil#load(String)} 使用
+ *
+ * @return 根布局对象
+ */
+ protected String getRootFxml() {
+ System.out.println(getClass().getSimpleName());
+ return "";
+ }
+
+ protected Stage getWindow() {
+ try {
+ return (Stage) getRootPanel().getScene().getWindow();
+ } catch (Throwable _) {
+ return Application.getPrimaryStage();
+ }
+ }
+
+ /**
+ * 初始化数据
+ */
+ public abstract void initData();
+
+ /**
+ * 视图样式
+ */
+ public void initViewStyle() {
+ }
+
+ /**
+ * 视图事件
+ */
+ public abstract void initViewAction();
+
+ private static List getAllField(Class> class1) {
+ List list = new ArrayList<>();
+ while (class1 != Object.class) {
+ list.addAll(Arrays.stream(class1.getDeclaredFields()).toList());
+ //获取父类
+ class1 = class1.getSuperclass();
+ }
+ return list;
+ }
+
+ public void exit() {
+ Platform.exit();
+ }
+
+ /**
+ * 关闭窗口
+ */
+ public void onDestroy() {
+ Stage stage = getWindow();
+ stage.hide();
+ stage.close();
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/base/BaseViewModel.java b/src/main/java/cn/octopusyan/dmt/common/base/BaseViewModel.java
new file mode 100644
index 0000000..799509a
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/base/BaseViewModel.java
@@ -0,0 +1,15 @@
+package cn.octopusyan.dmt.common.base;
+
+import lombok.Setter;
+
+/**
+ * View Model
+ *
+ * @author octopus_yan
+ */
+@Setter
+public abstract class BaseViewModel, T extends BaseController> {
+
+ protected T controller;
+
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/config/Constants.java b/src/main/java/cn/octopusyan/dmt/common/config/Constants.java
new file mode 100644
index 0000000..d003052
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/config/Constants.java
@@ -0,0 +1,27 @@
+package cn.octopusyan.dmt.common.config;
+
+
+import cn.octopusyan.dmt.common.util.PropertiesUtils;
+
+import java.io.File;
+import java.nio.file.Paths;
+
+/**
+ * 应用信息
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public class Constants {
+ public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
+ public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
+ public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
+
+ public static final String DATA_DIR_PATH = Paths.get("").toFile().getAbsolutePath();
+ public static final String BIN_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bin";
+ public static final String TMP_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}tmp";
+ public static final String BAK_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bak";
+
+ public static final String CONFIG_FILE_PATH = STR."\{DATA_DIR_PATH}\{File.separator}config.yaml";
+ public static final String PBOC_FILE = STR."\{BIN_DIR_PATH}\{File.separator}pboc.exe";
+ public static final String CFG_CONVERT_FILE = STR."\{BIN_DIR_PATH}\{File.separator}CfgConvert.exe";
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/config/Context.java b/src/main/java/cn/octopusyan/dmt/common/config/Context.java
new file mode 100644
index 0000000..1c9a4f4
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/config/Context.java
@@ -0,0 +1,117 @@
+package cn.octopusyan.dmt.common.config;
+
+import cn.octopusyan.dmt.Application;
+import cn.octopusyan.dmt.common.base.BaseController;
+import cn.octopusyan.dmt.common.util.FxmlUtil;
+import cn.octopusyan.dmt.common.util.ProcessesUtil;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.util.Callback;
+import lombok.Getter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * 上下文
+ *
+ * @author octopus_yan
+ */
+public class Context {
+ @Getter
+ private static Application application;
+ private static final Logger log = LoggerFactory.getLogger(Context.class);
+ public static final ObjectProperty sceneProperty = new SimpleObjectProperty<>();
+
+ /**
+ * 控制器集合
+ */
+ @Getter
+ private static final Map> controllers = new HashMap<>();
+
+
+ private Context() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ // 获取控制工厂
+ public static Callback, Object> getControlFactory() {
+ return type -> {
+ try {
+ return type.getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ log.error("", e);
+ return null;
+ }
+ };
+ }
+
+ public static void setApplication(Application application) {
+ Context.application = application;
+ }
+
+ /**
+ * 有此类所在路径决定相对路径
+ *
+ * @param path 资源文件相对路径
+ * @return 资源文件路径
+ */
+ // 加载资源文件
+ public static URL load(String path) {
+ return Context.class.getResource(path);
+ }
+
+ /**
+ * 初始化场景
+ *
+ * @return Scene
+ */
+ public static Scene initScene() {
+ try {
+ FXMLLoader loader = FxmlUtil.load("main-view");
+ //底层面板
+ Pane root = loader.load();
+ Optional.ofNullable(sceneProperty.get()).ifPresentOrElse(
+ s -> s.setRoot(root),
+ () -> {
+ Scene scene = new Scene(root, root.getPrefWidth() + 20, root.getPrefHeight() + 20, Color.TRANSPARENT);
+ URL resource = Objects.requireNonNull(Context.class.getResource("/css/main-view.css"));
+ scene.getStylesheets().addAll(resource.toExternalForm());
+ scene.setFill(Color.TRANSPARENT);
+ sceneProperty.set(scene);
+ }
+ );
+ } catch (Throwable e) {
+ log.error("loadScene error", e);
+ }
+ return sceneProperty.get();
+ }
+
+ public static void openUrl(String url) {
+ getApplication().getHostServices().showDocument(url);
+ }
+
+ public static void openFolder(File file) {
+ openFile(file);
+ }
+
+ public static void openFile(File file) {
+ if (!file.exists()) return;
+
+ if (file.isDirectory()) {
+ ProcessesUtil.init(file.getAbsolutePath()).exec("explorer.exe .");
+ } else {
+ ProcessesUtil.init(file.getParentFile().getAbsolutePath()).exec(STR."explorer.exe /select,\{file.getName()}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/octopusyan/dmt/common/config/LabelConstants.java b/src/main/java/cn/octopusyan/dmt/common/config/LabelConstants.java
new file mode 100644
index 0000000..0028590
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/config/LabelConstants.java
@@ -0,0 +1,11 @@
+package cn.octopusyan.dmt.common.config;
+
+/**
+ * 名称常量
+ *
+ * @author octopus_yan
+ */
+public class LabelConstants {
+ public static final String CONFIRM = "确认";
+ public static final String CANCEL = "取消";
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java b/src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java
new file mode 100644
index 0000000..a192847
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java
@@ -0,0 +1,29 @@
+package cn.octopusyan.dmt.common.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 代理类型
+ *
+ * @author octopus_yan
+ */
+@Getter
+@RequiredArgsConstructor
+public enum ProxySetup {
+ /**
+ * 不使用代理
+ */
+ NO_PROXY("no_proxy", "不使用代理"),
+ /**
+ * 系统代理
+ */
+ SYSTEM("system", "系统代理"),
+ /**
+ * 自定义代理
+ */
+ MANUAL("manual", "自定义代理");
+
+ private final String code;
+ private final String name;
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/manager/ConfigManager.java b/src/main/java/cn/octopusyan/dmt/common/manager/ConfigManager.java
new file mode 100644
index 0000000..4204cb2
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/ConfigManager.java
@@ -0,0 +1,253 @@
+package cn.octopusyan.dmt.common.manager;
+
+import atlantafx.base.theme.*;
+import cn.octopusyan.dmt.Application;
+import cn.octopusyan.dmt.common.config.Constants;
+import cn.octopusyan.dmt.common.enums.ProxySetup;
+import cn.octopusyan.dmt.common.manager.http.HttpUtil;
+import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
+import cn.octopusyan.dmt.model.ConfigModel;
+import cn.octopusyan.dmt.model.ProxyInfo;
+import cn.octopusyan.dmt.model.Translate;
+import cn.octopusyan.dmt.model.UpgradeConfig;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import javafx.application.Platform;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 客户端设置
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public class ConfigManager {
+ private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
+ public static ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
+
+ public static final UpgradeConfig upgradeConfig = new UpgradeConfig();
+ public static final String DEFAULT_THEME = new PrimerLight().getName();
+ public static List THEME_LIST = List.of(
+ new PrimerLight(), new PrimerDark(),
+ new NordLight(), new NordDark(),
+ new CupertinoLight(), new CupertinoDark(),
+ new Dracula()
+ );
+ public static Map THEME_MAP = THEME_LIST.stream()
+ .collect(Collectors.toMap(Theme::getName, Function.identity()));
+
+ private static ConfigModel configModel;
+
+ static {
+ objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+ }
+
+ public static void load() {
+ configModel = loadConfig(Constants.CONFIG_FILE_PATH, ConfigModel.class);
+ if (configModel == null)
+ configModel = new ConfigModel();
+ }
+
+ public static T loadConfig(String path, Class clazz) {
+ File src = new File(path);
+ try {
+ if (!src.exists()) {
+ checkFile(src, clazz);
+ }
+ return objectMapper.readValue(src, clazz);
+ } catch (Exception e) {
+ logger.error(String.format("load %s error", clazz.getSimpleName()), e);
+ }
+ return null;
+ }
+
+ private static void checkFile(File src, Class clazz) throws Exception {
+ if (!src.exists()) {
+ File parentDir = FileUtils.createParentDirectories(src);
+ if (!parentDir.exists())
+ logger.error("{} 创建失败", src.getAbsolutePath());
+ }
+ objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance());
+ }
+
+ public static void save() {
+ try {
+ objectMapper.writeValue(new File(Constants.CONFIG_FILE_PATH), configModel);
+ } catch (IOException e) {
+ logger.error("save config error", e);
+ }
+ }
+
+// --------------------------------{ 主题 }------------------------------------------
+
+ public static String themeName() {
+ return configModel.getTheme();
+ }
+
+ public static Theme theme() {
+ return THEME_MAP.get(themeName());
+ }
+
+ public static void theme(Theme theme) {
+ Application.setUserAgentStylesheet(theme.getUserAgentStylesheet());
+ configModel.setTheme(theme.getName());
+ }
+
+// --------------------------------{ 翻译接口配置 }------------------------------------------
+
+ public static TranslateApi translateApi() {
+ return TranslateApi.get(configModel.getTranslate().getUse());
+ }
+
+ public static void translateApi(TranslateApi api) {
+ configModel.getTranslate().setUse(api.getName());
+ }
+
+ public static Translate.Config getTranslateConfig(TranslateApi api) {
+ return Optional.of(configModel.getTranslate().getConfig().get(api.getName()))
+ .orElse(api.translate());
+ }
+
+ public static boolean hasTranslateApiKey(TranslateApi api) {
+ return StringUtils.isNoneEmpty(getTranslateConfig(api).getAppId());
+ }
+
+ public static void translateAppid(TranslateApi api, String appId) {
+ getTranslateConfig(api).setAppId(appId);
+ }
+
+ public static String translateAppid(TranslateApi api) {
+ return getTranslateConfig(api).getAppId();
+ }
+
+ public static void translateApikey(TranslateApi api, String secretKey) {
+ getTranslateConfig(api).setSecretKey(secretKey);
+ }
+
+ public static String translateApikey(TranslateApi api) {
+ return getTranslateConfig(api).getSecretKey();
+ }
+
+ public static void translateQps(TranslateApi api, int qps) {
+ getTranslateConfig(api).setQps(qps);
+ }
+
+ public static int translateQps(TranslateApi api) {
+ return getTranslateConfig(api).getQps();
+ }
+
+// --------------------------------{ 网络代理 }------------------------------------------
+
+ public static ProxySetup proxySetup() {
+ return ProxySetup.valueOf(StringUtils.upperCase(getProxyInfo().getSetup()));
+ }
+
+ public static void proxyTestUrl(String url) {
+ getProxyInfo().setTestUrl(url);
+ }
+
+ public static String proxyTestUrl() {
+ return getProxyInfo().getTestUrl();
+ }
+
+ public static void proxySetup(ProxySetup setup) {
+ getProxyInfo().setSetup(setup.getCode());
+
+ switch (setup) {
+ case NO_PROXY -> HttpUtil.getInstance().clearProxy();
+ case SYSTEM, MANUAL -> {
+ if (ProxySetup.MANUAL.equals(setup) && !hasProxy())
+ return;
+ HttpUtil.getInstance().proxy(setup, ConfigManager.getProxyInfo());
+ }
+ }
+ }
+
+ public static boolean hasProxy() {
+ if (configModel == null)
+ return false;
+ ProxyInfo proxyInfo = getProxyInfo();
+ return proxyInfo != null
+ && StringUtils.isNoneEmpty(proxyInfo.getHost())
+ && StringUtils.isNoneEmpty(proxyInfo.getPort())
+ && Integer.parseInt(proxyInfo.getPort()) > 0;
+ }
+
+ public static ProxyInfo getProxyInfo() {
+ ProxyInfo proxyInfo = configModel.getProxy();
+
+ if (proxyInfo == null)
+ setProxyInfo(new ProxyInfo());
+
+ return configModel.getProxy();
+ }
+
+ private static void setProxyInfo(ProxyInfo info) {
+ configModel.setProxy(info);
+ }
+
+ public static String proxyHost() {
+ return getProxyInfo().getHost();
+ }
+
+ public static void proxyHost(String host) {
+ getProxyInfo().setHost(host);
+ }
+
+ public static String proxyPort() {
+ return getProxyInfo().getPort();
+ }
+
+ public static int getProxyPort() {
+ return Integer.parseInt(proxyPort());
+ }
+
+ public static void proxyPort(String port) {
+ if (!NumberUtils.isParsable(port)) return;
+
+ getProxyInfo().setPort(port);
+ }
+
+ public static void checkProxy(BiConsumer consumer) {
+ if (ProxySetup.SYSTEM.equals(proxySetup())) {
+ consumer.accept(true, "");
+ return;
+ }
+ if (!hasProxy()) return;
+
+ ThreadPoolManager.getInstance().execute(() -> {
+ try {
+ try (Socket socket = new Socket(proxyHost(), getProxyPort())) {
+ Platform.runLater(() -> consumer.accept(true, "success"));
+ } catch (IOException e) {
+ Platform.runLater(() -> consumer.accept(false, "connection timed out"));
+ }
+ } catch (Exception e) {
+ logger.error(STR."host=\{proxyHost()},port=\{proxyPort()}", e);
+ Platform.runLater(() -> consumer.accept(false, e.getMessage()));
+ }
+ });
+ }
+
+// --------------------------------{ 版本检查 }------------------------------------------
+
+ public static UpgradeConfig upgradeConfig() {
+ return upgradeConfig;
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/manager/http/CookieManager.java b/src/main/java/cn/octopusyan/dmt/common/manager/http/CookieManager.java
new file mode 100644
index 0000000..2a3096d
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/http/CookieManager.java
@@ -0,0 +1,371 @@
+package cn.octopusyan.dmt.common.manager.http;
+
+import java.net.*;
+import java.util.*;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Cookie 管理
+ *
+ * @author octopus_yan
+ */
+public class CookieManager {
+
+ private static final InMemoryCookieStore inMemoryCookieStore = new InMemoryCookieStore();
+ private static final java.net.CookieManager cookieManager =
+ new java.net.CookieManager(inMemoryCookieStore, CookiePolicy.ACCEPT_ALL);
+
+ public static java.net.CookieManager get() {
+ return cookieManager;
+ }
+
+ public static InMemoryCookieStore getStore() {
+ return inMemoryCookieStore;
+ }
+
+ public static class InMemoryCookieStore implements CookieStore {
+ // the in-memory representation of cookies
+ private final List cookieJar;
+
+ // the cookies are indexed by its domain and associated uri (if present)
+ // CAUTION: when a cookie removed from main data structure (i.e. cookieJar),
+ // it won't be cleared in domainIndex & uriIndex. Double-check the
+ // presence of cookie when retrieve one form index store.
+ private final Map> domainIndex;
+ private final Map> uriIndex;
+
+ // use ReentrantLock instead of synchronized for scalability
+ private final ReentrantLock lock;
+
+
+ /**
+ * The default ctor
+ */
+ public InMemoryCookieStore() {
+ cookieJar = new ArrayList<>();
+ domainIndex = new HashMap<>();
+ uriIndex = new HashMap<>();
+
+ lock = new ReentrantLock(false);
+ }
+
+ /**
+ * Add one cookie into cookie store.
+ */
+ public void add(URI uri, HttpCookie cookie) {
+ // pre-condition : argument can't be null
+ if (cookie == null) {
+ throw new NullPointerException("cookie is null");
+ }
+
+
+ lock.lock();
+ try {
+ // remove the ole cookie if there has had one
+ cookieJar.remove(cookie);
+
+ // add new cookie if it has a non-zero max-age
+ if (cookie.getMaxAge() != 0) {
+ cookieJar.add(cookie);
+ // and add it to domain index
+ if (cookie.getDomain() != null) {
+ addIndex(domainIndex, cookie.getDomain(), cookie);
+ }
+ if (uri != null) {
+ // add it to uri index, too
+ addIndex(uriIndex, getEffectiveURI(uri), cookie);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+
+ /**
+ * Get all cookies, which:
+ * 1) given uri domain-matches with, or, associated with
+ * given uri when added to the cookie store.
+ * 3) not expired.
+ * See RFC 2965 sec. 3.3.4 for more detail.
+ */
+ public List get(URI uri) {
+ // argument can't be null
+ if (uri == null) {
+ throw new NullPointerException("uri is null");
+ }
+
+ List cookies = new ArrayList<>();
+ boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
+ lock.lock();
+ try {
+ // check domainIndex first
+ getInternal1(cookies, domainIndex, uri.getHost(), secureLink);
+ // check uriIndex then
+ getInternal2(cookies, uriIndex, getEffectiveURI(uri), secureLink);
+ } finally {
+ lock.unlock();
+ }
+
+ return cookies;
+ }
+
+ /**
+ * Get all cookies in cookie store, except those have expired
+ */
+ public List getCookies() {
+ List rt;
+
+ lock.lock();
+ try {
+ cookieJar.removeIf(HttpCookie::hasExpired);
+ } finally {
+ rt = Collections.unmodifiableList(cookieJar);
+ lock.unlock();
+ }
+
+ return rt;
+ }
+
+ /**
+ * Get all URIs, which are associated with at least one cookie
+ * of this cookie store.
+ */
+ public List getURIs() {
+ List uris;
+
+ lock.lock();
+ try {
+ Iterator it = uriIndex.keySet().iterator();
+ while (it.hasNext()) {
+ URI uri = it.next();
+ List cookies = uriIndex.get(uri);
+ if (cookies == null || cookies.isEmpty()) {
+ // no cookies list or an empty list associated with
+ // this uri entry, delete it
+ it.remove();
+ }
+ }
+ } finally {
+ uris = new ArrayList<>(uriIndex.keySet());
+ lock.unlock();
+ }
+
+ return uris;
+ }
+
+
+ /**
+ * Remove a cookie from store
+ */
+ public boolean remove(URI uri, HttpCookie ck) {
+ // argument can't be null
+ if (ck == null) {
+ throw new NullPointerException("cookie is null");
+ }
+
+ boolean modified;
+ lock.lock();
+ try {
+ modified = cookieJar.remove(ck);
+ } finally {
+ lock.unlock();
+ }
+
+ return modified;
+ }
+
+
+ /**
+ * Remove all cookies in this cookie store.
+ */
+ public boolean removeAll() {
+ lock.lock();
+ try {
+ if (cookieJar.isEmpty()) {
+ return false;
+ }
+ cookieJar.clear();
+ domainIndex.clear();
+ uriIndex.clear();
+ } finally {
+ lock.unlock();
+ }
+
+ return true;
+ }
+
+
+ /* ---------------- Private operations -------------- */
+
+
+ /*
+ * This is almost the same as HttpCookie.domainMatches except for
+ * one difference: It won't reject cookies when the 'H' part of the
+ * domain contains a dot ('.').
+ * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com
+ * and the cookie domain is .domain.com, then it should be rejected.
+ * However that's not how the real world works. Browsers don't reject and
+ * some sites, like yahoo.com do actually expect these cookies to be
+ * passed along.
+ * And should be used for 'old' style cookies (aka Netscape type of cookies)
+ */
+ private boolean netscapeDomainMatches(String domain, String host) {
+ if (domain == null || host == null) {
+ return false;
+ }
+
+ // if there's no embedded dot in domain and domain is not .local
+ boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
+ int embeddedDotInDomain = domain.indexOf('.');
+ if (embeddedDotInDomain == 0) {
+ embeddedDotInDomain = domain.indexOf('.', 1);
+ }
+ if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
+ return false;
+ }
+
+ // if the host name contains no dot and the domain name is .local
+ int firstDotInHost = host.indexOf('.');
+ if (firstDotInHost == -1 && isLocalDomain) {
+ return true;
+ }
+
+ int domainLength = domain.length();
+ int lengthDiff = host.length() - domainLength;
+ if (lengthDiff == 0) {
+ // if the host name and the domain name are just string-compare equal
+ return host.equalsIgnoreCase(domain);
+ } else if (lengthDiff > 0) {
+ // need to check H & D component
+ String H = host.substring(0, lengthDiff);
+ String D = host.substring(lengthDiff);
+
+ return (D.equalsIgnoreCase(domain));
+ } else if (lengthDiff == -1) {
+ // if domain is actually .host
+ return (domain.charAt(0) == '.' &&
+ host.equalsIgnoreCase(domain.substring(1)));
+ }
+
+ return false;
+ }
+
+ private void getInternal1(List cookies, Map> cookieIndex,
+ String host, boolean secureLink) {
+ // Use a separate list to handle cookies that need to be removed so
+ // that there is no conflict with iterators.
+ ArrayList toRemove = new ArrayList<>();
+ for (Map.Entry> entry : cookieIndex.entrySet()) {
+ String domain = entry.getKey();
+ List lst = entry.getValue();
+ for (HttpCookie c : lst) {
+ if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) ||
+ (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) {
+ if ((cookieJar.contains(c))) {
+ // the cookie still in main cookie store
+ if (!c.hasExpired()) {
+ // don't add twice and make sure it's the proper
+ // security level
+ if ((secureLink || !c.getSecure()) &&
+ !cookies.contains(c)) {
+ cookies.add(c);
+ }
+ } else {
+ toRemove.add(c);
+ }
+ } else {
+ // the cookie has been removed from main store,
+ // so also remove it from domain indexed store
+ toRemove.add(c);
+ }
+ }
+ }
+ // Clear up the cookies that need to be removed
+ for (HttpCookie c : toRemove) {
+ lst.remove(c);
+ cookieJar.remove(c);
+
+ }
+ toRemove.clear();
+ }
+ }
+
+ // @param cookies [OUT] contains the found cookies
+ // @param cookieIndex the index
+ // @param comparator the prediction to decide whether or not
+ // a cookie in index should be returned
+ private void getInternal2(List cookies,
+ Map> cookieIndex,
+ Comparable comparator, boolean secureLink) {
+ for (T index : cookieIndex.keySet()) {
+ if (comparator.compareTo(index) == 0) {
+ List indexedCookies = cookieIndex.get(index);
+ // check the list of cookies associated with this domain
+ if (indexedCookies != null) {
+ Iterator it = indexedCookies.iterator();
+ while (it.hasNext()) {
+ HttpCookie ck = it.next();
+ if (cookieJar.contains(ck)) {
+ // the cookie still in main cookie store
+ if (!ck.hasExpired()) {
+ // don't add twice
+ if ((secureLink || !ck.getSecure()) &&
+ !cookies.contains(ck))
+ cookies.add(ck);
+ } else {
+ it.remove();
+ cookieJar.remove(ck);
+ }
+ } else {
+ // the cookie has been removed from main store,
+ // so also remove it from domain indexed store
+ it.remove();
+ }
+ }
+ } // end of indexedCookies != null
+ } // end of comparator.compareTo(index) == 0
+ } // end of cookieIndex iteration
+ }
+
+ // add 'cookie' indexed by 'index' into 'indexStore'
+ private void addIndex(Map> indexStore,
+ T index,
+ HttpCookie cookie) {
+ if (index != null) {
+ List cookies = indexStore.get(index);
+ if (cookies != null) {
+ // there may already have the same cookie, so remove it first
+ cookies.remove(cookie);
+
+ cookies.add(cookie);
+ } else {
+ cookies = new ArrayList<>();
+ cookies.add(cookie);
+ indexStore.put(index, cookies);
+ }
+ }
+ }
+
+
+ //
+ // for cookie purpose, the effective uri should only be http://host
+ // the path will be taken into account when path-match algorithm applied
+ //
+ private URI getEffectiveURI(URI uri) {
+ URI effectiveURI;
+ try {
+ effectiveURI = new URI("http",
+ uri.getHost(),
+ null, // path component
+ null, // query component
+ null // fragment component
+ );
+ } catch (URISyntaxException ignored) {
+ effectiveURI = uri;
+ }
+
+ return effectiveURI;
+ }
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/manager/http/HttpConfig.java b/src/main/java/cn/octopusyan/dmt/common/manager/http/HttpConfig.java
new file mode 100644
index 0000000..46f5b7a
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/http/HttpConfig.java
@@ -0,0 +1,116 @@
+package cn.octopusyan.dmt.common.manager.http;
+
+import lombok.Data;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ProxySelector;
+import java.net.http.HttpClient;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.Executor;
+
+/**
+ * Http配置参数
+ *
+ * @author octopus_yan@foxmail.com
+ */
+@Data
+public class HttpConfig {
+ private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
+
+ static {
+ // 使用系统默认代理
+ System.setProperty("java.net.useSystemProxies", "true");
+ }
+
+ /**
+ * http版本
+ */
+ private HttpClient.Version version = HttpClient.Version.HTTP_2;
+
+ /**
+ * 转发策略
+ */
+ private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
+
+ /**
+ * 线程池
+ */
+ private Executor executor;
+
+ /**
+ * 认证
+ */
+ private Authenticator authenticator;
+
+ /**
+ * 代理
+ */
+ private ProxySelector proxySelector;
+
+ /**
+ * CookieHandler
+ */
+ private CookieHandler cookieHandler;
+
+ /**
+ * sslContext
+ */
+ private SSLContext sslContext;
+
+ /**
+ * sslParams
+ */
+ private SSLParameters sslParameters;
+
+ /**
+ * 连接超时时间毫秒
+ */
+ private int connectTimeout = 10000;
+
+ /**
+ * 默认读取数据超时时间
+ */
+ private int defaultReadTimeout = 1200000;
+
+
+ public HttpConfig() {
+ // SSL
+ sslParameters = new SSLParameters();
+ sslParameters.setEndpointIdentificationAlgorithm("");
+ sslParameters.setProtocols(new String[]{"TLSv1.2"});
+ try {
+ sslContext = SSLContext.getInstance("TLSv1.2");
+ System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
+ sslContext.init(null, trustAllCertificates, new SecureRandom());
+ } catch (NoSuchAlgorithmException | KeyManagementException e) {
+ logger.error("", e);
+ }
+ }
+
+ private static final TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0]; // Not relevant.
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
+ // TODO Auto-generated method stub
+ }
+ }};
+}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpUtil.java b/src/main/java/cn/octopusyan/dmt/common/manager/http/HttpUtil.java
similarity index 51%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpUtil.java
rename to src/main/java/cn/octopusyan/dmt/common/manager/http/HttpUtil.java
index 09caa16..2bd1e5a 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpUtil.java
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/http/HttpUtil.java
@@ -1,7 +1,13 @@
-package cn.octopusyan.dayzmodtranslator.manager.http;
+package cn.octopusyan.dmt.common.manager.http;
-import com.alibaba.fastjson2.JSONObject;
+import cn.octopusyan.dmt.common.enums.ProxySetup;
+import cn.octopusyan.dmt.common.manager.http.response.BodyHandler;
+import cn.octopusyan.dmt.common.util.JsonUtil;
+import cn.octopusyan.dmt.model.ProxyInfo;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
@@ -11,15 +17,19 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.time.Duration;
-import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.function.BiConsumer;
/**
* 网络请求封装
*
* @author octopus_yan@foxmail.com
*/
+@Slf4j
public class HttpUtil {
private volatile static HttpUtil util;
private volatile HttpClient httpClient;
@@ -57,15 +67,20 @@ public class HttpUtil {
return builder.build();
}
- public HttpUtil proxy(String host, int port) {
+ public void proxy(ProxySetup setup, ProxyInfo proxy) {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
- InetSocketAddress unresolved = InetSocketAddress.createUnresolved(host, port);
- ProxySelector other = ProxySelector.of(unresolved);
- this.httpConfig.setProxySelector(other);
+ switch (setup) {
+ case NO_PROXY -> clearProxy();
+ case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
+ case MANUAL -> {
+ InetSocketAddress unresolved = InetSocketAddress.createUnresolved(proxy.getHost(), Integer.parseInt(proxy.getPort()));
+ httpConfig.setProxySelector(ProxySelector.of(unresolved));
+ }
+ }
+
this.httpClient = createClient(httpConfig);
- return this;
}
public void clearProxy() {
@@ -76,24 +91,26 @@ public class HttpUtil {
httpClient = createClient(httpConfig);
}
- public HttpClient getHttpClient() {
- return httpClient;
+ public void close() {
+ if (httpClient == null) return;
+ httpClient.close();
}
- public String get(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
+ public String get(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header).GET();
HttpResponse response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
- public String post(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
+ public String post(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri, header)
- .POST(HttpRequest.BodyPublishers.ofString(param.toJSONString()));
+ .header("Content-Type", "application/json;charset=utf-8")
+ .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJsonString(param)));
HttpResponse response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
- public String postForm(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
+ public String postForm(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
.POST(HttpRequest.BodyPublishers.noBody());
@@ -101,39 +118,73 @@ public class HttpUtil {
return response.body();
}
- private HttpRequest.Builder getRequest(String uri, JSONObject header) {
+ public void download(String url, String savePath, BiConsumer listener) throws IOException, InterruptedException {
+ HttpRequest request = getRequest(url, null).build();
+ // 检查bin目录
+ File binDir = new File(savePath);
+ if (!binDir.exists()) {
+ log.debug(STR."dir [\{savePath}] not exists");
+ //noinspection ResultOfMethodCallIgnored
+ binDir.mkdirs();
+ log.debug(STR."created dir [\{savePath}]");
+ }
+
+ // 下载处理器
+ var handler = BodyHandler.create(
+ Path.of(savePath),
+ StandardOpenOption.CREATE, StandardOpenOption.WRITE
+ );
+
+ // 下载监听
+ if (listener != null)
+ handler.listener(listener);
+
+ HttpResponse response = httpClient.send(request, handler);
+ }
+
+ private HttpRequest.Builder getRequest(String uri, JsonNode header) {
HttpRequest.Builder request = HttpRequest.newBuilder();
// 请求地址
request.uri(URI.create(uri));
// 请求头
if (header != null && !header.isEmpty()) {
- for (String key : header.keySet()) {
- request.header(key, header.getString(key));
+ for (Map.Entry property : header.properties()) {
+ String key = property.getKey();
+ request.header(key, JsonUtil.toJsonString(property.getValue()));
}
}
+ // Cookie
+// List cookies = CookieManager.getStore().get(URI.create(uri));
+// if (!cookies.isEmpty()) {
+// String cookie = cookies.stream()
+// .map(item -> STR."\{item.getName()}=\{item.getValue()}")
+// .collect(Collectors.joining(";"));
+// request.header("Cookie", cookie);
+// }
return request;
}
- private String createFormParams(JSONObject params) {
+ private String createFormParams(JsonNode params) {
StringBuilder formParams = new StringBuilder();
if (params == null) {
return formParams.toString();
}
- for (String key : params.keySet()) {
- Object value = params.get(key);
- if (value instanceof String) {
- value = URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8);
+ for (Map.Entry property : params.properties()) {
+ String key = property.getKey();
+ JsonNode value = params.get(key);
+ if (value.isTextual()) {
+ String value_ = URLEncoder.encode(String.valueOf(value.asText()), StandardCharsets.UTF_8);
+ formParams.append("&").append(key).append("=").append(value_);
+ } else if (value.isNumber()) {
formParams.append("&").append(key).append("=").append(value);
- } else if (value instanceof Number) {
- formParams.append("&").append(key).append("=").append(value);
- } else if (value instanceof List) {
- formParams.append("&").append(key).append("=").append(params.getJSONArray(key));
+ } else if (value.isArray()) {
+ formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
} else {
- formParams.append("&").append(key).append("=").append(params.getJSONObject(key));
+ formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
}
}
if (!formParams.isEmpty()) {
- formParams = new StringBuilder("?" + formParams.substring(1));
+ formParams = new StringBuilder(STR."?\{formParams.substring(1)}");
}
return formParams.toString();
diff --git a/src/main/java/cn/octopusyan/dmt/common/manager/http/response/BodyHandler.java b/src/main/java/cn/octopusyan/dmt/common/manager/http/response/BodyHandler.java
new file mode 100644
index 0000000..8934dd8
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/http/response/BodyHandler.java
@@ -0,0 +1,102 @@
+package cn.octopusyan.dmt.common.manager.http.response;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * 下载处理
+ *
+ * @author octopus_yan
+ */
+@Slf4j
+public class BodyHandler implements HttpResponse.BodyHandler {
+ private final HttpResponse.BodyHandler handler;
+ private BiConsumer consumer;
+
+ private BodyHandler(HttpResponse.BodyHandler handler) {
+ this.handler = handler;
+ }
+
+ public static BodyHandler create(Path directory, OpenOption... openOptions) {
+ return new BodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions));
+ }
+
+ @Override
+ public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) {
+ AtomicLong length = new AtomicLong(-1);
+ // 获取文件大小
+ Optional string = responseInfo.headers().firstValue("content-length");
+ string.ifPresentOrElse(s -> {
+ length.set(Long.parseLong(s));
+ log.debug(STR."========={content-length = \{s}}=========");
+ }, () -> {
+ String msg = "response not has header [content-length]";
+ log.error(msg);
+ });
+
+ BodySubscriber subscriber = new BodySubscriber(handler.apply(responseInfo));
+ subscriber.setConsumer(progress -> consumer.accept(length.get(), progress));
+
+ return subscriber;
+ }
+
+ public void listener(BiConsumer consumer) {
+ this.consumer = consumer;
+ }
+
+ public static class BodySubscriber implements HttpResponse.BodySubscriber {
+ private final HttpResponse.BodySubscriber subscriber;
+ private final AtomicLong progress = new AtomicLong(0);
+ @Setter
+ private Consumer consumer;
+
+ public BodySubscriber(HttpResponse.BodySubscriber subscriber) {
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public CompletionStage getBody() {
+ return subscriber.getBody();
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ subscriber.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List item) {
+ subscriber.onNext(item);
+
+ // 记录进度
+ for (ByteBuffer byteBuffer : item) {
+ progress.addAndGet(byteBuffer.limit());
+ }
+ consumer.accept(progress.get());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ subscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ subscriber.onComplete();
+
+ consumer.accept(progress.get());
+ }
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadFactory.java b/src/main/java/cn/octopusyan/dmt/common/manager/thread/ThreadFactory.java
similarity index 82%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadFactory.java
rename to src/main/java/cn/octopusyan/dmt/common/manager/thread/ThreadFactory.java
index 8f3019e..3a31327 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadFactory.java
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/thread/ThreadFactory.java
@@ -1,4 +1,4 @@
-package cn.octopusyan.dayzmodtranslator.manager.thread;
+package cn.octopusyan.dmt.common.manager.thread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,7 +26,7 @@ public class ThreadFactory implements java.util.concurrent.ThreadFactory {
public ThreadFactory(String prefix) {
group = Thread.currentThread().getThreadGroup();
- namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
+ namePrefix = STR."\{prefix}-\{poolNumber.getAndIncrement()}-thread-";
}
@Override
@@ -36,9 +36,7 @@ public class ThreadFactory implements java.util.concurrent.ThreadFactory {
namePrefix + threadNumber.getAndIncrement(),
0);
- t.setUncaughtExceptionHandler((t1, e) -> {
- logger.error("thread : {}, error", t1.getName(), e);
- });
+ t.setUncaughtExceptionHandler((t1, e) -> logger.error("thread : {}, error", t1.getName(), e));
if (t.isDaemon())
t.setDaemon(false);
diff --git a/src/main/java/cn/octopusyan/dmt/common/manager/thread/ThreadPoolManager.java b/src/main/java/cn/octopusyan/dmt/common/manager/thread/ThreadPoolManager.java
new file mode 100644
index 0000000..726e5bf
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/manager/thread/ThreadPoolManager.java
@@ -0,0 +1,55 @@
+package cn.octopusyan.dmt.common.manager.thread;
+
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 线程池管理类
+ */
+public final class ThreadPoolManager extends ThreadPoolExecutor {
+
+ private static volatile ThreadPoolManager sInstance;
+ private static final List poolManagerList = new ArrayList<>();
+
+ private ThreadPoolManager() {
+ this("");
+ }
+
+ private ThreadPoolManager(String threadPoolName) {
+ super(32,
+ 200,
+ 10,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(200),
+ new ThreadFactory(StringUtils.isEmpty(threadPoolName) ? ThreadFactory.DEFAULT_THREAD_PREFIX : threadPoolName),
+ new DiscardPolicy());
+ }
+
+ public static ThreadPoolManager getInstance(String threadPoolName) {
+ ThreadPoolManager threadPoolManager = new ThreadPoolManager(threadPoolName);
+ poolManagerList.add(threadPoolManager);
+ return threadPoolManager;
+ }
+
+ public static ThreadPoolManager getInstance() {
+ if (sInstance == null) {
+ synchronized (ThreadPoolManager.class) {
+ if (sInstance == null) {
+ sInstance = new ThreadPoolManager();
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ public static void shutdownAll() {
+ getInstance().shutdown();
+ poolManagerList.forEach(ThreadPoolExecutor::shutdown);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/ClipUtil.java b/src/main/java/cn/octopusyan/dmt/common/util/ClipUtil.java
similarity index 79%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/util/ClipUtil.java
rename to src/main/java/cn/octopusyan/dmt/common/util/ClipUtil.java
index 771eb5e..f0b81b0 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/ClipUtil.java
+++ b/src/main/java/cn/octopusyan/dmt/common/util/ClipUtil.java
@@ -1,18 +1,21 @@
-package cn.octopusyan.dayzmodtranslator.util;
+package cn.octopusyan.dmt.common.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.datatransfer.*;
import java.io.IOException;
/**
- * author : octopus yan
- *
email : octopus_yan@foxmail.com
- *
description : 剪切板工具
- *
create : 2022-4-14 23:21
+ * 剪切板工具
+ *
+ * @author octopus_yan
*/
public class ClipUtil {
//获取系统剪切板
private static final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ private static final Logger log = LoggerFactory.getLogger(ClipUtil.class);
public static void setClip(String data) {
//构建String数据类型
@@ -28,7 +31,7 @@ public class ClipUtil {
//从数据中获取文本值
return (String) content.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException | IOException e) {
- e.printStackTrace();
+ log.error("", e);
return null;
}
}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/FxmlUtil.java b/src/main/java/cn/octopusyan/dmt/common/util/FxmlUtil.java
similarity index 80%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/util/FxmlUtil.java
rename to src/main/java/cn/octopusyan/dmt/common/util/FxmlUtil.java
index 1061b96..aa11f31 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/FxmlUtil.java
+++ b/src/main/java/cn/octopusyan/dmt/common/util/FxmlUtil.java
@@ -1,5 +1,6 @@
-package cn.octopusyan.dayzmodtranslator.util;
+package cn.octopusyan.dmt.common.util;
+import cn.octopusyan.dmt.common.config.Context;
import javafx.fxml.FXMLLoader;
import javafx.fxml.JavaFXBuilderFactory;
@@ -19,7 +20,7 @@ public class FxmlUtil {
FxmlUtil.class.getResource(prefix + name + suffix),
null,
new JavaFXBuilderFactory(),
- null,
+ Context.getControlFactory(),
StandardCharsets.UTF_8
);
}
diff --git a/src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java b/src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java
new file mode 100644
index 0000000..7964284
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java
@@ -0,0 +1,187 @@
+package cn.octopusyan.dmt.common.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+
+/**
+ * Jackson 封装工具类
+ *
+ * @author octopus_yan
+ */
+public class JsonUtil {
+ private static final Logger log = LoggerFactory.getLogger(JsonUtil.class);
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * 时间日期格式
+ */
+ private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
+
+ static {
+ //对象的所有字段全部列入序列化
+ objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
+ //取消默认转换timestamps形式
+ objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+ //忽略空Bean转json的错误
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ //所有的日期格式都统一为以下的格式,即yyyy-MM-dd HH:mm:ss
+ objectMapper.setDateFormat(new SimpleDateFormat(STANDARD_FORMAT));
+ //忽略 在json字符串中存在,但在java对象中不存在对应属性的情况。防止错误
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ /**
+ * Json字符串 转 JavaBean
+ *
+ * @param jsonString Json字符串
+ * @param clazz Java类对象
+ * @param Java类
+ * @return JavaBean
+ */
+ public static T parseObject(String jsonString, Class clazz) {
+ T t = null;
+ try {
+ t = objectMapper.readValue(jsonString, clazz);
+ } catch (JsonProcessingException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return t;
+ }
+
+ /**
+ * 读取Json文件 转 JavaBean
+ *
+ * @param file Json文件
+ * @param clazz Java类对象
+ * @param Java类
+ * @return JavaBean
+ */
+ public static T parseObject(File file, Class clazz) {
+ T t = null;
+ try {
+ t = objectMapper.readValue(file, clazz);
+ } catch (IOException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return t;
+ }
+
+ /**
+ * 读取Json字符串 转 JavaBean集合
+ *
+ * @param jsonArray Json字符串
+ * @param reference 类型
+ * @param JavaBean类型
+ * @return JavaBean集合
+ */
+ public static T parseJsonArray(String jsonArray, TypeReference reference) {
+ T t = null;
+ try {
+ t = objectMapper.readValue(jsonArray, reference);
+ } catch (JsonProcessingException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return t;
+ }
+
+
+ /**
+ * JavaBean 转 Json字符串
+ *
+ * @param object JavaBean
+ * @return Json字符串
+ */
+ public static String toJsonString(Object object) {
+ String jsonString = null;
+ try {
+ jsonString = objectMapper.writeValueAsString(object);
+ } catch (JsonProcessingException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return jsonString;
+ }
+
+ /**
+ * JavaBean 转 字节数组
+ *
+ * @param object JavaBean
+ * @return 字节数组
+ */
+ public static byte[] toByteArray(Object object) {
+ byte[] bytes = null;
+ try {
+ bytes = objectMapper.writeValueAsBytes(object);
+ } catch (JsonProcessingException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return bytes;
+ }
+
+ /**
+ * JavaBean序列化到文件
+ *
+ * @param file 写入文件对象
+ * @param object JavaBean
+ */
+ public static void objectToFile(File file, Object object) {
+ try {
+ objectMapper.writeValue(file, object);
+ } catch (Exception e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ }
+
+
+ /**
+ * Json字符串 转 JsonNode
+ *
+ * @param jsonString Json字符串
+ * @return JsonNode
+ */
+ public static JsonNode parseJsonObject(String jsonString) {
+ JsonNode jsonNode = null;
+ try {
+ jsonNode = objectMapper.readTree(jsonString);
+ } catch (JsonProcessingException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return jsonNode;
+ }
+
+ /**
+ * JavaBean 转 JsonNode
+ *
+ * @param object JavaBean
+ * @return JsonNode
+ */
+ public static JsonNode parseJsonObject(Object object) {
+ return objectMapper.valueToTree(object);
+ }
+
+ /**
+ * JsonNode 转 Json字符串
+ *
+ * @param jsonNode JsonNode
+ * @return Json字符串
+ */
+ public static String toJsonString(JsonNode jsonNode) {
+ String jsonString = null;
+ try {
+ jsonString = objectMapper.writeValueAsString(jsonNode);
+ } catch (JsonProcessingException e) {
+ log.error("失败:{}", e.getMessage());
+ }
+ return jsonString;
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java b/src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java
new file mode 100644
index 0000000..b814c48
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java
@@ -0,0 +1,119 @@
+package cn.octopusyan.dmt.common.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.exec.*;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 命令工具类
+ *
+ * @author octopus_yan@foxmail.com
+ */
+@Slf4j
+public class ProcessesUtil {
+ private static final String NEW_LINE = System.lineSeparator();
+ public static final int[] EXIT_VALUES = {0, 1};
+
+ private final DefaultExecutor executor;
+ private final ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
+ private OnExecuteListener listener;
+ private CommandLine commandLine;
+
+ private static final Set set = new HashSet<>();
+
+ /**
+ * Prevent construction.
+ */
+ private ProcessesUtil(String workingDirectory) {
+ this(new File(workingDirectory));
+ }
+
+ private ProcessesUtil(File workingDirectory) {
+ LogOutputStream logout = new LogOutputStream() {
+ @Override
+ protected void processLine(String line, int logLevel) {
+ if (listener != null)
+ listener.onExecute(line + NEW_LINE);
+ }
+ };
+ PumpStreamHandler streamHandler = new PumpStreamHandler(logout, logout);
+ executor = DefaultExecutor.builder()
+ .setExecuteStreamHandler(streamHandler)
+ .setWorkingDirectory(workingDirectory)
+ .get();
+ executor.setExitValues(EXIT_VALUES);
+ executor.setProcessDestroyer(processDestroyer);
+ }
+
+ public static ProcessesUtil init(String workingDirectory) {
+ return init(new File(workingDirectory));
+ }
+ public static ProcessesUtil init(File workingDirectory) {
+ ProcessesUtil util = new ProcessesUtil(workingDirectory);
+ set.add(util);
+ return util;
+ }
+
+ public boolean exec(String command) {
+ commandLine = CommandLine.parse(command);
+ try {
+ int execute = executor.execute(commandLine);
+ return Arrays.stream(EXIT_VALUES).anyMatch(item -> item == execute);
+ } catch (Exception e) {
+ log.error("exec error", e);
+ return false;
+ }
+ }
+
+ public void exec(String command, OnExecuteListener listener) {
+ this.listener = listener;
+ commandLine = CommandLine.parse(command);
+ DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
+ @Override
+ public void onProcessComplete(int exitValue) {
+ if (listener != null) {
+ listener.onExecuteSuccess(Arrays.stream(EXIT_VALUES).noneMatch(item -> item == exitValue));
+ }
+ }
+
+ @Override
+ public void onProcessFailed(ExecuteException e) {
+ if (listener != null) {
+ listener.onExecuteError(e);
+ }
+ }
+ };
+ try {
+ executor.execute(commandLine, handler);
+ } catch (Exception e) {
+ if (listener != null) listener.onExecuteError(e);
+ }
+ }
+
+ public void destroy() {
+ if (processDestroyer.isEmpty()) return;
+ processDestroyer.run();
+ }
+
+ public boolean isRunning() {
+ return !processDestroyer.isEmpty();
+ }
+
+ public static void destroyAll() {
+ set.forEach(ProcessesUtil::destroy);
+ }
+
+ public interface OnExecuteListener {
+ void onExecute(String msg);
+
+ default void onExecuteSuccess(boolean success) {
+ }
+
+ default void onExecuteError(Exception e) {
+ }
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/PropertiesUtils.java b/src/main/java/cn/octopusyan/dmt/common/util/PropertiesUtils.java
similarity index 69%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/util/PropertiesUtils.java
rename to src/main/java/cn/octopusyan/dmt/common/util/PropertiesUtils.java
index 1616515..839e110 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/PropertiesUtils.java
+++ b/src/main/java/cn/octopusyan/dmt/common/util/PropertiesUtils.java
@@ -1,10 +1,13 @@
-package cn.octopusyan.dayzmodtranslator.util;
+package cn.octopusyan.dmt.common.util;
+import cn.octopusyan.dmt.utils.Resources;
import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.util.Properties;
/**
@@ -17,14 +20,16 @@ public class PropertiesUtils {
/**
* 主配置文件
*/
- private Properties properties;
+ private final Properties properties;
/**
* 启用配置文件
*/
- private Properties propertiesCustom;
+ private final Properties propertiesCustom;
private static PropertiesUtils propertiesUtils = new PropertiesUtils();
+ public static final Logger logger = LoggerFactory.getLogger(PropertiesUtils.class);
+
/**
* 私有构造,禁止直接创建
*/
@@ -32,17 +37,18 @@ public class PropertiesUtils {
// 读取配置启用的配置文件名
properties = new Properties();
propertiesCustom = new Properties();
- InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("application.properties");
+
+ InputStream in = Resources.getResourceAsStream("application.properties");
try {
- properties.load(in);
+ properties.load(new InputStreamReader(in));
// 加载启用的配置
String property = properties.getProperty("profiles.active");
if (!StringUtils.isBlank(property)) {
- InputStream cin = PropertiesUtils.class.getClassLoader().getResourceAsStream("application-" + property + ".properties");
- propertiesCustom.load(cin);
+ InputStream cin = Resources.getResourceAsStream("application-" + property + ".properties");
+ propertiesCustom.load(new InputStreamReader(cin));
}
} catch (IOException e) {
- e.printStackTrace();
+ logger.error("读取配置文件失败", e);
}
}
@@ -58,17 +64,6 @@ public class PropertiesUtils {
return propertiesUtils;
}
- /**
- * 获取单例
- *
- * @return PropertiesUtils
- */
- public static PropertiesUtils getInstance(File file) {
- PropertiesUtils util = new PropertiesUtils();
-
- return util;
- }
-
/**
* 根据属性名读取值
* 先去主配置查询,如果查询不到,就去启用配置查询
diff --git a/src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java b/src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java
new file mode 100644
index 0000000..f08bc44
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java
@@ -0,0 +1,69 @@
+package cn.octopusyan.dmt.common.util;
+
+import cn.octopusyan.dmt.Application;
+import javafx.scene.layout.Pane;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 工具
+ *
+ * @author octopus_yan
+ */
+public class ViewUtil {
+ // 获取系统缩放比
+ public static final double scaleX = Screen.getPrimary().getOutputScaleX();
+ public static final double scaleY = Screen.getPrimary().getOutputScaleY();
+
+
+ private static final Map paneXOffset = new HashMap<>();
+ private static final Map paneYOffset = new HashMap<>();
+
+ public static void bindShadow(Pane pane) {
+ pane.setStyle("""
+ -fx-background-radius: 5;
+ -fx-border-radius: 5;
+ -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 15, 0, 0, 0);
+ -fx-background-insets: 20;
+ -fx-padding: 20;
+ """);
+ }
+
+ public static void bindDragged(Pane pane) {
+ Stage stage = getStage(pane);
+ bindDragged(pane, stage);
+ }
+
+ public static void unbindDragged(Pane pane) {
+ pane.setOnMousePressed(null);
+ pane.setOnMouseDragged(null);
+ paneXOffset.remove(pane);
+ paneYOffset.remove(pane);
+ }
+
+ public static void bindDragged(Pane pane, Stage stage) {
+ pane.setOnMousePressed(event -> {
+ paneXOffset.put(pane, stage.getX() - event.getScreenX());
+ paneYOffset.put(pane, stage.getY() - event.getScreenY());
+ });
+ pane.setOnMouseDragged(event -> {
+ stage.setX(event.getScreenX() + paneXOffset.get(pane));
+ stage.setY(event.getScreenY() + paneYOffset.get(pane));
+ });
+ }
+
+ public static Stage getStage() {
+ return Application.getPrimaryStage();
+ }
+
+ public static Stage getStage(Pane pane) {
+ try {
+ return (Stage) pane.getScene().getWindow();
+ } catch (Throwable e) {
+ return getStage();
+ }
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/controller/MainController.java b/src/main/java/cn/octopusyan/dmt/controller/MainController.java
new file mode 100644
index 0000000..b9bd2f8
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/controller/MainController.java
@@ -0,0 +1,451 @@
+package cn.octopusyan.dmt.controller;
+
+import atlantafx.base.controls.ModalPane;
+import atlantafx.base.theme.Styles;
+import atlantafx.base.theme.Theme;
+import cn.octopusyan.dmt.common.base.BaseController;
+import cn.octopusyan.dmt.common.config.Context;
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.common.util.ClipUtil;
+import cn.octopusyan.dmt.common.util.FxmlUtil;
+import cn.octopusyan.dmt.controller.component.WordEditController;
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.task.TranslateTask;
+import cn.octopusyan.dmt.view.ConsoleLog;
+import cn.octopusyan.dmt.view.EditButtonTableCell;
+import cn.octopusyan.dmt.view.alert.AlertUtil;
+import cn.octopusyan.dmt.view.filemanager.DirectoryTree;
+import cn.octopusyan.dmt.viewModel.MainViewModel;
+import javafx.application.Platform;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXMLLoader;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.control.cell.TextFieldTableCell;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.input.TransferMode;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.FileChooser;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * 主界面
+ *
+ * @author octopus_yan
+ */
+public class MainController extends BaseController {
+ private static final ConsoleLog consoleLog = ConsoleLog.getInstance(MainController.class);
+
+ public Pane root;
+
+ public Menu viewStyle;
+
+ public static final ToggleGroup viewStyleGroup = new ToggleGroup();
+
+ public StackPane mainView;
+ //
+ public VBox translateView;
+ // 打开文件
+ public StackPane selectFileBox;
+ public VBox openFileView;
+ public VBox dragFileView;
+ public VBox loadFileView;
+ // 工具栏
+ public Button fileNameLabel;
+ public Button translate;
+ public ProgressBar translateProgress;
+ // 文件树加载
+ public DirectoryTree treeFileBox;
+ public VBox loadWordBox;
+ public ProgressBar loadWordProgressBar;
+ // 翻译界面
+ public Pane wordBox;
+ public TableView wordTable;
+ // 信息
+ public TitledPane titledPane;
+ public TextArea logArea;
+
+ public final ModalPane modalPane = new ModalPane();
+ // 文件选择器
+ public static final FileChooser fileChooser = new FileChooser();
+
+ static {
+ var extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
+ fileChooser.getExtensionFilters().add(extFilter);
+ }
+
+ @Override
+ public Pane getRootPanel() {
+ return root;
+ }
+
+ @Override
+ public void initData() {
+ // 界面样式
+ List list = ConfigManager.THEME_LIST.stream().map(this::createViewStyleItem).toList();
+ viewStyle.getItems().addAll(list);
+
+ // 信息
+ ConsoleLog.init(logArea);
+ }
+
+ @Override
+ public void initViewStyle() {
+ // 遮罩
+ getRootPanel().getChildren().add(modalPane);
+ modalPane.displayProperty().addListener((_, _, val) -> {
+ if (!val) {
+ modalPane.setAlignment(Pos.CENTER);
+ modalPane.usePredefinedTransitionFactories(null);
+ }
+ });
+ }
+
+ @Override
+ public void initViewAction() {
+
+ // 文件拖拽
+ setDragAction(mainView);
+
+ // 复制单元格内容
+ Context.sceneProperty.addListener(_ -> Context.sceneProperty.get()
+ .getAccelerators()
+ .put(new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY), () -> {
+ ObservableList selectedCells = wordTable.getSelectionModel().getSelectedCells();
+ for (TablePosition tablePosition : selectedCells) {
+ Object cellData = tablePosition.getTableColumn().getCellData(tablePosition.getRow());
+ // 设置剪切板
+ ClipUtil.setClip(cellData.toString());
+ }
+ })
+ );
+
+ // 日志栏清空
+ logArea.contextMenuProperty().addListener(_ ->
+ logArea.getContextMenu().getItems().addListener((ListChangeListener) _ -> {
+ MenuItem clearLog = new MenuItem("清空");
+ clearLog.setOnAction(_ -> logArea.clear());
+ logArea.getContextMenu().getItems().add(clearLog);
+ })
+ );
+ }
+
+ /**
+ * 设置文件拖拽效果
+ */
+ private void setDragAction(Pane fileBox) {
+
+ // 进入
+ fileBox.setOnDragEntered(dragEvent -> {
+ var dragboard = dragEvent.getDragboard();
+ if (dragboard.hasFiles() && isPboFile(dragboard.getFiles().getFirst())) {
+ selectFileBox.setVisible(true);
+ dragFileView.setVisible(true);
+ }
+ });
+
+ //离开
+ fileBox.setOnDragExited(_ -> {
+ selectFileBox.setVisible(false);
+ dragFileView.setVisible(false);
+ });
+
+ //
+ fileBox.setOnDragOver(dragEvent -> {
+ var dragboard = dragEvent.getDragboard();
+ if (dragEvent.getGestureSource() != fileBox && dragboard.hasFiles()) {
+ /* allow for both copying and moving, whatever user chooses */
+ dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
+ }
+ dragEvent.consume();
+ });
+
+ // 松手
+ fileBox.setOnDragDropped(dragEvent -> {
+ dragFileView.setVisible(false);
+
+ var db = dragEvent.getDragboard();
+ boolean success = false;
+ var file = db.getFiles().getFirst();
+ if (db.hasFiles() && isPboFile(file)) {
+ selectFile(file);
+ success = true;
+ }
+ /* 让源知道字符串是否已成功传输和使用 */
+ dragEvent.setDropCompleted(success);
+
+ dragEvent.consume();
+ });
+ }
+
+ /**
+ * 打开文件选择器
+ */
+ public void selectFile() {
+ selectFile(fileChooser.showOpenDialog(getWindow()));
+ }
+
+ /**
+ * 打开代理设置
+ */
+ public void openSetupProxy() throws IOException {
+ FXMLLoader load = FxmlUtil.load("setup/proxy-view");
+ AlertUtil.builder(false)
+ .title("网络代理设置")
+ .content((Pane) load.load())
+ .show();
+ }
+
+ /**
+ * 打开翻译设置
+ */
+ public void openSetupTranslate() throws IOException {
+ FXMLLoader load = FxmlUtil.load("setup/translate-view");
+ AlertUtil.builder(false)
+ .title("翻译设置")
+ .content((Pane) load.load())
+ .show();
+ }
+
+ public void setFileName(String name) {
+ fileNameLabel.setText("PBO文件:" + name);
+ }
+
+ /**
+ * 显示加载PBO文件
+ */
+ public void onLoad() {
+ // 展示加载
+ selectFileBox.setVisible(true);
+ loadFileView.setVisible(true);
+ wordBox.getChildren().remove(wordTable);
+ }
+
+ /**
+ * 显示解包完成
+ *
+ * @param path 解包路径
+ */
+ public void onUnpack(File path) {
+ // 加载解包目录
+ treeFileBox.loadRoot(path);
+ // 隐藏文件选择
+ loadFileView.setVisible(false);
+ selectFileBox.setVisible(false);
+ // 展示翻译界面
+ translateView.setVisible(true);
+ loadWordBox.setVisible(true);
+ }
+
+ /**
+ * 加载可翻译文本数据
+ *
+ * @param wordItems 文本列表
+ */
+ public void onLoadWord(List wordItems) {
+ loadWordBox.setVisible(false);
+ wordBox.setVisible(true);
+ bindWordTable(wordItems);
+ translate.setDisable(false);
+ }
+
+ /**
+ * 打包完成
+ *
+ * @param packFile 打包临时文件
+ */
+ public void onPackOver(File packFile) {
+ // 选择文件保存地址
+ fileChooser.setInitialFileName(packFile.getName());
+ File file = fileChooser.showSaveDialog(getWindow());
+
+ if (file == null)
+ return;
+
+ if (file.exists()) {
+ //文件已存在,则删除覆盖文件
+ FileUtils.deleteQuietly(file);
+ }
+
+ String exportFilePath = file.getAbsolutePath();
+ consoleLog.info(STR."导出文件路径 => \{exportFilePath}");
+
+ try {
+ FileUtils.copyFile(packFile, file);
+ } catch (IOException e) {
+ consoleLog.error("保存文件失败!", e);
+ Platform.runLater(() -> AlertUtil.exception(e).content("保存文件失败!").show());
+ }
+ }
+
+ public void startTranslate() {
+ viewModel.startTranslate();
+ }
+
+ public void startPack() {
+ viewModel.pack();
+ }
+
+ public void selectAllLog() {
+ logArea.selectAll();
+ }
+
+ public void copyLog() {
+ logArea.copy();
+ }
+
+ public void clearLog() {
+ logArea.clear();
+ }
+
+ // ======================================{ }========================================
+
+ /**
+ * 打开文件
+ */
+ private void selectFile(File file) {
+ viewModel.selectFile(file);
+ viewModel.unpack();
+ }
+
+ /**
+ * 绑定表格数据
+ *
+ * @param words 单词列表
+ */
+ private void bindWordTable(List words) {
+
+ if (wordTable == null) {
+ wordTable = new TableView<>();
+ // 填满
+ VBox.setVgrow(wordTable, Priority.ALWAYS);
+ // 可编辑
+ wordTable.setEditable(true);
+ // 自动调整列宽
+ wordTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
+ // 边框
+ Styles.toggleStyleClass(wordTable, Styles.BORDERED);
+// // 行分隔
+// Styles.toggleStyleClass(wordTable, Styles.STRIPED);
+ // 单元格选择模式而不是行选择
+ wordTable.getSelectionModel().setCellSelectionEnabled(true);
+ // 不允许选择多个单元格
+ wordTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+
+ // 创建列
+ TableColumn colFile = createColumn("文件");
+ colFile.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getFile().getName()));
+ TableColumn colOriginal = createColumn("原文");
+ colOriginal.setCellValueFactory(param -> param.getValue().getOriginalProperty());
+ TableColumn colChinese = createColumn("中文翻译");
+ colChinese.setCellValueFactory(param -> param.getValue().getChineseProperty());
+ colChinese.setEditable(true);
+ TableColumn colIcon = new TableColumn<>("");
+ colIcon.setSortable(false);
+ colIcon.setCellFactory(EditButtonTableCell.forTableColumn(item -> {
+ // 展示编辑弹窗
+ try {
+ showModal(getEditWordPane(item), false);
+ } catch (IOException e) {
+ consoleLog.error("加载布局失败", e);
+ }
+ }, item -> {
+ // 翻译当前文本
+ new TranslateTask(Collections.singletonList(item)).execute();
+ }));
+
+ wordTable.getColumns().add(colFile);
+ wordTable.getColumns().add(colOriginal);
+ wordTable.getColumns().add(colChinese);
+ wordTable.getColumns().add(colIcon);
+ }
+
+ // 添加表数据
+ wordTable.getItems().clear();
+ wordBox.getChildren().addFirst(wordTable);
+ wordTable.getItems().addAll(words);
+ }
+
+ /**
+ * 表字段创建
+ *
+ * @param colName 列名
+ * @return 列定义
+ */
+ private TableColumn createColumn(String colName) {
+ TableColumn tableColumn = new TableColumn<>(colName);
+ tableColumn.setCellFactory(TextFieldTableCell.forTableColumn());
+ tableColumn.setPrefWidth(150);
+ tableColumn.setSortable(false);
+ tableColumn.setEditable("中文翻译".equals(colName));
+ return tableColumn;
+ }
+
+ private MenuItem createViewStyleItem(Theme theme) {
+ var item = new RadioMenuItem(theme.getName());
+ item.setSelected(theme.getName().equals(ConfigManager.themeName()));
+ item.setToggleGroup(viewStyleGroup);
+ item.setUserData(theme);
+ item.selectedProperty().subscribe(selected -> {
+ if (!selected) return;
+ ConfigManager.theme(theme);
+ });
+ return item;
+ }
+
+ /**
+ * 是否PBO文件
+ */
+ private boolean isPboFile(File file) {
+ if (file == null) return false;
+ return Pattern.compile(".*(.pbo)$").matcher(file.getName()).matches();
+ }
+
+ /**
+ * 展示遮罩弹窗
+ *
+ * 当{@code persistent}为{@code true}时,需要调用{@link #hideModal}才能关闭
+ *
+ * @param node 展示内容
+ * @param persistent 是否持久性内容
+ */
+ private void showModal(Node node, boolean persistent) {
+ modalPane.setAlignment(Pos.CENTER);
+ modalPane.usePredefinedTransitionFactories(null);
+ modalPane.show(node);
+ modalPane.setPersistent(persistent);
+ }
+
+ /**
+ * 关闭/隐藏遮罩弹窗
+ */
+ public void hideModal() {
+ modalPane.hide(false);
+ modalPane.setPersistent(false);
+ }
+
+ /**
+ * 编辑
+ */
+ private Node getEditWordPane(WordItem data) throws IOException {
+ FXMLLoader load = FxmlUtil.load("component/edit-view");
+ Pane pane = load.load();
+ WordEditController ctrl = load.getController();
+ ctrl.bindData(data);
+ return pane;
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/controller/ProxyController.java b/src/main/java/cn/octopusyan/dmt/controller/ProxyController.java
new file mode 100644
index 0000000..b0e1cec
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/controller/ProxyController.java
@@ -0,0 +1,70 @@
+package cn.octopusyan.dmt.controller;
+
+import cn.octopusyan.dmt.common.base.BaseController;
+import cn.octopusyan.dmt.common.enums.ProxySetup;
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.viewModel.ProxyViewModel;
+import javafx.beans.binding.Bindings;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.TextField;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+
+/**
+ * 设置
+ *
+ * @author octopus_yan
+ */
+public class ProxyController extends BaseController {
+ public VBox root;
+
+ public RadioButton noneProxy;
+ public RadioButton systemProxy;
+ public RadioButton manualProxy;
+ public ToggleGroup proxyGroup = new ToggleGroup();
+
+ public GridPane manualProxyView;
+ public TextField proxyHost;
+ public TextField proxyPort;
+
+ @Override
+ public Pane getRootPanel() {
+ return root;
+ }
+
+ @Override
+ public void initData() {
+ noneProxy.setUserData(ProxySetup.NO_PROXY);
+ systemProxy.setUserData(ProxySetup.SYSTEM);
+ manualProxy.setUserData(ProxySetup.MANUAL);
+
+ noneProxy.setToggleGroup(proxyGroup);
+ systemProxy.setToggleGroup(proxyGroup);
+ manualProxy.setToggleGroup(proxyGroup);
+
+ manualProxyView.disableProperty().bind(
+ Bindings.createBooleanBinding(() -> !manualProxy.selectedProperty().get(), manualProxy.selectedProperty())
+ );
+ }
+
+ @Override
+ public void initViewAction() {
+ proxyGroup.selectedToggleProperty().addListener((_, _, value) -> {
+ viewModel.proxySetupProperty().set((ProxySetup) value.getUserData());
+ });
+ proxyGroup.selectToggle(switch (ConfigManager.proxySetup()) {
+ case ProxySetup.SYSTEM -> systemProxy;
+ case ProxySetup.MANUAL -> manualProxy;
+ default -> noneProxy;
+ });
+
+ proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty());
+ proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty());
+ }
+
+ public void proxyTest() {
+ viewModel.proxyTest();
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/controller/TranslateController.java b/src/main/java/cn/octopusyan/dmt/controller/TranslateController.java
new file mode 100644
index 0000000..a879ab1
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/controller/TranslateController.java
@@ -0,0 +1,93 @@
+package cn.octopusyan.dmt.controller;
+
+import cn.octopusyan.dmt.common.base.BaseController;
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import cn.octopusyan.dmt.view.alert.AlertUtil;
+import cn.octopusyan.dmt.viewModel.TranslateViewModel;
+import javafx.collections.ObservableList;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+import javafx.util.StringConverter;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 翻译
+ *
+ * @author octopus_yan
+ */
+public class TranslateController extends BaseController {
+ public VBox root;
+ public ComboBox translateSourceCombo;
+ public TextField qps;
+ public VBox appidBox;
+ public TextField appid;
+ public VBox apikeyBox;
+ public TextField apikey;
+
+ @Override
+ public Pane getRootPanel() {
+ return root;
+ }
+
+ @Override
+ public void initData() {
+
+ // 翻译源
+ for (TranslateApi value : TranslateApi.values()) {
+ ObservableList items = translateSourceCombo.getItems();
+ items.addAll(value);
+ }
+
+ translateSourceCombo.setConverter(new StringConverter<>() {
+ @Override
+ public String toString(TranslateApi object) {
+ if (object == null) return null;
+
+ return object.getLabel();
+ }
+
+ @Override
+ public TranslateApi fromString(String string) {
+ return TranslateApi.getByLabel(string);
+ }
+ });
+
+ // 当前翻译源
+ translateSourceCombo.getSelectionModel().select(ConfigManager.translateApi());
+ viewModel.getSource().bind(translateSourceCombo.getSelectionModel().selectedItemProperty());
+
+ qps.textProperty().bindBidirectional(viewModel.getQps());
+ appid.textProperty().bindBidirectional(viewModel.getAppId());
+ apikey.textProperty().bindBidirectional(viewModel.getApiKey());
+
+ appidBox.visibleProperty().bind(viewModel.getNeedApiKey());
+ apikeyBox.visibleProperty().bind(viewModel.getNeedApiKey());
+ }
+
+ @Override
+ public void initViewAction() {
+ }
+
+ public void save() {
+ TranslateApi source = translateSourceCombo.getValue();
+ String apikey = this.apikey.getText();
+ String appid = this.appid.getText();
+ int qps = Integer.parseInt(this.qps.getText());
+
+ ConfigManager.translateApi(source);
+ ConfigManager.translateQps(source, qps);
+ if (source.needApiKey()) {
+ if (StringUtils.isBlank(apikey) || StringUtils.isBlank(appid)) {
+ AlertUtil.error("认证信息不能为空");
+ }
+
+ ConfigManager.translateApikey(source, apikey);
+ ConfigManager.translateAppid(source, appid);
+ }
+
+ onDestroy();
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/controller/component/WordEditController.java b/src/main/java/cn/octopusyan/dmt/controller/component/WordEditController.java
new file mode 100644
index 0000000..ddcbe2c
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/controller/component/WordEditController.java
@@ -0,0 +1,63 @@
+package cn.octopusyan.dmt.controller.component;
+
+import cn.octopusyan.dmt.common.base.BaseController;
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.translate.factory.TranslateFactoryImpl;
+import cn.octopusyan.dmt.view.alert.AlertUtil;
+import cn.octopusyan.dmt.viewModel.WordEditViewModel;
+import javafx.application.Platform;
+import javafx.scene.control.Button;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+
+/**
+ * 文本编辑
+ *
+ * @author octopus_yan
+ */
+public class WordEditController extends BaseController {
+
+ public VBox root;
+ public TextArea original;
+ public Button translate;
+ public TextArea chinese;
+ public ProgressIndicator progress;
+
+ @Override
+ public Pane getRootPanel() {
+ return root;
+ }
+
+ @Override
+ public void initData() {
+
+ }
+
+ @Override
+ public void initViewAction() {
+
+ }
+
+ public void bindData(WordItem data) {
+ viewModel.setData(data);
+ original.textProperty().bind(viewModel.getOriginalProperty());
+ chinese.textProperty().bindBidirectional(viewModel.getChineseProperty());
+ }
+
+ public void startTranslate() {
+ progress.setVisible(true);
+ ThreadPoolManager.getInstance().execute(() -> {
+ try {
+ String result = TranslateFactoryImpl.getInstance().translate(ConfigManager.translateApi(), original.getText());
+ Platform.runLater(() -> chinese.setText(result));
+ } catch (Exception e) {
+ Platform.runLater(() -> AlertUtil.exception(e).show());
+ }
+ Platform.runLater(() -> progress.setVisible(false));
+ });
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/model/ConfigModel.java b/src/main/java/cn/octopusyan/dmt/model/ConfigModel.java
new file mode 100644
index 0000000..02dc387
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/model/ConfigModel.java
@@ -0,0 +1,33 @@
+package cn.octopusyan.dmt.model;
+
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import lombok.Data;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GUI配置信息
+ *
+ * @author octopus_yan
+ */
+@Data
+public class ConfigModel {
+ private static final Logger log = LoggerFactory.getLogger(ConfigModel.class);
+
+ /**
+ * 主题
+ */
+ private String theme = ConfigManager.DEFAULT_THEME;
+
+ /**
+ * 代理设置
+ */
+ private ProxyInfo proxy = new ProxyInfo();
+
+ /**
+ * 代理设置
+ */
+ private Translate translate = new Translate();
+
+
+}
diff --git a/src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java b/src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java
new file mode 100644
index 0000000..a65a579
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java
@@ -0,0 +1,39 @@
+package cn.octopusyan.dmt.model;
+
+import cn.octopusyan.dmt.common.enums.ProxySetup;
+import lombok.Data;
+
+/**
+ * 代理信息
+ *
+ * @author octopus_yan
+ */
+@Data
+public class ProxyInfo {
+ /**
+ * 主机地址
+ */
+ private String host = "";
+ /**
+ * 端口
+ */
+ private String port = "";
+ /**
+ * 登录名
+ */
+ private String username = "";
+ /**
+ * 密码
+ */
+ private String password = "";
+ /**
+ * 测试Url
+ */
+ private String testUrl = "http://";
+ /**
+ * 代理类型
+ *
+ * @see ProxySetup
+ */
+ private String setup = ProxySetup.NO_PROXY.getCode();
+}
diff --git a/src/main/java/cn/octopusyan/dmt/model/Translate.java b/src/main/java/cn/octopusyan/dmt/model/Translate.java
new file mode 100644
index 0000000..c750f72
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/model/Translate.java
@@ -0,0 +1,52 @@
+package cn.octopusyan.dmt.model;
+
+import cn.octopusyan.dmt.translate.TranslateApi;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 翻译配置
+ *
+ * @author octopus_yan
+ */
+@Data
+public class Translate {
+ /**
+ * 当前使用接口
+ */
+ private String use = TranslateApi.FREE_BAIDU.getName();
+ /**
+ * 接口配置
+ */
+ private Map config = new HashMap<>() {
+ {
+ // 初始化
+ for (TranslateApi api : TranslateApi.values()) {
+ put(api.getName(), new Config("", "", api.getDefaultQps()));
+ }
+ }
+ };
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ public static class Config {
+ /**
+ * api key
+ */
+ private String appId;
+ /**
+ * api 密钥
+ */
+ private String secretKey;
+ /**
+ * 请求速率
+ */
+ private Integer qps;
+
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java b/src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java
new file mode 100644
index 0000000..e92b986
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java
@@ -0,0 +1,29 @@
+package cn.octopusyan.dmt.model;
+
+import cn.octopusyan.dmt.common.util.PropertiesUtils;
+import lombok.Data;
+
+/**
+ * 更新配置
+ *
+ * @author octopus_yan
+ */
+@Data
+public class UpgradeConfig {
+
+ private final String owner = "octopusYan";
+
+ private final String repo = "dayz-mod-translator";
+
+ private String releaseFile = "DMT-windows-nojre.zip";
+
+ private String version = PropertiesUtils.getInstance().getProperty("app.version");
+
+ public String getReleaseApi() {
+ return STR."https://api.github.com/repos/\{getOwner()}/\{getRepo()}/releases/latest";
+ }
+
+ public String getDownloadUrl(String version) {
+ return STR."https://github.com/\{getOwner()}/\{getRepo()}/releases/download/\{version}/\{getReleaseFile()}";
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/model/WordItem.java b/src/main/java/cn/octopusyan/dmt/model/WordItem.java
new file mode 100644
index 0000000..a7d8193
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/model/WordItem.java
@@ -0,0 +1,52 @@
+package cn.octopusyan.dmt.model;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import lombok.Data;
+
+import java.io.File;
+
+/**
+ * 翻译文本
+ *
+ * @author octopus_yan
+ */
+@Data
+public class WordItem {
+ /**
+ * 所在文件
+ */
+ private File file;
+ /**
+ * 行数
+ */
+ private Integer lines;
+ /**
+ * 开始下标
+ */
+ private Integer index;
+ /**
+ * 原文
+ */
+ private StringProperty originalProperty = new SimpleStringProperty();
+ /**
+ * 中文
+ */
+ private StringProperty chineseProperty = new SimpleStringProperty();
+
+ public WordItem(File file, Integer lines, Integer index, String original, String chinese) {
+ this.file = file;
+ this.lines = lines;
+ this.index = index;
+ this.originalProperty.set(original);
+ this.chineseProperty.set(chinese);
+ }
+
+ public String getChinese() {
+ return chineseProperty.get();
+ }
+
+ public String getOriginal() {
+ return originalProperty.get();
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/PackTask.java b/src/main/java/cn/octopusyan/dmt/task/PackTask.java
new file mode 100644
index 0000000..cfd8f72
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/PackTask.java
@@ -0,0 +1,80 @@
+package cn.octopusyan.dmt.task;
+
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.task.base.BaseTask;
+import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
+import cn.octopusyan.dmt.utils.PBOUtil;
+
+import java.io.File;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/**
+ * 打包任务
+ *
+ * @author octopus_yan
+ */
+public class PackTask extends BaseTask {
+
+ private static final Function, List> sortFunc = items -> items.stream().sorted(Comparator.comparing(WordItem::getLines)).toList();
+ private static final Collector> downstream = Collectors.collectingAndThen(Collectors.toList(), sortFunc);
+
+ private final Map> wordFileMap;
+ private final String unpackPath;
+
+ public PackTask(List words, String unpackPath) {
+ super("Pack");
+
+ if (words == null)
+ throw new RuntimeException("参数为null!");
+
+ this.unpackPath = unpackPath;
+ wordFileMap = words.stream().collect(Collectors.groupingBy(WordItem::getFile, downstream));
+ }
+
+ @Override
+ protected void task() throws Exception {
+ if (wordFileMap.isEmpty()) return;
+
+ // 写入文件
+ PBOUtil.writeWords(wordFileMap);
+ if (listener != null) listener.onWriteOver();
+ // 打包
+ File packFile = PBOUtil.pack(unpackPath);
+ if (listener != null) listener.onPackOver(packFile);
+ }
+
+ /**
+ * 解包监听
+ *
+ * @author octopus_yan
+ */
+ public abstract static class PackListener extends DefaultTaskListener {
+
+ public PackListener() {
+ super(true);
+ getProgress().setWidth(550);
+ }
+
+ @Override
+ protected void onSucceed() {
+
+ }
+
+ /**
+ * 写入完成
+ */
+ public abstract void onWriteOver();
+
+ /**
+ * 打包完成
+ *
+ * @param file 文件地址
+ */
+ public abstract void onPackOver(File file);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java b/src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java
new file mode 100644
index 0000000..04c819c
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java
@@ -0,0 +1,28 @@
+package cn.octopusyan.dmt.task;
+
+import cn.octopusyan.dmt.common.manager.http.HttpUtil;
+import cn.octopusyan.dmt.task.base.BaseTask;
+import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 代理检测任务
+ *
+ * @author octopus_yan
+ */
+@Slf4j
+public class ProxyCheckTask extends BaseTask {
+ private final String checkUrl;
+
+ public ProxyCheckTask(String checkUrl) {
+ super(STR."ProxyCheck[\{checkUrl}]");
+ this.checkUrl = checkUrl;
+ this.updateProgress(0d, 1d);
+ }
+
+ @Override
+ protected void task() throws Exception {
+ String response = HttpUtil.getInstance().get(checkUrl, null, null);
+ log.debug(STR."Proxy check response result => \n\{response}");
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/TranslateTask.java b/src/main/java/cn/octopusyan/dmt/task/TranslateTask.java
new file mode 100644
index 0000000..9c820aa
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/TranslateTask.java
@@ -0,0 +1,91 @@
+package cn.octopusyan.dmt.task;
+
+import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.task.base.BaseTask;
+import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
+import cn.octopusyan.dmt.translate.DelayWord;
+import cn.octopusyan.dmt.translate.TranslateUtil;
+import cn.octopusyan.dmt.view.ConsoleLog;
+import javafx.application.Platform;
+import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.DelayQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 翻译工具
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public class TranslateTask extends BaseTask {
+
+ public static final ConsoleLog consoleLog = ConsoleLog.getInstance(TranslateTask.class);
+ @Getter
+ private final ThreadPoolManager threadPoolManager = ThreadPoolManager.getInstance("translate-pool");
+ private final DelayQueue delayQueue;
+ private final long total;
+ private final AtomicLong quantity = new AtomicLong();
+ private final CountDownLatch countDownLatch;
+
+ public TranslateTask(List data) {
+ this(TranslateUtil.getDelayQueue(data), data.size());
+ }
+
+ public TranslateTask(DelayQueue queue, int total) {
+ super("Translate");
+ this.delayQueue = queue;
+ this.total = total;
+ this.quantity.set(total - delayQueue.size());
+ countDownLatch = new CountDownLatch(queue.size());
+
+ updateProgress(quantity.get(), total);
+ }
+
+ @Override
+ protected void task() throws Exception {
+
+ while (!delayQueue.isEmpty() && !isCancelled()) {
+ // 取出文本
+ DelayWord word = delayQueue.take();
+
+ // 多线程处理
+ threadPoolManager.execute(() -> {
+ // 翻译
+ try {
+ if (total == 1) {
+ consoleLog.info("正在翻译:{}", word.getWord().getOriginal());
+ }
+
+ String translate = TranslateUtil.translate(word.getApi(), word.getWord().getOriginal());
+ // 回调监听器
+ if (StringUtils.isEmpty(translate)) return;
+
+ synchronized (quantity) {
+ long progress = quantity.addAndGet(1);
+ // 设置翻译结果
+ Platform.runLater(() -> word.getWord().getChineseProperty().setValue(translate));
+ // 更新进度
+ updateProgress(progress, total);
+ // 输出信息
+ if (total != 1) {
+ consoleLog.info("正在翻译({}/{})", progress, total);
+ }
+ }
+
+ } catch (Exception e) {
+ if (!(e instanceof InterruptedException) || !isCancelled()) {
+ consoleLog.error("翻译失败", e);
+ }
+ delayQueue.add(word);
+ }
+ countDownLatch.countDown();
+ });
+ }
+
+ countDownLatch.await();
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/UnpackTask.java b/src/main/java/cn/octopusyan/dmt/task/UnpackTask.java
new file mode 100644
index 0000000..d85fd9d
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/UnpackTask.java
@@ -0,0 +1,63 @@
+package cn.octopusyan.dmt.task;
+
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.task.base.BaseTask;
+import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
+import cn.octopusyan.dmt.utils.PBOUtil;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * 解包PBO文件任务
+ *
+ * @author octopus_yan
+ */
+public class UnpackTask extends BaseTask {
+
+ private final File pboFile;
+
+ public UnpackTask(File pboFile) {
+ super("Unpack " + pboFile.getName());
+ this.pboFile = pboFile;
+ }
+
+ @Override
+ protected void task() throws Exception {
+ // 解包
+ String path = PBOUtil.unpack(pboFile);
+ if (listener != null)
+ listener.onUnpackOver(path);
+
+ List wordItems = PBOUtil.findWord(path);
+ if (listener != null)
+ listener.onFindWordOver(wordItems);
+ }
+
+ /**
+ * 解包监听
+ *
+ * @author octopus_yan
+ */
+ public abstract static class UnpackListener extends DefaultTaskListener {
+
+ @Override
+ protected void onSucceed() {
+
+ }
+
+ /**
+ * 解包完成
+ *
+ * @param path 输出路径
+ */
+ public abstract void onUnpackOver(String path);
+
+ /**
+ * 查找可翻译文本
+ *
+ * @param wordItems 可翻译文本列表
+ */
+ public abstract void onFindWordOver(List wordItems);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java b/src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java
new file mode 100644
index 0000000..85affc2
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java
@@ -0,0 +1,55 @@
+package cn.octopusyan.dmt.task;
+
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.common.manager.http.HttpUtil;
+import cn.octopusyan.dmt.common.util.JsonUtil;
+import cn.octopusyan.dmt.model.UpgradeConfig;
+import cn.octopusyan.dmt.task.base.BaseTask;
+import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 检查更新任务
+ *
+ * @author octopus_yan
+ */
+public class UpgradeTask extends BaseTask {
+
+ private final UpgradeConfig upgradeConfig = ConfigManager.upgradeConfig();
+
+ protected UpgradeTask() {
+ super("Check Update");
+ }
+
+ @Override
+ protected void task() throws Exception {
+ String responseStr = HttpUtil.getInstance().get(upgradeConfig.getReleaseApi(), null, null);
+ JsonNode response = JsonUtil.parseJsonObject(responseStr);
+
+ // TODO 校验返回内容
+ String newVersion = response.get("tag_name").asText();
+
+ if (listener != null)
+ listener.onChecked(!StringUtils.equals(upgradeConfig.getVersion(), newVersion), newVersion);
+ }
+
+ /**
+ * 检查更新监听默认实现
+ *
+ * @author octopus_yan
+ */
+ public abstract static class UpgradeListener extends DefaultTaskListener {
+
+ public UpgradeListener() {
+ super(true);
+ }
+
+ public abstract void onChecked(boolean hasUpgrade, String version);
+
+ @Override
+ protected void onSucceed() {
+ // do nothing ...
+ }
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java b/src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java
new file mode 100644
index 0000000..893917f
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java
@@ -0,0 +1,50 @@
+package cn.octopusyan.dmt.task.base;
+
+import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
+import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
+import javafx.concurrent.Task;
+import lombok.Getter;
+
+/**
+ * @author octopus_yan
+ */
+public abstract class BaseTask extends Task {
+ private final ThreadPoolManager Executor = ThreadPoolManager.getInstance("task-pool");
+ protected T listener;
+ @Getter
+ private final String name;
+
+ protected BaseTask(String name) {
+ this.name = name;
+ }
+
+ public String getNameTag() {
+ return "Task " + name;
+ }
+
+ @Override
+ protected Void call() throws Exception {
+ if (listener != null) listener.onStart();
+ task();
+ return null;
+ }
+
+ protected abstract void task() throws Exception;
+
+ public void onListen(T listener) {
+ this.listener = listener;
+ if (this.listener == null)
+ return;
+ if (listener instanceof DefaultTaskListener lis)
+ lis.setTask((BaseTask) this);
+
+ setOnRunning(_ -> listener.onRunning());
+ setOnCancelled(_ -> listener.onCancelled());
+ setOnFailed(_ -> listener.onFailed(getException()));
+ setOnSucceeded(_ -> listener.onSucceeded());
+ }
+
+ public void execute() {
+ Executor.execute(this);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/base/Listener.java b/src/main/java/cn/octopusyan/dmt/task/base/Listener.java
new file mode 100644
index 0000000..d2b2d2b
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/base/Listener.java
@@ -0,0 +1,23 @@
+package cn.octopusyan.dmt.task.base;
+
+/**
+ * 任务监听
+ *
+ * @author octopus_yan
+ */
+public interface Listener {
+
+ default void onStart() {
+ }
+
+ default void onRunning() {
+ }
+
+ default void onCancelled() {
+ }
+
+ default void onFailed(Throwable throwable) {
+ }
+
+ void onSucceeded();
+}
diff --git a/src/main/java/cn/octopusyan/dmt/task/listener/DefaultTaskListener.java b/src/main/java/cn/octopusyan/dmt/task/listener/DefaultTaskListener.java
new file mode 100644
index 0000000..795c4fc
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/task/listener/DefaultTaskListener.java
@@ -0,0 +1,78 @@
+package cn.octopusyan.dmt.task.listener;
+
+import cn.octopusyan.dmt.task.base.BaseTask;
+import cn.octopusyan.dmt.task.base.Listener;
+import cn.octopusyan.dmt.view.ConsoleLog;
+import cn.octopusyan.dmt.view.alert.AlertUtil;
+import cn.octopusyan.dmt.view.alert.builder.ProgressBuilder;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 任务监听器默认实现
+ *
+ * @author octopus_yan
+ */
+@Slf4j
+public abstract class DefaultTaskListener implements Listener {
+ private ConsoleLog consoleLog;
+ @Getter
+ private BaseTask extends DefaultTaskListener> task;
+ /**
+ * 加载弹窗
+ */
+ @Getter
+ final ProgressBuilder progress = AlertUtil.progress();
+
+ /**
+ * 是否展示加载弹窗
+ */
+ private final boolean showProgress;
+
+ public DefaultTaskListener() {
+ this(false);
+ }
+
+ public DefaultTaskListener(boolean showProgress) {
+ this.showProgress = showProgress;
+ }
+
+ public void setTask(BaseTask task) {
+ this.task = task;
+ consoleLog = ConsoleLog.getInstance(task.getClass().getSimpleName());
+ progress.onCancel(task::cancel);
+ }
+
+ @Override
+ public void onStart() {
+ consoleLog.info(STR."\{task.getNameTag()} start ...");
+ }
+
+ @Override
+ public void onRunning() {
+ // 展示加载弹窗
+ if (showProgress)
+ progress.show();
+ }
+
+ @Override
+ public void onCancelled() {
+ progress.close();
+ consoleLog.info(STR."\{task.getNameTag()} cancel ...");
+ }
+
+ @Override
+ public void onFailed(Throwable throwable) {
+ progress.close();
+ consoleLog.error(STR."\{task.getNameTag()} fail ...", throwable);
+ }
+
+ @Override
+ public void onSucceeded() {
+ progress.close();
+ consoleLog.info(STR."\{task.getNameTag()} success ...");
+ onSucceed();
+ }
+
+ protected abstract void onSucceed();
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/ApiKey.java b/src/main/java/cn/octopusyan/dmt/translate/ApiKey.java
new file mode 100644
index 0000000..e7dc838
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/ApiKey.java
@@ -0,0 +1,13 @@
+package cn.octopusyan.dmt.translate;
+
+import lombok.Getter;
+
+/**
+ * API 密钥配置
+ *
+ * @author octopus_yan@foxmail.com
+ */
+@Getter
+public record ApiKey(String appid, String apiKey) {
+
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/DelayWord.java b/src/main/java/cn/octopusyan/dmt/translate/DelayWord.java
new file mode 100644
index 0000000..a6f3abf
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/DelayWord.java
@@ -0,0 +1,39 @@
+package cn.octopusyan.dmt.translate;
+
+import cn.octopusyan.dmt.model.WordItem;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.concurrent.Delayed;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 延迟翻译对象
+ *
+ * @author octopus_yan
+ */
+@Getter
+public class DelayWord implements Delayed {
+ @Setter
+ private TranslateApi api;
+ private final WordItem word;
+ private long delayTime;
+
+ public DelayWord(WordItem word) {
+ this.word = word;
+ }
+
+ public void setDelayTime(long time, TimeUnit timeUnit) {
+ this.delayTime = System.currentTimeMillis() + (time > 0 ? TimeUnit.MILLISECONDS.convert(time, timeUnit) : 0);
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public int compareTo(Delayed o) {
+ return Long.compare(this.delayTime, ((DelayWord) o).delayTime);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java b/src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java
new file mode 100644
index 0000000..f30309f
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java
@@ -0,0 +1,60 @@
+package cn.octopusyan.dmt.translate;
+
+import cn.octopusyan.dmt.model.Translate;
+import lombok.Getter;
+
+/**
+ * 翻译引擎类型
+ *
+ * @author octopus_yan@foxmail.com
+ */
+@Getter
+public enum TranslateApi {
+ FREE_BAIDU("free_baidu", "百度", false),
+ FREE_GOOGLE("free_google", "谷歌", false),
+ BAIDU("baidu", "百度(需认证)", true),
+ ;
+
+ @Getter
+ private final String name;
+ @Getter
+ private final String label;
+ private final boolean needApiKey;
+ private final Integer defaultQps;
+
+ TranslateApi(String name, String label, boolean needApiKey) {
+ // 设置接口默认qps=10
+ this(name, label, needApiKey, 10);
+ }
+
+ TranslateApi(String name, String label, boolean needApiKey, int defaultQps) {
+ this.name = name;
+ this.label = label;
+ this.needApiKey = needApiKey;
+ this.defaultQps = defaultQps;
+ }
+
+ public boolean needApiKey() {
+ return needApiKey;
+ }
+
+ public Translate.Config translate() {
+ return new Translate.Config("", "", defaultQps);
+ }
+
+ public static TranslateApi get(String name) {
+ for (TranslateApi value : values()) {
+ if (value.getName().equals(name))
+ return value;
+ }
+ throw new RuntimeException("类型不存在");
+ }
+
+ public static TranslateApi getByLabel(String label) {
+ for (TranslateApi value : values()) {
+ if (value.getLabel().equals(label))
+ return value;
+ }
+ throw new RuntimeException("类型不存在");
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java b/src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java
new file mode 100644
index 0000000..4b8cfb2
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java
@@ -0,0 +1,66 @@
+package cn.octopusyan.dmt.translate;
+
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.translate.factory.TranslateFactory;
+import cn.octopusyan.dmt.translate.factory.TranslateFactoryImpl;
+
+import java.util.List;
+import java.util.concurrent.DelayQueue;
+
+/**
+ * 翻译
+ *
+ * @author octopus_yan
+ */
+public class TranslateUtil {
+ private static final TranslateFactory factory = TranslateFactoryImpl.getInstance();
+
+ /**
+ * 翻译(英->中)
+ * TODO 切换语种
+ *
+ * @param api 翻译源
+ * @param sourceString 原始文本
+ * @return 翻译结果
+ * @throws Exception 翻译出错
+ */
+ public static String translate(TranslateApi api, String sourceString) throws Exception {
+ return factory.translate(api, sourceString);
+ }
+
+ public static String translate(String sourceString) throws Exception {
+ return factory.translate(ConfigManager.translateApi(), sourceString);
+ }
+
+ /**
+ * 获取延迟翻译对象
+ *
+ * @param words 待翻译文本列表
+ * @return 延迟对象
+ */
+ public static DelayQueue getDelayQueue(List words) {
+ return getDelayQueue(ConfigManager.translateApi(), words);
+ }
+
+ /**
+ * 获取延迟翻译对象
+ *
+ * @param source 翻译接口
+ * @param words 待翻译文本列表
+ * @return 延迟对象
+ */
+ public static DelayQueue getDelayQueue(TranslateApi source, List words) {
+ return factory.getDelayQueue(source, words);
+ }
+
+ /**
+ * 重设延迟时间
+ *
+ * @param index 序列号
+ * @param delayWord 延迟对象
+ */
+ public static void resetDelayTime(int index, DelayWord delayWord) {
+ factory.resetDelayTime(ConfigManager.translateApi(), index, delayWord);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/factory/TranslateFactory.java b/src/main/java/cn/octopusyan/dmt/translate/factory/TranslateFactory.java
new file mode 100644
index 0000000..596a987
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/factory/TranslateFactory.java
@@ -0,0 +1,44 @@
+package cn.octopusyan.dmt.translate.factory;
+
+
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.translate.DelayWord;
+import cn.octopusyan.dmt.translate.TranslateApi;
+
+import java.util.List;
+import java.util.concurrent.DelayQueue;
+
+/**
+ * 翻译器接口
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public interface TranslateFactory {
+ /**
+ * 翻译处理
+ *
+ * @param api 翻译源
+ * @param sourceString 原始文本
+ * @return 翻译结果
+ * @throws Exception 翻译出错
+ */
+ String translate(TranslateApi api, String sourceString) throws Exception;
+
+ /**
+ * 获取延迟翻译对象
+ *
+ * @param api 翻译源
+ * @param word 待翻译文本对象
+ * @return 延迟翻译对象
+ */
+ DelayQueue getDelayQueue(TranslateApi api, List word);
+
+ /**
+ * 重设延迟时间
+ *
+ * @param api 翻译接口
+ * @param index 序列号
+ * @param delayWord 延迟对象
+ */
+ void resetDelayTime(TranslateApi api, int index, DelayWord delayWord);
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/factory/TranslateFactoryImpl.java b/src/main/java/cn/octopusyan/dmt/translate/factory/TranslateFactoryImpl.java
new file mode 100644
index 0000000..9951c64
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/factory/TranslateFactoryImpl.java
@@ -0,0 +1,95 @@
+package cn.octopusyan.dmt.translate.factory;
+
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.translate.DelayWord;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import cn.octopusyan.dmt.translate.processor.AbstractTranslateProcessor;
+import cn.octopusyan.dmt.translate.processor.BaiduTranslateProcessor;
+import cn.octopusyan.dmt.translate.processor.FreeBaiduTranslateProcessor;
+import cn.octopusyan.dmt.translate.processor.FreeGoogleTranslateProcessor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.*;
+import java.util.concurrent.DelayQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 翻译处理器
+ *
+ * @author octopus_yan@foxmail.com
+ */
+@Slf4j
+public class TranslateFactoryImpl implements TranslateFactory {
+ private static TranslateFactoryImpl impl;
+ private final Map processorMap = new HashMap<>();
+ private final List processorList = new ArrayList<>();
+
+ private TranslateFactoryImpl() {
+ }
+
+ public static synchronized TranslateFactoryImpl getInstance() {
+ if (impl == null) {
+ impl = new TranslateFactoryImpl();
+ impl.initProcessor();
+ }
+ return impl;
+ }
+
+ private void initProcessor() {
+ processorList.addAll(Arrays.asList(
+ new FreeGoogleTranslateProcessor(),
+ new FreeBaiduTranslateProcessor(),
+ new BaiduTranslateProcessor()
+ ));
+ for (AbstractTranslateProcessor processor : processorList) {
+ processorMap.put(processor.getSource(), processor);
+ }
+ }
+
+ private AbstractTranslateProcessor getProcessor(TranslateApi api) {
+ return processorMap.get(api.getName());
+ }
+
+ @Override
+ public DelayQueue getDelayQueue(TranslateApi api, List words) {
+ var queue = new DelayQueue();
+
+ // 设置翻译延迟
+ AbstractTranslateProcessor processor = getProcessor(api);
+ for (int i = 0; i < words.size(); i++) {
+ // 翻译对象
+ DelayWord delayWord = new DelayWord(words.get(i));
+
+ // 设置翻译源
+ delayWord.setApi(api);
+ long time = 1000L / processor.qps();
+ delayWord.setDelayTime(time * (i + 1), TimeUnit.MILLISECONDS);
+
+ queue.add(delayWord);
+ }
+ return queue;
+ }
+
+ @Override
+ public void resetDelayTime(TranslateApi api, int index, DelayWord delayWord) {
+ AbstractTranslateProcessor processor = getProcessor(api);
+ // 设置翻译源
+ delayWord.setApi(api);
+ long time = 1000L / processor.qps();
+ delayWord.setDelayTime(time * (index + 1), TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * 翻译(英->中)
+ * TODO 切换语种
+ *
+ * @param api 翻译源
+ * @param sourceString 原始文本
+ * @return 翻译结果
+ * @throws Exception 翻译出错
+ */
+ @Override
+ public String translate(TranslateApi api, String sourceString) throws Exception {
+ return getProcessor(api).translate(sourceString);
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/translate/processor/AbstractTranslateProcessor.java b/src/main/java/cn/octopusyan/dmt/translate/processor/AbstractTranslateProcessor.java
new file mode 100644
index 0000000..ce138c2
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/processor/AbstractTranslateProcessor.java
@@ -0,0 +1,81 @@
+package cn.octopusyan.dmt.translate.processor;
+
+import cn.octopusyan.dmt.common.manager.ConfigManager;
+import cn.octopusyan.dmt.common.manager.http.HttpUtil;
+import cn.octopusyan.dmt.translate.ApiKey;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 翻译处理器抽象类
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public abstract class AbstractTranslateProcessor implements TranslateProcessor {
+ protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+ protected static final HttpUtil httpUtil = HttpUtil.getInstance();
+ protected final TranslateApi translateApi;
+
+ public AbstractTranslateProcessor(TranslateApi translateApi) {
+ this.translateApi = translateApi;
+ }
+
+ public String getSource() {
+ return translateApi.getName();
+ }
+
+ public TranslateApi source() {
+ return translateApi;
+ }
+
+ @Override
+ public boolean needApiKey() {
+ return source().needApiKey();
+ }
+
+ @Override
+ public boolean configuredKey() {
+ return ConfigManager.hasTranslateApiKey(source());
+ }
+
+ @Override
+ public int qps() {
+ return ConfigManager.translateQps(source());
+ }
+
+ /**
+ * 获取Api配置信息
+ */
+ protected ApiKey getApiKey() {
+ if (!configuredKey()) {
+ String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
+ logger.error(message);
+ throw new RuntimeException(message);
+ }
+
+ String appid = ConfigManager.translateAppid(source());
+ String apikey = ConfigManager.translateApikey(source());
+ return new ApiKey(appid, apikey);
+ }
+
+ @Override
+ public String translate(String original) throws Exception {
+
+ if (needApiKey() && !configuredKey()) {
+ String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
+ logger.error(message);
+ throw new RuntimeException(message);
+ }
+
+ return customTranslate(original);
+ }
+
+ /**
+ * 翻译处理
+ *
+ * @param source 原始文本
+ * @return 翻译结果
+ */
+ public abstract String customTranslate(String source) throws Exception;
+}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/BaiduTranslateProcessor.java b/src/main/java/cn/octopusyan/dmt/translate/processor/BaiduTranslateProcessor.java
similarity index 72%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/BaiduTranslateProcessor.java
rename to src/main/java/cn/octopusyan/dmt/translate/processor/BaiduTranslateProcessor.java
index 2b1fd76..a3e1b18 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/BaiduTranslateProcessor.java
+++ b/src/main/java/cn/octopusyan/dmt/translate/processor/BaiduTranslateProcessor.java
@@ -1,13 +1,15 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
+package cn.octopusyan.dmt.translate.processor;
-import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONObject;
+import cn.octopusyan.dmt.common.util.JsonUtil;
+import cn.octopusyan.dmt.translate.ApiKey;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
/**
@@ -19,8 +21,8 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
private ApiKey apiKey;
- public BaiduTranslateProcessor(TranslateSource translateSource) {
- super(translateSource);
+ public BaiduTranslateProcessor() {
+ super(TranslateApi.BAIDU);
}
@Override
@@ -37,10 +39,10 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
apiKey = getApiKey();
- String appid = apiKey.getAppid();
+ String appid = apiKey.appid();
String salt = UUID.randomUUID().toString().replace("-", "");
- JSONObject param = new JSONObject();
+ Map param = new HashMap<>();
param.put("q", source);
param.put("from", "auto");
param.put("to", "zh");
@@ -48,20 +50,20 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
param.put("salt", salt);
param.put("sign", getSign(appid, source, salt));
- String resp = httpUtil.get(url(), null, param);
- JSONObject jsonObject = JSON.parseObject(resp);
+ String resp = httpUtil.get(url(), null, JsonUtil.parseJsonObject(param));
+ JsonNode json = JsonUtil.parseJsonObject(resp);
- if (!jsonObject.containsKey("trans_result")) {
- Object errorMsg = jsonObject.get("error_msg");
+ if (!json.has("trans_result")) {
+ Object errorMsg = json.get("error_msg");
logger.error("翻译失败: {}", errorMsg);
- throw new RuntimeException("翻译失败: " + errorMsg);
+ throw new RuntimeException(String.valueOf(errorMsg));
}
- return jsonObject.getJSONArray("trans_result").getJSONObject(0).getString("dst");
+ return json.get("trans_result").get(0).get("dst").asText();
}
private String getSign(String appid, String q, String salt) {
- return encrypt2ToMD5(appid + q + salt + apiKey.getApiKey());
+ return encrypt2ToMD5(appid + q + salt + apiKey.apiKey());
}
/**
diff --git a/src/main/java/cn/octopusyan/dmt/translate/processor/FreeBaiduTranslateProcessor.java b/src/main/java/cn/octopusyan/dmt/translate/processor/FreeBaiduTranslateProcessor.java
new file mode 100644
index 0000000..1e140fd
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/translate/processor/FreeBaiduTranslateProcessor.java
@@ -0,0 +1,107 @@
+package cn.octopusyan.dmt.translate.processor;
+
+import cn.octopusyan.dmt.common.manager.http.CookieManager;
+import cn.octopusyan.dmt.common.util.JsonUtil;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.io.IOException;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 谷歌 免费翻译接口
+ *
+ * @author octopus_yan@foxmail.com
+ */
+public class FreeBaiduTranslateProcessor extends AbstractTranslateProcessor {
+ private static final Map header = new HashMap<>();
+
+ static {
+ header.put("Origin", "https://fanyi.baidu.com");
+ header.put("Referer", "https://fanyi.baidu.com");
+ header.put("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
+ header.put("Accept","*/*");
+ }
+
+ public FreeBaiduTranslateProcessor() {
+ super(TranslateApi.FREE_BAIDU);
+ }
+
+ @Override
+ public String url() {
+ return "https://fanyi.baidu.com/transapi";
+ }
+
+ @Override
+ public int qps() {
+ return source().getDefaultQps();
+ }
+
+ /**
+ * 翻译处理
+ *
+ * @param source 待翻译单词
+ * @return 翻译结果
+ */
+ @Override
+ public String customTranslate(String source) throws IOException, InterruptedException {
+
+ Map param = new HashMap<>();
+ param.put("query", source);
+ param.put("from", "auto");
+ param.put("to", "zh");
+ param.put("source", "txt");
+
+ String resp = httpUtil.post(url(), JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(param));
+ JsonNode json = JsonUtil.parseJsonObject(resp);
+
+ if (!json.has("data")) {
+ String errorMsg = json.get("errmsg").asText();
+ if("访问出现异常,请刷新后重试!".equals(errorMsg) && !header.containsKey("Cookie")) {
+ checkCookie();
+ return customTranslate(source);
+ }
+ throw new RuntimeException(errorMsg);
+ }
+
+ return json.get("data").get(0).get("dst").asText();
+ }
+
+ private void checkCookie() throws IOException, InterruptedException {
+ // 短时大量请求会被ban,需要添加验证cookie
+
+ if (header.containsKey("Cookie")) return;
+
+ List cookieList = CookieManager.getStore().get(URI.create("https://baidu.com"));
+ boolean noneMatch = cookieList.stream()
+ .filter(cookie -> "ab_sr".equals(cookie.getName()) || "BAIDUID".equals(cookie.getName()))
+ .count() < 2;
+
+ if (noneMatch) {
+ String url = "https://miao.baidu.com/abdr?_o=https%3A%2F%2Ffanyi.baidu.com";
+ Map param = new HashMap<>();
+ param.put("data", miao_data);
+ param.put("key_id", "6e75c85adea0454a");
+ param.put("enc", 2);
+ httpUtil.post(url, JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(param));
+ }
+
+ cookieList = CookieManager.getStore().get(URI.create("https://baidu.com"));
+ List cookies = cookieList.stream()
+ .filter(cookie -> "ab_sr".equals(cookie.getName()) || "BAIDUID".equals(cookie.getName()))
+ .toList();
+
+ String collect = cookies.stream()
+ .map(cookie -> cookie.getName() + "=" + cookie.getValue())
+ .collect(Collectors.joining(";"));
+
+ header.put("Cookie", collect);
+ }
+
+ private static final String miao_data = "+wPJcl395nhVS+Fg/zTBIkBognAAK5IQi6wkGt3z1jk1jMuJOn+IkhaL1M7GHYxq/0V6+zKXaEVRs77WDfP/fDS0suNzgl3NdbhGSoHJEJk4fVMeU0pZxrgFq7Bzch6Aehw7MD6WlUDfyHWMarBN9OxH1UiW6Dn38uUkBjpsTDZYeNWOlmwy8YKq3FlRZiWzGYaP7K3DfBwWbT059D0KT2/mGUrHVgPVAkU572qNLzTxFyFneYTmPDQC0j+fl6+kxhA460WIG5aL19v4Gx+T7YJwIGe07QdhlQuy8Q6bpAIb8lj1SGjX9kS8P8GtI7TQv7gOlZ8ShUgV7941JXbIsJZgoWHFNq5Xge6XcVOUJsSQzAKsAgIlhgnlfC9IBAGK85KzeDC7JoBi4t0BXKgKUjYM9Veh7cA3yfudWKjSpyN6mv97apGR4Vl+8wbugPSCQRQlxPDunGUTGionf4hKxNZJYz62dwm0E/pP21PGPwnihDi7mQNxh40aE7IG0kORudLChvrcj85n4U/Rxkoq3TXTnSK1YAc4Pi/dIuR5HSUZyUBEqlvldU3jXqgUlfqeqjkn5Ug9gS6IqYEHE6FZdAGw/3y247pojWs2L56RSrUc6LoyQ7P9QNPOBr/v9HngHLRnFovrrZ57KmMl2hCiX5r1Q473CyjBhTjix3gQIFCwmM6rmEPIIU+cr0lvQrdhjxsiETk2sd17dR9gR2EjbNjeTby9QyHJTima5T6lk0K0orADrF2lNA/Xo2Iv/cMFXkc1ss7a19UQTdMLmiKgWdllWxH8Yp8B5o4KrLh+pMCGy6NeFn6FuWeAQgY8EB1r9H3PuenKaQ02VckkUPN1h1szjDI7otIFHYnQFJrp4vy4aM8wwcQS/+R2Aw0FwA5e2IWcZSwoTIc1i9YIeKgUpiConxhqZeKIhUEz33qExW0IS9mMs558JbAYsUG8KcMFtyqVo2sgtXteMAXMB9yX5/huZMR9HOO0v8x4KCZ+hwZczoVe3AmmUZLq1+rY9ngCp5D/3CcElcH3SU9HmlVXZ2VvLpa9wGIRP1UAHFuTxwVLvxJP07fbR31rLkoC22DbemRshBnv4EbIP/mkO8l/P1Y6RGEkS3b7b2MoBIWh4atrqOmUCG6w1KwESSi4Y5VKpdEkU/gL4ea7kr/GRJVWomSKSDqUEuloUI4hfLpmJs4h1JZTotOBcVjarOIW5d3j2Gm6R8X6vk24sUukOR1QOzP1gkoUaOYR+6Jcl+20IQZtIFMwFeMYtKrB5HquwYgHmUvO5hdxuFRwXosegVJmo+9MxkYjHTJbAgqZfKMc8kRjr64YBhwE30P+KLw+TafbgrA3trZQCyN3u5O5/2rk3GJmbhcDhZlYrYepj4Rymw8FB6LF0PkZZzRtWtQWv2gnMZuLmo7i5flL/MbiUMXKnsy3+Z6HHKNOqUzGUOZx84xizLkxwVMzGHrtSOub4wIhSWr7/z9GNa9qJuNJYsPXIE8wn245mHaheCm+MCxjIJMKdb72PBbQFF517kV+BxsNrxbFGe6ffJ/03KhXg0jJ1lAQc07obvOkdGVRr8DHTiS3IBfN7ofZMjVhXXpH2kUtepLfWvQf053yH2VQIEq62uxZP+RfpmdiBePEXQRBk5EA+178YbyavPE6yeu8zlub6GGlvuvrtnQmPUyvmO1hebiQK8B/V5TXxB+7IhPQ8okEll6bldVCoIxkbyAIBTK7rrDhD53HJ9capFgh/Q0XHMchYSYielT+Et8UIqUmVO94K5yl/nQhO1Vc2FsHkp2JWDDMQTuZyfmv+FS5OTeu39UznwXD36kyr2HvBEVoCMBqJrhI9WnEng53aEtcWQk4ewS+HllIoBgeJgexfs3MQK0mMP+6qV/idvB3S8Kzt/+SZsA0cHGvPvQzdePCRGNgCFOHPcpHPW/Yz4bwo4dzM64CYnL17Z4/fwcodMBG+KqG6ZyXe7EBby5RUE5OnhAGdR8T6WlIjNlU5TAIPbZ32i5nugWAcYJWJKsuITjAl4sRYY6Tpj19yIdHX2CDnU/O4arL8ijniydgcIEyYJbSCFmeJwyIv+ZSrB3RW0fYuAs4Q3AbeSK5TA9EICpB2w22kgditKO+uvrxw2LzGGND5C684YZq+XGmocAfpcte6+1PFs4NwuN41lOxDry8dwXr2mUIugcpnzsoF5Wgwyen0ISb2qaXODTvd/T5HEJcmpZdUJ1c+g+nEPi2OyX+MBXR";
+}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/FreeGoogleTranslateProcessor.java b/src/main/java/cn/octopusyan/dmt/translate/processor/FreeGoogleTranslateProcessor.java
similarity index 53%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/FreeGoogleTranslateProcessor.java
rename to src/main/java/cn/octopusyan/dmt/translate/processor/FreeGoogleTranslateProcessor.java
index 66c9bf4..6604af9 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/FreeGoogleTranslateProcessor.java
+++ b/src/main/java/cn/octopusyan/dmt/translate/processor/FreeGoogleTranslateProcessor.java
@@ -1,10 +1,13 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
+package cn.octopusyan.dmt.translate.processor;
-import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
-import com.alibaba.fastjson2.JSONArray;
-import com.alibaba.fastjson2.JSONObject;
+
+import cn.octopusyan.dmt.common.util.JsonUtil;
+import cn.octopusyan.dmt.translate.TranslateApi;
+import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
/**
* 谷歌 免费翻译接口
@@ -13,8 +16,8 @@ import java.io.IOException;
*/
public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
- public FreeGoogleTranslateProcessor(TranslateSource translateSource) {
- super(translateSource);
+ public FreeGoogleTranslateProcessor() {
+ super(TranslateApi.FREE_GOOGLE);
}
@Override
@@ -22,6 +25,11 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
return "https://translate.googleapis.com/translate_a/single";
}
+ @Override
+ public int qps() {
+ return source().getDefaultQps();
+ }
+
/**
* 翻译处理
*
@@ -31,21 +39,22 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
- JSONObject form = new JSONObject();
+ Map form = new HashMap<>();
form.put("client", "gtx");
form.put("dt", "t");
form.put("sl", "auto");
form.put("tl", "zh-CN");
form.put("q", source);
- JSONObject header = new JSONObject();
+ Map header = new HashMap<>();
StringBuilder retStr = new StringBuilder();
// TODO 短时大量请求会被ban,需要浏览器验证添加cookie
- String resp = httpUtil.get(url(), header, form);
- JSONArray jsonObject = JSONArray.parseArray(resp);
- for (Object o : jsonObject.getJSONArray(0)) {
- JSONArray a = (JSONArray) o;
- retStr.append(a.getString(0));
+
+ String resp = httpUtil.get(url(), JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(form));
+ JsonNode json = JsonUtil.parseJsonObject(resp);
+
+ for (JsonNode o : json.get(0)) {
+ retStr.append(o.get(0).asText());
}
return retStr.toString();
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/TranslateProcessor.java b/src/main/java/cn/octopusyan/dmt/translate/processor/TranslateProcessor.java
similarity index 76%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/TranslateProcessor.java
rename to src/main/java/cn/octopusyan/dmt/translate/processor/TranslateProcessor.java
index 74b87d8..8a01cf1 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/TranslateProcessor.java
+++ b/src/main/java/cn/octopusyan/dmt/translate/processor/TranslateProcessor.java
@@ -1,4 +1,4 @@
-package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
+package cn.octopusyan.dmt.translate.processor;
/**
* 翻译处理器
@@ -6,6 +6,7 @@ package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
* @author octopus_yan@foxmail.com
*/
public interface TranslateProcessor {
+
/**
* 翻译源 api接口地址
*/
@@ -29,9 +30,9 @@ public interface TranslateProcessor {
/**
* 翻译
*
- * @param source 原始文本
+ * @param original 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
- String translate(String source) throws Exception;
+ String translate(String original) throws Exception;
}
diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/FileUtil.java b/src/main/java/cn/octopusyan/dmt/utils/FileUtil.java
similarity index 74%
rename from src/main/java/cn/octopusyan/dayzmodtranslator/util/FileUtil.java
rename to src/main/java/cn/octopusyan/dmt/utils/FileUtil.java
index f507546..68508ec 100644
--- a/src/main/java/cn/octopusyan/dayzmodtranslator/util/FileUtil.java
+++ b/src/main/java/cn/octopusyan/dmt/utils/FileUtil.java
@@ -1,4 +1,4 @@
-package cn.octopusyan.dayzmodtranslator.util;
+package cn.octopusyan.dmt.utils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.CanReadFileFilter;
@@ -6,11 +6,9 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.*;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -96,8 +94,24 @@ public class FileUtil {
return fileName.substring(0, fileName.lastIndexOf("."));
}
+ public static String getMimeType(Path path) {
+ try {
+ return Files.probeContentType(path);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static Collection listFile(File file) {
+ return FileUtils.listFiles(file, CanReadFileFilter.CAN_READ, null);
+ }
+
public static List listFileNames(String path) {
- Collection files = FileUtils.listFiles(new File(path), CanReadFileFilter.CAN_READ, null);
+ return listFileNames(new File(path));
+ }
+
+ public static List listFileNames(File file) {
+ Collection files = listFile(file);
return files.stream().map(File::getName).collect(Collectors.toList());
}
@@ -132,4 +146,36 @@ public class FileUtil {
}
return path;
}
+
+ /**
+ * 判断文件的编码格式
+ *
+ * @return 文件编码格式
+ */
+ public static String getCharsets(File file) {
+ try (InputStream in = new FileInputStream(file)) {
+ int p = (in.read() << 8) + in.read();
+ String code = "GBK";
+
+ switch (p) {
+ case 59524:
+ code = "UTF-8";
+ break;
+ case 0xfffe:
+ code = "Unicode";
+ break;
+ case 0xfeff:
+ code = "UTF-16BE";
+ break;
+ case 48581:
+ code = "GBK";
+ break;
+ default:
+ }
+ return code;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
}
diff --git a/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java b/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
new file mode 100644
index 0000000..a283305
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
@@ -0,0 +1,407 @@
+package cn.octopusyan.dmt.utils;
+
+import cn.octopusyan.dmt.common.config.Constants;
+import cn.octopusyan.dmt.common.util.ProcessesUtil;
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.view.ConsoleLog;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.LineIterator;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * PBO 文件工具
+ *
+ * @author octopus_yan
+ */
+public class PBOUtil {
+ public static final ConsoleLog consoleLog = ConsoleLog.getInstance(PBOUtil.class);
+
+ private static final ProcessesUtil processesUtil = ProcessesUtil.init(Constants.BIN_DIR_PATH);
+
+ private static final String UNPACK_COMMAND = STR."\{Constants.PBOC_FILE} unpack -o \{Constants.TMP_DIR_PATH} %s";
+ private static final String PACK_COMMAND = STR."\{Constants.PBOC_FILE} pack -o %s %s";
+ private static final String CFG_COMMAND = STR."\{Constants.CFG_CONVERT_FILE} %s -dst %s %s";
+
+ private static final String FILE_NAME_STRING_TABLE = "stringtable.csv";
+ private static final String FILE_NAME_CONFIG_BIN = "config.bin";
+ private static final String FILE_NAME_CONFIG_CPP = "config.cpp";
+ private static final String[] FILE_NAME_LIST = new String[]{"csv", "bin", "cpp", "layout"};
+
+ private static final Pattern CPP_PATTERN = Pattern.compile(".*(displayName|descriptionShort) ?= ?\"(.*)\";.*");
+ private static final Pattern LAYOUT_PATTERN = Pattern.compile(".*text \"(.*)\".*");
+
+
+ public static void init() {
+
+ String srcFilePath = Objects.requireNonNull(PBOUtil.class.getResource("/bin")).getPath();
+ try {
+ File destDir = new File(Constants.BIN_DIR_PATH);
+ FileUtils.forceMkdir(destDir);
+ FileUtils.copyDirectory(new File(srcFilePath), destDir);
+ } catch (IOException e) {
+ consoleLog.error("Util 初始化失败", e);
+ }
+ }
+
+ /**
+ * 解包pbo文件
+ *
+ * @param path pbo文件地址
+ * @return 解包输出路径
+ */
+ public static String unpack(String path) {
+ return unpack(new File(path));
+ }
+
+ /**
+ * 解包pbo文件
+ *
+ * @param pboFile pbo文件
+ * @return 解包输出路径
+ */
+ public static String unpack(File pboFile) {
+ if (!pboFile.exists())
+ throw new RuntimeException("文件不存在!");
+
+ File directory = new File(Constants.TMP_DIR_PATH);
+ String outputPath = Constants.TMP_DIR_PATH + File.separator + FileUtil.mainName(pboFile);
+ try {
+ FileUtils.deleteQuietly(new File(outputPath));
+ FileUtils.forceMkdir(directory);
+ } catch (IOException e) {
+ throw new RuntimeException("文件夹创建失败", e);
+ }
+
+ String command = String.format(UNPACK_COMMAND, pboFile.getAbsolutePath());
+ consoleLog.debug(STR."unpack command ==> [\{command}]");
+ boolean exec = processesUtil.exec(command);
+ if (!exec)
+ throw new RuntimeException("解包失败!");
+
+ return outputPath;
+ }
+
+ /**
+ * 打包pbo文件
+ *
+ * @param unpackPath pbo解包文件路径
+ * @return 打包文件
+ */
+ public static File pack(String unpackPath) {
+ String outputPath = STR."\{unpackPath}.pbo";
+
+ // 打包文件临时保存路径
+ File packFile = new File(outputPath);
+ if (packFile.exists()) {
+ // 如果存在则删除
+ FileUtils.deleteQuietly(packFile);
+ }
+
+ String command = String.format(PACK_COMMAND, Constants.TMP_DIR_PATH, unpackPath);
+ consoleLog.debug(STR."pack command ==> [\{command}]");
+
+ boolean exec = processesUtil.exec(command);
+ if (!exec) throw new RuntimeException("打包失败!");
+
+ return packFile;
+ }
+
+ /**
+ * 查找可翻译文本
+ *
+ * @param path 根目录
+ */
+ public static List findWord(String path) {
+ return findWord(new File(path));
+ }
+
+ public static List findWord(File file) {
+ ArrayList wordItems = new ArrayList<>();
+ if (!file.exists())
+ return wordItems;
+
+ List files = new ArrayList<>(FileUtils.listFiles(file, FILE_NAME_LIST, true));
+ for (File item : files) {
+ wordItems.addAll(findWordByFile(item));
+ }
+
+ return wordItems;
+ }
+
+ /**
+ * 写入文件
+ *
+ * @param wordFileMap 文件对应文本map
+ */
+ public static void writeWords(Map> wordFileMap) {
+
+ for (Map.Entry> entry : wordFileMap.entrySet()) {
+
+ Map wordMap = entry.getValue().stream()
+ .collect(Collectors.toMap(WordItem::getLines, Function.identity()));
+
+ File file = entry.getKey();
+
+ // 需要转bin文件时,写入bak目录下cpp文件
+ boolean hasBin = new File(outFilePath(file, ".bin")).exists();
+ String writePath = file.getAbsolutePath().replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
+ File writeFile = hasBin ? file : new File(writePath);
+
+ AtomicInteger lineIndex = new AtomicInteger(0);
+ List lines = new ArrayList<>();
+
+ consoleLog.info("正在写入文件[{}]", writeFile.getAbsolutePath());
+
+ try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
+ while (it.hasNext()) {
+ String line = it.next();
+ WordItem word = wordMap.get(lineIndex.get());
+
+ // 当前行是否有需要替换的文本
+ // TODO 是否替换空文本
+ if (word != null && line.contains(word.getOriginal())) {
+ line = line.substring(0, word.getIndex()) +
+ line.substring(word.getIndex()).replace(word.getOriginal(), word.getChinese());
+ }
+
+ // 缓存行内容
+ lines.add(line);
+
+ lineIndex.addAndGet(1);
+ }
+ } catch (IOException e) {
+ consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
+ }
+
+ try {
+ // 写入文件
+ String charsets = writeFile.getName().endsWith(".layout") ? FileUtil.getCharsets(writeFile) : StandardCharsets.UTF_8.name();
+ FileUtils.writeLines(writeFile, charsets, lines);
+ } catch (IOException e) {
+ consoleLog.error(STR."文件(\{writeFile.getAbsoluteFile()})写入失败", e);
+ }
+
+ // CPP转BIN (覆盖TMP下BIN文件)
+ if (hasBin) cpp2bin(writeFile);
+ }
+ }
+
+ /**
+ * 查找文件内可翻译文本
+ *
+ * @param file 文件
+ * @return 可翻译文本信息列表
+ */
+ private static List findWordByFile(File file) {
+ if (!FILE_NAME_CONFIG_CPP.equals(file.getName())
+ && !FILE_NAME_CONFIG_BIN.equals(file.getName())
+ && !FILE_NAME_STRING_TABLE.equals(file.getName())
+ && !file.getName().endsWith(".layout")
+ ) {
+ return Collections.emptyList();
+ }
+
+ // 创建备份(在bak文件夹下的同级目录
+ file = createBak(file);
+
+ // bin转cpp
+ if (FILE_NAME_CONFIG_BIN.equals(file.getName())) {
+ file = bin2cpp(file);
+ }
+
+ String charset = file.getName().endsWith(".layout") ? FileUtil.getCharsets(file) : StandardCharsets.UTF_8.name();
+
+ try (LineIterator it = FileUtils.lineIterator(file, charset)) {
+ // CPP
+ if (FILE_NAME_CONFIG_CPP.equals(file.getName())) {
+ return findWordByCPP(file, it);
+ }
+ // CSV
+ if (FILE_NAME_STRING_TABLE.equals(file.getName())) {
+ return findWordByCSV(file, it);
+ }
+ // layout
+ if (file.getName().endsWith(".layout")) {
+ return findWordByLayout(file, it);
+ }
+
+ // TODO 待添加更多文件格式
+ return Collections.emptyList();
+
+ } catch (IOException e) {
+ consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * 从csv文件中读取可翻译文本
+ *
+ * @param file csv文件
+ * @param it 行内容遍历器
+ * @return 可翻译文本列表
+ */
+ private static List findWordByCSV(File file, LineIterator it) {
+ ArrayList wordItems = new ArrayList<>();
+ AtomicInteger lines = new AtomicInteger(0);
+ int index = -1;
+ String line;
+ while (it.hasNext()) {
+ line = it.next();
+ List split = Arrays.stream(line.split(",")).toList();
+
+ if (lines.get() == 0) {
+ index = split.indexOf("\"chinese\"");
+ } else if (index < split.size()) {
+ // 原文
+ String original = split.get(index).replaceAll("\"", "");
+ // 开始下标
+ Integer startIndex = line.indexOf(original);
+ // 添加单词
+ if (original.length() > 1) {
+ wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
+ }
+ }
+
+ lines.addAndGet(1);
+ }
+ return wordItems;
+ }
+
+ /**
+ * 从layout文件中读取可翻译文本
+ *
+ * @param file layout文件
+ * @param it 行内容遍历器
+ * @return 可翻译文本列表
+ */
+ private static List findWordByLayout(File file, LineIterator it) {
+ ArrayList wordItems = new ArrayList<>();
+ AtomicInteger lines = new AtomicInteger(0);
+ String line;
+ Matcher matcher;
+ while (it.hasNext()) {
+ line = it.next();
+ matcher = LAYOUT_PATTERN.matcher(line);
+ if (StringUtils.isNoneEmpty(line) && matcher.matches()) {
+ // 原文
+ String original = matcher.group(1);
+ // 开始下标
+ Integer startIndex = line.indexOf(original);
+ // 添加单词
+ if (original.length() > 1) {
+ wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
+ }
+ }
+
+ lines.addAndGet(1);
+ }
+ return wordItems;
+ }
+
+ /**
+ * 读取cpp文件内可翻译文本
+ *
+ * @param file cpp文件
+ * @param it 行内容遍历器
+ * @return 可翻译文本列表
+ */
+ private static List findWordByCPP(File file, LineIterator it) {
+ ArrayList wordItems = new ArrayList<>();
+ AtomicInteger lines = new AtomicInteger(0);
+
+ while (it.hasNext()) {
+ String line = it.next();
+
+ Matcher matcher = CPP_PATTERN.matcher(line);
+ if (!line.contains("$") && matcher.matches()) {
+
+ String name = matcher.group(1);
+
+ // 原始文本
+ int startIndex = line.indexOf(name) + name.length();
+ String original = matcher.group(2);
+
+ // 添加单词
+ if (original.length() > 1) {
+ wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
+ }
+ }
+
+ lines.addAndGet(1);
+ }
+ return wordItems;
+ }
+
+ /**
+ * 创建备份文件
+ */
+ private static File createBak(File file) {
+ try {
+ String absolutePath = file.getAbsolutePath().replace(Constants.TMP_DIR_PATH, Constants.BAK_DIR_PATH);
+ File destFile = new File(absolutePath);
+ FileUtils.copyFile(file, destFile);
+ return destFile;
+ } catch (IOException e) {
+ consoleLog.error(STR."创建备份文件失败[\{file.getAbsolutePath()}]", e);
+ }
+ return file;
+ }
+
+ /**
+ * bin 转 cpp
+ *
+ * @param file bin文件
+ * @return cpp文件
+ */
+ private static File bin2cpp(File file) {
+ boolean exec = processesUtil.exec(toTxtCommand(file));
+
+ if (!exec) throw new RuntimeException("bin2cpp 失败");
+
+ return new File(outFilePath(file, ".cpp"));
+ }
+
+ /**
+ * cpp 转 bin
+ *
+ * @param file bin文件
+ */
+ private static void cpp2bin(File file) {
+ boolean exec = processesUtil.exec(toBinCommand(file));
+
+ if (!exec) throw new RuntimeException("cpp2bin 失败");
+ }
+
+ /**
+ * cpp to bin 命令
+ */
+ private static String toBinCommand(File cppFile) {
+ String outFilePath = outFilePath(cppFile, ".bin");
+ outFilePath = outFilePath.replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
+ return String.format(CFG_COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath());
+ }
+
+ /**
+ * bin to cpp 命令
+ */
+ private static String toTxtCommand(File binFile) {
+ String outFilePath = outFilePath(binFile, ".cpp");
+ return String.format(CFG_COMMAND, "-txt", outFilePath, binFile.getAbsolutePath());
+ }
+
+ private static String outFilePath(File file, String suffix) {
+ return file.getParentFile().getAbsolutePath() + File.separator + FileUtil.mainName(file) + suffix;
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/utils/Resources.java b/src/main/java/cn/octopusyan/dmt/utils/Resources.java
new file mode 100644
index 0000000..e1e767d
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/utils/Resources.java
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: MIT */
+
+package cn.octopusyan.dmt.utils;
+
+import cn.octopusyan.dmt.AppLauncher;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.Objects;
+import java.util.prefs.Preferences;
+
+public final class Resources {
+
+ public static final String MODULE_DIR = "/";
+
+ public static InputStream getResourceAsStream(String resource) {
+ String path = resolve(resource);
+ return Objects.requireNonNull(
+ AppLauncher.class.getResourceAsStream(resolve(path)),
+ "Resource not found: " + path
+ );
+ }
+
+ public static URI getResource(String resource) {
+ String path = resolve(resource);
+ URL url = Objects.requireNonNull(AppLauncher.class.getResource(resolve(path)), "Resource not found: " + path);
+ return URI.create(url.toExternalForm());
+ }
+
+ public static String resolve(String resource) {
+ Objects.requireNonNull(resource);
+ return resource.startsWith("/") ? resource : MODULE_DIR + resource;
+ }
+
+ public static String getPropertyOrEnv(String propertyKey, String envKey) {
+ return System.getProperty(propertyKey, System.getenv(envKey));
+ }
+
+ public static Preferences getPreferences() {
+ return Preferences.userRoot().node("atlantafx");
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java b/src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java
new file mode 100644
index 0000000..bb2a6cc
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java
@@ -0,0 +1,151 @@
+package cn.octopusyan.dmt.view;
+
+import javafx.application.Platform;
+import javafx.scene.control.TextArea;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 模拟控制台输出
+ *
+ * @author octopus_yan
+ */
+public class ConsoleLog {
+ public static final String format = "yyyy/MM/dd hh:mm:ss";
+ private static final Logger log = LoggerFactory.getLogger(ConsoleLog.class);
+ private static Logger markerLog;
+ private static TextArea logArea;
+ private final String tag;
+ @Setter
+ private static boolean showDebug = false;
+
+ public static void init(TextArea logArea) {
+ ConsoleLog.logArea = logArea;
+ }
+
+ private ConsoleLog(String tag) {
+ this.tag = tag;
+ }
+
+ public static ConsoleLog getInstance(Class clazz) {
+ markerLog = LoggerFactory.getLogger(clazz);
+ return getInstance(clazz.getSimpleName());
+ }
+
+ public static ConsoleLog getInstance(String tag) {
+ return new ConsoleLog(tag);
+ }
+
+ public static boolean isInit() {
+ return log != null;
+ }
+
+ public void info(String message, Object... param) {
+ printLog(tag, Level.INFO, message, param);
+ }
+
+ public void warning(String message, Object... param) {
+ printLog(tag, Level.WARN, message, param);
+ }
+
+ public void debug(String message, Object... param) {
+ if (!showDebug) return;
+ printLog(tag, Level.DEBUG, message, param);
+ }
+
+ public void error(String message, Object... param) {
+ printLog(tag, Level.ERROR, message, param);
+ }
+
+ public void error(String message, Throwable throwable) {
+ markerLog.error(message, throwable);
+ message = STR."\{message} \{throwable.getMessage()}";
+ printLog(tag, Level.ERROR, message);
+ }
+
+ public void msg(String message, Object... params) {
+ if (StringUtils.isEmpty(message) || !isInit()) return;
+ message = format(message, params);
+ message = resetConsoleColor(message);
+
+ print(message);
+ }
+
+ final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ public void printLog(String tag, Level level, String message, Object... params) {
+ if (!isInit()) return;
+
+ // 时间
+ String time = LocalDateTime.now().format(formatter);
+ // 级别
+ String levelStr = level.code;
+ // 消息
+ message = format(message, params);
+
+ // 拼接后输出
+ String input = STR."\{time} \{levelStr} [\{tag}] - \{message.replace(tag, "")}";
+
+ switch (level) {
+ case WARN -> markerLog.warn(message);
+ case DEBUG -> markerLog.debug(message);
+// case ERROR -> markerLog.error(message);
+ default -> markerLog.info(message);
+ }
+
+ print(input);
+ }
+
+ private static void print(String message) {
+ var msg = message + (message.endsWith("\n") ? "" : "\n");
+ Platform.runLater(() -> {
+ ConsoleLog.logArea.appendText(msg);
+ // 滚动到底部
+ ConsoleLog.logArea.setScrollTop(Double.MAX_VALUE);
+ });
+ }
+
+//==========================================={ 私有方法 }===================================================
+
+ private static String format(String msg, Object... params) {
+ int i = 0;
+ while (msg.contains("{}") && params != null) {
+ msg = msg.replaceFirst("\\{}", String.valueOf(params[i++]).replace("\\", "\\\\"));
+ }
+ return msg;
+ }
+
+ /**
+ * 处理控制台输出颜色
+ *
+ * @param msg 输出消息
+ * @return 信息
+ */
+ private static String resetConsoleColor(String msg) {
+ if (!msg.contains("\033[")) return msg;
+
+ return msg.replaceAll("\\033\\[(\\d;)?(\\d+)m", "");
+ }
+
+//============================{ 枚举 }================================
+
+ @Getter
+ @RequiredArgsConstructor
+ public enum Level {
+ INFO("INFO", null),
+ DEBUG("DEBUG", null),
+ WARN("WARN", "-color-danger-emphasis"),
+ ERROR("ERROR", "-color-danger-fg"),
+ ;
+
+ private final String code;
+ private final String color;
+ }
+}
diff --git a/src/main/java/cn/octopusyan/dmt/view/EditButtonTableCell.java b/src/main/java/cn/octopusyan/dmt/view/EditButtonTableCell.java
new file mode 100644
index 0000000..fee39a9
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/view/EditButtonTableCell.java
@@ -0,0 +1,74 @@
+package cn.octopusyan.dmt.view;
+
+import atlantafx.base.theme.Styles;
+import cn.octopusyan.dmt.model.WordItem;
+import cn.octopusyan.dmt.utils.Resources;
+import javafx.scene.control.Button;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javafx.util.Callback;
+import org.kordamp.ikonli.feather.Feather;
+import org.kordamp.ikonli.javafx.FontIcon;
+
+import java.util.function.Consumer;
+
+/**
+ * 按钮
+ *
+ * @author octopus_yan
+ */
+public class EditButtonTableCell extends TableCell {
+
+ public static Callback, TableCell> forTableColumn(Consumer edit, Consumer translate) {
+ return _ -> new EditButtonTableCell("", edit, translate);
+ }
+
+ private final Button edit;
+ private final Button translate;
+
+ private static final ImageView translateIcon = new ImageView(new Image(Resources.getResourceAsStream("images/icon/translate.png")));
+
+ static {
+ translateIcon.setFitHeight(20);
+ translateIcon.setFitWidth(20);
+ }
+
+ public EditButtonTableCell(String text, Consumer