From a5b3b902494bd51b8156300860632bc27d6e6b83 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Thu, 14 Aug 2025 11:13:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=90=8D=E5=A4=A7=E5=B0=8F=E5=86=99=20-=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8F=8D=E7=BC=96=E8=AF=91=E5=B7=A5=E5=85=B7=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A**=E6=9C=AC=E5=9C=B0=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3**=20=E4=BF=AE=E6=AD=A3=E6=B7=B7=E6=B7=86=E8=A1=A8?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91=E8=AE=A9=E4=BB=96=E8=83=BD?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E6=89=93=E5=BC=80=E6=96=87=E4=BB=B6=20-=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84**ProgressBarManager=EF=BC=88=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E7=AA=97=E5=8F=A3=E7=9A=84=E4=BB=BB=E5=8A=A1=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=EF=BC=89**=20=E5=A2=9E=E5=BC=BA=E8=A7=86=E8=A7=89?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 + .../innovators/box/AxisInnovatorsBox.java | 2 - .../innovators/box/Log4j2OutputStream.java | 1 - .../java/com/axis/innovators/box/Main.java | 17 +- .../decompilation/gui/ModernJarViewer.java | 1365 ++++++++++++++++- .../axis/innovators/box/gui/MainWindow.java | 2 +- .../box/gui/ProgressBarManager.java | 425 ++++- 7 files changed, 1722 insertions(+), 94 deletions(-) diff --git a/build.gradle b/build.gradle index fb737f4..1768f8b 100644 --- a/build.gradle +++ b/build.gradle @@ -163,6 +163,10 @@ dependencies { implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9' + implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0' + implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' + //implementation 'org.jetbrains.java.decompiler:fernflower:1.9.0' + // 中文拼音处理 implementation 'com.belerweb:pinyin4j:2.5.1' diff --git a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java index dfe3bb8..1741b4a 100644 --- a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java +++ b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java @@ -827,13 +827,11 @@ public class AxisInnovatorsBox { } public static void run(String[] args, boolean isDebug) { - Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { if (main != null) { main.organizingCrashReports(throwable instanceof Exception ? (Exception) throwable : new Exception(throwable)); } else { - // 如果主类尚未初始化,创建临时实例处理崩溃 new AxisInnovatorsBox(args, isDebug) .organizingCrashReports(throwable instanceof Exception ? (Exception) throwable : new Exception(throwable)); diff --git a/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java b/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java index 60157c5..6f95af6 100644 --- a/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java +++ b/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java @@ -26,7 +26,6 @@ public class Log4j2OutputStream extends OutputStream { systemOutContent.write(b, off, len); String message = new String(b, off, len).trim(); logger.info(message); - } /** diff --git a/src/main/java/com/axis/innovators/box/Main.java b/src/main/java/com/axis/innovators/box/Main.java index 33f1f71..94f2a4a 100644 --- a/src/main/java/com/axis/innovators/box/Main.java +++ b/src/main/java/com/axis/innovators/box/Main.java @@ -28,19 +28,12 @@ public class Main { private static FileChannel lockChannel = null; public static void main(String[] args) { - if (!acquireLock()) { - JOptionPane.showMessageDialog( - null, - "程序已在运行中,无法启动多个实例", - "错误", - JOptionPane.ERROR_MESSAGE - ); - System.exit(1); - } - + // 清理日志文件(最大日志为10) FolderCleaner.cleanFolder(FolderCreator.getLogsFolder(), 10); + // 加载保存的语言 LanguageManager.loadSavedLanguage(); + // 如果没有加载的语言,则加载默认的语言 if (LanguageManager.getLoadedLanguages() == null) { LanguageManager.loadLanguage("system:zh_CN"); } @@ -84,6 +77,10 @@ public class Main { } } + if (!acquireLock()) { + return; + } + AxisInnovatorsBox.run(args, debugWindowEnabled); } diff --git a/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java b/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java index a01f120..1b814cb 100644 --- a/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java +++ b/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java @@ -5,38 +5,40 @@ import com.axis.innovators.box.util.AdvancedJFileChooser; import com.github.javaparser.JavaParser; import com.github.javaparser.ParseResult; import com.github.javaparser.Range; -import com.github.javaparser.Position; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.*; +import com.strobel.assembler.metadata.Buffer; +import com.strobel.assembler.metadata.ITypeLoader; +import com.strobel.decompiler.Decompiler; +import com.strobel.decompiler.DecompilerSettings; +import com.strobel.decompiler.PlainTextOutput; import org.apache.commons.compress.utils.IOUtils; import org.benf.cfr.reader.api.CfrDriver; import org.fife.ui.rsyntaxtextarea.*; import org.fife.ui.rtextarea.RTextScrollPane; -import com.github.javaparser.resolution.types.ResolvedType; import javax.sound.sampled.*; import javax.swing.*; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.text.BadLocationException; +import javax.swing.text.Document; import javax.swing.tree.*; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; -import java.awt.dnd.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.*; import java.util.jar.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -45,15 +47,15 @@ import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import javax.imageio.ImageIO; -import javax.swing.border.EmptyBorder; -import javax.swing.event.MouseInputAdapter; +import java.util.Base64; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; public class ModernJarViewer extends JFrame { // UI private JTree fileTree; private JTabbedPane openTabs; private DefaultMutableTreeNode root; - private JMenuBar menuBar; // 数据 private static File currentJarFile; @@ -69,12 +71,24 @@ public class ModernJarViewer extends JFrame { private final Map entryToComponent = new ConcurrentHashMap<>(); private final Map imageViewers = new ConcurrentHashMap<>(); + /** 索引文件位置(可以由用户设置) */ + private File indexFileLocation = new File(System.getProperty("user.home"), ".modernjarviewer.index"); + /** 配置文件(保存 indexFileLocation 的路径等) */ + private final File configPropsFile = new File(System.getProperty("user.home"), ".modernjarviewer.properties"); + // JavaParser private final JavaParser javaParser = new JavaParser(); // last mouse point for popup usage private Point lastMousePoint = null; + private String currentDecompiler = "CFR"; + + private volatile List deobfPatterns = Collections.emptyList(); + private volatile List deobfReplacements = Collections.emptyList(); + private final Map> inlineAnnotations = new ConcurrentHashMap<>(); + private final Object annotationsLock = new Object(); + public ModernJarViewer(Frame owner) { setTitle("Jar反编译工具"); setDefaultCloseOperation(DISPOSE_ON_CLOSE); @@ -85,7 +99,10 @@ public class ModernJarViewer extends JFrame { setTitle("Jar反编译工具"); setDefaultCloseOperation(EXIT_ON_CLOSE); initComponents(); - loadJar(new File(jarFile)); + if (jarFile != null && !jarFile.isEmpty()) { + // 在后台加载 jar,避免主线程卡死 + runBackground("加载 JAR...", () -> loadJar(new File(jarFile))); + } } private void initComponents() { @@ -97,13 +114,61 @@ public class ModernJarViewer extends JFrame { } catch (Exception ignored) {} // 菜单 - menuBar = new JMenuBar(); + JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("文件"); JMenuItem openItem = new JMenuItem("打开 JAR..."); openItem.addActionListener(e -> openJarFile()); fileMenu.add(openItem); menuBar.add(fileMenu); + // 新增设置菜单 + JMenu settingsMenu = new JMenu("设置"); + + // 索引子菜单 + JMenu indexMenu = new JMenu("索引"); + + JMenuItem clearIndexItem = new JMenuItem("清除索引"); + clearIndexItem.addActionListener(e -> clearIndex()); + indexMenu.add(clearIndexItem); + + JMenuItem indexSizeItem = new JMenuItem("索引文件大小"); + indexSizeItem.addActionListener(e -> showIndexSize()); + indexMenu.add(indexSizeItem); + + settingsMenu.add(indexMenu); + + // 反混淆器子菜单 + JMenu deobfuscatorMenu = new JMenu("反混淆器"); + ButtonGroup decompilerGroup = new ButtonGroup(); + + JRadioButtonMenuItem cfrItem = new JRadioButtonMenuItem("CFR 0.152"); + cfrItem.setSelected(true); + cfrItem.addActionListener(e -> setDecompiler("CFR")); + decompilerGroup.add(cfrItem); + deobfuscatorMenu.add(cfrItem); + + // 预留其他反混淆器选项 + JRadioButtonMenuItem fernflowerItem = new JRadioButtonMenuItem("Fernflower"); + fernflowerItem.setEnabled(true); + fernflowerItem.addActionListener(e -> setDecompiler("Fernflower")); + decompilerGroup.add(fernflowerItem); + deobfuscatorMenu.add(fernflowerItem); + + JRadioButtonMenuItem procyonItem = new JRadioButtonMenuItem("Procyon"); + procyonItem.setEnabled(true); + procyonItem.addActionListener(e -> setDecompiler("Procyon")); + decompilerGroup.add(procyonItem); + deobfuscatorMenu.add(procyonItem); + + settingsMenu.add(deobfuscatorMenu); + + // 帮助菜单项 + JMenuItem helpItem = new JMenuItem("帮助"); + helpItem.addActionListener(e -> showHelp()); + settingsMenu.add(helpItem); + + menuBar.add(settingsMenu); + JMenu tools = new JMenu("工具"); JMenuItem buildIndex = new JMenuItem("构建索引"); buildIndex.addActionListener(e -> runBackground("构建索引...", this::buildGlobalIndexWithCache)); @@ -119,15 +184,51 @@ public class ModernJarViewer extends JFrame { }); tools.add(loadMap); - JMenuItem exportJar = new JMenuItem("导出为新 JAR..."); - exportJar.addActionListener(e -> { + JMenuItem setIndexLocation = new JMenuItem("设置索引文件位置..."); + setIndexLocation.addActionListener(e -> { JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("选择索引文件保存位置(会覆盖或新建)"); + chooser.setSelectedFile(indexFileLocation); if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { - File out = chooser.getSelectedFile(); - runBackground("导出 JAR...", () -> exportOverrideJarBlocking(out)); + indexFileLocation = chooser.getSelectedFile(); + saveConfigProperties(); + JOptionPane.showMessageDialog(this, "索引文件位置已保存为:\n" + indexFileLocation.getAbsolutePath(), "已保存", JOptionPane.INFORMATION_MESSAGE); } }); - tools.add(exportJar); + tools.add(setIndexLocation); + + JMenuItem clearIndex = new JMenuItem("清理索引文件(删除)"); + clearIndex.addActionListener(e -> { + if (indexFileLocation != null && indexFileLocation.exists()) { + if (JOptionPane.showConfirmDialog(this, "确定删除索引文件?", "确认", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { + if (indexFileLocation.delete()) { + JOptionPane.showMessageDialog(this, "索引文件删除成功", "完成", JOptionPane.INFORMATION_MESSAGE); + } else { + JOptionPane.showMessageDialog(this, "索引文件删除失败,请手动删除: " + indexFileLocation.getAbsolutePath(), "失败", JOptionPane.ERROR_MESSAGE); + } + } + } else { + JOptionPane.showMessageDialog(this, "没有可删除的索引文件", "信息", JOptionPane.INFORMATION_MESSAGE); + } + }); + tools.add(clearIndex); + + JMenuItem exportWorkspace = new JMenuItem("导出 Java 工作间..."); + exportWorkspace.addActionListener(e -> { + JFileChooser chooser = new JFileChooser(); + chooser.setSelectedFile(new File("workspace.zip")); + if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { + File out = chooser.getSelectedFile(); + runBackground("导出 Java 工作间...", () -> { + try { + exportJavaWorkspaceBlocking(out); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + }); + tools.add(exportWorkspace); menuBar.add(tools); setJMenuBar(menuBar); @@ -174,6 +275,18 @@ public class ModernJarViewer extends JFrame { openTabs = new JTabbedPane(); openTabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + openTabs.addChangeListener(e -> { + // 在 EDT 中执行 + SwingUtilities.invokeLater(() -> { + RSyntaxTextArea ed = getCurrentEditor(); + if (ed == null) return; + String entry = getEntryNameForEditor(ed); + if (entry != null) { + applyAnnotationsToEditor(entry, ed); + } + }); + }); + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScroll, openTabs); split.setDividerLocation(300); add(split, BorderLayout.CENTER); @@ -181,6 +294,584 @@ public class ModernJarViewer extends JFrame { setupKeyBindings(); } + private File getAnnotationsFileForJar() { + if (currentJarFile == null) { + // fallback 存到 config 文件同目录或者用户家目录 + File parent = configPropsFile.getParentFile(); + if (parent == null) parent = new File(System.getProperty("user.home")); + return new File(parent, "annotations_global.properties"); + } + File parent = configPropsFile.getParentFile(); + if (parent == null) parent = new File(System.getProperty("user.home")); + String key = sha1Hex(currentJarFile.getAbsolutePath()); + return new File(parent, "annotations_" + key + ".properties"); + } + + private String sha1Hex(String s) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] b = md.digest(s.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte by : b) { + sb.append(String.format("%02x", by & 0xff)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException ex) { + // 不太可能发生 + return Integer.toHexString(s.hashCode()); + } + } + + + private void saveAnnotationsToDisk() { + File f = getAnnotationsFileForJar(); + synchronized (annotationsLock) { + Properties p = new Properties(); + try { + for (Map.Entry> fe : inlineAnnotations.entrySet()) { + String entryName = fe.getKey(); + String enc = Base64.getUrlEncoder().withoutPadding().encodeToString(entryName.getBytes(StandardCharsets.UTF_8)); + for (Map.Entry le : fe.getValue().entrySet()) { + String key = enc + "#" + le.getKey(); + String val = le.getValue(); + if (val == null) val = ""; + p.setProperty(key, val); + } + } + try (FileOutputStream fos = new FileOutputStream(f)) { + p.store(fos, "ModernJarViewer Annotations for jar: " + (currentJarFile == null ? "" : currentJarFile.getAbsolutePath())); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + private void loadAnnotationsFromDisk() { + File f = getAnnotationsFileForJar(); + if (!f.exists()) return; + synchronized (annotationsLock) { + Properties p = new Properties(); + try (FileInputStream fis = new FileInputStream(f)) { + p.load(fis); + inlineAnnotations.clear(); + for (String k : p.stringPropertyNames()) { + String v = p.getProperty(k, ""); + int sharp = k.indexOf('#'); + if (sharp < 0) continue; + String enc = k.substring(0, sharp); + String lineStr = k.substring(sharp + 1); + int line = 0; + try { line = Integer.parseInt(lineStr); } catch (Exception ex) { continue; } + String entryName; + try { + entryName = new String(Base64.getUrlDecoder().decode(enc), StandardCharsets.UTF_8); + } catch (Exception ex) { + continue; + } + Map fm = inlineAnnotations.computeIfAbsent(entryName, e -> new ConcurrentHashMap<>()); + fm.put(line, v); + } + // 在 EDT 中应用到当前已打开的编辑器(如果有) + SwingUtilities.invokeLater(() -> { + try { + for (Map.Entry e : openEditors.entrySet()) { + String entry = e.getKey(); + RSyntaxTextArea editor = e.getValue(); + if (entry != null && editor != null) { + applyAnnotationsToEditor(entry, editor); + } + } + } catch (Throwable ex) { + ex.printStackTrace(); + } + }); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + private void applyAnnotationsToEditor(String entryName, RSyntaxTextArea editor) { + if (entryName == null || editor == null) return; + Map fileMap = inlineAnnotations.get(entryName); + if (fileMap == null || fileMap.isEmpty()) return; + // 按行号从小到大插入(避免插入早期注解改变后续行号) + List lines = new ArrayList<>(fileMap.keySet()); + Collections.sort(lines); + // 需要在 EDT 操作文档 + SwingUtilities.invokeLater(() -> { + try { + Document doc = editor.getDocument(); + // 为避免行号错误,采用偏移累加的方式:插入之前记录每次插入后偏移增量 + int delta = 0; + for (int line : lines) { + String text = fileMap.get(line); + if (text == null) continue; + int targetLine = line; + try { + // 如果目标行超出当前行数,则追加到文档末尾 + if (targetLine >= editor.getLineCount()) targetLine = editor.getLineCount() - 1; + if (targetLine < 0) targetLine = 0; + int lineStart = editor.getLineStartOffset(targetLine); + int lineEnd = editor.getLineEndOffset(targetLine); + String lineText = doc.getText(lineStart, Math.max(0, lineEnd - lineStart)); + + // 先尝试删除已存在注释块(避免重复) + // 这里复用之前的删除逻辑:查找同行 // 并删除连续对齐注释 + int idx = lineText.indexOf("//"); + if (idx >= 0) { + int commentStart = lineStart + idx; + int removeStart = commentStart; + int scanLine = targetLine + 1; + int removeEnd = lineEnd; + while (scanLine <= editor.getLineCount() - 1) { + int s = editor.getLineStartOffset(scanLine); + int e = editor.getLineEndOffset(scanLine); + String t = doc.getText(s, Math.max(0, e - s)); + int nonSpace = 0; + while (nonSpace < t.length() && (t.charAt(nonSpace) == ' ' || t.charAt(nonSpace) == '\t')) nonSpace++; + if (t.substring(nonSpace).startsWith("//")) { + removeEnd = e; + scanLine++; + } else break; + } + if (removeEnd > removeStart) { + doc.remove(removeStart, removeEnd - removeStart); + } + } + // 插入注解(复用 buildInlineCommentString) + String insert = buildInlineCommentString(editor, lineText, text); + int insertPos = Math.min(doc.getLength(), lineStart + rtrim(lineText).length()); + doc.insertString(insertPos, insert, null); + // 更新 delta 并继续 + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + } + + + private void precompileDeobfPatterns() { + Map map = deobfMap; // 快照引用 + if (map == null || map.isEmpty()) { + deobfPatterns = Collections.emptyList(); + deobfReplacements = Collections.emptyList(); + return; + } + List ps = new ArrayList<>(map.size()); + List rs = new ArrayList<>(map.size()); + for (Map.Entry e : map.entrySet()) { + String k = e.getKey(); + String v = e.getValue(); + if (k == null || k.isEmpty() || v == null) continue; + try { + // 只匹配完整单词(类名/标识符替换常用) + Pattern p = Pattern.compile("\\b" + Pattern.quote(k) + "\\b"); + ps.add(p); + rs.add(v); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + deobfPatterns = ps; + deobfReplacements = rs; + } + + private void setDecompiler(String decompiler) { + currentDecompiler = decompiler; + loadJar(currentJarFile); + JOptionPane.showMessageDialog(this, "已设置反混淆器为: " + decompiler, "设置", JOptionPane.INFORMATION_MESSAGE); + } + + // ---------- 4) 新增:exportJavaWorkspaceBlocking 实现(把 .java / 反编译结果写入 zip,扩展名为 .java) ---------- + private void exportJavaWorkspaceBlocking(File out) throws IOException { + if (currentJarFile == null) throw new IOException("当前没有打开的 JAR"); + + // 预编译反混淆规则(仅做一次) + precompileDeobfPatterns(); + + // 收集条目并排序(稳定顺序) + List entries; + try (JarFile jf = new JarFile(currentJarFile)) { + entries = Collections.list(jf.entries()); + } + + entries.sort(Comparator.comparing(JarEntry::getName)); + + // 线程池(保守使用 CPU-1,不要完全占满机器) + int cpus = Runtime.getRuntime().availableProcessors(); + int threads = Math.max(1, Math.min(cpus - 1, 4)); // 限制到 4,避免反编译器线程安全问题 + ExecutorService pool = Executors.newFixedThreadPool(threads); + + // 为每个条目提交任务,任务负责读取/反编译/反混淆并返回 ExportResult + List> futures = new ArrayList<>(entries.size()); + try (JarFile jar = new JarFile(currentJarFile); + FileOutputStream fos = new FileOutputStream(out); + BufferedOutputStream bos = new BufferedOutputStream(fos); + ZipOutputStream zos = new ZipOutputStream(bos)) { + + for (JarEntry je : entries) { + // 提交任务 + futures.add(pool.submit(() -> { + String name = je.getName().replace('\\', '/'); + if (je.isDirectory()) { + // 目录条目,data 为 null + String dirName = name.endsWith("/") ? name : name + "/"; + return new ExportResult(dirName, null, je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); + } + + // 处理 .java(原始) .class(反编译) 其它资源(原样) + try { + if (name.endsWith(".java")) { + String src = getContentForEntry(name, jar); + if (src == null) src = ""; + // 反混淆内容与路径 + src = applyDeobfToContent(src); + String outName = applyDeobfToPath(name); + return new ExportResult(outName, src.getBytes(StandardCharsets.UTF_8), + je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); + } else if (name.endsWith(".class")) { + String logical = name.substring(0, name.length() - 6); // path without .class + // 去除尾部纯数字匿名类标识($10, $10$1 ...),把匿名类归并到外部类,减少重复 + String stripped = logical.replaceAll("\\$\\d+(?:\\$\\d+)*$", ""); + String targetPath = stripped + ".java"; + // 优先从 fullCodeCache 取,若无则走反编译 getContentForEntry(你的方法应能反编译) + String content = fullCodeCache.get(name); + if (content == null) content = getContentForEntry(name, jar); + if (content == null) content = ""; + content = applyDeobfToContent(content); + targetPath = applyDeobfToPath(targetPath); + return new ExportResult(targetPath, content.getBytes(StandardCharsets.UTF_8), + je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); + } else { + // 资源文件:二进制读取 + try (InputStream is = jar.getInputStream(je); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + if (is != null) { + byte[] buf = new byte[8192]; + int r; + while ((r = is.read(buf)) != -1) baos.write(buf, 0, r); + } + byte[] data = baos.toByteArray(); + // 资源路径也尝试做简单反混淆 + String outName = applyDeobfToPath(name); + return new ExportResult(outName, data, je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); + } + } + } catch (Throwable ex) { + // 单条目处理失败时返回一个空文件,以不中断整个导出流程(同时记录异常) + ex.printStackTrace(); + String fallbackName = applyDeobfToPath(name); + return new ExportResult(fallbackName, new byte[0], System.currentTimeMillis()); + } + })); + } + + // 顺序写入 Zip(保持 entries 排序与目录结构),并避免重复写入同一名字 + Set seen = new HashSet<>(); + for (int i = 0; i < futures.size(); i++) { + Future f = futures.get(i); + ExportResult er; + try { + er = f.get(); // 等待该条目处理完成(但其它条目已在并行处理中) + } catch (Exception ex) { + ex.printStackTrace(); + continue; + } + if (er == null) continue; + String outName = er.entryName.replace('\\', '/'); + if (er.data == null) { + // 目录 + String dirName = outName.endsWith("/") ? outName : outName + "/"; + if (!seen.contains(dirName)) { + ZipEntry ze = new ZipEntry(dirName); + ze.setTime(er.time); + zos.putNextEntry(ze); + zos.closeEntry(); + seen.add(dirName); + } + } else { + // 避免匿名类导致的重复(如果路径是由 class stripping 产生的重复,跳过重复匿名) + boolean wasAnonymous = outName.matches(".*\\$\\d+(?:\\$\\d+)*\\.java$"); + if (seen.contains(outName)) { + if (wasAnonymous) { + // 匿名类已被合并到外部类,跳过 + continue; + } + // 冲突则加 _dupN 后缀 + String base = outName; + int dot = base.lastIndexOf('.'); + String fallback; + int dup = 1; + do { + if (dot > 0) fallback = base.substring(0, dot) + "_dup" + dup + base.substring(dot); + else fallback = base + "_dup" + dup; + dup++; + } while (seen.contains(fallback)); + outName = fallback; + } + ZipEntry ze = new ZipEntry(outName); + ze.setTime(er.time); + zos.putNextEntry(ze); + if (er.data.length > 0) zos.write(er.data); + zos.closeEntry(); + seen.add(outName); + } + } + } finally { + // 关闭线程池 + pool.shutdownNow(); + try { pool.awaitTermination(2, TimeUnit.SECONDS); } catch (InterruptedException ignored) {} + } + } + + /** 对源码文本执行反混淆替换(安全替换,使用单词边界) */ + private String applyDeobfToContent(String content) { + if (content == null || content.isEmpty()) return content; + List ps = deobfPatterns; + List rs = deobfReplacements; + if (ps == null || ps.isEmpty()) return content; + StringBuilder sb = null; + String working = content; + // 使用 Matcher 的 appendReplacement/appendTail 以减少 string 创建(对大文本更友好) + for (int i = 0; i < ps.size(); i++) { + Pattern p = ps.get(i); + String repl = rs.get(i); + try { + Matcher m = p.matcher(working); + if (!m.find()) continue; + if (sb == null) sb = new StringBuilder(working.length() + 128); + sb.setLength(0); + m.reset(); + while (m.find()) { + m.appendReplacement(sb, Matcher.quoteReplacement(repl)); + } + m.appendTail(sb); + working = sb.toString(); + // 重用 sb(下一轮可能继续) + } catch (Exception ex) { + ex.printStackTrace(); + } + } + return working; + } + + private static class ExportResult { + final String entryName; + final byte[] data; // null 表示目录条目 + final long time; + ExportResult(String entryName, byte[] data, long time) { + this.entryName = entryName; + this.data = data; + this.time = time; + } + } + + /** 对路径/文件名做简单的反混淆(尽量只替换包名或类名,不破坏路径结构) */ + private String applyDeobfToPath(String path) { + if (path == null || deobfMap == null || deobfMap.isEmpty()) return path; + String p = path.replace('\\', '/'); + // 先替换典型的包/类出现位置 + for (Map.Entry me : deobfMap.entrySet()) { + String k = me.getKey(); + String v = me.getValue(); + if (k == null || k.isEmpty() || v == null) continue; + try { + // /k/ -> /v/ + p = p.replace("/" + k + "/", "/" + v + "/"); + // /k.java -> /v.java + p = p.replace("/" + k + ".java", "/" + v + ".java"); + // k.java -> v.java + p = p.replace(k + ".java", v + ".java"); + // /k$Inner -> /v$Inner + p = p.replace("/" + k + "$", "/" + v + "$"); + // 如果类名正好在路径末尾 + if (p.endsWith("/" + k)) p = p.substring(0, p.length() - k.length()) + v; + } catch (Exception ex) { + ex.printStackTrace(); + } + } + return p; + } + + // ---------- 3) 新增:配置文件读取/保存方法(用于记住索引文件位置) ---------- + private void loadConfigProperties() { + if (!configPropsFile.exists()) return; + Properties p = new Properties(); + try (FileInputStream fis = new FileInputStream(configPropsFile)) { + p.load(fis); + String idxPath = p.getProperty("indexFilePath"); + if (idxPath != null && !idxPath.isEmpty()) { + indexFileLocation = new File(idxPath); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private void saveConfigProperties() { + Properties p = new Properties(); + p.setProperty("indexFilePath", indexFileLocation == null ? "" : indexFileLocation.getAbsolutePath()); + try (FileOutputStream fos = new FileOutputStream(configPropsFile)) { + p.store(fos, "ModernJarViewer Config"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + // ---------- 3) 新增:索引保存(简单文本属性格式) ---------- + private void saveIndexToDisk(File idxFile) throws IOException { + if (idxFile == null) return; + Properties p = new Properties(); + // 记录当前 jar 路径以便匹配 + p.setProperty("jarPath", currentJarFile == null ? "" : currentJarFile.getAbsolutePath()); + p.setProperty("time", String.valueOf(System.currentTimeMillis())); + + // 把 globalIndex 序列化为 properties 键 gidx.=file@pos@text;file@pos@text;... + for (Map.Entry> e : globalIndex.entrySet()) { + String term = e.getKey(); + StringBuilder sb = new StringBuilder(); + for (SearchResult sr : e.getValue()) { + if (sb.length() > 0) sb.append(";"); + sb.append(escapeForIndex(sr.filePath)).append("@").append(sr.position).append("@").append(escapeForIndex(sr.matchText)); + } + p.setProperty("gidx." + term, sb.toString()); + } + + try (FileOutputStream fos = new FileOutputStream(idxFile)) { + p.store(fos, "ModernJarViewer Index"); + } + } + + private Map> loadIndexFromDisk(File idxFile) throws IOException { + Map> loaded = new HashMap<>(); + if (idxFile == null || !idxFile.exists()) return loaded; + Properties p = new Properties(); + try (FileInputStream fis = new FileInputStream(idxFile)) { + p.load(fis); + } + + String jarPath = p.getProperty("jarPath", ""); + // 如果索引关联的 jar 与当前打开的不一致,则返回空(调用方决定是否使用) + if (currentJarFile == null || !currentJarFile.getAbsolutePath().equals(jarPath)) { + return loaded; + } + + for (String name : p.stringPropertyNames()) { + if (!name.startsWith("gidx.")) continue; + String term = name.substring(5); + String value = p.getProperty(name, ""); + if (value.isEmpty()) continue; + String[] items = value.split(";"); + List list = new ArrayList<>(); + for (String it : items) { + String[] parts = it.split("@", 3); + if (parts.length >= 3) { + String fpath = unescapeFromIndex(parts[0]); + int pos = 0; + try { pos = Integer.parseInt(parts[1]); } catch (Exception ex) {} + String mt = unescapeFromIndex(parts[2]); + list.add(new SearchResult(fpath, pos, mt)); + } + } + if (!list.isEmpty()) loaded.put(term, list); + } + return loaded; + } + + // 简单的转义/反转义(用于 index 属性值) + private String escapeForIndex(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("@", "\\@").replace(";", "\\;"); + } + private String unescapeFromIndex(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(); + boolean esc = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (esc) { + sb.append(c); + esc = false; + } else { + if (c == '\\') esc = true; + else sb.append(c); + } + } + return sb.toString(); + } + + + private void clearIndex() { + int result = JOptionPane.showConfirmDialog(this, + "确定要清除所有索引吗?这可能会影响搜索和导航功能。", + "清除索引", + JOptionPane.YES_NO_OPTION); + + if (result == JOptionPane.YES_OPTION) { + methodIndex.clear(); + globalIndex.clear(); + JOptionPane.showMessageDialog(this, "索引已清除", "操作完成", JOptionPane.INFORMATION_MESSAGE); + } + } + + private void showIndexSize() { + long methodIndexSize = estimateSize(methodIndex); + long globalIndexSize = estimateSize(globalIndex); + long totalSize = methodIndexSize + globalIndexSize; + + String sizeInfo = String.format( + "索引大小统计:
" + + "方法索引: %s
" + + "全局索引: %s
" + + "总大小: %s", + formatSize(methodIndexSize), + formatSize(globalIndexSize), + formatSize(totalSize) + ); + + JOptionPane.showMessageDialog(this, sizeInfo, "索引大小", JOptionPane.INFORMATION_MESSAGE); + } + + // 估算对象大小(简化版) + private long estimateSize(Map map) { + // 每个键值对大约100字节的估算(简化处理) + return map.size() * 100L; + } + + // 格式化大小显示 + private String formatSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + char unit = "KMGTPE".charAt(exp-1); + return String.format("%.1f %sB", bytes / Math.pow(1024, exp), unit); + } + + // 实现帮助功能 + private void showHelp() { + String helpText = "
" + + "

设置帮助

" + + "

索引

" + + "

清除索引: 删除所有已构建的索引数据,释放内存。

" + + "

索引文件大小: 显示当前索引占用的内存大小。

" + + "

反混淆器

" + + "

选择用于反编译.class文件的工具:

" + + "
    " + + "
  • CFR 0.152: 当前支持的反混淆器
  • " + + "
  • Fernflower: IntelliJ IDEA使用的反混淆器(预留)
  • " + + "
  • Procyon: 另一个开源反混淆器(预留)
  • " + + "
" + + "

注意: 当前仅支持CFR反混淆器,其他选项为预留功能。

" + + "
"; + + JOptionPane.showMessageDialog(this, helpText, "设置帮助", JOptionPane.INFORMATION_MESSAGE); + } + private void registerFileIcons() { fileTree.setCellRenderer(new DefaultTreeCellRenderer() { private final ImageIcon jarIcon = new ImageIcon(LoadIcon.loadIcon("programming/JarApiViewer/file_jar.png", 18).getImage()); @@ -347,6 +1038,414 @@ public class ModernJarViewer extends JFrame { @Override public void actionPerformed(ActionEvent e) { showGlobalSearchDialog(); } }); + + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, 0), "toggleInlineAnnotation"); + getRootPane().getActionMap().put("toggleInlineAnnotation", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + RSyntaxTextArea ed = getCurrentEditor(); + if (ed == null) { + Toolkit.getDefaultToolkit().beep(); + return; + } + try { + handleToggleInlineAnnotation(ed); + } catch (Exception ex) { + ex.printStackTrace(); + Toolkit.getDefaultToolkit().beep(); + } + } + }); + } + + // ---------- 2) 获取当前编辑器的 entryName(路径/条目名),用于 map key(放到方法区) ---------- + private String getEntryNameForEditor(RSyntaxTextArea editor) { + if (editor == null) return null; + for (Map.Entry e : openEditors.entrySet()) { + if (e.getValue() == editor) return e.getKey(); + } + // 退路:尝试从 entryToComponent 查找 + for (Map.Entry ce : entryToComponent.entrySet()) { + Component c = ce.getValue(); + if (c == null) continue; + if (SwingUtilities.isDescendingFrom(editor, c) || SwingUtilities.isDescendingFrom(c, editor)) { + return ce.getKey(); + } + } + return null; + } + + // ---------- 3) 主处理函数:插入或编辑当前行的注解(放到方法区) ---------- + private void handleToggleInlineAnnotation(RSyntaxTextArea editor) throws BadLocationException { + String entryName = getEntryNameForEditor(editor); + if (entryName == null) { + // 无法识别文件名,仍允许临时注解但不持久化 + entryName = ""; + } + int caret = editor.getCaretPosition(); + int line = editor.getLineOfOffset(caret); + // 尝试从缓存中读取已有注解 + Map fileMap = inlineAnnotations.computeIfAbsent(entryName, k -> new ConcurrentHashMap<>()); + String existing = fileMap.get(line); + + + // 如果存在,则编辑;否则新建 + String initial = existing == null ? "" : existing; + String edited = showInlineAnnotationEditor(initial); + if (edited == null) { + // 用户取消,不做任何操作 + return; + } + + // 编辑为空 -> 删除注解;否则插入/替换 + if (edited.trim().isEmpty()) { + // 删除现有注解块(如果存在) + removeAnnotationBlockIfExists(editor, line); + fileMap.remove(line); + saveAnnotationsToDisk(); + return; + } else { + // 插入或替换注解块(更新文档并更新缓存) + insertOrReplaceAnnotationBlock(editor, line, edited); + fileMap.put(line, edited); + saveAnnotationsToDisk(); + } + } + + // ---------- 4) 显示多行注解编辑对话框(返回 null 表示取消,空字符串表示删除) ---------- + private String showInlineAnnotationEditor(String initialText) { + JDialog dlg = new JDialog(this, "编辑注解(Ctrl+Enter 提交;留空将删除)", true); + dlg.setLayout(new BorderLayout()); + JTextArea ta = new JTextArea(8, 60); + ta.setLineWrap(true); + ta.setWrapStyleWord(true); + ta.setText(initialText); + + // Ctrl+Enter 提交 + ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), "submit"); + ta.getActionMap().put("submit", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window w = SwingUtilities.getWindowAncestor((Component) e.getSource()); + if (w instanceof JDialog) ((JDialog) w).dispose(); + } + }); + + JPanel center = new JPanel(new BorderLayout()); + center.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); + center.add(new JScrollPane(ta), BorderLayout.CENTER); + + JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton ok = new JButton("确定"); + JButton cancel = new JButton("取消"); + bottom.add(cancel); + bottom.add(ok); + + ok.addActionListener(ae -> dlg.dispose()); + cancel.addActionListener(ae -> { + ta.setText(null); + dlg.dispose(); + }); + + dlg.add(center, BorderLayout.CENTER); + dlg.add(bottom, BorderLayout.SOUTH); + dlg.pack(); + dlg.setLocationRelativeTo(this); + dlg.setVisible(true); + + // 返回 null 代表取消 + if (ta.getText() == null && initialText != null) return null; + return ta.getText(); + } + + // ---------- 5) 删除当前行已有注解块(按 ///block 规则查找并删除) ---------- + private void removeAnnotationBlockIfExists(RSyntaxTextArea editor, int line) { + try { + Document doc = editor.getDocument(); + int lineStart = editor.getLineStartOffset(line); + int lineEnd = editor.getLineEndOffset(line); + String lineText = doc.getText(lineStart, Math.max(0, lineEnd - lineStart)); + int idx = lineText.indexOf("//"); + if (idx < 0) { + // 没有在同一行找到 //,尝试从下一行查找以防文档被手动修改 + // 如果下一行以空格+// 开头,则从下一行开始删除连续注释 + int scanLine = line + 1; + boolean found = false; + int removeStart = -1; + int removeEnd = -1; + while (true) { + if (scanLine > editor.getLineCount() - 1) break; + int s = editor.getLineStartOffset(scanLine); + int e = editor.getLineEndOffset(scanLine); + String t = doc.getText(s, Math.max(0, e - s)); + if (t.trim().startsWith("//")) { + if (!found) { removeStart = s; found = true; } + removeEnd = e; + scanLine++; + continue; + } + break; + } + if (found) { + doc.remove(removeStart, removeEnd - removeStart); + } + return; + } else { + // 找到同行的 //,从该列开始删除该行及后续连续以相同缩进包含 // 的行 + int commentStart = lineStart + idx; + int removeStart = commentStart; + int scanLine = line + 1; + int removeEnd = lineEnd; + while (scanLine <= editor.getLineCount() - 1) { + int s = editor.getLineStartOffset(scanLine); + int e = editor.getLineEndOffset(scanLine); + String t = doc.getText(s, Math.max(0, e - s)); + // 判断该行是否为对齐注释(前面若有空格再紧接 "//") + int nonSpace = 0; + while (nonSpace < t.length() && (t.charAt(nonSpace) == ' ' || t.charAt(nonSpace) == '\t')) nonSpace++; + if (t.substring(nonSpace).startsWith("//")) { + removeEnd = e; + scanLine++; + continue; + } + break; + } + if (removeEnd > removeStart) { + doc.remove(removeStart, removeEnd - removeStart); + } + } + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } + + // ---------- 6) 插入或替换注解块(核心:找到行尾插入或找到已有注释块并替换) ---------- + private void insertOrReplaceAnnotationBlock(RSyntaxTextArea editor, int line, String editedText) { + try { + Document doc = editor.getDocument(); + int lineStart = editor.getLineStartOffset(line); + int lineEnd = editor.getLineEndOffset(line); + String lineText = doc.getText(lineStart, Math.max(0, lineEnd - lineStart)); + + // 先查找当前行内是否已有 '//' 注释(简单匹配) + int idx = lineText.indexOf("//"); + if (idx >= 0) { + // 计算旧注释块的开始/结束位置(包括后续连续对齐的注释行) + int commentStart = lineStart + idx; + int removeStart = commentStart; + int scanLine = line + 1; + int removeEnd = lineEnd; + while (scanLine <= editor.getLineCount() - 1) { + int s = editor.getLineStartOffset(scanLine); + int e = editor.getLineEndOffset(scanLine); + String t = doc.getText(s, Math.max(0, e - s)); + int nonSpace = 0; + while (nonSpace < t.length() && (t.charAt(nonSpace) == ' ' || t.charAt(nonSpace) == '\t')) nonSpace++; + if (t.substring(nonSpace).startsWith("//")) { + removeEnd = e; + scanLine++; + } else { + break; + } + } + // 删除旧注解块 + doc.remove(removeStart, removeEnd - removeStart); + + // 在原注释开始处插入新的注解(即替换) + String insert = buildInlineCommentString(editor, lineText, editedText); + doc.insertString(removeStart, insert, null); + + // 将光标放在新注解最后 + int finalCaret = removeStart + insert.length(); + if (finalCaret <= doc.getLength()) editor.setCaretPosition(finalCaret); + return; + } else { + // 无已有注释:我们要在"最后一个非空字符"之后插入注释,避免插入到换行位置 + String trimmedLine = rtrim(lineText); + int insertPos = lineStart + trimmedLine.length(); // 在最后非空字符之后插入 + String insert = buildInlineCommentString(editor, lineText, editedText); + doc.insertString(insertPos, insert, null); + + int finalCaret = insertPos + insert.length(); + if (finalCaret <= doc.getLength()) editor.setCaretPosition(finalCaret); + return; + } + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } + + // ---------- 替换:buildInlineCommentString ---------- + private String buildInlineCommentString(RSyntaxTextArea editor, String lineText, String editedText) { + // editedText 可能包含多行 + String[] lines = editedText.replace("\r\n", "\n").replace("\r", "\n").split("\n", -1); + + // 计算 trimmedLine(不包含行尾空白),用于确定第一行注释插入列 + String trimmedLine = rtrim(lineText); + // 首个 // 的列:紧跟在 trimmedLine 之后,加一个空格 + int commentCol = Math.max(0, trimmedLine.length()) + 1; + + // 构造注释:首行直接紧跟在当前行末(以 " // ..." 开始) + StringBuilder sb = new StringBuilder(); + sb.append(" // "); + if (lines.length > 0) sb.append(lines[0]); + + // 后续行:换行 + 若当前行前面有缩进,则保留缩进 + 使 // 对齐到 commentCol 列 + // 先构造 prefix:由 commentCol 个空格构成(这是从行首到 // 的列数) + String prefixSpaces = repeat(' ', commentCol); + for (int i = 1; i < lines.length; i++) { + sb.append("\n"); + sb.append(prefixSpaces); + sb.append("// "); + sb.append(lines[i]); + } + // 最后加一个换行,保证插入后光标不粘到下一代码行 + sb.append("\n"); + return sb.toString(); + } + + + // ---------- 8) 小工具方法:去掉行尾空白 与 重复字符 ---------- + private static String rtrim(String s) { + if (s == null) return ""; + int i = s.length() - 1; + while (i >= 0 && Character.isWhitespace(s.charAt(i))) i--; + return s.substring(0, i + 1); + } + private static String repeat(char c, int n) { + if (n <= 0) return ""; + char[] arr = new char[n]; + Arrays.fill(arr, c); + return new String(arr); + } + + + // 2) 新增:获取当前活动编辑器(放到类的方法区) + private RSyntaxTextArea getCurrentEditor() { + if (openTabs == null) return null; + Component sel = openTabs.getSelectedComponent(); + if (sel == null) return null; + + // 常见情况:选中的是 JScrollPane,里面是 RSyntaxTextArea + if (sel instanceof JScrollPane) { + JViewport vp = ((JScrollPane) sel).getViewport(); + Component view = vp.getView(); + if (view instanceof RSyntaxTextArea) return (RSyntaxTextArea) view; + } + + // 若选项卡中嵌套结构比较复杂,则从 openEditors 中查找第一个位于选中组件里的编辑器 + for (RSyntaxTextArea ta : openEditors.values()) { + if (ta == null) continue; + if (SwingUtilities.isDescendingFrom(ta, sel)) return ta; + } + + // 最后回退:如果 openTabs 的组件直接就是编辑器 + if (sel instanceof RSyntaxTextArea) return (RSyntaxTextArea) sel; + return null; + } + + // 3) 新增:弹出注解输入对话框(多行),支持 Ctrl+Enter 提交 + private void insertAnnotationDialog(RSyntaxTextArea editor) { + JDialog dlg = new JDialog(this, "添加注解", true); + dlg.setLayout(new BorderLayout()); + JTextArea ta = new JTextArea(6, 60); + ta.setLineWrap(true); + ta.setWrapStyleWord(true); + + // 允许用户用 Ctrl+Enter 提交 + ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), "submit"); + ta.getActionMap().put("submit", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window w = SwingUtilities.getWindowAncestor((Component) e.getSource()); + if (w instanceof JDialog) ((JDialog) w).dispose(); + } + }); + + JPanel center = new JPanel(new BorderLayout(4,4)); + center.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); + center.add(new JScrollPane(ta), BorderLayout.CENTER); + + JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton ok = new JButton("确定"); + JButton cancel = new JButton("取消"); + bottom.add(cancel); + bottom.add(ok); + + ok.addActionListener(ae -> dlg.dispose()); + cancel.addActionListener(ae -> { + ta.setText(null); + dlg.dispose(); + }); + + dlg.add(center, BorderLayout.CENTER); + dlg.add(bottom, BorderLayout.SOUTH); + + dlg.pack(); + dlg.setLocationRelativeTo(this); + dlg.setVisible(true); + + String text = ta.getText(); + if (text == null || text.trim().isEmpty()) return; + + // 在 EDT 之外做少量处理,然后在 EDT 插入文本 + final String annotation = buildBlockCommentForText(editor, text); + SwingUtilities.invokeLater(() -> { + try { + insertAnnotationAtCurrentLine(editor, annotation); + } catch (Exception ex) { + ex.printStackTrace(); + Toolkit.getDefaultToolkit().beep(); + } + }); + } + + // 4) 新增:把用户输入的多行注解格式化为 Java 样式的多行注释,并保留缩进 + private String buildBlockCommentForText(RSyntaxTextArea editor, String rawText) { + // 将输入按行拆分,去掉尾部/头部多余空行 + String[] lines = rawText.replace("\r\n", "\n").replace("\r", "\n").split("\n"); + // 获取当前行缩进 + int caret = editor.getCaretPosition(); + String indent = ""; + try { + int line = editor.getLineOfOffset(caret); + int lineStart = editor.getLineStartOffset(line); + int lineEnd = editor.getLineEndOffset(line); + String lineText = editor.getText(lineStart, Math.max(0, Math.min(lineEnd - lineStart, 2000))); + // 提取行首空白 + int idx = 0; + while (idx < lineText.length() && (lineText.charAt(idx) == ' ' || lineText.charAt(idx) == '\t')) idx++; + indent = lineText.substring(0, idx); + } catch (BadLocationException ignored) {} + + StringBuilder sb = new StringBuilder(); + sb.append("\n"); // 在当前行后插入新行开始注解 + sb.append(indent).append("/*\n"); + for (String l : lines) { + // 去掉末尾空格以保持整洁 + String trimmed = l.replaceAll("\\s+$", ""); + sb.append(indent).append(" * "); + sb.append(trimmed); + sb.append("\n"); + } + sb.append(indent).append(" */"); + sb.append("\n"); + return sb.toString(); + } + + // 5) 新增:实际在当前行后插入注解文本 + private void insertAnnotationAtCurrentLine(RSyntaxTextArea editor, String annotation) throws BadLocationException { + if (annotation == null || annotation.isEmpty()) return; + int caret = editor.getCaretPosition(); + int line = editor.getLineOfOffset(caret); + int lineEnd = editor.getLineEndOffset(line); // 插入到行尾(在该行之后) + // 将注解插入文档 + Document doc = editor.getDocument(); + doc.insertString(lineEnd, annotation, null); + // 将光标移动到注解结束处 + int newPos = lineEnd + annotation.length(); + if (newPos <= doc.getLength()) editor.setCaretPosition(newPos); } // ---------- 打开 JAR,并构建树(目录在前、文件在后;隐掉被顶层源码覆盖的内部类) ---------- @@ -375,6 +1474,7 @@ public class ModernJarViewer extends JFrame { openTabs.removeAll(); root.removeAllChildren(); + try (JarFile jar = new JarFile(jarFile)) { Set names = new HashSet<>(); Enumeration en = jar.entries(); @@ -408,6 +1508,7 @@ public class ModernJarViewer extends JFrame { ((DefaultTreeModel) fileTree.getModel()).reload(); fileTree.expandRow(0); + loadAnnotationsFromDisk(); // 异步构建索引并缓存全部可文本内容(避免后续卡顿) runBackground("构建索引并缓存全jar代码...", this::buildGlobalIndexWithCache); @@ -1746,6 +2847,12 @@ public class ModernJarViewer extends JFrame { } else if (entryName.endsWith(".class")) { String out = decompileClass(je); if (out == null) out = ""; + if (!deobfMap.isEmpty()) { + for (Map.Entry map : deobfMap.entrySet()) { + out = out.replaceAll("\\b" + Pattern.quote(map.getKey()) + "\\b", + Matcher.quoteReplacement(map.getValue())); + } + } fullCodeCache.put(entryName, out); return out; } else { @@ -1760,15 +2867,25 @@ public class ModernJarViewer extends JFrame { private String decompileClass(JarEntry entry) { if (overrideContents.containsKey(entry.getName())) return overrideContents.get(entry.getName()); if (fullCodeCache.containsKey(entry.getName())) return fullCodeCache.get(entry.getName()); + try (JarFile jar = new JarFile(currentJarFile)) { - CFROutputSinkFactory sinkFactory = new CFROutputSinkFactory(); - CfrDriver driver = new CfrDriver.Builder() - .withClassFileSource(new JarClassFileSource(jar)) - .withOutputSink(sinkFactory) - .build(); - driver.analyse(Collections.singletonList(entry.getName())); - String out = sinkFactory.getOutput(); - if (out == null) out = "// Decompile failed\n"; + String out = "// Decompile failed\n"; + String className = entry.getName(); + + switch (currentDecompiler) { + case "CFR": + out = decompileWithCFR(jar, className); + break; + case "Fernflower": + out = decompileWithCFR(jar, className); + break; + case "Procyon": + out = decompileWithProcyon(jar, className); + break; + default: + out = decompileWithCFR(jar, className); + } + fullCodeCache.put(entry.getName(), out); return out; } catch (Exception e) { @@ -1777,8 +2894,125 @@ public class ModernJarViewer extends JFrame { } } + private String decompileWithCFR(JarFile jar, String className) { + try { + CFROutputSinkFactory sinkFactory = new CFROutputSinkFactory(); + CfrDriver driver = new CfrDriver.Builder() + .withClassFileSource(new JarClassFileSource(jar)) + .withOutputSink(sinkFactory) + .build(); + driver.analyse(Collections.singletonList(className)); + return sinkFactory.getOutput(); + } catch (Exception e) { + e.printStackTrace(); + return "// CFR decompile failed: " + e.getMessage(); + } + } + + private String decompileWithFernflower(JarFile jar, String className) { + try { + // Fernflower 反编译器实现 + //ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + //PrintStream printStream = new PrintStream(outputStream); +// + //Fernflower fernflower = new Fernflower( + // new JarFileSource(jar), + // new PrintStreamDecompiler(printStream), + // new FernflowerPreferences(), + // new SimpleLogger() + //); +// + //fernflower.getStructContext().addSpace(jar, true); + //fernflower.decompileContext(); + //fernflower.clearContext(); + + return decompileWithCFR(jar,className); + } catch (Exception e) { + e.printStackTrace(); + return "// Fernflower decompile failed: " + e.getMessage(); + } + } + + private String decompileWithProcyon(JarFile jar, String className) { + try { + StringWriter stringWriter = new StringWriter(); + PlainTextOutput output = new PlainTextOutput(stringWriter); + DecompilerSettings settings = new DecompilerSettings(); + + settings.setTypeLoader(new ITypeLoader() { + @Override + public boolean tryLoadType(String internalName, Buffer buffer) { + try { + String path = internalName.replace('.', '/') + ".class"; + JarEntry entry = jar.getJarEntry(path); + if (entry == null) { + return false; + } + try (InputStream input = jar.getInputStream(entry)) { + byte[] data = input.readAllBytes(); + if (data.length < 4 || + data[0] != (byte)0xCA || + data[1] != (byte)0xFE || + data[2] != (byte)0xBA || + data[3] != (byte)0xBE) { + System.err.println("[ERROR] Invalid class file format"); + return false; + } + buffer.reset(data.length + 1); + for (byte b : data) { + buffer.writeByte(b & 0xFF); + } + buffer.position(0); + return true; + } + } catch (Exception e) { + System.err.println("[ERROR] Failed to load class: " + e.getMessage()); + return false; + } + } + }); + + String fullClassName = className.replace(".class", "").replace('/', '.'); + + try { + Decompiler.decompile(fullClassName, output, settings); + } catch (Exception e) { + System.err.println("[ERROR] Decompilation failed: " + e.getMessage()); + throw e; + } + + String result = stringWriter.toString(); + if (result.trim().isEmpty()) { + return "// Decompilation produced empty output (possible error)"; + } + return result; + } catch (Exception e) { + System.err.println("[CRITICAL] Decompilation failed:"); + e.printStackTrace(); + return "// Procyon decompile failed: " + + e.getClass().getSimpleName() + " - " + + (e.getMessage() != null ? e.getMessage() : "Check class file integrity"); + } + } + // ---------- 全jar索引 + 缓存(构建并把所有可文本内容放入 fullCodeCache) ---------- private void buildGlobalIndexWithCache() { + + try { + // 如果用户已设置 indexFileLocation 且存在,先尝试加载 + if (indexFileLocation != null && indexFileLocation.exists()) { + Map> loaded = loadIndexFromDisk(indexFileLocation); + if (!loaded.isEmpty()) { + globalIndex.putAll(loaded); + // methodIndex 仍需按需重建(这里不重建 methodIndex,保留快速全局搜索) + return; // 提前返回(注意:若你需要 fullCodeCache,也可以在磁盘保存 fullCodeCache 的实现) + } + } + } catch (Exception ex) { + // 忽略索引加载错误,继续正常构建 + ex.printStackTrace(); + } + methodIndex.clear(); globalIndex.clear(); if (currentJarFile == null) return; @@ -1831,6 +3065,10 @@ public class ModernJarViewer extends JFrame { } } applyMappingToIndex(); + + if (indexFileLocation != null) { + saveIndexToDisk(indexFileLocation); + } } catch (Exception e) { e.printStackTrace(); } @@ -1901,6 +3139,20 @@ public class ModernJarViewer extends JFrame { if (!obf.equals(deobf)) deobfMap.put(obf, deobf); } } + + try (JarFile jar = new JarFile(currentJarFile)) { + Enumeration en = jar.entries(); + while (en.hasMoreElements()) { + JarEntry je = en.nextElement(); + if (je.isDirectory()) continue; + String name = je.getName(); + if (name.endsWith(".class")) { + // 触发反编译并应用混淆映射 + getContentForEntry(name, jar); + } + } + } + // 性能优化:使用单个 Pattern 对每个条目替换(一次扫描) applyMappingToAllEntriesBlocking(); // 重新构建索引(使用缓存) @@ -1992,22 +3244,56 @@ public class ModernJarViewer extends JFrame { globalIndex.putAll(newGlobal); } + // ---------- 2) 替换 applyMappingToOpenEditors 方法(将耗时替换放到后台线程) ---------- private void applyMappingToOpenEditors() { + if (deobfMap.isEmpty() || openEditors.isEmpty()) return; + + // 对每个打开的编辑器启动一个 SwingWorker,doInBackground 执行重替换,done 在 EDT 更新文本 for (Map.Entry e : openEditors.entrySet()) { - RSyntaxTextArea ed = e.getValue(); - String txt = ed.getText(); - String newTxt = txt; - // 使用单个替换循环(deobfMap 通常不大) - for (Map.Entry map : deobfMap.entrySet()) { - newTxt = newTxt.replaceAll("\\b" + Pattern.quote(map.getKey()) + "\\b", Matcher.quoteReplacement(map.getValue())); - } - if (!newTxt.equals(txt)) { - String finalNewTxt = newTxt; - SwingUtilities.invokeLater(() -> ed.setText(finalNewTxt)); - } + final RSyntaxTextArea ed = e.getValue(); + final String originalText = ed.getText(); + + SwingWorker sw = new SwingWorker<>() { + @Override + protected String doInBackground() { + String newTxt = originalText; + // deobfMap 通常不大,逐个替换 + for (Map.Entry map : deobfMap.entrySet()) { + try { + // 使用边界匹配并转义关键字,避免误替换 + String pattern = "\\b" + Pattern.quote(map.getKey()) + "\\b"; + newTxt = newTxt.replaceAll(pattern, Matcher.quoteReplacement(map.getValue())); + } catch (Exception ex) { + // 单个 regex 失败也不要阻塞其它替换 + ex.printStackTrace(); + } + } + return newTxt; + } + + @Override + protected void done() { + try { + String finalNewTxt = get(); + if (!finalNewTxt.equals(originalText)) { + // 在 EDT 更新文本 + SwingUtilities.invokeLater(() -> { + // 保持光标位置(尽量) + int caret = ed.getCaretPosition(); + ed.setText(finalNewTxt); + if (caret <= ed.getText().length()) ed.setCaretPosition(caret); + }); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + }; + sw.execute(); } } + // ---------- 搜索(当前/全局) ---------- private JDialog currentSearchDialog = null; @@ -2075,6 +3361,7 @@ public class ModernJarViewer extends JFrame { } } + private void showGlobalSearchDialog() { if (currentJarFile == null) { JOptionPane.showMessageDialog(this, "请先打开 JAR"); return; } JDialog dlg = new JDialog(this, "全局搜索", false); @@ -2200,16 +3487,6 @@ public class ModernJarViewer extends JFrame { n.endsWith(".bin") || n.endsWith(".dat"); } - private RSyntaxTextArea getCurrentEditor() { - int idx = openTabs.getSelectedIndex(); - if (idx < 0) return null; - Component comp = openTabs.getComponentAt(idx); - for (Map.Entry e : entryToComponent.entrySet()) { - if (e.getValue() == comp) return openEditors.get(e.getKey()); - } - return null; - } - private int lineColToOffset(String content, int line, int column) { if (line <= 0) return 0; String[] lines = content.split("\n", -1); diff --git a/src/main/java/com/axis/innovators/box/gui/MainWindow.java b/src/main/java/com/axis/innovators/box/gui/MainWindow.java index da8393d..433dc7c 100644 --- a/src/main/java/com/axis/innovators/box/gui/MainWindow.java +++ b/src/main/java/com/axis/innovators/box/gui/MainWindow.java @@ -125,7 +125,7 @@ public class MainWindow extends JFrame { */ public void initUI() { setTitle(LanguageManager.getLoadedLanguages().getText("mainWindow.title")); - setDefaultCloseOperation(EXIT_ON_CLOSE); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); setSize(1200, 800); setLocationRelativeTo(null); diff --git a/src/main/java/com/axis/innovators/box/gui/ProgressBarManager.java b/src/main/java/com/axis/innovators/box/gui/ProgressBarManager.java index 6fa9c88..1db9404 100644 --- a/src/main/java/com/axis/innovators/box/gui/ProgressBarManager.java +++ b/src/main/java/com/axis/innovators/box/gui/ProgressBarManager.java @@ -2,17 +2,20 @@ package com.axis.innovators.box.gui; import javax.swing.*; import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; import java.util.HashMap; import java.util.Map; /** - * 启动窗口的任务系统 - * @author tzdwindows 7 + * 启动窗口的任务系统(已改造为现代流畅动画视觉效果) + * 注意:保留了原有对外方法和签名(updateMainProgress/updateSubProgress/setTotalTasks/close) + * 作者: tzdwindows 7(UI 改造版) */ public class ProgressBarManager extends WindowsJDialog { private JFrame loadingFrame; - private JProgressBar mainProgressBar; - private JProgressBar subProgressBar; + private SmoothProgressBar mainProgressBar; + private SmoothProgressBar subProgressBar; private JLabel statusLabel; private JLabel timeLabel; private long startTime; @@ -21,81 +24,208 @@ public class ProgressBarManager extends WindowsJDialog { private int completedTasks; private Map subTasks = new HashMap<>(); + // 动画计时器(60FPS) + private Timer animationTimer; + + // 视觉参数 + private Color accentColor = new Color(0x00C2FF); // 科技感青蓝 + private Font uiFont; + public ProgressBarManager(String title, int totalTasks) { - this.totalTasks = totalTasks; + this.totalTasks = Math.max(1, totalTasks); this.completedTasks = 0; this.startTime = System.currentTimeMillis(); + // 尝试设置现代中文友好字体(Windows 常见) + try { + uiFont = new Font("Microsoft YaHei UI", Font.PLAIN, 13); + // 若系统无该字体则 fallback + if (!uiFont.getFamily().toLowerCase().contains("microsoft") && + !uiFont.getFamily().toLowerCase().contains("yahei")) { + uiFont = new Font("Segoe UI", Font.PLAIN, 13); + } + } catch (Throwable t) { + uiFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + } + loadingFrame = new JFrame(title); + loadingFrame.setUndecorated(true); + loadingFrame.setBackground(new Color(0, 0, 0, 0)); // 允许圆角透明背景 loadingFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - loadingFrame.setSize(400, 250); + loadingFrame.setSize(520, 300); loadingFrame.setLocationRelativeTo(null); loadingFrame.setIconImage(LoadIcon.loadIcon("logo.png", 64).getImage()); - JPanel loadingPanel = new JPanel(new BorderLayout()); + // 主容器(带动画背景和圆角卡片) + AnimatedBackgroundPanel root = new AnimatedBackgroundPanel(); + root.setLayout(new GridBagLayout()); + root.setBorder(BorderFactory.createEmptyBorder(18, 18, 18, 18)); - mainProgressBar = new JProgressBar(0, 100); - mainProgressBar.setStringPainted(true); + // 卡片面板 + JPanel card = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - subProgressBar = new JProgressBar(0, 100); - subProgressBar.setStringPainted(true); - subProgressBar.setString("Subtask Progress"); + // 卡片阴影(简单外发光) + int arc = 18; + int w = getWidth(); + int h = getHeight(); - statusLabel = new JLabel("Initializing...", SwingConstants.CENTER); - timeLabel = new JLabel("Time elapsed: 0s", SwingConstants.CENTER); + // 背景渐变 + GradientPaint gp = new GradientPaint(0, 0, new Color(20, 22, 25, 230), + 0, h, new Color(14, 16, 19, 230)); + g2.setPaint(gp); - JLabel logoLabel = new JLabel(LoadIcon.loadIcon("logo.png", 64)); + // 圆角矩形 + RoundRectangle2D rr = new RoundRectangle2D.Float(6, 6, w - 12, h - 12, arc, arc); + g2.fill(rr); - JPanel progressPanel = new JPanel(new GridLayout(2, 1)); - progressPanel.add(mainProgressBar); - progressPanel.add(subProgressBar); + // 细微边框 + g2.setStroke(new BasicStroke(1f)); + g2.setColor(new Color(255, 255, 255, 10)); + g2.draw(rr); - loadingPanel.add(logoLabel, BorderLayout.NORTH); - loadingPanel.add(progressPanel, BorderLayout.CENTER); - loadingPanel.add(statusLabel, BorderLayout.SOUTH); - loadingPanel.add(timeLabel, BorderLayout.SOUTH); + g2.dispose(); + super.paintComponent(g); + } + }; + card.setOpaque(false); + card.setLayout(new BorderLayout(12, 12)); + card.setPreferredSize(new Dimension(480, 240)); + card.setBorder(BorderFactory.createEmptyBorder(14, 14, 14, 14)); + + // 顶部 logo + 标题 + JPanel top = new JPanel(new BorderLayout()); + top.setOpaque(false); + JLabel logoLabel = new JLabel(LoadIcon.loadIcon("logo.png", 48)); + logoLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 12)); + JLabel titleLabel = new JLabel(title); + titleLabel.setFont(uiFont.deriveFont(Font.BOLD, 18f)); + titleLabel.setForeground(Color.WHITE); + + top.add(logoLabel, BorderLayout.WEST); + top.add(titleLabel, BorderLayout.CENTER); + + // 中间进度区 + JPanel center = new JPanel(new GridBagLayout()); + center.setOpaque(false); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.fill = GridBagConstraints.HORIZONTAL; + + mainProgressBar = new SmoothProgressBar(0); + mainProgressBar.setPreferredSize(new Dimension(420, 26)); + mainProgressBar.setAccentColor(accentColor); + + subProgressBar = new SmoothProgressBar(0); + subProgressBar.setPreferredSize(new Dimension(420, 18)); + subProgressBar.setAccentColor(new Color(0x6EE7FF)); + subProgressBar.setShowStripe(true); + + center.add(mainProgressBar, c); + c.gridy++; + c.insets = new Insets(8, 0, 0, 0); + center.add(subProgressBar, c); + + // 底部文本 + JPanel bottom = new JPanel(new BorderLayout()); + bottom.setOpaque(false); + statusLabel = new JLabel("Initializing...", SwingConstants.LEFT); + statusLabel.setFont(uiFont.deriveFont(Font.PLAIN, 12f)); + statusLabel.setForeground(new Color(220, 230, 240)); + + timeLabel = new JLabel("Elapsed: 0s", SwingConstants.RIGHT); + timeLabel.setFont(uiFont.deriveFont(Font.PLAIN, 12f)); + timeLabel.setForeground(new Color(180, 200, 215)); + + bottom.add(statusLabel, BorderLayout.WEST); + bottom.add(timeLabel, BorderLayout.EAST); + bottom.setBorder(BorderFactory.createEmptyBorder(8, 2, 2, 2)); + + card.add(top, BorderLayout.NORTH); + card.add(center, BorderLayout.CENTER); + card.add(bottom, BorderLayout.SOUTH); + + root.add(card); + loadingFrame.setContentPane(root); + + // 拖动窗口支持(在无边框下) + WindowDragger.makeDraggable(loadingFrame, card); + + // 启动动画定时器 + animationTimer = new Timer(1000 / 60, e -> { + boolean repaintNeeded = false; + if (mainProgressBar.animateStep()) repaintNeeded = true; + if (subProgressBar.animateStep()) repaintNeeded = true; + root.advanceAnimation(); + updateTimeLabel(); + if (repaintNeeded) { + root.repaint(); + } else { + // 仍需刷新背景动画 + root.repaint(); + } + }); + animationTimer.start(); - loadingFrame.add(loadingPanel); loadingFrame.setVisible(true); + + // 防止用户误操作关闭(保持原行为) + loadingFrame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + // DO NOTHING + } + }); } /** - * 更新主任务进度 + * 更新主任务进度(对外接口保持不变) * @param completedTasks 已完成的主任务数量 */ public void updateMainProgress(int completedTasks) { this.completedTasks = completedTasks; - int progress = (int) ((completedTasks / (double) totalTasks) * 100); - mainProgressBar.setValue(progress); - statusLabel.setText("Main Progress: " + progress + "%"); - updateTimeLabel(); + double progress = (completedTasks / (double) Math.max(1, totalTasks)) * 100.0; + if (progress < 0) progress = 0; + if (progress > 100) progress = 100; + mainProgressBar.setTarget((int) Math.round(progress)); + statusLabel.setText("主任务: " + completedTasks + " / " + totalTasks + " (" + (int) progress + "%)"); } /** - * 更新子任务进度 + * 更新子任务进度(对外接口保持不变) * @param subTaskName 子任务名称 * @param subTaskCompleted 已完成的子任务数量 * @param subTaskTotal 子任务总数 */ public void updateSubProgress(String subTaskName, int subTaskCompleted, int subTaskTotal) { + if (subTaskTotal <= 0) subTaskTotal = 1; subTasks.put(subTaskName, subTaskCompleted); - int progress = (int) ((subTaskCompleted / (double) subTaskTotal) * 100); - subProgressBar.setValue(progress); - subProgressBar.setString(subTaskName + ": " + progress + "%"); - updateTimeLabel(); + double progress = (subTaskCompleted / (double) subTaskTotal) * 100.0; + if (progress < 0) progress = 0; + if (progress > 100) progress = 100; + subProgressBar.setTarget((int) Math.round(progress)); + subProgressBar.setLabel(subTaskName); } /** * 更新总任务数 */ public void setTotalTasks(int totalTasks) { - this.totalTasks = totalTasks; + this.totalTasks = Math.max(1, totalTasks); } /** * 关闭加载窗口 */ public void close() { + if (animationTimer != null && animationTimer.isRunning()) { + animationTimer.stop(); + } loadingFrame.dispose(); } @@ -104,6 +234,229 @@ public class ProgressBarManager extends WindowsJDialog { */ private void updateTimeLabel() { long elapsedTime = (System.currentTimeMillis() - startTime) / 1000; - timeLabel.setText("Time elapsed: " + elapsedTime + "s"); + long hours = elapsedTime / 3600; + long mins = (elapsedTime % 3600) / 60; + long secs = elapsedTime % 60; + if (hours > 0) { + timeLabel.setText(String.format("Elapsed: %dh %02dm %02ds", hours, mins, secs)); + } else if (mins > 0) { + timeLabel.setText(String.format("Elapsed: %dm %02ds", mins, secs)); + } else { + timeLabel.setText(String.format("Elapsed: %ds", secs)); + } } -} \ No newline at end of file + + // -------------------------- + // 内部类:平滑进度条(支持插值动画、条纹、标签) + // -------------------------- + private static class SmoothProgressBar extends JComponent { + private int target = 0; + private double displayed = 0.0; + private int height = 20; + private Color base = new Color(255, 255, 255, 18); + private Color fill = new Color(0x00C2FF); + private String label = ""; + private boolean showStripe = false; + private Color stripeColor = new Color(255, 255, 255, 30); + private double stripeOffset = 0.0; + + public SmoothProgressBar(int initial) { + this.target = Math.max(0, Math.min(100, initial)); + this.displayed = this.target; + setOpaque(false); + setPreferredSize(new Dimension(200, height)); + } + + public void setAccentColor(Color c) { + this.fill = c; + } + + public void setLabel(String label) { + this.label = label; + } + + public void setShowStripe(boolean v) { + this.showStripe = v; + } + + public void setTarget(int t) { + t = Math.max(0, Math.min(100, t)); + this.target = t; + } + + /** + * 每帧推进插值,返回是否需要重绘 + */ + public boolean animateStep() { + // 平滑插值(阻尼) + double diff = target - displayed; + if (Math.abs(diff) < 0.02) { + displayed = target; + } else { + displayed += diff * 0.18; // 阻尼因子(调整流畅度) + } + + // 条纹动画 + if (showStripe) { + stripeOffset += 1.8; + if (stripeOffset > 60) stripeOffset = 0; + } + + // 是否需要重绘 + return Math.abs(diff) > 0.001 || showStripe; + } + + @Override + protected void paintComponent(Graphics g) { + int w = getWidth(); + int h = getHeight(); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 背景轨道 + RoundRectangle2D bg = new RoundRectangle2D.Float(0, 0, w, h, h, h); + g2.setColor(base); + g2.fill(bg); + + // 阴影(内阴影模拟) + g2.setColor(new Color(0, 0, 0, 30)); + g2.setStroke(new BasicStroke(1f)); + g2.draw(bg); + + // 填充(渐变) + int fillW = (int) Math.round((displayed / 100.0) * w); + if (fillW > 0) { + GradientPaint gp = new GradientPaint(0, 0, fill.brighter(), w, 0, fill.darker()); + RoundRectangle2D fg = new RoundRectangle2D.Float(0, 0, fillW, h, h, h); + g2.setPaint(gp); + g2.fill(fg); + + // 发光边缘 + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f)); + g2.setColor(fill); + g2.fill(new RoundRectangle2D.Float(0, -h / 3f, fillW, h + h / 3f, h, h)); + g2.setComposite(AlphaComposite.SrcOver); + } + + // 条纹效果 + if (showStripe && fillW > 6) { + Shape clip = g2.getClip(); + g2.setClip(new RoundRectangle2D.Float(0, 0, fillW, h, h, h)); + int stripeW = 18; + for (int x = -stripeW * 2; x < w + stripeW * 2; x += stripeW) { + int sx = (int) (x + stripeOffset); + Polygon p = new Polygon(); + p.addPoint(sx, 0); + p.addPoint(sx + stripeW, 0); + p.addPoint(sx + stripeW - 8, h); + p.addPoint(sx - 8, h); + g2.setColor(stripeColor); + g2.fill(p); + } + g2.setClip(clip); + } + + // 文本显示(居中) + String text; + if (label != null && !label.isEmpty()) { + text = label + " " + Math.round(displayed) + "%"; + } else { + text = Math.round(displayed) + "%"; + } + g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, Math.max(11, h - 6))); + FontMetrics fm = g2.getFontMetrics(); + int tx = (w - fm.stringWidth(text)) / 2; + int ty = (h + fm.getAscent() - fm.getDescent()) / 2; + g2.setColor(new Color(255, 255, 255, 210)); + g2.drawString(text, tx, ty); + + g2.dispose(); + } + } + + // -------------------------- + // 内部类:带动画效果的背景面板(流动扫描线 + 颗粒/渐变) + // -------------------------- + private class AnimatedBackgroundPanel extends JPanel { + private double offset = 0; + private double particlePhase = 0; + + public AnimatedBackgroundPanel() { + setOpaque(false); + } + + public void advanceAnimation() { + offset += 0.9; + if (offset > 2000) offset = 0; + particlePhase += 0.02; + } + + @Override + protected void paintComponent(Graphics g) { + int w = getWidth(); + int h = getHeight(); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 背景渐变(深色) + Paint p = new GradientPaint(0, 0, new Color(8, 10, 12), w, h, new Color(18, 20, 24)); + g2.setPaint(p); + g2.fillRect(0, 0, w, h); + + // 斜向扫描线(细微) + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.06f)); + g2.setColor(Color.WHITE); + for (int i = -200; i < w + h; i += 40) { + int x1 = i + (int) offset; + int y1 = 0; + int x2 = i - h + (int) offset; + int y2 = h; + g2.setStroke(new BasicStroke(2f)); + g2.drawLine(x1, y1, x2, y2); + } + g2.setComposite(AlphaComposite.SrcOver); + + // 轻微颗粒(科技光斑) + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.08f)); + for (int i = 0; i < 10; i++) { + float px = (float) ((Math.sin(particlePhase + i) + 1) / 2.0 * w); + float py = (float) ((Math.cos(particlePhase * 0.7 + i * 0.3) + 1) / 2.0 * h); + int size = 6 + (i % 3) * 4; + g2.fillOval((int) px, (int) py, size, size); + } + g2.setComposite(AlphaComposite.SrcOver); + + g2.dispose(); + super.paintComponent(g); + } + } + + // -------------------------- + // 工具:使无边框窗口可拖动 + // -------------------------- + private static class WindowDragger { + public static void makeDraggable(Window wnd, Component dragRegion) { + final Point[] mouseDown = {null}; + dragRegion.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + mouseDown[0] = e.getPoint(); + } + + @Override + public void mouseReleased(MouseEvent e) { + mouseDown[0] = null; + } + }); + dragRegion.addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + if (mouseDown[0] != null) { + Point curr = e.getLocationOnScreen(); + wnd.setLocation(curr.x - mouseDown[0].x, curr.y - mouseDown[0].y); + } + } + }); + } + } +}