commit 08c80f20ae38e98362811beec157dff173deba97 Author: octopus_yan Date: Mon Feb 19 14:17:43 2024 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbdb911 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.mvn/ +mvnw +mvnw.cmd +log/ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/ +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e417bb --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# DayZ Mod Translator + +使用 JavaFx 编写的 DayZ 游戏mod 汉化GUI工具 + +## 截图 + +![Main window start](doc/img/screenshot01.png 'Main application window start') +![Main window open file](doc/img/screenshot02.png 'Main window open file') diff --git a/doc/img/screenshot01.png b/doc/img/screenshot01.png new file mode 100644 index 0000000..10cfce3 Binary files /dev/null and b/doc/img/screenshot01.png differ diff --git a/doc/img/screenshot02.png b/doc/img/screenshot02.png new file mode 100644 index 0000000..920ea7c Binary files /dev/null and b/doc/img/screenshot02.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..96e26c0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + cn.octopusyan + dayz-mod-translator + 0.0.1 + DayzModTranslator + + + octopus_yan + octopus_yan@foxmail.com + + 2024 + DayZ 模组汉化工具 + + + UTF-8 + 17 + 17 + 17 + + 5.10.0 + 17.0.6 + 2.0.12 + 1.4.14 + 2.0.46 + 5.8.25 + + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.openjfx + javafx-swing + ${javafx.version} + + + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + + + org.apache.commons + commons-lang3 + 3.14.0 + + + + commons-io + commons-io + 2.15.1 + + + + org.apache.commons + commons-exec + 1.4.0 + + + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + + + src/main/resources + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + + exe + dll + + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + + default-cli + + cn.octopusyan.dayzmodtranslator/cn.octopusyan.dayzmodtranslator.AppLuncher + + launcher + app + app + true + true + true + + + + + + + io.github.fvarrui + javapackager + 1.7.5 + + true + cn.octopusyan.dayzmodtranslator.AppLuncher + false + + + + bundling-for-windows + package + + package + + + windows + true + + + + + + + \ No newline at end of file diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/AppLuncher.java b/src/main/java/cn/octopusyan/dayzmodtranslator/AppLuncher.java new file mode 100644 index 0000000..158706f --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/AppLuncher.java @@ -0,0 +1,19 @@ +package cn.octopusyan.dayzmodtranslator; + +/** + * 启动类 + * + * @author octopus_yan@foxmail.com + */ +public class AppLuncher { + + public static void main(String[] args) { +// try { +// Runtime.getRuntime().exec("Taskkill /IM " + FrpManager.FRPC_CLIENT_FILE_NAME + " /f"); +// } catch (IOException e) { +// e.printStackTrace(); +// } + + Application.launch(Application.class, args); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/Application.java b/src/main/java/cn/octopusyan/dayzmodtranslator/Application.java new file mode 100644 index 0000000..a61f8fb --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/Application.java @@ -0,0 +1,105 @@ +package cn.octopusyan.dayzmodtranslator; + +import cn.octopusyan.dayzmodtranslator.config.AppConstant; +import cn.octopusyan.dayzmodtranslator.config.CustomConfig; +import cn.octopusyan.dayzmodtranslator.manager.http.HttpConfig; +import cn.octopusyan.dayzmodtranslator.controller.MainController; +import cn.octopusyan.dayzmodtranslator.manager.CfgConvertUtil; +import cn.octopusyan.dayzmodtranslator.manager.PBOUtil; +import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil; +import cn.octopusyan.dayzmodtranslator.util.AlertUtil; +import cn.octopusyan.dayzmodtranslator.util.FxmlUtil; +import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.util.Objects; + +public class Application extends javafx.application.Application { + private static final Logger logger = LoggerFactory.getLogger(Application.class); + + @Override + public void init() throws Exception { + logger.info("application init ..."); + } + + @Override + public void start(Stage stage) throws IOException { + + logger.info("application start ..."); + + // bin转换 工具初始化 + CfgConvertUtil.init(); + + // PBO 工具初始化 + PBOUtil.init(CfgConvertUtil.getInstance()); + + // 客户端配置初始化 + CustomConfig.init(); + + // 初始化弹窗工具 + AlertUtil.initOwner(stage); + + // http请求工具初始化 + HttpConfig httpConfig = new HttpConfig(); + if (CustomConfig.hasProxy()) { + InetSocketAddress unresolved = InetSocketAddress.createUnresolved(CustomConfig.proxyHost(), CustomConfig.proxyPort()); + httpConfig.setProxySelector(ProxySelector.of(unresolved)); + } + httpConfig.setConnectTimeout(10); + HttpUtil.init(httpConfig); + + // TODO 全局异常处理 + Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog); + Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog); + + // 启动主界面 + try { + FXMLLoader fxmlLoader = FxmlUtil.load("main-view"); + VBox root = fxmlLoader.load();//底层面板 + Scene scene = new Scene(root); + scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm()); + stage.setScene(scene); + stage.setMinHeight(330); + stage.setMinWidth(430); + stage.setMaxWidth(Double.MAX_VALUE); + stage.setMaxHeight(Double.MAX_VALUE); + stage.setTitle(AppConstant.APP_TITLE + " v" + AppConstant.APP_VERSION); + stage.show(); + + MainController controller = fxmlLoader.getController(); + controller.setApplication(this); + } catch (Throwable t) { + showErrorDialog(Thread.currentThread(), t); + } + + logger.info("application start over ..."); + } + + private void showErrorDialog(Thread t, Throwable e) { + logger.error("", e); + AlertUtil.exceptionAlert(new Exception(e)).show(); + } + + @Override + public void stop() throws Exception { + logger.info("application stop ..."); + + // 清除翻译任务 + TranslateUtil.getInstance().clear(); + // 停止所有线程 + ThreadPoolManager.getInstance().shutdown(); + // 保存应用数据 + CustomConfig.store(); + // 清理缓存 + PBOUtil.clear(); + } +} \ 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 new file mode 100644 index 0000000..7095c64 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/base/BaseController.java @@ -0,0 +1,217 @@ +package cn.octopusyan.dayzmodtranslator.base; + +import cn.octopusyan.dayzmodtranslator.config.AppConstant; +import cn.octopusyan.dayzmodtranslator.util.FxmlUtil; +import cn.octopusyan.dayzmodtranslator.util.Loading; +import cn.octopusyan.dayzmodtranslator.util.TooltipUtil; +import javafx.application.Application; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; + +/** + * 通用视图控制器基类 + * + * @author octopus_yan@foxmail.com + */ +public abstract class BaseController

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> 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 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 new file mode 100644 index 0000000..70c0d11 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/config/AppConstant.java @@ -0,0 +1,21 @@ +package cn.octopusyan.dayzmodtranslator.config; + +import cn.octopusyan.dayzmodtranslator.util.PropertiesUtils; +import org.apache.commons.io.FileUtils; + +import java.io.File; + +/** + * 应用信息 + * + * @author octopus_yan@foxmail.com + */ +public class AppConstant { + public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title"); + public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name"); + public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version"); + public static final String DATA_DIR_PATH = System.getProperty("user.home") + File.separator + "AppData" + File.separator + "Local" + File.separator + APP_NAME; + public static final String TMP_DIR_PATH = FileUtils.getTempDirectoryPath() + APP_NAME; + public static final String CUSTOM_CONFIG_PATH = DATA_DIR_PATH + File.separator + "config.properties"; + public static final String BAK_FILE_PATH = AppConstant.TMP_DIR_PATH + File.separator + "bak"; +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/config/CustomConfig.java b/src/main/java/cn/octopusyan/dayzmodtranslator/config/CustomConfig.java new file mode 100644 index 0000000..a36ece7 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/config/CustomConfig.java @@ -0,0 +1,174 @@ +package cn.octopusyan.dayzmodtranslator.config; + +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +/** + * 客户端设置 + * + * @author octopus_yan@foxmail.com + */ +public class CustomConfig { + private static final Logger logger = LoggerFactory.getLogger(CustomConfig.class); + private static final Properties properties = new Properties(); + public static final String PROXY_HOST_KEY = "proxy.host"; + public static final String PROXY_PORT_KEY = "proxy.port"; + public static final String TRANSLATE_SOURCE_KEY = "translate.source"; + public static final String TRANSLATE_SOURCE_APPID_KEY = "translate.{}.appid"; + public static final String TRANSLATE_SOURCE_APIKEY_KEY = "translate.{}.apikey"; + public static final String TRANSLATE_SOURCE_QPS_KEY = "translate.{}.qps"; + + public static void init() { + File customConfigFile = new File(AppConstant.CUSTOM_CONFIG_PATH); + try { + if (!customConfigFile.exists()) { + // 初始配置 + properties.put(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName()); + // 保存配置文件 + store(); + } else { + properties.load(new FileInputStream(customConfigFile)); + } + } catch (IOException ignore) { + logger.error("读取配置文件失败"); + } + } + + /** + * 是否配置代理 + */ + public static boolean hasProxy() { + String host = proxyHost(); + Integer port = proxyPort(); + + return StringUtils.isNoneBlank(host) && null != port; + } + + /** + * 代理地址 + */ + public static String proxyHost() { + return properties.getProperty(PROXY_HOST_KEY); + } + + /** + * 代理地址 + */ + public static void proxyHost(String host) { + properties.setProperty(PROXY_HOST_KEY, host); + } + + /** + * 代理端口 + */ + public static Integer proxyPort() { + try { + return Integer.parseInt(properties.getProperty(PROXY_PORT_KEY)); + } catch (Exception ignored) { + } + return null; + } + + /** + * 代理端口 + */ + public static void proxyPort(int port) { + properties.setProperty(PROXY_PORT_KEY, String.valueOf(port)); + } + + /** + * 翻译源 + */ + public static TranslateSource translateSource() { + String name = properties.getProperty(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName()); + return TranslateSource.get(name); + } + + /** + * 翻译源 + */ + public static void translateSource(TranslateSource source) { + properties.setProperty(TRANSLATE_SOURCE_KEY, source.getName()); + } + + /** + * 是否配置接口认证 + * + * @param source 翻译源 + */ + public static boolean hasTranslateApiKey(TranslateSource source) { + return StringUtils.isNoneBlank(translateSourceAppid(source)) + && StringUtils.isNoneBlank(translateSourceApikey(source)); + } + + /** + * 设置翻译源appid + * + * @param source 翻译源 + * @param appid appid + */ + public static void translateSourceAppid(TranslateSource source, String appid) { + properties.setProperty(getTranslateSourceAppidKey(source), appid); + } + + /** + * 获取翻译源appid + * + * @param source 翻译源 + * @return appid + */ + public static String translateSourceAppid(TranslateSource source) { + return properties.getProperty(getTranslateSourceAppidKey(source)); + } + + public static void translateSourceApikey(TranslateSource source, String apikey) { + properties.setProperty(getTranslateSourceApikeyKey(source), apikey); + } + + public static String translateSourceApikey(TranslateSource source) { + return properties.getProperty(getTranslateSourceApikeyKey(source)); + } + + public static Integer translateSourceQps(TranslateSource source) { + String qpsStr = properties.getProperty(getTranslateSourceQpsKey(source)); + return qpsStr == null ? source.getDefaultQps() : Integer.parseInt(qpsStr); + } + + public static void translateSourceQps(TranslateSource source, int qps) { + properties.setProperty(getTranslateSourceQpsKey(source), String.valueOf(qps)); + } + + + /** + * 保存配置 + */ + public static void store() { + // 生成配置文件 + try { + properties.store(new PrintStream(AppConstant.CUSTOM_CONFIG_PATH), String.valueOf(StandardCharsets.UTF_8)); + } catch (IOException e) { + logger.error("保存客户端配置失败", e); + } + } + + private static String getTranslateSourceAppidKey(TranslateSource source) { + return StringUtils.replace(TRANSLATE_SOURCE_APPID_KEY, "{}", source.getName()); + } + + private static String getTranslateSourceApikeyKey(TranslateSource source) { + return StringUtils.replace(TRANSLATE_SOURCE_APIKEY_KEY, "{}", source.getName()); + } + + private static String getTranslateSourceQpsKey(TranslateSource source) { + return StringUtils.replace(TRANSLATE_SOURCE_QPS_KEY, "{}", source.getName()); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/controller/MainController.java b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/MainController.java new file mode 100644 index 0000000..46bd3d1 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/MainController.java @@ -0,0 +1,515 @@ +package cn.octopusyan.dayzmodtranslator.controller; + +import cn.octopusyan.dayzmodtranslator.base.BaseController; +import cn.octopusyan.dayzmodtranslator.manager.PBOUtil; +import cn.octopusyan.dayzmodtranslator.manager.file.FileTreeItem; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil; +import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem; +import cn.octopusyan.dayzmodtranslator.manager.word.WordItem; +import cn.octopusyan.dayzmodtranslator.util.AlertUtil; +import cn.octopusyan.dayzmodtranslator.util.ClipUtil; +import javafx.application.Platform; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.*; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.input.*; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** + * 主控制器 + * + * @author octopus_yan@foxmail.com + */ +public class MainController extends BaseController { + + 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 new file mode 100644 index 0000000..8694915 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupProxyController.java @@ -0,0 +1,162 @@ +package cn.octopusyan.dayzmodtranslator.controller; + +import cn.octopusyan.dayzmodtranslator.base.BaseController; +import cn.octopusyan.dayzmodtranslator.config.CustomConfig; +import cn.octopusyan.dayzmodtranslator.util.AlertUtil; +import cn.octopusyan.dayzmodtranslator.util.FxmlUtil; +import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil; +import javafx.scene.control.TextField; +import javafx.scene.layout.StackPane; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.io.IOException; +import java.net.URI; + +/** + * 应用配置 + * + * @author octopus_yan@foxmail.com + */ +public class SetupProxyController extends BaseController { + 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 new file mode 100644 index 0000000..f9c879f --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/controller/SetupTranslateController.java @@ -0,0 +1,127 @@ +package cn.octopusyan.dayzmodtranslator.controller; + +import cn.octopusyan.dayzmodtranslator.base.BaseController; +import cn.octopusyan.dayzmodtranslator.config.CustomConfig; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import cn.octopusyan.dayzmodtranslator.util.AlertUtil; +import javafx.collections.ObservableList; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; +import org.apache.commons.lang3.StringUtils; + +/** + * 翻译设置控制器 + * + * @author octopus_yan@foxmail.com + */ +public class SetupTranslateController extends BaseController { + 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 new file mode 100644 index 0000000..37b1882 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/CfgConvertUtil.java @@ -0,0 +1,137 @@ +package cn.octopusyan.dayzmodtranslator.manager; + +import cn.octopusyan.dayzmodtranslator.config.AppConstant; +import cn.octopusyan.dayzmodtranslator.util.FileUtil; +import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +/** + * Cfg文件转换工具类 + * + * @author octopus_yan@foxmail.com + */ +public class CfgConvertUtil { + private static final Logger logger = LoggerFactory.getLogger(CfgConvertUtil.class); + private static CfgConvertUtil util; + private static final String CfgConvert_DIR_PATH = AppConstant.DATA_DIR_PATH + File.separator + "CfgConvert"; + private static final File CfgConvert_DIR = new File(CfgConvert_DIR_PATH); + private static final String CfgConvert_FILE_PATH = CfgConvert_DIR_PATH + File.separator + "CfgConvert.exe"; + private static final File CfgConvert_FILE = new File(CfgConvert_FILE_PATH); + private static final String COMMAND = CfgConvert_FILE_PATH + " %s -dst %s %s"; + + private CfgConvertUtil() { + } + + public static void init() { + if (util == null) { + util = new CfgConvertUtil(); + } + // 检查pbo解析文件 + util.checkCfgConvert(); + } + + public static synchronized CfgConvertUtil getInstance() { + if (util == null) + throw new RuntimeException("are you ready ?"); + return util; + } + + /** + * 检查Cfg转换文件 + */ + private void checkCfgConvert() { + if (!CfgConvert_FILE.exists()) initCfgConvert(); + } + + private void initCfgConvert() { + try { + FileUtils.forceMkdir(CfgConvert_DIR); + String cfgConvertFileName = "CfgConvert.exe"; + FileUtil.copyFile(Objects.requireNonNull(CfgConvertUtil.class.getResourceAsStream("/static/CfgConvert/" + cfgConvertFileName)), new File(CfgConvert_DIR_PATH + File.separator + cfgConvertFileName)); + } catch (IOException e) { + logger.error("", e); + } + } + + public void toTxt(File binFile, String outPath, OnToTxtListener onToTxtListener) { + String fileName = binFile.getAbsolutePath(); + ProcessesUtil.exec(toTxtCommand(binFile, outPath), new ProcessesUtil.OnExecuteListener() { + @Override + public void onExecute(String msg) { + logger.info(fileName + " : " + msg); + } + + @Override + public void onExecuteSuccess(int exitValue) { + logger.info(fileName + " : to txt success"); + String outFilePath = outFilePath(binFile, outPath, ".cpp"); + if (onToTxtListener != null) { + onToTxtListener.onToTxtSuccess(outFilePath); + } + } + + @Override + public void onExecuteError(Exception e) { + logger.error(fileName + " : to txt error", e); + if (onToTxtListener != null) { + onToTxtListener.onToTxtError(e); + } + } + + @Override + public void onExecuteOver() { + logger.info(fileName + " : to txt end..."); + } + }); + } + + public void toBin(File cppFile) { + ProcessesUtil.exec(toBinCommand(cppFile, cppFile.getParentFile().getAbsolutePath()), new ProcessesUtil.OnExecuteListener() { + @Override + public void onExecute(String msg) { + + } + + @Override + public void onExecuteSuccess(int exitValue) { + + } + + @Override + public void onExecuteError(Exception e) { + + } + + @Override + public void onExecuteOver() { + + } + }); + } + + private String toBinCommand(File cppFile, String outPath) { + String outFilePath = outFilePath(cppFile, outPath, ".bin"); + return String.format(COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath()); + } + + private String toTxtCommand(File binFile, String outPath) { + String outFilePath = outFilePath(binFile, outPath, ".cpp"); + return String.format(COMMAND, "-txt", outFilePath, binFile.getAbsolutePath()); + } + + private String outFilePath(File file, String outPath, String suffix) { + return outPath + File.separator + FileUtil.mainName(file) + suffix; + } + + public interface OnToTxtListener { + void onToTxtSuccess(String txtFilePath); + + void onToTxtError(Exception e); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/PBOUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/PBOUtil.java new file mode 100644 index 0000000..c942531 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/PBOUtil.java @@ -0,0 +1,628 @@ +package cn.octopusyan.dayzmodtranslator.manager; + +import cn.octopusyan.dayzmodtranslator.config.AppConstant; +import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager; +import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem; +import cn.octopusyan.dayzmodtranslator.manager.word.WordItem; +import cn.octopusyan.dayzmodtranslator.util.AlertUtil; +import cn.octopusyan.dayzmodtranslator.util.FileUtil; +import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil; +import javafx.application.Platform; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.LineIterator; +import org.apache.commons.io.filefilter.NameFileFilter; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * PBO 工具类 + * + * @author octopus_yan@foxmail.com + * @see 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 new file mode 100644 index 0000000..1ce5598 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileIcon.java @@ -0,0 +1,49 @@ +package cn.octopusyan.dayzmodtranslator.manager.file; + + +import javafx.scene.canvas.Canvas; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritablePixelFormat; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.nio.IntBuffer; + +/** + * 文件图标 + * + * @author octopus_yan@foxmail.com + */ +public class FileIcon { + //设置图标 + public static Canvas getFileIconToNode(File file) { + //获取系统文件的图标 + Image image = ((ImageIcon) FileSystemView.getFileSystemView().getSystemIcon(file)).getImage(); + //构建图片缓冲区,设定图片缓冲区的大小和背景,背景为透明 + BufferedImage bi = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.BITMASK); + //把图片画到图片缓冲区 + bi.getGraphics().drawImage(image, 0, 0, null); + //将图片缓冲区的数据转换成int型数组 + int[] data = ((DataBufferInt) bi.getData().getDataBuffer()).getData(); + //获得写像素的格式模版 + WritablePixelFormat 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 new file mode 100644 index 0000000..9068700 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/file/FileTreeItem.java @@ -0,0 +1,90 @@ +package cn.octopusyan.dayzmodtranslator.manager.file; + +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * 文件目录 + * + * @author octopus_yan@foxmail.com + */ +public class FileTreeItem extends TreeItem { + 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 new file mode 100644 index 0000000..0936834 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpConfig.java @@ -0,0 +1,183 @@ +package cn.octopusyan.dayzmodtranslator.manager.http; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.Executor; + +/** + * Http配置参数 + * + * @author octopus_yan@foxmail.com + */ +public class HttpConfig { + private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class); + /** + * http版本 + */ + private HttpClient.Version version = HttpClient.Version.HTTP_2; + + /** + * 转发策略 + */ + private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL; + + /** + * 线程池 + */ + private Executor executor; + + /** + * 认证 + */ + private Authenticator authenticator; + + /** + * 代理 + */ + private ProxySelector proxySelector; + + /** + * CookieHandler + */ + private CookieHandler cookieHandler; + + /** + * sslContext + */ + private SSLContext sslContext; + + /** + * sslParams + */ + private SSLParameters sslParameters; + + /** + * 连接超时时间毫秒 + */ + private int connectTimeout = 10000; + + /** + * 默认读取数据超时时间 + */ + private int defaultReadTimeout = 1200000; + + + public HttpConfig() { + TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; // Not relevant. + } + + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + // TODO Auto-generated method stub + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + // TODO Auto-generated method stub + } + }}; + sslParameters = new SSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm(""); + + + try { + sslContext = SSLContext.getInstance("TLSv1.2"); + System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证 + sslContext.init(null, trustAllCertificates, new SecureRandom()); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + logger.error("", e); + } + + } + + + public HttpClient.Version getVersion() { + return version; + } + + public void setVersion(HttpClient.Version version) { + this.version = version; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + } + + + public HttpClient.Redirect getRedirect() { + return redirect; + } + + public void setRedirect(HttpClient.Redirect redirect) { + this.redirect = redirect; + } + + public Executor getExecutor() { + return executor; + } + + public void setExecutor(Executor executor) { + this.executor = executor; + } + + public Authenticator getAuthenticator() { + return authenticator; + } + + public void setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + } + + public ProxySelector getProxySelector() { + return proxySelector; + } + + public void setProxySelector(ProxySelector proxySelector) { + this.proxySelector = proxySelector; + } + + public CookieHandler getCookieHandler() { + return cookieHandler; + } + + public void setCookieHandler(CookieHandler cookieHandler) { + this.cookieHandler = cookieHandler; + } + + public int getDefaultReadTimeout() { + return defaultReadTimeout; + } + + public void setDefaultReadTimeout(int defaultReadTimeout) { + this.defaultReadTimeout = defaultReadTimeout; + } + + public SSLContext getSslContext() { + return sslContext; + } + + public SSLParameters getSslParameters() { + return sslParameters; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpUtil.java new file mode 100644 index 0000000..09caa16 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/http/HttpUtil.java @@ -0,0 +1,141 @@ +package cn.octopusyan.dayzmodtranslator.manager.http; + +import com.alibaba.fastjson2.JSONObject; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +/** + * 网络请求封装 + * + * @author octopus_yan@foxmail.com + */ +public class HttpUtil { + private volatile static HttpUtil util; + private volatile HttpClient httpClient; + private final HttpConfig httpConfig; + + private HttpUtil(HttpConfig httpConfig) { + this.httpConfig = httpConfig; + this.httpClient = createClient(httpConfig); + } + + public static HttpUtil getInstance() { + if (util == null) { + throw new RuntimeException("are you ready ?"); + } + return util; + } + + public static void init(HttpConfig httpConfig) { + synchronized (HttpUtil.class) { + util = new HttpUtil(httpConfig); + } + } + + private static HttpClient createClient(HttpConfig httpConfig) { + HttpClient.Builder builder = HttpClient.newBuilder() + .version(httpConfig.getVersion()) + .connectTimeout(Duration.ofMillis(httpConfig.getConnectTimeout())) + .sslContext(httpConfig.getSslContext()) + .sslParameters(httpConfig.getSslParameters()) + .followRedirects(httpConfig.getRedirect()); + Optional.ofNullable(httpConfig.getAuthenticator()).ifPresent(builder::authenticator); + Optional.ofNullable(httpConfig.getCookieHandler()).ifPresent(builder::cookieHandler); + Optional.ofNullable(httpConfig.getProxySelector()).ifPresent(builder::proxy); + Optional.ofNullable(httpConfig.getExecutor()).ifPresent(builder::executor); + return builder.build(); + } + + public HttpUtil proxy(String host, int port) { + if (httpClient == null) + throw new RuntimeException("are you ready ?"); + + InetSocketAddress unresolved = InetSocketAddress.createUnresolved(host, port); + ProxySelector other = ProxySelector.of(unresolved); + this.httpConfig.setProxySelector(other); + this.httpClient = createClient(httpConfig); + return this; + } + + public void clearProxy() { + if (httpClient == null) + throw new RuntimeException("are you ready ?"); + + httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY); + httpClient = createClient(httpConfig); + } + + public HttpClient getHttpClient() { + return httpClient; + } + + public String get(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException { + HttpRequest.Builder request = getRequest(uri + createFormParams(param), header).GET(); + HttpResponse response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + return response.body(); + } + + public String post(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException { + HttpRequest.Builder request = getRequest(uri, header) + .POST(HttpRequest.BodyPublishers.ofString(param.toJSONString())); + HttpResponse response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + return response.body(); + } + + public String postForm(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException { + HttpRequest.Builder request = getRequest(uri + createFormParams(param), header) + .POST(HttpRequest.BodyPublishers.noBody()); + + HttpResponse response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + return response.body(); + } + + private HttpRequest.Builder getRequest(String uri, JSONObject header) { + HttpRequest.Builder request = HttpRequest.newBuilder(); + // 请求地址 + request.uri(URI.create(uri)); + // 请求头 + if (header != null && !header.isEmpty()) { + for (String key : header.keySet()) { + request.header(key, header.getString(key)); + } + } + return request; + } + + private String createFormParams(JSONObject params) { + StringBuilder formParams = new StringBuilder(); + if (params == null) { + return formParams.toString(); + } + for (String key : params.keySet()) { + Object value = params.get(key); + if (value instanceof String) { + value = URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8); + formParams.append("&").append(key).append("=").append(value); + } else if (value instanceof Number) { + formParams.append("&").append(key).append("=").append(value); + } else if (value instanceof List) { + formParams.append("&").append(key).append("=").append(params.getJSONArray(key)); + } else { + formParams.append("&").append(key).append("=").append(params.getJSONObject(key)); + } + } + if (!formParams.isEmpty()) { + formParams = new StringBuilder("?" + formParams.substring(1)); + } + + return formParams.toString(); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadFactory.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadFactory.java new file mode 100644 index 0000000..8f3019e --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadFactory.java @@ -0,0 +1,49 @@ +package cn.octopusyan.dayzmodtranslator.manager.thread; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 自定义线程工厂 + * + * @author octopus_yan@foxmail.com + */ +public class ThreadFactory implements java.util.concurrent.ThreadFactory { + private static final Logger logger = LoggerFactory.getLogger(ThreadFactory.class); + + public static final String DEFAULT_THREAD_PREFIX = "thread-factory-pool"; + + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public ThreadFactory() { + this(DEFAULT_THREAD_PREFIX); + } + + public ThreadFactory(String prefix) { + group = Thread.currentThread().getThreadGroup(); + namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(Runnable runnable) { + + Thread t = new Thread(group, runnable, + namePrefix + threadNumber.getAndIncrement(), + 0); + + t.setUncaughtExceptionHandler((t1, e) -> { + logger.error("thread : {}, error", t1.getName(), e); + }); + + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadPoolManager.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadPoolManager.java new file mode 100644 index 0000000..be46215 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/thread/ThreadPoolManager.java @@ -0,0 +1,29 @@ +package cn.octopusyan.dayzmodtranslator.manager.thread; + + +import java.util.concurrent.*; + +/** + * 线程池管理类 + */ +public final class ThreadPoolManager extends ThreadPoolExecutor { + + private static volatile ThreadPoolManager sInstance; + + private static ScheduledExecutorService scheduledExecutorService; + + private ThreadPoolManager() { + super(32, + 200, + 10, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(200), + new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX), + new ThreadPoolExecutor.DiscardPolicy()); + } + + public static ThreadPoolManager getInstance() { + if (sInstance == null) sInstance = new ThreadPoolManager(); + return sInstance; + } +} \ 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 new file mode 100644 index 0000000..0e22252 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/ApiKey.java @@ -0,0 +1,27 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate; + +/** + * API 密钥配置 + * + * @author octopus_yan@foxmail.com + */ +public class ApiKey { + private String appid; + private String apiKey; + + public ApiKey() { + } + + public ApiKey(String appid, String apiKey) { + this.appid = appid; + this.apiKey = apiKey; + } + + public String getAppid() { + return appid; + } + + public String getApiKey() { + return apiKey; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateSource.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateSource.java new file mode 100644 index 0000000..b55db46 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateSource.java @@ -0,0 +1,65 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate; + +/** + * 翻译引擎类型 + * + * @author octopus_yan@foxmail.com + */ +public enum TranslateSource { + FREE_GOOGLE("free_google", "谷歌(免费)", false, 50), + BAIDU("baidu", "百度(需认证)", true), + + ; + private final String name; + private final String label; + private final boolean needApiKey; + private Integer defaultQps; + + TranslateSource(String name, String label, boolean needApiKey) { + // 设置接口默认qps=10 + this(name, label, needApiKey, 10); + } + + TranslateSource(String name, String label, boolean needApiKey, int defaultQps) { + this.name = name; + this.label = label; + this.needApiKey = needApiKey; + this.defaultQps = defaultQps; + } + + public String getName() { + return name; + } + + public String getLabel() { + return label; + } + + public boolean needApiKey() { + return needApiKey; + } + + public Integer getDefaultQps() { + return defaultQps; + } + + public String getDefaultQpsStr() { + return String.valueOf(defaultQps); + } + + public static TranslateSource get(String type) { + for (TranslateSource value : values()) { + if (value.getName().equals(type)) + return value; + } + throw new RuntimeException("类型不存在"); + } + + public static TranslateSource getByLabel(String label) { + for (TranslateSource value : values()) { + if (value.getLabel().equals(label)) + return value; + } + throw new RuntimeException("类型不存在"); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateUtil.java new file mode 100644 index 0000000..8395bc4 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/TranslateUtil.java @@ -0,0 +1,227 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate; + +import cn.octopusyan.dayzmodtranslator.config.CustomConfig; +import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadFactory; +import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager; +import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactory; +import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactoryImpl; +import javafx.application.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +/** + * 翻译工具 + * + * @author octopus_yan@foxmail.com + */ +public class TranslateUtil { + private static final Logger logger = LoggerFactory.getLogger(TranslateUtil.class); + private static TranslateUtil util; + private static final DelayQueue 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 new file mode 100644 index 0000000..76ca022 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactory.java @@ -0,0 +1,32 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate.factory; + +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil; + +/** + * 翻译器接口 + * + * @author octopus_yan@foxmail.com + */ +public interface TranslateFactory { + /** + * 翻译处理 + * + * @param source 翻译源 + * @param sourceString 原始文本 + * @return 翻译结果 + * @throws Exception 翻译出错 + */ + String translate(TranslateSource source, String sourceString) throws Exception; + + /** + * 获取延迟翻译对象 + * + * @param source 翻译源 + * @param index 序号 + * @param original 原始文本 + * @param listener 监听器 + * @return 延迟翻译对象 + */ + TranslateUtil.DelayWord getDelayWord(TranslateSource source, int index, String original, TranslateUtil.OnTranslateListener listener); +} 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 new file mode 100644 index 0000000..01dda12 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/factory/TranslateFactoryImpl.java @@ -0,0 +1,81 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate.factory; + +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil; +import cn.octopusyan.dayzmodtranslator.manager.translate.processor.AbstractTranslateProcessor; +import cn.octopusyan.dayzmodtranslator.manager.translate.processor.BaiduTranslateProcessor; +import cn.octopusyan.dayzmodtranslator.manager.translate.processor.FreeGoogleTranslateProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * 翻译处理器 + * + * @author octopus_yan@foxmail.com + */ +public class TranslateFactoryImpl implements TranslateFactory { + private static final Logger logger = LoggerFactory.getLogger(TranslateFactoryImpl.class); + private static TranslateFactoryImpl impl; + private final Map 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 new file mode 100644 index 0000000..517b03e --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/AbstractTranslateProcessor.java @@ -0,0 +1,99 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate.processor; + +import cn.octopusyan.dayzmodtranslator.config.CustomConfig; +import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil; +import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * 翻译处理器抽象类 + * + * @author octopus_yan@foxmail.com + */ +public abstract class AbstractTranslateProcessor implements TranslateProcessor { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + protected static final HttpUtil httpUtil = HttpUtil.getInstance(); + protected final TranslateSource translateSource; + protected ApiKey apiKey; + + public AbstractTranslateProcessor(TranslateSource translateSource) { + this.translateSource = translateSource; + } + + public String getSource() { + return translateSource.getName(); + } + + public TranslateSource source() { + return translateSource; + } + + @Override + public boolean needApiKey() { + return source().needApiKey(); + } + + @Override + public boolean configuredKey() { + return CustomConfig.hasTranslateApiKey(source()); + } + + @Override + public int qps() { + return CustomConfig.translateSourceQps(source()); + } + + /** + * 获取Api配置信息 + */ + protected ApiKey getApiKey() { + if (!configuredKey()) { + String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel()); + logger.error(message); + throw new RuntimeException(message); + } + + String appid = CustomConfig.translateSourceAppid(source()); + String apikey = CustomConfig.translateSourceApikey(source()); + return new ApiKey(appid, apikey); + } + + @Override + public String translate(String source) throws Exception { + + if (needApiKey() && !configuredKey()) { + String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel()); + logger.error(message); + throw new RuntimeException(message); + } + + return customTranslate(source); + } + + /** + * 翻译处理 + * + * @param source 原始文本 + * @return 翻译结果 + */ + public abstract String customTranslate(String source) throws Exception; + + /** + * 设置延迟对象 + * + * @param word 带翻译单词 + */ + public void setDelayTime(TranslateUtil.DelayWord word) { + // 设置翻译源 + word.setSource(source()); + + // 设置翻译延迟 + int time = word.getIndex() / qps(); + word.setTime(time, TimeUnit.SECONDS); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/BaiduTranslateProcessor.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/BaiduTranslateProcessor.java new file mode 100644 index 0000000..2b1fd76 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/BaiduTranslateProcessor.java @@ -0,0 +1,111 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate.processor; + +import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey; +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.UUID; + +/** + * 谷歌 免费翻译接口 + * + * @author octopus_yan@foxmail.com + */ +public class BaiduTranslateProcessor extends AbstractTranslateProcessor { + + private ApiKey apiKey; + + public BaiduTranslateProcessor(TranslateSource translateSource) { + super(translateSource); + } + + @Override + public String url() { + return "https://fanyi-api.baidu.com/api/trans/vip/translate"; + } + + /** + * 翻译处理 + * + * @param source 待翻译单词 + * @return 翻译结果 + */ + @Override + public String customTranslate(String source) throws IOException, InterruptedException { + apiKey = getApiKey(); + String appid = apiKey.getAppid(); + String salt = UUID.randomUUID().toString().replace("-", ""); + + JSONObject param = new JSONObject(); + param.put("q", source); + param.put("from", "auto"); + param.put("to", "zh"); + param.put("appid", appid); + param.put("salt", salt); + param.put("sign", getSign(appid, source, salt)); + + String resp = httpUtil.get(url(), null, param); + JSONObject jsonObject = JSON.parseObject(resp); + + if (!jsonObject.containsKey("trans_result")) { + Object errorMsg = jsonObject.get("error_msg"); + logger.error("翻译失败: {}", errorMsg); + throw new RuntimeException("翻译失败: " + errorMsg); + } + + return jsonObject.getJSONArray("trans_result").getJSONObject(0).getString("dst"); + } + + private String getSign(String appid, String q, String salt) { + return encrypt2ToMD5(appid + q + salt + apiKey.getApiKey()); + } + + /** + * MD5加密 + * + * @param str 待加密字符串 + * @return 16进制加密字符串 + */ + public String encrypt2ToMD5(String str) { + + // 加密后的16进制字符串 + String hexStr = ""; + try { + + // 此 MessageDigest 类为应用程序提供信息摘要算法的功能 + MessageDigest md5 = MessageDigest.getInstance("MD5"); + // 转换为MD5码 + byte[] digest = md5.digest(str.getBytes(StandardCharsets.UTF_8)); + hexStr = bytesToHexString(digest); + } catch (Exception e) { + logger.error("", e); + } + return hexStr.toLowerCase(); + } + + /** + * 将byte数组转换成string类型表示 + */ + private String bytesToHexString(byte[] src) { + StringBuilder builder = new StringBuilder(); + if (src == null || src.length == 0) { + return null; + } + String hv; + for (byte b : src) { + // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写 + hv = Integer.toHexString(b & 0xFF).toUpperCase(); + if (hv.length() < 2) { + builder.append(0); + } + builder.append(hv); + } + + return builder.toString(); + } + +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/FreeGoogleTranslateProcessor.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/FreeGoogleTranslateProcessor.java new file mode 100644 index 0000000..66c9bf4 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/FreeGoogleTranslateProcessor.java @@ -0,0 +1,53 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate.processor; + +import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; + +import java.io.IOException; + +/** + * 谷歌 免费翻译接口 + * + * @author octopus_yan@foxmail.com + */ +public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor { + + public FreeGoogleTranslateProcessor(TranslateSource translateSource) { + super(translateSource); + } + + @Override + public String url() { + return "https://translate.googleapis.com/translate_a/single"; + } + + /** + * 翻译处理 + * + * @param source 待翻译单词 + * @return 翻译结果 + */ + @Override + public String customTranslate(String source) throws IOException, InterruptedException { + + JSONObject form = new JSONObject(); + form.put("client", "gtx"); + form.put("dt", "t"); + form.put("sl", "auto"); + form.put("tl", "zh-CN"); + form.put("q", source); + + JSONObject header = new JSONObject(); + StringBuilder retStr = new StringBuilder(); + // TODO 短时大量请求会被ban,需要浏览器验证添加cookie + String resp = httpUtil.get(url(), header, form); + JSONArray jsonObject = JSONArray.parseArray(resp); + for (Object o : jsonObject.getJSONArray(0)) { + JSONArray a = (JSONArray) o; + retStr.append(a.getString(0)); + } + + return retStr.toString(); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/TranslateProcessor.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/TranslateProcessor.java new file mode 100644 index 0000000..74b87d8 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/translate/processor/TranslateProcessor.java @@ -0,0 +1,37 @@ +package cn.octopusyan.dayzmodtranslator.manager.translate.processor; + +/** + * 翻译处理器 + * + * @author octopus_yan@foxmail.com + */ +public interface TranslateProcessor { + /** + * 翻译源 api接口地址 + */ + String url(); + + /** + * 是否需要配置API认证 + */ + boolean needApiKey(); + + /** + * 已配置API认证 + */ + boolean configuredKey(); + + /** + * qps 每秒访问的数量限制 + */ + int qps(); + + /** + * 翻译 + * + * @param source 原始文本 + * @return 翻译结果 + * @throws Exception 翻译出错 + */ + String translate(String source) throws Exception; +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordCsvItem.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordCsvItem.java new file mode 100644 index 0000000..41c4eae --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordCsvItem.java @@ -0,0 +1,52 @@ +package cn.octopusyan.dayzmodtranslator.manager.word; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.io.File; + +/** + * csv单词对象 + * + * @author octopus_yan@foxmail.com + */ +public class WordCsvItem extends WordItem { + /** + * 简体中文 + */ + private StringProperty chineseSimp; + + /** + * 文件中坐标(简体中文) + */ + private int[] positionSimp; + + public WordCsvItem() { + } + + public WordCsvItem(File stringTable, int lines, String original, String chineses, int[] position, String chineseSimp, int[] positionSimp) { + super(stringTable, lines, original, chineses, position); + this.chineseSimp = new SimpleStringProperty(chineseSimp); + this.positionSimp = positionSimp; + } + + public StringProperty chineseSimpProperty() { + return chineseSimp; + } + + public String getChineseSimp() { + return chineseSimp.get(); + } + + public void setChineseSimp(String chineseSimp) { + this.chineseSimp.setValue(chineseSimp); + } + + public int[] getPositionSimp() { + return positionSimp; + } + + public void setPositionSimp(int[] positionSimp) { + this.positionSimp = positionSimp; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordItem.java b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordItem.java new file mode 100644 index 0000000..aae0d84 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/manager/word/WordItem.java @@ -0,0 +1,98 @@ +package cn.octopusyan.dayzmodtranslator.manager.word; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.io.File; + +/** + * 待翻译单词子项 + * + * @author octopus_yan@foxmail.com + */ +public class WordItem { + /** + * 所在文件 + *

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 new file mode 100644 index 0000000..718cd03 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/AlertUtil.java @@ -0,0 +1,228 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.stage.Stage; +import javafx.stage.Window; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 弹窗工具 + * + * @author octopus_yan@foxmail.com + */ +public class AlertUtil { + private static Window mOwner; + private static Builder builder; + + public static void initOwner(Stage stage) { + AlertUtil.mOwner = stage; + } + + public static class Builder { + T 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/ClipUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/ClipUtil.java new file mode 100644 index 0000000..771eb5e --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/ClipUtil.java @@ -0,0 +1,37 @@ +package cn.octopusyan.dayzmodtranslator.util; + +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 + */ +public class ClipUtil { + //获取系统剪切板 + private static final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + + public static void setClip(String data) { + //构建String数据类型 + StringSelection stringSelection = new StringSelection(data); + //添加文本到系统剪切板 + clipboard.setContents(stringSelection, null); + } + + public static String getString() { + Transferable content = clipboard.getContents(null);//从系统剪切板中获取数据 + if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {//判断是否为文本类型 + try { + //从数据中获取文本值 + return (String) content.getTransferData(DataFlavor.stringFlavor); + } catch (UnsupportedFlavorException | IOException e) { + e.printStackTrace(); + return null; + } + } + return null; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/FileUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/FileUtil.java new file mode 100644 index 0000000..f507546 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/FileUtil.java @@ -0,0 +1,135 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.CanReadFileFilter; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文件工具类 + * + * @author octopus_yan@foxmail.com + */ +public class FileUtil { + private static final Logger logger = LoggerFactory.getLogger(FileUtil.class); + + public static File[] ls(String path) { + File dir = new File(path); + if (!dir.exists()) + throw new RuntimeException(path + "不存在!"); + + if (!dir.isDirectory()) + throw new RuntimeException(path + "不是一个文件夹!"); + + return dir.listFiles(); + } + + public static void copyFilesFromDir(String path, String dest) throws IOException { + if (StringUtils.isBlank(path) || StringUtils.isBlank(dest)) { + logger.error("path is blank !"); + return; + } + File dir = new File(path); + if (!dir.exists()) { + logger.error("[" + path + "] 不存在!"); + return; + } + if (!dir.isDirectory()) { + logger.error("[" + path + "] 不是一个文件夹!"); + } + + File[] files = dir.listFiles(); + if (files == null) return; + + File directory = new File(dest); + if (directory.exists() && !directory.isDirectory()) { + logger.error("[" + dest + "] 不是一个文件夹!"); + } + + FileUtils.forceMkdir(directory); + + for (File file : files) { + copyFile(file, new File(dest + File.separator + file.getName())); + } + } + + public static void copyFile(File in, File out) throws IOException { + copyFile(Files.newInputStream(in.toPath()), out); + } + + public static void copyFile(InputStream input, File out) throws IOException { + OutputStream output = null; + try { + output = Files.newOutputStream(out.toPath()); + byte[] buf = new byte[1024]; + int bytesRead; + while ((bytesRead = input.read(buf)) > 0) { + output.write(buf, 0, bytesRead); + } + } catch (IOException e) { + logger.error("", e); + } finally { + if (output != null) input.close(); + if (output != null) output.close(); + } + } + + /** + * 获取文件主名称 + * + * @param file 文件对象 + * @return 文件名称 + */ + public static String mainName(File file) { + //忽略判断 + String fileName = file.getName(); + return fileName.substring(0, fileName.lastIndexOf(".")); + } + + public static List listFileNames(String path) { + Collection files = FileUtils.listFiles(new File(path), CanReadFileFilter.CAN_READ, null); + return files.stream().map(File::getName).collect(Collectors.toList()); + } + + /** + * 返回被查找到的文件的绝对路径(匹配到一个就返回) + * + * @param root 根目录文件 + * @param fileName 要找的文件名 + * @return 绝对路径 + */ + private static String findFiles(File root, String fileName) { + //定义一个返回值 + String path = null; + //如果传进来的是目录,并且存在 + if (root.exists() && root.isDirectory()) { + //遍历文件夹中的各个文件 + File[] files = root.listFiles(); + if (files != null) { + for (File file : files) { + //如果path的值没有变化 + if (path == null) { + if (file.isFile() && file.getName().contains(fileName)) { + path = file.getAbsolutePath(); + } else { + path = findFiles(file, fileName); + } + } else { + break;//跳出循环,增加性能 + } + } + } + } + return path; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/FxmlUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/FxmlUtil.java new file mode 100644 index 0000000..1061b96 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/FxmlUtil.java @@ -0,0 +1,26 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import javafx.fxml.FXMLLoader; +import javafx.fxml.JavaFXBuilderFactory; + +import java.nio.charset.StandardCharsets; + +/** + * FXML 工具 + * + * @author octopus_yan@foxmail.com + */ +public class FxmlUtil { + + public static FXMLLoader load(String name) { + String prefix = "/fxml/"; + String suffix = ".fxml"; + return new FXMLLoader( + FxmlUtil.class.getResource(prefix + name + suffix), + null, + new JavaFXBuilderFactory(), + null, + StandardCharsets.UTF_8 + ); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/Loading.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/Loading.java new file mode 100644 index 0000000..a444ca8 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/Loading.java @@ -0,0 +1,83 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +/** + * 加载等待弹窗 + * + * @author octopus_yan@foxmail.com + */ +public class Loading { + protected Stage stage; + protected StackPane root; + protected Label messageLb; + protected ImageView loadingView = new ImageView(new Image("https://blog-static.cnblogs.com/files/miaoqx/loading.gif")); + + public Loading(Stage owner) { + + messageLb = new Label("请耐心等待..."); + messageLb.setFont(Font.font(20)); + + root = new StackPane(); + root.setMouseTransparent(true); + root.setPrefSize(owner.getWidth(), owner.getHeight()); + root.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.3), null, null))); + root.getChildren().addAll(loadingView, messageLb); + + Scene scene = new Scene(root); + scene.setFill(Color.TRANSPARENT); + + stage = new Stage(); + stage.setX(owner.getX()); + stage.setY(owner.getY()); + stage.setScene(scene); + stage.setResizable(false); + stage.initOwner(owner); + stage.initStyle(StageStyle.TRANSPARENT); + stage.initModality(Modality.APPLICATION_MODAL); + stage.getIcons().addAll(owner.getIcons()); + stage.setX(owner.getX()); + stage.setY(owner.getY()); + stage.setHeight(owner.getHeight()); + stage.setWidth(owner.getWidth()); + } + + // 更改信息 + public Loading showMessage(String message) { + Platform.runLater(() -> messageLb.setText(message)); + return this; + } + + // 更改信息 + public Loading image(Image image) { + Platform.runLater(() -> loadingView.imageProperty().set(image)); + return this; + } + + // 显示 + public void show() { + Platform.runLater(() -> stage.show()); + } + + // 关闭 + public void closeStage() { + Platform.runLater(() -> stage.close()); + } + + // 是否正在展示 + public boolean showing() { + return stage.isShowing(); + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/ProcessesUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/ProcessesUtil.java new file mode 100644 index 0000000..5b67cab --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/ProcessesUtil.java @@ -0,0 +1,96 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import org.apache.commons.exec.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * 命令工具类 + * + * @author octopus_yan@foxmail.com + */ +public class ProcessesUtil { + private static final Logger logger = LoggerFactory.getLogger(ProcessesUtil.class); + private static final String NEWLINE = System.lineSeparator(); + private static final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler(); + + public static boolean exec(String command) { + try { + exec(command, new OnExecuteListener() { + @Override + public void onExecute(String msg) { + + } + + @Override + public void onExecuteSuccess(int exitValue) { + + } + + @Override + public void onExecuteError(Exception e) { + + } + + @Override + public void onExecuteOver() { + + } + }); + handler.waitFor(); + } catch (Exception e) { + logger.error("", e); + } + return 0 == handler.getExitValue(); + } + + public static void exec(String command, OnExecuteListener listener) { + LogOutputStream logout = new LogOutputStream() { + @Override + protected void processLine(String line, int logLevel) { + if (listener != null) listener.onExecute(line + NEWLINE); + } + }; + + CommandLine commandLine = CommandLine.parse(command); + DefaultExecutor executor = DefaultExecutor.builder().get(); + executor.setStreamHandler(new PumpStreamHandler(logout, logout)); + DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() { + @Override + public void onProcessComplete(int exitValue) { + if (listener != null) { + listener.onExecuteSuccess(exitValue); + } + } + + @Override + public void onProcessFailed(ExecuteException e) { + if (listener != null) { + listener.onExecuteError(e); + } + } + }; + try { + executor.execute(commandLine, handler); + } catch (IOException e) { + if (listener != null) listener.onExecuteError(e); + } + } + + public interface OnExecuteListener { + void onExecute(String msg); + + void onExecuteSuccess(int exitValue); + + void onExecuteError(Exception e); + void onExecuteOver(); + } + + /** + * Prevent construction. + */ + private ProcessesUtil() { + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/PropertiesUtils.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/PropertiesUtils.java new file mode 100644 index 0000000..1616515 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/PropertiesUtils.java @@ -0,0 +1,85 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * 配置文件读取工具 + * + * @author liubin5620 + * @see 配置文件信息读取工具类【PropertiesUtils】 + */ +public class PropertiesUtils { + /** + * 主配置文件 + */ + private Properties properties; + /** + * 启用配置文件 + */ + private Properties propertiesCustom; + + private static PropertiesUtils propertiesUtils = new PropertiesUtils(); + + /** + * 私有构造,禁止直接创建 + */ + private PropertiesUtils() { + // 读取配置启用的配置文件名 + properties = new Properties(); + propertiesCustom = new Properties(); + InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("application.properties"); + try { + properties.load(in); + // 加载启用的配置 + String property = properties.getProperty("profiles.active"); + if (!StringUtils.isBlank(property)) { + InputStream cin = PropertiesUtils.class.getClassLoader().getResourceAsStream("application-" + property + ".properties"); + propertiesCustom.load(cin); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 获取单例 + * + * @return PropertiesUtils + */ + public static PropertiesUtils getInstance() { + if (propertiesUtils == null) { + propertiesUtils = new PropertiesUtils(); + } + return propertiesUtils; + } + + /** + * 获取单例 + * + * @return PropertiesUtils + */ + public static PropertiesUtils getInstance(File file) { + PropertiesUtils util = new PropertiesUtils(); + + return util; + } + + /** + * 根据属性名读取值 + * 先去主配置查询,如果查询不到,就去启用配置查询 + * + * @param name 名称 + */ + public String getProperty(String name) { + String val = properties.getProperty(name); + if (StringUtils.isBlank(val)) { + val = propertiesCustom.getProperty(name); + } + return val; + } +} diff --git a/src/main/java/cn/octopusyan/dayzmodtranslator/util/TooltipUtil.java b/src/main/java/cn/octopusyan/dayzmodtranslator/util/TooltipUtil.java new file mode 100644 index 0000000..6c1b629 --- /dev/null +++ b/src/main/java/cn/octopusyan/dayzmodtranslator/util/TooltipUtil.java @@ -0,0 +1,113 @@ +package cn.octopusyan.dayzmodtranslator.util; + +import javafx.beans.value.ChangeListener; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.stage.Window; + +/** + * 提示工具 + * + * @author octopus_yan@foxmail.com + */ +public class TooltipUtil { + private static TooltipUtil util; + private final Tooltip tooltip = new Tooltip(); + private Window owner; + private ChangeListener 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/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..dd766d1 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,22 @@ +module cn.octopusyan.dayzmodtranslator { + requires java.net.http; + requires javafx.controls; + requires javafx.fxml; + requires javafx.swing; + requires org.apache.commons.io; + requires org.apache.commons.lang3; + requires org.apache.commons.exec; + requires org.slf4j; + requires ch.qos.logback.core; + requires ch.qos.logback.classic; + requires com.alibaba.fastjson2; + + exports cn.octopusyan.dayzmodtranslator; + opens cn.octopusyan.dayzmodtranslator to javafx.fxml; + exports cn.octopusyan.dayzmodtranslator.base; + opens cn.octopusyan.dayzmodtranslator.base to javafx.fxml; + exports cn.octopusyan.dayzmodtranslator.controller; + opens cn.octopusyan.dayzmodtranslator.controller to javafx.fxml; + exports cn.octopusyan.dayzmodtranslator.manager.word; + opens cn.octopusyan.dayzmodtranslator.manager.word to javafx.base; +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..cf1c3e0 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,3 @@ +app.name=${project.name} +app.title=DayZ\u6A21\u7EC4\u6C49\u5316\u5DE5\u5177 +app.version=${project.version} \ No newline at end of file diff --git a/src/main/resources/css/main-view.css b/src/main/resources/css/main-view.css new file mode 100644 index 0000000..9424355 --- /dev/null +++ b/src/main/resources/css/main-view.css @@ -0,0 +1,9 @@ +#root{ + -fx-background-color: white; +} + +#dragFileLabel { + -fx-font-family: "Microsoft YaHei"; + -fx-text-fill: black; + -fx-font-size: 20; +} \ No newline at end of file diff --git a/src/main/resources/css/root.css b/src/main/resources/css/root.css new file mode 100644 index 0000000..e23bc1d --- /dev/null +++ b/src/main/resources/css/root.css @@ -0,0 +1,10 @@ +.box_class { + -fx-background-radius: 5; + -fx-border-style: solid; + -fx-border-color: rgba(136, 136, 136, 0.5); + -fx-background-color: white; +} + +.conf_menu_item { + -fx-pref-width: 100; +} \ No newline at end of file diff --git a/src/main/resources/fxml/main-view.fxml b/src/main/resources/fxml/main-view.fxml new file mode 100644 index 0000000..6c80a54 --- /dev/null +++ b/src/main/resources/fxml/main-view.fxml @@ -0,0 +1,73 @@ + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/translate-view.fxml b/src/main/resources/fxml/translate-view.fxml new file mode 100644 index 0000000..ed2af77 --- /dev/null +++ b/src/main/resources/fxml/translate-view.fxml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..abc5348 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + ${CHARSET} + + + + + + + + ${logback.logdir}/${logback.app}.info.log + + + ${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.info.log + + 30 + + 1GB + + + + ${CHARSET} + %d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n + + + + INFO + ACCEPT + DENY + + + + + + + ${logback.logdir}/${logback.app}.debug.log + + + ${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.debug.log + + 30 + + 1GB + + + + UTF-8 + %d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n + + + + DEBUG + ACCEPT + DENY + + + + + + ${logback.logdir}/${logback.app}.err.log + + ${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.err.log + 7 + 1GB + + + UTF-8 + %d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/CfgConvert/CfgConvert.exe b/src/main/resources/static/CfgConvert/CfgConvert.exe new file mode 100644 index 0000000..27106b9 Binary files /dev/null and b/src/main/resources/static/CfgConvert/CfgConvert.exe differ diff --git a/src/main/resources/static/pboc/Qt6Core.dll b/src/main/resources/static/pboc/Qt6Core.dll new file mode 100644 index 0000000..615c4a3 Binary files /dev/null and b/src/main/resources/static/pboc/Qt6Core.dll differ diff --git a/src/main/resources/static/pboc/pboc.exe b/src/main/resources/static/pboc/pboc.exe new file mode 100644 index 0000000..953bef8 Binary files /dev/null and b/src/main/resources/static/pboc/pboc.exe differ