diff --git a/gui/README.md b/gui/README.md new file mode 100644 index 00000000..e9adcfa9 --- /dev/null +++ b/gui/README.md @@ -0,0 +1,30 @@ +# APKEditor GUI wrapper + +This is a Java Swing wrapper for the CLI commands described in `../cli comands`. +It supports Build (`b`), Decompile (`d`), Info (`info`), Merge (`m`), Protect (`p`), +and Refactor (`x`). The CLI JAR is selected in the UI, so the wrapper does not +bundle or assume a particular APKEditor version. + +## Build + +Requires Java 8 or newer. A JDK is needed to build the project. + +```sh +./build.sh +``` + +The result is `APKEditor-GUI.jar`. + +## Run + +```sh +java -jar APKEditor-GUI.jar +``` + +Run this command on a desktop session. A remote Codespace or SSH terminal without +an X11/Wayland display cannot show a Swing window. + +The interface uses Java's system look-and-feel, which gives native controls on +each supported desktop OS while keeping the application in one OS-independent JAR. +The target CLI JAR still needs to be downloaded separately and must be run with a +compatible Java runtime. \ No newline at end of file diff --git a/gui/build.sh b/gui/build.sh new file mode 100644 index 00000000..fc6bac75 --- /dev/null +++ b/gui/build.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BUILD="$ROOT/build" +rm -rf "$BUILD" +mkdir -p "$BUILD/classes" + +find "$ROOT/src" -name '*.java' -print0 | xargs -0 javac -source 8 -target 8 -d "$BUILD/classes" +printf 'Main-Class: com.apkeditor.gui.Main\n' > "$BUILD/manifest.mf" +jar cfm "$ROOT/APKEditor-GUI.jar" "$BUILD/manifest.mf" -C "$BUILD/classes" . +printf 'Created %s\n' "$ROOT/APKEditor-GUI.jar" \ No newline at end of file diff --git a/gui/build/classes/com/apkeditor/gui/ArgumentParser.class b/gui/build/classes/com/apkeditor/gui/ArgumentParser.class new file mode 100644 index 00000000..65ebeabf Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/ArgumentParser.class differ diff --git a/gui/build/classes/com/apkeditor/gui/BuildCommand.class b/gui/build/classes/com/apkeditor/gui/BuildCommand.class new file mode 100644 index 00000000..11a3d80b Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/BuildCommand.class differ diff --git a/gui/build/classes/com/apkeditor/gui/CliCommand.class b/gui/build/classes/com/apkeditor/gui/CliCommand.class new file mode 100644 index 00000000..4a6abbfa Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/CliCommand.class differ diff --git a/gui/build/classes/com/apkeditor/gui/CliProcessRunner$1.class b/gui/build/classes/com/apkeditor/gui/CliProcessRunner$1.class new file mode 100644 index 00000000..3e9ae037 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/CliProcessRunner$1.class differ diff --git a/gui/build/classes/com/apkeditor/gui/CliProcessRunner$Listener.class b/gui/build/classes/com/apkeditor/gui/CliProcessRunner$Listener.class new file mode 100644 index 00000000..17c4d44d Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/CliProcessRunner$Listener.class differ diff --git a/gui/build/classes/com/apkeditor/gui/CliProcessRunner.class b/gui/build/classes/com/apkeditor/gui/CliProcessRunner.class new file mode 100644 index 00000000..9c7a1878 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/CliProcessRunner.class differ diff --git a/gui/build/classes/com/apkeditor/gui/CommandCatalog.class b/gui/build/classes/com/apkeditor/gui/CommandCatalog.class new file mode 100644 index 00000000..9100c2e6 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/CommandCatalog.class differ diff --git a/gui/build/classes/com/apkeditor/gui/DecompileCommand.class b/gui/build/classes/com/apkeditor/gui/DecompileCommand.class new file mode 100644 index 00000000..0dba01cc Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/DecompileCommand.class differ diff --git a/gui/build/classes/com/apkeditor/gui/FileChooserService.class b/gui/build/classes/com/apkeditor/gui/FileChooserService.class new file mode 100644 index 00000000..ee044246 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/FileChooserService.class differ diff --git a/gui/build/classes/com/apkeditor/gui/InfoCommand.class b/gui/build/classes/com/apkeditor/gui/InfoCommand.class new file mode 100644 index 00000000..57e47e9d Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/InfoCommand.class differ diff --git a/gui/build/classes/com/apkeditor/gui/Main.class b/gui/build/classes/com/apkeditor/gui/Main.class new file mode 100644 index 00000000..f2a08ae7 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/Main.class differ diff --git a/gui/build/classes/com/apkeditor/gui/MainFrame$1.class b/gui/build/classes/com/apkeditor/gui/MainFrame$1.class new file mode 100644 index 00000000..d34f6086 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/MainFrame$1.class differ diff --git a/gui/build/classes/com/apkeditor/gui/MainFrame.class b/gui/build/classes/com/apkeditor/gui/MainFrame.class new file mode 100644 index 00000000..b4b37f26 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/MainFrame.class differ diff --git a/gui/build/classes/com/apkeditor/gui/MergeCommand.class b/gui/build/classes/com/apkeditor/gui/MergeCommand.class new file mode 100644 index 00000000..2ab34a85 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/MergeCommand.class differ diff --git a/gui/build/classes/com/apkeditor/gui/ProtectCommand.class b/gui/build/classes/com/apkeditor/gui/ProtectCommand.class new file mode 100644 index 00000000..4375c86e Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/ProtectCommand.class differ diff --git a/gui/build/classes/com/apkeditor/gui/RefactorCommand.class b/gui/build/classes/com/apkeditor/gui/RefactorCommand.class new file mode 100644 index 00000000..da1819f2 Binary files /dev/null and b/gui/build/classes/com/apkeditor/gui/RefactorCommand.class differ diff --git a/gui/build/manifest.mf b/gui/build/manifest.mf new file mode 100644 index 00000000..dde0b7a0 --- /dev/null +++ b/gui/build/manifest.mf @@ -0,0 +1 @@ +Main-Class: com.apkeditor.gui.Main diff --git a/gui/src/com/apkeditor/gui/ArgumentParser.java b/gui/src/com/apkeditor/gui/ArgumentParser.java new file mode 100644 index 00000000..3ca79fc9 --- /dev/null +++ b/gui/src/com/apkeditor/gui/ArgumentParser.java @@ -0,0 +1,23 @@ +package com.apkeditor.gui; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class ArgumentParser { + private static final Pattern ARGUMENT_PATTERN = Pattern + .compile("(?:\\\"([^\\\"]*)\\\"|'([^']*)'|(\\S+))"); + + private ArgumentParser() { } + + public static List split(String value) { + List arguments = new ArrayList<>(); + Matcher matcher = ARGUMENT_PATTERN.matcher(value); + while (matcher.find()) { + arguments.add(matcher.group(1) != null ? matcher.group(1) + : matcher.group(2) != null ? matcher.group(2) : matcher.group(3)); + } + return arguments; + } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/BuildCommand.java b/gui/src/com/apkeditor/gui/BuildCommand.java new file mode 100644 index 00000000..c4569598 --- /dev/null +++ b/gui/src/com/apkeditor/gui/BuildCommand.java @@ -0,0 +1,7 @@ +package com.apkeditor.gui; + +public final class BuildCommand implements CliCommand { + public String getCode() { return "b"; } + public String getLabel() { return "Build"; } + public String toString() { return getLabel() + " (" + getCode() + ")"; } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/CliCommand.java b/gui/src/com/apkeditor/gui/CliCommand.java new file mode 100644 index 00000000..60700c7a --- /dev/null +++ b/gui/src/com/apkeditor/gui/CliCommand.java @@ -0,0 +1,10 @@ +package com.apkeditor.gui; + +public interface CliCommand { + String getCode(); + + String getLabel(); + + @Override + String toString(); +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/CliProcessRunner.java b/gui/src/com/apkeditor/gui/CliProcessRunner.java new file mode 100644 index 00000000..55dd3b02 --- /dev/null +++ b/gui/src/com/apkeditor/gui/CliProcessRunner.java @@ -0,0 +1,44 @@ +package com.apkeditor.gui; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public final class CliProcessRunner { + public interface Listener { + void onOutput(String line); + void onFinished(int exitCode); + void onError(String message); + } + + public void run(final List command, final Listener listener) { + Thread worker = new Thread(new Runnable() { + @Override + public void run() { + execute(command, listener); + } + }, "cli-process"); + worker.start(); + } + + private void execute(List command, Listener listener) { + try { + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + listener.onOutput(line); + } + } + listener.onFinished(process.waitFor()); + } catch (IOException exception) { + listener.onError("Could not start CLI: " + exception.getMessage()); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + listener.onError("Process interrupted."); + } + } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/CommandCatalog.java b/gui/src/com/apkeditor/gui/CommandCatalog.java new file mode 100644 index 00000000..d86ade24 --- /dev/null +++ b/gui/src/com/apkeditor/gui/CommandCatalog.java @@ -0,0 +1,12 @@ +package com.apkeditor.gui; + +public final class CommandCatalog { + private CommandCatalog() { } + + public static CliCommand[] all() { + return new CliCommand[] { + new BuildCommand(), new DecompileCommand(), new InfoCommand(), + new MergeCommand(), new ProtectCommand(), new RefactorCommand() + }; + } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/DecompileCommand.java b/gui/src/com/apkeditor/gui/DecompileCommand.java new file mode 100644 index 00000000..332995af --- /dev/null +++ b/gui/src/com/apkeditor/gui/DecompileCommand.java @@ -0,0 +1,7 @@ +package com.apkeditor.gui; + +public final class DecompileCommand implements CliCommand { + public String getCode() { return "d"; } + public String getLabel() { return "Decompile"; } + public String toString() { return getLabel() + " (" + getCode() + ")"; } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/FileChooserService.java b/gui/src/com/apkeditor/gui/FileChooserService.java new file mode 100644 index 00000000..4fef272f --- /dev/null +++ b/gui/src/com/apkeditor/gui/FileChooserService.java @@ -0,0 +1,19 @@ +package com.apkeditor.gui; + +import java.awt.Component; +import java.io.File; +import javax.swing.JFileChooser; + +public final class FileChooserService { + private FileChooserService() { } + + public static File choose(Component parent, int selectionMode, String title) { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(title); + chooser.setFileSelectionMode(selectionMode); + if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { + return chooser.getSelectedFile(); + } + return null; + } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/InfoCommand.java b/gui/src/com/apkeditor/gui/InfoCommand.java new file mode 100644 index 00000000..1e7c0f59 --- /dev/null +++ b/gui/src/com/apkeditor/gui/InfoCommand.java @@ -0,0 +1,7 @@ +package com.apkeditor.gui; + +public final class InfoCommand implements CliCommand { + public String getCode() { return "info"; } + public String getLabel() { return "Info"; } + public String toString() { return getLabel() + " (" + getCode() + ")"; } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/Main.java b/gui/src/com/apkeditor/gui/Main.java new file mode 100644 index 00000000..be21f5b1 --- /dev/null +++ b/gui/src/com/apkeditor/gui/Main.java @@ -0,0 +1,24 @@ +package com.apkeditor.gui; + +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import java.awt.GraphicsEnvironment; + +public final class Main { + private Main() { + } + + public static void main(String[] args) { + if (GraphicsEnvironment.isHeadless()) { + System.err.println("APKEditor GUI cannot open because this Java session has no desktop display. " + + "Copy the JAR to Windows, macOS, or Linux desktop and run it there."); + return; + } + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignored) { + } + + SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true)); + } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/MainFrame.java b/gui/src/com/apkeditor/gui/MainFrame.java new file mode 100644 index 00000000..c5fd870c --- /dev/null +++ b/gui/src/com/apkeditor/gui/MainFrame.java @@ -0,0 +1,170 @@ +package com.apkeditor.gui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +public final class MainFrame extends JFrame { + private final JTextField cliJarField = new JTextField(); + private final JTextField inputField = new JTextField(); + private final JTextField outputField = new JTextField(); + private final JTextField extraArgsField = new JTextField(); + private final JComboBox commandBox = new JComboBox<>(CommandCatalog.all()); + private final JTextArea outputArea = new JTextArea(); + private final JButton runButton = new JButton("Run"); + private final CliProcessRunner processRunner = new CliProcessRunner(); + + public MainFrame() { + super("APKEditor GUI"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setMinimumSize(new Dimension(760, 520)); + setSize(900, 640); + setLocationByPlatform(true); + buildUi(); + } + + private void buildUi() { + JPanel form = new JPanel(new GridBagLayout()); + form.setBorder(BorderFactory.createEmptyBorder(12, 12, 4, 12)); + addRow(form, 0, "CLI JAR", cliJarField, "Browse", this::chooseCliJar); + addRow(form, 1, "Command", commandBox, null, null); + addRow(form, 2, "Input", inputField, "Browse", this::chooseInput); + addRow(form, 3, "Output", outputField, "Browse", this::chooseOutput); + addRow(form, 4, "Extra options", extraArgsField, null, null); + + JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton clearButton = new JButton("Clear output"); + clearButton.addActionListener(event -> outputArea.setText("")); + runButton.addActionListener(event -> runCommand()); + actions.add(clearButton); + actions.add(runButton); + outputArea.setEditable(false); + outputArea.setLineWrap(true); + outputArea.setWrapStyleWord(true); + outputArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + JScrollPane outputScroll = new JScrollPane(outputArea); + outputScroll.setBorder(BorderFactory.createTitledBorder("CLI output")); + + add(form, BorderLayout.NORTH); + add(outputScroll, BorderLayout.CENTER); + add(actions, BorderLayout.SOUTH); + } + + private void addRow(JPanel panel, int row, String label, java.awt.Component field, + String buttonLabel, Runnable browseAction) { + GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridy = row; + constraints.insets = new Insets(4, 4, 4, 4); + constraints.anchor = GridBagConstraints.LINE_START; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.weightx = 0; + panel.add(new JLabel(label), constraints); + + constraints.gridx = 1; + constraints.weightx = 1; + panel.add(field, constraints); + if (buttonLabel != null) { + constraints.gridx = 2; + constraints.weightx = 0; + JButton button = new JButton(buttonLabel); + button.addActionListener(event -> browseAction.run()); + panel.add(button, constraints); + } + } + + private void chooseCliJar() { + choosePath(cliJarField, JFileChooser.FILES_ONLY, "Select CLI JAR"); + } + + private void chooseInput() { + choosePath(inputField, JFileChooser.FILES_AND_DIRECTORIES, "Select input"); + } + + private void chooseOutput() { + choosePath(outputField, JFileChooser.FILES_AND_DIRECTORIES, "Select output"); + } + + private void choosePath(JTextField target, int selectionMode, String title) { + File selectedFile = FileChooserService.choose(this, selectionMode, title); + if (selectedFile != null) { + target.setText(selectedFile.getAbsolutePath()); + } + } + + private void runCommand() { + String cliJar = cliJarField.getText().trim(); + String input = inputField.getText().trim(); + if (cliJar.isEmpty() || input.isEmpty()) { + JOptionPane.showMessageDialog(this, "CLI JAR and input are required.", + "Missing input", JOptionPane.WARNING_MESSAGE); + return; + } + if (!Files.isRegularFile(Paths.get(cliJar))) { + JOptionPane.showMessageDialog(this, "The selected CLI JAR does not exist.", + "Invalid CLI JAR", JOptionPane.ERROR_MESSAGE); + return; + } + + List command = createCommand(cliJar, input); + outputArea.append("$ " + String.join(" ", command) + "\n"); + runButton.setEnabled(false); + processRunner.run(command, new CliProcessRunner.Listener() { + public void onOutput(final String line) { + SwingUtilities.invokeLater(() -> outputArea.append(line + "\n")); + } + + public void onFinished(final int exitCode) { + SwingUtilities.invokeLater(() -> { + outputArea.append("\nProcess finished with exit code " + exitCode + "\n"); + runButton.setEnabled(true); + }); + } + + public void onError(final String message) { + SwingUtilities.invokeLater(() -> { + outputArea.append(message + "\n"); + runButton.setEnabled(true); + }); + } + }); + } + + private List createCommand(String cliJar, String input) { + List command = new ArrayList<>(); + command.add(Paths.get(System.getProperty("java.home"), "bin", "java").toString()); + command.add("-jar"); + command.add(cliJar); + command.add(((CliCommand) commandBox.getSelectedItem()).getCode()); + command.add("-i"); + command.add(input); + if (!outputField.getText().trim().isEmpty()) { + command.add("-o"); + command.add(outputField.getText().trim()); + } + String extraOptions = extraArgsField.getText().trim(); + if (!extraOptions.isEmpty()) { + command.addAll(ArgumentParser.split(extraOptions)); + } + return command; + } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/MergeCommand.java b/gui/src/com/apkeditor/gui/MergeCommand.java new file mode 100644 index 00000000..63dbdb2b --- /dev/null +++ b/gui/src/com/apkeditor/gui/MergeCommand.java @@ -0,0 +1,7 @@ +package com.apkeditor.gui; + +public final class MergeCommand implements CliCommand { + public String getCode() { return "m"; } + public String getLabel() { return "Merge"; } + public String toString() { return getLabel() + " (" + getCode() + ")"; } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/ProtectCommand.java b/gui/src/com/apkeditor/gui/ProtectCommand.java new file mode 100644 index 00000000..9dbc49d5 --- /dev/null +++ b/gui/src/com/apkeditor/gui/ProtectCommand.java @@ -0,0 +1,7 @@ +package com.apkeditor.gui; + +public final class ProtectCommand implements CliCommand { + public String getCode() { return "p"; } + public String getLabel() { return "Protect"; } + public String toString() { return getLabel() + " (" + getCode() + ")"; } +} \ No newline at end of file diff --git a/gui/src/com/apkeditor/gui/RefactorCommand.java b/gui/src/com/apkeditor/gui/RefactorCommand.java new file mode 100644 index 00000000..047a813f --- /dev/null +++ b/gui/src/com/apkeditor/gui/RefactorCommand.java @@ -0,0 +1,7 @@ +package com.apkeditor.gui; + +public final class RefactorCommand implements CliCommand { + public String getCode() { return "x"; } + public String getLabel() { return "Refactor"; } + public String toString() { return getLabel() + " (" + getCode() + ")"; } +} \ No newline at end of file