pref: 更新界面、添加翻译进度反馈、展示日志信息

feat: 添加百度free翻译接口
This commit is contained in:
2024-11-15 01:55:34 +08:00
parent 50032cc599
commit 943056168f
1099 changed files with 7386 additions and 4106 deletions

View File

@ -1,19 +0,0 @@
package cn.octopusyan.dayzmodtranslator;
/**
* 启动类
*
* @author octopus_yan@foxmail.com
*/
public class AppLuncher {
public static void main(String[] args) {
// try {
// Runtime.getRuntime().exec("Taskkill /IM " + FrpManager.FRPC_CLIENT_FILE_NAME + " /f");
// } catch (IOException e) {
// e.printStackTrace();
// }
Application.launch(Application.class, args);
}
}

View File

@ -1,105 +0,0 @@
package cn.octopusyan.dayzmodtranslator;
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
import cn.octopusyan.dayzmodtranslator.manager.http.HttpConfig;
import cn.octopusyan.dayzmodtranslator.controller.MainController;
import cn.octopusyan.dayzmodtranslator.manager.CfgConvertUtil;
import cn.octopusyan.dayzmodtranslator.manager.PBOUtil;
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.util.Objects;
public class Application extends javafx.application.Application {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
@Override
public void init() throws Exception {
logger.info("application init ...");
}
@Override
public void start(Stage stage) throws IOException {
logger.info("application start ...");
// bin转换 工具初始化
CfgConvertUtil.init();
// PBO 工具初始化
PBOUtil.init(CfgConvertUtil.getInstance());
// 客户端配置初始化
CustomConfig.init();
// 初始化弹窗工具
AlertUtil.initOwner(stage);
// http请求工具初始化
HttpConfig httpConfig = new HttpConfig();
if (CustomConfig.hasProxy()) {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(CustomConfig.proxyHost(), CustomConfig.proxyPort());
httpConfig.setProxySelector(ProxySelector.of(unresolved));
}
httpConfig.setConnectTimeout(10);
HttpUtil.init(httpConfig);
// TODO 全局异常处理
Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
// 启动主界面
try {
FXMLLoader fxmlLoader = FxmlUtil.load("main-view");
VBox root = fxmlLoader.load();//底层面板
Scene scene = new Scene(root);
scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
stage.setScene(scene);
stage.setMinHeight(330);
stage.setMinWidth(430);
stage.setMaxWidth(Double.MAX_VALUE);
stage.setMaxHeight(Double.MAX_VALUE);
stage.setTitle(AppConstant.APP_TITLE + " v" + AppConstant.APP_VERSION);
stage.show();
MainController controller = fxmlLoader.getController();
controller.setApplication(this);
} catch (Throwable t) {
showErrorDialog(Thread.currentThread(), t);
}
logger.info("application start over ...");
}
private void showErrorDialog(Thread t, Throwable e) {
logger.error("", e);
AlertUtil.exceptionAlert(new Exception(e)).show();
}
@Override
public void stop() throws Exception {
logger.info("application stop ...");
// 清除翻译任务
TranslateUtil.getInstance().clear();
// 停止所有线程
ThreadPoolManager.getInstance().shutdown();
// 保存应用数据
CustomConfig.store();
// 清理缓存
PBOUtil.clear();
}
}

View File

@ -1,217 +0,0 @@
package cn.octopusyan.dayzmodtranslator.base;
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
import cn.octopusyan.dayzmodtranslator.util.Loading;
import cn.octopusyan.dayzmodtranslator.util.TooltipUtil;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import java.util.ResourceBundle;
/**
* 通用视图控制器基类
*
* @author octopus_yan@foxmail.com
*/
public abstract class BaseController<P extends Pane> implements Initializable {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private Application application;
private double xOffSet = 0, yOffSet = 0;
private volatile Loading loading;
protected TooltipUtil tooltipUtil;
public void jumpTo(BaseController<P> controller) throws IOException {
FXMLLoader fxmlLoader = FxmlUtil.load(controller.getRootFxml());
Scene scene = getRootPanel().getScene();
double oldHeight = getRootPanel().getPrefHeight();
double oldWidth = getRootPanel().getPrefWidth();
Pane root = fxmlLoader.load();
Stage stage = (Stage) scene.getWindow();
// 窗口大小
double newWidth = root.getPrefWidth();
double newHeight = root.getPrefHeight();
// 窗口位置
double newX = stage.getX() - (newWidth - oldWidth) / 2;
double newY = stage.getY() - (newHeight - oldHeight) / 2;
scene.setRoot(root);
stage.setX(newX < 0 ? 0 : newX);
stage.setY(newY < 0 ? 0 : newY);
stage.setWidth(newWidth);
stage.setHeight(newHeight);
controller = fxmlLoader.getController();
controller.setApplication(getApplication());
}
protected void open(Class<? extends BaseController<?>> clazz, String title) {
try {
FXMLLoader load = FxmlUtil.load(clazz.getDeclaredConstructor().newInstance().getRootFxml());
Parent root = load.load();
Scene scene = new Scene(root);
scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
Stage stage = new Stage();
stage.setScene(scene);
stage.setTitle(title);
stage.initOwner(getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.show();
load.getController();
} catch (Exception e) {
logger.error("", e);
}
}
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// 全局窗口拖拽
if (dragWindow()) {
// 窗口拖拽
getRootPanel().setOnMousePressed(event -> {
xOffSet = event.getSceneX();
yOffSet = event.getSceneY();
});
getRootPanel().setOnMouseDragged(event -> {
Stage stage = (Stage) getWindow();
stage.setX(event.getScreenX() - xOffSet);
stage.setY(event.getScreenY() - yOffSet);
});
}
// 窗口初始化完成监听
getRootPanel().sceneProperty().addListener((observable, oldValue, newValue) -> {
newValue.windowProperty().addListener(new ChangeListener<Window>() {
@Override
public void changed(ObservableValue<? extends Window> observable, Window oldValue, Window newValue) {
//关闭窗口监听
getWindow().setOnCloseRequest(windowEvent -> onDestroy());
// app 版本信息
if (getAppVersionLabel() != null) getAppVersionLabel().setText("v" + AppConstant.APP_VERSION);
// 初始化数据
initData();
// 初始化视图样式
initViewStyle();
// 初始化视图事件
initViewAction();
}
});
});
}
public void showLoading() {
showLoading(null);
}
public void showLoading(String message) {
if (loading == null) loading = new Loading((Stage) getWindow());
if (StringUtils.isNotEmpty(message)) loading.showMessage(message);
loading.show();
}
public void setApplication(Application application) {
this.application = application;
}
public Application getApplication() {
return application;
}
public boolean isLoadShowing() {
return loading != null && loading.showing();
}
public void stopLoading() {
if (isLoadShowing())
loading.closeStage();
}
protected TooltipUtil getTooltipUtil() {
if (tooltipUtil == null) tooltipUtil = TooltipUtil.getInstance(getRootPanel());
return tooltipUtil;
}
/**
* 窗口拖拽设置
*
* @return 是否启用
*/
public abstract boolean dragWindow();
/**
* 获取根布局
*
* @return 根布局对象
*/
public abstract P getRootPanel();
/**
* 获取根布局
* <p> 搭配 <code>FxmlUtil.load</code> 使用
*
* @return 根布局对象
* @see cn.octopusyan.dayzmodtranslator.util.FxmlUtil#load(String)
*/
public abstract String getRootFxml();
protected Window getWindow() {
return getRootPanel().getScene().getWindow();
}
/**
* App版本信息标签
*/
public Label getAppVersionLabel() {
return null;
}
/**
* 初始化数据
*/
public abstract void initData();
/**
* 视图样式
*/
public abstract void initViewStyle();
/**
* 视图事件
*/
public abstract void initViewAction();
/**
* 关闭窗口
*/
public void onDestroy() {
Stage stage = (Stage) getWindow();
stage.hide();
stage.close();
}
}

View File

@ -1,21 +0,0 @@
package cn.octopusyan.dayzmodtranslator.config;
import cn.octopusyan.dayzmodtranslator.util.PropertiesUtils;
import org.apache.commons.io.FileUtils;
import java.io.File;
/**
* 应用信息
*
* @author octopus_yan@foxmail.com
*/
public class AppConstant {
public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
public static final String DATA_DIR_PATH = System.getProperty("user.home") + File.separator + "AppData" + File.separator + "Local" + File.separator + APP_NAME;
public static final String TMP_DIR_PATH = FileUtils.getTempDirectoryPath() + APP_NAME;
public static final String CUSTOM_CONFIG_PATH = DATA_DIR_PATH + File.separator + "config.properties";
public static final String BAK_FILE_PATH = AppConstant.TMP_DIR_PATH + File.separator + "bak";
}

View File

@ -1,174 +0,0 @@
package cn.octopusyan.dayzmodtranslator.config;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
/**
* 客户端设置
*
* @author octopus_yan@foxmail.com
*/
public class CustomConfig {
private static final Logger logger = LoggerFactory.getLogger(CustomConfig.class);
private static final Properties properties = new Properties();
public static final String PROXY_HOST_KEY = "proxy.host";
public static final String PROXY_PORT_KEY = "proxy.port";
public static final String TRANSLATE_SOURCE_KEY = "translate.source";
public static final String TRANSLATE_SOURCE_APPID_KEY = "translate.{}.appid";
public static final String TRANSLATE_SOURCE_APIKEY_KEY = "translate.{}.apikey";
public static final String TRANSLATE_SOURCE_QPS_KEY = "translate.{}.qps";
public static void init() {
File customConfigFile = new File(AppConstant.CUSTOM_CONFIG_PATH);
try {
if (!customConfigFile.exists()) {
// 初始配置
properties.put(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName());
// 保存配置文件
store();
} else {
properties.load(new FileInputStream(customConfigFile));
}
} catch (IOException ignore) {
logger.error("读取配置文件失败");
}
}
/**
* 是否配置代理
*/
public static boolean hasProxy() {
String host = proxyHost();
Integer port = proxyPort();
return StringUtils.isNoneBlank(host) && null != port;
}
/**
* 代理地址
*/
public static String proxyHost() {
return properties.getProperty(PROXY_HOST_KEY);
}
/**
* 代理地址
*/
public static void proxyHost(String host) {
properties.setProperty(PROXY_HOST_KEY, host);
}
/**
* 代理端口
*/
public static Integer proxyPort() {
try {
return Integer.parseInt(properties.getProperty(PROXY_PORT_KEY));
} catch (Exception ignored) {
}
return null;
}
/**
* 代理端口
*/
public static void proxyPort(int port) {
properties.setProperty(PROXY_PORT_KEY, String.valueOf(port));
}
/**
* 翻译源
*/
public static TranslateSource translateSource() {
String name = properties.getProperty(TRANSLATE_SOURCE_KEY, TranslateSource.FREE_GOOGLE.getName());
return TranslateSource.get(name);
}
/**
* 翻译源
*/
public static void translateSource(TranslateSource source) {
properties.setProperty(TRANSLATE_SOURCE_KEY, source.getName());
}
/**
* 是否配置接口认证
*
* @param source 翻译源
*/
public static boolean hasTranslateApiKey(TranslateSource source) {
return StringUtils.isNoneBlank(translateSourceAppid(source))
&& StringUtils.isNoneBlank(translateSourceApikey(source));
}
/**
* 设置翻译源appid
*
* @param source 翻译源
* @param appid appid
*/
public static void translateSourceAppid(TranslateSource source, String appid) {
properties.setProperty(getTranslateSourceAppidKey(source), appid);
}
/**
* 获取翻译源appid
*
* @param source 翻译源
* @return appid
*/
public static String translateSourceAppid(TranslateSource source) {
return properties.getProperty(getTranslateSourceAppidKey(source));
}
public static void translateSourceApikey(TranslateSource source, String apikey) {
properties.setProperty(getTranslateSourceApikeyKey(source), apikey);
}
public static String translateSourceApikey(TranslateSource source) {
return properties.getProperty(getTranslateSourceApikeyKey(source));
}
public static Integer translateSourceQps(TranslateSource source) {
String qpsStr = properties.getProperty(getTranslateSourceQpsKey(source));
return qpsStr == null ? source.getDefaultQps() : Integer.parseInt(qpsStr);
}
public static void translateSourceQps(TranslateSource source, int qps) {
properties.setProperty(getTranslateSourceQpsKey(source), String.valueOf(qps));
}
/**
* 保存配置
*/
public static void store() {
// 生成配置文件
try {
properties.store(new PrintStream(AppConstant.CUSTOM_CONFIG_PATH), String.valueOf(StandardCharsets.UTF_8));
} catch (IOException e) {
logger.error("保存客户端配置失败", e);
}
}
private static String getTranslateSourceAppidKey(TranslateSource source) {
return StringUtils.replace(TRANSLATE_SOURCE_APPID_KEY, "{}", source.getName());
}
private static String getTranslateSourceApikeyKey(TranslateSource source) {
return StringUtils.replace(TRANSLATE_SOURCE_APIKEY_KEY, "{}", source.getName());
}
private static String getTranslateSourceQpsKey(TranslateSource source) {
return StringUtils.replace(TRANSLATE_SOURCE_QPS_KEY, "{}", source.getName());
}
}

View File

@ -1,515 +0,0 @@
package cn.octopusyan.dayzmodtranslator.controller;
import cn.octopusyan.dayzmodtranslator.base.BaseController;
import cn.octopusyan.dayzmodtranslator.manager.PBOUtil;
import cn.octopusyan.dayzmodtranslator.manager.file.FileTreeItem;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem;
import cn.octopusyan.dayzmodtranslator.manager.word.WordItem;
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
import cn.octopusyan.dayzmodtranslator.util.ClipUtil;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.*;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.regex.Pattern;
/**
* 主控制器
*
* @author octopus_yan@foxmail.com
*/
public class MainController extends BaseController<VBox> {
public VBox root;
public MenuItem openFileSetupBtn;
public MenuItem translateSetupBtn;
public MenuItem proxySetupBtn;
public Label filePath;
public StackPane fileBox;
public VBox openFileBox;
public Button openFile;
public VBox dragFileBox;
public Label dragFileLabel;
public VBox loadFileBox;
public Label loadFileLabel;
public ProgressBar loadFileProgressBar;
public TreeView<String> treeFileBox;
public StackPane wordBox;
public TableView<WordItem> wordTableBox;
public VBox wordMsgBox;
public Label wordMsgLabel;
public ProgressBar loadWordProgressBar;
public Button translateWordBtn;
public Button packBtn;
private final PBOUtil pboUtil = PBOUtil.getInstance();
private final TranslateUtil translateUtil = TranslateUtil.getInstance();
/**
* 翻译标志 用于停止翻译
*/
private AtomicBoolean transTag;
/**
* 已翻译文本下标缓存
*/
private Set<Integer> transNum;
@Override
public boolean dragWindow() {
return false;
}
@Override
public VBox getRootPanel() {
return root;
}
@Override
public String getRootFxml() {
return "main-view";
}
/**
* 初始化数据
*/
@Override
public void initData() {
// 解包监听
pboUtil.setOnUnpackListener(new PBOUtil.OnUnpackListener() {
@Override
public void onStart() {
refreshWordBox();
}
@Override
public void onUnpackSuccess(String unpackDirPath) {
loadFileLabel.textProperty().setValue("加载完成,正在获取文件目录");
// 展示解包文件内容
logger.info("正在获取文件目录。。");
showDirectory(new File(unpackDirPath));
// 展示可翻译语句
logger.info("正在查询待翻译文本目录。。");
showTranslateWord();
}
@Override
public void onUnpackError(String msg) {
loadFileLabel.textProperty().setValue("打开文件失败");
logger.info("打开文件失败: \n" + msg);
}
@Override
public void onUnpackOver() {
}
});
// 打包监听
pboUtil.setOnPackListener(new PBOUtil.OnPackListener() {
@Override
public void onStart() {
showLoading("正在打包pbo文件");
}
@Override
public void onProgress(long current, long all) {
showLoading(String.format("正在打包pbo文件(%d / %d)", current, all));
}
@Override
public void onPackSuccess(File packFile) {
// 选择文件保存地址
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
fileChooser.getExtensionFilters().add(extFilter);
File file = fileChooser.showSaveDialog(getWindow());
if (file == null)
return;
if (file.exists()) {
//文件已存在,则删除覆盖文件
FileUtils.deleteQuietly(file);
}
String exportFilePath = file.getAbsolutePath();
logger.info("导出文件的路径 =>" + exportFilePath);
try {
FileUtils.copyFile(packFile, file);
} catch (IOException e) {
logger.error("保存文件失败!", e);
Platform.runLater(() -> AlertUtil.exception(e).content("保存文件失败!").show());
}
}
@Override
public void onPackError(String msg) {
AlertUtil.error("保存文件失败!").show();
logger.info("保存文件失败: \n" + msg);
}
@Override
public void onPackOver() {
stopLoading();
}
});
// 获取待翻译文字
pboUtil.setOnFindTransWordListener((words, isOver) -> {
loadWordProgressBar.setVisible(false);
if (words == null || words.isEmpty()) {
if (isOver) {
wordMsgLabel.textProperty().set("未找到待翻译文本");
}
} else {
// 展示翻译按钮
translateWordBtn.setVisible(true);
// 展示打包按钮
packBtn.setVisible(true);
// 绑定TableView
boolean isCsvItem = (words.get(0) instanceof WordCsvItem);
bindWordTable(words, isCsvItem);
}
});
}
/**
* 视图样式
*/
@Override
public void initViewStyle() {
wordMsgLabel.textProperty().setValue("请打开PBO文件");
}
/**
* 视图事件
*/
@Override
public void initViewAction() {
// 翻译设置
translateSetupBtn.setOnAction(event -> open(SetupTranslateController.class, "翻译源设置"));
// 代理设置
proxySetupBtn.setOnAction(event -> open(SetupProxyController.class, "代理设置"));
// 选择pbo文件
EventHandler<ActionEvent> selectPboFileAction = actionEvent -> {
// 文件选择器
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
fileChooser.getExtensionFilters().add(extFilter);
selectFile(fileChooser.showOpenDialog(getWindow()));
};
openFileSetupBtn.setOnAction(selectPboFileAction);
openFile.setOnAction(selectPboFileAction);
// 拖拽效果 start ---------------------
fileBox.setOnDragEntered(dragEvent -> {
Dragboard dragboard = dragEvent.getDragboard();
if (dragboard.hasFiles() && isPboFile(dragboard.getFiles().get(0))) {
disableBox();
dragFileBox.setVisible(true);
}
});
fileBox.setOnDragExited(dragEvent -> {
if (!loadFileBox.isVisible()) {
disableBox();
openFileBox.setVisible(true);
}
});
fileBox.setOnDragOver(dragEvent -> {
Dragboard dragboard = dragEvent.getDragboard();
if (dragEvent.getGestureSource() != fileBox && dragboard.hasFiles()) {
/* allow for both copying and moving, whatever user chooses */
dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
dragEvent.consume();
});
fileBox.setOnDragDropped(dragEvent -> {
disableBox();
openFileBox.setVisible(true);
Dragboard db = dragEvent.getDragboard();
boolean success = false;
File file = db.getFiles().get(0);
if (db.hasFiles() && isPboFile(file)) {
selectFile(file);
success = true;
}
/* 让源知道字符串是否已成功传输和使用 */
dragEvent.setDropCompleted(success);
dragEvent.consume();
});
// 拖拽效果 end ---------------------
// 翻译按钮
translateWordBtn.setOnMouseClicked(mouseEvent -> {
// 是否初次翻译
if (transTag == null) {
transNum = new HashSet<>();
transTag = new AtomicBoolean(true);
// 开始翻译
startTranslate();
} else {
// 获取翻译列表
ObservableList<WordItem> items = wordTableBox.getItems();
// 未获取到翻译列表 或 翻译完成 则不做处理
if (items == null || items.isEmpty() || transNum.size() == items.size())
return;
// 设置翻译标识
transTag.set(!transTag.get());
if (Boolean.FALSE.equals(transTag.get())) {
stopTranslate();
} else {
startTranslate();
}
}
});
// 打包按钮
packBtn.setOnAction(event -> pboUtil.pack(wordTableBox.getItems()));
// 复制文本
getRootPanel().getScene().getAccelerators()
.put(new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY), new Runnable() {
@Override
public void run() {
TablePosition tablePosition = wordTableBox.getSelectionModel().getSelectedCells().get(0);
Object cellData = tablePosition.getTableColumn().getCellData(tablePosition.getRow());
ClipUtil.setClip(String.valueOf(cellData));
}
});
}
/**
* 开始翻译
*/
private void startTranslate() {
// 获取翻译列表
ObservableList<WordItem> items = wordTableBox.getItems();
if (items == null || items.isEmpty()) return;
// 开始/继续 翻译
String label = translateWordBtn.getText().replaceAll("已暂停|一键翻译", "正在翻译");
translateWordBtn.textProperty().setValue(label);
// 禁用打包按钮
packBtn.setDisable(true);
boolean isCsvItem = (items.get(0) instanceof WordCsvItem);
// 循环提交翻译任务
for (int i = 0; i < items.size(); i++) {
// 跳过已翻译文本
if (transNum.contains(i)) continue;
WordItem item = items.get(i);
// 提交翻译任务
int finalI = i;
translateUtil.translate(finalI, item.getOriginal(), new TranslateUtil.OnTranslateListener() {
@Override
public void onTranslate(String result) {
// 防止多线程执行时停止不及时
if (Boolean.FALSE.equals(transTag.get())) {
return;
}
// 含有中文则不翻译
if (!containsChinese(item.getChinese()))
item.setChinese(result);
// 设置简中文本
if (isCsvItem) {
WordCsvItem csvItem = ((WordCsvItem) item);
if (!containsChinese(csvItem.getChineseSimp()))
csvItem.setChineseSimp(result);
}
// 设置翻译进度
transNum.add(finalI);
String label;
if (transNum.size() >= items.size()) {
label = "翻译完成(" + items.size() + ")";
transTag.set(false);
// 启用打包按钮
packBtn.setDisable(false);
} else {
label = "正在翻译(" + transNum.size() + "/" + items.size() + ")";
}
translateWordBtn.textProperty().setValue(label);
}
});
}
}
/**
* 停止翻译
*/
private void stopTranslate() {
// 清除未完成的翻译任务
translateUtil.clear();
// 设置翻译状态
String label = translateWordBtn.getText().replace("正在翻译", "已暂停");
translateWordBtn.textProperty().setValue(label);
// 启用打包按钮
packBtn.setDisable(false);
}
/**
* 选择待汉化pbo文件
* <p>TODO 多文件汉化
*
* @param file 待汉化文件
*/
private void selectFile(File file) {
if (file == null || !file.exists()) return;
filePath.textProperty().set(file.getName());
// 重置文件界面
disableBox();
loadFileBox.setVisible(true);
// 重置翻译文本状态
wordBox.getChildren().remove(wordTableBox);
wordTableBox = null;
loadFileLabel.textProperty().setValue("正在加载模组文件");
pboUtil.unpack(file);
}
/**
* 展示文件夹内容
*
* @param file 根目录
*/
private void showDirectory(File file) {
if (file == null || !file.exists() || !file.isDirectory()) {
return;
}
disableBox();
treeFileBox.setVisible(true);
// 加载pbo文件目录
FileTreeItem fileTreeItem = new FileTreeItem(file, File::listFiles);
treeFileBox.setRoot(fileTreeItem);
treeFileBox.setShowRoot(false);
}
/**
* 展示待翻译语句
*/
private void showTranslateWord() {
wordMsgLabel.textProperty().setValue("正在获取可翻译文本");
loadWordProgressBar.setVisible(true);
pboUtil.startFindWord();
}
/**
* 绑定表格数据
*
* @param words 单词列表
* @param isCsvItem 是否csv
*/
private void bindWordTable(List<WordItem> words, boolean isCsvItem) {
if (wordTableBox == null) {
wordTableBox = new TableView<>();
wordBox.getChildren().add(wordTableBox);
// 可编辑
wordTableBox.setEditable(true);
// 单元格选择模式而不是行选择
wordTableBox.getSelectionModel().setCellSelectionEnabled(true);
// 不允许选择多个单元格
wordTableBox.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
// 鼠标事件清空
wordTableBox.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.isControlDown()) {
return;
}
if (wordTableBox.getEditingCell() == null) {
wordTableBox.getSelectionModel().clearSelection();
}
});
// 创建列
wordTableBox.getColumns().add(createColumn("原始文本", WordItem::originalProperty));
wordTableBox.getColumns().add(createColumn("中文", WordItem::chineseProperty));
if (isCsvItem) {
wordTableBox.getColumns().add(createColumn("简体中文", WordCsvItem::chineseSimpProperty));
}
}
// 添加表数据
wordTableBox.getItems().addAll(words);
}
private <T extends WordItem> TableColumn<WordItem, String> createColumn(String colName, Function<T, StringProperty> colField) {
TableColumn<WordItem, String> tableColumn = new TableColumn<>(colName);
tableColumn.setCellValueFactory(features -> colField.apply((T) features.getValue()));
tableColumn.setCellFactory(TextFieldTableCell.forTableColumn());
tableColumn.setPrefWidth(150);
tableColumn.setSortable(false);
tableColumn.setEditable(!"原始文本".equals(colName));
return tableColumn;
}
private void disableBox() {
openFileBox.setVisible(false);
dragFileBox.setVisible(false);
loadFileBox.setVisible(false);
treeFileBox.setVisible(false);
}
private void refreshWordBox() {
if (wordTableBox != null) {
wordBox.getChildren().remove(wordTableBox);
wordTableBox = null;
}
wordMsgLabel.textProperty().setValue("请打开pbo文件");
loadWordProgressBar.setVisible(false);
translateWordBtn.textProperty().setValue("一键翻译");
translateWordBtn.setVisible(false);
packBtn.setVisible(false);
}
private boolean isPboFile(File file) {
if (file == null) return false;
return Pattern.compile(".*(.pbo)$").matcher(file.getName()).matches();
}
/**
* 给定字符串是否含有中文
*
* @param str 需要判断的字符串
* @return 是否含有中文
*/
private boolean containsChinese(String str) {
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
}
}

View File

@ -1,162 +0,0 @@
package cn.octopusyan.dayzmodtranslator.controller;
import cn.octopusyan.dayzmodtranslator.base.BaseController;
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
import cn.octopusyan.dayzmodtranslator.util.FxmlUtil;
import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.io.IOException;
import java.net.URI;
/**
* 应用配置
*
* @author octopus_yan@foxmail.com
*/
public class SetupProxyController extends BaseController<StackPane> {
public StackPane root;
public TextField hostField;
public TextField portField;
public TextField testPath;
public static final String PROXY_ERROR = "ProxyError";
/**
* 窗口拖拽设置
*
* @return 是否启用
*/
@Override
public boolean dragWindow() {
return false;
}
/**
* 获取根布局
*
* @return 根布局对象
*/
@Override
public StackPane getRootPanel() {
return root;
}
/**
* 获取根布局
* <p> 搭配 <code>FxmlUtil.load</code> 使用
*
* @return 根布局对象
* @see FxmlUtil#load(String)
*/
@Override
public String getRootFxml() {
return "proxy-view";
}
/**
* 初始化数据
*/
@Override
public void initData() {
// 是否已有代理配置
if (CustomConfig.hasProxy()) {
hostField.textProperty().setValue(CustomConfig.proxyHost());
portField.textProperty().setValue(String.valueOf(CustomConfig.proxyPort()));
}
// 默认测试地址
testPath.textProperty().setValue("https://translate.googleapis.com");
}
/**
* 视图样式
*/
@Override
public void initViewStyle() {
}
/**
* 视图事件
*/
@Override
public void initViewAction() {
}
private String getHost() {
String text = hostField.getText();
if (StringUtils.isBlank(text)) {
throw new RuntimeException(PROXY_ERROR);
}
try {
URI.create(text);
} catch (Exception e) {
throw new RuntimeException(PROXY_ERROR);
}
return text;
}
private int getPort() {
String text = portField.getText();
if (StringUtils.isBlank(text)) {
throw new RuntimeException();
}
boolean creatable = NumberUtils.isCreatable(text);
if (!creatable) {
throw new RuntimeException(PROXY_ERROR);
}
return Integer.parseInt(text);
}
private String getTestPath() {
String text = testPath.getText();
if (StringUtils.isBlank(text)) {
throw new RuntimeException(PROXY_ERROR);
}
return (text.startsWith("http") ? "" : "http://") + text;
}
/**
* 测试代理有效性
*/
public void test() {
HttpUtil.getInstance().clearProxy();
try {
String resp = HttpUtil.getInstance().proxy(getHost(), getPort())
.get(getTestPath(), null, null);
AlertUtil.info("成功").show();
} catch (IOException | InterruptedException e) {
logger.error("代理访问失败", e);
AlertUtil.error("失败!").show();
}
}
/**
* 保存代理配置
*/
public void save() {
CustomConfig.proxyHost(getHost());
CustomConfig.proxyPort(getPort());
CustomConfig.store();
onDestroy();
}
/**
* 取消
*/
public void close() {
HttpUtil.getInstance().clearProxy();
onDestroy();
}
}

