From b2d68fce100d9d7012b3012f19fd4fd37bc05cf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:44:12 +0000 Subject: [PATCH 01/20] Initial plan From 1d861f5ddb507ffcdd26dd2c21c92f59186d3288 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:50:21 +0000 Subject: [PATCH 02/20] Add configurable translator selection with Bing as alternative to Google Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../tool/ui/form/TranslationLayoutForm.java | 49 +++++- .../luoboduner/moo/tool/util/ConfigUtil.java | 8 + .../util/translator/BingTranslatorUtil.java | 159 ++++++++++++++++++ .../util/translator/TranslatorFactory.java | 3 + 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java index 9167a889..f76963f5 100644 --- a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java +++ b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java @@ -4,6 +4,7 @@ import com.formdev.flatlaf.extras.FlatSVGIcon; import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.uiDesigner.core.Spacer; +import com.luoboduner.moo.tool.util.ConfigUtil; import com.luoboduner.moo.tool.util.UndoUtil; import com.luoboduner.moo.tool.util.translator.Translator; import com.luoboduner.moo.tool.util.translator.TranslatorFactory; @@ -28,6 +29,7 @@ public class TranslationLayoutForm { private JComboBox comboBox1; private JButton exchangeButton; private JComboBox comboBox2; + private JComboBox translatorComboBox; private static AtomicInteger changeCount = new AtomicInteger(0); @@ -49,10 +51,29 @@ public class TranslationLayoutForm { defaultComboBoxModel2.addElement("英语"); comboBox2.setModel(defaultComboBoxModel2); + translatorComboBox = new JComboBox(); + final DefaultComboBoxModel translatorComboBoxModel = new DefaultComboBoxModel(); + translatorComboBoxModel.addElement("Google翻译"); + translatorComboBoxModel.addElement("Bing翻译"); + translatorComboBoxModel.addElement("微软翻译"); + translatorComboBox.setModel(translatorComboBoxModel); + // Load saved translator preference + String savedTranslator = ConfigUtil.getInstance().getTranslatorType(); + if ("MICROSOFT".equals(savedTranslator)) { + translatorComboBox.setSelectedItem("微软翻译"); + } else if ("BING".equals(savedTranslator)) { + translatorComboBox.setSelectedItem("Bing翻译"); + } else { + translatorComboBox.setSelectedItem("Google翻译"); + } + leftMenuToolBar = new JToolBar(); leftMenuToolBar.add(comboBox1); leftMenuToolBar.add(exchangeButton); leftMenuToolBar.add(comboBox2); + leftMenuToolBar.addSeparator(); + leftMenuToolBar.add(new JLabel("翻译源: ")); + leftMenuToolBar.add(translatorComboBox); leftMenuPanel.add(leftMenuToolBar); @@ -116,6 +137,22 @@ public class TranslationLayoutForm { } }); + translatorComboBox.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + String itemName = e.getItem().toString(); + String translatorType; + if ("Google翻译".equals(itemName)) { + translatorType = "GOOGLE"; + } else if ("Bing翻译".equals(itemName)) { + translatorType = "BING"; + } else { + translatorType = "MICROSOFT"; + } + ConfigUtil.getInstance().setTranslatorType(translatorType); + translateControl(); + } + }); + textArea1.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { @@ -155,8 +192,18 @@ public class TranslationLayoutForm { String targetLanguage = comboBox2.getSelectedItem().toString(); String text = textArea1.getText(); + // Get the selected translator type from config + String translatorTypeStr = ConfigUtil.getInstance().getTranslatorType(); + TranslatorFactory.TranslatorType translatorType = TranslatorFactory.TranslatorType.GOOGLE; + try { + translatorType = TranslatorFactory.TranslatorType.valueOf(translatorTypeStr); + } catch (IllegalArgumentException e) { + // Default to GOOGLE if invalid + translatorType = TranslatorFactory.TranslatorType.GOOGLE; + } + TranslatorFactory translatorFactory = new TranslatorFactory(); - String result = translatorFactory.getTranslator(TranslatorFactory.TranslatorType.GOOGLE).translate(text, Translator.languageNameToCodeMap.get(sourceLanguage), Translator.languageNameToCodeMap.get(targetLanguage)); + String result = translatorFactory.getTranslator(translatorType).translate(text, Translator.languageNameToCodeMap.get(sourceLanguage), Translator.languageNameToCodeMap.get(targetLanguage)); textArea2.setText(result); } diff --git a/src/main/java/com/luoboduner/moo/tool/util/ConfigUtil.java b/src/main/java/com/luoboduner/moo/tool/util/ConfigUtil.java index c2d78172..baf2c88f 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/ConfigUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/ConfigUtil.java @@ -522,4 +522,12 @@ public class ConfigUtil extends ConfigBaseUtil { public void setRegexText(String regexText) { setting.putByGroup("regexText", "func.regex", regexText); } + + public String getTranslatorType() { + return setting.getStr("translatorType", "func.translation", "GOOGLE"); + } + + public void setTranslatorType(String translatorType) { + setting.putByGroup("translatorType", "func.translation", translatorType); + } } diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java new file mode 100644 index 00000000..c0b26256 --- /dev/null +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -0,0 +1,159 @@ +package com.luoboduner.moo.tool.util.translator; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.net.ssl.SSLHandshakeException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.net.http.HttpTimeoutException; +import java.nio.charset.StandardCharsets; + +/** + * Bing翻译工具类 (使用Bing翻译API,在中国大陆可访问) + */ +@Slf4j +public class BingTranslatorUtil implements Translator { + + /** + * @param word + * @param sourceLanguage 源语言 默认auto 英文为 en + * @param targetLanguage 目标语言 默认zh-CN + * @return + */ + public String translate(String word, String sourceLanguage, String targetLanguage) { + try { + if (StringUtils.isEmpty(word)) { + return ""; + } + + if (StringUtils.isEmpty(sourceLanguage)) { + sourceLanguage = "auto-detect"; + } + if (StringUtils.isEmpty(targetLanguage)) { + targetLanguage = "zh-Hans"; + } + + // Convert language codes to Bing format + sourceLanguage = convertToBingLanguageCode(sourceLanguage); + targetLanguage = convertToBingLanguageCode(targetLanguage); + + String url = "https://www.bing.com/ttranslatev3?isVertical=1" + + "&IG=" + System.currentTimeMillis() + + "&IID=translator.5028.1"; + + URL obj = new URL(url); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("POST"); + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + con.setRequestProperty("Referer", "https://www.bing.com/translator"); + con.setDoOutput(true); + + String postData = "fromLang=" + sourceLanguage + + "&to=" + targetLanguage + + "&text=" + URLEncoder.encode(word, StandardCharsets.UTF_8); + + con.getOutputStream().write(postData.getBytes(StandardCharsets.UTF_8)); + + BufferedReader in = new BufferedReader( + new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)); + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + return parseResult(response.toString()); + } catch (SSLHandshakeException e) { + log.error("SSLHandshakeException", e); + return "访问Bing翻译接口网络异常:" + e.getMessage(); + } catch (HttpTimeoutException e) { + log.error("HttpTimeoutException", e); + return "访问Bing翻译接口超时:" + e.getMessage(); + } catch (Exception e) { + log.error("访问Bing翻译异常", e); + return "访问Bing翻译接口异常:" + e.getMessage(); + } + } + + private String convertToBingLanguageCode(String code) { + if (StringUtils.isEmpty(code) || "auto".equals(code)) { + return "auto-detect"; + } + // Convert common codes to Bing format + switch (code) { + case "zh-CN": + return "zh-Hans"; + case "cht": + return "zh-Hant"; + case "en": + return "en"; + case "jp": + return "ja"; + case "kor": + return "ko"; + case "fra": + return "fr"; + case "spa": + return "es"; + case "th": + return "th"; + case "ara": + return "ar"; + case "ru": + return "ru"; + case "pt": + return "pt"; + case "de": + return "de"; + case "it": + return "it"; + case "el": + return "el"; + case "nl": + return "nl"; + case "pl": + return "pl"; + case "cs": + return "cs"; + case "swe": + return "sv"; + case "hu": + return "hu"; + case "vie": + return "vi"; + default: + return code; + } + } + + private static String parseResult(String inputJson) { + try { + JSONArray jsonArray = new JSONArray(inputJson); + if (jsonArray.size() > 0) { + JSONObject firstItem = jsonArray.getJSONObject(0); + if (firstItem.containsKey("translations")) { + JSONArray translations = firstItem.getJSONArray("translations"); + if (translations.size() > 0) { + JSONObject translation = translations.getJSONObject(0); + return translation.getStr("text"); + } + } + } + return "解析翻译结果失败"; + } catch (Exception e) { + log.error("解析翻译结果异常", e); + return "解析翻译结果异常:" + e.getMessage(); + } + } +} diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java b/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java index 60517e9b..afb08165 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java @@ -3,6 +3,7 @@ package com.luoboduner.moo.tool.util.translator; public class TranslatorFactory { public enum TranslatorType { GOOGLE, + BING, MICROSOFT } @@ -10,6 +11,8 @@ public class TranslatorFactory { switch (type) { case GOOGLE: return new GoogleTranslatorUtil(); + case BING: + return new BingTranslatorUtil(); case MICROSOFT: // return new MicrosoftTranslatorUtil(); default: From adbccbc42c97709ef09a93492a61e62f6dfbd544 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:53:01 +0000 Subject: [PATCH 03/20] Fix code review issues: correct exception handling and optimize factory usage Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../moo/tool/ui/form/TranslationLayoutForm.java | 10 ++++++++-- .../moo/tool/util/translator/BingTranslatorUtil.java | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java index f76963f5..2326e198 100644 --- a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java +++ b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java @@ -9,6 +9,7 @@ import com.luoboduner.moo.tool.util.UndoUtil; import com.luoboduner.moo.tool.util.translator.Translator; import com.luoboduner.moo.tool.util.translator.TranslatorFactory; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -192,6 +193,12 @@ public class TranslationLayoutForm { String targetLanguage = comboBox2.getSelectedItem().toString(); String text = textArea1.getText(); + // Skip translation if text is empty + if (StringUtils.isEmpty(text)) { + textArea2.setText(""); + return; + } + // Get the selected translator type from config String translatorTypeStr = ConfigUtil.getInstance().getTranslatorType(); TranslatorFactory.TranslatorType translatorType = TranslatorFactory.TranslatorType.GOOGLE; @@ -202,8 +209,7 @@ public class TranslationLayoutForm { translatorType = TranslatorFactory.TranslatorType.GOOGLE; } - TranslatorFactory translatorFactory = new TranslatorFactory(); - String result = translatorFactory.getTranslator(translatorType).translate(text, Translator.languageNameToCodeMap.get(sourceLanguage), Translator.languageNameToCodeMap.get(targetLanguage)); + String result = TranslatorFactory.getTranslator(translatorType).translate(text, Translator.languageNameToCodeMap.get(sourceLanguage), Translator.languageNameToCodeMap.get(targetLanguage)); textArea2.setText(result); } diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index c0b26256..a741feb7 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -9,9 +9,9 @@ import javax.net.ssl.SSLHandshakeException; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLEncoder; -import java.net.http.HttpTimeoutException; import java.nio.charset.StandardCharsets; /** @@ -77,8 +77,8 @@ public class BingTranslatorUtil implements Translator { } catch (SSLHandshakeException e) { log.error("SSLHandshakeException", e); return "访问Bing翻译接口网络异常:" + e.getMessage(); - } catch (HttpTimeoutException e) { - log.error("HttpTimeoutException", e); + } catch (SocketTimeoutException e) { + log.error("SocketTimeoutException", e); return "访问Bing翻译接口超时:" + e.getMessage(); } catch (Exception e) { log.error("访问Bing翻译异常", e); @@ -137,7 +137,7 @@ public class BingTranslatorUtil implements Translator { } } - private static String parseResult(String inputJson) { + private String parseResult(String inputJson) { try { JSONArray jsonArray = new JSONArray(inputJson); if (jsonArray.size() > 0) { From 18174e9108ef36d56a82b806c1ae88b38e9244e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:54:52 +0000 Subject: [PATCH 04/20] Add better error handling and validation for translation Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../moo/tool/ui/form/TranslationLayoutForm.java | 13 ++++++++++++- .../util/translator/BingTranslatorUtil.java | 17 ++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java index 2326e198..4dd743a7 100644 --- a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java +++ b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java @@ -209,7 +209,18 @@ public class TranslationLayoutForm { translatorType = TranslatorFactory.TranslatorType.GOOGLE; } - String result = TranslatorFactory.getTranslator(translatorType).translate(text, Translator.languageNameToCodeMap.get(sourceLanguage), Translator.languageNameToCodeMap.get(targetLanguage)); + // Get language codes, with fallback for null values + String sourceLangCode = Translator.languageNameToCodeMap.get(sourceLanguage); + String targetLangCode = Translator.languageNameToCodeMap.get(targetLanguage); + + if (sourceLangCode == null) { + sourceLangCode = "auto"; // Default to auto-detect + } + if (targetLangCode == null) { + targetLangCode = "zh-CN"; // Default to Simplified Chinese + } + + String result = TranslatorFactory.getTranslator(translatorType).translate(text, sourceLangCode, targetLangCode); textArea2.setText(result); } diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index a741feb7..71636257 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -20,6 +20,9 @@ import java.nio.charset.StandardCharsets; @Slf4j public class BingTranslatorUtil implements Translator { + // Bing Translator API endpoint identifier + private static final String BING_TRANSLATOR_IID = "translator.5028.1"; + /** * @param word * @param sourceLanguage 源语言 默认auto 英文为 en @@ -43,9 +46,10 @@ public class BingTranslatorUtil implements Translator { sourceLanguage = convertToBingLanguageCode(sourceLanguage); targetLanguage = convertToBingLanguageCode(targetLanguage); + // Note: IG parameter format may need adjustment based on actual Bing API requirements String url = "https://www.bing.com/ttranslatev3?isVertical=1" + "&IG=" + System.currentTimeMillis() + - "&IID=translator.5028.1"; + "&IID=" + BING_TRANSLATOR_IID; URL obj = new URL(url); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); @@ -139,6 +143,10 @@ public class BingTranslatorUtil implements Translator { private String parseResult(String inputJson) { try { + if (StringUtils.isEmpty(inputJson)) { + return "翻译返回结果为空"; + } + JSONArray jsonArray = new JSONArray(inputJson); if (jsonArray.size() > 0) { JSONObject firstItem = jsonArray.getJSONObject(0); @@ -146,11 +154,14 @@ public class BingTranslatorUtil implements Translator { JSONArray translations = firstItem.getJSONArray("translations"); if (translations.size() > 0) { JSONObject translation = translations.getJSONObject(0); - return translation.getStr("text"); + String result = translation.getStr("text"); + if (!StringUtils.isEmpty(result)) { + return result; + } } } } - return "解析翻译结果失败"; + return "解析翻译结果失败,返回格式不符合预期"; } catch (Exception e) { log.error("解析翻译结果异常", e); return "解析翻译结果异常:" + e.getMessage(); From c8005c36627743b192dd80c18207c7924281ca1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:57:43 +0000 Subject: [PATCH 05/20] Final improvements: resource management, constants, and Microsoft fallback Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../tool/ui/form/TranslationLayoutForm.java | 26 ++++++++++++------- .../util/translator/BingTranslatorUtil.java | 23 +++++++++------- .../util/translator/TranslatorFactory.java | 4 ++- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java index 4dd743a7..847556dc 100644 --- a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java +++ b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java @@ -34,6 +34,11 @@ public class TranslationLayoutForm { private static AtomicInteger changeCount = new AtomicInteger(0); + // Constants for translator names + private static final String TRANSLATOR_GOOGLE = "Google翻译"; + private static final String TRANSLATOR_BING = "Bing翻译"; + private static final String TRANSLATOR_MICROSOFT = "微软翻译"; + public TranslationLayoutForm() { exchangeButton = new JButton(); exchangeButton.setIcon(new FlatSVGIcon("icon/exchange.svg")); @@ -54,18 +59,18 @@ public class TranslationLayoutForm { translatorComboBox = new JComboBox(); final DefaultComboBoxModel translatorComboBoxModel = new DefaultComboBoxModel(); - translatorComboBoxModel.addElement("Google翻译"); - translatorComboBoxModel.addElement("Bing翻译"); - translatorComboBoxModel.addElement("微软翻译"); + translatorComboBoxModel.addElement(TRANSLATOR_GOOGLE); + translatorComboBoxModel.addElement(TRANSLATOR_BING); + translatorComboBoxModel.addElement(TRANSLATOR_MICROSOFT); translatorComboBox.setModel(translatorComboBoxModel); // Load saved translator preference String savedTranslator = ConfigUtil.getInstance().getTranslatorType(); if ("MICROSOFT".equals(savedTranslator)) { - translatorComboBox.setSelectedItem("微软翻译"); + translatorComboBox.setSelectedItem(TRANSLATOR_MICROSOFT); } else if ("BING".equals(savedTranslator)) { - translatorComboBox.setSelectedItem("Bing翻译"); + translatorComboBox.setSelectedItem(TRANSLATOR_BING); } else { - translatorComboBox.setSelectedItem("Google翻译"); + translatorComboBox.setSelectedItem(TRANSLATOR_GOOGLE); } leftMenuToolBar = new JToolBar(); @@ -142,15 +147,18 @@ public class TranslationLayoutForm { if (e.getStateChange() == ItemEvent.SELECTED) { String itemName = e.getItem().toString(); String translatorType; - if ("Google翻译".equals(itemName)) { + if (TRANSLATOR_GOOGLE.equals(itemName)) { translatorType = "GOOGLE"; - } else if ("Bing翻译".equals(itemName)) { + } else if (TRANSLATOR_BING.equals(itemName)) { translatorType = "BING"; } else { translatorType = "MICROSOFT"; } ConfigUtil.getInstance().setTranslatorType(translatorType); - translateControl(); + // Only translate if there's actual text to translate + if (!StringUtils.isEmpty(textArea1.getText())) { + translateControl(); + } } }); diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index 71636257..86e5ed43 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -65,17 +65,20 @@ public class BingTranslatorUtil implements Translator { "&to=" + targetLanguage + "&text=" + URLEncoder.encode(word, StandardCharsets.UTF_8); - con.getOutputStream().write(postData.getBytes(StandardCharsets.UTF_8)); - - BufferedReader in = new BufferedReader( - new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)); - String inputLine; - StringBuilder response = new StringBuilder(); - - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); + // Write request body with proper resource management + try (java.io.OutputStream os = con.getOutputStream()) { + os.write(postData.getBytes(StandardCharsets.UTF_8)); + } + + // Read response with proper resource management + StringBuilder response = new StringBuilder(); + try (BufferedReader in = new BufferedReader( + new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) { + String inputLine; + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } } - in.close(); return parseResult(response.toString()); } catch (SSLHandshakeException e) { diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java b/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java index afb08165..735cf5ff 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/TranslatorFactory.java @@ -14,7 +14,9 @@ public class TranslatorFactory { case BING: return new BingTranslatorUtil(); case MICROSOFT: -// return new MicrosoftTranslatorUtil(); + // Microsoft translator requires API key configuration + // Falling back to Google translator for now + return new GoogleTranslatorUtil(); default: throw new IllegalArgumentException("Unknown translator type: " + type); } From e5b851dc6b87095fa17cfb43913ef17e807fd28e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:00:06 +0000 Subject: [PATCH 06/20] Add comprehensive documentation and tooltips Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../moo/tool/ui/form/TranslationLayoutForm.java | 1 + .../tool/util/translator/BingTranslatorUtil.java | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java index 847556dc..a0523af1 100644 --- a/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java +++ b/src/main/java/com/luoboduner/moo/tool/ui/form/TranslationLayoutForm.java @@ -63,6 +63,7 @@ public class TranslationLayoutForm { translatorComboBoxModel.addElement(TRANSLATOR_BING); translatorComboBoxModel.addElement(TRANSLATOR_MICROSOFT); translatorComboBox.setModel(translatorComboBoxModel); + translatorComboBox.setToolTipText("选择翻译源。注意:微软翻译暂时回退到Google翻译"); // Load saved translator preference String savedTranslator = ConfigUtil.getInstance().getTranslatorType(); if ("MICROSOFT".equals(savedTranslator)) { diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index 86e5ed43..1631a402 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -20,7 +20,11 @@ import java.nio.charset.StandardCharsets; @Slf4j public class BingTranslatorUtil implements Translator { - // Bing Translator API endpoint identifier + /** + * Bing Translator API endpoint identifier + * This value is used by the Bing Translator public API + * It may need to be updated if Bing changes their API + */ private static final String BING_TRANSLATOR_IID = "translator.5028.1"; /** @@ -46,7 +50,12 @@ public class BingTranslatorUtil implements Translator { sourceLanguage = convertToBingLanguageCode(sourceLanguage); targetLanguage = convertToBingLanguageCode(targetLanguage); - // Note: IG parameter format may need adjustment based on actual Bing API requirements + /** + * Build Bing Translator API URL + * IG parameter: Uses current timestamp, may need adjustment based on actual API requirements + * IID parameter: Fixed identifier for translator endpoint + * Note: This uses Bing's public translator endpoint which may change over time + */ String url = "https://www.bing.com/ttranslatev3?isVertical=1" + "&IG=" + System.currentTimeMillis() + "&IID=" + BING_TRANSLATOR_IID; From 334a4ee51213c17c3781815faf72bff152594e9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:16:37 +0000 Subject: [PATCH 07/20] Initial plan From 99377d32ddd4bfc571931fe372717ee410fe4f9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:24:45 +0000 Subject: [PATCH 08/20] Add comprehensive logging and error handling to Bing translator Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../util/translator/BingTranslatorUtil.java | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index 1631a402..4f140b68 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -52,12 +52,13 @@ public class BingTranslatorUtil implements Translator { /** * Build Bing Translator API URL - * IG parameter: Uses current timestamp, may need adjustment based on actual API requirements - * IID parameter: Fixed identifier for translator endpoint - * Note: This uses Bing's public translator endpoint which may change over time + * The IG and IID parameters are required by Bing's translator API + * IG: timestamp-based parameter + * IID: translator version identifier */ + String ig = String.valueOf(System.currentTimeMillis() / 1000); String url = "https://www.bing.com/ttranslatev3?isVertical=1" + - "&IG=" + System.currentTimeMillis() + + "&IG=" + ig + "&IID=" + BING_TRANSLATOR_IID; URL obj = new URL(url); @@ -65,20 +66,45 @@ public class BingTranslatorUtil implements Translator { con.setRequestMethod("POST"); con.setConnectTimeout(10000); con.setReadTimeout(10000); - con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); con.setRequestProperty("Referer", "https://www.bing.com/translator"); + con.setRequestProperty("Origin", "https://www.bing.com"); + con.setRequestProperty("Accept", "*/*"); + con.setRequestProperty("Accept-Language", "en-US,en;q=0.9"); con.setDoOutput(true); String postData = "fromLang=" + sourceLanguage + "&to=" + targetLanguage + "&text=" + URLEncoder.encode(word, StandardCharsets.UTF_8); + log.debug("Bing translation request - from: {}, to: {}, text: {}", sourceLanguage, targetLanguage, word); + // Write request body with proper resource management try (java.io.OutputStream os = con.getOutputStream()) { os.write(postData.getBytes(StandardCharsets.UTF_8)); } + // Check response code + int responseCode = con.getResponseCode(); + log.debug("Bing API response code: {}", responseCode); + + if (responseCode != 200) { + // Try to read error stream for more details + StringBuilder errorResponse = new StringBuilder(); + try (BufferedReader errorReader = new BufferedReader( + new InputStreamReader(con.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = errorReader.readLine()) != null) { + errorResponse.append(line); + } + } catch (Exception ex) { + log.warn("Failed to read error stream", ex); + } + log.warn("Bing API error response: {}", errorResponse); + return "Bing翻译接口返回错误状态码: " + responseCode; + } + // Read response with proper resource management StringBuilder response = new StringBuilder(); try (BufferedReader in = new BufferedReader( @@ -89,7 +115,12 @@ public class BingTranslatorUtil implements Translator { } } - return parseResult(response.toString()); + String responseStr = response.toString(); + if (responseStr.isEmpty()) { + log.warn("Bing API returned empty response body despite 200 status code"); + } + + return parseResult(responseStr); } catch (SSLHandshakeException e) { log.error("SSLHandshakeException", e); return "访问Bing翻译接口网络异常:" + e.getMessage(); @@ -156,9 +187,12 @@ public class BingTranslatorUtil implements Translator { private String parseResult(String inputJson) { try { if (StringUtils.isEmpty(inputJson)) { + log.warn("Bing API returned empty response"); return "翻译返回结果为空"; } + log.debug("Bing API response: {}", inputJson); + JSONArray jsonArray = new JSONArray(inputJson); if (jsonArray.size() > 0) { JSONObject firstItem = jsonArray.getJSONObject(0); @@ -173,9 +207,10 @@ public class BingTranslatorUtil implements Translator { } } } + log.warn("Bing API response format unexpected: {}", inputJson); return "解析翻译结果失败,返回格式不符合预期"; } catch (Exception e) { - log.error("解析翻译结果异常", e); + log.error("解析翻译结果异常,原始响应: {}", inputJson, e); return "解析翻译结果异常:" + e.getMessage(); } } From 988669aed42e8658d55fc6ea686cc607971bc43e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:26:22 +0000 Subject: [PATCH 09/20] Simplify Bing translator API call by removing optional parameters Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../moo/tool/util/translator/BingTranslatorUtil.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index 4f140b68..bfa7315f 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -52,14 +52,9 @@ public class BingTranslatorUtil implements Translator { /** * Build Bing Translator API URL - * The IG and IID parameters are required by Bing's translator API - * IG: timestamp-based parameter - * IID: translator version identifier + * Note: Bing's translator API requires specific parameters */ - String ig = String.valueOf(System.currentTimeMillis() / 1000); - String url = "https://www.bing.com/ttranslatev3?isVertical=1" + - "&IG=" + ig + - "&IID=" + BING_TRANSLATOR_IID; + String url = "https://www.bing.com/ttranslatev3"; URL obj = new URL(url); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); From 7ebb668d7bc2bfd3c48e87ec6aae438b253ea9c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:28:14 +0000 Subject: [PATCH 10/20] Add IG parameter generation and basic unit tests for Bing translator Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../util/translator/BingTranslatorUtil.java | 19 +++++-- .../translator/BingTranslatorUtilTest.java | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index bfa7315f..ac911d18 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -51,10 +51,11 @@ public class BingTranslatorUtil implements Translator { targetLanguage = convertToBingLanguageCode(targetLanguage); /** - * Build Bing Translator API URL - * Note: Bing's translator API requires specific parameters + * Build Bing Translator API URL with required parameters + * The IG and IID parameters are required by Bing Translator API */ - String url = "https://www.bing.com/ttranslatev3"; + String url = "https://www.bing.com/ttranslatev3?isVertical=1&IG=" + + generateIG() + "&IID=" + BING_TRANSLATOR_IID; URL obj = new URL(url); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); @@ -128,6 +129,18 @@ public class BingTranslatorUtil implements Translator { } } + /** + * Generate IG parameter for Bing Translator API + * This is a hash-like value that changes over time + * @return IG parameter value + */ + private String generateIG() { + // Simple IG generation based on timestamp + // Format: hexadecimal string based on current time + long timestamp = System.currentTimeMillis(); + return Long.toHexString(timestamp).toUpperCase(); + } + private String convertToBingLanguageCode(String code) { if (StringUtils.isEmpty(code) || "auto".equals(code)) { return "auto-detect"; diff --git a/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java b/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java new file mode 100644 index 00000000..a8f81ffb --- /dev/null +++ b/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java @@ -0,0 +1,51 @@ +package com.luoboduner.moo.tool.util.translator; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Test for BingTranslatorUtil + * + * Note: These tests focus on the parsing logic and parameter handling + * rather than actual API calls which require network access. + */ +public class BingTranslatorUtilTest { + + private BingTranslatorUtil translator = new BingTranslatorUtil(); + + @Test + public void testTranslateEmptyString() { + String result = translator.translate("", "auto", "zh-CN"); + assertEquals("Empty string should return empty result", "", result); + } + + @Test + public void testTranslateNull() { + String result = translator.translate(null, "auto", "zh-CN"); + assertEquals("Null string should return empty result", "", result); + } + + @Test + public void testLanguageCodeConversion() { + // This is a white-box test to verify language code conversion + // We're testing the internal convertToBingLanguageCode method indirectly + + // Create a translator instance to test language conversion through actual translate call + // Note: This will make an actual API call, so it may fail if network is unavailable + // In a real scenario, we would mock the HTTP connection + + // For now, just verify that the translator doesn't crash with various language codes + try { + translator.translate("test", "zh-CN", "en"); + translator.translate("test", "en", "zh-CN"); + translator.translate("test", "auto", "zh-CN"); + // If we get here without exceptions, the language code handling is working + assertTrue("Language code handling should not throw exceptions", true); + } catch (Exception e) { + // Expected if no network - this is acceptable for this test + // We're mainly checking that language code conversion doesn't cause crashes + assertTrue("Exception should be network-related, not code-related", + e.getMessage().contains("网络") || e.getMessage().contains("接口")); + } + } +} From 7a7e5477c01536728d1b314f3621bbfeed24131b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:30:05 +0000 Subject: [PATCH 11/20] Fix potential NPE when reading error stream Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../util/translator/BingTranslatorUtil.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index ac911d18..88d5bb7b 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -88,16 +88,21 @@ public class BingTranslatorUtil implements Translator { if (responseCode != 200) { // Try to read error stream for more details StringBuilder errorResponse = new StringBuilder(); - try (BufferedReader errorReader = new BufferedReader( - new InputStreamReader(con.getErrorStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = errorReader.readLine()) != null) { - errorResponse.append(line); + try { + java.io.InputStream errorStream = con.getErrorStream(); + if (errorStream != null) { + try (BufferedReader errorReader = new BufferedReader( + new InputStreamReader(errorStream, StandardCharsets.UTF_8))) { + String line; + while ((line = errorReader.readLine()) != null) { + errorResponse.append(line); + } + } } } catch (Exception ex) { log.warn("Failed to read error stream", ex); } - log.warn("Bing API error response: {}", errorResponse); + log.warn("Bing API error response (code {}): {}", responseCode, errorResponse); return "Bing翻译接口返回错误状态码: " + responseCode; } From 0599334c40d1149b257e0bffa85ab1645f585de4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:31:46 +0000 Subject: [PATCH 12/20] Improve test assertions and update User-Agent version Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../util/translator/BingTranslatorUtil.java | 2 +- .../translator/BingTranslatorUtilTest.java | 42 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java index 88d5bb7b..b75cacf9 100644 --- a/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java +++ b/src/main/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtil.java @@ -62,7 +62,7 @@ public class BingTranslatorUtil implements Translator { con.setRequestMethod("POST"); con.setConnectTimeout(10000); con.setReadTimeout(10000); - con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); + con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"); con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); con.setRequestProperty("Referer", "https://www.bing.com/translator"); con.setRequestProperty("Origin", "https://www.bing.com"); diff --git a/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java b/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java index a8f81ffb..ebdafe58 100644 --- a/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java +++ b/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java @@ -27,25 +27,31 @@ public class BingTranslatorUtilTest { @Test public void testLanguageCodeConversion() { - // This is a white-box test to verify language code conversion - // We're testing the internal convertToBingLanguageCode method indirectly + // Test that language code conversion doesn't cause crashes + // This is important because the translator needs to convert between + // the app's language codes and Bing's expected format - // Create a translator instance to test language conversion through actual translate call - // Note: This will make an actual API call, so it may fail if network is unavailable - // In a real scenario, we would mock the HTTP connection + // We can't easily test the actual conversion without making it public, + // but we can verify the translator handles various language codes without exceptions - // For now, just verify that the translator doesn't crash with various language codes - try { - translator.translate("test", "zh-CN", "en"); - translator.translate("test", "en", "zh-CN"); - translator.translate("test", "auto", "zh-CN"); - // If we get here without exceptions, the language code handling is working - assertTrue("Language code handling should not throw exceptions", true); - } catch (Exception e) { - // Expected if no network - this is acceptable for this test - // We're mainly checking that language code conversion doesn't cause crashes - assertTrue("Exception should be network-related, not code-related", - e.getMessage().contains("网络") || e.getMessage().contains("接口")); - } + // Note: These calls will attempt to reach the API, so they may fail with network errors + // That's acceptable - we're mainly testing that language code handling doesn't crash + + String result; + + // Test Chinese to English + result = translator.translate("test", "zh-CN", "en"); + assertNotNull("Result should not be null", result); + + // Test English to Chinese + result = translator.translate("test", "en", "zh-CN"); + assertNotNull("Result should not be null", result); + + // Test auto-detect + result = translator.translate("test", "auto", "zh-CN"); + assertNotNull("Result should not be null", result); + + // Even if API fails, we should get an error message, not crash + assertTrue("Result should contain content or error message", result.length() >= 0); } } From 9c5b8ee63d27b9890f4d260cf0f8d3ec1ea5ddc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:33:07 +0000 Subject: [PATCH 13/20] Fix redundant assertion in unit test Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../moo/tool/util/translator/BingTranslatorUtilTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java b/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java index ebdafe58..6b269cea 100644 --- a/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java +++ b/src/test/java/com/luoboduner/moo/tool/util/translator/BingTranslatorUtilTest.java @@ -51,7 +51,7 @@ public class BingTranslatorUtilTest { result = translator.translate("test", "auto", "zh-CN"); assertNotNull("Result should not be null", result); - // Even if API fails, we should get an error message, not crash - assertTrue("Result should contain content or error message", result.length() >= 0); + // Even if API fails, we should get an error message, not crash or return null + assertNotNull("Result should not be null even on API failure", result); } } From 5db88a2075eb3b78c6f9bfb937c1e2ade3dcef4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:42:03 +0000 Subject: [PATCH 14/20] Initial plan From 39ec7293671c002897ead1df343435b198eecbae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:53:50 +0000 Subject: [PATCH 15/20] Add timezone selection with combo box and quick-switch buttons to time conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add JComboBox with 18 common timezone options (UTC, Asia/Shanghai, etc.) - Add quick timezone buttons for one-click switching (UTC, +8, +9, -5, -8, +1, +3) - Dynamically update the "本地时间" label to show the selected timezone offset - Update both toLocalTime() and toTimestamp() to use the selected timezone - Auto-detect system default timezone on startup - Add system timezone to combo box if not in common list Agent-Logs-Url: https://github.com/rememberber/MooTool/sessions/57c56859-79f5-455f-8bb8-c3e77af9d048 Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com> --- .../tool/ui/form/func/TimeConvertForm.java | 197 +++++++++++++++++- .../ui/listener/func/TimeConvertListener.java | 15 +- 2 files changed, 206 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java index 11df4199..b64dcc74 100644 --- a/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java +++ b/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java @@ -25,6 +25,7 @@ import javax.swing.text.StyleContext; import java.awt.*; import java.util.Date; import java.util.Locale; +import java.util.TimeZone; /** *
@@ -54,6 +55,9 @@ public class TimeConvertForm {
private JSplitPane splitPane;
private JScrollPane leftScrollPane;
private JButton clockButton;
+ private JComboBox timezoneComboBox;
+ private JLabel gmtLabel;
+ private JPanel timezoneQuickPanel;
private static final Log logger = LogFactory.get();
@@ -61,6 +65,43 @@ public class TimeConvertForm {
public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+ /**
+ * Common timezone IDs for the combo box
+ */
+ private static final String[] COMMON_TIMEZONE_IDS = {
+ "UTC",
+ "Asia/Shanghai",
+ "Asia/Tokyo",
+ "Asia/Seoul",
+ "Asia/Singapore",
+ "Asia/Hong_Kong",
+ "Asia/Kolkata",
+ "Asia/Dubai",
+ "Europe/London",
+ "Europe/Paris",
+ "Europe/Berlin",
+ "Europe/Moscow",
+ "America/New_York",
+ "America/Chicago",
+ "America/Denver",
+ "America/Los_Angeles",
+ "Australia/Sydney",
+ "Pacific/Auckland"
+ };
+
+ /**
+ * Quick timezone buttons: display label -> timezone ID
+ */
+ private static final String[][] QUICK_TIMEZONE_BUTTONS = {
+ {"UTC", "UTC"},
+ {"+8", "Asia/Shanghai"},
+ {"+9", "Asia/Tokyo"},
+ {"-5", "America/New_York"},
+ {"-8", "America/Los_Angeles"},
+ {"+1", "Europe/Paris"},
+ {"+3", "Europe/Moscow"},
+ };
+
private static TFuncContentMapper funcContentMapper = MybatisUtil.getSqlSession().getMapper(TFuncContentMapper.class);
private TimeConvertForm() {
@@ -74,9 +115,56 @@ public class TimeConvertForm {
return timeConvertForm;
}
+ /**
+ * Get the currently selected TimeZone from the combo box.
+ */
+ public TimeZone getSelectedTimeZone() {
+ if (timezoneComboBox == null || timezoneComboBox.getSelectedItem() == null) {
+ return TimeZone.getDefault();
+ }
+ String selected = (String) timezoneComboBox.getSelectedItem();
+ // Extract timezone ID from display string like "Asia/Shanghai (GMT+08:00)"
+ int parenIndex = selected.indexOf(" (");
+ String tzId = parenIndex > 0 ? selected.substring(0, parenIndex) : selected;
+ return TimeZone.getTimeZone(tzId);
+ }
+
+ /**
+ * Format a timezone ID for display: "Asia/Shanghai (GMT+08:00)"
+ */
+ private static String formatTimezoneDisplay(String tzId) {
+ TimeZone tz = TimeZone.getTimeZone(tzId);
+ int rawOffset = tz.getRawOffset();
+ int hours = rawOffset / 3600000;
+ int minutes = Math.abs((rawOffset % 3600000) / 60000);
+ return String.format("%s (GMT%+03d:%02d)", tzId, hours, minutes);
+ }
+
+ /**
+ * Update the gmtLabel to show the selected timezone.
+ */
+ private void updateGmtLabel() {
+ TimeZone tz = getSelectedTimeZone();
+ int rawOffset = tz.getRawOffset();
+ int hours = rawOffset / 3600000;
+ int minutes = Math.abs((rawOffset % 3600000) / 60000);
+ String offsetStr;
+ if (minutes == 0) {
+ offsetStr = String.format("GMT %+d", hours);
+ } else {
+ offsetStr = String.format("GMT %+d:%02d", hours, minutes);
+ }
+ if (gmtLabel != null) {
+ gmtLabel.setText("时间(" + offsetStr + ")");
+ }
+ }
+
public static void init() {
timeConvertForm = getInstance();
+ // Initialize timezone combo box
+ initTimezoneComponents();
+
ThreadUtil.execute(() -> {
while (true) {
timeConvertForm.getCurrentTimestampLabel().setText(String.valueOf(System.currentTimeMillis() / 1000));
@@ -89,7 +177,8 @@ public class TimeConvertForm {
timeConvertForm.getTimestampTextField().setText(String.valueOf(System.currentTimeMillis()));
}
if ("".equals(timeConvertForm.getGmtTextField().getText())) {
- timeConvertForm.getGmtTextField().setText(DateFormatUtils.format(new Date(), TIME_FORMAT));
+ TimeZone tz = timeConvertForm.getSelectedTimeZone();
+ timeConvertForm.getGmtTextField().setText(DateFormatUtils.format(new Date(), TIME_FORMAT, tz));
}
Style.emphaticIndicatorFont(timeConvertForm.getCurrentGmtLabel());
@@ -120,6 +209,112 @@ public class TimeConvertForm {
TimeConvertListener.addListeners();
}
+ /**
+ * Initialize timezone selection components and add them to the UI.
+ */
+ private static void initTimezoneComponents() {
+ // Create timezone combo box with common timezones
+ timeConvertForm.timezoneComboBox = new JComboBox<>();
+ for (String tzId : COMMON_TIMEZONE_IDS) {
+ timeConvertForm.timezoneComboBox.addItem(formatTimezoneDisplay(tzId));
+ }
+
+ // Set default selection to system default timezone
+ String systemTzId = TimeZone.getDefault().getID();
+ String systemDisplay = formatTimezoneDisplay(systemTzId);
+ boolean found = false;
+ for (int i = 0; i < timeConvertForm.timezoneComboBox.getItemCount(); i++) {
+ if (timeConvertForm.timezoneComboBox.getItemAt(i).equals(systemDisplay)) {
+ timeConvertForm.timezoneComboBox.setSelectedIndex(i);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // Add system timezone if not in the common list
+ timeConvertForm.timezoneComboBox.insertItemAt(systemDisplay, 0);
+ timeConvertForm.timezoneComboBox.setSelectedIndex(0);
+ }
+
+ // Create quick timezone buttons panel
+ timeConvertForm.timezoneQuickPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
+ JLabel tzLabel = new JLabel("时区:");
+ timeConvertForm.timezoneQuickPanel.add(tzLabel);
+ timeConvertForm.timezoneQuickPanel.add(timeConvertForm.timezoneComboBox);
+
+ for (String[] btnDef : QUICK_TIMEZONE_BUTTONS) {
+ JButton btn = new JButton(btnDef[0]);
+ btn.setMargin(new Insets(2, 6, 2, 6));
+ String tzId = btnDef[1];
+ btn.setToolTipText(formatTimezoneDisplay(tzId));
+ btn.addActionListener(e -> {
+ String display = formatTimezoneDisplay(tzId);
+ for (int i = 0; i < timeConvertForm.timezoneComboBox.getItemCount(); i++) {
+ if (timeConvertForm.timezoneComboBox.getItemAt(i).equals(display)) {
+ timeConvertForm.timezoneComboBox.setSelectedIndex(i);
+ return;
+ }
+ }
+ // If not found, add and select
+ timeConvertForm.timezoneComboBox.addItem(display);
+ timeConvertForm.timezoneComboBox.setSelectedItem(display);
+ });
+ timeConvertForm.timezoneQuickPanel.add(btn);
+ }
+
+ // Listen for timezone changes to update label
+ timeConvertForm.timezoneComboBox.addActionListener(e -> {
+ timeConvertForm.updateGmtLabel();
+ });
+
+ // Add timezone panel to panel5 (the conversion panel, parent of gmtTextField)
+ Container gmtParent = timeConvertForm.getGmtTextField().getParent();
+ if (gmtParent instanceof JPanel) {
+ JPanel panel5 = (JPanel) gmtParent;
+ LayoutManager lm = panel5.getLayout();
+ if (lm instanceof GridLayoutManager) {
+ // Store all existing components and their constraints
+ Component[] components = panel5.getComponents();
+ GridLayoutManager glm = (GridLayoutManager) lm;
+ GridConstraints[] storedConstraints = new GridConstraints[components.length];
+ for (int i = 0; i < components.length; i++) {
+ storedConstraints[i] = glm.getConstraintsForComponent(components[i]);
+ }
+
+ // Rebuild panel5 with 4 rows (was 3 rows x 3 cols)
+ panel5.removeAll();
+ panel5.setLayout(new GridLayoutManager(4, 3, new Insets(10, 10, 10, 10), -1, -1));
+
+ // Re-add existing components with their original constraints
+ for (int i = 0; i < components.length; i++) {
+ panel5.add(components[i], storedConstraints[i]);
+ }
+
+ // Add timezone panel at row 3, spanning all 3 columns
+ panel5.add(timeConvertForm.timezoneQuickPanel,
+ new GridConstraints(3, 0, 1, 3,
+ GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null, null, null, 0, false));
+ }
+
+ // Find and store reference to the "本地时间" label for dynamic updates
+ for (Component comp : panel5.getComponents()) {
+ if (comp instanceof JLabel) {
+ JLabel label = (JLabel) comp;
+ if (label.getText() != null && label.getText().contains("本地时间")) {
+ timeConvertForm.gmtLabel = label;
+ break;
+ }
+ }
+ }
+ }
+
+ // Update the label to show current timezone
+ timeConvertForm.updateGmtLabel();
+ }
+
public static int saveContent() {
timeConvertForm = getInstance();
String timeHisText = timeConvertForm.getTimeHisTextArea().getText();
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TimeConvertListener.java b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TimeConvertListener.java
index 3b90f045..69ed4a78 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TimeConvertListener.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TimeConvertListener.java
@@ -10,12 +10,13 @@ import com.luoboduner.moo.tool.util.AlertUtil;
import com.luoboduner.moo.tool.util.ConsoleUtil;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
-import org.apache.commons.lang3.time.DateUtils;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
+import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.TimeZone;
/**
*
@@ -141,7 +142,10 @@ public class TimeConvertListener {
try {
String localTime = timeConvertForm.getGmtTextField().getText();
String unit = (String) timeConvertForm.getUnitComboBox().getSelectedItem();
- Date date = DateUtils.parseDate(localTime, TimeConvertForm.TIME_FORMAT);
+ TimeZone tz = timeConvertForm.getSelectedTimeZone();
+ SimpleDateFormat sdf = new SimpleDateFormat(TimeConvertForm.TIME_FORMAT);
+ sdf.setTimeZone(tz);
+ Date date = sdf.parse(localTime);
long timeStamp = date.getTime();
if ("秒(s)".equals(unit)) {
timeStamp = timeStamp / 1000;
@@ -149,7 +153,7 @@ public class TimeConvertListener {
timeConvertForm.getTimestampTextField().setText(String.valueOf(timeStamp));
timeConvertForm.getTimestampTextField().grabFocus();
- output("本地时间: " + localTime + " --> 时间戳: " + timeStamp);
+ output("时间(" + tz.getID() + "): " + localTime + " --> 时间戳: " + timeStamp);
} catch (Exception ex) {
ex.printStackTrace();
logger.error(ExceptionUtils.getStackTrace(ex));
@@ -170,11 +174,12 @@ public class TimeConvertListener {
if ("秒(s)".equals(unit)) {
timeStamp = timeStamp * 1000;
}
- String localTime = DateFormatUtils.format(new Date(timeStamp), TimeConvertForm.TIME_FORMAT);
+ TimeZone tz = timeConvertForm.getSelectedTimeZone();
+ String localTime = DateFormatUtils.format(new Date(timeStamp), TimeConvertForm.TIME_FORMAT, tz);
timeConvertForm.getGmtTextField().setText(localTime);
timeConvertForm.getGmtTextField().grabFocus();
- output("时间戳: " + timeStamp + " --> 本地时间: " + localTime);
+ output("时间戳: " + timeStamp + " --> 时间(" + tz.getID() + "): " + localTime);
} catch (Exception ex) {
ex.printStackTrace();
logger.error(ExceptionUtils.getStackTrace(ex));
From b87e4c5ef248562419eaa655289796397933263d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Apr 2026 16:56:21 +0000
Subject: [PATCH 16/20] Fix GMT offset format consistency in updateGmtLabel
method
Agent-Logs-Url: https://github.com/rememberber/MooTool/sessions/57c56859-79f5-455f-8bb8-c3e77af9d048
Co-authored-by: rememberber <4653893+rememberber@users.noreply.github.com>
---
.../luoboduner/moo/tool/ui/form/func/TimeConvertForm.java | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java
index b64dcc74..7969ba93 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/form/func/TimeConvertForm.java
@@ -148,12 +148,7 @@ public class TimeConvertForm {
int rawOffset = tz.getRawOffset();
int hours = rawOffset / 3600000;
int minutes = Math.abs((rawOffset % 3600000) / 60000);
- String offsetStr;
- if (minutes == 0) {
- offsetStr = String.format("GMT %+d", hours);
- } else {
- offsetStr = String.format("GMT %+d:%02d", hours, minutes);
- }
+ String offsetStr = String.format("GMT%+03d:%02d", hours, minutes);
if (gmtLabel != null) {
gmtLabel.setText("时间(" + offsetStr + ")");
}
From 3039a1776cb43fa816ce1e17a92dc1572b9ea997 Mon Sep 17 00:00:00 2001
From: Cassian Florin
Date: Wed, 3 Jun 2026 10:00:59 +0800
Subject: [PATCH 17/20] fix: support Apple Silicon macOS package selection
---
.gitignore | 4 +-
pom.xml | 27 ++++++++++-
readme.md | 30 +++++++++++-
.../moo/tool/ui/dialog/UpdateDialog.java | 11 +----
.../moo/tool/util/DownloadLinkSelector.java | 46 +++++++++++++++++++
.../tool/util/DownloadLinkSelectorTest.java | 44 ++++++++++++++++++
6 files changed, 149 insertions(+), 13 deletions(-)
create mode 100644 src/main/java/com/luoboduner/moo/tool/util/DownloadLinkSelector.java
create mode 100644 src/test/java/com/luoboduner/moo/tool/util/DownloadLinkSelectorTest.java
diff --git a/.gitignore b/.gitignore
index e05a6901..b3eea2c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,4 +32,6 @@ build/
.vscode/
-logs/
\ No newline at end of file
+logs/
+.codegraph/
+.cursor/
diff --git a/pom.xml b/pom.xml
index 97d038b1..4f1aef90 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,9 @@
3.0.0
2.13.3
4.16
+ ${project.build.directory}
+ UNIVERSAL
+ ${java.home}
@@ -77,6 +80,25 @@
+
+
+ mac-intel
+
+ ${project.build.directory}/mac-intel
+ X86_64
+ ${env.MACOS_INTEL_JDK}
+
+
+
+ mac-apple-silicon
+
+ ${project.build.directory}/mac-apple-silicon
+ ARM64
+ ${env.MACOS_APPLE_SILICON_JDK}
+
+
+
+
@@ -412,7 +434,10 @@
javapackager
1.7.5
+ ${mac.outputDirectory}
+ ${project.name}
true
+ ${mac.jdkPath}
com.luoboduner.moo.tool.App
true
@@ -478,7 +503,7 @@
mac
true
- UNIVERSAL
+ ${mac.startup}
jdk.crypto.ec,jdk.charsets
diff --git a/readme.md b/readme.md
index 5b8295f7..a2f166ae 100644
--- a/readme.md
+++ b/readme.md
@@ -312,6 +312,32 @@ Windows • Linux • macOS
[iconfont](https://www.iconfont.cn/)
## 开发温馨提示
-最低JDK版本要求:**17**
+最低JDK版本要求:**21**
在你开始开发之前, **请按下图设置IntelliJ IDEA**, 然后 **maven clean**:
-
\ No newline at end of file
+
+
+### macOS打包
+
+默认打包使用当前运行 Maven 的 JDK:
+
+```bash
+mvn clean package -Dmaven.test.skip=true
+```
+
+Intel 芯片包需要使用 x86_64 JDK 21:
+
+```bash
+MACOS_INTEL_JDK=/path/to/jdk-21-x86_64 mvn -Pmac-intel clean package -Dmaven.test.skip=true
+```
+
+Apple Silicon 包需要使用 arm64/aarch64 JDK 21:
+
+```bash
+MACOS_APPLE_SILICON_JDK=/path/to/jdk-21-aarch64 mvn -Pmac-apple-silicon clean package -Dmaven.test.skip=true
+```
+
+对应产物目录:
+
+- 默认包:`target/`
+- Intel 包:`target/mac-intel/`
+- Apple Silicon 包:`target/mac-apple-silicon/`
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/dialog/UpdateDialog.java b/src/main/java/com/luoboduner/moo/tool/ui/dialog/UpdateDialog.java
index fde094b0..b1a35c94 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/dialog/UpdateDialog.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/dialog/UpdateDialog.java
@@ -13,6 +13,7 @@ import com.jayway.jsonpath.JsonPath;
import com.luoboduner.moo.tool.App;
import com.luoboduner.moo.tool.ui.UiConsts;
import com.luoboduner.moo.tool.util.ComponentUtil;
+import com.luoboduner.moo.tool.util.DownloadLinkSelector;
import com.luoboduner.moo.tool.util.SystemUtil;
import org.apache.commons.lang3.StringUtils;
@@ -102,15 +103,7 @@ public class UpdateDialog extends JDialog {
return;
} else {
DocumentContext parse = JsonPath.parse(downloadLinkInfo);
- if (SystemUtil.isWindowsOs()) {
- fileUrl = parse.read("$.windows");
- } else if (SystemUtil.isMacOs()) {
- fileUrl = parse.read("$.mac");
- } else if (SystemUtil.isMacSilicon()) {
- fileUrl = parse.read("$.macSilicon");
- } else if (SystemUtil.isLinuxOs()) {
- fileUrl = parse.read("$.linux");
- }
+ fileUrl = DownloadLinkSelector.select(parse);
}
String fileName = FileUtil.getName(fileUrl);
diff --git a/src/main/java/com/luoboduner/moo/tool/util/DownloadLinkSelector.java b/src/main/java/com/luoboduner/moo/tool/util/DownloadLinkSelector.java
new file mode 100644
index 00000000..fc32d12f
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/util/DownloadLinkSelector.java
@@ -0,0 +1,46 @@
+package com.luoboduner.moo.tool.util;
+
+import com.jayway.jsonpath.DocumentContext;
+import com.jayway.jsonpath.PathNotFoundException;
+import org.apache.commons.lang3.StringUtils;
+
+public class DownloadLinkSelector {
+
+ public static String select(DocumentContext links) {
+ return select(System.getProperty("os.name"), System.getProperty("os.arch"), links);
+ }
+
+ static String select(String osName, String osArch, DocumentContext links) {
+ if (contains(osName, "Windows")) {
+ return links.read("$.windows");
+ }
+
+ if (contains(osName, "Mac")) {
+ if ("aarch64".equals(osArch)) {
+ String appleSiliconLink = readOptional(links, "$.macSilicon");
+ if (StringUtils.isNotEmpty(appleSiliconLink)) {
+ return appleSiliconLink;
+ }
+ }
+ return links.read("$.mac");
+ }
+
+ if (contains(osName, "Linux")) {
+ return links.read("$.linux");
+ }
+
+ return "";
+ }
+
+ private static boolean contains(String value, String searchText) {
+ return value != null && value.contains(searchText);
+ }
+
+ private static String readOptional(DocumentContext links, String path) {
+ try {
+ return links.read(path);
+ } catch (PathNotFoundException e) {
+ return "";
+ }
+ }
+}
diff --git a/src/test/java/com/luoboduner/moo/tool/util/DownloadLinkSelectorTest.java b/src/test/java/com/luoboduner/moo/tool/util/DownloadLinkSelectorTest.java
new file mode 100644
index 00000000..e68ce144
--- /dev/null
+++ b/src/test/java/com/luoboduner/moo/tool/util/DownloadLinkSelectorTest.java
@@ -0,0 +1,44 @@
+package com.luoboduner.moo.tool.util;
+
+import com.jayway.jsonpath.DocumentContext;
+import com.jayway.jsonpath.JsonPath;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DownloadLinkSelectorTest {
+
+ @Test
+ public void selectsAppleSiliconDownloadBeforeGenericMacDownload() {
+ DocumentContext links = JsonPath.parse("{"
+ + "\"mac\":\"https://example.com/MooTool.dmg\","
+ + "\"macSilicon\":\"https://example.com/MooTool-AppleSilicon.dmg\""
+ + "}");
+
+ String selected = DownloadLinkSelector.select("Mac OS X", "aarch64", links);
+
+ Assert.assertEquals("https://example.com/MooTool-AppleSilicon.dmg", selected);
+ }
+
+ @Test
+ public void fallsBackToGenericMacDownloadWhenAppleSiliconDownloadIsMissing() {
+ DocumentContext links = JsonPath.parse("{"
+ + "\"mac\":\"https://example.com/MooTool.dmg\""
+ + "}");
+
+ String selected = DownloadLinkSelector.select("Mac OS X", "aarch64", links);
+
+ Assert.assertEquals("https://example.com/MooTool.dmg", selected);
+ }
+
+ @Test
+ public void selectsGenericMacDownloadForIntelMac() {
+ DocumentContext links = JsonPath.parse("{"
+ + "\"mac\":\"https://example.com/MooTool.dmg\","
+ + "\"macSilicon\":\"https://example.com/MooTool-AppleSilicon.dmg\""
+ + "}");
+
+ String selected = DownloadLinkSelector.select("Mac OS X", "x86_64", links);
+
+ Assert.assertEquals("https://example.com/MooTool.dmg", selected);
+ }
+}
From 9296376c91cb223dd05b87ebb1202422e9028fc4 Mon Sep 17 00:00:00 2001
From: Cassian Florin
Date: Wed, 3 Jun 2026 10:31:35 +0800
Subject: [PATCH 18/20] =?UTF-8?q?fix(tray):=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?=
=?UTF-8?q?=E5=B1=8F=E6=89=98=E7=9B=98=E8=8F=9C=E5=8D=95=E5=AE=9A=E4=BD=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 使用屏幕绝对坐标计算托盘右键菜单位置
- 根据触发点所在显示器边界限制菜单范围
- 补充多屏和边界定位单元测试
---
.../ui/component/JPopupMenuMouseAdapter.java | 30 ++++++++++++-
.../component/JPopupMenuMouseAdapterTest.java | 42 +++++++++++++++++++
2 files changed, 70 insertions(+), 2 deletions(-)
create mode 100644 src/test/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapterTest.java
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapter.java b/src/main/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapter.java
index 4e5c0329..d8dc5303 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapter.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapter.java
@@ -53,12 +53,38 @@ public class JPopupMenuMouseAdapter extends MouseAdapter {
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
Dimension size = popupMenu.getPreferredSize();
- popupMenu.setLocation(e.getX() - size.width, e.getY() - size.height);
+ Point mousePoint = new Point(e.getXOnScreen(), e.getYOnScreen());
+ Point popupLocation = calculatePopupLocation(mousePoint, size, getScreenBounds(mousePoint));
+ popupMenu.setLocation(popupLocation);
popupMenu.setInvoker(popupMenu);
popupMenu.setVisible(true);
}
}
+ static Point calculatePopupLocation(Point mousePoint, Dimension popupSize, Rectangle screenBounds) {
+ int x = mousePoint.x - popupSize.width;
+ int y = mousePoint.y - popupSize.height;
+
+ int maxX = Math.max(screenBounds.x, screenBounds.x + screenBounds.width - popupSize.width);
+ int maxY = Math.max(screenBounds.y, screenBounds.y + screenBounds.height - popupSize.height);
+
+ x = Math.max(screenBounds.x, Math.min(x, maxX));
+ y = Math.max(screenBounds.y, Math.min(y, maxY));
+
+ return new Point(x, y);
+ }
+
+ private static Rectangle getScreenBounds(Point point) {
+ GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ for (GraphicsDevice screenDevice : graphicsEnvironment.getScreenDevices()) {
+ Rectangle bounds = screenDevice.getDefaultConfiguration().getBounds();
+ if (bounds.contains(point)) {
+ return bounds;
+ }
+ }
+ return graphicsEnvironment.getDefaultScreenDevice().getDefaultConfiguration().getBounds();
+ }
+
public static void showMainFrame() {
App.mainFrame.setVisible(true);
if (App.mainFrame.getExtendedState() == Frame.ICONIFIED) {
@@ -68,4 +94,4 @@ public class JPopupMenuMouseAdapter extends MouseAdapter {
}
App.mainFrame.requestFocus();
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapterTest.java b/src/test/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapterTest.java
new file mode 100644
index 00000000..d31fedb1
--- /dev/null
+++ b/src/test/java/com/luoboduner/moo/tool/ui/component/JPopupMenuMouseAdapterTest.java
@@ -0,0 +1,42 @@
+package com.luoboduner.moo.tool.ui.component;
+
+import org.junit.Test;
+
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Rectangle;
+
+import static org.junit.Assert.assertEquals;
+
+public class JPopupMenuMouseAdapterTest {
+
+ @Test
+ public void calculatePopupLocationUsesAbsoluteCoordinatesOnSecondaryScreen() {
+ Point location = JPopupMenuMouseAdapter.calculatePopupLocation(
+ new Point(2500, 1040),
+ new Dimension(180, 120),
+ new Rectangle(1920, 0, 1920, 1080));
+
+ assertEquals(new Point(2320, 920), location);
+ }
+
+ @Test
+ public void calculatePopupLocationKeepsPopupInScreenWithNegativeOrigin() {
+ Point location = JPopupMenuMouseAdapter.calculatePopupLocation(
+ new Point(-10, 1040),
+ new Dimension(180, 120),
+ new Rectangle(-1920, 0, 1920, 1080));
+
+ assertEquals(new Point(-190, 920), location);
+ }
+
+ @Test
+ public void calculatePopupLocationClampsToCurrentScreenBounds() {
+ Point location = JPopupMenuMouseAdapter.calculatePopupLocation(
+ new Point(1930, 10),
+ new Dimension(180, 120),
+ new Rectangle(1920, 0, 1920, 1080));
+
+ assertEquals(new Point(1920, 0), location);
+ }
+}
From a87216510f8052696e0e93b944a28a94834d6527 Mon Sep 17 00:00:00 2001
From: Cassian Florin
Date: Wed, 3 Jun 2026 10:51:53 +0800
Subject: [PATCH 19/20] =?UTF-8?q?fix(undo):=20=E4=BF=AE=E5=A4=8D=20Ctrl+Z?=
=?UTF-8?q?=20=E6=92=A4=E9=94=80=E5=BC=82=E5=B8=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 撤销和重做后消费快捷键事件
- 支持直接注册 JTextComponent 撤销监听
- 补充 Ctrl+Z 撤销回归测试
---
.../luoboduner/moo/tool/util/UndoUtil.java | 57 ++++++++++---------
.../moo/tool/util/UndoUtilTest.java | 51 +++++++++++++++++
2 files changed, 82 insertions(+), 26 deletions(-)
create mode 100644 src/test/java/com/luoboduner/moo/tool/util/UndoUtilTest.java
diff --git a/src/main/java/com/luoboduner/moo/tool/util/UndoUtil.java b/src/main/java/com/luoboduner/moo/tool/util/UndoUtil.java
index 9066c76c..5882341a 100644
--- a/src/main/java/com/luoboduner/moo/tool/util/UndoUtil.java
+++ b/src/main/java/com/luoboduner/moo/tool/util/UndoUtil.java
@@ -6,7 +6,7 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import javax.swing.text.JTextComponent;
import javax.swing.undo.UndoManager;
import java.awt.event.KeyEvent;
-import java.awt.event.KeyListener;
+import java.awt.event.KeyAdapter;
import java.lang.reflect.Field;
/**
@@ -26,6 +26,10 @@ public class UndoUtil {
* @param object
*/
public static void register(Object object) {
+ if (object instanceof JTextComponent) {
+ registerTextComponent((JTextComponent) object);
+ return;
+ }
Class strClass = object.getClass();
Field[] declaredFields = strClass.getDeclaredFields();
for (Field field : declaredFields) {
@@ -33,37 +37,38 @@ public class UndoUtil {
if (RSyntaxTextArea.class.isAssignableFrom(field.getType())) {
continue;
}
- UndoManager undoManager = new UndoManager();
try {
field.setAccessible(true);
- ((JTextComponent) field.get(object)).getDocument().addUndoableEditListener(undoManager);
- ((JTextComponent) field.get(object)).addKeyListener(new KeyListener() {
- @Override
- public void keyReleased(KeyEvent arg0) {
- }
-
- @Override
- public void keyPressed(KeyEvent evt) {
- if ((evt.isControlDown() || evt.isMetaDown()) && evt.getKeyCode() == KeyEvent.VK_Z) {
- if (undoManager.canUndo()) {
- undoManager.undo();
- }
- }
- if ((evt.isControlDown() || evt.isMetaDown()) && evt.getKeyCode() == KeyEvent.VK_Y) {
- if (undoManager.canRedo()) {
- undoManager.redo();
- }
- }
- }
-
- @Override
- public void keyTyped(KeyEvent arg0) {
- }
- });
+ registerTextComponent((JTextComponent) field.get(object));
} catch (IllegalAccessException e) {
log.error(e.toString());
}
}
}
}
+
+ private static void registerTextComponent(JTextComponent textComponent) {
+ if (textComponent == null) {
+ return;
+ }
+ UndoManager undoManager = new UndoManager();
+ textComponent.getDocument().addUndoableEditListener(undoManager);
+ textComponent.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent evt) {
+ if ((evt.isControlDown() || evt.isMetaDown()) && evt.getKeyCode() == KeyEvent.VK_Z) {
+ if (undoManager.canUndo()) {
+ undoManager.undo();
+ }
+ evt.consume();
+ }
+ if ((evt.isControlDown() || evt.isMetaDown()) && evt.getKeyCode() == KeyEvent.VK_Y) {
+ if (undoManager.canRedo()) {
+ undoManager.redo();
+ }
+ evt.consume();
+ }
+ }
+ });
+ }
}
diff --git a/src/test/java/com/luoboduner/moo/tool/util/UndoUtilTest.java b/src/test/java/com/luoboduner/moo/tool/util/UndoUtilTest.java
new file mode 100644
index 00000000..f567bf01
--- /dev/null
+++ b/src/test/java/com/luoboduner/moo/tool/util/UndoUtilTest.java
@@ -0,0 +1,51 @@
+package com.luoboduner.moo.tool.util;
+
+import org.junit.Test;
+
+import javax.swing.JTextArea;
+import java.awt.event.KeyEvent;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class UndoUtilTest {
+
+ @Test
+ public void ctrlZUndoConsumesEvent() {
+ JTextArea textArea = new JTextArea();
+ TextAreaHolder holder = new TextAreaHolder(textArea);
+ UndoUtil.register(holder);
+ textArea.setText("before");
+ textArea.append(" after");
+
+ KeyEvent ctrlZ = new KeyEvent(textArea, KeyEvent.KEY_PRESSED, System.currentTimeMillis(),
+ KeyEvent.CTRL_DOWN_MASK, KeyEvent.VK_Z, KeyEvent.CHAR_UNDEFINED);
+ textArea.getKeyListeners()[0].keyPressed(ctrlZ);
+
+ assertEquals("before", textArea.getText());
+ assertTrue(ctrlZ.isConsumed());
+ }
+
+ @Test
+ public void directTextComponentCanBeRegistered() {
+ JTextArea textArea = new JTextArea();
+ UndoUtil.register(textArea);
+ textArea.setText("before");
+ textArea.append(" after");
+
+ KeyEvent ctrlZ = new KeyEvent(textArea, KeyEvent.KEY_PRESSED, System.currentTimeMillis(),
+ KeyEvent.CTRL_DOWN_MASK, KeyEvent.VK_Z, KeyEvent.CHAR_UNDEFINED);
+ textArea.getKeyListeners()[0].keyPressed(ctrlZ);
+
+ assertEquals("before", textArea.getText());
+ assertTrue(ctrlZ.isConsumed());
+ }
+
+ private static class TextAreaHolder {
+ private final JTextArea textArea;
+
+ private TextAreaHolder(JTextArea textArea) {
+ this.textArea = textArea;
+ }
+ }
+}
From 5599bb6f57f1da12ff4d4ee4465c418bac4948ee Mon Sep 17 00:00:00 2001
From: Cassian Florin
Date: Wed, 3 Jun 2026 11:18:44 +0800
Subject: [PATCH 20/20] =?UTF-8?q?feat(cron):=20=E6=94=AF=E6=8C=81Linux=20C?=
=?UTF-8?q?ron=E8=A1=A8=E8=BE=BE=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增CronExpressionUtil统一识别Linux 5段和Quartz 6/7段表达式
- 调整Cron面板和收藏弹窗复用统一解析逻辑
- 补充Linux Cron和Quartz兼容性单元测试
---
.../tool/ui/dialog/FavoriteCronDialog.java | 11 +--
.../tool/ui/listener/func/CronListener.java | 36 ++-------
.../moo/tool/util/CronExpressionUtil.java | 76 +++++++++++++++++++
.../moo/tool/util/CronExpressionUtilTest.java | 43 +++++++++++
4 files changed, 128 insertions(+), 38 deletions(-)
create mode 100644 src/main/java/com/luoboduner/moo/tool/util/CronExpressionUtil.java
create mode 100644 src/test/java/com/luoboduner/moo/tool/util/CronExpressionUtilTest.java
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/dialog/FavoriteCronDialog.java b/src/main/java/com/luoboduner/moo/tool/ui/dialog/FavoriteCronDialog.java
index 42b89278..c959ce16 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/dialog/FavoriteCronDialog.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/dialog/FavoriteCronDialog.java
@@ -1,10 +1,5 @@
package com.luoboduner.moo.tool.ui.dialog;
-import com.cronutils.descriptor.CronDescriptor;
-import com.cronutils.model.CronType;
-import com.cronutils.model.definition.CronDefinition;
-import com.cronutils.model.definition.CronDefinitionBuilder;
-import com.cronutils.parser.CronParser;
import com.formdev.flatlaf.util.SystemInfo;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
@@ -18,6 +13,7 @@ import com.luoboduner.moo.tool.ui.form.MainWindow;
import com.luoboduner.moo.tool.ui.form.func.CronForm;
import com.luoboduner.moo.tool.ui.form.func.FavoriteCronForm;
import com.luoboduner.moo.tool.util.ComponentUtil;
+import com.luoboduner.moo.tool.util.CronExpressionUtil;
import com.luoboduner.moo.tool.util.MybatisUtil;
import com.luoboduner.moo.tool.util.SqliteUtil;
import com.luoboduner.moo.tool.util.SystemUtil;
@@ -157,10 +153,7 @@ public class FavoriteCronDialog extends JDialog {
default -> {
}
}
- CronDescriptor descriptor = CronDescriptor.instance(selectedLocale);
- CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ);
- CronParser parser = new CronParser(cronDefinition);
- String description = descriptor.describe(parser.parse(cron));
+ String description = CronExpressionUtil.describe(cron, selectedLocale);
nameTextField.setText(description);
} catch (Exception e) {
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/CronListener.java b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/CronListener.java
index 0c778f11..a95398ce 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/CronListener.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/CronListener.java
@@ -3,28 +3,20 @@ package com.luoboduner.moo.tool.ui.listener.func;
import cn.hutool.core.date.DateUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
-import com.cronutils.descriptor.CronDescriptor;
-import com.cronutils.model.CronType;
-import com.cronutils.model.definition.CronDefinition;
-import com.cronutils.model.definition.CronDefinitionBuilder;
-import com.cronutils.model.time.ExecutionTime;
-import com.cronutils.parser.CronParser;
-import com.google.common.collect.Lists;
import com.luoboduner.moo.tool.App;
import com.luoboduner.moo.tool.ui.dialog.CommonCronDialog;
import com.luoboduner.moo.tool.ui.dialog.FavoriteCronDialog;
import com.luoboduner.moo.tool.ui.form.func.CronForm;
import com.luoboduner.moo.tool.ui.frame.FavoriteCronFrame;
+import com.luoboduner.moo.tool.util.CronExpressionUtil;
import org.apache.commons.lang3.exception.ExceptionUtils;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.event.ActionListener;
-import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Locale;
-import java.util.Optional;
/**
*
@@ -54,10 +46,7 @@ public class CronListener {
default -> {
}
}
- CronDescriptor descriptor = CronDescriptor.instance(selectedLocale);
- CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ);
- CronParser parser = new CronParser(cronDefinition);
- String description = descriptor.describe(parser.parse(cronExpression));
+ String description = CronExpressionUtil.describe(cronExpression, selectedLocale);
cronForm.getHumanReadableTextField().setText(description);
} catch (Exception ex) {
@@ -102,23 +91,12 @@ public class CronListener {
try {
String cronExpression = cronForm.getCronExpressionTextField().getText();
- CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ);
- CronParser parser = new CronParser(cronDefinition);
-
// 获取未来10次运行时间:
- List nextExecutionTimes = Lists.newArrayList();
- ExecutionTime executionTime = ExecutionTime.forCron(parser.parse(cronExpression));
- ZonedDateTime now = ZonedDateTime.now();
- Optional nextExecution = executionTime.nextExecution(now);
- for (int i = 0; i < 10; i++) {
- if (nextExecution.isPresent()) {
- // yyyy-MM-dd HH:mm:ss
- LocalDateTime localDateTime = nextExecution.get().toLocalDateTime();
- String format = DateUtil.format(localDateTime, "yyyy-MM-dd HH:mm:ss");
- nextExecutionTimes.add(format);
- nextExecution = executionTime.nextExecution(nextExecution.get());
- }
- }
+ List nextExecutionTimes = CronExpressionUtil.nextExecutions(cronExpression, ZonedDateTime.now(), 10)
+ .stream()
+ .map(ZonedDateTime::toLocalDateTime)
+ .map(localDateTime -> DateUtil.format(localDateTime, "yyyy-MM-dd HH:mm:ss"))
+ .toList();
cronForm.getNextExecutionTimeTextArea().setText("最近10次运行时间:\n" + String.join("\n", nextExecutionTimes));
} catch (Exception ex) {
cronForm.getNextExecutionTimeTextArea().setText("最近10次运行时间:\n" + ex.getMessage());
diff --git a/src/main/java/com/luoboduner/moo/tool/util/CronExpressionUtil.java b/src/main/java/com/luoboduner/moo/tool/util/CronExpressionUtil.java
new file mode 100644
index 00000000..f90f1643
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/util/CronExpressionUtil.java
@@ -0,0 +1,76 @@
+package com.luoboduner.moo.tool.util;
+
+import com.cronutils.descriptor.CronDescriptor;
+import com.cronutils.model.Cron;
+import com.cronutils.model.CronType;
+import com.cronutils.model.definition.CronDefinition;
+import com.cronutils.model.definition.CronDefinitionBuilder;
+import com.cronutils.model.time.ExecutionTime;
+import com.cronutils.parser.CronParser;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+public class CronExpressionUtil {
+
+ private CronExpressionUtil() {
+ }
+
+ public static String describe(String cronExpression, Locale locale) {
+ CronDescriptor descriptor = CronDescriptor.instance(locale);
+ return descriptor.describe(parse(cronExpression));
+ }
+
+ public static List nextExecutions(String cronExpression, ZonedDateTime startTime, int count) {
+ List nextExecutionTimes = new ArrayList<>();
+ if (count <= 0) {
+ return nextExecutionTimes;
+ }
+
+ ExecutionTime executionTime = ExecutionTime.forCron(parse(cronExpression));
+ Optional nextExecution = executionTime.nextExecution(startTime);
+ for (int i = 0; i < count && nextExecution.isPresent(); i++) {
+ ZonedDateTime execution = nextExecution.get();
+ nextExecutionTimes.add(execution);
+ nextExecution = executionTime.nextExecution(execution);
+ }
+ return nextExecutionTimes;
+ }
+
+ public static Cron parse(String cronExpression) {
+ String normalizedCronExpression = normalize(cronExpression);
+ CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(resolveCronType(normalizedCronExpression));
+ CronParser parser = new CronParser(cronDefinition);
+ return parser.parse(normalizedCronExpression);
+ }
+
+ private static CronType resolveCronType(String cronExpression) {
+ if (cronExpression.startsWith("@")) {
+ return CronType.UNIX;
+ }
+
+ int fieldCount = cronExpression.split("\\s+").length;
+ if (fieldCount == 5) {
+ return CronType.UNIX;
+ }
+ if (fieldCount == 6 || fieldCount == 7) {
+ return CronType.QUARTZ;
+ }
+ throw new IllegalArgumentException("Cron表达式格式错误,仅支持Linux 5段或Quartz 6/7段表达式");
+ }
+
+ private static String normalize(String cronExpression) {
+ if (cronExpression == null) {
+ throw new IllegalArgumentException("Cron表达式不能为空");
+ }
+
+ String normalizedCronExpression = cronExpression.trim();
+ if (normalizedCronExpression.isEmpty()) {
+ throw new IllegalArgumentException("Cron表达式不能为空");
+ }
+ return normalizedCronExpression;
+ }
+}
diff --git a/src/test/java/com/luoboduner/moo/tool/util/CronExpressionUtilTest.java b/src/test/java/com/luoboduner/moo/tool/util/CronExpressionUtilTest.java
new file mode 100644
index 00000000..12e2be8c
--- /dev/null
+++ b/src/test/java/com/luoboduner/moo/tool/util/CronExpressionUtilTest.java
@@ -0,0 +1,43 @@
+package com.luoboduner.moo.tool.util;
+
+import org.junit.Test;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Locale;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class CronExpressionUtilTest {
+
+ private static final ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");
+
+ @Test
+ public void shouldCalculateNextExecutionsForUnixCron() {
+ ZonedDateTime now = ZonedDateTime.of(2026, 6, 3, 10, 1, 30, 0, ZONE_ID);
+
+ List nextExecutionTimes = CronExpressionUtil.nextExecutions("*/5 * * * *", now, 3);
+
+ assertEquals(ZonedDateTime.of(2026, 6, 3, 10, 5, 0, 0, ZONE_ID), nextExecutionTimes.get(0));
+ assertEquals(ZonedDateTime.of(2026, 6, 3, 10, 10, 0, 0, ZONE_ID), nextExecutionTimes.get(1));
+ assertEquals(ZonedDateTime.of(2026, 6, 3, 10, 15, 0, 0, ZONE_ID), nextExecutionTimes.get(2));
+ }
+
+ @Test
+ public void shouldKeepQuartzCronCompatibility() {
+ ZonedDateTime now = ZonedDateTime.of(2026, 6, 3, 11, 59, 0, 0, ZONE_ID);
+
+ List nextExecutionTimes = CronExpressionUtil.nextExecutions("0 0 12 * * ?", now, 1);
+
+ assertEquals(ZonedDateTime.of(2026, 6, 3, 12, 0, 0, 0, ZONE_ID), nextExecutionTimes.get(0));
+ }
+
+ @Test
+ public void shouldDescribeUnixCron() {
+ String description = CronExpressionUtil.describe("*/5 * * * *", Locale.ENGLISH);
+
+ assertFalse(description.isBlank());
+ }
+}