mirror of
https://github.com/rememberber/MooTool.git
synced 2026-08-30 18:02:12 +08:00
Merge branch 'github-master' into github-develop
This commit is contained in:
+3
-1
@@ -32,4 +32,6 @@ build/
|
||||
.vscode/
|
||||
|
||||
|
||||
logs/
|
||||
logs/
|
||||
.codegraph/
|
||||
.cursor/
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
<tika.version>3.0.0</tika.version>
|
||||
<jackson-databind.version>2.13.3</jackson-databind.version>
|
||||
<java-diff.version>4.16</java-diff.version>
|
||||
<mac.outputDirectory>${project.build.directory}</mac.outputDirectory>
|
||||
<mac.startup>UNIVERSAL</mac.startup>
|
||||
<mac.jdkPath>${java.home}</mac.jdkPath>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
@@ -77,6 +80,25 @@
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>mac-intel</id>
|
||||
<properties>
|
||||
<mac.outputDirectory>${project.build.directory}/mac-intel</mac.outputDirectory>
|
||||
<mac.startup>X86_64</mac.startup>
|
||||
<mac.jdkPath>${env.MACOS_INTEL_JDK}</mac.jdkPath>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>mac-apple-silicon</id>
|
||||
<properties>
|
||||
<mac.outputDirectory>${project.build.directory}/mac-apple-silicon</mac.outputDirectory>
|
||||
<mac.startup>ARM64</mac.startup>
|
||||
<mac.jdkPath>${env.MACOS_APPLE_SILICON_JDK}</mac.jdkPath>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 行级 diff(Myers)+ 生成 unified diff -->
|
||||
@@ -412,7 +434,10 @@
|
||||
<artifactId>javapackager</artifactId>
|
||||
<version>1.7.5</version>
|
||||
<configuration>
|
||||
<outputDirectory>${mac.outputDirectory}</outputDirectory>
|
||||
<displayName>${project.name}</displayName>
|
||||
<bundleJre>true</bundleJre>
|
||||
<jdkPath>${mac.jdkPath}</jdkPath>
|
||||
<mainClass>com.luoboduner.moo.tool.App</mainClass>
|
||||
<generateInstaller>true</generateInstaller>
|
||||
<!-- 这行不能被格式化为多行,否则会出错-->
|
||||
@@ -478,7 +503,7 @@
|
||||
<platform>mac</platform>
|
||||
<createTarball>true</createTarball>
|
||||
<macConfig>
|
||||
<macStartup>UNIVERSAL</macStartup>
|
||||
<macStartup>${mac.startup}</macStartup>
|
||||
<!-- <developerId>rememberber@163.com</developerId>-->
|
||||
</macConfig>
|
||||
<additionalModules>jdk.crypto.ec,jdk.charsets</additionalModules>
|
||||
|
||||
@@ -315,6 +315,32 @@ Windows • Linux • macOS
|
||||
[iconfont](https://www.iconfont.cn/)
|
||||
|
||||
## 开发温馨提示
|
||||
最低JDK版本要求:**17**
|
||||
最低JDK版本要求:**21**
|
||||
在你开始开发之前, **请按下图设置IntelliJ IDEA**, 然后 **maven clean**:
|
||||

|
||||

|
||||
|
||||
### 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/`
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,10 +4,12 @@ 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;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
@@ -28,9 +30,15 @@ public class TranslationLayoutForm {
|
||||
private JComboBox comboBox1;
|
||||
private JButton exchangeButton;
|
||||
private JComboBox comboBox2;
|
||||
private JComboBox translatorComboBox;
|
||||
|
||||
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"));
|
||||
@@ -49,10 +57,30 @@ public class TranslationLayoutForm {
|
||||
defaultComboBoxModel2.addElement("英语");
|
||||
comboBox2.setModel(defaultComboBoxModel2);
|
||||
|
||||
translatorComboBox = new JComboBox();
|
||||
final DefaultComboBoxModel translatorComboBoxModel = new DefaultComboBoxModel();
|
||||
translatorComboBoxModel.addElement(TRANSLATOR_GOOGLE);
|
||||
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)) {
|
||||
translatorComboBox.setSelectedItem(TRANSLATOR_MICROSOFT);
|
||||
} else if ("BING".equals(savedTranslator)) {
|
||||
translatorComboBox.setSelectedItem(TRANSLATOR_BING);
|
||||
} else {
|
||||
translatorComboBox.setSelectedItem(TRANSLATOR_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 +144,25 @@ public class TranslationLayoutForm {
|
||||
}
|
||||
});
|
||||
|
||||
translatorComboBox.addItemListener(e -> {
|
||||
if (e.getStateChange() == ItemEvent.SELECTED) {
|
||||
String itemName = e.getItem().toString();
|
||||
String translatorType;
|
||||
if (TRANSLATOR_GOOGLE.equals(itemName)) {
|
||||
translatorType = "GOOGLE";
|
||||
} else if (TRANSLATOR_BING.equals(itemName)) {
|
||||
translatorType = "BING";
|
||||
} else {
|
||||
translatorType = "MICROSOFT";
|
||||
}
|
||||
ConfigUtil.getInstance().setTranslatorType(translatorType);
|
||||
// Only translate if there's actual text to translate
|
||||
if (!StringUtils.isEmpty(textArea1.getText())) {
|
||||
translateControl();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
textArea1.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
@@ -155,8 +202,34 @@ public class TranslationLayoutForm {
|
||||
String targetLanguage = comboBox2.getSelectedItem().toString();
|
||||
String text = textArea1.getText();
|
||||
|
||||
TranslatorFactory translatorFactory = new TranslatorFactory();
|
||||
String result = translatorFactory.getTranslator(TranslatorFactory.TranslatorType.GOOGLE).translate(text, Translator.languageNameToCodeMap.get(sourceLanguage), Translator.languageNameToCodeMap.get(targetLanguage));
|
||||
// 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;
|
||||
try {
|
||||
translatorType = TranslatorFactory.TranslatorType.valueOf(translatorTypeStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Default to GOOGLE if invalid
|
||||
translatorType = TranslatorFactory.TranslatorType.GOOGLE;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import javax.swing.text.StyleContext;
|
||||
import java.awt.*;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
@@ -54,6 +55,9 @@ public class TimeConvertForm {
|
||||
private JSplitPane splitPane;
|
||||
private JScrollPane leftScrollPane;
|
||||
private JButton clockButton;
|
||||
private JComboBox<String> 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,51 @@ 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 = String.format("GMT%+03d:%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 +172,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 +204,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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
@@ -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<String> nextExecutionTimes = Lists.newArrayList();
|
||||
ExecutionTime executionTime = ExecutionTime.forCron(parser.parse(cronExpression));
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
Optional<ZonedDateTime> 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<String> 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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ZonedDateTime> nextExecutions(String cronExpression, ZonedDateTime startTime, int count) {
|
||||
List<ZonedDateTime> nextExecutionTimes = new ArrayList<>();
|
||||
if (count <= 0) {
|
||||
return nextExecutionTimes;
|
||||
}
|
||||
|
||||
ExecutionTime executionTime = ExecutionTime.forCron(parse(cronExpression));
|
||||
Optional<ZonedDateTime> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
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.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Bing翻译工具类 (使用Bing翻译API,在中国大陆可访问)
|
||||
*/
|
||||
@Slf4j
|
||||
public class BingTranslatorUtil implements Translator {
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* 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?isVertical=1&IG=" +
|
||||
generateIG() + "&IID=" + BING_TRANSLATOR_IID;
|
||||
|
||||
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 (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");
|
||||
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 {
|
||||
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 (code {}): {}", responseCode, errorResponse);
|
||||
return "Bing翻译接口返回错误状态码: " + responseCode;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} catch (SocketTimeoutException e) {
|
||||
log.error("SocketTimeoutException", e);
|
||||
return "访问Bing翻译接口超时:" + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
log.error("访问Bing翻译异常", e);
|
||||
return "访问Bing翻译接口异常:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
// 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 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);
|
||||
if (firstItem.containsKey("translations")) {
|
||||
JSONArray translations = firstItem.getJSONArray("translations");
|
||||
if (translations.size() > 0) {
|
||||
JSONObject translation = translations.getJSONObject(0);
|
||||
String result = translation.getStr("text");
|
||||
if (!StringUtils.isEmpty(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("Bing API response format unexpected: {}", inputJson);
|
||||
return "解析翻译结果失败,返回格式不符合预期";
|
||||
} catch (Exception e) {
|
||||
log.error("解析翻译结果异常,原始响应: {}", inputJson, e);
|
||||
return "解析翻译结果异常:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.luoboduner.moo.tool.util.translator;
|
||||
public class TranslatorFactory {
|
||||
public enum TranslatorType {
|
||||
GOOGLE,
|
||||
BING,
|
||||
MICROSOFT
|
||||
}
|
||||
|
||||
@@ -10,8 +11,12 @@ public class TranslatorFactory {
|
||||
switch (type) {
|
||||
case GOOGLE:
|
||||
return new GoogleTranslatorUtil();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<ZonedDateTime> 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<ZonedDateTime> 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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() {
|
||||
// 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
|
||||
|
||||
// We can't easily test the actual conversion without making it public,
|
||||
// but we can verify the translator handles various language codes without exceptions
|
||||
|
||||
// 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 or return null
|
||||
assertNotNull("Result should not be null even on API failure", result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user