View File

@ -1,127 +0,0 @@
package cn.octopusyan.dayzmodtranslator.controller;
import cn.octopusyan.dayzmodtranslator.base.BaseController;
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
import javafx.collections.ObservableList;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.apache.commons.lang3.StringUtils;
/**
* 翻译设置控制器
*
* @author octopus_yan@foxmail.com
*/
public class SetupTranslateController extends BaseController<StackPane> {
public StackPane root;
public ComboBox<TranslateSource> translateSourceCombo;
public TextField qps;
public VBox appidBox;
public TextField appid;
public VBox apikeyBox;
public TextField apikey;
@Override
public boolean dragWindow() {
return false;
}
@Override
public StackPane getRootPanel() {
return root;
}
@Override
public String getRootFxml() {
return "translate-view";
}
/**
* 初始化数据
*/
@Override
public void initData() {
// 翻译源
for (TranslateSource value : TranslateSource.values()) {
ObservableList<TranslateSource> items = translateSourceCombo.getItems();
items.addAll(value);
}
translateSourceCombo.setConverter(new StringConverter<>() {
@Override
public String toString(TranslateSource object) {
if (object == null) return null;
return object.getLabel();
}
@Override
public TranslateSource fromString(String string) {
return TranslateSource.getByLabel(string);
}
});
translateSourceCombo.getSelectionModel()
.selectedItemProperty()
.addListener((observable, oldValue, newValue) -> {
boolean needApiKey = newValue.needApiKey();
appidBox.setVisible(needApiKey);
apikeyBox.setVisible(needApiKey);
if (needApiKey) {
appid.textProperty().setValue(CustomConfig.translateSourceAppid(newValue));
apikey.textProperty().setValue(CustomConfig.translateSourceApikey(newValue));
}
qps.textProperty().setValue(String.valueOf(CustomConfig.translateSourceQps(newValue)));
});
// 当前翻译源
translateSourceCombo.getSelectionModel().select(CustomConfig.translateSource());
}
/**
* 视图样式
*/
@Override
public void initViewStyle() {
}
/**
* 视图事件
*/
@Override
public void initViewAction() {
qps.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
qps.setText(oldValue);
}
});
}
public void save() {
TranslateSource source = translateSourceCombo.getValue();
String apikey = this.apikey.getText();
String appid = this.appid.getText();
int qps = Integer.parseInt(this.qps.getText());
CustomConfig.translateSource(source);
if (source.needApiKey()) {
if (StringUtils.isBlank(apikey) || StringUtils.isBlank(appid)) {
AlertUtil.error("认证信息不能为空");
}
CustomConfig.translateSourceApikey(source, apikey);
CustomConfig.translateSourceAppid(source, appid);
CustomConfig.translateSourceQps(source, qps);
}
// 保存到文件
CustomConfig.store();
// 退出
onDestroy();
}
}

View File

@ -1,137 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager;
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
import cn.octopusyan.dayzmodtranslator.util.FileUtil;
import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
/**
* Cfg文件转换工具类
*
* @author octopus_yan@foxmail.com
*/
public class CfgConvertUtil {
private static final Logger logger = LoggerFactory.getLogger(CfgConvertUtil.class);
private static CfgConvertUtil util;
private static final String CfgConvert_DIR_PATH = AppConstant.DATA_DIR_PATH + File.separator + "CfgConvert";
private static final File CfgConvert_DIR = new File(CfgConvert_DIR_PATH);
private static final String CfgConvert_FILE_PATH = CfgConvert_DIR_PATH + File.separator + "CfgConvert.exe";
private static final File CfgConvert_FILE = new File(CfgConvert_FILE_PATH);
private static final String COMMAND = CfgConvert_FILE_PATH + " %s -dst %s %s";
private CfgConvertUtil() {
}
public static void init() {
if (util == null) {
util = new CfgConvertUtil();
}
// 检查pbo解析文件
util.checkCfgConvert();
}
public static synchronized CfgConvertUtil getInstance() {
if (util == null)
throw new RuntimeException("are you ready ?");
return util;
}
/**
* 检查Cfg转换文件
*/
private void checkCfgConvert() {
if (!CfgConvert_FILE.exists()) initCfgConvert();
}
private void initCfgConvert() {
try {
FileUtils.forceMkdir(CfgConvert_DIR);
String cfgConvertFileName = "CfgConvert.exe";
FileUtil.copyFile(Objects.requireNonNull(CfgConvertUtil.class.getResourceAsStream("/static/CfgConvert/" + cfgConvertFileName)), new File(CfgConvert_DIR_PATH + File.separator + cfgConvertFileName));
} catch (IOException e) {
logger.error("", e);
}
}
public void toTxt(File binFile, String outPath, OnToTxtListener onToTxtListener) {
String fileName = binFile.getAbsolutePath();
ProcessesUtil.exec(toTxtCommand(binFile, outPath), new ProcessesUtil.OnExecuteListener() {
@Override
public void onExecute(String msg) {
logger.info(fileName + " : " + msg);
}
@Override
public void onExecuteSuccess(int exitValue) {
logger.info(fileName + " : to txt success");
String outFilePath = outFilePath(binFile, outPath, ".cpp");
if (onToTxtListener != null) {
onToTxtListener.onToTxtSuccess(outFilePath);
}
}
@Override
public void onExecuteError(Exception e) {
logger.error(fileName + " : to txt error", e);
if (onToTxtListener != null) {
onToTxtListener.onToTxtError(e);
}
}
@Override
public void onExecuteOver() {
logger.info(fileName + " : to txt end...");
}
});
}
public void toBin(File cppFile) {
ProcessesUtil.exec(toBinCommand(cppFile, cppFile.getParentFile().getAbsolutePath()), new ProcessesUtil.OnExecuteListener() {
@Override
public void onExecute(String msg) {
}
@Override
public void onExecuteSuccess(int exitValue) {
}
@Override
public void onExecuteError(Exception e) {
}
@Override
public void onExecuteOver() {
}
});
}
private String toBinCommand(File cppFile, String outPath) {
String outFilePath = outFilePath(cppFile, outPath, ".bin");
return String.format(COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath());
}
private String toTxtCommand(File binFile, String outPath) {
String outFilePath = outFilePath(binFile, outPath, ".cpp");
return String.format(COMMAND, "-txt", outFilePath, binFile.getAbsolutePath());
}
private String outFilePath(File file, String outPath, String suffix) {
return outPath + File.separator + FileUtil.mainName(file) + suffix;
}
public interface OnToTxtListener {
void onToTxtSuccess(String txtFilePath);
void onToTxtError(Exception e);
}
}

View File

@ -1,628 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager;
import cn.octopusyan.dayzmodtranslator.config.AppConstant;
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
import cn.octopusyan.dayzmodtranslator.manager.word.WordCsvItem;
import cn.octopusyan.dayzmodtranslator.manager.word.WordItem;
import cn.octopusyan.dayzmodtranslator.util.AlertUtil;
import cn.octopusyan.dayzmodtranslator.util.FileUtil;
import cn.octopusyan.dayzmodtranslator.util.ProcessesUtil;
import javafx.application.Platform;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.io.filefilter.NameFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* PBO 工具类
*
* @author octopus_yan@foxmail.com
* @see <a href="https://github.com/winseros/pboman3">https://github.com/winseros/pboman3</a>
*/
public class PBOUtil {
private static final Logger logger = LoggerFactory.getLogger(PBOUtil.class);
private static PBOUtil util;
private static final String PBOC_DIR_PATH = AppConstant.DATA_DIR_PATH + File.separator + "pboman";
private static final File PBOC_DIR = new File(PBOC_DIR_PATH);
private static final String PBOC_FILE_PATH = PBOC_DIR_PATH + File.separator + "pboc.exe";
private static final File PBOC_FILE = new File(PBOC_FILE_PATH);
private static final String UNPACK_COMMAND = PBOC_FILE_PATH + " unpack -o " + AppConstant.TMP_DIR_PATH + " %s";
private static final String PACK_COMMAND = PBOC_FILE_PATH + " pack -o %s %s";
private OnPackListener onPackListener;
private OnUnpackListener onUnpackListener;
private OnFindTransWordListener onFindTransWordListener;
private String unpackPath;
private CfgConvertUtil cfgConvertUtil;
private static final String FILE_NAME_STRING_TABLE = "stringtable.csv";
private static final String FILE_NAME_CONFIG_BIN = "config.bin";
private static final String FILE_NAME_CONFIG_CPP = "config.cpp";
private PBOUtil() {
}
public static void init(CfgConvertUtil cfgConvertUtil) {
if (util == null) {
util = new PBOUtil();
}
// cfg转换工具
util.cfgConvertUtil = cfgConvertUtil;
// 检查pbo解析文件
util.checkPboc();
}
public static synchronized PBOUtil getInstance() {
if (util == null)
throw new RuntimeException("are you ready ?");
return util;
}
/**
* 设置打包监听器
*
* @param onPackListener 打包监听器
*/
public void setOnPackListener(OnPackListener onPackListener) {
this.onPackListener = onPackListener;
}
/**
* 设置解包监听器
*
* @param onUnpackListener 监听器
*/
public void setOnUnpackListener(OnUnpackListener onUnpackListener) {
this.onUnpackListener = onUnpackListener;
}
public void setOnFindTransWordListener(OnFindTransWordListener onFindTransWordListener) {
this.onFindTransWordListener = onFindTransWordListener;
}
private void checkPboc() {
if (!PBOC_FILE.exists()) initPboc();
}
private void initPboc() {
try {
FileUtils.forceMkdir(PBOC_DIR);
String pbocFileName = "pboc.exe";
String dllFileName = "Qt6Core.dll";
FileUtil.copyFile(Objects.requireNonNull(PBOUtil.class.getResourceAsStream("/static/pboc/" + pbocFileName)), new File(PBOC_DIR_PATH + File.separator + pbocFileName));
FileUtil.copyFile(Objects.requireNonNull(PBOUtil.class.getResourceAsStream("/static/pboc/" + dllFileName)), new File(PBOC_DIR_PATH + File.separator + dllFileName));
} catch (IOException e) {
logger.error("", e);
}
}
/**
* 解压PBO文件
*
* @param file PBO文件
*/
public void unpack(File file) {
// 检查pbo解包程序
checkPboc();
// 清理缓存
clear();
if (onUnpackListener != null) {
onUnpackListener.onStart();
}
String filePath = file.getAbsolutePath();
if (filePath.contains(" ")) filePath = "\"" + filePath + "\"";
String command = String.format(UNPACK_COMMAND, filePath);
logger.info(command);
try {
FileUtils.forceMkdir(new File(AppConstant.TMP_DIR_PATH));
// 执行命令
ProcessesUtil.exec(command, new ProcessesUtil.OnExecuteListener() {
@Override
public void onExecute(String msg) {
logger.info(msg);
}
@Override
public void onExecuteSuccess(int exitValue) {
if (exitValue != 0) {
String msg = "打开PBO文件失败";
logger.error(msg);
if (onUnpackListener != null) {
Platform.runLater(() -> {
onUnpackListener.onUnpackError(msg);
onUnpackListener.onUnpackOver();
});
}
return;
}
logger.info("打开PBO文件成功");
unpackPath = AppConstant.TMP_DIR_PATH + File.separator + FileUtil.mainName(file);
if (onUnpackListener != null) {
Platform.runLater(() -> {
onUnpackListener.onUnpackSuccess(unpackPath);
onUnpackListener.onUnpackOver();
});
}
}
@Override
public void onExecuteError(Exception e) {
logger.error("", e);
if (onUnpackListener != null) {
Platform.runLater(() -> {
onUnpackListener.onUnpackError(e.getMessage());
onUnpackListener.onUnpackOver();
});
}
}
@Override
public void onExecuteOver() {
if (onUnpackListener != null) {
Platform.runLater(() -> onUnpackListener.onUnpackOver());
}
}
});
} catch (Exception e) {
logger.error("", e);
if (onUnpackListener != null) {
Platform.runLater(() -> {
onUnpackListener.onUnpackError(e.getMessage());
onUnpackListener.onUnpackOver();
});
}
}
}
/**
* 获取待翻译单词列表
*/
public void startFindWord() {
// 检查pbo解包文件
if (unpackPath == null || StringUtils.isBlank(unpackPath))
throw new RuntimeException("No PBO file was obtained !");
ThreadPoolManager.getInstance().execute(() -> {
if (hasStringTable()) {
List<WordItem> worlds = new ArrayList<>(readCsvFile());
if (onFindTransWordListener != null) {
Platform.runLater(() -> onFindTransWordListener.onFoundWords(worlds, true));
}
} else {
findConfigWord();
}
});
}
/**
* 获取csv中 原文及 简中单词
*
* @return 待翻译语句列表
*/
private List<WordCsvItem> readCsvFile() {
List<WordCsvItem> list = new ArrayList<>();
AtomicInteger position = new AtomicInteger(0);
File stringTable = new File(unpackPath + File.separator + FILE_NAME_STRING_TABLE);
try (LineIterator it = FileUtils.lineIterator(stringTable, StandardCharsets.UTF_8.name())) {
while (it.hasNext()) {
String line = it.nextLine();
if (line.isEmpty() || line.startsWith("//") || line.startsWith("\"Language\"")) {
position.addAndGet(1);
continue;
}
// 原句
int startIndex = line.indexOf(",\"") + 2;
int endIndex = line.indexOf("\"", startIndex);
String original = line.substring(startIndex, endIndex);
// 中文
startIndex = StringUtils.ordinalIndexOf(line, ",\"", 11) + 2;
endIndex = line.indexOf("\"", startIndex);
int[] chinesePosition = new int[]{startIndex, endIndex};
String chinese = line.substring(startIndex, endIndex);
// 简中
startIndex = StringUtils.ordinalIndexOf(line, ",\"", 14) + 2;
endIndex = line.indexOf("\"", startIndex);
int[] chineseSimpPosition = new int[]{startIndex, endIndex};
String chineseSimp = line.substring(startIndex, endIndex);
// 添加单词
list.add(new WordCsvItem(stringTable, position.get(), original, chinese, chinesePosition, chineseSimp, chineseSimpPosition));
position.addAndGet(1);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
list.sort(Comparator.comparingInt(WordItem::getLines));
return list;
}
/**
* 获取所有 config.bin 文件内 可翻译内容
*/
private void findConfigWord() {
// 搜索所有的 config.bin 文件并按路径解包到bak文件夹
List<File> files = new ArrayList<>(FileUtils.listFiles(new File(unpackPath), new NameFileFilter(FILE_NAME_CONFIG_BIN, FILE_NAME_CONFIG_CPP), TrueFileFilter.INSTANCE));
files.forEach(file -> {
// 转换bin文件为cpp可读取文件
if (file.getName().endsWith("bin")) {
cfgConvertUtil.toTxt(file, file.getParentFile().getAbsolutePath(), new CfgConvertUtil.OnToTxtListener() {
@Override
public void onToTxtSuccess(String txtFilePath) {
// 读取 cpp 文件
readCppFile(new File(txtFilePath), file.equals(files.get(files.size() - 1)));
}
@Override
public void onToTxtError(Exception e) {
Platform.runLater(() -> {
AlertUtil.exception(e).content(FILE_NAME_CONFIG_BIN + "文件转换失败").show();
});
}
});
} else {
// 读取 cpp 文件
readCppFile(file, file.equals(files.get(files.size() - 1)));
}
});
}
private static final Pattern pattern = Pattern.compile(".*((displayName|descriptionShort).?=.?\").*");
/**
* 读取cpp文件查询可翻译文本
*
* @param file cpp文件
*/
private void readCppFile(File file, boolean isEnd) {
List<WordItem> list = new ArrayList<>();
AtomicInteger lines = new AtomicInteger(0);
try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
while (it.hasNext()) {
String line = it.nextLine();
Matcher matcher = pattern.matcher(line);
if (!line.contains("$") && matcher.find()) {
String name = matcher.group(1);
// 原始文本
int startIndex = line.indexOf(name) + name.length();
int endIndex = line.indexOf("\"", startIndex);
String original;
try {
original = line.substring(startIndex, endIndex);
} catch (Exception e) {
lines.addAndGet(1);
continue;
}
// 添加单词
if (!"".endsWith(original) && !containsChinese(original)) {
list.add(new WordItem(file, lines.get(), original, "", new int[]{startIndex, endIndex}));
}
}
lines.addAndGet(1);
}
} catch (IOException e) {
logger.error("", e);
throw new RuntimeException(e);
}
list.sort(Comparator.comparingInt(WordItem::getLines));
if (onFindTransWordListener != null) {
Platform.runLater(() -> onFindTransWordListener.onFoundWords(list, isEnd));
}
}
/**
* 给定字符串是否存在中文
*
* @param str 字符串
* @return 是否存在中文
*/
private boolean containsChinese(String str) {
// [\u4e00-\u9fa5]
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
}
/**
* 打包PBO文件
*/
public void pack(List<WordItem> words) {
if (onPackListener != null) {
Platform.runLater(() -> onPackListener.onStart());
}
ThreadPoolManager.getInstance().execute(() -> {
File unpackDir;
if (StringUtils.isBlank(unpackPath)
|| !(unpackDir = new File(unpackPath)).exists()
|| !unpackDir.isDirectory()
) {
AlertUtil.error("未获取到打开的pbo文件").show();
return;
}
// 写入翻译后文本
try {
writeWords(words);
} catch (Exception e) {
logger.error("writeWords error", e);
if (onPackListener != null) {
Platform.runLater(() -> {
onPackListener.onPackOver();
onPackListener.onPackError("writeWords error ==> " + e.getMessage());
});
}
throw new RuntimeException(e);
}
// 打包文件临时保存路径
String packFilePath = unpackPath + ".pbo";
File packFile = new File(packFilePath);
if (packFile.exists()) {
// 如果存在则删除
FileUtils.deleteQuietly(packFile);
}
// 执行打包指令
String command = String.format(PACK_COMMAND, AppConstant.TMP_DIR_PATH, unpackPath);
logger.info(command);
ProcessesUtil.exec(command, new ProcessesUtil.OnExecuteListener() {
@Override
public void onExecute(String msg) {
logger.info(msg);
}
@Override
public void onExecuteSuccess(int exitValue) {
Platform.runLater(() -> {
if (exitValue != 0) {
logger.error("保存PBO文件失败");
if (onPackListener != null) {
onPackListener.onPackOver();
onPackListener.onPackError("保存PBO文件失败");
}
} else {
if (onPackListener != null) {
onPackListener.onPackOver();
onPackListener.onPackSuccess(packFile);
}
}
});
}
@Override
public void onExecuteError(Exception e) {
logger.error("保存PBO文件失败");
if (onPackListener != null) {
Platform.runLater(() -> {
onPackListener.onPackOver();
onPackListener.onPackError(e.getMessage());
});
}
}
@Override
public void onExecuteOver() {
if (onPackListener != null) {
Platform.runLater(() -> onPackListener.onPackOver());
}
}
});
});
}
/**
* 写入翻译文本
*
* @param words 已经翻译好的文本对象
*/
private void writeWords(List<WordItem> words) throws Exception {
Map<File, List<WordItem>> wordMap = words.stream()
.collect(Collectors.groupingBy(WordItem::getFile, Collectors.toList()));
AtomicInteger progress = new AtomicInteger(0);
// 0 执行成功 大于0 执行失败
List<Future<Integer>> result = new ArrayList<>();
for (Map.Entry<File, List<WordItem>> entry : wordMap.entrySet()) {
Future<Integer> submit = ThreadPoolManager.getInstance().submit(() -> {
try {
entry.getValue().sort(Comparator.comparingInt(WordItem::getLines));
File file = entry.getKey();
// 创建备份文件
File bakFile = getWordBakFile(file);
// 判断重复打包时 备份文件处理
if (!bakFile.exists()) {
FileUtils.copyFile(file, bakFile);
}
// 清空原始文件
FileWriter fileWriter = new FileWriter(file);
fileWriter.write("");
fileWriter.flush();
fileWriter.close();
// 遍历拼接翻译文本并写入
long lines = 0;
String line;
LineIterator it = FileUtils.lineIterator(bakFile, StandardCharsets.UTF_8.name());
while (it.hasNext() && !entry.getValue().isEmpty()) {
line = it.nextLine();
WordItem item = entry.getValue().get(0);
int[] ip = item.getPosition();
// 拼接翻译后的文本
if (lines == item.getLines()) {
if (item instanceof WordCsvItem csv) {
int[] simp = csv.getPositionSimp();
// 判断 ip 是否比 simp 靠前
boolean tag = ip[0] < simp[0];
line = line.substring(0, (tag ? ip : simp)[0])
+ (tag ? csv.getChinese() : csv.getChineseSimp())
+ line.substring((tag ? ip : simp)[1], (tag ? simp : ip)[0])
+ (tag ? csv.getChineseSimp() : csv.getChinese())
+ line.substring((tag ? simp : ip)[1]);
} else {
try {
line = line.substring(0, ip[0])
+ item.getChinese()
+ line.substring(ip[1]);
} catch (Exception e) {
System.out.println(line);
}
}
entry.getValue().remove(item);
if (onPackListener != null) {
Platform.runLater(() -> onPackListener.onProgress(progress.addAndGet(1), words.size()));
}
}
// 写入原始文件
FileUtils.writeStringToFile(file, line + System.lineSeparator(), StandardCharsets.UTF_8, true);
lines++;
}
// 关闭流
IOUtils.closeQuietly(it);
// cpp 文件需要 转换为 bin
if (file.getName().endsWith("cpp")) {
File binFile = new File(file.getParent() + File.separator + FileUtil.mainName(file) + ".bin");
if (binFile.exists()) {
// 转为bin文件
cfgConvertUtil.toBin(file);
// 删除cpp文件
FileUtils.deleteQuietly(file);
}
// 同目录下不存在bin文件说明原始文件为cpp 无需转换
}
} catch (Exception e) {
logger.error("写入翻译文本失败", e);
// 执行失败
return 1;
}
// 执行成功
return 0;
});
// 添加执行结果
result.add(submit);
}
for (Future<Integer> future : result) {
if (future.get() > 0)
throw new IOException();
}
}
private File getWordBakFile(File wordFile) {
return new File(wordFile.getAbsolutePath().replace(unpackPath, AppConstant.BAK_FILE_PATH));
}
/**
* 是否有国际化翻译文件
*
* @return 是否有 <code>stringtable.csv</code> 文件
*/
private boolean hasStringTable() {
List<String> fileNames = FileUtil.listFileNames(unpackPath);
return fileNames.stream().anyMatch(FILE_NAME_STRING_TABLE::equals);
}
/**
* 清理缓存文件
*/
public static void clear() {
File tmpDest = new File(AppConstant.TMP_DIR_PATH);
if (tmpDest.exists())
FileUtils.deleteQuietly(tmpDest);
}
public interface OnUnpackListener {
/**
* 开始解包
*/
void onStart();
/**
* 解包完成
*
* @param unpackDirPath 解包文件夹绝对路径
*/
void onUnpackSuccess(String unpackDirPath);
/**
* 解包失败
*
* @param msg 失败信息
*/
void onUnpackError(String msg);
void onUnpackOver();
}
public interface OnPackListener {
/**
* 开始解包
*/
void onStart();
void onProgress(long current, long all);
/**
* 打包完成
*/
void onPackSuccess(File packFile);
/**
* 打包失败
*
* @param msg 失败信息
*/
void onPackError(String msg);
void onPackOver();
}
public interface OnFindTransWordListener {
void onFoundWords(List<WordItem> worlds, boolean isOver);
}
}

View File

@ -1,49 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.file;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritablePixelFormat;
import javax.swing.*;
import javax.swing.filechooser.FileSystemView;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.File;
import java.nio.IntBuffer;
/**
* 文件图标
*
* @author octopus_yan@foxmail.com
*/
public class FileIcon {
//设置图标
public static Canvas getFileIconToNode(File file) {
//获取系统文件的图标
Image image = ((ImageIcon) FileSystemView.getFileSystemView().getSystemIcon(file)).getImage();
//构建图片缓冲区,设定图片缓冲区的大小和背景,背景为透明
BufferedImage bi = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.BITMASK);
//把图片画到图片缓冲区
bi.getGraphics().drawImage(image, 0, 0, null);
//将图片缓冲区的数据转换成int型数组
int[] data = ((DataBufferInt) bi.getData().getDataBuffer()).getData();
//获得写像素的格式模版
WritablePixelFormat<IntBuffer> pixelFormat = PixelFormat.getIntArgbInstance();
//新建javafx的画布
Canvas canvas = new Canvas(bi.getWidth() + 2, bi.getHeight() + 2);
//获取像素的写入器
PixelWriter pixelWriter = canvas.getGraphicsContext2D().getPixelWriter();
//根据写像素的格式模版把int型数组写到画布
pixelWriter.setPixels(0, 0, bi.getWidth(), bi.getHeight(), pixelFormat, data, 0, bi.getWidth());
//设置树节点的图标
return canvas;
}
public static String getFileName(File file) {
return FileSystemView.getFileSystemView().getSystemDisplayName(file);
}
}

View File

@ -1,90 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.file;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
/**
* 文件目录
*
* @author octopus_yan@foxmail.com
*/
public class FileTreeItem extends TreeItem<String> {
public static File ROOT_FILE = FileSystemView.getFileSystemView().getRoots()[0];
//判断树节点是否被初始化,没有初始化为真
private boolean notInitialized = true;
private final File file;
private final Function<File, File[]> supplier;
public FileTreeItem(File file) {
super(FileIcon.getFileName(file), FileIcon.getFileIconToNode(file));
this.file = file;
supplier = (File f) -> {
if (((FileTreeItem) this.getParent()).getFile() == ROOT_FILE) {
String name = FileIcon.getFileName(f);
if (name.equals("网络") || name.equals("家庭组")) {
return new File[0];
}
}
return f.listFiles();
};
}
public FileTreeItem(File file, Function<File, File[]> supplier) {
super(FileIcon.getFileName(file), FileIcon.getFileIconToNode(file));
this.file = file;
this.supplier = supplier;
}
//重写getchildren方法让节点被展开时加载子目录
@Override
public ObservableList<TreeItem<String>> getChildren() {
ObservableList<TreeItem<String>> children = super.getChildren();
//没有加载子目录时,则加载子目录作为树节点的孩子
if (this.notInitialized && this.isExpanded()) {
this.notInitialized = false; //设置没有初始化为假
/*
*判断树节点的文件是否是目录,
*如果是目录,着把目录里面的所有的文件添加入树节点的孩子中。
*/
if (this.getFile().isDirectory()) {
List<FileTreeItem> fileList = new ArrayList<>();
for (File f : supplier.apply(this.getFile())) {
if (f.isDirectory())
children.add(new FileTreeItem(f));
else
fileList.add(new FileTreeItem(f));
}
children.addAll(fileList);
}
}
return children;
}
//重写叶子方法,如果该文件不是目录,则返回真
@Override
public boolean isLeaf() {
return !file.isDirectory();
}
/**
* @return the file
*/
public File getFile() {
return file;
}
}

View File

@ -1,183 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.http;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.Executor;
/**
* Http配置参数
*
* @author octopus_yan@foxmail.com
*/
public class HttpConfig {
private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
/**
* http版本
*/
private HttpClient.Version version = HttpClient.Version.HTTP_2;
/**
* 转发策略
*/
private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
/**
* 线程池
*/
private Executor executor;
/**
* 认证
*/
private Authenticator authenticator;
/**
* 代理
*/
private ProxySelector proxySelector;
/**
* CookieHandler
*/
private CookieHandler cookieHandler;
/**
* sslContext
*/
private SSLContext sslContext;
/**
* sslParams
*/
private SSLParameters sslParameters;
/**
* 连接超时时间毫秒
*/
private int connectTimeout = 10000;
/**
* 默认读取数据超时时间
*/
private int defaultReadTimeout = 1200000;
public HttpConfig() {
TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; // Not relevant.
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
// TODO Auto-generated method stub
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
// TODO Auto-generated method stub
}
}};
sslParameters = new SSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("");
try {
sslContext = SSLContext.getInstance("TLSv1.2");
System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
sslContext.init(null, trustAllCertificates, new SecureRandom());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.error("", e);
}
}
public HttpClient.Version getVersion() {
return version;
}
public void setVersion(HttpClient.Version version) {
this.version = version;
}
public int getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
public HttpClient.Redirect getRedirect() {
return redirect;
}
public void setRedirect(HttpClient.Redirect redirect) {
this.redirect = redirect;
}
public Executor getExecutor() {
return executor;
}
public void setExecutor(Executor executor) {
this.executor = executor;
}
public Authenticator getAuthenticator() {
return authenticator;
}
public void setAuthenticator(Authenticator authenticator) {
this.authenticator = authenticator;
}
public ProxySelector getProxySelector() {
return proxySelector;
}
public void setProxySelector(ProxySelector proxySelector) {
this.proxySelector = proxySelector;
}
public CookieHandler getCookieHandler() {
return cookieHandler;
}
public void setCookieHandler(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
}
public int getDefaultReadTimeout() {
return defaultReadTimeout;
}
public void setDefaultReadTimeout(int defaultReadTimeout) {
this.defaultReadTimeout = defaultReadTimeout;
}
public SSLContext getSslContext() {
return sslContext;
}
public SSLParameters getSslParameters() {
return sslParameters;
}
}

