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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -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;
}
}

View 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
);
}
}

View File

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

View File

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

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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;
}

View 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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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