diff --git a/bootstrap/build.gradle.kts b/bootstrap/build.gradle.kts index 1f0590f..d3a4e63 100644 --- a/bootstrap/build.gradle.kts +++ b/bootstrap/build.gradle.kts @@ -14,6 +14,9 @@ sourceSets { dependencies { api(project(":converter")) + implementation("com.twelvemonkeys.imageio:imageio-tga:3.9.4") + implementation("com.formdev:flatlaf:3.6") + implementation("com.formdev:flatlaf-intellij-themes:3.6") } application { diff --git a/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java b/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java new file mode 100644 index 0000000..d9ac9dc --- /dev/null +++ b/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/PackConverter + * + */ + +package org.geysermc.pack.converter.bootstrap; + +import org.geysermc.pack.converter.util.LogListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.concurrent.atomic.AtomicBoolean; + +public class BootstrapLogListener implements LogListener { + private final JTextArea output; + public final AtomicBoolean isDebug = new AtomicBoolean(false); + + public BootstrapLogListener(JTextArea output) { + this.output = output; + } + + @Override + public void debug(@NotNull String message) { + if (this.isDebug.get()) { + appendText("DEBUG: " + message); + } + } + + @Override + public void info(@NotNull String message) { + appendText(message); + } + + @Override + public void warn(@NotNull String message) { + appendText("WARNING: " + message); + } + + @Override + public void error(@NotNull String message) { + appendText("ERROR: " + message); + } + + @Override + public void error(@NotNull String message, @Nullable Throwable exception) { + appendText("ERROR: " + message); + } + + private void appendText(String text) { + if (output.getText().isEmpty()) { + output.setText( + text + ); + return; + } + + output.setText( + output.getText() + "\n" + text + ); + + output.setCaretPosition(output.getDocument().getLength()); + } +} diff --git a/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/Main.java b/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/Main.java index 99ddf6f..adb526f 100644 --- a/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/Main.java +++ b/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/Main.java @@ -26,42 +26,203 @@ package org.geysermc.pack.converter.bootstrap; +import com.formdev.flatlaf.intellijthemes.FlatArcDarkIJTheme; +import com.twelvemonkeys.image.BufferedImageIcon; import org.geysermc.pack.converter.PackConverter; import org.geysermc.pack.converter.converter.Converters; +import org.geysermc.pack.converter.util.ImageUtil; +import org.geysermc.pack.converter.util.ZipUtils; -import java.io.FileNotFoundException; -import java.io.IOException; +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.io.*; +import java.math.RoundingMode; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; public class Main { + private static final boolean canOpenExplorer = Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR); - public static void main(String[] args) throws FileNotFoundException { - if (args.length != 1) { - throw new AssertionError("Please choose a .zip file to convert"); + private JFrame frame; + private Path javaPackPath = null; + private Path output = null; + + private final AtomicBoolean converting = new AtomicBoolean(false); + private final AtomicLong startTime = new AtomicLong(0); + private final BootstrapLogListener logListener; + + public static void main(String[] arguments) throws IOException { + List args = Arrays.asList(arguments); + boolean debug = args.contains("debug"); + + if (args.contains("nogui")) { + if (!args.contains("--input")) { + throw new IllegalArgumentException("No input provided."); + } + + String inputPath = args.get(args.indexOf("--input") + 1); + + new PackConverter() + .input(Path.of(inputPath)) + .output(Path.of(inputPath.substring(0, inputPath.length() - 4))) + .converters(Converters.defaultConverters(debug)) + .convert() + .pack(); } else { - Path packFile = Paths.get(args[0]); - - // Check the file exists - if (!packFile.toFile().exists()) { - throw new FileNotFoundException(String.format("Specified pack zip file not found (%s)", packFile)); - } - - // Check its a zip - if (!packFile.toString().endsWith(".zip") && !packFile.toString().endsWith(".jar")) { - throw new AssertionError(String.format("Specified pack is not a zip (%s)", packFile)); - } - - try { - new PackConverter() - .input(packFile) - .output(Paths.get(packFile.toString().replaceFirst("[.][^.]+$", ".mcpack"))) - .converters(Converters.defaultConverters()) - .convert() - .pack(); - } catch (IOException e) { - e.printStackTrace(); - } + new Main(debug); } } + + private Main(boolean debug) throws IOException { + DecimalFormat df = new DecimalFormat("#.##"); + df.setRoundingMode(RoundingMode.CEILING); + + FlatArcDarkIJTheme.setup(); + + frame = new JFrame(); + frame.setTitle("Icicle"); + InputStream iconStream = Main.class.getResourceAsStream("/icon.png"); + if (iconStream != null) frame.setIconImage(ImageIO.read(iconStream)); + frame.setSize(800, 300); + frame.setLayout(null); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.setResizable(false); + + JLabel dataLabel = new JLabel("Input: None | Output: None"); + dataLabel.setBounds(225, 20, 535, 50); + frame.add(dataLabel); + + JTextArea outputArea = new JTextArea(""); + outputArea.setEditable(false); + outputArea.setFocusable(false); + outputArea.setDisabledTextColor(Color.BLACK); + outputArea.setLineWrap(true); + logListener = new BootstrapLogListener(outputArea); + + JScrollPane scrollArea = new JScrollPane(outputArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scrollArea.setBounds(225, 90, 535, 125); + + frame.add(scrollArea); + + JCheckBox debugCheckbox; + + if (debug) { + debugCheckbox = new JCheckBox("Debug Mode"); + debugCheckbox.setBounds(650, 225, 105, 25); + debugCheckbox.addActionListener(event -> { + logListener.isDebug.set(debugCheckbox.isSelected()); + }); + frame.add(debugCheckbox); + } else { + debugCheckbox = null; + } + + JButton convertButton = new JButton("Convert"); + convertButton.setBounds(20, 225, debug ? 625 : 740, 25); + convertButton.setEnabled(false); + convertButton.addActionListener(event -> { + if (converting.get()) return; + + startTime.set(System.currentTimeMillis()); + converting.set(true); + if (debugCheckbox != null) debugCheckbox.setEnabled(false); + convertButton.setEnabled(false); + outputArea.setText(""); + dataLabel.setText("Converting %s...".formatted(javaPackPath.getFileName().toString())); + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + try { + new Thread(() -> { + try { + new PackConverter() + .input(javaPackPath) + .output(output) + .converters(Converters.defaultConverters(logListener.isDebug.get())) + .logListener(logListener) + .convert() + .pack(); + + logListener.info("Converted pack."); + dataLabel.setText("%s Converted! Time elapsed: %ss".formatted(javaPackPath.getFileName().toString(), df.format((System.currentTimeMillis() - startTime.get()) / 1000d))); + converting.set(false); + convertButton.setEnabled(true); + if (debugCheckbox != null) debugCheckbox.setEnabled(true); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + } catch (Exception e) { + e.printStackTrace(); + converting.set(false); + convertButton.setEnabled(true); + if (debugCheckbox != null) debugCheckbox.setEnabled(true); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + } + }); + frame.add(convertButton); + + JButton javaPackButton = new JButton("Select Pack"); + javaPackButton.setBounds(20, 20, 200, 200); + javaPackButton.addActionListener(event -> { + FileDialog chooser = new FileDialog(frame); + chooser.setTitle("Select Resource Pack"); + chooser.setMultipleMode(false); + chooser.setMode(FileDialog.LOAD); + chooser.setFilenameFilter((dir, name) -> name.endsWith(".zip") || name.endsWith(".jar")); + chooser.setVisible(true); + + if (chooser.getFile() == null) return; + + javaPackPath = Path.of(chooser.getDirectory(), chooser.getFile()); + + if (!javaPackPath.toString().endsWith(".zip") && !javaPackPath.toString().endsWith(".jar")) { + dataLabel.setText("Please provide a Java Edition resource pack."); + convertButton.setEnabled(false); + } else { + String name = javaPackPath.toAbsolutePath().toString(); + output = Path.of(name.substring(0, name.length() - 4) + ".mcpack"); + + dataLabel.setText( + "Input: %s | Output: %s" + .formatted(javaPackPath.toAbsolutePath().toString(), output.toAbsolutePath().toString()) + ); + convertButton.setEnabled(true); + + try { + ZipUtils.openFileSystem(javaPackPath, true, path -> { + Path iconPath = path.resolve("pack.png"); + + if (!Files.exists(iconPath)) { + javaPackButton.setText("No Icon."); + javaPackButton.setIcon(null); + } else { + javaPackButton.setText(null); + javaPackButton.setIcon( + new BufferedImageIcon( + ImageUtil.resize(ImageIO.read(Files.newInputStream(iconPath)), 175, 175) + ) + ); + } + }); + } catch (IOException e) { + javaPackButton.setText("No Icon."); + javaPackButton.setIcon(null); + } + } + }); + frame.add(javaPackButton); + + JLabel outputLabel = new JLabel("Output:"); + outputLabel.setBounds(225, 50, 535, 50); + frame.add(outputLabel); + + + frame.setVisible(true); + } } diff --git a/bootstrap/src/main/resources/icon.png b/bootstrap/src/main/resources/icon.png new file mode 100644 index 0000000..0d09eee Binary files /dev/null and b/bootstrap/src/main/resources/icon.png differ