View File

@ -1,29 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.thread;
import java.util.concurrent.*;
/**
* 线程池管理类
*/
public final class ThreadPoolManager extends ThreadPoolExecutor {
private static volatile ThreadPoolManager sInstance;
private static ScheduledExecutorService scheduledExecutorService;
private ThreadPoolManager() {
super(32,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX),
new ThreadPoolExecutor.DiscardPolicy());
}
public static ThreadPoolManager getInstance() {
if (sInstance == null) sInstance = new ThreadPoolManager();
return sInstance;
}
}

View File

@ -1,27 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.translate;
/**
* API 密钥配置
*
* @author octopus_yan@foxmail.com
*/
public class ApiKey {
private String appid;
private String apiKey;
public ApiKey() {
}
public ApiKey(String appid, String apiKey) {
this.appid = appid;
this.apiKey = apiKey;
}
public String getAppid() {
return appid;
}
public String getApiKey() {
return apiKey;
}
}

View File

@ -1,65 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.translate;
/**
* 翻译引擎类型
*
* @author octopus_yan@foxmail.com
*/
public enum TranslateSource {
FREE_GOOGLE("free_google", "谷歌(免费)", false, 50),
BAIDU("baidu", "百度(需认证)", true),
;
private final String name;
private final String label;
private final boolean needApiKey;
private Integer defaultQps;
TranslateSource(String name, String label, boolean needApiKey) {
// 设置接口默认qps=10
this(name, label, needApiKey, 10);
}
TranslateSource(String name, String label, boolean needApiKey, int defaultQps) {
this.name = name;
this.label = label;
this.needApiKey = needApiKey;
this.defaultQps = defaultQps;
}
public String getName() {
return name;
}
public String getLabel() {
return label;
}
public boolean needApiKey() {
return needApiKey;
}
public Integer getDefaultQps() {
return defaultQps;
}
public String getDefaultQpsStr() {
return String.valueOf(defaultQps);
}
public static TranslateSource get(String type) {
for (TranslateSource value : values()) {
if (value.getName().equals(type))
return value;
}
throw new RuntimeException("类型不存在");
}
public static TranslateSource getByLabel(String label) {
for (TranslateSource value : values()) {
if (value.getLabel().equals(label))
return value;
}
throw new RuntimeException("类型不存在");
}
}

View File

@ -1,227 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.translate;
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadFactory;
import cn.octopusyan.dayzmodtranslator.manager.thread.ThreadPoolManager;
import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactory;
import cn.octopusyan.dayzmodtranslator.manager.translate.factory.TranslateFactoryImpl;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* 翻译工具
*
* @author octopus_yan@foxmail.com
*/
public class TranslateUtil {
private static final Logger logger = LoggerFactory.getLogger(TranslateUtil.class);
private static TranslateUtil util;
private static final DelayQueue<DelayWord> delayQueue = new DelayQueue<>();
private final TranslateFactory factory;
private static WordThread wordThread;
private static ThreadPoolExecutor threadPoolExecutor;
private TranslateUtil(TranslateFactory factory) {
this.factory = factory;
}
public static TranslateUtil getInstance() {
if (util == null) {
util = new TranslateUtil(TranslateFactoryImpl.getInstance());
}
return util;
}
/**
* 提交翻译任务
*
* @param index 序号
* @param original 原始文本
* @param listener 翻译结果回调 (主线程)
*/
public void translate(int index, String original, OnTranslateListener listener) {
// 设置延迟时间
DelayWord word = factory.getDelayWord(CustomConfig.translateSource(), index, original, listener);
// 添加到延迟队列
delayQueue.add(word);
if (wordThread == null) {
wordThread = new WordThread();
wordThread.start();
}
}
/**
* 清除翻译任务
*/
public void clear() {
// 尝试停止所有线程
getThreadPoolExecutor().shutdownNow();
// 清空队列
delayQueue.clear();
// 设置停止标记
if (wordThread != null)
wordThread.setStop(true);
wordThread = null;
}
/**
* 获取翻译任务用线程池
*/
public static ThreadPoolExecutor getThreadPoolExecutor() {
if (threadPoolExecutor == null || threadPoolExecutor.isShutdown()) {
threadPoolExecutor = new ThreadPoolExecutor(32,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX),
new ThreadPoolExecutor.DiscardPolicy());
}
return threadPoolExecutor;
}
public interface OnTranslateListener {
void onTranslate(String result);
}
/**
* 延迟翻译对象
*/
public static class DelayWord implements Delayed {
private TranslateSource source;
private final int index;
private final String original;
private final OnTranslateListener listener;
private long time;
public DelayWord(int index, String original, OnTranslateListener listener) {
this.index = index;
this.original = original;
this.listener = listener;
}
public void setSource(TranslateSource source) {
this.source = source;
}
public void setTime(long time, TimeUnit timeUnit) {
this.time = System.currentTimeMillis() + (time > 0 ? timeUnit.toMillis(time) : 0);
}
public TranslateSource getSource() {
return source;
}
public int getIndex() {
return index;
}
public String getOriginal() {
return original;
}
public OnTranslateListener getListener() {
return listener;
}
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
DelayWord word = (DelayWord) o;
return Integer.compare(this.index, word.index);
}
}
/**
* 延迟队列处理线程
*/
private static class WordThread extends Thread {
private boolean stop = false;
public void setStop(boolean stop) {
this.stop = stop;
}
@Override
public void run() {
List<DelayWord> tmp = new ArrayList<>();
while (!delayQueue.isEmpty()) {
// 停止处理
if (stop) {
this.interrupt();
return;
}
try {
// 取出待翻译文本
DelayWord take = delayQueue.take();
tmp.add(take);
if (tmp.size() < CustomConfig.translateSourceQps(take.source))
continue;
tmp.forEach(word -> {
try {
getThreadPoolExecutor().execute(() -> {
// 翻译
try {
String translate = util.factory.translate(word.getSource(), word.getOriginal());
// 回调监听器
if (word.getListener() != null)
// 主线程处理翻译结果
Platform.runLater(() -> word.getListener().onTranslate(translate));
} catch (InterruptedException ignored) {
} catch (Exception e) {
logger.error("翻译出错", e);
throw new RuntimeException(e);
}
});
} catch (Exception e) {
logger.error("翻译出错", e);
throw new RuntimeException(e);
}
});
tmp.clear();
} catch (InterruptedException ignored) {
}
}
// 处理剩余
tmp.forEach(word -> {
try {
ThreadPoolManager.getInstance().execute(() -> {
// 翻译
try {
String translate = util.factory.translate(word.getSource(), word.getOriginal());
// 回调监听器
if (word.getListener() != null)
// 主线程处理翻译结果
Platform.runLater(() -> word.getListener().onTranslate(translate));
} catch (Exception e) {
logger.error("翻译出错", e);
throw new RuntimeException(e);
}
});
} catch (Exception e) {
logger.error("翻译出错", e);
throw new RuntimeException(e);
}
});
tmp.clear();
}
}
}

View File

@ -1,32 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.factory;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
/**
* 翻译器接口
*
* @author octopus_yan@foxmail.com
*/
public interface TranslateFactory {
/**
* 翻译处理
*
* @param source 翻译源
* @param sourceString 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
String translate(TranslateSource source, String sourceString) throws Exception;
/**
* 获取延迟翻译对象
*
* @param source 翻译源
* @param index 序号
* @param original 原始文本
* @param listener 监听器
* @return 延迟翻译对象
*/
TranslateUtil.DelayWord getDelayWord(TranslateSource source, int index, String original, TranslateUtil.OnTranslateListener listener);
}

View File

@ -1,81 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.factory;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
import cn.octopusyan.dayzmodtranslator.manager.translate.processor.AbstractTranslateProcessor;
import cn.octopusyan.dayzmodtranslator.manager.translate.processor.BaiduTranslateProcessor;
import cn.octopusyan.dayzmodtranslator.manager.translate.processor.FreeGoogleTranslateProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* 翻译处理器
*
* @author octopus_yan@foxmail.com
*/
public class TranslateFactoryImpl implements TranslateFactory {
private static final Logger logger = LoggerFactory.getLogger(TranslateFactoryImpl.class);
private static TranslateFactoryImpl impl;
private final Map<String, AbstractTranslateProcessor> processorMap = new HashMap<>();
private final List<AbstractTranslateProcessor> processorList = new ArrayList<>();
private TranslateFactoryImpl() {
}
public static synchronized TranslateFactoryImpl getInstance() {
if (impl == null) {
impl = new TranslateFactoryImpl();
impl.initProcessor();
}
return impl;
}
private void initProcessor() {
processorList.addAll(Arrays.asList(
new FreeGoogleTranslateProcessor(TranslateSource.FREE_GOOGLE),
new BaiduTranslateProcessor(TranslateSource.BAIDU)
));
for (AbstractTranslateProcessor processor : processorList) {
processorMap.put(processor.getSource(), processor);
}
}
private AbstractTranslateProcessor getProcessor(TranslateSource source) {
return processorMap.get(source.getName());
}
/**
* 获取延迟翻译对象
*
* @param source 翻译源
* @param index 序号
* @param original 原始文本
* @param listener 监听器
* @return 延迟翻译对象
*/
@Override
public TranslateUtil.DelayWord getDelayWord(TranslateSource source, int index, String original, TranslateUtil.OnTranslateListener listener) {
// 生产翻译对象
TranslateUtil.DelayWord word = new TranslateUtil.DelayWord(index, original, listener);
// 设置延迟
getProcessor(source).setDelayTime(word);
return word;
}
/**
* 翻译(英->中)
* <p> TODO 切换语种
*
* @param source 翻译源
* @param sourceString 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
@Override
public String translate(TranslateSource source, String sourceString) throws Exception {
return getProcessor(source).translate(sourceString);
}
}

View File

@ -1,99 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
import cn.octopusyan.dayzmodtranslator.config.CustomConfig;
import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateUtil;
import cn.octopusyan.dayzmodtranslator.manager.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
/**
* 翻译处理器抽象类
*
* @author octopus_yan@foxmail.com
*/
public abstract class AbstractTranslateProcessor implements TranslateProcessor {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected static final HttpUtil httpUtil = HttpUtil.getInstance();
protected final TranslateSource translateSource;
protected ApiKey apiKey;
public AbstractTranslateProcessor(TranslateSource translateSource) {
this.translateSource = translateSource;
}
public String getSource() {
return translateSource.getName();
}
public TranslateSource source() {
return translateSource;
}
@Override
public boolean needApiKey() {
return source().needApiKey();
}
@Override
public boolean configuredKey() {
return CustomConfig.hasTranslateApiKey(source());
}
@Override
public int qps() {
return CustomConfig.translateSourceQps(source());
}
/**
* 获取Api配置信息
*/
protected ApiKey getApiKey() {
if (!configuredKey()) {
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
logger.error(message);
throw new RuntimeException(message);
}
String appid = CustomConfig.translateSourceAppid(source());
String apikey = CustomConfig.translateSourceApikey(source());
return new ApiKey(appid, apikey);
}
@Override
public String translate(String source) throws Exception {
if (needApiKey() && !configuredKey()) {
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
logger.error(message);
throw new RuntimeException(message);
}
return customTranslate(source);
}
/**
* 翻译处理
*
* @param source 原始文本
* @return 翻译结果
*/
public abstract String customTranslate(String source) throws Exception;
/**
* 设置延迟对象
*
* @param word 带翻译单词
*/
public void setDelayTime(TranslateUtil.DelayWord word) {
// 设置翻译源
word.setSource(source());
// 设置翻译延迟
int time = word.getIndex() / qps();
word.setTime(time, TimeUnit.SECONDS);
}
}

View File

@ -1,52 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.word;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.io.File;
/**
* csv单词对象
*
* @author octopus_yan@foxmail.com
*/
public class WordCsvItem extends WordItem {
/**
* 简体中文
*/
private StringProperty chineseSimp;
/**
* 文件中坐标(简体中文)
*/
private int[] positionSimp;
public WordCsvItem() {
}
public WordCsvItem(File stringTable, int lines, String original, String chineses, int[] position, String chineseSimp, int[] positionSimp) {
super(stringTable, lines, original, chineses, position);
this.chineseSimp = new SimpleStringProperty(chineseSimp);
this.positionSimp = positionSimp;
}
public StringProperty chineseSimpProperty() {
return chineseSimp;
}
public String getChineseSimp() {
return chineseSimp.get();
}
public void setChineseSimp(String chineseSimp) {
this.chineseSimp.setValue(chineseSimp);
}
public int[] getPositionSimp() {
return positionSimp;
}
public void setPositionSimp(int[] positionSimp) {
this.positionSimp = positionSimp;
}
}

View File

@ -1,98 +0,0 @@
package cn.octopusyan.dayzmodtranslator.manager.word;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.io.File;
/**
* 待翻译单词子项
*
* @author octopus_yan@foxmail.com
*/
public class WordItem {
/**
* 所在文件
* <p>PS: 文本所在的文件
*/
private File file;
/**
* 原始文本
*/
private StringProperty original;
/**
* 汉化
*/
private StringProperty chinese;
/**
* 行内下标
*/
private int[] position;
/**
* 文件第几行
*/
private int lines;
public WordItem() {
}
public WordItem(File file, int lines, String original, String chinese, int[] position) {
this.file = file;
this.original = new SimpleStringProperty(original);
this.chinese = new SimpleStringProperty(chinese);
this.position = position;
this.lines = lines;
}
public StringProperty originalProperty() {
return original;
}
public String getOriginal() {
return original.get();
}
public void setOriginal(String original) {
this.original.setValue(original);
}
public StringProperty chineseProperty() {
return chinese;
}
public String getChinese() {
return chinese.get();
}
public void setChinese(String chinese) {
this.chinese.setValue(chinese);
}
public int[] getPosition() {
return position;
}
public void setPosition(int[] position) {
this.position = position;
}
public int getLines() {
return lines;
}
public void setLines(int lines) {
this.lines = lines;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
}

View File

@ -1,228 +0,0 @@
package cn.octopusyan.dayzmodtranslator.util;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 弹窗工具
*
* @author octopus_yan@foxmail.com
*/
public class AlertUtil {
private static Window mOwner;
private static Builder builder;
public static void initOwner(Stage stage) {
AlertUtil.mOwner = stage;
}
public static class Builder<T extends Dialog> {
T alert;
public Builder(T alert) {
this.alert = alert;
if (mOwner != null) this.alert.initOwner(mOwner);
}
public Builder<T> title(String title) {
alert.setTitle(title);
return this;
}
public Builder<T> header(String header) {
alert.setHeaderText(header);
return this;
}
public Builder<T> content(String content) {
alert.setContentText(content);
return this;
}
public Builder<T> icon(String path) {
icon(new Image(Objects.requireNonNull(this.getClass().getResource(path)).toString()));
return this;
}
public Builder<T> icon(Image image) {
getStage().getIcons().add(image);
return this;
}
public void show() {
if (AlertUtil.builder == null) {
AlertUtil.builder = this;
} else if (AlertUtil.builder.alert.isShowing()) {
if (!Objects.equals(AlertUtil.builder.alert.getContentText(), alert.getContentText()))
((Alert) AlertUtil.builder.alert).setOnHidden(event -> {
AlertUtil.builder = null;
show();
});
}
alert.showAndWait();
}
/**
* AlertUtil.confirm
*/
public void show(OnClickListener listener) {
Optional<ButtonType> result = alert.showAndWait();
listener.onClicked(result.get().getText());
}
/**
* AlertUtil.confirm
*/
public void show(OnChoseListener listener) {
Optional<ButtonType> result = alert.showAndWait();
if (result.get() == ButtonType.OK) {
listener.confirm();
} else {
listener.cancelOrClose(result.get());
}
}
/**
* AlertUtil.input
* 如果用户点击了取消按钮,将会返回null
*/
public String getInput() {
Optional<String> result = alert.showAndWait();
if (result.isPresent()) {
return result.get();
}
return null;
}
/**
* AlertUtil.choices
*/
public <R> R getChoice(R... choices) {
Optional result = alert.showAndWait();
return (R) result.get();
}
private Stage getStage() {
return (Stage) alert.getDialogPane().getScene().getWindow();
}
}
public static Builder<Alert> info(String content) {
return new Builder<Alert>(new Alert(Alert.AlertType.INFORMATION)).content(content).header(null);
}
public static Builder<Alert> info() {
return new Builder<Alert>(new Alert(Alert.AlertType.INFORMATION));
}
public static Builder<Alert> error(String message) {
return new Builder<Alert>(new Alert(Alert.AlertType.ERROR)).header(null).content(message);
}
public static Builder<Alert> warning() {
return new Builder<Alert>(new Alert(Alert.AlertType.WARNING));
}
public static Builder<Alert> exception(Exception ex) {
return new Builder<Alert>(exceptionAlert(ex));
}
public static Alert exceptionAlert(Exception ex) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Exception Dialog");
alert.setHeaderText(ex.getClass().getSimpleName());
alert.setContentText(ex.getMessage());
// 创建可扩展的异常。
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
String exceptionText = sw.toString();
Label label = new Label("The exception stacktrace was :");
TextArea textArea = new TextArea(exceptionText);
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
GridPane expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
// 将可扩展异常设置到对话框窗格中。
alert.getDialogPane().setExpandableContent(expContent);
return alert;
}
/**
* 确认对话框
*/
public static Builder<Alert> confirm() {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("确认对话框");
return new Builder<Alert>(alert);
}
/**
* 自定义确认对话框 <p>
* <code>"Cancel"</code> OR <code>"取消"</code> 为取消按钮
*/
public static Builder<Alert> confirm(String... buttons) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
List<ButtonType> buttonList = Arrays.stream(buttons).map((type) -> {
ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER;
if ("Cancel".equals(type) || "取消".equals(type))
buttonData = ButtonBar.ButtonData.CANCEL_CLOSE;
return new ButtonType(type, buttonData);
}).collect(Collectors.toList());
alert.getButtonTypes().setAll(buttonList);
return new Builder<Alert>(alert);
}
public static Builder<TextInputDialog> input(String content) {
TextInputDialog dialog = new TextInputDialog();
dialog.setContentText(content);
return new Builder<TextInputDialog>(dialog);
}
@SafeVarargs
public static <T> Builder<ChoiceDialog<T>> choices(String hintText, T... choices) {
ChoiceDialog<T> dialog = new ChoiceDialog<T>(choices[0], choices);
dialog.setContentText(hintText);
return new Builder<ChoiceDialog<T>>(dialog);
}
public interface OnChoseListener {
void confirm();
void cancelOrClose(ButtonType buttonType);
}
public interface OnClickListener {
void onClicked(String result);
}
}

View File

@ -1,83 +0,0 @@
package cn.octopusyan.dayzmodtranslator.util;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* 加载等待弹窗
*
* @author octopus_yan@foxmail.com
*/
public class Loading {
protected Stage stage;
protected StackPane root;
protected Label messageLb;
protected ImageView loadingView = new ImageView(new Image("https://blog-static.cnblogs.com/files/miaoqx/loading.gif"));
public Loading(Stage owner) {
messageLb = new Label("请耐心等待...");
messageLb.setFont(Font.font(20));
root = new StackPane();
root.setMouseTransparent(true);
root.setPrefSize(owner.getWidth(), owner.getHeight());
root.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.3), null, null)));
root.getChildren().addAll(loadingView, messageLb);
Scene scene = new Scene(root);
scene.setFill(Color.TRANSPARENT);
stage = new Stage();
stage.setX(owner.getX());
stage.setY(owner.getY());
stage.setScene(scene);
stage.setResizable(false);
stage.initOwner(owner);
stage.initStyle(StageStyle.TRANSPARENT);
stage.initModality(Modality.APPLICATION_MODAL);
stage.getIcons().addAll(owner.getIcons());
stage.setX(owner.getX());
stage.setY(owner.getY());
stage.setHeight(owner.getHeight());
stage.setWidth(owner.getWidth());
}
// 更改信息
public Loading showMessage(String message) {
Platform.runLater(() -> messageLb.setText(message));
return this;
}
// 更改信息
public Loading image(Image image) {
Platform.runLater(() -> loadingView.imageProperty().set(image));
return this;
}
// 显示
public void show() {
Platform.runLater(() -> stage.show());
}
// 关闭
public void closeStage() {
Platform.runLater(() -> stage.close());
}
// 是否正在展示
public boolean showing() {
return stage.isShowing();
}
}

View File

@ -1,96 +0,0 @@
package cn.octopusyan.dayzmodtranslator.util;
import org.apache.commons.exec.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* 命令工具类
*
* @author octopus_yan@foxmail.com
*/
public class ProcessesUtil {
private static final Logger logger = LoggerFactory.getLogger(ProcessesUtil.class);
private static final String NEWLINE = System.lineSeparator();
private static final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
public static boolean exec(String command) {
try {
exec(command, new OnExecuteListener() {
@Override
public void onExecute(String msg) {
}
@Override
public void onExecuteSuccess(int exitValue) {
}
@Override
public void onExecuteError(Exception e) {
}
@Override
public void onExecuteOver() {
}
});
handler.waitFor();
} catch (Exception e) {
logger.error("", e);
}
return 0 == handler.getExitValue();
}
public static void exec(String command, OnExecuteListener listener) {
LogOutputStream logout = new LogOutputStream() {
@Override
protected void processLine(String line, int logLevel) {
if (listener != null) listener.onExecute(line + NEWLINE);
}
};
CommandLine commandLine = CommandLine.parse(command);
DefaultExecutor executor = DefaultExecutor.builder().get();
executor.setStreamHandler(new PumpStreamHandler(logout, logout));
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
if (listener != null) {
listener.onExecuteSuccess(exitValue);
}
}
@Override
public void onProcessFailed(ExecuteException e) {
if (listener != null) {
listener.onExecuteError(e);
}
}
};
try {
executor.execute(commandLine, handler);
} catch (IOException e) {
if (listener != null) listener.onExecuteError(e);
}
}
public interface OnExecuteListener {
void onExecute(String msg);
void onExecuteSuccess(int exitValue);
void onExecuteError(Exception e);
void onExecuteOver();
}
/**
* Prevent construction.
*/
private ProcessesUtil() {
}
}

View File

@ -1,113 +0,0 @@
package cn.octopusyan.dayzmodtranslator.util;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.stage.Window;
/**
* 提示工具
*
* @author octopus_yan@foxmail.com
*/
public class TooltipUtil {
private static TooltipUtil util;
private final Tooltip tooltip = new Tooltip();
private Window owner;
private ChangeListener<Number> xListener;
private ChangeListener<Number> yListener;
private boolean paneMove = false;
private TooltipUtil(Window window) {
this.owner = window;
this.tooltip.styleProperty().set(
"-fx-background-color: white;" +
"-fx-text-fill: grey;" +
"-fx-font-size: 12px;"
);
}
public static TooltipUtil getInstance(Pane pane) {
if (pane == null) return null;
Window window = pane.getScene().getWindow();
if (window == null) return null;
if (util == null) {
util = new TooltipUtil(window);
// 窗口位置监听
util.xListener = (observable, oldValue, newValue) -> {
util.tooltip.setAnchorX(util.tooltip.getAnchorX() + (newValue.doubleValue() - oldValue.doubleValue()));
util.paneMove = true;
};
util.yListener = (observable, oldValue, newValue) -> {
util.tooltip.setAnchorY(util.tooltip.getAnchorY() + (newValue.doubleValue() - oldValue.doubleValue()));
util.paneMove = true;
};
util.tooltip.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) util.paneMove = false;
});
// 随窗口移动
util.owner.xProperty().addListener(util.xListener);
util.owner.yProperty().addListener(util.yListener);
}
if (!window.equals(util.owner)) {
// 删除旧监听
util.owner.xProperty().removeListener(util.xListener);
util.owner.yProperty().removeListener(util.yListener);
// 新窗口
util.owner = window;
// 随窗口移动
util.owner.xProperty().addListener(util.xListener);
util.owner.yProperty().addListener(util.yListener);
}
// 点击关闭
pane.setOnMouseClicked(event -> {
if (!util.paneMove) util.tooltip.hide();
util.paneMove = false;
});
util.tooltip.hide();
return util;
}
public void showProxyTypeTip(MouseEvent event) {
tooltip.setText(
"提示XTCP 映射成功率并不高,具体取决于 NAT 设备的复杂度。\n" +
"TCP :基础的 TCP 映射适用于大多数服务例如远程桌面、SSH、Minecraft、泰拉瑞亚等\n" +
"UDP :基础的 UDP 映射,适用于域名解析、部分基于 UDP 协议的游戏等\n" +
"HTTP :搭建网站专用映射,并通过 80 端口访问\n" +
"HTTPS :带有 SSL 加密的网站映射,通过 443 端口访问,服务器需要支持 SSL\n" +
"XTCP :客户端之间点对点 (P2P) 连接协议,流量不经过服务器,适合大流量传输的场景,需要两台设备之间都运行一个客户端\n" +
"STCP :安全交换 TCP 连接协议,基于 TCP访问此服务的用户也需要运行一个客户端才能建立连接流量由服务器转发"
);
show(event);
}
private void show(MouseEvent event) {
if (tooltip.isShowing()) {
tooltip.hide();
} else {
tooltip.show(owner);
double mx = event.getScreenX();
double my = event.getScreenY();
double tw = tooltip.widthProperty().doubleValue();
double th = tooltip.heightProperty().doubleValue();
tooltip.setX(mx - tw / 2);
tooltip.setY(my - th - 10);
}
}
public void hide() {
tooltip.hide();
}
public boolean isShowing() {
return tooltip.isShowing();
}
}

View File

@ -0,0 +1,13 @@
package cn.octopusyan.dmt;
/**
* 启动类
*
* @author octopus_yan@foxmail.com
*/
public class AppLauncher {
public static void main(String[] args) {
Application.launch(Application.class, args);
}
}

View File

@ -0,0 +1,114 @@
package cn.octopusyan.dmt;
import cn.octopusyan.dmt.common.config.Constants;
import cn.octopusyan.dmt.common.config.Context;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.common.manager.http.CookieManager;
import cn.octopusyan.dmt.common.manager.http.HttpConfig;
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
import cn.octopusyan.dmt.common.util.ProcessesUtil;
import cn.octopusyan.dmt.utils.PBOUtil;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import lombok.Getter;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.util.Objects;
public class Application extends javafx.application.Application {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
@Getter
private static Stage primaryStage;
@Override
public void init() {
logger.info("application init ...");
// 初始化客户端配置
ConfigManager.load();
// 初始化 PBO工具
PBOUtil.init();
// http请求工具初始化
HttpConfig httpConfig = new HttpConfig();
httpConfig.setCookieHandler(CookieManager.get());
httpConfig.setExecutor(ThreadPoolManager.getInstance("http-pool"));
// 加载代理设置
switch (ConfigManager.proxySetup()) {
case NO_PROXY -> httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
case MANUAL -> {
if (ConfigManager.hasProxy()) {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(
Objects.requireNonNull(ConfigManager.proxyHost()),
ConfigManager.getProxyPort()
);
httpConfig.setProxySelector(ProxySelector.of(unresolved));
}
}
}
httpConfig.setConnectTimeout(3000);
HttpUtil.init(httpConfig);
}
@Override
public void start(Stage primaryStage) throws IOException {
logger.info("application start ...");
Application.primaryStage = primaryStage;
Context.setApplication(this);
// 初始化弹窗工具
AlertUtil.initOwner(primaryStage);
// 全局异常处理
Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
// 主题样式
Application.setUserAgentStylesheet(ConfigManager.theme().getUserAgentStylesheet());
// 启动主界面
primaryStage.setTitle(String.format("%s %s", Constants.APP_TITLE, Constants.APP_VERSION));
Scene scene = Context.initScene();
primaryStage.setScene(scene);
primaryStage.show();
}
private void showErrorDialog(Thread t, Throwable e) {
logger.error("未知异常", e);
Platform.runLater(() -> AlertUtil.exception(new Exception(e)).show());
}
@Override
public void stop() {
logger.info("application stop ...");
// 关闭所有命令
ProcessesUtil.destroyAll();
// 保存应用数据
ConfigManager.save();
// 停止所有线程
ThreadPoolManager.shutdownAll();
// 删除缓存
FileUtils.deleteQuietly(new File(Constants.TMP_DIR_PATH));
FileUtils.deleteQuietly(new File(Constants.BAK_DIR_PATH));
// 关闭主界面
Platform.exit();
System.exit(0);
}
}

