mirror of
https://github.com/octopusYan/dayz-mod-translator.git
synced 2025-12-13 11:41:57 +08:00
pref: 更新界面、添加翻译进度反馈、展示日志信息
feat: 添加百度free翻译接口
This commit is contained in:
13
src/main/java/cn/octopusyan/dmt/AppLauncher.java
Normal file
13
src/main/java/cn/octopusyan/dmt/AppLauncher.java
Normal 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);
|
||||
}
|
||||
}
|
||||
114
src/main/java/cn/octopusyan/dmt/Application.java
Normal file
114
src/main/java/cn/octopusyan/dmt/Application.java
Normal 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);
|
||||
}
|
||||
}
|
||||
57
src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java
Normal file
57
src/main/java/cn/octopusyan/dmt/common/base/BaseBuilder.java
Normal 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();
|
||||
}
|
||||
}
|
||||
146
src/main/java/cn/octopusyan/dmt/common/base/BaseController.java
Normal file
146
src/main/java/cn/octopusyan/dmt/common/base/BaseController.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
27
src/main/java/cn/octopusyan/dmt/common/config/Constants.java
Normal file
27
src/main/java/cn/octopusyan/dmt/common/config/Constants.java
Normal 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";
|
||||
}
|
||||
117
src/main/java/cn/octopusyan/dmt/common/config/Context.java
Normal file
117
src/main/java/cn/octopusyan/dmt/common/config/Context.java
Normal 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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 = "取消";
|
||||
}
|
||||
29
src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java
Normal file
29
src/main/java/cn/octopusyan/dmt/common/enums/ProxySetup.java
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}};
|
||||
}
|
||||
@ -0,0 +1,192 @@
|
||||
package cn.octopusyan.dmt.common.manager.http;
|
||||
|
||||
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;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Duration;
|
||||
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;
|
||||
private final HttpConfig httpConfig;
|
||||
|
||||
private HttpUtil(HttpConfig httpConfig) {
|
||||
this.httpConfig = httpConfig;
|
||||
this.httpClient = createClient(httpConfig);
|
||||
}
|
||||
|
||||
public static HttpUtil getInstance() {
|
||||
if (util == null) {
|
||||
throw new RuntimeException("are you ready ?");
|
||||
}
|
||||
return util;
|
||||
}
|
||||
|
||||
public static void init(HttpConfig httpConfig) {
|
||||
synchronized (HttpUtil.class) {
|
||||
util = new HttpUtil(httpConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient createClient(HttpConfig httpConfig) {
|
||||
HttpClient.Builder builder = HttpClient.newBuilder()
|
||||
.version(httpConfig.getVersion())
|
||||
.connectTimeout(Duration.ofMillis(httpConfig.getConnectTimeout()))
|
||||
.sslContext(httpConfig.getSslContext())
|
||||
.sslParameters(httpConfig.getSslParameters())
|
||||
.followRedirects(httpConfig.getRedirect());
|
||||
Optional.ofNullable(httpConfig.getAuthenticator()).ifPresent(builder::authenticator);
|
||||
Optional.ofNullable(httpConfig.getCookieHandler()).ifPresent(builder::cookieHandler);
|
||||
Optional.ofNullable(httpConfig.getProxySelector()).ifPresent(builder::proxy);
|
||||
Optional.ofNullable(httpConfig.getExecutor()).ifPresent(builder::executor);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public void proxy(ProxySetup setup, ProxyInfo proxy) {
|
||||
if (httpClient == null)
|
||||
throw new RuntimeException("are you ready ?");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void clearProxy() {
|
||||
if (httpClient == null)
|
||||
throw new RuntimeException("are you ready ?");
|
||||
|
||||
httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
|
||||
httpClient = createClient(httpConfig);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (httpClient == null) return;
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
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, JsonNode header, JsonNode param) throws IOException, InterruptedException {
|
||||
HttpRequest.Builder request = getRequest(uri, header)
|
||||
.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, JsonNode header, JsonNode param) throws IOException, InterruptedException {
|
||||
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
|
||||
.POST(HttpRequest.BodyPublishers.noBody());
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
return response.body();
|
||||
}
|
||||
|
||||
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 (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(JsonNode params) {
|
||||
StringBuilder formParams = new StringBuilder();
|
||||
if (params == null) {
|
||||
return formParams.toString();
|
||||
}
|
||||
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.isArray()) {
|
||||
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
|
||||
} else {
|
||||
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
|
||||
}
|
||||
}
|
||||
if (!formParams.isEmpty()) {
|
||||
formParams = new StringBuilder(STR."?\{formParams.substring(1)}");
|
||||
}
|
||||
|
||||
return formParams.toString();
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package cn.octopusyan.dmt.common.manager.thread;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 自定义线程工厂
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class ThreadFactory implements java.util.concurrent.ThreadFactory {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ThreadFactory.class);
|
||||
|
||||
public static final String DEFAULT_THREAD_PREFIX = "thread-factory-pool";
|
||||
|
||||
private static final AtomicInteger poolNumber = new AtomicInteger(1);
|
||||
private final ThreadGroup group;
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
private final String namePrefix;
|
||||
|
||||
public ThreadFactory() {
|
||||
this(DEFAULT_THREAD_PREFIX);
|
||||
}
|
||||
|
||||
public ThreadFactory(String prefix) {
|
||||
group = Thread.currentThread().getThreadGroup();
|
||||
namePrefix = STR."\{prefix}-\{poolNumber.getAndIncrement()}-thread-";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
|
||||
Thread t = new Thread(group, runnable,
|
||||
namePrefix + threadNumber.getAndIncrement(),
|
||||
0);
|
||||
|
||||
t.setUncaughtExceptionHandler((t1, e) -> logger.error("thread : {}, error", t1.getName(), e));
|
||||
|
||||
if (t.isDaemon())
|
||||
t.setDaemon(false);
|
||||
if (t.getPriority() != Thread.NORM_PRIORITY)
|
||||
t.setPriority(Thread.NORM_PRIORITY);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
40
src/main/java/cn/octopusyan/dmt/common/util/ClipUtil.java
Normal file
40
src/main/java/cn/octopusyan/dmt/common/util/ClipUtil.java
Normal file
@ -0,0 +1,40 @@
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 剪切板工具
|
||||
*
|
||||
* @author octopus_yan
|
||||
*/
|
||||
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数据类型
|
||||
StringSelection stringSelection = new StringSelection(data);
|
||||
//添加文本到系统剪切板
|
||||
clipboard.setContents(stringSelection, null);
|
||||
}
|
||||
|
||||
public static String getString() {
|
||||
Transferable content = clipboard.getContents(null);//从系统剪切板中获取数据
|
||||
if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {//判断是否为文本类型
|
||||
try {
|
||||
//从数据中获取文本值
|
||||
return (String) content.getTransferData(DataFlavor.stringFlavor);
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
log.error("", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
27
src/main/java/cn/octopusyan/dmt/common/util/FxmlUtil.java
Normal file
27
src/main/java/cn/octopusyan/dmt/common/util/FxmlUtil.java
Normal file
@ -0,0 +1,27 @@
|
||||
package cn.octopusyan.dmt.common.util;
|
||||
|
||||
import cn.octopusyan.dmt.common.config.Context;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.JavaFXBuilderFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* FXML 工具
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class FxmlUtil {
|
||||
|
||||
public static FXMLLoader load(String name) {
|
||||
String prefix = "/fxml/";
|
||||
String suffix = ".fxml";
|
||||
return new FXMLLoader(
|
||||
FxmlUtil.class.getResource(prefix + name + suffix),
|
||||
null,
|
||||
new JavaFXBuilderFactory(),
|
||||
Context.getControlFactory(),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
}
|
||||
}
|
||||
187
src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java
Normal file
187
src/main/java/cn/octopusyan/dmt/common/util/JsonUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
119
src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java
Normal file
119
src/main/java/cn/octopusyan/dmt/common/util/ProcessesUtil.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 配置文件读取工具
|
||||
*
|
||||
* @author liubin5620
|
||||
* @see <a href="https://blog.csdn.net/liubin5620/article/details/104618950">配置文件信息读取工具类【PropertiesUtils】</a>
|
||||
*/
|
||||
public class PropertiesUtils {
|
||||
/**
|
||||
* 主配置文件
|
||||
*/
|
||||
private final Properties properties;
|
||||
/**
|
||||
* 启用配置文件
|
||||
*/
|
||||
private final Properties propertiesCustom;
|
||||
|
||||
private static PropertiesUtils propertiesUtils = new PropertiesUtils();
|
||||
|
||||
public static final Logger logger = LoggerFactory.getLogger(PropertiesUtils.class);
|
||||
|
||||
/**
|
||||
* 私有构造,禁止直接创建
|
||||
*/
|
||||
private PropertiesUtils() {
|
||||
// 读取配置启用的配置文件名
|
||||
properties = new Properties();
|
||||
propertiesCustom = new Properties();
|
||||
|
||||
InputStream in = Resources.getResourceAsStream("application.properties");
|
||||
try {
|
||||
properties.load(new InputStreamReader(in));
|
||||
// 加载启用的配置
|
||||
String property = properties.getProperty("profiles.active");
|
||||
if (!StringUtils.isBlank(property)) {
|
||||
InputStream cin = Resources.getResourceAsStream("application-" + property + ".properties");
|
||||
propertiesCustom.load(new InputStreamReader(cin));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("读取配置文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例
|
||||
*
|
||||
* @return PropertiesUtils
|
||||
*/
|
||||
public static PropertiesUtils getInstance() {
|
||||
if (propertiesUtils == null) {
|
||||
propertiesUtils = new PropertiesUtils();
|
||||
}
|
||||
return propertiesUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据属性名读取值
|
||||
* 先去主配置查询,如果查询不到,就去启用配置查询
|
||||
*
|
||||
* @param name 名称
|
||||
*/
|
||||
public String getProperty(String name) {
|
||||
String val = properties.getProperty(name);
|
||||
if (StringUtils.isBlank(val)) {
|
||||
val = propertiesCustom.getProperty(name);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
69
src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java
Normal file
69
src/main/java/cn/octopusyan/dmt/common/util/ViewUtil.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
451
src/main/java/cn/octopusyan/dmt/controller/MainController.java
Normal file
451
src/main/java/cn/octopusyan/dmt/controller/MainController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
33
src/main/java/cn/octopusyan/dmt/model/ConfigModel.java
Normal file
33
src/main/java/cn/octopusyan/dmt/model/ConfigModel.java
Normal 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();
|
||||
|
||||
|
||||
}
|
||||
39
src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java
Normal file
39
src/main/java/cn/octopusyan/dmt/model/ProxyInfo.java
Normal 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();
|
||||
}
|
||||
52
src/main/java/cn/octopusyan/dmt/model/Translate.java
Normal file
52
src/main/java/cn/octopusyan/dmt/model/Translate.java
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
29
src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java
Normal file
29
src/main/java/cn/octopusyan/dmt/model/UpgradeConfig.java
Normal 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()}";
|
||||
}
|
||||
}
|
||||
52
src/main/java/cn/octopusyan/dmt/model/WordItem.java
Normal file
52
src/main/java/cn/octopusyan/dmt/model/WordItem.java
Normal 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();
|
||||
}
|
||||
}
|
||||
80
src/main/java/cn/octopusyan/dmt/task/PackTask.java
Normal file
80
src/main/java/cn/octopusyan/dmt/task/PackTask.java
Normal 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);
|
||||
}
|
||||
}
|
||||
28
src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java
Normal file
28
src/main/java/cn/octopusyan/dmt/task/ProxyCheckTask.java
Normal 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}");
|
||||
}
|
||||
}
|
||||
91
src/main/java/cn/octopusyan/dmt/task/TranslateTask.java
Normal file
91
src/main/java/cn/octopusyan/dmt/task/TranslateTask.java
Normal 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();
|
||||
}
|
||||
}
|
||||
63
src/main/java/cn/octopusyan/dmt/task/UnpackTask.java
Normal file
63
src/main/java/cn/octopusyan/dmt/task/UnpackTask.java
Normal 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);
|
||||
}
|
||||
}
|
||||
55
src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java
Normal file
55
src/main/java/cn/octopusyan/dmt/task/UpgradeTask.java
Normal 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 ...
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java
Normal file
50
src/main/java/cn/octopusyan/dmt/task/base/BaseTask.java
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/main/java/cn/octopusyan/dmt/task/base/Listener.java
Normal file
23
src/main/java/cn/octopusyan/dmt/task/base/Listener.java
Normal 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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
13
src/main/java/cn/octopusyan/dmt/translate/ApiKey.java
Normal file
13
src/main/java/cn/octopusyan/dmt/translate/ApiKey.java
Normal 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) {
|
||||
|
||||
}
|
||||
39
src/main/java/cn/octopusyan/dmt/translate/DelayWord.java
Normal file
39
src/main/java/cn/octopusyan/dmt/translate/DelayWord.java
Normal 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);
|
||||
}
|
||||
}
|
||||
60
src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java
Normal file
60
src/main/java/cn/octopusyan/dmt/translate/TranslateApi.java
Normal 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("类型不存在");
|
||||
}
|
||||
}
|
||||
66
src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java
Normal file
66
src/main/java/cn/octopusyan/dmt/translate/TranslateUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 谷歌 免费翻译接口
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class BaiduTranslateProcessor extends AbstractTranslateProcessor {
|
||||
|
||||
private ApiKey apiKey;
|
||||
|
||||
public BaiduTranslateProcessor() {
|
||||
super(TranslateApi.BAIDU);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String url() {
|
||||
return "https://fanyi-api.baidu.com/api/trans/vip/translate";
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param source 待翻译单词
|
||||
* @return 翻译结果
|
||||
*/
|
||||
@Override
|
||||
public String customTranslate(String source) throws IOException, InterruptedException {
|
||||
apiKey = getApiKey();
|
||||
String appid = apiKey.appid();
|
||||
String salt = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("q", source);
|
||||
param.put("from", "auto");
|
||||
param.put("to", "zh");
|
||||
param.put("appid", appid);
|
||||
param.put("salt", salt);
|
||||
param.put("sign", getSign(appid, source, salt));
|
||||
|
||||
String resp = httpUtil.get(url(), null, JsonUtil.parseJsonObject(param));
|
||||
JsonNode json = JsonUtil.parseJsonObject(resp);
|
||||
|
||||
if (!json.has("trans_result")) {
|
||||
Object errorMsg = json.get("error_msg");
|
||||
logger.error("翻译失败: {}", errorMsg);
|
||||
throw new RuntimeException(String.valueOf(errorMsg));
|
||||
}
|
||||
|
||||
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.apiKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5加密
|
||||
*
|
||||
* @param str 待加密字符串
|
||||
* @return 16进制加密字符串
|
||||
*/
|
||||
public String encrypt2ToMD5(String str) {
|
||||
|
||||
// 加密后的16进制字符串
|
||||
String hexStr = "";
|
||||
try {
|
||||
|
||||
// 此 MessageDigest 类为应用程序提供信息摘要算法的功能
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
// 转换为MD5码
|
||||
byte[] digest = md5.digest(str.getBytes(StandardCharsets.UTF_8));
|
||||
hexStr = bytesToHexString(digest);
|
||||
} catch (Exception e) {
|
||||
logger.error("", e);
|
||||
}
|
||||
return hexStr.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将byte数组转换成string类型表示
|
||||
*/
|
||||
private String bytesToHexString(byte[] src) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (src == null || src.length == 0) {
|
||||
return null;
|
||||
}
|
||||
String hv;
|
||||
for (byte b : src) {
|
||||
// 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写
|
||||
hv = Integer.toHexString(b & 0xFF).toUpperCase();
|
||||
if (hv.length() < 2) {
|
||||
builder.append(0);
|
||||
}
|
||||
builder.append(hv);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 谷歌 免费翻译接口
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
|
||||
|
||||
public FreeGoogleTranslateProcessor() {
|
||||
super(TranslateApi.FREE_GOOGLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String url() {
|
||||
return "https://translate.googleapis.com/translate_a/single";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int qps() {
|
||||
return source().getDefaultQps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译处理
|
||||
*
|
||||
* @param source 待翻译单词
|
||||
* @return 翻译结果
|
||||
*/
|
||||
@Override
|
||||
public String customTranslate(String source) throws IOException, InterruptedException {
|
||||
|
||||
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);
|
||||
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
StringBuilder retStr = new StringBuilder();
|
||||
// TODO 短时大量请求会被ban,需要浏览器验证添加cookie
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package cn.octopusyan.dmt.translate.processor;
|
||||
|
||||
/**
|
||||
* 翻译处理器
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public interface TranslateProcessor {
|
||||
|
||||
/**
|
||||
* 翻译源 api接口地址
|
||||
*/
|
||||
String url();
|
||||
|
||||
/**
|
||||
* 是否需要配置API认证
|
||||
*/
|
||||
boolean needApiKey();
|
||||
|
||||
/**
|
||||
* 已配置API认证
|
||||
*/
|
||||
boolean configuredKey();
|
||||
|
||||
/**
|
||||
* qps 每秒访问的数量限制
|
||||
*/
|
||||
int qps();
|
||||
|
||||
/**
|
||||
* 翻译
|
||||
*
|
||||
* @param original 原始文本
|
||||
* @return 翻译结果
|
||||
* @throws Exception 翻译出错
|
||||
*/
|
||||
String translate(String original) throws Exception;
|
||||
}
|
||||
181
src/main/java/cn/octopusyan/dmt/utils/FileUtil.java
Normal file
181
src/main/java/cn/octopusyan/dmt/utils/FileUtil.java
Normal file
@ -0,0 +1,181 @@
|
||||
package cn.octopusyan.dmt.utils;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.CanReadFileFilter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 文件工具类
|
||||
*
|
||||
* @author octopus_yan@foxmail.com
|
||||
*/
|
||||
public class FileUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
|
||||
|
||||
public static File[] ls(String path) {
|
||||
File dir = new File(path);
|
||||
if (!dir.exists())
|
||||
throw new RuntimeException(path + "不存在!");
|
||||
|
||||
if (!dir.isDirectory())
|
||||
throw new RuntimeException(path + "不是一个文件夹!");
|
||||
|
||||
return dir.listFiles();
|
||||
}
|
||||
|
||||
public static void copyFilesFromDir(String path, String dest) throws IOException {
|
||||
if (StringUtils.isBlank(path) || StringUtils.isBlank(dest)) {
|
||||
logger.error("path is blank !");
|
||||
return;
|
||||
}
|
||||
File dir = new File(path);
|
||||
if (!dir.exists()) {
|
||||
logger.error("[" + path + "] 不存在!");
|
||||
return;
|
||||
}
|
||||
if (!dir.isDirectory()) {
|
||||
logger.error("[" + path + "] 不是一个文件夹!");
|
||||
}
|
||||
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return;
|
||||
|
||||
File directory = new File(dest);
|
||||
if (directory.exists() && !directory.isDirectory()) {
|
||||
logger.error("[" + dest + "] 不是一个文件夹!");
|
||||
}
|
||||
|
||||
FileUtils.forceMkdir(directory);
|
||||
|
||||
for (File file : files) {
|
||||
copyFile(file, new File(dest + File.separator + file.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFile(File in, File out) throws IOException {
|
||||
copyFile(Files.newInputStream(in.toPath()), out);
|
||||
}
|
||||
|
||||
public static void copyFile(InputStream input, File out) throws IOException {
|
||||
OutputStream output = null;
|
||||
try {
|
||||
output = Files.newOutputStream(out.toPath());
|
||||
byte[] buf = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buf)) > 0) {
|
||||
output.write(buf, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("", e);
|
||||
} finally {
|
||||
if (output != null) input.close();
|
||||
if (output != null) output.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件主名称
|
||||
*
|
||||
* @param file 文件对象
|
||||
* @return 文件名称
|
||||
*/
|
||||
public static String mainName(File file) {
|
||||
//忽略判断
|
||||
String fileName = file.getName();
|
||||
return fileName.substring(0, fileName.lastIndexOf("."));
|
||||
}
|
||||
|
||||
public static 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) {
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回被查找到的文件的绝对路径(匹配到一个就返回)
|
||||
*
|
||||
* @param root 根目录文件
|
||||
* @param fileName 要找的文件名
|
||||
* @return 绝对路径
|
||||
*/
|
||||
private static String findFiles(File root, String fileName) {
|
||||
//定义一个返回值
|
||||
String path = null;
|
||||
//如果传进来的是目录,并且存在
|
||||
if (root.exists() && root.isDirectory()) {
|
||||
//遍历文件夹中的各个文件
|
||||
File[] files = root.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
//如果path的值没有变化
|
||||
if (path == null) {
|
||||
if (file.isFile() && file.getName().contains(fileName)) {
|
||||
path = file.getAbsolutePath();
|
||||
} else {
|
||||
path = findFiles(file, fileName);
|
||||
}
|
||||
} else {
|
||||
break;//跳出循环,增加性能
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件的编码格式
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
407
src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
Normal file
407
src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
43
src/main/java/cn/octopusyan/dmt/utils/Resources.java
Normal file
43
src/main/java/cn/octopusyan/dmt/utils/Resources.java
Normal 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");
|
||||
}
|
||||
}
|
||||
151
src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java
Normal file
151
src/main/java/cn/octopusyan/dmt/view/ConsoleLog.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/main/java/cn/octopusyan/dmt/view/PopupMenu.java
Normal file
137
src/main/java/cn/octopusyan/dmt/view/PopupMenu.java
Normal 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;
|
||||
}
|
||||
}
|
||||
100
src/main/java/cn/octopusyan/dmt/view/alert/AlertUtil.java
Normal file
100
src/main/java/cn/octopusyan/dmt/view/alert/AlertUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
202
src/main/java/cn/octopusyan/dmt/viewModel/MainViewModel.java
Normal file
202
src/main/java/cn/octopusyan/dmt/viewModel/MainViewModel.java
Normal 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();
|
||||
}
|
||||
}
|
||||
107
src/main/java/cn/octopusyan/dmt/viewModel/ProxyViewModel.java
Normal file
107
src/main/java/cn/octopusyan/dmt/viewModel/ProxyViewModel.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user