View File

@ -0,0 +1,57 @@
package cn.octopusyan.dmt.common.base;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.Dialog;
import javafx.stage.Window;
import lombok.Getter;
/**
* @author octopus_yan
*/
@Getter
public abstract class BaseBuilder<T extends BaseBuilder<T, ?>, D extends Dialog<?>> {
protected D dialog;
public BaseBuilder(D dialog, Window mOwner) {
this.dialog = dialog;
if (mOwner != null)
this.dialog.initOwner(mOwner);
}
public T title(String title) {
dialog.setTitle(title);
return (T) this;
}
public T header(String header) {
dialog.setHeaderText(header);
return (T) this;
}
public T content(String content) {
dialog.setContentText(content);
return (T) this;
}
public void show() {
Node dialogPane = dialog.getDialogPane().getContent();
if (dialogPane != null && ConfigManager.theme().isDarkMode()) {
dialogPane.setStyle(STR."""
\{dialogPane.getStyle()}
-fx-border-color: rgb(209, 209, 214, 0.5);
-fx-border-width: 1;
-fx-border-radius: 10;
""");
}
Platform.runLater(() -> dialog.showAndWait());
}
public void close() {
if (dialog.isShowing())
dialog.close();
}
}

View File

@ -0,0 +1,146 @@
package cn.octopusyan.dmt.common.base;
import cn.octopusyan.dmt.Application;
import cn.octopusyan.dmt.common.config.Context;
import cn.octopusyan.dmt.common.util.FxmlUtil;
import cn.octopusyan.dmt.common.util.ViewUtil;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
/**
* 通用视图控制器基类
*
* @author octopus_yan@foxmail.com
*/
public abstract class BaseController<VM extends BaseViewModel> implements Initializable {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Getter
protected final VM viewModel;
public BaseController() {
//初始化时保存当前Controller实例
Context.getControllers().put(this.getClass().getSimpleName(), this);
// view model
VM vm = null;
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType type) {
Class<VM> clazz = (Class<VM>) type.getActualTypeArguments()[0];
try {
vm = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
logger.error("", e);
}
}
viewModel = vm;
viewModel.setController(this);
}
@FXML
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// 全局窗口拖拽
if (dragWindow() && getRootPanel() != null) {
// 窗口拖拽
ViewUtil.bindDragged(getRootPanel());
}
// 初始化数据
initData();
// 初始化视图样式
initViewStyle();
// 初始化视图事件
initViewAction();
}
/**
* 窗口拖拽设置
*
* @return 是否启用
*/
public boolean dragWindow() {
return false;
}
/**
* 获取根布局
*
* @return 根布局对象
*/
public abstract Pane getRootPanel();
/**
* 获取根布局
* <p> 搭配 {@link FxmlUtil#load(String)} 使用
*
* @return 根布局对象
*/
protected String getRootFxml() {
System.out.println(getClass().getSimpleName());
return "";
}
protected Stage getWindow() {
try {
return (Stage) getRootPanel().getScene().getWindow();
} catch (Throwable _) {
return Application.getPrimaryStage();
}
}
/**
* 初始化数据
*/
public abstract void initData();
/**
* 视图样式
*/
public void initViewStyle() {
}
/**
* 视图事件
*/
public abstract void initViewAction();
private static List<Field> getAllField(Class<?> class1) {
List<Field> list = new ArrayList<>();
while (class1 != Object.class) {
list.addAll(Arrays.stream(class1.getDeclaredFields()).toList());
//获取父类
class1 = class1.getSuperclass();
}
return list;
}
public void exit() {
Platform.exit();
}
/**
* 关闭窗口
*/
public void onDestroy() {
Stage stage = getWindow();
stage.hide();
stage.close();
}
}

View File

@ -0,0 +1,15 @@
package cn.octopusyan.dmt.common.base;
import lombok.Setter;
/**
* View Model
*
* @author octopus_yan
*/
@Setter
public abstract class BaseViewModel<VM extends BaseViewModel<VM, T>, T extends BaseController<VM>> {
protected T controller;
}

View File

@ -0,0 +1,27 @@
package cn.octopusyan.dmt.common.config;
import cn.octopusyan.dmt.common.util.PropertiesUtils;
import java.io.File;
import java.nio.file.Paths;
/**
* 应用信息
*
* @author octopus_yan@foxmail.com
*/
public class Constants {
public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
public static final String DATA_DIR_PATH = Paths.get("").toFile().getAbsolutePath();
public static final String BIN_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bin";
public static final String TMP_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}tmp";
public static final String BAK_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bak";
public static final String CONFIG_FILE_PATH = STR."\{DATA_DIR_PATH}\{File.separator}config.yaml";
public static final String PBOC_FILE = STR."\{BIN_DIR_PATH}\{File.separator}pboc.exe";
public static final String CFG_CONVERT_FILE = STR."\{BIN_DIR_PATH}\{File.separator}CfgConvert.exe";
}

View File

@ -0,0 +1,117 @@
package cn.octopusyan.dmt.common.config;
import cn.octopusyan.dmt.Application;
import cn.octopusyan.dmt.common.base.BaseController;
import cn.octopusyan.dmt.common.util.FxmlUtil;
import cn.octopusyan.dmt.common.util.ProcessesUtil;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* 上下文
*
* @author octopus_yan
*/
public class Context {
@Getter
private static Application application;
private static final Logger log = LoggerFactory.getLogger(Context.class);
public static final ObjectProperty<Scene> sceneProperty = new SimpleObjectProperty<>();
/**
* 控制器集合
*/
@Getter
private static final Map<String, BaseController<?>> controllers = new HashMap<>();
private Context() {
throw new IllegalStateException("Utility class");
}
// 获取控制工厂
public static Callback<Class<?>, Object> getControlFactory() {
return type -> {
try {
return type.getDeclaredConstructor().newInstance();
} catch (Exception e) {
log.error("", e);
return null;
}
};
}
public static void setApplication(Application application) {
Context.application = application;
}
/**
* 有此类所在路径决定相对路径
*
* @param path 资源文件相对路径
* @return 资源文件路径
*/
// 加载资源文件
public static URL load(String path) {
return Context.class.getResource(path);
}
/**
* 初始化场景
*
* @return Scene
*/
public static Scene initScene() {
try {
FXMLLoader loader = FxmlUtil.load("main-view");
//底层面板
Pane root = loader.load();
Optional.ofNullable(sceneProperty.get()).ifPresentOrElse(
s -> s.setRoot(root),
() -> {
Scene scene = new Scene(root, root.getPrefWidth() + 20, root.getPrefHeight() + 20, Color.TRANSPARENT);
URL resource = Objects.requireNonNull(Context.class.getResource("/css/main-view.css"));
scene.getStylesheets().addAll(resource.toExternalForm());
scene.setFill(Color.TRANSPARENT);
sceneProperty.set(scene);
}
);
} catch (Throwable e) {
log.error("loadScene error", e);
}
return sceneProperty.get();
}
public static void openUrl(String url) {
getApplication().getHostServices().showDocument(url);
}
public static void openFolder(File file) {
openFile(file);
}
public static void openFile(File file) {
if (!file.exists()) return;
if (file.isDirectory()) {
ProcessesUtil.init(file.getAbsolutePath()).exec("explorer.exe .");
} else {
ProcessesUtil.init(file.getParentFile().getAbsolutePath()).exec(STR."explorer.exe /select,\{file.getName()}");
}
}
}

View File

@ -0,0 +1,11 @@
package cn.octopusyan.dmt.common.config;
/**
* 名称常量
*
* @author octopus_yan
*/
public class LabelConstants {
public static final String CONFIRM = "确认";
public static final String CANCEL = "取消";
}

View File

@ -0,0 +1,29 @@
package cn.octopusyan.dmt.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 代理类型
*
* @author octopus_yan
*/
@Getter
@RequiredArgsConstructor
public enum ProxySetup {
/**
* 不使用代理
*/
NO_PROXY("no_proxy", "不使用代理"),
/**
* 系统代理
*/
SYSTEM("system", "系统代理"),
/**
* 自定义代理
*/
MANUAL("manual", "自定义代理");
private final String code;
private final String name;
}

View File

@ -0,0 +1,253 @@
package cn.octopusyan.dmt.common.manager;
import atlantafx.base.theme.*;
import cn.octopusyan.dmt.Application;
import cn.octopusyan.dmt.common.config.Constants;
import cn.octopusyan.dmt.common.enums.ProxySetup;
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
import cn.octopusyan.dmt.model.ConfigModel;
import cn.octopusyan.dmt.model.ProxyInfo;
import cn.octopusyan.dmt.model.Translate;
import cn.octopusyan.dmt.model.UpgradeConfig;
import cn.octopusyan.dmt.translate.TranslateApi;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import javafx.application.Platform;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 客户端设置
*
* @author octopus_yan@foxmail.com
*/
public class ConfigManager {
private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
public static ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
public static final UpgradeConfig upgradeConfig = new UpgradeConfig();
public static final String DEFAULT_THEME = new PrimerLight().getName();
public static List<Theme> THEME_LIST = List.of(
new PrimerLight(), new PrimerDark(),
new NordLight(), new NordDark(),
new CupertinoLight(), new CupertinoDark(),
new Dracula()
);
public static Map<String, Theme> THEME_MAP = THEME_LIST.stream()
.collect(Collectors.toMap(Theme::getName, Function.identity()));
private static ConfigModel configModel;
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
}
public static void load() {
configModel = loadConfig(Constants.CONFIG_FILE_PATH, ConfigModel.class);
if (configModel == null)
configModel = new ConfigModel();
}
public static <T> T loadConfig(String path, Class<T> clazz) {
File src = new File(path);
try {
if (!src.exists()) {
checkFile(src, clazz);
}
return objectMapper.readValue(src, clazz);
} catch (Exception e) {
logger.error(String.format("load %s error", clazz.getSimpleName()), e);
}
return null;
}
private static <T> void checkFile(File src, Class<T> clazz) throws Exception {
if (!src.exists()) {
File parentDir = FileUtils.createParentDirectories(src);
if (!parentDir.exists())
logger.error("{} 创建失败", src.getAbsolutePath());
}
objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance());
}
public static void save() {
try {
objectMapper.writeValue(new File(Constants.CONFIG_FILE_PATH), configModel);
} catch (IOException e) {
logger.error("save config error", e);
}
}
// --------------------------------{ 主题 }------------------------------------------
public static String themeName() {
return configModel.getTheme();
}
public static Theme theme() {
return THEME_MAP.get(themeName());
}
public static void theme(Theme theme) {
Application.setUserAgentStylesheet(theme.getUserAgentStylesheet());
configModel.setTheme(theme.getName());
}
// --------------------------------{ 翻译接口配置 }------------------------------------------
public static TranslateApi translateApi() {
return TranslateApi.get(configModel.getTranslate().getUse());
}
public static void translateApi(TranslateApi api) {
configModel.getTranslate().setUse(api.getName());
}
public static Translate.Config getTranslateConfig(TranslateApi api) {
return Optional.of(configModel.getTranslate().getConfig().get(api.getName()))
.orElse(api.translate());
}
public static boolean hasTranslateApiKey(TranslateApi api) {
return StringUtils.isNoneEmpty(getTranslateConfig(api).getAppId());
}
public static void translateAppid(TranslateApi api, String appId) {
getTranslateConfig(api).setAppId(appId);
}
public static String translateAppid(TranslateApi api) {
return getTranslateConfig(api).getAppId();
}
public static void translateApikey(TranslateApi api, String secretKey) {
getTranslateConfig(api).setSecretKey(secretKey);
}
public static String translateApikey(TranslateApi api) {
return getTranslateConfig(api).getSecretKey();
}
public static void translateQps(TranslateApi api, int qps) {
getTranslateConfig(api).setQps(qps);
}
public static int translateQps(TranslateApi api) {
return getTranslateConfig(api).getQps();
}
// --------------------------------{ 网络代理 }------------------------------------------
public static ProxySetup proxySetup() {
return ProxySetup.valueOf(StringUtils.upperCase(getProxyInfo().getSetup()));
}
public static void proxyTestUrl(String url) {
getProxyInfo().setTestUrl(url);
}
public static String proxyTestUrl() {
return getProxyInfo().getTestUrl();
}
public static void proxySetup(ProxySetup setup) {
getProxyInfo().setSetup(setup.getCode());
switch (setup) {
case NO_PROXY -> HttpUtil.getInstance().clearProxy();
case SYSTEM, MANUAL -> {
if (ProxySetup.MANUAL.equals(setup) && !hasProxy())
return;
HttpUtil.getInstance().proxy(setup, ConfigManager.getProxyInfo());
}
}
}
public static boolean hasProxy() {
if (configModel == null)
return false;
ProxyInfo proxyInfo = getProxyInfo();
return proxyInfo != null
&& StringUtils.isNoneEmpty(proxyInfo.getHost())
&& StringUtils.isNoneEmpty(proxyInfo.getPort())
&& Integer.parseInt(proxyInfo.getPort()) > 0;
}
public static ProxyInfo getProxyInfo() {
ProxyInfo proxyInfo = configModel.getProxy();
if (proxyInfo == null)
setProxyInfo(new ProxyInfo());
return configModel.getProxy();
}
private static void setProxyInfo(ProxyInfo info) {
configModel.setProxy(info);
}
public static String proxyHost() {
return getProxyInfo().getHost();
}
public static void proxyHost(String host) {
getProxyInfo().setHost(host);
}
public static String proxyPort() {
return getProxyInfo().getPort();
}
public static int getProxyPort() {
return Integer.parseInt(proxyPort());
}
public static void proxyPort(String port) {
if (!NumberUtils.isParsable(port)) return;
getProxyInfo().setPort(port);
}
public static void checkProxy(BiConsumer<Boolean, String> consumer) {
if (ProxySetup.SYSTEM.equals(proxySetup())) {
consumer.accept(true, "");
return;
}
if (!hasProxy()) return;
ThreadPoolManager.getInstance().execute(() -> {
try {
try (Socket socket = new Socket(proxyHost(), getProxyPort())) {
Platform.runLater(() -> consumer.accept(true, "success"));
} catch (IOException e) {
Platform.runLater(() -> consumer.accept(false, "connection timed out"));
}
} catch (Exception e) {
logger.error(STR."host=\{proxyHost()},port=\{proxyPort()}", e);
Platform.runLater(() -> consumer.accept(false, e.getMessage()));
}
});
}
// --------------------------------{ 版本检查 }------------------------------------------
public static UpgradeConfig upgradeConfig() {
return upgradeConfig;
}
}

View File

@ -0,0 +1,371 @@
package cn.octopusyan.dmt.common.manager.http;
import java.net.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
/**
* Cookie 管理
*
* @author octopus_yan
*/
public class CookieManager {
private static final InMemoryCookieStore inMemoryCookieStore = new InMemoryCookieStore();
private static final java.net.CookieManager cookieManager =
new java.net.CookieManager(inMemoryCookieStore, CookiePolicy.ACCEPT_ALL);
public static java.net.CookieManager get() {
return cookieManager;
}
public static InMemoryCookieStore getStore() {
return inMemoryCookieStore;
}
public static class InMemoryCookieStore implements CookieStore {
// the in-memory representation of cookies
private final List<HttpCookie> cookieJar;
// the cookies are indexed by its domain and associated uri (if present)
// CAUTION: when a cookie removed from main data structure (i.e. cookieJar),
// it won't be cleared in domainIndex & uriIndex. Double-check the
// presence of cookie when retrieve one form index store.
private final Map<String, List<HttpCookie>> domainIndex;
private final Map<URI, List<HttpCookie>> uriIndex;
// use ReentrantLock instead of synchronized for scalability
private final ReentrantLock lock;
/**
* The default ctor
*/
public InMemoryCookieStore() {
cookieJar = new ArrayList<>();
domainIndex = new HashMap<>();
uriIndex = new HashMap<>();
lock = new ReentrantLock(false);
}
/**
* Add one cookie into cookie store.
*/
public void add(URI uri, HttpCookie cookie) {
// pre-condition : argument can't be null
if (cookie == null) {
throw new NullPointerException("cookie is null");
}
lock.lock();
try {
// remove the ole cookie if there has had one
cookieJar.remove(cookie);
// add new cookie if it has a non-zero max-age
if (cookie.getMaxAge() != 0) {
cookieJar.add(cookie);
// and add it to domain index
if (cookie.getDomain() != null) {
addIndex(domainIndex, cookie.getDomain(), cookie);
}
if (uri != null) {
// add it to uri index, too
addIndex(uriIndex, getEffectiveURI(uri), cookie);
}
}
} finally {
lock.unlock();
}
}
/**
* Get all cookies, which:
* 1) given uri domain-matches with, or, associated with
* given uri when added to the cookie store.
* 3) not expired.
* See RFC 2965 sec. 3.3.4 for more detail.
*/
public List<HttpCookie> get(URI uri) {
// argument can't be null
if (uri == null) {
throw new NullPointerException("uri is null");
}
List<HttpCookie> cookies = new ArrayList<>();
boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
lock.lock();
try {
// check domainIndex first
getInternal1(cookies, domainIndex, uri.getHost(), secureLink);
// check uriIndex then
getInternal2(cookies, uriIndex, getEffectiveURI(uri), secureLink);
} finally {
lock.unlock();
}
return cookies;
}
/**
* Get all cookies in cookie store, except those have expired
*/
public List<HttpCookie> getCookies() {
List<HttpCookie> rt;
lock.lock();
try {
cookieJar.removeIf(HttpCookie::hasExpired);
} finally {
rt = Collections.unmodifiableList(cookieJar);
lock.unlock();
}
return rt;
}
/**
* Get all URIs, which are associated with at least one cookie
* of this cookie store.
*/
public List<URI> getURIs() {
List<URI> uris;
lock.lock();
try {
Iterator<URI> it = uriIndex.keySet().iterator();
while (it.hasNext()) {
URI uri = it.next();
List<HttpCookie> cookies = uriIndex.get(uri);
if (cookies == null || cookies.isEmpty()) {
// no cookies list or an empty list associated with
// this uri entry, delete it
it.remove();
}
}
} finally {
uris = new ArrayList<>(uriIndex.keySet());
lock.unlock();
}
return uris;
}
/**
* Remove a cookie from store
*/
public boolean remove(URI uri, HttpCookie ck) {
// argument can't be null
if (ck == null) {
throw new NullPointerException("cookie is null");
}
boolean modified;
lock.lock();
try {
modified = cookieJar.remove(ck);
} finally {
lock.unlock();
}
return modified;
}
/**
* Remove all cookies in this cookie store.
*/
public boolean removeAll() {
lock.lock();
try {
if (cookieJar.isEmpty()) {
return false;
}
cookieJar.clear();
domainIndex.clear();
uriIndex.clear();
} finally {
lock.unlock();
}
return true;
}
/* ---------------- Private operations -------------- */
/*
* This is almost the same as HttpCookie.domainMatches except for
* one difference: It won't reject cookies when the 'H' part of the
* domain contains a dot ('.').
* I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com
* and the cookie domain is .domain.com, then it should be rejected.
* However that's not how the real world works. Browsers don't reject and
* some sites, like yahoo.com do actually expect these cookies to be
* passed along.
* And should be used for 'old' style cookies (aka Netscape type of cookies)
*/
private boolean netscapeDomainMatches(String domain, String host) {
if (domain == null || host == null) {
return false;
}
// if there's no embedded dot in domain and domain is not .local
boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
int embeddedDotInDomain = domain.indexOf('.');
if (embeddedDotInDomain == 0) {
embeddedDotInDomain = domain.indexOf('.', 1);
}
if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
return false;
}
// if the host name contains no dot and the domain name is .local
int firstDotInHost = host.indexOf('.');
if (firstDotInHost == -1 && isLocalDomain) {
return true;
}
int domainLength = domain.length();
int lengthDiff = host.length() - domainLength;
if (lengthDiff == 0) {
// if the host name and the domain name are just string-compare equal
return host.equalsIgnoreCase(domain);
} else if (lengthDiff > 0) {
// need to check H & D component
String H = host.substring(0, lengthDiff);
String D = host.substring(lengthDiff);
return (D.equalsIgnoreCase(domain));
} else if (lengthDiff == -1) {
// if domain is actually .host
return (domain.charAt(0) == '.' &&
host.equalsIgnoreCase(domain.substring(1)));
}
return false;
}
private void getInternal1(List<HttpCookie> cookies, Map<String, List<HttpCookie>> cookieIndex,
String host, boolean secureLink) {
// Use a separate list to handle cookies that need to be removed so
// that there is no conflict with iterators.
ArrayList<HttpCookie> toRemove = new ArrayList<>();
for (Map.Entry<String, List<HttpCookie>> entry : cookieIndex.entrySet()) {
String domain = entry.getKey();
List<HttpCookie> lst = entry.getValue();
for (HttpCookie c : lst) {
if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) ||
(c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) {
if ((cookieJar.contains(c))) {
// the cookie still in main cookie store
if (!c.hasExpired()) {
// don't add twice and make sure it's the proper
// security level
if ((secureLink || !c.getSecure()) &&
!cookies.contains(c)) {
cookies.add(c);
}
} else {
toRemove.add(c);
}
} else {
// the cookie has been removed from main store,
// so also remove it from domain indexed store
toRemove.add(c);
}
}
}
// Clear up the cookies that need to be removed
for (HttpCookie c : toRemove) {
lst.remove(c);
cookieJar.remove(c);
}
toRemove.clear();
}
}
// @param cookies [OUT] contains the found cookies
// @param cookieIndex the index
// @param comparator the prediction to decide whether or not
// a cookie in index should be returned
private <T> void getInternal2(List<HttpCookie> cookies,
Map<T, List<HttpCookie>> cookieIndex,
Comparable<T> comparator, boolean secureLink) {
for (T index : cookieIndex.keySet()) {
if (comparator.compareTo(index) == 0) {
List<HttpCookie> indexedCookies = cookieIndex.get(index);
// check the list of cookies associated with this domain
if (indexedCookies != null) {
Iterator<HttpCookie> it = indexedCookies.iterator();
while (it.hasNext()) {
HttpCookie ck = it.next();
if (cookieJar.contains(ck)) {
// the cookie still in main cookie store
if (!ck.hasExpired()) {
// don't add twice
if ((secureLink || !ck.getSecure()) &&
!cookies.contains(ck))
cookies.add(ck);
} else {
it.remove();
cookieJar.remove(ck);
}
} else {
// the cookie has been removed from main store,
// so also remove it from domain indexed store
it.remove();
}
}
} // end of indexedCookies != null
} // end of comparator.compareTo(index) == 0
} // end of cookieIndex iteration
}
// add 'cookie' indexed by 'index' into 'indexStore'
private <T> void addIndex(Map<T, List<HttpCookie>> indexStore,
T index,
HttpCookie cookie) {
if (index != null) {
List<HttpCookie> cookies = indexStore.get(index);
if (cookies != null) {
// there may already have the same cookie, so remove it first
cookies.remove(cookie);
cookies.add(cookie);
} else {
cookies = new ArrayList<>();
cookies.add(cookie);
indexStore.put(index, cookies);
}
}
}
//
// for cookie purpose, the effective uri should only be http://host
// the path will be taken into account when path-match algorithm applied
//
private URI getEffectiveURI(URI uri) {
URI effectiveURI;
try {
effectiveURI = new URI("http",
uri.getHost(),
null, // path component
null, // query component
null // fragment component
);
} catch (URISyntaxException ignored) {
effectiveURI = uri;
}
return effectiveURI;
}
}
}

View File

@ -0,0 +1,116 @@
package cn.octopusyan.dmt.common.manager.http;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.concurrent.Executor;
/**
* Http配置参数
*
* @author octopus_yan@foxmail.com
*/
@Data
public class HttpConfig {
private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
static {
// 使用系统默认代理
System.setProperty("java.net.useSystemProxies", "true");
}
/**
* http版本
*/
private HttpClient.Version version = HttpClient.Version.HTTP_2;
/**
* 转发策略
*/
private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
/**
* 线程池
*/
private Executor executor;
/**
* 认证
*/
private Authenticator authenticator;
/**
* 代理
*/
private ProxySelector proxySelector;
/**
* CookieHandler
*/
private CookieHandler cookieHandler;
/**
* sslContext
*/
private SSLContext sslContext;
/**
* sslParams
*/
private SSLParameters sslParameters;
/**
* 连接超时时间毫秒
*/
private int connectTimeout = 10000;
/**
* 默认读取数据超时时间
*/
private int defaultReadTimeout = 1200000;
public HttpConfig() {
// SSL
sslParameters = new SSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("");
sslParameters.setProtocols(new String[]{"TLSv1.2"});
try {
sslContext = SSLContext.getInstance("TLSv1.2");
System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
sslContext.init(null, trustAllCertificates, new SecureRandom());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.error("", e);
}
}
private static final TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; // Not relevant.
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
// TODO Auto-generated method stub
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
// TODO Auto-generated method stub
}
}};
}

View File

@ -1,7 +1,13 @@
package cn.octopusyan.dayzmodtranslator.manager.http;
package cn.octopusyan.dmt.common.manager.http;
import com.alibaba.fastjson2.JSONObject;
import cn.octopusyan.dmt.common.enums.ProxySetup;
import cn.octopusyan.dmt.common.manager.http.response.BodyHandler;
import cn.octopusyan.dmt.common.util.JsonUtil;
import cn.octopusyan.dmt.model.ProxyInfo;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
@ -11,15 +17,19 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
/**
* 网络请求封装
*
* @author octopus_yan@foxmail.com
*/
@Slf4j
public class HttpUtil {
private volatile static HttpUtil util;
private volatile HttpClient httpClient;
@ -57,15 +67,20 @@ public class HttpUtil {
return builder.build();
}
public HttpUtil proxy(String host, int port) {
public void proxy(ProxySetup setup, ProxyInfo proxy) {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(host, port);
ProxySelector other = ProxySelector.of(unresolved);
this.httpConfig.setProxySelector(other);
switch (setup) {
case NO_PROXY -> clearProxy();
case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
case MANUAL -> {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(proxy.getHost(), Integer.parseInt(proxy.getPort()));
httpConfig.setProxySelector(ProxySelector.of(unresolved));
}
}
this.httpClient = createClient(httpConfig);
return this;
}
public void clearProxy() {
@ -76,24 +91,26 @@ public class HttpUtil {
httpClient = createClient(httpConfig);
}
public HttpClient getHttpClient() {
return httpClient;
public void close() {
if (httpClient == null) return;
httpClient.close();
}
public String get(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
public String get(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header).GET();
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
public String post(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
public String post(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri, header)
.POST(HttpRequest.BodyPublishers.ofString(param.toJSONString()));
.header("Content-Type", "application/json;charset=utf-8")
.POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJsonString(param)));
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
public String postForm(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
public String postForm(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
.POST(HttpRequest.BodyPublishers.noBody());
@ -101,39 +118,73 @@ public class HttpUtil {
return response.body();
}
private HttpRequest.Builder getRequest(String uri, JSONObject header) {
public void download(String url, String savePath, BiConsumer<Long, Long> listener) throws IOException, InterruptedException {
HttpRequest request = getRequest(url, null).build();
// 检查bin目录
File binDir = new File(savePath);
if (!binDir.exists()) {
log.debug(STR."dir [\{savePath}] not exists");
//noinspection ResultOfMethodCallIgnored
binDir.mkdirs();
log.debug(STR."created dir [\{savePath}]");
}
// 下载处理器
var handler = BodyHandler.create(
Path.of(savePath),
StandardOpenOption.CREATE, StandardOpenOption.WRITE
);
// 下载监听
if (listener != null)
handler.listener(listener);
HttpResponse<Path> response = httpClient.send(request, handler);
}
private HttpRequest.Builder getRequest(String uri, JsonNode header) {
HttpRequest.Builder request = HttpRequest.newBuilder();
// 请求地址
request.uri(URI.create(uri));
// 请求头
if (header != null && !header.isEmpty()) {
for (String key : header.keySet()) {
request.header(key, header.getString(key));
for (Map.Entry<String, JsonNode> property : header.properties()) {
String key = property.getKey();
request.header(key, JsonUtil.toJsonString(property.getValue()));
}
}
// Cookie
// List<HttpCookie> cookies = CookieManager.getStore().get(URI.create(uri));
// if (!cookies.isEmpty()) {
// String cookie = cookies.stream()
// .map(item -> STR."\{item.getName()}=\{item.getValue()}")
// .collect(Collectors.joining(";"));
// request.header("Cookie", cookie);
// }
return request;
}
private String createFormParams(JSONObject params) {
private String createFormParams(JsonNode params) {
StringBuilder formParams = new StringBuilder();
if (params == null) {
return formParams.toString();
}
for (String key : params.keySet()) {
Object value = params.get(key);
if (value instanceof String) {
value = URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8);
for (Map.Entry<String, JsonNode> property : params.properties()) {
String key = property.getKey();
JsonNode value = params.get(key);
if (value.isTextual()) {
String value_ = URLEncoder.encode(String.valueOf(value.asText()), StandardCharsets.UTF_8);
formParams.append("&").append(key).append("=").append(value_);
} else if (value.isNumber()) {
formParams.append("&").append(key).append("=").append(value);
} else if (value instanceof Number) {
formParams.append("&").append(key).append("=").append(value);
} else if (value instanceof List) {
formParams.append("&").append(key).append("=").append(params.getJSONArray(key));
} else if (value.isArray()) {
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
} else {
formParams.append("&").append(key).append("=").append(params.getJSONObject(key));
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
}
}
if (!formParams.isEmpty()) {
formParams = new StringBuilder("?" + formParams.substring(1));
formParams = new StringBuilder(STR."?\{formParams.substring(1)}");
}
return formParams.toString();

View File

@ -0,0 +1,102 @@
package cn.octopusyan.dmt.common.manager.http.response;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* 下载处理
*
* @author octopus_yan
*/
@Slf4j
public class BodyHandler implements HttpResponse.BodyHandler<Path> {
private final HttpResponse.BodyHandler<Path> handler;
private BiConsumer<Long, Long> consumer;
private BodyHandler(HttpResponse.BodyHandler<Path> handler) {
this.handler = handler;
}
public static BodyHandler create(Path directory, OpenOption... openOptions) {
return new BodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions));
}
@Override
public HttpResponse.BodySubscriber<Path> apply(HttpResponse.ResponseInfo responseInfo) {
AtomicLong length = new AtomicLong(-1);
// 获取文件大小
Optional<String> string = responseInfo.headers().firstValue("content-length");
string.ifPresentOrElse(s -> {
length.set(Long.parseLong(s));
log.debug(STR."========={content-length = \{s}}=========");
}, () -> {
String msg = "response not has header [content-length]";
log.error(msg);
});
BodySubscriber subscriber = new BodySubscriber(handler.apply(responseInfo));
subscriber.setConsumer(progress -> consumer.accept(length.get(), progress));
return subscriber;
}
public void listener(BiConsumer<Long, Long> consumer) {
this.consumer = consumer;
}
public static class BodySubscriber implements HttpResponse.BodySubscriber<Path> {
private final HttpResponse.BodySubscriber<Path> subscriber;
private final AtomicLong progress = new AtomicLong(0);
@Setter
private Consumer<Long> consumer;
public BodySubscriber(HttpResponse.BodySubscriber<Path> subscriber) {
this.subscriber = subscriber;
}
@Override
public CompletionStage<Path> getBody() {
return subscriber.getBody();
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscriber.onSubscribe(subscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
subscriber.onNext(item);
// 记录进度
for (ByteBuffer byteBuffer : item) {
progress.addAndGet(byteBuffer.limit());
}
consumer.accept(progress.get());
}
@Override
public void onError(Throwable throwable) {
subscriber.onError(throwable);
}
@Override
public void onComplete() {
subscriber.onComplete();
consumer.accept(progress.get());
}
}
}

View File

@ -1,4 +1,4 @@
package cn.octopusyan.dayzmodtranslator.manager.thread;
package cn.octopusyan.dmt.common.manager.thread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -26,7 +26,7 @@ public class ThreadFactory implements java.util.concurrent.ThreadFactory {
public ThreadFactory(String prefix) {
group = Thread.currentThread().getThreadGroup();
namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
namePrefix = STR."\{prefix}-\{poolNumber.getAndIncrement()}-thread-";
}
@Override
@ -36,9 +36,7 @@ public class ThreadFactory implements java.util.concurrent.ThreadFactory {
namePrefix + threadNumber.getAndIncrement(),
0);
t.setUncaughtExceptionHandler((t1, e) -> {
logger.error("thread : {}, error", t1.getName(), e);
});
t.setUncaughtExceptionHandler((t1, e) -> logger.error("thread : {}, error", t1.getName(), e));
if (t.isDaemon())
t.setDaemon(false);

View File

@ -0,0 +1,55 @@
package cn.octopusyan.dmt.common.manager.thread;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池管理类
*/
public final class ThreadPoolManager extends ThreadPoolExecutor {
private static volatile ThreadPoolManager sInstance;
private static final List<ThreadPoolManager> poolManagerList = new ArrayList<>();
private ThreadPoolManager() {
this("");
}
private ThreadPoolManager(String threadPoolName) {
super(32,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadFactory(StringUtils.isEmpty(threadPoolName) ? ThreadFactory.DEFAULT_THREAD_PREFIX : threadPoolName),
new DiscardPolicy());
}
public static ThreadPoolManager getInstance(String threadPoolName) {
ThreadPoolManager threadPoolManager = new ThreadPoolManager(threadPoolName);
poolManagerList.add(threadPoolManager);
return threadPoolManager;
}
public static ThreadPoolManager getInstance() {
if (sInstance == null) {
synchronized (ThreadPoolManager.class) {
if (sInstance == null) {
sInstance = new ThreadPoolManager();
}
}
}
return sInstance;
}
public static void shutdownAll() {
getInstance().shutdown();
poolManagerList.forEach(ThreadPoolExecutor::shutdown);
}
}

View File

@ -1,18 +1,21 @@
package cn.octopusyan.dayzmodtranslator.util;
package cn.octopusyan.dmt.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.datatransfer.*;
import java.io.IOException;
/**
* <p> author : octopus yan
* <p> email : octopus_yan@foxmail.com
* <p> description : 剪切板工具
* <p> create : 2022-4-14 23:21
* 剪切板工具
*
* @author octopus_yan
*/
public class ClipUtil {
//获取系统剪切板
private static final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
private static final Logger log = LoggerFactory.getLogger(ClipUtil.class);
public static void setClip(String data) {
//构建String数据类型
@ -28,7 +31,7 @@ public class ClipUtil {
//从数据中获取文本值
return (String) content.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException | IOException e) {
e.printStackTrace();
log.error("", e);
return null;
}
}

View File

@ -1,5 +1,6 @@
package cn.octopusyan.dayzmodtranslator.util;
package cn.octopusyan.dmt.common.util;
import cn.octopusyan.dmt.common.config.Context;
import javafx.fxml.FXMLLoader;
import javafx.fxml.JavaFXBuilderFactory;
@ -19,7 +20,7 @@ public class FxmlUtil {
FxmlUtil.class.getResource(prefix + name + suffix),
null,
new JavaFXBuilderFactory(),
null,
Context.getControlFactory(),
StandardCharsets.UTF_8
);
}

View File

@ -0,0 +1,187 @@
package cn.octopusyan.dmt.common.util;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
/**
* Jackson 封装工具类
*
* @author octopus_yan
*/
public class JsonUtil {
private static final Logger log = LoggerFactory.getLogger(JsonUtil.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 时间日期格式
*/
private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
static {
//对象的所有字段全部列入序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
//取消默认转换timestamps形式
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//忽略空Bean转json的错误
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//所有的日期格式都统一为以下的格式即yyyy-MM-dd HH:mm:ss
objectMapper.setDateFormat(new SimpleDateFormat(STANDARD_FORMAT));
//忽略 在json字符串中存在但在java对象中不存在对应属性的情况。防止错误
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* Json字符串 转 JavaBean
*
* @param jsonString Json字符串
* @param clazz Java类对象
* @param <T> Java类
* @return JavaBean
*/
public static <T> T parseObject(String jsonString, Class<T> clazz) {
T t = null;
try {
t = objectMapper.readValue(jsonString, clazz);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return t;
}
/**
* 读取Json文件 转 JavaBean
*
* @param file Json文件
* @param clazz Java类对象
* @param <T> Java类
* @return JavaBean
*/
public static <T> T parseObject(File file, Class<T> clazz) {
T t = null;
try {
t = objectMapper.readValue(file, clazz);
} catch (IOException e) {
log.error("失败:{}", e.getMessage());
}
return t;
}
/**
* 读取Json字符串 转 JavaBean集合
*
* @param jsonArray Json字符串
* @param reference 类型
* @param <T> JavaBean类型
* @return JavaBean集合
*/
public static <T> T parseJsonArray(String jsonArray, TypeReference<T> reference) {
T t = null;
try {
t = objectMapper.readValue(jsonArray, reference);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return t;
}
/**
* JavaBean 转 Json字符串
*
* @param object JavaBean
* @return Json字符串
*/
public static String toJsonString(Object object) {
String jsonString = null;
try {
jsonString = objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return jsonString;
}
/**
* JavaBean 转 字节数组
*
* @param object JavaBean
* @return 字节数组
*/
public static byte[] toByteArray(Object object) {
byte[] bytes = null;
try {
bytes = objectMapper.writeValueAsBytes(object);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return bytes;
}
/**
* JavaBean序列化到文件
*
* @param file 写入文件对象
* @param object JavaBean
*/
public static void objectToFile(File file, Object object) {
try {
objectMapper.writeValue(file, object);
} catch (Exception e) {
log.error("失败:{}", e.getMessage());
}
}
/**
* Json字符串 转 JsonNode
*
* @param jsonString Json字符串
* @return JsonNode
*/
public static JsonNode parseJsonObject(String jsonString) {
JsonNode jsonNode = null;
try {
jsonNode = objectMapper.readTree(jsonString);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return jsonNode;
}
/**
* JavaBean 转 JsonNode
*
* @param object JavaBean
* @return JsonNode
*/
public static JsonNode parseJsonObject(Object object) {
return objectMapper.valueToTree(object);
}
/**
* JsonNode 转 Json字符串
*
* @param jsonNode JsonNode
* @return Json字符串
*/
public static String toJsonString(JsonNode jsonNode) {
String jsonString = null;
try {
jsonString = objectMapper.writeValueAsString(jsonNode);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return jsonString;
}
}

View File

@ -0,0 +1,119 @@
package cn.octopusyan.dmt.common.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 命令工具类
*
* @author octopus_yan@foxmail.com
*/
@Slf4j
public class ProcessesUtil {
private static final String NEW_LINE = System.lineSeparator();
public static final int[] EXIT_VALUES = {0, 1};
private final DefaultExecutor executor;
private final ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
private OnExecuteListener listener;
private CommandLine commandLine;
private static final Set<ProcessesUtil> set = new HashSet<>();
/**
* Prevent construction.
*/
private ProcessesUtil(String workingDirectory) {
this(new File(workingDirectory));
}
private ProcessesUtil(File workingDirectory) {
LogOutputStream logout = new LogOutputStream() {
@Override
protected void processLine(String line, int logLevel) {
if (listener != null)
listener.onExecute(line + NEW_LINE);
}
};
PumpStreamHandler streamHandler = new PumpStreamHandler(logout, logout);
executor = DefaultExecutor.builder()
.setExecuteStreamHandler(streamHandler)
.setWorkingDirectory(workingDirectory)
.get();
executor.setExitValues(EXIT_VALUES);
executor.setProcessDestroyer(processDestroyer);
}
public static ProcessesUtil init(String workingDirectory) {
return init(new File(workingDirectory));
}
public static ProcessesUtil init(File workingDirectory) {
ProcessesUtil util = new ProcessesUtil(workingDirectory);
set.add(util);
return util;
}
public boolean exec(String command) {
commandLine = CommandLine.parse(command);
try {
int execute = executor.execute(commandLine);
return Arrays.stream(EXIT_VALUES).anyMatch(item -> item == execute);
} catch (Exception e) {
log.error("exec error", e);
return false;
}
}
public void exec(String command, OnExecuteListener listener) {
this.listener = listener;
commandLine = CommandLine.parse(command);
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
if (listener != null) {
listener.onExecuteSuccess(Arrays.stream(EXIT_VALUES).noneMatch(item -> item == exitValue));
}
}
@Override
public void onProcessFailed(ExecuteException e) {
if (listener != null) {
listener.onExecuteError(e);
}
}
};
try {
executor.execute(commandLine, handler);
} catch (Exception e) {
if (listener != null) listener.onExecuteError(e);
}
}
public void destroy() {
if (processDestroyer.isEmpty()) return;
processDestroyer.run();
}
public boolean isRunning() {
return !processDestroyer.isEmpty();
}
public static void destroyAll() {
set.forEach(ProcessesUtil::destroy);
}
public interface OnExecuteListener {
void onExecute(String msg);
default void onExecuteSuccess(boolean success) {
}
default void onExecuteError(Exception e) {
}
}
}

View File

@ -1,10 +1,13 @@
package cn.octopusyan.dayzmodtranslator.util;
package cn.octopusyan.dmt.common.util;
import cn.octopusyan.dmt.utils.Resources;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
/**
@ -17,14 +20,16 @@ public class PropertiesUtils {
/**
* 主配置文件
*/
private Properties properties;
private final Properties properties;
/**
* 启用配置文件
*/
private Properties propertiesCustom;
private final Properties propertiesCustom;
private static PropertiesUtils propertiesUtils = new PropertiesUtils();
public static final Logger logger = LoggerFactory.getLogger(PropertiesUtils.class);
/**
* 私有构造禁止直接创建
*/
@ -32,17 +37,18 @@ public class PropertiesUtils {
// 读取配置启用的配置文件名
properties = new Properties();
propertiesCustom = new Properties();
InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("application.properties");
InputStream in = Resources.getResourceAsStream("application.properties");
try {
properties.load(in);
properties.load(new InputStreamReader(in));
// 加载启用的配置
String property = properties.getProperty("profiles.active");
if (!StringUtils.isBlank(property)) {
InputStream cin = PropertiesUtils.class.getClassLoader().getResourceAsStream("application-" + property + ".properties");
propertiesCustom.load(cin);
InputStream cin = Resources.getResourceAsStream("application-" + property + ".properties");
propertiesCustom.load(new InputStreamReader(cin));
}
} catch (IOException e) {
e.printStackTrace();
logger.error("读取配置文件失败", e);
}
}
@ -58,17 +64,6 @@ public class PropertiesUtils {
return propertiesUtils;
}
/**
* 获取单例
*
* @return PropertiesUtils
*/
public static PropertiesUtils getInstance(File file) {
PropertiesUtils util = new PropertiesUtils();
return util;
}
/**
* 根据属性名读取值
* 先去主配置查询如果查询不到就去启用配置查询

View File

@ -0,0 +1,69 @@
package cn.octopusyan.dmt.common.util;
import cn.octopusyan.dmt.Application;
import javafx.scene.layout.Pane;
import javafx.stage.Screen;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;
/**
* 工具
*
* @author octopus_yan
*/
public class ViewUtil {
// 获取系统缩放比
public static final double scaleX = Screen.getPrimary().getOutputScaleX();
public static final double scaleY = Screen.getPrimary().getOutputScaleY();
private static final Map<Pane, Double> paneXOffset = new HashMap<>();
private static final Map<Pane, Double> paneYOffset = new HashMap<>();
public static void bindShadow(Pane pane) {
pane.setStyle("""
-fx-background-radius: 5;
-fx-border-radius: 5;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 15, 0, 0, 0);
-fx-background-insets: 20;
-fx-padding: 20;
""");
}
public static void bindDragged(Pane pane) {
Stage stage = getStage(pane);
bindDragged(pane, stage);
}
public static void unbindDragged(Pane pane) {
pane.setOnMousePressed(null);
pane.setOnMouseDragged(null);
paneXOffset.remove(pane);
paneYOffset.remove(pane);
}
public static void bindDragged(Pane pane, Stage stage) {
pane.setOnMousePressed(event -> {
paneXOffset.put(pane, stage.getX() - event.getScreenX());
paneYOffset.put(pane, stage.getY() - event.getScreenY());
});
pane.setOnMouseDragged(event -> {
stage.setX(event.getScreenX() + paneXOffset.get(pane));
stage.setY(event.getScreenY() + paneYOffset.get(pane));
});
}
public static Stage getStage() {
return Application.getPrimaryStage();
}
public static Stage getStage(Pane pane) {
try {
return (Stage) pane.getScene().getWindow();
} catch (Throwable e) {
return getStage();
}
}
}

View File

@ -0,0 +1,451 @@
package cn.octopusyan.dmt.controller;
import atlantafx.base.controls.ModalPane;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Theme;
import cn.octopusyan.dmt.common.base.BaseController;
import cn.octopusyan.dmt.common.config.Context;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.common.util.ClipUtil;
import cn.octopusyan.dmt.common.util.FxmlUtil;
import cn.octopusyan.dmt.controller.component.WordEditController;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.task.TranslateTask;
import cn.octopusyan.dmt.view.ConsoleLog;
import cn.octopusyan.dmt.view.EditButtonTableCell;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import cn.octopusyan.dmt.view.filemanager.DirectoryTree;
import cn.octopusyan.dmt.viewModel.MainViewModel;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
/**
* 主界面
*
* @author octopus_yan
*/
public class MainController extends BaseController<MainViewModel> {
private static final ConsoleLog consoleLog = ConsoleLog.getInstance(MainController.class);
public Pane root;
public Menu viewStyle;
public static final ToggleGroup viewStyleGroup = new ToggleGroup();
public StackPane mainView;
//
public VBox translateView;
// 打开文件
public StackPane selectFileBox;
public VBox openFileView;
public VBox dragFileView;
public VBox loadFileView;
// 工具栏
public Button fileNameLabel;
public Button translate;
public ProgressBar translateProgress;
// 文件树加载
public DirectoryTree treeFileBox;
public VBox loadWordBox;
public ProgressBar loadWordProgressBar;
// 翻译界面
public Pane wordBox;
public TableView<WordItem> wordTable;
// 信息
public TitledPane titledPane;
public TextArea logArea;
public final ModalPane modalPane = new ModalPane();
// 文件选择器
public static final FileChooser fileChooser = new FileChooser();
static {
var extFilter = new FileChooser.ExtensionFilter("PBO files (*.pbo)", "*.pbo");
fileChooser.getExtensionFilters().add(extFilter);
}
@Override
public Pane getRootPanel() {
return root;
}
@Override
public void initData() {
// 界面样式
List<MenuItem> list = ConfigManager.THEME_LIST.stream().map(this::createViewStyleItem).toList();
viewStyle.getItems().addAll(list);
// 信息
ConsoleLog.init(logArea);
}
@Override
public void initViewStyle() {
// 遮罩
getRootPanel().getChildren().add(modalPane);
modalPane.displayProperty().addListener((_, _, val) -> {
if (!val) {
modalPane.setAlignment(Pos.CENTER);
modalPane.usePredefinedTransitionFactories(null);
}
});
}
@Override
public void initViewAction() {
// 文件拖拽
setDragAction(mainView);
// 复制单元格内容
Context.sceneProperty.addListener(_ -> Context.sceneProperty.get()
.getAccelerators()
.put(new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY), () -> {
ObservableList<TablePosition> selectedCells = wordTable.getSelectionModel().getSelectedCells();
for (TablePosition tablePosition : selectedCells) {
Object cellData = tablePosition.getTableColumn().getCellData(tablePosition.getRow());
// 设置剪切板
ClipUtil.setClip(cellData.toString());
}
})
);
// 日志栏清空
logArea.contextMenuProperty().addListener(_ ->
logArea.getContextMenu().getItems().addListener((ListChangeListener<MenuItem>) _ -> {
MenuItem clearLog = new MenuItem("清空");
clearLog.setOnAction(_ -> logArea.clear());
logArea.getContextMenu().getItems().add(clearLog);
})
);
}
/**
* 设置文件拖拽效果
*/
private void setDragAction(Pane fileBox) {
// 进入
fileBox.setOnDragEntered(dragEvent -> {
var dragboard = dragEvent.getDragboard();
if (dragboard.hasFiles() && isPboFile(dragboard.getFiles().getFirst())) {
selectFileBox.setVisible(true);
dragFileView.setVisible(true);
}
});
//离开
fileBox.setOnDragExited(_ -> {
selectFileBox.setVisible(false);
dragFileView.setVisible(false);
});
//
fileBox.setOnDragOver(dragEvent -> {
var dragboard = dragEvent.getDragboard();
if (dragEvent.getGestureSource() != fileBox && dragboard.hasFiles()) {
/* allow for both copying and moving, whatever user chooses */
dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
dragEvent.consume();
});
// 松手
fileBox.setOnDragDropped(dragEvent -> {
dragFileView.setVisible(false);
var db = dragEvent.getDragboard();
boolean success = false;
var file = db.getFiles().getFirst();
if (db.hasFiles() && isPboFile(file)) {
selectFile(file);
success = true;
}
/* 让源知道字符串是否已成功传输和使用 */
dragEvent.setDropCompleted(success);
dragEvent.consume();
});
}
/**
* 打开文件选择器
*/
public void selectFile() {
selectFile(fileChooser.showOpenDialog(getWindow()));
}
/**
* 打开代理设置
*/
public void openSetupProxy() throws IOException {
FXMLLoader load = FxmlUtil.load("setup/proxy-view");
AlertUtil.builder(false)
.title("网络代理设置")
.content((Pane) load.load())
.show();
}
/**
* 打开翻译设置
*/
public void openSetupTranslate() throws IOException {
FXMLLoader load = FxmlUtil.load("setup/translate-view");
AlertUtil.builder(false)
.title("翻译设置")
.content((Pane) load.load())
.show();
}
public void setFileName(String name) {
fileNameLabel.setText("PBO文件" + name);
}
/**
* 显示加载PBO文件
*/
public void onLoad() {
// 展示加载
selectFileBox.setVisible(true);
loadFileView.setVisible(true);
wordBox.getChildren().remove(wordTable);
}
/**
* 显示解包完成
*
* @param path 解包路径
*/
public void onUnpack(File path) {
// 加载解包目录
treeFileBox.loadRoot(path);
// 隐藏文件选择
loadFileView.setVisible(false);
selectFileBox.setVisible(false);
// 展示翻译界面
translateView.setVisible(true);
loadWordBox.setVisible(true);
}
/**
* 加载可翻译文本数据
*
* @param wordItems 文本列表
*/
public void onLoadWord(List<WordItem> wordItems) {
loadWordBox.setVisible(false);
wordBox.setVisible(true);
bindWordTable(wordItems);
translate.setDisable(false);
}
/**
* 打包完成
*
* @param packFile 打包临时文件
*/
public void onPackOver(File packFile) {
// 选择文件保存地址
fileChooser.setInitialFileName(packFile.getName());
File file = fileChooser.showSaveDialog(getWindow());
if (file == null)
return;
if (file.exists()) {
//文件已存在,则删除覆盖文件
FileUtils.deleteQuietly(file);
}
String exportFilePath = file.getAbsolutePath();
consoleLog.info(STR."导出文件路径 => \{exportFilePath}");
try {
FileUtils.copyFile(packFile, file);
} catch (IOException e) {
consoleLog.error("保存文件失败!", e);
Platform.runLater(() -> AlertUtil.exception(e).content("保存文件失败!").show());
}
}
public void startTranslate() {
viewModel.startTranslate();
}
public void startPack() {
viewModel.pack();
}
public void selectAllLog() {
logArea.selectAll();
}
public void copyLog() {
logArea.copy();
}
public void clearLog() {
logArea.clear();
}
// ======================================{ }========================================
/**
* 打开文件
*/
private void selectFile(File file) {
viewModel.selectFile(file);
viewModel.unpack();
}
/**
* 绑定表格数据
*
* @param words 单词列表
*/
private void bindWordTable(List<WordItem> words) {
if (wordTable == null) {
wordTable = new TableView<>();
// 填满
VBox.setVgrow(wordTable, Priority.ALWAYS);
// 可编辑
wordTable.setEditable(true);
// 自动调整列宽
wordTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
// 边框
Styles.toggleStyleClass(wordTable, Styles.BORDERED);
// // 行分隔
// Styles.toggleStyleClass(wordTable, Styles.STRIPED);
// 单元格选择模式而不是行选择
wordTable.getSelectionModel().setCellSelectionEnabled(true);
// 不允许选择多个单元格
wordTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
// 创建列
TableColumn<WordItem, String> colFile = createColumn("文件");
colFile.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getFile().getName()));
TableColumn<WordItem, String> colOriginal = createColumn("原文");
colOriginal.setCellValueFactory(param -> param.getValue().getOriginalProperty());
TableColumn<WordItem, String> colChinese = createColumn("中文翻译");
colChinese.setCellValueFactory(param -> param.getValue().getChineseProperty());
colChinese.setEditable(true);
TableColumn<WordItem, WordItem> colIcon = new TableColumn<>("");
colIcon.setSortable(false);
colIcon.setCellFactory(EditButtonTableCell.forTableColumn(item -> {
// 展示编辑弹窗
try {
showModal(getEditWordPane(item), false);
} catch (IOException e) {
consoleLog.error("加载布局失败", e);
}
}, item -> {
// 翻译当前文本
new TranslateTask(Collections.singletonList(item)).execute();
}));
wordTable.getColumns().add(colFile);
wordTable.getColumns().add(colOriginal);
wordTable.getColumns().add(colChinese);
wordTable.getColumns().add(colIcon);
}
// 添加表数据
wordTable.getItems().clear();
wordBox.getChildren().addFirst(wordTable);
wordTable.getItems().addAll(words);
}
/**
* 表字段创建
*
* @param colName 列名
* @return 列定义
*/
private TableColumn<WordItem, String> createColumn(String colName) {
TableColumn<WordItem, String> tableColumn = new TableColumn<>(colName);
tableColumn.setCellFactory(TextFieldTableCell.forTableColumn());
tableColumn.setPrefWidth(150);
tableColumn.setSortable(false);
tableColumn.setEditable("中文翻译".equals(colName));
return tableColumn;
}
private MenuItem createViewStyleItem(Theme theme) {
var item = new RadioMenuItem(theme.getName());
item.setSelected(theme.getName().equals(ConfigManager.themeName()));
item.setToggleGroup(viewStyleGroup);
item.setUserData(theme);
item.selectedProperty().subscribe(selected -> {
if (!selected) return;
ConfigManager.theme(theme);
});
return item;
}
/**
* 是否PBO文件
*/
private boolean isPboFile(File file) {
if (file == null) return false;
return Pattern.compile(".*(.pbo)$").matcher(file.getName()).matches();
}
/**
* 展示遮罩弹窗
* <p>
* 当{@code persistent}为{@code true}时,需要调用{@link #hideModal}才能关闭
*
* @param node 展示内容
* @param persistent 是否持久性内容
*/
private void showModal(Node node, boolean persistent) {
modalPane.setAlignment(Pos.CENTER);
modalPane.usePredefinedTransitionFactories(null);
modalPane.show(node);
modalPane.setPersistent(persistent);
}
/**
* 关闭/隐藏遮罩弹窗
*/
public void hideModal() {
modalPane.hide(false);
modalPane.setPersistent(false);
}
/**
* 编辑
*/
private Node getEditWordPane(WordItem data) throws IOException {
FXMLLoader load = FxmlUtil.load("component/edit-view");
Pane pane = load.load();
WordEditController ctrl = load.getController();
ctrl.bindData(data);
return pane;
}
}

View File

@ -0,0 +1,70 @@
package cn.octopusyan.dmt.controller;
import cn.octopusyan.dmt.common.base.BaseController;
import cn.octopusyan.dmt.common.enums.ProxySetup;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.viewModel.ProxyViewModel;
import javafx.beans.binding.Bindings;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
/**
* 设置
*
* @author octopus_yan
*/
public class ProxyController extends BaseController<ProxyViewModel> {
public VBox root;
public RadioButton noneProxy;
public RadioButton systemProxy;
public RadioButton manualProxy;
public ToggleGroup proxyGroup = new ToggleGroup();
public GridPane manualProxyView;
public TextField proxyHost;
public TextField proxyPort;
@Override
public Pane getRootPanel() {
return root;
}
@Override
public void initData() {
noneProxy.setUserData(ProxySetup.NO_PROXY);
systemProxy.setUserData(ProxySetup.SYSTEM);
manualProxy.setUserData(ProxySetup.MANUAL);
noneProxy.setToggleGroup(proxyGroup);
systemProxy.setToggleGroup(proxyGroup);
manualProxy.setToggleGroup(proxyGroup);
manualProxyView.disableProperty().bind(
Bindings.createBooleanBinding(() -> !manualProxy.selectedProperty().get(), manualProxy.selectedProperty())
);
}
@Override
public void initViewAction() {
proxyGroup.selectedToggleProperty().addListener((_, _, value) -> {
viewModel.proxySetupProperty().set((ProxySetup) value.getUserData());
});
proxyGroup.selectToggle(switch (ConfigManager.proxySetup()) {
case ProxySetup.SYSTEM -> systemProxy;
case ProxySetup.MANUAL -> manualProxy;
default -> noneProxy;
});
proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty());
proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty());
}
public void proxyTest() {
viewModel.proxyTest();
}
}

View File

@ -0,0 +1,93 @@
package cn.octopusyan.dmt.controller;
import cn.octopusyan.dmt.common.base.BaseController;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.translate.TranslateApi;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import cn.octopusyan.dmt.viewModel.TranslateViewModel;
import javafx.collections.ObservableList;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.apache.commons.lang3.StringUtils;
/**
* 翻译
*
* @author octopus_yan
*/
public class TranslateController extends BaseController<TranslateViewModel> {
public VBox root;
public ComboBox<TranslateApi> translateSourceCombo;
public TextField qps;
public VBox appidBox;
public TextField appid;
public VBox apikeyBox;
public TextField apikey;
@Override
public Pane getRootPanel() {
return root;
}
@Override
public void initData() {
// 翻译源
for (TranslateApi value : TranslateApi.values()) {
ObservableList<TranslateApi> items = translateSourceCombo.getItems();
items.addAll(value);
}
translateSourceCombo.setConverter(new StringConverter<>() {
@Override
public String toString(TranslateApi object) {
if (object == null) return null;
return object.getLabel();
}
@Override
public TranslateApi fromString(String string) {
return TranslateApi.getByLabel(string);
}
});
// 当前翻译源
translateSourceCombo.getSelectionModel().select(ConfigManager.translateApi());
viewModel.getSource().bind(translateSourceCombo.getSelectionModel().selectedItemProperty());
qps.textProperty().bindBidirectional(viewModel.getQps());
appid.textProperty().bindBidirectional(viewModel.getAppId());
apikey.textProperty().bindBidirectional(viewModel.getApiKey());
appidBox.visibleProperty().bind(viewModel.getNeedApiKey());
apikeyBox.visibleProperty().bind(viewModel.getNeedApiKey());
}
@Override
public void initViewAction() {
}
public void save() {
TranslateApi source = translateSourceCombo.getValue();
String apikey = this.apikey.getText();
String appid = this.appid.getText();
int qps = Integer.parseInt(this.qps.getText());
ConfigManager.translateApi(source);
ConfigManager.translateQps(source, qps);
if (source.needApiKey()) {
if (StringUtils.isBlank(apikey) || StringUtils.isBlank(appid)) {
AlertUtil.error("认证信息不能为空");
}
ConfigManager.translateApikey(source, apikey);
ConfigManager.translateAppid(source, appid);
}
onDestroy();
}
}

View File

@ -0,0 +1,63 @@
package cn.octopusyan.dmt.controller.component;
import cn.octopusyan.dmt.common.base.BaseController;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.translate.factory.TranslateFactoryImpl;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import cn.octopusyan.dmt.viewModel.WordEditViewModel;
import javafx.application.Platform;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
/**
* 文本编辑
*
* @author octopus_yan
*/
public class WordEditController extends BaseController<WordEditViewModel> {
public VBox root;
public TextArea original;
public Button translate;
public TextArea chinese;
public ProgressIndicator progress;
@Override
public Pane getRootPanel() {
return root;
}
@Override
public void initData() {
}
@Override
public void initViewAction() {
}
public void bindData(WordItem data) {
viewModel.setData(data);
original.textProperty().bind(viewModel.getOriginalProperty());
chinese.textProperty().bindBidirectional(viewModel.getChineseProperty());
}
public void startTranslate() {
progress.setVisible(true);
ThreadPoolManager.getInstance().execute(() -> {
try {
String result = TranslateFactoryImpl.getInstance().translate(ConfigManager.translateApi(), original.getText());
Platform.runLater(() -> chinese.setText(result));
} catch (Exception e) {
Platform.runLater(() -> AlertUtil.exception(e).show());
}
Platform.runLater(() -> progress.setVisible(false));
});
}
}

View File

@ -0,0 +1,33 @@
package cn.octopusyan.dmt.model;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* GUI配置信息
*
* @author octopus_yan
*/
@Data
public class ConfigModel {
private static final Logger log = LoggerFactory.getLogger(ConfigModel.class);
/**
* 主题
*/
private String theme = ConfigManager.DEFAULT_THEME;
/**
* 代理设置
*/
private ProxyInfo proxy = new ProxyInfo();
/**
* 代理设置
*/
private Translate translate = new Translate();
}

View File

@ -0,0 +1,39 @@
package cn.octopusyan.dmt.model;
import cn.octopusyan.dmt.common.enums.ProxySetup;
import lombok.Data;
/**
* 代理信息
*
* @author octopus_yan
*/
@Data
public class ProxyInfo {
/**
* 主机地址
*/
private String host = "";
/**
* 端口
*/
private String port = "";
/**
* 登录名
*/
private String username = "";
/**
* 密码
*/
private String password = "";
/**
* 测试Url
*/
private String testUrl = "http://";
/**
* 代理类型
*
* @see ProxySetup
*/
private String setup = ProxySetup.NO_PROXY.getCode();
}

View File

@ -0,0 +1,52 @@
package cn.octopusyan.dmt.model;
import cn.octopusyan.dmt.translate.TranslateApi;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
/**
* 翻译配置
*
* @author octopus_yan
*/
@Data
public class Translate {
/**
* 当前使用接口
*/
private String use = TranslateApi.FREE_BAIDU.getName();
/**
* 接口配置
*/
private Map<String, Config> config = new HashMap<>() {
{
// 初始化
for (TranslateApi api : TranslateApi.values()) {
put(api.getName(), new Config("", "", api.getDefaultQps()));
}
}
};
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
/**
* api key
*/
private String appId;
/**
* api 密钥
*/
private String secretKey;
/**
* 请求速率
*/
private Integer qps;
}
}

View File

@ -0,0 +1,29 @@
package cn.octopusyan.dmt.model;
import cn.octopusyan.dmt.common.util.PropertiesUtils;
import lombok.Data;
/**
* 更新配置
*
* @author octopus_yan
*/
@Data
public class UpgradeConfig {
private final String owner = "octopusYan";
private final String repo = "dayz-mod-translator";
private String releaseFile = "DMT-windows-nojre.zip";
private String version = PropertiesUtils.getInstance().getProperty("app.version");
public String getReleaseApi() {
return STR."https://api.github.com/repos/\{getOwner()}/\{getRepo()}/releases/latest";
}
public String getDownloadUrl(String version) {
return STR."https://github.com/\{getOwner()}/\{getRepo()}/releases/download/\{version}/\{getReleaseFile()}";
}
}

View File

@ -0,0 +1,52 @@
package cn.octopusyan.dmt.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import lombok.Data;
import java.io.File;
/**
* 翻译文本
*
* @author octopus_yan
*/
@Data
public class WordItem {
/**
* 所在文件
*/
private File file;
/**
* 行数
*/
private Integer lines;
/**
* 开始下标
*/
private Integer index;
/**
* 原文
*/
private StringProperty originalProperty = new SimpleStringProperty();
/**
* 中文
*/
private StringProperty chineseProperty = new SimpleStringProperty();
public WordItem(File file, Integer lines, Integer index, String original, String chinese) {
this.file = file;
this.lines = lines;
this.index = index;
this.originalProperty.set(original);
this.chineseProperty.set(chinese);
}
public String getChinese() {
return chineseProperty.get();
}
public String getOriginal() {
return originalProperty.get();
}
}

View File

@ -0,0 +1,80 @@
package cn.octopusyan.dmt.task;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.task.base.BaseTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import cn.octopusyan.dmt.utils.PBOUtil;
import java.io.File;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
/**
* 打包任务
*
* @author octopus_yan
*/
public class PackTask extends BaseTask<PackTask.PackListener> {
private static final Function<List<WordItem>, List<WordItem>> sortFunc = items -> items.stream().sorted(Comparator.comparing(WordItem::getLines)).toList();
private static final Collector<WordItem, Object, List<WordItem>> downstream = Collectors.collectingAndThen(Collectors.toList(), sortFunc);
private final Map<File, List<WordItem>> wordFileMap;
private final String unpackPath;
public PackTask(List<WordItem> words, String unpackPath) {
super("Pack");
if (words == null)
throw new RuntimeException("参数为null!");
this.unpackPath = unpackPath;
wordFileMap = words.stream().collect(Collectors.groupingBy(WordItem::getFile, downstream));
}
@Override
protected void task() throws Exception {
if (wordFileMap.isEmpty()) return;
// 写入文件
PBOUtil.writeWords(wordFileMap);
if (listener != null) listener.onWriteOver();
// 打包
File packFile = PBOUtil.pack(unpackPath);
if (listener != null) listener.onPackOver(packFile);
}
/**
* 解包监听
*
* @author octopus_yan
*/
public abstract static class PackListener extends DefaultTaskListener {
public PackListener() {
super(true);
getProgress().setWidth(550);
}
@Override
protected void onSucceed() {
}
/**
* 写入完成
*/
public abstract void onWriteOver();
/**
* 打包完成
*
* @param file 文件地址
*/
public abstract void onPackOver(File file);
}
}

View File

@ -0,0 +1,28 @@
package cn.octopusyan.dmt.task;
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
import cn.octopusyan.dmt.task.base.BaseTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import lombok.extern.slf4j.Slf4j;
/**
* 代理检测任务
*
* @author octopus_yan
*/
@Slf4j
public class ProxyCheckTask extends BaseTask<DefaultTaskListener> {
private final String checkUrl;
public ProxyCheckTask(String checkUrl) {
super(STR."ProxyCheck[\{checkUrl}]");
this.checkUrl = checkUrl;
this.updateProgress(0d, 1d);
}
@Override
protected void task() throws Exception {
String response = HttpUtil.getInstance().get(checkUrl, null, null);
log.debug(STR."Proxy check response result => \n\{response}");
}
}

View File

@ -0,0 +1,91 @@
package cn.octopusyan.dmt.task;
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.task.base.BaseTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import cn.octopusyan.dmt.translate.DelayWord;
import cn.octopusyan.dmt.translate.TranslateUtil;
import cn.octopusyan.dmt.view.ConsoleLog;
import javafx.application.Platform;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.atomic.AtomicLong;
/**
* 翻译工具
*
* @author octopus_yan@foxmail.com
*/
public class TranslateTask extends BaseTask<DefaultTaskListener> {
public static final ConsoleLog consoleLog = ConsoleLog.getInstance(TranslateTask.class);
@Getter
private final ThreadPoolManager threadPoolManager = ThreadPoolManager.getInstance("translate-pool");
private final DelayQueue<DelayWord> delayQueue;
private final long total;
private final AtomicLong quantity = new AtomicLong();
private final CountDownLatch countDownLatch;
public TranslateTask(List<WordItem> data) {
this(TranslateUtil.getDelayQueue(data), data.size());
}
public TranslateTask(DelayQueue<DelayWord> queue, int total) {
super("Translate");
this.delayQueue = queue;
this.total = total;
this.quantity.set(total - delayQueue.size());
countDownLatch = new CountDownLatch(queue.size());
updateProgress(quantity.get(), total);
}
@Override
protected void task() throws Exception {
while (!delayQueue.isEmpty() && !isCancelled()) {
// 取出文本
DelayWord word = delayQueue.take();
// 多线程处理
threadPoolManager.execute(() -> {
// 翻译
try {
if (total == 1) {
consoleLog.info("正在翻译:{}", word.getWord().getOriginal());
}
String translate = TranslateUtil.translate(word.getApi(), word.getWord().getOriginal());
// 回调监听器
if (StringUtils.isEmpty(translate)) return;
synchronized (quantity) {
long progress = quantity.addAndGet(1);
// 设置翻译结果
Platform.runLater(() -> word.getWord().getChineseProperty().setValue(translate));
// 更新进度
updateProgress(progress, total);
// 输出信息
if (total != 1) {
consoleLog.info("正在翻译({}/{}", progress, total);
}
}
} catch (Exception e) {
if (!(e instanceof InterruptedException) || !isCancelled()) {
consoleLog.error("翻译失败", e);
}
delayQueue.add(word);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
}
}

View File

@ -0,0 +1,63 @@
package cn.octopusyan.dmt.task;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.task.base.BaseTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import cn.octopusyan.dmt.utils.PBOUtil;
import java.io.File;
import java.util.List;
/**
* 解包PBO文件任务
*
* @author octopus_yan
*/
public class UnpackTask extends BaseTask<UnpackTask.UnpackListener> {
private final File pboFile;
public UnpackTask(File pboFile) {
super("Unpack " + pboFile.getName());
this.pboFile = pboFile;
}
@Override
protected void task() throws Exception {
// 解包
String path = PBOUtil.unpack(pboFile);
if (listener != null)
listener.onUnpackOver(path);
List<WordItem> wordItems = PBOUtil.findWord(path);
if (listener != null)
listener.onFindWordOver(wordItems);
}
/**
* 解包监听
*
* @author octopus_yan
*/
public abstract static class UnpackListener extends DefaultTaskListener {
@Override
protected void onSucceed() {
}
/**
* 解包完成
*
* @param path 输出路径
*/
public abstract void onUnpackOver(String path);
/**
* 查找可翻译文本
*
* @param wordItems 可翻译文本列表
*/
public abstract void onFindWordOver(List<WordItem> wordItems);
}
}

View File

@ -0,0 +1,55 @@
package cn.octopusyan.dmt.task;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
import cn.octopusyan.dmt.common.util.JsonUtil;
import cn.octopusyan.dmt.model.UpgradeConfig;
import cn.octopusyan.dmt.task.base.BaseTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.StringUtils;
/**
* 检查更新任务
*
* @author octopus_yan
*/
public class UpgradeTask extends BaseTask<UpgradeTask.UpgradeListener> {
private final UpgradeConfig upgradeConfig = ConfigManager.upgradeConfig();
protected UpgradeTask() {
super("Check Update");
}
@Override
protected void task() throws Exception {
String responseStr = HttpUtil.getInstance().get(upgradeConfig.getReleaseApi(), null, null);
JsonNode response = JsonUtil.parseJsonObject(responseStr);
// TODO 校验返回内容
String newVersion = response.get("tag_name").asText();
if (listener != null)
listener.onChecked(!StringUtils.equals(upgradeConfig.getVersion(), newVersion), newVersion);
}
/**
* 检查更新监听默认实现
*
* @author octopus_yan
*/
public abstract static class UpgradeListener extends DefaultTaskListener {
public UpgradeListener() {
super(true);
}
public abstract void onChecked(boolean hasUpgrade, String version);
@Override
protected void onSucceed() {
// do nothing ...
}
}
}

View File

@ -0,0 +1,50 @@
package cn.octopusyan.dmt.task.base;
import cn.octopusyan.dmt.common.manager.thread.ThreadPoolManager;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import javafx.concurrent.Task;
import lombok.Getter;
/**
* @author octopus_yan
*/
public abstract class BaseTask<T extends Listener> extends Task<Void> {
private final ThreadPoolManager Executor = ThreadPoolManager.getInstance("task-pool");
protected T listener;
@Getter
private final String name;
protected BaseTask(String name) {
this.name = name;
}
public String getNameTag() {
return "Task " + name;
}
@Override
protected Void call() throws Exception {
if (listener != null) listener.onStart();
task();
return null;
}
protected abstract void task() throws Exception;
public void onListen(T listener) {
this.listener = listener;
if (this.listener == null)
return;
if (listener instanceof DefaultTaskListener lis)
lis.setTask((BaseTask) this);
setOnRunning(_ -> listener.onRunning());
setOnCancelled(_ -> listener.onCancelled());
setOnFailed(_ -> listener.onFailed(getException()));
setOnSucceeded(_ -> listener.onSucceeded());
}
public void execute() {
Executor.execute(this);
}
}

View File

@ -0,0 +1,23 @@
package cn.octopusyan.dmt.task.base;
/**
* 任务监听
*
* @author octopus_yan
*/
public interface Listener {
default void onStart() {
}
default void onRunning() {
}
default void onCancelled() {
}
default void onFailed(Throwable throwable) {
}
void onSucceeded();
}

View File

@ -0,0 +1,78 @@
package cn.octopusyan.dmt.task.listener;
import cn.octopusyan.dmt.task.base.BaseTask;
import cn.octopusyan.dmt.task.base.Listener;
import cn.octopusyan.dmt.view.ConsoleLog;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import cn.octopusyan.dmt.view.alert.builder.ProgressBuilder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* 任务监听器默认实现
*
* @author octopus_yan
*/
@Slf4j
public abstract class DefaultTaskListener implements Listener {
private ConsoleLog consoleLog;
@Getter
private BaseTask<? extends DefaultTaskListener> task;
/**
* 加载弹窗
*/
@Getter
final ProgressBuilder progress = AlertUtil.progress();
/**
* 是否展示加载弹窗
*/
private final boolean showProgress;
public DefaultTaskListener() {
this(false);
}
public DefaultTaskListener(boolean showProgress) {
this.showProgress = showProgress;
}
public <L extends DefaultTaskListener> void setTask(BaseTask<L> task) {
this.task = task;
consoleLog = ConsoleLog.getInstance(task.getClass().getSimpleName());
progress.onCancel(task::cancel);
}
@Override
public void onStart() {
consoleLog.info(STR."\{task.getNameTag()} start ...");
}
@Override
public void onRunning() {
// 展示加载弹窗
if (showProgress)
progress.show();
}
@Override
public void onCancelled() {
progress.close();
consoleLog.info(STR."\{task.getNameTag()} cancel ...");
}
@Override
public void onFailed(Throwable throwable) {
progress.close();
consoleLog.error(STR."\{task.getNameTag()} fail ...", throwable);
}
@Override
public void onSucceeded() {
progress.close();
consoleLog.info(STR."\{task.getNameTag()} success ...");
onSucceed();
}
protected abstract void onSucceed();
}

View File

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

View File

@ -0,0 +1,39 @@
package cn.octopusyan.dmt.translate;
import cn.octopusyan.dmt.model.WordItem;
import lombok.Getter;
import lombok.Setter;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* 延迟翻译对象
*
* @author octopus_yan
*/
@Getter
public class DelayWord implements Delayed {
@Setter
private TranslateApi api;
private final WordItem word;
private long delayTime;
public DelayWord(WordItem word) {
this.word = word;
}
public void setDelayTime(long time, TimeUnit timeUnit) {
this.delayTime = System.currentTimeMillis() + (time > 0 ? TimeUnit.MILLISECONDS.convert(time, timeUnit) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.delayTime, ((DelayWord) o).delayTime);
}
}

View File

@ -0,0 +1,60 @@
package cn.octopusyan.dmt.translate;
import cn.octopusyan.dmt.model.Translate;
import lombok.Getter;
/**
* 翻译引擎类型
*
* @author octopus_yan@foxmail.com
*/
@Getter
public enum TranslateApi {
FREE_BAIDU("free_baidu", "百度", false),
FREE_GOOGLE("free_google", "谷歌", false),
BAIDU("baidu", "百度(需认证)", true),
;
@Getter
private final String name;
@Getter
private final String label;
private final boolean needApiKey;
private final Integer defaultQps;
TranslateApi(String name, String label, boolean needApiKey) {
// 设置接口默认qps=10
this(name, label, needApiKey, 10);
}
TranslateApi(String name, String label, boolean needApiKey, int defaultQps) {
this.name = name;
this.label = label;
this.needApiKey = needApiKey;
this.defaultQps = defaultQps;
}
public boolean needApiKey() {
return needApiKey;
}
public Translate.Config translate() {
return new Translate.Config("", "", defaultQps);
}
public static TranslateApi get(String name) {
for (TranslateApi value : values()) {
if (value.getName().equals(name))
return value;
}
throw new RuntimeException("类型不存在");
}
public static TranslateApi getByLabel(String label) {
for (TranslateApi value : values()) {
if (value.getLabel().equals(label))
return value;
}
throw new RuntimeException("类型不存在");
}
}

View File

@ -0,0 +1,66 @@
package cn.octopusyan.dmt.translate;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.translate.factory.TranslateFactory;
import cn.octopusyan.dmt.translate.factory.TranslateFactoryImpl;
import java.util.List;
import java.util.concurrent.DelayQueue;
/**
* 翻译
*
* @author octopus_yan
*/
public class TranslateUtil {
private static final TranslateFactory factory = TranslateFactoryImpl.getInstance();
/**
* 翻译(英->中)
* <p> TODO 切换语种
*
* @param api 翻译源
* @param sourceString 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
public static String translate(TranslateApi api, String sourceString) throws Exception {
return factory.translate(api, sourceString);
}
public static String translate(String sourceString) throws Exception {
return factory.translate(ConfigManager.translateApi(), sourceString);
}
/**
* 获取延迟翻译对象
*
* @param words 待翻译文本列表
* @return 延迟对象
*/
public static DelayQueue<DelayWord> getDelayQueue(List<WordItem> words) {
return getDelayQueue(ConfigManager.translateApi(), words);
}
/**
* 获取延迟翻译对象
*
* @param source 翻译接口
* @param words 待翻译文本列表
* @return 延迟对象
*/
public static DelayQueue<DelayWord> getDelayQueue(TranslateApi source, List<WordItem> words) {
return factory.getDelayQueue(source, words);
}
/**
* 重设延迟时间
*
* @param index 序列号
* @param delayWord 延迟对象
*/
public static void resetDelayTime(int index, DelayWord delayWord) {
factory.resetDelayTime(ConfigManager.translateApi(), index, delayWord);
}
}

View File

@ -0,0 +1,44 @@
package cn.octopusyan.dmt.translate.factory;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.translate.DelayWord;
import cn.octopusyan.dmt.translate.TranslateApi;
import java.util.List;
import java.util.concurrent.DelayQueue;
/**
* 翻译器接口
*
* @author octopus_yan@foxmail.com
*/
public interface TranslateFactory {
/**
* 翻译处理
*
* @param api 翻译源
* @param sourceString 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
String translate(TranslateApi api, String sourceString) throws Exception;
/**
* 获取延迟翻译对象
*
* @param api 翻译源
* @param word 待翻译文本对象
* @return 延迟翻译对象
*/
DelayQueue<DelayWord> getDelayQueue(TranslateApi api, List<WordItem> word);
/**
* 重设延迟时间
*
* @param api 翻译接口
* @param index 序列号
* @param delayWord 延迟对象
*/
void resetDelayTime(TranslateApi api, int index, DelayWord delayWord);
}

View File

@ -0,0 +1,95 @@
package cn.octopusyan.dmt.translate.factory;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.translate.DelayWord;
import cn.octopusyan.dmt.translate.TranslateApi;
import cn.octopusyan.dmt.translate.processor.AbstractTranslateProcessor;
import cn.octopusyan.dmt.translate.processor.BaiduTranslateProcessor;
import cn.octopusyan.dmt.translate.processor.FreeBaiduTranslateProcessor;
import cn.octopusyan.dmt.translate.processor.FreeGoogleTranslateProcessor;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
/**
* 翻译处理器
*
* @author octopus_yan@foxmail.com
*/
@Slf4j
public class TranslateFactoryImpl implements TranslateFactory {
private static TranslateFactoryImpl impl;
private final Map<String, AbstractTranslateProcessor> processorMap = new HashMap<>();
private final List<AbstractTranslateProcessor> processorList = new ArrayList<>();
private TranslateFactoryImpl() {
}
public static synchronized TranslateFactoryImpl getInstance() {
if (impl == null) {
impl = new TranslateFactoryImpl();
impl.initProcessor();
}
return impl;
}
private void initProcessor() {
processorList.addAll(Arrays.asList(
new FreeGoogleTranslateProcessor(),
new FreeBaiduTranslateProcessor(),
new BaiduTranslateProcessor()
));
for (AbstractTranslateProcessor processor : processorList) {
processorMap.put(processor.getSource(), processor);
}
}
private AbstractTranslateProcessor getProcessor(TranslateApi api) {
return processorMap.get(api.getName());
}
@Override
public DelayQueue<DelayWord> getDelayQueue(TranslateApi api, List<WordItem> words) {
var queue = new DelayQueue<DelayWord>();
// 设置翻译延迟
AbstractTranslateProcessor processor = getProcessor(api);
for (int i = 0; i < words.size(); i++) {
// 翻译对象
DelayWord delayWord = new DelayWord(words.get(i));
// 设置翻译源
delayWord.setApi(api);
long time = 1000L / processor.qps();
delayWord.setDelayTime(time * (i + 1), TimeUnit.MILLISECONDS);
queue.add(delayWord);
}
return queue;
}
@Override
public void resetDelayTime(TranslateApi api, int index, DelayWord delayWord) {
AbstractTranslateProcessor processor = getProcessor(api);
// 设置翻译源
delayWord.setApi(api);
long time = 1000L / processor.qps();
delayWord.setDelayTime(time * (index + 1), TimeUnit.MILLISECONDS);
}
/**
* 翻译(英->中)
* <p> TODO 切换语种
*
* @param api 翻译源
* @param sourceString 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
@Override
public String translate(TranslateApi api, String sourceString) throws Exception {
return getProcessor(api).translate(sourceString);
}
}

View File

@ -0,0 +1,81 @@
package cn.octopusyan.dmt.translate.processor;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
import cn.octopusyan.dmt.translate.ApiKey;
import cn.octopusyan.dmt.translate.TranslateApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 翻译处理器抽象类
*
* @author octopus_yan@foxmail.com
*/
public abstract class AbstractTranslateProcessor implements TranslateProcessor {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected static final HttpUtil httpUtil = HttpUtil.getInstance();
protected final TranslateApi translateApi;
public AbstractTranslateProcessor(TranslateApi translateApi) {
this.translateApi = translateApi;
}
public String getSource() {
return translateApi.getName();
}
public TranslateApi source() {
return translateApi;
}
@Override
public boolean needApiKey() {
return source().needApiKey();
}
@Override
public boolean configuredKey() {
return ConfigManager.hasTranslateApiKey(source());
}
@Override
public int qps() {
return ConfigManager.translateQps(source());
}
/**
* 获取Api配置信息
*/
protected ApiKey getApiKey() {
if (!configuredKey()) {
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
logger.error(message);
throw new RuntimeException(message);
}
String appid = ConfigManager.translateAppid(source());
String apikey = ConfigManager.translateApikey(source());
return new ApiKey(appid, apikey);
}
@Override
public String translate(String original) throws Exception {
if (needApiKey() && !configuredKey()) {
String message = String.format("未配置【%s】翻译源认证信息!", source().getLabel());
logger.error(message);
throw new RuntimeException(message);
}
return customTranslate(original);
}
/**
* 翻译处理
*
* @param source 原始文本
* @return 翻译结果
*/
public abstract String customTranslate(String source) throws Exception;
}

View File

@ -1,13 +1,15 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
package cn.octopusyan.dmt.translate.processor;
import cn.octopusyan.dayzmodtranslator.manager.translate.ApiKey;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import cn.octopusyan.dmt.common.util.JsonUtil;
import cn.octopusyan.dmt.translate.ApiKey;
import cn.octopusyan.dmt.translate.TranslateApi;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
@ -19,8 +21,8 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
private ApiKey apiKey;
public BaiduTranslateProcessor(TranslateSource translateSource) {
super(translateSource);
public BaiduTranslateProcessor() {
super(TranslateApi.BAIDU);
}
@Override
@ -37,10 +39,10 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
apiKey = getApiKey();
String appid = apiKey.getAppid();
String appid = apiKey.appid();
String salt = UUID.randomUUID().toString().replace("-", "");
JSONObject param = new JSONObject();
Map<String, Object> param = new HashMap<>();
param.put("q", source);
param.put("from", "auto");
param.put("to", "zh");
@ -48,20 +50,20 @@ public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
param.put("salt", salt);
param.put("sign", getSign(appid, source, salt));
String resp = httpUtil.get(url(), null, param);
JSONObject jsonObject = JSON.parseObject(resp);
String resp = httpUtil.get(url(), null, JsonUtil.parseJsonObject(param));
JsonNode json = JsonUtil.parseJsonObject(resp);
if (!jsonObject.containsKey("trans_result")) {
Object errorMsg = jsonObject.get("error_msg");
if (!json.has("trans_result")) {
Object errorMsg = json.get("error_msg");
logger.error("翻译失败: {}", errorMsg);
throw new RuntimeException("翻译失败: " + errorMsg);
throw new RuntimeException(String.valueOf(errorMsg));
}
return jsonObject.getJSONArray("trans_result").getJSONObject(0).getString("dst");
return json.get("trans_result").get(0).get("dst").asText();
}
private String getSign(String appid, String q, String salt) {
return encrypt2ToMD5(appid + q + salt + apiKey.getApiKey());
return encrypt2ToMD5(appid + q + salt + apiKey.apiKey());
}
/**

View File

@ -0,0 +1,107 @@
package cn.octopusyan.dmt.translate.processor;
import cn.octopusyan.dmt.common.manager.http.CookieManager;
import cn.octopusyan.dmt.common.util.JsonUtil;
import cn.octopusyan.dmt.translate.TranslateApi;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 谷歌 免费翻译接口
*
* @author octopus_yan@foxmail.com
*/
public class FreeBaiduTranslateProcessor extends AbstractTranslateProcessor {
private static final Map<String, Object> header = new HashMap<>();
static {
header.put("Origin", "https://fanyi.baidu.com");
header.put("Referer", "https://fanyi.baidu.com");
header.put("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
header.put("Accept","*/*");
}
public FreeBaiduTranslateProcessor() {
super(TranslateApi.FREE_BAIDU);
}
@Override
public String url() {
return "https://fanyi.baidu.com/transapi";
}
@Override
public int qps() {
return source().getDefaultQps();
}
/**
* 翻译处理
*
* @param source 待翻译单词
* @return 翻译结果
*/
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
Map<String, Object> param = new HashMap<>();
param.put("query", source);
param.put("from", "auto");
param.put("to", "zh");
param.put("source", "txt");
String resp = httpUtil.post(url(), JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(param));
JsonNode json = JsonUtil.parseJsonObject(resp);
if (!json.has("data")) {
String errorMsg = json.get("errmsg").asText();
if("访问出现异常,请刷新后重试!".equals(errorMsg) && !header.containsKey("Cookie")) {
checkCookie();
return customTranslate(source);
}
throw new RuntimeException(errorMsg);
}
return json.get("data").get(0).get("dst").asText();
}
private void checkCookie() throws IOException, InterruptedException {
// 短时大量请求会被ban需要添加验证cookie
if (header.containsKey("Cookie")) return;
List<HttpCookie> cookieList = CookieManager.getStore().get(URI.create("https://baidu.com"));
boolean noneMatch = cookieList.stream()
.filter(cookie -> "ab_sr".equals(cookie.getName()) || "BAIDUID".equals(cookie.getName()))
.count() < 2;
if (noneMatch) {
String url = "https://miao.baidu.com/abdr?_o=https%3A%2F%2Ffanyi.baidu.com";
Map<String, Object> param = new HashMap<>();
param.put("data", miao_data);
param.put("key_id", "6e75c85adea0454a");
param.put("enc", 2);
httpUtil.post(url, JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(param));
}
cookieList = CookieManager.getStore().get(URI.create("https://baidu.com"));
List<HttpCookie> cookies = cookieList.stream()
.filter(cookie -> "ab_sr".equals(cookie.getName()) || "BAIDUID".equals(cookie.getName()))
.toList();
String collect = cookies.stream()
.map(cookie -> cookie.getName() + "=" + cookie.getValue())
.collect(Collectors.joining(";"));
header.put("Cookie", collect);
}
private static final String miao_data = "+wPJcl395nhVS+Fg/zTBIkBognAAK5IQi6wkGt3z1jk1jMuJOn+IkhaL1M7GHYxq/0V6+zKXaEVRs77WDfP/fDS0suNzgl3NdbhGSoHJEJk4fVMeU0pZxrgFq7Bzch6Aehw7MD6WlUDfyHWMarBN9OxH1UiW6Dn38uUkBjpsTDZYeNWOlmwy8YKq3FlRZiWzGYaP7K3DfBwWbT059D0KT2/mGUrHVgPVAkU572qNLzTxFyFneYTmPDQC0j+fl6+kxhA460WIG5aL19v4Gx+T7YJwIGe07QdhlQuy8Q6bpAIb8lj1SGjX9kS8P8GtI7TQv7gOlZ8ShUgV7941JXbIsJZgoWHFNq5Xge6XcVOUJsSQzAKsAgIlhgnlfC9IBAGK85KzeDC7JoBi4t0BXKgKUjYM9Veh7cA3yfudWKjSpyN6mv97apGR4Vl+8wbugPSCQRQlxPDunGUTGionf4hKxNZJYz62dwm0E/pP21PGPwnihDi7mQNxh40aE7IG0kORudLChvrcj85n4U/Rxkoq3TXTnSK1YAc4Pi/dIuR5HSUZyUBEqlvldU3jXqgUlfqeqjkn5Ug9gS6IqYEHE6FZdAGw/3y247pojWs2L56RSrUc6LoyQ7P9QNPOBr/v9HngHLRnFovrrZ57KmMl2hCiX5r1Q473CyjBhTjix3gQIFCwmM6rmEPIIU+cr0lvQrdhjxsiETk2sd17dR9gR2EjbNjeTby9QyHJTima5T6lk0K0orADrF2lNA/Xo2Iv/cMFXkc1ss7a19UQTdMLmiKgWdllWxH8Yp8B5o4KrLh+pMCGy6NeFn6FuWeAQgY8EB1r9H3PuenKaQ02VckkUPN1h1szjDI7otIFHYnQFJrp4vy4aM8wwcQS/+R2Aw0FwA5e2IWcZSwoTIc1i9YIeKgUpiConxhqZeKIhUEz33qExW0IS9mMs558JbAYsUG8KcMFtyqVo2sgtXteMAXMB9yX5/huZMR9HOO0v8x4KCZ+hwZczoVe3AmmUZLq1+rY9ngCp5D/3CcElcH3SU9HmlVXZ2VvLpa9wGIRP1UAHFuTxwVLvxJP07fbR31rLkoC22DbemRshBnv4EbIP/mkO8l/P1Y6RGEkS3b7b2MoBIWh4atrqOmUCG6w1KwESSi4Y5VKpdEkU/gL4ea7kr/GRJVWomSKSDqUEuloUI4hfLpmJs4h1JZTotOBcVjarOIW5d3j2Gm6R8X6vk24sUukOR1QOzP1gkoUaOYR+6Jcl+20IQZtIFMwFeMYtKrB5HquwYgHmUvO5hdxuFRwXosegVJmo+9MxkYjHTJbAgqZfKMc8kRjr64YBhwE30P+KLw+TafbgrA3trZQCyN3u5O5/2rk3GJmbhcDhZlYrYepj4Rymw8FB6LF0PkZZzRtWtQWv2gnMZuLmo7i5flL/MbiUMXKnsy3+Z6HHKNOqUzGUOZx84xizLkxwVMzGHrtSOub4wIhSWr7/z9GNa9qJuNJYsPXIE8wn245mHaheCm+MCxjIJMKdb72PBbQFF517kV+BxsNrxbFGe6ffJ/03KhXg0jJ1lAQc07obvOkdGVRr8DHTiS3IBfN7ofZMjVhXXpH2kUtepLfWvQf053yH2VQIEq62uxZP+RfpmdiBePEXQRBk5EA+178YbyavPE6yeu8zlub6GGlvuvrtnQmPUyvmO1hebiQK8B/V5TXxB+7IhPQ8okEll6bldVCoIxkbyAIBTK7rrDhD53HJ9capFgh/Q0XHMchYSYielT+Et8UIqUmVO94K5yl/nQhO1Vc2FsHkp2JWDDMQTuZyfmv+FS5OTeu39UznwXD36kyr2HvBEVoCMBqJrhI9WnEng53aEtcWQk4ewS+HllIoBgeJgexfs3MQK0mMP+6qV/idvB3S8Kzt/+SZsA0cHGvPvQzdePCRGNgCFOHPcpHPW/Yz4bwo4dzM64CYnL17Z4/fwcodMBG+KqG6ZyXe7EBby5RUE5OnhAGdR8T6WlIjNlU5TAIPbZ32i5nugWAcYJWJKsuITjAl4sRYY6Tpj19yIdHX2CDnU/O4arL8ijniydgcIEyYJbSCFmeJwyIv+ZSrB3RW0fYuAs4Q3AbeSK5TA9EICpB2w22kgditKO+uvrxw2LzGGND5C684YZq+XGmocAfpcte6+1PFs4NwuN41lOxDry8dwXr2mUIugcpnzsoF5Wgwyen0ISb2qaXODTvd/T5HEJcmpZdUJ1c+g+nEPi2OyX+MBXR";
}

View File

@ -1,10 +1,13 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
package cn.octopusyan.dmt.translate.processor;
import cn.octopusyan.dayzmodtranslator.manager.translate.TranslateSource;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import cn.octopusyan.dmt.common.util.JsonUtil;
import cn.octopusyan.dmt.translate.TranslateApi;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 谷歌 免费翻译接口
@ -13,8 +16,8 @@ import java.io.IOException;
*/
public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
public FreeGoogleTranslateProcessor(TranslateSource translateSource) {
super(translateSource);
public FreeGoogleTranslateProcessor() {
super(TranslateApi.FREE_GOOGLE);
}
@Override
@ -22,6 +25,11 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
return "https://translate.googleapis.com/translate_a/single";
}
@Override
public int qps() {
return source().getDefaultQps();
}
/**
* 翻译处理
*
@ -31,21 +39,22 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
@Override
public String customTranslate(String source) throws IOException, InterruptedException {
JSONObject form = new JSONObject();
Map<String, Object> form = new HashMap<>();
form.put("client", "gtx");
form.put("dt", "t");
form.put("sl", "auto");
form.put("tl", "zh-CN");
form.put("q", source);
JSONObject header = new JSONObject();
Map<String, Object> header = new HashMap<>();
StringBuilder retStr = new StringBuilder();
// TODO 短时大量请求会被ban需要浏览器验证添加cookie
String resp = httpUtil.get(url(), header, form);
JSONArray jsonObject = JSONArray.parseArray(resp);
for (Object o : jsonObject.getJSONArray(0)) {
JSONArray a = (JSONArray) o;
retStr.append(a.getString(0));
String resp = httpUtil.get(url(), JsonUtil.parseJsonObject(header), JsonUtil.parseJsonObject(form));
JsonNode json = JsonUtil.parseJsonObject(resp);
for (JsonNode o : json.get(0)) {
retStr.append(o.get(0).asText());
}
return retStr.toString();

View File

@ -1,4 +1,4 @@
package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
package cn.octopusyan.dmt.translate.processor;
/**
* 翻译处理器
@ -6,6 +6,7 @@ package cn.octopusyan.dayzmodtranslator.manager.translate.processor;
* @author octopus_yan@foxmail.com
*/
public interface TranslateProcessor {
/**
* 翻译源 api接口地址
*/
@ -29,9 +30,9 @@ public interface TranslateProcessor {
/**
* 翻译
*
* @param source 原始文本
* @param original 原始文本
* @return 翻译结果
* @throws Exception 翻译出错
*/
String translate(String source) throws Exception;
String translate(String original) throws Exception;
}

View File

@ -1,4 +1,4 @@
package cn.octopusyan.dayzmodtranslator.util;
package cn.octopusyan.dmt.utils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.CanReadFileFilter;
@ -6,11 +6,9 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@ -96,8 +94,24 @@ public class FileUtil {
return fileName.substring(0, fileName.lastIndexOf("."));
}
public static String getMimeType(Path path) {
try {
return Files.probeContentType(path);
} catch (IOException e) {
return null;
}
}
public static Collection<File> listFile(File file) {
return FileUtils.listFiles(file, CanReadFileFilter.CAN_READ, null);
}
public static List<String> listFileNames(String path) {
Collection<File> files = FileUtils.listFiles(new File(path), CanReadFileFilter.CAN_READ, null);
return listFileNames(new File(path));
}
public static List<String> listFileNames(File file) {
Collection<File> files = listFile(file);
return files.stream().map(File::getName).collect(Collectors.toList());
}
@ -132,4 +146,36 @@ public class FileUtil {
}
return path;
}
/**
* 判断文件的编码格式
*
* @return 文件编码格式
*/
public static String getCharsets(File file) {
try (InputStream in = new FileInputStream(file)) {
int p = (in.read() << 8) + in.read();
String code = "GBK";
switch (p) {
case 59524:
code = "UTF-8";
break;
case 0xfffe:
code = "Unicode";
break;
case 0xfeff:
code = "UTF-16BE";
break;
case 48581:
code = "GBK";
break;
default:
}
return code;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,407 @@
package cn.octopusyan.dmt.utils;
import cn.octopusyan.dmt.common.config.Constants;
import cn.octopusyan.dmt.common.util.ProcessesUtil;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.view.ConsoleLog;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* PBO 文件工具
*
* @author octopus_yan
*/
public class PBOUtil {
public static final ConsoleLog consoleLog = ConsoleLog.getInstance(PBOUtil.class);
private static final ProcessesUtil processesUtil = ProcessesUtil.init(Constants.BIN_DIR_PATH);
private static final String UNPACK_COMMAND = STR."\{Constants.PBOC_FILE} unpack -o \{Constants.TMP_DIR_PATH} %s";
private static final String PACK_COMMAND = STR."\{Constants.PBOC_FILE} pack -o %s %s";
private static final String CFG_COMMAND = STR."\{Constants.CFG_CONVERT_FILE} %s -dst %s %s";
private static final String FILE_NAME_STRING_TABLE = "stringtable.csv";
private static final String FILE_NAME_CONFIG_BIN = "config.bin";
private static final String FILE_NAME_CONFIG_CPP = "config.cpp";
private static final String[] FILE_NAME_LIST = new String[]{"csv", "bin", "cpp", "layout"};
private static final Pattern CPP_PATTERN = Pattern.compile(".*(displayName|descriptionShort) ?= ?\"(.*)\";.*");
private static final Pattern LAYOUT_PATTERN = Pattern.compile(".*text \"(.*)\".*");
public static void init() {
String srcFilePath = Objects.requireNonNull(PBOUtil.class.getResource("/bin")).getPath();
try {
File destDir = new File(Constants.BIN_DIR_PATH);
FileUtils.forceMkdir(destDir);
FileUtils.copyDirectory(new File(srcFilePath), destDir);
} catch (IOException e) {
consoleLog.error("Util 初始化失败", e);
}
}
/**
* 解包pbo文件
*
* @param path pbo文件地址
* @return 解包输出路径
*/
public static String unpack(String path) {
return unpack(new File(path));
}
/**
* 解包pbo文件
*
* @param pboFile pbo文件
* @return 解包输出路径
*/
public static String unpack(File pboFile) {
if (!pboFile.exists())
throw new RuntimeException("文件不存在!");
File directory = new File(Constants.TMP_DIR_PATH);
String outputPath = Constants.TMP_DIR_PATH + File.separator + FileUtil.mainName(pboFile);
try {
FileUtils.deleteQuietly(new File(outputPath));
FileUtils.forceMkdir(directory);
} catch (IOException e) {
throw new RuntimeException("文件夹创建失败", e);
}
String command = String.format(UNPACK_COMMAND, pboFile.getAbsolutePath());
consoleLog.debug(STR."unpack command ==> [\{command}]");
boolean exec = processesUtil.exec(command);
if (!exec)
throw new RuntimeException("解包失败!");
return outputPath;
}
/**
* 打包pbo文件
*
* @param unpackPath pbo解包文件路径
* @return 打包文件
*/
public static File pack(String unpackPath) {
String outputPath = STR."\{unpackPath}.pbo";
// 打包文件临时保存路径
File packFile = new File(outputPath);
if (packFile.exists()) {
// 如果存在则删除
FileUtils.deleteQuietly(packFile);
}
String command = String.format(PACK_COMMAND, Constants.TMP_DIR_PATH, unpackPath);
consoleLog.debug(STR."pack command ==> [\{command}]");
boolean exec = processesUtil.exec(command);
if (!exec) throw new RuntimeException("打包失败!");
return packFile;
}
/**
* 查找可翻译文本
*
* @param path 根目录
*/
public static List<WordItem> findWord(String path) {
return findWord(new File(path));
}
public static List<WordItem> findWord(File file) {
ArrayList<WordItem> wordItems = new ArrayList<>();
if (!file.exists())
return wordItems;
List<File> files = new ArrayList<>(FileUtils.listFiles(file, FILE_NAME_LIST, true));
for (File item : files) {
wordItems.addAll(findWordByFile(item));
}
return wordItems;
}
/**
* 写入文件
*
* @param wordFileMap 文件对应文本map
*/
public static void writeWords(Map<File, List<WordItem>> wordFileMap) {
for (Map.Entry<File, List<WordItem>> entry : wordFileMap.entrySet()) {
Map<Integer, WordItem> wordMap = entry.getValue().stream()
.collect(Collectors.toMap(WordItem::getLines, Function.identity()));
File file = entry.getKey();
// 需要转bin文件时写入bak目录下cpp文件
boolean hasBin = new File(outFilePath(file, ".bin")).exists();
String writePath = file.getAbsolutePath().replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
File writeFile = hasBin ? file : new File(writePath);
AtomicInteger lineIndex = new AtomicInteger(0);
List<String> lines = new ArrayList<>();
consoleLog.info("正在写入文件[{}]", writeFile.getAbsolutePath());
try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
while (it.hasNext()) {
String line = it.next();
WordItem word = wordMap.get(lineIndex.get());
// 当前行是否有需要替换的文本
// TODO 是否替换空文本
if (word != null && line.contains(word.getOriginal())) {
line = line.substring(0, word.getIndex()) +
line.substring(word.getIndex()).replace(word.getOriginal(), word.getChinese());
}
// 缓存行内容
lines.add(line);
lineIndex.addAndGet(1);
}
} catch (IOException e) {
consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
}
try {
// 写入文件
String charsets = writeFile.getName().endsWith(".layout") ? FileUtil.getCharsets(writeFile) : StandardCharsets.UTF_8.name();
FileUtils.writeLines(writeFile, charsets, lines);
} catch (IOException e) {
consoleLog.error(STR."文件(\{writeFile.getAbsoluteFile()})写入失败", e);
}
// CPP转BIN (覆盖TMP下BIN文件)
if (hasBin) cpp2bin(writeFile);
}
}
/**
* 查找文件内可翻译文本
*
* @param file 文件
* @return 可翻译文本信息列表
*/
private static List<WordItem> findWordByFile(File file) {
if (!FILE_NAME_CONFIG_CPP.equals(file.getName())
&& !FILE_NAME_CONFIG_BIN.equals(file.getName())
&& !FILE_NAME_STRING_TABLE.equals(file.getName())
&& !file.getName().endsWith(".layout")
) {
return Collections.emptyList();
}
// 创建备份(在bak文件夹下的同级目录
file = createBak(file);
// bin转cpp
if (FILE_NAME_CONFIG_BIN.equals(file.getName())) {
file = bin2cpp(file);
}
String charset = file.getName().endsWith(".layout") ? FileUtil.getCharsets(file) : StandardCharsets.UTF_8.name();
try (LineIterator it = FileUtils.lineIterator(file, charset)) {
// CPP
if (FILE_NAME_CONFIG_CPP.equals(file.getName())) {
return findWordByCPP(file, it);
}
// CSV
if (FILE_NAME_STRING_TABLE.equals(file.getName())) {
return findWordByCSV(file, it);
}
// layout
if (file.getName().endsWith(".layout")) {
return findWordByLayout(file, it);
}
// TODO 待添加更多文件格式
return Collections.emptyList();
} catch (IOException e) {
consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
}
return Collections.emptyList();
}
/**
* 从csv文件中读取可翻译文本
*
* @param file csv文件
* @param it 行内容遍历器
* @return 可翻译文本列表
*/
private static List<WordItem> findWordByCSV(File file, LineIterator it) {
ArrayList<WordItem> wordItems = new ArrayList<>();
AtomicInteger lines = new AtomicInteger(0);
int index = -1;
String line;
while (it.hasNext()) {
line = it.next();
List<String> split = Arrays.stream(line.split(",")).toList();
if (lines.get() == 0) {
index = split.indexOf("\"chinese\"");
} else if (index < split.size()) {
// 原文
String original = split.get(index).replaceAll("\"", "");
// 开始下标
Integer startIndex = line.indexOf(original);
// 添加单词
if (original.length() > 1) {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
}
lines.addAndGet(1);
}
return wordItems;
}
/**
* 从layout文件中读取可翻译文本
*
* @param file layout文件
* @param it 行内容遍历器
* @return 可翻译文本列表
*/
private static List<WordItem> findWordByLayout(File file, LineIterator it) {
ArrayList<WordItem> wordItems = new ArrayList<>();
AtomicInteger lines = new AtomicInteger(0);
String line;
Matcher matcher;
while (it.hasNext()) {
line = it.next();
matcher = LAYOUT_PATTERN.matcher(line);
if (StringUtils.isNoneEmpty(line) && matcher.matches()) {
// 原文
String original = matcher.group(1);
// 开始下标
Integer startIndex = line.indexOf(original);
// 添加单词
if (original.length() > 1) {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
}
lines.addAndGet(1);
}
return wordItems;
}
/**
* 读取cpp文件内可翻译文本
*
* @param file cpp文件
* @param it 行内容遍历器
* @return 可翻译文本列表
*/
private static List<WordItem> findWordByCPP(File file, LineIterator it) {
ArrayList<WordItem> wordItems = new ArrayList<>();
AtomicInteger lines = new AtomicInteger(0);
while (it.hasNext()) {
String line = it.next();
Matcher matcher = CPP_PATTERN.matcher(line);
if (!line.contains("$") && matcher.matches()) {
String name = matcher.group(1);
// 原始文本
int startIndex = line.indexOf(name) + name.length();
String original = matcher.group(2);
// 添加单词
if (original.length() > 1) {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
}
lines.addAndGet(1);
}
return wordItems;
}
/**
* 创建备份文件
*/
private static File createBak(File file) {
try {
String absolutePath = file.getAbsolutePath().replace(Constants.TMP_DIR_PATH, Constants.BAK_DIR_PATH);
File destFile = new File(absolutePath);
FileUtils.copyFile(file, destFile);
return destFile;
} catch (IOException e) {
consoleLog.error(STR."创建备份文件失败[\{file.getAbsolutePath()}]", e);
}
return file;
}
/**
* bin 转 cpp
*
* @param file bin文件
* @return cpp文件
*/
private static File bin2cpp(File file) {
boolean exec = processesUtil.exec(toTxtCommand(file));
if (!exec) throw new RuntimeException("bin2cpp 失败");
return new File(outFilePath(file, ".cpp"));
}
/**
* cpp 转 bin
*
* @param file bin文件
*/
private static void cpp2bin(File file) {
boolean exec = processesUtil.exec(toBinCommand(file));
if (!exec) throw new RuntimeException("cpp2bin 失败");
}
/**
* cpp to bin 命令
*/
private static String toBinCommand(File cppFile) {
String outFilePath = outFilePath(cppFile, ".bin");
outFilePath = outFilePath.replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
return String.format(CFG_COMMAND, "-bin", outFilePath, cppFile.getAbsolutePath());
}
/**
* bin to cpp 命令
*/
private static String toTxtCommand(File binFile) {
String outFilePath = outFilePath(binFile, ".cpp");
return String.format(CFG_COMMAND, "-txt", outFilePath, binFile.getAbsolutePath());
}
private static String outFilePath(File file, String suffix) {
return file.getParentFile().getAbsolutePath() + File.separator + FileUtil.mainName(file) + suffix;
}
}

View File

@ -0,0 +1,43 @@
/* SPDX-License-Identifier: MIT */
package cn.octopusyan.dmt.utils;
import cn.octopusyan.dmt.AppLauncher;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Objects;
import java.util.prefs.Preferences;
public final class Resources {
public static final String MODULE_DIR = "/";
public static InputStream getResourceAsStream(String resource) {
String path = resolve(resource);
return Objects.requireNonNull(
AppLauncher.class.getResourceAsStream(resolve(path)),
"Resource not found: " + path
);
}
public static URI getResource(String resource) {
String path = resolve(resource);
URL url = Objects.requireNonNull(AppLauncher.class.getResource(resolve(path)), "Resource not found: " + path);
return URI.create(url.toExternalForm());
}
public static String resolve(String resource) {
Objects.requireNonNull(resource);
return resource.startsWith("/") ? resource : MODULE_DIR + resource;
}
public static String getPropertyOrEnv(String propertyKey, String envKey) {
return System.getProperty(propertyKey, System.getenv(envKey));
}
public static Preferences getPreferences() {
return Preferences.userRoot().node("atlantafx");
}
}

View File

@ -0,0 +1,151 @@
package cn.octopusyan.dmt.view;
import javafx.application.Platform;
import javafx.scene.control.TextArea;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 模拟控制台输出
*
* @author octopus_yan
*/
public class ConsoleLog {
public static final String format = "yyyy/MM/dd hh:mm:ss";
private static final Logger log = LoggerFactory.getLogger(ConsoleLog.class);
private static Logger markerLog;
private static TextArea logArea;
private final String tag;
@Setter
private static boolean showDebug = false;
public static void init(TextArea logArea) {
ConsoleLog.logArea = logArea;
}
private ConsoleLog(String tag) {
this.tag = tag;
}
public static <T> ConsoleLog getInstance(Class<T> clazz) {
markerLog = LoggerFactory.getLogger(clazz);
return getInstance(clazz.getSimpleName());
}
public static ConsoleLog getInstance(String tag) {
return new ConsoleLog(tag);
}
public static boolean isInit() {
return log != null;
}
public void info(String message, Object... param) {
printLog(tag, Level.INFO, message, param);
}
public void warning(String message, Object... param) {
printLog(tag, Level.WARN, message, param);
}
public void debug(String message, Object... param) {
if (!showDebug) return;
printLog(tag, Level.DEBUG, message, param);
}
public void error(String message, Object... param) {
printLog(tag, Level.ERROR, message, param);
}
public void error(String message, Throwable throwable) {
markerLog.error(message, throwable);
message = STR."\{message} \{throwable.getMessage()}";
printLog(tag, Level.ERROR, message);
}
public void msg(String message, Object... params) {
if (StringUtils.isEmpty(message) || !isInit()) return;
message = format(message, params);
message = resetConsoleColor(message);
print(message);
}
final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public void printLog(String tag, Level level, String message, Object... params) {
if (!isInit()) return;
// 时间
String time = LocalDateTime.now().format(formatter);
// 级别
String levelStr = level.code;
// 消息
message = format(message, params);
// 拼接后输出
String input = STR."\{time} \{levelStr} [\{tag}] - \{message.replace(tag, "")}";
switch (level) {
case WARN -> markerLog.warn(message);
case DEBUG -> markerLog.debug(message);
// case ERROR -> markerLog.error(message);
default -> markerLog.info(message);
}
print(input);
}
private static void print(String message) {
var msg = message + (message.endsWith("\n") ? "" : "\n");
Platform.runLater(() -> {
ConsoleLog.logArea.appendText(msg);
// 滚动到底部
ConsoleLog.logArea.setScrollTop(Double.MAX_VALUE);
});
}
//==========================================={ 私有方法 }===================================================
private static String format(String msg, Object... params) {
int i = 0;
while (msg.contains("{}") && params != null) {
msg = msg.replaceFirst("\\{}", String.valueOf(params[i++]).replace("\\", "\\\\"));
}
return msg;
}
/**
* 处理控制台输出颜色
*
* @param msg 输出消息
* @return 信息
*/
private static String resetConsoleColor(String msg) {
if (!msg.contains("\033[")) return msg;
return msg.replaceAll("\\033\\[(\\d;)?(\\d+)m", "");
}
//============================{ 枚举 }================================
@Getter
@RequiredArgsConstructor
public enum Level {
INFO("INFO", null),
DEBUG("DEBUG", null),
WARN("WARN", "-color-danger-emphasis"),
ERROR("ERROR", "-color-danger-fg"),
;
private final String code;
private final String color;
}
}

View File

@ -0,0 +1,74 @@
package cn.octopusyan.dmt.view;
import atlantafx.base.theme.Styles;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.utils.Resources;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.function.Consumer;
/**
* 按钮
*
* @author octopus_yan
*/
public class EditButtonTableCell extends TableCell<WordItem, WordItem> {
public static Callback<TableColumn<WordItem, WordItem>, TableCell<WordItem, WordItem>> forTableColumn(Consumer<WordItem> edit, Consumer<WordItem> translate) {
return _ -> new EditButtonTableCell("", edit, translate);
}
private final Button edit;
private final Button translate;
private static final ImageView translateIcon = new ImageView(new Image(Resources.getResourceAsStream("images/icon/translate.png")));
static {
translateIcon.setFitHeight(20);
translateIcon.setFitWidth(20);
}
public EditButtonTableCell(String text, Consumer<WordItem> edit, Consumer<WordItem> translate) {
// 编辑
this.edit = new Button(text);
this.edit.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT);
this.edit.setOnMouseClicked(_ -> {
WordItem data = getTableView().getItems().get(getIndex());
edit.accept(data);
});
this.edit.setGraphic(new FontIcon(Feather.EDIT));
// 翻译
ImageView translateIcon = new ImageView(new Image(Resources.getResourceAsStream("images/icon/translate.png")));
translateIcon.setFitHeight(20);
translateIcon.setFitWidth(20);
this.translate = new Button("", translateIcon);
this.translate.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT);
this.translate.setOnMouseClicked(_ -> {
WordItem data = getTableView().getItems().get(getIndex());
translate.accept(data);
});
}
@Override
protected void updateItem(WordItem item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
/*
* TODO 添加多个操作按钮
* setGraphic(Hbox(btn1,btn2));
*/
setGraphic(new HBox(edit, translate));
}
}
}

View File

@ -0,0 +1,137 @@
package cn.octopusyan.dmt.view;
import atlantafx.base.controls.CaptionMenuItem;
import cn.octopusyan.dmt.common.config.Constants;
import cn.octopusyan.dmt.common.util.ViewUtil;
import javafx.application.Platform;
import javafx.beans.binding.StringBinding;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* 托盘图标 菜单
*
* @author octopus_yan
*/
public class PopupMenu {
// 用来隐藏弹出窗口的任务栏图标
private static final Stage utilityStage = new Stage();
// 菜单栏
private final ContextMenu root = new ContextMenu();
static {
utilityStage.initStyle(StageStyle.UTILITY);
utilityStage.setScene(new Scene(new Region()));
utilityStage.setOpacity(0);
}
public PopupMenu() {
root.focusedProperty().addListener((_, _, focused) -> {
if (!focused)
Platform.runLater(() -> {
root.hide();
utilityStage.hide();
});
});
}
public PopupMenu addItem(String label, EventHandler<ActionEvent> handler) {
return addItem(new MenuItem(label), handler);
}
public PopupMenu addItem(StringBinding bind, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem();
menuItem.textProperty().bind(bind);
return addItem(menuItem, handler);
}
public PopupMenu addItem(MenuItem node, EventHandler<ActionEvent> handler) {
node.setOnAction(handler);
return addItem(node);
}
public PopupMenu addSeparator() {
return addItem(new SeparatorMenuItem());
}
public PopupMenu addCaptionItem() {
return addCaptionItem(null);
}
public PopupMenu addCaptionItem(String title) {
return addItem(new CaptionMenuItem(title));
}
public PopupMenu addMenu(String label, MenuItem... items) {
return addMenu(new Menu(label), items);
}
public PopupMenu addMenu(StringBinding label, MenuItem... items) {
Menu menu = new Menu();
menu.textProperty().bind(label);
return addMenu(menu, items);
}
public PopupMenu addMenu(Menu menu, MenuItem... items) {
menu.getItems().addAll(items);
return addItem(menu);
}
public PopupMenu addTitleItem() {
return addTitleItem(Constants.APP_TITLE);
}
public PopupMenu addTitleItem(String label) {
return addExitItem(label);
}
public PopupMenu addExitItem() {
return addExitItem("Exit");
}
public PopupMenu addExitItem(String label) {
return addItem(label, _ -> Platform.exit());
}
private PopupMenu addItem(MenuItem node) {
root.getItems().add(node);
return this;
}
public void show(java.awt.event.MouseEvent event) {
// 必须调用show才会隐藏任务栏图标
utilityStage.show();
if (root.isShowing())
root.hide();
root.show(utilityStage,
event.getX() / ViewUtil.scaleX,
event.getY() / ViewUtil.scaleY
);
// 获取焦点 (失去焦点隐藏自身)
root.requestFocus();
}
public static MenuItem menuItem(String label, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem(label);
menuItem.setOnAction(handler);
return menuItem;
}
public static MenuItem menuItem(StringBinding stringBinding, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem();
menuItem.textProperty().bind(stringBinding);
menuItem.setOnAction(handler);
return menuItem;
}
}

View File

@ -0,0 +1,100 @@
package cn.octopusyan.dmt.view.alert;
import cn.octopusyan.dmt.view.alert.builder.*;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;
import javafx.stage.Window;
/**
* 弹窗工具
*
* @author octopus_yan@foxmail.com
*/
public class AlertUtil {
private static Window mOwner;
public static void initOwner(Stage stage) {
AlertUtil.mOwner = stage;
}
public static DefaultBuilder builder() {
return new DefaultBuilder(mOwner, true);
}
public static DefaultBuilder builder(boolean transparent) {
return new DefaultBuilder(mOwner, transparent);
}
public static AlertBuilder info(String content) {
return info().content(content).header(null);
}
public static AlertBuilder info() {
return alert(Alert.AlertType.INFORMATION);
}
public static AlertBuilder error(String message) {
return alert(Alert.AlertType.ERROR).header(null).content(message);
}
public static AlertBuilder warning() {
return alert(Alert.AlertType.WARNING);
}
public static AlertBuilder exception(Exception ex) {
return alert(Alert.AlertType.ERROR).exception(ex);
}
/**
* 确认对话框
*/
public static AlertBuilder confirm() {
return alert(Alert.AlertType.CONFIRMATION);
}
/**
* 自定义确认对话框 <p>
*
* @param buttons <code>"Cancel"</code> OR <code>"取消"</code> 为取消按钮
*/
public static AlertBuilder confirm(String... buttons) {
return confirm().buttons(buttons);
}
public static AlertBuilder confirm(ButtonType... buttons) {
return confirm().buttons(buttons);
}
public static AlertBuilder alert(Alert.AlertType type) {
return new AlertBuilder(mOwner, type);
}
public static TextInputBuilder input(String content) {
return new TextInputBuilder(mOwner);
}
public static TextInputBuilder input(String content, String defaultResult) {
return new TextInputBuilder(mOwner, defaultResult).content(content);
}
@SafeVarargs
public static <T> ChoiceBuilder<T> choices(String hintText, T... choices) {
return new ChoiceBuilder<>(mOwner, choices).content(hintText);
}
public static ProgressBuilder progress() {
return new ProgressBuilder(mOwner);
}
public interface OnChoseListener {
void confirm();
default void cancelOrClose(ButtonType buttonType) {
}
}
public interface OnClickListener {
void onClicked(String result);
}
}

View File

@ -0,0 +1,110 @@
package cn.octopusyan.dmt.view.alert.builder;
import cn.octopusyan.dmt.common.base.BaseBuilder;
import cn.octopusyan.dmt.common.config.LabelConstants;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Window;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author octopus_yan
*/
public class AlertBuilder extends BaseBuilder<AlertBuilder, Alert> {
public AlertBuilder(Window owner, Alert.AlertType alertType) {
super(new Alert(alertType), owner);
}
public AlertBuilder buttons(String... buttons) {
dialog.getButtonTypes().addAll(getButtonList(buttons));
return this;
}
public AlertBuilder buttons(ButtonType... buttons) {
dialog.getButtonTypes().addAll(buttons);
return this;
}
public AlertBuilder exception(Exception ex) {
dialog.setTitle("Exception Dialog");
dialog.setHeaderText(ex.getClass().getSimpleName());
dialog.setContentText(ex.getMessage());
// 创建可扩展的异常。
var sw = new StringWriter();
var pw = new PrintWriter(sw);
ex.printStackTrace(pw);
var exceptionText = sw.toString();
var label = new Label("The exception stacktrace was :");
var textArea = new TextArea(exceptionText);
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
var expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
// 将可扩展异常设置到对话框窗格中。
dialog.getDialogPane().setExpandableContent(expContent);
return this;
}
/**
* 获取按钮列表
*
* @param buttons "Cancel" / "取消" 为取消按钮
*/
private List<ButtonType> getButtonList(String[] buttons) {
if (ArrayUtils.isEmpty(buttons)) return Collections.emptyList();
return Arrays.stream(buttons).map((type) -> {
ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER;
if ("cancel".equals(StringUtils.lowerCase(type)) || LabelConstants.CANCEL.equals(type)) {
return ButtonType.CANCEL;
}
return new ButtonType(type, buttonData);
}).collect(Collectors.toList());
}
/**
* AlertUtil.confirm
*/
public void show(AlertUtil.OnClickListener listener) {
Optional<ButtonType> result = dialog.showAndWait();
result.ifPresent(r -> listener.onClicked(r.getText()));
}
/**
* AlertUtil.confirm
*/
public void show(AlertUtil.OnChoseListener listener) {
Optional<ButtonType> result = dialog.showAndWait();
result.ifPresent(r -> {
if (r == ButtonType.OK) {
listener.confirm();
} else {
listener.cancelOrClose(r);
}
});
}
}

View File

@ -0,0 +1,30 @@
package cn.octopusyan.dmt.view.alert.builder;
import cn.octopusyan.dmt.common.base.BaseBuilder;
import javafx.scene.control.ChoiceDialog;
import javafx.stage.Window;
import java.util.Optional;
/**
* @author octopus_yan
*/
public class ChoiceBuilder<R> extends BaseBuilder<ChoiceBuilder<R>, ChoiceDialog<R>> {
@SafeVarargs
public ChoiceBuilder(Window mOwner, R... choices) {
this(new ChoiceDialog<>(choices[0], choices), mOwner);
}
public ChoiceBuilder(ChoiceDialog<R> dialog, Window mOwner) {
super(dialog, mOwner);
}
/**
* AlertUtil.choices
*/
public R showAndGetChoice() {
Optional<R> result = dialog.showAndWait();
return result.orElse(null);
}
}

View File

@ -0,0 +1,52 @@
package cn.octopusyan.dmt.view.alert.builder;
import cn.octopusyan.dmt.common.base.BaseBuilder;
import cn.octopusyan.dmt.common.util.ViewUtil;
import javafx.scene.Node;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.paint.Color;
import javafx.stage.StageStyle;
import javafx.stage.Window;
/**
* 默认弹窗
*
* @author octopus_yan
*/
public class DefaultBuilder extends BaseBuilder<DefaultBuilder, Dialog<?>> {
public DefaultBuilder(Window mOwner) {
this(mOwner, true);
}
public DefaultBuilder(Window mOwner, boolean transparent) {
super(new Dialog<>(), mOwner);
header(null);
DialogPane dialogPane = dialog.getDialogPane();
if (transparent) {
dialogPane.getScene().setFill(Color.TRANSPARENT);
ViewUtil.bindDragged(dialogPane);
ViewUtil.bindShadow(dialogPane);
ViewUtil.getStage(dialogPane).initStyle(StageStyle.TRANSPARENT);
}
dialogPane.getButtonTypes().add(new ButtonType("取消", ButtonType.CANCEL.getButtonData()));
for (Node child : dialogPane.getChildren()) {
if (child instanceof ButtonBar) {
dialogPane.getChildren().remove(child);
break;
}
}
}
public DefaultBuilder content(Node content) {
dialog.getDialogPane().setContent(content);
return this;
}
}

View File

@ -0,0 +1,59 @@
package cn.octopusyan.dmt.view.alert.builder;
import cn.octopusyan.dmt.common.config.LabelConstants;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Window;
/**
* 加载弹窗
*
* @author octopus_yan
*/
public class ProgressBuilder extends DefaultBuilder {
private HBox hBox;
public ProgressBuilder(Window mOwner) {
super(mOwner);
content(getContent());
}
public void setWidth(double width) {
hBox.setPrefWidth(width);
}
private Pane getContent() {
hBox = new HBox();
hBox.setPrefWidth(350);
hBox.setAlignment(Pos.CENTER);
hBox.setSpacing(10);
hBox.setPadding(new Insets(10, 0, 10, 0));
// 取消按钮
Button cancel = new Button(LabelConstants.CANCEL);
cancel.setCancelButton(true);
cancel.setOnAction(_ -> dialog.close());
// 进度条
ProgressBar progressBar = new ProgressBar(-1);
progressBar.prefWidthProperty().bind(Bindings.createDoubleBinding(
() -> hBox.widthProperty().get() - cancel.widthProperty().get() - 40,
hBox.widthProperty(), cancel.widthProperty()
));
hBox.getChildren().add(progressBar);
hBox.getChildren().add(cancel);
return hBox;
}
public ProgressBuilder onCancel(Runnable run) {
dialog.setOnCloseRequest(_ -> run.run());
return this;
}
}

View File

@ -0,0 +1,36 @@
package cn.octopusyan.dmt.view.alert.builder;
import cn.octopusyan.dmt.common.base.BaseBuilder;
import javafx.scene.control.TextInputDialog;
import javafx.stage.Window;
import java.util.Optional;
/**
* 获取用户输入弹窗
*
* @author octopus_yan
*/
public class TextInputBuilder extends BaseBuilder<TextInputBuilder, TextInputDialog> {
public TextInputBuilder(Window mOwner) {
this(new TextInputDialog(), mOwner);
}
public TextInputBuilder(Window mOwner, String defaultResult) {
this(new TextInputDialog(defaultResult), mOwner);
}
public TextInputBuilder(TextInputDialog dialog, Window mOwner) {
super(dialog, mOwner);
}
/**
* AlertUtil.input
* 如果用户点击了取消按钮,将会返回null
*/
public String getInput() {
Optional<String> result = dialog.showAndWait();
return result.orElse(null);
}
}

View File

@ -0,0 +1,88 @@
/* SPDX-License-Identifier: MIT */
package cn.octopusyan.dmt.view.filemanager;
import atlantafx.base.theme.Tweaks;
import cn.octopusyan.dmt.utils.FileUtil;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.ImageView;
import java.io.File;
import java.nio.file.Files;
import java.util.Comparator;
public final class DirectoryTree extends TreeView<File> {
public static final FileIconRepository fileIcon = new FileIconRepository();
// 文件夹在前
static final Comparator<TreeItem<File>> FILE_TYPE_COMPARATOR = Comparator.comparing(
item -> !Files.isDirectory(item.getValue().toPath())
);
public DirectoryTree() {
super();
getStyleClass().add(Tweaks.ALT_ICON);
setCellFactory(_ -> new TreeCell<>() {
@Override
protected void updateItem(File item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.getName());
var image = new ImageView(item.isDirectory() ?
FileIconRepository.FOLDER :
fileIcon.getByMimeType(FileUtil.getMimeType(item.toPath()))
);
image.setFitWidth(20);
image.setFitHeight(20);
setGraphic(image);
}
}
});
}
public void loadRoot(File value) {
var root = new TreeItem<>(value);
root.setExpanded(true);
setRoot(root);
// scan file tree two levels deep for starters
scan(root, 5);
// scan deeper as the user navigates down the tree
root.addEventHandler(TreeItem.branchExpandedEvent(), event -> {
TreeItem parent = event.getTreeItem();
parent.getChildren().forEach(child -> {
var item = (TreeItem<File>) child;
if (item.getChildren().isEmpty()) {
scan(item, 1);
}
});
});
}
public static void scan(TreeItem<File> parent, int depth) {
File[] files = parent.getValue().listFiles();
depth--;
if (files != null) {
for (File f : files) {
var item = new TreeItem<>(f);
parent.getChildren().add(item);
if (depth > 0) {
scan(item, depth);
}
}
// 文件类型+名称排序
parent.getChildren().sort(FILE_TYPE_COMPARATOR.thenComparing(TreeItem::getValue));
}
}
}

View File

@ -0,0 +1,53 @@
/* SPDX-License-Identifier: MIT */
package cn.octopusyan.dmt.view.filemanager;
import cn.octopusyan.dmt.utils.Resources;
import javafx.scene.image.Image;
import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public final class FileIconRepository {
public static final String IMAGE_DIRECTORY = "images/papirus/";
public static final Image UNKNOWN_FILE = new Image(
Resources.getResourceAsStream(IMAGE_DIRECTORY + "mimetypes/text-plain.png")
);
public static final Image FOLDER = new Image(
Resources.getResourceAsStream(IMAGE_DIRECTORY + "places/folder-paleorange.png")
);
private final Map<String, Image> cache = new HashMap<>();
private final Set<String> unknownMimeTypes = new HashSet<>();
public Image getByMimeType(String mimeType) {
if (mimeType == null || unknownMimeTypes.contains(mimeType)) {
return UNKNOWN_FILE;
}
var cachedImage = cache.get(mimeType);
if (cachedImage != null) {
return cachedImage;
}
var fileName = mimeType.replaceAll("/", "-") + ".png";
try {
var image = new Image(Resources.getResourceAsStream(IMAGE_DIRECTORY + "mimetypes/" + fileName));
cache.put(mimeType, image);
return image;
} catch (Exception e) {
unknownMimeTypes.add(mimeType);
return UNKNOWN_FILE;
}
}
public static String getFileName(File file) {
return FileSystemView.getFileSystemView().getSystemDisplayName(file);
}
}

View File

@ -0,0 +1,202 @@
package cn.octopusyan.dmt.viewModel;
import cn.octopusyan.dmt.common.base.BaseViewModel;
import cn.octopusyan.dmt.controller.MainController;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.task.PackTask;
import cn.octopusyan.dmt.task.TranslateTask;
import cn.octopusyan.dmt.task.UnpackTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import cn.octopusyan.dmt.translate.DelayWord;
import cn.octopusyan.dmt.translate.TranslateUtil;
import cn.octopusyan.dmt.view.ConsoleLog;
import javafx.application.Platform;
import javafx.concurrent.Worker;
import javafx.scene.control.ProgressIndicator;
import org.apache.commons.lang3.StringUtils;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* 主界面
*
* @author octopus_yan
*/
public class MainViewModel extends BaseViewModel<MainViewModel, MainController> {
private static final ConsoleLog consoleLog = ConsoleLog.getInstance(MainViewModel.class);
/**
* 解包任务
*/
private UnpackTask unpackTask;
/**
* 翻译任务
*/
private TranslateTask translateTask;
private DelayQueue<DelayWord> delayQueue;
private String unpackPath;
private int total;
FontIcon startIcon = new FontIcon(Feather.PLAY);
FontIcon pauseIcon = new FontIcon(Feather.PAUSE);
private List<WordItem> wordItems;
/**
* 加载PBO文件
*/
public void selectFile(File pboFile) {
if (pboFile == null) return;
controller.setFileName(pboFile.getAbsolutePath());
unpackTask = new UnpackTask(pboFile);
}
/**
* 解包
*/
public void unpack() {
if (unpackTask == null) return;
unpackTask.onListen(new UnpackTask.UnpackListener() {
@Override
public void onRunning() {
// 展示加载
controller.onLoad();
// 重置进度
resetProgress();
}
@Override
public void onUnpackOver(String path) {
MainViewModel.this.unpackPath = path;
Platform.runLater(() -> controller.onUnpack(new File(path)));
}
@Override
public void onFindWordOver(List<WordItem> wordItems) {
total = wordItems.size();
MainViewModel.this.wordItems = wordItems;
Platform.runLater(() -> controller.onLoadWord(wordItems));
}
});
unpackTask.execute();
}
/**
* 开始翻译
*/
public void startTranslate() {
if(wordItems.isEmpty()) return;
if (translateTask == null) {
List<WordItem> words = wordItems.stream().filter(item -> StringUtils.isEmpty(item.getChinese())).toList();
delayQueue = TranslateUtil.getDelayQueue(words);
translateTask = createTask();
}
if (!translateTask.isRunning()) {
// 检查进度
if (!delayQueue.isEmpty()) {
AtomicInteger index = new AtomicInteger(0);
delayQueue.forEach(item -> TranslateUtil.resetDelayTime(index.getAndIncrement(), item));
translateTask = createTask();
}
if (translateTask.getState() != Worker.State.SUCCEEDED) {
translateTask.execute();
// 展示进度
controller.translateProgress.setVisible(true);
controller.translateProgress.progressProperty().bind(translateTask.progressProperty());
}
} else {
translateTask.cancel();
}
}
/**
* 打包
*/
public void pack() {
if(wordItems.isEmpty()) return;
PackTask packTask = new PackTask(wordItems, unpackPath);
packTask.onListen(new PackTask.PackListener() {
@Override
public void onWriteOver() {
consoleLog.info("写入完成");
}
@Override
public void onPackOver(File file) {
Platform.runLater(() -> controller.onPackOver(file));
}
});
packTask.execute();
}
private TranslateTask createTask() {
TranslateTask task = new TranslateTask(delayQueue, total);
task.onListen(new DefaultTaskListener() {
@Override
public void onRunning() {
ProgressIndicator graphic = new ProgressIndicator();
graphic.setPrefWidth(15);
graphic.setPrefHeight(15);
graphic.setOnMouseClicked(_ -> controller.startTranslate());
controller.translate.setGraphic(graphic);
controller.translateProgress.setVisible(true);
}
@Override
public void onCancelled() {
task.getThreadPoolManager().shutdownNow();
TranslateTask.consoleLog.info("翻译暂停");
Platform.runLater(() -> controller.translate.setGraphic(pauseIcon));
}
@Override
protected void onSucceed() {
if (delayQueue.isEmpty()) {
Platform.runLater(() -> controller.translate.setGraphic(startIcon));
} else {
Platform.runLater(() -> controller.translate.setGraphic(pauseIcon));
}
}
});
return task;
}
/**
* 加载PBO文件后重置进度
*/
private void resetProgress() {
translateTask = null;
controller.translate.setGraphic(startIcon);
controller.translateProgress.progressProperty().unbind();
controller.translateProgress.setProgress(0);
controller.translateProgress.setVisible(false);
}
/**
* 给定字符串是否含有中文
*
* @param str 需要判断的字符串
* @return 是否含有中文
*/
private boolean containsChinese(String str) {
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
}
}

View File

@ -0,0 +1,107 @@
package cn.octopusyan.dmt.viewModel;
import cn.octopusyan.dmt.common.base.BaseViewModel;
import cn.octopusyan.dmt.common.enums.ProxySetup;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.common.manager.http.HttpUtil;
import cn.octopusyan.dmt.controller.ProxyController;
import cn.octopusyan.dmt.task.ProxyCheckTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import cn.octopusyan.dmt.view.alert.builder.ProgressBuilder;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.lang3.StringUtils;
/**
* 设置
*
* @author octopus_yan
*/
public class ProxyViewModel extends BaseViewModel<ProxyViewModel, ProxyController> {
private final StringProperty proxyHost = new SimpleStringProperty(ConfigManager.proxyHost());
private final StringProperty proxyPort = new SimpleStringProperty(ConfigManager.proxyPort());
private final ObjectProperty<ProxySetup> proxySetup = new SimpleObjectProperty<>(ConfigManager.proxySetup());
private final StringProperty proxyTestUrl = new SimpleStringProperty(ConfigManager.proxyTestUrl());
public ProxyViewModel() {
proxySetup.addListener((_, _, newValue) -> ConfigManager.proxySetup(newValue));
proxyTestUrl.addListener((_, _, newValue) -> ConfigManager.proxyTestUrl(newValue));
proxyHost.addListener((_, _, newValue) -> {
ConfigManager.proxyHost(newValue);
setProxy();
});
proxyPort.addListener((_, _, newValue) -> {
ConfigManager.proxyPort(newValue);
setProxy();
});
}
public ObjectProperty<ProxySetup> proxySetupProperty() {
return proxySetup;
}
public StringProperty proxyHostProperty() {
return proxyHost;
}
public StringProperty proxyPortProperty() {
return proxyPort;
}
public void proxyTest() {
var checkUrl = AlertUtil.input("URL :", proxyTestUrl.getValue())
.title("检查代理设置")
.header("请输入您要检查的任何URL")
.getInput();
if (StringUtils.isEmpty(checkUrl)) return;
proxyTestUrl.setValue(checkUrl);
ProgressBuilder progress = AlertUtil.progress();
progress.show();
ConfigManager.checkProxy((success, msg) -> {
Platform.runLater(progress::close);
if (!success) {
final var tmp = "连接问题: ";
AlertUtil.error(STR."\{tmp}\{msg}").show();
return;
}
HttpUtil.getInstance().proxy(ConfigManager.proxySetup(), ConfigManager.getProxyInfo());
getProxyCheckTask(checkUrl).execute();
});
}
private void setProxy() {
ConfigManager.checkProxy((success, msg) -> {
if (!success) {
return;
}
HttpUtil.getInstance().proxy(ConfigManager.proxySetup(), ConfigManager.getProxyInfo());
});
}
private static ProxyCheckTask getProxyCheckTask(String checkUrl) {
var task = new ProxyCheckTask(checkUrl);
task.onListen(new DefaultTaskListener(true) {
@Override
public void onSucceed() {
AlertUtil.info("连接成功").show();
}
@Override
public void onFailed(Throwable throwable) {
super.onFailed(throwable);
AlertUtil.exception(new Exception(throwable)).show();
}
});
return task;
}
}

View File

@ -0,0 +1,32 @@
package cn.octopusyan.dmt.viewModel;
import cn.octopusyan.dmt.common.base.BaseViewModel;
import cn.octopusyan.dmt.common.manager.ConfigManager;
import cn.octopusyan.dmt.controller.TranslateController;
import cn.octopusyan.dmt.translate.TranslateApi;
import javafx.beans.property.*;
import lombok.Getter;
/**
* 翻译VM
*
* @author octopus_yan
*/
@Getter
public class TranslateViewModel extends BaseViewModel<TranslateViewModel, TranslateController> {
private final ObjectProperty<TranslateApi> source = new SimpleObjectProperty<>(ConfigManager.translateApi()) {
{
addListener((_, _, newValue) -> {
appId.setValue(ConfigManager.translateAppid(newValue));
apiKey.setValue(ConfigManager.translateApikey(newValue));
qps.setValue(String.valueOf(ConfigManager.translateQps(newValue)));
needApiKey.setValue(newValue.needApiKey());
});
}
};
private final StringProperty appId = new SimpleStringProperty(ConfigManager.translateAppid(ConfigManager.translateApi()));
private final StringProperty apiKey = new SimpleStringProperty(ConfigManager.translateApikey(ConfigManager.translateApi()));
private final StringProperty qps = new SimpleStringProperty(String.valueOf(ConfigManager.translateQps(ConfigManager.translateApi())));
private final BooleanProperty needApiKey = new SimpleBooleanProperty(ConfigManager.translateApi().needApiKey());
}

View File

@ -0,0 +1,37 @@
package cn.octopusyan.dmt.viewModel;
import cn.octopusyan.dmt.common.base.BaseViewModel;
import cn.octopusyan.dmt.controller.component.WordEditController;
import cn.octopusyan.dmt.model.WordItem;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 文本编辑
*
* @author octopus_yan
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class WordEditViewModel extends BaseViewModel<WordEditViewModel, WordEditController> {
private WordItem data;
/**
* 原文
*/
private StringProperty originalProperty = new SimpleStringProperty();
/**
* 中文
*/
private StringProperty chineseProperty = new SimpleStringProperty();
public void setData(WordItem data) {
if(data == null) return;
this.data = data;
originalProperty.bind(data.getOriginalProperty());
chineseProperty.bindBidirectional(data.getChineseProperty());
}
}

View File

@ -1,4 +1,4 @@
module cn.octopusyan.dayzmodtranslator {
module cn.octopusyan.dmt {
requires java.net.http;
requires javafx.controls;
requires javafx.fxml;
@ -9,14 +9,20 @@ module cn.octopusyan.dayzmodtranslator {
requires org.slf4j;
requires ch.qos.logback.core;
requires ch.qos.logback.classic;
requires com.alibaba.fastjson2;
requires static lombok;
requires atlantafx.base;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.dataformat.yaml;
requires java.prefs;
requires org.kordamp.ikonli.javafx;
requires org.kordamp.ikonli.feather;
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;
exports cn.octopusyan.dmt;
exports cn.octopusyan.dmt.model to com.fasterxml.jackson.databind;
opens cn.octopusyan.dmt.model to javafx.base;
opens cn.octopusyan.dmt.common.base to javafx.fxml;
opens cn.octopusyan.dmt.controller to javafx.fxml;
opens cn.octopusyan.dmt.controller.component to javafx.fxml;
opens cn.octopusyan.dmt.view.filemanager to javafx.fxml;
}

View File

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

Binary file not shown.

View File

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

Some files were not shown because too many files have changed in this diff Show More